beautifulremi / R2 - Reactive Essentials(WIP)

Created Mon, 16 Mar 2026 22:21:44 +0800 Modified Mon, 23 Mar 2026 05:26:54 +0000
14151 Words

Reactive Mainnet & Lasna Testnet

无论是主网还是测试网,最关键的是 System Contract 地址是统一的

参数项目Reactive Mainnet (主网)Lasna Testnet (测试网)
网络名称Reactive MainnetReactive Lasna
RPC URLhttps://mainnet-rpc.rnk.dev/https://lasna-rpc.rnk.dev/
Chain ID15975318007
代币符号REACTlREACT
区块浏览器reactscan.netlasna.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(正式生产网络)中执行。

这种设计类似于在代码中区分 DevelopmentProduction 环境,确保敏感操作(如订阅管理)只在正确的地方发生。

构造函数负责把合约接入 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/2data 来提取业务特征。

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_1topic_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 (十六进制哈希)
Cron11 块~7 秒0xf02d...cb514
Cron1010 块~1 分钟0x0446...7b687
Cron100100 块~12 分钟0xb499...503c70
Cron10001000 块~2 小时0xe20b...b325e3f4
Cron1000010,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_numberblock_hashtx_hashlog_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 中,topicuint256。如果原始事件里这个参数是 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 编码的函数调用数据(包括函数名和参数)。

这个过程是自动化的:

  1. 监测:Reactive Network 实时扫描 ReactVM 的交易追踪(Transaction Trace)。

  2. 捕获:一旦发现有 Callback 事件被触发,网络节点就会提取其中的参数。

  3. 中继:节点作为中继者,在目标链上发起一笔真实的交易,调用 _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)。

  • 接口要求:为了实现订阅,你的合约需要实现 IReactiveAbstractReactiveISystemContract

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 提供了非常精细的过滤能力,它基于源合约的四个维度进行拦截:

  1. Chain ID:区分是以太坊、Polygon 还是其他链。

  2. Contract Address:指定具体的业务合约(如某个特定的 Uniswap Pool)。

  3. Topics 0-3

    • Topic 0:事件类型。

    • Topic 1-3:事件中的 indexed 参数(如转账的发送方、接收方)。

注意: 只有被标记为 indexed 的参数才会出现在 Topic 中,其他的参数都在 LogRecorddata 字段里。

通配符工具箱 (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 > 100Topic < 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 的只读沙盒属性

  1. ReactVM (沙盒):负责逻辑计算,但无法直接调用系统合约(System Contract)。

  2. RNK (Reactive Network 节点):拥有管理权,可以操作系统合约。

因此,动态订阅不能直接在 react() 里完成,必须走一个“曲线救国”的路线:

标准执行流 (Typical Flow):

  1. 接收事件:ReactVM 收到一个信号。

  2. 逻辑判断:合约决定是否需要新增或删除订阅。

  3. 发出回调:合约抛出一个 Callback 事件(指向自己)。

  4. 最终执行: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_1log.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交易的发起者与接收合约地址。
执行详情status1 代表成功,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 笔交易(从序号 0x00x9)。

rnk_getHeadNumber 在开发中通常有三个核心用途:

  1. 心跳检测 (Health Check): 如果源链(如 Sepolia)产生了大量事件,但你多次调用 rnk_getHeadNumber 发现结果一直停在 0x9 不动,那说明你的 Reactive Contract 可能出 Bug 了,或者网络出现了延迟,没有在“反应”。

  2. 轮询起点 (Polling Start): 当你想写一个脚本来拉取所有历史交易时,你会先调用这个方法。比如发现 Head 是 0x9,你就知道应该去抓取从 0x00x9 的数据。

  3. 增量更新 (Incremental Updates)

    你的后端程序可以记下上次处理到的编号(比如 0x7),然后定时调用 rnk_getHeadNumber。一旦发现变成了 0x9,就说明有 2 笔新交易(0x80x9)需要处理。

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 / numberstring交易哈希与序号(如 0x9)。
timeuint64交易发生的 Unix 时间戳。
statusuint81 为成功,0 为失败。这是判断逻辑是否跑通的关键。
used / limituint32实际消耗与设定的 Gas 限制。
from / tostring发起者(ReactVM ID)与目标合约地址。
sessionIduint64所在的 Reactive Network 区块号。
refChainIduint32溯源: 触发此交易的源链 ID(如 Sepolia: 11155111)。
refTxstring溯源: 触发此交易的原始链交易哈希。
refEventIndexuint32溯源: 触发该响应的 LOG 操作码。
datastring具体的调用数据(Payload)。
rDatastring额外的响应数据(通常为 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)。

  • 批量性:虽然示例里 limit0x1,但在实际开发中,你可以把 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 之下。