区块链钱包开发(十)—— 构建主控制器metamask-controller.js

前言

metamask-controller.js可以说是钱包中最重要的同时也是最复杂的脚本,它就像大脑里的中枢神经,负责管理整个钱包的核心业务逻辑和状态。这个文件实现了一个庞大的 MetamaskController 类,整合了账户管理、网络切换、交易处理、权限控制、硬件钱包支持、通知、钓鱼检测等所有核心功能,并对外暴露统一的 API 和事件流。所有前端页面、内容脚本、DApp 以及后台服务的交互,最终都要经过这个控制器。

源码:github.com/MetaMask/me...

建立通信通道

我们在上一章埋下了一个坑,在介绍建立可信通道与不可信通道时没有深入讲解,因为它的具体实现在metamask-controller.js中,所以我们统一在这一章讲解。

根据不同的请求发起方,钱包会建立不同的通信通道(通过多路复用流)。在metamask中大致可以分为以下五种:

可信通信通道

当通信对象为内部UI界面(popup、notification、fullscreen)会建立可信通信通道。UI 需要既能访问完整的后台 API(如账户管理、签名、网络切换等),又能像 Dapp 一样通过 provider 访问区块链。

伪代码:

js 复制代码
// 伪代码:MetaMaskController.js 关键通信方法缩写

function setupTrustedCommunication(connectionStream, sender) {
  // 多路复用流
  mux = multiplex(connectionStream)
  // 连接控制器API
  setupControllerConnection(mux.createStream('controller'))
  // 连接EIP1193 provider
  setupProviderConnectionEip1193(mux.createStream('provider'), sender, 'Internal')
}

function setupControllerConnection(outStream) {
  patchStore = new PatchStore(memStore)
  uiReady = false

  // 监听状态更新,推送补丁
  on('update', () => {
    if (uiReady && streamWritable(outStream)) {
      patches = patchStore.flush()
      outStream.write({ method: 'sendUpdate', params: [patches] })
    }
  })

  // 提供API
  api = { ...getApi(), startPatches: () => { uiReady = true } }
  outStream.on('data', handleRPC(api, outStream))

  // 连接计数
  activeControllerConnections += 1
  emit('controllerConnectionChanged', activeControllerConnections)

  // 断开时清理
  outStream.on('end', () => {
    activeControllerConnections -= 1
    emit('controllerConnectionChanged', activeControllerConnections)
    removeListener('update')
    patchStore.destroy()
  })
}

function setupProviderConnectionEip1193(outStream, sender, subjectType) {
  tabId = sender.tab?.id

  // 构建provider engine
  engine = setupProviderEngineEip1193({sender, subjectType, tabId })

  // 连接流
  providerStream = createEngineStream(engine)

  // 管道连接
  pipeline(outStream, providerStream, outStream, (err) => {
    engine.destroy()
  })
}

function setupProviderEngineEip1193({ origin, subjectType, sender, tabId }) {
  engine = new JsonRpcEngine()
  // 中间件链
  engine.push(originMiddleware(origin))
  if (tabId) engine.push(tabIdMiddleware(tabId))
  engine.push(loggerMiddleware(origin))
  engine.push(permissionLogMiddleware())
  engine.push(tracingMiddleware())
  engine.push(ppomMiddleware())
  engine.push(trustSignalsMiddleware())
  engine.push(rpcMethodTrackingMiddleware())
  engine.push(unsupportedMethodMiddleware())
  // 权限与账户
  engine.push(ethAccountsMiddleware(getPermittedAccounts(origin)))
  if (subjectType !== 'Internal') {
    engine.push(permissionControllerMiddleware(origin))
  }
  // 其他功能
  engine.push(eip1193MethodMiddleware({ ...commonHooks(origin) }))
  engine.push(snapsMethodMiddleware())
  engine.push(filterMiddleware())
  engine.push(subscriptionManagerMiddleware())
  engine.push(metamaskMiddleware())
  engine.push(providerAsMiddleware(proxyClient.provider))
  return engine
}

创建多路复用(Multiplex)流

  • 首先对 connectionStream 进行多路复用(multiplex),这样可以在同一个物理流上开多个逻辑子流(channels)。
js 复制代码
const mux = setupMultiplex(connectionStream);

暴露完整后台 API(controller channel)

  • 通过 mux.createStream('controller') 创建一个名为 controller 的子流。
  • 调用 setupControllerConnection,将后台 API(getApi() 返回的所有方法)通过 JSON-RPC 方式暴露给 UI。
  • UI 通过这个通道可以直接调用后台的各种管理方法(如账户、网络、签名、设置等)。
