Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/money-account-api-data-service/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Response validation now tolerates unknown fields returned by the Money Account API instead of throwing `MoneyAccountApiResponseValidationError`, so additive backend changes no longer break clients ([#10538](https://github.com/MetaMask/core/pull/10538))
- Bump `@tanstack/query-core` from `^5.89.0` to `^5.103.2` ([#10511](https://github.com/MetaMask/core/pull/10511))

## [2.0.0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1298,6 +1298,124 @@ describe('MoneyAccountApiDataService', () => {
});
});

describe('additive API fields', () => {
it('accepts unknown fields on positions responses without stripping them', async () => {
const { service } = createService(Env.DEV);
const responseWithExtras = {
...MOCK_POSITION_RESPONSE,
future_top_level: 'ok',
balance: {
...MOCK_POSITION_RESPONSE.balance,
future_balance_field: 1,
by_asset: [
{
...MOCK_POSITION_RESPONSE.balance.by_asset[0],
future_asset_field: true,
},
],
},
positions: [
{
...MOCK_POSITION_RESPONSE.positions[0],
future_position_field: 'premium',
},
],
};

nock(MONEY_ACCOUNT_API_URL_MAP[Env.DEV])
.get(`/v1/positions/${MOCK_ADDRESS}`)
.reply(200, responseWithExtras);

const result = await service.fetchPositions(MOCK_ADDRESS);
expect(result).toStrictEqual(responseWithExtras);
service.destroy();
});

it('accepts unknown fields on interest responses without stripping them', async () => {
const { service } = createService(Env.DEV);
const responseWithExtras = {
...MOCK_INTEREST_RESPONSE,
future_interest_field: 'ok',
};

nock(MONEY_ACCOUNT_API_URL_MAP[Env.DEV])
.get(`/v1/positions/${MOCK_ADDRESS}/interest`)
.query({
vault_address: MOCK_VAULT_ADDRESS,
window: '7d',
})
.reply(200, responseWithExtras);

const result = await service.fetchInterest(MOCK_ADDRESS, {
vaultAddress: MOCK_VAULT_ADDRESS,
window: '7d',
});
expect(result).toStrictEqual(responseWithExtras);
service.destroy();
});

it('accepts unknown fields on history responses without stripping them', async () => {
const { service } = createService(Env.DEV);
const responseWithExtras = {
...MOCK_HISTORY_RESPONSE,
future_history_field: 'ok',
cash_flows: [
{
...MOCK_HISTORY_RESPONSE.cash_flows[0],
future_cash_flow_field: 42,
},
],
};

nock(MONEY_ACCOUNT_API_URL_MAP[Env.DEV])
.get(`/v1/positions/${MOCK_ADDRESS}/history`)
.reply(200, responseWithExtras);

const result = await service.fetchHistory(MOCK_ADDRESS);
expect(result).toStrictEqual(responseWithExtras);
service.destroy();
});

it('accepts unknown fields on rate-history responses without stripping them', async () => {
const { service } = createService(Env.DEV);
const responseWithExtras = {
...MOCK_RATE_HISTORY_RESPONSE,
future_rate_history_field: 'ok',
rates: [
{
...MOCK_RATE_HISTORY_RESPONSE.rates[0],
future_rate_entry_field: true,
},
MOCK_RATE_HISTORY_RESPONSE.rates[1],
],
};

nock(MONEY_ACCOUNT_API_URL_MAP[Env.DEV])
.get(`/v1/vaults/${MOCK_VAULT_ADDRESS}/rate-history`)
.reply(200, responseWithExtras);

const result = await service.fetchRateHistory(MOCK_VAULT_ADDRESS);
expect(result).toStrictEqual(responseWithExtras);
service.destroy();
});

it('accepts unknown fields on vault-rate responses without stripping them', async () => {
const { service } = createService(Env.DEV);
const responseWithExtras = {
...MOCK_VAULT_RATE_RESPONSE,
future_vault_rate_field: 'ok',
};

nock(MONEY_ACCOUNT_API_URL_MAP[Env.DEV])
.get(`/v1/vaults/${MOCK_VAULT_ADDRESS}/rate`)
.reply(200, responseWithExtras);

const result = await service.fetchVaultRate(MOCK_VAULT_ADDRESS);
expect(result).toStrictEqual(responseWithExtras);
service.destroy();
});
});

describe('invalidateQueries', () => {
it('invalidates cached queries', async () => {
const { service } = createService(Env.DEV);
Expand Down
26 changes: 15 additions & 11 deletions packages/money-account-api-data-service/src/structs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ import {
enums,
nullable,
number,
object,
optional,
string,
type,
} from '@metamask/superstruct';

// Every schema here uses `type()` rather than `object()` so that fields the
// Money Account API adds later pass validation instead of throwing. Additive
// backend changes should not be breaking changes for clients.

const DataFreshnessStruct = enums(['live', 'degraded']);

const VaultPositionStruct = object({
const VaultPositionStruct = type({
chain_id: number(),
vault_key: string(),
name: string(),
Expand All @@ -35,7 +39,7 @@ const VaultPositionStruct = object({
/**
* One underlying asset in the positions balance breakdown.
*/
const AssetBalanceStruct = object({
const AssetBalanceStruct = type({
asset_contract_address: string(),
asset_symbol: string(),
asset_decimals: number(),
Expand All @@ -52,7 +56,7 @@ const AssetBalanceStruct = object({
* `musd_balance`, `vmusd_value_in_musd`, and `total_balance` are mUSD-only
* and deprecated on the API in favor of `by_asset` and `total_balance_usd`.
*/
const PositionBalanceStruct = object({
const PositionBalanceStruct = type({
musd_balance: string(),
vmusd_value_in_musd: string(),
total_balance: string(),
Expand All @@ -62,7 +66,7 @@ const PositionBalanceStruct = object({
total_balance_usd: string(),
});

export const PositionResponseStruct = object({
export const PositionResponseStruct = type({
address: string(),
as_of_block: number(),
as_of_timestamp: string(),
Expand All @@ -74,7 +78,7 @@ export const PositionResponseStruct = object({
positions: array(VaultPositionStruct),
});

export const InterestResponseStruct = object({
export const InterestResponseStruct = type({
address: string(),
vault_address: string(),
window: string(),
Expand All @@ -89,7 +93,7 @@ export const InterestResponseStruct = object({
indexer_lag_seconds: number(),
});

const CashFlowEntryStruct = object({
const CashFlowEntryStruct = type({
type: enums(['deposit', 'withdraw', 'transfer_in', 'transfer_out']),
chain_id: number(),
vault_address: string(),
Expand All @@ -110,7 +114,7 @@ const CashFlowEntryStruct = object({
]),
});

export const HistoryResponseStruct = object({
export const HistoryResponseStruct = type({
address: string(),
cash_flows: array(CashFlowEntryStruct),
next_cursor: nullable(string()),
Expand All @@ -121,14 +125,14 @@ export const HistoryResponseStruct = object({
indexer_lag_seconds: number(),
});

const RateHistoryEntryStruct = object({
const RateHistoryEntryStruct = type({
timestamp: string(),
block_number: number(),
rate: string(),
tx_hash: string(),
});

export const RateHistoryResponseStruct = object({
export const RateHistoryResponseStruct = type({
vault_address: string(),
chain_id: number(),
range_start: string(),
Expand All @@ -140,7 +144,7 @@ export const RateHistoryResponseStruct = object({
indexer_lag_seconds: number(),
});

export const VaultRateResponseStruct = object({
export const VaultRateResponseStruct = type({
vault_address: string(),
chain_id: number(),
rate: string(),
Expand Down
Loading