Skip to content

Commit

Permalink
fix: multichainToken rate for non evm
Browse files Browse the repository at this point in the history
  • Loading branch information
salimtb committed Feb 3, 2025
1 parent a6c32ef commit b000312
Show file tree
Hide file tree
Showing 6 changed files with 745 additions and 3 deletions.
6 changes: 3 additions & 3 deletions eslint-warning-thresholds.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@
"import-x/namespace": 189,
"import-x/no-named-as-default": 1,
"import-x/no-named-as-default-member": 8,
"import-x/order": 211,
"import-x/order": 209,
"jest/no-conditional-in-test": 113,
"jest/prefer-lowercase-title": 2,
"jest/prefer-strict-equal": 2,
"jsdoc/check-tag-names": 375,
"jsdoc/require-returns": 25,
"jsdoc/tag-lines": 334,
"jsdoc/tag-lines": 332,
"n/no-unsupported-features/node-builtins": 4,
"n/prefer-global/text-encoder": 4,
"n/prefer-global/text-decoder": 4,
"prettier/prettier": 114,
"prettier/prettier": 94,
"promise/always-return": 3,
"promise/catch-or-return": 2,
"promise/param-names": 8,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
import { ControllerMessenger } from '@metamask/base-controller';
import type { InternalAccount } from '@metamask/keyring-internal-api';
import { KeyringClient } from '@metamask/keyring-snap-client';
import { useFakeTimers } from 'sinon';

import {
MultiChainTokensRatesController,
type AllowedActions,
type AllowedEvents,
} from './MultichainTokensRatesController';

// A fake non‑EVM account (with Snap metadata) that meets the controller’s criteria.
const fakeNonEvmAccount: InternalAccount = {
id: 'account1',
type: 'solana:data-account',
address: '0x123',
metadata: {
name: 'Test Account',
// @ts-expect-error-next-line
snap: { id: 'test-snap', enabled: true },
},
scopes: ['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'],
options: {},
methods: [],
};

// A fake EVM account (which should be filtered out).
const fakeEvmAccount: InternalAccount = {
id: 'account2',
type: 'eip155:eoa',
address: '0x456',
// @ts-expect-error-next-line
metadata: { name: 'EVM Account' },
scopes: [],
options: {},
methods: [],
};

// A fake conversion rates response returned by the SnapController.
const fakeAccountRates = {
conversionRates: {
token1: {
'swift:0/iso4217:USD': {
rate: '202.11',
conversionTime: 1738539923277,
},
},
},
};

const setupController = ({
config,
}: {
config?: Partial<
ConstructorParameters<typeof MultiChainTokensRatesController>[0]
>;
} = {}) => {
const messenger = new ControllerMessenger<AllowedActions, AllowedEvents>();

messenger.registerActionHandler('AccountsController:getState', () => ({
accounts: {
account1: {
type: 'eip155:eoa',
id: 'account1',
options: {},
metadata: { name: 'Test Account' },
address: '0x123',
methods: [],
},
},
selectedAccount: 'account1',
internalAccounts: { accounts: {}, selectedAccount: 'account1' },
}));

messenger.registerActionHandler(
'AccountsController:listMultichainAccounts',
() => [fakeNonEvmAccount, fakeEvmAccount],
);

const multiChainTokensRatesControllerMessenger = messenger.getRestricted({
name: 'MultiChainTokensRatesController',
allowedActions: [
'AccountsController:getState',
'AccountsController:listMultichainAccounts',
'SnapController:handleRequest',
],
allowedEvents: [
'AccountsController:selectedAccountChange',
'AccountsController:accountRemoved',
'KeyringController:lock',
'KeyringController:unlock',
],
});

return {
controller: new MultiChainTokensRatesController({
messenger: multiChainTokensRatesControllerMessenger,
...config,
}),
messenger,
};
};

