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
4 changes: 4 additions & 0 deletions packages/core-backend/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `AccountsApiClient` support for the Accounts API `/v6/multiaccount/balances` endpoint via `fetchV6MultiAccountBalances` / `getV6MultiAccountBalancesQueryOptions`, returning token balances plus optional DeFi positions and spot prices (new types `V6BalancesResponse`, `V6AccountBalancesEntry`, `V6BalanceItem`, `V6BalanceMetadata`, `V6TokenMetadata`, `V6VsCurrency`) ([#9302](https://github.com/MetaMask/core/pull/9302))

## [6.4.0]

### Changed
Expand Down
1 change: 1 addition & 0 deletions packages/core-backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,7 @@ Handles account-related operations including balances, transactions, NFTs, and t
| `fetchV2Balances(address, queryOptions?, options?)` | Get balances for single address (supports networks, filterSupportedTokens, includeTokenAddresses, includeStakedAssets) |
| `fetchV4MultiAccountBalances(addresses, queryOptions?, options?)` | Get balances for multiple addresses |
| `fetchV5MultiAccountBalances(accountIds, queryOptions?, options?)` | Get balances using CAIP-10 IDs |
| `fetchV6MultiAccountBalances(accountIds, queryOptions?, options?)` | Get balances + DeFi positions + spot prices using CAIP-10 IDs |
| `fetchV1TransactionByHash(chainId, txHash, queryOptions?, options?)` | Get transaction by hash |
| `fetchV1AccountTransactions(address, queryOptions?, options?)` | Get account transactions |
| `fetchV4MultiAccountTransactions(accountAddresses, queryOptions?, options?)` | Get multi-account transactions |
Expand Down
131 changes: 131 additions & 0 deletions packages/core-backend/src/api/accounts/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
V2SupportedNetworksResponse,
V2BalancesResponse,
V5BalancesResponse,
V6BalancesResponse,
} from './types';

describe('AccountsApiClient', () => {
Expand Down Expand Up @@ -251,6 +252,89 @@ describe('AccountsApiClient', () => {
});
expect(mockFetch).not.toHaveBeenCalled();
});

it('fetches v6 multi-account balances with token and defi rows', async () => {
const mockResponse: V6BalancesResponse = {
unprocessedNetworks: ['eip155:1329'],
unprocessedIncludeAssetIds: ['eip155:1/erc20:0xabc'],
accounts: [
{
accountId: 'eip155:1:0x123',
balances: [
{
category: 'token',
assetId: 'eip155:1/erc20:0xc02aaa39',
name: 'Wrapped Ether',
symbol: 'WETH',
decimals: 18,
balance: '0.283549083429656057',
price: '2119.66',
},
{
category: 'token',
assetId:
'stellar:pubnet/asset:USDC-GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN',
name: 'USD Coin',
symbol: 'USDC',
decimals: 7,
balance: '10.5',
metadata: {
limit: '9223372036854775807',
authorized: true,
},
},
{
category: 'defi',
assetId: 'eip155:1/erc20:0x4fef9d74',
name: 'MetaMask Swaps',
symbol: 'MMS',
decimals: 18,
balance: '1.0',
metadata: {
protocolId: 'metamask',
protocolName: 'MetaMask Swaps',
description: 'MetaMask Swaps on ethereum',
protocolUrl: 'https://metamask.io/',
protocolIconUrl: 'https://example.com/icon.jpg',
positionType: 'deposit',
poolAddress: '0x4fef9d741011476750a243ac70b9789a63dd47df',
},
},
],
processingDefiPositions: false,
},
],
};
mockFetch.mockResolvedValueOnce(createMockResponse(mockResponse));

const result = await client.accounts.fetchV6MultiAccountBalances(
['eip155:1:0x123'],
{
includeDeFiBalances: true,
includePrices: true,
vsCurrency: 'usd',
includeAssetIds: ['eip155:1/erc20:0xabc'],
},
);

expect(result).toStrictEqual(mockResponse);
const calledUrl = mockFetch.mock.calls[0]?.[0] as string;
expect(calledUrl).toContain('/v6/multiaccount/balances');
expect(calledUrl).toContain('includeDeFiBalances=true');
expect(calledUrl).toContain('includePrices=true');
expect(calledUrl).toContain('vsCurrency=usd');
});

it('returns empty balances for empty accountIds in v6', async () => {
const result = await client.accounts.fetchV6MultiAccountBalances([]);

expect(result).toStrictEqual({
unprocessedNetworks: [],
unprocessedIncludeAssetIds: [],
accounts: [],
});
expect(mockFetch).not.toHaveBeenCalled();
});
});

describe('Transactions', () => {
Expand Down Expand Up @@ -752,6 +836,32 @@ describe('AccountsApiClient', () => {
]);
});

