Logo
新用户订阅 RPC,首月享 6.5 折优惠查看优惠
OnFinality Learn
网络与协议指南阅读约 13 分钟

Monad 预留余额:为什么 eth_getBalance 会低估可花费的 MON

Monad 会在执行前预留发送者的 gas 上限,因此 eth_getBalance 可能高于账户实际可花费的金额;本指南展示如何读取预留余额接口并计算可花费的 MON。

TL;DR

在 Monad 上,协议会在执行前从交易发送者的余额中预留其 gas 上限,因此交易不会因余额不足而在执行中途失败。结果是,eth_getBalance 返回的是账户总余额,而非立即可花费的金额:当交易处于待处理或执行中状态时,部分余额已被预留且不可用。因此,简单的 balance >= value 检查会与链上状态不一致,可能产生虚假的“余额不足”错误,或导致 UI 显示的余额低于用户预期。Monad 为钱包额外暴露了一个预留余额 RPC 接口,使预留金额可以被显式读取,而无需推断。本文解释该机制,展示一个可运行的 Node.js 检查脚本,用于计算可花费的 MON,并为不支持预留方法的端点提供回退方案,同时提供一个结果表格,供你对照自己的端点进行测量。

预留余额所解决的并行执行问题

Monad 并行执行交易,这意味着交易的结果必须在运行前就可知。如果交易在执行中途才发现发送者已没有足够余额支付 gas,链就必须回滚其他交易可能已经依赖的工作。Monad 的预留余额机制通过在执行开始前从发送者的预留余额中扣除其 gas 上限来防止这种情况,从而保证资金可用。Monad 关于预留余额的文档将其描述为协议层面的保证:交易不会因余额不足而在中途失败。

这是刻意的设计选择,而非记账上的怪癖。在顺序执行的链上,gas 耗尽的交易只需回滚,发送者保留未花费的部分。在并行执行的链上,协议必须预先承诺最大可能成本,以便并发交易不会争抢同一笔资金。预留机制正是使这一承诺显式化的手段。

对于后端开发者和钱包集成方而言,实际后果是链上“可用余额”的概念比总余额更窄。总余额是 eth_getBalance 返回的值;可花费金额是总余额减去当前已预留的部分。理解这一区别,就是余额检查与 Monad 一致还是悄然不一致的分水岭。

该机制由 Monad 自身在预留余额文档和钱包集成指南中记录,而它所扩展的标准方法则由以太坊 JSON-RPC 规范中的 eth_getBalance定义。阅读 Monad 页面了解预留语义,阅读规范了解基础契约,因为只有前者解释了为什么两者在原始数值上一致,但在可花费金额上不同。

  • 预留余额是一种协议保证:发送者的 gas 上限在执行前即被承诺。
  • 它存在的原因是并行执行无法容忍交易中途出现资金不足。
  • 预留金额针对待处理和执行中的交易持有,并在它们结算时释放。

为什么 eth_getBalance 不是完整的可花费余额答案

以太坊 JSON-RPC 规范将 eth_getBalance 定义为返回账户余额(以 wei 为单位)。Monad 实现了该契约,因此该方法返回账户的总余额。规范没有定义单独的“可花费”数值,因为在顺序执行的链上,调用时刻两者实际上相同。在 Monad 上,只要有交易在执行中或账户维持预留,两者就可能不同。

因此,像 balance >= value 这样的简单检查,在账户总余额充足但可花费余额不足时也会通过。随后交易被拒绝,或者钱包显示用户无法与 UI 中显示的余额对上的错误。Monad 文档的《钱包开发者集成指南》将此视为一等集成问题:钱包应读取预留接口,而不是仅凭 eth_getBalance 推断可花费余额。

这一差距不是 eth_getBalance 的缺陷,而是标准方法在回答与客户端所问不同的问题。标准问题是“该账户持有多少”,客户端的问题是“该账户现在能花多少”。在 Monad 上,这两个问题截然不同,而预留接口正是回答第二个问题的方式。

  • 根据以太坊 JSON-RPC 规范,eth_getBalance 返回总余额。
  • 可花费余额等于总余额减去当前预留金额。
  • 当交易处于待处理或执行中状态,或维持预留时,两者会出现差异。

