区块链钱包开发(七)—— 创建注入到网页的Provider

66 阅读4分钟

前言

通过前面几节的讲解,我们对钱包后端框架有了比较深入的理解,这对我们后面的业务实现打下了基础。 这一节我们介绍如何实现一个Provider。

源码:github.com/MetaMask/pr…

什么是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

下期预告:创建内容脚本