Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
55 changes: 55 additions & 0 deletions packages/assets-controller/src/AssetsController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2853,6 +2853,61 @@ describe('AssetsController', () => {
});
});

it('force refreshes assets when unapproved transaction is added', async () => {
await withController(async ({ controller, messenger }) => {
const getAssetsSpy = jest
.spyOn(controller, 'getAssets')
.mockResolvedValue({});

messenger.publish('TransactionController:unapprovedTransactionAdded', {
chainId: '0xa4b1',
txParams: { from: '0x1234567890123456789012345678901234567890' },
});

await flushPromises();

expect(getAssetsSpy).toHaveBeenCalledWith(
[expect.objectContaining({ id: MOCK_ACCOUNT_ID })],
{
chainIds: ['eip155:42161'],
forceUpdate: true,
},
);

getAssetsSpy.mockRestore();
});
});

it('does not force refresh assets on transaction events for AccountActivity-active chains', async () => {
await withController(async ({ controller, messenger }) => {
const getAssetsSpy = jest
.spyOn(controller, 'getAssets')
.mockResolvedValue({});

messenger.publish('AccountActivityService:statusChanged', {
chainIds: ['eip155:42161'],
status: 'up',
});

await flushPromises();

messenger.publish('TransactionController:unapprovedTransactionAdded', {
chainId: '0xa4b1',
txParams: { from: '0x1234567890123456789012345678901234567890' },
});
messenger.publish('TransactionController:transactionConfirmed', {
chainId: '0xa4b1',
txParams: { from: '0x1234567890123456789012345678901234567890' },
});

await flushPromises();

expect(getAssetsSpy).not.toHaveBeenCalled();

getAssetsSpy.mockRestore();
});
});

it('publishes balanceChanged event when balance updates', async () => {
await withController(async ({ controller, messenger }) => {
const balanceChangedHandler = jest.fn();
Expand Down
52 changes: 21 additions & 31 deletions packages/assets-controller/src/AssetsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1196,22 +1196,24 @@ export class AssetsController extends BaseController<
this.#updateActive();
});

// Subscribe to unapproved transactions - TXs that need confirmation
// Ensures that balances for the account making transaction are updated (e.g. for gas estimations)
// Subscribe to unapproved transactions - TXs that need confirmation.
// Ensures balances for the account making the transaction are updated
// (e.g. for gas estimations), except on AccountActivity-active chains.
this.messenger.subscribe(
'TransactionController:unapprovedTransactionAdded',
(transactionMeta: TransactionMeta) => {
this.#onUnapprovedTransactionAdded(transactionMeta);
this.#refreshAssetsForTransaction(transactionMeta);
},
);

// Post-tx refresh via the full fetch pipeline (Accounts API + RPC fallback).
// Skipped for chains covered by AccountActivity (real-time WS updates).
// RpcDataSource also listens for transactionConfirmed, but only refreshes
// chains it owns via an active subscription.
this.messenger.subscribe(
'TransactionController:transactionConfirmed',
(transactionMeta: TransactionMeta) => {
this.#onTransactionConfirmed(transactionMeta);
this.#refreshAssetsForTransaction(transactionMeta);
},
);
// Start tracking only after the account tree is fully built. Unlock can
Expand All @@ -1227,42 +1229,30 @@ export class AssetsController extends BaseController<
});
}

#onUnapprovedTransactionAdded(transactionMeta: TransactionMeta): void {
/**
* Force-refresh assets for the account/chain of a transaction, unless the
* chain is already covered by AccountActivity (real-time WebSocket balances).
*
* @param transactionMeta - The transaction that triggered the refresh.
*/
#refreshAssetsForTransaction(transactionMeta: TransactionMeta): void {
const hexChainId = transactionMeta.chainId;
if (!hexChainId) {
return;
}

const caipChainId = `eip155:${parseInt(hexChainId, 16)}` as ChainId;
const fromAddress = transactionMeta.txParams.from?.toLowerCase();
if (!fromAddress) {
return;
}

const matchedAccount = this.#getSelectedAccounts().find(
(account) => account.address.toLowerCase() === fromAddress,
);
if (!matchedAccount) {
return;
}

this.getAssets([matchedAccount], {
chainIds: [caipChainId],
forceUpdate: true,
}).catch((error) => {
log('Failed to refresh assets after unapproved transaction added', {
error,
});
});
}

#onTransactionConfirmed(transactionMeta: TransactionMeta): void {
const hexChainId = transactionMeta.chainId;
if (!hexChainId) {
// AccountActivity pushes live balance updates for its active chains; a
// force getAssets would be redundant and can race the WebSocket path.
if (
this.#accountActivityDataSource
.getActiveChainsSync()
.includes(caipChainId)
) {
return;
}

const caipChainId = `eip155:${parseInt(hexChainId, 16)}` as ChainId;
const fromAddress = transactionMeta.txParams.from?.toLowerCase();
if (!fromAddress) {
return;
Expand All @@ -1279,7 +1269,7 @@ export class AssetsController extends BaseController<
chainIds: [caipChainId],
forceUpdate: true,
}).catch((error) => {
log('Failed to refresh assets after transaction confirmed', { error });
log('Failed to refresh assets after transaction event', { error });

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.

good one on renaming this

});
}

Expand Down