← 返回 MegaWallet 工作文档

MEGAWALLET · WORKING DOCUMENT 04

钱包怎么处理来自 DApp 发起的请求

原始文件 · my-history/4.Wallet-RPC.md

当用户在 dApp 中点击“Connect Wallet”或发起交易时,钱包需要接收、校验、处理这些请求并返回结果。本文档说明 Mega Wallet 为什么要重构这部分架构,以及重构后是如何设计的。


现有架构

整体流程

dApp 调用 window.ethereum.request()


┌─────────────────────────────────────────────────────────┐
│  Provider(mega-wallet-provider)                   │
│                                                         │
│  ProvideEvm → MetaMaskInpageProvider → BaseProvider     │
│           │                                             │
│           └── 通过 globalThis.$jsBridge 发送请求        │
└────────────────────────────┬────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│  Service(mega-wallet/packages/service)            │
│                                                         │
│  service.dApp.request.handleRequest(request)            │
│           │                                             │
│           ▼                                             │
│  ┌─────────────────────────────────────────────────┐   │
│  │  const handler = getHandler(base, service)      │   │
│  │  → new EvmHandler(service)                      │   │
│  │                                                 │   │
│  │  handler[method](request)                       │   │
│  │  → 直接调用对应方法                              │   │
│  │  → 没有的方法走 handler.request() fallback      │   │
│  └─────────────────────────────────────────────────┘   │
│                                                         │
└─────────────────────────────────────────────────────────┘

核心代码

request.ts

async handleRequest(request: JsonRpcRequest) {
  const req = this.addRequest(request)
  try {
    await this.service.dApp.connection.connect(req)
    const { result, error } = await this.#handlerCall(req)
    // ...
  } catch (error) {
    return { error }
  }
}

async #handlerCall(request: TRequest) {
  const handler = getHandler(request.base, this.service)
  const method = handler[request.request.method] || handler.request
  return await method.call(handler, request)
}

handler/evm.ts

class EvmHandler extends BaseHandler {
  async eth_requestAccounts(request: TRequest) {
    const { result } = await this.service.ui.showConfirmation(request, wallet)
    this.emit('connect', { chainId, isConnected: true })
    this.emit('accountsChanged', [address])
    return this.service.dApp.request.updateRequest({ ...request, result: [result.address] })
  }

  async personal_sign(request: TRequest) {
    if (address.toLowerCase() !== this.wallet.address.toLowerCase()) {
      throw new Error('Invalid params')
    }
    return this.#signMessage(request, rawMsg, type)
  }
}

现有架构的场景问题

场景 1:实现细粒度权限控制(CAIP-25)

背景:用户连接 dApp 后,dApp 可以请求签名、发交易。但有些用户只想授权“查看地址”,不想授权“签名”。这需要实现 CAIP-25 权限系统。

需求

如果要改

方案 A:在每个 Handler 方法里加检查

class EvmHandler extends BaseHandler {
  async personal_sign(request: TRequest) {
    if (!this.hasPermission(request.origin, 'sign')) {
      throw new Error('No sign permission')
    }
    // ... 原有逻辑
  }

  async eth_sendTransaction(request: TRequest) {
    if (!this.hasPermission(request.origin, 'transact')) {
      throw new Error('No transact permission')
    }
  }
  // 还有 eth_signTypedData, eth_signTypedData_v3, eth_signTypedData_v4... 每个都要加
}

方案 B:在 handleRequest 里加映射表

async handleRequest(request) {
  const permissionMap = {
    personal_sign: 'sign',
    eth_signTypedData: 'sign',
    eth_sendTransaction: 'transact',
    // ... 几十个方法
  }

  const requiredPermission = permissionMap[method]
  if (requiredPermission && !this.hasPermission(origin, requiredPermission)) {
    if (method === 'eth_accounts') return { result: [] }  // 不同方法不同处理
    return { error: 'Unauthorized' }
  }
}

