17 KiB
eip | title | author | discussions-to | status | type | category | created | requires |
---|---|---|---|---|---|---|---|---|
1193 | Ethereum Provider JavaScript API | Ryan Ghods (@ryanio), Marc Garreau (@marcgarreau) | https://ethereum-magicians.org/t/eip-1193-ethereum-provider-javascript-api/640 | Draft | Standards Track | Interface | 2018-06-30 | 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:
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:
ethereum.send(method: String, params?: Array<any>): Promise<any>;
Promise resolves with result
or rejects with Error
.
See the available methods.
Subscriptions
Subscribe
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 and shh subscription methods.
Promise resolves with subscriptionId: String
or rejects with Error
.
Results emit on subscriptionId
using EventEmitter. Attach listeners with:
ethereum.on(subscriptionId, listener: (result: any) => void): this;
The event emits with result
, the subscription result
or an Error
object.
Unsubscribe
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 listeners on subscriptionId
will also be removed.
Events
Events are emitted using EventEmitter.
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' public keys.
Constructor
ethereum.constructor.name;
> 'EthereumProvider'
Examples
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.
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.
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 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 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
.
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, 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
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.