Skip to content

Commit

Permalink
Merge branch 'main' into initialize-caip-multichain
Browse files Browse the repository at this point in the history
  • Loading branch information
jiexi committed Oct 21, 2024
2 parents 1e67641 + 5c04d50 commit d7ed999
Show file tree
Hide file tree
Showing 51 changed files with 1,414 additions and 228 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@metamask/core-monorepo",
"version": "222.0.0",
"version": "223.0.0",
"private": true,
"description": "Monorepo for packages shared between MetaMask clients",
"repository": {
Expand Down Expand Up @@ -56,7 +56,7 @@
"@metamask/eslint-config-jest": "^12.1.0",
"@metamask/eslint-config-nodejs": "^12.1.0",
"@metamask/eslint-config-typescript": "^12.1.0",
"@metamask/eth-block-tracker": "^10.0.0",
"@metamask/eth-block-tracker": "^11.0.2",
"@metamask/eth-json-rpc-provider": "^4.1.5",
"@metamask/json-rpc-engine": "^10.0.0",
"@metamask/utils": "^9.1.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/accounts-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
},
"devDependencies": {
"@metamask/auto-changelog": "^3.4.4",
"@metamask/keyring-controller": "^17.2.2",
"@metamask/keyring-controller": "^17.3.0",
"@metamask/snaps-controllers": "^9.7.0",
"@types/jest": "^27.4.1",
"@types/readable-stream": "^2.3.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/approval-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
},
"dependencies": {
"@metamask/base-controller": "^7.0.1",
"@metamask/rpc-errors": "^6.3.1",
"@metamask/rpc-errors": "^7.0.0",
"@metamask/utils": "^9.1.0",
"nanoid": "^3.1.31"
},
Expand Down
11 changes: 10 additions & 1 deletion packages/assets-controllers/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [39.0.0]

### Changed

- **BREAKING:** `AccountTrackerController`, `CurrencyRateController`, `TokenDetectionController`, `TokenListController`, and `TokenRatesController` now use a new polling interface that accepts the generic parameter `PollingInput` ([#4752](https://github.com/MetaMask/core/pull/4752))
- **BREAKING:** The inherited `AbstractPollingController` method `startPollingByNetworkClientId` has been renamed to `startPolling` ([#4752](https://github.com/MetaMask/core/pull/4752))
- **BREAKING:** The inherited `AbstractPollingController` method `onPollingComplete` now returns the entire input object of type `PollingInput`, instead of a network client id ([#4752](https://github.com/MetaMask/core/pull/4752))

## [38.3.0]

### Changed
Expand Down Expand Up @@ -1142,7 +1150,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Use Ethers for AssetsContractController ([#845](https://github.com/MetaMask/core/pull/845))
[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/[email protected]
[Unreleased]: https://github.com/MetaMask/core/compare/@metamask/[email protected]
[39.0.0]: https://github.com/MetaMask/core/compare/@metamask/[email protected]...@metamask/[email protected]
[38.3.0]: https://github.com/MetaMask/core/compare/@metamask/[email protected]...@metamask/[email protected]
[38.2.0]: https://github.com/MetaMask/core/compare/@metamask/[email protected]...@metamask/[email protected]
[38.1.0]: https://github.com/MetaMask/core/compare/@metamask/[email protected]...@metamask/[email protected]
Expand Down
12 changes: 6 additions & 6 deletions packages/assets-controllers/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@metamask/assets-controllers",
"version": "38.3.0",
"version": "39.0.0",
"description": "Controllers which manage interactions involving ERC-20, ERC-721, and ERC-1155 tokens (including NFTs)",
"keywords": [
"MetaMask",
Expand Down Expand Up @@ -58,8 +58,8 @@
"@metamask/controller-utils": "^11.3.0",
"@metamask/eth-query": "^4.0.0",
"@metamask/metamask-eth-abis": "^3.1.1",
"@metamask/polling-controller": "^10.0.1",
"@metamask/rpc-errors": "^6.3.1",
"@metamask/polling-controller": "^11.0.0",
"@metamask/rpc-errors": "^7.0.0",
"@metamask/utils": "^9.1.0",
"@types/bn.js": "^5.1.5",
"@types/uuid": "^8.3.0",
Expand All @@ -78,9 +78,9 @@
"@metamask/auto-changelog": "^3.4.4",
"@metamask/ethjs-provider-http": "^0.3.0",
"@metamask/keyring-api": "^8.1.3",
"@metamask/keyring-controller": "^17.2.2",
"@metamask/network-controller": "^21.0.1",
"@metamask/preferences-controller": "^13.0.3",
"@metamask/keyring-controller": "^17.3.0",
"@metamask/network-controller": "^21.1.0",
"@metamask/preferences-controller": "^13.1.0",
"@types/jest": "^27.4.1",
"@types/lodash": "^4.14.191",
"@types/node": "^16.18.54",
Expand Down
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 d7ed999

Please sign in to comment.