UNPKG

@bithomp/xrpl-api

Version:

A Bithomp JavaScript/TypeScript library for interacting with the XRP Ledger

560 lines (559 loc) 20.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (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 () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Connection = exports.DEFAULT_API_VERSION = void 0; const crypto_1 = __importDefault(require("crypto")); const events_1 = require("events"); const xrpl_1 = require("xrpl"); const ledger_1 = require("./models/ledger"); const common_1 = require("./common"); const utils_1 = require("./common/utils"); const XRPLConnection = __importStar(require("xrpl/dist/npm/client/connection")); const RECONNECT_TIMEOUT = 1000 * 5; const LEDGER_CLOSED_TIMEOUT = 1000 * 20; const SERVER_INFO_UPDATE_INTERVAL = 1000 * 60 * 5; const AVAILABLE_LEDGER_INDEX_WINDOW = 1000; exports.DEFAULT_API_VERSION = xrpl_1.RIPPLED_API_V1; class Connection extends events_1.EventEmitter { constructor(url, type, options = {}) { super(); this.types = []; this.latency = []; this.onlineSince = null; this.serverInfo = {}; this.shutdown = false; this.connectionTimer = null; this.shutdown = false; this.url = url; this.type = type; this.updateTypes(); this.client = null; this.logger = options.logger; this.timeout = options.timeout || LEDGER_CLOSED_TIMEOUT; this.connectionTimeout = options.connectionTimeout || RECONNECT_TIMEOUT; this.hash = crypto_1.default.createHash("sha256").update(url).digest("hex"); if (typeof options.networkID === "number") { this.networkID = options.networkID; } this.apiVersion = options.apiVersion || exports.DEFAULT_API_VERSION; this.serverInfoUpdating = false; this.serverInfo = null; this.streams = { ledger: 1, }; this.accounts = {}; this.streamsSubscribed = false; } async connect() { try { this.logger?.debug({ service: "Bithomp::XRPL::Connection", function: "connect", url: this.url, shutdown: this.shutdown, }); await this.removeClient(); this.client = new XRPLConnection.Connection(this.url, (0, common_1.removeUndefined)({ timeout: this.timeout, connectionTimeout: this.connectionTimeout })); this.setupEmitter(); await this.client.connect(); await this.updateServerInfo(); await this.subscribe(); } catch (err) { this.logger?.warn({ service: "Bithomp::XRPL::Connection", function: "connect", url: this.url, error: err?.message || err?.name || err, }); } this.connectionValidation(); } async disconnect() { this.shutdown = true; await this.unsubscribe(); await this.client?.disconnect(); delete this.client; clearTimeout(this.connectionTimer); } async request(request, options) { const result = await this._request(request, options); if (result?.error === "timeout") { const timeouts = this.latency.filter((info) => info.delta >= this.timeout).length; if (timeouts >= 3) { this.logger?.debug({ service: "Bithomp::XRPL::Connection", function: "request", url: this.url, error: `Too many timeouts (${timeouts}) in last ${this.latency.length} requests, reconnecting...`, }); this.reconnect(); } } return result; } async _request(request, options) { try { if (options?.skip_subscription_update !== true && (request.command === "subscribe" || request.command === "unsubscribe")) { return this.updateSubscriptions(request); } const waitTime = (0, utils_1.getTimestamp)() + RECONNECT_TIMEOUT; while (!this.client || !this.isConnected()) { await (0, utils_1.sleep)(100); if (this.shutdown) { return { error: "shutdownConnection", error_message: "Connection is shutdown.", status: "error" }; } if ((0, utils_1.getTimestamp)() > waitTime) { return { error: "notConnected", error_message: "Not connected.", status: "error" }; } } const startTimestamp = (0, utils_1.getTimestamp)(); if (this.apiVersion && !request.hasOwnProperty("api_version") && exports.DEFAULT_API_VERSION !== this.apiVersion) { request.api_version = this.apiVersion; } const response = await this.client.request(request); this.updateLatency((0, utils_1.getTimestamp)() - startTimestamp); this.connectionValidation(); return response; } catch (err) { this.updateLatency(err.name === "TimeoutError" ? this.timeout : this.connectionTimeout); this.logger?.debug({ service: "Bithomp::XRPL::Connection", function: "request", url: this.url, error: err?.message || err?.name || err, }); if (err.name === "TimeoutError") { return { error: "timeout", error_message: "Request timeout.", status: "error" }; } else if (err.data) { return err.data; } else { return { error: err?.message || err?.name || err, status: "error" }; } } } async submit(transaction) { try { return await this.request({ command: "submit", tx_blob: transaction }); } catch (err) { this.logger?.debug({ service: "Bithomp::XRPL::Connection", function: "submit", url: this.url, error: err?.message || err?.name || err, }); if (err.data) { return err.data; } else { return { error: err?.message || err?.name || err }; } } } isConnected() { if (!this.client) { return false; } return this.client.isConnected(); } getOnlinePeriodMs() { if (this.isConnected()) { return this.onlineSince ? (0, utils_1.getTimestamp)() - this.onlineSince : 0; } return null; } getLatencyMs() { return this.latency.map((info) => info.delta).reduce((a, b) => a + b, 0) / this.latency.length || 0; } getNetworkID() { if (typeof this.serverInfo?.network_id === "number") { return this.serverInfo.network_id; } return this.networkID; } isLedgerIndexAvailable(ledgerIndex) { if (typeof ledgerIndex !== "number") { return true; } if (!this.serverInfo?.complete_ledgers) { return true; } const completeLedgers = this.serverInfo.complete_ledgers.split("-"); if (completeLedgers.length !== 2) { return true; } completeLedgers[0] = parseInt(completeLedgers[0], 10); completeLedgers[1] = parseInt(completeLedgers[1], 10); if (ledgerIndex < completeLedgers[0] - AVAILABLE_LEDGER_INDEX_WINDOW || ledgerIndex > completeLedgers[1] + AVAILABLE_LEDGER_INDEX_WINDOW) { return false; } return true; } updateLatency(delta) { this.latency.push({ timestamp: new Date(), delta, }); this.latency.splice(0, this.latency.length - 10); } async reconnect() { this.logger?.debug({ service: "Bithomp::XRPL::Connection", function: "reconnect", url: this.url, shutdown: this.shutdown, }); if (!this.shutdown) { this.emit("reconnect"); try { await this.removeClient(); await (0, utils_1.sleep)(RECONNECT_TIMEOUT); this.updateTypes(); this.serverInfoUpdating = false; await this.connect(); } catch (e) { this.logger?.warn({ service: "Bithomp::XRPL::Connection", function: "reconnect", url: this.url, error: e.message, }); } this.connectionValidation(); } } async removeClient() { try { if (this.client) { await this.client.disconnect(); this.client.removeAllListeners(); this.client = undefined; } } catch (_err) { } } setupEmitter() { if (!this.client) { return; } this.client.on("connected", () => { this.logger?.debug({ service: "Bithomp::XRPL::Connection", emit: "connected", url: this.url, }); this.emit("connected"); this.onlineSince = (0, utils_1.getTimestamp)(); }); this.client.on("disconnected", (code) => { this.logger?.debug({ service: "Bithomp::XRPL::Connection", emit: "disconnected", code, url: this.url, }); this.onlineSince = 0; this.serverInfo = null; this.streamsSubscribed = false; this.emit("disconnected", code); }); this.client.on("error", (source, message, error) => { try { this.logger?.error({ service: "Bithomp::XRPL::Connection", emit: "error", source, url: this.url, error: message || error?.name || error, }); this.emit("error", source, message, error); } catch (err) { this.logger?.warn({ service: "Bithomp::XRPL::Connection", emit: "error", url: this.url, error: err?.message || err?.name || err, }); } this.connectionValidation(); }); this.client.on("ledgerClosed", (ledgerStream) => { this.onLedgerClosed(ledgerStream); this.emit("ledgerClosed", ledgerStream); }); this.client.on("transaction", (transactionStream) => { this.emit("transaction", transactionStream); }); this.client.on("validationReceived", (validation) => { this.emit("validationReceived", validation); }); this.client.on("manifestReceived", (manifest) => { this.emit("manifestReceived", manifest); }); this.client.on("peerStatusChange", (status) => { this.emit("peerStatusChange", status); }); this.client.on("consensusPhase", (consensus) => { this.emit("consensusPhase", consensus); }); this.client.on("path_find", (path) => { this.emit("path_find", path); }); } updateTypes() { if (typeof this.type === "string") { this.types = this.type.split(",").map((v) => v.trim()); } else { this.types = []; } } async updateSubscriptions(request) { if (request.command === "subscribe") { const addStreams = []; const addAccounts = []; if (request.streams) { for (const stream of request.streams) { if (this.streams[stream] === undefined) { this.streams[stream] = 1; addStreams.push(stream); } else if (stream !== "ledger") { this.streams[stream]++; } } } if (request.accounts) { for (const account of request.accounts) { if (this.accounts[account] === undefined) { this.accounts[account] = 1; addAccounts.push(account); } else { this.accounts[account]++; } } } if (addStreams.length > 0 || addAccounts.length > 0) { return await this.subscribe(addStreams, addAccounts); } } else if (request.command === "unsubscribe") { const removeStreams = []; const removeAccounts = []; if (request.streams) { for (const stream of request.streams) { if (this.streams[stream] === undefined) { continue; } if (stream !== "ledger") { this.streams[stream]--; } if (this.streams[stream] === 0) { delete this.streams[stream]; removeStreams.push(stream); } } } if (request.accounts) { for (const account of request.accounts) { if (this.accounts[account] === undefined) { continue; } this.accounts[account]--; if (this.accounts[account] === 0) { delete this.accounts[account]; removeAccounts.push(account); } } } if (removeStreams.length > 0 || removeAccounts.length > 0) { return await this.unsubscribe(removeStreams, removeAccounts); } } return { status: "success" }; } async subscribe(streams, accounts) { if (this.shutdown) { return { error: "shutdownConnection", error_message: "Connection is shutdown.", status: "error" }; } if (this.streamsSubscribed === true && streams === undefined && accounts === undefined) { return { status: "success" }; } streams = streams || Object.keys(this.streams); accounts = accounts || Object.keys(this.accounts); const request = { command: "subscribe" }; if (streams.length > 0) { request.streams = streams; } if (accounts.length > 0) { request.accounts = accounts; } const result = await this.request(request, { skip_subscription_update: true }); if (result.result) { this.streamsSubscribed = true; } return result; } async unsubscribe(streams, accounts) { if (streams === undefined && accounts === undefined) { this.streamsSubscribed = false; } streams = streams || Object.keys(this.streams); accounts = accounts || Object.keys(this.accounts); const request = { command: "unsubscribe" }; if (streams.length > 0) { request.streams = streams; } if (accounts.length > 0) { request.accounts = accounts; } return await this.request(request, { skip_subscription_update: true }); } onLedgerClosed(ledgerStream) { const time = (0, utils_1.getTimestamp)(); const ledgerTime = (0, ledger_1.ledgerTimeToTimestamp)(ledgerStream.ledger_time); if (ledgerTime < time) { this.updateLatency(time - ledgerTime); } if (this.serverInfo) { this.serverInfo.complete_ledgers = ledgerStream.validated_ledgers; if (this.serverInfo.validated_ledger) { this.serverInfo.validated_ledger.age = Math.round((time - ledgerTime) / 1000); this.serverInfo.validated_ledger.seq = ledgerStream.ledger_index; this.serverInfo.validated_ledger.hash = ledgerStream.ledger_hash; this.serverInfo.validated_ledger.base_fee_xrp = (0, common_1.dropsToXrp)(ledgerStream.fee_base); this.serverInfo.validated_ledger.reserve_base_xrp = (0, common_1.dropsToXrp)(ledgerStream.reserve_base); this.serverInfo.validated_ledger.reserve_inc_xrp = (0, common_1.dropsToXrp)(ledgerStream.reserve_inc); } const serverInfoTime = new Date(this.serverInfo.time).getTime(); if (serverInfoTime + SERVER_INFO_UPDATE_INTERVAL < time) { this.updateServerInfo(); } } else { this.updateServerInfo(); } this.connectionValidation(); } async updateServerInfo() { if (this.serverInfoUpdating || this.shutdown) { return; } this.serverInfoUpdating = true; try { const serverInfo = await this.request({ command: "server_info" }); if (serverInfo?.result?.info) { this.serverInfo = serverInfo.result.info; if (typeof this.serverInfo?.clio_version === "string") { if (!this.types.includes("clio")) { this.types.push("clio"); } } else { const index = this.types.indexOf("clio"); if (index !== -1) { this.types.splice(index, 1); } } } } catch (_err) { } this.serverInfoUpdating = false; } connectionValidation() { this.logger?.debug({ service: "Bithomp::XRPL::Connection", function: "connectionValidation", url: this.url, shutdown: this.shutdown, }); if (this.connectionTimer !== null) { clearTimeout(this.connectionTimer); this.connectionTimer = null; } if (!this.shutdown) { if (this.streamsSubscribed === false) { this.subscribe(); } if (this.serverInfo === null) { this.updateServerInfo(); } this.connectionTimer = setTimeout(() => { this.connectionValidationTimeout(); }, LEDGER_CLOSED_TIMEOUT); } else { this.client?.disconnect(); } } async connectionValidationTimeout() { this.logger?.debug({ service: "Bithomp::XRPL::Connection", function: "connectionValidationTimeout", url: this.url, timeout: LEDGER_CLOSED_TIMEOUT, shutdown: this.shutdown, }); this.connectionTimer = null; this.updateLatency(LEDGER_CLOSED_TIMEOUT); try { await this.reconnect(); } catch (e) { this.logger?.warn({ service: "Bithomp::XRPL::Connection", function: "connectionValidationTimeout", url: this.url, error: e.message, }); this.connectionValidation(); } } } exports.Connection = Connection;