@bithomp/xrpl-api
Version:
A Bithomp JavaScript/TypeScript library for interacting with the XRP Ledger
560 lines (559 loc) • 20.9 kB
JavaScript
"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;