@metamask-previews/assets-controller 16.1.0-preview-b1e33e9f1 → 16.1.0-preview-d2ea3cc
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 +0 -28
- package/dist/AssetsController-method-action-types.d.ts +1 -5
- package/dist/AssetsController-method-action-types.d.ts.map +1 -1
- package/dist/AssetsController-method-action-types.js.map +1 -1
- package/dist/AssetsController.d.ts +40 -8
- package/dist/AssetsController.d.ts.map +1 -1
- package/dist/AssetsController.js +235 -570
- package/dist/AssetsController.js.map +1 -1
- package/dist/data-sources/AbstractDataSource.d.ts +6 -1
- package/dist/data-sources/AbstractDataSource.d.ts.map +1 -1
- package/dist/data-sources/AbstractDataSource.js.map +1 -1
- package/dist/data-sources/AccountsApiDataSource.d.ts +3 -17
- package/dist/data-sources/AccountsApiDataSource.d.ts.map +1 -1
- package/dist/data-sources/AccountsApiDataSource.js +125 -275
- package/dist/data-sources/AccountsApiDataSource.js.map +1 -1
- package/dist/data-sources/PriceDataSource.d.ts +6 -7
- package/dist/data-sources/PriceDataSource.d.ts.map +1 -1
- package/dist/data-sources/PriceDataSource.js +20 -15
- package/dist/data-sources/PriceDataSource.js.map +1 -1
- package/dist/data-sources/RpcDataSource.d.ts +1 -15
- package/dist/data-sources/RpcDataSource.d.ts.map +1 -1
- package/dist/data-sources/RpcDataSource.js +107 -398
- package/dist/data-sources/RpcDataSource.js.map +1 -1
- package/dist/data-sources/SnapDataSource.d.ts +0 -5
- package/dist/data-sources/SnapDataSource.d.ts.map +1 -1
- package/dist/data-sources/SnapDataSource.js +0 -114
- package/dist/data-sources/SnapDataSource.js.map +1 -1
- package/dist/data-sources/TokenDataSource.d.ts +1 -3
- package/dist/data-sources/TokenDataSource.d.ts.map +1 -1
- package/dist/data-sources/TokenDataSource.js +2 -4
- package/dist/data-sources/TokenDataSource.js.map +1 -1
- package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.d.ts +0 -8
- package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.d.ts.map +1 -1
- package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.js +2 -47
- package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/middlewares/CustomAssetGraduationMiddleware.d.ts +1 -3
- package/dist/middlewares/CustomAssetGraduationMiddleware.d.ts.map +1 -1
- package/dist/middlewares/CustomAssetGraduationMiddleware.js +1 -3
- package/dist/middlewares/CustomAssetGraduationMiddleware.js.map +1 -1
- package/dist/middlewares/DetectionMiddleware.d.ts +2 -10
- package/dist/middlewares/DetectionMiddleware.d.ts.map +1 -1
- package/dist/middlewares/DetectionMiddleware.js +23 -5
- package/dist/middlewares/DetectionMiddleware.js.map +1 -1
- package/dist/middlewares/ParallelMiddleware.d.ts.map +1 -1
- package/dist/middlewares/ParallelMiddleware.js +3 -0
- package/dist/middlewares/ParallelMiddleware.js.map +1 -1
- package/dist/middlewares/RpcFallbackMiddleware.d.ts +15 -16
- package/dist/middlewares/RpcFallbackMiddleware.d.ts.map +1 -1
- package/dist/middlewares/RpcFallbackMiddleware.js +107 -125
- package/dist/middlewares/RpcFallbackMiddleware.js.map +1 -1
- package/dist/middlewares/index.d.ts +0 -1
- package/dist/middlewares/index.d.ts.map +1 -1
- package/dist/middlewares/index.js.map +1 -1
- package/dist/migrations/healAssetsInfoMetadata.d.ts +5 -5
- package/dist/migrations/healAssetsInfoMetadata.d.ts.map +1 -1
- package/dist/migrations/healAssetsInfoMetadata.js.map +1 -1
- package/dist/pipeline/buildFastFetchSources.d.ts +0 -5
- package/dist/pipeline/buildFastFetchSources.d.ts.map +1 -1
- package/dist/pipeline/buildFastFetchSources.js +1 -7
- package/dist/pipeline/buildFastFetchSources.js.map +1 -1
- package/dist/pipeline/executeAssetsPipeline.d.ts +3 -1
- package/dist/pipeline/executeAssetsPipeline.d.ts.map +1 -1
- package/dist/pipeline/executeAssetsPipeline.js +2 -1
- package/dist/pipeline/executeAssetsPipeline.js.map +1 -1
- package/dist/selectors/balance.js +1 -1
- package/dist/selectors/balance.js.map +1 -1
- package/dist/types.d.ts +23 -29
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/dist/utils/assetVisibility.d.ts +0 -29
- package/dist/utils/assetVisibility.d.ts.map +0 -1
- package/dist/utils/assetVisibility.js +0 -80
- package/dist/utils/assetVisibility.js.map +0 -1
- package/dist/utils/filterFailedChainBalances.d.ts +0 -11
- package/dist/utils/filterFailedChainBalances.d.ts.map +0 -1
- package/dist/utils/filterFailedChainBalances.js +0 -22
- package/dist/utils/filterFailedChainBalances.js.map +0 -1
package/dist/AssetsController.js
CHANGED
|
@@ -21,7 +21,6 @@ import { createParallelBalanceMiddleware, createParallelMiddleware, } from './mi
|
|
|
21
21
|
import { RpcFallbackMiddleware } from './middlewares/RpcFallbackMiddleware.js';
|
|
22
22
|
import { cleanSpamAssets, isUnlockCleanupEnabled, tempHealAssetsInfoMetadata, } from './migrations/healAssetsInfoMetadata.js';
|
|
23
23
|
import { buildFastFetchSources, executeAssetsPipeline, } from './pipeline/index.js';
|
|
24
|
-
import { getAssetVisibility } from './utils/assetVisibility.js';
|
|
25
24
|
import { ZERO_ADDRESS } from './utils/constants.js';
|
|
26
25
|
import { pickRpcCustomAssetsSupplement } from './utils/customAssetsRpcSupplement.js';
|
|
27
26
|
import { normalizeAmountString, normalizeAssetId, formatExchangeRatesForBridge, formatStateForTransactionPay, buildNativeAssetsFromConstant, buildNativeAssetsFromApi, getDefaultNativeAssetBalance, } from './utils/index.js';
|
|
@@ -191,17 +190,16 @@ function normalizeResponse(response) {
|
|
|
191
190
|
return normalized;
|
|
192
191
|
}
|
|
193
192
|
/**
|
|
194
|
-
*
|
|
193
|
+
* Merge account balances from a data-source response into prior state.
|
|
195
194
|
*
|
|
196
195
|
* @param previousBalances - Balances already in state for this account.
|
|
197
196
|
* @param accountBalances - Balances from the incoming response.
|
|
198
|
-
* @param customAssetIds - Custom assets to preserve when replacing covered chains
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* @returns The effective balance map for the account.
|
|
197
|
+
* @param customAssetIds - Custom assets to preserve when replacing covered chains.
|
|
198
|
+
* @param replaceCoveredChains - When true, drop prior balances on chains present
|
|
199
|
+
* in the response before applying it (authoritative chain slice).
|
|
200
|
+
* @returns The merged balance map for the account.
|
|
203
201
|
*/
|
|
204
|
-
function
|
|
202
|
+
function mergeAccountBalances(previousBalances, accountBalances, customAssetIds, replaceCoveredChains) {
|
|
205
203
|
if (!replaceCoveredChains) {
|
|
206
204
|
return { ...previousBalances, ...accountBalances };
|
|
207
205
|
}
|
|
@@ -230,42 +228,6 @@ function effectiveAccountBalancesV5(previousBalances, accountBalances, customAss
|
|
|
230
228
|
}
|
|
231
229
|
return next;
|
|
232
230
|
}
|
|
233
|
-
/**
|
|
234
|
-
* Compute the effective account balances for an Accounts API v6 update.
|
|
235
|
-
*
|
|
236
|
-
* `merge` overlays the incoming balances on prior state (Account Activity,
|
|
237
|
-
* Snap events, staking). `full` replaces each covered chain slice. v6 asks
|
|
238
|
-
* for natives, pins, and default tracked assets through `includeAssetIds`
|
|
239
|
-
* and either returns them (at zero when the account holds none) or names
|
|
240
|
-
* them in `unprocessedIncludeAssetIds`. Assets the client tracks on purpose
|
|
241
|
-
* survive a `full` replace even when the response omits them, so an upstream
|
|
242
|
-
* gap cannot wipe them — see `skipDelete`.
|
|
243
|
-
*
|
|
244
|
-
* @param previousBalances - Balances already in state for this account.
|
|
245
|
-
* @param accountBalances - Balances from the incoming response.
|
|
246
|
-
* @param skipDelete - Whether a prior asset must be kept even though the
|
|
247
|
-
* response omits it, because the client asked for it as a visible asset
|
|
248
|
-
* (native, pin, or default tracked). Hidden assets are excluded from
|
|
249
|
-
* snapshots and dropped here; unhide is expected to fetch a new snapshot.
|
|
250
|
-
* @param updateMode - `'merge'` overlays incoming balances; `'full'` replaces
|
|
251
|
-
* each covered chain slice except assets kept by `skipDelete`.
|
|
252
|
-
* @returns The effective balance map for the account.
|
|
253
|
-
*/
|
|
254
|
-
function effectiveAccountBalancesV6(previousBalances, accountBalances, skipDelete, updateMode) {
|
|
255
|
-
if (updateMode === 'merge') {
|
|
256
|
-
return { ...previousBalances, ...accountBalances };
|
|
257
|
-
}
|
|
258
|
-
const coveredChains = new Set(Object.keys(accountBalances).map((assetId) => assetId.split('/')[0]));
|
|
259
|
-
const next = {};
|
|
260
|
-
for (const [assetId, balance] of Object.entries(previousBalances)) {
|
|
261
|
-
if (!coveredChains.has(assetId.split('/')[0]) ||
|
|
262
|
-
skipDelete(assetId)) {
|
|
263
|
-
next[assetId] = balance;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
Object.assign(next, accountBalances);
|
|
267
|
-
return next;
|
|
268
|
-
}
|
|
269
231
|
// ============================================================================
|
|
270
232
|
// CONTROLLER IMPLEMENTATION
|
|
271
233
|
// ============================================================================
|
|
@@ -501,24 +463,17 @@ export class AssetsController extends BaseController {
|
|
|
501
463
|
queryApiClient,
|
|
502
464
|
onActiveChainsUpdated: this.#onActiveChainsUpdated,
|
|
503
465
|
...accountsApiDataSourceConfig,
|
|
504
|
-
isBalanceV6Enabled: () => this.#isBalanceV6Enabled(),
|
|
505
|
-
getAssetsState: () => this.state,
|
|
506
|
-
getAssetVisibility: this.#getAssetVisibility.bind(this),
|
|
507
466
|
});
|
|
508
467
|
this.#snapDataSource = new SnapDataSource({
|
|
509
468
|
messenger: this.messenger,
|
|
510
469
|
onActiveChainsUpdated: this.#onActiveChainsUpdated,
|
|
511
470
|
onAssetsUpdate: (response) => this.handleAssetsUpdate(response, 'SnapDataSource'),
|
|
512
|
-
isBalanceV6Enabled: () => this.#isBalanceV6Enabled(),
|
|
513
|
-
getAssetVisibility: this.#getAssetVisibility.bind(this),
|
|
514
471
|
});
|
|
515
472
|
this.#rpcDataSource = new RpcDataSource({
|
|
516
473
|
messenger: this.messenger,
|
|
517
|
-
getAssetsState: () => this.state,
|
|
518
|
-
isBalanceV6Enabled: () => this.#isBalanceV6Enabled(),
|
|
519
|
-
getAssetVisibility: this.#getAssetVisibility.bind(this),
|
|
520
474
|
onActiveChainsUpdated: this.#onActiveChainsUpdated,
|
|
521
|
-
getNativeAssetForChain: (chainId) => this.#
|
|
475
|
+
getNativeAssetForChain: (chainId) => this.#getNativeAssetMap()[chainId] ??
|
|
476
|
+
`${chainId}/erc20:${ZERO_ADDRESS}`,
|
|
522
477
|
// Share the API platform's TanStack Query client so the RPC token
|
|
523
478
|
// detector caches/dedupes its top-token-list fetches alongside the rest
|
|
524
479
|
// of the package's API calls. Caller-provided rpcConfig.queryClient
|
|
@@ -539,17 +494,13 @@ export class AssetsController extends BaseController {
|
|
|
539
494
|
return this.#getNativeAssetIdsForEnabledChains();
|
|
540
495
|
},
|
|
541
496
|
getAssetType: (assetId) => this.#getAssetType(assetId),
|
|
542
|
-
getAssetsState: () => this.state,
|
|
543
497
|
});
|
|
544
498
|
this.#priceDataSource = new PriceDataSource({
|
|
545
499
|
queryApiClient,
|
|
546
500
|
getSelectedCurrency: () => this.state.selectedCurrency,
|
|
547
|
-
getAssetsState: () => this.state,
|
|
548
501
|
...priceDataSourceConfig,
|
|
549
502
|
});
|
|
550
|
-
this.#detectionMiddleware = new DetectionMiddleware(
|
|
551
|
-
getAssetsState: () => this.state,
|
|
552
|
-
});
|
|
503
|
+
this.#detectionMiddleware = new DetectionMiddleware();
|
|
553
504
|
this.#customAssetGraduationMiddleware = new CustomAssetGraduationMiddleware({
|
|
554
505
|
getSelectedAccountId: () => {
|
|
555
506
|
try {
|
|
@@ -560,12 +511,9 @@ export class AssetsController extends BaseController {
|
|
|
560
511
|
}
|
|
561
512
|
},
|
|
562
513
|
removeCustomAsset: (accountId, assetId) => this.removeCustomAsset(accountId, assetId),
|
|
563
|
-
getAssetsState: () => this.state,
|
|
564
514
|
});
|
|
565
515
|
this.#rpcFallbackMiddleware = new RpcFallbackMiddleware({
|
|
566
516
|
rpcDataSource: this.#rpcDataSource,
|
|
567
|
-
isBalanceV6Enabled: () => this.#isBalanceV6Enabled(),
|
|
568
|
-
getAssetsState: () => this.state,
|
|
569
517
|
});
|
|
570
518
|
log('Initializing AssetsController', {
|
|
571
519
|
defaultUpdateInterval: this.#defaultUpdateInterval,
|
|
@@ -893,7 +841,7 @@ export class AssetsController extends BaseController {
|
|
|
893
841
|
* Returns response and exclusive duration per source (sum ≈ wall time).
|
|
894
842
|
*
|
|
895
843
|
* Thin wrapper over {@link executeAssetsPipeline} that supplies the
|
|
896
|
-
* controller-owned exception reporter.
|
|
844
|
+
* controller-owned state accessor and exception reporter.
|
|
897
845
|
*
|
|
898
846
|
* @param params - Middleware execution options.
|
|
899
847
|
* @param params.sources - Data sources or middlewares with getName() and assetsMiddleware.
|
|
@@ -906,6 +854,7 @@ export class AssetsController extends BaseController {
|
|
|
906
854
|
async #executeMiddlewares(params) {
|
|
907
855
|
return executeAssetsPipeline({
|
|
908
856
|
...params,
|
|
857
|
+
getAssetsState: () => this.state,
|
|
909
858
|
captureException: this.#captureException,
|
|
910
859
|
});
|
|
911
860
|
}
|
|
@@ -919,255 +868,143 @@ export class AssetsController extends BaseController {
|
|
|
919
868
|
if (accounts.length === 0 || chainIds.length === 0) {
|
|
920
869
|
return this.#getAssetsFromState(accounts, chainIds, assetTypes);
|
|
921
870
|
}
|
|
871
|
+
// Collect custom assets for all requested accounts
|
|
872
|
+
const customAssets = [];
|
|
873
|
+
for (const account of accounts) {
|
|
874
|
+
const accountCustomAssets = this.getCustomAssets(account.id);
|
|
875
|
+
customAssets.push(...accountCustomAssets);
|
|
876
|
+
}
|
|
922
877
|
if (options?.forceUpdate) {
|
|
878
|
+
// Pipeline spans only on unlock/first-init fetch; later forceUpdates pass
|
|
879
|
+
// `undefined` so `emitTrace` / `withTrace` no-op without call-site if/else.
|
|
923
880
|
const pipelineTrace = this.#firstInitFetchReported
|
|
924
881
|
? undefined
|
|
925
882
|
: this.#trace;
|
|
926
|
-
const
|
|
883
|
+
const request = this.#buildDataRequest(accounts, chainIds, {
|
|
927
884
|
assetTypes,
|
|
928
885
|
dataTypes,
|
|
886
|
+
customAssets: customAssets.length > 0 ? customAssets : undefined,
|
|
929
887
|
forceUpdate: true,
|
|
930
888
|
bypassServerCache: options?.bypassServerCache,
|
|
931
889
|
assetsForPriceUpdate: options?.assetsForPriceUpdate,
|
|
932
|
-
};
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
890
|
+
});
|
|
891
|
+
// Fast pipeline: accountsApi + stakedBalance → detection → token + price.
|
|
892
|
+
// Snap and RPC are excluded here due to their latency (snap triggers account
|
|
893
|
+
// creation, RPC is slow on many chains). Results are committed to state
|
|
894
|
+
// immediately so the UI can display balances without waiting for them.
|
|
895
|
+
//
|
|
896
|
+
// Fast/slow pipelines use merge so partial API snapshots cannot wipe
|
|
897
|
+
// tokens missing from the response (e.g. USDC when only native balance
|
|
898
|
+
// is returned). Balances present in the response are still refreshed.
|
|
899
|
+
const fastSources = buildFastFetchSources({
|
|
900
|
+
accountsApiDataSource: this.#accountsApiDataSource,
|
|
901
|
+
stakedBalanceDataSource: this.#stakedBalanceDataSource,
|
|
902
|
+
customAssetGraduationMiddleware: this.#customAssetGraduationMiddleware,
|
|
903
|
+
rpcFallbackMiddleware: this.#rpcFallbackMiddleware,
|
|
904
|
+
detectionMiddleware: this.#detectionMiddleware,
|
|
905
|
+
tokenDataSource: this.#tokenDataSource,
|
|
906
|
+
priceDataSource: this.#priceDataSource,
|
|
907
|
+
}, { isBasicFunctionality: this.#isBasicFunctionality() });
|
|
908
|
+
const { response } = await withTrace({
|
|
909
|
+
name: TRACE_FETCH_PIPELINE,
|
|
910
|
+
trace: pipelineTrace,
|
|
911
|
+
data: {
|
|
912
|
+
chain_count: chainIds.length,
|
|
913
|
+
account_count: accounts.length,
|
|
914
|
+
basic_functionality: this.#isBasicFunctionality(),
|
|
915
|
+
},
|
|
916
|
+
fn: async (parentContext) => {
|
|
917
|
+
const startTime = performance.now();
|
|
918
|
+
const result = await this.#executeMiddlewares({
|
|
919
|
+
sources: fastSources,
|
|
920
|
+
request,
|
|
921
|
+
parentContext,
|
|
922
|
+
trace: pipelineTrace,
|
|
923
|
+
});
|
|
924
|
+
await this.#updateState({
|
|
925
|
+
...result.response,
|
|
926
|
+
updateMode: 'merge',
|
|
927
|
+
replaceCoveredChainBalances: true,
|
|
928
|
+
});
|
|
929
|
+
const durationMs = performance.now() - startTime;
|
|
930
|
+
// Summary fields for Assets Health (nested under the parent span).
|
|
931
|
+
emitTrace({
|
|
932
|
+
name: TRACE_FULL_FETCH,
|
|
933
|
+
trace: pipelineTrace,
|
|
934
|
+
data: {
|
|
935
|
+
duration_ms: durationMs,
|
|
936
|
+
chain_count: chainIds.length,
|
|
937
|
+
account_count: accounts.length,
|
|
938
|
+
basic_functionality: this.#isBasicFunctionality(),
|
|
939
|
+
asset_count: result.response.assetsBalance
|
|
940
|
+
? Object.values(result.response.assetsBalance).reduce((sum, acct) => sum + Object.keys(acct).length, 0)
|
|
941
|
+
: 0,
|
|
942
|
+
price_count: result.response.assetsPrice
|
|
943
|
+
? Object.keys(result.response.assetsPrice).length
|
|
944
|
+
: 0,
|
|
945
|
+
...result.durationByDataSource,
|
|
946
|
+
},
|
|
947
|
+
parentContext,
|
|
948
|
+
});
|
|
949
|
+
emitTrace({
|
|
950
|
+
name: TRACE_FIRST_INIT_FETCH,
|
|
951
|
+
trace: pipelineTrace,
|
|
952
|
+
data: {
|
|
953
|
+
duration_ms: durationMs,
|
|
954
|
+
chain_ids: JSON.stringify(chainIds),
|
|
955
|
+
...result.durationByDataSource,
|
|
956
|
+
},
|
|
957
|
+
parentContext,
|
|
958
|
+
});
|
|
959
|
+
return result;
|
|
960
|
+
},
|
|
961
|
+
});
|
|
962
|
+
// Mark after the unlock/first forceUpdate so later polls skip pipeline spans.
|
|
963
|
+
this.#firstInitFetchReported = true;
|
|
964
|
+
// Background (slow) lane — flattened sibling of the fast lane (not nested
|
|
965
|
+
// inside its withTrace callback). Still fire-and-forget so UI is not blocked.
|
|
966
|
+
const slowPipelineChainIds = this.#getSlowPipelineChainIds(chainIds, response);
|
|
967
|
+
if (slowPipelineChainIds.length > 0) {
|
|
968
|
+
const slowSources = this.#isBasicFunctionality()
|
|
969
|
+
? [this.#snapDataSource, this.#rpcDataSource]
|
|
970
|
+
: [this.#rpcDataSource];
|
|
971
|
+
const slowRequest = { ...request, chainIds: slowPipelineChainIds };
|
|
972
|
+
withTrace({
|
|
973
|
+
name: TRACE_BACKGROUND_FETCH,
|
|
974
|
+
trace: pipelineTrace,
|
|
975
|
+
data: {
|
|
976
|
+
chain_count: slowPipelineChainIds.length,
|
|
977
|
+
account_count: accounts.length,
|
|
978
|
+
},
|
|
979
|
+
fn: async (slowParentContext) => {
|
|
980
|
+
const { response: slowResponse } = await this.#executeMiddlewares({
|
|
981
|
+
sources: [
|
|
982
|
+
createParallelBalanceMiddleware(slowSources),
|
|
983
|
+
this.#detectionMiddleware,
|
|
984
|
+
...(this.#isBasicFunctionality()
|
|
985
|
+
? [
|
|
986
|
+
createParallelMiddleware([
|
|
987
|
+
this.#tokenDataSource,
|
|
988
|
+
this.#priceDataSource,
|
|
989
|
+
]),
|
|
990
|
+
]
|
|
991
|
+
: []),
|
|
992
|
+
],
|
|
993
|
+
request: slowRequest,
|
|
994
|
+
parentContext: slowParentContext,
|
|
995
|
+
trace: pipelineTrace,
|
|
996
|
+
});
|
|
997
|
+
await this.#updateState({
|
|
998
|
+
...slowResponse,
|
|
999
|
+
updateMode: 'merge',
|
|
1000
|
+
});
|
|
1001
|
+
},
|
|
1002
|
+
}).catch((error) => log('Background pipeline failed', { error }));
|
|
948
1003
|
}
|
|
949
1004
|
}
|
|
950
1005
|
const result = this.#getAssetsFromState(accounts, chainIds, assetTypes);
|
|
951
1006
|
return result;
|
|
952
1007
|
}
|
|
953
|
-
/**
|
|
954
|
-
* v5 force-update request: every pin of the requested accounts, unscoped.
|
|
955
|
-
* Delete with the rest of the v5 path when `assetsAccountsApiV6` is the default.
|
|
956
|
-
*
|
|
957
|
-
* @param accounts - Accounts in this fetch.
|
|
958
|
-
* @param chainIds - Chains in this fetch.
|
|
959
|
-
* @param requestOptions - Shared force-update request fields.
|
|
960
|
-
* @param requestOptions.assetTypes - Asset types to fetch.
|
|
961
|
-
* @param requestOptions.dataTypes - Data types to fetch.
|
|
962
|
-
* @param requestOptions.forceUpdate - Always `true` to bypass caches.
|
|
963
|
-
* @param requestOptions.bypassServerCache - Also bypass server-side HTTP caches.
|
|
964
|
-
* @param requestOptions.assetsForPriceUpdate - Assets to refresh prices for.
|
|
965
|
-
* @returns The v5 data request.
|
|
966
|
-
*/
|
|
967
|
-
#buildForceUpdateRequestV5(accounts, chainIds, requestOptions) {
|
|
968
|
-
const customAssets = [];
|
|
969
|
-
for (const account of accounts) {
|
|
970
|
-
customAssets.push(...this.getCustomAssets(account.id));
|
|
971
|
-
}
|
|
972
|
-
return this.#buildDataRequest(accounts, chainIds, {
|
|
973
|
-
...requestOptions,
|
|
974
|
-
customAssets: customAssets.length > 0 ? customAssets : undefined,
|
|
975
|
-
});
|
|
976
|
-
}
|
|
977
|
-
async #forceUpdateAssetsV5({ accounts, chainIds, request, pipelineTrace, }) {
|
|
978
|
-
const isBasicFunctionality = this.#isBasicFunctionality();
|
|
979
|
-
const fastSources = buildFastFetchSources({
|
|
980
|
-
accountsApiDataSource: this.#accountsApiDataSource,
|
|
981
|
-
stakedBalanceDataSource: this.#stakedBalanceDataSource,
|
|
982
|
-
customAssetGraduationMiddleware: this.#customAssetGraduationMiddleware,
|
|
983
|
-
rpcFallbackMiddleware: this.#rpcFallbackMiddleware,
|
|
984
|
-
detectionMiddleware: this.#detectionMiddleware,
|
|
985
|
-
tokenDataSource: this.#tokenDataSource,
|
|
986
|
-
priceDataSource: this.#priceDataSource,
|
|
987
|
-
}, { isBasicFunctionality, includeCustomAssetGraduation: true });
|
|
988
|
-
const fastResponse = await this.#runFastFetchV5({
|
|
989
|
-
accounts,
|
|
990
|
-
chainIds,
|
|
991
|
-
request,
|
|
992
|
-
pipelineTrace,
|
|
993
|
-
fastSources,
|
|
994
|
-
isBasicFunctionality,
|
|
995
|
-
});
|
|
996
|
-
this.#firstInitFetchReported = true;
|
|
997
|
-
this.#runBackgroundFetch({
|
|
998
|
-
accounts,
|
|
999
|
-
request,
|
|
1000
|
-
pipelineTrace,
|
|
1001
|
-
chainIds: this.#getSlowPipelineChainIds(chainIds, fastResponse),
|
|
1002
|
-
isBasicFunctionality,
|
|
1003
|
-
});
|
|
1004
|
-
}
|
|
1005
|
-
async #forceUpdateAssetsV6({ accounts, chainIds, request, pipelineTrace, }) {
|
|
1006
|
-
const isBasicFunctionality = this.#isBasicFunctionality();
|
|
1007
|
-
const fastSources = buildFastFetchSources({
|
|
1008
|
-
accountsApiDataSource: this.#accountsApiDataSource,
|
|
1009
|
-
stakedBalanceDataSource: this.#stakedBalanceDataSource,
|
|
1010
|
-
customAssetGraduationMiddleware: this.#customAssetGraduationMiddleware,
|
|
1011
|
-
rpcFallbackMiddleware: this.#rpcFallbackMiddleware,
|
|
1012
|
-
detectionMiddleware: this.#detectionMiddleware,
|
|
1013
|
-
tokenDataSource: this.#tokenDataSource,
|
|
1014
|
-
priceDataSource: this.#priceDataSource,
|
|
1015
|
-
}, {
|
|
1016
|
-
isBasicFunctionality,
|
|
1017
|
-
includeCustomAssetGraduation: false,
|
|
1018
|
-
});
|
|
1019
|
-
const fastResponse = await this.#runFastFetchV6({
|
|
1020
|
-
accounts,
|
|
1021
|
-
chainIds,
|
|
1022
|
-
request,
|
|
1023
|
-
pipelineTrace,
|
|
1024
|
-
fastSources,
|
|
1025
|
-
isBasicFunctionality,
|
|
1026
|
-
});
|
|
1027
|
-
this.#firstInitFetchReported = true;
|
|
1028
|
-
this.#runBackgroundFetch({
|
|
1029
|
-
accounts,
|
|
1030
|
-
request,
|
|
1031
|
-
pipelineTrace,
|
|
1032
|
-
chainIds: this.#getSlowPipelineChainIds(chainIds, fastResponse),
|
|
1033
|
-
isBasicFunctionality,
|
|
1034
|
-
});
|
|
1035
|
-
}
|
|
1036
|
-
async #runFastFetchV5({ accounts, chainIds, request, pipelineTrace, fastSources, isBasicFunctionality, }) {
|
|
1037
|
-
const { response } = await withTrace({
|
|
1038
|
-
name: TRACE_FETCH_PIPELINE,
|
|
1039
|
-
trace: pipelineTrace,
|
|
1040
|
-
data: {
|
|
1041
|
-
chain_count: chainIds.length,
|
|
1042
|
-
account_count: accounts.length,
|
|
1043
|
-
basic_functionality: isBasicFunctionality,
|
|
1044
|
-
},
|
|
1045
|
-
fn: async (parentContext) => {
|
|
1046
|
-
const startTime = performance.now();
|
|
1047
|
-
const result = await this.#executeMiddlewares({
|
|
1048
|
-
sources: fastSources,
|
|
1049
|
-
request,
|
|
1050
|
-
parentContext,
|
|
1051
|
-
trace: pipelineTrace,
|
|
1052
|
-
});
|
|
1053
|
-
await this.#updateState({
|
|
1054
|
-
...result.response,
|
|
1055
|
-
updateMode: 'merge',
|
|
1056
|
-
replaceCoveredChainBalances: true,
|
|
1057
|
-
});
|
|
1058
|
-
this.#emitFastFetchTraces({
|
|
1059
|
-
accounts,
|
|
1060
|
-
chainIds,
|
|
1061
|
-
result,
|
|
1062
|
-
durationMs: performance.now() - startTime,
|
|
1063
|
-
isBasicFunctionality,
|
|
1064
|
-
parentContext,
|
|
1065
|
-
pipelineTrace,
|
|
1066
|
-
});
|
|
1067
|
-
return result;
|
|
1068
|
-
},
|
|
1069
|
-
});
|
|
1070
|
-
return response;
|
|
1071
|
-
}
|
|
1072
|
-
async #runFastFetchV6({ accounts, chainIds, request, pipelineTrace, fastSources, isBasicFunctionality, }) {
|
|
1073
|
-
const { response } = await withTrace({
|
|
1074
|
-
name: TRACE_FETCH_PIPELINE,
|
|
1075
|
-
trace: pipelineTrace,
|
|
1076
|
-
data: {
|
|
1077
|
-
chain_count: chainIds.length,
|
|
1078
|
-
account_count: accounts.length,
|
|
1079
|
-
basic_functionality: isBasicFunctionality,
|
|
1080
|
-
},
|
|
1081
|
-
fn: async (parentContext) => {
|
|
1082
|
-
const startTime = performance.now();
|
|
1083
|
-
const result = await this.#executeMiddlewares({
|
|
1084
|
-
sources: fastSources,
|
|
1085
|
-
request,
|
|
1086
|
-
parentContext,
|
|
1087
|
-
trace: pipelineTrace,
|
|
1088
|
-
});
|
|
1089
|
-
await this.#updateState(result.response);
|
|
1090
|
-
this.#emitFastFetchTraces({
|
|
1091
|
-
accounts,
|
|
1092
|
-
chainIds,
|
|
1093
|
-
result,
|
|
1094
|
-
durationMs: performance.now() - startTime,
|
|
1095
|
-
isBasicFunctionality,
|
|
1096
|
-
parentContext,
|
|
1097
|
-
pipelineTrace,
|
|
1098
|
-
});
|
|
1099
|
-
return result;
|
|
1100
|
-
},
|
|
1101
|
-
});
|
|
1102
|
-
return response;
|
|
1103
|
-
}
|
|
1104
|
-
#runBackgroundFetch({ accounts, chainIds, request, pipelineTrace, isBasicFunctionality, }) {
|
|
1105
|
-
if (chainIds.length === 0) {
|
|
1106
|
-
return;
|
|
1107
|
-
}
|
|
1108
|
-
const slowSources = isBasicFunctionality
|
|
1109
|
-
? [this.#snapDataSource, this.#rpcDataSource]
|
|
1110
|
-
: [this.#rpcDataSource];
|
|
1111
|
-
const slowRequest = { ...request, chainIds };
|
|
1112
|
-
withTrace({
|
|
1113
|
-
name: TRACE_BACKGROUND_FETCH,
|
|
1114
|
-
trace: pipelineTrace,
|
|
1115
|
-
data: {
|
|
1116
|
-
chain_count: chainIds.length,
|
|
1117
|
-
account_count: accounts.length,
|
|
1118
|
-
},
|
|
1119
|
-
fn: async (parentContext) => {
|
|
1120
|
-
const { response } = await this.#executeMiddlewares({
|
|
1121
|
-
sources: [
|
|
1122
|
-
createParallelBalanceMiddleware(slowSources),
|
|
1123
|
-
this.#detectionMiddleware,
|
|
1124
|
-
...(isBasicFunctionality
|
|
1125
|
-
? [
|
|
1126
|
-
createParallelMiddleware([
|
|
1127
|
-
this.#tokenDataSource,
|
|
1128
|
-
this.#priceDataSource,
|
|
1129
|
-
]),
|
|
1130
|
-
]
|
|
1131
|
-
: []),
|
|
1132
|
-
],
|
|
1133
|
-
request: slowRequest,
|
|
1134
|
-
parentContext,
|
|
1135
|
-
trace: pipelineTrace,
|
|
1136
|
-
});
|
|
1137
|
-
await this.#updateState(response);
|
|
1138
|
-
},
|
|
1139
|
-
}).catch((error) => log('Background pipeline failed', { error }));
|
|
1140
|
-
}
|
|
1141
|
-
#emitFastFetchTraces({ accounts, chainIds, result, durationMs, isBasicFunctionality, parentContext, pipelineTrace, }) {
|
|
1142
|
-
emitTrace({
|
|
1143
|
-
name: TRACE_FULL_FETCH,
|
|
1144
|
-
trace: pipelineTrace,
|
|
1145
|
-
data: {
|
|
1146
|
-
duration_ms: durationMs,
|
|
1147
|
-
chain_count: chainIds.length,
|
|
1148
|
-
account_count: accounts.length,
|
|
1149
|
-
basic_functionality: isBasicFunctionality,
|
|
1150
|
-
asset_count: result.response.assetsBalance
|
|
1151
|
-
? Object.values(result.response.assetsBalance).reduce((sum, account) => sum + Object.keys(account).length, 0)
|
|
1152
|
-
: 0,
|
|
1153
|
-
price_count: result.response.assetsPrice
|
|
1154
|
-
? Object.keys(result.response.assetsPrice).length
|
|
1155
|
-
: 0,
|
|
1156
|
-
...result.durationByDataSource,
|
|
1157
|
-
},
|
|
1158
|
-
parentContext,
|
|
1159
|
-
});
|
|
1160
|
-
emitTrace({
|
|
1161
|
-
name: TRACE_FIRST_INIT_FETCH,
|
|
1162
|
-
trace: pipelineTrace,
|
|
1163
|
-
data: {
|
|
1164
|
-
duration_ms: durationMs,
|
|
1165
|
-
chain_ids: JSON.stringify(chainIds),
|
|
1166
|
-
...result.durationByDataSource,
|
|
1167
|
-
},
|
|
1168
|
-
parentContext,
|
|
1169
|
-
});
|
|
1170
|
-
}
|
|
1171
1008
|
async getAssetsBalance(accounts, options) {
|
|
1172
1009
|
// Reuse getAssets with dataTypes: ['balance'] only
|
|
1173
1010
|
const assets = await this.getAssets(accounts, {
|
|
@@ -1376,8 +1213,7 @@ export class AssetsController extends BaseController {
|
|
|
1376
1213
|
/**
|
|
1377
1214
|
* Add a custom asset for an account.
|
|
1378
1215
|
* Custom assets are included in subscription and fetch operations.
|
|
1379
|
-
* Adding a custom asset also unhides it if it was previously hidden
|
|
1380
|
-
* and force-fetches that asset's chain (including every visible pin).
|
|
1216
|
+
* Adding a custom asset also unhides it if it was previously hidden.
|
|
1381
1217
|
*
|
|
1382
1218
|
* When `pendingMetadata` is provided (e.g. from the extension's pending-tokens
|
|
1383
1219
|
* flow), the token metadata is persisted immediately into `assetsInfo` so the
|
|
@@ -1436,16 +1272,20 @@ export class AssetsController extends BaseController {
|
|
|
1436
1272
|
balances[accountId] ??= {};
|
|
1437
1273
|
balances[accountId][normalizedAssetId] ??= { amount: '0' };
|
|
1438
1274
|
});
|
|
1275
|
+
// Fetch data for the newly added custom asset (merge to preserve other chains)
|
|
1439
1276
|
const account = this.#getSelectedAccounts().find((a) => a.id === accountId);
|
|
1440
1277
|
if (account) {
|
|
1278
|
+
const chainId = extractChainId(normalizedAssetId);
|
|
1441
1279
|
await this.getAssets([account], {
|
|
1442
|
-
chainIds: [
|
|
1280
|
+
chainIds: [chainId],
|
|
1443
1281
|
dataTypes: ['balance', 'metadata', 'price'],
|
|
1444
1282
|
assetTypes: ['fungible'],
|
|
1445
1283
|
forceUpdate: true,
|
|
1284
|
+
updateMode: 'merge',
|
|
1446
1285
|
});
|
|
1447
1286
|
}
|
|
1448
|
-
// Re-evaluate subscriptions so
|
|
1287
|
+
// Re-evaluate subscriptions so the supplemental RPC poll picks up the
|
|
1288
|
+
// new customAsset on chains another data source already owns.
|
|
1449
1289
|
this.#subscribeAssets();
|
|
1450
1290
|
}
|
|
1451
1291
|
/**
|
|
@@ -1466,7 +1306,8 @@ export class AssetsController extends BaseController {
|
|
|
1466
1306
|
}
|
|
1467
1307
|
}
|
|
1468
1308
|
});
|
|
1469
|
-
// Re-evaluate subscriptions so
|
|
1309
|
+
// Re-evaluate subscriptions so the supplemental RPC poll for that chain
|
|
1310
|
+
// is torn down when no more customAssets remain there.
|
|
1470
1311
|
this.#subscribeAssets();
|
|
1471
1312
|
}
|
|
1472
1313
|
/**
|
|
@@ -1497,18 +1338,13 @@ export class AssetsController extends BaseController {
|
|
|
1497
1338
|
}
|
|
1498
1339
|
state.assetPreferences[normalizedAssetId].hidden = true;
|
|
1499
1340
|
});
|
|
1500
|
-
// Re-evaluate subscriptions so polls exclude the newly hidden asset.
|
|
1501
|
-
this.#subscribeAssets();
|
|
1502
1341
|
}
|
|
1503
1342
|
/**
|
|
1504
1343
|
* Unhide an asset globally.
|
|
1505
|
-
* Force-fetches that asset's chain (including every visible pin) so a
|
|
1506
|
-
* `full` snapshot can restore the balance immediately, then re-evaluates
|
|
1507
|
-
* subscriptions so later polls stop excluding it.
|
|
1508
1344
|
*
|
|
1509
1345
|
* @param assetId - The CAIP-19 asset ID to unhide.
|
|
1510
1346
|
*/
|
|
1511
|
-
|
|
1347
|
+
unhideAsset(assetId) {
|
|
1512
1348
|
const normalizedAssetId = normalizeAssetId(assetId);
|
|
1513
1349
|
log('Unhiding asset', { assetId: normalizedAssetId });
|
|
1514
1350
|
this.update((state) => {
|
|
@@ -1520,35 +1356,6 @@ export class AssetsController extends BaseController {
|
|
|
1520
1356
|
}
|
|
1521
1357
|
}
|
|
1522
1358
|
});
|
|
1523
|
-
const chainId = extractChainId(normalizedAssetId);
|
|
1524
|
-
const account = this.#getSelectedAccounts().find((selectedAccount) => this.#getEnabledChainsForAccount(selectedAccount).includes(chainId));
|
|
1525
|
-
if (account) {
|
|
1526
|
-
await this.getAssets([account], {
|
|
1527
|
-
chainIds: [chainId],
|
|
1528
|
-
dataTypes: ['balance', 'metadata', 'price'],
|
|
1529
|
-
assetTypes: ['fungible'],
|
|
1530
|
-
forceUpdate: true,
|
|
1531
|
-
});
|
|
1532
|
-
}
|
|
1533
|
-
this.#subscribeAssets();
|
|
1534
|
-
}
|
|
1535
|
-
/**
|
|
1536
|
-
* Whether Accounts API v6 (and the v6 custom-asset path) is enabled.
|
|
1537
|
-
* Injected into AccountsApiDataSource and RpcFallbackMiddleware.
|
|
1538
|
-
*
|
|
1539
|
-
* `RemoteFeatureFlagController` resolves a threshold-scoped flag to the
|
|
1540
|
-
* selected group's `value`, so processed state is a boolean.
|
|
1541
|
-
*
|
|
1542
|
-
* @returns `true` when the v6 remote flag is on.
|
|
1543
|
-
*/
|
|
1544
|
-
#isBalanceV6Enabled() {
|
|
1545
|
-
try {
|
|
1546
|
-
const { remoteFeatureFlags } = this.messenger.call('RemoteFeatureFlagController:getState');
|
|
1547
|
-
return remoteFeatureFlags?.assetsAccountsApiV6 === true;
|
|
1548
|
-
}
|
|
1549
|
-
catch {
|
|
1550
|
-
return false;
|
|
1551
|
-
}
|
|
1552
1359
|
}
|
|
1553
1360
|
// ============================================================================
|
|
1554
1361
|
// CURRENT CURRENCY MANAGEMENT
|
|
@@ -1654,6 +1461,7 @@ export class AssetsController extends BaseController {
|
|
|
1654
1461
|
subscriptionId: subscriptionKey,
|
|
1655
1462
|
isUpdate,
|
|
1656
1463
|
onAssetsUpdate: (response) => this.handleAssetsUpdate(response, 'PriceDataSource'),
|
|
1464
|
+
getAssetsState: () => this.state,
|
|
1657
1465
|
};
|
|
1658
1466
|
this.#priceDataSource.subscribe(subscribeReq).catch(console.error);
|
|
1659
1467
|
// Track subscription
|
|
@@ -1693,51 +1501,6 @@ export class AssetsController extends BaseController {
|
|
|
1693
1501
|
#getNativeAssetMap() {
|
|
1694
1502
|
return (this.#queryApiClient.getCachedData(NATIVE_ASSETS_QUERY_KEY) ?? EMPTY_NATIVE_ASSET_MAP);
|
|
1695
1503
|
}
|
|
1696
|
-
/**
|
|
1697
|
-
* Canonical native CAIP-19 ID for a chain. Uses the native asset map when
|
|
1698
|
-
* the chain is registered; otherwise falls back to the zero-address ERC-20
|
|
1699
|
-
* encoding used for EVM natives without a SLIP-44 id.
|
|
1700
|
-
*
|
|
1701
|
-
* @param chainId - CAIP-2 chain ID.
|
|
1702
|
-
* @returns The native asset ID for the chain.
|
|
1703
|
-
*/
|
|
1704
|
-
#getNativeAssetForChain(chainId) {
|
|
1705
|
-
return (this.#getNativeAssetMap()[chainId] ??
|
|
1706
|
-
`${chainId}/erc20:${ZERO_ADDRESS}`);
|
|
1707
|
-
}
|
|
1708
|
-
#getAssetVisibility(accountIds, chainIds) {
|
|
1709
|
-
return getAssetVisibility({
|
|
1710
|
-
state: this.state,
|
|
1711
|
-
accountIds,
|
|
1712
|
-
chainIds,
|
|
1713
|
-
getNativeAssetForChain: (chainId) => this.#getNativeAssetForChain(chainId),
|
|
1714
|
-
});
|
|
1715
|
-
}
|
|
1716
|
-
/**
|
|
1717
|
-
* Lowercased asset IDs a v6 `full` snapshot must keep for an account, even
|
|
1718
|
-
* when the response omits them: natives, pins, and default tracked assets
|
|
1719
|
-
* the client asked for. Hidden assets are sent as `excludeAssetIds` and are
|
|
1720
|
-
* dropped here; unhide is expected to fetch a new snapshot. Everything else
|
|
1721
|
-
* on a covered chain was discovered by the source, so its absence means the
|
|
1722
|
-
* balance is gone.
|
|
1723
|
-
*
|
|
1724
|
-
* @param state - State being updated (the in-flight draft, not `this.state`).
|
|
1725
|
-
* @param accountId - Account whose pins apply.
|
|
1726
|
-
* @param accountBalances - Incoming balances, which define the covered chains.
|
|
1727
|
-
* @returns Lowercased CAIP-19 IDs that must survive the replace.
|
|
1728
|
-
*/
|
|
1729
|
-
#getUndeletableAssetIds(state, accountId, accountBalances) {
|
|
1730
|
-
const coveredChainIds = [
|
|
1731
|
-
...new Set(Object.keys(accountBalances).map((assetId) => assetId.split('/')[0])),
|
|
1732
|
-
];
|
|
1733
|
-
const { visibleAssetIds } = getAssetVisibility({
|
|
1734
|
-
state,
|
|
1735
|
-
accountIds: [accountId],
|
|
1736
|
-
chainIds: coveredChainIds,
|
|
1737
|
-
getNativeAssetForChain: (chainId) => this.#getNativeAssetForChain(chainId),
|
|
1738
|
-
});
|
|
1739
|
-
return new Set(visibleAssetIds.map((assetId) => assetId.toLowerCase()));
|
|
1740
|
-
}
|
|
1741
1504
|
/**
|
|
1742
1505
|
* Checks whether the given CAIP-19 asset ID represents a native asset
|
|
1743
1506
|
* according to the cached native asset map, the asset namespace, and the asset reference.
|
|
@@ -1930,7 +1693,6 @@ export class AssetsController extends BaseController {
|
|
|
1930
1693
|
async #updateState(response) {
|
|
1931
1694
|
const normalizedResponse = normalizeResponse(response);
|
|
1932
1695
|
const mode = normalizedResponse.updateMode ?? 'merge';
|
|
1933
|
-
const isBalanceV6Enabled = this.#isBalanceV6Enabled();
|
|
1934
1696
|
const releaseLock = await this.#controllerMutex.acquire();
|
|
1935
1697
|
try {
|
|
1936
1698
|
const previousState = this.state;
|
|
@@ -1990,23 +1752,24 @@ export class AssetsController extends BaseController {
|
|
|
1990
1752
|
}
|
|
1991
1753
|
}
|
|
1992
1754
|
if (normalizedResponse.assetsBalance) {
|
|
1993
|
-
const
|
|
1755
|
+
for (const [accountId, accountBalances] of Object.entries(normalizedResponse.assetsBalance)) {
|
|
1756
|
+
const previousBalances = previousState.assetsBalance[accountId] ?? {};
|
|
1994
1757
|
const customAssetIds = state.customAssets[accountId] ?? [];
|
|
1995
1758
|
const replaceCoveredChains = mode === 'full' ||
|
|
1996
1759
|
normalizedResponse.replaceCoveredChainBalances === true;
|
|
1997
|
-
const
|
|
1760
|
+
const effective = mergeAccountBalances(previousBalances, accountBalances, customAssetIds, replaceCoveredChains);
|
|
1998
1761
|
// Ensure native tokens have an entry (0 if missing) for chains this account supports
|
|
1999
1762
|
const account = this.#getSelectedAccounts().find((a) => a.id === accountId);
|
|
2000
1763
|
const nativeAssetIdsForAccount = account
|
|
2001
1764
|
? this.#getNativeAssetIdsForAccount(account)
|
|
2002
1765
|
: this.#getNativeAssetIdsForEnabledChains();
|
|
2003
1766
|
for (const nativeAssetId of nativeAssetIdsForAccount) {
|
|
2004
|
-
if (!Object.prototype.hasOwnProperty.call(
|
|
2005
|
-
|
|
1767
|
+
if (!Object.prototype.hasOwnProperty.call(effective, nativeAssetId)) {
|
|
1768
|
+
effective[nativeAssetId] =
|
|
2006
1769
|
getDefaultNativeAssetBalance(nativeAssetId);
|
|
2007
1770
|
}
|
|
2008
1771
|
}
|
|
2009
|
-
for (const [assetId, balance] of Object.entries(
|
|
1772
|
+
for (const [assetId, balance] of Object.entries(effective)) {
|
|
2010
1773
|
const previousBalance = previousBalances[assetId];
|
|
2011
1774
|
// Coerce amounts (e.g. "1e-18" from a data source stringifying
|
|
2012
1775
|
// a JS Number) into a plain decimal so downstream BigInt()
|
|
@@ -2019,55 +1782,15 @@ export class AssetsController extends BaseController {
|
|
|
2019
1782
|
const newAmount = normalizeAmountString(balance.amount, assetDecimals);
|
|
2020
1783
|
const newMetadata = balance.metadata ??
|
|
2021
1784
|
previousBalance?.metadata;
|
|
2022
|
-
|
|
1785
|
+
effective[assetId] = {
|
|
2023
1786
|
amount: newAmount,
|
|
2024
1787
|
...(newMetadata === undefined ? {} : { metadata: newMetadata }),
|
|
2025
1788
|
};
|
|
2026
1789
|
const oldAmount = previousBalance?.amount;
|
|
2027
|
-
const
|
|
1790
|
+
const isNewDefaultNativeZero = oldAmount === undefined &&
|
|
2028
1791
|
newAmount === '0' &&
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
undefined);
|
|
2032
|
-
if (oldAmount !== newAmount && !isNewSeededZero) {
|
|
2033
|
-
changedBalances.push({
|
|
2034
|
-
accountId,
|
|
2035
|
-
assetId,
|
|
2036
|
-
oldAmount,
|
|
2037
|
-
newAmount,
|
|
2038
|
-
});
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
2041
|
-
if (!isEqual(previousBalances, effectiveAccountBalances)) {
|
|
2042
|
-
balances[accountId] = effectiveAccountBalances;
|
|
2043
|
-
}
|
|
2044
|
-
};
|
|
2045
|
-
// Delete with the rest of the v6 path if `assetsAccountsApiV6` is
|
|
2046
|
-
// rolled back; v5 keeps using `applyV5AccountBalanceUpdate`.
|
|
2047
|
-
const applyV6AccountBalanceUpdate = (accountId, accountBalances, previousBalances) => {
|
|
2048
|
-
let undeletableAssetIds;
|
|
2049
|
-
const skipDelete = (assetId) => {
|
|
2050
|
-
undeletableAssetIds ??= this.#getUndeletableAssetIds(state, accountId, accountBalances);
|
|
2051
|
-
return (undeletableAssetIds.has(assetId.toLowerCase()) ||
|
|
2052
|
-
isStakingContractAssetId(assetId));
|
|
2053
|
-
};
|
|
2054
|
-
const effectiveAccountBalances = effectiveAccountBalancesV6(previousBalances, accountBalances, skipDelete, mode);
|
|
2055
|
-
for (const [assetId, balance] of Object.entries(effectiveAccountBalances)) {
|
|
2056
|
-
const previousBalance = previousBalances[assetId];
|
|
2057
|
-
const assetDecimals = metadata[assetId]?.decimals;
|
|
2058
|
-
const newAmount = normalizeAmountString(balance.amount, assetDecimals);
|
|
2059
|
-
const newMetadata = balance.metadata ??
|
|
2060
|
-
previousBalance?.metadata;
|
|
2061
|
-
effectiveAccountBalances[assetId] = {
|
|
2062
|
-
amount: newAmount,
|
|
2063
|
-
...(newMetadata === undefined ? {} : { metadata: newMetadata }),
|
|
2064
|
-
};
|
|
2065
|
-
const oldAmount = previousBalance?.amount;
|
|
2066
|
-
// A v6 snapshot reports assets the account holds none of (its
|
|
2067
|
-
// `includeAssetIds`) at zero. Publishing those as a change would
|
|
2068
|
-
// emit `0 -> 0`, since a missing previous amount reads as '0'.
|
|
2069
|
-
const isNewZero = oldAmount === undefined && newAmount === '0';
|
|
2070
|
-
if (oldAmount !== newAmount && !isNewZero) {
|
|
1792
|
+
nativeAssetIdsForAccount.includes(assetId);
|
|
1793
|
+
if (oldAmount !== newAmount && !isNewDefaultNativeZero) {
|
|
2071
1794
|
changedBalances.push({
|
|
2072
1795
|
accountId,
|
|
2073
1796
|
assetId,
|
|
@@ -2076,17 +1799,8 @@ export class AssetsController extends BaseController {
|
|
|
2076
1799
|
});
|
|
2077
1800
|
}
|
|
2078
1801
|
}
|
|
2079
|
-
if (!isEqual(previousBalances,
|
|
2080
|
-
balances[accountId] =
|
|
2081
|
-
}
|
|
2082
|
-
};
|
|
2083
|
-
for (const [accountId, accountBalances] of Object.entries(normalizedResponse.assetsBalance)) {
|
|
2084
|
-
const previousBalances = previousState.assetsBalance[accountId] ?? {};
|
|
2085
|
-
if (isBalanceV6Enabled) {
|
|
2086
|
-
applyV6AccountBalanceUpdate(accountId, accountBalances, previousBalances);
|
|
2087
|
-
}
|
|
2088
|
-
else {
|
|
2089
|
-
applyV5AccountBalanceUpdate(accountId, accountBalances, previousBalances);
|
|
1802
|
+
if (!isEqual(previousBalances, effective)) {
|
|
1803
|
+
balances[accountId] = effective;
|
|
2090
1804
|
}
|
|
2091
1805
|
}
|
|
2092
1806
|
}
|
|
@@ -2110,8 +1824,10 @@ export class AssetsController extends BaseController {
|
|
|
2110
1824
|
changedPriceAssets.length > 0) {
|
|
2111
1825
|
log('State updated', {
|
|
2112
1826
|
changedBalances: changedBalances.length > 0 ? changedBalances : undefined,
|
|
2113
|
-
|
|
2114
|
-
|
|
1827
|
+
changedMetadataCount: changedMetadata.length > 0 ? changedMetadata.length : undefined,
|
|
1828
|
+
changedPricesCount: changedPriceAssets.length > 0
|
|
1829
|
+
? changedPriceAssets.length
|
|
1830
|
+
: undefined,
|
|
2115
1831
|
newAssets: Object.keys(detectedAssets).length > 0
|
|
2116
1832
|
? Object.entries(detectedAssets).map(([accountId, assets]) => ({
|
|
2117
1833
|
accountId,
|
|
@@ -2333,10 +2049,16 @@ export class AssetsController extends BaseController {
|
|
|
2333
2049
|
if (!subscriptionKey.startsWith('ds:')) {
|
|
2334
2050
|
continue;
|
|
2335
2051
|
}
|
|
2336
|
-
// Subscription keys take the form `ds:<SourceName
|
|
2052
|
+
// Subscription keys take the form `ds:<SourceName>` for the regular
|
|
2053
|
+
// subscription or `ds:<SourceName>:<suffix>` for supplemental
|
|
2054
|
+
// subscriptions (e.g. `ds:RpcDataSource:custom`). Split on `:` and
|
|
2055
|
+
// pick the source-name segment so both shapes resolve correctly.
|
|
2337
2056
|
const [, sourceId] = subscriptionKey.split(':');
|
|
2338
2057
|
const source = allSources.find((ds) => ds.getName() === sourceId);
|
|
2339
2058
|
if (source) {
|
|
2059
|
+
// Unsubscribe by the actual key — `#unsubscribeDataSource` only
|
|
2060
|
+
// knows the regular `ds:<SourceName>` shape and would miss
|
|
2061
|
+
// supplemental subscriptions, leaking their polling timers.
|
|
2340
2062
|
this.#unsubscribeBySubscriptionKey(source, subscriptionKey);
|
|
2341
2063
|
}
|
|
2342
2064
|
}
|
|
@@ -2380,12 +2102,10 @@ export class AssetsController extends BaseController {
|
|
|
2380
2102
|
* Strategy to minimize data source calls:
|
|
2381
2103
|
* 1. Collect all chains to subscribe based on enabled networks
|
|
2382
2104
|
* 2. Map chains to accounts based on their scopes
|
|
2383
|
-
* 3. Split by data source (priority
|
|
2384
|
-
*
|
|
2385
|
-
*
|
|
2386
|
-
*
|
|
2387
|
-
* pins, and default tracked assets). v5 uses a separate RPC
|
|
2388
|
-
* `customAssetsOnly` supplement for pins on chains another source owns.
|
|
2105
|
+
* 3. Split by data source (ordered by priority) - each data source gets ONE subscription
|
|
2106
|
+
*
|
|
2107
|
+
* This ensures we make minimal subscriptions to each data source while covering
|
|
2108
|
+
* all accounts and chains.
|
|
2389
2109
|
*
|
|
2390
2110
|
* @param accounts - Accounts to subscribe balance updates for.
|
|
2391
2111
|
* @param chainIds - Chain IDs to subscribe for.
|
|
@@ -2395,7 +2115,10 @@ export class AssetsController extends BaseController {
|
|
|
2395
2115
|
#subscribeAssetsBalance(accounts, chainIds, options) {
|
|
2396
2116
|
const chainToAccounts = this.#buildChainToAccountsMap(accounts, new Set(chainIds));
|
|
2397
2117
|
const remainingChains = new Set(chainToAccounts.keys());
|
|
2398
|
-
|
|
2118
|
+
// When basic functionality is on, use all balance data sources; when off,
|
|
2119
|
+
// RPC only.
|
|
2120
|
+
const isBasicFunctionality = this.#isBasicFunctionality();
|
|
2121
|
+
const balanceDataSources = isBasicFunctionality
|
|
2399
2122
|
? this.#allBalanceDataSources
|
|
2400
2123
|
: [this.#rpcDataSource];
|
|
2401
2124
|
let rpcAssignedChains = new Set();
|
|
@@ -2434,15 +2157,22 @@ export class AssetsController extends BaseController {
|
|
|
2434
2157
|
});
|
|
2435
2158
|
}
|
|
2436
2159
|
}
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2160
|
+
// Supplemental RPC subscription for customAssets on chains another data
|
|
2161
|
+
// source claimed during regular handoff. RPC is the sole balance fetcher
|
|
2162
|
+
// for customAssets, so we must always poll them — even when (e.g.)
|
|
2163
|
+
// AccountsApi is already covering the chain for normal balances. The
|
|
2164
|
+
// supplemental subscription runs in `customAssetsOnly` mode so it does
|
|
2165
|
+
// NOT double-poll the regular tracked balances.
|
|
2166
|
+
this.#subscribeRpcCustomAssetsSupplement(accounts, chainToAccounts, rpcAssignedChains);
|
|
2440
2167
|
}
|
|
2441
2168
|
/**
|
|
2442
2169
|
* Guarantee that customAssets are **always** polled by RPC, even when
|
|
2443
2170
|
* AccountsApi or another data source has claimed the chain in the
|
|
2444
2171
|
* regular handoff. RPC is the sole balance fetcher for user-imported
|
|
2445
|
-
* tokens
|
|
2172
|
+
* tokens (see `pickRpcCustomAssetsSupplement` for the full rationale),
|
|
2173
|
+
* so we run a dedicated subscription in `customAssetsOnly` mode under a
|
|
2174
|
+
* distinct subscription key (`ds:RpcDataSource:custom`) that does not
|
|
2175
|
+
* interfere with the regular RPC subscription.
|
|
2446
2176
|
*
|
|
2447
2177
|
* @param accounts - Accounts to consider for customAssets.
|
|
2448
2178
|
* @param chainToAccounts - Map of chain → accounts (built by caller).
|
|
@@ -2540,8 +2270,7 @@ export class AssetsController extends BaseController {
|
|
|
2540
2270
|
* @param chains - Array of chain IDs to subscribe for.
|
|
2541
2271
|
* @param options - Optional subscription overrides.
|
|
2542
2272
|
* @param options.subscriptionKey - Custom subscription key (default: `ds:<sourceId>`).
|
|
2543
|
-
* @param options.customAssetsOnly - When true, only poll customAssets for these
|
|
2544
|
-
* chains (Accounts API v5 supplemental RPC subscription).
|
|
2273
|
+
* @param options.customAssetsOnly - When true, only poll customAssets for these chains.
|
|
2545
2274
|
* @param options.skipInitialFetch - When true, skip the data source's subscribe-time fetch.
|
|
2546
2275
|
*/
|
|
2547
2276
|
#subscribeDataSource(source, accounts, chains, options = {}) {
|
|
@@ -2570,6 +2299,7 @@ export class AssetsController extends BaseController {
|
|
|
2570
2299
|
subscriptionId: subscriptionKey,
|
|
2571
2300
|
isUpdate,
|
|
2572
2301
|
onAssetsUpdate: (response, request) => this.handleAssetsUpdate(response, sourceId, request),
|
|
2302
|
+
getAssetsState: () => this.state,
|
|
2573
2303
|
...(options.skipInitialFetch === true ? { skipInitialFetch: true } : {}),
|
|
2574
2304
|
};
|
|
2575
2305
|
source.subscribe(subscribeReq).catch((error) => {
|
|
@@ -2753,13 +2483,13 @@ export class AssetsController extends BaseController {
|
|
|
2753
2483
|
// The data will simply not be updated until the network is re-enabled.
|
|
2754
2484
|
// Refresh subscriptions for new chain set
|
|
2755
2485
|
this.#subscribeAssets();
|
|
2756
|
-
//
|
|
2757
|
-
// those chains; existing balances on other chains are left untouched.
|
|
2486
|
+
// Do one-time fetch for newly enabled chains; merge so we keep existing chain balances
|
|
2758
2487
|
const accounts = this.#getSelectedAccounts();
|
|
2759
2488
|
if (addedChains.length > 0 && accounts.length > 0) {
|
|
2760
2489
|
await this.getAssets(accounts, {
|
|
2761
2490
|
chainIds: addedChains,
|
|
2762
2491
|
forceUpdate: true,
|
|
2492
|
+
updateMode: 'merge',
|
|
2763
2493
|
});
|
|
2764
2494
|
}
|
|
2765
2495
|
this.#ensureNativeBalancesDefaultZero();
|
|
@@ -2919,28 +2649,48 @@ export class AssetsController extends BaseController {
|
|
|
2919
2649
|
async handleAssetsUpdate(response, sourceId, request) {
|
|
2920
2650
|
log('Assets updated from data source', {
|
|
2921
2651
|
sourceId,
|
|
2922
|
-
|
|
2923
|
-
|
|
2652
|
+
hasBalance: Boolean(response.assetsBalance),
|
|
2653
|
+
hasPrice: Boolean(response.assetsPrice),
|
|
2924
2654
|
});
|
|
2925
|
-
|
|
2926
|
-
await this.#handleAssetsUpdateV6(response, sourceId, request);
|
|
2927
|
-
return;
|
|
2928
|
-
}
|
|
2929
|
-
await this.#handleAssetsUpdateV5(response, sourceId, request);
|
|
2930
|
-
}
|
|
2931
|
-
async #handleAssetsUpdateV5(response, sourceId, request) {
|
|
2655
|
+
// Enrichment spans only before unlock/first-init fetch completes.
|
|
2932
2656
|
const pipelineTrace = this.#firstInitFetchReported
|
|
2933
2657
|
? undefined
|
|
2934
2658
|
: this.#trace;
|
|
2935
2659
|
await withTrace({
|
|
2936
2660
|
name: TRACE_UPDATE_PARENT,
|
|
2937
2661
|
trace: pipelineTrace,
|
|
2938
|
-
data:
|
|
2662
|
+
data: {
|
|
2663
|
+
source: sourceId,
|
|
2664
|
+
has_balance: Boolean(response.assetsBalance),
|
|
2665
|
+
has_price: Boolean(response.assetsPrice),
|
|
2666
|
+
balance_account_count: response.assetsBalance
|
|
2667
|
+
? Object.keys(response.assetsBalance).length
|
|
2668
|
+
: 0,
|
|
2669
|
+
},
|
|
2939
2670
|
fn: async (parentContext) => {
|
|
2940
2671
|
const updateStart = performance.now();
|
|
2941
|
-
const
|
|
2672
|
+
const resolvedRequest = request ?? {
|
|
2673
|
+
accountsWithSupportedChains: [],
|
|
2674
|
+
chainIds: [],
|
|
2675
|
+
dataTypes: ['balance', 'metadata', 'price'],
|
|
2676
|
+
};
|
|
2677
|
+
// RPC-only mode (basic functionality off): never run token/price APIs. Strip
|
|
2678
|
+
// those data types so downstream middleware cannot treat them as requested.
|
|
2679
|
+
const pipelineRequest = this.#isBasicFunctionality()
|
|
2680
|
+
? resolvedRequest
|
|
2681
|
+
: {
|
|
2682
|
+
...resolvedRequest,
|
|
2683
|
+
dataTypes: resolvedRequest.dataTypes.filter((dt) => dt !== 'metadata' && dt !== 'price'),
|
|
2684
|
+
};
|
|
2685
|
+
// Graduate custom assets only when AccountsAPI / AccountActivity reports
|
|
2686
|
+
// them. RPC already fetches custom assets on purpose, and Snap handles
|
|
2687
|
+
// non-EVM chains the rule does not apply to, so skip the middleware for
|
|
2688
|
+
// those.
|
|
2942
2689
|
const shouldGraduateCustomAssets = sourceId === 'AccountsApiDataSource' ||
|
|
2943
2690
|
sourceId === 'AccountActivityDataSource';
|
|
2691
|
+
// Websocket updates can carry brand-new spam airdrops: enrich them
|
|
2692
|
+
// with Token API occurrences and drop below-floor tokens BEFORE
|
|
2693
|
+
// detection, so spam is never detected, enriched, priced or persisted.
|
|
2944
2694
|
const shouldFilterOccurrences = sourceId === 'AccountActivityDataSource' &&
|
|
2945
2695
|
this.#isBasicFunctionality();
|
|
2946
2696
|
const shouldRunRpcFallback = sourceId === 'AccountsApiDataSource';
|
|
@@ -2976,110 +2726,25 @@ export class AssetsController extends BaseController {
|
|
|
2976
2726
|
...enrichedResponse,
|
|
2977
2727
|
replaceCoveredChainBalances: response.replaceCoveredChainBalances,
|
|
2978
2728
|
});
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
enrichedResponse,
|
|
2983
|
-
updateStart,
|
|
2984
|
-
pipelineTrace,
|
|
2985
|
-
parentContext,
|
|
2986
|
-
});
|
|
2987
|
-
},
|
|
2988
|
-
});
|
|
2989
|
-
}
|
|
2990
|
-
async #handleAssetsUpdateV6(response, sourceId, request) {
|
|
2991
|
-
const pipelineTrace = this.#firstInitFetchReported
|
|
2992
|
-
? undefined
|
|
2993
|
-
: this.#trace;
|
|
2994
|
-
await withTrace({
|
|
2995
|
-
name: TRACE_UPDATE_PARENT,
|
|
2996
|
-
trace: pipelineTrace,
|
|
2997
|
-
data: this.#getUpdateTraceData(response, sourceId),
|
|
2998
|
-
fn: async (parentContext) => {
|
|
2999
|
-
const updateStart = performance.now();
|
|
3000
|
-
const pipelineRequest = this.#getUpdatePipelineRequest(request);
|
|
3001
|
-
const shouldFilterOccurrences = sourceId === 'AccountActivityDataSource' &&
|
|
3002
|
-
this.#isBasicFunctionality();
|
|
3003
|
-
const enrichmentSources = [
|
|
3004
|
-
...(shouldFilterOccurrences
|
|
3005
|
-
? [
|
|
3006
|
-
{
|
|
3007
|
-
getName: () => 'OccurrenceFloorFilter',
|
|
3008
|
-
assetsMiddleware: this.#tokenDataSource.occurrenceFilterMiddleware,
|
|
3009
|
-
},
|
|
3010
|
-
]
|
|
3011
|
-
: []),
|
|
3012
|
-
];
|
|
3013
|
-
if (this.#isBasicFunctionality()) {
|
|
3014
|
-
enrichmentSources.push(this.#rpcFallbackMiddleware);
|
|
3015
|
-
}
|
|
3016
|
-
enrichmentSources.push(this.#detectionMiddleware);
|
|
3017
|
-
if (this.#isBasicFunctionality()) {
|
|
3018
|
-
enrichmentSources.push(createParallelMiddleware([
|
|
3019
|
-
this.#tokenDataSource,
|
|
3020
|
-
this.#priceDataSource,
|
|
3021
|
-
]));
|
|
3022
|
-
}
|
|
3023
|
-
const { response: enrichedResponse } = await this.#executeMiddlewares({
|
|
3024
|
-
sources: enrichmentSources,
|
|
3025
|
-
request: pipelineRequest,
|
|
3026
|
-
initialResponse: response,
|
|
3027
|
-
parentContext,
|
|
2729
|
+
// Summary fields for Assets Health (nested under the parent span).
|
|
2730
|
+
emitTrace({
|
|
2731
|
+
name: TRACE_UPDATE_PIPELINE,
|
|
3028
2732
|
trace: pipelineTrace,
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
2733
|
+
data: {
|
|
2734
|
+
source: sourceId,
|
|
2735
|
+
duration_ms: performance.now() - updateStart,
|
|
2736
|
+
has_balance: Boolean(response.assetsBalance),
|
|
2737
|
+
has_price: Boolean(response.assetsPrice),
|
|
2738
|
+
has_metadata: Boolean(enrichedResponse.assetsInfo),
|
|
2739
|
+
balance_account_count: response.assetsBalance
|
|
2740
|
+
? Object.keys(response.assetsBalance).length
|
|
2741
|
+
: 0,
|
|
2742
|
+
},
|
|
3037
2743
|
parentContext,
|
|
3038
2744
|
});
|
|
3039
2745
|
},
|
|
3040
2746
|
});
|
|
3041
2747
|
}
|
|
3042
|
-
#getUpdatePipelineRequest(request) {
|
|
3043
|
-
const resolvedRequest = request ?? {
|
|
3044
|
-
accountsWithSupportedChains: [],
|
|
3045
|
-
chainIds: [],
|
|
3046
|
-
dataTypes: ['balance', 'metadata', 'price'],
|
|
3047
|
-
};
|
|
3048
|
-
if (this.#isBasicFunctionality()) {
|
|
3049
|
-
return resolvedRequest;
|
|
3050
|
-
}
|
|
3051
|
-
return {
|
|
3052
|
-
...resolvedRequest,
|
|
3053
|
-
dataTypes: resolvedRequest.dataTypes.filter((dataType) => dataType !== 'metadata' && dataType !== 'price'),
|
|
3054
|
-
};
|
|
3055
|
-
}
|
|
3056
|
-
#getUpdateTraceData(response, sourceId) {
|
|
3057
|
-
return {
|
|
3058
|
-
source: sourceId,
|
|
3059
|
-
has_balance: Boolean(response.assetsBalance),
|
|
3060
|
-
has_price: Boolean(response.assetsPrice),
|
|
3061
|
-
balance_account_count: response.assetsBalance
|
|
3062
|
-
? Object.keys(response.assetsBalance).length
|
|
3063
|
-
: 0,
|
|
3064
|
-
};
|
|
3065
|
-
}
|
|
3066
|
-
#emitUpdateTrace({ response, sourceId, enrichedResponse, updateStart, pipelineTrace, parentContext, }) {
|
|
3067
|
-
emitTrace({
|
|
3068
|
-
name: TRACE_UPDATE_PIPELINE,
|
|
3069
|
-
trace: pipelineTrace,
|
|
3070
|
-
data: {
|
|
3071
|
-
source: sourceId,
|
|
3072
|
-
duration_ms: performance.now() - updateStart,
|
|
3073
|
-
has_balance: Boolean(response.assetsBalance),
|
|
3074
|
-
has_price: Boolean(response.assetsPrice),
|
|
3075
|
-
has_metadata: Boolean(enrichedResponse.assetsInfo),
|
|
3076
|
-
balance_account_count: response.assetsBalance
|
|
3077
|
-
? Object.keys(response.assetsBalance).length
|
|
3078
|
-
: 0,
|
|
3079
|
-
},
|
|
3080
|
-
parentContext,
|
|
3081
|
-
});
|
|
3082
|
-
}
|
|
3083
2748
|
// ============================================================================
|
|
3084
2749
|
// CLEANUP
|
|
3085
2750
|
// ============================================================================
|