Skip to content

Commit

Permalink
feat: Fetch token allowlists from verified tokens API (#2102)
Browse files Browse the repository at this point in the history
  • Loading branch information
luads committed Aug 28, 2024
1 parent 6c8ea1b commit 8071264
Show file tree
Hide file tree
Showing 14 changed files with 650 additions and 328 deletions.
1 change: 1 addition & 0 deletions packages/checkout/sdk/src/api/blockscout/blockscoutType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface BlockscoutTokenData {
decimals: string
name: string
symbol: string
icon_url: string;
type: BlockscoutTokenType
}

Expand Down
88 changes: 43 additions & 45 deletions packages/checkout/sdk/src/balances/balances.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,15 +315,13 @@ describe('balances', () => {
});

it('should fail if no wallet address or provider are given', async () => {
jest.spyOn(Blockscout, 'isChainSupported').mockReturnValue(false);

let message;
try {
await getAllBalances(
{
remote: {
getTokensConfig: () => ({
blockscout: false,
}),
},
remote: {},
networkMap: testCheckoutConfig.networkMap,
} as unknown as CheckoutConfiguration,
undefined,
Expand All @@ -337,15 +335,13 @@ describe('balances', () => {
});

it('should fail if no provider is given and indexer is disabled', async () => {
jest.spyOn(Blockscout, 'isChainSupported').mockReturnValue(false);

let message;
try {
await getAllBalances(
{
remote: {
getTokensConfig: () => ({
blockscout: false,
}),
},
remote: {},
networkMap: testCheckoutConfig.networkMap,
} as unknown as CheckoutConfiguration,
undefined,
Expand All @@ -359,12 +355,11 @@ describe('balances', () => {
});

it('should call getBalance and getERC20Balance functions with native and ERC20 tokens', async () => {
jest.spyOn(Blockscout, 'isChainSupported').mockReturnValue(false);

const getAllBalancesResult = await getAllBalances(
{
remote: {
getTokensConfig: () => ({
blockscout: false,
}),
getHttpClient: () => mockedHttpClient,
},
networkMap: testCheckoutConfig.networkMap,
Expand Down Expand Up @@ -415,7 +410,24 @@ describe('balances', () => {
);
});

it('should call getIndexerBalance', async () => {
it('should call getBlockscoutBalance', async () => {
(tokens.getTokenAllowList as jest.Mock).mockReturnValue({
tokens: [
{
name: 'Immutable X',
address: 'native',
symbol: 'IMX',
decimals: 18,
} as TokenInfo,
{
name: ChainName.ETHEREUM,
address: '0x65AA7a21B0f3ce9B478aAC3408fE75b423939b1F',
symbol: 'ETH',
decimals: 18,
} as TokenInfo,
],
});

getTokensByWalletAddressMock = jest.fn().mockResolvedValue({
items: [
{
Expand Down Expand Up @@ -451,9 +463,6 @@ describe('balances', () => {
const getAllBalancesResult = await getAllBalances(
{
remote: {
getTokensConfig: () => ({
blockscout: true,
}),
getHttpClient: () => mockedHttpClient,
},
networkMap: testCheckoutConfig.networkMap,
Expand Down Expand Up @@ -491,7 +500,7 @@ describe('balances', () => {
]);
});

it('should call getIndexerBalance with undefined filterTokens', async () => {
it('should call getBlockscoutBalance with undefined filterTokens', async () => {
getTokenAllowListMock = jest.fn().mockReturnValue({
tokens: [],
} as GetTokenAllowListResult);
Expand Down Expand Up @@ -534,9 +543,6 @@ describe('balances', () => {
const getAllBalancesResult = await getAllBalances(
{
remote: {
getTokensConfig: () => ({
blockscout: true,
}),
getHttpClient: () => mockedHttpClient,
},
networkMap: new CheckoutConfiguration(
Expand All @@ -557,7 +563,7 @@ describe('balances', () => {
expect(getAllBalancesResult.balances).toEqual([]);
});

it('should call getIndexerBalance and return native balance on ERC20 404', async () => {
it('should call getBlockscoutBalance and return native balance on ERC20 404', async () => {
getTokensByWalletAddressMock = jest.fn().mockRejectedValue(
{ code: HttpStatusCode.NotFound, message: 'not found' },
);
Expand All @@ -580,9 +586,6 @@ describe('balances', () => {
const getAllBalancesResult = await getAllBalances(
{
remote: {
getTokensConfig: () => ({
blockscout: true,
}),
getHttpClient: () => mockedHttpClient,
},
networkMap: testCheckoutConfig.networkMap,
Expand All @@ -609,7 +612,18 @@ describe('balances', () => {
]);
});

it('should call getIndexerBalance and return ERC20 balances on native 404', async () => {
it('should call getBlockscoutBalance and return ERC20 balances on native 404', async () => {
(tokens.getTokenAllowList as jest.Mock).mockReturnValue({
tokens: [
{
name: ChainName.ETHEREUM,
address: '0x65AA7a21B0f3ce9B478aAC3408fE75b423939b1F',
symbol: 'ETH',
decimals: 18,
} as TokenInfo,
],
});

getTokensByWalletAddressMock = jest.fn().mockResolvedValue({
items: [
{
Expand Down Expand Up @@ -639,9 +653,6 @@ describe('balances', () => {
const getAllBalancesResult = await getAllBalances(
{
remote: {
getTokensConfig: () => ({
blockscout: true,
}),
getHttpClient: () => mockedHttpClient,
},
networkMap: testCheckoutConfig.networkMap,
Expand Down Expand Up @@ -669,7 +680,7 @@ describe('balances', () => {
]);
});

it('should call getIndexerBalance and return empty balance due to 404', async () => {
it('should call getBlockscoutBalance and return empty balance due to 404', async () => {
getTokensByWalletAddressMock = jest.fn().mockRejectedValue(
{ code: HttpStatusCode.NotFound, message: 'not found' },
);
Expand All @@ -686,9 +697,6 @@ describe('balances', () => {
const getAllBalancesResult = await getAllBalances(
{
remote: {
getTokensConfig: () => ({
blockscout: true,
}),
getHttpClient: () => mockedHttpClient,
},
networkMap: testCheckoutConfig.networkMap,
Expand Down Expand Up @@ -720,9 +728,6 @@ describe('balances', () => {
getAllBalancesResult = await getAllBalances(
{
remote: {
getTokensConfig: () => ({
blockscout: true,
}),
getHttpClient: () => mockedHttpClient,
},
networkMap: testCheckoutConfig.networkMap,
Expand Down Expand Up @@ -780,7 +785,7 @@ describe('balances', () => {
}];

testCases.forEach(async (testCase) => {
it('should call getIndexerBalance and throw error', async () => {
it('should call getBlockscoutBalance and throw error', async () => {
getTokensByWalletAddressMock = jest.fn().mockRejectedValue(
{ code: HttpStatusCode.Forbidden, message: testCase.errorMessage },
);
Expand All @@ -797,9 +802,6 @@ describe('balances', () => {
await getAllBalances(
{
remote: {
getTokensConfig: () => ({
blockscout: true,
}),
getHttpClient: () => mockedHttpClient,
},
networkMap: testCheckoutConfig.networkMap,
Expand Down Expand Up @@ -831,11 +833,7 @@ describe('balances', () => {
try {
await getAllBalances(
{
remote: {
getTokensConfig: () => ({
blockscout: true,
}),
},
remote: {},
networkMap: testCheckoutConfig.networkMap,
} as unknown as CheckoutConfiguration,
jest.fn() as unknown as Web3Provider,
Expand Down
38 changes: 13 additions & 25 deletions packages/checkout/sdk/src/balances/balances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import {
import { CheckoutError, CheckoutErrorType, withCheckoutError } from '../errors';
import { getNetworkInfo } from '../network';
import { getERC20TokenInfo, getTokenAllowList } from '../tokens';
import { CheckoutConfiguration, getL1ChainId } from '../config';
import { CheckoutConfiguration } from '../config';
import {
Blockscout,
BlockscoutToken,
BlockscoutTokenData,
BlockscoutTokens,
BlockscoutTokenType,
} from '../api/blockscout';
Expand Down Expand Up @@ -96,7 +97,7 @@ const blockscoutClientMap: Map<ChainId, Blockscout> = new Map();
// blockscout map and therefore clear all the cache.
export const resetBlockscoutClientMap = () => blockscoutClientMap.clear();

export const getIndexerBalance = async (
export const getBlockscoutBalance = async (
config: CheckoutConfiguration,
walletAddress: string,
chainId: ChainId,
Expand All @@ -107,7 +108,7 @@ export const getIndexerBalance = async (
const shouldFilter = filterTokens !== undefined;
const mapFilterTokens = Object.assign(
{},
...((filterTokens ?? []).map((t) => ({ [t.address || NATIVE]: t }))),
...((filterTokens ?? []).map((t) => ({ [t.address?.toLowerCase() || NATIVE]: t }))),
);

// Get blockscout client for the given chain
Expand Down Expand Up @@ -186,7 +187,8 @@ export const getIndexerBalance = async (

const balances: GetBalanceResult[] = [];
items.forEach((item) => {
if (shouldFilter && !mapFilterTokens[item.token.address]) return;
const allowlistedToken = mapFilterTokens[item.token.address.toLowerCase()];
if (shouldFilter && !allowlistedToken) return;

const tokenData = item.token || {};

Expand All @@ -196,9 +198,12 @@ export const getIndexerBalance = async (
let decimals = parseInt(tokenData.decimals, 10);
if (Number.isNaN(decimals)) decimals = DEFAULT_TOKEN_DECIMALS;

const icon = (tokenData as BlockscoutTokenData).icon_url ?? allowlistedToken.icon;

const token = {
...tokenData,
decimals,
icon,
};

const formattedBalance = utils.formatUnits(item.value, token.decimals);
Expand Down Expand Up @@ -304,35 +309,18 @@ export const getAllBalances = async (
},
);

// In order to prevent unnecessary RPC calls
// let's use the Indexer if available for the
// given chain.
let flag = false;
try {
flag = (await config.remote.getTokensConfig(chainId)).blockscout || flag;
} catch (err: any) {
// eslint-disable-next-line no-console
console.error(err);
}

if (forceFetch) {
resetBlockscoutClientMap();
}

let address = walletAddress;
if (flag && Blockscout.isChainSupported(chainId)) {
// This is a hack because the widgets are still using the tokens symbol
// to drive the conversions. If we remove all the token symbols from e.g. zkevm
// then we would not have fiat conversions.
// Please remove this hack once https://immutable.atlassian.net/browse/WT-1710
// is done.
const isL1Chain = getL1ChainId(config) === chainId;
if (!address) address = await web3Provider?.getSigner().getAddress();
if (Blockscout.isChainSupported(chainId)) {
const address = walletAddress ?? await web3Provider?.getSigner().getAddress();

try {
return await measureAsyncExecution<GetAllBalancesResult>(
config,
`Time to fetch balances using blockscout for ${chainId}`,
getIndexerBalance(config, address!, chainId, isL1Chain ? tokens : undefined),
getBlockscoutBalance(config, address!, chainId, tokens),
);
} catch (error) {
// Blockscout rate limiting, fallback to RPC node
Expand Down
8 changes: 8 additions & 0 deletions packages/checkout/sdk/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
SANDBOX_CHAIN_ID_NETWORK_MAP,
} from '../env';
import { HttpClient } from '../api/http/httpClient';
import { TokensFetcher } from './tokensFetcher';

export class CheckoutConfigurationError extends Error {
public message: string;
Expand Down Expand Up @@ -67,6 +68,8 @@ export class CheckoutConfiguration {

readonly remote: RemoteConfigFetcher;

readonly tokens: TokensFetcher;

readonly environment: Environment;

readonly networkMap: NetworkMap;
Expand Down Expand Up @@ -98,5 +101,10 @@ export class CheckoutConfiguration {
isDevelopment: this.isDevelopment,
isProduction: this.isProduction,
});

this.tokens = new TokensFetcher(httpClient, this.remote, {
isDevelopment: this.isDevelopment,
isProduction: this.isProduction,
});
}
}
Loading

0 comments on commit 8071264

Please sign in to comment.