04 · MetaMask 怎么知道你有 2 美元
你恢复了 Account 1,MetaMask 很快就显示:Tether USD · $2.00。
它做了什么?是在本地浏览器里把二十条链扫了一遍,还是把地址发给 MetaMask 的服务器,由服务器查一张缓存表?查询免费吗?有没有 QPS?
先把结论摆出来:这不是一道二选一。现代钱包同时使用本地密钥计算、远程 RPC 节点、共享资产索引、短期缓存和价格服务。
本地:助记词 → 地址
远程:地址 → 链上余额与资产
链外:代币数量 × 市场价格 → 美元价值助记词不需要离开设备;公开地址必须被拿去查询,否则钱包无法知道链上发生了什么。

本地只负责密钥与签名;链上状态、资产索引和美元价格来自不同的远程系统。
01 一次打开钱包,数据走过哪些地方
┌──────────────────────────┐
│ 本地钱包插件 │
│ 解密密钥、派生地址、签名 │
└────────────┬─────────────┘
│ 只发送公开地址/网络
▼
┌──────────────────────────┐
│ 账户与资产聚合 API │
│ 汇总多链、代币、NFT │
└──────┬───────────┬───────┘
│ │
▼ ▼
RPC 节点 资产索引数据库
当前链状态 地址→代币/NFT/历史
│ │
└─────┬─────┘
▼
价格与元数据服务
│
▼
MetaMask 显示 $2MetaMask 当前公开代码和网络清单中可以看到 accounts.api.cx.metamask.io、token.api.cx.metamask.io 等服务痕迹,同时也会使用 RPC 节点读取链状态。具体端点和分工会随版本变化,不能把某个当前 URL 当成协议的一部分。
记忆锚点:密钥在本地,余额在远程读取,美元价格来自链外。
02 查询原生 ETH:节点已经把账算好了
查询一个地址的 ETH 原生余额,可以调用标准 JSON-RPC:
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_getBalance",
"params": [
"0x1111111111111111111111111111111111111111",
"latest"
]
}节点不会为了这次请求从创世区块重新扫描。它在同步每个新区块时,已经持续更新本地状态数据库:
地址 → 原生币余额
地址 → nonce
合约地址 → 合约代码与存储latest 表示使用该 RPC 节点当前看到的最新区块状态。不同服务商可能因为节点同步、负载均衡和短缓存,短时间返回略有差异。
查询历史区块也可以把 latest 换成区块号;但较久的历史状态通常需要 archive node(归档节点),存储和服务成本更高。
记忆锚点:
eth_getBalance是读节点现成的状态,不是扫描一遍交易历史。
03 查询一个 ERC-20:一个合约读一次余额
ETH 是原生币;USDT、USDC、DAI 等 ERC-20 余额存放在各自合约中。
最朴素的查询是通过 eth_call 执行只读的:
USDT.balanceOf(walletAddress)对应请求大致长这样:
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_call",
"params": [
{
"to": "0xUSDT_CONTRACT_ADDRESS",
"data": "0x70a08231...ENCODED_WALLET_ADDRESS"
},
"latest"
]
}to是这个网络上的 USDT 合约地址。0x70a08231是balanceOf(address)的函数选择器。latest指定在哪个区块状态上执行。
这不是查询“最新区块里有没有 USDT 交易”,而是在最新区块完成后的世界状态快照上,读取 USDT 合约为该地址保存的余额。
一个代币一个合约,所以完全暴力的办法确实是:
USDT.balanceOf(address) → 1 次
USDC.balanceOf(address) → 1 次
DAI.balanceOf(address) → 1 次
……问题不是“怎么查已知 USDT”,而是“怎么知道该查哪几个代币”。
04 为什么不能对 50,000 个代币全查一遍
假设一条链候选代币有 50,000 个,扫描 20 条链:
20 × 50,000 = 1,000,000 次合约读取这还只是一个地址。钱包有上百万用户时,现场暴力查询不现实。
Multicall 可以把几百个 balanceOf 包进一个 RPC 请求,减少 HTTP 往返:
一次 RPC
└── Multicall
├── USDT.balanceOf
├── USDC.balanceOf
└── DAI.balanceOf但节点内部仍然要执行这些子调用。Multicall 降低网络开销,不会凭空消除计算、响应大小和服务商限额。
真正的解法是资产索引器。
05 索引器:全链扫描一次,所有用户共享结果
索引器持续消费新区块,解析 ERC-20、ERC-721、ERC-1155 的转移事件,然后建立适合按地址查询的数据库。
链上事件流
→ 解析 Transfer / TransferSingle / TransferBatch
→ 更新地址资产关系
→ address_assets[address] = [USDT, USDC, NFT...]于是钱包查询新地址时,不需要从头扫描:
先问索引器:这个地址与哪些代币相关?
索引器回答:USDT、USDC
再确认:两个合约的当前余额是多少?从 50,000 次降到 2 次,或者由聚合 API直接返回经过验证的结果。
索引器一般是全局共享的。即使这是某地址第一次打开 MetaMask,只要该地址的链上事件早已被索引,数据库里已经可能有记录。
需要注意:并非所有代币都严格通过标准 Transfer 事件改变余额。重基、反射或非标准合约可能需要额外解析和 balanceOf 复核。
记忆锚点:RPC回答“现在余额多少”,索引器回答“这个地址可能拥有哪些资产”。

