Skip to content

Commit

Permalink
Merge branch 'main' into feat/new-notification-types
Browse files Browse the repository at this point in the history
  • Loading branch information
matteoscurati authored Oct 18, 2024
2 parents e3f423a + 1251b86 commit 6194f7f
Show file tree
Hide file tree
Showing 8 changed files with 681 additions and 5 deletions.
237 changes: 237 additions & 0 deletions packages/assets-controllers/src/TokenDetectionController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ import {
buildInfuraNetworkConfiguration,
} from '../../network-controller/tests/helpers';
import { formatAggregatorNames } from './assetsUtil';
import * as MutliChainAccountsServiceModule from './multi-chain-accounts-service';
import {
MOCK_GET_BALANCES_RESPONSE,
createMockGetBalancesResponse,
} from './multi-chain-accounts-service/mocks/mock-get-balances';
import { MOCK_GET_SUPPORTED_NETWORKS_RESPONSE } from './multi-chain-accounts-service/mocks/mock-get-supported-networks';
import { TOKEN_END_POINT_API } from './token-service';
import type {
AllowedActions,
Expand All @@ -46,9 +52,11 @@ import {
} from './TokenDetectionController';
import {
getDefaultTokenListState,
type TokenListMap,
type TokenListState,
type TokenListToken,
} from './TokenListController';
import type { Token } from './TokenRatesController';
import type {
TokensController,
TokensControllerState,
Expand Down Expand Up @@ -173,9 +181,25 @@ function buildTokenDetectionControllerMessenger(
});
}

const mockMultiChainAccountsService = () => {
const mockFetchSupportedNetworks = jest
.spyOn(MutliChainAccountsServiceModule, 'fetchSupportedNetworks')
.mockResolvedValue(MOCK_GET_SUPPORTED_NETWORKS_RESPONSE.fullSupport);
const mockFetchMultiChainBalances = jest
.spyOn(MutliChainAccountsServiceModule, 'fetchMultiChainBalances')
.mockResolvedValue(MOCK_GET_BALANCES_RESPONSE);

return {
mockFetchSupportedNetworks,
mockFetchMultiChainBalances,
};
};

