@metamask-previews/assets-controllers 93.1.0-preview-267e79c3 → 93.1.0-preview-d2037635
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 +8 -10
- package/dist/MultichainAssetsRatesController/MultichainAssetsRatesController.cjs +14 -9
- package/dist/MultichainAssetsRatesController/MultichainAssetsRatesController.cjs.map +1 -1
- package/dist/MultichainAssetsRatesController/MultichainAssetsRatesController.d.cts.map +1 -1
- package/dist/MultichainAssetsRatesController/MultichainAssetsRatesController.d.mts.map +1 -1
- package/dist/MultichainAssetsRatesController/MultichainAssetsRatesController.mjs +14 -9
- package/dist/MultichainAssetsRatesController/MultichainAssetsRatesController.mjs.map +1 -1
- package/dist/NftDetectionController.cjs +10 -6
- package/dist/NftDetectionController.cjs.map +1 -1
- package/dist/NftDetectionController.d.cts +4 -0
- package/dist/NftDetectionController.d.cts.map +1 -1
- package/dist/NftDetectionController.d.mts +4 -0
- package/dist/NftDetectionController.d.mts.map +1 -1
- package/dist/NftDetectionController.mjs +10 -6
- package/dist/NftDetectionController.mjs.map +1 -1
- package/dist/Standards/NftStandards/ERC721/ERC721Standard.cjs +1 -1
- package/dist/Standards/NftStandards/ERC721/ERC721Standard.cjs.map +1 -1
- package/dist/Standards/NftStandards/ERC721/ERC721Standard.mjs +1 -1
- package/dist/Standards/NftStandards/ERC721/ERC721Standard.mjs.map +1 -1
- package/dist/TokenBalancesController.cjs +377 -376
- package/dist/TokenBalancesController.cjs.map +1 -1
- package/dist/TokenBalancesController.d.cts +40 -20
- package/dist/TokenBalancesController.d.cts.map +1 -1
- package/dist/TokenBalancesController.d.mts +40 -20
- package/dist/TokenBalancesController.d.mts.map +1 -1
- package/dist/TokenBalancesController.mjs +377 -376
- package/dist/TokenBalancesController.mjs.map +1 -1
- package/dist/TokenDetectionController.cjs +205 -44
- package/dist/TokenDetectionController.cjs.map +1 -1
- package/dist/TokenDetectionController.d.cts +10 -12
- package/dist/TokenDetectionController.d.cts.map +1 -1
- package/dist/TokenDetectionController.d.mts +10 -12
- package/dist/TokenDetectionController.d.mts.map +1 -1
- package/dist/TokenDetectionController.mjs +206 -45
- package/dist/TokenDetectionController.mjs.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/rpc-service/rpc-balance-fetcher.cjs +0 -4
- package/dist/rpc-service/rpc-balance-fetcher.cjs.map +1 -1
- package/dist/rpc-service/rpc-balance-fetcher.d.cts.map +1 -1
- package/dist/rpc-service/rpc-balance-fetcher.d.mts.map +1 -1
- package/dist/rpc-service/rpc-balance-fetcher.mjs +0 -4
- package/dist/rpc-service/rpc-balance-fetcher.mjs.map +1 -1
- package/package.json +3 -3
|
@@ -9,7 +9,7 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (
|
|
|
9
9
|
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
10
10
|
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
11
11
|
};
|
|
12
|
-
var _TokenBalancesController_instances, _TokenBalancesController_platform, _TokenBalancesController_queryAllAccounts, _TokenBalancesController_accountsApiChainIds, _TokenBalancesController_balanceFetchers, _TokenBalancesController_allTokens, _TokenBalancesController_detectedTokens, _TokenBalancesController_allIgnoredTokens,
|
|
12
|
+
var _TokenBalancesController_instances, _TokenBalancesController_platform, _TokenBalancesController_queryAllAccounts, _TokenBalancesController_accountsApiChainIds, _TokenBalancesController_balanceFetchers, _TokenBalancesController_allTokens, _TokenBalancesController_detectedTokens, _TokenBalancesController_allIgnoredTokens, _TokenBalancesController_defaultInterval, _TokenBalancesController_websocketActivePollingInterval, _TokenBalancesController_chainPollingConfig, _TokenBalancesController_intervalPollingTimers, _TokenBalancesController_isControllerPollingActive, _TokenBalancesController_requestedChainIds, _TokenBalancesController_statusChangeDebouncer, _TokenBalancesController_normalizeAccountAddresses, _TokenBalancesController_chainIdsWithTokens, _TokenBalancesController_getProvider, _TokenBalancesController_getNetworkClient, _TokenBalancesController_createAccountsApiFetcher, _TokenBalancesController_startIntervalGroupPolling, _TokenBalancesController_startPollingForInterval, _TokenBalancesController_setPollingTimer, _TokenBalancesController_isTokenTracked, _TokenBalancesController_onTokensChanged, _TokenBalancesController_onNetworkChanged, _TokenBalancesController_onAccountRemoved, _TokenBalancesController_onAccountChanged, _TokenBalancesController_prepareBalanceUpdates, _TokenBalancesController_onAccountActivityBalanceUpdate, _TokenBalancesController_onAccountActivityStatusChanged, _TokenBalancesController_processAccumulatedStatusChanges;
|
|
13
13
|
import { Web3Provider } from "@ethersproject/providers";
|
|
14
14
|
import { BNToHex, isValidHexAddress, safelyExecuteWithTimeout, toChecksumHexAddress, toHex } from "@metamask/controller-utils";
|
|
15
15
|
import { StaticIntervalPollingController } from "@metamask/polling-controller";
|
|
@@ -31,14 +31,19 @@ const metadata = {
|
|
|
31
31
|
usedInUi: true,
|
|
32
32
|
},
|
|
33
33
|
};
|
|
34
|
+
// endregion
|
|
35
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
36
|
+
// region: Helper utilities
|
|
34
37
|
const draft = (base, fn) => produce(base, fn);
|
|
35
38
|
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
36
39
|
const checksum = (addr) => toChecksumHexAddress(addr);
|
|
37
40
|
/**
|
|
38
|
-
* Convert CAIP chain ID or hex chain ID to hex chain ID
|
|
41
|
+
* Convert CAIP chain ID or hex chain ID to hex chain ID
|
|
42
|
+
* Handles both CAIP-2 format (e.g., "eip155:1") and hex format (e.g., "0x1")
|
|
39
43
|
*
|
|
40
|
-
* @param chainId - CAIP chain ID or hex chain ID.
|
|
41
|
-
* @returns Hex chain ID.
|
|
44
|
+
* @param chainId - CAIP chain ID (e.g., "eip155:1") or hex chain ID (e.g., "0x1")
|
|
45
|
+
* @returns Hex chain ID (e.g., "0x1")
|
|
46
|
+
* @throws {Error} If chainId is neither a valid CAIP-2 chain ID nor a hex string
|
|
42
47
|
*/
|
|
43
48
|
export const caipChainIdToHex = (chainId) => {
|
|
44
49
|
if (isStrictHexString(chainId)) {
|
|
@@ -50,26 +55,30 @@ export const caipChainIdToHex = (chainId) => {
|
|
|
50
55
|
throw new Error('caipChainIdToHex - Failed to provide CAIP-2 or Hex chainId');
|
|
51
56
|
};
|
|
52
57
|
/**
|
|
53
|
-
* Extract token address from asset type
|
|
58
|
+
* Extract token address from asset type
|
|
59
|
+
* Returns tuple of [tokenAddress, isNativeToken] or null if invalid
|
|
54
60
|
*
|
|
55
|
-
* @param assetType - Asset type string.
|
|
56
|
-
* @returns Tuple of [tokenAddress, isNativeToken] or null if invalid
|
|
61
|
+
* @param assetType - Asset type string (e.g., 'eip155:1/erc20:0x...' or 'eip155:1/slip44:60')
|
|
62
|
+
* @returns Tuple of [tokenAddress, isNativeToken] or null if invalid
|
|
57
63
|
*/
|
|
58
64
|
export const parseAssetType = (assetType) => {
|
|
59
65
|
if (!isCaipAssetType(assetType)) {
|
|
60
66
|
return null;
|
|
61
67
|
}
|
|
62
68
|
const parsed = parseCaipAssetType(assetType);
|
|
69
|
+
// ERC20 token (e.g., "eip155:1/erc20:0x...")
|
|
63
70
|
if (parsed.assetNamespace === 'erc20') {
|
|
64
71
|
return [parsed.assetReference, false];
|
|
65
72
|
}
|
|
73
|
+
// Native token (e.g., "eip155:1/slip44:60")
|
|
66
74
|
if (parsed.assetNamespace === 'slip44') {
|
|
67
75
|
return [ZERO_ADDRESS, true];
|
|
68
76
|
}
|
|
69
77
|
return null;
|
|
70
78
|
};
|
|
79
|
+
// endregion
|
|
71
80
|
// ────────────────────────────────────────────────────────────────────────────
|
|
72
|
-
// Main controller
|
|
81
|
+
// region: Main controller
|
|
73
82
|
export class TokenBalancesController extends StaticIntervalPollingController() {
|
|
74
83
|
constructor({ messenger, interval = DEFAULT_INTERVAL_MS, websocketActivePollingInterval = DEFAULT_WEBSOCKET_ACTIVE_POLLING_INTERVAL_MS, chainPollingIntervals = {}, state = {}, queryMultipleAccounts = true, accountsApiChainIds = () => [], allowExternalServices = () => true, platform, }) {
|
|
75
84
|
super({
|
|
@@ -86,8 +95,6 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
|
|
|
86
95
|
_TokenBalancesController_allTokens.set(this, {});
|
|
87
96
|
_TokenBalancesController_detectedTokens.set(this, {});
|
|
88
97
|
_TokenBalancesController_allIgnoredTokens.set(this, {});
|
|
89
|
-
/** Token metadata cache from TokenListController */
|
|
90
|
-
_TokenBalancesController_tokensChainsCache.set(this, {});
|
|
91
98
|
/** Default polling interval for chains without specific configuration */
|
|
92
99
|
_TokenBalancesController_defaultInterval.set(this, void 0);
|
|
93
100
|
/** Polling interval when WebSocket is active and providing real-time updates */
|
|
@@ -98,8 +105,6 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
|
|
|
98
105
|
_TokenBalancesController_intervalPollingTimers.set(this, new Map());
|
|
99
106
|
/** Track if controller-level polling is active */
|
|
100
107
|
_TokenBalancesController_isControllerPollingActive.set(this, false);
|
|
101
|
-
/** Track if the keyring is unlocked */
|
|
102
|
-
_TokenBalancesController_isUnlocked.set(this, false);
|
|
103
108
|
/** Store original chainIds from startPolling to preserve intent */
|
|
104
109
|
_TokenBalancesController_requestedChainIds.set(this, []);
|
|
105
110
|
/** Debouncing for rapid status changes to prevent excessive HTTP calls */
|
|
@@ -109,34 +114,52 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
|
|
|
109
114
|
});
|
|
110
115
|
_TokenBalancesController_getProvider.set(this, (chainId) => {
|
|
111
116
|
const { networkConfigurationsByChainId } = this.messenger.call('NetworkController:getState');
|
|
112
|
-
const
|
|
113
|
-
const { networkClientId } =
|
|
117
|
+
const cfg = networkConfigurationsByChainId[chainId];
|
|
118
|
+
const { networkClientId } = cfg.rpcEndpoints[cfg.defaultRpcEndpointIndex];
|
|
114
119
|
const client = this.messenger.call('NetworkController:getNetworkClientById', networkClientId);
|
|
115
120
|
return new Web3Provider(client.provider);
|
|
116
121
|
});
|
|
117
122
|
_TokenBalancesController_getNetworkClient.set(this, (chainId) => {
|
|
118
123
|
const { networkConfigurationsByChainId } = this.messenger.call('NetworkController:getState');
|
|
119
|
-
const
|
|
120
|
-
const { networkClientId } =
|
|
124
|
+
const cfg = networkConfigurationsByChainId[chainId];
|
|
125
|
+
const { networkClientId } = cfg.rpcEndpoints[cfg.defaultRpcEndpointIndex];
|
|
121
126
|
return this.messenger.call('NetworkController:getNetworkClientById', networkClientId);
|
|
122
127
|
});
|
|
128
|
+
/**
|
|
129
|
+
* Creates an AccountsApiBalanceFetcher that only supports chains in the accountsApiChainIds array
|
|
130
|
+
*
|
|
131
|
+
* @returns A BalanceFetcher that wraps AccountsApiBalanceFetcher with chainId filtering
|
|
132
|
+
*/
|
|
123
133
|
_TokenBalancesController_createAccountsApiFetcher.set(this, () => {
|
|
124
134
|
const originalFetcher = new AccountsApiBalanceFetcher(__classPrivateFieldGet(this, _TokenBalancesController_platform, "f"), __classPrivateFieldGet(this, _TokenBalancesController_getProvider, "f"));
|
|
125
135
|
return {
|
|
126
|
-
supports: (chainId) =>
|
|
127
|
-
|
|
136
|
+
supports: (chainId) => {
|
|
137
|
+
// Only support chains that are both:
|
|
138
|
+
// 1. In our specified accountsApiChainIds array
|
|
139
|
+
// 2. Actually supported by the AccountsApi
|
|
140
|
+
return (__classPrivateFieldGet(this, _TokenBalancesController_accountsApiChainIds, "f").call(this).includes(chainId) &&
|
|
141
|
+
originalFetcher.supports(chainId));
|
|
142
|
+
},
|
|
128
143
|
fetch: originalFetcher.fetch.bind(originalFetcher),
|
|
129
144
|
};
|
|
130
145
|
});
|
|
131
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
132
|
-
// TokensController / Network / Accounts events
|
|
133
146
|
_TokenBalancesController_onTokensChanged.set(this, async (state) => {
|
|
134
147
|
const changed = [];
|
|
135
148
|
let hasChanges = false;
|
|
149
|
+
// Get chains that have existing balances
|
|
150
|
+
const chainsWithBalances = new Set();
|
|
151
|
+
for (const address of Object.keys(this.state.tokenBalances)) {
|
|
152
|
+
const addressKey = address;
|
|
153
|
+
for (const chainId of Object.keys(this.state.tokenBalances[addressKey] || {})) {
|
|
154
|
+
chainsWithBalances.add(chainId);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Only process chains that are explicitly mentioned in the incoming state change
|
|
136
158
|
const incomingChainIds = new Set([
|
|
137
159
|
...Object.keys(state.allTokens),
|
|
138
160
|
...Object.keys(state.allDetectedTokens),
|
|
139
161
|
]);
|
|
162
|
+
// Only proceed if there are actual changes to chains that have balances or are being added
|
|
140
163
|
const relevantChainIds = Array.from(incomingChainIds).filter((chainId) => {
|
|
141
164
|
const id = chainId;
|
|
142
165
|
const hasTokensNow = (state.allTokens[id] && Object.keys(state.allTokens[id]).length > 0) ||
|
|
@@ -145,16 +168,20 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
|
|
|
145
168
|
const hadTokensBefore = (__classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")[id] && Object.keys(__classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")[id]).length > 0) ||
|
|
146
169
|
(__classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[id] &&
|
|
147
170
|
Object.keys(__classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[id]).length > 0);
|
|
171
|
+
// Check if there's an actual change in token state
|
|
148
172
|
const hasTokenChange = !isEqual(state.allTokens[id], __classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")[id]) ||
|
|
149
173
|
!isEqual(state.allDetectedTokens[id], __classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[id]);
|
|
174
|
+
// Process chains that have actual changes OR are new chains getting tokens
|
|
150
175
|
return hasTokenChange || (!hadTokensBefore && hasTokensNow);
|
|
151
176
|
});
|
|
152
|
-
if (
|
|
177
|
+
if (relevantChainIds.length === 0) {
|
|
178
|
+
// No relevant changes, just update internal state
|
|
153
179
|
__classPrivateFieldSet(this, _TokenBalancesController_allTokens, state.allTokens, "f");
|
|
154
180
|
__classPrivateFieldSet(this, _TokenBalancesController_detectedTokens, state.allDetectedTokens, "f");
|
|
155
181
|
return;
|
|
156
182
|
}
|
|
157
|
-
|
|
183
|
+
// Handle both cleanup and updates in a single state update
|
|
184
|
+
this.update((s) => {
|
|
158
185
|
for (const chainId of relevantChainIds) {
|
|
159
186
|
const id = chainId;
|
|
160
187
|
const hasTokensNow = (state.allTokens[id] &&
|
|
@@ -165,20 +192,20 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
|
|
|
165
192
|
Object.keys(__classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")[id]).length > 0) ||
|
|
166
193
|
(__classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[id] &&
|
|
167
194
|
Object.keys(__classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[id]).length > 0);
|
|
168
|
-
|
|
169
|
-
!isEqual(state.allDetectedTokens[id], __classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[id])
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
195
|
+
if (!isEqual(state.allTokens[id], __classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")[id]) ||
|
|
196
|
+
!isEqual(state.allDetectedTokens[id], __classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[id])) {
|
|
197
|
+
if (hasTokensNow) {
|
|
198
|
+
// Chain still has tokens - mark for async balance update
|
|
199
|
+
changed.push(id);
|
|
200
|
+
}
|
|
201
|
+
else if (hadTokensBefore) {
|
|
202
|
+
// Chain had tokens before but doesn't now - clean up balances immediately
|
|
203
|
+
for (const address of Object.keys(s.tokenBalances)) {
|
|
204
|
+
const addressKey = address;
|
|
205
|
+
if (s.tokenBalances[addressKey]?.[id]) {
|
|
206
|
+
s.tokenBalances[addressKey][id] = {};
|
|
207
|
+
hasChanges = true;
|
|
208
|
+
}
|
|
182
209
|
}
|
|
183
210
|
}
|
|
184
211
|
}
|
|
@@ -187,6 +214,7 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
|
|
|
187
214
|
__classPrivateFieldSet(this, _TokenBalancesController_allTokens, state.allTokens, "f");
|
|
188
215
|
__classPrivateFieldSet(this, _TokenBalancesController_detectedTokens, state.allDetectedTokens, "f");
|
|
189
216
|
__classPrivateFieldSet(this, _TokenBalancesController_allIgnoredTokens, state.allIgnoredTokens, "f");
|
|
217
|
+
// Only update balances for chains that still have tokens (and only if we haven't already updated state)
|
|
190
218
|
if (changed.length && !hasChanges) {
|
|
191
219
|
this.updateBalances({ chainIds: changed }).catch((error) => {
|
|
192
220
|
console.warn('Error updating balances after token change:', error);
|
|
@@ -194,7 +222,9 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
|
|
|
194
222
|
}
|
|
195
223
|
});
|
|
196
224
|
_TokenBalancesController_onNetworkChanged.set(this, (state) => {
|
|
225
|
+
// Check if any networks were removed by comparing with previous state
|
|
197
226
|
const currentNetworks = new Set(Object.keys(state.networkConfigurationsByChainId));
|
|
227
|
+
// Get all networks that currently have balances
|
|
198
228
|
const networksWithBalances = new Set();
|
|
199
229
|
for (const address of Object.keys(this.state.tokenBalances)) {
|
|
200
230
|
const addressKey = address;
|
|
@@ -202,59 +232,83 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
|
|
|
202
232
|
networksWithBalances.add(network);
|
|
203
233
|
}
|
|
204
234
|
}
|
|
235
|
+
// Find networks that were removed
|
|
205
236
|
const removedNetworks = Array.from(networksWithBalances).filter((network) => !currentNetworks.has(network));
|
|
206
|
-
if (
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
237
|
+
if (removedNetworks.length > 0) {
|
|
238
|
+
this.update((s) => {
|
|
239
|
+
// Remove balances for all accounts on the deleted networks
|
|
240
|
+
for (const address of Object.keys(s.tokenBalances)) {
|
|
241
|
+
const addressKey = address;
|
|
242
|
+
for (const removedNetwork of removedNetworks) {
|
|
243
|
+
const networkKey = removedNetwork;
|
|
244
|
+
if (s.tokenBalances[addressKey]?.[networkKey]) {
|
|
245
|
+
delete s.tokenBalances[addressKey][networkKey];
|
|
246
|
+
}
|
|
216
247
|
}
|
|
217
248
|
}
|
|
218
|
-
}
|
|
219
|
-
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
220
251
|
});
|
|
221
252
|
_TokenBalancesController_onAccountRemoved.set(this, (addr) => {
|
|
222
253
|
if (!isStrictHexString(addr) || !isValidHexAddress(addr)) {
|
|
223
254
|
return;
|
|
224
255
|
}
|
|
225
|
-
this.update((
|
|
226
|
-
delete
|
|
256
|
+
this.update((s) => {
|
|
257
|
+
delete s.tokenBalances[addr];
|
|
227
258
|
});
|
|
228
259
|
});
|
|
260
|
+
/**
|
|
261
|
+
* Handle account selection changes
|
|
262
|
+
* Triggers immediate balance fetch to ensure we have the latest balances
|
|
263
|
+
* since WebSocket only provides updates for changes going forward
|
|
264
|
+
*/
|
|
229
265
|
_TokenBalancesController_onAccountChanged.set(this, () => {
|
|
266
|
+
// Fetch balances for all chains with tokens when account changes
|
|
230
267
|
const chainIds = __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_chainIdsWithTokens).call(this);
|
|
231
|
-
if (
|
|
232
|
-
|
|
268
|
+
if (chainIds.length > 0) {
|
|
269
|
+
this.updateBalances({ chainIds }).catch(() => {
|
|
270
|
+
// Silently handle polling errors
|
|
271
|
+
});
|
|
233
272
|
}
|
|
234
|
-
this.updateBalances({ chainIds }).catch(() => {
|
|
235
|
-
// Silently handle polling errors
|
|
236
|
-
});
|
|
237
273
|
});
|
|
274
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
275
|
+
// AccountActivityService event handlers
|
|
276
|
+
/**
|
|
277
|
+
* Handle real-time balance updates from AccountActivityService
|
|
278
|
+
* Processes balance updates and updates the token balance state
|
|
279
|
+
* If any balance update has an error, triggers fallback polling for the chain
|
|
280
|
+
*
|
|
281
|
+
* @param options0 - Balance update parameters
|
|
282
|
+
* @param options0.address - Account address
|
|
283
|
+
* @param options0.chain - CAIP chain identifier
|
|
284
|
+
* @param options0.updates - Array of balance updates for the account
|
|
285
|
+
*/
|
|
238
286
|
_TokenBalancesController_onAccountActivityBalanceUpdate.set(this, async ({ address, chain, updates, }) => {
|
|
239
287
|
const chainId = caipChainIdToHex(chain);
|
|
240
288
|
const checksummedAccount = checksum(address);
|
|
241
289
|
try {
|
|
290
|
+
// Process all balance updates at once
|
|
242
291
|
const { tokenBalances, newTokens, nativeBalanceUpdates } = __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_prepareBalanceUpdates).call(this, updates, checksummedAccount, chainId);
|
|
292
|
+
// Update state once with all token balances
|
|
243
293
|
if (tokenBalances.length > 0) {
|
|
244
294
|
this.update((state) => {
|
|
245
295
|
var _a, _b;
|
|
296
|
+
// Temporary until ADR to normalize all keys - tokenBalances state requires: account in lowercase, token in checksum
|
|
246
297
|
const lowercaseAccount = checksummedAccount.toLowerCase();
|
|
247
298
|
(_a = state.tokenBalances)[lowercaseAccount] ?? (_a[lowercaseAccount] = {});
|
|
248
299
|
(_b = state.tokenBalances[lowercaseAccount])[chainId] ?? (_b[chainId] = {});
|
|
300
|
+
// Apply all token balance updates
|
|
249
301
|
for (const { tokenAddress, balance } of tokenBalances) {
|
|
250
302
|
state.tokenBalances[lowercaseAccount][chainId][tokenAddress] =
|
|
251
303
|
balance;
|
|
252
304
|
}
|
|
253
305
|
});
|
|
254
306
|
}
|
|
307
|
+
// Update native balances in AccountTrackerController
|
|
255
308
|
if (nativeBalanceUpdates.length > 0) {
|
|
256
309
|
this.messenger.call('AccountTrackerController:updateNativeBalances', nativeBalanceUpdates);
|
|
257
310
|
}
|
|
311
|
+
// Import any new tokens that were discovered (balance already updated from websocket)
|
|
258
312
|
if (newTokens.length > 0) {
|
|
259
313
|
await this.messenger.call('TokenDetectionController:addDetectedTokensViaWs', {
|
|
260
314
|
tokensSlice: newTokens,
|
|
@@ -265,22 +319,35 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
|
|
|
265
319
|
catch (error) {
|
|
266
320
|
console.warn(`Error updating balances from AccountActivityService for chain ${chain}, account ${address}:`, error);
|
|
267
321
|
console.warn('Balance update data:', JSON.stringify(updates, null, 2));
|
|
322
|
+
// On error, trigger fallback polling
|
|
268
323
|
await this.updateBalances({ chainIds: [chainId] }).catch(() => {
|
|
269
324
|
// Silently handle polling errors
|
|
270
325
|
});
|
|
271
326
|
}
|
|
272
327
|
});
|
|
328
|
+
/**
|
|
329
|
+
* Handle status changes from AccountActivityService
|
|
330
|
+
* Uses aggressive debouncing to prevent excessive HTTP calls from rapid up/down changes
|
|
331
|
+
*
|
|
332
|
+
* @param options0 - Status change event data
|
|
333
|
+
* @param options0.chainIds - Array of chain identifiers
|
|
334
|
+
* @param options0.status - Connection status ('up' for connected, 'down' for disconnected)
|
|
335
|
+
*/
|
|
273
336
|
_TokenBalancesController_onAccountActivityStatusChanged.set(this, ({ chainIds, status, }) => {
|
|
337
|
+
// Update pending changes (latest status wins for each chain)
|
|
274
338
|
for (const chainId of chainIds) {
|
|
275
339
|
__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").pendingChanges.set(chainId, status);
|
|
276
340
|
}
|
|
341
|
+
// Clear existing timer to extend debounce window
|
|
277
342
|
if (__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer) {
|
|
278
343
|
clearTimeout(__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer);
|
|
279
344
|
}
|
|
345
|
+
// Set new timer - only process changes after activity settles
|
|
280
346
|
__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer = setTimeout(() => {
|
|
281
347
|
__classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_processAccumulatedStatusChanges).call(this);
|
|
282
|
-
}, 5000);
|
|
348
|
+
}, 5000); // 5-second debounce window
|
|
283
349
|
});
|
|
350
|
+
// Normalize all account addresses to lowercase in existing state
|
|
284
351
|
__classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_normalizeAccountAddresses).call(this);
|
|
285
352
|
__classPrivateFieldSet(this, _TokenBalancesController_platform, platform ?? 'extension', "f");
|
|
286
353
|
__classPrivateFieldSet(this, _TokenBalancesController_queryAllAccounts, queryMultipleAccounts, "f");
|
|
@@ -288,6 +355,7 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
|
|
|
288
355
|
__classPrivateFieldSet(this, _TokenBalancesController_defaultInterval, interval, "f");
|
|
289
356
|
__classPrivateFieldSet(this, _TokenBalancesController_websocketActivePollingInterval, websocketActivePollingInterval, "f");
|
|
290
357
|
__classPrivateFieldSet(this, _TokenBalancesController_chainPollingConfig, { ...chainPollingIntervals }, "f");
|
|
358
|
+
// Strategy order: API first, then RPC fallback
|
|
291
359
|
__classPrivateFieldSet(this, _TokenBalancesController_balanceFetchers, [
|
|
292
360
|
...(accountsApiChainIds().length > 0 && allowExternalServices()
|
|
293
361
|
? [__classPrivateFieldGet(this, _TokenBalancesController_createAccountsApiFetcher, "f").call(this)]
|
|
@@ -298,185 +366,302 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
|
|
|
298
366
|
})),
|
|
299
367
|
], "f");
|
|
300
368
|
this.setIntervalLength(interval);
|
|
369
|
+
// initial token state & subscriptions
|
|
301
370
|
const { allTokens, allDetectedTokens, allIgnoredTokens } = this.messenger.call('TokensController:getState');
|
|
302
371
|
__classPrivateFieldSet(this, _TokenBalancesController_allTokens, allTokens, "f");
|
|
303
372
|
__classPrivateFieldSet(this, _TokenBalancesController_detectedTokens, allDetectedTokens, "f");
|
|
304
373
|
__classPrivateFieldSet(this, _TokenBalancesController_allIgnoredTokens, allIgnoredTokens, "f");
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
__classPrivateFieldGet(this,
|
|
374
|
+
this.messenger.subscribe('TokensController:stateChange', (tokensState) => {
|
|
375
|
+
__classPrivateFieldGet(this, _TokenBalancesController_onTokensChanged, "f").call(this, tokensState).catch((error) => {
|
|
376
|
+
console.warn('Error handling token state change:', error);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
this.messenger.subscribe('NetworkController:stateChange', __classPrivateFieldGet(this, _TokenBalancesController_onNetworkChanged, "f"));
|
|
380
|
+
this.messenger.subscribe('KeyringController:accountRemoved', __classPrivateFieldGet(this, _TokenBalancesController_onAccountRemoved, "f"));
|
|
381
|
+
this.messenger.subscribe('AccountsController:selectedEvmAccountChange', __classPrivateFieldGet(this, _TokenBalancesController_onAccountChanged, "f"));
|
|
382
|
+
// Register action handlers for polling interval control
|
|
383
|
+
this.messenger.registerActionHandler(`TokenBalancesController:updateChainPollingConfigs`, this.updateChainPollingConfigs.bind(this));
|
|
384
|
+
this.messenger.registerActionHandler(`TokenBalancesController:getChainPollingConfig`, this.getChainPollingConfig.bind(this));
|
|
385
|
+
// Subscribe to AccountActivityService balance updates for real-time updates
|
|
386
|
+
this.messenger.subscribe('AccountActivityService:balanceUpdated', __classPrivateFieldGet(this, _TokenBalancesController_onAccountActivityBalanceUpdate, "f").bind(this));
|
|
387
|
+
// Subscribe to AccountActivityService status changes for dynamic polling management
|
|
388
|
+
this.messenger.subscribe('AccountActivityService:statusChanged', __classPrivateFieldGet(this, _TokenBalancesController_onAccountActivityStatusChanged, "f").bind(this));
|
|
311
389
|
}
|
|
312
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
313
|
-
// Address + network helpers
|
|
314
390
|
/**
|
|
315
|
-
*
|
|
316
|
-
* When locked, balance updates should be skipped.
|
|
391
|
+
* Override to support per-chain polling intervals by grouping chains by interval
|
|
317
392
|
*
|
|
318
|
-
* @
|
|
393
|
+
* @param options0 - The polling options
|
|
394
|
+
* @param options0.chainIds - Chain IDs to start polling for
|
|
319
395
|
*/
|
|
320
|
-
get isActive() {
|
|
321
|
-
return __classPrivateFieldGet(this, _TokenBalancesController_isUnlocked, "f");
|
|
322
|
-
}
|
|
323
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
324
|
-
// Polling overrides
|
|
325
396
|
_startPolling({ chainIds }) {
|
|
397
|
+
// Store the original chainIds to preserve intent across config updates
|
|
326
398
|
__classPrivateFieldSet(this, _TokenBalancesController_requestedChainIds, [...chainIds], "f");
|
|
327
399
|
__classPrivateFieldSet(this, _TokenBalancesController_isControllerPollingActive, true, "f");
|
|
328
400
|
__classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_startIntervalGroupPolling).call(this, chainIds, true);
|
|
329
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* Override to handle our custom polling approach
|
|
404
|
+
*
|
|
405
|
+
* @param tokenSetId - The token set ID to stop polling for
|
|
406
|
+
*/
|
|
330
407
|
_stopPollingByPollingTokenSetId(tokenSetId) {
|
|
408
|
+
let parsedTokenSetId;
|
|
331
409
|
let chainsToStop = [];
|
|
332
410
|
try {
|
|
333
|
-
|
|
334
|
-
chainsToStop = parsedTokenSetId.chainIds
|
|
411
|
+
parsedTokenSetId = JSON.parse(tokenSetId);
|
|
412
|
+
chainsToStop = parsedTokenSetId.chainIds || [];
|
|
335
413
|
}
|
|
336
414
|
catch (error) {
|
|
337
415
|
console.warn('Failed to parse tokenSetId, stopping all polling:', error);
|
|
338
|
-
|
|
416
|
+
// Fallback: stop all polling if we can't parse the tokenSetId
|
|
417
|
+
__classPrivateFieldSet(this, _TokenBalancesController_isControllerPollingActive, false, "f");
|
|
418
|
+
__classPrivateFieldSet(this, _TokenBalancesController_requestedChainIds, [], "f");
|
|
419
|
+
__classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").forEach((timer) => clearInterval(timer));
|
|
420
|
+
__classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").clear();
|
|
339
421
|
return;
|
|
340
422
|
}
|
|
423
|
+
// Compare with current chains - only stop if it matches our current session
|
|
341
424
|
const currentChainsSet = new Set(__classPrivateFieldGet(this, _TokenBalancesController_requestedChainIds, "f"));
|
|
342
425
|
const stopChainsSet = new Set(chainsToStop);
|
|
426
|
+
// Check if this stop request is for our current session
|
|
343
427
|
const isCurrentSession = currentChainsSet.size === stopChainsSet.size &&
|
|
344
428
|
[...currentChainsSet].every((chain) => stopChainsSet.has(chain));
|
|
345
429
|
if (isCurrentSession) {
|
|
346
|
-
|
|
430
|
+
__classPrivateFieldSet(this, _TokenBalancesController_isControllerPollingActive, false, "f");
|
|
431
|
+
__classPrivateFieldSet(this, _TokenBalancesController_requestedChainIds, [], "f");
|
|
432
|
+
__classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").forEach((timer) => clearInterval(timer));
|
|
433
|
+
__classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").clear();
|
|
347
434
|
}
|
|
348
435
|
}
|
|
436
|
+
/**
|
|
437
|
+
* Get polling configuration for a chain (includes default fallback)
|
|
438
|
+
*
|
|
439
|
+
* @param chainId - The chain ID to get config for
|
|
440
|
+
* @returns The polling configuration for the chain
|
|
441
|
+
*/
|
|
349
442
|
getChainPollingConfig(chainId) {
|
|
350
443
|
return (__classPrivateFieldGet(this, _TokenBalancesController_chainPollingConfig, "f")[chainId] ?? {
|
|
351
444
|
interval: __classPrivateFieldGet(this, _TokenBalancesController_defaultInterval, "f"),
|
|
352
445
|
});
|
|
353
446
|
}
|
|
354
447
|
async _executePoll({ chainIds, queryAllAccounts = false, }) {
|
|
448
|
+
// This won't be called with our custom implementation, but keep for compatibility
|
|
355
449
|
await this.updateBalances({ chainIds, queryAllAccounts });
|
|
356
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* Update multiple chain polling configurations at once
|
|
453
|
+
*
|
|
454
|
+
* @param configs - Object mapping chain IDs to polling configurations
|
|
455
|
+
* @param options - Optional configuration for the update behavior
|
|
456
|
+
* @param options.immediateUpdate - Whether to immediately fetch balances after updating configs (default: true)
|
|
457
|
+
*/
|
|
357
458
|
updateChainPollingConfigs(configs, options = { immediateUpdate: true }) {
|
|
358
459
|
Object.assign(__classPrivateFieldGet(this, _TokenBalancesController_chainPollingConfig, "f"), configs);
|
|
460
|
+
// If polling is currently active, restart with new interval groupings
|
|
359
461
|
if (__classPrivateFieldGet(this, _TokenBalancesController_isControllerPollingActive, "f")) {
|
|
462
|
+
// Restart polling with immediate fetch by default, unless explicitly disabled
|
|
360
463
|
__classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_startIntervalGroupPolling).call(this, __classPrivateFieldGet(this, _TokenBalancesController_requestedChainIds, "f"), options.immediateUpdate);
|
|
361
464
|
}
|
|
362
465
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
async updateBalances({ chainIds, tokenAddresses, queryAllAccounts = false, } = {}) {
|
|
366
|
-
if (!this.isActive) {
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
const targetChains = __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_getTargetChains).call(this, chainIds);
|
|
466
|
+
async updateBalances({ chainIds, queryAllAccounts = false, } = {}) {
|
|
467
|
+
const targetChains = chainIds ?? __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_chainIdsWithTokens).call(this);
|
|
370
468
|
if (!targetChains.length) {
|
|
371
469
|
return;
|
|
372
470
|
}
|
|
373
|
-
const {
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
const
|
|
382
|
-
|
|
471
|
+
const { address: selected } = this.messenger.call('AccountsController:getSelectedAccount');
|
|
472
|
+
const allAccounts = this.messenger.call('AccountsController:listAccounts');
|
|
473
|
+
const jwtToken = await safelyExecuteWithTimeout(() => {
|
|
474
|
+
return this.messenger.call('AuthenticationController:getBearerToken');
|
|
475
|
+
}, false, 5000);
|
|
476
|
+
const aggregated = [];
|
|
477
|
+
let remainingChains = [...targetChains];
|
|
478
|
+
// Try each fetcher in order, removing successfully processed chains
|
|
479
|
+
for (const fetcher of __classPrivateFieldGet(this, _TokenBalancesController_balanceFetchers, "f")) {
|
|
480
|
+
const supportedChains = remainingChains.filter((c) => fetcher.supports(c));
|
|
481
|
+
if (!supportedChains.length) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
try {
|
|
485
|
+
const result = await fetcher.fetch({
|
|
486
|
+
chainIds: supportedChains,
|
|
487
|
+
queryAllAccounts: queryAllAccounts ?? __classPrivateFieldGet(this, _TokenBalancesController_queryAllAccounts, "f"),
|
|
488
|
+
selectedAccount: selected,
|
|
489
|
+
allAccounts,
|
|
490
|
+
jwtToken,
|
|
491
|
+
});
|
|
492
|
+
if (result.balances && result.balances.length > 0) {
|
|
493
|
+
aggregated.push(...result.balances);
|
|
494
|
+
// Remove chains that were successfully processed
|
|
495
|
+
const processedChains = new Set(result.balances.map((b) => b.chainId));
|
|
496
|
+
remainingChains = remainingChains.filter((chain) => !processedChains.has(chain));
|
|
497
|
+
}
|
|
498
|
+
// Add unprocessed chains back to remainingChains for next fetcher
|
|
499
|
+
if (result.unprocessedChainIds &&
|
|
500
|
+
result.unprocessedChainIds.length > 0) {
|
|
501
|
+
const currentRemainingChains = remainingChains;
|
|
502
|
+
const chainsToAdd = result.unprocessedChainIds.filter((chainId) => supportedChains.includes(chainId) &&
|
|
503
|
+
!currentRemainingChains.includes(chainId));
|
|
504
|
+
remainingChains.push(...chainsToAdd);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
catch (error) {
|
|
508
|
+
console.warn(`Balance fetcher failed for chains ${supportedChains.join(', ')}: ${String(error)}`);
|
|
509
|
+
// Continue to next fetcher (fallback)
|
|
510
|
+
}
|
|
511
|
+
// If all chains have been processed, break early
|
|
512
|
+
if (remainingChains.length === 0) {
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// Determine which accounts to process based on queryAllAccounts parameter
|
|
517
|
+
const accountsToProcess = (queryAllAccounts ?? __classPrivateFieldGet(this, _TokenBalancesController_queryAllAccounts, "f"))
|
|
518
|
+
? allAccounts.map((a) => a.address)
|
|
519
|
+
: [selected];
|
|
383
520
|
const prev = this.state;
|
|
384
|
-
const next =
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
521
|
+
const next = draft(prev, (d) => {
|
|
522
|
+
var _a, _b;
|
|
523
|
+
// Initialize account and chain structures if they don't exist, but preserve existing balances
|
|
524
|
+
for (const chainId of targetChains) {
|
|
525
|
+
for (const account of accountsToProcess) {
|
|
526
|
+
// Ensure the nested structure exists without overwriting existing balances
|
|
527
|
+
(_a = d.tokenBalances)[account] ?? (_a[account] = {});
|
|
528
|
+
(_b = d.tokenBalances[account])[chainId] ?? (_b[chainId] = {});
|
|
529
|
+
// Initialize tokens from allTokens only if they don't exist yet
|
|
530
|
+
const chainTokens = __classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")[chainId];
|
|
531
|
+
if (chainTokens?.[account]) {
|
|
532
|
+
Object.values(chainTokens[account]).forEach((token) => {
|
|
533
|
+
const tokenAddress = checksum(token.address);
|
|
534
|
+
// Only initialize if the token balance doesn't exist yet
|
|
535
|
+
if (!(tokenAddress in d.tokenBalances[account][chainId])) {
|
|
536
|
+
d.tokenBalances[account][chainId][tokenAddress] = '0x0';
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
// Initialize tokens from allDetectedTokens only if they don't exist yet
|
|
541
|
+
const detectedChainTokens = __classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[chainId];
|
|
542
|
+
if (detectedChainTokens?.[account]) {
|
|
543
|
+
Object.values(detectedChainTokens[account]).forEach((token) => {
|
|
544
|
+
const tokenAddress = checksum(token.address);
|
|
545
|
+
// Only initialize if the token balance doesn't exist yet
|
|
546
|
+
if (!(tokenAddress in d.tokenBalances[account][chainId])) {
|
|
547
|
+
d.tokenBalances[account][chainId][tokenAddress] = '0x0';
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
// Update with actual fetched balances only if the value has changed
|
|
554
|
+
aggregated.forEach(({ success, value, account, token, chainId }) => {
|
|
555
|
+
var _a, _b;
|
|
556
|
+
if (success && value !== undefined) {
|
|
557
|
+
// Ensure all accounts we add/update are in lower-case
|
|
558
|
+
const lowerCaseAccount = account.toLowerCase();
|
|
559
|
+
const newBalance = toHex(value);
|
|
560
|
+
const tokenAddress = checksum(token);
|
|
561
|
+
const currentBalance = d.tokenBalances[lowerCaseAccount]?.[chainId]?.[tokenAddress];
|
|
562
|
+
// Only update if the balance has actually changed
|
|
563
|
+
if (currentBalance !== newBalance) {
|
|
564
|
+
((_b = ((_a = d.tokenBalances)[lowerCaseAccount] ?? (_a[lowerCaseAccount] = {})))[chainId] ?? (_b[chainId] = {}))[tokenAddress] = newBalance;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
});
|
|
389
568
|
});
|
|
390
569
|
if (!isEqual(prev, next)) {
|
|
391
570
|
this.update(() => next);
|
|
571
|
+
const nativeBalances = aggregated.filter((r) => r.success && r.token === ZERO_ADDRESS);
|
|
572
|
+
// Get current AccountTracker state to compare existing balances
|
|
392
573
|
const accountTrackerState = this.messenger.call('AccountTrackerController:getState');
|
|
393
|
-
|
|
394
|
-
if (
|
|
395
|
-
|
|
574
|
+
// Update native token balances only if they have changed
|
|
575
|
+
if (nativeBalances.length > 0) {
|
|
576
|
+
const balanceUpdates = nativeBalances
|
|
577
|
+
.map((balance) => ({
|
|
578
|
+
address: balance.account,
|
|
579
|
+
chainId: balance.chainId,
|
|
580
|
+
balance: balance.value ? BNToHex(balance.value) : '0x0',
|
|
581
|
+
}))
|
|
582
|
+
.filter((update) => {
|
|
583
|
+
const currentBalance = accountTrackerState.accountsByChainId[update.chainId]?.[checksum(update.address)]?.balance;
|
|
584
|
+
// Only include if the balance has actually changed
|
|
585
|
+
return currentBalance !== update.balance;
|
|
586
|
+
});
|
|
587
|
+
if (balanceUpdates.length > 0) {
|
|
588
|
+
this.messenger.call('AccountTrackerController:updateNativeBalances', balanceUpdates);
|
|
589
|
+
}
|
|
396
590
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
591
|
+
// Filter and update staked balances in a single batch operation for better performance
|
|
592
|
+
const stakedBalances = aggregated.filter((r) => {
|
|
593
|
+
if (!r.success || r.token === ZERO_ADDRESS) {
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
// Check if the chainId and token address match any staking contract
|
|
597
|
+
const stakingContractAddress = STAKING_CONTRACT_ADDRESS_BY_CHAINID[r.chainId];
|
|
598
|
+
return (stakingContractAddress &&
|
|
599
|
+
stakingContractAddress.toLowerCase() === r.token.toLowerCase());
|
|
600
|
+
});
|
|
601
|
+
if (stakedBalances.length > 0) {
|
|
602
|
+
const stakedBalanceUpdates = stakedBalances
|
|
603
|
+
.map((balance) => ({
|
|
604
|
+
address: balance.account,
|
|
605
|
+
chainId: balance.chainId,
|
|
606
|
+
stakedBalance: balance.value ? toHex(balance.value) : '0x0',
|
|
607
|
+
}))
|
|
608
|
+
.filter((update) => {
|
|
609
|
+
const currentStakedBalance = accountTrackerState.accountsByChainId[update.chainId]?.[checksum(update.address)]?.stakedBalance;
|
|
610
|
+
// Only include if the staked balance has actually changed
|
|
611
|
+
return currentStakedBalance !== update.stakedBalance;
|
|
612
|
+
});
|
|
613
|
+
if (stakedBalanceUpdates.length > 0) {
|
|
614
|
+
this.messenger.call('AccountTrackerController:updateStakedBalances', stakedBalanceUpdates);
|
|
615
|
+
}
|
|
400
616
|
}
|
|
401
617
|
}
|
|
402
|
-
await __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_importUntrackedTokens).call(this, filteredAggregated);
|
|
403
618
|
}
|
|
404
619
|
resetState() {
|
|
405
620
|
this.update(() => ({ tokenBalances: {} }));
|
|
406
621
|
}
|
|
407
|
-
|
|
408
|
-
|
|
622
|
+
/**
|
|
623
|
+
* Clean up all timers and resources when controller is destroyed
|
|
624
|
+
*/
|
|
409
625
|
destroy() {
|
|
410
626
|
__classPrivateFieldSet(this, _TokenBalancesController_isControllerPollingActive, false, "f");
|
|
411
627
|
__classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").forEach((timer) => clearInterval(timer));
|
|
412
628
|
__classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").clear();
|
|
629
|
+
// Clean up debouncing timer
|
|
413
630
|
if (__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer) {
|
|
414
631
|
clearTimeout(__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer);
|
|
415
632
|
__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer = null;
|
|
416
633
|
}
|
|
634
|
+
// Unregister action handlers
|
|
417
635
|
this.messenger.unregisterActionHandler(`TokenBalancesController:updateChainPollingConfigs`);
|
|
418
636
|
this.messenger.unregisterActionHandler(`TokenBalancesController:getChainPollingConfig`);
|
|
419
637
|
super.destroy();
|
|
420
638
|
}
|
|
421
639
|
}
|
|
422
|
-
_TokenBalancesController_platform = new WeakMap(), _TokenBalancesController_queryAllAccounts = new WeakMap(), _TokenBalancesController_accountsApiChainIds = new WeakMap(), _TokenBalancesController_balanceFetchers = new WeakMap(), _TokenBalancesController_allTokens = new WeakMap(), _TokenBalancesController_detectedTokens = new WeakMap(), _TokenBalancesController_allIgnoredTokens = new WeakMap(),
|
|
423
|
-
this.messenger.subscribe('TokensController:stateChange', (tokensState) => {
|
|
424
|
-
__classPrivateFieldGet(this, _TokenBalancesController_onTokensChanged, "f").call(this, tokensState).catch((error) => {
|
|
425
|
-
console.warn('Error handling token state change:', error);
|
|
426
|
-
});
|
|
427
|
-
});
|
|
428
|
-
this.messenger.subscribe('NetworkController:stateChange', __classPrivateFieldGet(this, _TokenBalancesController_onNetworkChanged, "f"));
|
|
429
|
-
this.messenger.subscribe('TokenListController:stateChange', ({ tokensChainsCache }) => {
|
|
430
|
-
__classPrivateFieldSet(this, _TokenBalancesController_tokensChainsCache, tokensChainsCache, "f");
|
|
431
|
-
});
|
|
432
|
-
this.messenger.subscribe('KeyringController:unlock', () => {
|
|
433
|
-
__classPrivateFieldSet(this, _TokenBalancesController_isUnlocked, true, "f");
|
|
434
|
-
});
|
|
435
|
-
this.messenger.subscribe('KeyringController:lock', () => {
|
|
436
|
-
__classPrivateFieldSet(this, _TokenBalancesController_isUnlocked, false, "f");
|
|
437
|
-
});
|
|
438
|
-
this.messenger.subscribe('KeyringController:accountRemoved', __classPrivateFieldGet(this, _TokenBalancesController_onAccountRemoved, "f"));
|
|
439
|
-
this.messenger.subscribe('AccountsController:selectedEvmAccountChange', __classPrivateFieldGet(this, _TokenBalancesController_onAccountChanged, "f"));
|
|
440
|
-
this.messenger.subscribe('AccountActivityService:balanceUpdated', (event) => {
|
|
441
|
-
__classPrivateFieldGet(this, _TokenBalancesController_onAccountActivityBalanceUpdate, "f").call(this, event).catch((error) => {
|
|
442
|
-
console.warn('Error handling balance update:', error);
|
|
443
|
-
});
|
|
444
|
-
});
|
|
445
|
-
this.messenger.subscribe('AccountActivityService:statusChanged', __classPrivateFieldGet(this, _TokenBalancesController_onAccountActivityStatusChanged, "f").bind(this));
|
|
446
|
-
this.messenger.subscribe('TransactionController:transactionConfirmed', (transactionMeta) => {
|
|
447
|
-
this.updateBalances({
|
|
448
|
-
chainIds: [transactionMeta.chainId],
|
|
449
|
-
}).catch(() => {
|
|
450
|
-
// Silently handle balance update errors
|
|
451
|
-
});
|
|
452
|
-
});
|
|
453
|
-
this.messenger.subscribe('TransactionController:incomingTransactionsReceived', (incomingTransactions) => {
|
|
454
|
-
this.updateBalances({
|
|
455
|
-
chainIds: incomingTransactions.map((tx) => tx.chainId),
|
|
456
|
-
}).catch(() => {
|
|
457
|
-
// Silently handle balance update errors
|
|
458
|
-
});
|
|
459
|
-
});
|
|
460
|
-
}, _TokenBalancesController_registerActions = function _TokenBalancesController_registerActions() {
|
|
461
|
-
this.messenger.registerActionHandler(`TokenBalancesController:updateChainPollingConfigs`, this.updateChainPollingConfigs.bind(this));
|
|
462
|
-
this.messenger.registerActionHandler(`TokenBalancesController:getChainPollingConfig`, this.getChainPollingConfig.bind(this));
|
|
463
|
-
}, _TokenBalancesController_normalizeAccountAddresses = function _TokenBalancesController_normalizeAccountAddresses() {
|
|
464
|
-
var _a;
|
|
640
|
+
_TokenBalancesController_platform = new WeakMap(), _TokenBalancesController_queryAllAccounts = new WeakMap(), _TokenBalancesController_accountsApiChainIds = new WeakMap(), _TokenBalancesController_balanceFetchers = new WeakMap(), _TokenBalancesController_allTokens = new WeakMap(), _TokenBalancesController_detectedTokens = new WeakMap(), _TokenBalancesController_allIgnoredTokens = new WeakMap(), _TokenBalancesController_defaultInterval = new WeakMap(), _TokenBalancesController_websocketActivePollingInterval = new WeakMap(), _TokenBalancesController_chainPollingConfig = new WeakMap(), _TokenBalancesController_intervalPollingTimers = new WeakMap(), _TokenBalancesController_isControllerPollingActive = new WeakMap(), _TokenBalancesController_requestedChainIds = new WeakMap(), _TokenBalancesController_statusChangeDebouncer = new WeakMap(), _TokenBalancesController_getProvider = new WeakMap(), _TokenBalancesController_getNetworkClient = new WeakMap(), _TokenBalancesController_createAccountsApiFetcher = new WeakMap(), _TokenBalancesController_onTokensChanged = new WeakMap(), _TokenBalancesController_onNetworkChanged = new WeakMap(), _TokenBalancesController_onAccountRemoved = new WeakMap(), _TokenBalancesController_onAccountChanged = new WeakMap(), _TokenBalancesController_onAccountActivityBalanceUpdate = new WeakMap(), _TokenBalancesController_onAccountActivityStatusChanged = new WeakMap(), _TokenBalancesController_instances = new WeakSet(), _TokenBalancesController_normalizeAccountAddresses = function _TokenBalancesController_normalizeAccountAddresses() {
|
|
465
641
|
const currentState = this.state.tokenBalances;
|
|
466
642
|
const normalizedBalances = {};
|
|
643
|
+
// Iterate through all accounts and normalize to lowercase
|
|
467
644
|
for (const address of Object.keys(currentState)) {
|
|
468
645
|
const lowercaseAddress = address.toLowerCase();
|
|
469
646
|
const accountBalances = currentState[address];
|
|
470
647
|
if (!accountBalances) {
|
|
471
648
|
continue;
|
|
472
649
|
}
|
|
473
|
-
|
|
650
|
+
// If this lowercase address doesn't exist yet, create it
|
|
651
|
+
if (!normalizedBalances[lowercaseAddress]) {
|
|
652
|
+
normalizedBalances[lowercaseAddress] = {};
|
|
653
|
+
}
|
|
654
|
+
// Merge chain data
|
|
474
655
|
for (const chainId of Object.keys(accountBalances)) {
|
|
475
656
|
const chainIdKey = chainId;
|
|
476
|
-
(
|
|
657
|
+
if (!normalizedBalances[lowercaseAddress][chainIdKey]) {
|
|
658
|
+
normalizedBalances[lowercaseAddress][chainIdKey] = {};
|
|
659
|
+
}
|
|
660
|
+
// Merge token balances (later values override earlier ones if duplicates exist)
|
|
477
661
|
Object.assign(normalizedBalances[lowercaseAddress][chainIdKey], accountBalances[chainIdKey]);
|
|
478
662
|
}
|
|
479
663
|
}
|
|
664
|
+
// Only update if there were changes
|
|
480
665
|
if (Object.keys(currentState).length !==
|
|
481
666
|
Object.keys(normalizedBalances).length ||
|
|
482
667
|
Object.keys(currentState).some((addr) => addr !== addr.toLowerCase())) {
|
|
@@ -490,15 +675,18 @@ _TokenBalancesController_platform = new WeakMap(), _TokenBalancesController_quer
|
|
|
490
675
|
]),
|
|
491
676
|
];
|
|
492
677
|
}, _TokenBalancesController_startIntervalGroupPolling = function _TokenBalancesController_startIntervalGroupPolling(chainIds, immediate = true) {
|
|
678
|
+
// Stop any existing interval timers
|
|
493
679
|
__classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").forEach((timer) => clearInterval(timer));
|
|
494
680
|
__classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").clear();
|
|
681
|
+
// Group chains by their polling intervals
|
|
495
682
|
const intervalGroups = new Map();
|
|
496
683
|
for (const chainId of chainIds) {
|
|
497
684
|
const config = this.getChainPollingConfig(chainId);
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
intervalGroups.set(config.interval,
|
|
685
|
+
const existing = intervalGroups.get(config.interval) || [];
|
|
686
|
+
existing.push(chainId);
|
|
687
|
+
intervalGroups.set(config.interval, existing);
|
|
501
688
|
}
|
|
689
|
+
// Start separate polling loop for each interval group
|
|
502
690
|
for (const [interval, chainIdsGroup] of intervalGroups) {
|
|
503
691
|
__classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_startPollingForInterval).call(this, interval, chainIdsGroup, immediate);
|
|
504
692
|
}
|
|
@@ -514,237 +702,33 @@ _TokenBalancesController_platform = new WeakMap(), _TokenBalancesController_quer
|
|
|
514
702
|
console.warn(`Polling failed for chains ${chainIds.join(', ')} with interval ${interval}:`, error);
|
|
515
703
|
}
|
|
516
704
|
};
|
|
705
|
+
// Poll immediately first if requested
|
|
517
706
|
if (immediate) {
|
|
518
707
|
pollFunction().catch((error) => {
|
|
519
708
|
console.warn(`Immediate polling failed for chains ${chainIds.join(', ')}:`, error);
|
|
520
709
|
});
|
|
521
710
|
}
|
|
711
|
+
// Then start regular interval polling
|
|
522
712
|
__classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_setPollingTimer).call(this, interval, chainIds, pollFunction);
|
|
523
713
|
}, _TokenBalancesController_setPollingTimer = function _TokenBalancesController_setPollingTimer(interval, chainIds, pollFunction) {
|
|
714
|
+
// Clear any existing timer for this interval first
|
|
715
|
+
const existingTimer = __classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").get(interval);
|
|
716
|
+
if (existingTimer) {
|
|
717
|
+
clearInterval(existingTimer);
|
|
718
|
+
}
|
|
524
719
|
const timer = setInterval(() => {
|
|
525
720
|
pollFunction().catch((error) => {
|
|
526
721
|
console.warn(`Interval polling failed for chains ${chainIds.join(', ')}:`, error);
|
|
527
722
|
});
|
|
528
723
|
}, interval);
|
|
529
724
|
__classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").set(interval, timer);
|
|
530
|
-
}, _TokenBalancesController_stopAllPolling = function _TokenBalancesController_stopAllPolling() {
|
|
531
|
-
__classPrivateFieldSet(this, _TokenBalancesController_isControllerPollingActive, false, "f");
|
|
532
|
-
__classPrivateFieldSet(this, _TokenBalancesController_requestedChainIds, [], "f");
|
|
533
|
-
__classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").forEach((timer) => clearInterval(timer));
|
|
534
|
-
__classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").clear();
|
|
535
|
-
}, _TokenBalancesController_getTargetChains = function _TokenBalancesController_getTargetChains(chainIds) {
|
|
536
|
-
return chainIds?.length ? chainIds : __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_chainIdsWithTokens).call(this);
|
|
537
|
-
}, _TokenBalancesController_getAccountsAndJwt = async function _TokenBalancesController_getAccountsAndJwt() {
|
|
538
|
-
const { address: selected } = this.messenger.call('AccountsController:getSelectedAccount');
|
|
539
|
-
const allAccounts = this.messenger.call('AccountsController:listAccounts');
|
|
540
|
-
const jwtToken = await safelyExecuteWithTimeout(() => {
|
|
541
|
-
return this.messenger.call('AuthenticationController:getBearerToken');
|
|
542
|
-
}, false, 5000);
|
|
543
|
-
return {
|
|
544
|
-
selectedAccount: selected,
|
|
545
|
-
allAccounts,
|
|
546
|
-
jwtToken,
|
|
547
|
-
};
|
|
548
|
-
}, _TokenBalancesController_fetchAllBalances = async function _TokenBalancesController_fetchAllBalances({ targetChains, selectedAccount, allAccounts, jwtToken, queryAllAccounts, }) {
|
|
549
|
-
const aggregated = [];
|
|
550
|
-
let remainingChains = [...targetChains];
|
|
551
|
-
for (const fetcher of __classPrivateFieldGet(this, _TokenBalancesController_balanceFetchers, "f")) {
|
|
552
|
-
const supportedChains = remainingChains.filter((chain) => fetcher.supports(chain));
|
|
553
|
-
if (!supportedChains.length) {
|
|
554
|
-
continue;
|
|
555
|
-
}
|
|
556
|
-
try {
|
|
557
|
-
const result = await fetcher.fetch({
|
|
558
|
-
chainIds: supportedChains,
|
|
559
|
-
queryAllAccounts,
|
|
560
|
-
selectedAccount,
|
|
561
|
-
allAccounts,
|
|
562
|
-
jwtToken,
|
|
563
|
-
});
|
|
564
|
-
if (result.balances?.length) {
|
|
565
|
-
aggregated.push(...result.balances);
|
|
566
|
-
const processed = new Set(result.balances.map((b) => b.chainId));
|
|
567
|
-
remainingChains = remainingChains.filter((chain) => !processed.has(chain));
|
|
568
|
-
}
|
|
569
|
-
if (result.unprocessedChainIds?.length) {
|
|
570
|
-
const currentRemaining = [...remainingChains];
|
|
571
|
-
const chainsToAdd = result.unprocessedChainIds.filter((chainId) => supportedChains.includes(chainId) &&
|
|
572
|
-
!currentRemaining.includes(chainId));
|
|
573
|
-
remainingChains.push(...chainsToAdd);
|
|
574
|
-
this.messenger
|
|
575
|
-
.call('TokenDetectionController:detectTokens', {
|
|
576
|
-
chainIds: result.unprocessedChainIds,
|
|
577
|
-
forceRpc: true,
|
|
578
|
-
})
|
|
579
|
-
.catch(() => {
|
|
580
|
-
// Silently handle token detection errors
|
|
581
|
-
});
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
catch (error) {
|
|
585
|
-
console.warn(`Balance fetcher failed for chains ${supportedChains.join(', ')}: ${String(error)}`);
|
|
586
|
-
this.messenger
|
|
587
|
-
.call('TokenDetectionController:detectTokens', {
|
|
588
|
-
chainIds: supportedChains,
|
|
589
|
-
forceRpc: true,
|
|
590
|
-
})
|
|
591
|
-
.catch(() => {
|
|
592
|
-
// Silently handle token detection errors
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
if (!remainingChains.length) {
|
|
596
|
-
break;
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
return aggregated;
|
|
600
|
-
}, _TokenBalancesController_filterByTokenAddresses = function _TokenBalancesController_filterByTokenAddresses(balances, tokenAddresses) {
|
|
601
|
-
if (!tokenAddresses?.length) {
|
|
602
|
-
return balances;
|
|
603
|
-
}
|
|
604
|
-
const lowered = tokenAddresses.map((a) => a.toLowerCase());
|
|
605
|
-
return balances.filter((balance) => lowered.includes(balance.token.toLowerCase()));
|
|
606
|
-
}, _TokenBalancesController_getAccountsToProcess = function _TokenBalancesController_getAccountsToProcess(queryAllAccountsParam, allAccounts, selectedAccount) {
|
|
607
|
-
const effectiveQueryAll = queryAllAccountsParam ?? __classPrivateFieldGet(this, _TokenBalancesController_queryAllAccounts, "f") ?? false;
|
|
608
|
-
if (!effectiveQueryAll) {
|
|
609
|
-
return [selectedAccount];
|
|
610
|
-
}
|
|
611
|
-
return allAccounts.map((account) => account.address);
|
|
612
|
-
}, _TokenBalancesController_applyTokenBalancesToState = function _TokenBalancesController_applyTokenBalancesToState({ prev, targetChains, accountsToProcess, balances, }) {
|
|
613
|
-
return draft(prev, (draftState) => {
|
|
614
|
-
var _a, _b;
|
|
615
|
-
for (const chainId of targetChains) {
|
|
616
|
-
for (const account of accountsToProcess) {
|
|
617
|
-
(_a = draftState.tokenBalances)[account] ?? (_a[account] = {});
|
|
618
|
-
(_b = draftState.tokenBalances[account])[chainId] ?? (_b[chainId] = {});
|
|
619
|
-
const chainTokens = __classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")[chainId];
|
|
620
|
-
if (chainTokens?.[account]) {
|
|
621
|
-
Object.values(chainTokens[account]).forEach((token) => {
|
|
622
|
-
var _a;
|
|
623
|
-
const tokenAddress = checksum(token.address);
|
|
624
|
-
(_a = draftState.tokenBalances[account][chainId])[tokenAddress] ?? (_a[tokenAddress] = '0x0');
|
|
625
|
-
});
|
|
626
|
-
}
|
|
627
|
-
const detectedChainTokens = __classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[chainId];
|
|
628
|
-
if (detectedChainTokens?.[account]) {
|
|
629
|
-
Object.values(detectedChainTokens[account]).forEach((token) => {
|
|
630
|
-
var _a;
|
|
631
|
-
const tokenAddress = checksum(token.address);
|
|
632
|
-
(_a = draftState.tokenBalances[account][chainId])[tokenAddress] ?? (_a[tokenAddress] = '0x0');
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
balances.forEach(({ success, value, account, token, chainId }) => {
|
|
638
|
-
var _a, _b;
|
|
639
|
-
if (!success || value === undefined) {
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
const lowerCaseAccount = account.toLowerCase();
|
|
643
|
-
const newBalance = toHex(value);
|
|
644
|
-
const tokenAddress = checksum(token);
|
|
645
|
-
const currentBalance = draftState.tokenBalances[lowerCaseAccount]?.[chainId]?.[tokenAddress];
|
|
646
|
-
if (currentBalance !== newBalance) {
|
|
647
|
-
((_b = ((_a = draftState.tokenBalances)[lowerCaseAccount] ?? (_a[lowerCaseAccount] = {})))[chainId] ?? (_b[chainId] = {}))[tokenAddress] = newBalance;
|
|
648
|
-
}
|
|
649
|
-
});
|
|
650
|
-
});
|
|
651
|
-
}, _TokenBalancesController_buildNativeBalanceUpdates = function _TokenBalancesController_buildNativeBalanceUpdates(balances, accountTrackerState) {
|
|
652
|
-
const nativeBalances = balances.filter((balance) => balance.success && balance.token === ZERO_ADDRESS);
|
|
653
|
-
if (!nativeBalances.length) {
|
|
654
|
-
return [];
|
|
655
|
-
}
|
|
656
|
-
return nativeBalances
|
|
657
|
-
.map((balance) => ({
|
|
658
|
-
address: balance.account,
|
|
659
|
-
chainId: balance.chainId,
|
|
660
|
-
balance: balance.value ? BNToHex(balance.value) : '0x0',
|
|
661
|
-
}))
|
|
662
|
-
.filter((update) => {
|
|
663
|
-
const currentBalance = accountTrackerState.accountsByChainId[update.chainId]?.[checksum(update.address)]?.balance;
|
|
664
|
-
return currentBalance !== update.balance;
|
|
665
|
-
});
|
|
666
|
-
}, _TokenBalancesController_buildStakedBalanceUpdates = function _TokenBalancesController_buildStakedBalanceUpdates(balances, accountTrackerState) {
|
|
667
|
-
const stakedBalances = balances.filter((balance) => {
|
|
668
|
-
if (!balance.success || balance.token === ZERO_ADDRESS) {
|
|
669
|
-
return false;
|
|
670
|
-
}
|
|
671
|
-
const stakingContractAddress = STAKING_CONTRACT_ADDRESS_BY_CHAINID[balance.chainId];
|
|
672
|
-
return (stakingContractAddress &&
|
|
673
|
-
stakingContractAddress.toLowerCase() === balance.token.toLowerCase());
|
|
674
|
-
});
|
|
675
|
-
if (!stakedBalances.length) {
|
|
676
|
-
return [];
|
|
677
|
-
}
|
|
678
|
-
return stakedBalances
|
|
679
|
-
.map((balance) => ({
|
|
680
|
-
address: balance.account,
|
|
681
|
-
chainId: balance.chainId,
|
|
682
|
-
stakedBalance: balance.value ? toHex(balance.value) : '0x0',
|
|
683
|
-
}))
|
|
684
|
-
.filter((update) => {
|
|
685
|
-
const currentStakedBalance = accountTrackerState.accountsByChainId[update.chainId]?.[checksum(update.address)]?.stakedBalance;
|
|
686
|
-
return currentStakedBalance !== update.stakedBalance;
|
|
687
|
-
});
|
|
688
|
-
}, _TokenBalancesController_importUntrackedTokens =
|
|
689
|
-
/**
|
|
690
|
-
* Import untracked tokens that have non-zero balances.
|
|
691
|
-
* This mirrors the v2 behavior where only tokens with actual balances are added.
|
|
692
|
-
* Directly calls TokensController:addTokens for the polling flow.
|
|
693
|
-
*
|
|
694
|
-
* @param balances - Array of processed balance results from fetchers
|
|
695
|
-
*/
|
|
696
|
-
async function _TokenBalancesController_importUntrackedTokens(balances) {
|
|
697
|
-
const untrackedTokensByChain = new Map();
|
|
698
|
-
for (const balance of balances) {
|
|
699
|
-
// Skip failed fetches, native tokens, and zero balances (like v2 did)
|
|
700
|
-
if (!balance.success ||
|
|
701
|
-
balance.token === ZERO_ADDRESS ||
|
|
702
|
-
!balance.value ||
|
|
703
|
-
balance.value.isZero()) {
|
|
704
|
-
continue;
|
|
705
|
-
}
|
|
706
|
-
const tokenAddress = checksum(balance.token);
|
|
707
|
-
const account = balance.account.toLowerCase();
|
|
708
|
-
if (!__classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_isTokenTracked).call(this, tokenAddress, account, balance.chainId)) {
|
|
709
|
-
const existing = untrackedTokensByChain.get(balance.chainId) ?? [];
|
|
710
|
-
if (!existing.includes(tokenAddress)) {
|
|
711
|
-
existing.push(tokenAddress);
|
|
712
|
-
untrackedTokensByChain.set(balance.chainId, existing);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
}
|
|
716
|
-
// Add detected tokens directly via TokensController:addTokens (polling flow)
|
|
717
|
-
for (const [chainId, tokenAddresses] of untrackedTokensByChain) {
|
|
718
|
-
const tokensWithMetadata = [];
|
|
719
|
-
for (const tokenAddress of tokenAddresses) {
|
|
720
|
-
const lowercaseAddress = tokenAddress.toLowerCase();
|
|
721
|
-
const tokenData = __classPrivateFieldGet(this, _TokenBalancesController_tokensChainsCache, "f")[chainId]?.data?.[lowercaseAddress];
|
|
722
|
-
if (!tokenData) {
|
|
723
|
-
console.warn(`Token metadata not found in cache for ${tokenAddress} on chain ${chainId}`);
|
|
724
|
-
continue;
|
|
725
|
-
}
|
|
726
|
-
const { decimals, symbol, aggregators, iconUrl, name } = tokenData;
|
|
727
|
-
tokensWithMetadata.push({
|
|
728
|
-
address: tokenAddress,
|
|
729
|
-
decimals,
|
|
730
|
-
symbol,
|
|
731
|
-
aggregators,
|
|
732
|
-
image: iconUrl,
|
|
733
|
-
isERC721: false,
|
|
734
|
-
name,
|
|
735
|
-
});
|
|
736
|
-
}
|
|
737
|
-
if (tokensWithMetadata.length) {
|
|
738
|
-
const networkClientId = this.messenger.call('NetworkController:findNetworkClientIdByChainId', chainId);
|
|
739
|
-
await this.messenger.call('TokensController:addTokens', tokensWithMetadata, networkClientId);
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
725
|
}, _TokenBalancesController_isTokenTracked = function _TokenBalancesController_isTokenTracked(tokenAddress, account, chainId) {
|
|
743
|
-
|
|
744
|
-
if (__classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")?.[chainId]?.[
|
|
726
|
+
// Check if token exists in allTokens
|
|
727
|
+
if (__classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")?.[chainId]?.[account.toLowerCase()]?.some((token) => token.address === tokenAddress)) {
|
|
745
728
|
return true;
|
|
746
729
|
}
|
|
747
|
-
|
|
730
|
+
// Check if token exists in allIgnoredTokens
|
|
731
|
+
if (__classPrivateFieldGet(this, _TokenBalancesController_allIgnoredTokens, "f")?.[chainId]?.[account.toLowerCase()]?.some((token) => token === tokenAddress)) {
|
|
748
732
|
return true;
|
|
749
733
|
}
|
|
750
734
|
return false;
|
|
@@ -754,25 +738,31 @@ async function _TokenBalancesController_importUntrackedTokens(balances) {
|
|
|
754
738
|
const nativeBalanceUpdates = [];
|
|
755
739
|
for (const update of updates) {
|
|
756
740
|
const { asset, postBalance } = update;
|
|
741
|
+
// Throw if balance update has an error
|
|
757
742
|
if (postBalance.error) {
|
|
758
743
|
throw new Error('Balance update has error');
|
|
759
744
|
}
|
|
745
|
+
// Parse token address from asset type
|
|
760
746
|
const parsed = parseAssetType(asset.type);
|
|
761
747
|
if (!parsed) {
|
|
762
748
|
throw new Error('Failed to parse asset type');
|
|
763
749
|
}
|
|
764
750
|
const [tokenAddress, isNativeToken] = parsed;
|
|
751
|
+
// Validate token address
|
|
765
752
|
if (!isStrictHexString(tokenAddress) ||
|
|
766
753
|
!isValidHexAddress(tokenAddress)) {
|
|
767
754
|
throw new Error('Invalid token address');
|
|
768
755
|
}
|
|
769
756
|
const checksumTokenAddress = checksum(tokenAddress);
|
|
770
757
|
const isTracked = __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_isTokenTracked).call(this, checksumTokenAddress, account, chainId);
|
|
758
|
+
// postBalance.amount is in hex format (raw units)
|
|
771
759
|
const balanceHex = postBalance.amount;
|
|
760
|
+
// Add token balance (tracked tokens, ignored tokens, and native tokens all get balance updates)
|
|
772
761
|
tokenBalances.push({
|
|
773
762
|
tokenAddress: checksumTokenAddress,
|
|
774
763
|
balance: balanceHex,
|
|
775
764
|
});
|
|
765
|
+
// Add native balance update if this is a native token
|
|
776
766
|
if (isNativeToken) {
|
|
777
767
|
nativeBalanceUpdates.push({
|
|
778
768
|
address: account,
|
|
@@ -780,6 +770,7 @@ async function _TokenBalancesController_importUntrackedTokens(balances) {
|
|
|
780
770
|
balance: balanceHex,
|
|
781
771
|
});
|
|
782
772
|
}
|
|
773
|
+
// Handle untracked ERC20 tokens - queue for import
|
|
783
774
|
if (!isNativeToken && !isTracked) {
|
|
784
775
|
newTokens.push(checksumTokenAddress);
|
|
785
776
|
}
|
|
@@ -789,18 +780,28 @@ async function _TokenBalancesController_importUntrackedTokens(balances) {
|
|
|
789
780
|
const changes = Array.from(__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").pendingChanges.entries());
|
|
790
781
|
__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").pendingChanges.clear();
|
|
791
782
|
__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer = null;
|
|
792
|
-
if (
|
|
783
|
+
if (changes.length === 0) {
|
|
793
784
|
return;
|
|
794
785
|
}
|
|
786
|
+
// Calculate final polling configurations
|
|
795
787
|
const chainConfigs = {};
|
|
796
788
|
for (const [chainId, status] of changes) {
|
|
789
|
+
// Convert CAIP format (eip155:1) to hex format (0x1)
|
|
790
|
+
// chainId is always in CAIP format from AccountActivityService
|
|
797
791
|
const hexChainId = caipChainIdToHex(chainId);
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
792
|
+
if (status === 'down') {
|
|
793
|
+
// Chain is down - use default polling since no real-time updates available
|
|
794
|
+
chainConfigs[hexChainId] = { interval: __classPrivateFieldGet(this, _TokenBalancesController_defaultInterval, "f") };
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
// Chain is up - use longer intervals since WebSocket provides real-time updates
|
|
798
|
+
chainConfigs[hexChainId] = {
|
|
799
|
+
interval: __classPrivateFieldGet(this, _TokenBalancesController_websocketActivePollingInterval, "f"),
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
802
|
}
|
|
803
|
-
|
|
803
|
+
// Add jitter to prevent synchronized requests across instances
|
|
804
|
+
const jitterDelay = Math.random() * __classPrivateFieldGet(this, _TokenBalancesController_defaultInterval, "f"); // 0 to default interval
|
|
804
805
|
setTimeout(() => {
|
|
805
806
|
this.updateChainPollingConfigs(chainConfigs, { immediateUpdate: true });
|
|
806
807
|
}, jitterDelay);
|