Source: dock-api-retry.js

import { createSubmittable } from '@polkadot/api/submittable';
import { SubmittableResult } from '@polkadot/api/cjs/submittable/Result';
import { filterEvents } from '@polkadot/api/util';
import { retry } from './utils/async';
import {
  ensureExtrinsicSucceeded,
  findExtrinsicBlock,
} from './utils/extrinsic';
import { BlocksProvider } from './utils/block';

/** Block time in ms for the standard build configuration. */
export const STANDARD_BLOCK_TIME_MS = 3e3;
/** Block time in ms for the fastblock build configuration. */
export const FASTBLOCK_TIME_MS = 5e2;

/**
 * @typedef {object} RetryConfig
 * @prop {number} finalizedTimeoutBlocks - Amount of blocks to wait for the extrinsic to be finalized before retrying.
 * @prop {number} inBlockTimeoutBlocks - Amount of blocks to wait for the extrinsic to be included in the block before retrying.
 * @prop {number} retryDelayBlocks - Amount of blocks to wait before a retry attempt.
 * @prop {number} BLOCK_TIME_MS - Block time in ms.
 * @prop {number} maxAttempts - Max retry attempts (doesn't include initial request).
 * @prop {number} fetchGapBlocks - Amount of blocks to be fetched in addition to the strict amount of blocks. It will cover timing for fetching the block data.
 */

export const STANDARD_CONFIG = {
  /** Amount of blocks to wait for the extrinsic to be finalized before retrying. */
  finalizedTimeoutBlocks: 5,
  /** Amount of blocks to wait for the extrinsic to be included in the block before retrying. */
  inBlockTimeoutBlocks: 3,
  /** Amount of blocks to wait before a retry attempt. */
  retryDelayBlocks: 1,
  /** Block time in ms. */
  blockTimeMs: STANDARD_BLOCK_TIME_MS,
  /** Max retry attempts (doesn't include initial request). */
  maxAttempts: 2,
  /** Amount of blocks to be fetched in addition to the strict amount of blocks. It will cover timing for fetching the block data. */
  fetchGapBlocks: 1,
};

export const FASTBLOCK_CONFIG = {
  /** Amount of blocks to wait for the extrinsic to be finalized before retrying. */
  finalizedTimeoutBlocks: 8,
  /** Amount of blocks to wait for the extrinsic to be included in the block before retrying. */
  inBlockTimeoutBlocks: 4,
  /** Amount of blocks to wait before a retry attempt. */
  retryDelayBlocks: 6,
  /** Block time in ms. */
  blockTimeMs: FASTBLOCK_TIME_MS,
  /** Max retry attempts (doesn't include initial request). */
  maxAttempts: 3,
  /** Amount of blocks to be fetched in addition to the strict amount of blocks. It will cover timing for fetching the block data. */
  fetchGapBlocks: 5,
};

/**
 * Properties that won't be patched/visited during the patching.
 */
const BlacklistedProperties = new Set([
  'meta',
  'registry',
  'toJSON',
  'is',
  'creator',
  'hash',
  'key',
  'keyPrefix',
]);

/**
 * Recursively patches supplied object property and all underlying objects, so all functions will attempt to retry 2 times and
 * will throw an error if there's no result within the `8 seconds` timeout.
 *
 * @param {*} obj
 * @param {*} prop
 * @param {string[]} [path=[]]
 */
const wrapFnWithRetries = (obj, prop, path = []) => {
  const value = obj[prop];
  if (
    BlacklistedProperties.has(prop)
    || !value
    || (typeof value !== 'object' && typeof value !== 'function')
  ) {
    return;
  }

  try {
    let newValue;
    if (typeof value !== 'function') {
      newValue = Object.create(Object.getPrototypeOf(value));
    } else {
      newValue = async function with8SecsTimeoutAnd2Retries(...args) {
        const wrappedFn = () => value.apply(this, args);
        wrappedFn.toString = () => value.toString();

        return await retry(wrappedFn, 8e3, {
          maxAttempts: 2,
          delay: 5e2,
          onTimeoutExceeded: (retrySym) => {
            console.error(`\`${path.concat('.')}\` exceeded timeout`);

            return retrySym;
          },
        });
      };

      Object.setPrototypeOf(newValue, Object.getPrototypeOf(value));
    }

    for (const key of Object.keys(value)) {
      newValue[key] = value[key];
      wrapFnWithRetries(newValue, key, path.concat(key));
    }

    // eslint-disable-next-line no-param-reassign
    delete obj[prop];
    Object.defineProperty(obj, prop, {
      value: newValue,
    });
  } catch (err) {
    console.error(
      `Failed to wrap the prop \`${prop}\` of \`${obj}\`: \`${
        err.message || err
      }\``,
    );
  }
};

