Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
6fbb0db
disable fetching
bergarces Oct 24, 2025
561d671
changelog
bergarces Oct 24, 2025
513b9f4
Merge branch 'main' into disable-fetching
bergarces Oct 24, 2025
16269c5
fix changelog
bergarces Oct 24, 2025
2f1d63a
missing things
bergarces Oct 24, 2025
c5ed43d
missing
bergarces Oct 24, 2025
5f5eac4
wrong file removed
bergarces Oct 24, 2025
e77db65
add event
bergarces Oct 25, 2025
ebf01f5
Merge branch 'main' into disable-fetching
bergarces Oct 25, 2025
7219a99
Merge branch 'main' into disable-fetching
bergarces Oct 27, 2025
f4fc838
changelog and allow syncing empty accounts before preventing fetching
bergarces Oct 27, 2025
46e7fff
Merge branch 'main' into disable-fetching
bergarces Oct 27, 2025
e9a7668
Merge branch 'main' into disable-fetching
bergarces Oct 27, 2025
8d1b9ed
Merge branch 'main' into disable-fetching
bergarces Oct 27, 2025
82946f2
Merge branch 'main' into disable-fetching
bergarces Oct 27, 2025
5bd049e
Merge branch 'main' into disable-fetching
bergarces Oct 27, 2025
05db5d7
fix changelog
bergarces Oct 27, 2025
59c7b13
fix test
bergarces Oct 27, 2025
d25d92e
missing tests
bergarces Oct 27, 2025
18b3603
fix changelog
bergarces Oct 27, 2025
05c9402
Merge branch 'main' into disable-fetching
bergarces Oct 27, 2025
421e829
refactor refreshAddresses to allow multiple
bergarces Oct 28, 2025
21c6dc6
Merge branch 'main' into disable-fetching
bergarces Oct 29, 2025
5703ebf
test fixes
bergarces Oct 29, 2025
0eed652
consoloe.log
bergarces Oct 29, 2025
e66e1d6
fix issue at fetcher level
bergarces Oct 29, 2025
d632f1d
revert
bergarces Oct 29, 2025
d8e6584
revert
bergarces Oct 29, 2025
6416b07
revert
bergarces Oct 29, 2025
887c9c3
fix test event
bergarces Oct 29, 2025
bdfc708
Merge branch 'main' into disable-fetching
bergarces Oct 30, 2025
fb233ab
fix changelog
bergarces Oct 30, 2025
e929311
make type compatible with mobile and platform
bergarces Oct 30, 2025
64d885f
Merge branch 'main' into disable-fetching
bergarces Oct 30, 2025
55dcb92
Merge branch 'main' into disable-fetching
bergarces Oct 30, 2025
878ed7e
Merge branch 'main' into disable-fetching
bergarces Oct 30, 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
10 changes: 10 additions & 0 deletions packages/assets-controllers/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- **BREAKING:** Modify AccountTrackerController events and actions ([#6938](https://github.com/MetaMask/core/pull/6938))
- `AccountsController:selectedAccountChange` is no longer part of the list of allowed events
- `NetworkController:getState` refetches balances
- `TransactionController:unapprovedTransactionAdded` refetches balances
- `TransactionController:unapprovedTransactionAdded'` refetches balances for the transaction from address and network
- `TransactionController:transactionConfirmed` refetches balances for the transaction from address and network
- Add optional `fetchingEnabled` callback to `AccountTrackerController` constructor to stop it from fetching balances ([#6938](https://github.com/MetaMask/core/pull/6938))

## [85.0.0]

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type MessengerEvents,
type MockAnyNamespace,
} from '@metamask/messenger';
import type { NetworkConfiguration } from '@metamask/network-controller';
import {
type NetworkClientId,
type NetworkClientConfiguration,
Expand Down Expand Up @@ -167,7 +168,7 @@ describe('AccountTrackerController', () => {
mockedGetTokenBalancesForMultipleAddresses.mockResolvedValueOnce({
tokenBalances: {
'0x0000000000000000000000000000000000000000': {
[CHECKSUM_ADDRESS_1]: new BN('123456', 16),
[ADDRESS_1]: new BN('123456', 16),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@salimtb I'd like your input here if possible.

From what I can see, if we use multicall (queryAllAccounts: true), the resulting addresses will be lowercase, but when we query a single address (queryAllAccounts: false) the result is a checksummed address.

Therefore, since I have changed the these two events to query multiple accounts (both the from and the to), I had to change this return value from the test.

Does that sound right?

@salimtb salimtb Oct 29, 2025 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this sounds right yes

},
},
stakedBalances: {},
Expand Down Expand Up @@ -208,7 +209,7 @@ describe('AccountTrackerController', () => {
mockedGetTokenBalancesForMultipleAddresses.mockResolvedValueOnce({
tokenBalances: {
'0x0000000000000000000000000000000000000000': {
[CHECKSUM_ADDRESS_1]: new BN('abcdef', 16),
[ADDRESS_1]: new BN('abcdef', 16),
},
},
stakedBalances: {},
Expand Down Expand Up @@ -239,7 +240,88 @@ describe('AccountTrackerController', () => {
);
});

it('refreshes addresses when network is added', async () => {
await withController(
{
selectedAccount: ACCOUNT_1,
listAccounts: [ACCOUNT_1],
},
async ({ controller, messenger }) => {
mockedGetTokenBalancesForMultipleAddresses.mockResolvedValueOnce({
tokenBalances: {
'0x0000000000000000000000000000000000000000': {
[CHECKSUM_ADDRESS_1]: new BN('abcdef', 16),
},
},
stakedBalances: {},
});

messenger.publish(
'NetworkController:networkAdded',
{} as NetworkConfiguration,
);

await clock.tickAsync(1);

expect(
controller.state.accountsByChainId['0x1'][CHECKSUM_ADDRESS_1].balance,
).toBe('0xabcdef');
},
);
});

it('refreshes addresses when keyring is unlocked', async () => {
await withController(
{
selectedAccount: ACCOUNT_1,
listAccounts: [ACCOUNT_1],
},
async ({ controller, messenger }) => {
mockedGetTokenBalancesForMultipleAddresses.mockResolvedValueOnce({
tokenBalances: {
'0x0000000000000000000000000000000000000000': {
[CHECKSUM_ADDRESS_1]: new BN('abcdef', 16),
},
},
stakedBalances: {},
});

messenger.publish('KeyringController:unlock');

await clock.tickAsync(1);

expect(
controller.state.accountsByChainId['0x1'][CHECKSUM_ADDRESS_1].balance,
).toBe('0xabcdef');
},
);
});
Comment thread
bergarces marked this conversation as resolved.
Comment thread
bergarces marked this conversation as resolved.

describe('refresh', () => {
it('does not refresh when fetching is disabled', async () => {
const expectedState = {
accountsByChainId: {
'0x1': {
[CHECKSUM_ADDRESS_1]: { balance: '0x0' },
[CHECKSUM_ADDRESS_2]: { balance: '0x0' },
},
},
};

await withController(
{
options: { fetchingEnabled: () => false },
selectedAccount: ACCOUNT_1,
listAccounts: [ACCOUNT_1, ACCOUNT_2],
},
async ({ controller, refresh }) => {
await refresh(clock, ['mainnet'], true);

expect(controller.state).toStrictEqual(expectedState);
},
);
});

describe('without networkClientId', () => {
it('should sync addresses', async () => {
await withController(
Expand Down Expand Up @@ -1613,6 +1695,8 @@ async function withController<ReturnValue>(
'AccountsController:selectedEvmAccountChange',
'TransactionController:unapprovedTransactionAdded',
'TransactionController:transactionConfirmed',
'NetworkController:networkAdded',
'KeyringController:unlock',
],
});

Expand Down
82 changes: 64 additions & 18 deletions packages/assets-controllers/src/AccountTrackerController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type {
AccountsControllerSelectedEvmAccountChangeEvent,
AccountsControllerGetSelectedAccountAction,
AccountsControllerListAccountsAction,
AccountsControllerSelectedAccountChangeEvent,
} from '@metamask/accounts-controller';
import type {
ControllerStateChangeEvent,
Expand All @@ -16,16 +15,17 @@ import {
toChecksumHexAddress,
} from '@metamask/controller-utils';
import EthQuery from '@metamask/eth-query';
import type { KeyringControllerUnlockEvent } from '@metamask/keyring-controller';
import type { InternalAccount } from '@metamask/keyring-internal-api';
import type { Messenger } from '@metamask/messenger';
import type {
NetworkClient,
NetworkClientId,
NetworkControllerGetNetworkClientByIdAction,
NetworkControllerGetStateAction,
NetworkControllerNetworkAddedEvent,
} from '@metamask/network-controller';
import { StaticIntervalPollingController } from '@metamask/polling-controller';
import type { PreferencesControllerGetStateAction } from '@metamask/preferences-controller';
import type {
TransactionControllerTransactionConfirmedEvent,
TransactionControllerUnapprovedTransactionAddedEvent,
Expand Down Expand Up @@ -174,7 +174,10 @@ export type AccountTrackerControllerActions =
*/
export type AllowedActions =
| AccountsControllerListAccountsAction
| PreferencesControllerGetStateAction
| {
type: 'PreferencesController:getState';
handler: () => { isMultiAccountBalancesEnabled: boolean };
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes type issues in clients, as the extension PreferencesController state is not compatible with core. We only care about isMultiAccountBalancesEnabled.

| AccountsControllerGetSelectedAccountAction
| NetworkControllerGetStateAction
| NetworkControllerGetNetworkClientByIdAction;
Expand All @@ -199,9 +202,10 @@ export type AccountTrackerControllerEvents =
*/
export type AllowedEvents =
| AccountsControllerSelectedEvmAccountChangeEvent
| AccountsControllerSelectedAccountChangeEvent
| TransactionControllerUnapprovedTransactionAddedEvent
| TransactionControllerTransactionConfirmedEvent;
| TransactionControllerTransactionConfirmedEvent
| NetworkControllerNetworkAddedEvent
| KeyringControllerUnlockEvent;

/**
* The messenger of the {@link AccountTrackerController}.
Expand Down Expand Up @@ -236,6 +240,8 @@ export class AccountTrackerController extends StaticIntervalPollingController<Ac

readonly #balanceFetchers: BalanceFetcher[];

readonly #fetchingEnabled: () => boolean;

/**
* Creates an AccountTracker instance.
*
Expand All @@ -247,6 +253,7 @@ export class AccountTrackerController extends StaticIntervalPollingController<Ac
* @param options.includeStakedAssets - Whether to include staked assets in the account balances.
* @param options.accountsApiChainIds - Function that returns array of chainIds that should use Accounts-API strategy (if supported by API).
* @param options.allowExternalServices - Disable external HTTP calls (privacy / offline mode).
* @param options.fetchingEnabled - Function that returns whether the controller is fetching enabled.
*/
constructor({
interval = 10000,
Expand All @@ -256,6 +263,7 @@ export class AccountTrackerController extends StaticIntervalPollingController<Ac
includeStakedAssets = false,
accountsApiChainIds = () => [],
allowExternalServices = () => true,
fetchingEnabled = () => true,
}: {
interval?: number;
state?: Partial<AccountTrackerControllerState>;
Expand All @@ -264,6 +272,7 @@ export class AccountTrackerController extends StaticIntervalPollingController<Ac
includeStakedAssets?: boolean;
accountsApiChainIds?: () => ChainIdHex[];
allowExternalServices?: () => boolean;
fetchingEnabled?: () => boolean;
}) {
const { selectedNetworkClientId } = messenger.call(
'NetworkController:getState',
Expand Down Expand Up @@ -302,6 +311,8 @@ export class AccountTrackerController extends StaticIntervalPollingController<Ac
),
];

this.#fetchingEnabled = fetchingEnabled;

this.setIntervalLength(interval);

this.messenger.subscribe(
Expand All @@ -316,23 +327,39 @@ export class AccountTrackerController extends StaticIntervalPollingController<Ac
(event): string => event.address,
);

this.messenger.subscribe('NetworkController:networkAdded', async () => {
await this.refresh(this.#getNetworkClientIds());
});

this.messenger.subscribe('KeyringController:unlock', async () => {
await this.refresh(this.#getNetworkClientIds());
});
Comment thread
bergarces marked this conversation as resolved.

this.messenger.subscribe(
'TransactionController:unapprovedTransactionAdded',
async (transactionMeta: TransactionMeta) => {
await this.#refreshAddress(
[transactionMeta.networkClientId],
transactionMeta.txParams.from,
);
const addresses = [transactionMeta.txParams.from];
if (transactionMeta.txParams.to) {
addresses.push(transactionMeta.txParams.to);
}
await this.refreshAddresses({
networkClientIds: [transactionMeta.networkClientId],
addresses,
});
},
);

this.messenger.subscribe(
'TransactionController:transactionConfirmed',
async (transactionMeta: TransactionMeta) => {
await this.#refreshAddress(
[transactionMeta.networkClientId],
transactionMeta.txParams.from,
);
const addresses = [transactionMeta.txParams.from];
if (transactionMeta.txParams.to) {
addresses.push(transactionMeta.txParams.to);
}
await this.refreshAddresses({
networkClientIds: [transactionMeta.networkClientId],
addresses,
});
},
);

Expand Down Expand Up @@ -540,13 +567,28 @@ export class AccountTrackerController extends StaticIntervalPollingController<Ac
});
}

async #refreshAddress(networkClientIds: NetworkClientId[], address: string) {
const checksumAddress = toChecksumHexAddress(address) as ChecksumAddress;
async refreshAddresses({

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making this public the same as refresh. The reason being, that if we end up missing some edge case in which we need to update and there is no event available to listen to, we can then dispatch an action from the client to do it.

networkClientIds,
addresses,
}: {
networkClientIds: NetworkClientId[];
addresses: string[];
}) {
const checksummedAddresses = addresses.map((address) =>
toChecksumHexAddress(address),
);

const accounts = this.messenger
.call('AccountsController:listAccounts')
.filter((account) =>
checksummedAddresses.includes(toChecksumHexAddress(account.address)),
);

await this.#refreshAccounts({
networkClientIds,
queryAllAccounts: false,
selectedAccount: checksumAddress,
allAccounts: [],
queryAllAccounts: true,
selectedAccount: '0x0',

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With queryAllAccounts set to true, this value is not being used.

We really shouldn't be delegating this logic down to the BalanceFetcher class, but that'd require a chunkier refactor.

allAccounts: accounts,
});
}

Expand All @@ -570,6 +612,10 @@ export class AccountTrackerController extends StaticIntervalPollingController<Ac

this.syncAccounts(chainIds);

if (!this.#fetchingEnabled()) {
return;
}
Comment thread
bergarces marked this conversation as resolved.
Comment thread
bergarces marked this conversation as resolved.
Comment thread
bergarces marked this conversation as resolved.

// Use balance fetchers with fallback strategy
const aggregated: ProcessedBalance[] = [];
let remainingChains = [...chainIds] as ChainIdHex[];
Expand Down
Loading