EIPs/EIPS/eip-1193.md

518 lines
17 KiB
Markdown
Raw Normal View History

---
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<Boolean>;
```
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<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, subscriptionMethod: String, params?: Array<any>): Promise<String>;
```
`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<Boolean>;
```
`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<String>) => 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<any>]`. 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<String>` 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/).