diff --git a/packages/money-account-balance-service/CHANGELOG.md b/packages/money-account-balance-service/CHANGELOG.md index 4d1aacfbc8..3ff8423098 100644 --- a/packages/money-account-balance-service/CHANGELOG.md +++ b/packages/money-account-balance-service/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add optional `trace` constructor option to `MoneyAccountBalanceService` for tracing network requests (RPC calls and the Veda APY API fetch). Tracing is best-effort and does not affect query results if it fails. ([#9434](https://github.com/MetaMask/core/pull/9434)) + ### Changed - Bump `@metamask/messenger` from `^1.2.0` to `^2.0.0` ([#9392](https://github.com/MetaMask/core/pull/9392)) diff --git a/packages/money-account-balance-service/src/index.ts b/packages/money-account-balance-service/src/index.ts index c58553257d..1b25bbd773 100644 --- a/packages/money-account-balance-service/src/index.ts +++ b/packages/money-account-balance-service/src/index.ts @@ -3,6 +3,9 @@ export type { MoneyAccountBalanceServiceActions, MoneyAccountBalanceServiceEvents, MoneyAccountBalanceServiceMessenger, + MoneyAccountBalanceServiceOptions, + MoneyAccountBalanceServiceTraceCallback, + MoneyAccountBalanceServiceTraceRequest, } from './money-account-balance-service'; export type { MoneyAccountBalanceServiceGetMoneyAccountBalanceAction, diff --git a/packages/money-account-balance-service/src/money-account-balance-service.test.ts b/packages/money-account-balance-service/src/money-account-balance-service.test.ts index 9c4d0fb6fb..c2d9897d2e 100644 --- a/packages/money-account-balance-service/src/money-account-balance-service.test.ts +++ b/packages/money-account-balance-service/src/money-account-balance-service.test.ts @@ -1,6 +1,11 @@ import { Contract } from '@ethersproject/contracts'; import { Web3Provider } from '@ethersproject/providers'; import { DEFAULT_MAX_RETRIES, HttpError } from '@metamask/controller-utils'; +import type { + TraceCallback, + TraceContext, + TraceRequest, +} from '@metamask/controller-utils'; import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger'; import type { MockAnyNamespace, @@ -59,6 +64,11 @@ const MOCK_VAULT_CONFIG = { chainId: '0xa4b1' as const, }; +const MOCK_VAULT_CONFIG_WITH_UNDERLYING_TOKEN = { + ...MOCK_VAULT_CONFIG, + underlyingToken: MOCK_UNDERLYING_TOKEN_ADDRESS, +}; + const MOCK_NETWORK_CONFIG = { chainId: '0xa4b1' as const, rpcEndpoints: [ @@ -389,6 +399,55 @@ function mockMoneyAccountBalanceMulticall({ return aggregate3Mock; } +function createTraceCallback(): jest.MockedFunction { + return jest + .fn() + .mockImplementation( + async ( + _request: TraceRequest, + fn?: (context?: TraceContext) => ReturnType, + ): Promise => { + if (!fn) { + return undefined as ReturnType; + } + return await Promise.resolve(fn()); + }, + ) as jest.MockedFunction; +} + +function expectTraceRequest( + traceCallback: jest.MockedFunction, + { + errorName, + name, + operation, + success = true, + tokenAddress, + }: { + errorName?: string; + name: string; + operation: string; + success?: boolean; + tokenAddress?: string; + }, +): void { + const traceRequest = traceCallback.mock.calls.find( + ([request]) => request.name === name, + )?.[0]; + + expect(traceRequest).toStrictEqual({ + name, + startTime: expect.any(Number), + data: { + chainId: MOCK_VAULT_CONFIG.chainId, + ...(errorName ? { errorName } : {}), + operation, + success, + ...(tokenAddress ? { tokenAddress } : {}), + }, + }); +} + // ============================================================ // Tests // ============================================================ @@ -697,6 +756,283 @@ describe('MoneyAccountBalanceService', () => { }); }); + // ---------------------------------------------------------- + // tracing + // ---------------------------------------------------------- + + describe('tracing', () => { + it('traces getMusdBalance ERC-20 balance RPC on cache miss', async () => { + mockErc20BalanceOf('5000000'); + const trace = createTraceCallback(); + const { service } = createService({ + rffcFlags: { + [VAULT_CONFIG_FEATURE_FLAG_KEY]: + MOCK_VAULT_CONFIG_WITH_UNDERLYING_TOKEN, + }, + options: { trace }, + }); + + await service.getMusdBalance(MOCK_ACCOUNT_ADDRESS); + + expect(trace).toHaveBeenCalledTimes(1); + expectTraceRequest(trace, { + name: 'Get Money Account ERC20 Balance RPC', + operation: 'balanceOf', + tokenAddress: MOCK_UNDERLYING_TOKEN_ADDRESS, + }); + }); + + it('traces getMoneyAccountBalance Multicall3 RPC on cache miss', async () => { + mockMoneyAccountBalanceMulticall({ + musdBalance: '5000000', + vmusdValueInMusd: '2200000', + }); + const trace = createTraceCallback(); + const { service } = createService({ + rffcFlags: { + [VAULT_CONFIG_FEATURE_FLAG_KEY]: + MOCK_VAULT_CONFIG_WITH_UNDERLYING_TOKEN, + }, + options: { trace }, + }); + + await service.getMoneyAccountBalance(MOCK_ACCOUNT_ADDRESS); + + expect(trace).toHaveBeenCalledTimes(1); + expectTraceRequest(trace, { + name: 'Get Money Account Balance RPC', + operation: 'aggregate3', + }); + }); + + it('traces getVmusdBalance ERC-20 balance RPC on cache miss', async () => { + mockErc20BalanceOf('3000000'); + const trace = createTraceCallback(); + const { service } = createService({ options: { trace } }); + + await service.getVmusdBalance(MOCK_ACCOUNT_ADDRESS); + + expect(trace).toHaveBeenCalledTimes(1); + expectTraceRequest(trace, { + name: 'Get Money Account ERC20 Balance RPC', + operation: 'balanceOf', + tokenAddress: MOCK_VAULT_ADDRESS, + }); + }); + + it('traces getExchangeRate Accountant RPC on cache miss', async () => { + mockAccountantGetRate('1050000'); + const trace = createTraceCallback(); + const { service } = createService({ options: { trace } }); + + await service.getExchangeRate(); + + expect(trace).toHaveBeenCalledTimes(1); + expectTraceRequest(trace, { + name: 'Get Money Account Exchange Rate RPC', + operation: 'getRate', + }); + }); + + it('traces getMusdEquivalentValue Lens RPC on cache miss', async () => { + mockLensBalanceOfInAssets('1000000'); + const trace = createTraceCallback(); + const { service } = createService({ options: { trace } }); + + await service.getMusdEquivalentValue(MOCK_ACCOUNT_ADDRESS); + + expect(trace).toHaveBeenCalledTimes(1); + expectTraceRequest(trace, { + name: 'Get Money Account mUSD Equivalent Value RPC', + operation: 'balanceOfInAssets', + }); + }); + + it('traces getVaultApy Veda API fetch on cache miss', async () => { + nock('https://api.sevenseas.capital') + .get(`/performance/arbitrum/${MOCK_VAULT_ADDRESS}`) + .reply(200, MOCK_VAULT_APY_RAW_RESPONSE); + const trace = createTraceCallback(); + const { service } = createService({ options: { trace } }); + + await service.getVaultApy(); + + expect(trace).toHaveBeenCalledTimes(1); + expectTraceRequest(trace, { + name: 'Get Money Account Vault APY API', + operation: 'fetchVaultApy', + }); + }); + + it('traces fallback underlying token RPC when configured token is absent', async () => { + mockAccountantBase(); + mockErc20BalanceOf('5000000'); + const trace = createTraceCallback(); + const { service } = createService({ options: { trace } }); + + await service.getMusdBalance(MOCK_ACCOUNT_ADDRESS); + + expect(trace).toHaveBeenCalledTimes(2); + expectTraceRequest(trace, { + name: 'Get Money Account Underlying Token RPC', + operation: 'base', + }); + expectTraceRequest(trace, { + name: 'Get Money Account ERC20 Balance RPC', + operation: 'balanceOf', + tokenAddress: MOCK_UNDERLYING_TOKEN_ADDRESS, + }); + }); + + it('does not trace a cached query result', async () => { + mockAccountantGetRate('1050000'); + const trace = createTraceCallback(); + const { service } = createService({ options: { trace } }); + + await service.getExchangeRate(); + await service.getExchangeRate(); + + expect(trace).toHaveBeenCalledTimes(1); + expectTraceRequest(trace, { + name: 'Get Money Account Exchange Rate RPC', + operation: 'getRate', + }); + }); + + it('does not fail or refetch when the trace callback rejects', async () => { + const mockGetRate = jest + .fn() + .mockResolvedValue({ toString: () => '1050000' }); + MockContract.mockImplementation( + () => ({ getRate: mockGetRate }) as unknown as Contract, + ); + const trace = jest + .fn() + .mockRejectedValue( + new Error('trace boom'), + ) as jest.MockedFunction; + const { service } = createService({ options: { trace } }); + + expect(await service.getExchangeRate()).toStrictEqual({ + rate: '1050000', + }); + expect(await service.getExchangeRate()).toStrictEqual({ + rate: '1050000', + }); + + expect(mockGetRate).toHaveBeenCalledTimes(1); + expect(trace).toHaveBeenCalledTimes(1); + }); + + it('does not fail when the trace callback returns undefined', async () => { + mockAccountantGetRate('1050000'); + const trace = jest + .fn() + .mockReturnValue(undefined) as jest.MockedFunction; + const { service } = createService({ options: { trace } }); + + expect(await service.getExchangeRate()).toStrictEqual({ + rate: '1050000', + }); + + expect(trace).toHaveBeenCalledTimes(1); + expectTraceRequest(trace, { + name: 'Get Money Account Exchange Rate RPC', + operation: 'getRate', + }); + }); + + it('does not fail when the trace callback throws synchronously', async () => { + const mockGetRate = jest + .fn() + .mockResolvedValue({ toString: () => '1050000' }); + MockContract.mockImplementation( + () => ({ getRate: mockGetRate }) as unknown as Contract, + ); + const trace = jest.fn().mockImplementation(() => { + throw new Error('trace boom'); + }) as jest.MockedFunction; + const { service } = createService({ options: { trace } }); + + expect(await service.getExchangeRate()).toStrictEqual({ + rate: '1050000', + }); + expect(await service.getExchangeRate()).toStrictEqual({ + rate: '1050000', + }); + + expect(mockGetRate).toHaveBeenCalledTimes(1); + expect(trace).toHaveBeenCalledTimes(1); + }); + + it('propagates aggregate3 rejections after emitting a failed trace', async () => { + const aggregate3 = jest + .fn() + .mockRejectedValue(new Error('execution reverted')); + mockMoneyAccountBalanceMulticall({ aggregate3 }); + const trace = createTraceCallback(); + const { service } = createService({ + rffcFlags: { + [VAULT_CONFIG_FEATURE_FLAG_KEY]: + MOCK_VAULT_CONFIG_WITH_UNDERLYING_TOKEN, + }, + options: { trace }, + }); + + await expect( + service.getMoneyAccountBalance(MOCK_ACCOUNT_ADDRESS), + ).rejects.toThrow('execution reverted'); + expectTraceRequest(trace, { + errorName: 'Error', + name: 'Get Money Account Balance RPC', + operation: 'aggregate3', + success: false, + }); + }); + + it('traces non-Error aggregate3 rejections with the thrown value type', async () => { + const aggregate3 = jest.fn().mockRejectedValue('execution reverted'); + mockMoneyAccountBalanceMulticall({ aggregate3 }); + const trace = createTraceCallback(); + const { service } = createService({ + rffcFlags: { + [VAULT_CONFIG_FEATURE_FLAG_KEY]: + MOCK_VAULT_CONFIG_WITH_UNDERLYING_TOKEN, + }, + options: { trace }, + }); + + await expect( + service.getMoneyAccountBalance(MOCK_ACCOUNT_ADDRESS), + ).rejects.toBe('execution reverted'); + expectTraceRequest(trace, { + errorName: 'string', + name: 'Get Money Account Balance RPC', + operation: 'aggregate3', + success: false, + }); + }); + + it('propagates Veda API errors after emitting a failed trace', async () => { + nock('https://api.sevenseas.capital') + .get(`/performance/arbitrum/${MOCK_VAULT_ADDRESS}`) + .times(DEFAULT_MAX_RETRIES + 1) + .reply(500); + const trace = createTraceCallback(); + const { service } = createService({ options: { trace } }); + + await expect(service.getVaultApy()).rejects.toThrow( + new HttpError(500, "Veda performance API failed with status '500'"), + ); + expectTraceRequest(trace, { + errorName: 'Error', + name: 'Get Money Account Vault APY API', + operation: 'fetchVaultApy', + success: false, + }); + }); + }); + // ---------------------------------------------------------- // getMusdBalance // ---------------------------------------------------------- @@ -1062,15 +1398,6 @@ describe('MoneyAccountBalanceService', () => { // ---------------------------------------------------------- describe('getMoneyAccountBalance', () => { - /** - * Vault config including `underlyingToken`, so balance resolution skips the - * on-chain `base()` read. - */ - const VAULT_CONFIG_WITH_UNDERLYING_TOKEN = { - ...MOCK_VAULT_CONFIG, - underlyingToken: MOCK_UNDERLYING_TOKEN_ADDRESS, - }; - it('returns musdBalance, vmusdValueInMusd, and totalBalance from a single aggregate3 call', async () => { const aggregate3 = mockMoneyAccountBalanceMulticall({ musdBalance: '5000000', @@ -1078,7 +1405,8 @@ describe('MoneyAccountBalanceService', () => { }); const { service } = createService({ rffcFlags: { - [VAULT_CONFIG_FEATURE_FLAG_KEY]: VAULT_CONFIG_WITH_UNDERLYING_TOKEN, + [VAULT_CONFIG_FEATURE_FLAG_KEY]: + MOCK_VAULT_CONFIG_WITH_UNDERLYING_TOKEN, }, }); @@ -1127,7 +1455,8 @@ describe('MoneyAccountBalanceService', () => { const { service } = createService({ rffcFlags: { - [VAULT_CONFIG_FEATURE_FLAG_KEY]: VAULT_CONFIG_WITH_UNDERLYING_TOKEN, + [VAULT_CONFIG_FEATURE_FLAG_KEY]: + MOCK_VAULT_CONFIG_WITH_UNDERLYING_TOKEN, }, }); @@ -1157,7 +1486,8 @@ describe('MoneyAccountBalanceService', () => { const aggregate3 = mockMoneyAccountBalanceMulticall(); const { service } = createService({ rffcFlags: { - [VAULT_CONFIG_FEATURE_FLAG_KEY]: VAULT_CONFIG_WITH_UNDERLYING_TOKEN, + [VAULT_CONFIG_FEATURE_FLAG_KEY]: + MOCK_VAULT_CONFIG_WITH_UNDERLYING_TOKEN, }, }); @@ -1224,7 +1554,8 @@ describe('MoneyAccountBalanceService', () => { }); const { rootMessenger } = createService({ rffcFlags: { - [VAULT_CONFIG_FEATURE_FLAG_KEY]: VAULT_CONFIG_WITH_UNDERLYING_TOKEN, + [VAULT_CONFIG_FEATURE_FLAG_KEY]: + MOCK_VAULT_CONFIG_WITH_UNDERLYING_TOKEN, }, }); @@ -1247,7 +1578,8 @@ describe('MoneyAccountBalanceService', () => { mockMoneyAccountBalanceMulticall({ aggregate3 }); const { service } = createService({ rffcFlags: { - [VAULT_CONFIG_FEATURE_FLAG_KEY]: VAULT_CONFIG_WITH_UNDERLYING_TOKEN, + [VAULT_CONFIG_FEATURE_FLAG_KEY]: + MOCK_VAULT_CONFIG_WITH_UNDERLYING_TOKEN, }, }); @@ -1261,7 +1593,7 @@ describe('MoneyAccountBalanceService', () => { const { service } = createService({ rffcFlags: { [VAULT_CONFIG_FEATURE_FLAG_KEY]: { - ...VAULT_CONFIG_WITH_UNDERLYING_TOKEN, + ...MOCK_VAULT_CONFIG_WITH_UNDERLYING_TOKEN, chainId: '0x1', }, }, diff --git a/packages/money-account-balance-service/src/money-account-balance-service.ts b/packages/money-account-balance-service/src/money-account-balance-service.ts index 7a3692ea2f..955ce61e5c 100644 --- a/packages/money-account-balance-service/src/money-account-balance-service.ts +++ b/packages/money-account-balance-service/src/money-account-balance-service.ts @@ -6,7 +6,11 @@ import type { DataServiceInvalidateQueriesAction, } from '@metamask/base-data-service'; import { BaseDataService } from '@metamask/base-data-service'; -import type { CreateServicePolicyOptions } from '@metamask/controller-utils'; +import type { + CreateServicePolicyOptions, + TraceContext, + TraceRequest, +} from '@metamask/controller-utils'; import { handleWhen, HttpError } from '@metamask/controller-utils'; import type { Messenger } from '@metamask/messenger'; import { abiERC20 } from '@metamask/metamask-eth-abis'; @@ -74,7 +78,33 @@ const PENDING_READ_OVERRIDES = { blockTag: 'pending' } as const; */ export const serviceName = 'MoneyAccountBalanceService'; +export const TRACES = { + ERC20_BALANCE_RPC: 'Get Money Account ERC20 Balance RPC', + UNDERLYING_TOKEN_RPC: 'Get Money Account Underlying Token RPC', + MONEY_ACCOUNT_BALANCE_RPC: 'Get Money Account Balance RPC', + EXCHANGE_RATE_RPC: 'Get Money Account Exchange Rate RPC', + MUSD_EQUIVALENT_VALUE_RPC: 'Get Money Account mUSD Equivalent Value RPC', + VAULT_APY_API: 'Get Money Account Vault APY API', +} as const; + +export type MoneyAccountBalanceServiceTraceName = + (typeof TRACES)[keyof typeof TRACES]; + +export type MoneyAccountBalanceServiceTraceRequest = Omit< + TraceRequest, + 'name' +> & { + name: MoneyAccountBalanceServiceTraceName; + startTime?: number; +}; + +export type MoneyAccountBalanceServiceTraceCallback = ( + request: MoneyAccountBalanceServiceTraceRequest, + fn?: (context?: TraceContext) => ReturnType, +) => Promise; + const configLogger = createModuleLogger(projectLogger, 'config'); +const traceLogger = createModuleLogger(projectLogger, 'trace'); // === MESSENGER === @@ -171,9 +201,10 @@ export type MoneyAccountBalanceServiceMessenger = Messenger< * ``` */ -type MoneyAccountBalanceServiceOptions = { +export type MoneyAccountBalanceServiceOptions = { messenger: MoneyAccountBalanceServiceMessenger; policyOptions?: CreateServicePolicyOptions; + trace?: MoneyAccountBalanceServiceTraceCallback; }; export class MoneyAccountBalanceService extends BaseDataService< @@ -185,14 +216,18 @@ export class MoneyAccountBalanceService extends BaseDataService< /** Cache stale time (ms) for on-chain balance reads. Overridable via remote feature flag. */ #balanceStaleTime: number = DEFAULT_BALANCE_STALE_TIME; + readonly #trace: MoneyAccountBalanceServiceTraceCallback; + /** * @param options - Constructor options. * @param options.messenger - The messenger for this service. * @param options.policyOptions - Options passed to `createServicePolicy`. + * @param options.trace - Optional callback to trace network requests. */ constructor({ messenger, policyOptions = {}, + trace, }: MoneyAccountBalanceServiceOptions) { super({ name: serviceName, @@ -207,6 +242,15 @@ export class MoneyAccountBalanceService extends BaseDataService< }, }); + this.#trace = + trace ?? + (async ( + _request: MoneyAccountBalanceServiceTraceRequest, + fn?: (context?: TraceContext) => ReturnType, + ): Promise => { + return await Promise.resolve(fn?.() as ReturnType); + }); + this.messenger.subscribe( // eslint-disable-next-line no-restricted-syntax 'RemoteFeatureFlagController:stateChange', @@ -219,6 +263,55 @@ export class MoneyAccountBalanceService extends BaseDataService< ); } + /** + * Runs a network request and emits a best-effort backdated trace. + * + * @param request - Trace metadata for the network request. + * @param fn - Network request to execute. + * @returns The network request result. + */ + async #traceNetworkRequest( + request: MoneyAccountBalanceServiceTraceRequest, + fn: () => Promise, + ): Promise { + const startTime = Date.now(); + let success = false; + let errorName: string | undefined; + + try { + const result = await fn(); + success = true; + return result; + } catch (error) { + errorName = error instanceof Error ? error.name : typeof error; + throw error; + } finally { + const traceRequest = { + ...request, + startTime, + data: { + ...request.data, + success, + ...(errorName ? { errorName } : {}), + }, + }; + const onTraceError = (traceError: unknown): void => { + traceLogger('Failed to emit trace', { + traceName: request.name, + traceError, + }); + }; + + try { + Promise.resolve(this.#trace(traceRequest, () => undefined)).catch( + onTraceError, + ); + } catch (traceError) { + onTraceError(traceError); + } + } + } + /** * Eagerly reads already-loaded feature flags and initialises service state. * @@ -445,9 +538,17 @@ export class MoneyAccountBalanceService extends BaseDataService< ): Promise { const provider = this.#getProvider(chainId); const contract = new Contract(contractAddress, abiERC20, provider); - const balance = await contract.balanceOf( - accountAddress, - PENDING_READ_OVERRIDES, + const balance = await this.#traceNetworkRequest( + { + name: TRACES.ERC20_BALANCE_RPC, + data: { + chainId, + tokenAddress: contractAddress, + operation: 'balanceOf', + }, + }, + async () => + await contract.balanceOf(accountAddress, PENDING_READ_OVERRIDES), ); return balance.toString(); } @@ -462,7 +563,13 @@ export class MoneyAccountBalanceService extends BaseDataService< const { accountantAddress } = this.#requireConfig(); const provider = this.#getProvider(chainId); const contract = new Contract(accountantAddress, ACCOUNTANT_ABI, provider); - const underlyingTokenAddress = await contract.base(); + const underlyingTokenAddress = await this.#traceNetworkRequest( + { + name: TRACES.UNDERLYING_TOKEN_RPC, + data: { chainId, operation: 'base' }, + }, + async () => await contract.base(), + ); return underlyingTokenAddress; } @@ -578,11 +685,17 @@ export class MoneyAccountBalanceService extends BaseDataService< MULTICALL3_ABI, provider, ); - const [musdResult, vmusdResult] = - (await multicall3.callStatic.aggregate3( - calls, - PENDING_READ_OVERRIDES, - )) as [Multicall3Result, Multicall3Result]; + const [musdResult, vmusdResult] = (await this.#traceNetworkRequest( + { + name: TRACES.MONEY_ACCOUNT_BALANCE_RPC, + data: { chainId, operation: 'aggregate3' }, + }, + async () => + await multicall3.callStatic.aggregate3( + calls, + PENDING_READ_OVERRIDES, + ), + )) as [Multicall3Result, Multicall3Result]; const musdBalanceBN = erc20.interface.decodeFunctionResult( 'balanceOf', @@ -650,7 +763,13 @@ export class MoneyAccountBalanceService extends BaseDataService< ACCOUNTANT_ABI, provider, ); - const rate = await contract.getRate(); + const rate = await this.#traceNetworkRequest( + { + name: TRACES.EXCHANGE_RATE_RPC, + data: { chainId, operation: 'getRate' }, + }, + async () => await contract.getRate(), + ); return { rate: rate.toString() }; }, staleTime: staleTime ?? this.#balanceStaleTime, @@ -675,11 +794,18 @@ export class MoneyAccountBalanceService extends BaseDataService< this.#requireConfig(); const provider = this.#getProvider(chainId); const contract = new Contract(lensAddress, LENS_ABI, provider); - const balanceOfInAssets = await contract.balanceOfInAssets( - accountAddress, - boringVault, - accountantAddress, - PENDING_READ_OVERRIDES, + const balanceOfInAssets = await this.#traceNetworkRequest( + { + name: TRACES.MUSD_EQUIVALENT_VALUE_RPC, + data: { chainId, operation: 'balanceOfInAssets' }, + }, + async () => + await contract.balanceOfInAssets( + accountAddress, + boringVault, + accountantAddress, + PENDING_READ_OVERRIDES, + ), ); return { balanceOfInAssets: balanceOfInAssets.toString() }; @@ -712,16 +838,24 @@ export class MoneyAccountBalanceService extends BaseDataService< VEDA_PERFORMANCE_API_BASE_URL, ); - const response = await fetch(url); + const rawResponse = await this.#traceNetworkRequest( + { + name: TRACES.VAULT_APY_API, + data: { chainId, operation: 'fetchVaultApy' }, + }, + async () => { + const response = await fetch(url); - if (!response.ok) { - throw new HttpError( - response.status, - `Veda performance API failed with status '${response.status}'`, - ); - } + if (!response.ok) { + throw new HttpError( + response.status, + `Veda performance API failed with status '${response.status}'`, + ); + } - const rawResponse = await response.json(); + return await response.json(); + }, + ); // Validate raw response inside queryFn to avoid poisoned cache. if (!is(rawResponse, VaultApyRawResponseStruct)) {