/**
 * Patches the query API methods, so they will throw an error if there's no result within the `8 seconds` timeout after `2` retries.
 *
 * @param {*} queryApi
 */
export const patchQueryApi = (queryApi) => {
  const exclude = new Set(['substrate.code']);

  for (const modName of Object.keys(queryApi)) {
    const mod = queryApi[modName];

    for (const method of Object.keys(mod)) {
      const path = `${modName}.${method}`;

      if (!exclude.has(path)) {
        wrapFnWithRetries(mod, method, [modName, method]);
      }
    }
  }
};

/**
 * Helper function to send with retries a transaction that has already been signed.
 * @param {DockAPI} dock
 * @param {*} extrinsic - Extrinsic to send
 * @param {boolean} [waitForFinalization=true] - If true, waits for extrinsic's block to be finalized,
 * @param {RetryConfig} retryConfig
 * else only wait to be included in the block.
 * @returns {Promise<SubmittableResult>}
 */
/* eslint-disable sonarjs/cognitive-complexity */
export async function sendWithRetries(
  dock,
  extrinsic,
  waitForFinalization = true,
  config,
) {
  const { api } = dock;

  const extrTimeoutBlocks = waitForFinalization
    ? config.finalizedTimeoutBlocks
    : config.inBlockTimeoutBlocks;
  const blocksProvider = new BlocksProvider({
    api,
    finalized: waitForFinalization,
  });

  let sent;
  const startTimestamp = +new Date();

  const errorRegExp = /Transaction is (temporarily banned|outdated)|The operation was aborted/;
  const onError = async (err, retrySym) => {
    sent.unsubscribe();

    if (!errorRegExp.test(err?.message || '')) {
      throw err;
    }

    const txHash = extrinsic.hash;

    let block;
    try {
      const lastBlockNumber = (await blocksProvider.lastNumber()).toNumber();
      const requestAmount = config.fetchGapBlocks
        + (((+new Date() - startTimestamp) / config.blockTimeMs) | 0); // eslint-disable-line no-bitwise

      const blockNumbersToCheck = Array.from(
        { length: Math.min(lastBlockNumber + 1, requestAmount) },
        (_, idx) => lastBlockNumber - idx,
      );

      block = await findExtrinsicBlock(
        blocksProvider,
        blockNumbersToCheck,
        txHash,
      );
    } catch (txErr) {
      console.error(`Failed to find extrinsic's block: \`${txErr}\``);
    }

    if (block != null) {
      const blockHash = block.block.header.hash;

      const status = api.createType(
        'ExtrinsicStatus',
        waitForFinalization
          ? {
            finalized: blockHash,
          }
          : { inBlock: blockHash },
      );
      const filtered = filterEvents(txHash, block, block.events, status);

      ensureExtrinsicSucceeded(api, filtered.events, status);

      return new SubmittableResult({
        ...filtered,
        status,
        txHash,
      });
    } else {
      console.error(
        `Transaction \`${txHash}\` is not yet ${
          waitForFinalization ? 'finalized' : 'included in the block'
        }`,
      );
    }

    return retrySym;
  };

  const sendExtrinsic = async () => {
    sent = dock.sendNoRetries(extrinsic, waitForFinalization);
    return await sent;
  };

  const onTimeoutExceeded = (retrySym) => {
    sent.unsubscribe();
    // eslint-disable-next-line no-underscore-dangle
    const Sub = createSubmittable(api._type, api._rx, api._decorateMethod);
    // eslint-disable-next-line no-param-reassign
    extrinsic = Sub(extrinsic.toU8a());
    console.error(`Timeout exceeded for the extrinsic \`${extrinsic.hash}\``);

    return retrySym;
  };

  const timeout = config.blockTimeMs * extrTimeoutBlocks;

  return await retry(sendExtrinsic, 1e3 + timeout, {
    maxAttempts: config.maxAttempts,
    delay: config.blockTimeMs * config.retryDelayBlocks,
    onTimeoutExceeded,
    onError,
  });
}
/* eslint-enable sonarjs/cognitive-complexity */