From 36fe897f4bfcfa3ba593626df857b054f6161dc4 Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Mon, 24 Nov 2025 19:33:37 -0500 Subject: [PATCH] improve(PriceClient): Gracefully fallback when price feed queries fail - `acrossApi.ts`: Return undefined for a specific token price query if unavailable rather than throwing if any individual query fails - `priceClient.ts`: When falling back to the next `priceFeed`, only query token addresses that we don't have a price for. This allows the `priceFeed. getPricesByAddress()` to "partially resolve", like in the case when querying the Across API via the `acrossApi` adapter. --- src/priceClient/adapters/acrossApi.ts | 13 +++++++++++-- src/priceClient/priceClient.ts | 14 ++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/priceClient/adapters/acrossApi.ts b/src/priceClient/adapters/acrossApi.ts index 094ac9631..db9c1b993 100644 --- a/src/priceClient/adapters/acrossApi.ts +++ b/src/priceClient/adapters/acrossApi.ts @@ -33,8 +33,17 @@ export class PriceFeed extends BaseHTTPAdapter implements PriceFeedAdapter { } // todo: Support bundled prices in the API endpoint. - getPricesByAddress(addresses: string[], currency = "usd"): Promise { - return Promise.all(addresses.map((address) => this.getPriceByAddress(address, currency))); + // Unlike other adapters, this adapter returns undefined prices when the Across API fails to resolve a price. + // This might need to change if the API allows bundles price requests. + async getPricesByAddress(addresses: string[], currency = "usd"): Promise<(TokenPrice | undefined)[]> { + const promises = await Promise.allSettled(addresses.map((address) => this.getPriceByAddress(address, currency))); + return promises.map((result, index) => { + const address = addresses[index]; + if (result.status === "rejected") { + return undefined; + } + return { address, price: result.value.price, timestamp: result.value.timestamp }; + }); } private validateResponse(response: unknown): response is AcrossPrice { diff --git a/src/priceClient/priceClient.ts b/src/priceClient/priceClient.ts index ae7b8fe3b..b32bd54fd 100644 --- a/src/priceClient/priceClient.ts +++ b/src/priceClient/priceClient.ts @@ -10,7 +10,7 @@ export type TokenPrice = CoinGeckoPrice; // Temporary inversion; CoinGecko shoul export interface PriceFeedAdapter { readonly name: string; getPriceByAddress(address: string, currency: string): Promise; - getPricesByAddress(addresses: string[], currency: string): Promise; + getPricesByAddress(addresses: string[], currency: string): Promise<(TokenPrice | undefined)[]>; } // It's convenient to map TokenPrice objects by their address, but consumers typically want an array @@ -129,20 +129,22 @@ export class PriceClient implements PriceFeedAdapter { const addrsToRequest = Object.entries(priceCache).map((entry: [string, TokenPrice]) => entry[1].address); for (const priceFeed of this.priceFeeds) { + // Only request prices for addresses that are not already in the price cache. + const _addrsToRequest = addrsToRequest.filter((address) => !priceCache[address.toLowerCase()]); this.logger.debug({ at: "PriceClient#updatePrices", message: `Looking up prices via ${priceFeed.name}.`, - tokens: addrsToRequest, + tokens: _addrsToRequest, }); try { - const feedPricesResponse = await priceFeed.getPricesByAddress(addrsToRequest, currency); - skipped = this.updateCache(priceCache, feedPricesResponse, addrsToRequest); + const feedPricesResponse = await priceFeed.getPricesByAddress(_addrsToRequest, currency); + skipped = this.updateCache(priceCache, feedPricesResponse, _addrsToRequest); if (skipped.length === 0) break; // All done } catch (err) { this.logger.debug({ at: "PriceClient#updatePrices", message: `Price lookup against ${priceFeed.name} failed (${err}).`, - tokens: addrsToRequest, + tokens: _addrsToRequest, }); // Failover to the next price feed... } @@ -159,7 +161,7 @@ export class PriceClient implements PriceFeedAdapter { } } - private updateCache(priceCache: PriceCache, prices: TokenPrice[], expected: string[]): string[] { + private updateCache(priceCache: PriceCache, prices: (TokenPrice | undefined)[], expected: string[]): string[] { const updated: TokenPrice[] = []; const skipped: { [token: string]: string } = {}; // Includes reason for skipping const now = msToS(Date.now());