js 复制代码
this.setupControllerConnection(mux.createStream('controller'));
  • 具体做法是监听 outStream.on('data', ...),收到 JSON-RPC 请求后分发到 API 方法,并将结果写回流。

暴露 EIP-1193 provider 接口(provider channel)

  • 通过 mux.createStream('provider') 创建一个名为 provider 的子流。
  • 调用 setupProviderConnectionEip1193,将 EIP-1193 provider(即以太坊 JSON-RPC 标准接口)暴露给 UI。
  • UI 通过这个通道可以像 Dapp 一样发起 eth_accountseth_sendTransaction 等区块链请求。
js 复制代码
this.setupProviderConnectionEip1193(mux.createStream('provider'), sender, SubjectType.Internal);
  • 这里会构建一个 provider engine(中间件链),并通过流与 UI 连接。

管道连接(pipeline)

  • setupProviderConnectionEip1193 内部,使用 pipelineoutStream、provider engine stream 连接起来,形成双向通信。
  • 这样 UI 发来的请求会经过 provider engine 处理,响应再写回 UI。
js 复制代码
pipeline(
  outStream,
  providerStream,
  outStream,
  (err) => { ... }
);
  • 其中 providerStream 是通过 createEngineStream({ engine }) 创建的,负责把 JSON-RPC 请求转发到 provider engine。

不可信通信通道

当通信对象为内容脚本(DAPP),只能通过 provider 访问区块链,不能访问后台管理API。

伪代码:

js 复制代码
function setupUntrustedCommunicationEip1193({ connectionStream, sender, subjectType }) {
  // 钓鱼检测
  const phishingTestResponse = this.phishingController.test(sender.url);
  if (phishingTestResponse?.result) {
    this.sendPhishingWarning(connectionStream, sender.url);
  }
  // 多路复用主流
  mux = multiplex(connectionStream)

  // provider channel:暴露EIP-1193 provider接口
  setupProviderConnectionEip1193(
    mux.createStream('provider'),
    sender,
    subjectType || inferSubjectType(sender)
  )

  // publicConfig channel(兼容老Dapp):暴露部分公开配置信息
  if (sender.url) {
    setupPublicConfig(mux.createStream('publicConfig'))
  }
}

function setupProviderConnectionEip1193(outStream, sender, subjectType) {
  tabId = sender.tab?.id

  // 构建provider engine(中间件链)
  engine = setupProviderEngineEip1193({ sender, subjectType, tabId })

  // 连接流
  providerStream = createEngineStream(engine)

  // 管道连接
  pipeline(
    outStream,
    providerStream,
    outStream,
    (err) => {
      engine.destroy()
    }
  )
}

不可信通信通道setupUntrustedCommunicationEip1193的实现实际上就是在可信通道setupTrustedCommunication的基础上:

  • 增加了钓鱼检测
  • 去除了访问后台API功能

多链通信通道

适用于支持多链的DAPP,不做深入研究。

钓鱼警告页通信通道

当通信对象是钓鱼警告页面时会建立专用通信通道。其主要作用是让钓鱼警告页面能够与后台安全地交互,执行如"加入白名单"或"返回安全页面"等操作。

js 复制代码
  setupPhishingCommunication({ connectionStream }) {
    const mux = setupMultiplex(connectionStream);
    const phishingStream = mux.createStream(PHISHING_SAFELIST);

    phishingStream.on(
      'data',
      createMetaRPCHandler(
        {
          safelistPhishingDomain: this.safelistPhishingDomain.bind(this),
          backToSafetyPhishingWarning:
            this.backToSafetyPhishingWarning.bind(this),
        },
        phishingStream,
      ),
    );
  }
  • 监听 phishingStream 上的 data 事件(即收到消息)。

  • 使用 createMetaRPCHandler 创建一个 RPC 处理器,将两类方法暴露给前端页面:

  • safelistPhishingDomain:将某个域名加入钓鱼白名单(例如用户点击继续访问此网站)。

  • backToSafetyPhishingWarning:让用户返回安全页面(关闭当前危险页面或跳转到安全页面)。

营销页通信通道

与营销页面通信时的通信通道,不做深入研究。

控制器的初始化

主控制器的初始化是一个非常复杂的过程涉及到:

类别 主要内容
状态与存储 memStore、initState、状态订阅与同步
控制器实例化 交易、账户、网络、权限、签名、审批、钓鱼检测、通知、资产、NFT、DeFi等
Messenger与事件 各控制器专属Messenger、事件订阅与分发
UI相关 getApi、通知、Badge、Popup、UI状态同步
安全与风控 钓鱼检测、风控、日志、追踪等安全控制器
API/Provider/Middleware provider、middleware、API初始化

其中最主要的是控制器的实例化,它实例化了数十个控制器,然后通过消息总线(见第六章)规定了它们的事件订阅关系,方法调用关系。

伪代码:

js 复制代码
class MetamaskController extends EventEmitter {
  constructor(opts) {
    // 控制器消息总线实例化
    this.controllerMessenger = new Messenger();
    // 存储与状态初始化
    this.store = new ComposableObservableStore();
    this.memStore = new ObservableStore();

    // 控制器实例化
    this.accountsController = new AccountsController({
      messenger: this.controllerMessenger.getRestricted({
        name: 'AccountsController',
        allowedEvents: [
          'SnapController:stateChange',
          'KeyringController:accountRemoved',
          'KeyringController:stateChange',
          'MultichainNetworkController:networkDidChange',
        ],
        allowedActions: [
          'KeyringController:getState',
          'KeyringController:getKeyringsByType',
        ],
    });,
      state: initState.AccountsController,
    });
    this.networkController = new NetworkController();
    this.permissionController = new PermissionController();
    this.signatureController = new SignatureController();
    this.approvalController = new ApprovalController();
    this.keyringController = new KeyringController();
    // ... 其他控制器

    // 事件监听与订阅
    this.controllerMessenger.subscribe('KeyringController:unlock', () =>
      this._onUnlock(),
    );
    this.controllerMessenger.subscribe('KeyringController:lock', () =>
      this._onLock(),
    );
    this.on('update', (memState) => this._onStateUpdate(memState));
    this.on('update', handleUpdate);
    // ... 其他事件监听

    // 中间件与API
    this.metamaskMiddleware = createMetamaskMiddleware(...);
    this.provider = this.networkController.getProviderAndBlockTracker().provider;
  }
}

Messenger(消息总线)机制

  • 每个控制器实例化时,都会被分配一个"受限的 Messenger"对象(通过 this.controllerMessenger.getRestricted({ name: 'XxxController', ... }))。

  • 这个 Messenger 只允许控制器监听和调用特定的事件和方法,实现了控制器之间的安全通信和依赖注入。

  • 控制器之间不直接引用彼此,而是通过 Messenger 进行事件订阅、状态获取、方法调用等。

事件订阅与发布

  • 控制器通过 Messenger 订阅其他控制器的事件(如 stateChange、networkDidChange、transactionStatusUpdated 等)。
  • 这样可以在某个控制器状态变化时,自动通知相关控制器进行同步或联动。

其他

我们将在后面两个章节继续介绍主控制器中的:

  • 构建存储策略
  • 与UI端的同步机制

学习交流请添加vx: gh313061

本教程配套视频教程:space.bilibili.com/382494787/l...

下期预告:构建安全高可用的钱包数据持久化策略

相关推荐
数据与人工智能律师13 小时前
智能合约漏洞导致的损失,法律责任应如何分配
大数据·网络·人工智能·算法·区块链
Amore052519 小时前
Web3合约ABI,合约地址生成部署调用及创建,连接钱包,基础交易流程
web3·区块链·ethers
caijingshiye1 天前
BitMart 启动中文品牌“币市”:引领加密资产本地化发展新篇章
人工智能·区块链
Dream Algorithm2 天前
保证金率(Margin Ratio)
区块链
Ashlee_code2 天前
北极圈金融科技革命:奥斯陆证券交易所的绿色跃迁之路 ——从Visma千倍增长到碳信用衍生品,解码挪威资本市场的技术重构
科技·算法·金融·重构·架构·系统架构·区块链
软件工程小施同学2 天前
【最新区块链论文录用资讯】CCF A--WWW 2025 23篇
区块链
Ashlee_code2 天前
关税战火中的技术方舟:新西兰证券交易所的破局之道 ——从15%关税冲击到跨塔斯曼结算联盟,解码下一代交易基础设施
java·python·算法·金融·架构·系统架构·区块链
懒猫gg2 天前
区块链概述
区块链
小七mod2 天前
【BTC】挖矿
区块链·比特币·btc·挖矿·pow·矿池·轻节点