From 5f96dbf759bf62bd81fec6ae915c769589bba4f9 Mon Sep 17 00:00:00 2001 From: Victor Kirov Date: Mon, 28 Oct 2024 16:13:30 +0200 Subject: [PATCH] Replace the axios rate limiter lib with own --- api/esplora/esploraAPiProvider.ts | 18 +++++- package-lock.json | 12 ---- package.json | 1 - utils/axiosRateLimit.ts | 100 ++++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+), 15 deletions(-) create mode 100644 utils/axiosRateLimit.ts diff --git a/api/esplora/esploraAPiProvider.ts b/api/esplora/esploraAPiProvider.ts index 0dfd36ea..f6cdf2b5 100644 --- a/api/esplora/esploraAPiProvider.ts +++ b/api/esplora/esploraAPiProvider.ts @@ -1,5 +1,4 @@ import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; -import rateLimit from 'axios-rate-limit'; import axiosRetry from 'axios-retry'; import { XVERSE_BTC_BASE_URI_MAINNET, XVERSE_BTC_BASE_URI_SIGNET, XVERSE_BTC_BASE_URI_TESTNET } from '../../constant'; import { @@ -12,6 +11,7 @@ import { TransactionOutspend, UTXO, } from '../../types'; +import { AxiosRateLimit } from '../../utils/axiosRateLimit'; export interface EsploraApiProviderOptions { network: NetworkType; @@ -22,6 +22,8 @@ export interface EsploraApiProviderOptions { export class BitcoinEsploraApiProvider { bitcoinApi: AxiosInstance; + rateLimiter: AxiosRateLimit; + fallbackBitcoinApi?: AxiosInstance; _network: NetworkType; @@ -48,13 +50,25 @@ export class BitcoinEsploraApiProvider { const axiosConfig: AxiosRequestConfig = { baseURL }; this._network = network; - this.bitcoinApi = rateLimit(axios.create(axiosConfig), { + this.bitcoinApi = axios.create(axiosConfig); + + this.rateLimiter = new AxiosRateLimit(this.bitcoinApi, { maxRPS: 10, }); axiosRetry(this.bitcoinApi, { retries: 1, retryDelay: axiosRetry.exponentialDelay, + retryCondition: (error) => { + if ( + error?.code === 'ECONNABORTED' || + error?.response?.status === 429 || + (error?.response?.status ?? 0) >= 500 + ) { + return true; + } + return false; + }, }); if (fallbackUrl) { diff --git a/package-lock.json b/package-lock.json index 09a46bbf..3cd27ab2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,6 @@ "@zondax/ledger-stacks": "^1.0.4", "async-mutex": "^0.4.0", "axios": "1.7.7", - "axios-rate-limit": "1.4.0", "axios-retry": "4.5.0", "base64url": "^3.0.1", "bip32": "^4.0.0", @@ -2341,17 +2340,6 @@ "axios": ">= 0.17.0" } }, - "node_modules/axios-rate-limit": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/axios-rate-limit/-/axios-rate-limit-1.4.0.tgz", - "integrity": "sha512-uM5PbmSUdSle1I+59Av/wpLuNRobfatIR+FyylSoHcVHT20ohjflNnLMEHZQr7N2QVG/Wlt8jekIPhWwoKtpXQ==", - "dependencies": { - "axios": ">=0.18.0" - }, - "peerDependencies": { - "axios": "*" - } - }, "node_modules/axios-retry": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", diff --git a/package.json b/package.json index 20149b65..4c42f392 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "@zondax/ledger-stacks": "^1.0.4", "async-mutex": "^0.4.0", "axios": "1.7.7", - "axios-rate-limit": "1.4.0", "axios-retry": "4.5.0", "base64url": "^3.0.1", "bip32": "^4.0.0", diff --git a/utils/axiosRateLimit.ts b/utils/axiosRateLimit.ts new file mode 100644 index 00000000..f427717a --- /dev/null +++ b/utils/axiosRateLimit.ts @@ -0,0 +1,100 @@ +/** + * inspired by axios-rate-limit with a few adjustments and bug fixes + * https://github.com/aishek/axios-rate-limit + */ + +import { AxiosInstance, InternalAxiosRequestConfig } from 'axios'; + +type RateLimitRequestHandler = { + resolve: () => boolean; +}; + +type RateLimitOptions = + | { + maxRPS: number; + } + | { + maxRequests: number; + perMilliseconds: number; + }; + +function throwIfCancellationRequested(config: InternalAxiosRequestConfig) { + if (config.cancelToken) { + config.cancelToken.throwIfRequested(); + } +} + +export class AxiosRateLimit { + private queue: RateLimitRequestHandler[]; + + private timeslotRequests: number; + + private perMilliseconds: number; + + private maxRequests: number; + + constructor(axios: AxiosInstance, options: RateLimitOptions) { + this.queue = []; + this.timeslotRequests = 0; + + if ('maxRPS' in options) { + this.perMilliseconds = 1000; + this.maxRequests = options.maxRPS; + } else { + this.perMilliseconds = options.perMilliseconds; + this.maxRequests = options.maxRequests; + } + + function handleError(error: unknown) { + return Promise.reject(error); + } + + axios.interceptors.request.use(this.handleRequest, handleError); + } + + private handleRequest = (request: InternalAxiosRequestConfig) => { + return new Promise>((resolve, reject) => { + this.push({ + resolve: function () { + try { + throwIfCancellationRequested(request); + } catch (error) { + reject(error); + return false; + } + resolve(request); + return true; + }, + }); + }); + }; + + private push = (requestHandler: RateLimitRequestHandler) => { + this.queue.push(requestHandler); + this.shift(); + }; + + private onRequestTimerMet = () => { + this.timeslotRequests--; + this.shift(); + }; + + private shift = () => { + if (this.timeslotRequests >= this.maxRequests) return; + + const queued = this.queue.shift(); + if (!queued) return; + + const resolved = queued.resolve(); + + if (!resolved) { + this.shift(); // rejected request --> shift another request + return; + } + + this.timeslotRequests++; + setTimeout(this.onRequestTimerMet, this.perMilliseconds); + + this.timeslotRequests += 1; + }; +}