跳到主要内容

EIP-1193: Ethereum Provider JavaScript API

原文

在浏览器钱包插件中存储着用户私钥, 当在 dapp 网页发起交易时, 需要告诉钱包帮助我们签名交易和发送交易。因此就需要在 dapp 网页暴露一个对象, 用于 dapp 和 钱包之间的交互。这个对象就是 Provider。钱包插插件通过向 window 对象添加属性实现Provider 注入。

由于以太坊生态的完善,出现了各种各样的钱包。这些钱包提供的 Provider 对象也大不相同。为了规范 Provider 对象,因此提出了 EIP-1193, 该标准可以保证各个钱包提供相同的接口。

关联协议

连接

Provider 能够为至少一个链提供 RPC 请求时,被称为已连接

Provider 无法为任何链提供 RPC 请求时,被称为未连接

API Request

Provider 提供的 RPC 请求参数遵循 RequestArguments 格式, 通过 request 方法发送

interface RequestArguments {
readonly method: string;
readonly params?: readonly unknown[] | object;
}

Provider.request(args: RequestArguments): Promise<unknown>;

支持的 RPC 方法见 EIP-1474

账户相关 EIP-1102

RPC 错误

错误结构

interface ProviderRpcError extends Error {
code: number
data?: unknown
}

code 有以下取值

  • 4001: User Rejected Request(用户拒绝)
  • 4100: Unauthorized(请求方法未授权)
  • 4200: Unsupported Method(不支持的方法)
  • 4900: Disconnected(Provider 已断开与所有链的连接)
  • 4901: Chain Disconnected(Provider 未连接到目标链)

Events

Provider 需要实现下面的事件处理方法

  • on 事件监听处理
  • removeListener 移除事件监听

这些方法必须按照 Node.js EventEmitter API 进行实现。

on 方法可监听的事件有

  • message
  • Subscriptions
  • connect
  • disconnect
  • chainChanged
  • accountsChanged

message

message 为通用型事件,触发时接收参数类型为

interface ProviderMessage {
readonly type: string
readonly data: unknown
}
Provider.on('message', listener: (message: ProviderMessage) => void): Provider;

Subscriptions

如果 Provider 支持以太坊 RPC 订阅,例如 eth_subscribe,当接收到订阅通知时,须发出消息事件。

如果 Provider 从例如 eth_subscribe 订阅中接收到一个订阅消息,则 Provider 必须发出具有以下形式的 ProviderMessage 对象的消息事件:

interface EthSubscription extends ProviderMessage {
readonly type: 'eth_subscription'
readonly data: {
readonly subscription: string
readonly result: unknown
}
}

connect

如果 Provider 变为已连接状态,则需要发出 connect的事件。

包括以下情况:

  • 在初始化后,Provider 首次连接到链。
  • 在断开事件被发出后,Provider 重新连接到链。

此事件必须使用以下形式的对象进行发出:

interface ProviderConnectInfo {
readonly chainId: string
}

Provider.on('connect', listener: (connectInfo: ProviderConnectInfo) => void): Provider;

disconnect

如果 Provider 与所有链断开连接,Provider 须按照 RPC 错误部分中定义的接口发出名为 disconnect 的事件,并附带值 error: ProviderRpcError

Provider.on('disconnect', listener: (error: ProviderRpcError) => void): Provider;

chainChanged

如果连接到的链发生变化,Provider 须触发 chainChanged的事件,并使用值 chainId: string 来指定新链的整数 ID 作为十六进制字符串,符合 eth_chainId 以太坊 RPC 方法

Provider.on('chainChanged', listener: (chainId: string) => void): Provider;

accountsChanged

如果 Provider 可用的账户发生变化,须触发 accountsChanged 的事件,并附带值 accounts: string[],其中包含 eth_accounts 以太坊 RPC 方法返回的账户地址。

Provider.on('accountsChanged', listener: (accounts: string[]) => void): Provider;

总的来说 Provider 主要提供两个能力

  • 发出以太坊 RPC 请求
  • 响应链、客户端和钱包的状态变化

Provider API 规范由一个方法和五个事件组成。仅方法 request 和 message 事件就足以实现一个完整的 Provider。它们旨在分别发出任意 RPC 请求和传送任意消息。

示例

假设 Provider 对象注入在 window.ethereum

// 获取 Provider 对象
const ethereum = window.ethereum

// Example 1: 获取 chainId
ethereum
.request({ method: 'eth_chainId' })
.then((chainId) => {
console.log(`hexadecimal string: ${chainId}`)
console.log(`decimal number: ${parseInt(chainId, 16)}`)
})
.catch((error) => {
console.error(`Error fetching chainId: ${error.code}: ${error.message}`)
})

// Example 2: 获取最新的区块号
ethereum
.request({
method: 'eth_getBlockByNumber',
params: ['latest', true]
})
.then((block) => {
console.log(`Block ${block.number}:`, block)
})
.catch((error) => {
console.error(
`Error fetching last block: ${error.message}.
Code: ${error.code}. Data: ${error.data}`
)
})

// Example 3: 获取账户列表
ethereum
.request({ method: 'eth_accounts' })
.then((accounts) => {
console.log(`Accounts:\n${accounts.join('\n')}`)
})
.catch((error) => {
console.error(
`Error fetching accounts: ${error.message}.
Code: ${error.code}. Data: ${error.data}`
)
})

// Example 4: 监听新区块产出
ethereum
.request({
method: 'eth_subscribe',
params: ['newHeads']
})
.then((subscriptionId) => {
ethereum.on('message', (message) => {
if (message.type === 'eth_subscription') {
const { data } = message
if (data.subscription === subscriptionId) {
if ('result' in data && typeof data.result === 'object') {
const block = data.result
console.log(`New block ${block.number}:`, block)
} else {
console.error(`Something went wrong: ${data.result}`)
}
}
}
})
})
.catch((error) => {
console.error(
`Error making newHeads subscription: ${error.message}.
Code: ${error.code}. Data: ${error.data}`
)
})

// Example 5: 监听切换账户
const logAccounts = (accounts) => {
console.log(`Accounts:\n${accounts.join('\n')}`)
}
ethereum.on('accountsChanged', logAccounts)
// 移除监听事件
ethereum.removeListener('accountsChanged', logAccounts)

// Example 6: 监听链接断开
ethereum.on('disconnect', (code, reason) => {
console.log(`Ethereum Provider connection closed: ${reason}. Code: ${code}`)
})