EIP-1193 - Ethereum 提供者 (Provider) JavaScript API
原文 EIP-1193: Ethereum Provider JavaScript API
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
)、其电子钱包或其客户端处理某些过程。
- 远程过程调用 (RPC) 是提交给 提供者(
连接性 (Connectivity
)
当 Provider
可以为至少一个链提供 RPC
请求时,就被称为“已连接(connected
)”。
当 Provider
根本无法为任何链提供 RPC
请求时,它被称为“断开连接(disconnected
)”。
要为
RPC
请求提供服务, 提供者(Provider
)必须成功地将请求提交到远程位置,并收到响应。换句话说,如果 提供者(
Provider
) 无法与其 客户端(Client
) 通信,例如由于网络问题,提供者(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
方法的规范以结果解决。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
代码和消息返回值应该按照优先顺序遵循这些约定:
- 下面 提供者错误 (
Provider Errors
) 部分的错误。 - 错误的
RPC
方法规范规定的任何错误。 - 状态
CloseEvent
码。
提供者错误 (Provider Errors
)
Status code | Name | Description |
---|---|---|
4001 | User Rejected Request | 用户拒绝了请求。 |
4100 | Unauthorized | 请求的方法和/或帐户未被用户授权。 |
4200 | Unsupported Method | 提供者(Provider )不支持请求的方法。 |
4900 | Disconnected | 提供者(Provider )与所有链断开连接。 |
4901 | Chain Disconnected | 提供者(Provider )未连接到请求的链。 |
状态码
4900
旨在指示 提供者(Provider
) 与所有链断开连接,而状态码
4901
旨在指示 提供者(Provider
) 仅与特定链断开连接。换句话说,4901
意味着 提供者(Provider
) 连接到其他链,而不是请求的链。
事件 (Events
)
提供者(Provider
) 必须实现以下事件处理方法:
on
removeListener
这些方法必须根据 Node.js EventEmitterAPI
实现。
为了满足这些要求, 提供者(
Provider
) 的实现者应该考虑简单地扩展 Node.jsEventEmitter
类并将其捆绑到目标环境中。
信息 (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
的状态代码之后。
切换链 (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
规范由一个方法和五个事件组成。 仅 request
和 message
事件就足以实现一个完整的 Provider
。 它们旨在分别发出任意 RPC
请求和传递任意消息。
其余四个事件可以分为两类:
- 更改 提供者(
Provider
) 发出RPC
请求的能力connect
disconnect
- 任何重要应用程序必须处理的常见客户端或钱包状态更改
chainChanged
accountsChanged
由于在撰写本文时相关模式在生产中的广泛使用,因此包括了这些事件。
向后兼容性 (Backwards Compatibility
)
许多 提供者(Provider
) 在最终确定之前采用了该规范的草案版本。
当前的 API
被设计为遗留版本的严格超集,并且此规范在这个意义上是完全向后兼容的。
有关遗留 API
,请参阅附录 III 。
仅实现此规范的 提供者(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)
通过CC0放弃版权和相关权利。
附录 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的约定。
连接 (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
状态代码表。
切换链 (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和shh_subscription依赖于此事件来发出订阅更新。
例如 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
取代。
网络已更改(已弃用)
该事件networkChanged
被chainChanged
取代。
详情参考EIP-155:简单重放攻击保护和EIP-695:为 JSON-RPC 创建 eth_chainId 方法。
通知(弃用)
此事件由message
取代。
从历史上看,此事件已通过例如eth_subscribe
表单的订阅更新发出{ subscription: string, result: unknown }
。
原文引用 (Original Citation)
Please cite this document as:
Fabian Vogelsteller, Ryan Ghods, Victor Maia, Marc Garreau, Erik Marks, "EIP-1193: Ethereum Provider JavaScript API," Ethereum Improvement Proposals, no. 1193, June 2018. [Online serial]. Available: eips.ethereum.org/EIPS/eip-11….