06 到底有没有缓存,延迟来自哪
至少存在四种不同的“缓存或预计算”:
| 层次 | 保存什么 | 常见新鲜度 |
|---|---|---|
| 节点状态数据库 | 最新区块执行后的账户与合约状态 | 跟随节点头部,通常按区块更新 |
| 资产索引数据库 | 地址与代币、NFT、交易的反向关系 | 通常落后数秒到数分钟 |
| API/CDN 缓存 | 热门元数据、历史数据、短期查询结果 | 按服务策略变化 |
| 钱包本地缓存 | 上次余额、代币列表、UI 状态 | 切换、轮询或手动刷新时更新 |
美元价格还会单独缓存。钱包显示 $2.00,可能是链上数量已经更新、价格仍是几十秒前的;也可能代币索引刚落后一个区块。
因此准确说法不是“缓存还是假的,实时才是真的”。节点状态和索引表都来自真实链数据,只是它们代表的区块高度和更新时间可能不同。
07 查询匿名吗
从链上看,余额查询是匿名且不留痕的:
- 不发送交易
- 不签名
- 不消耗 Gas
- 不写入区块
但对 RPC 或聚合 API运营者,它通常不匿名。服务商可能看到:
IP 地址
请求时间
API Key / 客户端标识
查询的网络
查询的钱包公开地址所以是:链上不留查询记录,链下服务商可能关联 IP 与公开地址。
自己运行节点可以减少第三方观察,但要承担节点、存储、带宽和维护成本。更换 RPC 只能改变信任对象,不能自动保证匿名。
08 限流和费用到底怎么算
区块链协议没有规定 eth_getBalance 每秒只能调用多少次。QPS 是 RPC 服务商的运营规则。
以 2026-09-05 核验的 Infura 公开套餐为例:
| 套餐 | 月费 | 每日 credits | 每秒 credits |
|---|---|---|---|
| Core | 免费 | 300 万 | 500 |
| Developer | 50 美元 | 1,500 万 | 4,000 |
| Team | 225 美元 | 7,500 万 | 40,000 |
官方文档中一次 eth_getBalance 消耗 80 credits。只调用这一种方法时,免费档理论上约为:
3,000,000 ÷ 80 = 37,500 次/天
500 ÷ 80 = 6.25 次/秒实际还会受到突发流量、批量请求、方法权重、网络拥堵和风控影响。MetaMask 内部企业配额与限流没有公开,不能直接套用零售套餐。
普通用户查询余额不付 Gas,也通常不单独向 MetaMask 付费。成本由钱包公司、RPC公司、链基金会或项目方承担,常见商业模式是:
- 免费额度吸引开发者,付费套餐覆盖重度使用。
- 钱包统一购买企业服务。
- 公链或项目补贴公共 RPC。
- 去中心化 RPC 协议向节点运营者付费。
- 服务商通过归档数据、索引、高可用 SLA 等高级能力收费。
“免费 RPC”不是没有成本,而是成本没有在这一次查询时向最终用户收取。
09 自己做一个恢复扫描器,成本多大
假设 10 个 Account,检查 20 条 EVM 链的原生币:
10 × 20 = 200 次 eth_getBalance这对免费额度非常轻。真正昂贵的是:
- 自动发现全部 ERC-20 和 NFT
- 查询完整交易历史
- 支持几十条不同架构的链
- 解析 DeFi 存款、借贷、LP 和质押头寸
- 为大量用户持续保持低延迟
个人恢复工具可以依赖免费 RPC和公开索引 API,成本接近零。大型钱包则需要节点供应商、索引集群、数据库、缓存、监控和多区域容灾,实际企业成本通常不会公开。
10 动手:亲眼看一次只读查询
下面只查询示例地址,不签名、不上链、不付 Gas。将 <RPC_URL> 换成你自己的可信 RPC:
curl -sS <RPC_URL> \
-H 'content-type: application/json' \
--data '{
"jsonrpc":"2.0",
"method":"eth_getBalance",
"params":["0x1111111111111111111111111111111111111111","latest"],
"id":1
}'预期得到十六进制余额,例如:
{"jsonrpc":"2.0","id":1,"result":"0x0"}0x0 表示该地址在这个 RPC 所对应网络上的原生币余额为零。换一个网络 RPC,即使地址不变,结果也可能不同。
预期结果
你会收到一段 JSON,result 是十六进制余额。整个过程没有弹出签名、没有交易哈希,也没有 Gas 费用。
如果不同怎么办
- 返回
401或403:RPC URL 或 API Key 无效,换用该服务商控制台提供的完整端点。 - 返回
429:触发服务商限流,降低频率或稍后重试。 - 返回网络错误:检查 URL、网络连接和服务状态,不要把助记词交给所谓“RPC 客服”。
- 余额与钱包不同:先确认双方连接的是同一条链,再比较区块高度和钱包缓存。
11 小结
- 地址和签名在本地生成,余额需要远程读取。
- 原生币通过节点状态直接查询。
- 已知 ERC-20 可逐合约调用
balanceOf。 - 全部资产发现依赖持续维护的共享索引器。
- 节点、索引器、API和钱包本地都有不同层次的缓存。
- 查询不写链、不付 Gas,但 API运营者可能看到 IP与地址。
- 免费 RPC 的成本由服务商、钱包、项目或协议补贴承担。
一句话记住:钱包显示余额,是把 RPC 的当前状态、索引器的资产目录和价格服务拼在一起。
你现在应该能:把屏幕上的 $2 追溯到地址、网络、代币合约、节点状态、资产索引和美元价格,而不是把它当成 MetaMask 自己保存的一条余额。