Skip to content
Draft
Changes from 1 commit
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e51cfbe
feat: improve token detection
salimtb Dec 5, 2025
13b9baa
fix: clean token balances
salimtb Dec 5, 2025
72d64e7
Merge branch 'main' into feat/improve-token-detection
salimtb Dec 8, 2025
04bee5e
fix: clean up
salimtb Dec 8, 2025
772e48d
fix: fix unit tests
salimtb Dec 8, 2025
d72ecd2
Fix: clean up
salimtb Dec 8, 2025
906f515
fix: fix tests
salimtb Dec 8, 2025
a99fabb
fix: clean up
salimtb Dec 8, 2025
7cb1afd
fix: clean up
salimtb Dec 8, 2025
1a88361
fix: refacto
salimtb Dec 8, 2025
e6cd79b
fix: fix changelog
salimtb Dec 9, 2025
3f54dda
fix: fix linter
salimtb Dec 9, 2025
94e214c
fix: add unlock logic to balance controllers
salimtb Dec 9, 2025
9538897
Merge branch 'main' into feat/improve-token-detection
salimtb Dec 9, 2025
781b5c9
fix: fix linter
salimtb Dec 9, 2025
8d92115
fix: fix unit tests
salimtb Dec 9, 2025
bb75ce3
fix: fix lintet
salimtb Dec 9, 2025
48caea1
Merge branch 'main' into feat/improve-token-detection
salimtb Dec 9, 2025
c92c27f
fix: fix lint errors
salimtb Dec 9, 2025
ff51333
fix: export action
salimtb Dec 10, 2025
267e79c
fix: fix linter
salimtb Dec 10, 2025
8eaef62
fix: clean up comments
salimtb Dec 10, 2025
d84f849
fix: clean up
salimtb Dec 11, 2025
1c55e69
fix: fix PR comments
salimtb Dec 11, 2025
2d867cd
Merge branch 'main' into feat/improve-token-detection
salimtb Dec 11, 2025
4b18143
fix: fix tests
salimtb Dec 11, 2025
10463da
fix: websocket token detection should match the use token detection t…
salimtb Dec 11, 2025
cb52272
Merge branch 'main' into fix/improve-core-logic
salimtb Dec 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix: clean token balances
  • Loading branch information
salimtb committed Dec 8, 2025
commit 13b9baaaf2d5742f5e91bbe73315f8c782f98c3e
123 changes: 103 additions & 20 deletions packages/assets-controllers/src/TokenBalancesController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import type {
PreferencesControllerStateChangeEvent,
} from '@metamask/preferences-controller';
import type { AuthenticationController } from '@metamask/profile-sync-controller';
import type { TransactionControllerIncomingTransactionsReceivedEvent, TransactionControllerTransactionConfirmedEvent } from '@metamask/transaction-controller';
import type { Hex } from '@metamask/utils';
import {
isCaipAssetType,
Expand Down Expand Up @@ -143,7 +144,9 @@ export type AllowedEvents =
| KeyringControllerAccountRemovedEvent
| AccountActivityServiceBalanceUpdatedEvent
| AccountActivityServiceStatusChangedEvent
| AccountsControllerSelectedEvmAccountChangeEvent;
| AccountsControllerSelectedEvmAccountChangeEvent
| TransactionControllerTransactionConfirmedEvent
| TransactionControllerIncomingTransactionsReceivedEvent;

export type TokenBalancesControllerMessenger = Messenger<
typeof CONTROLLER,
Expand Down Expand Up @@ -381,6 +384,36 @@ export class TokenBalancesController extends StaticIntervalPollingController<{
'AccountActivityService:statusChanged',
this.#onAccountActivityStatusChanged.bind(this),
);

this.messenger.subscribe(
'TransactionController:transactionConfirmed',
(transactionMeta) => {
console.log(
'Transaction confirmed: ++++++++++++++++++',
transactionMeta,
);
this.updateBalances({
chainIds: [transactionMeta.chainId],
}).catch(() => {
// Silently handle balance update errors
});
},
);

this.messenger.subscribe(
'TransactionController:incomingTransactionsReceived',
(transactionMeta) => {
console.log(
'Incoming transaction block received: ++++++++++++++++++',
transactionMeta,
);
// this.updateBalances({
// chainIds: [transactionMeta.chainId],
// }).catch(() => {
// // Silently handle balance update errors
// });
},
);
}

