--- eip: 1193 title: Ethereum Provider JavaScript API author: Ryan Ghods (@ryanio), Marc Garreau (@marcgarreau) 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 requires: 1102 --- ## Summary This EIP formalizes an Ethereum Provider JavaScript API for consistency across clients and applications. The provider is designed to be minimal, containing 4 methods: `enable`, `send`, `subscribe`, and `unsubscribe`. It emits 4 types of events: `connect`, `close`, `networkChanged`, and `accountsChanged`. It is intended to be available on `window.ethereum`. ## API ### Enable By default a "read-only" provider is supplied to allow access to the blockchain while preserving user privacy. A full provider can be requested to allow account-level methods: ```js ethereum.enable(): Promise; ``` This shows a dialog to the user asking if they would like to authenticate any account(s) to the dapp. Promise resolves with `true` or rejects with `Error`. ### 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). ### Subscriptions #### Subscribe ```js ethereum.subscribe(subscriptionType: String, subscriptionMethod: String, params?: Array): Promise; ``` `subscriptionType` is expected to be `eth_subscribe` or `shh_subscribe`. 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). Promise resolves with `subscriptionId: String` or rejects with `Error`. Results emit on `subscriptionId` using [EventEmitter](https://nodejs.org/api/events.html). Attach listeners with: ```js ethereum.on(subscriptionId, listener: (result: any) => void): this; ``` The event emits with `result`, the subscription `result` or an `Error` object. #### Unsubscribe ```js ethereum.unsubscribe(subscriptionType: String, subscriptionId: String): Promise; ``` `subscriptionType` is expected to be `eth_unsubscribe` or `shh_unsubscribe`. Promise resolves with `success: Boolean` or rejects with `Error`. All [EventEmitter](https://nodejs.org/api/events.html) listeners on `subscriptionId` will also be removed. ### Events Events are emitted using [EventEmitter](https://nodejs.org/api/events.html). #### 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' public keys. ### Constructor ```js ethereum.constructor.name; > 'EthereumProvider' ``` ## Examples ```js const ethereum = window.ethereum; // A) Primary use case - set provider in web3.js web3.setProvider(ethereum); // B) Secondary use case - 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: Enable full provider ethereum .enable() .then(success => { if (success) { console.log(`Ethereum provider enabled enabled!`); // 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}` ); }); } }) .catch(error => { console.error( `Error enabling provider: ${error.message}. Code: ${error.code}. Data: ${error.data}` ); }); // Example 4: Log new blocks let subId; ethereum .subscribe('eth_subscribe', 'newHeads') .then(subscriptionId => { subId = subscriptionId; ethereum.on(subscriptionId, block => { if (block instanceof Error) { const error = result; console.error( `Error from newHeads subscription: ${error.message}. Code: ${error.code}. Data: ${error.data}` ); } else { 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 .unsubscribe('eth_unsubscribe', subId) .then(result => { console.log(`Unsubscribed newHeads subscription ${subscriptionId}`); }) .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 ### Enable 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 `enable` 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 `enable` method **MAY** immediately return. The `enable` method **MUST** return a Promise that resolves with true or rejects with an `Error`. If the accounts enabled by provider change, the `accountsChanged` event **MUST** also emit. ### Send The `send` method **MUST** send a properly formatted [JSON-RPC request](https://www.jsonrpc.org/specification#request_object). If the Ethereum JSON-RPC API returns a response object with no error, then the Promise **MUST** resolve with the `response.result` object untouched by the implementing Ethereum Provider. If the Ethereum JSON-RPC API returns response object that contains an error property then the Promise **MUST** reject with an Error object containing the `response.error.message` as the Error message, `response.error.code` as a code property on the error and `response.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 implementing Ethereum Provider is not talking to an external Ethereum JSON-RPC API provider then it **MUST** resolve with an object that matches the JSON-RPC API object as specified in the [Ethereum JSON-RPC documentation](https://github.com/ethereum/wiki/wiki/JSON-RPC). If the JSON-RPC request requires an account that is not yet authenticated, the Promise **MUST** reject with an `Error`. ### Subscriptions The `subscribe` method **MUST** send a properly formatted [JSON-RPC request](https://www.jsonrpc.org/specification#request_object) with method `subscriptionType` (`eth_subscribe` or `shh_subscribe`) and params `[subscriptionMethod: String, ...params: Array]`. It **MUST** return a Promise that resolves with `id: String` or rejects with an Error object. The `unsubscribe` method **MUST** send a properly formatted [JSON-RPC request](https://www.jsonrpc.org/specification#request_object) with method `subscriptionType` (`eth_unsubscribe` or `shh_unsubscribe`) and params `[subscriptionId: String]`. It **MUST** return a Promise that resolves with `result: Boolean` or rejects with an Error object. If the `unsubscribe` method returns successfully with a `True` result, the implementing provider **MUST** remove all listeners on the `subscriptionId` using `ethereum.removeAllListeners(subscriptionId);`. If an error occurs during processing of the subscription, such as an HTTP error or internal parsing error then the Promise **MUST** return with an Error object. The implementing Ethereum Provider **MUST** emit every subscription response `result` to the eventName of the `subscriptionId`. If an error occurs or the network changes during the listening of the subscription, the Ethereum Provider **MUST** emit an Error object to the eventName of the `subscriptionId`. If the implementing provider does not support subscriptions, then it **MUST** leave the `subscribe` and `unsubscribe` methods undefined. ### Events If the network connects, the Ethereum Provider **MUST** emit an event named `connect`. 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). 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`). If the accounts connected to the Ethereum Provider change, the Ethereum Provider **MUST** send an event with the name `accountsChanged` with args `accounts: Array` containing the accounts' public key(s). ### Class The name of the constructor of the Ethereum Provider **MUST** be `EthereumProvider`. ### web3.js Backwards Compatibility If the implementing Ethereum Provider would like to be compatible with `web3.js` prior to `1.0.0-beta37`, it **MUST** provide two methods: `sendAsync(payload: Object, callback: (error: any, result: any) => void): void` and `isConnected(): Boolean`. ### 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 Full Provider | User denied enabling the full Ethereum Provider by choosing not to authorize 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. | ## Sample Class Implementation ```js class EthereumProvider extends EventEmitter { constructor() { // Call super for `this` to be defined super(); // Init storage this._isConnected = false; this._nextJsonrpcId = 0; this._promises = {}; this._activeSubscriptions = []; // Fire the connect this._connect(); // Listen for jsonrpc responses window.addEventListener('message', this._handleJsonrpcMessage.bind(this)); } /* Methods */ enable() { return new Promise((resolve, reject) => { window.mist .requestAccounts() .then(accounts => { if (accounts.length > 0) { resolve(true); } else { const error = new Error('User Denied Full Provider'); error.code = 4001; reject(error); } }) .catch(reject); }); } 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; } subscribe(subscriptionType, subscriptionMethod, params = []) { return this.send(subscriptionType, [subscriptionMethod, ...params]).then( subscriptionId => { this._activeSubscriptions.push(subscriptionId); return subscriptionId; } ); } unsubscribe(subscriptionType, subscriptionId) { return this.send(subscriptionType, [subscriptionId]).then(success => { if (success) { // Remove subscription this._activeSubscription = this._activeSubscription.filter( id => id !== subscriptionId ); // Remove listeners on subscriptionId this.removeAllListeners(subscriptionId); return success; } }); } /* 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 result const { subscription, result } = message.params; this.emit(subscription, result); } } } /* 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 */ _emitConnect() { this._isConnected = true; this.emit('connect'); } _emitClose(code, reason) { this._isConnected = false; this.emit('close', code, reason); // Send Error objects to any open subscriptions this._activeSubscriptions.forEach(id => { const error = new Error( `Provider connection to network closed. Subscription lost, please subscribe again.` ); this.emit(id, error); }); // Clear subscriptions this._activeSubscriptions = []; } _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}` ); }); } isConnected() { return this._isConnected; } } ``` ## Copyright Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).