交易生命周期中的预留余额记账

预留余额针对待处理和执行中的交易持有,并在它们结算时释放。当交易被提交时,协议预留发送者的 gas 上限;当交易结算时,该预留中未使用的部分被释放回可花费余额。这意味着可花费余额是一个动态量,只有在结算后才会恢复,而非在提交时。

生命周期交互对轮询逻辑很重要。在提交交易后立即读取 eth_getBalance 的客户端会看到总余额不变,因为预留不会减少总余额。读取预留接口的客户端会看到预留金额上升、可花费余额下降,直到交易结算。Monad 交易生命周期与异步执行收据指南解释了为什么结算时机并不总是立即的,而eth_getTransactionReceipt 返回 null 与收据轮询涵盖了确认释放已发生的轮询模式。

对于从同一账户提交多笔交易的后端,记账会叠加:每笔执行中的交易都持有自己的 gas 上限预留,因此可花费余额会被所有未结算预留的总和所减少。这正是 UI 显示余额低于用户预期这一可见症状背后的机制——部分余额被针对执行中交易的 gas 上限预留了。

  • 预留发生在提交时;释放在结算时发生。
  • 预留不会改变总余额;可花费余额会减少。
  • 多笔执行中的交易会对同一账户累积预留。

显式读取预留的 RPC 接口

钱包或后端不应通过两次余额读取之差来推断预留,而应直接读取预留接口。Monad 文档的 JSON-RPC 概览记录了与标准方法一同暴露给钱包的额外预留余额 RPC 接口。确切的方法名和响应结构由 Monad 记录;请将该方法视为 Monad 专有,并在生产环境依赖它之前,对照目标网络的当前文档进行验证。

标准调用仍然必要。eth_getBalance 提供总余额,eth_getTransactionCount 提供用于交易构造和替换的 nonce。预留接口补充了缺失的第三个输入:当前预留的金额。可花费余额即为总余额减去预留金额。使用 eth_getTransactionCount 进行 EVM nonce 管理指南涵盖了这个三元组中 nonce 的一面,这很重要,因为卡住的 nonce 会使预留保持未结算的时间超出预期。

预留方法是否可用取决于端点。有些提供商暴露它,有些则没有。这是一个“已记录 / 因提供商而异”的区别:机制是 Monad 协议行为,但 RPC 接口的可用性是端点属性。健壮的客户端应探测该方法,并在其缺失时回退到推断,且应将推断视为近似值而非权威数值。

  • 使用 eth_getBalance 读取总余额,使用 eth_getTransactionCount 读取 nonce。
  • 在可用的情况下,从 Monad 的预留余额 RPC 接口读取预留金额。
  • 计算可花费 = 总余额 - 预留;仅当方法缺失时才回退到推断。

Gas 上限、eth_estimateGas 与并行可花费余额

由于整个 gas 上限都会被预留,高估的上限会在交易结算前占用可花费余额。如果一笔交易实际消耗 40,000 gas,但客户端设置了 200,000 的上限,协议会在整个期间按 200,000 预留。差额并未丢失——它会在结算时释放——但在此期间不可用,从而减少了账户可并行花费的金额。

这将 eth_estimateGas 直接与可花费余额联系起来。紧凑而准确的估算能最小化预留金额,并最大化可用于并发交易的余额。留有余量的估算更能防止 out-of-gas 回滚,但会消耗可花费余量。这种权衡是真实存在的,应有意识地做出:对于一次只提交一笔交易的钱包,留余量成本很低;对于从同一账户提交多笔交易的后端,留余量会在每笔执行中的交易上叠加。

