Alert Source Discuss

EIP-:

Authors


eip: 1193 title: Ethereum Provider JavaScript API author: Fabian Vogelsteller (@frozeman), Ryan Ghods (@ryanio), Victor Maia (@MaiaVictor), Marc Garreau (@wolovim), Erik Marks (@rekmarks) discussions-to: https://github.com/ethereum/EIPs/issues/2319 status: Final type: Standards Track category: Interface created: 2018-06-30 requires: 155, 695

摘要

用于在客户端和应用程序之间保持一致性的 JavaScript Ethereum Provider API。

概要

以太坊 Web 应用程序(“dapp”)生态系统中的一个常见约定是,密钥管理软件(“钱包”)通过 Web 页面中的 JavaScript 对象公开其 API。 此对象称为“Provider”。

从历史上看,Provider 实现已经展示了钱包之间冲突的接口和行为。 此 EIP 形式化了一个 Ethereum Provider API,以促进钱包的互操作性。 该 API 被设计为最小化、事件驱动以及与传输和 RPC 协议无关。 通过定义新的 RPC 方法和 message 事件类型,可以轻松扩展其功能。

从历史上看,Provider 在 Web 浏览器中以 window.ethereum 的形式提供,但此约定不是规范的一部分。

规范

本文档中的关键词“MUST”、“MUST NOT”、“REQUIRED”、“SHALL”、“SHALL NOT”、“SHOULD”、“SHOULD NOT”、“RECOMMENDED”、“MAY”和“OPTIONAL”应按照 RFC-2119 中所述进行解释。

像这样的注释是非规范性的。

定义

本节是非规范性的。

  • Provider
    • 提供给消费者的 JavaScript 对象,它通过客户端提供对以太坊的访问。
  • 客户端
    • 接收来自 Provider 的远程过程调用 (RPC) 请求并返回其结果的端点。
  • 钱包
    • 管理私钥、执行签名操作并充当 Provider 和客户端之间中间件的最终用户应用程序。
  • 远程过程调用 (RPC)
    • 远程过程调用(RPC),是提交给 Provider 的任何过程的请求,该过程将由 Provider、其钱包或其客户端处理。

连接性

当 Provider 可以为至少一个链提供 RPC 请求服务时,称该 Provider 为“已连接”。

当 Provider 根本无法为任何链提供 RPC 请求服务时,称该 Provider 为“已断开连接”。

要为 RPC 请求提供服务,Provider 必须成功将请求提交到远程位置并收到响应。 换句话说,如果 Provider 无法与其客户端通信,例如由于网络问题,则 Provider 已断开连接。

API

Provider API 是使用 TypeScript 指定的。 作者鼓励实施者声明自己的类型和接口,并使用本节中的类型和接口作为基础。

有关面向消费者的 API 文档,请参见 附录 I

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 方法规范的结果来解析。除非 RPC 方法的返回类型如此定义,否则 Promise 不得 使用任何 RPC 协议特定的响应对象来解析。

如果返回的 Promise 拒绝,它 必须 使用 ProviderRpcError 拒绝,如以下 RPC 错误 部分中所指定。

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

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

如果 Provider 实现了任何类型的授权逻辑,作者建议在授权失败的情况下使用 4100 错误拒绝。

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

  • Provider 已断开连接。
    • 如果因此原因而拒绝,则 Promise 拒绝错误 code 必须4900
  • RPC 请求指向特定链,并且 Provider 未连接到该链,但已连接到至少一个其他链。
    • 如果因此原因而拒绝,则 Promise 拒绝错误 code 必须4901

有关“已连接”和“已断开连接”的定义,请参见 连接性 部分。

支持的 RPC 方法

“支持的 RPC 方法”是可以通过 Provider 调用的任何 RPC 方法。

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

Provider 可以 支持满足其目的所需的任何 RPC 方法,无论是否标准化。

如果未支持已完成的 EIP 中定义的 RPC 方法,则 应该 使用 Provider 错误 部分中的 4200 错误或每个 RPC 方法规范中的相应错误来拒绝该方法。

