From 14c30de93d00adf0f744ab1b41bc1f0b7ccfefa4 Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Thu, 9 Jul 2026 17:31:11 +0200 Subject: [PATCH 1/2] feat: add optional tracing for money-account-api-data-service network requests Add a `trace` constructor callback to MoneyAccountApiDataService that emits best-effort backdated spans for all HTTP calls (fetchPositions, fetchInterest, fetchHistory, fetchRateHistory). Tracing is isolated from fetch/retry logic so trace failures never impact queries. Mirrors the approach used in money-account-balance-service (PR #9434). Co-authored-by: Cursor --- .../CHANGELOG.md | 6 + .../src/index.ts | 3 + .../money-account-api-data-service.test.ts | 199 +++++++++++- .../src/money-account-api-data-service.ts | 306 +++++++++++++----- 4 files changed, 423 insertions(+), 91 deletions(-) diff --git a/packages/money-account-api-data-service/CHANGELOG.md b/packages/money-account-api-data-service/CHANGELOG.md index 21a36bc23b..cfd9115fee 100644 --- a/packages/money-account-api-data-service/CHANGELOG.md +++ b/packages/money-account-api-data-service/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Added optional `trace` callback to `MoneyAccountApiDataService` constructor for network request tracing ([#TBD](https://github.com/MetaMask/core/pull/TBD)) + - All HTTP calls (`fetchPositions`, `fetchInterest`, `fetchHistory`, `fetchRateHistory`) emit best-effort backdated traces with `startTime`, `success`, and `errorName` attributes + - Tracing is isolated from fetch/retry logic; trace failures do not impact queries + ## [0.1.0] ### Added diff --git a/packages/money-account-api-data-service/src/index.ts b/packages/money-account-api-data-service/src/index.ts index 780facd0ac..f4f3b2b7e0 100644 --- a/packages/money-account-api-data-service/src/index.ts +++ b/packages/money-account-api-data-service/src/index.ts @@ -3,6 +3,9 @@ export type { MoneyAccountApiDataServiceActions, MoneyAccountApiDataServiceEvents, MoneyAccountApiDataServiceMessenger, + MoneyAccountApiDataServiceOptions, + MoneyAccountApiDataServiceTraceCallback, + MoneyAccountApiDataServiceTraceRequest, } from './money-account-api-data-service'; export type { MoneyAccountApiDataServiceFetchPositionsAction, diff --git a/packages/money-account-api-data-service/src/money-account-api-data-service.test.ts b/packages/money-account-api-data-service/src/money-account-api-data-service.test.ts index f396130de6..8cac6e8dde 100644 --- a/packages/money-account-api-data-service/src/money-account-api-data-service.test.ts +++ b/packages/money-account-api-data-service/src/money-account-api-data-service.test.ts @@ -9,10 +9,15 @@ import nock, { cleanAll as nockCleanAll } from 'nock'; import { Env, MONEY_ACCOUNT_API_URL_MAP } from './constants'; import { MoneyAccountApiResponseValidationError } from './errors'; -import type { MoneyAccountApiDataServiceMessenger } from './money-account-api-data-service'; +import type { + MoneyAccountApiDataServiceMessenger, + MoneyAccountApiDataServiceTraceCallback, + MoneyAccountApiDataServiceTraceRequest, +} from './money-account-api-data-service'; import { MoneyAccountApiDataService, serviceName, + TRACES, } from './money-account-api-data-service'; // ============================================================ @@ -139,7 +144,12 @@ function createServiceMessenger( // Factory // ============================================================ -function createService(env: Env = Env.DEV): { +function createService( + env: Env = Env.DEV, + { + trace, + }: { trace?: MoneyAccountApiDataServiceTraceCallback } = {}, +): { service: MoneyAccountApiDataService; rootMessenger: RootMessenger; messenger: MoneyAccountApiDataServiceMessenger; @@ -149,6 +159,7 @@ function createService(env: Env = Env.DEV): { const service = new MoneyAccountApiDataService({ messenger, env, + trace, }); return { service, rootMessenger, messenger }; } @@ -568,6 +579,190 @@ describe('MoneyAccountApiDataService', () => { }); }); + describe('tracing', () => { + let mockTrace: jest.Mock; + + beforeEach(() => { + mockTrace = jest.fn().mockResolvedValue(undefined); + }); + + it('emits a trace for fetchPositions on cache miss', async () => { + const { service } = createService(Env.DEV, { trace: mockTrace }); + + nock(MONEY_ACCOUNT_API_URL_MAP[Env.DEV]) + .get(`/v1/positions/${MOCK_ADDRESS}`) + .reply(200, MOCK_POSITION_RESPONSE); + + await service.fetchPositions(MOCK_ADDRESS); + + expect(mockTrace).toHaveBeenCalledTimes(1); + const [request] = mockTrace.mock.calls[0] as [ + MoneyAccountApiDataServiceTraceRequest, + unknown, + ]; + expect(request.name).toBe(TRACES.POSITIONS_API); + expect(request.data).toStrictEqual( + expect.objectContaining({ + operation: 'fetchPositions', + success: true, + }), + ); + expect(request.startTime).toStrictEqual(expect.any(Number)); + service.destroy(); + }); + + it('emits a trace for fetchInterest on cache miss', async () => { + const { service } = createService(Env.DEV, { trace: mockTrace }); + + nock(MONEY_ACCOUNT_API_URL_MAP[Env.DEV]) + .get(`/v1/positions/${MOCK_ADDRESS}/interest`) + .query({ vault_address: MOCK_VAULT_ADDRESS, window: '7d' }) + .reply(200, MOCK_INTEREST_RESPONSE); + + await service.fetchInterest(MOCK_ADDRESS, { + vaultAddress: MOCK_VAULT_ADDRESS, + window: '7d', + }); + + expect(mockTrace).toHaveBeenCalledTimes(1); + const [request] = mockTrace.mock.calls[0] as [ + MoneyAccountApiDataServiceTraceRequest, + unknown, + ]; + expect(request.name).toBe(TRACES.INTEREST_API); + expect(request.data).toStrictEqual( + expect.objectContaining({ + operation: 'fetchInterest', + success: true, + }), + ); + service.destroy(); + }); + + it('emits a trace for fetchHistory on cache miss', async () => { + const { service } = createService(Env.DEV, { trace: mockTrace }); + + nock(MONEY_ACCOUNT_API_URL_MAP[Env.DEV]) + .get(`/v1/positions/${MOCK_ADDRESS}/history`) + .reply(200, MOCK_HISTORY_RESPONSE); + + await service.fetchHistory(MOCK_ADDRESS); + + expect(mockTrace).toHaveBeenCalledTimes(1); + const [request] = mockTrace.mock.calls[0] as [ + MoneyAccountApiDataServiceTraceRequest, + unknown, + ]; + expect(request.name).toBe(TRACES.HISTORY_API); + expect(request.data).toStrictEqual( + expect.objectContaining({ + operation: 'fetchHistory', + success: true, + }), + ); + service.destroy(); + }); + + it('emits a trace for fetchRateHistory on cache miss', async () => { + const { service } = createService(Env.DEV, { trace: mockTrace }); + + nock(MONEY_ACCOUNT_API_URL_MAP[Env.DEV]) + .get(`/v1/vaults/${MOCK_VAULT_ADDRESS}/rate-history`) + .reply(200, MOCK_RATE_HISTORY_RESPONSE); + + await service.fetchRateHistory(MOCK_VAULT_ADDRESS); + + expect(mockTrace).toHaveBeenCalledTimes(1); + const [request] = mockTrace.mock.calls[0] as [ + MoneyAccountApiDataServiceTraceRequest, + unknown, + ]; + expect(request.name).toBe(TRACES.RATE_HISTORY_API); + expect(request.data).toStrictEqual( + expect.objectContaining({ + operation: 'fetchRateHistory', + success: true, + }), + ); + service.destroy(); + }); + + it('does not emit a trace on cache hit', async () => { + const { service } = createService(Env.DEV, { trace: mockTrace }); + + nock(MONEY_ACCOUNT_API_URL_MAP[Env.DEV]) + .get(`/v1/positions/${MOCK_ADDRESS}`) + .once() + .reply(200, MOCK_POSITION_RESPONSE); + + await service.fetchPositions(MOCK_ADDRESS); + mockTrace.mockClear(); + + await service.fetchPositions(MOCK_ADDRESS); + expect(mockTrace).not.toHaveBeenCalled(); + service.destroy(); + }); + + it('records success: false and errorName on failed request', async () => { + const { service } = createService(Env.DEV, { trace: mockTrace }); + + nock(MONEY_ACCOUNT_API_URL_MAP[Env.DEV]) + .get(`/v1/positions/${MOCK_ADDRESS}`) + .times(DEFAULT_MAX_RETRIES + 1) + .reply(500); + + await expect(service.fetchPositions(MOCK_ADDRESS)).rejects.toThrow( + HttpError, + ); + + expect(mockTrace).toHaveBeenCalled(); + const lastCall = mockTrace.mock.calls[ + mockTrace.mock.calls.length - 1 + ] as [MoneyAccountApiDataServiceTraceRequest, unknown]; + expect(lastCall[0].data).toStrictEqual( + expect.objectContaining({ + success: false, + errorName: expect.any(String), + }), + ); + service.destroy(); + }); + + it('does not break the request when trace callback throws synchronously', async () => { + const throwingTrace = jest.fn().mockImplementation(() => { + throw new Error('trace sync failure'); + }) as unknown as MoneyAccountApiDataServiceTraceCallback; + + const { service } = createService(Env.DEV, { trace: throwingTrace }); + + nock(MONEY_ACCOUNT_API_URL_MAP[Env.DEV]) + .get(`/v1/positions/${MOCK_ADDRESS}`) + .reply(200, MOCK_POSITION_RESPONSE); + + const result = await service.fetchPositions(MOCK_ADDRESS); + expect(result).toStrictEqual(MOCK_POSITION_RESPONSE); + service.destroy(); + }); + + it('does not break the request when trace callback rejects', async () => { + const rejectingTrace = jest + .fn() + .mockRejectedValue( + new Error('trace async failure'), + ) as unknown as MoneyAccountApiDataServiceTraceCallback; + + const { service } = createService(Env.DEV, { trace: rejectingTrace }); + + nock(MONEY_ACCOUNT_API_URL_MAP[Env.DEV]) + .get(`/v1/positions/${MOCK_ADDRESS}`) + .reply(200, MOCK_POSITION_RESPONSE); + + const result = await service.fetchPositions(MOCK_ADDRESS); + expect(result).toStrictEqual(MOCK_POSITION_RESPONSE); + service.destroy(); + }); + }); + describe('invalidateQueries', () => { it('invalidates cached queries', async () => { const { service } = createService(Env.DEV); diff --git a/packages/money-account-api-data-service/src/money-account-api-data-service.ts b/packages/money-account-api-data-service/src/money-account-api-data-service.ts index 2f216249d0..de726e6080 100644 --- a/packages/money-account-api-data-service/src/money-account-api-data-service.ts +++ b/packages/money-account-api-data-service/src/money-account-api-data-service.ts @@ -4,7 +4,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 { validate } from '@metamask/superstruct'; @@ -47,6 +51,30 @@ import type { export const serviceName = 'MoneyAccountApiDataService'; const log = createModuleLogger(projectLogger, serviceName); +const traceLogger = createModuleLogger(projectLogger, 'trace'); + +export const TRACES = { + POSITIONS_API: 'Money Account API Fetch Positions', + INTEREST_API: 'Money Account API Fetch Interest', + HISTORY_API: 'Money Account API Fetch History', + RATE_HISTORY_API: 'Money Account API Fetch Rate History', +} as const; + +export type MoneyAccountApiDataServiceTraceName = + (typeof TRACES)[keyof typeof TRACES]; + +export type MoneyAccountApiDataServiceTraceRequest = Omit< + TraceRequest, + 'name' +> & { + name: MoneyAccountApiDataServiceTraceName; + startTime?: number; +}; + +export type MoneyAccountApiDataServiceTraceCallback = ( + request: MoneyAccountApiDataServiceTraceRequest, + fn?: (context?: TraceContext) => ReturnType, +) => Promise; // === MESSENGER === @@ -113,6 +141,14 @@ export type MoneyAccountApiDataServiceMessenger = Messenger< // === SERVICE DEFINITION === +export type MoneyAccountApiDataServiceOptions = { + messenger: MoneyAccountApiDataServiceMessenger; + env?: Env; + queryClientConfig?: QueryClientConfig; + policyOptions?: CreateServicePolicyOptions; + trace?: MoneyAccountApiDataServiceTraceCallback; +}; + /** * Data service responsible for fetching positions, interest, cash-flow * history, and vault rate history from the Money Account APY Tracking API. @@ -123,27 +159,26 @@ export class MoneyAccountApiDataService extends BaseDataService< > { readonly #baseUrl: string; + readonly #trace: MoneyAccountApiDataServiceTraceCallback; + /** * Constructs a new MoneyAccountApiDataService. * - * @param args - The constructor arguments. - * @param args.messenger - The messenger suited for this service. - * @param args.env - The target environment. Defaults to production. - * @param args.queryClientConfig - Configuration for the underlying TanStack - * Query client. - * @param args.policyOptions - Options to pass to `createServicePolicy`. + * @param options - The constructor arguments. + * @param options.messenger - The messenger suited for this service. + * @param options.env - The target environment. Defaults to production. + * @param options.queryClientConfig - Configuration for the underlying + * TanStack Query client. + * @param options.policyOptions - Options to pass to `createServicePolicy`. + * @param options.trace - Optional callback to trace network requests. */ constructor({ messenger, env = Env.PRD, queryClientConfig = {}, policyOptions = {}, - }: { - messenger: MoneyAccountApiDataServiceMessenger; - env?: Env; - queryClientConfig?: QueryClientConfig; - policyOptions?: CreateServicePolicyOptions; - }) { + trace, + }: MoneyAccountApiDataServiceOptions) { super({ name: serviceName, messenger, @@ -158,6 +193,15 @@ export class MoneyAccountApiDataService extends BaseDataService< this.#baseUrl = MONEY_ACCOUNT_API_URL_MAP[env]; + this.#trace = + trace ?? + (async ( + _request: MoneyAccountApiDataServiceTraceRequest, + fn?: (context?: TraceContext) => Result, + ): Promise => { + return await Promise.resolve(fn?.() as Result); + }); + this.messenger.registerMethodActionHandlers( this, MESSENGER_EXPOSED_METHODS, @@ -182,25 +226,33 @@ export class MoneyAccountApiDataService extends BaseDataService< queryKey: [`${this.name}:fetchPositions`, address.toLowerCase()], staleTime: DEFAULT_STALE_TIME_MS, queryFn: async () => { - const response = await fetch(url); - - if (!response.ok) { - throw new HttpError( - response.status, - `Money Account API positions request failed with status '${response.status}'`, - ); - } - - const json: Json = await response.json(); - - const [error, validated] = validate(json, PositionResponseStruct); - if (error) { - throw new MoneyAccountApiResponseValidationError( - `Malformed response from positions endpoint: ${error.message}`, - ); - } - - return validated as unknown as PositionResponse; + return this.#traceNetworkRequest( + { + name: TRACES.POSITIONS_API, + data: { operation: 'fetchPositions' }, + }, + async () => { + const response = await fetch(url); + + if (!response.ok) { + throw new HttpError( + response.status, + `Money Account API positions request failed with status '${response.status}'`, + ); + } + + const json: Json = await response.json(); + + const [error, validated] = validate(json, PositionResponseStruct); + if (error) { + throw new MoneyAccountApiResponseValidationError( + `Malformed response from positions endpoint: ${error.message}`, + ); + } + + return validated as unknown as PositionResponse; + }, + ); }, }); } @@ -240,25 +292,33 @@ export class MoneyAccountApiDataService extends BaseDataService< ], staleTime: DEFAULT_STALE_TIME_MS, queryFn: async () => { - const response = await fetch(url); - - if (!response.ok) { - throw new HttpError( - response.status, - `Money Account API interest request failed with status '${response.status}'`, - ); - } - - const json: Json = await response.json(); - - const [error, validated] = validate(json, InterestResponseStruct); - if (error) { - throw new MoneyAccountApiResponseValidationError( - `Malformed response from interest endpoint: ${error.message}`, - ); - } - - return validated as unknown as InterestResponse; + return this.#traceNetworkRequest( + { + name: TRACES.INTEREST_API, + data: { operation: 'fetchInterest' }, + }, + async () => { + const response = await fetch(url); + + if (!response.ok) { + throw new HttpError( + response.status, + `Money Account API interest request failed with status '${response.status}'`, + ); + } + + const json: Json = await response.json(); + + const [error, validated] = validate(json, InterestResponseStruct); + if (error) { + throw new MoneyAccountApiResponseValidationError( + `Malformed response from interest endpoint: ${error.message}`, + ); + } + + return validated as unknown as InterestResponse; + }, + ); }, }); } @@ -313,25 +373,33 @@ export class MoneyAccountApiDataService extends BaseDataService< url.searchParams.append('limit', String(options.limit)); } - const response = await fetch(url); - - if (!response.ok) { - throw new HttpError( - response.status, - `Money Account API history request failed with status '${response.status}'`, - ); - } - - const json: Json = await response.json(); - - const [error, validated] = validate(json, HistoryResponseStruct); - if (error) { - throw new MoneyAccountApiResponseValidationError( - `Malformed response from history endpoint: ${error.message}`, - ); - } - - return validated as unknown as HistoryResponse; + return this.#traceNetworkRequest( + { + name: TRACES.HISTORY_API, + data: { operation: 'fetchHistory' }, + }, + async () => { + const response = await fetch(url); + + if (!response.ok) { + throw new HttpError( + response.status, + `Money Account API history request failed with status '${response.status}'`, + ); + } + + const json: Json = await response.json(); + + const [error, validated] = validate(json, HistoryResponseStruct); + if (error) { + throw new MoneyAccountApiResponseValidationError( + `Malformed response from history endpoint: ${error.message}`, + ); + } + + return validated as unknown as HistoryResponse; + }, + ); }, }, options?.cursor ?? undefined, @@ -373,26 +441,86 @@ export class MoneyAccountApiDataService extends BaseDataService< ], staleTime: RATE_HISTORY_STALE_TIME_MS, queryFn: async () => { - const response = await fetch(url); - - if (!response.ok) { - throw new HttpError( - response.status, - `Money Account API rate-history request failed with status '${response.status}'`, - ); - } - - const json: Json = await response.json(); - - const [error, validated] = validate(json, RateHistoryResponseStruct); - if (error) { - throw new MoneyAccountApiResponseValidationError( - `Malformed response from rate-history endpoint: ${error.message}`, - ); - } - - return validated as unknown as RateHistoryResponse; + return this.#traceNetworkRequest( + { + name: TRACES.RATE_HISTORY_API, + data: { operation: 'fetchRateHistory' }, + }, + async () => { + const response = await fetch(url); + + if (!response.ok) { + throw new HttpError( + response.status, + `Money Account API rate-history request failed with status '${response.status}'`, + ); + } + + const json: Json = await response.json(); + + const [error, validated] = validate( + json, + RateHistoryResponseStruct, + ); + if (error) { + throw new MoneyAccountApiResponseValidationError( + `Malformed response from rate-history endpoint: ${error.message}`, + ); + } + + return validated as unknown as RateHistoryResponse; + }, + ); }, }); } + + /** + * 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: MoneyAccountApiDataServiceTraceRequest, + 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); + } + } + } } From 8fd5dfabc28ea0f5242a4c50150dbc25b1b9b90d Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Fri, 10 Jul 2026 12:56:41 +0200 Subject: [PATCH 2/2] fix: add balance field to PositionResponseStruct for API compatibility The live Money Account API now returns a `balance` object in the positions response. The strict superstruct `object()` validator rejected the unknown key, causing `fetchPositions` to throw a validation error. Add the `balance` field as optional to the struct and export the new `Balance` type. Co-authored-by: Cursor --- packages/money-account-api-data-service/CHANGELOG.md | 4 ++++ packages/money-account-api-data-service/src/index.ts | 1 + .../src/money-account-api-data-service.test.ts | 5 +++++ .../money-account-api-data-service/src/response.types.ts | 9 +++++++++ packages/money-account-api-data-service/src/structs.ts | 8 ++++++++ 5 files changed, 27 insertions(+) diff --git a/packages/money-account-api-data-service/CHANGELOG.md b/packages/money-account-api-data-service/CHANGELOG.md index cfd9115fee..f9c7cf2cb3 100644 --- a/packages/money-account-api-data-service/CHANGELOG.md +++ b/packages/money-account-api-data-service/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - All HTTP calls (`fetchPositions`, `fetchInterest`, `fetchHistory`, `fetchRateHistory`) emit best-effort backdated traces with `startTime`, `success`, and `errorName` attributes - Tracing is isolated from fetch/retry logic; trace failures do not impact queries +### Fixed + +- Fixed `fetchPositions` struct validation failing when the API returns the `balance` field ([#TBD](https://github.com/MetaMask/core/pull/TBD)) + ## [0.1.0] ### Added diff --git a/packages/money-account-api-data-service/src/index.ts b/packages/money-account-api-data-service/src/index.ts index f4f3b2b7e0..ad47a66c6a 100644 --- a/packages/money-account-api-data-service/src/index.ts +++ b/packages/money-account-api-data-service/src/index.ts @@ -14,6 +14,7 @@ export type { MoneyAccountApiDataServiceFetchRateHistoryAction, } from './money-account-api-data-service-method-action-types'; export type { + Balance, PositionResponse, InterestResponse, HistoryResponse, diff --git a/packages/money-account-api-data-service/src/money-account-api-data-service.test.ts b/packages/money-account-api-data-service/src/money-account-api-data-service.test.ts index 8cac6e8dde..030b5fa2a5 100644 --- a/packages/money-account-api-data-service/src/money-account-api-data-service.test.ts +++ b/packages/money-account-api-data-service/src/money-account-api-data-service.test.ts @@ -33,6 +33,11 @@ const MOCK_POSITION_RESPONSE = { as_of_timestamp: '2026-06-01T12:00:00Z', data_freshness: 'live' as const, indexer_lag_seconds: 5, + balance: { + musd_balance: '1000000', + vmusd_value_in_musd: '1052340000000000000', + total_balance: '1052341000000000000', + }, positions: [ { vault_address: MOCK_VAULT_ADDRESS, diff --git a/packages/money-account-api-data-service/src/response.types.ts b/packages/money-account-api-data-service/src/response.types.ts index ff779e24b2..28d4d39d4c 100644 --- a/packages/money-account-api-data-service/src/response.types.ts +++ b/packages/money-account-api-data-service/src/response.types.ts @@ -16,6 +16,15 @@ import type { */ export type DataFreshness = 'live' | 'degraded'; +/** + * Aggregated balance summary within the positions response. + */ +export type Balance = { + musd_balance: string; + vmusd_value_in_musd: string; + total_balance: string; +}; + /** * A single vault position within the positions response. */ diff --git a/packages/money-account-api-data-service/src/structs.ts b/packages/money-account-api-data-service/src/structs.ts index ae08ec3dae..b46a42cf28 100644 --- a/packages/money-account-api-data-service/src/structs.ts +++ b/packages/money-account-api-data-service/src/structs.ts @@ -4,6 +4,7 @@ import { enums, number, object, + optional, string, nullable, } from '@metamask/superstruct'; @@ -25,12 +26,19 @@ const VaultPositionStruct = object({ effective_apy: string(), }); +const BalanceStruct = object({ + musd_balance: string(), + vmusd_value_in_musd: string(), + total_balance: string(), +}); + export const PositionResponseStruct = object({ address: string(), as_of_block: number(), as_of_timestamp: string(), data_freshness: DataFreshnessStruct, indexer_lag_seconds: number(), + balance: optional(BalanceStruct), positions: array(VaultPositionStruct), });