使用 eth_feeHistory 估算 gas 价格指南涵盖了交易构造的费用方面。gas 上限方面才是预留交互所在之处,值得测量:提交一笔已知高估的交易,观察可花费余额在结算前的表现。

  • 预留的是完整的 gas 上限,而非实际消耗的 gas。
  • 高估的上限会在结算前减少并行可花费余额。
  • 更紧凑的估算增加可花费余量,但提高 out-of-gas 风险。

一个可运行的 Node.js 可花费余额检查

以下脚本读取总余额,尝试读取预留接口,并报告可花费金额。它仅使用标准 fetch API 和 JSON-RPC POST 请求体,因此可在 Node.js 18 或更高版本上无依赖运行。将端点 URL 替换为你自己的 Monad RPC 端点;Monad RPC 端点(RPC Assistant)页面列出了选项,Monad 主网涵盖了网络本身。

该脚本通过调用预留方法并检查 JSON-RPC 错误来探测它。如果方法未实现,端点会返回错误对象,脚本则回退为报告总余额,并明确注明无法确定可花费余额。这是正确的行为:当预留接口不可用时,绝不要静默地将总余额报告为可花费余额。

// spendable-balance.js — Node.js 18+
// Usage: node spendable-balance.js <rpcUrl> <address>

const rpcUrl = process.argv[2];
const address = process.argv[3];

if (!rpcUrl || !address) {
  console.error('Usage: node spendable-balance.js <rpcUrl> <address>');
  process.exit(1);
}

async function rpc(method, params) {
  const res = await fetch(rpcUrl, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
  });
  const json = await res.json();
  if (json.error) {
    const err = new Error(json.error.message || 'rpc error');
    err.code = json.error.code;
    throw err;
  }
  return json.result;
}

function toMon(weiHex) {
  return Number(BigInt(weiHex)) / 1e18;
}

(async () => {
  const totalHex = await rpc('eth_getBalance', [address, 'latest']);
  const total = toMon(totalHex);
  console.log('total balance (MON):', total);

  // Probe the Monad reserve-balance surface.
  // Method name and params are documented by Monad; verify against current docs.
  let reserved = null;
  try {
    const reservedHex = await rpc('eth_getReserveBalance', [address, 'latest']);
    reserved = toMon(reservedHex);
    console.log('reserved (MON):', reserved);
    console.log('spendable (MON):', total - reserved);
  } catch (e) {
    console.warn('reserve surface unavailable on this endpoint:', e.message);
    console.warn('spendable balance cannot be determined; total shown above.');
  }
})();

用于探测端点预留接口可用性的 curl 命令

在将预留接口接入生产客户端之前,请确认你的端点实现了它。以下 curl 命令发送单个 JSON-RPC 请求并打印原始响应。成功的响应包含一个带有十六进制数量的 result 字段;未实现该方法的端点会返回带有 method-not-found 代码的错误对象。对你打算使用的每个端点运行它,因为可用性是“已记录 / 因提供商而异”的属性。

同样的探测模式适用于任何 Monad 专有方法。将探测保留在你的部署检查清单中,这样提供商变更就不会静默地将你的可花费余额计算降级为推断。

curl -s -X POST "$MONAD_RPC_URL" \
  -H 'content-type: application/json' \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "eth_getReserveBalance",
    "params": ["0xYourAddressHere", "latest"]
  }'

结果表格:在你的端点上测量预留行为

下表是用于测量你自己端点的模板。通过对你使用的每个端点运行 Node.js 脚本和 curl 探测来填写它,然后在交易待处理时重复余额读取。目标是为你的基础设施确定预留接口是否存在,以及可花费余额在负载下的表现。不要依赖本文中的数字;请对照你自己的端点进行测量。

记录端点标签、eth_getBalance 是否存在、预留接口是否存在、总余额、预留金额,以及待处理交易下的可花费金额。预留接口缺失的行并不是端点的失败——它意味着你的客户端必须回退到推断,并应在 UI 或日志中说明这一点。

  • 端点标签:一个你以后能认出的名称。
  • eth_getBalance 是否存在:是/否。
  • 预留接口是否存在:是/否(来自 curl 探测)。
  • 总余额(MON):来自 eth_getBalance。
  • 预留(MON):来自预留接口,或“不适用”。
  • 待处理交易下的可花费(MON):总余额减去预留,或“无法确定”。

