@metamask-previews/assets-controllers 93.1.0-preview-c92c27f1 → 93.1.0-preview-d717276a
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 -337
- package/dist/TokenBalancesController.cjs.map +1 -1
- package/dist/TokenBalancesController.d.cts +38 -17
- package/dist/TokenBalancesController.d.cts.map +1 -1
- package/dist/TokenBalancesController.d.mts +38 -17
- package/dist/TokenBalancesController.d.mts.map +1 -1
- package/dist/TokenBalancesController.mjs +377 -337
- 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/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, _TokenBalancesController_defaultInterval, _TokenBalancesController_websocketActivePollingInterval, _TokenBalancesController_chainPollingConfig, _TokenBalancesController_intervalPollingTimers, _TokenBalancesController_isControllerPollingActive,
|
|
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({
|
|
@@ -96,8 +105,6 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
|
|
|
96
105
|
_TokenBalancesController_intervalPollingTimers.set(this, new Map());
|
|
97
106
|
/** Track if controller-level polling is active */
|
|
98
107
|
_TokenBalancesController_isControllerPollingActive.set(this, false);
|
|
99
|
-
/** Track if the keyring is unlocked */
|
|
100
|
-
_TokenBalancesController_isUnlocked.set(this, false);
|
|
101
108
|
/** Store original chainIds from startPolling to preserve intent */
|
|
102
109
|
_TokenBalancesController_requestedChainIds.set(this, []);
|
|
103
110
|
/** Debouncing for rapid status changes to prevent excessive HTTP calls */
|
|
@@ -107,34 +114,52 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
|
|
|
107
114
|
});
|
|
108
115
|
_TokenBalancesController_getProvider.set(this, (chainId) => {
|
|
109
116
|
const { networkConfigurationsByChainId } = this.messenger.call('NetworkController:getState');
|
|
110
|
-
const
|
|
111
|
-
const { networkClientId } =
|
|
117
|
+
const cfg = networkConfigurationsByChainId[chainId];
|
|
118
|
+
const { networkClientId } = cfg.rpcEndpoints[cfg.defaultRpcEndpointIndex];
|
|
112
119
|
const client = this.messenger.call('NetworkController:getNetworkClientById', networkClientId);
|
|
113
120
|
return new Web3Provider(client.provider);
|
|
114
121
|
});
|
|
115
122
|
_TokenBalancesController_getNetworkClient.set(this, (chainId) => {
|
|
116
123
|
const { networkConfigurationsByChainId } = this.messenger.call('NetworkController:getState');
|
|
117
|
-
const
|
|
118
|
-
const { networkClientId } =
|
|
124
|
+
const cfg = networkConfigurationsByChainId[chainId];
|
|
125
|
+
const { networkClientId } = cfg.rpcEndpoints[cfg.defaultRpcEndpointIndex];
|
|
119
126
|
return this.messenger.call('NetworkController:getNetworkClientById', networkClientId);
|
|
120
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
|
+
*/
|
|
121
133
|
_TokenBalancesController_createAccountsApiFetcher.set(this, () => {
|
|
122
134
|
const originalFetcher = new AccountsApiBalanceFetcher(__classPrivateFieldGet(this, _TokenBalancesController_platform, "f"), __classPrivateFieldGet(this, _TokenBalancesController_getProvider, "f"));
|
|
123
135
|
return {
|
|
124
|
-
supports: (chainId) =>
|
|
125
|
-
|
|
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
|
+
},
|
|
126
143
|
fetch: originalFetcher.fetch.bind(originalFetcher),
|
|
127
144
|
};
|
|
128
145
|
});
|
|
129
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
130
|
-
// TokensController / Network / Accounts events
|
|
131
146
|
_TokenBalancesController_onTokensChanged.set(this, async (state) => {
|
|
132
147
|
const changed = [];
|
|
133
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
|
|
134
158
|
const incomingChainIds = new Set([
|
|
135
159
|
...Object.keys(state.allTokens),
|
|
136
160
|
...Object.keys(state.allDetectedTokens),
|
|
137
161
|
]);
|
|
162
|
+
// Only proceed if there are actual changes to chains that have balances or are being added
|
|
138
163
|
const relevantChainIds = Array.from(incomingChainIds).filter((chainId) => {
|
|
139
164
|
const id = chainId;
|
|
140
165
|
const hasTokensNow = (state.allTokens[id] && Object.keys(state.allTokens[id]).length > 0) ||
|
|
@@ -143,16 +168,20 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
|
|
|
143
168
|
const hadTokensBefore = (__classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")[id] && Object.keys(__classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")[id]).length > 0) ||
|
|
144
169
|
(__classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[id] &&
|
|
145
170
|
Object.keys(__classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[id]).length > 0);
|
|
171
|
+
// Check if there's an actual change in token state
|
|
146
172
|
const hasTokenChange = !isEqual(state.allTokens[id], __classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")[id]) ||
|
|
147
173
|
!isEqual(state.allDetectedTokens[id], __classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[id]);
|
|
174
|
+
// Process chains that have actual changes OR are new chains getting tokens
|
|
148
175
|
return hasTokenChange || (!hadTokensBefore && hasTokensNow);
|
|
149
176
|
});
|
|
150
|
-
if (
|
|
177
|
+
if (relevantChainIds.length === 0) {
|
|
178
|
+
// No relevant changes, just update internal state
|
|
151
179
|
__classPrivateFieldSet(this, _TokenBalancesController_allTokens, state.allTokens, "f");
|
|
152
180
|
__classPrivateFieldSet(this, _TokenBalancesController_detectedTokens, state.allDetectedTokens, "f");
|
|
153
181
|
return;
|
|
154
182
|
}
|
|
155
|
-
|
|
183
|
+
// Handle both cleanup and updates in a single state update
|
|
184
|
+
this.update((s) => {
|
|
156
185
|
for (const chainId of relevantChainIds) {
|
|
157
186
|
const id = chainId;
|
|
158
187
|
const hasTokensNow = (state.allTokens[id] &&
|
|
@@ -163,20 +192,20 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
|
|
|
163
192
|
Object.keys(__classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")[id]).length > 0) ||
|
|
164
193
|
(__classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[id] &&
|
|
165
194
|
Object.keys(__classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[id]).length > 0);
|
|
166
|
-
|
|
167
|
-
!isEqual(state.allDetectedTokens[id], __classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[id])
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
+
}
|
|
180
209
|
}
|
|
181
210
|
}
|
|
182
211
|
}
|
|
@@ -185,6 +214,7 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
|
|
|
185
214
|
__classPrivateFieldSet(this, _TokenBalancesController_allTokens, state.allTokens, "f");
|
|
186
215
|
__classPrivateFieldSet(this, _TokenBalancesController_detectedTokens, state.allDetectedTokens, "f");
|
|
187
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)
|
|
188
218
|
if (changed.length && !hasChanges) {
|
|
189
219
|
this.updateBalances({ chainIds: changed }).catch((error) => {
|
|
190
220
|
console.warn('Error updating balances after token change:', error);
|
|
@@ -192,7 +222,9 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
|
|
|
192
222
|
}
|
|
193
223
|
});
|
|
194
224
|
_TokenBalancesController_onNetworkChanged.set(this, (state) => {
|
|
225
|
+
// Check if any networks were removed by comparing with previous state
|
|
195
226
|
const currentNetworks = new Set(Object.keys(state.networkConfigurationsByChainId));
|
|
227
|
+
// Get all networks that currently have balances
|
|
196
228
|
const networksWithBalances = new Set();
|
|
197
229
|
for (const address of Object.keys(this.state.tokenBalances)) {
|
|
198
230
|
const addressKey = address;
|
|
@@ -200,59 +232,83 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
|
|
|
200
232
|
networksWithBalances.add(network);
|
|
201
233
|
}
|
|
202
234
|
}
|
|
235
|
+
// Find networks that were removed
|
|
203
236
|
const removedNetworks = Array.from(networksWithBalances).filter((network) => !currentNetworks.has(network));
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
+
}
|
|
214
247
|
}
|
|
215
248
|
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
218
251
|
});
|
|
219
252
|
_TokenBalancesController_onAccountRemoved.set(this, (addr) => {
|
|
220
253
|
if (!isStrictHexString(addr) || !isValidHexAddress(addr)) {
|
|
221
254
|
return;
|
|
222
255
|
}
|
|
223
|
-
this.update((
|
|
224
|
-
delete
|
|
256
|
+
this.update((s) => {
|
|
257
|
+
delete s.tokenBalances[addr];
|
|
225
258
|
});
|
|
226
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
|
+
*/
|
|
227
265
|
_TokenBalancesController_onAccountChanged.set(this, () => {
|
|
266
|
+
// Fetch balances for all chains with tokens when account changes
|
|
228
267
|
const chainIds = __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_chainIdsWithTokens).call(this);
|
|
229
|
-
if (
|
|
230
|
-
|
|
268
|
+
if (chainIds.length > 0) {
|
|
269
|
+
this.updateBalances({ chainIds }).catch(() => {
|
|
270
|
+
// Silently handle polling errors
|
|
271
|
+
});
|
|
231
272
|
}
|
|
232
|
-
this.updateBalances({ chainIds }).catch(() => {
|
|
233
|
-
// Silently handle polling errors
|
|
234
|
-
});
|
|
235
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
|
+
*/
|
|
236
286
|
_TokenBalancesController_onAccountActivityBalanceUpdate.set(this, async ({ address, chain, updates, }) => {
|
|
237
287
|
const chainId = caipChainIdToHex(chain);
|
|
238
288
|
const checksummedAccount = checksum(address);
|
|
239
289
|
try {
|
|
290
|
+
// Process all balance updates at once
|
|
240
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
|
|
241
293
|
if (tokenBalances.length > 0) {
|
|
242
294
|
this.update((state) => {
|
|
243
295
|
var _a, _b;
|
|
296
|
+
// Temporary until ADR to normalize all keys - tokenBalances state requires: account in lowercase, token in checksum
|
|
244
297
|
const lowercaseAccount = checksummedAccount.toLowerCase();
|
|
245
298
|
(_a = state.tokenBalances)[lowercaseAccount] ?? (_a[lowercaseAccount] = {});
|
|
246
299
|
(_b = state.tokenBalances[lowercaseAccount])[chainId] ?? (_b[chainId] = {});
|
|
300
|
+
// Apply all token balance updates
|
|
247
301
|
for (const { tokenAddress, balance } of tokenBalances) {
|
|
248
302
|
state.tokenBalances[lowercaseAccount][chainId][tokenAddress] =
|
|
249
303
|
balance;
|
|
250
304
|
}
|
|
251
305
|
});
|
|
252
306
|
}
|
|
307
|
+
// Update native balances in AccountTrackerController
|
|
253
308
|
if (nativeBalanceUpdates.length > 0) {
|
|
254
309
|
this.messenger.call('AccountTrackerController:updateNativeBalances', nativeBalanceUpdates);
|
|
255
310
|
}
|
|
311
|
+
// Import any new tokens that were discovered (balance already updated from websocket)
|
|
256
312
|
if (newTokens.length > 0) {
|
|
257
313
|
await this.messenger.call('TokenDetectionController:addDetectedTokensViaWs', {
|
|
258
314
|
tokensSlice: newTokens,
|
|
@@ -263,22 +319,35 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
|
|
|
263
319
|
catch (error) {
|
|
264
320
|
console.warn(`Error updating balances from AccountActivityService for chain ${chain}, account ${address}:`, error);
|
|
265
321
|
console.warn('Balance update data:', JSON.stringify(updates, null, 2));
|
|
322
|
+
// On error, trigger fallback polling
|
|
266
323
|
await this.updateBalances({ chainIds: [chainId] }).catch(() => {
|
|
267
324
|
// Silently handle polling errors
|
|
268
325
|
});
|
|
269
326
|
}
|
|
270
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
|
+
*/
|
|
271
336
|
_TokenBalancesController_onAccountActivityStatusChanged.set(this, ({ chainIds, status, }) => {
|
|
337
|
+
// Update pending changes (latest status wins for each chain)
|
|
272
338
|
for (const chainId of chainIds) {
|
|
273
339
|
__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").pendingChanges.set(chainId, status);
|
|
274
340
|
}
|
|
341
|
+
// Clear existing timer to extend debounce window
|
|
275
342
|
if (__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer) {
|
|
276
343
|
clearTimeout(__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer);
|
|
277
344
|
}
|
|
345
|
+
// Set new timer - only process changes after activity settles
|
|
278
346
|
__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer = setTimeout(() => {
|
|
279
347
|
__classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_processAccumulatedStatusChanges).call(this);
|
|
280
|
-
}, 5000);
|
|
348
|
+
}, 5000); // 5-second debounce window
|
|
281
349
|
});
|
|
350
|
+
// Normalize all account addresses to lowercase in existing state
|
|
282
351
|
__classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_normalizeAccountAddresses).call(this);
|
|
283
352
|
__classPrivateFieldSet(this, _TokenBalancesController_platform, platform ?? 'extension', "f");
|
|
284
353
|
__classPrivateFieldSet(this, _TokenBalancesController_queryAllAccounts, queryMultipleAccounts, "f");
|
|
@@ -286,6 +355,7 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
|
|
|
286
355
|
__classPrivateFieldSet(this, _TokenBalancesController_defaultInterval, interval, "f");
|
|
287
356
|
__classPrivateFieldSet(this, _TokenBalancesController_websocketActivePollingInterval, websocketActivePollingInterval, "f");
|
|
288
357
|
__classPrivateFieldSet(this, _TokenBalancesController_chainPollingConfig, { ...chainPollingIntervals }, "f");
|
|
358
|
+
// Strategy order: API first, then RPC fallback
|
|
289
359
|
__classPrivateFieldSet(this, _TokenBalancesController_balanceFetchers, [
|
|
290
360
|
...(accountsApiChainIds().length > 0 && allowExternalServices()
|
|
291
361
|
? [__classPrivateFieldGet(this, _TokenBalancesController_createAccountsApiFetcher, "f").call(this)]
|
|
@@ -296,180 +366,302 @@ export class TokenBalancesController extends StaticIntervalPollingController() {
|
|
|
296
366
|
})),
|
|
297
367
|
], "f");
|
|
298
368
|
this.setIntervalLength(interval);
|
|
369
|
+
// initial token state & subscriptions
|
|
299
370
|
const { allTokens, allDetectedTokens, allIgnoredTokens } = this.messenger.call('TokensController:getState');
|
|
300
371
|
__classPrivateFieldSet(this, _TokenBalancesController_allTokens, allTokens, "f");
|
|
301
372
|
__classPrivateFieldSet(this, _TokenBalancesController_detectedTokens, allDetectedTokens, "f");
|
|
302
373
|
__classPrivateFieldSet(this, _TokenBalancesController_allIgnoredTokens, allIgnoredTokens, "f");
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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));
|
|
307
389
|
}
|
|
308
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
309
|
-
// Address + network helpers
|
|
310
390
|
/**
|
|
311
|
-
*
|
|
312
|
-
* When locked, balance updates should be skipped.
|
|
391
|
+
* Override to support per-chain polling intervals by grouping chains by interval
|
|
313
392
|
*
|
|
314
|
-
* @
|
|
393
|
+
* @param options0 - The polling options
|
|
394
|
+
* @param options0.chainIds - Chain IDs to start polling for
|
|
315
395
|
*/
|
|
316
|
-
get isActive() {
|
|
317
|
-
return __classPrivateFieldGet(this, _TokenBalancesController_isUnlocked, "f");
|
|
318
|
-
}
|
|
319
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
320
|
-
// Polling overrides
|
|
321
396
|
_startPolling({ chainIds }) {
|
|
397
|
+
// Store the original chainIds to preserve intent across config updates
|
|
322
398
|
__classPrivateFieldSet(this, _TokenBalancesController_requestedChainIds, [...chainIds], "f");
|
|
323
399
|
__classPrivateFieldSet(this, _TokenBalancesController_isControllerPollingActive, true, "f");
|
|
324
400
|
__classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_startIntervalGroupPolling).call(this, chainIds, true);
|
|
325
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* Override to handle our custom polling approach
|
|
404
|
+
*
|
|
405
|
+
* @param tokenSetId - The token set ID to stop polling for
|
|
406
|
+
*/
|
|
326
407
|
_stopPollingByPollingTokenSetId(tokenSetId) {
|
|
408
|
+
let parsedTokenSetId;
|
|
327
409
|
let chainsToStop = [];
|
|
328
410
|
try {
|
|
329
|
-
|
|
330
|
-
chainsToStop = parsedTokenSetId.chainIds
|
|
411
|
+
parsedTokenSetId = JSON.parse(tokenSetId);
|
|
412
|
+
chainsToStop = parsedTokenSetId.chainIds || [];
|
|
331
413
|
}
|
|
332
414
|
catch (error) {
|
|
333
415
|
console.warn('Failed to parse tokenSetId, stopping all polling:', error);
|
|
334
|
-
|
|
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();
|
|
335
421
|
return;
|
|
336
422
|
}
|
|
423
|
+
// Compare with current chains - only stop if it matches our current session
|
|
337
424
|
const currentChainsSet = new Set(__classPrivateFieldGet(this, _TokenBalancesController_requestedChainIds, "f"));
|
|
338
425
|
const stopChainsSet = new Set(chainsToStop);
|
|
426
|
+
// Check if this stop request is for our current session
|
|
339
427
|
const isCurrentSession = currentChainsSet.size === stopChainsSet.size &&
|
|
340
428
|
[...currentChainsSet].every((chain) => stopChainsSet.has(chain));
|
|
341
429
|
if (isCurrentSession) {
|
|
342
|
-
|
|
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();
|
|
343
434
|
}
|
|
344
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
|
+
*/
|
|
345
442
|
getChainPollingConfig(chainId) {
|
|
346
443
|
return (__classPrivateFieldGet(this, _TokenBalancesController_chainPollingConfig, "f")[chainId] ?? {
|
|
347
444
|
interval: __classPrivateFieldGet(this, _TokenBalancesController_defaultInterval, "f"),
|
|
348
445
|
});
|
|
349
446
|
}
|
|
350
447
|
async _executePoll({ chainIds, queryAllAccounts = false, }) {
|
|
448
|
+
// This won't be called with our custom implementation, but keep for compatibility
|
|
351
449
|
await this.updateBalances({ chainIds, queryAllAccounts });
|
|
352
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
|
+
*/
|
|
353
458
|
updateChainPollingConfigs(configs, options = { immediateUpdate: true }) {
|
|
354
459
|
Object.assign(__classPrivateFieldGet(this, _TokenBalancesController_chainPollingConfig, "f"), configs);
|
|
460
|
+
// If polling is currently active, restart with new interval groupings
|
|
355
461
|
if (__classPrivateFieldGet(this, _TokenBalancesController_isControllerPollingActive, "f")) {
|
|
462
|
+
// Restart polling with immediate fetch by default, unless explicitly disabled
|
|
356
463
|
__classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_startIntervalGroupPolling).call(this, __classPrivateFieldGet(this, _TokenBalancesController_requestedChainIds, "f"), options.immediateUpdate);
|
|
357
464
|
}
|
|
358
465
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
async updateBalances({ chainIds, tokenAddresses, queryAllAccounts = false, } = {}) {
|
|
362
|
-
if (!this.isActive) {
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
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);
|
|
366
468
|
if (!targetChains.length) {
|
|
367
469
|
return;
|
|
368
470
|
}
|
|
369
|
-
const {
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const
|
|
378
|
-
|
|
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];
|
|
379
520
|
const prev = this.state;
|
|
380
|
-
const next =
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
+
});
|
|
385
568
|
});
|
|
386
569
|
if (!isEqual(prev, next)) {
|
|
387
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
|
|
388
573
|
const accountTrackerState = this.messenger.call('AccountTrackerController:getState');
|
|
389
|
-
|
|
390
|
-
if (
|
|
391
|
-
|
|
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
|
+
}
|
|
392
590
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
+
}
|
|
396
616
|
}
|
|
397
617
|
}
|
|
398
|
-
await __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_importUntrackedTokens).call(this, filteredAggregated);
|
|
399
618
|
}
|
|
400
619
|
resetState() {
|
|
401
620
|
this.update(() => ({ tokenBalances: {} }));
|
|
402
621
|
}
|
|
403
|
-
|
|
404
|
-
|
|
622
|
+
/**
|
|
623
|
+
* Clean up all timers and resources when controller is destroyed
|
|
624
|
+
*/
|
|
405
625
|
destroy() {
|
|
406
626
|
__classPrivateFieldSet(this, _TokenBalancesController_isControllerPollingActive, false, "f");
|
|
407
627
|
__classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").forEach((timer) => clearInterval(timer));
|
|
408
628
|
__classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").clear();
|
|
629
|
+
// Clean up debouncing timer
|
|
409
630
|
if (__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer) {
|
|
410
631
|
clearTimeout(__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer);
|
|
411
632
|
__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer = null;
|
|
412
633
|
}
|
|
634
|
+
// Unregister action handlers
|
|
413
635
|
this.messenger.unregisterActionHandler(`TokenBalancesController:updateChainPollingConfigs`);
|
|
414
636
|
this.messenger.unregisterActionHandler(`TokenBalancesController:getChainPollingConfig`);
|
|
415
637
|
super.destroy();
|
|
416
638
|
}
|
|
417
639
|
}
|
|
418
|
-
_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(),
|
|
419
|
-
this.messenger.subscribe('TokensController:stateChange', (tokensState) => {
|
|
420
|
-
__classPrivateFieldGet(this, _TokenBalancesController_onTokensChanged, "f").call(this, tokensState).catch((error) => {
|
|
421
|
-
console.warn('Error handling token state change:', error);
|
|
422
|
-
});
|
|
423
|
-
});
|
|
424
|
-
this.messenger.subscribe('NetworkController:stateChange', __classPrivateFieldGet(this, _TokenBalancesController_onNetworkChanged, "f"));
|
|
425
|
-
this.messenger.subscribe('KeyringController:unlock', () => {
|
|
426
|
-
__classPrivateFieldSet(this, _TokenBalancesController_isUnlocked, true, "f");
|
|
427
|
-
});
|
|
428
|
-
this.messenger.subscribe('KeyringController:lock', () => {
|
|
429
|
-
__classPrivateFieldSet(this, _TokenBalancesController_isUnlocked, false, "f");
|
|
430
|
-
});
|
|
431
|
-
this.messenger.subscribe('KeyringController:accountRemoved', __classPrivateFieldGet(this, _TokenBalancesController_onAccountRemoved, "f"));
|
|
432
|
-
this.messenger.subscribe('AccountsController:selectedEvmAccountChange', __classPrivateFieldGet(this, _TokenBalancesController_onAccountChanged, "f"));
|
|
433
|
-
this.messenger.subscribe('AccountActivityService:balanceUpdated', (event) => {
|
|
434
|
-
__classPrivateFieldGet(this, _TokenBalancesController_onAccountActivityBalanceUpdate, "f").call(this, event).catch((error) => {
|
|
435
|
-
console.warn('Error handling balance update:', error);
|
|
436
|
-
});
|
|
437
|
-
});
|
|
438
|
-
this.messenger.subscribe('AccountActivityService:statusChanged', __classPrivateFieldGet(this, _TokenBalancesController_onAccountActivityStatusChanged, "f").bind(this));
|
|
439
|
-
this.messenger.subscribe('TransactionController:transactionConfirmed', (transactionMeta) => {
|
|
440
|
-
this.updateBalances({
|
|
441
|
-
chainIds: [transactionMeta.chainId],
|
|
442
|
-
}).catch(() => {
|
|
443
|
-
// Silently handle balance update errors
|
|
444
|
-
});
|
|
445
|
-
});
|
|
446
|
-
this.messenger.subscribe('TransactionController:incomingTransactionsReceived', (incomingTransactions) => {
|
|
447
|
-
this.updateBalances({
|
|
448
|
-
chainIds: incomingTransactions.map((tx) => tx.chainId),
|
|
449
|
-
}).catch(() => {
|
|
450
|
-
// Silently handle balance update errors
|
|
451
|
-
});
|
|
452
|
-
});
|
|
453
|
-
}, _TokenBalancesController_registerActions = function _TokenBalancesController_registerActions() {
|
|
454
|
-
this.messenger.registerActionHandler(`TokenBalancesController:updateChainPollingConfigs`, this.updateChainPollingConfigs.bind(this));
|
|
455
|
-
this.messenger.registerActionHandler(`TokenBalancesController:getChainPollingConfig`, this.getChainPollingConfig.bind(this));
|
|
456
|
-
}, _TokenBalancesController_normalizeAccountAddresses = function _TokenBalancesController_normalizeAccountAddresses() {
|
|
457
|
-
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() {
|
|
458
641
|
const currentState = this.state.tokenBalances;
|
|
459
642
|
const normalizedBalances = {};
|
|
643
|
+
// Iterate through all accounts and normalize to lowercase
|
|
460
644
|
for (const address of Object.keys(currentState)) {
|
|
461
645
|
const lowercaseAddress = address.toLowerCase();
|
|
462
646
|
const accountBalances = currentState[address];
|
|
463
647
|
if (!accountBalances) {
|
|
464
648
|
continue;
|
|
465
649
|
}
|
|
466
|
-
|
|
650
|
+
// If this lowercase address doesn't exist yet, create it
|
|
651
|
+
if (!normalizedBalances[lowercaseAddress]) {
|
|
652
|
+
normalizedBalances[lowercaseAddress] = {};
|
|
653
|
+
}
|
|
654
|
+
// Merge chain data
|
|
467
655
|
for (const chainId of Object.keys(accountBalances)) {
|
|
468
656
|
const chainIdKey = chainId;
|
|
469
|
-
(
|
|
657
|
+
if (!normalizedBalances[lowercaseAddress][chainIdKey]) {
|
|
658
|
+
normalizedBalances[lowercaseAddress][chainIdKey] = {};
|
|
659
|
+
}
|
|
660
|
+
// Merge token balances (later values override earlier ones if duplicates exist)
|
|
470
661
|
Object.assign(normalizedBalances[lowercaseAddress][chainIdKey], accountBalances[chainIdKey]);
|
|
471
662
|
}
|
|
472
663
|
}
|
|
664
|
+
// Only update if there were changes
|
|
473
665
|
if (Object.keys(currentState).length !==
|
|
474
666
|
Object.keys(normalizedBalances).length ||
|
|
475
667
|
Object.keys(currentState).some((addr) => addr !== addr.toLowerCase())) {
|
|
@@ -483,15 +675,18 @@ _TokenBalancesController_platform = new WeakMap(), _TokenBalancesController_quer
|
|
|
483
675
|
]),
|
|
484
676
|
];
|
|
485
677
|
}, _TokenBalancesController_startIntervalGroupPolling = function _TokenBalancesController_startIntervalGroupPolling(chainIds, immediate = true) {
|
|
678
|
+
// Stop any existing interval timers
|
|
486
679
|
__classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").forEach((timer) => clearInterval(timer));
|
|
487
680
|
__classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").clear();
|
|
681
|
+
// Group chains by their polling intervals
|
|
488
682
|
const intervalGroups = new Map();
|
|
489
683
|
for (const chainId of chainIds) {
|
|
490
684
|
const config = this.getChainPollingConfig(chainId);
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
intervalGroups.set(config.interval,
|
|
685
|
+
const existing = intervalGroups.get(config.interval) || [];
|
|
686
|
+
existing.push(chainId);
|
|
687
|
+
intervalGroups.set(config.interval, existing);
|
|
494
688
|
}
|
|
689
|
+
// Start separate polling loop for each interval group
|
|
495
690
|
for (const [interval, chainIdsGroup] of intervalGroups) {
|
|
496
691
|
__classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_startPollingForInterval).call(this, interval, chainIdsGroup, immediate);
|
|
497
692
|
}
|
|
@@ -507,205 +702,33 @@ _TokenBalancesController_platform = new WeakMap(), _TokenBalancesController_quer
|
|
|
507
702
|
console.warn(`Polling failed for chains ${chainIds.join(', ')} with interval ${interval}:`, error);
|
|
508
703
|
}
|
|
509
704
|
};
|
|
705
|
+
// Poll immediately first if requested
|
|
510
706
|
if (immediate) {
|
|
511
707
|
pollFunction().catch((error) => {
|
|
512
708
|
console.warn(`Immediate polling failed for chains ${chainIds.join(', ')}:`, error);
|
|
513
709
|
});
|
|
514
710
|
}
|
|
711
|
+
// Then start regular interval polling
|
|
515
712
|
__classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_setPollingTimer).call(this, interval, chainIds, pollFunction);
|
|
516
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
|
+
}
|
|
517
719
|
const timer = setInterval(() => {
|
|
518
720
|
pollFunction().catch((error) => {
|
|
519
721
|
console.warn(`Interval polling failed for chains ${chainIds.join(', ')}:`, error);
|
|
520
722
|
});
|
|
521
723
|
}, interval);
|
|
522
724
|
__classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").set(interval, timer);
|
|
523
|
-
}, _TokenBalancesController_stopAllPolling = function _TokenBalancesController_stopAllPolling() {
|
|
524
|
-
__classPrivateFieldSet(this, _TokenBalancesController_isControllerPollingActive, false, "f");
|
|
525
|
-
__classPrivateFieldSet(this, _TokenBalancesController_requestedChainIds, [], "f");
|
|
526
|
-
__classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").forEach((timer) => clearInterval(timer));
|
|
527
|
-
__classPrivateFieldGet(this, _TokenBalancesController_intervalPollingTimers, "f").clear();
|
|
528
|
-
}, _TokenBalancesController_getTargetChains = function _TokenBalancesController_getTargetChains(chainIds) {
|
|
529
|
-
return chainIds?.length ? chainIds : __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_chainIdsWithTokens).call(this);
|
|
530
|
-
}, _TokenBalancesController_getAccountsAndJwt = async function _TokenBalancesController_getAccountsAndJwt() {
|
|
531
|
-
const { address: selected } = this.messenger.call('AccountsController:getSelectedAccount');
|
|
532
|
-
const allAccounts = this.messenger.call('AccountsController:listAccounts');
|
|
533
|
-
const jwtToken = await safelyExecuteWithTimeout(() => {
|
|
534
|
-
return this.messenger.call('AuthenticationController:getBearerToken');
|
|
535
|
-
}, false, 5000);
|
|
536
|
-
return {
|
|
537
|
-
selectedAccount: selected,
|
|
538
|
-
allAccounts,
|
|
539
|
-
jwtToken,
|
|
540
|
-
};
|
|
541
|
-
}, _TokenBalancesController_fetchAllBalances = async function _TokenBalancesController_fetchAllBalances({ targetChains, selectedAccount, allAccounts, jwtToken, queryAllAccounts, }) {
|
|
542
|
-
const aggregated = [];
|
|
543
|
-
let remainingChains = [...targetChains];
|
|
544
|
-
for (const fetcher of __classPrivateFieldGet(this, _TokenBalancesController_balanceFetchers, "f")) {
|
|
545
|
-
const supportedChains = remainingChains.filter((chain) => fetcher.supports(chain));
|
|
546
|
-
if (!supportedChains.length) {
|
|
547
|
-
continue;
|
|
548
|
-
}
|
|
549
|
-
try {
|
|
550
|
-
const result = await fetcher.fetch({
|
|
551
|
-
chainIds: supportedChains,
|
|
552
|
-
queryAllAccounts,
|
|
553
|
-
selectedAccount,
|
|
554
|
-
allAccounts,
|
|
555
|
-
jwtToken,
|
|
556
|
-
});
|
|
557
|
-
if (result.balances?.length) {
|
|
558
|
-
aggregated.push(...result.balances);
|
|
559
|
-
const processed = new Set(result.balances.map((b) => b.chainId));
|
|
560
|
-
remainingChains = remainingChains.filter((chain) => !processed.has(chain));
|
|
561
|
-
}
|
|
562
|
-
if (result.unprocessedChainIds?.length) {
|
|
563
|
-
const currentRemaining = [...remainingChains];
|
|
564
|
-
const chainsToAdd = result.unprocessedChainIds.filter((chainId) => supportedChains.includes(chainId) &&
|
|
565
|
-
!currentRemaining.includes(chainId));
|
|
566
|
-
remainingChains.push(...chainsToAdd);
|
|
567
|
-
this.messenger
|
|
568
|
-
.call('TokenDetectionController:detectTokens', {
|
|
569
|
-
chainIds: result.unprocessedChainIds,
|
|
570
|
-
forceRpc: true,
|
|
571
|
-
})
|
|
572
|
-
.catch(() => {
|
|
573
|
-
// Silently handle token detection errors
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
catch (error) {
|
|
578
|
-
console.warn(`Balance fetcher failed for chains ${supportedChains.join(', ')}: ${String(error)}`);
|
|
579
|
-
this.messenger
|
|
580
|
-
.call('TokenDetectionController:detectTokens', {
|
|
581
|
-
chainIds: supportedChains,
|
|
582
|
-
forceRpc: true,
|
|
583
|
-
})
|
|
584
|
-
.catch(() => {
|
|
585
|
-
// Silently handle token detection errors
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
if (!remainingChains.length) {
|
|
589
|
-
break;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
return aggregated;
|
|
593
|
-
}, _TokenBalancesController_filterByTokenAddresses = function _TokenBalancesController_filterByTokenAddresses(balances, tokenAddresses) {
|
|
594
|
-
if (!tokenAddresses?.length) {
|
|
595
|
-
return balances;
|
|
596
|
-
}
|
|
597
|
-
const lowered = tokenAddresses.map((a) => a.toLowerCase());
|
|
598
|
-
return balances.filter((balance) => lowered.includes(balance.token.toLowerCase()));
|
|
599
|
-
}, _TokenBalancesController_getAccountsToProcess = function _TokenBalancesController_getAccountsToProcess(queryAllAccountsParam, allAccounts, selectedAccount) {
|
|
600
|
-
const effectiveQueryAll = queryAllAccountsParam ?? __classPrivateFieldGet(this, _TokenBalancesController_queryAllAccounts, "f") ?? false;
|
|
601
|
-
if (!effectiveQueryAll) {
|
|
602
|
-
return [selectedAccount];
|
|
603
|
-
}
|
|
604
|
-
return allAccounts.map((account) => account.address);
|
|
605
|
-
}, _TokenBalancesController_applyTokenBalancesToState = function _TokenBalancesController_applyTokenBalancesToState({ prev, targetChains, accountsToProcess, balances, }) {
|
|
606
|
-
return draft(prev, (draftState) => {
|
|
607
|
-
var _a, _b;
|
|
608
|
-
for (const chainId of targetChains) {
|
|
609
|
-
for (const account of accountsToProcess) {
|
|
610
|
-
(_a = draftState.tokenBalances)[account] ?? (_a[account] = {});
|
|
611
|
-
(_b = draftState.tokenBalances[account])[chainId] ?? (_b[chainId] = {});
|
|
612
|
-
const chainTokens = __classPrivateFieldGet(this, _TokenBalancesController_allTokens, "f")[chainId];
|
|
613
|
-
if (chainTokens?.[account]) {
|
|
614
|
-
Object.values(chainTokens[account]).forEach((token) => {
|
|
615
|
-
var _a;
|
|
616
|
-
const tokenAddress = checksum(token.address);
|
|
617
|
-
(_a = draftState.tokenBalances[account][chainId])[tokenAddress] ?? (_a[tokenAddress] = '0x0');
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
const detectedChainTokens = __classPrivateFieldGet(this, _TokenBalancesController_detectedTokens, "f")[chainId];
|
|
621
|
-
if (detectedChainTokens?.[account]) {
|
|
622
|
-
Object.values(detectedChainTokens[account]).forEach((token) => {
|
|
623
|
-
var _a;
|
|
624
|
-
const tokenAddress = checksum(token.address);
|
|
625
|
-
(_a = draftState.tokenBalances[account][chainId])[tokenAddress] ?? (_a[tokenAddress] = '0x0');
|
|
626
|
-
});
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
balances.forEach(({ success, value, account, token, chainId }) => {
|
|
631
|
-
var _a, _b;
|
|
632
|
-
if (!success || value === undefined) {
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
const lowerCaseAccount = account.toLowerCase();
|
|
636
|
-
const newBalance = toHex(value);
|
|
637
|
-
const tokenAddress = checksum(token);
|
|
638
|
-
const currentBalance = draftState.tokenBalances[lowerCaseAccount]?.[chainId]?.[tokenAddress];
|
|
639
|
-
if (currentBalance !== newBalance) {
|
|
640
|
-
((_b = ((_a = draftState.tokenBalances)[lowerCaseAccount] ?? (_a[lowerCaseAccount] = {})))[chainId] ?? (_b[chainId] = {}))[tokenAddress] = newBalance;
|
|
641
|
-
}
|
|
642
|
-
});
|
|
643
|
-
});
|
|
644
|
-
}, _TokenBalancesController_buildNativeBalanceUpdates = function _TokenBalancesController_buildNativeBalanceUpdates(balances, accountTrackerState) {
|
|
645
|
-
const nativeBalances = balances.filter((balance) => balance.success && balance.token === ZERO_ADDRESS);
|
|
646
|
-
if (!nativeBalances.length) {
|
|
647
|
-
return [];
|
|
648
|
-
}
|
|
649
|
-
return nativeBalances
|
|
650
|
-
.map((balance) => ({
|
|
651
|
-
address: balance.account,
|
|
652
|
-
chainId: balance.chainId,
|
|
653
|
-
balance: balance.value ? BNToHex(balance.value) : '0x0',
|
|
654
|
-
}))
|
|
655
|
-
.filter((update) => {
|
|
656
|
-
const currentBalance = accountTrackerState.accountsByChainId[update.chainId]?.[checksum(update.address)]?.balance;
|
|
657
|
-
return currentBalance !== update.balance;
|
|
658
|
-
});
|
|
659
|
-
}, _TokenBalancesController_buildStakedBalanceUpdates = function _TokenBalancesController_buildStakedBalanceUpdates(balances, accountTrackerState) {
|
|
660
|
-
const stakedBalances = balances.filter((balance) => {
|
|
661
|
-
if (!balance.success || balance.token === ZERO_ADDRESS) {
|
|
662
|
-
return false;
|
|
663
|
-
}
|
|
664
|
-
const stakingContractAddress = STAKING_CONTRACT_ADDRESS_BY_CHAINID[balance.chainId];
|
|
665
|
-
return (stakingContractAddress &&
|
|
666
|
-
stakingContractAddress.toLowerCase() === balance.token.toLowerCase());
|
|
667
|
-
});
|
|
668
|
-
if (!stakedBalances.length) {
|
|
669
|
-
return [];
|
|
670
|
-
}
|
|
671
|
-
return stakedBalances
|
|
672
|
-
.map((balance) => ({
|
|
673
|
-
address: balance.account,
|
|
674
|
-
chainId: balance.chainId,
|
|
675
|
-
stakedBalance: balance.value ? toHex(balance.value) : '0x0',
|
|
676
|
-
}))
|
|
677
|
-
.filter((update) => {
|
|
678
|
-
const currentStakedBalance = accountTrackerState.accountsByChainId[update.chainId]?.[checksum(update.address)]?.stakedBalance;
|
|
679
|
-
return currentStakedBalance !== update.stakedBalance;
|
|
680
|
-
});
|
|
681
|
-
}, _TokenBalancesController_importUntrackedTokens = async function _TokenBalancesController_importUntrackedTokens(balances) {
|
|
682
|
-
const untrackedTokensByChain = new Map();
|
|
683
|
-
for (const balance of balances) {
|
|
684
|
-
if (!balance.success || balance.token === ZERO_ADDRESS) {
|
|
685
|
-
continue;
|
|
686
|
-
}
|
|
687
|
-
const tokenAddress = checksum(balance.token);
|
|
688
|
-
const account = balance.account.toLowerCase();
|
|
689
|
-
if (!__classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_isTokenTracked).call(this, tokenAddress, account, balance.chainId)) {
|
|
690
|
-
const existing = untrackedTokensByChain.get(balance.chainId) ?? [];
|
|
691
|
-
if (!existing.includes(tokenAddress)) {
|
|
692
|
-
existing.push(tokenAddress);
|
|
693
|
-
untrackedTokensByChain.set(balance.chainId, existing);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
for (const [chainId, tokens] of untrackedTokensByChain) {
|
|
698
|
-
await this.messenger.call('TokenDetectionController:addDetectedTokensViaWs', {
|
|
699
|
-
tokensSlice: tokens,
|
|
700
|
-
chainId,
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
725
|
}, _TokenBalancesController_isTokenTracked = function _TokenBalancesController_isTokenTracked(tokenAddress, account, chainId) {
|
|
704
|
-
|
|
705
|
-
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)) {
|
|
706
728
|
return true;
|
|
707
729
|
}
|
|
708
|
-
|
|
730
|
+
// Check if token exists in allIgnoredTokens
|
|
731
|
+
if (__classPrivateFieldGet(this, _TokenBalancesController_allIgnoredTokens, "f")?.[chainId]?.[account.toLowerCase()]?.some((token) => token === tokenAddress)) {
|
|
709
732
|
return true;
|
|
710
733
|
}
|
|
711
734
|
return false;
|
|
@@ -715,25 +738,31 @@ _TokenBalancesController_platform = new WeakMap(), _TokenBalancesController_quer
|
|
|
715
738
|
const nativeBalanceUpdates = [];
|
|
716
739
|
for (const update of updates) {
|
|
717
740
|
const { asset, postBalance } = update;
|
|
741
|
+
// Throw if balance update has an error
|
|
718
742
|
if (postBalance.error) {
|
|
719
743
|
throw new Error('Balance update has error');
|
|
720
744
|
}
|
|
745
|
+
// Parse token address from asset type
|
|
721
746
|
const parsed = parseAssetType(asset.type);
|
|
722
747
|
if (!parsed) {
|
|
723
748
|
throw new Error('Failed to parse asset type');
|
|
724
749
|
}
|
|
725
750
|
const [tokenAddress, isNativeToken] = parsed;
|
|
751
|
+
// Validate token address
|
|
726
752
|
if (!isStrictHexString(tokenAddress) ||
|
|
727
753
|
!isValidHexAddress(tokenAddress)) {
|
|
728
754
|
throw new Error('Invalid token address');
|
|
729
755
|
}
|
|
730
756
|
const checksumTokenAddress = checksum(tokenAddress);
|
|
731
757
|
const isTracked = __classPrivateFieldGet(this, _TokenBalancesController_instances, "m", _TokenBalancesController_isTokenTracked).call(this, checksumTokenAddress, account, chainId);
|
|
758
|
+
// postBalance.amount is in hex format (raw units)
|
|
732
759
|
const balanceHex = postBalance.amount;
|
|
760
|
+
// Add token balance (tracked tokens, ignored tokens, and native tokens all get balance updates)
|
|
733
761
|
tokenBalances.push({
|
|
734
762
|
tokenAddress: checksumTokenAddress,
|
|
735
763
|
balance: balanceHex,
|
|
736
764
|
});
|
|
765
|
+
// Add native balance update if this is a native token
|
|
737
766
|
if (isNativeToken) {
|
|
738
767
|
nativeBalanceUpdates.push({
|
|
739
768
|
address: account,
|
|
@@ -741,6 +770,7 @@ _TokenBalancesController_platform = new WeakMap(), _TokenBalancesController_quer
|
|
|
741
770
|
balance: balanceHex,
|
|
742
771
|
});
|
|
743
772
|
}
|
|
773
|
+
// Handle untracked ERC20 tokens - queue for import
|
|
744
774
|
if (!isNativeToken && !isTracked) {
|
|
745
775
|
newTokens.push(checksumTokenAddress);
|
|
746
776
|
}
|
|
@@ -750,18 +780,28 @@ _TokenBalancesController_platform = new WeakMap(), _TokenBalancesController_quer
|
|
|
750
780
|
const changes = Array.from(__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").pendingChanges.entries());
|
|
751
781
|
__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").pendingChanges.clear();
|
|
752
782
|
__classPrivateFieldGet(this, _TokenBalancesController_statusChangeDebouncer, "f").timer = null;
|
|
753
|
-
if (
|
|
783
|
+
if (changes.length === 0) {
|
|
754
784
|
return;
|
|
755
785
|
}
|
|
786
|
+
// Calculate final polling configurations
|
|
756
787
|
const chainConfigs = {};
|
|
757
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
|
|
758
791
|
const hexChainId = caipChainIdToHex(chainId);
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
+
}
|
|
763
802
|
}
|
|
764
|
-
|
|
803
|
+
// Add jitter to prevent synchronized requests across instances
|
|
804
|
+
const jitterDelay = Math.random() * __classPrivateFieldGet(this, _TokenBalancesController_defaultInterval, "f"); // 0 to default interval
|
|
765
805
|
setTimeout(() => {
|
|
766
806
|
this.updateChainPollingConfigs(chainConfigs, { immediateUpdate: true });
|
|
767
807
|
}, jitterDelay);
|