UNPKG

@parity/api

Version:

The Parity Promise-based API library for interfacing with Ethereum over RPC

406 lines (405 loc) 16.1 kB
"use strict"; // Copyright 2015-2019 Parity Technologies (UK) Ltd. // This file is part of Parity. var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); // Parity is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // Parity is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with Parity. If not, see <http://www.gnu.org/licenses/>. require('./polyfill'); var keccak_256 = require('js-sha3').keccak_256; // eslint-disable-line camelcase var Logging = require('../../subscriptions').Logging; var JsonRpcBase = require('../jsonRpcBase'); var TransportError = require('../error'); /* global WebSocket */ var Ws = /** @class */ (function (_super) { __extends(Ws, _super); // token is optional (secure API) function Ws(url, token, autoconnect) { if (token === void 0) { token = null; } if (autoconnect === void 0) { autoconnect = true; } var _this = _super.call(this) || this; _this._url = url; _this._token = token; _this._messages = {}; _this._subscriptions = {}; _this._sessionHash = null; _this._connecting = false; _this._connected = false; _this._lastError = null; _this._autoConnect = autoconnect; _this._retries = 0; _this._reconnectTimeoutId = null; _this._connectPromise = null; _this._connectPromiseFunctions = {}; _this._onClose = _this._onClose.bind(_this); _this._onError = _this._onError.bind(_this); _this._onMessage = _this._onMessage.bind(_this); _this._onOpen = _this._onOpen.bind(_this); _this._extract = _this._extract.bind(_this); _this._send = _this._send.bind(_this); if (autoconnect) { _this.connect(); } return _this; } Ws.prototype.updateToken = function (token, connect) { if (connect === void 0) { connect = true; } this._token = token; // this._autoConnect = true; if (connect) { this.connect(); } }; Ws.prototype.connect = function () { var _this = this; if (this._connected) { return Promise.resolve(); } if (this._connecting) { return this._connectPromise || Promise.resolve(); } if (this._reconnectTimeoutId) { clearTimeout(this._reconnectTimeoutId); this._reconnectTimeoutId = null; } if (this._ws) { this._ws.onerror = null; this._ws.onopen = null; this._ws.onclose = null; this._ws.onmessage = null; this._ws.close(); this._ws = null; this._sessionHash = null; } this._connecting = true; this._connected = false; this._lastError = null; // rpc secure API if (this._token) { var time = parseInt(new Date().getTime() / 1000, 10); var sha3 = keccak_256(this._token + ":" + time); var hash = sha3 + "_" + time; this._sessionHash = sha3; this._ws = new WebSocket(this._url, hash); // non-secure API } else { this._ws = new WebSocket(this._url); } this._ws.onerror = this._onError; this._ws.onopen = this._onOpen; this._ws.onclose = this._onClose; this._ws.onmessage = this._onMessage; // Get counts in dev mode only if (process.env.NODE_ENV === 'development') { this._count = 0; this._lastCount = { timestamp: Date.now(), count: 0 }; setInterval(function () { var n = _this._count - _this._lastCount.count; var t = (Date.now() - _this._lastCount.timestamp) / 1000; var s = Math.round(1000 * n / t) / 1000; if (_this._debug) { console.log('::parityWS', "speed: " + s + " req/s", "count: " + _this._count, "(+" + n + ")"); } _this._lastCount = { timestamp: Date.now(), count: _this._count }; }, 5000); if (typeof window !== 'undefined') { window._parityWS = this; } } this._connectPromise = new Promise(function (resolve, reject) { _this._connectPromiseFunctions = { resolve: resolve, reject: reject }; }); return this._connectPromise; }; Ws.prototype._onOpen = function (event) { var _this = this; this._setConnected(); this._connecting = false; this._retries = 0; Object.keys(this._messages) .filter(function (id) { return _this._messages[id].queued; }) .forEach(this._send); this._connectPromiseFunctions.resolve(); this._connectPromise = null; this._connectPromiseFunctions = {}; }; Ws.prototype._onClose = function (event) { var _this = this; this._setDisconnected(); this._connecting = false; event.timestamp = Date.now(); this._lastError = event; if (this._autoConnect) { var timeout = this.retryTimeout; var time = timeout < 1000 ? Math.round(timeout) + 'ms' : (Math.round(timeout / 10) / 100) + 's'; console.log('ws:onClose', "trying again in " + time + "..."); this._reconnectTimeoutId = setTimeout(function () { _this.connect(); }, timeout); return; } if (this._connectPromise) { this._connectPromiseFunctions.reject(event); this._connectPromise = null; this._connectPromiseFunctions = {}; } console.log('ws:onClose'); }; Ws.prototype._onError = function (event) { var _this = this; // Only print error if the WS is connected // ie. don't print if error == closed setTimeout(function () { if (_this._connected) { console.error('ws:onError'); event.timestamp = Date.now(); _this._lastError = event; if (_this._connectPromise) { _this._connectPromiseFunctions.reject(event); _this._connectPromise = null; _this._connectPromiseFunctions = {}; } } }, 50); }; Ws.prototype._extract = function (result) { var res = result.result, error = result.error, id = result.id, method = result.method, params = result.params; var msg = this._messages[id]; // initial pubsub ACK if (id && msg.subscription && !error) { // save subscription to map subId -> messageId this._subscriptions[msg.subscription] = this._subscriptions[msg.subscription] || {}; this._subscriptions[msg.subscription][res] = id; // resolve promise with messageId because subId's can collide (eth/parity) msg.resolve(id); // save subId for unsubscribing later msg.subId = res; return msg; } // normal message if (id) { return msg; } // pubsub format if (this._subscriptions[method]) { var messageId = this._messages[this._subscriptions[method][params.subscription]]; if (messageId) { return messageId; } else { throw Error("Received Subscription which is already unsubscribed " + JSON.stringify(result)); } } throw Error("Unknown message format: No ID or subscription " + JSON.stringify(result)); }; Ws.prototype._onMessage = function (event) { try { var result = JSON.parse(event.data); var _a = this._extract(result), method = _a.method, params = _a.params, json = _a.json, resolve = _a.resolve, reject = _a.reject, callback = _a.callback, subscription = _a.subscription; Logging.send(method, params, { json: json, result: result }); result.error = (result.params && result.params.error) || result.error; if (result.error) { this.error(event.data); // Don't print error if request rejected or not is not yet up... if (!/(rejected|not yet up)/.test(result.error.message)) { // fether Issue #317 // js-libs Issue #77 Masks the password param when logging error to console on methods that contain it as a param. // e.g. ["0x2",{},"myincorrectpassword"] -> ["0x2",{},"***"] var dangerous_methods = ['signer_confirmRequest', 'signer_confirmRequestWithToken', 'parity_exportAccount']; var safe_params = void 0; if (dangerous_methods.includes(method)) { safe_params = params.slice(); safe_params[params.length - 1] = '***'; } console.error(method + "(" + JSON.stringify(safe_params || params) + "): " + result.error.code + ": " + result.error.message); } var error = new TransportError(method, result.error.code, result.error.message); if (result.id) { reject(error); } else { callback(error); } delete this._messages[result.id]; return; } // if not initial subscription message resolve & delete if (result.id && !subscription) { resolve(result.result); delete this._messages[result.id]; } else if (result.params) { callback(null, result.params.result); } } catch (e) { console.error('ws::_onMessage', event.data, e); } }; Ws.prototype._send = function (id) { var message = this._messages[id]; if (this._connected) { if (process.env.NODE_ENV === 'development') { this._count++; } return this._ws.send(message.json); } message.queued = !this._connected; message.timestamp = Date.now(); }; Ws.prototype._execute = function (method, params) { var _this = this; return new Promise(function (resolve, reject) { var id = _this.id; var json = _this.encode(method, params); _this._messages[id] = { id: id, method: method, params: params, json: json, resolve: resolve, reject: reject }; _this._send(id); }); }; Ws.prototype._methodsFromApi = function (api) { if (api.subscription) { var subscribe = api.subscribe, unsubscribe = api.unsubscribe, subscription = api.subscription; return { method: subscribe, uMethod: unsubscribe, subscription: subscription }; } return { method: api + "_subscribe", uMethod: api + "_unsubscribe", subscription: api + "_subscription" }; }; Ws.prototype.subscribe = function (api, callback, params) { var _this = this; return new Promise(function (resolve, reject) { var id = _this.id; var _a = _this._methodsFromApi(api), method = _a.method, uMethod = _a.uMethod, subscription = _a.subscription; var json = _this.encode(method, params); _this._messages[id] = { id: id, method: method, uMethod: uMethod, params: params, json: json, resolve: resolve, reject: reject, callback: callback, subscription: subscription }; _this._send(id); }); }; Ws.prototype.unsubscribe = function (messageId) { var _this = this; return new Promise(function (resolve, reject) { var id = _this.id; var _a = _this._messages[messageId], subId = _a.subId, uMethod = _a.uMethod, subscription = _a.subscription; var params = [subId]; var json = _this.encode(uMethod, params); var uResolve = function (v) { delete _this._messages[messageId]; delete _this._subscriptions[subscription][subId]; resolve(v); }; _this._messages[id] = { id: id, method: uMethod, params: params, json: json, resolve: uResolve, reject: reject }; _this._send(id); }); }; Ws.prototype.unsubscribeAll = function () { var _this = this; return new Promise(function (resolve, reject) { var unsubscribed = 0; var keys = Object.keys(_this._messages); keys.forEach(function (i) { return _this._messages[i].subscription ? _this.unsubscribe(_this._messages[i].subId).then(function (_) { return unsubscribed++; }, reject) : null; }); resolve(unsubscribed); }); }; Object.defineProperty(Ws.prototype, "url", { set: function (url) { this._url = url; }, enumerable: true, configurable: true }); Object.defineProperty(Ws.prototype, "token", { get: function () { return this._token; }, enumerable: true, configurable: true }); Object.defineProperty(Ws.prototype, "sessionHash", { get: function () { return this._sessionHash; }, enumerable: true, configurable: true }); Object.defineProperty(Ws.prototype, "isAutoConnect", { get: function () { return this._autoConnect; }, enumerable: true, configurable: true }); Object.defineProperty(Ws.prototype, "isConnecting", { get: function () { return this._connecting; }, enumerable: true, configurable: true }); Object.defineProperty(Ws.prototype, "lastError", { get: function () { return this._lastError; }, enumerable: true, configurable: true }); Object.defineProperty(Ws.prototype, "retryTimeout", { /** * Exponential Timeout for Retries * * @see http://dthain.blogspot.de/2009/02/exponential-backoff-in-distributed.html */ get: function () { // R between 1 and 2 var R = Math.random() + 1; // Initial timeout (100ms) var T = 100; // Exponential Factor var F = 2; // Max timeout (4s) var M = 4000; // Current number of retries var N = this._retries; // Increase retries number this._retries++; return Math.min(R * T * Math.pow(F, N), M); }, enumerable: true, configurable: true }); return Ws; }(JsonRpcBase)); module.exports = Ws;