EIP-1193 - Ethereum 提供者 (Provider) JavaScript API

EIP-1193 - Ethereum 提供者 (Provider) JavaScript API

原文 EIP-1193: Ethereum Provider JavaScript API (opens in a new tab)

EIP-1193是由 Ethereum 基金会起草的一个关于 JavaScript Ethereum Provider API ,用于在客户端(Client)和应用程序(dapp)之间保持一致性。

抽象 (Abstract)

Ethereum 网络应用程序“(dapp)”生态系统中的一个常见约定是密钥管理软件 “钱包(Wallet)” 通过网页中的 JavaScript 对象公开其 API。该对象称为“提供者(Provider)”。

从历史上看,提供者(Provider) 实现在钱包之间表现出冲突的接口和行为。这个 EIP 正式规范化了一个 Ethereum 提供者(Provider) API,以促进钱包的规范操作性。

API 被设计为最小的、事件驱动的,并且与传输和 RPC 协议无关。它的功能很容易通过定义新的 RPC 方法和 message 事件类型来扩展。

从历史上看,提供程序 window.ethereum 已在 Web 浏览器中可用,但此约定不是此规范的一部分。

定义 (Definitions)

本节是非规范的。

  • 提供者 (Provider)
    • 一个可供消费者使用的 JavaScript 对象,它通过客户端提供对 Ethereum 的访问。
  • 客户端 (Client)
    • 从 提供者(Provider)接收远程过程调用 (RPC) 请求并返回其结果的端点。
  • 钱包 (Wallet)
    • 管理私钥(private keys)、执行签名(signing)操作并充当 提供者(Provider)和客户端(Client)之间的中间件的最终用户应用程序。
  • 远程过程调用 (RPC) (Remote Procedure Call (RPC))
    • 远程过程调用 (RPC) 是提交给 提供者(Provider)的任何请求,要求 提供者(Provider)、其电子钱包或其客户端处理某些过程。

连接性 (Connectivity)

Provider 可以为至少一个链提供 RPC 请求时,就被称为“已连接(connected)”。

Provider 根本无法为任何链提供 RPC 请求时,它被称为“断开连接(disconnected)”。

要为 RPC 请求提供服务, 提供者(Provider)必须成功地将请求提交到远程位置,并收到响应。

换句话说,如果 提供者(Provider) 无法与其 客户端(Client) 通信,例如由于网络问题,提供者(Provider) 将断开连接。

应用程序接口 (API)

Provider API 是使用 TypeScript 指定的。

作者鼓励实现者声明他们自己的类型和接口,使用本节中的类型和接口作为基础。

有关面向消费者的 API 文档,请参阅 附录-i 面向消费者的 api

提供者(Provider)必须实现并公开本节中定义的 API。所有 API 实体都必须遵守本节中定义的类型和接口。

请求 (request)

request方法旨在作为远程过程调用 (RPC) 的与传输和协议无关的包装函数。

interface RequestArguments {
  readonly method: string;
  readonly params?: readonly unknown[] | object;
}
 
Provider.request(args: RequestArguments): Promise<unknown>;

提供者(Provider)必须通过 RequestArguments.method 的值来识别请求的 RPC 方法。

如果请求的 RPC 方法采用任何参数, 提供者(Provider)必须接受它们作为 RequestArguments.params 的值。

必须处理 RPC 请求,以便返回的 Promise 要么根据请求的 RPC 方法的规范解析为一个值,要么因错误而拒绝。

如果已解决,则 Promise必须根据 RPC 方法的规范以结果解决。Promise 不得使用任何特定于 RPC 协议的响应对象进行解析,除非 RPC 方法的返回类型是这样定义的。

如果返回的 Promise 拒绝,它必须按照下面 RPC Errors (RPC 错误)部分中指定的 ProviderRpcError 拒绝。

如果满足以下任何条件,则返回的 Promise 必须拒绝:

  • RPC 请求返回错误。
    • 如果返回的错误与 ProviderRpcError 接口兼容, Promise 可以直接拒绝该错误。
  • 提供者(Provider) 遇到错误或由于任何原因无法处理请求。

如果 提供者(Provider) 实现了任何类型的授权逻辑,作者建议在授权失败的情况下拒绝并返回 4100 错误。

如果满足以下任何条件,则返回的 Promise 应该拒绝:

  • 提供者(Provider) 已断开连接。
    • 如果因为这个原因拒绝,Promise 拒绝错误码 必须4900
  • RPC 请求针对特定的链,提供者(Provider) 不连接到该链,但连接到至少一个其他链。
    • 如果因为这个原因拒绝, Promise 拒绝错误码 必须4901

