mirror of https://github.com/status-im/EIPs.git
451 lines
14 KiB
Markdown
451 lines
14 KiB
Markdown
---
|
|
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 3 methods: `send`, `subscribe`, and `unsubscribe`. It emits 4 types of events: `connect`, `close`, `networkChanged`, and `accountsChanged`.
|
|
|
|
## API
|
|
|
|
### Send
|
|
|
|
```js
|
|
ethereum.send(method: String, params?: Array<any>): Promise<any>;
|
|
```
|
|
|
|
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, params?: Array<any>): Promise<String|Error>;
|
|
```
|
|
|
|
Promise resolves with `subscriptionId: String` or rejects with `Error`.
|
|
|
|
See the [types of subscriptions](https://github.com/ethereum/go-ethereum/wiki/RPC-PUB-SUB#supported-subscriptions).
|
|
|
|
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(subscriptionId: String): Promise<Boolean|Error>;
|
|
```
|
|
|
|
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<String>) => void): this;
|
|
```
|
|
|
|
The event emits with `accounts`, an array of the accounts' public keys.
|
|
|
|
### Constructor
|
|
|
|
```js
|
|
ethereum.constructor.name;
|
|
> 'EthereumProvider'
|
|
```
|
|
|
|
## Examples
|
|
|
|
```js
|
|
// Request Ethereum Provider (EIP 1102)
|
|
window.addEventListener('message', event => {
|
|
if (event.data && event.data.type === 'ETHEREUM_PROVIDER_SUCCESS') {
|
|
start(window.ethereum);
|
|
}
|
|
});
|
|
window.postMessage({ type: 'ETHEREUM_PROVIDER_REQUEST' }, this.origin);
|
|
|
|
function start(ethereum) {
|
|
// A) Primary use case - set provider in web3.js
|
|
web3.setProvider(ethereum);
|
|
|
|
// B) Secondary use case - use provider object directly
|
|
// Example: Log 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: Log last block
|
|
ethereum
|
|
.send('eth_getBlockByNumber', ['latest', 'true'])
|
|
.then(block => {
|
|
console.log(`Block ${block.number}:\n${block}`);
|
|
})
|
|
.catch(error => {
|
|
console.error(
|
|
`Error fetching last block: ${error.message}.
|
|
Code: ${error.code}. Data: ${error.data}`
|
|
);
|
|
});
|
|
|
|
// Example: Log new blocks
|
|
let subId;
|
|
ethereum
|
|
.subscribe('newHeads')
|
|
.then(subscriptionId => {
|
|
subId = subscriptionId;
|
|
ethereum.on(subscriptionId, block => {
|
|
if (result 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}:\n${block}`);
|
|
}
|
|
});
|
|
})
|
|
.catch(error => {
|
|
console.error(
|
|
`Error making newHeads subscription: ${error.message}.
|
|
Code: ${error.code}. Data: ${error.data}`
|
|
);
|
|
});
|
|
// to unsubscribe
|
|
ethereum
|
|
.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: 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: Log if connection ends
|
|
ethereum.on('close', (code, reason) => {
|
|
console.log(
|
|
`Ethereum provider connection closed: ${reason}. Code: ${code}`
|
|
);
|
|
});
|
|
}
|
|
```
|
|
|
|
## Specification
|
|
|
|
### 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** 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.
|
|
|
|
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<any>}]` 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.
|
|
|
|
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` 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`.
|
|
|
|
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<String>` containing the accounts' public key(s).
|
|
|
|
### Class
|
|
|
|
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).
|
|
|
|
## 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 */
|
|
|
|
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 },
|
|
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 Mist
|
|
window.postMessage({ type: 'mistAPI_ethereum_provider_connect' }, 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/).
|