"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Connection = void 0; const _ = __importStar(require("lodash")); const events_1 = require("events"); const url_1 = require("url"); const ws_1 = __importDefault(require("ws")); const rangeset_1 = __importDefault(require("./rangeset")); const errors_1 = require("./errors"); const backoff_1 = require("./backoff"); const INTENTIONAL_DISCONNECT_CODE = 4000; function createWebSocket(url, config) { const options = {}; if (config.proxy != null) { const parsedURL = url_1.parse(url); const parsedProxyURL = url_1.parse(config.proxy); const proxyOverrides = _.omitBy({ secureEndpoint: parsedURL.protocol === 'wss:', secureProxy: parsedProxyURL.protocol === 'https:', auth: config.proxyAuthorization, ca: config.trustedCertificates, key: config.key, passphrase: config.passphrase, cert: config.certificate }, (value) => value == null); const proxyOptions = Object.assign({}, parsedProxyURL, proxyOverrides); let HttpsProxyAgent; try { HttpsProxyAgent = require('https-proxy-agent'); } catch (error) { throw new Error('"proxy" option is not supported in the browser'); } options.agent = new HttpsProxyAgent(proxyOptions); } if (config.authorization != null) { const base64 = Buffer.from(config.authorization).toString('base64'); options.headers = { Authorization: `Basic ${base64}` }; } const optionsOverrides = _.omitBy({ ca: config.trustedCertificates, key: config.key, passphrase: config.passphrase, cert: config.certificate }, (value) => value == null); const websocketOptions = Object.assign({}, options, optionsOverrides); const websocket = new ws_1.default(url, null, websocketOptions); if (typeof websocket.setMaxListeners === 'function') { websocket.setMaxListeners(Infinity); } return websocket; } function websocketSendAsync(ws, message) { return new Promise((resolve, reject) => { ws.send(message, undefined, (error) => { if (error) { reject(new errors_1.DisconnectedError(error.message, error)); } else { resolve(); } }); }); } class LedgerHistory { constructor() { this.feeBase = null; this.feeRef = null; this.latestVersion = null; this.reserveBase = null; this.availableVersions = new rangeset_1.default(); } hasVersion(version) { return this.availableVersions.containsValue(version); } hasVersions(lowVersion, highVersion) { return this.availableVersions.containsRange(lowVersion, highVersion); } update(ledgerMessage) { this.feeBase = ledgerMessage.fee_base; this.feeRef = ledgerMessage.fee_ref; this.latestVersion = ledgerMessage.ledger_index; this.reserveBase = ledgerMessage.reserve_base; if (ledgerMessage.validated_ledgers) { this.availableVersions.reset(); this.availableVersions.parseAndAddRanges(ledgerMessage.validated_ledgers); } else { this.availableVersions.addValue(this.latestVersion); } } } class ConnectionManager { constructor() { this.promisesAwaitingConnection = []; } resolveAllAwaiting() { this.promisesAwaitingConnection.map(({ resolve }) => resolve()); this.promisesAwaitingConnection = []; } rejectAllAwaiting(error) { this.promisesAwaitingConnection.map(({ reject }) => reject(error)); this.promisesAwaitingConnection = []; } awaitConnection() { return new Promise((resolve, reject) => { this.promisesAwaitingConnection.push({ resolve, reject }); }); } } class RequestManager { constructor() { this.nextId = 0; this.promisesAwaitingResponse = []; } cancel(id) { const { timer } = this.promisesAwaitingResponse[id]; clearTimeout(timer); delete this.promisesAwaitingResponse[id]; } resolve(id, data) { const { timer, resolve } = this.promisesAwaitingResponse[id]; clearTimeout(timer); resolve(data); delete this.promisesAwaitingResponse[id]; } reject(id, error) { const { timer, reject } = this.promisesAwaitingResponse[id]; clearTimeout(timer); reject(error); delete this.promisesAwaitingResponse[id]; } rejectAll(error) { this.promisesAwaitingResponse.forEach((_, id) => { this.reject(id, error); }); } createRequest(data, timeout) { const newId = this.nextId++; const newData = JSON.stringify(Object.assign(Object.assign({}, data), { id: newId })); const timer = setTimeout(() => this.reject(newId, new errors_1.TimeoutError()), timeout); if (timer.unref) { timer.unref(); } const newPromise = new Promise((resolve, reject) => { this.promisesAwaitingResponse[newId] = { resolve, reject, timer }; }); return [newId, newData, newPromise]; } handleResponse(data) { if (!Number.isInteger(data.id) || data.id < 0) { throw new errors_1.ResponseFormatError('valid id not found in response', data); } if (!this.promisesAwaitingResponse[data.id]) { return; } if (data.status === 'error') { const error = new errors_1.RippledError(data.error_message || data.error, data); this.reject(data.id, error); return; } if (data.status !== 'success') { const error = new errors_1.ResponseFormatError(`unrecognized status: ${data.status}`, data); this.reject(data.id, error); return; } this.resolve(data.id, data.result); } } class Connection extends events_1.EventEmitter { constructor(url, options = {}) { super(); this._ws = null; this._reconnectTimeoutID = null; this._heartbeatIntervalID = null; this._retryConnectionBackoff = new backoff_1.ExponentialBackoff({ min: 100, max: 60 * 1000 }); this._trace = () => { }; this._ledger = new LedgerHistory(); this._requestManager = new RequestManager(); this._connectionManager = new ConnectionManager(); this._clearHeartbeatInterval = () => { clearInterval(this._heartbeatIntervalID); }; this._startHeartbeatInterval = () => { this._clearHeartbeatInterval(); this._heartbeatIntervalID = setInterval(() => this._heartbeat(), this._config.timeout); }; this._heartbeat = () => { return this.request({ command: 'ping' }).catch(() => { return this.reconnect().catch((error) => { this.emit('error', 'reconnect', error.message, error); }); }); }; this._onConnectionFailed = (errorOrCode) => { if (this._ws) { this._ws.removeAllListeners(); this._ws.on('error', () => { }); this._ws.close(); this._ws = null; } if (typeof errorOrCode === 'number') { this._connectionManager.rejectAllAwaiting(new errors_1.NotConnectedError(`Connection failed with code ${errorOrCode}.`, { code: errorOrCode })); } else if (errorOrCode && errorOrCode.message) { this._connectionManager.rejectAllAwaiting(new errors_1.NotConnectedError(errorOrCode.message, errorOrCode)); } else { this._connectionManager.rejectAllAwaiting(new errors_1.NotConnectedError('Connection failed.')); } }; this.setMaxListeners(Infinity); this._url = url; this._config = Object.assign({ timeout: 20 * 1000, connectionTimeout: 5 * 1000 }, options); if (typeof options.trace === 'function') { this._trace = options.trace; } else if (options.trace === true) { this._trace = console.log; } } _onMessage(message) { this._trace('receive', message); let data; try { data = JSON.parse(message); } catch (error) { this.emit('error', 'badMessage', error.message, message); return; } if (data.type == null && data.error) { this.emit('error', data.error, data.error_message, data); return; } if (data.type) { this.emit(data.type, data); } if (data.type === 'ledgerClosed') { this._ledger.update(data); } if (data.type === 'response') { try { this._requestManager.handleResponse(data); } catch (error) { this.emit('error', 'badMessage', error.message, message); } } } get _state() { return this._ws ? this._ws.readyState : ws_1.default.CLOSED; } get _shouldBeConnected() { return this._ws !== null; } _waitForReady() { return new Promise((resolve, reject) => { if (!this._shouldBeConnected) { reject(new errors_1.NotConnectedError()); } else if (this._state === ws_1.default.OPEN) { resolve(); } else { this.once('connected', () => resolve()); } }); } _subscribeToLedger() { return __awaiter(this, void 0, void 0, function* () { const data = yield this.request({ command: 'subscribe', streams: ['ledger'] }); if (_.isEmpty(data) || !data.ledger_index) { try { yield this.disconnect(); } catch (error) { } finally { throw new errors_1.RippledNotInitializedError('Rippled not initialized'); } } this._ledger.update(data); }); } isConnected() { return this._state === ws_1.default.OPEN; } connect() { if (this.isConnected()) { return Promise.resolve(); } if (this._state === ws_1.default.CONNECTING) { return this._connectionManager.awaitConnection(); } if (!this._url) { return Promise.reject(new errors_1.ConnectionError('Cannot connect because no server was specified')); } if (this._ws) { return Promise.reject(new errors_1.RippleError('Websocket connection never cleaned up.', { state: this._state })); } const connectionTimeoutID = setTimeout(() => { this._onConnectionFailed(new errors_1.ConnectionError(`Error: connect() timed out after ${this._config.connectionTimeout} ms. ` + `If your internet connection is working, the rippled server may be blocked or inaccessible. ` + `You can also try setting the 'connectionTimeout' option in the RippleAPI constructor.`)); }, this._config.connectionTimeout); this._ws = createWebSocket(this._url, this._config); this._ws.on('error', this._onConnectionFailed); this._ws.on('error', () => clearTimeout(connectionTimeoutID)); this._ws.on('close', this._onConnectionFailed); this._ws.on('close', () => clearTimeout(connectionTimeoutID)); this._ws.once('open', () => __awaiter(this, void 0, void 0, function* () { this._ws.removeAllListeners(); clearTimeout(connectionTimeoutID); this._ws.on('message', (message) => this._onMessage(message)); this._ws.on('error', (error) => this.emit('error', 'websocket', error.message, error)); this._ws.once('close', (code) => { this._clearHeartbeatInterval(); this._requestManager.rejectAll(new errors_1.DisconnectedError('websocket was closed')); this._ws.removeAllListeners(); this._ws = null; this.emit('disconnected', code); if (code !== INTENTIONAL_DISCONNECT_CODE) { const retryTimeout = this._retryConnectionBackoff.duration(); this._trace('reconnect', `Retrying connection in ${retryTimeout}ms.`); this.emit('reconnecting', this._retryConnectionBackoff.attempts); this._reconnectTimeoutID = setTimeout(() => { this.reconnect().catch((error) => { this.emit('error', 'reconnect', error.message, error); }); }, retryTimeout); } }); try { this._retryConnectionBackoff.reset(); yield this._subscribeToLedger(); this._startHeartbeatInterval(); this._connectionManager.resolveAllAwaiting(); this.emit('connected'); } catch (error) { this._connectionManager.rejectAllAwaiting(error); yield this.disconnect().catch(() => { }); } })); return this._connectionManager.awaitConnection(); } disconnect() { clearTimeout(this._reconnectTimeoutID); this._reconnectTimeoutID = null; if (this._state === ws_1.default.CLOSED || !this._ws) { return Promise.resolve(undefined); } return new Promise((resolve) => { this._ws.once('close', (code) => resolve(code)); if (this._state !== ws_1.default.CLOSING) { this._ws.close(INTENTIONAL_DISCONNECT_CODE); } }); } reconnect() { return __awaiter(this, void 0, void 0, function* () { this.emit('reconnect'); yield this.disconnect(); yield this.connect(); }); } getFeeBase() { return __awaiter(this, void 0, void 0, function* () { yield this._waitForReady(); return this._ledger.feeBase; }); } getFeeRef() { return __awaiter(this, void 0, void 0, function* () { yield this._waitForReady(); return this._ledger.feeRef; }); } getLedgerVersion() { return __awaiter(this, void 0, void 0, function* () { yield this._waitForReady(); return this._ledger.latestVersion; }); } getReserveBase() { return __awaiter(this, void 0, void 0, function* () { yield this._waitForReady(); return this._ledger.reserveBase; }); } hasLedgerVersions(lowLedgerVersion, highLedgerVersion) { return __awaiter(this, void 0, void 0, function* () { if (!highLedgerVersion) { return this.hasLedgerVersion(lowLedgerVersion); } yield this._waitForReady(); return this._ledger.hasVersions(lowLedgerVersion, highLedgerVersion); }); } hasLedgerVersion(ledgerVersion) { return __awaiter(this, void 0, void 0, function* () { yield this._waitForReady(); return this._ledger.hasVersion(ledgerVersion); }); } request(request, timeout) { return __awaiter(this, void 0, void 0, function* () { if (!this._shouldBeConnected) { throw new errors_1.NotConnectedError(); } const [id, message, responsePromise] = this._requestManager.createRequest(request, timeout || this._config.timeout); this._trace('send', message); websocketSendAsync(this._ws, message).catch((error) => { this._requestManager.reject(id, error); }); return responsePromise; }); } getUrl() { return this._url; } } exports.Connection = Connection; //# sourceMappingURL=connection.js.map