有关“已连接(connected)”和“已断开连接(disconnected)”的定义,请参阅连接性 (connectivity)部分。

支持的RPC方法 (Supported RPC Methods)

“支持的RPC方法(Supported RPC Methods)”是可以通过 提供者(Provider) 调用的任何 RPC 方法。

所有受支持的 RPC 方法必须由唯一的字符串标识。

提供者(Provider) 可以支持实现其目的所需的任何 RPC 方法,无论是标准化的还是其他方式。

如果不支持在最终 EIP 中定义的 RPC 方法,它应该根据下面的提供者错误(Provider Errors)部分的错误返回状态码4200拒绝,或者根据 RPC 方法的规范适当的错误返回。

RPC 错误 (RPC Errors)

interface ProviderRpcError extends Error {
  code: number;
  data?: unknown;
  message?: unknown;
}
  • code
    • 必须是整数
    • 应该遵守下面错误标准部分的规范
  • data
    • 应该包含关于错误的任何其他有用信息
  • message
    • 必须是人类可读的字符串
    • 应该遵守下面错误标准部分的规范

错误标准 (Error Standards)

ProviderRpcError 代码和消息返回值应该按照优先顺序遵循这些约定:

  1. 下面 提供者错误 (Provider Errors) 部分的错误。
  2. 错误的 RPC 方法规范规定的任何错误。
  3. 状态 CloseEvent 码。

提供者错误 (Provider Errors)

Status codeNameDescription
4001User Rejected Request用户拒绝了请求。
4100Unauthorized请求的方法和/或帐户未被用户授权。
4200Unsupported Method提供者(Provider)不支持请求的方法。
4900Disconnected提供者(Provider)与所有链断开连接。
4901Chain Disconnected提供者(Provider)未连接到请求的链。

状态码 4900 旨在指示 提供者(Provider) 与所有链断开连接,

而状态码 4901 旨在指示 提供者(Provider) 仅与特定链断开连接。换句话说,4901 意味着 提供者(Provider) 连接到其他链,而不是请求的链。

事件 (Events)

提供者(Provider) 必须实现以下事件处理方法:

  • on
  • removeListener

这些方法必须根据 Node.js EventEmitterAPI (opens in a new tab)实现。

为了满足这些要求, 提供者(Provider) 的实现者应该考虑简单地扩展 Node.js EventEmitter 类并将其捆绑到目标环境中。

信息 (message)

message 事件旨在用于其他事件未涵盖的任意通知。

发射时,必须使用以下形式的对象参数发射 message 事件:

interface ProviderMessage {
  readonly type: string;
  readonly data: unknown;
}

订阅 (Subscriptions)

如果 提供者(Provider) 支持 Ethereum RPC 订阅,例如 eth_subscribe, 提供者(Provider) 必须在收到订阅通知时发出 消息(message) 事件。

如果 提供者(Provider) 从 eth_subscribe 订阅中接受到订阅信息, 提供者(Provider) 必须使用以下形式的 ProviderMessage 对象发出消息事件:

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

连接 (connectivity)

有关 connected(已连接) 的定义,请参阅连接性 (connectivity)部分。

如果 提供者(Provider) 已连接,则 提供者(Provider) 必须发出名为 connect 的事件。

这包括以下情况:

  • 提供者(Provider) 在初始化后首先连接到一条链上。
  • 断开连接(disconnect) 事件(Event) 发出后, 提供者(Provider) 连接到链。

此事件必须与以下形式的对象一起发出:

interface ProviderConnectInfo {
  readonly chainId: string;
}

根据 eth_chainId Ethereum RPC 方法, chainId必须将连接链的整数 ID 指定为十六进制字符串。

断开连接 (disconnect)

有关 断开连接(disconnect) 的定义,请参阅连接性(connectivity)部分。

如果 提供者(Provider) 与所有链 断开连接(disconnect),提供者(Provider)必须根据RPC错误(RPC Errors)部分中定义的接口发出名为 断开链接(disconnect) 的事件,其值为 error: ProviderRpcError。 错误代码属性的值必须跟在 CloseEvent状态代码 (opens in a new tab)之后。

切换链 (chainChanged)

如果 提供者(Provider) 连接的链发生变化, 提供者(Provider)必须发出名为 chainChanged 的事件,其值为 chainId: string,根据 eth_chainId Ethereum RPC 方法,将新链的整数 ID 指定为十六进制字符串。

切换钱包帐号 (accountsChanged)

