Reactive Mainnet & Lasna Testnet
无论是主网还是测试网,最关键的是 System Contract 地址是统一的
| 参数项目 | Reactive Mainnet (主网) | Lasna Testnet (测试网) |
|---|---|---|
| 网络名称 | Reactive Mainnet | Reactive Lasna |
| RPC URL | https://mainnet-rpc.rnk.dev/ | https://lasna-rpc.rnk.dev/ |
| Chain ID | 1597 | 5318007 |
| 代币符号 | REACT | lREACT |
| 区块浏览器 | reactscan.net | lasna.reactscan.net |
| 系统合约地址 | 0x0000000000000000000000000000000000fffFfF | (同左) |
获取测试币 (lREACT)
需要跨链水龙 机制,将外部测试网(如 Ethereum Sepolia 或 Base Sepolia)的资源转化为 Reactive Network (Lasna Testnet) 的原生测试币 lREACT。
当你向 Faucet 合约发送 ETH 时,实际上是触发了一个事件。Reactive Network 的节点会监听到这个事件,并在 Lasna 测试网上为你对应的地址铸造(Mint)或分配 lREACT。
兑换比例: $1 \text{ ETH} = 100 \text{ lREACT}$。
支持源链: Ethereum Sepolia 和 Base Sepolia。
这里有三种方式:
A. 命令行操作
如果你安装了 Foundry 工具链,使用 cast 是最高效的方式。这不仅是发送交易,更是调用合约的 request(address) 函数。
代码实现:
# 1. 设置环境变量 (建议放入 .env 文件)
export ETHEREUM_SEPOLIA_RPC="你的Sepolia节点链接"
export PRIVATE_KEY="你的钱包私钥"
export MY_ADDRESS="你的钱包地址"
# 2. 向 Ethereum Sepolia Faucet 发送 0.1 ETH 换取 10 lREACT
cast send 0x9b9BB25f1A81078C544C829c5EB7822d747Cf434 \
--rpc-url $ETHEREUM_SEPOLIA_RPC \
--private-key $PRIVATE_KEY \
"request(address)" $MY_ADDRESS \
--value 0.1ether
# 或者向 Base Sepolia Faucet 发送
# 地址: 0x2afaFD298b23b62760711756088F75B7409f5967
参数拆解:
request(address):这是合约定义的函数,参数是接收lREACT的地址(通常填你自己)。--value 0.1ether:这是你投入的“成本”。
B. 手动钱包转账
你也可以直接在 MetaMask 里点击“发送”,输入 Faucet 合约地址,填入 ETH 金额即可。
- 注意: 这种方式是直接触发合约的
receive()或fallback()函数,合约会自动识别发送者并兑换。
C. ReacDEFI Swap
适合新手的图形化界面,本质上是把上述 cast 命令封装成了网页端的交互。
https://reacdefi.app/markets#testnet-faucet
注意,兑换有一下限制:
| 约束项 | 限制值 | 后果 |
|---|---|---|
| 单笔最大发送量 | 5 ETH | 超过部分将直接丢失,且不会产生额外的 lREACT。 |
| 单笔最大获得量 | 500 lREACT | 对应 5 ETH 的上限。 |
Reactive Library
核心脚手架
Foundry 项目根目录下运行:
forge install Reactive-Network/reactive-lib
这会将库下载到 lib/ 文件夹。在你的 .sol 文件中,你可以这样引入:
import 'reactive-lib/interfaces/IReactive.sol';
import 'reactive-lib/abstract-contracts/AbstractReactive.sol';
这个库主要提供了四个函数分别处理安全校验、运行环境检测、 Gas 支付管理和订阅状态控制。
| 合约名称 | 核心功能 | 开发者关注点 |
|---|---|---|
AbstractReactive | 基石。自动检测运行环境(VM 还是 RN),连接系统合约。 | 必须继承,它是所有 Reactive 合约的起点。 |
AbstractPayer | 管钱。处理 Gas 支付、债务结算和授权支付者。 | 确保合约里有足够的 lREACT 来支付回调费用。 |
AbstractCallback | 权限控权。确保只有授权的 ReactVM 才能触发回调。 | 防止黑客伪造回调请求,增强安全性。 |
AbstractPausableReactive | 开关控制。支持暂停(Pause)和恢复(Resume)订阅。 | 用于维护或紧急情况,一键停止监听链上事件。 |
AbstractCallback
AbstractCallback 继承自 AbstractPayer.sol ,并为 Reactive Contracts 提供回调授权。AbstractCallback 确保了两个关键点:只有合法的虚拟机(ReactVM)能指挥你的合约,以及你的合约有钱支付给帮你干活的代理(Callback Proxy)。
合约首先定义了两个关键变量,用于确立“谁能发指令”和“通过谁发指令”。
rvm_id(ReactVM Identifier):这是授权的虚拟机 ID。你可以把它理解为你的 Reactive 合约在 Reactive Network 环境中的“身份证号”。vendor(Callback Proxy):回调代理地址。所有的跨链指令都会先发到这个代理地址,由它负责搬运到目标链。
然后通过 rvmIdOnly 确保只有合法的 ReactVM 能够触发回调函数
modifier rvmIdOnly(address _rvm_id) { require(rvm_id == address(0) || rvm_id == _rvm_id, 'Authorized RVM ID only'); _; }
逻辑解析:
它检查传入的
_rvm_id是否匹配合约记录的rvm_id。rvm_id == address(0)是一个安全缓冲,通常用于初始化阶段。如果身份不匹配,交易会直接失败(Revert),防止非法调用。
构造函数在合约部署时运行,完成身份绑定和支付授权。
constructor(address _callback_sender) {
rvm_id = msg.sender;
vendor = IPayable(payable(_callback_sender));
addAuthorizedSender(_callback_sender);
}
rvm_id = msg.sender:在 Reactive Network 部署环境下,msg.sender就是部署该合约的 ReactVM 实例。通过这行代码,合约锁定了它的“主人”。vendor = ...:将传入的地址设定为官方的回调代理。addAuthorizedSender(_callback_sender):这一步非常关键。它继承自AbstractPayer,意思是:“我授权这个回调代理合约可以从我的账户里划钱”。因为在目标链执行动作需要消耗 Gas(lREACT),代理合约必须有权扣款才能帮你办事。
AbstractPausableReactive
AbstractPausableReactive 提供可暂停的事件订阅功能。简单来说,它的作用是为你的 Reactive 合约提供一个“制动开关”。在区块链世界中,能够动态地停止和恢复对链上事件的监听是非常关键的。
AbstractPausableReactive 扩展自 AbstractReactive.sol。它不仅继承了基础的反应能力,还引入了状态管理。
其核心是 Subscription 结构体,它完整定义了一个监听任务的“五元组”:
chain_id: 目标链 ID。
_contract: 被监听的合约地址。
topic_0 到 topic_3: 事件的特征哈希和索引参数(用于精确过滤)。
该合约内置了所有权控制,确保只有合约的部署者可以控制监听状态。
constructor() {
owner = msg.sender; // 将部署者设置为所有者
}
The pause() Function
当你调用 pause() 时,合约会遍历所有定义为“可暂停”的订阅任务,并通知系统服务(Service)停止推送这些事件。
function pause() external rnOnly onlyOwner {
require(!paused, 'Already paused');
// 获取需要暂停的订阅列表
Subscription[] memory subscriptions = getPausableSubscriptions();
for (uint256 ix = 0; ix != subscriptions.length; ++ix) {
// 调用系统服务进行取消订阅
service.unsubscribe(
subscriptions[ix].chain_id,
subscriptions[ix]._contract,
subscriptions[ix].topic_0,
subscriptions[ix].topic_1,
subscriptions[ix].topic_2,
subscriptions[ix].topic_3
);
}
paused = true;
}
The resume() Function
resume() 是 pause() 的镜像操作。它重新向系统注册那些之前被取消的订阅任务。
function resume() external rnOnly onlyOwner {
require(paused, 'Not paused');
// 重新获取订阅配置
Subscription[] memory subscriptions = getPausableSubscriptions();
for (uint256 ix = 0; ix != subscriptions.length; ++ix) {
// 调用系统服务重新建立订阅
service.subscribe(
subscriptions[ix].chain_id,
subscriptions[ix]._contract,
subscriptions[ix].topic_0,
subscriptions[ix].topic_1,
subscriptions[ix].topic_2,
subscriptions[ix].topic_3
);
}
paused = false;
}
AbstractPayer
在 Reactive Network 中,合约执行回调(Callback)是需要支付 Gas 费用的。AbstractPayer 的核心作用就是管理合约的资金,确保它能支付给帮你干活的“供应商”(Vendor,通常指系统合约或回调代理),并处理债务结算。
授权
并不是任何人都能从你的合约里取钱去付 Gas。合约通过 senders 映射表来实现精细的权限管理。
modifier authorizedSenderOnly() { require(senders[msg.sender], 'Authorized sender only'); _; }
该修饰符确保了只有在白名单里的地址(如官方的回调代理合约)才能触发支付逻辑。
这两个函数通常在构造函数或内部逻辑中调用,用于动态增删授权人。
function addAuthorizedSender(address sender) internal { senders[sender] = true; }
function removeAuthorizedSender(address sender) internal { senders[sender] = false; }
支付与结算
这是 AbstractPayer 最关键的功能。
主动支付 (pay): 授权发送者(比如 Callback 代理)调用此函数,直接向其转账。
function pay(uint256 amount) external authorizedSenderOnly {
_pay(payable(msg.sender), amount);
}
自动结账 (coverDebt): Reactive Network 的系统合约会记录你欠了多少 Gas 费。调用此函数会查询欠款并自动付清。
function coverDebt() external {
// 这里的 vendor 是在子类中定义的 IPayable 接口实例
uint256 amount = vendor.debt(address(this));
_pay(payable(vendor), amount);
}
底层转账机制:_pay
这是所有支付功能的最终执行者,包含了基本的安全检查。
function _pay(address payable recipient, uint256 amount) internal {
// 检查合约余额是否充足
require(address(this).balance >= amount, 'Insufficient funds');
if (amount > 0) {
// 使用底层的 call 方法进行转账
(bool success,) = payable(recipient).call{value: amount}(new bytes(0));
require(success, 'Transfer failed');
}
}
技术细节:它使用了 call 而不是 transfer,这在现代 Solidity 开发中更推荐,因为它能处理更复杂的接收逻辑且没有固定 Gas 限制。
为了让合约能付钱,它必须能够接收 lREACT。
receive() virtual external payable {
}
这行代码让你的合约变成了一个“存钱罐”。你可以直接从 MetaMask 或通过水龙头向这个合约地址发送测试币,它会自动接收并存入余额。
AbstractReactive
AbstractReactive 是 Reactive Contracts 的基础合约。它继承自 AbstractPayer.sol 并实现了 IReactive.sol ,提供了对 Reactive Network 系统合约和订阅服务的访问,是合约与 Reactive Network 系统进行通信的桥梁。
为了确保合约在不同的开发阶段(本地模拟 vs. 线上生产)都能正确运行,该合约定义了两个关键的执行模式:
vmOnly:限制函数仅在 ReactVM(本地开发/测试环境)中执行。rnOnly:限制函数仅在 Reactive Network(正式生产网络)中执行。
这种设计类似于在代码中区分 Development 和 Production 环境,确保敏感操作(如订阅管理)只在正确的地方发生。
构造函数负责把合约接入 Reactive Network 的基础设施。它将系统合约(SERVICE_ADDR)同时设定为“收款人”和“服务商”。
constructor() {
// 1. 将系统合约地址同时赋值给供应商和订阅服务
vendor = service = SERVICE_ADDR;
// 2. 授权系统合约可以发起支付操作(用于结算 Gas 费)
addAuthorizedSender(address(SERVICE_ADDR));
// 3. 自动探测当前的运行环境
detectVm();
}
detectVm() 实现了环境的判断,之前讲过。
function detectVm() internal {
uint256 size;
// 使用内联汇编获取系统合约地址的代码长度
// solhint-disable-next-line no-inline-assembly
assembly { size := extcodesize(0x0000000000000000000000000000000000fffFfF) }
// 如果该地址没有代码 (size == 0),说明当前是模拟环境 (ReactVM)
// 否则就是已经部署了系统合约的真实网络
vm = size == 0;
}
因此当你写 contract MyContract is AbstractReactive 时,你其实同时拥有了“监听事件的能力 (IReactive)”和“付钱的能力 (AbstractPayer)”。代码中的 0x0000000000000000000000000000000000fffFfF 是 Reactive Network 的系统预留地址,类似于操作系统中的内核地址空间。
Interfaces
IPayable
可以将 IPayable 理解为一个财务管理接口,它确保合约能够处理资金流(lREACT)并追踪债务状态。
interface IPayable {
// 接受直接支付
receive() external payable;
// 返回合约的未偿债务
function debt(address _contract) external view returns (uint256);
}
receive() external payable
这是 Solidity 中的标准特殊函数,用于接收原生代币(在 Reactive Network 中是 lREACT)。
功能:使合约具备“收钱”的能力。
意义:Reactive 合约必须先存入一定的
lREACT余额,才能支付后续监听和触发回调(Callback)产生的 Gas 费用。没有这个函数,任何向该合约转账的操作都会失败。
debt(address _contract) external view returns (uint256)
这是一个只读(View)函数,用于查询账单。
功能:返回指定合约地址产生的、尚未结算的债务总额。
应用场景:Reactive Network 的系统合约会实现这个接口。当你的 Reactive 合约运行一段时间后,你可以调用这个函数来查看自己欠了系统多少 Gas 费。
IPayer
此接口赋予了合约主动支付的能力。
interface IPayer {
// 发起支付逻辑
function pay(uint256 amount) external;
// 接受直接转账(原生代币进入)
receive() external payable;
}
pay(uint256 amount) external
这是合约主动花钱的出口。
功能:发起一笔指定金额的支付。
应用逻辑:在 Reactive Network 中,当你的合约需要向系统合约(Vendor)结算 Gas 债务,或者向其他授权地址转账时,就会调用这个函数。
在具体的实现类(如
AbstractPayer)中,这个函数通常会配合权限校验(如authorizedSenderOnly),防止合约里的钱被陌生人乱花。
receive() external payable
这依旧是合约的“存钱入口”。
功能:允许合约接收
lREACT原生代币。这和
IPayable中的receive是一致的,确保了资金的双向流动(既能付出去,也能充进来)。
IReactive
IReactive 接口定义的合约的感知(Input)、思考(Process)和行动。
LogRecord 结构体
当 Reactive Network 监听到你订阅的链上事件时,它会把所有的现场信息封装成一个 LogRecord 报文推送到你的合约里。
struct LogRecord {
uint256 chain_id; // 哪个链产生的事件 (如 Sepolia: 11155111)
address _contract; // 哪个合约发出的事件 (如某个 ERC20 地址)
uint256 topic_0; // 事件签名的 Hash (比如 Transfer 事件的 Hash)
uint256 topic_1; // 第一个 indexed 参数 (通常是 sender)
uint256 topic_2; // 第二个 indexed 参数 (通常是 receiver)
uint256 topic_3; // 第三个 indexed 参数
bytes data; // 非 indexed 的原始数据 (如具体的 Amount)
uint256 block_number; // 区块高度
uint256 op_code; // 操作码
uint256 block_hash; // 区块哈希
uint256 tx_hash; // 交易哈希
uint256 log_index; // 日志索引
}
在
react()逻辑里,通常先检查topic_0来判断这是不是你关心的事件类型,再解析topic_1/2和data来提取业务特征。
react() 函数
这是编写业务逻辑的 Entry Point(入口函数)。Reactive VM 会在监听到匹配事件时自动触发它。
function react(LogRecord calldata log) external;
- 逻辑重心: 这是一个异步回调过程。你的合约收到
log后,可以在这里进行复杂的逻辑计算,比如“如果转账金额超过 100 且发送者是某白名单地址”。
Callback 事件
Reactive 合约不能像普通合约那样直接通过 call 调用其他链。它通过 “发射信号” 的方式实现跨链动作。当你发出这个事件,Reactive Network 的执行节点会捕获它,并在目标链上发起交易。
event Callback(
uint256 indexed chain_id, // 你想在哪个链执行动作?
address indexed _contract, // 你想调用那个链的哪个合约?
uint64 indexed gas_limit, // 你愿意为这次执行付多少 Gas?
bytes payload // 具体的函数调用数据 (用 abi.encodeWithSignature 构造)
);
ISubscriptionService
通过这个接口,Reactive 合约实现订阅机制。
ISubscriptionService 扩展自 IPayable.sol。
- 订阅服务并不是免费的。维护一个实时监听的“过滤器”需要消耗网络资源,因此该接口继承了支付功能,以便系统能够处理与订阅相关的费用。
subscribe()
这是最常用的函数,用于注册一个新的监听器。
function subscribe(
uint256 chain_id, // 目标链的 ID (如 Sepolia: 11155111)
address _contract, // 被监听的智能合约地址
uint256 topic_0, // 事件签名的 Hash (Topic 0)
uint256 topic_1, // 索引参数 1 (Topic 1)
uint256 topic_2, // 索引参数 2 (Topic 2)
uint256 topic_3 // 索引参数 3 (Topic 3)
) external;
参数细节拆解:
chain_id:实现了跨链监听的能力。你可以同时订阅以太坊、BNB Chain 或 Polygon 上的事件。topic_0:通常是事件名的 Keccak-256 哈希(例如Transfer(address,address,uint256)的哈希)。topic_1到topic_3:对应 Solidity 事件中的indexed参数。- 技巧:如果你想监听“所有”人的转账,你可以对
topic_1(发送者)使用通配符(通常在库中定义为REACTIVE_IGNORE)。
- 技巧:如果你想监听“所有”人的转账,你可以对
unsubscribe()
当你不再需要监听某个事件时(例如合约任务已完成,或者为了节省 Gas),使用此函数移除过滤器。
function unsubscribe(
uint256 chain_id,
address _contract,
uint256 topic_0,
uint256 topic_1,
uint256 topic_2,
uint256 topic_3
) external;
- 精确匹配:移除订阅时,传入的参数必须与
subscribe时完全一致。只有参数完全匹配的那个特定“过滤器”才会被移除。
ISystemContract
这是一种典型的接口聚合。它并不直接定义新的函数,而是通过继承,将分布在不同模块的功能整合在一起,形成一个统一的交互标准。
import './IPayable.sol';
import './ISubscriptionService.sol';
// ISystemContract 结合了支付和订阅管理的功能
interface ISystemContract is IPayable, ISubscriptionService {
}
网络核心组件
Reactive Network 的操作由三个核心合约处理:
System Contract
系统合约是整个网络的指挥中心,主要负责行政和时间调度。
支付处理:它是总账本,记录所有 Reactive Contracts 的资金往来。
权限管理:通过白名单和黑名单机制,决定哪些合约有资格在网络中运行,确保安全性。
Cron 事件:这是最独特的功能。它会定期发出
Cron事件,就像系统节拍一样,让合约能够实现“定时执行”,而不需要外部触发。
Callback Proxy
由于 Reactive Contracts 运行在 ReactVM 中,不能直接“肉身”去以太坊主网发交易,所以需要一个代理人。
跨链交付:它把你在 Reactive VM 里生成的指令,真正送到目标链(如 Sepolia)的合约手中。
精细理财:它管理着存款、储备金和债务。最重要的是,它会精确计算 Gas 成本。
安全校验:它只允许经过授权的 Reactive Contracts 发起回调,防止被恶意利用。
回扣机制(Kickbacks):它还负责处理 Gas 的返还或回扣逻辑,优化成本。
AbstractSubscriptionService
这个组件负责“从噪音中提取信号”,管理所有的事件订阅。
精准过滤:它支持多维度的过滤——哪条链?哪个地址?哪个事件主题(Topic)?
通配符支持 (
REACTIVE_IGNORE):这是一个非常强大的设计。如果你想监听某个合约的所有事件,或者所有合约的Transfer事件,可以使用这个标记来实现“模糊匹配”。动态更新:每当订阅状态发生变化时,它会发出更新事件,确保整个网络节点同步最新的监听列表。
CRON
在传统的区块链(如以太坊)中,智能合约是“被动”的:如果没有人发送交易触发它,它就永远静止。为了实现定时任务,开发者通常得花钱雇佣外部服务(如 Chainlink Keepers 或 Gelato)。
Reactive Network 通过在系统底层集成 Cron 机制,让合约具备了“主动执行”的能力。
基于区块的可除性
你可以把 SystemContract 想象成一个每产生一个新区块就会打一次卡的时钟。它并不是真的跑了一个 setTimeout,而是根据 区块高度(Block Number) 的整除性来发射(Emit)事件。
触发者:只有获得授权的验证节点(Validators)有权调用
cron()。分发逻辑:如果当前区块号是 $N$:
每一块都会触发
Cron1。如果 $N \pmod{10} == 0$,则同时触发
Cron10。如果 $N \pmod{100} == 0$,则同时触发
Cron100。依此类推。这意味着在一个整万号区块,系统会瞬间并发射出所有等级的 Cron 事件。
使用的话,只需要订阅对应的 Topic 0(事件签名的哈希值)。
| 事件名称 | 区块间隔 | 近似时间 | Topic 0 (十六进制哈希) |
|---|---|---|---|
| Cron1 | 1 块 | ~7 秒 | 0xf02d...cb514 |
| Cron10 | 10 块 | ~1 分钟 | 0x0446...7b687 |
| Cron100 | 100 块 | ~12 分钟 | 0xb499...503c70 |
| Cron1000 | 1000 块 | ~2 小时 | 0xe20b...b325e3f4 |
| Cron10000 | 10,000 块 | ~28 小时 | 0xd214...677a56 |
在 Reactive Contract 中,使用 Cron 只需要两步:
第一步:在部署时订阅
在构造函数或初始化函数中,调用系统服务的 subscribe,并将 _contract 设置为系统合约地址,topic_0 设置为你想要的频率哈希。
第二步:在 react() 中处理
当 react(LogRecord calldata log) 收到信号时,通过判断 log.topic_0 来执行定时任务。
function react(LogRecord calldata log) external override {
if (log.topic_0 == 0x04463f7c1651e6b9774d7f85c85bb94654e3c46ca79b0c16fb16d4183307b687) {
// 这段逻辑大约每分钟会执行一次
doSomethingEveryMinute();
}
}
Events & Callbacks
Event Processing
当源链(Source Chain)发生一个你订阅的事件时,Reactive Network 会将其打包成一个 LogRecord 结构体传给你的合约。
身份标识:
chain_id: 发生事件的链 ID(例如以太坊主网是 1)。_contract: 触发该事件的原生智能合约地址。
事件内容(EVM 标准):
topic_0: 通常是事件签名的哈希值(用于区分是Transfer还是Approval事件)。topic_1,topic_2,topic_3: 事件中被标记为indexed的参数。data: 事件中非indexed的参数,通常以字节码形式存储。
区块元数据:
- 包含
block_number、block_hash、tx_hash和log_index。这些数据提供了确定性,确保你可以追溯该事件在区块链上的确切位置,防止重放攻击或数据混淆。
- 包含
接着,整个合约的入口是 react() 函数:
这是 Reactive 合约的“主循环”或“消息处理器”。
function react(LogRecord calldata log) external vmOnly { ... }
external: 必须由外部(Reactive Network 节点)调用。vmOnly: 这是一个关键的安全修饰符。它确保该函数只能在 ReactVM 内部被网络节点调用,外部用户无法直接通过交易来触发它,从而保证了逻辑执行的封闭性。calldata: 使用calldata而不是memory是为了节省 Gas,因为传入的事件数据是只读的。
一个Demo逻辑:
if (log.topic_3 >= 0.001 ether) { bytes memory payload = abi.encodeWithSignature("callback(address)", address(0)); emit Callback(destinationChainId, callback, GAS_LIMIT, payload); }
条件过滤:
log.topic_3 >= 0.001 ether。这里假设该事件的第三个索引参数(
topic_3)代表金额。注意:在 Solidity 中,
topic是uint256。如果原始事件里这个参数是address,它会被填充为 32 字节。
构建负载 (Payload):
使用
abi.encodeWithSignature编码要执行的函数。关键点:这里填写的
address(0)是一个占位符。根据你之前提供的文档,Reactive Network 会在执行时自动将其替换为你的 ReactVM ID(即部署者的地址)。
触发动作:
emit Callback(...)并不是真的在当前链发出一笔交易,而是向 Reactive Network 发出一个信号。网络监听到这个
Callback事件后,会跨链去destinationChainId目标链,调用callback合约地址里的函数。
Callbacks to Destination Chains
在 Reactive Network 中,合约并不直接调用其他链的函数(因为跨链直接调用在技术上极其复杂),而是通过抛出一个特定的事件来“喊话”。
event Callback(
uint256 indexed chain_id,
address indexed _contract,
uint64 indexed gas_limit,
bytes payload
);
当你在 react() 函数中 emit Callback(...) 时,你实际上是在向 Reactive Network 节点提交一份“工单”。这份工单包含四个关键要素:
chain_id:告诉网络,这笔交易要去哪条链(比如 Polygon, BSC 或以太坊)。_contract:目标链上具体的合约地址。gas_limit:预拨多少 Gas 费。这很重要,因为跨链操作需要支付目标链的资源消耗。payload:核心载荷。这是经过 ABI 编码的函数调用数据(包括函数名和参数)。
这个过程是自动化的:
监测:Reactive Network 实时扫描 ReactVM 的交易追踪(Transaction Trace)。
捕获:一旦发现有
Callback事件被触发,网络节点就会提取其中的参数。中继:节点作为中继者,在目标链上发起一笔真实的交易,调用
_contract里的逻辑。
这其中需要注意的是,网络会自动把 payload 的前 160 位(即前 20 个字节)替换为 ReactVM ID(也就是部署者的地址)。当你编写 payload 的编码逻辑时:
// 即使你在这里写了 address(0)
bytes memory payload = abi.encodeWithSignature(
"stop(address,address,...)",
address(0), // 这个位置就是前 160 位
pair,
...
);
在实际执行时,目标链收到的第一个参数会被网络自动改为你的 ReactVM 地址。
Uniswap 止损单代码 example
bytes memory payload = abi.encodeWithSignature(
"stop(address,address,address,bool,uint256,uint256)",
address(0),
pair,
client,
token0,
coefficient,
threshold
);
triggered = true;
emit Callback(log.chain_id, stop_order, CALLBACK_GAS_LIMIT, payload);
payload
这相当于将一个函数调用序列化为字节流。
bytes memory payload = abi.encodeWithSignature(
"stop(address,address,address,bool,uint256,uint256)",
address(0),
pair,
client,
token0,
coefficient,
threshold
);
函数签名:
"stop(address,address,address,bool,uint256,uint256)"定义了目标链上执行逻辑的接口。参数详解:
address(0)(关键占位符):这是代码中最特殊的地方。根据上一节提到的 Callback Authorization 规则,这 160 位(20 字节)在传输过程中会被 Reactive Network 强行替换为当前 ReactVM 的地址。你填入address(0)只是为了占位,确保后续参数的偏移量正确。pair:Uniswap 的交易对地址(监控对象)。client:下单的用户地址(止损单的受益人)。token0:指示交易对中的哪种代币。coefficient/threshold:止损的具体数值标准(例如价格低于某个阈值时触发)。
triggered = true;
逻辑幂等性:在处理异步事件时,这是一个非常重要的工程实践。
作用:一旦满足止损条件并生成了回调,就将
triggered设为true。这样在 ReactVM 的下一次轮询或处理新事件时,可以防止为同一个止损单重复发送多次回调交易,避免浪费 Gas 费或造成逻辑混乱。emit Callback
这是整个 Reactive 流程的最后一步,也是真正触发链外操作的信号。
emit Callback(log.chain_id, stop_order, CALLBACK_GAS_LIMIT, payload);
log.chain_id:直接从捕获到的事件(log)中获取源链 ID,实现“在哪跌倒,在哪止损”的原路返回操作。stop_order:这是部署在目标链上的执行合约地址。它里面包含了stop()函数的具体实现(比如调用 Uniswap 接口卖出代币)。CALLBACK_GAS_LIMIT:预设的 Gas 限额,确保目标链上的stop()函数有足够的资源运行。
Subscriptions
在 Reactive Network 中,订阅不是自动发生的。你必须显式地告诉系统:“我想听哪些链、哪些合约、哪些事件。”
执行者:系统合约(System Contract)。
接口要求:为了实现订阅,你的合约需要实现
IReactive、AbstractReactive和ISystemContract。
uint256 public originChainId;
uint256 public destinationChainId;
uint64 private constant GAS_LIMIT = 1000000;
address private callback;
constructor(
address _service,
uint256 _originChainId,
uint256 _destinationChainId,
address _contract,
uint256 _topic_0,
address _callback
) payable {
// 1. 初始化系统合约接口
service = ISystemContract(payable(_service));
// 2. 存储基础配置信息
originChainId = _originChainId;
destinationChainId = _destinationChainId;
callback = _callback;
// 3. 静态订阅逻辑
if (!vm) {
service.subscribe(
originChainId, // 监听哪条链
_contract, // 监听哪个合约
_topic_0, // 监听哪个事件签名 (Topic 0)
REACTIVE_IGNORE, // Topic 1: 忽略
REACTIVE_IGNORE, // Topic 2: 忽略
REACTIVE_IGNORE // Topic 3: 忽略
);
}
}
_topic_0:通常是事件的签名(例如keccak256("Transfer(address,address,uint256)"))。REACTIVE_IGNORE:这是一个通配符,表示“我不关心这一位参数是什么,只要 Topic 0 匹配就行”。
Reactive Network 提供了非常精细的过滤能力,它基于源合约的四个维度进行拦截:
Chain ID:区分是以太坊、Polygon 还是其他链。
Contract Address:指定具体的业务合约(如某个特定的 Uniswap Pool)。
Topics 0-3:
Topic 0:事件类型。
Topic 1-3:事件中的
indexed参数(如转账的发送方、接收方)。
注意: 只有被标记为
indexed的参数才会出现在 Topic 中,其他的参数都在LogRecord的data字段里。
通配符工具箱 (Wildcards)
为了实现灵活的过滤,RNK 预定义了特殊的通配符:
A. REACTIVE_IGNORE
这是最重要的通配符,用于匹配 Topic 0 到 Topic 3 中的任何值。
具体值:
0xa65f96fc951c35ead38878e0f0b7a3c744a6f5ccc1476b313353ce31712313ad作用:当你不在乎某个索引参数的具体内容时使用。
B. 零值通配符 (Zero Values)
uint256(0):匹配 任何链 ID (Any chain ID)。address(0):匹配 任何合约地址 (Any contract)。
[!WARNING] 底层约束:为了防止网络过载,你不能把所有参数都设为通配符。至少必须有一个参数是具体确定的值。
**监听某个合约的所有动静 **
如果你想监控一个特定金库(Vault)的所有操作,不论它是转账还是授权
service.subscribe(
CHAIN_ID,
0x7E0987E5b3a30e3f2828572Bb659A548460a3003, // 固定的合约地址
REACTIVE_IGNORE, // 忽略 Topic 0 (即忽略事件类型)
REACTIVE_IGNORE,
REACTIVE_IGNORE,
REACTIVE_IGNORE
);
监听全链某种类型的事件
比如你想捕捉全链上所有的 Uniswap V2 Sync 事件,来计算流动性变化。
service.subscribe(
CHAIN_ID,
address(0), // 匹配该链上任何合约
0x1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1, // Sync 事件的 Topic 0
REACTIVE_IGNORE,
REACTIVE_IGNORE,
REACTIVE_IGNORE
);
精准打击
最常见的生产环境用法:只看特定合约的特定事件。
service.subscribe(
CHAIN_ID,
0x7E0987E5b3a30e3f2828572Bb659A548460a3003, // 锁定合约
0x1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1, // 锁定事件
REACTIVE_IGNORE,
REACTIVE_IGNORE,
REACTIVE_IGNORE
);
Multiple Subscriptions
如果你的逻辑需要同时监控多个目标,你可以在构造函数(或回调)中多次调用 subscribe()。
if (!vm) { // 再次强调,必须在非 ReactVM 环境下执行
// 订阅合约1的某个事件
service.subscribe(
originChainId,
_contract1,
_topic0,
REACTIVE_IGNORE,
REACTIVE_IGNORE,
REACTIVE_IGNORE
);
// 订阅合约2的另一个基于 Topic 1 过滤的事件
service.subscribe(
originChainId,
_contract2,
REACTIVE_IGNORE,
_topic1,
REACTIVE_IGNORE,
REACTIVE_IGNORE
);
}
Unsubscribing
在执行取消订阅操作时,你必须使用与订阅时完全一致的参数(包括通配符)。
首先,需要定义这个预定义的通配符常量:
export REACTIVE_IGNORE=0xa65f96fc951c35ead38878e0f0b7a3c744a6f5ccc1476b313353ce31712313ad
注意: 这里的
REACTIVE_IGNORE哈希值是一个特定值,它告诉系统合约“这个字段在之前的订阅中是被忽略的”。
这里提供一个使用 Foundry 套件中的 cast 工具 直接与系统合约交互的例子。这通常用于手动管理或维护已经部署的合约。
cast send \
--rpc-url $REACTIVE_RPC \
--private-key $REACTIVE_PRIVATE_KEY \
$SYSTEM_CONTRACT_ADDR \
"unsubscribeContract(address,uint256,address,uint256,uint256,uint256,uint256)" \
$REACTIVE_CONTRACT_ADDR \
$ORIGIN_CHAIN_ID \
$ORIGIN_CONTRACT \
$TOPIC_0 \
$REACTIVE_IGNORE \
$REACTIVE_IGNORE \
$REACTIVE_IGNORE
这个 unsubscribeContract 函数调用的参数序列非常严格,必须与订阅时创建的条目精确匹配:
| 参数顺序 | 变量名 | 说明 |
|---|---|---|
| 1 | $REACTIVE_CONTRACT_ADDR | 你自己的 Reactive 合约地址。 |
| 2 | $ORIGIN_CHAIN_ID | 之前监听的源链 ID。 |
| 3 | $ORIGIN_CONTRACT | 之前监听的目标合约地址。 |
| 4 | $TOPIC_0 | 事件的主题哈希。 |
| 5-7 | $REACTIVE_IGNORE | 对应 Topic 1, 2, 3 的通配符占位。 |
Limitations
Equality Matching Only
目前的订阅系统是一个确定性过滤器,不支持任何模糊或范围查询。
支持的操作:
==(等于)。不支持的操作:
数值比较:例如
Topic > 100或Topic < 50。区间范围:例如
Topic在[10, 20]之间。位运算过滤:例如检查某个 Bit 位是否为 1。
启发:如果你需要根据金额大小(如交易额 > 1 ETH)来过滤,你不能在订阅层面解决,而必须在
react()函数内部用 Solidity 代码逻辑来实现判断。
Complex Criteria Sets
每个订阅只能定义一套匹配标准。
不支持逻辑“或”(OR/Disjunctions):你不能在一个订阅请求中说“监听合约 A 或者 合约 B”。
不支持单次订阅多个准则:每个
subscribe()调用都是原子性的,只对应一个过滤规则。
如果你有多个目标,必须多次调用 subscribe()。
- 风险提示:如果你需要监听成千上万个对象,这会导致大量的订阅操作,管理起来会非常复杂,且极度消耗 Gas。
No Global Subscriptions
为了防止系统被垃圾信息淹没(Spam Protection)并保证 ReactVM 的性能,RNK 不允许“听所有人的话”。
以下行为是被禁止的:
全链监听:监听所有链的所有活动。
全合约监听:监听某条链上所有的合约。
全事件监听:监听某条链上所有的事件类型。
规则回顾:正如之前提到的,订阅参数中至少必须有一个是具体的非通配符值。
Duplicate Subscriptions
如果你不小心(或者故意)对同一个目标进行了两次完全相同的 subscribe() 调用:
逻辑上:系统会把它们视为一个订阅。你不会因为重复订阅而收到双倍的事件推送。
经济上:每一次
subscribe()都是一笔交易,你必须为每一笔重复的交易支付 Gas 费。所以,代码逻辑中维护一个“已订阅列表”来避免重复调用是很有必要的。
Dynamic Subscriptions
在实际业务中,我们往往无法预知要监听的对象。
例子:一个监控用户授权(Approval)的合约。你不可能在部署时就知道未来哪个用户会来使用你的服务。
解决方案:当一个新用户触发了“注册”事件时,合约动态地把这个用户的地址加入监听列表。
这里有一个非常关键的设计模式,源于 ReactVM 的只读沙盒属性:
ReactVM (沙盒):负责逻辑计算,但无法直接调用系统合约(System Contract)。
RNK (Reactive Network 节点):拥有管理权,可以操作系统合约。
因此,动态订阅不能直接在 react() 里完成,必须走一个“曲线救国”的路线:
标准执行流 (Typical Flow):
接收事件:ReactVM 收到一个信号。
逻辑判断:合约决定是否需要新增或删除订阅。
发出回调:合约抛出一个
Callback事件(指向自己)。最终执行:Reactive Network 捕获回调,并在 RNK 实例中执行真正的订阅操作。
以下代码运行在 Reactive Network 合约实例上,而不是 ReactVM 内部。这些函数是直接操作 service(系统合约)的工具。
A. 订阅函数 (subscribe)
function subscribe(address rvm_id, address subscriber)
external
rnOnly
callbackOnly(rvm_id)
{
service.subscribe(
SEPOLIA_CHAIN_ID,
address(0), // 监听所有合约
APPROVAL_TOPIC_0, // 监听 Approval 事件
REACTIVE_IGNORE,
uint256(uint160(subscriber)), // 动态锁定这个特定用户 (Topic 1)
REACTIVE_IGNORE
);
}
B. 取消订阅函数 (unsubscribe)
- **subscribe**:将特定的 `subscriber`(比如某个新活跃的用户)加入 `APPROVAL_TOPIC_0` 的监听名单。
- **unsubscribe**:当用户不再使用服务时,将其从监听名单中移除,释放资源。
function unsubscribe(address rvm_id, address subscriber)
external
rnOnly
callbackOnly(rvm_id)
{
service.unsubscribe(
SEPOLIA_CHAIN_ID,
address(0),
APPROVAL_TOPIC_0,
REACTIVE_IGNORE,
uint256(uint160(subscriber)),
REACTIVE_IGNORE
);
}
动态订阅的本质是 “通过事件触发配置变更”。
subscribe:将特定的
subscriber(比如某个新活跃的用户)加入APPROVAL_TOPIC_0的监听名单。unsubscribe:当用户不再使用服务时,将其从监听名单中移除,释放资源。
react() 逻辑如下:
// Methods specific to ReactVM contract instance
function react(LogRecord calldata log) external vmOnly {
if (log.topic_0 == SUBSCRIBE_TOPIC_0) {
bytes memory payload = abi.encodeWithSignature(
"subscribe(address,address)",
address(0),
address(uint160(log.topic_1))
);
emit Callback(
REACTIVE_CHAIN_ID,
address(this),
CALLBACK_GAS_LIMIT,
payload
);
} else if (log.topic_0 == UNSUBSCRIBE_TOPIC_0) {
bytes memory payload = abi.encodeWithSignature(
"unsubscribe(address,address)",
address(0),
address(uint160(log.topic_1))
);
emit Callback(
REACTIVE_CHAIN_ID,
address(this),
CALLBACK_GAS_LIMIT,
payload);
} else {
(uint256 amount) = abi.decode(log.data, (uint256));
bytes memory payload = abi.encodeWithSignature(
"onApproval(address,address,address,address,uint256)",
address(0),
address(uint160(log.topic_2)),
address(uint160(log.topic_1)),
log._contract,
amount
);
emit Callback(
SEPOLIA_CHAIN_ID,
address(approval_service),
CALLBACK_GAS_LIMIT,
payload
);
}
}
分支一:动态订阅 (SUBSCRIBE)
if (log.topic_0 == SUBSCRIBE_TOPIC_0) {
bytes memory payload = abi.encodeWithSignature(
"subscribe(address,address)",
address(0),
address(uint160(log.topic_1))
);
emit Callback(REACTIVE_CHAIN_ID, address(this), CALLBACK_GAS_LIMIT, payload);
}
触发条件:收到一个“订阅”信号事件。
核心逻辑:
自调用模式:注意
address(this)。这意味着 Reactive Contract 正在给自己发指令。参数转换:
log.topic_1存储的是uint256,需要通过address(uint160(...))强转回地址类型,这通常是用户想要监听的目标。链 ID:目标链是
REACTIVE_CHAIN_ID,即在响应式网络内部完成配置更新。
分支二:取消订阅 (UNSUBSCRIBE)
else if (log.topic_0 == UNSUBSCRIBE_TOPIC_0) {
bytes memory payload = abi.encodeWithSignature(
"unsubscribe(address,address)",
address(0),
address(uint160(log.topic_1))
);
emit Callback(REACTIVE_CHAIN_ID, address(this), CALLBACK_GAS_LIMIT, payload);
}
触发条件:收到一个“取消订阅”信号事件。
逻辑:与订阅逻辑镜像,通过回调触发 RNK 实例中的
unsubscribe函数,从系统合约中移除该过滤准则。
分支三:业务逻辑执行 (Application Logic)
这是最重要的一步,处理实际监控到的业务事件(例如 Approval 事件)。
else {
(uint256 amount) = abi.decode(log.data, (uint256)); // 解析非索引数据
bytes memory payload = abi.encodeWithSignature(
"onApproval(address,address,address,address,uint256)",
address(0),
address(uint160(log.topic_2)), // 对应事件中的 indexed 参数
address(uint160(log.topic_1)), // 对应事件中的 indexed 参数
log._contract,
amount
);
emit Callback(SEPOLIA_CHAIN_ID, address(approval_service), CALLBACK_GAS_LIMIT, payload);
}
数据解析:
abi.decode(log.data, (uint256)):从事件的data区域(非 indexed 字段)提取数值。log.topic_1和log.topic_2:直接从 Topic 区域提取索引参数(如授权者和被授权者地址)。
跨链回调:
目标链:
SEPOLIA_CHAIN_ID(这是一个真实的外部测试网)。目标合约:
approval_service(部署在 Sepolia 上的业务处理合约)。动作:调用
onApproval函数,将处理后的数据同步回目标链。
RNK RPC Methods
由于 Reactive Network 运行在定制化的 Geth(以太坊客户端)上,除了标准的以太坊 RPC,它额外增加了这些以 rnk_ 开头的方法,专门用于查看 ReactVM 内部的状态。
rnk_getTransactionByHash
该方法允许你通过 ReactVM 的唯一标识符(rvmId)和交易哈希(txHash)来获取一笔发生在虚拟机内部的详细交易记录。
请求参数 (Parameters)
rvmId:DATA, 20 字节。关联该交易的 ReactVM ID(通常对应部署者的地址)。txHash:DATA, 32 字节。需要查询的具体交易哈希。
cURL 示例
你可以直接通过命令行工具与 RNK 节点交互:
curl --location 'https://lasna-rpc.rnk.dev/' \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"method": "rnk_getTransactionByHash",
"params": [
"0xa7d9aa89cbcd216900a04cdc13eb5789d643176a",
"0xe32b9f60321f7a83ef9dda5daf8cf5b2f5cd523156ee484f417d62d84d1e3044"
],
"id": 1
}' | jq
响应字段解析 (Response Fields)
返回的对象中除了包含标准的以太坊字段(如 hash, from, to, gasUsed 等),最核心的是 RNK 特有的元数据,用于描述这笔交易是如何被“触发”的:
基础字段
hash/number: 交易哈希及其在虚拟机内的序列号(十六进制)。status: 交易状态(1为成功,0为失败)。used/limit: 实际消耗的 Gas 和设定的 Gas 上限。sessionId: 该交易所在的 Reactive Network 区块号。
核心溯源字段 (RNK Specific)
这是最需要关注的,它们构成了因果链:
refChainId: 触发该交易的源链 ID(例如11155111代表 Sepolia)。refTx: 触发该交易的源链原始交易哈希。这让你能瞬间找到是哪笔外部操作导致了 ReactVM 的运行。refEventIndex: 触发事件的类型/索引。对应 EVM 的日志操作码(如LOG3等)。
一个例子:
{
"result": {
"hash": "0xe32b9f60321f7a83ef9dda5daf8cf5b2f5cd523156ee484f417d62d84d1e3044",
"number": "0x9",
"status": 1,
"from": "0xa7d9aa89cbcd216900a04cdc13eb5789d643176a",
"to": "0x6ba34385d9018cfa3341db62b68b5a55839fe71f",
"refChainId": 11155111,
"refTx": "0x52daf0ff44c50da56024f02530ba70fcf653ad11dadb1788b24b20fc824520f5",
"data": "0x0d152c2c0000..."
}
}
因果关系:在 Sepolia 测试网(
11155111)上有一笔哈希为0x52da...的交易产生了一个事件。反应执行:Reactive Network 监测到后,在编号为
109252的区块中触发了 ReactVM 内部的一笔编号为0x9的交易。结果:这笔内部交易执行成功(
status: 1),且你可以通过data字段看到react()函数接收到的具体 payload。
rnk_getTransactionByNumber
该方法允许你通过 ReactVM ID 和 交易序号 (Sequence Number) 来精准定位一笔交易。在 ReactVM 这种事件驱动的环境中,交易通常是按顺序产生的,因此按编号查询非常适合用于遍历某个合约的所有历史行为。
请求参数 (Parameters)
rvmId:DATA, 20 字节。ReactVM 的唯一标识(通常是部署者的钱包地址)。txNumber:HEX。交易的序号,例如示例中的0x9。
cURL 示例代码
curl --location 'https://lasna-rpc.rnk.dev/' \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"method": "rnk_getTransactionByNumber",
"params": [
"0xa7d9aa89cbcd216900a04cdc13eb5789d643176a",
"0x9"
],
"id": 1
}' | jq
响应字段详解 (Response Breakdown)
返回的结果是一个包含 17 个字段的复杂对象。分为 基础元数据、执行详情 和 响应式关联信息 三类:
| 类别 | 字段名 | 含义解析 |
|---|---|---|
| 基础元数据 | hash / number | 交易的哈希值与十六进制编号(如 0x9)。 |
time | 交易发生的时间戳。 | |
sessionId | 该交易所在的 Reactive Network 区块号(十六进制)。 | |
from / to | 交易的发起者与接收合约地址。 | |
| 执行详情 | status | 1 代表成功,0 代表失败。 |
limit / used | 设定的 Gas 上限与实际消耗的 Gas。 | |
root | 交易关联的 Merkle 树根哈希(用于状态验证)。 | |
data | 核心载荷:react() 函数接收到的编码数据。 | |
createContract | 布尔值,标记该交易是否创建了新合约。 | |
| 响应式关联 | refChainId | 源链 ID:触发该交易的原始链(如 Sepolia 为 11155111)。 |
refTx | 源链交易哈希:导致这笔 VM 交易发生的“罪魁祸首”。 | |
refEventIndex | 日志操作码:标记是哪种类型的 LOG 触发了响应。 |
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"hash": "0xe32b9f60321f7a83ef9dda5daf8cf5b2f5cd523156ee484f417d62d84d1e3044",
"number": "0x9",
"time": 1753427529,
"root": "0x8df166bb5c9843696457dbdc5ab20ca1ab9acdd8703b6f1fd1f51766f34fad7d",
"limit": 900000,
"used": 47429,
"type": 2,
"status": 1,
"from": "0xa7d9aa89cbcd216900a04cdc13eb5789d643176a",
"to": "0x6ba34385d9018cfa3341db62b68b5a55839fe71f",
"createContract": false,
"sessionId": 109252,
"refChainId": 11155111,
"refTx": "0x52daf0ff44c50da56024f02530ba70fcf653ad11dadb1788b24b20fc824520f5",
"refEventIndex": 328,
"data": "0x0d152c2c00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000aa36a7000000000000000000000000c156ad2846d093e0ce4d31cf6d780357e9675dce8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925000000000000000000000000a7d9aa89cbcd216900a04cdc13eb5789d643176a00000000000000000000000065a9b8b03a2ef50356104cb594ba2c91223973de00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000086da6000000000000000000000000000000000000000000000000000000000000000034570ac2a3bbfa2809982e69218a745aa83e1bff79b54e2a2ce10e5d6d4c5c00a52daf0ff44c50da56024f02530ba70fcf653ad11dadb1788b24b20fc824520f50000000000000000000000000000000000000000000000000000000000000148000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000003e8",
"rData": "0x"
}
}
rnk_getTransactionLogs
该方法用于检索由特定的 ReactVM 交易序号所触发的所有日志条目。
请求参数 (Parameters)
rvmId:DATA, 20 字节。你的 ReactVM 标识符。txNumber:HEX。交易在虚拟机内的序号(如0x9)。
cURL 示例
通过这个命令,你可以调取编号为 0x9 的交易产生的所有事件:
curl --location 'https://lasna-rpc.rnk.dev/' \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"method": "rnk_getTransactionLogs",
"params": [
"0xA7D9AA89cbcd216900a04Cdc13eB5789D643176a",
"0x9"
],
"id": 1
}' | jq
响应字段详解 (Response Breakdown)
返回的是一个数组(因为一笔交易可以发出多个事件),每个对象包含:
txHash: 产生该日志的 ReactVM 交易哈希。address: 触发该日志的合约地址(即你的 Reactive Contract 地址)。topics: 索引字段数组(最多 4 个)。topics[0]: 事件签名哈希。通过它你可以知道这是Callback事件还是其他自定义事件。topics[1-3]: 事件中标记为indexed的参数。
data: 非索引数据的十六进制字符串。
{
"jsonrpc": "2.0",
"id": 1,
"result": [
{
"txHash": "0xe32b9f60321f7a83ef9dda5daf8cf5b2f5cd523156ee484f417d62d84d1e3044",
"address": "0x6ba34385d9018cfa3341db62b68b5a55839fe71f",
"topics": [
"0x8dd725fa9d6cd150017ab9e60318d40616439424e2fade9c1c58854950917dfc",
"0x0000000000000000000000000000000000000000000000000000000000aa36a7",
"0x000000000000000000000000fc2236a0d3421473676c4c422046fbc4f1afdffe",
"0x00000000000000000000000000000000000000000000000000000000000f4240"
],
"data": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000a42f90252d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000065a9b8b03a2ef50356104cb594ba2c91223973de000000000000000000000000a7d9aa89cbcd216900a04cdc13eb5789d643176a000000000000000000000000c156ad2846d093e0ce4d31cf6d780357e9675dce00000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000"
}
]
}
rnk_getHeadNumber
如果把 ReactVM 的交易历史看作是一条不断延伸的队列,那么这个方法就是用来获取这个队列的当前长度或最新序号。该方法非常简单直接:返回指定 ReactVM 已执行的最后一笔交易的编号。
请求参数 (Parameters)
rvmId:DATA, 20 字节。你需要查询的 ReactVM 唯一标识。
cURL 示例代码
通过这个简单的请求,你可以监控某个虚拟机的最新动态:
curl --location 'https://lasna-rpc.rnk.dev/' \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"method": "rnk_getHeadNumber",
"params": [
"0xA7D9AA89cbcd216900a04Cdc13eB5789D643176a"
],
"id": 1
}' | jq
响应结果解析 (Response Breakdown)
返回的结果非常精简,只有一个十六进制字符串:
{
"jsonrpc": "2.0",
"id": 1,
"result": "0x9"
}
result:0x9(十六进制)。转换:
0x9等于十进制的 9。含义:这意味着该虚拟机目前一共处理了 10 笔交易(从序号
0x0到0x9)。
rnk_getHeadNumber 在开发中通常有三个核心用途:
心跳检测 (Health Check): 如果源链(如 Sepolia)产生了大量事件,但你多次调用
rnk_getHeadNumber发现结果一直停在0x9不动,那说明你的 Reactive Contract 可能出 Bug 了,或者网络出现了延迟,没有在“反应”。轮询起点 (Polling Start): 当你想写一个脚本来拉取所有历史交易时,你会先调用这个方法。比如发现 Head 是
0x9,你就知道应该去抓取从0x0到0x9的数据。增量更新 (Incremental Updates):
你的后端程序可以记下上次处理到的编号(比如
0x7),然后定时调用rnk_getHeadNumber。一旦发现变成了0x9,就说明有 2 笔新交易(0x8和0x9)需要处理。
rnk_getTransactions
该方法允许你通过指定起始编号和数量,批量获取某个 ReactVM 的交易历史。
请求参数 (Parameters)
rvmId:DATA, 20 字节。ReactVM 的唯一标识符。from:HEX。查询的起始交易编号。limit:HEX。本次查询返回的最大交易数量。
cURL 示例
在这个例子中,用户从编号 0x9 开始,请求获取 0x1 笔交易:
curl --location 'https://lasna-rpc.rnk.dev/' \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"method": "rnk_getTransactions",
"params": [
"0xA7D9AA89cbcd216900a04Cdc13eB5789D643176a",
"0x9",
"0x1"
],
"id": 1
}' | jq
响应字段详解 (Response Fields)
返回的结果是一个 数组(Array),数组中的每个对象都包含了该笔交易的完整档案。
| 字段名 | 类型 | 说明 |
|---|---|---|
hash / number | string | 交易哈希与序号(如 0x9)。 |
time | uint64 | 交易发生的 Unix 时间戳。 |
status | uint8 | 1 为成功,0 为失败。这是判断逻辑是否跑通的关键。 |
used / limit | uint32 | 实际消耗与设定的 Gas 限制。 |
from / to | string | 发起者(ReactVM ID)与目标合约地址。 |
sessionId | uint64 | 所在的 Reactive Network 区块号。 |
refChainId | uint32 | 溯源: 触发此交易的源链 ID(如 Sepolia: 11155111)。 |
refTx | string | 溯源: 触发此交易的原始链交易哈希。 |
refEventIndex | uint32 | 溯源: 触发该响应的 LOG 操作码。 |
data | string | 具体的调用数据(Payload)。 |
rData | string | 额外的响应数据(通常为 0x)。 |
一个例子:
{
"jsonrpc": "2.0",
"id": 1,
"result": [
{
"hash": "0xe32b9f60321f7a83ef9dda5daf8cf5b2f5cd523156ee484f417d62d84d1e3044",
"number": "0x9",
"time": 1753427529,
"root": "0x8df166bb5c9843696457dbdc5ab20ca1ab9acdd8703b6f1fd1f51766f34fad7d",
"limit": 900000,
"used": 47429,
"type": 2,
"status": 1,
"from": "0xa7d9aa89cbcd216900a04cdc13eb5789d643176a",
"to": "0x6ba34385d9018cfa3341db62b68b5a55839fe71f",
"createContract": false,
"sessionId": 109252,
"refChainId": 11155111,
"refTx": "0x52daf0ff44c50da56024f02530ba70fcf653ad11dadb1788b24b20fc824520f5",
"refEventIndex": 328,
"data": "0x0d152c2c00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000aa36a7000000000000000000000000c156ad2846d093e0ce4d31cf6d780357e9675dce8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925000000000000000000000000a7d9aa89cbcd216900a04cdc13eb5789d643176a00000000000000000000000065a9b8b03a2ef50356104cb594ba2c91223973de00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000086da6000000000000000000000000000000000000000000000000000000000000000034570ac2a3bbfa2809982e69218a745aa83e1bff79b54e2a2ce10e5d6d4c5c00a52daf0ff44c50da56024f02530ba70fcf653ad11dadb1788b24b20fc824520f50000000000000000000000000000000000000000000000000000000000000148000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000003e8",
"rData": "0x"
}
]
}
逻辑链条:在 Sepolia (
11155111) 上有人做了一笔交易 (0x52da...),你的 Reactive Contract 监测到后,在虚拟机内部执行了第 9 号交易 (0x9)。执行结果:
status: 1说明你的react()函数成功运行,没有抛出异常(Revert)。批量性:虽然示例里
limit是0x1,但在实际开发中,你可以把limit设为0x64(十进制 100),一次性拉取 100 笔记录,极大地减少了网络往返时间(RTT)。
rnk_getRnkAddressMapping
在 Reactive Network 的架构中,合约的地址和执行环境的 ID 是成对存在的,该方法允许你输入一个已经在 Reactive Network 部署好的合约地址,返回该合约所属的 ReactVM ID。
请求参数 (Parameters)
reactNetworkContrAddr:DATA, 20 字节。这是你在 Reactive Network (RNK) 上部署合约后得到的合约地址。
cURL 示例代码
如果你手里有一个合约地址 0xc3e1...,但不知道它的日志或交易去哪里查,你就需要先执行这个命令:
curl --location 'https://lasna-rpc.rnk.dev/' \
--header 'Content-Type: application/json' \
--data '{
"jsonrpc": "2.0",
"method": "rnk_getRnkAddressMapping",
"params": [
"0xc3e185561D2a8b04F0Fcd104A562f460D6cC503c"
],
"id": 1
}' | jq
响应结果解析 (Response Breakdown)
返回的结果非常精简,直接给出了关联的虚拟机 ID:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"rvmId": "0xa7d9aa89cbcd216900a04cdc13eb5789d643176a"
}
}
rvmId: 这是该合约逻辑实际运行的“沙盒”编号。技术关联:在 Reactive Network 中,ReactVM ID 通常等于合约部署者的钱包地址。这意味着同一个用户部署的所有响应式合约通常都会运行在同一个
rvmId之下。