morphit/node_modules/@beblurt/dblurt/lib/utils.js
Morphit Team 24240cef4c
Some checks failed
morphit-release / Build + publish release tarball (push) Has been cancelled
Reputation flags clearable across all four signals; names and avatars stop vanishing
2026-07-23 14:28:04 -07:00

292 lines
11 KiB
JavaScript

"use strict";
/**
* Misc utility functions.
* @author BeBlurt <https://beblurt.com/@beblurt>
* Adaptation from Johan Nordberg <code@johan-nordberg.com> Misc utility functions.
* @license
* Copyright (c) 2017 Johan Nordberg. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistribution of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistribution in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*
* You acknowledge that this software is not designed, licensed or intended for use
* in the design, construction, operation or maintenance of any military facility.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.retryingFetch = exports.copy = exports.iteratorStream = exports.sleep = exports.waitForEvent = exports.makeBitwiseFilter = exports.makeBitMaskFilter = exports.virtualOps = exports.operationOrders = void 0;
const stream_1 = require("stream");
const JSBI = require('jsbi');
exports.operationOrders = {
vote: 0,
comment: 1,
transfer: 2,
transfer_to_vesting: 3,
withdraw_vesting: 4,
account_create: 5,
account_update: 6,
witness_update: 7,
account_witness_vote: 8,
account_witness_proxy: 9,
custom: 10,
delete_comment: 11,
custom_json: 12,
comment_options: 13,
set_withdraw_vesting_route: 14,
claim_account: 15,
create_claimed_account: 16,
request_account_recovery: 17,
recover_account: 18,
change_recovery_account: 19,
escrow_transfer: 20,
escrow_dispute: 21,
escrow_release: 22,
escrow_approve: 23,
transfer_to_savings: 24,
transfer_from_savings: 25,
cancel_transfer_from_savings: 26,
custom_binary: 27,
decline_voting_rights: 28,
reset_account: 29,
set_reset_account: 30,
claim_reward_balance: 31,
delegate_vesting_shares: 32,
witness_set_properties: 33,
create_proposal: 34,
update_proposal_votes: 35,
remove_proposal: 36,
// virtual ops
author_reward: 37,
curation_reward: 38,
comment_reward: 39,
fill_vesting_withdraw: 40,
shutdown_witness: 41,
fill_transfer_from_savings: 42,
hardfork: 43,
comment_payout_update: 44,
return_vesting_delegation: 45,
comment_benefactor_reward: 46,
producer_reward: 47,
clear_null_account_balance: 48,
proposal_pay: 49,
sps_fund: 50,
fee_pay: 51
};
exports.virtualOps = {
// virtual ops
author_reward: 0,
curation_reward: 1,
comment_reward: 2,
fill_vesting_withdraw: 3,
shutdown_witness: 4,
fill_transfer_from_savings: 5,
hardfork: 6,
comment_payout_update: 7,
return_vesting_delegation: 8,
comment_benefactor_reward: 9,
producer_reward: 10,
clear_null_account_balance: 11,
proposal_pay: 12,
sps_fund: 13,
fee_pay: 14
};
const redFunction = ([low, high], allowedOperation) => {
if (allowedOperation < 64) {
return [
JSBI.bitwiseOr(low, JSBI.leftShift(JSBI.BigInt(1), JSBI.BigInt(allowedOperation))),
high
];
}
else {
return [
low,
JSBI.bitwiseOr(high, JSBI.leftShift(JSBI.BigInt(1), JSBI.BigInt(allowedOperation - 64)))
];
}
};
/**
* Make bitmask filter to be used with getAccountHistory call
* @param allowedOperations Array of operations index numbers
*/
const makeBitMaskFilter = (allowedOperations) => allowedOperations
.reduce(redFunction, [JSBI.BigInt(0), JSBI.BigInt(0)])
.map(value => JSBI.notEqual(value, JSBI.BigInt(0)) ? parseInt(value.toString(), 10) : null);
exports.makeBitMaskFilter = makeBitMaskFilter;
/**
* Make bitmask filter to be used with enum_virtual_ops call
* @param allowedOperations Array of operations index numbers
*/
const makeBitwiseFilter = (allowedOperations) => {
const [low, high] = allowedOperations.reduce(redFunction, [JSBI.BigInt(0), JSBI.BigInt(0)]);
const value = JSBI.bitwiseOr(low, high);
return JSBI.notEqual(value, JSBI.BigInt(0)) ? parseInt(value.toString(), 10) : 0;
};
exports.makeBitwiseFilter = makeBitwiseFilter;
// TODO: Add more errors that should trigger a failover
const timeoutErrors = ['timeout', 'ENOTFOUND', 'ECONNREFUSED', 'database lock', 'CERT_HAS_EXPIRED', 'EHOSTUNREACH'];
const getErrorCode = (error) => {
return error && typeof error.code === 'string' ? error.code : undefined;
};
const getErrorName = (error) => {
return error && typeof error.name === 'string' ? error.name : undefined;
};
const isAbortError = (error) => {
return getErrorName(error) === 'AbortError' || getErrorCode(error) === 'ABORT_ERR';
};
const isFailoverError = (error) => {
const code = getErrorCode(error);
return isAbortError(error) || timeoutErrors.some(fe => code ? code.includes(fe) : false);
};
const describeError = (error) => {
const code = getErrorCode(error);
if (code) {
return { label: 'code', value: code };
}
return { label: 'message', value: error && error.message };
};
const setErrorMessageIfPossible = (error, message) => {
try {
error.message = message;
}
catch (_error) {
// Some native errors, including DOMException in strict mode, expose an immutable message.
}
};
const getGlobalFetch = () => {
if (typeof fetch !== 'function') {
throw new Error('dblurt requires a runtime with native fetch support (Node.js >=18 or a modern browser)');
}
return fetch;
};
const createAbortSignal = (timeout) => {
if (!timeout || timeout <= 0 || typeof AbortController === 'undefined') {
return { cancel: () => undefined };
}
const controller = new AbortController();
const timer = setTimeout(() => { controller.abort(); }, timeout);
return {
cancel: () => { clearTimeout(timer); },
signal: controller.signal
};
};
/** Return a promise that will resove when a specific event is emitted. */
const waitForEvent = (emitter, eventName) => new Promise(resolve => {
emitter.once(eventName, resolve);
});
exports.waitForEvent = waitForEvent;
/** Sleep for N milliseconds. */
const sleep = (ms) => new Promise(resolve => { setTimeout(resolve, ms); });
exports.sleep = sleep;
/** Return a stream that emits iterator values. */
const iteratorStream = (iterator) => {
const stream = new stream_1.PassThrough({ objectMode: true });
const iterate = async () => {
for await (const item of iterator) {
if (!stream.write(item)) {
await (0, exports.waitForEvent)(stream, 'drain');
}
}
};
iterate()
.then(() => {
stream.end();
})
.catch(error => {
stream.emit('error', error);
stream.end();
});
return stream;
};
exports.iteratorStream = iteratorStream;
/** Return a deep copy of a JSON-serializable object */
const copy = (object) => JSON.parse(JSON.stringify(object));
exports.copy = copy;
/** Fetch API wrapper that retries until timeout is reached. */
const failover = (url, urls, currentAddress, consoleOnFailover) => {
const index = urls.indexOf(url);
const targetUrl = urls.length === index + 1 ? urls[0] : urls[index + 1];
// eslint-disable-next-line no-console
if (consoleOnFailover) {
console.log(`Switched Blurt RPC: ${targetUrl} (previous: ${currentAddress})`);
}
return targetUrl ? targetUrl : url;
};
const retryingFetch = async (currentAddress, allAddresses, opts, timeout, failoverThreshold, consoleOnFailover, backoff, fetchTimeout) => {
let start = Date.now();
let tries = 0;
let round = 0;
do {
try {
const attemptTimeout = fetchTimeout ? fetchTimeout(tries) : undefined;
const abort = createAbortSignal(attemptTimeout);
const fetchOptions = abort.signal ? { ...opts, signal: abort.signal } : opts;
let response;
try {
response = await getGlobalFetch()(currentAddress, fetchOptions);
}
finally {
abort.cancel();
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return { response: await response.json(), currentAddress };
}
catch (error) {
if (timeout !== 0 && Date.now() - start > timeout) {
if ((!error || !error.code) && Array.isArray(allAddresses)) {
// If error is empty or not code is present, it means rpc is down => switch
currentAddress = failover(currentAddress, allAddresses, currentAddress, consoleOnFailover);
}
else {
if (isFailoverError(error) &&
Array.isArray(allAddresses) &&
allAddresses.length > 1) {
if (round < failoverThreshold) {
start = Date.now();
tries = -1;
if (failoverThreshold > 0) {
round++;
}
currentAddress = failover(currentAddress, allAddresses, currentAddress, consoleOnFailover);
}
else {
const errorCode = getErrorCode(error);
setErrorMessageIfPossible(error, `[${errorCode || getErrorName(error) || (error && error.message)}] tried ${failoverThreshold} times with ${allAddresses.join(',')}`);
throw error;
}
}
else {
const describedError = describeError(error);
// eslint-disable-next-line no-console
console.error(`Didn't failover for error ${describedError.label}: [${describedError.value}]`);
throw error;
}
}
}
await (0, exports.sleep)(backoff(tries++));
}
} while (true);
};
exports.retryingFetch = retryingFetch;