如果 提供者(Provider) 活跃的钱包账户发生变化, 提供者(Provider)必须发出名为 accountsChanged 的事件,其值为 accounts: string[],其中包含每个 eth_accounts Ethereum RPC 方法的账户地址。

eth_accounts 的返回值改变时,“提供者可用账户(Provider available accounts)”也会改变。

基本原理 (Rationale)

提供者(Provider) 的目的是为消费者提供对 Ethereum 的直接访问。通常, 提供者(Provider)必须使 Ethereum Web 应用程序能够做两件事:

  • 发出Ethereum RPC 请求
  • 响应 提供者(Provider) 的 Ethereum 链、客户端和钱包中的状态变化

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

其余四个事件可以分为两类:

  • 更改 提供者(Provider) 发出 RPC 请求的能力
    • connect
    • disconnect
  • 任何重要应用程序必须处理的常见客户端或钱包状态更改
    • chainChanged
    • accountsChanged

由于在撰写本文时相关模式在生产中的广泛使用,因此包括了这些事件。

向后兼容性 (Backwards Compatibility)

许多 提供者(Provider) 在最终确定之前采用了该规范的草案版本。 当前的 API 被设计为遗留版本的严格超集,并且此规范在这个意义上是完全向后兼容的。 有关遗留 API,请参阅 附录 III 旧版提供程序 api

仅实现此规范的 提供者(Provider) 将不兼容以遗留 API 为目标的 Ethereum Web 应用程序。

实现 (Implementations)

在撰写本文时,以下项目具有有效的实现:

安全注意事项 (Security Considerations)

提供者(Provider) 旨在在 Ethereum 客户端和 Ethereum 应用程序之间传递消息。不负责私钥或账户管理;

它仅处理 RPC 消息并发出事件。因此,账户安全和用户隐私需要在 提供者(Provider) 和它的 Ethereum 客户端之间的中间件中实现。

在实践中,我们将这些中间件应用程序称为 “钱包(Wallet)”,它们通常管理用户的 私钥(private keys) 和 帐户("Accounts")。

提供者(Provider) 可以被认为是 钱包(Wallet) 的扩展,在某些第三方(例如网站)的控制下暴露在不受信任的环境中。

处理对抗行为 (Handling Adversarial Behavior)

由于是JavaScript对象,消费者一般可以对 提供者(Provider) 进行任意操作,它的所有属性都可以被读取或覆盖。

因此,最好将 提供者(Provider) 对象视为由对手控制。 提供者(Provider) 的实现者通过确保以下内容来保护用户、钱包(Wallet)和客户端(Client),这一点至关重要:

  • 提供者(Provider) 不包含任何私人用户数据。
  • 提供者(Provider) 和 钱包(Wallet) 程序是相互隔离的。
  • 对来自供应商的钱包或客户端速率限制请求。
  • 钱包或客户端验证从 提供者(Provider) 发送的所有数据。

链变化 (Chain Changes)

eth_chainId 由于所有 Ethereum 操作都针对特定链,因此根据 Ethereum RPC 方法(请参阅EIP-695), 提供者(Provider) 准确反映客户端配置的链非常重要。

这包括确保 eth_chainId 具有正确的返回值,并且 chainChanged 只要该值发生变化就会发出事件。

用户帐户公开和帐户更改 (User Account Exposure and Account Changes)

许多 Ethereum 写入操作(例如 eth_sendTransaction )需要指定用户帐户。

提供者(Provider) 通过消费者 RPC 方法访问这些帐户 eth_accounts,并监听 accountsChanged 事件。

与之一样,具有正确的返回值 eth_chainId 至关重要,并且只要该值发生变化就会触发事件。

eth_accounts accountsChanged 的返回值 eth_accounts 最终由钱包或客户端控制。

为了保护用户隐私,作者建议默认不暴露任何账户。

相反, 提供者(Provider) 应该支持 RPC 方法来明确请求帐户访问,例如 eth_requestAccounts (参考EIP-1102)或 wallet_requestPermissions (参考EIP-2255)。

参考 (References)

原文版权 (Original Copyright) (opens in a new tab)

通过CC0 (opens in a new tab)放弃版权和相关权利。


附录 I:面向消费者的 API 文档 (Appendix I: Consumer-Facing API Documentation)

request

进行Ethereum RPC 方法调用。

interface RequestArguments {
  readonly method: string;
  readonly params?: readonly unknown[] | object;
}
 
Provider.request(args: RequestArguments): Promise<unknown>;

返回的 Promise 使用方法的结果解析或拒绝使用 ProviderRpcError. 例如:

Provider.request({ method: 'eth_accounts' })
  .then((accounts) => console.log(accounts))
  .catch((error) => console.error(error));

