EIP 1193: Ethereum Provider JavaScript API Source

AuthorRyan Ghods, Marc Garreau
Discussions-Tohttps://ethereum-magicians.org/t/eip-1193-ethereum-provider-javascript-api/640
StatusDraft
TypeStandards Track
CategoryInterface
Created2018-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 2 methods: send and on. It emits 5 types of events: notification, connect, close, networkChanged, and accountsChanged.

It is intended to be available on window.ethereum.

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.

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 account(s) public keys.

Events

Events are emitted using EventEmitter.

notification

All subscriptions from the node emit on notification. Attach listeners with:

ethereum.on('notification', listener: (result: any) => void): this;

To create a subscription, call ethereum.send('eth_subscribe') or ethereum.send('shh_subscribe'). The subscription result object will emit through notification.

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’ 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: 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('notification', 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

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 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) public keys or reject with an Error. If the account(s) enabled by the provider change, the accountsChanged event MUST also emit.

Events

notification

All subscriptions received from the node MUST emit the message.params to the eventName notification without modification.

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, 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 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.

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 = {};

    // 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._isConnected = true;
    this.emit('connect');
  }

  _emitClose(code, reason) {
    this._isConnected = false;
    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}`
        );
      });
  }

  isConnected() {
    return this._isConnected;
  }
}

Copyright and related rights waived via CC0.