故障模式与排查

最常见的症状是钱包或后端在 eth_getBalance 看起来充足时报告“余额不足”。当客户端将总余额与 value 比较而未减去预留时,就会发生这种情况。修复方法是计算可花费余额并与之比较。如果预留接口不可用,客户端应暴露这种不确定性,而不是断言充足。

第二个症状是可花费余额只在结算后恢复。这是预期行为,不是卡住状态:预留会在交易结算时释放。如果恢复看起来缓慢,请通过轮询收据检查交易是否真的在结算,如eth_getTransactionReceipt 返回 null 与待处理收据轮询所述。永不结算的交易——例如卡在 nonce 缺口后面的交易——将无限期持有其预留,这就是 nonce 管理很重要的原因。

第三个症状是端点未实现预留方法,迫使进行推断。推断意味着读取总余额并减去根据你自己的待处理交易得出的预留估计值。该估计值的准确性仅取决于你对内存池的视图,而后者并非权威。将推断的可花费余额视为下限,并在任何区分很重要的流程中优先选择暴露预留接口的端点。

  • 虚假的“余额不足”:与可花费余额比较,而非总余额。
  • 恢复缓慢:在假定故障前,通过收据轮询确认结算。
  • 缺少预留方法:回退到推断,并将结果标记为近似值。

预留模型的局限性与权衡

预留是一种协议保证,但带有 UX 成本。它使并行执行安全,但也意味着在交易执行中时,用户看到的余额与用户能花费的余额不是同一个数字。忽视这一区别的钱包将与 Monad 自身的钱包行为不一致,用户会注意到。

预留接口是 Monad 专有的,不可移植到其他 EVM 链。针对它编写的代码无法在缺少该机制的链上原样运行,而针对那些链编写的代码会在 Monad 上低估可花费余额。如果你维护多链客户端,请将预留逻辑隔离在能力检查之后,而不是在所有地方都假定它存在。

最后,预留接口的可用性因端点而异。机制是已记录的 Monad 行为;RPC 接口是端点属性。设计你的客户端,使缺失预留方法时能优雅降级,并且提供商变更不会静默地将权威的可花费余额数值变成推断值。对于生产工作负载,请审查 RPC 定价API 服务选项,并将预留接口可用性作为明确要求。

  • 预留以更复杂的余额模型为代价提高了执行安全性。
  • 预留接口是 Monad 专有的,不可跨 EVM 链移植。
  • 端点可用性各异;优雅降级并标记推断值。

集成可花费余额的后续步骤

首先使用上面的 curl 命令探测你的端点是否支持预留接口,并将结果记录在表格中。然后将 Node.js 检查接入你的余额显示和预检验证路径,将任何 balance >= value 比较替换为 spendable >= value 比较。在预留接口缺失的地方,记录回退情况,以便了解推断被使用的频率。

接下来,收紧你的 gas 上限策略。审查 eth_estimateGas 的调用位置,以及留余量对你的提交模式是否合理。对于并发提交的账户,测量留余量消耗了多少可花费余量并进行调整。Monad RPC 超时与可靠重试模式指南在这里很有用,因为重试可能产生额外的执行中交易,从而产生额外的预留。

最后,在调试余额差异时始终牢记该机制。OnFinality Learn 中心收集了关于 Monad RPC 行为的相关指南,而 Monad RPC 端点(RPC Assistant)页面是端点选择的起点。将可花费余额视为客户端中的一等量,而非事后的派生值。

  • 探测端点并记录预留接口可用性。
  • 在验证路径中将 balance >= value 替换为 spendable >= value。
  • 审查 gas 上限留余量和重试行为对预留的影响。

永远不用担心基础设施

OnFinality 消除了 DevOps 的繁重工作,让您能够更聪明、更快地构建。

开始