RPC 错误

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

ProviderRpcError 代码和消息 应该 遵循以下约定,按优先级排序:

  1. 以下 Provider 错误 部分中的错误

  2. 任何错误 RPC 方法的规范所要求的任何错误

  3. CloseEvent 状态代码

Provider 错误

状态代码 名称 描述
4001 用户拒绝请求 用户拒绝了请求。
4100 未授权 请求的方法和/或帐户未经用户授权。
4200 不支持的方法 Provider 不支持请求的方法。
4900 已断开连接 Provider 与所有链断开连接。
4901 链已断开连接 Provider 未连接到请求的链。

4900 旨在指示 Provider 与所有链断开连接,而 4901 旨在指示 Provider 仅与特定链断开连接。 换句话说,4901 意味着 Provider 已连接到其他链,只是未连接到请求的链。

事件

Provider 必须 实现以下事件处理方法:

  • on
  • removeListener

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

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

message

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

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

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

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

如果 Provider 从例如 eth_subscribe 订阅收到订阅消息,则 Provider 必须 发出 message 事件,其中包含以下形式的 ProviderMessage 对象:

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

connect

有关“已连接”的定义,请参见 连接性 部分。

如果 Provider 变为已连接,则 Provider 必须 发出名为 connect 的事件。

这包括以下情况:

  • Provider 在初始化后首次连接到链。
  • 在发出 disconnect 事件后,Provider 连接到链。

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

interface ProviderConnectInfo {
  readonly chainId: string;
}

chainId 必须 按照 eth_chainId 以太坊 RPC 方法指定连接的链的整数 ID,以十六进制字符串表示。

disconnect

有关“已断开连接”的定义,请参见 连接性 部分。

如果 Provider 与所有链断开连接,则 Provider 必须 发出名为 disconnect 的事件,值为 error: ProviderRpcError,按照 RPC 错误 部分中定义的接口。 错误 code 属性的值 必须 遵循 CloseEvent 的状态代码

chainChanged

如果 Provider 连接到的链发生更改,则 Provider 必须 发出名为 chainChanged 的事件,值为 chainId: string,按照 eth_chainId 以太坊 RPC 方法指定新链的整数 ID,以十六进制字符串表示。

accountsChanged

如果 Provider 可用的帐户发生更改,则 Provider 必须 发出名为 accountsChanged 的事件,值为 accounts: string[],其中包含每个 eth_accounts 以太坊 RPC 方法的帐户地址。

eth_accounts 的返回值发生更改时,“Provider 可用的帐户”会发生更改。

理由

Provider 的目的是_向_消费者_提供_对以太坊的访问权限。 通常,Provider 必须使以太坊 Web 应用程序能够执行两件事:

  • 发出以太坊 RPC 请求
  • 响应 Provider 的以太坊链、客户端和钱包中的状态更改

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

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

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

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

向后兼容性

许多 Provider 在此规范最终确定之前采用了该规范的草案版本。 当前的 API 旨在成为旧版本的严格超集,并且从这个意义上讲,该规范是完全向后兼容的。 有关旧 API,请参见 附录 III

仅实现此规范的 Provider 将与以旧 API 为目标的以太坊 Web 应用程序不兼容。

实现

在编写本文时,以下项目具有可行的实现:

安全注意事项

Provider 旨在在以太坊客户端和以太坊应用程序之间传递消息。 它_不_负责私钥或帐户管理;它仅处理 RPC 消息并发出事件。 因此,帐户安全和用户隐私需要在 Provider 与其以太坊客户端之间的中间件中实现。 实际上,我们将这些中间件应用程序称为“钱包”,它们通常管理用户的私钥和帐户。 可以将 Provider 视为钱包的扩展,该扩展在不受信任的环境中公开,并由某些第三方(例如网站)控制。

处理对抗行为

