前言
metamask-controller.js可以说是钱包中最重要的同时也是最复杂的脚本,它就像大脑里的中枢神经,负责管理整个钱包的核心业务逻辑和状态。这个文件实现了一个庞大的 MetamaskController 类,整合了账户管理、网络切换、交易处理、权限控制、硬件钱包支持、通知、钓鱼检测等所有核心功能,并对外暴露统一的 API 和事件流。所有前端页面、内容脚本、DApp 以及后台服务的交互,最终都要经过这个控制器。
建立通信通道
我们在上一章埋下了一个坑,在介绍建立可信通道与不可信通道时没有深入讲解,因为它的具体实现在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_accounts
、eth_sendTransaction
等区块链请求。
js
this.setupProviderConnectionEip1193(mux.createStream('provider'), sender, SubjectType.Internal);
- 这里会构建一个 provider engine(中间件链),并通过流与 UI 连接。
管道连接(pipeline)
- 在
setupProviderConnectionEip1193
内部,使用pipeline
将outStream
、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...
下期预告:构建安全高可用的钱包数据持久化策略