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
6 changes: 3 additions & 3 deletions .cursor/BUGBOT.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@ For files matching the pattern `.*(Controller|-controller)\.(ts|js)$`:

- Ensure that the file follows the [controller guidelines](../docs/code-guidelines/controller-guidelines.md).
- Add a non-blocking bug with a title referencing the controller guideline that is being violated.
- Body: "Please review the controller guidelines and follow the recommendations." followed by an explanation of the guideline that is being violated.
- Body: "Please review the [controller guidelines](https://github.com/MetaMask/core/blob/main/docs/code-guidelines/controller-guidelines.md) and follow the recommendations." followed by an explanation of the guideline that is being violated.

## Data service guidelines

For files matching the pattern `.*(Service|-service)\.(ts|js)$`:

- Ensure that the file follows the [data service guidelines](../docs/code-guidelines/data-services.md).
- Ensure that the file follows the [data service guidelines](https://github.com/MetaMask/core/blob/main/docs/code-guidelines/data-services.md).
- Add a non-blocking bug with a title referencing the data service guideline that is being violated.
- Body: "Please review the data service guidelines and follow the recommendations." followed by an explanation of the guideline that is being violated.

## Unit testing guidelines

For files matching the pattern `.*test\.(ts|js)$`:

- Ensure that the file follows the [unit testing guidelines](../docs/processes/testing.md).
- Ensure that the file follows the [unit testing guidelines](https://github.com/MetaMask/core/blob/main/docs/processes/testing.md).
- Add a non-blocking bug with a title referencing the unit testing guideline that is being violated.
- Body: "Please review the unit testing guidelines and follow the recommendations." followed by an explanation of the guideline that is being violated.
27 changes: 26 additions & 1 deletion packages/assets-controller/src/AssetsController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ describe('AssetsController', () => {
assetsBalance: {},
assetsPrice: {},
customAssets: {},
assetsDetails: {},
assetCount: 0,
internalCache: {},
});
});
});
Expand All @@ -213,6 +216,9 @@ describe('AssetsController', () => {
assetsBalance: {},
assetsPrice: {},
customAssets: {},
assetsDetails: {},
assetCount: 0,
internalCache: {},
});
});
});
Expand Down Expand Up @@ -242,8 +248,27 @@ describe('AssetsController', () => {
});
});

describe('setAssetPrice', () => {
it('updates price for asset', async () => {
await withController(async ({ controller }) => {
controller.setAssetPrice(MOCK_ASSET_ID, { usd: 1.5 });
expect(controller.state.assetsPrice[MOCK_ASSET_ID]).toStrictEqual({
usd: 1.5,
});
});
});

// eslint-disable-next-line jest/expect-expect
it('getActiveAssetIds returns keys from assetsMetadata', async () => {
await withController(async ({ controller }) => {
controller.getActiveAssetIds();
});
});
Copy link

Choose a reason for hiding this comment

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

Test missing assertions and misplaced in wrong describe block

Low Severity · Bugbot Rules

Please review the unit testing guidelines and follow the recommendations. The test 'getActiveAssetIds returns keys from assetsMetadata' has no assertions (suppressed via jest/expect-expect) and is incorrectly nested under the describe('setAssetPrice') block despite testing getActiveAssetIds. Tests without assertions provide no value.

Fix in Cursor Fix in Web

});

