ripple-lib
Version:
Deprecated - consider migrating to xrpl.js: https://xrpl.org/xrpljs2-migration-guide.html
467 lines • 18.1 kB
JavaScript
"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