/**
Expand Down Expand Up @@ -688,8 +721,13 @@ export class TokenBalancesController extends StaticIntervalPollingController<{

async updateBalances({
chainIds,
tokenAddresses,
queryAllAccounts = false,
}: { chainIds?: ChainIdHex[]; queryAllAccounts?: boolean } = {}) {
}: {
chainIds?: ChainIdHex[];
tokenAddresses?: string[];
queryAllAccounts?: boolean;
} = {}) {
const targetChains = chainIds ?? this.#chainIdsWithTokens();
if (!targetChains.length) {
return;
Expand Down Expand Up @@ -766,6 +804,15 @@ export class TokenBalancesController extends StaticIntervalPollingController<{
}
}

// Filter aggregated results by tokenAddresses if provided
const filteredAggregated = tokenAddresses?.length
? aggregated.filter((balance) =>
tokenAddresses.some(
(addr) => addr.toLowerCase() === balance.token.toLowerCase(),
),
)
: aggregated;

// Determine which accounts to process based on queryAllAccounts parameter
const accountsToProcess =
(queryAllAccounts ?? this.#queryAllAccounts)
Expand Down Expand Up @@ -811,29 +858,31 @@ export class TokenBalancesController extends StaticIntervalPollingController<{
}

// Update with actual fetched balances only if the value has changed
aggregated.forEach(({ success, value, account, token, chainId }) => {
if (success && value !== undefined) {
// Ensure all accounts we add/update are in lower-case
const lowerCaseAccount = account.toLowerCase() as ChecksumAddress;
const newBalance = toHex(value);
const tokenAddress = checksum(token);
const currentBalance =
d.tokenBalances[lowerCaseAccount]?.[chainId]?.[tokenAddress];

// Only update if the balance has actually changed
if (currentBalance !== newBalance) {
((d.tokenBalances[lowerCaseAccount] ??= {})[chainId] ??= {})[
tokenAddress
] = newBalance;
filteredAggregated.forEach(
({ success, value, account, token, chainId }) => {
if (success && value !== undefined) {
// Ensure all accounts we add/update are in lower-case
const lowerCaseAccount = account.toLowerCase() as ChecksumAddress;
const newBalance = toHex(value);
const tokenAddress = checksum(token);
const currentBalance =
d.tokenBalances[lowerCaseAccount]?.[chainId]?.[tokenAddress];

// Only update if the balance has actually changed
if (currentBalance !== newBalance) {
((d.tokenBalances[lowerCaseAccount] ??= {})[chainId] ??= {})[
tokenAddress
] = newBalance;
}
}
}
});
},
);
});

if (!isEqual(prev, next)) {
this.update(() => next);

const nativeBalances = aggregated.filter(
const nativeBalances = filteredAggregated.filter(
(r) => r.success && r.token === ZERO_ADDRESS,
);

Expand Down Expand Up @@ -868,7 +917,7 @@ export class TokenBalancesController extends StaticIntervalPollingController<{
}

// Filter and update staked balances in a single batch operation for better performance
const stakedBalances = aggregated.filter((r) => {
const stakedBalances = filteredAggregated.filter((r) => {
if (!r.success || r.token === ZERO_ADDRESS) {
return false;
}
Expand Down Expand Up @@ -906,6 +955,40 @@ export class TokenBalancesController extends StaticIntervalPollingController<{
}
}
}

// Check for untracked tokens and import them via TokenDetectionController
// Group by chainId for batch processing
const untrackedTokensByChain = new Map<ChainIdHex, string[]>();
for (const balance of filteredAggregated) {
if (!balance.success || balance.token === ZERO_ADDRESS) {
continue;
}

const tokenAddress = checksum(balance.token);
const account = balance.account.toLowerCase() as ChecksumAddress;

// Check if token is not tracked (not in allTokens or allIgnoredTokens)
if (!this.#isTokenTracked(tokenAddress, account, balance.chainId)) {
const existing = untrackedTokensByChain.get(balance.chainId) ?? [];
if (!existing.includes(tokenAddress)) {
existing.push(tokenAddress);
untrackedTokensByChain.set(balance.chainId, existing);
}
}
}

// Import untracked tokens for each chain
for (const [chainId, tokens] of untrackedTokensByChain) {
if (tokens.length > 0) {
await this.messenger.call(
'TokenDetectionController:addDetectedTokensViaWs',
{
tokensSlice: tokens,
chainId,
},
);
}
}
}

resetState() {
Expand Down