From 4aa9a47ba72838da88cfe029f4f042cd54e7df3b Mon Sep 17 00:00:00 2001 From: Charlie McKenzie Date: Thu, 21 Mar 2024 16:47:32 +1100 Subject: [PATCH] fix: Smart checkout balance token information (#1612) Co-authored-by: Mimi Immutable --- .../balanceCheck/balanceCheck.ts | 13 +- .../balanceCheck/balanceRequirement.test.ts | 138 +++++++++++------- .../balanceCheck/balanceRequirement.ts | 123 ++++++++++++---- 3 files changed, 186 insertions(+), 88 deletions(-) diff --git a/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceCheck.ts b/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceCheck.ts index 19636fc559..d249066e09 100644 --- a/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceCheck.ts +++ b/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceCheck.ts @@ -158,14 +158,17 @@ export const balanceCheck = async ( // Wait for all balances and calculate the requirements const promisesResponses = await Promise.all(balancePromises); - const balanceRequirements: BalanceRequirement[] = []; + const erc721BalanceRequirements: BalanceRequirement[] = []; + const tokenBalanceRequirementPromises: Promise[] = []; // Get all ERC20 and NATIVE balances if (requiredToken.length > 0 && promisesResponses.length > 0) { const result = promisesResponses.shift(); if (result) { requiredToken.forEach((item) => { - balanceRequirements.push(getTokenBalanceRequirement(item as (NativeItem | ERC20Item), result)); + tokenBalanceRequirementPromises.push( + getTokenBalanceRequirement(item as (NativeItem | ERC20Item), result, provider), + ); }); } } @@ -175,10 +178,14 @@ export const balanceCheck = async ( const result = promisesResponses.shift(); if (result) { requiredERC721.forEach((item) => { - balanceRequirements.push(getERC721BalanceRequirement(item as (ERC721Item), result)); + erc721BalanceRequirements.push(getERC721BalanceRequirement(item as (ERC721Item), result)); }); } } + const balanceRequirements = [ + ...erc721BalanceRequirements, + ...(await Promise.all(tokenBalanceRequirementPromises)), + ]; // Find if there are any requirements that aren't sufficient. // If there is not item with sufficient === false then the requirements diff --git a/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceRequirement.test.ts b/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceRequirement.test.ts index 840a105607..c01acc451d 100644 --- a/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceRequirement.test.ts +++ b/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceRequirement.test.ts @@ -1,6 +1,13 @@ import { BigNumber } from 'ethers'; +import { Web3Provider } from '@ethersproject/providers'; import { - ERC20Item, ERC721Item, ItemBalance, ItemRequirement, ItemType, NativeItem, + ChainId, + ERC20Item, + ERC721Item, + ItemBalance, + ItemRequirement, + ItemType, + NativeItem, } from '../../types'; import { getERC721BalanceRequirement, @@ -63,30 +70,28 @@ describe('balanceRequirement', () => { }, ]; const result = getERC721BalanceRequirement(itemRequirement, balances); - expect(result).toEqual( - { - sufficient: true, + expect(result).toEqual({ + sufficient: true, + type: ItemType.ERC721, + delta: { + balance: BigNumber.from(0), + formattedBalance: '0', + }, + required: { type: ItemType.ERC721, - delta: { - balance: BigNumber.from(0), - formattedBalance: '0', - }, - required: { - type: ItemType.ERC721, - balance: BigNumber.from(1), - formattedBalance: '1', - contractAddress: '0xERC721', - id: '0', - }, - current: { - type: ItemType.ERC721, - balance: BigNumber.from(1), - formattedBalance: '1', - contractAddress: '0xERC721', - id: '0', - }, + balance: BigNumber.from(1), + formattedBalance: '1', + contractAddress: '0xERC721', + id: '0', }, - ); + current: { + type: ItemType.ERC721, + balance: BigNumber.from(1), + formattedBalance: '1', + contractAddress: '0xERC721', + id: '0', + }, + }); }); it('should return sufficient false if does not meet requirement for ERC721', () => { @@ -110,35 +115,46 @@ describe('balanceRequirement', () => { }, ]; const result = getERC721BalanceRequirement(itemRequirement, balances); - expect(result).toEqual( - { - sufficient: false, + expect(result).toEqual({ + sufficient: false, + type: ItemType.ERC721, + delta: { + balance: BigNumber.from(1), + formattedBalance: '1', + }, + required: { type: ItemType.ERC721, - delta: { - balance: BigNumber.from(1), - formattedBalance: '1', - }, - required: { - type: ItemType.ERC721, - balance: BigNumber.from(1), - formattedBalance: '1', - contractAddress: '0xERC721', - id: '0', - }, - current: { - type: ItemType.ERC721, - balance: BigNumber.from(0), - formattedBalance: '0', - contractAddress: '0xERC721', - id: '0', - }, + balance: BigNumber.from(1), + formattedBalance: '1', + contractAddress: '0xERC721', + id: '0', }, - ); + current: { + type: ItemType.ERC721, + balance: BigNumber.from(0), + formattedBalance: '0', + contractAddress: '0xERC721', + id: '0', + }, + }); }); }); describe('getTokenBalanceRequirement', () => { - it('should return sufficient true if meets requirements for NATIVE', () => { + let mockProvider: Web3Provider; + + beforeEach(() => { + mockProvider = { + getSigner: jest.fn().mockReturnValue({ + getAddress: jest.fn().mockResolvedValue('0xADDRESS'), + }), + network: { + chainId: ChainId.ETHEREUM, + }, + } as unknown as Web3Provider; + }); + + it('should return sufficient true if meets requirements for NATIVE', async () => { const itemRequirement: NativeItem = { type: ItemType.NATIVE, amount: BigNumber.from('1000000000000000000'), @@ -156,7 +172,11 @@ describe('balanceRequirement', () => { }, ]; - const result = getTokenBalanceRequirement(itemRequirement, balances); + const result = await getTokenBalanceRequirement( + itemRequirement, + balances, + mockProvider, + ); expect(result).toEqual({ sufficient: true, type: ItemType.NATIVE, @@ -187,7 +207,7 @@ describe('balanceRequirement', () => { }); }); - it('should return sufficient true if meets requirements for ERC20', () => { + it('should return sufficient true if meets requirements for ERC20', async () => { const itemRequirement: ERC20Item = { type: ItemType.ERC20, tokenAddress: '0xERC20', @@ -208,7 +228,11 @@ describe('balanceRequirement', () => { }, ]; - const result = getTokenBalanceRequirement(itemRequirement, balances); + const result = await getTokenBalanceRequirement( + itemRequirement, + balances, + mockProvider, + ); expect(result).toEqual({ sufficient: true, type: ItemType.ERC20, @@ -241,7 +265,7 @@ describe('balanceRequirement', () => { }); }); - it('should return sufficient false if requirements not met for NATIVE', () => { + it('should return sufficient false if requirements not met for NATIVE', async () => { const itemRequirement: NativeItem = { type: ItemType.NATIVE, amount: BigNumber.from('1000000000000000000'), @@ -270,7 +294,11 @@ describe('balanceRequirement', () => { }, ]; - const result = getTokenBalanceRequirement(itemRequirement, balances); + const result = await getTokenBalanceRequirement( + itemRequirement, + balances, + mockProvider, + ); expect(result).toEqual({ sufficient: false, type: ItemType.NATIVE, @@ -301,7 +329,7 @@ describe('balanceRequirement', () => { }); }); - it('should return sufficient false if requirements not met for ERC20', () => { + it('should return sufficient false if requirements not met for ERC20', async () => { const itemRequirement: ERC20Item = { type: ItemType.ERC20, tokenAddress: '0xERC20', @@ -332,7 +360,11 @@ describe('balanceRequirement', () => { }, ]; - const result = getTokenBalanceRequirement(itemRequirement, balances); + const result = await getTokenBalanceRequirement( + itemRequirement, + balances, + mockProvider, + ); expect(result).toEqual({ sufficient: false, type: ItemType.ERC20, diff --git a/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceRequirement.ts b/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceRequirement.ts index 2e5e027652..ba4089effd 100644 --- a/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceRequirement.ts +++ b/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceRequirement.ts @@ -1,5 +1,6 @@ /* eslint-disable arrow-body-style */ -import { BigNumber, utils } from 'ethers'; +import { BigNumber, Contract, utils } from 'ethers'; +import { Web3Provider } from '@ethersproject/providers'; import { ERC20Item, ERC721Balance, @@ -16,28 +17,34 @@ import { BalanceERC721Requirement, BalanceNativeRequirement, } from './types'; -import { DEFAULT_TOKEN_DECIMALS, NATIVE, ZKEVM_NATIVE_TOKEN } from '../../env'; +import { + DEFAULT_TOKEN_DECIMALS, + ERC20ABI, + NATIVE, + ZKEVM_NATIVE_TOKEN, +} from '../../env'; import { isNativeToken } from '../../tokens'; import { isMatchingAddress } from '../../utils/utils'; -export const getTokensFromRequirements = (itemRequirements: ItemRequirement[]): TokenInfo[] => itemRequirements - .map((itemRequirement) => { - switch (itemRequirement.type) { - case ItemType.ERC20: - return { - address: itemRequirement.tokenAddress, - } as TokenInfo; - case ItemType.NATIVE: - return { - address: NATIVE, - } as TokenInfo; - case ItemType.ERC721: - default: - return { - address: itemRequirement.contractAddress, - } as TokenInfo; - } - }); +export const getTokensFromRequirements = ( + itemRequirements: ItemRequirement[], +): TokenInfo[] => itemRequirements.map((itemRequirement) => { + switch (itemRequirement.type) { + case ItemType.ERC20: + return { + address: itemRequirement.tokenAddress, + } as TokenInfo; + case ItemType.NATIVE: + return { + address: NATIVE, + } as TokenInfo; + case ItemType.ERC721: + default: + return { + address: itemRequirement.contractAddress, + } as TokenInfo; + } +}); /** * Gets the balance requirement with delta for an ERC721 requirement. @@ -45,20 +52,27 @@ export const getTokensFromRequirements = (itemRequirements: ItemRequirement[]): export const getERC721BalanceRequirement = ( itemRequirement: ERC721Item, balances: ItemBalance[], -) : BalanceERC721Requirement => { +): BalanceERC721Requirement => { const requiredBalance = BigNumber.from(1); // Find the requirements related balance const itemBalanceResult = balances.find((balance) => { const balanceERC721Result = balance as ERC721Balance; - return isMatchingAddress(balanceERC721Result.contractAddress, itemRequirement.contractAddress) - && balanceERC721Result.id === itemRequirement.id; + return ( + isMatchingAddress( + balanceERC721Result.contractAddress, + itemRequirement.contractAddress, + ) && balanceERC721Result.id === itemRequirement.id + ); }); // Calculate the balance delta - const sufficient = (requiredBalance.isNegative() || requiredBalance.isZero()) + const sufficient = requiredBalance.isNegative() + || requiredBalance.isZero() || (itemBalanceResult?.balance.gte(requiredBalance) ?? false); - const delta = requiredBalance.sub(itemBalanceResult?.balance ?? BigNumber.from(0)); + const delta = requiredBalance.sub( + itemBalanceResult?.balance ?? BigNumber.from(0), + ); let erc721BalanceResult = itemBalanceResult as ERC721Balance; if (!erc721BalanceResult) { erc721BalanceResult = { @@ -88,16 +102,20 @@ export const getERC721BalanceRequirement = ( /** * Gets the balance requirement for a NATIVE or ERC20 requirement. */ -export const getTokenBalanceRequirement = ( +export const getTokenBalanceRequirement = async ( itemRequirement: ERC20Item | NativeItem, balances: ItemBalance[], -) : BalanceNativeRequirement | BalanceERC20Requirement => { + provider: Web3Provider, +): Promise => { let itemBalanceResult: ItemBalance | undefined; // Get the requirements related balance if (itemRequirement.type === ItemType.ERC20) { itemBalanceResult = balances.find((balance) => { - return isMatchingAddress((balance as TokenBalance).token?.address, itemRequirement.tokenAddress); + return isMatchingAddress( + (balance as TokenBalance).token?.address, + itemRequirement.tokenAddress, + ); }); } else if (itemRequirement.type === ItemType.NATIVE) { itemBalanceResult = balances.find((balance) => { @@ -107,17 +125,44 @@ export const getTokenBalanceRequirement = ( // Calculate the balance delta const requiredBalance: BigNumber = itemRequirement.amount; - const sufficient = (requiredBalance.isNegative() || requiredBalance.isZero()) + const sufficient = requiredBalance.isNegative() + || requiredBalance.isZero() || (itemBalanceResult?.balance.gte(requiredBalance) ?? false); - const delta = requiredBalance.sub(itemBalanceResult?.balance ?? BigNumber.from(0)); + const delta = requiredBalance.sub( + itemBalanceResult?.balance ?? BigNumber.from(0), + ); let name = ''; let symbol = ''; let decimals = DEFAULT_TOKEN_DECIMALS; if (itemBalanceResult) { - decimals = (itemBalanceResult as TokenBalance).token?.decimals ?? DEFAULT_TOKEN_DECIMALS; + decimals = (itemBalanceResult as TokenBalance).token?.decimals + ?? DEFAULT_TOKEN_DECIMALS; name = (itemBalanceResult as TokenBalance).token.name; symbol = (itemBalanceResult as TokenBalance).token.symbol; + } else if (itemRequirement.type === ItemType.ERC20) { + // Missing item balance so we need to query contract + try { + const contract = new Contract( + itemRequirement.tokenAddress, + JSON.stringify(ERC20ABI), + provider, + ); + const [contractName, contractSymbol, contractDecimals] = await Promise.all([ + contract.name(), + contract.symbol(), + contract.decimals(), + ]); + decimals = contractDecimals; + name = contractName; + symbol = contractSymbol; + } catch (error) { + // eslint-disable-next-line no-console + console.error( + 'Failed to query contract information', + itemRequirement.tokenAddress, + ); + } } let tokenBalanceResult = itemBalanceResult as TokenBalance; @@ -174,9 +219,23 @@ export const getTokenBalanceRequirement = ( balance: delta, formattedBalance: utils.formatUnits(delta, decimals), }, - current: tokenBalanceResult, + current: { + ...tokenBalanceResult, + token: { + address: itemRequirement.tokenAddress, + name, + symbol, + decimals, + }, + }, required: { ...tokenBalanceResult, + token: { + address: itemRequirement.tokenAddress, + name, + symbol, + decimals, + }, balance: BigNumber.from(itemRequirement.amount), formattedBalance: utils.formatUnits(itemRequirement.amount, decimals), },