由于它是一个 JavaScript 对象,因此消费者通常可以对 Provider 执行任意操作,并且可以读取或覆盖其所有属性。 因此,最好将 Provider 对象视为由对手控制。 至关重要的是,Provider 实施者通过确保以下各项来保护用户、钱包和客户端:

  • Provider 不包含任何私有用户数据。
  • Provider 和钱包程序彼此隔离。
  • 钱包和/或客户端限制来自 Provider 的请求速率。
  • 钱包和/或客户端验证从 Provider 发送的所有数据。

链更改

由于所有以太坊操作都指向特定链,因此重要的是,Provider 按照 eth_chainId 以太坊 RPC 方法准确地反映客户端配置的链(请参见 EIP-695)。

这包括确保 eth_chainId 具有正确的返回值,并且每当该值更改时都发出 chainChanged 事件。

用户帐户公开和帐户更改

许多以太坊写入操作(例如 eth_sendTransaction)都需要指定用户帐户。 Provider 消费者通过 eth_accounts RPC 方法并侦听 accountsChanged 事件来访问这些帐户。

eth_chainId 一样,至关重要的是,eth_accounts 具有正确的返回值,并且每当该值更改时都发出 accountsChanged 事件。

eth_accounts 的返回值最终由钱包或客户端控制。 为了保护用户隐私,作者建议默认情况下不公开任何帐户。 相反,Provider 应该支持用于显式请求帐户访问的 RPC 方法,例如 eth_requestAccounts(请参见 EIP-1102)或 wallet_requestPermissions(请参见 EIP-2255)。

参考

版权

通过 CC0 放弃版权和相关权利。

附录 I:面向消费者的 API 文档

request

进行以太坊 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));

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

RPC 协议

可能有多个 RPC 协议可用。例如,请参见:

事件

事件遵循 Node.js EventEmitter API 的约定。

connect

Provider 在以下情况下发出 connect

  • 在初始化后首次连接到链。
  • 在发出 disconnect 事件后首次连接到链。
interface ProviderConnectInfo {
  readonly chainId: string;
}

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

该事件会发出一个对象,其中包含一个十六进制字符串 chainId,该字符串符合 eth_chainId 以太坊 RPC 方法的规定,以及 Provider 确定的其他属性。

disconnect

当 Provider 与所有链断开连接时,它会发出 disconnect

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

此事件会发出一个 ProviderRpcError。错误 code 遵循 CloseEvent 状态代码 表。

chainChanged

当 Provider 连接到新链时,它会发出 chainChanged

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

该事件会发出一个十六进制字符串 chainId,该字符串符合 eth_chainId 以太坊 RPC 方法的规定。

accountsChanged

如果从 Provider (eth_accounts) 返回的帐户发生更改,则 Provider 会发出 accountsChanged

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

该事件会发出 accounts,这是一个帐户地址数组,符合 eth_accounts 以太坊 RPC 方法的规定。

message

Provider 发出 message 以向消费者传递任意消息。 消息可能包括 JSON-RPC 通知、GraphQL 订阅和/或 Provider 定义的任何其他事件。

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

Provider.on('message', listener: (message: ProviderMessage) => void): Provider;
订阅

eth_ 订阅方法shh_ 订阅方法 依赖此事件来发出订阅更新。

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

错误

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

附录 II:示例

这些示例假定为 Web 浏览器环境。

// 大多数 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:旧 Provider API

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

sendAsync(已弃用)

此方法已被 request 取代。

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

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

从历史上看,请求和响应对象接口都遵循 以太坊 JSON-RPC 规范

send(已弃用)

此方法已被 request 取代。

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

旧事件

close(已弃用)

此事件已被 disconnect 取代。

networkChanged(已弃用)

事件 networkChanged 已被 chainChanged 取代。

有关详细信息,请参见 EIP-155:简单的重放攻击保护EIP-695:为 JSON-RPC 创建 eth_chainId 方法

notification(已弃用)

此事件已被 message 取代。

从历史上看,此事件已发出,例如,格式为 { subscription: string, result: unknown }eth_subscribe 订阅更新。

Citation

Please cite this document as:

, "EIP-: ," Ethereum Improvement Proposals, no. , . [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-.