describe('TokenDetectionController', () => {
const defaultSelectedAccount = createMockInternalAccount();

mockMultiChainAccountsService();

beforeEach(async () => {
nock(TOKEN_END_POINT_API)
.get(getTokensPath(ChainId.mainnet))
Expand Down Expand Up @@ -2236,6 +2260,218 @@ describe('TokenDetectionController', () => {
},
);
});

/**
* Test Utility - Arrange and Act `detectTokens()` with the Accounts API feature
* RPC flow will return `sampleTokenA` and the Accounts API flow will use `sampleTokenB`
* @param props - options to modify these tests
* @param props.overrideMockTokensCache - change the tokens cache
* @param props.mockMultiChainAPI - change the Accounts API responses
* @param props.overrideMockTokenGetState - change the external TokensController state
* @returns properties that can be used for assertions
*/
const arrangeActTestDetectTokensWithAccountsAPI = async (props?: {
/** Overwrite the tokens cache inside Tokens Controller */
overrideMockTokensCache?: (typeof sampleTokenA)[];
mockMultiChainAPI?: ReturnType<typeof mockMultiChainAccountsService>;
overrideMockTokenGetState?: Partial<TokensControllerState>;
}) => {
const {
overrideMockTokensCache = [sampleTokenA, sampleTokenB],
mockMultiChainAPI,
overrideMockTokenGetState,
} = props ?? {};

// Arrange - RPC Tokens Flow - Uses sampleTokenA
const mockGetBalancesInSingleCall = jest.fn().mockResolvedValue({
[sampleTokenA.address]: new BN(1),
});

// Arrange - API Tokens Flow - Uses sampleTokenB
const { mockFetchSupportedNetworks, mockFetchMultiChainBalances } =
mockMultiChainAPI ?? mockMultiChainAccountsService();

if (!mockMultiChainAPI) {
mockFetchSupportedNetworks.mockResolvedValue([1]);
mockFetchMultiChainBalances.mockResolvedValue(
createMockGetBalancesResponse([sampleTokenB.address], 1),
);
}

// Arrange - Selected Account
const selectedAccount = createMockInternalAccount({
address: '0x0000000000000000000000000000000000000001',
});

// Arrange / Act - withController setup + invoke detectTokens
const { callAction } = await withController(
{
options: {
disabled: false,
getBalancesInSingleCall: mockGetBalancesInSingleCall,
useAccountsAPI: true, // USING ACCOUNTS API
},
mocks: {
getSelectedAccount: selectedAccount,
getAccount: selectedAccount,
},
},
async ({
controller,
mockTokenListGetState,
callActionSpy,
mockTokensGetState,
}) => {
const tokenCacheData: TokenListMap = {};
overrideMockTokensCache.forEach(
(t) =>
(tokenCacheData[t.address] = {
name: t.name,
symbol: t.symbol,
decimals: t.decimals,
address: t.address,
occurrences: 1,
aggregators: t.aggregators,
iconUrl: t.image,
}),
);

mockTokenListGetState({
...getDefaultTokenListState(),
tokensChainsCache: {
'0x1': {
timestamp: 0,
data: tokenCacheData,
},
},
});

if (overrideMockTokenGetState) {
mockTokensGetState({
...getDefaultTokensState(),
...overrideMockTokenGetState,
});
}

// Act
await controller.detectTokens({
networkClientId: NetworkType.mainnet,
selectedAddress: selectedAccount.address,
});

return {
callAction: callActionSpy,
};
},
);

const assertAddedTokens = (token: Token) =>
expect(callAction).toHaveBeenCalledWith(
'TokensController:addDetectedTokens',
[token],
{
chainId: ChainId.mainnet,
selectedAddress: selectedAccount.address,
},
);

const assertTokensNeverAdded = () =>
expect(callAction).not.toHaveBeenCalledWith(
'TokensController:addDetectedTokens',
);

return {
assertAddedTokens,
assertTokensNeverAdded,
mockFetchMultiChainBalances,
mockGetBalancesInSingleCall,
rpcToken: sampleTokenA,
apiToken: sampleTokenB,
};
};

it('should trigger and use Accounts API for detection', async () => {
const {
assertAddedTokens,
mockFetchMultiChainBalances,
apiToken,
mockGetBalancesInSingleCall,
} = await arrangeActTestDetectTokensWithAccountsAPI();

expect(mockFetchMultiChainBalances).toHaveBeenCalled();
expect(mockGetBalancesInSingleCall).not.toHaveBeenCalled();
assertAddedTokens(apiToken);
});

it('uses the Accounts API but does not add unknown tokens', async () => {
// API returns sampleTokenB
// As this is not a known token (in cache), then is not added
const {
assertTokensNeverAdded,
mockFetchMultiChainBalances,
mockGetBalancesInSingleCall,
} = await arrangeActTestDetectTokensWithAccountsAPI({
overrideMockTokensCache: [sampleTokenA],
});

expect(mockFetchMultiChainBalances).toHaveBeenCalled();
expect(mockGetBalancesInSingleCall).not.toHaveBeenCalled();
assertTokensNeverAdded();
});

it('fallbacks from using the Accounts API if fails', async () => {
// Test 1 - fetch supported networks fails
let mockAPI = mockMultiChainAccountsService();
mockAPI.mockFetchSupportedNetworks.mockRejectedValue(
new Error('Mock Error'),
);
let actResult = await arrangeActTestDetectTokensWithAccountsAPI({
mockMultiChainAPI: mockAPI,
});

expect(actResult.mockFetchMultiChainBalances).not.toHaveBeenCalled(); // never called as could not fetch supported networks...
expect(actResult.mockGetBalancesInSingleCall).toHaveBeenCalled(); // ...so then RPC flow was initiated
actResult.assertAddedTokens(actResult.rpcToken);

// Test 2 - fetch multi chain fails
mockAPI = mockMultiChainAccountsService();
mockAPI.mockFetchMultiChainBalances.mockRejectedValue(
new Error('Mock Error'),
);
actResult = await arrangeActTestDetectTokensWithAccountsAPI({
mockMultiChainAPI: mockAPI,
});

expect(actResult.mockFetchMultiChainBalances).toHaveBeenCalled(); // API was called, but failed...
expect(actResult.mockGetBalancesInSingleCall).toHaveBeenCalled(); // ...so then RPC flow was initiated
actResult.assertAddedTokens(actResult.rpcToken);
});

it('uses the Accounts API but does not add tokens that are already added', async () => {
// Here we populate the token state with a token that exists in the tokenAPI.
// So the token retrieved from the API should not be added
const { assertTokensNeverAdded, mockFetchMultiChainBalances } =
await arrangeActTestDetectTokensWithAccountsAPI({
overrideMockTokenGetState: {
allDetectedTokens: {
'0x1': {
'0x0000000000000000000000000000000000000001': [
{
address: sampleTokenB.address,
name: sampleTokenB.name,
symbol: sampleTokenB.symbol,
decimals: sampleTokenB.decimals,
aggregators: sampleTokenB.aggregators,
},
],
},
},
},
});

expect(mockFetchMultiChainBalances).toHaveBeenCalled();
assertTokensNeverAdded();
});
});
});

Expand Down Expand Up @@ -2415,6 +2651,7 @@ async function withController<ReturnValue>(
getBalancesInSingleCall: jest.fn(),
trackMetaMetricsEvent: jest.fn(),
messenger: buildTokenDetectionControllerMessenger(controllerMessenger),
useAccountsAPI: false,
...options,
});
try {
Expand Down
Loading

0 comments on commit 6194f7f

Please sign in to comment.