请查阅每个 Ethereum RPC 方法的文档以了解其 params 返回类型。您可以在此处找到常用方法的列表。

RPC Protocols

可能有多个 RPC 协议可用。有关示例,请参阅:

事件 (Events)

事件遵循 Node.js EventEmitterAPI (opens in a new tab)的约定。

连接 (connect)

提供者(Provider) 在以下情况下发出 connect:

  • 初始化后首先连接到链。
  • disconnect 在事件发出后,首先连接到链。
interface ProviderConnectInfo {
  readonly chainId: string;
}
 
Provider.on('connect', listener: (connectInfo: ProviderConnectInfo) => void): Provider;

该事件根据 eth_chainId Ethereum RPC 方法发出一个带有十六进制字符串 chainId 的对象,以及 提供者(Provider) 确定的其他属性。

断开 (disconnect)

在提供者(Provider) 与所有链断开连接 disconnect 时发出。

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

此事件发出一个 ProviderRpcError. 错误 code 遵循 CloseEvent 状态代码表 (opens in a new tab)

切换链 (chainChanged)

在 提供者(Provider) chainChanged 在连接到新链时发出。

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

该事件根据 eth_chainId Ethereum RPC 方法发出一个十六进制字符串 chainId

切换钱包帐号 (accountsChanged)

在提供者(Provider) 从(eth_accounts) 切换钱包帐号 accountsChanged 返回的帐户更改时发出。

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

根据 eth_accounts Ethereum RPC 方法,该事件与帐户一起发出,帐户地址数组。

信息 (message)

在 提供者(Provider) 发出 消息(message) 以向消费者传达任意消息。消息(message) 可能包括 JSON-RPC 通知、GraphQL订阅、提供者(Provider) 定义的任何其他事件。

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

订阅 (Subscriptions)

eth_subscription (opens in a new tab)shh_subscription (opens in a new tab)依赖于此事件来发出订阅更新。

例如 eth_subscribe 订阅更新,ProviderMessage.type 将等于字符串 eth_subscription,订阅数据将是 ProviderMessage.data 的值。

错误 (Error)

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

附录 II:示例

// 大多数 提供者(`Provider`)在页面加载时都可以作为 window.ethereum 使用。
// 这只是约定俗成,并非标准,实际中未必如此。
// 请查阅 Provider 实现的文档。
const ethereum = window.ethereum;
 
// 示例 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}`);
  });
 
// 示例 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}`
    );
  });
 
// 示例 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}`
    );
  });
 
// 示例 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}`
    );
  });
 
// 示例 5:在帐户更改时记录
const logAccounts = (accounts) => {
  console.log(`Accounts:\n${accounts.join('\n')}`);
};
ethereum.on('accountsChanged', logAccounts);
// to unsubscribe
ethereum.removeListener('accountsChanged', logAccounts);
 
// 示例 6:记录连接是否结束
ethereum.on('disconnect', (code, reason) => {
  console.log(`Ethereum Provider connection closed: ${reason}. Code: ${code}`);
});

附录 III:旧版提供程序 API

本节记录遗留的 Provider API,在撰写本文时它已广泛用于生产环境。由于它从未完全标准化,因此在实践中会出现重大偏差。作者建议不要实施它,除非是为了支持遗留的Ethereum应用程序。

发送异步(已弃用)

此方法已被request取代。

sendAsync类似于request,但带有 JSON-RPC 对象和回调。

Provider.sendAsync(request: Object, callback: Function): void;

历史上,请求和响应对象接口一直遵循 Ethereum JSON-RPC 规范

发送(弃用)

此方法已被request取代。

Provider.send(...args: unknown[]): unknown;

遗产事件

关闭(弃用)

此事件已被disconnect取代。

网络已更改(已弃用)

该事件networkChangedchainChanged取代。

详情参考EIP-155:简单重放攻击保护EIP-695:为 JSON-RPC 创建 eth_chainId 方法

通知(弃用)

此事件由message取代。

从历史上看,此事件已通过例如eth_subscribe表单的订阅更新发出{ subscription: string, result: unknown }

原文引用 (Original Citation)

Please cite this document as:

Fabian Vogelsteller (opens in a new tab), Ryan Ghods (opens in a new tab), Victor Maia (opens in a new tab), Marc Garreau (opens in a new tab), Erik Marks (opens in a new tab), "EIP-1193: Ethereum Provider JavaScript API," Ethereum Improvement Proposals, no. 1193, June 2018. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-1193 (opens in a new tab).

Last updated on December 16, 2022