--- eip: 1193 title: Ethereum Provider JavaScript API author: Fabian Vogelsteller (@frozeman), Ryan Ghods (@ryanio), Marc Garreau (@marcgarreau), Victor Maia (@MaiaVictor) discussions-to: https://ethereum-magicians.org/t/eip-1193-ethereum-provider-javascript-api/640 status: Draft type: Standards Track category: Interface created: 2018-06-30 --- ## Summary This EIP formalizes an Ethereum Provider JavaScript API for consistency across clients and applications. The provider is designed to be minimal and is intended to be available on `window.ethereum` for cross environment compatibility. ## API ### Send Ethereum API methods can be sent and received: ```js ethereum.send(method: String, params?: Array): Promise; ``` Promise resolves with `result` or rejects with `Error`. See the [available methods](https://github.com/ethereum/wiki/wiki/JSON-RPC#json-rpc-methods). #### eth_requestAccounts By default, the provider supplied to a new dapp has is a "read-only" provider with no accounts authenticated. See [EIP 1102: Opt-in account exposure](https://eips.ethereum.org/EIPS/eip-1102). To request accounts, call `ethereum.send('eth_requestAccounts')`. This will ask the user which account(s) they would like to authenticate to the dapp. Promise resolves with an array of the enabled account(s) addresses. ### Events Events are emitted using [EventEmitter](https://nodejs.org/api/events.html). #### notification All subscriptions from the node emit on "subscription type" (e.g. `eth_subscription`, or `ssh_subscription`). Attach listeners with: ```js ethereum.on('eth_subscription', listener: (result: any) => void): this; ``` To create a subscription, call `ethereum.send('eth_subscribe')` or `ethereum.send('shh_subscribe')`. The subscription object will emit through the specifc subscription type. The result object will look as follows: ```js { "subscription":"0xc3b33aa549fb9a60e95d21862596617c", "result": {...} } ``` 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). #### connect The provider emits `connect` on connect to a network. ```js ethereum.on('connect', listener: () => void): this; ``` You can detect which network by sending `net_version`: ```js const network = await ethereum.send('net_version'); > '1' ``` #### close The provider emits `close` on disconnect from a network. ```js ethereum.on('close', listener: (code: Number, reason: String) => void): this; ``` 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). #### networkChanged The provider emits `networkChanged` on connect to a new network. ```js ethereum.on('networkChanged', listener: (networkId: String) => void): this; ``` The event emits with `networkId`, the new network returned from `net_version`. #### accountsChanged The provider emits `accountsChanged` if the accounts returned from the provider (`eth_accounts`) changes. ```js ethereum.on('accountsChanged', listener: (accounts: Array) => void): this; ``` The event emits with `accounts`, an array of the accounts' addresses. ## Examples ```js const ethereum = window.ethereum; // A) Set provider in web3.js var web3 = new Web3(ethereum); // B) Use provider object directly // Example 1: Log last block ethereum .send('eth_getBlockByNumber', ['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 2: Request accounts ethereum .send('eth_requestAccounts') .then(accounts => { if (accounts.length > 0) { console.log(`Accounts enabled:\n${accounts.join('\n')}`); } else { console.error(`No accounts enabled.`); } }) .catch(error => { console.error( `Error requesting accounts: ${error.message}. Code: ${error.code}. Data: ${error.data}` ); }); // Example 3: Log available accounts ethereum .send('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: Log new blocks let subId; ethereum .send('eth_subscribe', ['newHeads']) .then(subscriptionId => { subId = subscriptionId; ethereum.on('eth_subscription', 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); } } }); }) .catch(error => { console.error( `Error making newHeads subscription: ${error.message}. Code: ${error.code}. Data: ${error.data}` ); }); // to unsubscribe ethereum .send('eth_unsubscribe', [subId]) .then(result => { console.log(`Unsubscribed newHeads subscription ${subId}`); }) .catch(error => { console.error( `Error unsubscribing newHeads subscription: ${error.message}. Code: ${error.code}. Data: ${error.data}` ); }); // 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 6: Log if connection ends ethereum.on('close', (code, reason) => { console.log(`Ethereum provider connection closed: ${reason}. Code: ${code}`); }); ``` ## Specification ### Errors 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. If an error occurs during processing, such as an HTTP error or internal parsing error, then the Promise **MUST** reject with an `Error` object. If the request requires an account that is not yet authenticated, the Promise **MUST** reject with Error code 4100. #### eth_requestAccounts The provider supplied to a new dapp **MUST** be a "read-only" provider: authenticating no accounts by default, returning a blank array for `eth_accounts`, and rejecting any methods that require an account with Error code `4100`. If the dapp has been previously authenticated and remembered by the user, then the provider supplied on load **MAY** automatically be enabled with the previously authenticated accounts. If no accounts are authenticated, the `eth_requestAccounts` method **MUST** ask the user which account(s) they would like to authenticate to the dapp. If the request has been previously granted and remembered, the `eth_requestAccounts` method **MAY** immediately return. The `eth_requestAccounts` method **MUST** resolve with an array of the account(s) addresses or reject with an `Error`. If the account(s) enabled by the provider change, the `accountsChanged` event **MUST** also emit. For full specification of the `eth_requestAccounts` RPC method, see [EIP 1102: Opt-in account exposure](https://eips.ethereum.org/EIPS/eip-1102). ### Events 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`. #### notification All subscriptions received from the node **MUST** emit the `subscription` property with the subscription ID and a `results` property. #### connect If the network connects, the Ethereum Provider **MUST** emit an event named `connect`. #### close 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). #### networkChanged If the network the provider is connected to changes, the provider **MUST** emit an event named `networkChanged` with args `networkId: String` containing the ID of the new network (using the Ethereum JSON-RPC call `net_version`). #### accountsChanged 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. ### web3.js Backwards Compatibility If the implementing Ethereum Provider would like to be compatible with `web3.js` prior to `1.0.0-beta38`, it **MUST** provide the method: `sendAsync(payload: Object, callback: (error: any, result: any) => void): void`. ### Error object and codes 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. 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: | Status code | Name | Description | | ----------- | ---------------------------- | --------------------------------------------------------------------- | | 4001 | User Denied Request Accounts | User denied authorizing any accounts for the dapp. | | 4010 | User Denied Create Account | User denied creating a new account. | | 4100 | Unauthorized | The requested account has not been authorized by the user. | | 4200 | Unsupported Method | The requested method is not supported by the given Ethereum Provider. | ## Sample Class Implementation ```js class EthereumProvider extends EventEmitter { constructor() { // Call super for `this` to be defined super(); // Init storage this._nextJsonrpcId = 0; this._promises = {}; // Fire the connect this._connect(); // Listen for jsonrpc responses window.addEventListener('message', this._handleJsonrpcMessage.bind(this)); } /* Methods */ send(method, params = []) { if (!method || typeof method !== 'string') { return new Error('Method is not a valid string.'); } if (!(params instanceof Array)) { return new Error('Params is not a valid array.'); } const id = this._nextJsonrpcId++; const jsonrpc = '2.0'; const payload = { jsonrpc, id, method, params }; const promise = new Promise((resolve, reject) => { this._promises[payload.id] = { resolve, reject }; }); // Send jsonrpc request to Mist window.postMessage( { type: 'mistAPI_ethereum_provider_write', message: payload }, targetOrigin ); 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); } } } /* Connection handling */ _connect() { // Send to Mist window.postMessage( { type: 'mistAPI_ethereum_provider_connect' }, targetOrigin ); // 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); } _emitNetworkChanged(networkId) { this.emit('networkChanged', networkId); } _emitAccountsChanged(accounts) { this.emit('accountsChanged', accounts); } /* web3.js Provider Backwards Compatibility */ 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}` ); }); } } ``` ## Copyright Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).