EIPs/EIPS/eip-1193.md

14 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 3 methods: send, subscribe, and unsubscribe. It emits 4 types of events: connect, close, networkChanged, and accountsChanged.

API

Send

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, params?: Array<any>): Promise<String|Error>;

Promise resolves with subscriptionId: String or rejects with Error.

See the types of subscriptions.

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(subscriptionId: String): Promise<Boolean|Error>;

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

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

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.

Subscriptions

The subscribe method MUST send a properly formatted JSON-RPC request 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 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.

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.

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 */

  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 and related rights waived via CC0.