Some checks failed
morphit-release / Build + publish release tarball (push) Has been cancelled
292 lines
11 KiB
JavaScript
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;
|