前言
通过前面几节的讲解,我们对钱包后端框架有了比较深入的理解,这对我们后面的业务实现打下了基础。 这一节我们介绍如何实现一个Provider。
什么是Provider
Provider是一个注入到网页上下文的对象,通过它可以建立网页DAPP和钱包之间的通信桥梁。它实现了EIP-1193标准接口,允许网页应用通过window.ethereum对象与区块链交互,包括发送交易、签名消息、读取区块链数据等。
所以要实现一个Provider主要做三件事:
- 实现EIP-1193标准
- 建立和钱包的通信
- 如何把provider注入到页面
EIP-1193标准实现
EIP-1193定义了以太坊提供商的JavaScript API,主要包含以下要素:
- 请求方法:request({method, params})
- 事件:connect, disconnect, chainChanged, accountsChanged
- 状态:chainId, selectedAddress
核心实现在BaseProvider类中:
// 简化的BaseProvider实现
class BaseProvider extends EventEmitter {
#chainId = null;
#selectedAddress = null;
#isConnected = false;
constructor() {
super();
this._rpcEngine = new JsonRpcEngine();
}
// 主要的EIP-1193接口方法
async request({method, params}) {
if (!method || typeof method !== 'string') {
throw new Error('Invalid request method');
}
return new Promise((resolve, reject) => {
this._rpcEngine.handle(
{ method, params, id: generateId(), jsonrpc: '2.0' },
(err, response) => {
if (err || response.error) {
reject(err || response.error);
} else {
resolve(response.result);
}
}
);
});
}
// 状态查询方法
isConnected() {
return this.#isConnected;
}
get chainId() {
return this.#chainId;
}
get selectedAddress() {
return this.#selectedAddress;
}
// 状态处理方法
_handleChainChanged({chainId}) {
if (this.#chainId !== chainId) {
this.#chainId = chainId;
this.emit('chainChanged', chainId);
}
}
_handleAccountsChanged(accounts) {
const address = accounts[0] || null;
if (this.#selectedAddress !== address) {
this.#selectedAddress = address;
this.emit('accountsChanged', accounts);
}
}
_handleConnect({chainId}) {
this.#isConnected = true;
this.emit('connect', { chainId });
}
_handleDisconnect(error) {
this.#isConnected = false;
this.emit('disconnect', error);
}
}
既然提到了EIP-1193,那么EIP-6963无论如何也要提一下。EIP-6963是为了解决多钱包扩展共存时的提供商发现和选择问题。EIP-6963 定义了两个关键事件:
- 请求事件 (
eip6963:requestProvider): DApp 触发此事件请求所有钱包提供商
- 宣告事件 (
eip6963:announceProvider): 钱包扩展响应请求并宣告自己的提供商
以下是 EIP-6963 在Metamask中的简化实现:
// 核心事件名称
const EIP6963Events = {
Announce: 'eip6963:announceProvider',
Request: 'eip6963:requestProvider'
};
// 提供商信息接口
type ProviderInfo = {
uuid: string; // 必须是v4 UUID
name: string; // 钱包名称
icon: string; // 数据URI格式的图标
rdns: string; // 反向域名标识符
};
// 提供商详情接口
type ProviderDetail = {
info: ProviderInfo;
provider: any; // 提供商实例
};
/**
* DApp 调用此函数来请求并获取所有可用的提供商
*/
function requestProvider(handleProvider) {
// 监听所有宣告事件
window.addEventListener(
EIP6963Events.Announce,
(event) => {
// 验证事件格式
if (isValidEvent(event)) {
// 将提供商详情传递给回调函数
handleProvider(event.detail);
}
}
);
// 触发请求事件,通知所有钱包
window.dispatchEvent(new Event(EIP6963Events.Request));
}
/**
* 钱包调用此函数来宣告自己的提供商
*/
function announceProvider(providerDetail) {
const { info, provider } = providerDetail;
// 宣告函数
const announce = () => {
window.dispatchEvent(
new CustomEvent(EIP6963Events.Announce, {
detail: Object.freeze({
info: {...info},
provider
})
})
);
};
// 立即宣告一次
announce();
// 监听请求事件,随时准备重新宣告
window.addEventListener(EIP6963Events.Request, (event) => {
if (isValidRequestEvent(event)) {
announce();
}
});
}
如果想要兼容多链(BTC,Solana),还需要实现CAIP-294,这里不展开讲。
与钱包的通信机制
Provider使用双向流(Duplex Stream)建立页面与扩展的通信通道,并通过JSON-RPC协议传递消息:
// 创建JSON-RPC流连接, 关于createStreamMiddleware我们在第五章已经讲解过
this._jsonRpcConnection = createStreamMiddleware();
// 将连接流(contentscript)与JSON-RPC流连接起来
pipeline(
connectionStream, // 源(contentscript)
this._jsonRpcConnection.stream, // 处理层
connectionStream, // 目标
this._handleStreamDisconnect.bind(this)
);
// 将JSON-RPC中间件添加到RPC引擎
this._rpcEngine.push(this._jsonRpcConnection.middleware);
// 处理通知事件
this._jsonRpcConnection.events.on('notification', (payload) => {
const { method, params } = payload;
if (method === 'metamask_accountsChanged') {
this._handleAccountsChanged(params);
}
else if (method === 'metamask_chainChanged') {
this._handleChainChanged(params);
}
// 其他通知处理...
});
这样我们既可以把DAPP的请求发送到contenscript,进而发送到后台处理,又可以处理后台发送到Provider的通知。
初始化和注入到页面
// 简化的初始化函数
function initializeProvider(options) {
const { connectionStream, shouldSetOnWindow = true } = options;
// 创建provider实例
const provider = new MetaMaskInpageProvider(connectionStream);
// 创建代理,防止第三方库修改 Provider API
const proxiedProvider = new Proxy(provider, {
deleteProperty: () => true,
get(target, prop) {
return target[prop];
}
});
// 以window.ethereum的形式把Provider注入到页面,设为全局对象
window.ethereum = proxiedProvider;
window.dispatchEvent(new Event('ethereum#initialized'));
return proxiedProvider;
}
Provider初始化流程
下图描述了Provider完整的初始化过程,建议对照源码学习:
flowchart TD
A[inpage.js调用initializeProvider] --> B["创建MetaMaskInpageProvider实例"]
subgraph "MetaMaskInpageProvider初始化"
B1["调用父类AbstractStreamProvider构造函数"]
B1 --> B2["调用BaseProvider构造函数"]
B2 --> B3["初始化基础属性(_log, _state等)"]
B3 --> B4["创建JsonRpcEngine实例"]
B4 --> B5["添加RPC中间件"]
B5 --> B6["返回到AbstractStreamProvider"]
B6 --> B7["创建JSON-RPC连接(createStreamMiddleware)"]
B7 --> B8["设置pipeline连接streams"]
B8 --> B9["将JSON-RPC连接中间件添加到RPC引擎"]
B9 --> B10["设置JSON-RPC通知监听器"]
B10 --> B11["返回到MetaMaskInpageProvider"]
B11 --> B12["调用_initializeStateAsync(异步)"]
B12 --> B13["设置初始状态(networkVersion等)"]
B13 --> B14["绑定方法(enable, send等)"]
B14 --> B15["创建_metamask实验性API"]
B15 --> B16["设置JSON-RPC通知监听器"]
end
subgraph "RPC中间件设置"
B5a["getDefaultExternalMiddleware"]
B5a --> B5b["createIdRemapMiddleware\n(重映射JSON-RPC请求ID)"]
B5a --> B5c["createErrorMiddleware\n(验证请求和记录错误)"]
B5a --> B5d["createRpcWarningMiddleware\n(记录已弃用RPC方法警告)"]
end
subgraph "pipeline设置"
B8a["连接connectionStream"]
B8a --> B8b["连接_jsonRpcConnection.stream"]
B8b --> B8c["连接connectionStream"]
B8c --> B8d["设置错误处理(_handleStreamDisconnect)"]
end
B --> B1
B5 --> B5a
B8 --> B8a
C["创建Proxy包装provider\n防止外部修改和访问私有变量"]
B --> C
D{是否提供providerInfo?}
C --> D
D -->|是| E["调用announceEip6963Provider\n注册EIP-6963事件"]
E --> F{是否启用CAIP-294?}
F -->|是| G["调用announceCaip294WalletData\n注册CAIP-294事件"]
D -->|否| H{是否设置全局provider?}
F -->|否| H
G --> H
H -->|是| I["调用setGlobalProvider\n设置window.ethereum"]
H -->|否| J{是否需要shimWeb3?}
I --> J
J -->|是| K["调用shimWeb3\n创建window.web3兼容层"]
J -->|否| L["返回proxiedProvider"]
K --> L
subgraph "站点元数据发送"
B16 --> B17{是否发送站点元数据?}
B17 -->|是| B18["调用sendSiteMetadata"]
B18 --> B19["获取站点名称和图标"]
B19 --> B20["发送metamask_sendDomainMetadata请求"]
end
subgraph "EIP-6963事件注册"
E1["创建announceProvider事件监听器"]
E1 --> E2["监听requestProvider事件"]
E2 --> E3["在事件触发时发布provider信息"]
end
subgraph "CAIP-294事件注册"
G1["请求provider状态获取extensionId"]
G1 --> G2["创建wallet数据(包含targets)"]
G2 --> G3["调用announceWallet发布事件"]
G3 --> G4["监听wallet_prompt事件"]
end
E --> E1
G --> G1
classDef primary fill:#f9f,stroke:#333,stroke-width:2px;
classDef secondary fill:#bbf,stroke:#333,stroke-width:2px;
classDef event fill:#bfb,stroke:#333,stroke-width:2px;
classDef middleware fill:#fdb,stroke:#333,stroke-width:2px;
class A,B,C,D primary;
class B1,B2,B3,B4,B6,B7,B8,B9,B10,B11,B12,B13,B14,B15,B16 secondary;
class E1,E2,E3,G1,G2,G3,G4 event;
class B5,B5a,B5b,B5c,B5d,B8a,B8b,B8c,B8d middleware;
DAPP请求处理过程
下图模拟了一条DAPP请求是如何在Provider中处理和转发的:
flowchart TD
subgraph "DApp"
A[DApp调用ethereum.request] --> C["eth_requestAccounts"]
end
subgraph "Inpage Provider"
C --> D["BaseProvider.request(method: 'eth_requestAccounts')"]
D --> E["_rpcRequest(payload, callback)"]
end
subgraph "RPC处理流程"
E --> F["_rpcEngine.handle(payload, callbackWrapper)"]
F --> G["JsonRpcEngine处理请求"]
G --> H["createStreamMiddleware中间件"]
end
subgraph "通信流程"
H --> I["_jsonRpcConnection.stream发送请求"]
I --> J["通过pipeline连接的stream"]
J --> K["WindowPostMessageStream传输"]
K --> L["ContentScript接收请求"]
L --> M["PortStream传输到后台"]
M --> N["MetaMask后台处理请求"]
end
subgraph "响应处理"
N --> O["后台返回响应"]
O --> P["ContentScript接收响应"]
P --> Q["WindowPostMessageStream返回"]
Q --> R["_jsonRpcConnection接收响应"]
R --> S["_handleAccountsChanged处理结果"]
S --> T["更新provider.selectedAddress"]
T --> U["触发accountsChanged事件"]
end
%% 特殊处理eth_requestAccounts
E -- "检测到eth_requestAccounts" --> V["创建特殊的callbackWrapper"]
V --> W["包装回调以处理账户变更"]
W --> F
%% 连接关系样式
classDef provider fill:#f9f,stroke:#333,stroke-width:2px;
classDef communication fill:#bbf,stroke:#333,stroke-width:2px;
classDef response fill:#bfb,stroke:#333,stroke-width:2px;
class B,C,D,E provider;
class I,J,K,L,M communication;
class O,P,Q,R,S,T,U response;
学习交流请添加vx: gh313061
下期预告:创建内容脚本