题目
一个 DApp 同时支持 Ethereum Mainnet 和一条 L2。用户的钱包当前在错误网络,目标网络可能尚未加入钱包。请设计从检测、切链、加链到恢复页面数据的完整流程,并说明用户拒绝、钱包不支持、待处理交易和同构 EVM 地址分别怎样处理。
考察目标
- 是否能区分请求切换网络与请求添加网络,两者都不能绕过用户决定。
- 是否能在
chainChanged后使旧链数据、缓存、合约地址和待签名操作失效。 - 是否理解同一个十六进制地址不足以唯一标识跨链资产、合约或业务记录。
考察 wallet_switchEthereumChain、wallet_addEthereumChain 的完整流程以及链上下文隔离与用户拒绝处理。
一个 DApp 同时支持 Ethereum Mainnet 和一条 L2。用户的钱包当前在错误网络,目标网络可能尚未加入钱包。请设计从检测、切链、加链到恢复页面数据的完整流程,并说明用户拒绝、钱包不支持、待处理交易和同构 EVM 地址分别怎样处理。
chainChanged 后使旧链数据、缓存、合约地址和待签名操作失效。页面先从钱包读取实际 chainId,与当前业务目标链比较;读操作可以继续展示明确标注链来源的公共数据,但写按钮必须绑定目标链并在错链时阻止提交。用户点击切链后调用 wallet_switchEthereumChain。若钱包明确表示目标链未知,可以用受信配置调用 wallet_addEthereumChain,成功后再次切链,因为“添加成功”不保证已经成为活动链。任何一步都可能被拒绝或不支持,UI 应保留手动操作说明。收到 chainChanged 后,要原子地更新链上下文、取消或忽略旧请求、重建客户端并重新读取账户与数据。所有缓存键和业务主键至少包含 chainId;相同的 0x 地址在不同链上不能被当成同一个资产或合约。
多链页面常同时存在两个概念:
只读页面可以允许两者不同,例如用户在未连接钱包时浏览 L2 市场。但所有写操作必须在确认页再次校验钱包活动链、账户、合约地址和参数,不能因为列表最初加载于目标链就假设钱包仍在该链。
建议流程是:
wallet_switchEthereumChain,参数 chainId 使用 0x 前缀的十六进制字符串。wallet_addEthereumChain。eth_chainId 验证结果。错误码 4001 通常表示用户拒绝。4902 常被钱包用作未知链提示,但它不是 EIP-1193 规定的通用 Provider 错误码,不能只靠这个数字覆盖所有钱包;连接器层应保留钱包差异,并为不支持自动加链的情况提供手动说明。
EIP-3085 也不要求添加网络后自动切换,所以不能在 wallet_addEthereumChain 返回后立即假设操作环境已经正确。
wallet_addEthereumChain 的 chainId、名称、原生币符号、RPC URL 和浏览器地址必须来自版本化的受信配置,不能直接采纳 URL 参数或普通业务 API 返回值。钱包应该自行验证元数据,但 DApp 不能把安全责任完全推给钱包。
需要检查:
链配置变化应走发布审查,不应让客户端在运行时静默覆盖。
链切换不是只改导航栏标签。下列状态都带有链上下文:
收到 chainChanged 后应取消旧链请求或让其结果因 cache key 不匹配而失效,清理旧订阅,创建目标链客户端并重新读取。已经广播到旧链的交易不能丢失,应保留原 chainId 与 hash,在独立的交易中心继续跟踪;尚未签名的旧请求应作废并要求用户重新确认。
EVM 链可能使用相同的 20 字节地址格式,但这不代表该地址在不同链上对应同一个对象:
因此资产、合约、缓存和交易的标识至少使用 (chainId, address) 或 (chainId, transactionHash)。跨链映射必须由显式配置或桥协议关系提供,不能靠地址相等推断。
type Eip1193Provider = {
request(args: {
method: string;
params?: readonly unknown[];
}): Promise<unknown>;
};
async function ensureChain(
provider: Eip1193Provider,
chain: TrustedChainConfig,
) {
const chainId = `0x${chain.id.toString(16)}`;
try {
await provider.request({
method: "wallet_switchEthereumChain",
params: [{ chainId }],
});
} catch (error) {
if (!isUnknownChainError(error)) throw error;
await provider.request({
method: "wallet_addEthereumChain",
params: [{ ...chain.walletMetadata, chainId }],
});
await provider.request({
method: "wallet_switchEthereumChain",
params: [{ chainId }],
});
}
const actualChainId = await provider.request({ method: "eth_chainId" });
if (actualChainId !== chainId) throw new Error("CHAIN_SWITCH_NOT_APPLIED");
}
示例中的 TrustedChainConfig 和 isUnknownChainError 需要由连接器层实现。产品还应单独识别用户拒绝与不支持的方法,不能把所有异常都进入自动加链分支。
wallet_addEthereumChain 成功就等于已经切到该链。发现这道题有问题? 反馈此题