问题

查看重构后如何解决


场景 2:防止重复弹窗堆积

背景:dApp 可能短时间内发送大量请求,导致用户界面被弹窗淹没。比如 MetaMask 卡顿时经常出现十几个待处理的弹窗请求列表;恶意 dApp 也可能利用这种方式进行钓鱼攻击

需求:相同的 Approval 请求不应重复弹窗,新请求应等待已有弹窗的结果。

如果要改

export default class DAppRequest {
  private pendingApprovals = new Map<string, Promise<unknown>>()

  async handleRequest(request) {
    const approvalMethods = ['eth_requestAccounts', 'personal_sign', 'eth_sendTransaction', ...]

    if (approvalMethods.includes(method)) {
      const key = this.#makeKey(req)
      const existing = this.pendingApprovals.get(key)
      if (existing) return existing

      const promise = this.#handlerCall(req).finally(() => {
        this.pendingApprovals.delete(key)
      })
      this.pendingApprovals.set(key, promise)
      return promise
    }

    return this.#handlerCall(req)
  }
}

问题

查看重构后如何解决


场景 3:钱包锁定状态处理

背景:用户可以设置钱包自动锁定。锁定后,不同的 RPC 方法应该有不同的行为:

如果要改

方案 A:在每个 Handler 方法里加检查

class EvmHandler extends BaseHandler {
  async eth_accounts(request: TRequest) {
    if (this.service.wallet.isLocked) {
      return { result: [] }  // 返回空数组
    }
    // ... 原有逻辑
  }

  async personal_sign(request: TRequest) {
    if (this.service.wallet.isLocked) {
      await this.service.wallet.waitForUnlock()  // 等待解锁
    }
    // ... 原有逻辑
  }

  async eth_chainId(request: TRequest) {
    // 不需要检查锁定状态
    return { result: this.chainId }
  }
}

方案 B:在 handleRequest 里加映射表

async handleRequest(request) {
  const lockBehavior = {
    eth_accounts: 'returnEmpty',
    personal_sign: 'waitUnlock',
    eth_chainId: 'allow',
    // ... 几十个方法
  }

  if (this.service.wallet.isLocked) {
    const behavior = lockBehavior[method]
    if (behavior === 'returnEmpty') return { result: [] }
    if (behavior === 'waitUnlock') await this.service.wallet.waitForUnlock()
    // else allow
  }
}

问题

查看重构后如何解决


场景 4:添加新链(如 Bitcoin)

背景:现有架构通过 getHandler(base, service) 选择 Handler,添加新链需要改多个地方。

如果要改

// 1. 新建 handler/btc.ts
class BtcHandler extends BaseHandler {
  async bitcoin_connect(request) { ... }
  async bitcoin_signPsbt(request) { ... }
}

// 2. 修改 handler/index.ts
const HandlerMap = { evm: EvmHandler, sol: SolHandler, btc: BtcHandler }
export const providerTypeMap = { 'mega-provider-btc': 'btc' }

// 3. 新建 Provider 包(mega-wallet-provider/packages/btc/)

// 4. 修改 types: TNetworkBase = 'evm' | 'sol' | 'btc'

// 5. 找到所有按 base 分支的地方...

问题

查看重构后如何解决


重构后的架构

整体设计

dApp 调用 window.ethereum.request()


┌─────────────────────────────────────────────────────────┐
│  Provider(packages/provider)                          │
│                                                         │
│  EthereumProvider / SolanaProvider                      │
│           │                                             │
│           └── 通过 Transport 抽象发送请求               │
│               ├── MobileTransport (Mobile WebView)      │
│               ├── DesktopTransport (Desktop Electron)   │
│               ├── ExtensionInpageTransport (Inpage)     │
│               └── ExtensionContentTransport (Content)   │
└────────────────────────────┬────────────────────────────┘