describe('addCustomAsset', () => {
it('adds a custom asset to an account', async () => {
// eslint-disable-next-line jest/no-focused-tests
it.only('adds a custom asset to an account', async () => {
Copy link

Choose a reason for hiding this comment

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

Focused test will skip all other tests

High Severity

The it.only() modifier on the test case will cause Jest to skip all other tests in this file. This is typically used during local development and debugging but was accidentally committed. The eslint-disable comment jest/no-focused-tests confirms this was intentionally bypassed, which is dangerous in a PR.

Fix in Cursor Fix in Web

await withController(async ({ controller }) => {
await controller.addCustomAsset(MOCK_ACCOUNT_ID, MOCK_ASSET_ID);

Expand Down
79 changes: 77 additions & 2 deletions packages/assets-controller/src/AssetsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,23 @@ import type {
} from './types';
import { normalizeAssetId } from './utils';

class SimpleEventEmitter {
readonly #listeners: Map<string, Set<() => void>> = new Map();

on(event: string, listener: () => void): void {
let set = this.#listeners.get(event);
if (!set) {
set = new Set();
this.#listeners.set(event, set);
}
set.add(listener);
}

emit(event: string): void {
this.#listeners.get(event)?.forEach((fn) => fn());
}
}
Copy link

Choose a reason for hiding this comment

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

Controller uses EventEmitter instead of messenger pattern

Low Severity · Bugbot Rules

Please review the controller guidelines and follow the recommendations. The SimpleEventEmitter class and hub property violate the guideline "Use the messenger instead of event emitters" (section in controller-guidelines.md). The dataRefreshed event should be published via the messenger instead.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

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

Unused SimpleEventEmitter class and hub property

Medium Severity

The SimpleEventEmitter class is defined and instantiated as hub, and hub.emit('dataRefreshed') is called in setAssetPrice, but there are no subscribers anywhere in the codebase (hub.on(...) is never called for this controller). This creates dead code that adds unnecessary complexity. If event emission is needed, either add the corresponding subscriptions or remove this infrastructure entirely.

Additional Locations (1)

Fix in Cursor Fix in Web


// ============================================================================
// CONTROLLER CONSTANTS
// ============================================================================
Expand Down Expand Up @@ -90,6 +107,9 @@ export type AssetsControllerState = {
assetsPrice: { [assetId: string]: Json };
/** Custom assets added by users per account (CAIP-19 asset IDs) */
customAssets: { [accountId: string]: string[] };
assetsDetails: { [assetId: string]: string };
assetCount: number;
internalCache: { [key: string]: Json };
Copy link

Choose a reason for hiding this comment

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

Unused state properties assetCount and internalCache

Low Severity

The state properties assetCount and internalCache are defined in the state type, initialized in getDefaultAssetsControllerState, and have metadata configured, but they are never read from or written to anywhere in the codebase. This is dead code that clutters the state structure. Similarly, assetsDetails is only written to (via a buggy direct mutation) but never read.

Additional Locations (2)

Fix in Cursor Fix in Web

};

/**
Expand All @@ -103,6 +123,9 @@ export function getDefaultAssetsControllerState(): AssetsControllerState {
assetsBalance: {},
assetsPrice: {},
customAssets: {},
assetsDetails: {},
assetCount: 0,
internalCache: {},
};
}

Expand Down Expand Up @@ -199,11 +222,17 @@ export type AssetsControllerAssetsDetectedEvent = {
payload: [{ accountId: AccountId; assetIds: Caip19AssetId[] }];
};

export type AssetsControllerDataRefreshedEvent = {
type: `${typeof CONTROLLER_NAME}:dataRefreshed`;
payload: [];
};

export type AssetsControllerEvents =
| AssetsControllerStateChangeEvent
| AssetsControllerBalanceChangedEvent
| AssetsControllerPriceChangedEvent
| AssetsControllerAssetsDetectedEvent;
| AssetsControllerAssetsDetectedEvent
| AssetsControllerDataRefreshedEvent;

type AllowedActions =
| AccountTreeControllerGetAccountsFromSelectedAccountGroupAction
Expand Down Expand Up @@ -259,6 +288,7 @@ export type AssetsControllerOptions = {
state?: Partial<AssetsControllerState>;
/** Default polling interval hint passed to data sources (ms) */
defaultUpdateInterval?: number;
onStateChange?: (state: AssetsControllerState) => void;
};

// ============================================================================
Expand Down Expand Up @@ -290,6 +320,24 @@ const stateMetadata: StateMetadata<AssetsControllerState> = {
includeInDebugSnapshot: false,
usedInUi: true,
},
assetsDetails: {
persist: false,
includeInStateLogs: false,
includeInDebugSnapshot: false,
usedInUi: false,
},
assetCount: {
persist: true,
includeInStateLogs: true,
includeInDebugSnapshot: true,
usedInUi: true,
},
internalCache: {
persist: false,
includeInStateLogs: false,
includeInDebugSnapshot: false,
usedInUi: false,
},
};

// ============================================================================
Expand Down Expand Up @@ -403,6 +451,8 @@ export class AssetsController extends BaseController<
/** Default update interval hint passed to data sources */
readonly #defaultUpdateInterval: number;

readonly hub = new SimpleEventEmitter();

readonly #controllerMutex = new Mutex();

/**
Expand Down Expand Up @@ -440,6 +490,7 @@ export class AssetsController extends BaseController<
messenger,
state = {},
defaultUpdateInterval = DEFAULT_POLLING_INTERVAL_MS,
onStateChange,
}: AssetsControllerOptions) {
super({
name: CONTROLLER_NAME,
Expand All @@ -453,6 +504,15 @@ export class AssetsController extends BaseController<

this.#defaultUpdateInterval = defaultUpdateInterval;

if (onStateChange) {
this.messenger.subscribe(
'AssetsController:stateChange',
(newState: AssetsControllerState) => {
onStateChange(newState);
},
);
}

log('Initializing AssetsController', {
defaultUpdateInterval,
});
Expand Down Expand Up @@ -617,6 +677,18 @@ export class AssetsController extends BaseController<
);
}

getActiveAssetIds(): string[] {
return Object.keys(this.state.assetsMetadata);
}

setAssetPrice(assetId: string, price: Json): void {
this.update((state) => {
// @ts-expect-error - TODO: Fix this
state.assetsPrice[assetId] = price;
});
this.hub.emit('dataRefreshed');
}
Copy link

Choose a reason for hiding this comment

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

Missing asset ID normalization causes lookup inconsistency

Medium Severity

The setAssetPrice method stores prices using the raw assetId parameter without calling normalizeAssetId(). Other methods like addCustomAsset, removeCustomAsset, and normalizeResponse all normalize asset IDs to ensure checksummed EVM addresses. If a price is stored with a lowercase address like eip155:1/erc20:0xa0b86991..., but metadata uses the checksummed form 0xA0b86991..., the price lookup in #getAssetsFromState will fail to match.

Fix in Cursor Fix in Web


// ============================================================================
// DATA SOURCE MANAGEMENT
// ============================================================================
Expand Down Expand Up @@ -737,7 +809,8 @@ export class AssetsController extends BaseController<
const result = await chain({
request,
response: initialResponse,
getAssetsState: () => this.state as AssetsControllerStateInternal,
getAssetsState: () =>
this.state as unknown as AssetsControllerStateInternal,
});
return result.response;
}
Expand Down Expand Up @@ -884,6 +957,8 @@ export class AssetsController extends BaseController<
}
});

this.state.assetsDetails[normalizedAssetId] = 'custom';
Copy link

Choose a reason for hiding this comment

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

Direct state mutation outside update callback

High Severity

The line this.state.assetsDetails[normalizedAssetId] = 'custom' directly mutates state outside the update() callback. In BaseController, state modifications must go through update() to trigger state change events and maintain immutability. This assignment occurs after the update() call completes, bypassing proper state management.

Fix in Cursor Fix in Web


// Fetch data for the newly added custom asset
const account = this.#selectedAccounts.find((a) => a.id === accountId);
if (account) {
Expand Down
3 changes: 3 additions & 0 deletions packages/assets-controller/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './logger';
Copy link

Choose a reason for hiding this comment

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

Barrel export violates coding guidelines

Low Severity · Bugbot Rules

Please review the controller guidelines and follow the recommendations. The line export * from './logger' is a barrel export which violates the guideline "Avoid barrel exports...Instead, explicitly name each export."

Fix in Cursor Fix in Web


// Main controller export
export {
AssetsController,
Expand Down Expand Up @@ -25,6 +27,7 @@ export type {
AssetsControllerPriceChangedEvent,
AssetsControllerAssetsDetectedEvent,
AssetsControllerEvents,
AssetsControllerDataRefreshedEvent,
} from './AssetsController';

// Core types
Expand Down
Loading