diff --git a/package.json b/package.json index 77ee4a16..459008b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "btc-assets-api", - "version": "2.5.6", + "version": "2.5.7", "title": "Bitcoin/RGB++ Assets API", "description": "", "main": "index.js", diff --git a/src/env.ts b/src/env.ts index 99fa9151..067adaa1 100644 --- a/src/env.ts +++ b/src/env.ts @@ -225,6 +225,15 @@ const envSchema = z * used for fallback when the mempool.space API is not available. */ BITCOIN_ELECTRS_API_URL: z.string().optional(), + /** + * The IBitcoinClient methods to use Electrs as default data provider + * use electrs as default, mempool.space as fallback + */ + BITCOIN_METHODS_USE_ELECTRS_BY_DEFAULT: z + .string() + .default('') + .transform((value) => value.split(',')) + .pipe(z.string().array()), /** * Bitcoin data provider, support mempool and electrs * use mempool.space as default, electrs as fallback diff --git a/src/services/bitcoin/index.ts b/src/services/bitcoin/index.ts index 4ee0c326..5bf69f57 100644 --- a/src/services/bitcoin/index.ts +++ b/src/services/bitcoin/index.ts @@ -102,20 +102,32 @@ export default class BitcoinClient implements IBitcoinClient { method: K, ...args: MethodParameters ): Promise> { + const dataSource = { source: this.source, fallback: this.fallback }; + + const { env } = this.cradle; + if (env.BITCOIN_DATA_PROVIDER === 'mempool' && env.BITCOIN_METHODS_USE_ELECTRS_BY_DEFAULT.includes(method)) { + if (this.fallback) { + dataSource.source = this.fallback; + dataSource.fallback = this.source; + } else { + this.cradle.logger.warn('No fallback provider, skip using Electrs as default'); + } + } + try { this.cradle.logger.debug(`Calling ${method} with args: ${JSON.stringify(args)}`); - const result = await (this.source[method] as Function).apply(this.source, args); + const result = await (dataSource.source[method] as Function).apply(this.source, args); return result as MethodReturnType; } catch (err) { let calledError = err; this.cradle.logger.error(err); Sentry.captureException(err); - if (this.fallback) { + if (dataSource.fallback) { this.cradle.logger.warn( - `Fallback to ${this.fallback.constructor.name} due to error: ${(err as Error).message}`, + `Fallback to ${dataSource.fallback.constructor.name} due to error: ${(err as Error).message}`, ); try { - const result = await (this.fallback[method] as Function).apply(this.fallback, args); + const result = await (dataSource.fallback[method] as Function).apply(this.fallback, args); return result as MethodReturnType; } catch (fallbackError) { this.cradle.logger.error(fallbackError); diff --git a/test/services/bitcoin.test.ts b/test/services/bitcoin.test.ts index f93e836e..5277626d 100644 --- a/test/services/bitcoin.test.ts +++ b/test/services/bitcoin.test.ts @@ -23,6 +23,23 @@ describe('BitcoinClient', () => { } }); + test('BitcoinClient: Should be use Electrs as default data provider for methods', async () => { + const cradle = container.cradle; + if (cradle.env.BITCOIN_DATA_PROVIDER === 'mempool') { + cradle.env.BITCOIN_METHODS_USE_ELECTRS_BY_DEFAULT = ['getAddressTxs']; + bitcoin = new BitcoinClient(cradle); + expect(bitcoin['source'].constructor).toBe(MempoolClient); + expect(bitcoin['fallback']?.constructor).toBe(ElectrsClient); + + // @ts-expect-error just for test, so we don't need to check the return value + const mempoolFn = vi.spyOn(bitcoin['source']!, 'getAddressTxs').mockResolvedValue([{}]); + const electrsFn = vi.spyOn(bitcoin['fallback']!, 'getAddressTxs').mockResolvedValue([]); + expect(await bitcoin.getAddressTxs({ address: 'test' })).toEqual([]); + expect(mempoolFn).not.toHaveBeenCalled(); + expect(electrsFn).toHaveBeenCalled(); + } + }); + test('BitcoinClient: throw BitcoinClientError when source provider failed', async () => { bitcoin['fallback'] = undefined; vi.spyOn(bitcoin['source'], 'postTx').mockRejectedValue(new AxiosError('source provider error'));