┌─────────────────────────────────────────────────────────┐
│  rpc-engine(packages/rpc-engine)                      │
│                                                         │
│  rpcEngine.handle(request, context)                     │
│           │                                             │
│  ┌────────┴────────────────────────────────────┐        │
│  │           Middleware Pipeline               │        │
│  │                                             │        │
│  │  logger → permissionGuard → dedupe →        │        │
│  │  lockGuard → executor                       │        │
│  └────────┬────────────────────────────────────┘        │
│           │                                             │
│     ┌─────┼─────────────────────┐                       │
│     ▼     ▼                     ▼                       │
│  handlers   onApproval      RPC Proxy                   │
│  (即时返回)  (需用户确认)    (转发到节点)                │
└─────────────────────────────────────────────────────────┘

rpc-engine 核心设计

1. 中间件管道(Middleware Pipeline)

// engine.ts
const middleware = [
  loggerMiddleware,          // 日志记录
  permissionGuardMiddleware, // 权限检查
  dedupe.middleware,         // 请求去重
  lockGuardMiddleware,       // 锁定状态检查
  executorMiddleware         // 执行请求
]
const engine = JsonRpcEngineV2.create({ middleware })

每个中间件职责单一,可独立测试、可插拔。

2. 方法配置(声明式)

// chainConfigs/eip155/index.ts
const config: Record<string, MethodConfig> = {
  eth_accounts: {
    requiresPermission: {
      scope: PermissionScope.Accounts,
      missingBehavior: { returnValue: [] }
    },
    lockBehavior: { returnValue: [] }
  },
  personal_sign: {
    requiresPermission: { scope: PermissionScope.Sign },
    requestCategory: RequestCategory.RequiresApproval
  },
  eth_chainId: {
    lockBehavior: LockBehavior.Allow
  }
}

3. 请求分类

类型 说明 判断条件 示例
handlers 钱包内部处理 handlers[method] 存在 eth_chainId, eth_accounts
onApproval 需用户确认 requestCategory === RequiresApproval personal_sign, eth_sendTransaction
RPC Proxy 转发到节点 以上都不满足 eth_getBalance, eth_call

4. 遵循 CAIP 标准的异构链模块化

整体架构遵循 CAIP (Chain Agnostic Improvement Proposal) 标准规范,支持异构链钱包:

CAIP 标准 名称 用途 使用位置
CAIP-2 区块链标识 链 ID 格式 namespace:reference RpcContext、ChainModule
CAIP-10 账户标识 账户格式 namespace:reference:address SessionScope
CAIP-25 权限请求 权限 scope 模型 (accounts/sign/transact) permissionGuard 中间件

链标识示例 (CAIP-2)

type ChainModule<Provider = unknown> = {
  namespace: string
  info: WalletInfo
  createProvider: (transport: Transport) => Provider
  expose: (options: ExposeOptions<Provider>) => () => void  // 返回 cleanup
}

// 每个链在 index.ts 中定义 ChainModule
// chains/ethereum/index.ts
export const ethereumChainModule: ChainModule<EthereumProvider> = {
  namespace: 'eip155',
  info: EthereumProvider.info,
  createProvider: (transport) => createEthereumProvider({ transport }),
  expose: (options) => exposeEthereumProvider(options)
}

// 使用
const providers = initializeProviders({
  targetWindow: window,
  transport,
  chains: [ethereumChainModule, solanaChainModule]
})

重构后的场景解决

场景 1:实现细粒度权限控制(CAIP-25)

对应现有架构的问题

解决方式:通过方法配置声明权限,permissionGuard 中间件自动处理。

配置声明:

// chainConfigs/eip155/index.ts
eth_accounts: {
  requiresPermission: { scope: PermissionScope.Accounts, missingBehavior: { returnValue: [] } }
},
personal_sign: {
  requiresPermission: { scope: PermissionScope.Sign },
  requestCategory: RequestCategory.RequiresApproval
}

