@metamask-previews/assets-controller 14.0.3-preview-6b7c25c → 14.0.3-preview-00a181c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/dist/AssetsController.cjs +20 -103
- package/dist/AssetsController.cjs.map +1 -1
- package/dist/AssetsController.d.cts +5 -5
- package/dist/AssetsController.d.cts.map +1 -1
- package/dist/AssetsController.d.mts +5 -5
- package/dist/AssetsController.d.mts.map +1 -1
- package/dist/AssetsController.mjs +20 -103
- package/dist/AssetsController.mjs.map +1 -1
- package/dist/data-sources/RpcDataSource.cjs +9 -0
- package/dist/data-sources/RpcDataSource.cjs.map +1 -1
- package/dist/data-sources/RpcDataSource.d.cts.map +1 -1
- package/dist/data-sources/RpcDataSource.d.mts.map +1 -1
- package/dist/data-sources/RpcDataSource.mjs +9 -0
- package/dist/data-sources/RpcDataSource.mjs.map +1 -1
- package/dist/middlewares/RpcFallbackMiddleware.cjs +142 -14
- package/dist/middlewares/RpcFallbackMiddleware.cjs.map +1 -1
- package/dist/middlewares/RpcFallbackMiddleware.d.cts +14 -6
- package/dist/middlewares/RpcFallbackMiddleware.d.cts.map +1 -1
- package/dist/middlewares/RpcFallbackMiddleware.d.mts +14 -6
- package/dist/middlewares/RpcFallbackMiddleware.d.mts.map +1 -1
- package/dist/middlewares/RpcFallbackMiddleware.mjs +142 -14
- package/dist/middlewares/RpcFallbackMiddleware.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RpcFallbackMiddleware.cjs","sourceRoot":"","sources":["../../src/middlewares/RpcFallbackMiddleware.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,6CAAiE;AACjE,2CAA2C;AAO3C,oEAA6D;AAE7D,MAAM,eAAe,GAAG,uBAAuB,CAAC;AAEhD,MAAM,GAAG,GAAG,IAAA,8BAAkB,EAAC,yBAAa,EAAE,eAAe,CAAC,CAAC;AAO/D;;;;;;;;;GASG;AACH,MAAa,qBAAqB;IAKhC,YAAY,OAAqC;QAJxC,SAAI,GAAG,eAAe,CAAC;QAEvB,uDAAiC;QAGxC,uBAAA,IAAI,wCAAkB,OAAO,CAAC,aAAa,MAAA,CAAC;IAC9C,CAAC;IAED,OAAO;QACL,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAED,IAAI,gBAAgB;QAClB,OAAO,IAAA,uBAAY,EAAC,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;YACnD,MAAM,aAAa,GAAG,IAAI,GAAG,CAC3B,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,IAAI,EAAE,CAAc,CACpD,CAAC;YACF,IAAI,aAAa,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;gBAC7B,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;YACnB,CAAC;YAED,GAAG,CAAC,+BAA+B,EAAE;gBACnC,MAAM,EAAE,CAAC,GAAG,aAAa,CAAC;aAC3B,CAAC,CAAC;YAEH,MAAM,eAAe,GAAG;gBACtB,GAAG,GAAG,CAAC,OAAO;gBACd,QAAQ,EAAE,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;aACrE,CAAC;YAEF,MAAM,QAAQ,GAAG,KAAK,EAAE,KAAiB,EAAuB,EAAE,CAAC,KAAK,CAAC;YACzE,MAAM,SAAS,GAAG,MAAM,uBAAA,IAAI,4CAAe,CAAC,gBAAgB,CAC1D;gBACE,GAAG,GAAG;gBACN,OAAO,EAAE,eAAe;gBACxB,QAAQ,EAAE,EAAE;aACb,EACD,QAAQ,CACT,CAAC;YAEF,MAAM,MAAM,GAAiB,IAAA,0CAAkB,EAAC;gBAC9C,GAAG,CAAC,QAAQ;gBACZ,SAAS,CAAC,QAAQ;aACnB,CAAC,CAAC;YAEH,qEAAqE;YACrE,mEAAmE;YACnE,kEAAkE;YAClE,kEAAkE;YAClE,sDAAsD;YACtD,iEAAiE;YACjE,mEAAmE;YACnE,MAAM,gBAAgB,GAAG,SAAS,CAAC,QAAQ,CAAC,aAAa,CAAC;YAC1D,IAAI,MAAM,CAAC,MAAM,IAAI,gBAAgB,EAAE,CAAC;gBACtC,MAAM,oBAAoB,GAAG,IAAI,GAAG,EAAU,CAAC;gBAC/C,KAAK,MAAM,eAAe,IAAI,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAAC,EAAE,CAAC;oBAC9D,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,EAAE,CAAC;wBACnD,oBAAoB,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;oBAClD,CAAC;gBACH,CAAC;gBACD,KAAK,MAAM,OAAO,IAAI,aAAa,EAAE,CAAC;oBACpC,IAAI,oBAAoB,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;wBACtC,OAAO,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;oBAChC,CAAC;gBACH,CAAC;YACH,CAAC;YAED,OAAO,IAAI,CAAC,EAAE,GAAG,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;QAC5C,CAAC,CAAC,CAAC;IACL,CAAC;CACF;AAvED,sDAuEC","sourcesContent":["import { projectLogger, createModuleLogger } from '../logger.js';\nimport { forDataTypes } from '../types.js';\nimport type {\n AssetsDataSource,\n ChainId,\n DataResponse,\n Middleware,\n} from '../types.js';\nimport { mergeDataResponses } from './ParallelMiddleware.js';\n\nconst CONTROLLER_NAME = 'RpcFallbackMiddleware';\n\nconst log = createModuleLogger(projectLogger, CONTROLLER_NAME);\n\nexport type RpcFallbackMiddlewareOptions = {\n /** The RPC data source to use as a fallback. */\n rpcDataSource: AssetsDataSource;\n};\n\n/**\n * RpcFallbackMiddleware retries chains that failed upstream on the RPC data\n * source. Any chain present in `response.errors` (network error,\n * unprocessedNetworks, timeout, …) is handed off to RPC with the request\n * filtered to just those chains. Successful RPC results are merged into the\n * response and their entries are cleared from `response.errors`.\n *\n * Place this immediately after `createParallelBalanceMiddleware` in the fast\n * pipeline.\n */\nexport class RpcFallbackMiddleware {\n readonly name = CONTROLLER_NAME;\n\n readonly #rpcDataSource: AssetsDataSource;\n\n constructor(options: RpcFallbackMiddlewareOptions) {\n this.#rpcDataSource = options.rpcDataSource;\n }\n\n getName(): string {\n return this.name;\n }\n\n get assetsMiddleware(): Middleware {\n return forDataTypes(['balance'], async (ctx, next) => {\n const erroredChains = new Set<ChainId>(\n Object.keys(ctx.response.errors ?? {}) as ChainId[],\n );\n if (erroredChains.size === 0) {\n return next(ctx);\n }\n\n log('Retrying failed chains on RPC', {\n chains: [...erroredChains],\n });\n\n const filteredRequest = {\n ...ctx.request,\n chainIds: ctx.request.chainIds.filter((id) => erroredChains.has(id)),\n };\n\n const noopNext = async (inner: typeof ctx): Promise<typeof ctx> => inner;\n const rpcResult = await this.#rpcDataSource.assetsMiddleware(\n {\n ...ctx,\n request: filteredRequest,\n response: {},\n },\n noopNext,\n );\n\n const merged: DataResponse = mergeDataResponses([\n ctx.response,\n rpcResult.response,\n ]);\n\n // Clear errors only for chains RPC actually recovered a balance for.\n // We must inspect rpcResult.response — NOT merged — because merged\n // also contains balances from the upstream sources (AccountsApi /\n // Websocket / Staked). If those sources returned partial data for\n // a chain that they also flagged as errored (e.g. via\n // unprocessedNetworks), and RPC then failed for that same chain,\n // looking at merged would incorrectly mark the error as recovered.\n const rpcAssetsBalance = rpcResult.response.assetsBalance;\n if (merged.errors && rpcAssetsBalance) {\n const chainsRecoveredByRpc = new Set<string>();\n for (const accountBalances of Object.values(rpcAssetsBalance)) {\n for (const assetId of Object.keys(accountBalances)) {\n chainsRecoveredByRpc.add(assetId.split('/')[0]);\n }\n }\n for (const chainId of erroredChains) {\n if (chainsRecoveredByRpc.has(chainId)) {\n delete merged.errors[chainId];\n }\n }\n }\n\n return next({ ...ctx, response: merged });\n });\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"RpcFallbackMiddleware.cjs","sourceRoot":"","sources":["../../src/middlewares/RpcFallbackMiddleware.ts"],"names":[],"mappings":"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","sourcesContent":["import { KnownCaipNamespace } from '@metamask/utils';\n\nimport { isStakingContractAssetId } from '../data-sources/evm-rpc-services/index.js';\nimport { projectLogger, createModuleLogger } from '../logger.js';\nimport { forDataTypes } from '../types.js';\nimport type {\n AssetBalance,\n AssetsDataSource,\n Caip19AssetId,\n ChainId,\n Context,\n DataResponse,\n Middleware,\n} from '../types.js';\nimport { mergeDataResponses } from './ParallelMiddleware.js';\n\nconst CONTROLLER_NAME = 'RpcFallbackMiddleware';\n\nconst log = createModuleLogger(projectLogger, CONTROLLER_NAME);\n\nexport type RpcFallbackMiddlewareOptions = {\n /** The RPC data source to use as a fallback. */\n rpcDataSource: AssetsDataSource;\n};\n\n/**\n * RpcFallbackMiddleware re-reads balances from the RPC data source in two\n * cases:\n *\n * 1. Chains present in `response.errors` (network error, unprocessedNetworks,\n * timeout, …). Successful RPC results are merged into the response and\n * their entries are cleared from `response.errors`.\n * 2. EVM assets tracked in state (`state.assetsBalance` or\n * `state.customAssets`) that this balance response left empty. The Accounts\n * API omits tokens it does not index, and a returned `0` is\n * indistinguishable from \"not indexed\", so the `merge` state update would\n * otherwise keep a stale amount forever. They are passed to RPC as\n * `customAssets` so the balance fetcher includes them in its multicall.\n *\n * Place this immediately after `createParallelBalanceMiddleware` in the fast\n * pipeline, after `CustomAssetGraduationMiddleware` so the custom assets RPC\n * carries never trigger graduation.\n */\nexport class RpcFallbackMiddleware {\n readonly name = CONTROLLER_NAME;\n\n readonly #rpcDataSource: AssetsDataSource;\n\n constructor(options: RpcFallbackMiddlewareOptions) {\n this.#rpcDataSource = options.rpcDataSource;\n }\n\n getName(): string {\n return this.name;\n }\n\n get assetsMiddleware(): Middleware {\n return forDataTypes(['balance'], async (ctx, next) => {\n const erroredChains = new Set<ChainId>(\n Object.keys(ctx.response.errors ?? {}) as ChainId[],\n );\n const staleAssets = collectStaleTrackedAssets(ctx);\n\n const chainsToFetch = [\n ...new Set([\n ...ctx.request.chainIds.filter((id) => erroredChains.has(id)),\n // Already restricted to requested chains. Their chain may not be\n // errored: the Accounts API can answer for a chain while omitting a\n // token it does not index.\n ...staleAssets.map((assetId) => assetId.split('/')[0] as ChainId),\n ]),\n ];\n\n if (chainsToFetch.length === 0) {\n return next(ctx);\n }\n\n log('Re-reading balances on RPC', {\n erroredChains: [...erroredChains],\n staleAssets,\n chains: chainsToFetch,\n });\n\n const filteredRequest = {\n ...ctx.request,\n chainIds: chainsToFetch,\n customAssets: [\n ...new Set([...(ctx.request.customAssets ?? []), ...staleAssets]),\n ],\n };\n\n const noopNext = async (inner: typeof ctx): Promise<typeof ctx> => inner;\n const rpcResult = await this.#rpcDataSource.assetsMiddleware(\n {\n ...ctx,\n request: filteredRequest,\n response: {},\n },\n noopNext,\n );\n\n // A chain RPC itself failed on contributed nothing trustworthy: its\n // balances are failure stubs (native 0) that would overwrite correct\n // upstream amounts and, with replaceCoveredChainBalances, wipe the\n // chain's token slice from state. Drop them before merging — this also\n // keeps failed chains from counting as \"recovered\" below.\n const rpcFailedChains = new Set<ChainId>(\n Object.keys(rpcResult.response.errors ?? {}) as ChainId[],\n );\n const rpcAssetsBalance = filterOutChainBalances(\n rpcResult.response.assetsBalance,\n rpcFailedChains,\n );\n\n // RPC errors are kept only for chains that were already errored\n // upstream. For chains fetched solely for stale tracked assets the\n // upstream response succeeded and stays authoritative — the stale asset\n // keeps its previous amount and is retried on the next pass.\n const rpcErrors = Object.fromEntries(\n Object.entries(rpcResult.response.errors ?? {}).filter(([chainId]) =>\n erroredChains.has(chainId as ChainId),\n ),\n );\n\n const merged: DataResponse = mergeDataResponses([\n ctx.response,\n {\n ...rpcResult.response,\n assetsBalance: rpcAssetsBalance,\n errors: rpcErrors,\n },\n ]);\n\n // Clear errors only for chains RPC actually recovered a balance for.\n // We must inspect the (filtered) RPC balances — NOT merged — because\n // merged also contains balances from the upstream sources (AccountsApi /\n // Websocket / Staked). If those sources returned partial data for\n // a chain that they also flagged as errored (e.g. via\n // unprocessedNetworks), and RPC then failed for that same chain,\n // looking at merged would incorrectly mark the error as recovered.\n if (merged.errors && rpcAssetsBalance) {\n const chainsRecoveredByRpc = new Set<string>();\n for (const accountBalances of Object.values(rpcAssetsBalance)) {\n for (const assetId of Object.keys(accountBalances)) {\n chainsRecoveredByRpc.add(assetId.split('/')[0]);\n }\n }\n for (const chainId of erroredChains) {\n if (chainsRecoveredByRpc.has(chainId)) {\n delete merged.errors[chainId];\n }\n }\n }\n\n return next({ ...ctx, response: merged });\n });\n }\n}\n\n/**\n * Remove all balances that belong to the given chains.\n *\n * Used to discard results for chains the RPC source itself failed on, whose\n * entries are failure stubs (native `0`) rather than real readings.\n *\n * @param assetsBalance - Balances by account from the RPC response.\n * @param chainIds - Chains whose balances should be dropped.\n * @returns The filtered balance map, without accounts left empty.\n */\nfunction filterOutChainBalances(\n assetsBalance: DataResponse['assetsBalance'],\n chainIds: Set<ChainId>,\n): DataResponse['assetsBalance'] {\n if (!assetsBalance || chainIds.size === 0) {\n return assetsBalance;\n }\n\n const filtered: NonNullable<DataResponse['assetsBalance']> = {};\n for (const [accountId, accountBalances] of Object.entries(assetsBalance)) {\n const kept = Object.fromEntries(\n Object.entries(accountBalances).filter(\n ([assetId]) => !chainIds.has(assetId.split('/')[0] as ChainId),\n ),\n ) as Record<Caip19AssetId, AssetBalance>;\n if (Object.keys(kept).length > 0) {\n filtered[accountId] = kept;\n }\n }\n return filtered;\n}\n\n/**\n * EVM assets tracked in state that this balance response left empty and RPC\n * should re-read.\n *\n * Limited to chains both requested and supported by the owning account:\n * RpcDataSource fetches per account and skips chains outside its supported\n * set, so anything else would be queued and then silently dropped.\n *\n * @param ctx - Pipeline context.\n * @returns Asset IDs to hand to the RPC data source.\n */\nfunction collectStaleTrackedAssets(ctx: Context): Caip19AssetId[] {\n const { assetsBalance: stateAssetsBalance, customAssets: stateCustomAssets } =\n ctx.getAssetsState();\n\n const staleAssets = new Set<Caip19AssetId>();\n\n for (const { account, supportedChains } of ctx.request\n .accountsWithSupportedChains) {\n const accountId = account.id;\n const trackedAssetIds = new Set<Caip19AssetId>([\n ...(Object.keys(stateAssetsBalance[accountId] ?? {}) as Caip19AssetId[]),\n ...(stateCustomAssets?.[accountId] ?? []),\n ]);\n\n const chainsForAccount = supportedChains.filter((chainId) =>\n ctx.request.chainIds.includes(chainId),\n );\n\n for (const assetId of trackedAssetIds) {\n if (\n isEvmAssetOnChains(assetId, chainsForAccount) &&\n // Staked vault balances belong to StakedBalanceDataSource; an RPC\n // ERC-20 read of the share token would clobber them.\n !isStakingContractAssetId(assetId) &&\n isBalanceEmpty(ctx.response.assetsBalance?.[accountId], assetId)\n ) {\n staleAssets.add(assetId);\n }\n }\n }\n\n return [...staleAssets];\n}\n\n/**\n * Whether a balance response carries no positive amount for an asset. A\n * returned `0` cannot be distinguished from \"not indexed\", so both count as\n * empty. Asset IDs are matched case-insensitively: state keys ERC-20 assets by\n * checksummed address, while some data sources return them lower-cased.\n *\n * @param balances - Balance map for a single account from the response.\n * @param assetId - Asset ID to check.\n * @returns True when the response holds no positive amount for the asset.\n */\nfunction isBalanceEmpty(\n balances: Record<string, AssetBalance> | undefined,\n assetId: Caip19AssetId,\n): boolean {\n let amount = balances?.[assetId]?.amount;\n if (amount === undefined && balances) {\n const lowerCasedAssetId = assetId.toLowerCase();\n amount = Object.entries(balances).find(\n ([id]) => id.toLowerCase() === lowerCasedAssetId,\n )?.[1]?.amount;\n }\n return !(Number(amount) > 0);\n}\n\n/**\n * Whether an asset is an EVM asset on one of the given chains.\n *\n * @param assetId - CAIP-19 asset ID.\n * @param chainIds - Chains to match against.\n * @returns True for EVM assets whose chain is in `chainIds`.\n */\nfunction isEvmAssetOnChains(\n assetId: Caip19AssetId,\n chainIds: ChainId[],\n): boolean {\n if (!assetId.startsWith(`${KnownCaipNamespace.Eip155}:`)) {\n return false;\n }\n return chainIds.includes(assetId.split('/')[0] as ChainId);\n}\n"]}
|
|
@@ -4,14 +4,22 @@ export type RpcFallbackMiddlewareOptions = {
|
|
|
4
4
|
rpcDataSource: AssetsDataSource;
|
|
5
5
|
};
|
|
6
6
|
/**
|
|
7
|
-
* RpcFallbackMiddleware
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* RpcFallbackMiddleware re-reads balances from the RPC data source in two
|
|
8
|
+
* cases:
|
|
9
|
+
*
|
|
10
|
+
* 1. Chains present in `response.errors` (network error, unprocessedNetworks,
|
|
11
|
+
* timeout, …). Successful RPC results are merged into the response and
|
|
12
|
+
* their entries are cleared from `response.errors`.
|
|
13
|
+
* 2. EVM assets tracked in state (`state.assetsBalance` or
|
|
14
|
+
* `state.customAssets`) that this balance response left empty. The Accounts
|
|
15
|
+
* API omits tokens it does not index, and a returned `0` is
|
|
16
|
+
* indistinguishable from "not indexed", so the `merge` state update would
|
|
17
|
+
* otherwise keep a stale amount forever. They are passed to RPC as
|
|
18
|
+
* `customAssets` so the balance fetcher includes them in its multicall.
|
|
12
19
|
*
|
|
13
20
|
* Place this immediately after `createParallelBalanceMiddleware` in the fast
|
|
14
|
-
* pipeline
|
|
21
|
+
* pipeline, after `CustomAssetGraduationMiddleware` so the custom assets RPC
|
|
22
|
+
* carries never trigger graduation.
|
|
15
23
|
*/
|
|
16
24
|
export declare class RpcFallbackMiddleware {
|
|
17
25
|
#private;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RpcFallbackMiddleware.d.cts","sourceRoot":"","sources":["../../src/middlewares/RpcFallbackMiddleware.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"RpcFallbackMiddleware.d.cts","sourceRoot":"","sources":["../../src/middlewares/RpcFallbackMiddleware.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAEV,gBAAgB,EAKhB,UAAU,EACX,qBAAoB;AAOrB,MAAM,MAAM,4BAA4B,GAAG;IACzC,gDAAgD;IAChD,aAAa,EAAE,gBAAgB,CAAC;CACjC,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,qBAAqB;;IAChC,QAAQ,CAAC,IAAI,2BAAmB;gBAIpB,OAAO,EAAE,4BAA4B;IAIjD,OAAO,IAAI,MAAM;IAIjB,IAAI,gBAAgB,IAAI,UAAU,CAoGjC;CACF"}
|
|
@@ -4,14 +4,22 @@ export type RpcFallbackMiddlewareOptions = {
|
|
|
4
4
|
rpcDataSource: AssetsDataSource;
|
|
5
5
|
};
|
|
6
6
|
/**
|
|
7
|
-
* RpcFallbackMiddleware
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* RpcFallbackMiddleware re-reads balances from the RPC data source in two
|
|
8
|
+
* cases:
|
|
9
|
+
*
|
|
10
|
+
* 1. Chains present in `response.errors` (network error, unprocessedNetworks,
|
|
11
|
+
* timeout, …). Successful RPC results are merged into the response and
|
|
12
|
+
* their entries are cleared from `response.errors`.
|
|
13
|
+
* 2. EVM assets tracked in state (`state.assetsBalance` or
|
|
14
|
+
* `state.customAssets`) that this balance response left empty. The Accounts
|
|
15
|
+
* API omits tokens it does not index, and a returned `0` is
|
|
16
|
+
* indistinguishable from "not indexed", so the `merge` state update would
|
|
17
|
+
* otherwise keep a stale amount forever. They are passed to RPC as
|
|
18
|
+
* `customAssets` so the balance fetcher includes them in its multicall.
|
|
12
19
|
*
|
|
13
20
|
* Place this immediately after `createParallelBalanceMiddleware` in the fast
|
|
14
|
-
* pipeline
|
|
21
|
+
* pipeline, after `CustomAssetGraduationMiddleware` so the custom assets RPC
|
|
22
|
+
* carries never trigger graduation.
|
|
15
23
|
*/
|
|
16
24
|
export declare class RpcFallbackMiddleware {
|
|
17
25
|
#private;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RpcFallbackMiddleware.d.mts","sourceRoot":"","sources":["../../src/middlewares/RpcFallbackMiddleware.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"RpcFallbackMiddleware.d.mts","sourceRoot":"","sources":["../../src/middlewares/RpcFallbackMiddleware.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAEV,gBAAgB,EAKhB,UAAU,EACX,qBAAoB;AAOrB,MAAM,MAAM,4BAA4B,GAAG;IACzC,gDAAgD;IAChD,aAAa,EAAE,gBAAgB,CAAC;CACjC,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,qBAAqB;;IAChC,QAAQ,CAAC,IAAI,2BAAmB;gBAIpB,OAAO,EAAE,4BAA4B;IAIjD,OAAO,IAAI,MAAM;IAIjB,IAAI,gBAAgB,IAAI,UAAU,CAoGjC;CACF"}
|
|
@@ -10,20 +10,30 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
10
10
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
11
|
};
|
|
12
12
|
var _RpcFallbackMiddleware_rpcDataSource;
|
|
13
|
+
import { KnownCaipNamespace } from "@metamask/utils";
|
|
14
|
+
import { isStakingContractAssetId } from "../data-sources/evm-rpc-services/index.mjs";
|
|
13
15
|
import { projectLogger, createModuleLogger } from "../logger.mjs";
|
|
14
16
|
import { forDataTypes } from "../types.mjs";
|
|
15
17
|
import { mergeDataResponses } from "./ParallelMiddleware.mjs";
|
|
16
18
|
const CONTROLLER_NAME = 'RpcFallbackMiddleware';
|
|
17
19
|
const log = createModuleLogger(projectLogger, CONTROLLER_NAME);
|
|
18
20
|
/**
|
|
19
|
-
* RpcFallbackMiddleware
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* RpcFallbackMiddleware re-reads balances from the RPC data source in two
|
|
22
|
+
* cases:
|
|
23
|
+
*
|
|
24
|
+
* 1. Chains present in `response.errors` (network error, unprocessedNetworks,
|
|
25
|
+
* timeout, …). Successful RPC results are merged into the response and
|
|
26
|
+
* their entries are cleared from `response.errors`.
|
|
27
|
+
* 2. EVM assets tracked in state (`state.assetsBalance` or
|
|
28
|
+
* `state.customAssets`) that this balance response left empty. The Accounts
|
|
29
|
+
* API omits tokens it does not index, and a returned `0` is
|
|
30
|
+
* indistinguishable from "not indexed", so the `merge` state update would
|
|
31
|
+
* otherwise keep a stale amount forever. They are passed to RPC as
|
|
32
|
+
* `customAssets` so the balance fetcher includes them in its multicall.
|
|
24
33
|
*
|
|
25
34
|
* Place this immediately after `createParallelBalanceMiddleware` in the fast
|
|
26
|
-
* pipeline
|
|
35
|
+
* pipeline, after `CustomAssetGraduationMiddleware` so the custom assets RPC
|
|
36
|
+
* carries never trigger graduation.
|
|
27
37
|
*/
|
|
28
38
|
export class RpcFallbackMiddleware {
|
|
29
39
|
constructor(options) {
|
|
@@ -37,15 +47,30 @@ export class RpcFallbackMiddleware {
|
|
|
37
47
|
get assetsMiddleware() {
|
|
38
48
|
return forDataTypes(['balance'], async (ctx, next) => {
|
|
39
49
|
const erroredChains = new Set(Object.keys(ctx.response.errors ?? {}));
|
|
40
|
-
|
|
50
|
+
const staleAssets = collectStaleTrackedAssets(ctx);
|
|
51
|
+
const chainsToFetch = [
|
|
52
|
+
...new Set([
|
|
53
|
+
...ctx.request.chainIds.filter((id) => erroredChains.has(id)),
|
|
54
|
+
// Already restricted to requested chains. Their chain may not be
|
|
55
|
+
// errored: the Accounts API can answer for a chain while omitting a
|
|
56
|
+
// token it does not index.
|
|
57
|
+
...staleAssets.map((assetId) => assetId.split('/')[0]),
|
|
58
|
+
]),
|
|
59
|
+
];
|
|
60
|
+
if (chainsToFetch.length === 0) {
|
|
41
61
|
return next(ctx);
|
|
42
62
|
}
|
|
43
|
-
log('
|
|
44
|
-
|
|
63
|
+
log('Re-reading balances on RPC', {
|
|
64
|
+
erroredChains: [...erroredChains],
|
|
65
|
+
staleAssets,
|
|
66
|
+
chains: chainsToFetch,
|
|
45
67
|
});
|
|
46
68
|
const filteredRequest = {
|
|
47
69
|
...ctx.request,
|
|
48
|
-
chainIds:
|
|
70
|
+
chainIds: chainsToFetch,
|
|
71
|
+
customAssets: [
|
|
72
|
+
...new Set([...(ctx.request.customAssets ?? []), ...staleAssets]),
|
|
73
|
+
],
|
|
49
74
|
};
|
|
50
75
|
const noopNext = async (inner) => inner;
|
|
51
76
|
const rpcResult = await __classPrivateFieldGet(this, _RpcFallbackMiddleware_rpcDataSource, "f").assetsMiddleware({
|
|
@@ -53,18 +78,33 @@ export class RpcFallbackMiddleware {
|
|
|
53
78
|
request: filteredRequest,
|
|
54
79
|
response: {},
|
|
55
80
|
}, noopNext);
|
|
81
|
+
// A chain RPC itself failed on contributed nothing trustworthy: its
|
|
82
|
+
// balances are failure stubs (native 0) that would overwrite correct
|
|
83
|
+
// upstream amounts and, with replaceCoveredChainBalances, wipe the
|
|
84
|
+
// chain's token slice from state. Drop them before merging — this also
|
|
85
|
+
// keeps failed chains from counting as "recovered" below.
|
|
86
|
+
const rpcFailedChains = new Set(Object.keys(rpcResult.response.errors ?? {}));
|
|
87
|
+
const rpcAssetsBalance = filterOutChainBalances(rpcResult.response.assetsBalance, rpcFailedChains);
|
|
88
|
+
// RPC errors are kept only for chains that were already errored
|
|
89
|
+
// upstream. For chains fetched solely for stale tracked assets the
|
|
90
|
+
// upstream response succeeded and stays authoritative — the stale asset
|
|
91
|
+
// keeps its previous amount and is retried on the next pass.
|
|
92
|
+
const rpcErrors = Object.fromEntries(Object.entries(rpcResult.response.errors ?? {}).filter(([chainId]) => erroredChains.has(chainId)));
|
|
56
93
|
const merged = mergeDataResponses([
|
|
57
94
|
ctx.response,
|
|
58
|
-
|
|
95
|
+
{
|
|
96
|
+
...rpcResult.response,
|
|
97
|
+
assetsBalance: rpcAssetsBalance,
|
|
98
|
+
errors: rpcErrors,
|
|
99
|
+
},
|
|
59
100
|
]);
|
|
60
101
|
// Clear errors only for chains RPC actually recovered a balance for.
|
|
61
|
-
// We must inspect
|
|
62
|
-
// also contains balances from the upstream sources (AccountsApi /
|
|
102
|
+
// We must inspect the (filtered) RPC balances — NOT merged — because
|
|
103
|
+
// merged also contains balances from the upstream sources (AccountsApi /
|
|
63
104
|
// Websocket / Staked). If those sources returned partial data for
|
|
64
105
|
// a chain that they also flagged as errored (e.g. via
|
|
65
106
|
// unprocessedNetworks), and RPC then failed for that same chain,
|
|
66
107
|
// looking at merged would incorrectly mark the error as recovered.
|
|
67
|
-
const rpcAssetsBalance = rpcResult.response.assetsBalance;
|
|
68
108
|
if (merged.errors && rpcAssetsBalance) {
|
|
69
109
|
const chainsRecoveredByRpc = new Set();
|
|
70
110
|
for (const accountBalances of Object.values(rpcAssetsBalance)) {
|
|
@@ -83,4 +123,92 @@ export class RpcFallbackMiddleware {
|
|
|
83
123
|
}
|
|
84
124
|
}
|
|
85
125
|
_RpcFallbackMiddleware_rpcDataSource = new WeakMap();
|
|
126
|
+
/**
|
|
127
|
+
* Remove all balances that belong to the given chains.
|
|
128
|
+
*
|
|
129
|
+
* Used to discard results for chains the RPC source itself failed on, whose
|
|
130
|
+
* entries are failure stubs (native `0`) rather than real readings.
|
|
131
|
+
*
|
|
132
|
+
* @param assetsBalance - Balances by account from the RPC response.
|
|
133
|
+
* @param chainIds - Chains whose balances should be dropped.
|
|
134
|
+
* @returns The filtered balance map, without accounts left empty.
|
|
135
|
+
*/
|
|
136
|
+
function filterOutChainBalances(assetsBalance, chainIds) {
|
|
137
|
+
if (!assetsBalance || chainIds.size === 0) {
|
|
138
|
+
return assetsBalance;
|
|
139
|
+
}
|
|
140
|
+
const filtered = {};
|
|
141
|
+
for (const [accountId, accountBalances] of Object.entries(assetsBalance)) {
|
|
142
|
+
const kept = Object.fromEntries(Object.entries(accountBalances).filter(([assetId]) => !chainIds.has(assetId.split('/')[0])));
|
|
143
|
+
if (Object.keys(kept).length > 0) {
|
|
144
|
+
filtered[accountId] = kept;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return filtered;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* EVM assets tracked in state that this balance response left empty and RPC
|
|
151
|
+
* should re-read.
|
|
152
|
+
*
|
|
153
|
+
* Limited to chains both requested and supported by the owning account:
|
|
154
|
+
* RpcDataSource fetches per account and skips chains outside its supported
|
|
155
|
+
* set, so anything else would be queued and then silently dropped.
|
|
156
|
+
*
|
|
157
|
+
* @param ctx - Pipeline context.
|
|
158
|
+
* @returns Asset IDs to hand to the RPC data source.
|
|
159
|
+
*/
|
|
160
|
+
function collectStaleTrackedAssets(ctx) {
|
|
161
|
+
const { assetsBalance: stateAssetsBalance, customAssets: stateCustomAssets } = ctx.getAssetsState();
|
|
162
|
+
const staleAssets = new Set();
|
|
163
|
+
for (const { account, supportedChains } of ctx.request
|
|
164
|
+
.accountsWithSupportedChains) {
|
|
165
|
+
const accountId = account.id;
|
|
166
|
+
const trackedAssetIds = new Set([
|
|
167
|
+
...Object.keys(stateAssetsBalance[accountId] ?? {}),
|
|
168
|
+
...(stateCustomAssets?.[accountId] ?? []),
|
|
169
|
+
]);
|
|
170
|
+
const chainsForAccount = supportedChains.filter((chainId) => ctx.request.chainIds.includes(chainId));
|
|
171
|
+
for (const assetId of trackedAssetIds) {
|
|
172
|
+
if (isEvmAssetOnChains(assetId, chainsForAccount) &&
|
|
173
|
+
// Staked vault balances belong to StakedBalanceDataSource; an RPC
|
|
174
|
+
// ERC-20 read of the share token would clobber them.
|
|
175
|
+
!isStakingContractAssetId(assetId) &&
|
|
176
|
+
isBalanceEmpty(ctx.response.assetsBalance?.[accountId], assetId)) {
|
|
177
|
+
staleAssets.add(assetId);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return [...staleAssets];
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Whether a balance response carries no positive amount for an asset. A
|
|
185
|
+
* returned `0` cannot be distinguished from "not indexed", so both count as
|
|
186
|
+
* empty. Asset IDs are matched case-insensitively: state keys ERC-20 assets by
|
|
187
|
+
* checksummed address, while some data sources return them lower-cased.
|
|
188
|
+
*
|
|
189
|
+
* @param balances - Balance map for a single account from the response.
|
|
190
|
+
* @param assetId - Asset ID to check.
|
|
191
|
+
* @returns True when the response holds no positive amount for the asset.
|
|
192
|
+
*/
|
|
193
|
+
function isBalanceEmpty(balances, assetId) {
|
|
194
|
+
let amount = balances?.[assetId]?.amount;
|
|
195
|
+
if (amount === undefined && balances) {
|
|
196
|
+
const lowerCasedAssetId = assetId.toLowerCase();
|
|
197
|
+
amount = Object.entries(balances).find(([id]) => id.toLowerCase() === lowerCasedAssetId)?.[1]?.amount;
|
|
198
|
+
}
|
|
199
|
+
return !(Number(amount) > 0);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Whether an asset is an EVM asset on one of the given chains.
|
|
203
|
+
*
|
|
204
|
+
* @param assetId - CAIP-19 asset ID.
|
|
205
|
+
* @param chainIds - Chains to match against.
|
|
206
|
+
* @returns True for EVM assets whose chain is in `chainIds`.
|
|
207
|
+
*/
|
|
208
|
+
function isEvmAssetOnChains(assetId, chainIds) {
|
|
209
|
+
if (!assetId.startsWith(`${KnownCaipNamespace.Eip155}:`)) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
return chainIds.includes(assetId.split('/')[0]);
|
|
213
|
+
}
|
|
86
214
|
//# sourceMappingURL=RpcFallbackMiddleware.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RpcFallbackMiddleware.mjs","sourceRoot":"","sources":["../../src/middlewares/RpcFallbackMiddleware.ts"],"names":[],"mappings":"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","sourcesContent":["import { projectLogger, createModuleLogger } from '../logger.js';\nimport { forDataTypes } from '../types.js';\nimport type {\n AssetsDataSource,\n ChainId,\n DataResponse,\n Middleware,\n} from '../types.js';\nimport { mergeDataResponses } from './ParallelMiddleware.js';\n\nconst CONTROLLER_NAME = 'RpcFallbackMiddleware';\n\nconst log = createModuleLogger(projectLogger, CONTROLLER_NAME);\n\nexport type RpcFallbackMiddlewareOptions = {\n /** The RPC data source to use as a fallback. */\n rpcDataSource: AssetsDataSource;\n};\n\n/**\n * RpcFallbackMiddleware retries chains that failed upstream on the RPC data\n * source. Any chain present in `response.errors` (network error,\n * unprocessedNetworks, timeout, …) is handed off to RPC with the request\n * filtered to just those chains. Successful RPC results are merged into the\n * response and their entries are cleared from `response.errors`.\n *\n * Place this immediately after `createParallelBalanceMiddleware` in the fast\n * pipeline.\n */\nexport class RpcFallbackMiddleware {\n readonly name = CONTROLLER_NAME;\n\n readonly #rpcDataSource: AssetsDataSource;\n\n constructor(options: RpcFallbackMiddlewareOptions) {\n this.#rpcDataSource = options.rpcDataSource;\n }\n\n getName(): string {\n return this.name;\n }\n\n get assetsMiddleware(): Middleware {\n return forDataTypes(['balance'], async (ctx, next) => {\n const erroredChains = new Set<ChainId>(\n Object.keys(ctx.response.errors ?? {}) as ChainId[],\n );\n if (erroredChains.size === 0) {\n return next(ctx);\n }\n\n log('Retrying failed chains on RPC', {\n chains: [...erroredChains],\n });\n\n const filteredRequest = {\n ...ctx.request,\n chainIds: ctx.request.chainIds.filter((id) => erroredChains.has(id)),\n };\n\n const noopNext = async (inner: typeof ctx): Promise<typeof ctx> => inner;\n const rpcResult = await this.#rpcDataSource.assetsMiddleware(\n {\n ...ctx,\n request: filteredRequest,\n response: {},\n },\n noopNext,\n );\n\n const merged: DataResponse = mergeDataResponses([\n ctx.response,\n rpcResult.response,\n ]);\n\n // Clear errors only for chains RPC actually recovered a balance for.\n // We must inspect rpcResult.response — NOT merged — because merged\n // also contains balances from the upstream sources (AccountsApi /\n // Websocket / Staked). If those sources returned partial data for\n // a chain that they also flagged as errored (e.g. via\n // unprocessedNetworks), and RPC then failed for that same chain,\n // looking at merged would incorrectly mark the error as recovered.\n const rpcAssetsBalance = rpcResult.response.assetsBalance;\n if (merged.errors && rpcAssetsBalance) {\n const chainsRecoveredByRpc = new Set<string>();\n for (const accountBalances of Object.values(rpcAssetsBalance)) {\n for (const assetId of Object.keys(accountBalances)) {\n chainsRecoveredByRpc.add(assetId.split('/')[0]);\n }\n }\n for (const chainId of erroredChains) {\n if (chainsRecoveredByRpc.has(chainId)) {\n delete merged.errors[chainId];\n }\n }\n }\n\n return next({ ...ctx, response: merged });\n });\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"RpcFallbackMiddleware.mjs","sourceRoot":"","sources":["../../src/middlewares/RpcFallbackMiddleware.ts"],"names":[],"mappings":"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","sourcesContent":["import { KnownCaipNamespace } from '@metamask/utils';\n\nimport { isStakingContractAssetId } from '../data-sources/evm-rpc-services/index.js';\nimport { projectLogger, createModuleLogger } from '../logger.js';\nimport { forDataTypes } from '../types.js';\nimport type {\n AssetBalance,\n AssetsDataSource,\n Caip19AssetId,\n ChainId,\n Context,\n DataResponse,\n Middleware,\n} from '../types.js';\nimport { mergeDataResponses } from './ParallelMiddleware.js';\n\nconst CONTROLLER_NAME = 'RpcFallbackMiddleware';\n\nconst log = createModuleLogger(projectLogger, CONTROLLER_NAME);\n\nexport type RpcFallbackMiddlewareOptions = {\n /** The RPC data source to use as a fallback. */\n rpcDataSource: AssetsDataSource;\n};\n\n/**\n * RpcFallbackMiddleware re-reads balances from the RPC data source in two\n * cases:\n *\n * 1. Chains present in `response.errors` (network error, unprocessedNetworks,\n * timeout, …). Successful RPC results are merged into the response and\n * their entries are cleared from `response.errors`.\n * 2. EVM assets tracked in state (`state.assetsBalance` or\n * `state.customAssets`) that this balance response left empty. The Accounts\n * API omits tokens it does not index, and a returned `0` is\n * indistinguishable from \"not indexed\", so the `merge` state update would\n * otherwise keep a stale amount forever. They are passed to RPC as\n * `customAssets` so the balance fetcher includes them in its multicall.\n *\n * Place this immediately after `createParallelBalanceMiddleware` in the fast\n * pipeline, after `CustomAssetGraduationMiddleware` so the custom assets RPC\n * carries never trigger graduation.\n */\nexport class RpcFallbackMiddleware {\n readonly name = CONTROLLER_NAME;\n\n readonly #rpcDataSource: AssetsDataSource;\n\n constructor(options: RpcFallbackMiddlewareOptions) {\n this.#rpcDataSource = options.rpcDataSource;\n }\n\n getName(): string {\n return this.name;\n }\n\n get assetsMiddleware(): Middleware {\n return forDataTypes(['balance'], async (ctx, next) => {\n const erroredChains = new Set<ChainId>(\n Object.keys(ctx.response.errors ?? {}) as ChainId[],\n );\n const staleAssets = collectStaleTrackedAssets(ctx);\n\n const chainsToFetch = [\n ...new Set([\n ...ctx.request.chainIds.filter((id) => erroredChains.has(id)),\n // Already restricted to requested chains. Their chain may not be\n // errored: the Accounts API can answer for a chain while omitting a\n // token it does not index.\n ...staleAssets.map((assetId) => assetId.split('/')[0] as ChainId),\n ]),\n ];\n\n if (chainsToFetch.length === 0) {\n return next(ctx);\n }\n\n log('Re-reading balances on RPC', {\n erroredChains: [...erroredChains],\n staleAssets,\n chains: chainsToFetch,\n });\n\n const filteredRequest = {\n ...ctx.request,\n chainIds: chainsToFetch,\n customAssets: [\n ...new Set([...(ctx.request.customAssets ?? []), ...staleAssets]),\n ],\n };\n\n const noopNext = async (inner: typeof ctx): Promise<typeof ctx> => inner;\n const rpcResult = await this.#rpcDataSource.assetsMiddleware(\n {\n ...ctx,\n request: filteredRequest,\n response: {},\n },\n noopNext,\n );\n\n // A chain RPC itself failed on contributed nothing trustworthy: its\n // balances are failure stubs (native 0) that would overwrite correct\n // upstream amounts and, with replaceCoveredChainBalances, wipe the\n // chain's token slice from state. Drop them before merging — this also\n // keeps failed chains from counting as \"recovered\" below.\n const rpcFailedChains = new Set<ChainId>(\n Object.keys(rpcResult.response.errors ?? {}) as ChainId[],\n );\n const rpcAssetsBalance = filterOutChainBalances(\n rpcResult.response.assetsBalance,\n rpcFailedChains,\n );\n\n // RPC errors are kept only for chains that were already errored\n // upstream. For chains fetched solely for stale tracked assets the\n // upstream response succeeded and stays authoritative — the stale asset\n // keeps its previous amount and is retried on the next pass.\n const rpcErrors = Object.fromEntries(\n Object.entries(rpcResult.response.errors ?? {}).filter(([chainId]) =>\n erroredChains.has(chainId as ChainId),\n ),\n );\n\n const merged: DataResponse = mergeDataResponses([\n ctx.response,\n {\n ...rpcResult.response,\n assetsBalance: rpcAssetsBalance,\n errors: rpcErrors,\n },\n ]);\n\n // Clear errors only for chains RPC actually recovered a balance for.\n // We must inspect the (filtered) RPC balances — NOT merged — because\n // merged also contains balances from the upstream sources (AccountsApi /\n // Websocket / Staked). If those sources returned partial data for\n // a chain that they also flagged as errored (e.g. via\n // unprocessedNetworks), and RPC then failed for that same chain,\n // looking at merged would incorrectly mark the error as recovered.\n if (merged.errors && rpcAssetsBalance) {\n const chainsRecoveredByRpc = new Set<string>();\n for (const accountBalances of Object.values(rpcAssetsBalance)) {\n for (const assetId of Object.keys(accountBalances)) {\n chainsRecoveredByRpc.add(assetId.split('/')[0]);\n }\n }\n for (const chainId of erroredChains) {\n if (chainsRecoveredByRpc.has(chainId)) {\n delete merged.errors[chainId];\n }\n }\n }\n\n return next({ ...ctx, response: merged });\n });\n }\n}\n\n/**\n * Remove all balances that belong to the given chains.\n *\n * Used to discard results for chains the RPC source itself failed on, whose\n * entries are failure stubs (native `0`) rather than real readings.\n *\n * @param assetsBalance - Balances by account from the RPC response.\n * @param chainIds - Chains whose balances should be dropped.\n * @returns The filtered balance map, without accounts left empty.\n */\nfunction filterOutChainBalances(\n assetsBalance: DataResponse['assetsBalance'],\n chainIds: Set<ChainId>,\n): DataResponse['assetsBalance'] {\n if (!assetsBalance || chainIds.size === 0) {\n return assetsBalance;\n }\n\n const filtered: NonNullable<DataResponse['assetsBalance']> = {};\n for (const [accountId, accountBalances] of Object.entries(assetsBalance)) {\n const kept = Object.fromEntries(\n Object.entries(accountBalances).filter(\n ([assetId]) => !chainIds.has(assetId.split('/')[0] as ChainId),\n ),\n ) as Record<Caip19AssetId, AssetBalance>;\n if (Object.keys(kept).length > 0) {\n filtered[accountId] = kept;\n }\n }\n return filtered;\n}\n\n/**\n * EVM assets tracked in state that this balance response left empty and RPC\n * should re-read.\n *\n * Limited to chains both requested and supported by the owning account:\n * RpcDataSource fetches per account and skips chains outside its supported\n * set, so anything else would be queued and then silently dropped.\n *\n * @param ctx - Pipeline context.\n * @returns Asset IDs to hand to the RPC data source.\n */\nfunction collectStaleTrackedAssets(ctx: Context): Caip19AssetId[] {\n const { assetsBalance: stateAssetsBalance, customAssets: stateCustomAssets } =\n ctx.getAssetsState();\n\n const staleAssets = new Set<Caip19AssetId>();\n\n for (const { account, supportedChains } of ctx.request\n .accountsWithSupportedChains) {\n const accountId = account.id;\n const trackedAssetIds = new Set<Caip19AssetId>([\n ...(Object.keys(stateAssetsBalance[accountId] ?? {}) as Caip19AssetId[]),\n ...(stateCustomAssets?.[accountId] ?? []),\n ]);\n\n const chainsForAccount = supportedChains.filter((chainId) =>\n ctx.request.chainIds.includes(chainId),\n );\n\n for (const assetId of trackedAssetIds) {\n if (\n isEvmAssetOnChains(assetId, chainsForAccount) &&\n // Staked vault balances belong to StakedBalanceDataSource; an RPC\n // ERC-20 read of the share token would clobber them.\n !isStakingContractAssetId(assetId) &&\n isBalanceEmpty(ctx.response.assetsBalance?.[accountId], assetId)\n ) {\n staleAssets.add(assetId);\n }\n }\n }\n\n return [...staleAssets];\n}\n\n/**\n * Whether a balance response carries no positive amount for an asset. A\n * returned `0` cannot be distinguished from \"not indexed\", so both count as\n * empty. Asset IDs are matched case-insensitively: state keys ERC-20 assets by\n * checksummed address, while some data sources return them lower-cased.\n *\n * @param balances - Balance map for a single account from the response.\n * @param assetId - Asset ID to check.\n * @returns True when the response holds no positive amount for the asset.\n */\nfunction isBalanceEmpty(\n balances: Record<string, AssetBalance> | undefined,\n assetId: Caip19AssetId,\n): boolean {\n let amount = balances?.[assetId]?.amount;\n if (amount === undefined && balances) {\n const lowerCasedAssetId = assetId.toLowerCase();\n amount = Object.entries(balances).find(\n ([id]) => id.toLowerCase() === lowerCasedAssetId,\n )?.[1]?.amount;\n }\n return !(Number(amount) > 0);\n}\n\n/**\n * Whether an asset is an EVM asset on one of the given chains.\n *\n * @param assetId - CAIP-19 asset ID.\n * @param chainIds - Chains to match against.\n * @returns True for EVM assets whose chain is in `chainIds`.\n */\nfunction isEvmAssetOnChains(\n assetId: Caip19AssetId,\n chainIds: ChainId[],\n): boolean {\n if (!assetId.startsWith(`${KnownCaipNamespace.Eip155}:`)) {\n return false;\n }\n return chainIds.includes(assetId.split('/')[0] as ChainId);\n}\n"]}
|
package/package.json
CHANGED