@metamask-previews/assets-controller 16.1.0-preview-fb486948f → 16.1.0-preview-aecbce4ff
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 -15
- package/dist/AssetsController.d.ts +3 -7
- package/dist/AssetsController.d.ts.map +1 -1
- package/dist/AssetsController.js +223 -520
- package/dist/AssetsController.js.map +1 -1
- package/dist/data-sources/AccountsApiDataSource.d.ts +1 -12
- package/dist/data-sources/AccountsApiDataSource.d.ts.map +1 -1
- package/dist/data-sources/AccountsApiDataSource.js +47 -155
- package/dist/data-sources/AccountsApiDataSource.js.map +1 -1
- package/dist/data-sources/RpcDataSource.d.ts +1 -6
- package/dist/data-sources/RpcDataSource.d.ts.map +1 -1
- package/dist/data-sources/RpcDataSource.js +34 -44
- package/dist/data-sources/RpcDataSource.js.map +1 -1
- package/dist/middlewares/ParallelMiddleware.d.ts.map +1 -1
- package/dist/middlewares/ParallelMiddleware.js +0 -9
- package/dist/middlewares/ParallelMiddleware.js.map +1 -1
- package/dist/middlewares/RpcFallbackMiddleware.d.ts +14 -12
- package/dist/middlewares/RpcFallbackMiddleware.d.ts.map +1 -1
- package/dist/middlewares/RpcFallbackMiddleware.js +120 -170
- package/dist/middlewares/RpcFallbackMiddleware.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/types.d.ts +19 -28
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/AssetsController.js
CHANGED
|
@@ -13,7 +13,7 @@ import { RpcDataSource } from './data-sources/RpcDataSource.js';
|
|
|
13
13
|
import { SnapDataSource } from './data-sources/SnapDataSource.js';
|
|
14
14
|
import { StakedBalanceDataSource } from './data-sources/StakedBalanceDataSource.js';
|
|
15
15
|
import { CaipAssetNamespace, TokenDataSource, } from './data-sources/TokenDataSource.js';
|
|
16
|
-
import { CHAINS_WITH_DEFAULT_TRACKED_ASSETS, DEFAULT_TRACKED_ASSETS_BY_CHAIN, buildDefaultAssetsInfo, getDefaultAssetMetadata,
|
|
16
|
+
import { CHAINS_WITH_DEFAULT_TRACKED_ASSETS, DEFAULT_TRACKED_ASSETS_BY_CHAIN, buildDefaultAssetsInfo, getDefaultAssetMetadata, } from './defaults.js';
|
|
17
17
|
import { projectLogger, createModuleLogger } from './logger.js';
|
|
18
18
|
import { CustomAssetGraduationMiddleware } from './middlewares/CustomAssetGraduationMiddleware.js';
|
|
19
19
|
import { DetectionMiddleware } from './middlewares/DetectionMiddleware.js';
|
|
@@ -187,10 +187,6 @@ function normalizeResponse(response) {
|
|
|
187
187
|
if (response.replaceCoveredChainBalances) {
|
|
188
188
|
normalized.replaceCoveredChainBalances = true;
|
|
189
189
|
}
|
|
190
|
-
if (response.unprocessedCustomAssets &&
|
|
191
|
-
response.unprocessedCustomAssets.length > 0) {
|
|
192
|
-
normalized.unprocessedCustomAssets = response.unprocessedCustomAssets.map((assetId) => normalizeAssetId(assetId));
|
|
193
|
-
}
|
|
194
190
|
return normalized;
|
|
195
191
|
}
|
|
196
192
|
/**
|
|
@@ -198,13 +194,12 @@ function normalizeResponse(response) {
|
|
|
198
194
|
*
|
|
199
195
|
* @param previousBalances - Balances already in state for this account.
|
|
200
196
|
* @param accountBalances - Balances from the incoming response.
|
|
201
|
-
* @param customAssetIds - Custom assets to preserve when replacing covered chains
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
* on chains present in the response before applying it, then restore custom assets.
|
|
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).
|
|
205
200
|
* @returns The merged balance map for the account.
|
|
206
201
|
*/
|
|
207
|
-
function
|
|
202
|
+
function mergeAccountBalances(previousBalances, accountBalances, customAssetIds, replaceCoveredChains) {
|
|
208
203
|
if (!replaceCoveredChains) {
|
|
209
204
|
return { ...previousBalances, ...accountBalances };
|
|
210
205
|
}
|
|
@@ -233,53 +228,6 @@ function mergeAccountBalancesV5(previousBalances, accountBalances, customAssetId
|
|
|
233
228
|
}
|
|
234
229
|
return next;
|
|
235
230
|
}
|
|
236
|
-
/**
|
|
237
|
-
* Apply an Accounts API v6 authoritative chain snapshot.
|
|
238
|
-
*
|
|
239
|
-
* @param previousBalances - Balances already in state for this account.
|
|
240
|
-
* @param accountBalances - Balances from the incoming response.
|
|
241
|
-
* @param unprocessedCustomAssets - Pins that v6 and RPC could not resolve.
|
|
242
|
-
* @returns The merged balance map for the account.
|
|
243
|
-
*/
|
|
244
|
-
function mergeAccountBalancesV6(previousBalances, accountBalances, unprocessedCustomAssets = []) {
|
|
245
|
-
const coveredChains = new Set(Object.keys(accountBalances).map((assetId) => assetId.split('/')[0]));
|
|
246
|
-
const next = {};
|
|
247
|
-
for (const [assetId, balance] of Object.entries(previousBalances)) {
|
|
248
|
-
if (!coveredChains.has(assetId.split('/')[0])) {
|
|
249
|
-
next[assetId] = balance;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
Object.assign(next, accountBalances);
|
|
253
|
-
const unprocessedSet = new Set(unprocessedCustomAssets.map((assetId) => normalizeAssetId(assetId)));
|
|
254
|
-
for (const [assetId, balance] of Object.entries(previousBalances)) {
|
|
255
|
-
if (unprocessedSet.has(normalizeAssetId(assetId)) &&
|
|
256
|
-
!Object.prototype.hasOwnProperty.call(next, assetId)) {
|
|
257
|
-
next[assetId] = balance;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
for (const [assetId, balance] of Object.entries(previousBalances)) {
|
|
261
|
-
if (isStakingContractAssetId(assetId) &&
|
|
262
|
-
!Object.prototype.hasOwnProperty.call(next, assetId)) {
|
|
263
|
-
next[assetId] = balance;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
// Default tracked assets (mUSD) are controller-managed and must render at
|
|
267
|
-
// zero when the account holds none. The v6 snapshot omits them in that
|
|
268
|
-
// case, so re-assert them on covered chains — otherwise a force refresh
|
|
269
|
-
// would drop them from the token list.
|
|
270
|
-
// TODO: seed defaults as visible pins in `customAssets` instead (skip if
|
|
271
|
-
// already present or `assetPreferences[assetId].hidden`), then drop this
|
|
272
|
-
// merge special case.
|
|
273
|
-
for (const chainId of coveredChains) {
|
|
274
|
-
for (const assetId of getDefaultTrackedAssetsForChain(chainId)) {
|
|
275
|
-
if (!Object.prototype.hasOwnProperty.call(next, assetId)) {
|
|
276
|
-
next[assetId] =
|
|
277
|
-
previousBalances[assetId] ?? { amount: '0' };
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
return next;
|
|
282
|
-
}
|
|
283
231
|
// ============================================================================
|
|
284
232
|
// CONTROLLER IMPLEMENTATION
|
|
285
233
|
// ============================================================================
|
|
@@ -515,8 +463,6 @@ export class AssetsController extends BaseController {
|
|
|
515
463
|
queryApiClient,
|
|
516
464
|
onActiveChainsUpdated: this.#onActiveChainsUpdated,
|
|
517
465
|
...accountsApiDataSourceConfig,
|
|
518
|
-
isBalanceV6Enabled: () => this.#isBalanceV6Enabled(),
|
|
519
|
-
getAssetsState: () => this.state,
|
|
520
466
|
});
|
|
521
467
|
this.#snapDataSource = new SnapDataSource({
|
|
522
468
|
messenger: this.messenger,
|
|
@@ -525,7 +471,6 @@ export class AssetsController extends BaseController {
|
|
|
525
471
|
});
|
|
526
472
|
this.#rpcDataSource = new RpcDataSource({
|
|
527
473
|
messenger: this.messenger,
|
|
528
|
-
getAssetsState: () => this.state,
|
|
529
474
|
onActiveChainsUpdated: this.#onActiveChainsUpdated,
|
|
530
475
|
getNativeAssetForChain: (chainId) => this.#getNativeAssetMap()[chainId] ??
|
|
531
476
|
`${chainId}/erc20:${ZERO_ADDRESS}`,
|
|
@@ -569,7 +514,6 @@ export class AssetsController extends BaseController {
|
|
|
569
514
|
});
|
|
570
515
|
this.#rpcFallbackMiddleware = new RpcFallbackMiddleware({
|
|
571
516
|
rpcDataSource: this.#rpcDataSource,
|
|
572
|
-
isBalanceV6Enabled: () => this.#isBalanceV6Enabled(),
|
|
573
517
|
});
|
|
574
518
|
log('Initializing AssetsController', {
|
|
575
519
|
defaultUpdateInterval: this.#defaultUpdateInterval,
|
|
@@ -924,298 +868,143 @@ export class AssetsController extends BaseController {
|
|
|
924
868
|
if (accounts.length === 0 || chainIds.length === 0) {
|
|
925
869
|
return this.#getAssetsFromState(accounts, chainIds, assetTypes);
|
|
926
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
|
+
}
|
|
927
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.
|
|
928
880
|
const pipelineTrace = this.#firstInitFetchReported
|
|
929
881
|
? undefined
|
|
930
882
|
: this.#trace;
|
|
931
|
-
const
|
|
883
|
+
const request = this.#buildDataRequest(accounts, chainIds, {
|
|
932
884
|
assetTypes,
|
|
933
885
|
dataTypes,
|
|
886
|
+
customAssets: customAssets.length > 0 ? customAssets : undefined,
|
|
934
887
|
forceUpdate: true,
|
|
935
888
|
bypassServerCache: options?.bypassServerCache,
|
|
936
889
|
assetsForPriceUpdate: options?.assetsForPriceUpdate,
|
|
937
|
-
};
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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 }));
|
|
956
1003
|
}
|
|
957
1004
|
}
|
|
958
1005
|
const result = this.#getAssetsFromState(accounts, chainIds, assetTypes);
|
|
959
1006
|
return result;
|
|
960
1007
|
}
|
|
961
|
-
/**
|
|
962
|
-
* v5 force-update request: every pin of the requested accounts, unscoped.
|
|
963
|
-
* Delete with the rest of the v5 path when `assetsAccountsApiV6` is the default.
|
|
964
|
-
*
|
|
965
|
-
* @param accounts - Accounts in this fetch.
|
|
966
|
-
* @param chainIds - Chains in this fetch.
|
|
967
|
-
* @param requestOptions - Shared force-update request fields.
|
|
968
|
-
* @param requestOptions.assetTypes - Asset types to fetch.
|
|
969
|
-
* @param requestOptions.dataTypes - Data types to fetch.
|
|
970
|
-
* @param requestOptions.forceUpdate - Always `true` to bypass caches.
|
|
971
|
-
* @param requestOptions.bypassServerCache - Also bypass server-side HTTP caches.
|
|
972
|
-
* @param requestOptions.assetsForPriceUpdate - Assets to refresh prices for.
|
|
973
|
-
* @returns The v5 data request.
|
|
974
|
-
*/
|
|
975
|
-
#buildForceUpdateRequestV5(accounts, chainIds, requestOptions) {
|
|
976
|
-
const customAssets = [];
|
|
977
|
-
for (const account of accounts) {
|
|
978
|
-
customAssets.push(...this.getCustomAssets(account.id));
|
|
979
|
-
}
|
|
980
|
-
return this.#buildDataRequest(accounts, chainIds, {
|
|
981
|
-
...requestOptions,
|
|
982
|
-
customAssets: customAssets.length > 0 ? customAssets : undefined,
|
|
983
|
-
});
|
|
984
|
-
}
|
|
985
|
-
/**
|
|
986
|
-
* v6 force-update request. Pins and hides are read from state by the
|
|
987
|
-
* Accounts API unless `customAssetsOverride` scopes this fetch.
|
|
988
|
-
*
|
|
989
|
-
* @param accounts - Accounts in this fetch.
|
|
990
|
-
* @param chainIds - Chains in this fetch.
|
|
991
|
-
* @param requestOptions - Shared force-update request fields and optional pin scope.
|
|
992
|
-
* @param requestOptions.assetTypes - Asset types to fetch.
|
|
993
|
-
* @param requestOptions.dataTypes - Data types to fetch.
|
|
994
|
-
* @param requestOptions.forceUpdate - Always `true` to bypass caches.
|
|
995
|
-
* @param requestOptions.bypassServerCache - Also bypass server-side HTTP caches.
|
|
996
|
-
* @param requestOptions.assetsForPriceUpdate - Assets to refresh prices for.
|
|
997
|
-
* @param requestOptions.customAssetsOverride - When set, fetch only these pins.
|
|
998
|
-
* @returns The v6 data request.
|
|
999
|
-
*/
|
|
1000
|
-
#buildForceUpdateRequestV6(accounts, chainIds, requestOptions) {
|
|
1001
|
-
const requestedChains = new Set(chainIds);
|
|
1002
|
-
const customAssetsSet = new Set();
|
|
1003
|
-
for (const assetId of requestOptions.customAssetsOverride ?? []) {
|
|
1004
|
-
try {
|
|
1005
|
-
const normalizedAssetId = normalizeAssetId(assetId);
|
|
1006
|
-
if (requestedChains.has(parseCaipAssetType(normalizedAssetId).chainId) &&
|
|
1007
|
-
!this.state.assetPreferences[normalizedAssetId]?.hidden) {
|
|
1008
|
-
customAssetsSet.add(normalizedAssetId);
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
catch {
|
|
1012
|
-
// Skip unparseable asset IDs
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
const customAssets = [...customAssetsSet];
|
|
1016
|
-
return this.#buildDataRequest(accounts, chainIds, {
|
|
1017
|
-
assetTypes: requestOptions.assetTypes,
|
|
1018
|
-
dataTypes: requestOptions.dataTypes,
|
|
1019
|
-
forceUpdate: requestOptions.forceUpdate,
|
|
1020
|
-
bypassServerCache: requestOptions.bypassServerCache,
|
|
1021
|
-
assetsForPriceUpdate: requestOptions.assetsForPriceUpdate,
|
|
1022
|
-
customAssets: customAssets.length > 0 ? customAssets : undefined,
|
|
1023
|
-
});
|
|
1024
|
-
}
|
|
1025
|
-
async #forceUpdateAssetsV5({ accounts, chainIds, request, pipelineTrace, }) {
|
|
1026
|
-
const isBasicFunctionality = this.#isBasicFunctionality();
|
|
1027
|
-
const fastSources = buildFastFetchSources({
|
|
1028
|
-
accountsApiDataSource: this.#accountsApiDataSource,
|
|
1029
|
-
stakedBalanceDataSource: this.#stakedBalanceDataSource,
|
|
1030
|
-
customAssetGraduationMiddleware: this.#customAssetGraduationMiddleware,
|
|
1031
|
-
rpcFallbackMiddleware: this.#rpcFallbackMiddleware,
|
|
1032
|
-
detectionMiddleware: this.#detectionMiddleware,
|
|
1033
|
-
tokenDataSource: this.#tokenDataSource,
|
|
1034
|
-
priceDataSource: this.#priceDataSource,
|
|
1035
|
-
}, { isBasicFunctionality, includeCustomAssetGraduation: true });
|
|
1036
|
-
const fastResponse = await this.#runFastFetchV5({
|
|
1037
|
-
accounts,
|
|
1038
|
-
chainIds,
|
|
1039
|
-
request,
|
|
1040
|
-
pipelineTrace,
|
|
1041
|
-
fastSources,
|
|
1042
|
-
isBasicFunctionality,
|
|
1043
|
-
});
|
|
1044
|
-
this.#firstInitFetchReported = true;
|
|
1045
|
-
this.#runBackgroundFetch({
|
|
1046
|
-
accounts,
|
|
1047
|
-
request,
|
|
1048
|
-
pipelineTrace,
|
|
1049
|
-
chainIds: this.#getSlowPipelineChainIds(chainIds, fastResponse),
|
|
1050
|
-
isBasicFunctionality,
|
|
1051
|
-
});
|
|
1052
|
-
}
|
|
1053
|
-
async #forceUpdateAssetsV6({ accounts, chainIds, request, pipelineTrace, }) {
|
|
1054
|
-
const isBasicFunctionality = this.#isBasicFunctionality();
|
|
1055
|
-
const fastSources = buildFastFetchSources({
|
|
1056
|
-
accountsApiDataSource: this.#accountsApiDataSource,
|
|
1057
|
-
stakedBalanceDataSource: this.#stakedBalanceDataSource,
|
|
1058
|
-
customAssetGraduationMiddleware: this.#customAssetGraduationMiddleware,
|
|
1059
|
-
rpcFallbackMiddleware: this.#rpcFallbackMiddleware,
|
|
1060
|
-
detectionMiddleware: this.#detectionMiddleware,
|
|
1061
|
-
tokenDataSource: this.#tokenDataSource,
|
|
1062
|
-
priceDataSource: this.#priceDataSource,
|
|
1063
|
-
}, {
|
|
1064
|
-
isBasicFunctionality,
|
|
1065
|
-
includeCustomAssetGraduation: false,
|
|
1066
|
-
});
|
|
1067
|
-
const fastResponse = await this.#runFastFetchV6({
|
|
1068
|
-
accounts,
|
|
1069
|
-
chainIds,
|
|
1070
|
-
request,
|
|
1071
|
-
pipelineTrace,
|
|
1072
|
-
fastSources,
|
|
1073
|
-
isBasicFunctionality,
|
|
1074
|
-
});
|
|
1075
|
-
this.#firstInitFetchReported = true;
|
|
1076
|
-
this.#runBackgroundFetch({
|
|
1077
|
-
accounts,
|
|
1078
|
-
request,
|
|
1079
|
-
pipelineTrace,
|
|
1080
|
-
chainIds: this.#getSlowPipelineChainIds(chainIds, fastResponse),
|
|
1081
|
-
isBasicFunctionality,
|
|
1082
|
-
});
|
|
1083
|
-
}
|
|
1084
|
-
async #runFastFetchV5({ accounts, chainIds, request, pipelineTrace, fastSources, isBasicFunctionality, }) {
|
|
1085
|
-
const { response } = await withTrace({
|
|
1086
|
-
name: TRACE_FETCH_PIPELINE,
|
|
1087
|
-
trace: pipelineTrace,
|
|
1088
|
-
data: {
|
|
1089
|
-
chain_count: chainIds.length,
|
|
1090
|
-
account_count: accounts.length,
|
|
1091
|
-
basic_functionality: isBasicFunctionality,
|
|
1092
|
-
},
|
|
1093
|
-
fn: async (parentContext) => {
|
|
1094
|
-
const startTime = performance.now();
|
|
1095
|
-
const result = await this.#executeMiddlewares({
|
|
1096
|
-
sources: fastSources,
|
|
1097
|
-
request,
|
|
1098
|
-
parentContext,
|
|
1099
|
-
trace: pipelineTrace,
|
|
1100
|
-
});
|
|
1101
|
-
await this.#updateState({
|
|
1102
|
-
...result.response,
|
|
1103
|
-
updateMode: 'merge',
|
|
1104
|
-
replaceCoveredChainBalances: true,
|
|
1105
|
-
});
|
|
1106
|
-
this.#emitFastFetchTraces({
|
|
1107
|
-
accounts,
|
|
1108
|
-
chainIds,
|
|
1109
|
-
result,
|
|
1110
|
-
durationMs: performance.now() - startTime,
|
|
1111
|
-
isBasicFunctionality,
|
|
1112
|
-
parentContext,
|
|
1113
|
-
pipelineTrace,
|
|
1114
|
-
});
|
|
1115
|
-
return result;
|
|
1116
|
-
},
|
|
1117
|
-
});
|
|
1118
|
-
return response;
|
|
1119
|
-
}
|
|
1120
|
-
async #runFastFetchV6({ accounts, chainIds, request, pipelineTrace, fastSources, isBasicFunctionality, }) {
|
|
1121
|
-
const { response } = await withTrace({
|
|
1122
|
-
name: TRACE_FETCH_PIPELINE,
|
|
1123
|
-
trace: pipelineTrace,
|
|
1124
|
-
data: {
|
|
1125
|
-
chain_count: chainIds.length,
|
|
1126
|
-
account_count: accounts.length,
|
|
1127
|
-
basic_functionality: isBasicFunctionality,
|
|
1128
|
-
},
|
|
1129
|
-
fn: async (parentContext) => {
|
|
1130
|
-
const startTime = performance.now();
|
|
1131
|
-
const result = await this.#executeMiddlewares({
|
|
1132
|
-
sources: fastSources,
|
|
1133
|
-
request,
|
|
1134
|
-
parentContext,
|
|
1135
|
-
trace: pipelineTrace,
|
|
1136
|
-
});
|
|
1137
|
-
await this.#updateState(result.response);
|
|
1138
|
-
this.#emitFastFetchTraces({
|
|
1139
|
-
accounts,
|
|
1140
|
-
chainIds,
|
|
1141
|
-
result,
|
|
1142
|
-
durationMs: performance.now() - startTime,
|
|
1143
|
-
isBasicFunctionality,
|
|
1144
|
-
parentContext,
|
|
1145
|
-
pipelineTrace,
|
|
1146
|
-
});
|
|
1147
|
-
return result;
|
|
1148
|
-
},
|
|
1149
|
-
});
|
|
1150
|
-
return response;
|
|
1151
|
-
}
|
|
1152
|
-
#runBackgroundFetch({ accounts, chainIds, request, pipelineTrace, isBasicFunctionality, }) {
|
|
1153
|
-
if (chainIds.length === 0) {
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
const slowSources = isBasicFunctionality
|
|
1157
|
-
? [this.#snapDataSource, this.#rpcDataSource]
|
|
1158
|
-
: [this.#rpcDataSource];
|
|
1159
|
-
const slowRequest = { ...request, chainIds };
|
|
1160
|
-
withTrace({
|
|
1161
|
-
name: TRACE_BACKGROUND_FETCH,
|
|
1162
|
-
trace: pipelineTrace,
|
|
1163
|
-
data: {
|
|
1164
|
-
chain_count: chainIds.length,
|
|
1165
|
-
account_count: accounts.length,
|
|
1166
|
-
},
|
|
1167
|
-
fn: async (parentContext) => {
|
|
1168
|
-
const { response } = await this.#executeMiddlewares({
|
|
1169
|
-
sources: [
|
|
1170
|
-
createParallelBalanceMiddleware(slowSources),
|
|
1171
|
-
this.#detectionMiddleware,
|
|
1172
|
-
...(isBasicFunctionality
|
|
1173
|
-
? [
|
|
1174
|
-
createParallelMiddleware([
|
|
1175
|
-
this.#tokenDataSource,
|
|
1176
|
-
this.#priceDataSource,
|
|
1177
|
-
]),
|
|
1178
|
-
]
|
|
1179
|
-
: []),
|
|
1180
|
-
],
|
|
1181
|
-
request: slowRequest,
|
|
1182
|
-
parentContext,
|
|
1183
|
-
trace: pipelineTrace,
|
|
1184
|
-
});
|
|
1185
|
-
await this.#updateState({ ...response, updateMode: 'merge' });
|
|
1186
|
-
},
|
|
1187
|
-
}).catch((error) => log('Background pipeline failed', { error }));
|
|
1188
|
-
}
|
|
1189
|
-
#emitFastFetchTraces({ accounts, chainIds, result, durationMs, isBasicFunctionality, parentContext, pipelineTrace, }) {
|
|
1190
|
-
emitTrace({
|
|
1191
|
-
name: TRACE_FULL_FETCH,
|
|
1192
|
-
trace: pipelineTrace,
|
|
1193
|
-
data: {
|
|
1194
|
-
duration_ms: durationMs,
|
|
1195
|
-
chain_count: chainIds.length,
|
|
1196
|
-
account_count: accounts.length,
|
|
1197
|
-
basic_functionality: isBasicFunctionality,
|
|
1198
|
-
asset_count: result.response.assetsBalance
|
|
1199
|
-
? Object.values(result.response.assetsBalance).reduce((sum, account) => sum + Object.keys(account).length, 0)
|
|
1200
|
-
: 0,
|
|
1201
|
-
price_count: result.response.assetsPrice
|
|
1202
|
-
? Object.keys(result.response.assetsPrice).length
|
|
1203
|
-
: 0,
|
|
1204
|
-
...result.durationByDataSource,
|
|
1205
|
-
},
|
|
1206
|
-
parentContext,
|
|
1207
|
-
});
|
|
1208
|
-
emitTrace({
|
|
1209
|
-
name: TRACE_FIRST_INIT_FETCH,
|
|
1210
|
-
trace: pipelineTrace,
|
|
1211
|
-
data: {
|
|
1212
|
-
duration_ms: durationMs,
|
|
1213
|
-
chain_ids: JSON.stringify(chainIds),
|
|
1214
|
-
...result.durationByDataSource,
|
|
1215
|
-
},
|
|
1216
|
-
parentContext,
|
|
1217
|
-
});
|
|
1218
|
-
}
|
|
1219
1008
|
async getAssetsBalance(accounts, options) {
|
|
1220
1009
|
// Reuse getAssets with dataTypes: ['balance'] only
|
|
1221
1010
|
const assets = await this.getAssets(accounts, {
|
|
@@ -1483,28 +1272,20 @@ export class AssetsController extends BaseController {
|
|
|
1483
1272
|
balances[accountId] ??= {};
|
|
1484
1273
|
balances[accountId][normalizedAssetId] ??= { amount: '0' };
|
|
1485
1274
|
});
|
|
1275
|
+
// Fetch data for the newly added custom asset (merge to preserve other chains)
|
|
1486
1276
|
const account = this.#getSelectedAccounts().find((a) => a.id === accountId);
|
|
1487
1277
|
if (account) {
|
|
1488
1278
|
const chainId = extractChainId(normalizedAssetId);
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
});
|
|
1497
|
-
}
|
|
1498
|
-
else {
|
|
1499
|
-
await this.getAssets([account], {
|
|
1500
|
-
chainIds: [chainId],
|
|
1501
|
-
dataTypes: ['balance', 'metadata', 'price'],
|
|
1502
|
-
assetTypes: ['fungible'],
|
|
1503
|
-
forceUpdate: true,
|
|
1504
|
-
});
|
|
1505
|
-
}
|
|
1279
|
+
await this.getAssets([account], {
|
|
1280
|
+
chainIds: [chainId],
|
|
1281
|
+
dataTypes: ['balance', 'metadata', 'price'],
|
|
1282
|
+
assetTypes: ['fungible'],
|
|
1283
|
+
forceUpdate: true,
|
|
1284
|
+
updateMode: 'merge',
|
|
1285
|
+
});
|
|
1506
1286
|
}
|
|
1507
|
-
// 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.
|
|
1508
1289
|
this.#subscribeAssets();
|
|
1509
1290
|
}
|
|
1510
1291
|
/**
|
|
@@ -1525,7 +1306,8 @@ export class AssetsController extends BaseController {
|
|
|
1525
1306
|
}
|
|
1526
1307
|
}
|
|
1527
1308
|
});
|
|
1528
|
-
// 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.
|
|
1529
1311
|
this.#subscribeAssets();
|
|
1530
1312
|
}
|
|
1531
1313
|
/**
|
|
@@ -1556,8 +1338,6 @@ export class AssetsController extends BaseController {
|
|
|
1556
1338
|
}
|
|
1557
1339
|
state.assetPreferences[normalizedAssetId].hidden = true;
|
|
1558
1340
|
});
|
|
1559
|
-
// Re-evaluate subscriptions so polls exclude the newly hidden asset.
|
|
1560
|
-
this.#subscribeAssets();
|
|
1561
1341
|
}
|
|
1562
1342
|
/**
|
|
1563
1343
|
* Unhide an asset globally.
|
|
@@ -1576,26 +1356,6 @@ export class AssetsController extends BaseController {
|
|
|
1576
1356
|
}
|
|
1577
1357
|
}
|
|
1578
1358
|
});
|
|
1579
|
-
// Re-evaluate subscriptions so polls stop excluding the asset.
|
|
1580
|
-
this.#subscribeAssets();
|
|
1581
|
-
}
|
|
1582
|
-
/**
|
|
1583
|
-
* Whether Accounts API v6 (and the v6 custom-asset path) is enabled.
|
|
1584
|
-
* Injected into AccountsApiDataSource and RpcFallbackMiddleware.
|
|
1585
|
-
*
|
|
1586
|
-
* `RemoteFeatureFlagController` resolves a threshold-scoped flag to the
|
|
1587
|
-
* selected group's `value`, so processed state is a boolean.
|
|
1588
|
-
*
|
|
1589
|
-
* @returns `true` when the v6 remote flag is on.
|
|
1590
|
-
*/
|
|
1591
|
-
#isBalanceV6Enabled() {
|
|
1592
|
-
try {
|
|
1593
|
-
const { remoteFeatureFlags } = this.messenger.call('RemoteFeatureFlagController:getState');
|
|
1594
|
-
return remoteFeatureFlags?.assetsAccountsApiV6 === true;
|
|
1595
|
-
}
|
|
1596
|
-
catch {
|
|
1597
|
-
return false;
|
|
1598
|
-
}
|
|
1599
1359
|
}
|
|
1600
1360
|
// ============================================================================
|
|
1601
1361
|
// CURRENT CURRENCY MANAGEMENT
|
|
@@ -1829,16 +1589,10 @@ export class AssetsController extends BaseController {
|
|
|
1829
1589
|
*/
|
|
1830
1590
|
#getSlowPipelineChainIds(chainIds, fastResponse) {
|
|
1831
1591
|
const accountsApiChains = new Set(this.#accountsApiDataSource.getActiveChainsSync());
|
|
1832
|
-
// Chains whose pins went unresolved (`unprocessedCustomAssets`): route
|
|
1833
|
-
// them to the slow pipeline so RPC fetches the pins.
|
|
1834
|
-
const unprocessedCustomAssetChains = new Set((fastResponse.unprocessedCustomAssets ?? []).map((assetId) => assetId.split('/')[0]));
|
|
1835
1592
|
return chainIds.filter((chainId) => {
|
|
1836
1593
|
if (fastResponse.errors?.[chainId]) {
|
|
1837
1594
|
return true;
|
|
1838
1595
|
}
|
|
1839
|
-
if (unprocessedCustomAssetChains.has(chainId)) {
|
|
1840
|
-
return true;
|
|
1841
|
-
}
|
|
1842
1596
|
if (!accountsApiChains.has(chainId)) {
|
|
1843
1597
|
return true;
|
|
1844
1598
|
}
|
|
@@ -2001,9 +1755,9 @@ export class AssetsController extends BaseController {
|
|
|
2001
1755
|
for (const [accountId, accountBalances] of Object.entries(normalizedResponse.assetsBalance)) {
|
|
2002
1756
|
const previousBalances = previousState.assetsBalance[accountId] ?? {};
|
|
2003
1757
|
const customAssetIds = state.customAssets[accountId] ?? [];
|
|
2004
|
-
const
|
|
2005
|
-
|
|
2006
|
-
|
|
1758
|
+
const replaceCoveredChains = mode === 'full' ||
|
|
1759
|
+
normalizedResponse.replaceCoveredChainBalances === true;
|
|
1760
|
+
const effective = mergeAccountBalances(previousBalances, accountBalances, customAssetIds, replaceCoveredChains);
|
|
2007
1761
|
// Ensure native tokens have an entry (0 if missing) for chains this account supports
|
|
2008
1762
|
const account = this.#getSelectedAccounts().find((a) => a.id === accountId);
|
|
2009
1763
|
const nativeAssetIdsForAccount = account
|
|
@@ -2033,12 +1787,10 @@ export class AssetsController extends BaseController {
|
|
|
2033
1787
|
...(newMetadata === undefined ? {} : { metadata: newMetadata }),
|
|
2034
1788
|
};
|
|
2035
1789
|
const oldAmount = previousBalance?.amount;
|
|
2036
|
-
const
|
|
1790
|
+
const isNewDefaultNativeZero = oldAmount === undefined &&
|
|
2037
1791
|
newAmount === '0' &&
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
undefined);
|
|
2041
|
-
if (oldAmount !== newAmount && !isNewSeededZero) {
|
|
1792
|
+
nativeAssetIdsForAccount.includes(assetId);
|
|
1793
|
+
if (oldAmount !== newAmount && !isNewDefaultNativeZero) {
|
|
2042
1794
|
changedBalances.push({
|
|
2043
1795
|
accountId,
|
|
2044
1796
|
assetId,
|
|
@@ -2072,8 +1824,10 @@ export class AssetsController extends BaseController {
|
|
|
2072
1824
|
changedPriceAssets.length > 0) {
|
|
2073
1825
|
log('State updated', {
|
|
2074
1826
|
changedBalances: changedBalances.length > 0 ? changedBalances : undefined,
|
|
2075
|
-
|
|
2076
|
-
|
|
1827
|
+
changedMetadataCount: changedMetadata.length > 0 ? changedMetadata.length : undefined,
|
|
1828
|
+
changedPricesCount: changedPriceAssets.length > 0
|
|
1829
|
+
? changedPriceAssets.length
|
|
1830
|
+
: undefined,
|
|
2077
1831
|
newAssets: Object.keys(detectedAssets).length > 0
|
|
2078
1832
|
? Object.entries(detectedAssets).map(([accountId, assets]) => ({
|
|
2079
1833
|
accountId,
|
|
@@ -2295,10 +2049,16 @@ export class AssetsController extends BaseController {
|
|
|
2295
2049
|
if (!subscriptionKey.startsWith('ds:')) {
|
|
2296
2050
|
continue;
|
|
2297
2051
|
}
|
|
2298
|
-
// 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.
|
|
2299
2056
|
const [, sourceId] = subscriptionKey.split(':');
|
|
2300
2057
|
const source = allSources.find((ds) => ds.getName() === sourceId);
|
|
2301
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.
|
|
2302
2062
|
this.#unsubscribeBySubscriptionKey(source, subscriptionKey);
|
|
2303
2063
|
}
|
|
2304
2064
|
}
|
|
@@ -2342,11 +2102,10 @@ export class AssetsController extends BaseController {
|
|
|
2342
2102
|
* Strategy to minimize data source calls:
|
|
2343
2103
|
* 1. Collect all chains to subscribe based on enabled networks
|
|
2344
2104
|
* 2. Map chains to accounts based on their scopes
|
|
2345
|
-
* 3. Split by data source (priority
|
|
2346
|
-
*
|
|
2347
|
-
*
|
|
2348
|
-
*
|
|
2349
|
-
* `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.
|
|
2350
2109
|
*
|
|
2351
2110
|
* @param accounts - Accounts to subscribe balance updates for.
|
|
2352
2111
|
* @param chainIds - Chain IDs to subscribe for.
|
|
@@ -2356,7 +2115,10 @@ export class AssetsController extends BaseController {
|
|
|
2356
2115
|
#subscribeAssetsBalance(accounts, chainIds, options) {
|
|
2357
2116
|
const chainToAccounts = this.#buildChainToAccountsMap(accounts, new Set(chainIds));
|
|
2358
2117
|
const remainingChains = new Set(chainToAccounts.keys());
|
|
2359
|
-
|
|
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
|
|
2360
2122
|
? this.#allBalanceDataSources
|
|
2361
2123
|
: [this.#rpcDataSource];
|
|
2362
2124
|
let rpcAssignedChains = new Set();
|
|
@@ -2395,15 +2157,22 @@ export class AssetsController extends BaseController {
|
|
|
2395
2157
|
});
|
|
2396
2158
|
}
|
|
2397
2159
|
}
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
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);
|
|
2401
2167
|
}
|
|
2402
2168
|
/**
|
|
2403
2169
|
* Guarantee that customAssets are **always** polled by RPC, even when
|
|
2404
2170
|
* AccountsApi or another data source has claimed the chain in the
|
|
2405
2171
|
* regular handoff. RPC is the sole balance fetcher for user-imported
|
|
2406
|
-
* 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.
|
|
2407
2176
|
*
|
|
2408
2177
|
* @param accounts - Accounts to consider for customAssets.
|
|
2409
2178
|
* @param chainToAccounts - Map of chain → accounts (built by caller).
|
|
@@ -2501,8 +2270,7 @@ export class AssetsController extends BaseController {
|
|
|
2501
2270
|
* @param chains - Array of chain IDs to subscribe for.
|
|
2502
2271
|
* @param options - Optional subscription overrides.
|
|
2503
2272
|
* @param options.subscriptionKey - Custom subscription key (default: `ds:<sourceId>`).
|
|
2504
|
-
* @param options.customAssetsOnly - When true, only poll customAssets for these
|
|
2505
|
-
* chains (Accounts API v5 supplemental RPC subscription).
|
|
2273
|
+
* @param options.customAssetsOnly - When true, only poll customAssets for these chains.
|
|
2506
2274
|
* @param options.skipInitialFetch - When true, skip the data source's subscribe-time fetch.
|
|
2507
2275
|
*/
|
|
2508
2276
|
#subscribeDataSource(source, accounts, chains, options = {}) {
|
|
@@ -2715,13 +2483,13 @@ export class AssetsController extends BaseController {
|
|
|
2715
2483
|
// The data will simply not be updated until the network is re-enabled.
|
|
2716
2484
|
// Refresh subscriptions for new chain set
|
|
2717
2485
|
this.#subscribeAssets();
|
|
2718
|
-
//
|
|
2719
|
-
// 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
|
|
2720
2487
|
const accounts = this.#getSelectedAccounts();
|
|
2721
2488
|
if (addedChains.length > 0 && accounts.length > 0) {
|
|
2722
2489
|
await this.getAssets(accounts, {
|
|
2723
2490
|
chainIds: addedChains,
|
|
2724
2491
|
forceUpdate: true,
|
|
2492
|
+
updateMode: 'merge',
|
|
2725
2493
|
});
|
|
2726
2494
|
}
|
|
2727
2495
|
this.#ensureNativeBalancesDefaultZero();
|
|
@@ -2881,28 +2649,48 @@ export class AssetsController extends BaseController {
|
|
|
2881
2649
|
async handleAssetsUpdate(response, sourceId, request) {
|
|
2882
2650
|
log('Assets updated from data source', {
|
|
2883
2651
|
sourceId,
|
|
2884
|
-
|
|
2885
|
-
|
|
2652
|
+
hasBalance: Boolean(response.assetsBalance),
|
|
2653
|
+
hasPrice: Boolean(response.assetsPrice),
|
|
2886
2654
|
});
|
|
2887
|
-
|
|
2888
|
-
await this.#handleAssetsUpdateV6(response, sourceId, request);
|
|
2889
|
-
return;
|
|
2890
|
-
}
|
|
2891
|
-
await this.#handleAssetsUpdateV5(response, sourceId, request);
|
|
2892
|
-
}
|
|
2893
|
-
async #handleAssetsUpdateV5(response, sourceId, request) {
|
|
2655
|
+
// Enrichment spans only before unlock/first-init fetch completes.
|
|
2894
2656
|
const pipelineTrace = this.#firstInitFetchReported
|
|
2895
2657
|
? undefined
|
|
2896
2658
|
: this.#trace;
|
|
2897
2659
|
await withTrace({
|
|
2898
2660
|
name: TRACE_UPDATE_PARENT,
|
|
2899
2661
|
trace: pipelineTrace,
|
|
2900
|
-
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
|
+
},
|
|
2901
2670
|
fn: async (parentContext) => {
|
|
2902
2671
|
const updateStart = performance.now();
|
|
2903
|
-
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.
|
|
2904
2689
|
const shouldGraduateCustomAssets = sourceId === 'AccountsApiDataSource' ||
|
|
2905
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.
|
|
2906
2694
|
const shouldFilterOccurrences = sourceId === 'AccountActivityDataSource' &&
|
|
2907
2695
|
this.#isBasicFunctionality();
|
|
2908
2696
|
const shouldRunRpcFallback = sourceId === 'AccountsApiDataSource';
|
|
@@ -2938,110 +2726,25 @@ export class AssetsController extends BaseController {
|
|
|
2938
2726
|
...enrichedResponse,
|
|
2939
2727
|
replaceCoveredChainBalances: response.replaceCoveredChainBalances,
|
|
2940
2728
|
});
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
enrichedResponse,
|
|
2945
|
-
updateStart,
|
|
2946
|
-
pipelineTrace,
|
|
2947
|
-
parentContext,
|
|
2948
|
-
});
|
|
2949
|
-
},
|
|
2950
|
-
});
|
|
2951
|
-
}
|
|
2952
|
-
async #handleAssetsUpdateV6(response, sourceId, request) {
|
|
2953
|
-
const pipelineTrace = this.#firstInitFetchReported
|
|
2954
|
-
? undefined
|
|
2955
|
-
: this.#trace;
|
|
2956
|
-
await withTrace({
|
|
2957
|
-
name: TRACE_UPDATE_PARENT,
|
|
2958
|
-
trace: pipelineTrace,
|
|
2959
|
-
data: this.#getUpdateTraceData(response, sourceId),
|
|
2960
|
-
fn: async (parentContext) => {
|
|
2961
|
-
const updateStart = performance.now();
|
|
2962
|
-
const pipelineRequest = this.#getUpdatePipelineRequest(request);
|
|
2963
|
-
const shouldFilterOccurrences = sourceId === 'AccountActivityDataSource' &&
|
|
2964
|
-
this.#isBasicFunctionality();
|
|
2965
|
-
const enrichmentSources = [
|
|
2966
|
-
...(shouldFilterOccurrences
|
|
2967
|
-
? [
|
|
2968
|
-
{
|
|
2969
|
-
getName: () => 'OccurrenceFloorFilter',
|
|
2970
|
-
assetsMiddleware: this.#tokenDataSource.occurrenceFilterMiddleware,
|
|
2971
|
-
},
|
|
2972
|
-
]
|
|
2973
|
-
: []),
|
|
2974
|
-
];
|
|
2975
|
-
if (this.#isBasicFunctionality()) {
|
|
2976
|
-
enrichmentSources.push(this.#rpcFallbackMiddleware);
|
|
2977
|
-
}
|
|
2978
|
-
enrichmentSources.push(this.#detectionMiddleware);
|
|
2979
|
-
if (this.#isBasicFunctionality()) {
|
|
2980
|
-
enrichmentSources.push(createParallelMiddleware([
|
|
2981
|
-
this.#tokenDataSource,
|
|
2982
|
-
this.#priceDataSource,
|
|
2983
|
-
]));
|
|
2984
|
-
}
|
|
2985
|
-
const { response: enrichedResponse } = await this.#executeMiddlewares({
|
|
2986
|
-
sources: enrichmentSources,
|
|
2987
|
-
request: pipelineRequest,
|
|
2988
|
-
initialResponse: response,
|
|
2989
|
-
parentContext,
|
|
2729
|
+
// Summary fields for Assets Health (nested under the parent span).
|
|
2730
|
+
emitTrace({
|
|
2731
|
+
name: TRACE_UPDATE_PIPELINE,
|
|
2990
2732
|
trace: pipelineTrace,
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
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
|
+
},
|
|
2999
2743
|
parentContext,
|
|
3000
2744
|
});
|
|
3001
2745
|
},
|
|
3002
2746
|
});
|
|
3003
2747
|
}
|
|
3004
|
-
#getUpdatePipelineRequest(request) {
|
|
3005
|
-
const resolvedRequest = request ?? {
|
|
3006
|
-
accountsWithSupportedChains: [],
|
|
3007
|
-
chainIds: [],
|
|
3008
|
-
dataTypes: ['balance', 'metadata', 'price'],
|
|
3009
|
-
};
|
|
3010
|
-
if (this.#isBasicFunctionality()) {
|
|
3011
|
-
return resolvedRequest;
|
|
3012
|
-
}
|
|
3013
|
-
return {
|
|
3014
|
-
...resolvedRequest,
|
|
3015
|
-
dataTypes: resolvedRequest.dataTypes.filter((dataType) => dataType !== 'metadata' && dataType !== 'price'),
|
|
3016
|
-
};
|
|
3017
|
-
}
|
|
3018
|
-
#getUpdateTraceData(response, sourceId) {
|
|
3019
|
-
return {
|
|
3020
|
-
source: sourceId,
|
|
3021
|
-
has_balance: Boolean(response.assetsBalance),
|
|
3022
|
-
has_price: Boolean(response.assetsPrice),
|
|
3023
|
-
balance_account_count: response.assetsBalance
|
|
3024
|
-
? Object.keys(response.assetsBalance).length
|
|
3025
|
-
: 0,
|
|
3026
|
-
};
|
|
3027
|
-
}
|
|
3028
|
-
#emitUpdateTrace({ response, sourceId, enrichedResponse, updateStart, pipelineTrace, parentContext, }) {
|
|
3029
|
-
emitTrace({
|
|
3030
|
-
name: TRACE_UPDATE_PIPELINE,
|
|
3031
|
-
trace: pipelineTrace,
|
|
3032
|
-
data: {
|
|
3033
|
-
source: sourceId,
|
|
3034
|
-
duration_ms: performance.now() - updateStart,
|
|
3035
|
-
has_balance: Boolean(response.assetsBalance),
|
|
3036
|
-
has_price: Boolean(response.assetsPrice),
|
|
3037
|
-
has_metadata: Boolean(enrichedResponse.assetsInfo),
|
|
3038
|
-
balance_account_count: response.assetsBalance
|
|
3039
|
-
? Object.keys(response.assetsBalance).length
|
|
3040
|
-
: 0,
|
|
3041
|
-
},
|
|
3042
|
-
parentContext,
|
|
3043
|
-
});
|
|
3044
|
-
}
|
|
3045
2748
|
// ============================================================================
|
|
3046
2749
|
// CLEANUP
|
|
3047
2750
|
// ============================================================================
|