中间件自动检查:

// middlewares/permissionGuard.ts
const methodConfig = getMethodConfig(namespace, method)
const requiredScope = getPermissionScope(methodConfig?.requiresPermission)

if (!requiredScope) return next()
if (hasPermission({ origin, scope: requiredScope })) return next()

if (methodConfig?.requiresPermission?.missingBehavior?.returnValue !== undefined) {
  return methodConfig.requiresPermission.missingBehavior.returnValue
}
throw providerErrors.unauthorized(`Requires "${requiredScope}" permission`)

如果要加新方法的权限:只需在配置文件加一行。

对比项 现有架构 重构后
改动位置 每个 Handler 方法 或 handleRequest 配置文件一处
新增方法 手动加检查,容易漏 加配置,中间件自动生效
missingBehavior 分散在各处的 if-else 配置声明,统一处理

场景 2:防止重复弹窗堆积

对应现有架构的问题

解决方式:独立的 dedupe 中间件。

// middlewares/dedupe.ts
const APPROVAL_METHODS = new Set(['eth_requestAccounts', 'personal_sign', ...])

export function createDedupeMiddleware() {
  const pending = new Map<string, Promise<unknown>>()

  return createMiddleware(async ({ request, context, next }) => {
    if (APPROVAL_METHODS.has(method)) {
      const key = `${origin}:${caip2}:${method}`
      const existing = pending.get(key)
      if (existing) return existing

      const promise = next().finally(() => pending.delete(key))
      pending.set(key, promise)
      return promise
    }
    return next()
  })
}

如果要调整去重逻辑:只改这一个中间件文件。

对比项 现有架构 重构后
代码位置 混在 handleRequest 业务逻辑里 独立中间件文件
可测试性 需要 mock 整个 Service 中间件可独立测试
扩展 改 handleRequest 修改中间件或配置

场景 3:钱包锁定状态处理

对应现有架构的问题

解决方式:通过方法配置声明 lockBehavior,lockGuard 中间件自动处理。

配置声明:

// chainConfigs/eip155/index.ts
eth_chainId: {
  lockBehavior: LockBehavior.Allow  // 锁定时也允许
},
eth_accounts: {
  lockBehavior: { returnValue: [] }  // 锁定时返回空数组
},
personal_sign: {
  lockBehavior: LockBehavior.WaitForUnlock,  // 等待解锁
  requestCategory: RequestCategory.RequiresApproval
}

中间件自动检查:

// middlewares/lockGuard.ts
const methodConfig = getMethodConfig(namespace, method)
const lockBehavior = methodConfig?.lockBehavior

if (!isLocked()) return next()

if (lockBehavior === LockBehavior.Allow) return next()
if (lockBehavior?.returnValue !== undefined) return lockBehavior.returnValue
if (lockBehavior === LockBehavior.WaitForUnlock) {
  await waitForUnlock()
  return next()
}
throw providerErrors.unauthorized('Wallet is locked')

如果要改某个方法的锁定行为:只需在配置文件改一行。

对比项 现有架构 重构后
改动位置 每个 Handler 方法 或 handleRequest 配置文件一处
新增方法 手动加检查,容易漏 加配置,中间件自动生效
多种行为 if-else 分支 枚举值声明,统一处理

场景 4:添加新链(如 Bitcoin)

对应现有架构的问题

解决方式:ChainModule + 方法配置。

  1. 添加方法配置:
// chainConfigs/bitcoin/index.ts
const config = {
  bitcoin_connect: { requestCategory: RequestCategory.RequiresApproval },
  bitcoin_signPsbt: {
    requiresPermission: { scope: PermissionScope.Transact },
    requestCategory: RequestCategory.RequiresApproval
  }
}
  1. 实现 ChainModule:
// chains/bitcoin/index.ts
export const bitcoinChainModule: ChainModule<BitcoinProvider> = {
  namespace: 'bitcoin',
  info: BitcoinProvider.info,
  createProvider: (transport) => new BitcoinProvider({ transport }),
  expose: ({ targetWindow, provider }) => {
    targetWindow.bitcoin = provider
    return () => delete targetWindow.bitcoin
  }
}
  1. 注册到 Engine:
createRPCEngine({
  chains: {
    bitcoin: { handlers: bitcoinHandlers, onApproval: handleBitcoinApproval }
  }
})

如果要加新链:按模块添加,位置明确。

对比项 现有架构 重构后
权限检查 每个 Handler 方法里加 配置声明,自动生效
中间件复用 没有中间件 logger/dedupe/lockGuard 自动应用
修改点 多个分散的文件 按模块组织,位置明确

其他钱包的 RPC 处理架构

MetaMask

MetaMask 使用 @metamask/json-rpc-engine 中间件架构处理 dApp 请求,设计思路与 Mega 的 rpc-engine 类似:

Mega rpc-engine 参考了 MetaMask 的中间件设计,但针对多链场景做了扩展(CAIP-2 链标识、按 namespace 组织配置)。

Rainbow

Rainbow 是纯 React Native 钱包(全 JS/TS,用 zustand 管理状态),没有独立的 RPC 中间件架构:

没有中间件架构意味着:权限检查、方法路由等逻辑直接写在请求处理函数里,代码相对简单但扩展性有限。


如果要加,WalletConnect 应该接在哪层

WalletConnect 与 Transport 平级,直接调用 rpcEngine.handle()

内置浏览器 dApp ───▶ Transport ───┐


                           rpc-engine.handle()


外部 dApp ─────────▶ WalletConnect ─┘
// service/dapp/walletconnect.ts
onSessionRequest = async (event: WCSessionRequest) => {
  const { chainId, request } = event.params

  const result = await this.service.rpcEngine.handle(request, {
    origin: session.peer.metadata.url,
    caip2: chainId,
    namespace: chainId.split(':')[0]
  })

  await this.client.respondSessionRequest({ topic, response: { id, result } })
}

好处:复用所有中间件(权限、去重、锁定检查),CAIP-2 原生兼容。


Provider 包的改进

对比现有架构(mega-wallet-providermega-wallet-js-bridge)和 MetaMask Provider,重构后的 @megaeth-labs/provider 包有以下改进:

1. Transport 内聚

现有架构

// Provider 依赖全局 bridge
class ProviderDelegate extends ProviderBase {
  delegateRequest = async (payload, cb) => {
    return this.bridgeRequest(payload, cb)
  }
}

// 各平台继承实现,硬编码平台 API
class MobileInjectJsBridge extends JsBridge {
  sendData(message: string) {
    globalThis.ReactNativeWebView.postMessage(message)
  }
}

重构后的架构

// Transport 接口统一
interface Transport {
  connect(): Promise<void>
  disconnect(): Promise<void>
  isConnected(): boolean
  getConnectionState(): TransportState
  request(args: RequestArguments, options?: TransportRequestOptions): Promise<unknown>
  on(event: string, listener: (...args: unknown[]) => void): void
  removeListener(event: string, listener: (...args: unknown[]) => void): void
}

// 不同平台,同一个 Provider,不同 Transport
const transport = new MobileTransport()          // Mobile WebView
const transport = new DesktopTransport()         // Desktop Electron
const transport = new ExtensionInpageTransport() // Extension Inpage

const provider = new EthereumProvider({ transport })
对比项 现有架构 重构后的架构
新增平台 改两个包 + Provider 继承链 实现一个 Transport 类
代码复用 Provider 逻辑重复 同一个 Provider,不同 Transport
测试 Mock 全局对象 Mock Transport 接口

2. 移除复杂继承链

MetaMask Provider 的问题

现有架构(基于 MetaMask)

