14 KiB
eip | title | author | discussions-to | status | type | category | created |
---|---|---|---|---|---|---|---|
1193 | Ethereum Provider JavaScript API | Fabian Vogelsteller (@frozeman), Ryan Ghods (@ryanio), Marc Garreau (@marcgarreau), Victor Maia (@MaiaVictor) | https://ethereum-magicians.org/t/eip-1193-ethereum-provider-javascript-api/640 | Draft | Standards Track | Interface | 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:
ethereum.send(method: String, params?: Array<any>): Promise<any>;
Promise resolves with result
or rejects with Error
.
See the available 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.
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.
notification
All subscriptions from the node emit on "subscription type" (e.g. eth_subscription
, or ssh_subscription
). Attach listeners with:
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:
{
"subscription":"0xc3b33aa549fb9a60e95d21862596617c",
"result": {...}
}
See the eth subscription methods and shh subscription methods.
connect
The provider emits connect
on connect to a network.
ethereum.on('connect', listener: () => void): this;
You can detect which network by sending net_version
:
const network = await ethereum.send('net_version');
> '1'
close
The provider emits close
on disconnect from a network.
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.
networkChanged
The provider emits networkChanged
on connect to a new network.
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.
ethereum.on('accountsChanged', listener: (accounts: Array<String>) => void): this;
The event emits with accounts
, an array of the accounts' addresses.
Examples
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.
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
.
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<String>
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, 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
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.