Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BREAKING: Token detection V2 #763

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6edf1e7
Update token detection logic. Support Mainnet, Polygon, BSC, and Avax…
Cal-L Mar 4, 2022
196e83b
Add network listener callback to AssetsContractController
Cal-L Mar 7, 2022
f3076d6
Add support for detected tokens on TokensController. TODO: Add functi…
Cal-L Mar 14, 2022
e498ccb
Fix TS errors. Complete v2 of Token Detection
Cal-L Mar 15, 2022
9e55c21
Support detected tokens on balance and rates controllers
Cal-L Mar 21, 2022
e941866
Add missing AbortController
Cal-L Mar 22, 2022
edb8d6b
Remove lock for fetching token metadata
Cal-L Mar 30, 2022
650fc32
converting chainId from hex to decimal for fetchTokenMetadata for ext…
NiranjanaBinoy Mar 30, 2022
807f290
Clean up token detection util function. Expose for consuming apps to …
Cal-L Mar 31, 2022
7eca4fb
Account for networks when storing token list in TokenListController. …
Cal-L Apr 1, 2022
6726fa5
Remove comment
Cal-L Apr 3, 2022
769bca4
Format aggregator names when adding token
Cal-L Apr 3, 2022
08a2561
Re-add contract metadata
Cal-L Apr 5, 2022
e13cecc
Fix all tests. Update tokens controller to dedupe imported tokens
Cal-L Apr 6, 2022
3f54f63
Fix token detection tests - WIP
Cal-L Apr 6, 2022
1057095
Fix non-BN call in AssetsContractController
Cal-L Apr 6, 2022
301d1a4
Provide token detection controller with token list controller listener
Cal-L Apr 8, 2022
f1a594a
Fix github checks
Cal-L Apr 8, 2022
e92f296
Fix yarn liny
Cal-L Apr 8, 2022
50df7ad
Increase test coverage
Cal-L Apr 8, 2022
f3d32b0
Fix lint and tests
Cal-L Apr 10, 2022
969ba69
fixing rebase errors
NiranjanaBinoy Apr 27, 2022
7607290
Merge branch 'main' of https://github.com/MetaMask/controllers into f…
Cal-L Apr 27, 2022
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
12 changes: 10 additions & 2 deletions src/ComposableController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ describe('ComposableController', () => {
const assetContractController = new AssetsContractController({
onPreferencesStateChange: (listener) =>
preferencesController.subscribe(listener),
onNetworkStateChange: (listener) =>
networkController.subscribe(listener),
});
const collectiblesController = new CollectiblesController({
onPreferencesStateChange: (listener) =>
Expand Down Expand Up @@ -152,6 +154,8 @@ describe('ComposableController', () => {
allIgnoredTokens: {},
suggestedAssets: [],
tokens: [],
detectedTokens: [],
allDetectedTokens: {},
},
EnsController: {
ensEntries: {},
Expand All @@ -169,7 +173,7 @@ describe('ComposableController', () => {
ipfsGateway: 'https://ipfs.io/ipfs/',
lostIdentities: {},
selectedAddress: '',
useStaticTokenList: false,
useTokenDetection: false,
useCollectibleDetection: false,
openSeaEnabled: false,
},
Expand All @@ -182,6 +186,8 @@ describe('ComposableController', () => {
const assetContractController = new AssetsContractController({
onPreferencesStateChange: (listener) =>
preferencesController.subscribe(listener),
onNetworkStateChange: (listener) =>
networkController.subscribe(listener),
});
const collectiblesController = new CollectiblesController({
onPreferencesStateChange: (listener) =>
Expand Down Expand Up @@ -234,14 +240,16 @@ describe('ComposableController', () => {
ignoredCollectibles: [],
ignoredTokens: [],
allIgnoredTokens: {},
detectedTokens: [],
allDetectedTokens: {},
ipfsGateway: 'https://ipfs.io/ipfs/',
lostIdentities: {},
network: 'loading',
isCustomNetwork: false,
properties: { isEIP1559Compatible: false },
provider: { type: 'mainnet', chainId: NetworksChainId.mainnet },
selectedAddress: '',
useStaticTokenList: false,
useTokenDetection: false,
useCollectibleDetection: false,
openSeaEnabled: false,
suggestedAssets: [],
Expand Down
80 changes: 50 additions & 30 deletions src/apis/token-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ describe('Token service', () => {
expect(token).toStrictEqual(sampleToken);
});

it('should return undefined if the fetch is aborted', async () => {
it('should throw error if the fetch is aborted', async () => {
const abortController = new AbortController();
nock(TOKEN_END_POINT_API)
.get(`/tokens/${NetworksChainId.mainnet}`)
Expand All @@ -242,49 +242,55 @@ describe('Token service', () => {
.reply(200, sampleTokenList)
.persist();

const fetchPromise = fetchTokenMetadata(
NetworksChainId.mainnet,
'0x514910771af9ca656af840dff83e8264ecf986ca',
abortController.signal,
await expect(async () => {
await fetchTokenMetadata(
NetworksChainId.mainnet,
'0x514910771af9ca656af840dff83e8264ecf986ca',
abortController.signal,
);
abortController.abort();
}).rejects.toThrow(
`TokenService Error: No response from fetchTokenMetadata`,
);
abortController.abort();

expect(await fetchPromise).toBeUndefined();
});

it('should return undefined if the fetch fails with a network error', async () => {
it('should throw error if the fetch fails with a network error', async () => {
const { signal } = new AbortController();
nock(TOKEN_END_POINT_API)
.get(`/tokens/${NetworksChainId.mainnet}`)
.replyWithError('Example network error')
.persist();

const result = await fetchTokenMetadata(
NetworksChainId.mainnet,
'0x514910771af9ca656af840dff83e8264ecf986ca',
signal,
await expect(async () => {
await fetchTokenMetadata(
NetworksChainId.mainnet,
'0x514910771af9ca656af840dff83e8264ecf986ca',
signal,
);
}).rejects.toThrow(
`TokenService Error: No response from fetchTokenMetadata`,
);

expect(result).toBeUndefined();
});

it('should return undefined if the fetch fails with an unsuccessful status code', async () => {
it('should throw error if the fetch fails with an unsuccessful status code', async () => {
const { signal } = new AbortController();
nock(TOKEN_END_POINT_API)
.get(`/tokens/${NetworksChainId.mainnet}`)
.reply(500)
.persist();

const result = await fetchTokenMetadata(
NetworksChainId.mainnet,
'0x514910771af9ca656af840dff83e8264ecf986ca',
signal,
await expect(async () => {
await fetchTokenMetadata(
NetworksChainId.mainnet,
'0x514910771af9ca656af840dff83e8264ecf986ca',
signal,
);
}).rejects.toThrow(
`TokenService Error: No response from fetchTokenMetadata`,
);

expect(result).toBeUndefined();
});

it('should return undefined if the fetch fails with a timeout', async () => {
it('should throw error if the fetch fails with a timeout', async () => {
const { signal } = new AbortController();
nock(TOKEN_END_POINT_API)
.get(`/tokens/${NetworksChainId.mainnet}`)
Expand All @@ -293,14 +299,28 @@ describe('Token service', () => {
.reply(200, sampleTokenList)
.persist();

const result = await fetchTokenMetadata(
NetworksChainId.mainnet,
'0x514910771af9ca656af840dff83e8264ecf986ca',
signal,
{ timeout: ONE_MILLISECOND },
await expect(async () => {
await fetchTokenMetadata(
NetworksChainId.mainnet,
'0x514910771af9ca656af840dff83e8264ecf986ca',
signal,
{ timeout: ONE_MILLISECOND },
);
}).rejects.toThrow(
`TokenService Error: No response from fetchTokenMetadata`,
);

expect(result).toBeUndefined();
});
});

it('should call the tokens api and return undefined', async () => {
const { signal } = new AbortController();
nock(TOKEN_END_POINT_API)
.get(`/tokens/${NetworksChainId.mainnet}`)
.reply(404, undefined)
.persist();

const tokens = await fetchTokenList(NetworksChainId.mainnet, signal);

expect(tokens).toBeUndefined();
});
});
8 changes: 4 additions & 4 deletions src/apis/token-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,18 @@ export async function fetchTokenList(
* @param options.timeout - The fetch timeout.
* @returns The token metadata, or `undefined` if the request was cancelled.
*/
export async function fetchTokenMetadata(
export async function fetchTokenMetadata<T>(
chainId: string,
tokenAddress: string,
abortSignal: AbortSignal,
{ timeout = defaultTimeout } = {},
): Promise<unknown> {
): Promise<T> {
const tokenMetadataURL = getTokenMetadataURL(chainId, tokenAddress);
const response = await queryApi(tokenMetadataURL, abortSignal, timeout);
if (response) {
return parseJsonResponse(response);
return parseJsonResponse(response) as Promise<T>;
}
return undefined;
throw new Error(`TokenService Error: No response from fetchTokenMetadata`);
}

/**
Expand Down
57 changes: 56 additions & 1 deletion src/assets/AssetsContractController.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import HttpProvider from 'ethjs-provider-http';
import { IPFS_DEFAULT_GATEWAY_URL } from '../constants';
import { SupportedTokenDetectionNetworks } from '../util';
import { PreferencesController } from '../user/PreferencesController';
import { AssetsContractController } from './AssetsContractController';
import { NetworkController } from '../network/NetworkController';
import {
AssetsContractController,
MISSING_PROVIDER_ERROR,
} from './AssetsContractController';

const MAINNET_PROVIDER = new HttpProvider(
'https://mainnet.infura.io/v3/341eacb578dd44a1a049cbc5f6fd4035',
Expand All @@ -19,29 +24,36 @@ const TEST_ACCOUNT_PUBLIC_ADDRESS =
describe('AssetsContractController', () => {
let assetsContract: AssetsContractController;
let preferences: PreferencesController;
let network: NetworkController;

beforeEach(() => {
preferences = new PreferencesController();
network = new NetworkController();
assetsContract = new AssetsContractController({
onPreferencesStateChange: (listener) => preferences.subscribe(listener),
onNetworkStateChange: (listener) => network.subscribe(listener),
});
});

it('should set default config', () => {
expect(assetsContract.config).toStrictEqual({
chainId: SupportedTokenDetectionNetworks.mainnet,
ipfsGateway: IPFS_DEFAULT_GATEWAY_URL,
provider: undefined,
});
});

it('should update the ipfsGateWay config value when this value is changed in the preferences controller', () => {
expect(assetsContract.config).toStrictEqual({
chainId: SupportedTokenDetectionNetworks.mainnet,
ipfsGateway: IPFS_DEFAULT_GATEWAY_URL,
provider: undefined,
});

preferences.setIpfsGateway('newIPFSGateWay');
expect(assetsContract.config).toStrictEqual({
ipfsGateway: 'newIPFSGateWay',
chainId: SupportedTokenDetectionNetworks.mainnet,
provider: undefined,
});
});
Expand All @@ -52,6 +64,24 @@ describe('AssetsContractController', () => {
);
});

it('should throw missing provider error when getting ERC-20 token balance when missing provider', async () => {
assetsContract.configure({ provider: undefined });
await expect(
async () =>
await assetsContract.getERC20BalanceOf(
ERC20_UNI_ADDRESS,
TEST_ACCOUNT_PUBLIC_ADDRESS,
),
).rejects.toThrow(MISSING_PROVIDER_ERROR);
});

it('should throw missing provider error when getting ERC-20 token decimal when missing provider', async () => {
assetsContract.configure({ provider: undefined });
await expect(
async () => await assetsContract.getERC20TokenDecimals(ERC20_UNI_ADDRESS),
).rejects.toThrow(MISSING_PROVIDER_ERROR);
});

it('should get balance of ERC-20 token contract correctly', async () => {
assetsContract.configure({ provider: MAINNET_PROVIDER });
const UNIBalance = await assetsContract.getERC20BalanceOf(
Expand All @@ -76,6 +106,17 @@ describe('AssetsContractController', () => {
expect(tokenId).not.toStrictEqual(0);
});

it('should throw missing provider error when getting ERC-721 token standard and details when missing provider', async () => {
assetsContract.configure({ provider: undefined });
await expect(
async () =>
await assetsContract.getTokenStandardAndDetails(
ERC20_UNI_ADDRESS,
TEST_ACCOUNT_PUBLIC_ADDRESS,
),
).rejects.toThrow(MISSING_PROVIDER_ERROR);
});

it('should get ERC-721 collectible tokenURI correctly', async () => {
assetsContract.configure({ provider: MAINNET_PROVIDER });
const tokenId = await assetsContract.getERC721TokenURI(
Expand Down Expand Up @@ -138,6 +179,20 @@ describe('AssetsContractController', () => {
expect(balances[ERC20_DAI_ADDRESS]).not.toStrictEqual(0);
});

it('should throw missing provider error when transfering single ERC-1155 when missing provider', async () => {
assetsContract.configure({ provider: undefined });
await expect(
async () =>
await assetsContract.transferSingleERC1155(
ERC1155_ADDRESS,
TEST_ACCOUNT_PUBLIC_ADDRESS,
TEST_ACCOUNT_PUBLIC_ADDRESS,
ERC1155_ID,
'1',
),
).rejects.toThrow(MISSING_PROVIDER_ERROR);
});

it('should get the balance of a ERC-1155 collectible for a given address', async () => {
assetsContract.configure({ provider: MAINNET_PROVIDER });
const balance = await assetsContract.getERC1155BalanceOf(
Expand Down
Loading