// 4 层继承,改底层影响所有上层
class ProviderEvm extends MetaMaskInpageProvider { }
class MetaMaskInpageProvider extends AbstractStreamProvider { }
class AbstractStreamProvider extends BaseProvider { }
class BaseProvider extends ProviderDelegate { }

继承链带来的问题:

重构后的架构

// 单一类,组合优于继承
class EthereumProvider extends EventEmitter {
  #transport: Transport
  #state: EthereumProviderState
}

3. 支持异构链(CAIP-25 / CAIP-217)

MetaMask Provider:只支持 EVM,无法扩展到其他链。

重构后的架构:遵循 CAIP-25(Wallet Request/Respond)和 CAIP-217(Multi-Chain Authorization)标准,ChainModule 插件化支持 EVM + Solana + 更多链。

// chains/ethereum/index.ts
export const ethereumChainModule: ChainModule<EthereumProvider> = {
  namespace: 'eip155',  // CAIP-2 namespace
  info: EthereumProvider.info,
  createProvider: (transport) => createEthereumProvider({ transport }),
  expose: (options) => exposeEthereumProvider(options)  // window.ethereum + EIP-6963
}

// chains/solana/index.ts
export const solanaChainModule: ChainModule<SolanaProvider> = {
  namespace: 'solana',  // CAIP-2 namespace
  info: SolanaProvider.info,
  createProvider: (transport) => new SolanaProvider({ transport }),
  expose: (options) => exposeSolanaProvider(options)  // window.solana + Wallet Standard
}

// 初始化时按需加载
initializeProviders({
  transport,
  chains: [ethereumChainModule, solanaChainModule]
})

4. 事件处理标准化(CAIP-311 / CAIP-319)

现有架构

重构后的架构

// Transport 发出标准化事件(CAIP-311)
Transport.emit('sessionChanged', {
  sessionScopes: {
    "eip155:1": { accounts: ["eip155:1:0x..."] },      // CAIP-10 账户格式
    "solana:mainnet": { accounts: ["solana:mainnet:ABC..."] }
  }
})

// 各 Provider 按 CAIP-2 scope 过滤
EthereumProvider → isEip155Scope() → emit('accountsChanged')
SolanaProvider   → isSolanaScope() → emit('accountChanged')
// EthereumProvider 解析 CAIP-311 sessionScopes
#onSessionChanged = (params) => {
  const { sessionScopes } = params
  const parsed = EthereumProviderState.parseSessionScopes(sessionScopes)
  if (parsed) {
    this.#applyChainChanged(parsed.chainId)
    this.#applyAccountsChanged(parsed.accounts)
  }
}

5. Timeout 配置标准化

现有架构

重构后的架构

Provider 和 rpc-engine 使用分层超时策略,Provider 超时略长于 rpc-engine,确保 rpc-engine 先超时并返回明确错误,而非 Transport 层断开:

配置 Approval Normal Readonly
rpc-engine DEFAULT_TIMEOUTS 5 min 30s 30s
Provider PROVIDER_TIMEOUTS 5.5 min 35s 35s

Provider 根据方法类型自动选择超时:

const config = getMethodConfig('eip155', method)  // 从 rpc-engine 获取方法配置
switch (config?.requestCategory) {
  case RequestCategory.RequiresApproval:
    return PROVIDER_TIMEOUTS.approvalTimeoutMs   // 需审批 5.5min
  case RequestCategory.Readonly:
    return PROVIDER_TIMEOUTS.readonlyTimeoutMs   // 只读 35s
  default:
    return PROVIDER_TIMEOUTS.normalTimeoutMs     // 普通 35s
}

总结

改进点 现有架构问题 重构后的架构方案
通信层 分包 + 硬编码 Transport 接口内聚
继承链 4 层深继承 单层组合
多链支持 仅 EVM ChainModule 插件化(CAIP-25/217)
事件格式 自定义 CAIP-311/319 标准
超时配置 硬编码 与 rpc-engine 联动,分层超时