From 1251b8676b9c9ca11739548a5510f810a1c8ae3b Mon Sep 17 00:00:00 2001 From: Prithpal Sooriya Date: Fri, 18 Oct 2024 11:27:32 +0100 Subject: [PATCH] feat: add and use Accounts API for Account balance calls (#4781) ## Explanation This integrates the Accounts API (Multi-chain Balances Endpoint) to help alleviate expensive RPC calls made by Token Detection. The aim is to attempt to use the Accounts API when making balance calls for expensive functionality (e.g. Token Detection)
Code Walkthrough https://www.loom.com/share/e540cae3967746b0aca343d4c59d0af6?sid=69c2556c-96d3-451e-bd67-7d03f32fff03
## References https://github.com/MetaMask/core/issues/4743 https://consensyssoftware.atlassian.net/browse/NOTIFY-1179 ## Changelog ### `@metamask/assets-controllers` - **ADDED**: MultiChain Accounts Service - **ADDED**: `fetchSupportedNetworks()` function to dynamically fetch supported networks by the Accounts API - **ADDED**: `fetchMultiChainBalances()` function to get balances for a given address - **ADDED**: `useAccountsAPI` to the `TokenDetectionController` constructor to enable/disable the accounts API feature. - **ADDED**: `#addDetectedTokensViaAPI()` private method in `TokenDetectionController` to get detected tokens via the Accounts API. - **CHANGED**: `detectTokens()` method in `TokenDetectionController` to try AccountsAPI first before using RPC flow to detect tokens. ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've highlighted breaking changes using the "BREAKING" category above as appropriate - [x] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes --- .../src/TokenDetectionController.test.ts | 237 ++++++++++++++++++ .../src/TokenDetectionController.ts | 171 ++++++++++++- .../src/multi-chain-accounts-service/index.ts | 9 + .../mocks/mock-get-balances.ts | 83 ++++++ .../mocks/mock-get-supported-networks.ts | 9 + .../multi-chain-accounts.test.ts | 87 +++++++ .../multi-chain-accounts.ts | 52 ++++ .../src/multi-chain-accounts-service/types.ts | 38 +++ 8 files changed, 681 insertions(+), 5 deletions(-) create mode 100644 packages/assets-controllers/src/multi-chain-accounts-service/index.ts create mode 100644 packages/assets-controllers/src/multi-chain-accounts-service/mocks/mock-get-balances.ts create mode 100644 packages/assets-controllers/src/multi-chain-accounts-service/mocks/mock-get-supported-networks.ts create mode 100644 packages/assets-controllers/src/multi-chain-accounts-service/multi-chain-accounts.test.ts create mode 100644 packages/assets-controllers/src/multi-chain-accounts-service/multi-chain-accounts.ts create mode 100644 packages/assets-controllers/src/multi-chain-accounts-service/types.ts diff --git a/packages/assets-controllers/src/TokenDetectionController.test.ts b/packages/assets-controllers/src/TokenDetectionController.test.ts index 72af3018be..fdce86d678 100644 --- a/packages/assets-controllers/src/TokenDetectionController.test.ts +++ b/packages/assets-controllers/src/TokenDetectionController.test.ts @@ -33,6 +33,12 @@ import { buildInfuraNetworkConfiguration, } from '../../network-controller/tests/helpers'; import { formatAggregatorNames } from './assetsUtil'; +import * as MutliChainAccountsServiceModule from './multi-chain-accounts-service'; +import { + MOCK_GET_BALANCES_RESPONSE, + createMockGetBalancesResponse, +} from './multi-chain-accounts-service/mocks/mock-get-balances'; +import { MOCK_GET_SUPPORTED_NETWORKS_RESPONSE } from './multi-chain-accounts-service/mocks/mock-get-supported-networks'; import { TOKEN_END_POINT_API } from './token-service'; import type { AllowedActions, @@ -46,9 +52,11 @@ import { } from './TokenDetectionController'; import { getDefaultTokenListState, + type TokenListMap, type TokenListState, type TokenListToken, } from './TokenListController'; +import type { Token } from './TokenRatesController'; import type { TokensController, TokensControllerState, @@ -173,9 +181,25 @@ function buildTokenDetectionControllerMessenger( }); } +const mockMultiChainAccountsService = () => { + const mockFetchSupportedNetworks = jest + .spyOn(MutliChainAccountsServiceModule, 'fetchSupportedNetworks') + .mockResolvedValue(MOCK_GET_SUPPORTED_NETWORKS_RESPONSE.fullSupport); + const mockFetchMultiChainBalances = jest + .spyOn(MutliChainAccountsServiceModule, 'fetchMultiChainBalances') + .mockResolvedValue(MOCK_GET_BALANCES_RESPONSE); + + return { + mockFetchSupportedNetworks, + mockFetchMultiChainBalances, + }; +}; + describe('TokenDetectionController', () => { const defaultSelectedAccount = createMockInternalAccount(); + mockMultiChainAccountsService(); + beforeEach(async () => { nock(TOKEN_END_POINT_API) .get(getTokensPath(ChainId.mainnet)) @@ -2236,6 +2260,218 @@ describe('TokenDetectionController', () => { }, ); }); + + /** + * Test Utility - Arrange and Act `detectTokens()` with the Accounts API feature + * RPC flow will return `sampleTokenA` and the Accounts API flow will use `sampleTokenB` + * @param props - options to modify these tests + * @param props.overrideMockTokensCache - change the tokens cache + * @param props.mockMultiChainAPI - change the Accounts API responses + * @param props.overrideMockTokenGetState - change the external TokensController state + * @returns properties that can be used for assertions + */ + const arrangeActTestDetectTokensWithAccountsAPI = async (props?: { + /** Overwrite the tokens cache inside Tokens Controller */ + overrideMockTokensCache?: (typeof sampleTokenA)[]; + mockMultiChainAPI?: ReturnType; + overrideMockTokenGetState?: Partial; + }) => { + const { + overrideMockTokensCache = [sampleTokenA, sampleTokenB], + mockMultiChainAPI, + overrideMockTokenGetState, + } = props ?? {}; + + // Arrange - RPC Tokens Flow - Uses sampleTokenA + const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({ + [sampleTokenA.address]: new BN(1), + }); + + // Arrange - API Tokens Flow - Uses sampleTokenB + const { mockFetchSupportedNetworks, mockFetchMultiChainBalances } = + mockMultiChainAPI ?? mockMultiChainAccountsService(); + + if (!mockMultiChainAPI) { + mockFetchSupportedNetworks.mockResolvedValue([1]); + mockFetchMultiChainBalances.mockResolvedValue( + createMockGetBalancesResponse([sampleTokenB.address], 1), + ); + } + + // Arrange - Selected Account + const selectedAccount = createMockInternalAccount({ + address: '0x0000000000000000000000000000000000000001', + }); + + // Arrange / Act - withController setup + invoke detectTokens + const { callAction } = await withController( + { + options: { + disabled: false, + getBalancesInSingleCall: mockGetBalancesInSingleCall, + useAccountsAPI: true, // USING ACCOUNTS API + }, + mocks: { + getSelectedAccount: selectedAccount, + getAccount: selectedAccount, + }, + }, + async ({ + controller, + mockTokenListGetState, + callActionSpy, + mockTokensGetState, + }) => { + const tokenCacheData: TokenListMap = {}; + overrideMockTokensCache.forEach( + (t) => + (tokenCacheData[t.address] = { + name: t.name, + symbol: t.symbol, + decimals: t.decimals, + address: t.address, + occurrences: 1, + aggregators: t.aggregators, + iconUrl: t.image, + }), + ); + + mockTokenListGetState({ + ...getDefaultTokenListState(), + tokensChainsCache: { + '0x1': { + timestamp: 0, + data: tokenCacheData, + }, + }, + }); + + if (overrideMockTokenGetState) { + mockTokensGetState({ + ...getDefaultTokensState(), + ...overrideMockTokenGetState, + }); + } + + // Act + await controller.detectTokens({ + networkClientId: NetworkType.mainnet, + selectedAddress: selectedAccount.address, + }); + + return { + callAction: callActionSpy, + }; + }, + ); + + const assertAddedTokens = (token: Token) => + expect(callAction).toHaveBeenCalledWith( + 'TokensController:addDetectedTokens', + [token], + { + chainId: ChainId.mainnet, + selectedAddress: selectedAccount.address, + }, + ); + + const assertTokensNeverAdded = () => + expect(callAction).not.toHaveBeenCalledWith( + 'TokensController:addDetectedTokens', + ); + + return { + assertAddedTokens, + assertTokensNeverAdded, + mockFetchMultiChainBalances, + mockGetBalancesInSingleCall, + rpcToken: sampleTokenA, + apiToken: sampleTokenB, + }; + }; + + it('should trigger and use Accounts API for detection', async () => { + const { + assertAddedTokens, + mockFetchMultiChainBalances, + apiToken, + mockGetBalancesInSingleCall, + } = await arrangeActTestDetectTokensWithAccountsAPI(); + + expect(mockFetchMultiChainBalances).toHaveBeenCalled(); + expect(mockGetBalancesInSingleCall).not.toHaveBeenCalled(); + assertAddedTokens(apiToken); + }); + + it('uses the Accounts API but does not add unknown tokens', async () => { + // API returns sampleTokenB + // As this is not a known token (in cache), then is not added + const { + assertTokensNeverAdded, + mockFetchMultiChainBalances, + mockGetBalancesInSingleCall, + } = await arrangeActTestDetectTokensWithAccountsAPI({ + overrideMockTokensCache: [sampleTokenA], + }); + + expect(mockFetchMultiChainBalances).toHaveBeenCalled(); + expect(mockGetBalancesInSingleCall).not.toHaveBeenCalled(); + assertTokensNeverAdded(); + }); + + it('fallbacks from using the Accounts API if fails', async () => { + // Test 1 - fetch supported networks fails + let mockAPI = mockMultiChainAccountsService(); + mockAPI.mockFetchSupportedNetworks.mockRejectedValue( + new Error('Mock Error'), + ); + let actResult = await arrangeActTestDetectTokensWithAccountsAPI({ + mockMultiChainAPI: mockAPI, + }); + + expect(actResult.mockFetchMultiChainBalances).not.toHaveBeenCalled(); // never called as could not fetch supported networks... + expect(actResult.mockGetBalancesInSingleCall).toHaveBeenCalled(); // ...so then RPC flow was initiated + actResult.assertAddedTokens(actResult.rpcToken); + + // Test 2 - fetch multi chain fails + mockAPI = mockMultiChainAccountsService(); + mockAPI.mockFetchMultiChainBalances.mockRejectedValue( + new Error('Mock Error'), + ); + actResult = await arrangeActTestDetectTokensWithAccountsAPI({ + mockMultiChainAPI: mockAPI, + }); + + expect(actResult.mockFetchMultiChainBalances).toHaveBeenCalled(); // API was called, but failed... + expect(actResult.mockGetBalancesInSingleCall).toHaveBeenCalled(); // ...so then RPC flow was initiated + actResult.assertAddedTokens(actResult.rpcToken); + }); + + it('uses the Accounts API but does not add tokens that are already added', async () => { + // Here we populate the token state with a token that exists in the tokenAPI. + // So the token retrieved from the API should not be added + const { assertTokensNeverAdded, mockFetchMultiChainBalances } = + await arrangeActTestDetectTokensWithAccountsAPI({ + overrideMockTokenGetState: { + allDetectedTokens: { + '0x1': { + '0x0000000000000000000000000000000000000001': [ + { + address: sampleTokenB.address, + name: sampleTokenB.name, + symbol: sampleTokenB.symbol, + decimals: sampleTokenB.decimals, + aggregators: sampleTokenB.aggregators, + }, + ], + }, + }, + }, + }); + + expect(mockFetchMultiChainBalances).toHaveBeenCalled(); + assertTokensNeverAdded(); + }); }); }); @@ -2415,6 +2651,7 @@ async function withController( getBalancesInSingleCall: jest.fn(), trackMetaMetricsEvent: jest.fn(), messenger: buildTokenDetectionControllerMessenger(controllerMessenger), + useAccountsAPI: false, ...options, }); try { diff --git a/packages/assets-controllers/src/TokenDetectionController.ts b/packages/assets-controllers/src/TokenDetectionController.ts index 2459baea38..7c7b9e60d8 100644 --- a/packages/assets-controllers/src/TokenDetectionController.ts +++ b/packages/assets-controllers/src/TokenDetectionController.ts @@ -9,7 +9,12 @@ import type { ControllerStateChangeEvent, } from '@metamask/base-controller'; import contractMap from '@metamask/contract-metadata'; -import { ChainId, safelyExecute } from '@metamask/controller-utils'; +import { + ASSET_TYPES, + ChainId, + ERC20, + safelyExecute, +} from '@metamask/controller-utils'; import type { KeyringControllerGetStateAction, KeyringControllerLockEvent, @@ -28,9 +33,14 @@ import type { PreferencesControllerStateChangeEvent, } from '@metamask/preferences-controller'; import type { Hex } from '@metamask/utils'; +import { hexToNumber } from '@metamask/utils'; import type { AssetsContractController } from './AssetsContractController'; import { isTokenDetectionSupportedForNetwork } from './assetsUtil'; +import { + fetchMultiChainBalances, + fetchSupportedNetworks, +} from './multi-chain-accounts-service'; import type { GetTokenListState, TokenListMap, @@ -191,6 +201,48 @@ export class TokenDetectionController extends StaticIntervalPollingController void; + #accountsAPI = { + isAccountsAPIEnabled: true, + supportedNetworksCache: null as number[] | null, + async getSupportedNetworks() { + /* istanbul ignore next */ + if (!this.isAccountsAPIEnabled) { + throw new Error('Accounts API Feature Switch is disabled'); + } + + /* istanbul ignore next */ + if (this.supportedNetworksCache) { + return this.supportedNetworksCache; + } + + const result = await fetchSupportedNetworks().catch(() => null); + this.supportedNetworksCache = result; + return result; + }, + + async getMultiChainBalances(address: string, chainId: Hex) { + if (!this.isAccountsAPIEnabled) { + throw new Error('Accounts API Feature Switch is disabled'); + } + + const chainIdNumber = hexToNumber(chainId); + const supportedNetworks = await this.getSupportedNetworks(); + + if (!supportedNetworks || !supportedNetworks.includes(chainIdNumber)) { + const supportedNetworksErrStr = (supportedNetworks ?? []).toString(); + throw new Error( + `Unsupported Network: supported networks ${supportedNetworksErrStr}, network: ${chainIdNumber}`, + ); + } + + const result = await fetchMultiChainBalances(address, { + networks: [chainIdNumber], + }); + + return result.balances; + }, + }; + /** * Creates a TokenDetectionController instance. * @@ -200,6 +252,7 @@ export class TokenDetectionController extends StaticIntervalPollingController void; messenger: TokenDetectionControllerMessenger; + useAccountsAPI?: boolean; }) { super({ name: controllerName, @@ -257,6 +312,8 @@ export class TokenDetectionController extends StaticIntervalPollingController + tokenCandidateSlices, + }); + if (accountAPIResult?.result === 'success') { + return; + } + + // Attempt RPC Detection + const tokenDetectionPromises = tokenCandidateSlices.map((tokensSlice) => this.#addDetectedTokens({ tokensSlice, selectedAddress: addressAgainstWhichToDetect, @@ -578,6 +648,97 @@ export class TokenDetectionController extends StaticIntervalPollingController { + const tokenBalances = await this.#accountsAPI + .getMultiChainBalances(selectedAddress, chainId) + .catch(() => null); + + if (!tokenBalances || tokenBalances.length === 0) { + return { result: 'failed' } as const; + } + + const tokensWithBalance: Token[] = []; + const eventTokensDetails: string[] = []; + + const tokenCandidateSet = new Set(tokenCandidateSlices.flat()); + + tokenBalances.forEach((token) => { + const tokenAddress = token.address; + + // Make sure that the token to add is in our candidate list + // Ensures we don't add tokens we already own + if (!tokenCandidateSet.has(token.address)) { + return; + } + + // We need specific data from tokenList to correctly create a token + // So even if we have a token that was detected correctly by the API, if its missing data we cannot safely add it. + if (!this.#tokenList[token.address]) { + return; + } + + const { decimals, symbol, aggregators, iconUrl, name } = + this.#tokenList[token.address]; + eventTokensDetails.push(`${symbol} - ${tokenAddress}`); + tokensWithBalance.push({ + address: tokenAddress, + decimals, + symbol, + aggregators, + image: iconUrl, + isERC721: false, + name, + }); + }); + + if (tokensWithBalance.length) { + this.#trackMetaMetricsEvent({ + event: 'Token Detected', + category: 'Wallet', + properties: { + tokens: eventTokensDetails, + // TODO: Either fix this lint violation or explain why it's necessary to ignore. + // eslint-disable-next-line @typescript-eslint/naming-convention + token_standard: ERC20, + // TODO: Either fix this lint violation or explain why it's necessary to ignore. + // eslint-disable-next-line @typescript-eslint/naming-convention + asset_type: ASSET_TYPES.TOKEN, + }, + }); + + await this.messagingSystem.call( + 'TokensController:addDetectedTokens', + tokensWithBalance, + { + selectedAddress, + chainId, + }, + ); + } + + return { result: 'success' } as const; + }); + } + async #addDetectedTokens({ tokensSlice, selectedAddress, @@ -621,10 +782,10 @@ export class TokenDetectionController extends StaticIntervalPollingController ({ + count: tokenAddrs.length, + balances: tokenAddrs.map((a) => ({ + object: 'token', + address: a, + name: 'Mock Token', + symbol: 'MOCK', + decimals: 18, + balance: '10.18', + chainId, + })), + unprocessedNetworks: [], +}); diff --git a/packages/assets-controllers/src/multi-chain-accounts-service/mocks/mock-get-supported-networks.ts b/packages/assets-controllers/src/multi-chain-accounts-service/mocks/mock-get-supported-networks.ts new file mode 100644 index 0000000000..7337015072 --- /dev/null +++ b/packages/assets-controllers/src/multi-chain-accounts-service/mocks/mock-get-supported-networks.ts @@ -0,0 +1,9 @@ +import type { GetSupportedNetworksResponse } from '../types'; + +export const MOCK_GET_SUPPORTED_NETWORKS_RESPONSE: GetSupportedNetworksResponse = + { + fullSupport: [1, 137, 56, 59144, 8453, 10, 42161, 534352], + partialSupport: { + balances: [59141, 42220, 43114], + }, + }; diff --git a/packages/assets-controllers/src/multi-chain-accounts-service/multi-chain-accounts.test.ts b/packages/assets-controllers/src/multi-chain-accounts-service/multi-chain-accounts.test.ts new file mode 100644 index 0000000000..0545a5ef4a --- /dev/null +++ b/packages/assets-controllers/src/multi-chain-accounts-service/multi-chain-accounts.test.ts @@ -0,0 +1,87 @@ +import nock from 'nock'; + +import { MOCK_GET_BALANCES_RESPONSE } from './mocks/mock-get-balances'; +import { MOCK_GET_SUPPORTED_NETWORKS_RESPONSE } from './mocks/mock-get-supported-networks'; +import { + MULTICHAIN_ACCOUNTS_DOMAIN, + fetchMultiChainBalances, + fetchSupportedNetworks, +} from './multi-chain-accounts'; + +const MOCK_ADDRESS = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'; + +describe('fetchSupportedNetworks()', () => { + const createMockAPI = () => + nock(MULTICHAIN_ACCOUNTS_DOMAIN).get('/v1/supportedNetworks'); + + it('should successfully return supported networks array', async () => { + const mockAPI = createMockAPI().reply( + 200, + MOCK_GET_SUPPORTED_NETWORKS_RESPONSE, + ); + + const result = await fetchSupportedNetworks(); + expect(result).toStrictEqual( + MOCK_GET_SUPPORTED_NETWORKS_RESPONSE.fullSupport, + ); + expect(mockAPI.isDone()).toBe(true); + }); + + it('should throw error when fetch fails', async () => { + const mockAPI = createMockAPI().reply(500); + + await expect(async () => await fetchSupportedNetworks()).rejects.toThrow( + expect.any(Error), + ); + expect(mockAPI.isDone()).toBe(true); + }); +}); + +describe('fetchMultiChainBalances()', () => { + const createMockAPI = () => + nock(MULTICHAIN_ACCOUNTS_DOMAIN).get( + `/v2/accounts/${MOCK_ADDRESS}/balances`, + ); + + it('should successfully return balances response', async () => { + const mockAPI = createMockAPI().reply(200, MOCK_GET_BALANCES_RESPONSE); + + const result = await fetchMultiChainBalances(MOCK_ADDRESS); + expect(result).toBeDefined(); + expect(result).toStrictEqual(MOCK_GET_BALANCES_RESPONSE); + expect(mockAPI.isDone()).toBe(true); + }); + + it('should successfully return balances response with query params to refine search', async () => { + const mockAPI = createMockAPI() + .query({ + networks: '1,10', + }) + .reply(200, MOCK_GET_BALANCES_RESPONSE); + + const result = await fetchMultiChainBalances(MOCK_ADDRESS, { + networks: [1, 10], + }); + expect(result).toBeDefined(); + expect(result).toStrictEqual(MOCK_GET_BALANCES_RESPONSE); + expect(mockAPI.isDone()).toBe(true); + }); + + const testMatrix = [ + { httpCode: 429, httpCodeName: 'Too Many Requests' }, // E.g. Rate Limit + { httpCode: 422, httpCodeName: 'Unprocessable Content' }, // E.g. fails to fetch any balances from specified chains + { httpCode: 500, httpCodeName: 'Internal Server Error' }, // E.g. Server Rekt + ]; + + it.each(testMatrix)( + 'should throw when $httpCode "$httpCodeName"', + async ({ httpCode }) => { + const mockAPI = createMockAPI().reply(httpCode); + + await expect( + async () => await fetchMultiChainBalances(MOCK_ADDRESS), + ).rejects.toThrow(expect.any(Error)); + expect(mockAPI.isDone()).toBe(true); + }, + ); +}); diff --git a/packages/assets-controllers/src/multi-chain-accounts-service/multi-chain-accounts.ts b/packages/assets-controllers/src/multi-chain-accounts-service/multi-chain-accounts.ts new file mode 100644 index 0000000000..f40afd2809 --- /dev/null +++ b/packages/assets-controllers/src/multi-chain-accounts-service/multi-chain-accounts.ts @@ -0,0 +1,52 @@ +import { handleFetch } from '@metamask/controller-utils'; + +import type { + GetBalancesQueryParams, + GetBalancesResponse, + GetSupportedNetworksResponse, +} from './types'; + +export const MULTICHAIN_ACCOUNTS_DOMAIN = 'https://accounts.api.cx.metamask.io'; + +const getBalancesUrl = ( + address: string, + queryParams?: GetBalancesQueryParams, +) => { + const url = new URL( + `${MULTICHAIN_ACCOUNTS_DOMAIN}/v2/accounts/${address}/balances`, + ); + + if (queryParams?.networks !== undefined) { + url.searchParams.append('networks', queryParams.networks); + } + + return url; +}; + +/** + * Fetches Supported Networks. + * @returns supported networks (decimal) + */ +export async function fetchSupportedNetworks(): Promise { + const url = new URL(`${MULTICHAIN_ACCOUNTS_DOMAIN}/v1/supportedNetworks`); + const response: GetSupportedNetworksResponse = await handleFetch(url); + return response.fullSupport; +} + +/** + * Fetches Balances for multiple networks. + * @param address - address to fetch balances from + * @param options - params to pass down for a more refined search + * @param options.networks - the networks (in decimal) that you want to filter by + * @returns a Balances Response + */ +export async function fetchMultiChainBalances( + address: string, + options?: { networks?: number[] }, +) { + const url = getBalancesUrl(address, { + networks: options?.networks?.join(), + }); + const response: GetBalancesResponse = await handleFetch(url); + return response; +} diff --git a/packages/assets-controllers/src/multi-chain-accounts-service/types.ts b/packages/assets-controllers/src/multi-chain-accounts-service/types.ts new file mode 100644 index 0000000000..3778d3a671 --- /dev/null +++ b/packages/assets-controllers/src/multi-chain-accounts-service/types.ts @@ -0,0 +1,38 @@ +export type GetSupportedNetworksResponse = { + fullSupport: number[]; + partialSupport: { + balances: number[]; + }; +}; + +export type GetBalancesQueryParams = { + /** Comma-separated network/chain IDs */ + networks?: string; + /** Whether or not to filter the assets to contain only the tokens existing in the Token API */ + filterSupportedTokens?: boolean; + /** Specific token addresses to fetch balances for across specified network(s) */ + includeTokenAddresses?: string; + /** Whether to include balances of the account's staked asset balances */ + includeStakedAssets?: boolean; +}; + +export type GetBalancesResponse = { + count: number; + balances: { + /** Underlying object type. Seems to be always `token` */ + object: string; + /** Token Type: This is only supplied as `native` to native chain tokens (e.g. - ETH, POL) */ + type?: string; + /** Timestamp is only provided for `native` chain tokens */ + timestamp?: string; + address: string; + symbol: string; + name: string; + decimals: number; + chainId: number; + /** string representation of the balance in decimal format (decimals adjusted). e.g. - 123.456789 */ + balance: string; + }[]; + /** networks that failed to process, if no network is processed, returns HTTP 422 */ + unprocessedNetworks: number[]; +};