it('getV6MultiAccountBalancesQueryOptions includes sorted networks, includeAssetIds and excludeAssetIds in queryKey when provided', () => {
const options = client.accounts.getV6MultiAccountBalancesQueryOptions(
['eip155:1:0xzzz', 'eip155:1:0xaaa'],
{
networks: ['eip155:137', 'eip155:1'],
includeAssetIds: ['eip155:1/erc20:0xddd', 'eip155:1/erc20:0xccc'],
excludeAssetIds: ['eip155:1/erc20:0xbbb', 'eip155:1/erc20:0xaaa'],
includeDeFiBalances: true,
},
);
expect(options.queryKey).toStrictEqual([
'accounts',
'balances',
'v6',
{
accountIds: ['eip155:1:0xaaa', 'eip155:1:0xzzz'],
options: {
networks: ['eip155:1', 'eip155:137'],
includeAssetIds: ['eip155:1/erc20:0xccc', 'eip155:1/erc20:0xddd'],
excludeAssetIds: ['eip155:1/erc20:0xaaa', 'eip155:1/erc20:0xbbb'],
includeDeFiBalances: true,
},
},
]);
});

it('getV2AccountNftsQueryOptions includes sorted networks in queryKey when queryOptions.networks provided', () => {
const options = client.accounts.getV2AccountNftsQueryOptions('0x123', {
networks: [137, 1],
Expand Down Expand Up @@ -868,6 +978,27 @@ describe('AccountsApiClient', () => {
expect(mockFetch).not.toHaveBeenCalled();
});

it('getV6MultiAccountBalancesQueryOptions queryFn returns empty result for empty accountIds without calling fetch', async () => {
const options = client.accounts.getV6MultiAccountBalancesQueryOptions([]);
const { queryFn } = options;
if (typeof queryFn !== 'function') {
throw new Error('queryFn is required');
}
const result = await queryFn({
client: client.queryClient,
queryKey: options.queryKey,
signal: new AbortController().signal,
meta: undefined,
});

expect(result).toStrictEqual({
unprocessedNetworks: [],
unprocessedIncludeAssetIds: [],
accounts: [],
});
expect(mockFetch).not.toHaveBeenCalled();
});

it('getV1AccountTransactionsQueryOptions queryFn returns empty result for empty address without calling fetch', async () => {
const options = client.accounts.getV1AccountTransactionsQueryOptions('');
const { queryFn } = options;
Expand Down
156 changes: 156 additions & 0 deletions packages/core-backend/src/api/accounts/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import type {
V2BalancesResponse,
V4BalancesResponse,
V5BalancesResponse,
V6BalancesResponse,
V6VsCurrency,
V1TransactionByHashResponse,
V1AccountTransactionsResponse,
V4MultiAccountTransactionsResponse,
Expand Down Expand Up @@ -489,6 +491,160 @@ export class AccountsApiClient extends BaseApiClient {
);
}

/**
* Returns the TanStack Query options object for v6 multi-account balances.
* The v6 endpoint returns token balances and, optionally, DeFi positions and
* spot prices.
*
* @param accountIds - Array of CAIP-10 account IDs.
* @param queryOptions - Query filter options.
* @param queryOptions.networks - Comma-separated CAIP-2 chain IDs to filter by.
* @param queryOptions.filterSupportedTokens - Whether to filter the assets to only tokens existing in the Token API.
* @param queryOptions.startTimestamp - Start timestamp (epoch) from which to return results.
* @param queryOptions.endTimestamp - End timestamp (epoch) for which to return results.
* @param queryOptions.includeLabels - Whether to include asset metadata labels in the response.
* @param queryOptions.includeCanonicalHead - Whether to include the canonical head asset ID in the response.
* @param queryOptions.includeDeFiBalances - Whether to include DeFi positions (token balances are always returned).
* @param queryOptions.forceFetchDeFiPositions - Whether to fetch DeFi positions for all accounts, skipping the cached non-DeFi-user check.
* @param queryOptions.includePrices - Whether to include spot prices for each token and DeFi position asset.
* @param queryOptions.vsCurrency - Quote currency for spot prices when `includePrices` is true (default `usd`).
* @param queryOptions.includeAssetIds - ERC-20 CAIP-19 asset IDs to confirm detection for; undetected IDs are returned in `unprocessedIncludeAssetIds`.
* @param queryOptions.excludeAssetIds - ERC-20 CAIP-19 asset IDs to exclude from balance results.
* @param options - Fetch options including cache settings.
* @returns TanStack Query options for use with useQuery, useSuspenseQuery, etc.
*/
getV6MultiAccountBalancesQueryOptions(
accountIds: string[],
queryOptions?: {
networks?: string[];
filterSupportedTokens?: boolean;
startTimestamp?: number;
endTimestamp?: number;
includeLabels?: boolean;
includeCanonicalHead?: boolean;
includeDeFiBalances?: boolean;
forceFetchDeFiPositions?: boolean;
includePrices?: boolean;
vsCurrency?: V6VsCurrency;
includeAssetIds?: string[];
excludeAssetIds?: string[];
},
options?: FetchOptions,
): FetchQueryOptions<V6BalancesResponse> {
return {
queryKey: [
'accounts',
'balances',
'v6',
{
accountIds: [...accountIds].sort(),
options: queryOptions && {
...queryOptions,
networks:
queryOptions.networks && [...queryOptions.networks].sort(),
includeAssetIds:
queryOptions.includeAssetIds &&
[...queryOptions.includeAssetIds].sort(),
excludeAssetIds:
queryOptions.excludeAssetIds &&
[...queryOptions.excludeAssetIds].sort(),
},
},
],
queryFn: async ({
signal,
}: QueryFunctionContext): Promise<V6BalancesResponse> => {
if (accountIds.length === 0) {
return {
unprocessedNetworks: [],
unprocessedIncludeAssetIds: [],
accounts: [],
};
}
return this.fetch<V6BalancesResponse>(
API_URLS.ACCOUNTS,
'/v6/multiaccount/balances',
{
signal,
params: {
accountIds,
networks: queryOptions?.networks,
filterSupportedTokens: queryOptions?.filterSupportedTokens,
startTimestamp: queryOptions?.startTimestamp,
endTimestamp: queryOptions?.endTimestamp,
includeLabels: queryOptions?.includeLabels,
includeCanonicalHead: queryOptions?.includeCanonicalHead,
includeDeFiBalances: queryOptions?.includeDeFiBalances,
forceFetchDeFiPositions: queryOptions?.forceFetchDeFiPositions,
includePrices: queryOptions?.includePrices,
vsCurrency: queryOptions?.vsCurrency,
includeAssetIds: queryOptions?.includeAssetIds,
excludeAssetIds: queryOptions?.excludeAssetIds,
},
},
);
},
...getQueryOptionsOverrides(options),
staleTime: options?.staleTime ?? STALE_TIMES.BALANCES,
gcTime: options?.gcTime ?? GC_TIMES.DEFAULT,
};
}

/**
* Get balances and DeFi positions for multiple accounts using CAIP-10 IDs
* (v6 endpoint).
*
* @param accountIds - Array of CAIP-10 account IDs.
* @param queryOptions - Query filter options.
* @param queryOptions.networks - Comma-separated CAIP-2 chain IDs to filter by.
* @param queryOptions.filterSupportedTokens - Whether to filter the assets to only tokens existing in the Token API.
* @param queryOptions.startTimestamp - Start timestamp (epoch) from which to return results.
* @param queryOptions.endTimestamp - End timestamp (epoch) for which to return results.
* @param queryOptions.includeLabels - Whether to include asset metadata labels in the response.
* @param queryOptions.includeCanonicalHead - Whether to include the canonical head asset ID in the response.
* @param queryOptions.includeDeFiBalances - Whether to include DeFi positions (token balances are always returned).
* @param queryOptions.forceFetchDeFiPositions - Whether to fetch DeFi positions for all accounts, skipping the cached non-DeFi-user check.
* @param queryOptions.includePrices - Whether to include spot prices for each token and DeFi position asset.
* @param queryOptions.vsCurrency - Quote currency for spot prices when `includePrices` is true (default `usd`).
* @param queryOptions.includeAssetIds - ERC-20 CAIP-19 asset IDs to confirm detection for; undetected IDs are returned in `unprocessedIncludeAssetIds`.
* @param queryOptions.excludeAssetIds - ERC-20 CAIP-19 asset IDs to exclude from balance results.
* @param options - Fetch options including cache settings.
* @returns The multi-account balances and DeFi positions response.
*/
async fetchV6MultiAccountBalances(
accountIds: string[],
queryOptions?: {
networks?: string[];
filterSupportedTokens?: boolean;
startTimestamp?: number;
endTimestamp?: number;
includeLabels?: boolean;
includeCanonicalHead?: boolean;
includeDeFiBalances?: boolean;
forceFetchDeFiPositions?: boolean;
includePrices?: boolean;
vsCurrency?: V6VsCurrency;
includeAssetIds?: string[];
excludeAssetIds?: string[];
},
options?: FetchOptions,
): Promise<V6BalancesResponse> {
if (accountIds.length === 0) {
return {
unprocessedNetworks: [],
unprocessedIncludeAssetIds: [],
accounts: [],
};
}
return this.queryClient.fetchQuery(
this.getV6MultiAccountBalancesQueryOptions(
accountIds,
queryOptions,
options,
),
);
}

// ==========================================================================
// TRANSACTIONS
// ==========================================================================
Expand Down
6 changes: 6 additions & 0 deletions packages/core-backend/src/api/accounts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ export type {
V2BalanceItem,
V2BalancesResponse,
V4BalancesResponse,
V6VsCurrency,
V6BalanceMetadata,
V6TokenMetadata,
V6BalanceItem,
V6AccountBalancesEntry,
V6BalancesResponse,
V1SupportedNetworksResponse,
V2SupportedNetworksResponse,
V2ActiveNetworksResponse,
Expand Down
Loading
Loading