Skip to content
Draft
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
1 change: 1 addition & 0 deletions packages/assets-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Convert WebSocket balance updates in `BackendWebsocketDataSource` from raw smallest-units to human-readable amounts using asset decimals (same behavior as RPC/Accounts API), so `assetsBalance` remains consistent across data sources ([#8032](https://github.com/MetaMask/core/pull/8032))
- Include all assets from balance and each account's custom assets from state in `detectedAssets`, so prices and metadata are fetched for existing assets and custom tokens (previously only assets without metadata were included, so existing assets did not get prices) ([#8021](https://github.com/MetaMask/core/pull/8021))
- Request `includeAggregators: true` when fetching token metadata from the v3 assets API so aggregator data is returned and stored in `assetsInfo` ([#8021](https://github.com/MetaMask/core/pull/8021))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -593,11 +593,12 @@ describe('BackendWebsocketDataSource', () => {
notificationCallback(notification);
await new Promise(process.nextTick);

// Raw 10e18 wei (0x8ac7230489e80000) with 18 decimals → human-readable "10"
expect(assetsUpdateHandler).toHaveBeenCalledWith(
expect.objectContaining({
assetsBalance: expect.objectContaining({
'mock-account-id': expect.objectContaining({
'eip155:8453/slip44:60': { amount: '10000000000000000000' },
'eip155:8453/slip44:60': { amount: '10' },
}),
}),
assetsInfo: expect.objectContaining({
Expand Down Expand Up @@ -659,12 +660,13 @@ describe('BackendWebsocketDataSource', () => {
notificationCallback(notification);
await new Promise(process.nextTick);

// Raw 1000000 (1 USDC) with 6 decimals → human-readable "1"
expect(assetsUpdateHandler).toHaveBeenCalledWith(
expect.objectContaining({
assetsBalance: expect.objectContaining({
'mock-account-id': expect.objectContaining({
'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': {
amount: '1000000',
amount: '1',
},
}),
}),
Expand All @@ -682,6 +684,122 @@ describe('BackendWebsocketDataSource', () => {
controller.destroy();
});

it('converts raw WebSocket balance (hex) to human-readable using asset decimals', async () => {
const { controller, wsSubscribeMock, assetsUpdateHandler } =
setupController({
initialActiveChains: [CHAIN_MAINNET],
connectionState: WebSocketState.CONNECTED,
});

let notificationCallback: (
notification: ServerNotificationMessage,
) => void = () => undefined;

wsSubscribeMock.mockImplementation(({ callback }) => {
notificationCallback = callback;
return Promise.resolve(createMockWsSubscription());
});

await controller.subscribe({
subscriptionId: 'sub-1',
request: createDataRequest(),
isUpdate: false,
onAssetsUpdate: assetsUpdateHandler,
});

// 0x26f0e5 = 2552037 raw; USDC 6 decimals → 2.552037
const notification = createMockNotification({
channel: `account-activity.v1.eip155:0:${MOCK_ADDRESS.toLowerCase()}`,
data: {
address: MOCK_ADDRESS,
tx: { chain: CHAIN_MAINNET },
updates: [
{
asset: {
type: 'eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
unit: 'USDC',
decimals: 6,
},
postBalance: {
amount: '0x26f0e5',
},
},
],
},
});

notificationCallback(notification);
await new Promise(process.nextTick);

// assetId key is as in notification (mixed case)
expect(assetsUpdateHandler).toHaveBeenCalledWith(
expect.objectContaining({
assetsBalance: expect.objectContaining({
'mock-account-id': expect.objectContaining({
'eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48': {
amount: '2.552037',
},
}),
}),
}),
);

controller.destroy();
});

it('skips balance update when asset.decimals is missing', async () => {
const { controller, wsSubscribeMock, assetsUpdateHandler } =
setupController({
initialActiveChains: [CHAIN_MAINNET],
connectionState: WebSocketState.CONNECTED,
});

let notificationCallback: (
notification: ServerNotificationMessage,
) => void = () => undefined;

wsSubscribeMock.mockImplementation(({ callback }) => {
notificationCallback = callback;
return Promise.resolve(createMockWsSubscription());
});

await controller.subscribe({
subscriptionId: 'sub-1',
request: createDataRequest(),
isUpdate: false,
onAssetsUpdate: assetsUpdateHandler,
});

// No decimals on asset → update is skipped (we assume decimals are always present)
const notification = createMockNotification({
channel: `account-activity.v1.eip155:0:${MOCK_ADDRESS.toLowerCase()}`,
data: {
address: MOCK_ADDRESS,
tx: { chain: CHAIN_MAINNET },
updates: [
{
asset: {
type: 'eip155:1/erc20:0x0000000000000000000000000000000000000001',
unit: 'UNKNOWN',
decimals: undefined,
},
postBalance: {
amount: '1000000000000000000',
},
},
],
},
});

notificationCallback(notification);
await new Promise(process.nextTick);

// No valid updates → response has only updateMode, no assetsBalance
expect(assetsUpdateHandler).toHaveBeenCalledWith({ updateMode: 'merge' });

controller.destroy();
});

it('ignores notification with missing data', async () => {
const { controller, wsSubscribeMock, assetsUpdateHandler } =
setupController({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
KnownCaipNamespace,
toCaipChainId,
} from '@metamask/utils';
import BigNumberJS from 'bignumber.js';

import { AbstractDataSource } from './AbstractDataSource';
import type {
Expand Down Expand Up @@ -607,13 +608,23 @@ export class BackendWebsocketDataSource extends AbstractDataSource<
const isNative = asset.type.includes('/slip44:');
const tokenType = isNative ? 'native' : 'erc20';

// Parse balance amount (already in hex format like "0xc350")
const balanceAmount = postBalance.amount.startsWith('0x')
// We assume decimals are always present; skip malformed updates
if (asset.decimals === undefined) {
continue;
}

// Parse raw balance (hex like "0x26f0e5" or decimal string)
const rawBalanceStr = postBalance.amount.startsWith('0x')
? BigInt(postBalance.amount).toString()
: postBalance.amount;

// Convert to human-readable using asset decimals (match RpcDataSource / pipeline format)
const humanReadableAmount = new BigNumberJS(rawBalanceStr)
.dividedBy(new BigNumberJS(10).pow(asset.decimals))
.toString();

assetsBalance[accountId][assetId] = {
amount: balanceAmount,
amount: humanReadableAmount,
};

assetsMetadata[assetId] = {
Expand Down