describe('MultiChainTokensRatesController', () => {
let clock: sinon.SinonFakeTimers;

const mockedDate = 1705760550000;

beforeEach(() => {
clock = useFakeTimers();
jest.spyOn(Date, 'now').mockReturnValue(mockedDate);
});

afterEach(() => {
clock.restore();
jest.restoreAllMocks();
});

it('should initialize with an empty conversionRates state', () => {
const { controller } = setupController();
expect(controller.state).toStrictEqual({ conversionRates: {} });
});

it('should update conversion rates for a valid non-EVM account', async () => {
const { controller, messenger } = setupController();

// Stub KeyringClient.listAccountAssets so that the controller “discovers” one asset.
jest
.spyOn(KeyringClient.prototype, 'listAccountAssets')
.mockResolvedValue([
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501',
]);

// Override the SnapController:handleRequest handler to return our fake conversion rates.
const snapHandler = jest.fn().mockResolvedValue(fakeAccountRates);
messenger.registerActionHandler(
'SnapController:handleRequest',
snapHandler,
);

// Call updateTokensRates for the valid non-EVM account.
await controller.updateTokensRates('account1');

// Verify that listAccountAssets was called with the correct account.
expect(KeyringClient.prototype.listAccountAssets).toHaveBeenCalledWith(
'account1',
);

// Check that the Snap request was made with the expected parameters.
expect(snapHandler).toHaveBeenCalledWith(
expect.objectContaining({
handler: 'onAssetsConversion',
origin: 'metamask',
request: {
id: '4dbf133d-9ce3-4d3f-96ac-bfc88d351046',
jsonrpc: '2.0',
method: 'onAssetsConversion',
params: {
conversions: [
{
from: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501',
to: 'swift:0/iso4217:USD',
},
],
},
},
snapId: 'test-snap',
}),
);

// The controller state should now contain the conversion rates returned.
expect(controller.state.conversionRates).toStrictEqual(
// fakeAccountRates.conversionRates,
{
account1: {
token1: {
'swift:0/iso4217:USD': {
rate: '202.11',
conversionTime: 1738539923277,
},
},
},
},
);
});

it('should not update conversion rates if the controller is not active', async () => {
const { controller, messenger } = setupController();

// Simulate a keyring lock event to set the controller as inactive.
messenger.publish('KeyringController:lock');
// Override SnapController:handleRequest and stub listAccountAssets.
const snapHandler = jest.fn().mockResolvedValue(fakeAccountRates);
messenger.registerActionHandler(
'SnapController:handleRequest',
snapHandler,
);
jest
.spyOn(KeyringClient.prototype, 'listAccountAssets')
.mockResolvedValue([
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501',
]);

await controller.updateTokensRates('account1');
// Since the controller is locked, no update should occur.
expect(controller.state.conversionRates).toStrictEqual({});
expect(snapHandler).not.toHaveBeenCalled();
});

it('should not update conversion rates for an unknown account', async () => {
const { controller } = setupController();
// Calling updateTokensRates for an account that does not exist should leave state unchanged.
await controller.updateTokensRates('nonexistent');
expect(controller.state.conversionRates).toStrictEqual({});
});

it('should call updateTokensRates when _executePoll is invoked', async () => {
const { controller, messenger } = setupController();

jest
.spyOn(KeyringClient.prototype, 'listAccountAssets')
.mockResolvedValue([
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501',
]);

messenger.registerActionHandler(
'SnapController:handleRequest',
async () => ({
conversionRates: {
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501': {
'swift:0/iso4217:USD': {
rate: '202.11',
conversionTime: 1738539923277,
},
},
},
}),
);

// Spy on updateTokensRates.
const updateSpy = jest.spyOn(controller, 'updateTokensRates');
await controller._executePoll();
expect(updateSpy).toHaveBeenCalledWith(fakeNonEvmAccount.id);
});

it('should remove conversion rates when an account is removed', async () => {
const { controller, messenger } = setupController();

jest
.spyOn(KeyringClient.prototype, 'listAccountAssets')
.mockResolvedValue([
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501',
]);

messenger.registerActionHandler(
'SnapController:handleRequest',
async () => ({
conversionRates: {
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501': {
'swift:0/iso4217:USD': {
rate: '202.11',
conversionTime: 1738539923277,
},
},
},
}),
);

await controller.updateTokensRates('account1');
expect(controller.state.conversionRates.account1).toBeDefined();

// Simulate an account removal event.
messenger.publish('AccountsController:accountRemoved', 'account1');
// Wait a tick so that asynchronous event handlers finish.
await Promise.resolve();
expect(controller.state.conversionRates.account1).toBeUndefined();
});

it('should call updateTokensRates when the selected account changes', async () => {
const { controller, messenger } = setupController();
// Create a new non‑EVM account.
const newAccount = {
id: 'account3',
type: 'solana:data-account',
address: '0x789',
metadata: { name: 'New Account', snap: { id: 'new-snap' } },
scopes: ['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'],
options: {},
methods: [],
};

// Spy on updateTokensRates.
const updateSpy = jest
.spyOn(controller, 'updateTokensRates')
.mockResolvedValue();

// Publish a selectedAccountChange event.
// @ts-expect-error-next-line
messenger.publish('AccountsController:selectedAccountChange', newAccount);
// Wait for the asynchronous subscriber to run.
await Promise.resolve();
expect(updateSpy).toHaveBeenCalledWith('account3');
});
});
Loading

0 comments on commit b000312

Please sign in to comment.