diff --git a/EIPS/eip-1193.md b/EIPS/eip-1193.md index e9359a346f11ef..b828986081bf3d 100644 --- a/EIPS/eip-1193.md +++ b/EIPS/eip-1193.md @@ -7,370 +7,431 @@ status: Draft type: Standards Track category: Interface created: 2018-06-30 -requires: 155, 695, 1102, 1474 +requires: 155, 695, 1102, 1474, 1767 --- ## Summary -This EIP formalizes an Ethereum Provider JavaScript API for consistency across clients and applications. The provider is designed to be minimal and intended to be available on `window.ethereum` for cross environment compatibility. The events are provided as a convenience to enable reactive dapp UIs. +This EIP formalizes a JavaScript Ethereum Provider API for consistency across clients and applications. + +The Provider is intended to be available as `globalThis.ethereum` (i.e. `window.ethereum` in browsers), so that JavaScript dapps can be written once and function in perpetuity. + +The Provider's interface is designed to be minimal, preferring that features are introduced in the API layer (e.g. see [`eth_requestAccounts`](https://eips.ethereum.org/EIPS/eip-1102)), and agnostic of transport and RPC protocols. + +The events `connect`, `close`, `chainChanged`, and `accountsChanged` are provided as a convenience to help enable reactive dapp UIs. ## API -### Send +### request + +Makes an Ethereum RPC method call. + +```typescript +type RequestParams = Array | { [key: string]: any }; + +ethereum.request(method: string, params?: RequestParams): Promise; +``` -Ethereum JSON-RPC API methods can be sent and received: +The Promise resolves with the method's result or rejects with an `Error`. For example: -```js -ethereum.send(method: String, params?: Array): Promise; +```javascript +ethereum + .request('eth_accounts') + .then((accounts) => console.log(accounts)) + .catch((error) => console.error(error)); ``` -Promise resolves with `result` or rejects with `Error`. +Consult each Ethereum RPC method's documentation for its return type. +You can find a list of common methods [here](https://eips.ethereum.org/EIPS/eip-1474). -See [available JSON-RPC methods](https://github.com/ethereumproject/go-ethereum/wiki/JSON-RPC#json-rpc-methods). +The Promise rejects with errors of the following form: -### Events +```typescript +{ + message: string, + code: number, + data?: any +} +``` + +See the [RPC Errors](#rpc-errors) section for more details. -Events are emitted using [EventEmitter](https://nodejs.org/api/events.html). +#### RPC Protocols -#### notification +Multiple RPC protocols may be available. -All subscriptions from the node emit on notification. Attach listeners with: +[EIP 1474](https://eips.ethereum.org/EIPS/eip-1474) specifies the Ethereum JSON-RPC API. -```js -ethereum.on('notification', listener: (result: any) => void): this; +[EIP 1767](https://eips.ethereum.org/EIPS/eip-1767) specifies the Ethereum GraphQL schema. + +### sendAsync (DEPRECATED) + +Submits a JSON-RPC request to the Provider. +As [`ethereum.request`](#request), but with JSON-RPC objects and a callback. + +```typescript +ethereum.sendAsync(request: Object, callback: Function): Object; +``` + +The interfaces of request and response objects are not specified here. +Historically, they have followed the [Ethereum JSON-RPC specification](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1474.md). + +### send (DEPRECATED) + +Due to conflicting implementations and specifications, this method is unreliable and should not be used. + +```typescript +ethereum.send(...args: Array): unknown; ``` -To create a subscription, call `ethereum.send('eth_subscribe', [])` or `ethereum.send('shh_subscribe', [])`. +### Events -See the [eth subscription methods](https://github.com/ethereum/go-ethereum/wiki/RPC-PUB-SUB#supported-subscriptions) and [shh subscription methods](https://github.com/ethereum/go-ethereum/wiki/Whisper-v6-RPC-API#shh_subscribe). +Events follow the [Node.js `EventEmitter`](https://nodejs.org/api/events.html) API. #### connect -The provider emits `connect` on connect to a network. +The Provider emits `connect` when it: -```js -ethereum.on('connect', listener: () => void): this; -``` +- first connects to a chain after being initialized. +- first connects to a chain, after the `close` event was emitted. -You can detect which chain by sending `eth_chainId`: +```typescript +interface ProviderConnectInfo { + chainId: string; + [key: string]: unknown; +} -```js -const chainId = await ethereum.send('eth_chainId'); -> 1 +ethereum.on('connect', listener: (connectInfo: ProviderConnectInfo) => void): ethereum; ``` +The event emits an object with a hexadecimal string `chainId` per the `eth_chainId` Ethereum RPC method, and other properties as determined by the Provider. + #### close -The provider emits `close` on disconnect from a network. +The Provider emits `close` when it becomes disconnected from all chains. -```js -ethereum.on('close', listener: (code: Number, reason: String) => void): this; +```typescript +ethereum.on('close', listener: (code: number, reason: string) => void): ethereum; ``` -The event emits with `code` and `reason`. The code follows the table of [`CloseEvent` status codes](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes). +This event emits with `code` and `reason`. The code follows the table of [`CloseEvent` status codes](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes). #### chainChanged -The provider emits `chainChanged` on connect to a new chain. +The Provider emits `chainChanged` when connecting to a new chain. -```js -ethereum.on('chainChanged', listener: (chainId: Integer) => void): this; +```typescript +ethereum.on('chainChanged', listener: (chainId: string) => void): ethereum; ``` -The event emits with integer `chainId`, the new chain returned from `eth_chainId`. +The event emits a hexadecimal string `chainId` per the `eth_chainId` Ethereum RPC method. -The event `networkChanged` is deprecated in favor of `chainChanged`. For more info, see [EIP 155: Simple replay attack protection](https://eips.ethereum.org/EIPS/eip-155) and [EIP 695: Create eth_chainId method for JSON-RPC](https://eips.ethereum.org/EIPS/eip-695). +#### networkChanged (DEPRECATED) + +The event `networkChanged` is deprecated in favor of `chainChanged`. For details, see [EIP 155: Simple replay attack protection](https://eips.ethereum.org/EIPS/eip-155) and [EIP 695: Create eth_chainId method for JSON-RPC](https://eips.ethereum.org/EIPS/eip-695). #### accountsChanged -The provider emits `accountsChanged` if the accounts returned from the provider (`eth_accounts`) changes. +The Provider emits `accountsChanged` if the accounts returned from the Provider (`eth_accounts`) change. + +```typescript +ethereum.on('accountsChanged', listener: (accounts: Array) => void): ethereum; +``` + +The event emits with `accounts`, an array of account addresses, per the `eth_accounts` Ethereum RPC method. + +#### message + +The Provider emits `message` to communicate arbitrary messages to the consumer. +Messages may include JSON-RPC notifications, GraphQL subscriptions, and/or any other event as defined by the Provider. -```js -ethereum.on('accountsChanged', listener: (accounts: Array) => void): this; +```typescript +interface ProviderMessage { + type: string; + data: unknown; +} + +ethereum.on('message', listener: (notification: ProviderMessage) => void): Provider; ``` -The event emits with `accounts`, an array of the accounts' addresses. +##### Subscriptions + +Some clients like [Geth](https://geth.ethereum.org/docs/rpc/pubsub) and [Parity](https://wiki.parity.io/JSONRPC-eth_pubsub-module) support Publish-Subscribe (_Pub-Sub_) using JSON-RPC notifications. This lets you subscribe and wait for events instead of polling for them. +See the [`eth_` subscription methods](https://geth.ethereum.org/docs/rpc/pubsub) and [`shh_` subscription methods](https://github.com/ethereum/go-ethereum/wiki/Whisper-v6-RPC-API#shh_subscribe) for details. + +For e.g. `eth_subscribe` subscription updates, `ProviderMessage.type` will equal the string `'eth_subscription'`. + +#### notification (DEPRECATED) + +This event should not be relied upon, and may not be implemented. + +Historically, it has returned e.g. `eth_subscribe` subscription updates of the form `{ subscription: string, result: unknown }`. ## Examples -```js +```javascript const ethereum = window.ethereum; -// A) Set provider in web3.js +// A) Set Provider in web3.js var web3 = new Web3(ethereum); // web3.eth.getBlock('latest', true).then(...) +// B) Use Provider object directly +// Example 1: Log chainId +ethereum + .request('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}`); + }); -// B) Use provider object directly -// Example 1: Log last block +// Example 2: Log last block ethereum - .send('eth_getBlockByNumber', ['latest', 'true']) - .then(block => { + .request('eth_getBlockByNumber', ['latest', 'true']) + .then((block) => { console.log(`Block ${block.number}:`, block); }) - .catch(error => { + .catch((error) => { console.error( `Error fetching last block: ${error.message}. Code: ${error.code}. Data: ${error.data}` ); }); -// Example 2: Log available accounts +// Example 3: Log available accounts ethereum - .send('eth_accounts') - .then(accounts => { + .request('eth_accounts') + .then((accounts) => { console.log(`Accounts:\n${accounts.join('\n')}`); }) - .catch(error => { + .catch((error) => { console.error( `Error fetching accounts: ${error.message}. - Code: ${error.code}. Data: ${error.data}` + Code: ${error.code}. Data: ${error.data}` ); }); - -// Example 3: Log new blocks -let subId; +// Example 4: Log new blocks ethereum - .send('eth_subscribe', ['newHeads']) - .then(subscriptionId => { - subId = subscriptionId; - ethereum.on('notification', result => { - if (result.subscription === subscriptionId) { - if (result.result instanceof Error) { - const error = result.result; - console.error( - `Error from newHeads subscription: ${error.message}. - Code: ${error.code}. Data: ${error.data}` - ); - } else { - const block = result.result; - console.log(`New block ${block.number}:`, block); + .request('eth_subscribe', ['newHeads']) + .then((subscriptionId) => { + ethereum.on('notification', (notification) => { + if (notification.type === 'eth_subscription') { + const { data } = notification; + if (data.subscription === subscriptionId) { + if (typeof data.result === 'string' && data.result) { + const block = data.result; + console.log(`New block ${block.number}:`, block); + } else { + console.error(`Something went wrong: ${data.result}`); + } } } }); }) - .catch(error => { + .catch((error) => { console.error( `Error making newHeads subscription: ${error.message}. Code: ${error.code}. Data: ${error.data}` ); }); - -// Example 4: Log when accounts change -const logAccounts = accounts => { +// Example 5: Log when accounts change +const logAccounts = (accounts) => { console.log(`Accounts:\n${accounts.join('\n')}`); }; ethereum.on('accountsChanged', logAccounts); // to unsubscribe ethereum.removeListener('accountsChanged', logAccounts); -// Example 5: Log if connection ends +// Example 6: Log if connection ends ethereum.on('close', (code, reason) => { - console.log(`Ethereum provider connection closed: ${reason}. Code: ${code}`); + console.log(`Ethereum Provider connection closed: ${reason}. Code: ${code}`); }); ``` ## Specification -### Errors +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC-2119](https://www.ietf.org/rfc/rfc2119.txt). -If the Ethereum Provider request returns an error property then the Promise **MUST** reject with an `Error` object containing the `error.message` as the Error message, `error.code` as a code property on the error and `error.data` as a data property on the error. +### Definitions -If an error occurs during processing, such as an HTTP error or internal parsing error, then the Promise **MUST** reject with an `Error` object. +> This section is non-normative. -If the request requires an account that is not yet authenticated, the Promise **MUST** reject with Error code 4100. +- Provider + - A JavaScript object made available to a dapp, that provides access to Ethereum by means of a Client +- Client + - An endpoint accessed by a Provider, that receives Remote Procedure Call (RPC) requests and returns their results +- Remote Procedure Call (RPC) + - A Remote Procedure Call (RPC), is any request submitted to a Provider for some procedure that is to be processed by a Provider or its Client. -### Events +### Availability -The provider **SHOULD** extend from `EventEmitter` to provide dapps flexibility in listening to events. In place of full `EventEmitter` functionality, the provider **MAY** provide as many methods as it can reasonably provide, but **MUST** provide at least `on`, `emit`, and `removeListener`. +In a browser environment, the Provider **MUST** be made available as the `ethereum` property on the global `window` object. -#### notification +In a non-browser environment, the Provider **SHOULD** be made available as the `ethereum` property on the `globalThis` object. -All subscriptions received from the node **MUST** emit the `subscription` property with the subscription ID and a `results` property. +### API -#### connect +The Provider **MUST** expose the API defined in this section. All API entities **MUST** adhere to the types and interfaces defined in this section. -If the network connects, the Ethereum Provider **MUST** emit an event named `connect`. +The Provider **MAY** expose methods and properties not specified in this document. -#### close +#### request -If the network connection closes, the Ethereum Provider **MUST** emit an event named `close` with args `code: Number, reason: String` following the [status codes for `CloseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes). +```typescript +type RequestParams = Array | { [key: string]: any }; -#### chainChanged +ethereum.request(method: string, params?: RequestParams): Promise; +``` -If the chain the provider is connected to changes, the provider **MUST** emit an event named `chainChanged` with args `chainId: Integer` containing the ID of the new chain (using the Ethereum JSON-RPC call `eth_chainId`). +The `request` method is intended as a transport- and protocol-agnostic wrapper function for Remote Procedure Calls (RPCs). -#### accountsChanged +The `request` method **MUST** send a properly formatted request to the Provider's Ethereum client. +Requests **MUST** be handled such that, for a given set of arguments, the returned Promise either resolves with a value per the RPC method's specification, or rejects with an error. -If the accounts connected to the Ethereum Provider change at any time, the Ethereum Provider **MUST** send an event with the name `accountsChanged` with args `accounts: Array` containing the accounts' addresses. +If present, the `params` argument **MUST** be structured value, either by-position as an `Array` or by-name as an `Object`. -### Error object and codes +If resolved, the Promise **MUST NOT** resolve with any RPC protocol-specific response objects, unless the RPC method's return type is so defined by its specification. -If an Error object is returned, it **MUST** contain a human readable string message describing the error and **SHOULD** populate the `code` and `data` properties on the error object with additional error details. +If resolved, the Promise **MUST** resolve with a result per the RPC method's specification. -Appropriate error codes **SHOULD** follow the table of [`CloseEvent` status codes](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes), along with the following table: +If the returned Promise rejects, it **MUST** reject with an `Error` of the form specified in the [RPC Errors](#rpc-errors) section below. -| Status code | Name | Description | -| ----------- | ---------------------------- | ------------------------------------------------------------------------ | -| 4001 | User Rejected Request | The user rejected the request. | -| 4100 | Unauthorized | The requested method and/or account has not been authorized by the user. | -| 4200 | Unsupported Method | The requested method is not supported by the given Ethereum Provider. | +The returned Promise **MUST** reject if any of the following conditions are met: -## Sample Class Implementation +- The client returns an error for the RPC request + - If error returned from the client is compatible with the `ProviderRpcError` interface, the Promise **MAY** reject with that error directly +- The Provider encounters an fails for any reason +- The request requires access to an unauthorized account, per [EIP 1102](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1102.md) + - In this case, the Promise rejection error `code` **MUST** be `4100` -```js -class EthereumProvider extends EventEmitter { - constructor() { - // Call super for `this` to be defined - super(); +### Supported RPC Methods - // Init storage - this._nextJsonRpcId = 0; - this._promises = {}; +A "supported RPC method" is any RPC method that may be called via the Provider. - // Fire the connect - this._connect(); +All supported RPC methods **MUST** be identified by unique strings. - // Listen for jsonrpc responses - window.addEventListener('message', this._handleJsonRpcMessage.bind(this)); - } +Providers **MAY** support whatever RPC methods required to fulfill their purpose, standardized or otherwise. - /* Methods */ +If a Provider supports a method defined in a finalized EIP, the Provider's implementation of this method **MUST** adhere to the method's specification. - send(method, params = []) { - if (!method || typeof method !== 'string') { - return new Error('Method is not a valid string.'); - } +If an RPC method defined in a finalized EIP is not supported, it **SHOULD** be rejected with an appropriate error per [EIP 1474](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1474.md#error-codes). - if (!(params instanceof Array)) { - return new Error('Params is not a valid array.'); - } +#### RPC Errors - const id = this._nextJsonRpcId++; - const jsonrpc = '2.0'; - const payload = { jsonrpc, id, method, params }; +```typescript +interface ProviderRpcError extends Error { + message: string; + code: number; + data?: any; +} +``` - const promise = new Promise((resolve, reject) => { - this._promises[payload.id] = { resolve, reject }; - }); +- `message` + - **MUST** be a human-readable string + - **SHOULD** adhere to the specifications in the [Error Standards](#error-standards) section below +- `code` + - **MUST** be an integer number + - **SHOULD** adhere to the specifications in the [Error Standards](#error-standards) section below +- `data` + - **SHOULD** contain any other useful information about the error - // Send jsonrpc request to Mist - window.postMessage( - { type: 'mistAPI_ethereum_provider_write', message: payload }, - targetOrigin - ); +##### Error Standards - return promise; - } - - /* Internal methods */ - - _handleJsonRpcMessage(event) { - // Return if no data to parse - if (!event || !event.data) { - return; - } - - let data; - try { - data = JSON.parse(event.data); - } catch (error) { - // Return if we can't parse a valid object - return; - } - - // Return if not a jsonrpc response - if (!data || !data.message || !data.message.jsonrpc) { - return; - } - - const message = data.message; - const { id, method, error, result } = message; - - if (typeof id !== 'undefined') { - const promise = this._promises[id]; - if (promise) { - // Handle pending promise - if (data.type === 'error') { - promise.reject(message); - } else if (message.error) { - promise.reject(error); - } else { - promise.resolve(result); - } - delete this._promises[id]; - } - } else { - if (method && method.indexOf('_subscription') > -1) { - // Emit subscription notification - this._emitNotification(message.params); - } - } - } +`ProviderRpcError` codes and messages **SHOULD** follow these conventions, in order of priority: - /* Connection handling */ +1. The errors in the [Provider Errors](#provider-errors) section below - _connect() { - // Send to Mist - window.postMessage( - { type: 'mistAPI_ethereum_provider_connect' }, - targetOrigin - ); +2. The [Ethereum JSON-RPC API error codes](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1474.md#error-codes) + +3. The [`CloseEvent` status codes](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes) + +#### Provider Errors + +| Status code | Name | Description | +| ----------- | --------------------- | ------------------------------------------------------------------------ | +| 4001 | User Rejected Request | The user rejected the request. | +| 4100 | Unauthorized | The requested method and/or account has not been authorized by the user. | +| 4200 | Unsupported Method | The requested method is not supported by the given Ethereum Provider. | + +### Events + +The Provider **SHOULD** extend the [Node.js `EventEmitter`](https://nodejs.org/api/events.html) to provide dapps flexibility in listening to events. In place of full `EventEmitter` functionality, the Provider **MAY** provide as many methods as it can reasonably provide, but **MUST** provide at least `on`, `emit`, and `removeListener`. + +#### message + +The Provider **MAY** emit the event named `message`, for any reason. + +If the Provider supports Ethereum RPC subscriptions, e.g. [`eth_subscribe`](https://geth.ethereum.org/docs/rpc/pubsub), the Provider **MUST** emit the `message` event when it receives a subscription notification. + +When emitted, the `message` event **MUST** be emitted with an object argument of the following form: + +```typescript +interface ProviderMessage { + type: string; + data: unknown; +} +``` - // Reconnect on close - this.once('close', this._connect.bind(this)); - } - - /* Events */ - - _emitNotification(result) { - this.emit('notification', result); - } - - _emitConnect() { - this.emit('connect'); - } - - _emitClose(code, reason) { - this.emit('close', code, reason); - } - - _emitChainChanged(chainId) { - this.emit('chainChanged', chainId); - } - - _emitAccountsChanged(accounts) { - this.emit('accountsChanged', accounts); - } - - /* - Provide `sendAsync` to be compatible as an older web3.js provider. - */ - - sendAsync(payload, callback) { - return this.send(payload.method, payload.params) - .then(result => { - const response = payload; - response.result = result; - callback(null, response); - }) - .catch(error => { - callback(error, null); - // eslint-disable-next-line no-console - console.error( - `Error from EthereumProvider sendAsync ${payload}: ${error}` - ); - }); - } +##### Converting a Subscription Message to a ProviderMessage + +If the Provider receives a subscription message from e.g. an `eth_subscribe` subscription, the Provider **MUST** emit a `message` event with a `ProviderMessage` object of the following form: + +```typescript +interface EthSubscription extends ProviderMessage { + type: 'eth_subscription'; + data: { + subscription: string; + result: any; + }; +} +``` + +#### connect + +If the Provider becomes connected, the Provider **MUST** emit the event named `connect`. + +The Provider "becomes connected" when: + +- it first connects to a chain after initialization. +- it connects to a chain after the `close` event was emitted. + +This event **MUST** be emitted with an object of the following form: + +```typescript +interface ProviderConnectInfo { + chainId: string; + [key: string]: unknown; } ``` +`chainId` **MUST** specify the integer ID of the connected chain as a hexadecimal string, per the [`eth_chainId`](https://eips.ethereum.org/EIPS/eip-695) Ethereum RPC method. + +The `ProviderConnectInfo` object **MAY** contain any other `string` properties with values of any type. + +#### close + +If the Provider becomes disconnected from all chains, the Provider **MUST** emit the event named `close` with ordered values `code: number, reason: string` following the [status codes for `CloseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes). + +#### chainChanged + +If the chain the Provider is connected to changes, the Provider **MUST** emit the event named `chainChanged` with value `chainId: string`, specifying the integer ID of the new chain as a hexadecimal string, per the [`eth_chainId`](https://eips.ethereum.org/EIPS/eip-695) Ethereum RPC method. + +#### accountsChanged + +If the accounts available to the Provider change, the Provider **MUST** emit the event named `accountsChanged` with value `accounts: Array`, containing the account addresses per the `eth_accounts` Ethereum RPC method. + +The "accounts available to the Provider" change when the return value of `eth_accounts` changes. + ## References -* [Initial discussion in `ethereum/interfaces`](https://github.com/ethereum/interfaces/issues/16) -* [Ethereum Magicians thread](https://ethereum-magicians.org/t/eip-1193-ethereum-provider-javascript-api/640) +- [Initial discussion in `ethereum/interfaces`](https://github.com/ethereum/interfaces/issues/16) +- [Ethereum Magicians thread](https://ethereum-magicians.org/t/eip-1193-ethereum-provider-javascript-api/640) +- [Continuing EIP-1193 discussion](https://github.com/ethereum/EIPs/issues/2319) ## Copyright