From a8e18356ea7e9b4a28419dd03e21472b9a5bcec1 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Mon, 23 Jul 2018 13:32:56 -0700 Subject: [PATCH] Automatically merged updates to draft EIP(s) 1193 Hi, I'm a bot! This change was automatically merged because: - It only modifies existing Draft or Last Call EIP(s) - The PR was approved or written by at least one author of each modified EIP - The build is passing --- EIPS/eip-1193.md | 211 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 201 insertions(+), 10 deletions(-) diff --git a/EIPS/eip-1193.md b/EIPS/eip-1193.md index deaf17a0..c78161fc 100644 --- a/EIPS/eip-1193.md +++ b/EIPS/eip-1193.md @@ -2,7 +2,7 @@ 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/640 +discussions-to: https://ethereum-magicians.org/t/eip-1193-ethereum-provider-javascript-api/640 status: Draft type: Standards Track category: Interface @@ -33,7 +33,7 @@ See the [available methods](https://github.com/ethereum/wiki/wiki/JSON-RPC#json- #### Subscribe ```js -ethereum.subscribe(subscriptionType: String, params?: Array): Promise; +ethereum.subscribe(subscriptionType: String, params?: Array): Promise; ``` Promise resolves with `subscriptionId: String` or rejects with `Error`. @@ -220,23 +220,23 @@ If the Ethereum JSON-RPC API returns a response object with no error, then the P If the Ethereum JSON-RPC API returns response object that contains an error property then the Promise **MUST** be rejected 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** be rejected with an Error object containing 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 an error occurs during processing, such as an HTTP error or internal parsing error, then the Promise **MUST** be rejected 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). In case of an error, ensure that the Promise is rejected with an Error that matches the above shape. +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). ### Subscriptions -The `subscribe` method **MUST** send a properly formatted [JSON-RPC request](https://www.jsonrpc.org/specification#request_object) with method `eth_subscribe` and params `[subscriptionType: String, {...params: Array}]` and **MUST** return a Promise that resolves with `subscriptionId: String` or rejected with an Error object containing a human readable string message describing the error and **SHOULD** populate the `code` and `data` properties on the error object with additional error details. +The `subscribe` method **MUST** send a properly formatted [JSON-RPC request](https://www.jsonrpc.org/specification#request_object) with method `eth_subscribe` and params `[subscriptionType: String, {...params: Array}]` and **MUST** return a Promise that resolves with `subscriptionId: String` or rejected with an Error object. -The `unsubscribe` method **MUST** send a properly formatted [JSON-RPC request](https://www.jsonrpc.org/specification#request_object) with method `eth_unsubscribe` and params `[subscriptionId: String]` and **MUST** return a Promise that resolves with `result: Boolean` or rejected with an Error object containing a human readable string message describing the error and **SHOULD** populate the `code` and `data` properties on the error object with additional error details. +The `unsubscribe` method **MUST** send a properly formatted [JSON-RPC request](https://www.jsonrpc.org/specification#request_object) with method `eth_unsubscribe` and params `[subscriptionId: String]` and **MUST** return a Promise that resolves with `result: Boolean` or rejected 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 containing 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 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` with the eventName `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 `subscriptionId` containing 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 an error occurs or the network changes during the listening of the subscription, the Ethereum Provider **MUST** emit an Error object to the eventName `subscriptionId`. If the implementing provider does not support subscriptions, then it **MUST** leave the `subscribe` and `unsubscribe` methods undefined. @@ -244,9 +244,9 @@ If the implementing provider does not support subscriptions, then it **MUST** le 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` using the [status codes for `CloseEvent`](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Status_codes) and a short human readable reason. +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 Ethereum Provider is connected to changes, the Ethereum 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 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). @@ -254,6 +254,197 @@ If the accounts connected to the Ethereum Provider change, the Ethereum Provider The name of the constructor of the Ethereum Provider **MUST** be `EthereumProvider`. +### web3.js Provider + +The implementing Ethereum Provider **MUST** be compatible as a `web3.js` provider. This is accomplished by providing two methods in the `EthereumProvider`: `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). + +## Example 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 */ + + 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 payload = { + id: this._nextJsonrpcId++, + jsonrpc: '2.0', + method, + params + }; + + const promise = new Promise((resolve, reject) => { + this._promises[payload.id] = { resolve, reject }; + }); + + // Send jsonrpc request to node + window.postMessage({ type: 'write', message: payload }, origin); + + return promise; + } + + subscribe(subscriptionType, params) { + return this.send('eth_subscribe', [subscriptionType, ...params]).then( + subscriptionId => { + this._activeSubscriptions.push(subscriptionId); + } + ); + } + + unsubscribe(subscriptionId) { + return this.send('eth_unsubscribe', [subscriptionId]).then(success => { + if (success) { + // Remove subscription + this._activeSubscription = this._activeSubscription.filter( + id => id !== subscriptionId + ); + // Remove listeners on subscriptionId + this.removeAllListeners(subscriptionId); + } + }); + } + + /* 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 node + window.postMessage({ type: 'create' }, origin); + + // 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 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/).