From 4b800486a986845ba7a77dd0e590f8c26b39b0e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=A3=20de=20Souza?= Date: Fri, 6 Sep 2024 11:02:37 +1000 Subject: [PATCH] feat: Display transaction fees during Primary Sales - CM-781 (#2138) --- .../src/components/SmartCheckoutForm.tsx | 2 +- .../aggregators/allowanceAggregator.test.ts | 8 + .../aggregators/balanceAggregator.test.ts | 176 ++++++++++++++- .../aggregators/balanceAggregator.ts | 4 +- .../aggregators/itemAggregator.test.ts | 158 ++++++++++++- .../aggregators/itemAggregator.ts | 26 ++- .../smartCheckout/allowance/erc1155.test.ts | 3 + .../src/smartCheckout/allowance/erc20.test.ts | 8 + .../smartCheckout/allowance/erc721.test.ts | 3 + .../balanceCheck/balanceCheck.test.ts | 135 ++++++++---- .../balanceCheck/balanceCheck.ts | 73 +++--- .../balanceCheck/balanceRequirement.test.ts | 208 +++++++++++++----- .../balanceCheck/balanceRequirement.ts | 194 +++++++++------- .../src/smartCheckout/balanceCheck/types.ts | 3 + .../sdk/src/smartCheckout/buy/buy.test.ts | 29 +++ .../checkout/sdk/src/smartCheckout/buy/buy.ts | 3 + .../smartCheckout/gas/gasCalculator.test.ts | 10 + .../src/smartCheckout/gas/gasCalculator.ts | 2 + .../bridgeAndSwap/bridgeAndSwapRoute.test.ts | 2 + .../routing/routingCalculator.test.ts | 3 + .../routing/swap/swapRoute.test.ts | 2 + .../src/smartCheckout/smartCheckout.test.ts | 10 + .../checkout/sdk/src/types/smartCheckout.ts | 8 +- .../src/widgets/sale/components/OrderFees.tsx | 12 +- .../widgets/sale/components/OrderReview.tsx | 46 +++- .../sale/functions/fetchFundingBalances.ts | 18 +- .../sale/functions/getTopUpViewData.test.ts | 6 + .../widgets/sale/hooks/useFundingBalances.ts | 7 +- .../src/widgets/sale/views/OrderSummary.tsx | 2 + 29 files changed, 907 insertions(+), 254 deletions(-) diff --git a/packages/checkout/sdk-sample-app/src/components/SmartCheckoutForm.tsx b/packages/checkout/sdk-sample-app/src/components/SmartCheckoutForm.tsx index 51f6586597..704ad254db 100644 --- a/packages/checkout/sdk-sample-app/src/components/SmartCheckoutForm.tsx +++ b/packages/checkout/sdk-sample-app/src/components/SmartCheckoutForm.tsx @@ -174,7 +174,7 @@ export const SmartCheckoutForm = ({ checkout, provider }: SmartCheckoutProps) => } updateItemRequirements({ type: ItemType.NATIVE, - amount + amount, }); } diff --git a/packages/checkout/sdk/src/smartCheckout/aggregators/allowanceAggregator.test.ts b/packages/checkout/sdk/src/smartCheckout/aggregators/allowanceAggregator.test.ts index e142ccc26e..d72ffbd8f2 100644 --- a/packages/checkout/sdk/src/smartCheckout/aggregators/allowanceAggregator.test.ts +++ b/packages/checkout/sdk/src/smartCheckout/aggregators/allowanceAggregator.test.ts @@ -17,6 +17,7 @@ describe('allowanceAggregator', () => { tokenAddress: '0xERC20_1', amount: BigNumber.from(1), spenderAddress: '0xSEAPORT', + isFee: false, }, approvalTransaction: undefined, }, @@ -27,6 +28,7 @@ describe('allowanceAggregator', () => { tokenAddress: '0xERC20_2', amount: BigNumber.from(1), spenderAddress: '0xSEAPORT', + isFee: false, }, }, ], @@ -57,6 +59,7 @@ describe('allowanceAggregator', () => { tokenAddress: '0xERC20_1', amount: BigNumber.from(1), spenderAddress: '0xSEAPORT', + isFee: false, }, approvalTransaction: undefined, }]); @@ -72,6 +75,7 @@ describe('allowanceAggregator', () => { tokenAddress: '0xERC20', amount: BigNumber.from(1), spenderAddress: '0xSEAPORT', + isFee: false, }, }], }; @@ -131,6 +135,7 @@ describe('allowanceAggregator', () => { tokenAddress: '0xERC20', amount: BigNumber.from(1), spenderAddress: '0xSEAPORT', + isFee: false, }, }], }; @@ -189,6 +194,7 @@ describe('allowanceAggregator', () => { tokenAddress: '0xERC20', amount: BigNumber.from(1), spenderAddress: '0xSEAPORT', + isFee: false, }, approvalTransaction: undefined, }], @@ -235,6 +241,7 @@ describe('allowanceAggregator', () => { tokenAddress: '0xERC20', amount: BigNumber.from(1), spenderAddress: '0xSEAPORT', + isFee: false, }, approvalTransaction: undefined, }, @@ -275,6 +282,7 @@ describe('allowanceAggregator', () => { tokenAddress: '0xERC20', amount: BigNumber.from(1), spenderAddress: '0xSEAPORT', + isFee: false, }, }, ], diff --git a/packages/checkout/sdk/src/smartCheckout/aggregators/balanceAggregator.test.ts b/packages/checkout/sdk/src/smartCheckout/aggregators/balanceAggregator.test.ts index c07a18fcd6..5235ab5132 100644 --- a/packages/checkout/sdk/src/smartCheckout/aggregators/balanceAggregator.test.ts +++ b/packages/checkout/sdk/src/smartCheckout/aggregators/balanceAggregator.test.ts @@ -10,22 +10,31 @@ describe('balanceAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, + }, + { + type: ItemType.NATIVE, + amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.ERC721, @@ -43,15 +52,22 @@ describe('balanceAggregator', () => { const aggregatedItems = balanceAggregator(itemRequirements); expect(aggregatedItems).toEqual([ + { + type: ItemType.NATIVE, + amount: BigNumber.from(1), + isFee: true, + }, { type: ItemType.NATIVE, amount: BigNumber.from(2), + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(2), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.ERC721, @@ -68,23 +84,77 @@ describe('balanceAggregator', () => { const itemRequirements: ItemRequirement[] = [ { type: ItemType.NATIVE, + amount: BigNumber.from(2), + isFee: false, + }, + { + type: ItemType.NATIVE, + amount: BigNumber.from(1), + isFee: true, + }, + { + type: ItemType.ERC20, amount: BigNumber.from(1), + tokenAddress: '0xERC20', + spenderAddress: '0xSEAPORT', + isFee: false, + }, + { + type: ItemType.ERC20, + amount: BigNumber.from(1), + tokenAddress: '0xERC20', + spenderAddress: '0xSEAPORT', + isFee: false, + }, + ]; + + const aggregatedItems = erc20BalanceAggregator(itemRequirements); + expect(aggregatedItems).toEqual([ + { + type: ItemType.NATIVE, + amount: BigNumber.from(2), + isFee: false, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.ERC20, + amount: BigNumber.from(2), + tokenAddress: '0xERC20', + spenderAddress: '0xSEAPORT', + isFee: false, + }, + ]); + }); + + it('should not aggregate erc20 items when one is a fee', () => { + const itemRequirements: ItemRequirement[] = [ + { + type: ItemType.NATIVE, + amount: BigNumber.from(1), + isFee: false, + }, + { + type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, + }, + { + type: ItemType.ERC20, + amount: BigNumber.from(2), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: true, }, ]; @@ -93,33 +163,45 @@ describe('balanceAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, + }, + { + type: ItemType.ERC20, + amount: BigNumber.from(1), + tokenAddress: '0xERC20', + spenderAddress: '0xSEAPORT', + isFee: true, }, { type: ItemType.ERC20, amount: BigNumber.from(2), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, ]); }); - it('should not aggregate erc20s', () => { + it('should not aggregate different erc20s', () => { const itemRequirements: ItemRequirement[] = [ { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20_1', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20_2', spenderAddress: '0xSEAPORT', + isFee: false, }, ]; @@ -130,12 +212,14 @@ describe('balanceAggregator', () => { amount: BigNumber.from(1), tokenAddress: '0xERC20_1', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20_2', spenderAddress: '0xSEAPORT', + isFee: false, }, ]); }); @@ -151,23 +235,27 @@ describe('balanceAggregator', () => { const itemRequirements: ItemRequirement[] = [ { type: ItemType.NATIVE, - amount: BigNumber.from(1), + amount: BigNumber.from(2), + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, ]; @@ -176,17 +264,20 @@ describe('balanceAggregator', () => { expect.arrayContaining([ { type: ItemType.NATIVE, - amount: BigNumber.from(1), + amount: BigNumber.from(2), + isFee: false, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.ERC20, amount: BigNumber.from(2), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, ]), ); @@ -197,22 +288,26 @@ describe('balanceAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.NATIVE, - amount: BigNumber.from(1), + amount: BigNumber.from(2), + isFee: true, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: 'ERC1559' as ItemType, @@ -232,16 +327,19 @@ describe('balanceAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.NATIVE, - amount: BigNumber.from(1), + amount: BigNumber.from(2), + isFee: true, }, { type: ItemType.ERC20, amount: BigNumber.from(2), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: 'ERC1559' as ItemType, @@ -264,10 +362,12 @@ describe('balanceAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.ERC721, @@ -288,10 +388,12 @@ describe('balanceAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.ERC721, @@ -307,6 +409,7 @@ describe('balanceAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.ERC721, @@ -327,6 +430,7 @@ describe('balanceAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.ERC721, @@ -355,6 +459,7 @@ describe('balanceAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.ERC721, @@ -365,6 +470,7 @@ describe('balanceAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.ERC721, @@ -380,10 +486,12 @@ describe('balanceAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.ERC721, @@ -399,11 +507,13 @@ describe('balanceAggregator', () => { const itemRequirements: ItemRequirement[] = [ { type: ItemType.NATIVE, - amount: BigNumber.from(1), + amount: BigNumber.from(2), + isFee: false, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.ERC721, @@ -434,11 +544,13 @@ describe('balanceAggregator', () => { expect.arrayContaining([ { type: ItemType.NATIVE, - amount: BigNumber.from(1), + amount: BigNumber.from(2), + isFee: false, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.ERC721, @@ -467,22 +579,26 @@ describe('balanceAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, ]; @@ -493,16 +609,62 @@ describe('balanceAggregator', () => { amount: BigNumber.from(1), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, + }, + { + type: ItemType.ERC20, + amount: BigNumber.from(1), + tokenAddress: '0xERC20', + spenderAddress: '0xSEAPORT', + isFee: false, + }, + { + type: ItemType.NATIVE, + amount: BigNumber.from(2), + isFee: false, + }, + ]); + }); + + it('should not aggregate native fee items', () => { + const itemRequirements: ItemRequirement[] = [ + { + type: ItemType.NATIVE, + amount: BigNumber.from(2), + isFee: false, + }, + { + type: ItemType.NATIVE, + amount: BigNumber.from(1), + isFee: true, + }, + { + type: ItemType.ERC20, + amount: BigNumber.from(1), + tokenAddress: '0xERC20', + spenderAddress: '0xSEAPORT', + isFee: false, + }, + ]; + + const aggregatedItems = nativeAggregator(itemRequirements); + expect(aggregatedItems).toEqual([ + { + type: ItemType.NATIVE, + amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.NATIVE, amount: BigNumber.from(2), + isFee: false, }, ]); }); diff --git a/packages/checkout/sdk/src/smartCheckout/aggregators/balanceAggregator.ts b/packages/checkout/sdk/src/smartCheckout/aggregators/balanceAggregator.ts index deb1bb20ee..a701f7f3d3 100644 --- a/packages/checkout/sdk/src/smartCheckout/aggregators/balanceAggregator.ts +++ b/packages/checkout/sdk/src/smartCheckout/aggregators/balanceAggregator.ts @@ -10,7 +10,7 @@ export const nativeBalanceAggregator = ( itemRequirements.forEach((itemRequirement) => { const { type } = itemRequirement; - if (type !== ItemType.NATIVE) { + if (type !== ItemType.NATIVE || itemRequirement.isFee) { aggregatedItemRequirements.push(itemRequirement); return; } @@ -37,7 +37,7 @@ export const erc20BalanceAggregator = ( itemRequirements.forEach((itemRequirement) => { const { type } = itemRequirement; - if (type !== ItemType.ERC20) { + if (type !== ItemType.ERC20 || itemRequirement.isFee) { aggregatedItemRequirements.push(itemRequirement); return; } diff --git a/packages/checkout/sdk/src/smartCheckout/aggregators/itemAggregator.test.ts b/packages/checkout/sdk/src/smartCheckout/aggregators/itemAggregator.test.ts index 74c40abec3..8abdf0d200 100644 --- a/packages/checkout/sdk/src/smartCheckout/aggregators/itemAggregator.test.ts +++ b/packages/checkout/sdk/src/smartCheckout/aggregators/itemAggregator.test.ts @@ -12,22 +12,31 @@ describe('itemAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, + }, + { + type: ItemType.NATIVE, + amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.ERC721, @@ -45,15 +54,22 @@ describe('itemAggregator', () => { const aggregatedItems = itemAggregator(itemRequirements); expect(aggregatedItems).toEqual([ + { + type: ItemType.NATIVE, + amount: BigNumber.from(1), + isFee: true, + }, { type: ItemType.NATIVE, amount: BigNumber.from(2), + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(2), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.ERC721, @@ -71,22 +87,26 @@ describe('itemAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, ]; @@ -95,33 +115,38 @@ describe('itemAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(2), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, ]); }); - it('should not aggregate erc20s', () => { + it('should not aggregate different erc20s', () => { const itemRequirements: ItemRequirement[] = [ { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20_1', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20_2', spenderAddress: '0xSEAPORT', + isFee: false, }, ]; @@ -132,12 +157,51 @@ describe('itemAggregator', () => { amount: BigNumber.from(1), tokenAddress: '0xERC20_1', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20_2', spenderAddress: '0xSEAPORT', + isFee: false, + }, + ]); + }); + + it('should not aggregate erc20s when one is a fee', () => { + const itemRequirements: ItemRequirement[] = [ + { + type: ItemType.ERC20, + amount: BigNumber.from(1), + tokenAddress: '0xERC20_1', + spenderAddress: '0xSEAPORT', + isFee: true, + }, + { + type: ItemType.ERC20, + amount: BigNumber.from(2), + tokenAddress: '0xERC20_1', + spenderAddress: '0xSEAPORT', + isFee: false, + }, + ]; + + const aggregatedItems = erc20ItemAggregator(itemRequirements); + expect(aggregatedItems).toEqual([ + { + type: ItemType.ERC20, + amount: BigNumber.from(1), + tokenAddress: '0xERC20_1', + spenderAddress: '0xSEAPORT', + isFee: true, + }, + { + type: ItemType.ERC20, + amount: BigNumber.from(2), + tokenAddress: '0xERC20_1', + spenderAddress: '0xSEAPORT', + isFee: false, }, ]); }); @@ -154,22 +218,26 @@ describe('itemAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, ]; @@ -179,16 +247,19 @@ describe('itemAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(2), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, ]), ); @@ -199,22 +270,26 @@ describe('itemAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.NATIVE, - amount: BigNumber.from(1), + amount: BigNumber.from(2), + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: 'ERC1559' as ItemType, @@ -234,16 +309,19 @@ describe('itemAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.NATIVE, - amount: BigNumber.from(1), + amount: BigNumber.from(2), + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(2), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: 'ERC1559' as ItemType, @@ -266,10 +344,12 @@ describe('itemAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.NATIVE, - amount: BigNumber.from(1), + amount: BigNumber.from(2), + isFee: false, }, { type: ItemType.ERC721, @@ -290,10 +370,12 @@ describe('itemAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.NATIVE, - amount: BigNumber.from(1), + amount: BigNumber.from(2), + isFee: false, }, { type: ItemType.ERC721, @@ -309,6 +391,7 @@ describe('itemAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.ERC721, @@ -329,6 +412,7 @@ describe('itemAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.ERC721, @@ -357,6 +441,7 @@ describe('itemAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.ERC721, @@ -367,6 +452,7 @@ describe('itemAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.ERC721, @@ -382,10 +468,12 @@ describe('itemAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.ERC721, @@ -402,10 +490,12 @@ describe('itemAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.ERC721, @@ -437,10 +527,12 @@ describe('itemAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.ERC721, @@ -469,10 +561,12 @@ describe('itemAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.ERC1155, @@ -495,10 +589,12 @@ describe('itemAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.ERC1155, @@ -595,7 +691,8 @@ describe('itemAggregator', () => { const itemRequirements: ItemRequirement[] = [ { type: ItemType.NATIVE, - amount: BigNumber.from(1), + amount: BigNumber.from(2), + isFee: false, }, { type: ItemType.ERC1155, @@ -607,6 +704,7 @@ describe('itemAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.ERC1155, @@ -623,10 +721,12 @@ describe('itemAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.NATIVE, - amount: BigNumber.from(1), + amount: BigNumber.from(2), + isFee: false, }, { type: ItemType.ERC1155, @@ -643,11 +743,13 @@ describe('itemAggregator', () => { const itemRequirements: ItemRequirement[] = [ { type: ItemType.NATIVE, - amount: BigNumber.from(1), + amount: BigNumber.from(2), + isFee: false, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.ERC1155, @@ -680,11 +782,13 @@ describe('itemAggregator', () => { expect.arrayContaining([ { type: ItemType.NATIVE, - amount: BigNumber.from(1), + amount: BigNumber.from(2), + isFee: false, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.ERC1155, @@ -714,22 +818,26 @@ describe('itemAggregator', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, ]; @@ -740,16 +848,48 @@ describe('itemAggregator', () => { amount: BigNumber.from(1), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(1), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, + }, + { + type: ItemType.NATIVE, + amount: BigNumber.from(2), + isFee: false, + }, + ]); + }); + + it('should not aggregate native fee items', () => { + const itemRequirements: ItemRequirement[] = [ + { + type: ItemType.NATIVE, + amount: BigNumber.from(1), + isFee: true, + }, + { + type: ItemType.NATIVE, + amount: BigNumber.from(2), + isFee: true, + }, + ]; + + const aggregatedItems = nativeAggregator(itemRequirements); + expect(aggregatedItems).toEqual([ + { + type: ItemType.NATIVE, + amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.NATIVE, amount: BigNumber.from(2), + isFee: true, }, ]); }); diff --git a/packages/checkout/sdk/src/smartCheckout/aggregators/itemAggregator.ts b/packages/checkout/sdk/src/smartCheckout/aggregators/itemAggregator.ts index 9b2edb19fd..c1805c9c38 100644 --- a/packages/checkout/sdk/src/smartCheckout/aggregators/itemAggregator.ts +++ b/packages/checkout/sdk/src/smartCheckout/aggregators/itemAggregator.ts @@ -7,14 +7,19 @@ export const nativeAggregator = ( const aggregatedMap = new Map(); const aggregatedItemRequirements: ItemRequirement[] = []; - itemRequirements.forEach((itemRequirement) => { - const { type } = itemRequirement; + itemRequirements.forEach((item) => { + const { type } = item; - if (type !== ItemType.NATIVE) { - aggregatedItemRequirements.push(itemRequirement); + if (type !== ItemType.NATIVE || item.isFee) { + aggregatedItemRequirements.push(item); return; } + const itemRequirement = { + ...item, + isFee: 'isFee' in item ? item.isFee : false, + }; + const { amount } = itemRequirement; const aggregateItem = aggregatedMap.get(type); @@ -34,14 +39,19 @@ export const erc20ItemAggregator = ( const aggregatedMap = new Map(); const aggregatedItemRequirements: ItemRequirement[] = []; - itemRequirements.forEach((itemRequirement) => { - const { type } = itemRequirement; + itemRequirements.forEach((item) => { + const { type } = item; - if (type !== ItemType.ERC20) { - aggregatedItemRequirements.push(itemRequirement); + if (type !== ItemType.ERC20 || item.isFee) { + aggregatedItemRequirements.push(item); return; } + const itemRequirement = { + ...item, + isFee: 'isFee' in item ? item.isFee : false, + }; + const { tokenAddress, spenderAddress, amount } = itemRequirement; const key = `${tokenAddress}${spenderAddress}`; const aggregateItem = aggregatedMap.get(key); diff --git a/packages/checkout/sdk/src/smartCheckout/allowance/erc1155.test.ts b/packages/checkout/sdk/src/smartCheckout/allowance/erc1155.test.ts index 0b8edf02b9..cac527a8ed 100644 --- a/packages/checkout/sdk/src/smartCheckout/allowance/erc1155.test.ts +++ b/packages/checkout/sdk/src/smartCheckout/allowance/erc1155.test.ts @@ -126,6 +126,7 @@ describe('erc1155', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.ERC721, @@ -165,6 +166,7 @@ describe('erc1155', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.ERC721, @@ -205,6 +207,7 @@ describe('erc1155', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.ERC1155, diff --git a/packages/checkout/sdk/src/smartCheckout/allowance/erc20.test.ts b/packages/checkout/sdk/src/smartCheckout/allowance/erc20.test.ts index a004b970a5..f7ff7563be 100644 --- a/packages/checkout/sdk/src/smartCheckout/allowance/erc20.test.ts +++ b/packages/checkout/sdk/src/smartCheckout/allowance/erc20.test.ts @@ -129,12 +129,14 @@ describe('allowance', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.ERC20, tokenAddress: '0xERC20', amount: BigNumber.from(2), spenderAddress: '0xSEAPORT', + isFee: false, }, ]; @@ -165,12 +167,14 @@ describe('allowance', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.ERC20, tokenAddress: '0xERC20', amount: BigNumber.from(1), spenderAddress: '0xSEAPORT', + isFee: false, }, ]; @@ -198,24 +202,28 @@ describe('allowance', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.ERC20, tokenAddress: '0xERC20a', amount: BigNumber.from(2), spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.ERC20, tokenAddress: '0xERC20b', amount: BigNumber.from(1), spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.ERC20, tokenAddress: '0xERC20c', amount: BigNumber.from(2), spenderAddress: '0xSEAPORT', + isFee: false, }, ]; diff --git a/packages/checkout/sdk/src/smartCheckout/allowance/erc721.test.ts b/packages/checkout/sdk/src/smartCheckout/allowance/erc721.test.ts index 5d4e07c5bb..71dc3527a9 100644 --- a/packages/checkout/sdk/src/smartCheckout/allowance/erc721.test.ts +++ b/packages/checkout/sdk/src/smartCheckout/allowance/erc721.test.ts @@ -197,6 +197,7 @@ describe('erc721', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.ERC721, @@ -231,6 +232,7 @@ describe('erc721', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.ERC721, @@ -266,6 +268,7 @@ describe('erc721', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.ERC721, diff --git a/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceCheck.test.ts b/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceCheck.test.ts index d47a8ccc5f..2f54a39c13 100644 --- a/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceCheck.test.ts +++ b/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceCheck.test.ts @@ -46,6 +46,7 @@ describe('balanceCheck', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, ]; const getBalancesResult = { @@ -73,6 +74,7 @@ describe('balanceCheck', () => { { type: ItemType.NATIVE, amount: BigNumber.from(3), + isFee: false, }, ]; const getBalancesResult = { @@ -81,11 +83,7 @@ describe('balanceCheck', () => { { balance: BigNumber.from(1), formattedBalance: '0.000000000000000001', - token: { - name: 'IMX', - symbol: 'IMX', - decimals: 18, - }, + token: ZKEVM_NATIVE_TOKEN, }, ], }; @@ -105,17 +103,15 @@ describe('balanceCheck', () => { current: { ...getBalancesResult.balances[0], type: ItemType.NATIVE, + token: ZKEVM_NATIVE_TOKEN, }, required: { type: ItemType.NATIVE, balance: BigNumber.from(3), formattedBalance: '0.000000000000000003', - token: { - name: 'IMX', - symbol: 'IMX', - decimals: 18, - }, + token: ZKEVM_NATIVE_TOKEN, }, + isFee: false, }, ], sufficient: false, @@ -131,6 +127,7 @@ describe('balanceCheck', () => { amount: BigNumber.from(10), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, ]; const getBalancesResult = { @@ -161,6 +158,7 @@ describe('balanceCheck', () => { amount: BigNumber.from(10), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, ]; const getBalancesResult = { @@ -202,6 +200,7 @@ describe('balanceCheck', () => { address: '0xERC20', }, }, + isFee: false, }, ], sufficient: false, @@ -235,22 +234,26 @@ describe('balanceCheck', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: true, }, { type: ItemType.ERC20, amount: BigNumber.from(10), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from(10), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, ]; const getBalancesResult = { balances: [] }; @@ -276,17 +279,39 @@ describe('balanceCheck', () => { token: ZKEVM_NATIVE_TOKEN, }, delta: { - balance: BigNumber.from(2), - formattedBalance: '0.000000000000000002', + balance: BigNumber.from(1), + formattedBalance: '0.000000000000000001', + }, + required: { + type: ItemType.NATIVE, + balance: BigNumber.from(1), + formattedBalance: '0.000000000000000001', + token: ZKEVM_NATIVE_TOKEN, + }, + sufficient: false, + type: ItemType.NATIVE, + isFee: false, + }, + { + current: { + type: ItemType.NATIVE, + balance: BigNumber.from(0), + formattedBalance: '0', + token: ZKEVM_NATIVE_TOKEN, + }, + delta: { + balance: BigNumber.from(1), + formattedBalance: '0.000000000000000001', }, required: { type: ItemType.NATIVE, - balance: BigNumber.from(2), - formattedBalance: '0.000000000000000002', + balance: BigNumber.from(1), + formattedBalance: '0.000000000000000001', token: ZKEVM_NATIVE_TOKEN, }, sufficient: false, type: ItemType.NATIVE, + isFee: true, }, { delta: { @@ -315,6 +340,7 @@ describe('balanceCheck', () => { decimals: DEFAULT_TOKEN_DECIMALS, }, }, + isFee: false, sufficient: false, type: ItemType.ERC20, }, @@ -328,12 +354,14 @@ describe('balanceCheck', () => { { type: ItemType.NATIVE, amount: BigNumber.from('1'), + isFee: true, }, { type: ItemType.ERC20, amount: BigNumber.from('10'), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.ERC721, @@ -380,22 +408,26 @@ describe('balanceCheck', () => { { type: ItemType.NATIVE, amount: BigNumber.from('1'), + isFee: false, }, { type: ItemType.NATIVE, amount: BigNumber.from('1'), + isFee: true, }, { type: ItemType.ERC20, amount: BigNumber.from('10'), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.ERC20, amount: BigNumber.from('10'), tokenAddress: '0xERC20', spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.ERC721, @@ -438,34 +470,6 @@ describe('balanceCheck', () => { expect(result.sufficient).toBeFalsy(); expect(result.balanceRequirements) .toEqual(expect.arrayContaining([ - { - current: { - type: ItemType.NATIVE, - balance: BigNumber.from(1), - formattedBalance: '0.000000000000000001', - token: { - decimals: 18, - name: '', - symbol: '', - }, - }, - delta: { - balance: BigNumber.from(1), - formattedBalance: '0.000000000000000001', - }, - required: { - type: ItemType.NATIVE, - balance: BigNumber.from(2), - formattedBalance: '0.000000000000000002', - token: { - name: '', - symbol: '', - decimals: 18, - }, - }, - sufficient: false, - type: ItemType.NATIVE, - }, { delta: { balance: BigNumber.from(20), @@ -495,6 +499,49 @@ describe('balanceCheck', () => { }, sufficient: false, type: ItemType.ERC20, + isFee: false, + }, + { + current: { + type: ItemType.NATIVE, + balance: BigNumber.from(1), + formattedBalance: '0.000000000000000001', + token: ZKEVM_NATIVE_TOKEN, + }, + delta: { + balance: BigNumber.from(0), + formattedBalance: '0.0', + }, + required: { + type: ItemType.NATIVE, + balance: BigNumber.from(1), + formattedBalance: '0.000000000000000001', + token: ZKEVM_NATIVE_TOKEN, + }, + sufficient: true, + type: ItemType.NATIVE, + isFee: false, + }, + { + current: { + type: ItemType.NATIVE, + balance: BigNumber.from(0), + formattedBalance: '0.0', + token: ZKEVM_NATIVE_TOKEN, + }, + delta: { + balance: BigNumber.from(1), + formattedBalance: '0.000000000000000001', + }, + required: { + type: ItemType.NATIVE, + balance: BigNumber.from(1), + formattedBalance: '0.000000000000000001', + token: ZKEVM_NATIVE_TOKEN, + }, + sufficient: false, + type: ItemType.NATIVE, + isFee: true, }, { delta: { @@ -517,6 +564,7 @@ describe('balanceCheck', () => { }, sufficient: true, type: ItemType.ERC721, + isFee: false, }, ])); }); @@ -580,6 +628,7 @@ describe('balanceCheck', () => { }, sufficient: false, type: ItemType.ERC721, + isFee: false, }, { current: { @@ -602,6 +651,7 @@ describe('balanceCheck', () => { }, sufficient: false, type: ItemType.ERC721, + isFee: false, }, ])); }); @@ -612,6 +662,7 @@ describe('balanceCheck', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, ]; const getBalancesResult = { diff --git a/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceCheck.ts b/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceCheck.ts index d249066e09..f3f4a266eb 100644 --- a/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceCheck.ts +++ b/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceCheck.ts @@ -1,5 +1,5 @@ import { Web3Provider } from '@ethersproject/providers'; -import { BigNumber, Contract } from 'ethers'; +import { BigNumber, Contract, utils } from 'ethers'; import { ERC20Item, ERC721Balance, @@ -20,6 +20,7 @@ import { getERC721BalanceRequirement, getTokenBalanceRequirement, getTokensFromRequirements, + getTokensInfo, } from './balanceRequirement'; import { ERC721ABI, NATIVE } from '../../env'; import { isMatchingAddress } from '../../utils/utils'; @@ -122,7 +123,7 @@ export const balanceCheck = async ( ) : Promise => { const aggregatedItems = balanceAggregator(itemRequirements); - const requiredToken: ItemRequirement[] = []; + const requiredToken: Array = []; const requiredERC721: ItemRequirement[] = []; aggregatedItems.forEach((item) => { @@ -138,6 +139,9 @@ export const balanceCheck = async ( } }); + // Non-fee requirements first + requiredToken.sort((token) => ('isFee' in token && token.isFee ? 1 : -1)); + if (requiredERC721.length === 0 && requiredToken.length === 0) { throw new CheckoutError( 'Unsupported item requirement balance check', @@ -158,39 +162,56 @@ export const balanceCheck = async ( // Wait for all balances and calculate the requirements const promisesResponses = await Promise.all(balancePromises); - const erc721BalanceRequirements: BalanceRequirement[] = []; - const tokenBalanceRequirementPromises: Promise[] = []; + const balanceRequirements: BalanceRequirement[] = []; - // Get all ERC20 and NATIVE balances - if (requiredToken.length > 0 && promisesResponses.length > 0) { - const result = promisesResponses.shift(); - if (result) { - requiredToken.forEach((item) => { - tokenBalanceRequirementPromises.push( - getTokenBalanceRequirement(item as (NativeItem | ERC20Item), result, provider), - ); + // Check ERC20 and NATIVE requirements against balances + if (requiredToken.length > 0) { + const tokenBalances = promisesResponses.shift() ?? []; + + const balances = new Map(tokenBalances.map((balance) => { + const address = balance.type === ItemType.NATIVE + ? NATIVE + : (balance as TokenBalance).token.address?.toLowerCase(); + return [address, balance]; + })); + const tokensInfo = await getTokensInfo(requiredToken, tokenBalances, provider); + + requiredToken.forEach((item) => { + const tokenAddress = ((item as ERC20Item).tokenAddress ?? NATIVE).toLowerCase(); + const tokenInfo = tokensInfo[tokenAddress]; + const currentBalance = balances.get(tokenAddress); + + const requirement = getTokenBalanceRequirement(item, [...balances.values()], tokenInfo); + + balanceRequirements.push(requirement); + + if (!currentBalance) { + return; + } + + const updatedBalance = currentBalance.balance.sub(requirement.required.balance); + + balances.set(tokenAddress, { + ...currentBalance, + balance: updatedBalance, + formattedBalance: utils.formatUnits(updatedBalance, requirement.required.token.decimals), }); - } + }); } - // Get all ERC721 balances - if (requiredERC721.length > 0 && promisesResponses.length > 0) { - const result = promisesResponses.shift(); - if (result) { - requiredERC721.forEach((item) => { - erc721BalanceRequirements.push(getERC721BalanceRequirement(item as (ERC721Item), result)); - }); - } + // Check ERC721 requirements against balances + if (requiredERC721.length > 0) { + const erc721Balances = promisesResponses.shift() ?? []; + + requiredERC721.forEach((item) => { + balanceRequirements.push(getERC721BalanceRequirement(item as (ERC721Item), erc721Balances)); + }); } - 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 // are satisfied. - const sufficient = balanceRequirements.find((req) => req.sufficient === false) === undefined; + const sufficient = balanceRequirements.find((req) => !req.sufficient) === undefined; return { sufficient, diff --git a/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceRequirement.test.ts b/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceRequirement.test.ts index c01acc451d..7fdeeb4ae4 100644 --- a/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceRequirement.test.ts +++ b/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceRequirement.test.ts @@ -1,4 +1,4 @@ -import { BigNumber } from 'ethers'; +import { BigNumber, Contract } from 'ethers'; import { Web3Provider } from '@ethersproject/providers'; import { ChainId, @@ -8,13 +8,20 @@ import { ItemRequirement, ItemType, NativeItem, + TokenInfo, } from '../../types'; import { getERC721BalanceRequirement, getTokenBalanceRequirement, getTokensFromRequirements, + getTokensInfo, } from './balanceRequirement'; -import { NATIVE } from '../../env'; +import { NATIVE, ZKEVM_NATIVE_TOKEN } from '../../env'; + +jest.mock('ethers', () => ({ + ...jest.requireActual('ethers'), + Contract: jest.fn(), +})); describe('balanceRequirement', () => { describe('getTokensFromRequirements', () => { @@ -23,12 +30,14 @@ describe('balanceRequirement', () => { { type: ItemType.NATIVE, amount: BigNumber.from('1000000000000000000'), + isFee: false, }, { type: ItemType.ERC20, tokenAddress: '0xERC20', amount: BigNumber.from('1000000000000000000'), spenderAddress: '0xSEAPORT', + isFee: false, }, { type: ItemType.ERC721, @@ -91,6 +100,7 @@ describe('balanceRequirement', () => { contractAddress: '0xERC721', id: '0', }, + isFee: false, }); }); @@ -136,14 +146,17 @@ describe('balanceRequirement', () => { contractAddress: '0xERC721', id: '0', }, + isFee: false, }); }); }); - describe('getTokenBalanceRequirement', () => { + describe('getTokensInfo', () => { let mockProvider: Web3Provider; beforeEach(() => { + jest.resetAllMocks(); + mockProvider = { getSigner: jest.fn().mockReturnValue({ getAddress: jest.fn().mockResolvedValue('0xADDRESS'), @@ -154,10 +167,101 @@ describe('balanceRequirement', () => { } as unknown as Web3Provider; }); - it('should return sufficient true if meets requirements for NATIVE', async () => { + it('should return native token data if type is native', async () => { + const itemRequirements: NativeItem[] = [ + { + type: ItemType.NATIVE, + amount: BigNumber.from('1000000000000000000'), + isFee: true, + }, + ]; + const balances: ItemBalance[] = [ + { + type: ItemType.NATIVE, + balance: BigNumber.from('1000000000000000000'), + formattedBalance: '1.0', + token: { + name: 'IMX', + symbol: 'IMX', + decimals: 18, + }, + }, + ]; + + const tokensInfo = await getTokensInfo(itemRequirements, balances, mockProvider); + + expect(tokensInfo).toHaveProperty(NATIVE, ZKEVM_NATIVE_TOKEN); + }); + + it('should fetch ERC20 details from balance when available', async () => { + const itemRequirements: ERC20Item[] = [ + { + type: ItemType.ERC20, + tokenAddress: '0xERC20', + amount: BigNumber.from('1000000000000000000'), + spenderAddress: '0xSEAPORT', + isFee: true, + }, + ]; + const balances: ItemBalance[] = [ + { + type: ItemType.ERC20, + balance: BigNumber.from('1000000000000000000'), + formattedBalance: '1.0', + token: { + name: 'ERC20', + symbol: 'ERC20', + decimals: 18, + address: '0xERC20', + }, + }, + ]; + + const tokensInfo = await getTokensInfo(itemRequirements, balances, mockProvider); + + expect(tokensInfo).toHaveProperty('0xerc20', { + name: 'ERC20', + symbol: 'ERC20', + decimals: 18, + address: '0xERC20', + }); + }); + + it('should fetch ERC20 details from contract when not available in balance', async () => { + const itemRequirements: ERC20Item[] = [ + { + type: ItemType.ERC20, + tokenAddress: '0xERC20', + amount: BigNumber.from('1000000000000000000'), + spenderAddress: '0xSEAPORT', + isFee: true, + }, + ]; + const balances: ItemBalance[] = []; + + (Contract as unknown as jest.Mock).mockImplementation(() => ({ + symbol: jest.fn().mockResolvedValue('ERC20'), + name: jest.fn().mockResolvedValue('ERC20'), + decimals: jest.fn().mockResolvedValue(18), + })); + + const tokensInfo = await getTokensInfo(itemRequirements, balances, mockProvider); + + expect(tokensInfo).toHaveProperty('0xerc20', { + name: 'ERC20', + symbol: 'ERC20', + decimals: 18, + address: '0xERC20', + }); + }); + }); + + describe('getTokenBalanceRequirement', () => { + it('should return sufficient true if meets requirements for NATIVE', () => { const itemRequirement: NativeItem = { type: ItemType.NATIVE, amount: BigNumber.from('1000000000000000000'), + isFee: true, }; const balances: ItemBalance[] = [ { @@ -171,11 +275,12 @@ describe('balanceRequirement', () => { }, }, ]; + const tokenInfo: TokenInfo = ZKEVM_NATIVE_TOKEN; - const result = await getTokenBalanceRequirement( + const result = getTokenBalanceRequirement( itemRequirement, balances, - mockProvider, + tokenInfo, ); expect(result).toEqual({ sufficient: true, @@ -188,31 +293,25 @@ describe('balanceRequirement', () => { type: ItemType.NATIVE, balance: BigNumber.from('1000000000000000000'), formattedBalance: '1.0', - token: { - name: 'IMX', - symbol: 'IMX', - decimals: 18, - }, + token: tokenInfo, }, current: { type: ItemType.NATIVE, balance: BigNumber.from('1000000000000000000'), formattedBalance: '1.0', - token: { - name: 'IMX', - symbol: 'IMX', - decimals: 18, - }, + token: tokenInfo, }, + isFee: true, }); }); - it('should return sufficient true if meets requirements for ERC20', async () => { + it('should return sufficient true if meets requirements for ERC20', () => { const itemRequirement: ERC20Item = { type: ItemType.ERC20, tokenAddress: '0xERC20', amount: BigNumber.from('1000000000000000000'), spenderAddress: '0xSEAPORT', + isFee: true, }; const balances: ItemBalance[] = [ { @@ -227,11 +326,17 @@ describe('balanceRequirement', () => { }, }, ]; + const tokenInfo: TokenInfo = { + name: 'ERC20', + symbol: 'ERC20', + decimals: 18, + address: '0xERC20', + }; - const result = await getTokenBalanceRequirement( + const result = getTokenBalanceRequirement( itemRequirement, balances, - mockProvider, + tokenInfo, ); expect(result).toEqual({ sufficient: true, @@ -244,31 +349,23 @@ describe('balanceRequirement', () => { type: ItemType.ERC20, balance: BigNumber.from('1000000000000000000'), formattedBalance: '1.0', - token: { - name: 'ERC20', - symbol: 'ERC20', - decimals: 18, - address: '0xERC20', - }, + token: tokenInfo, }, current: { type: ItemType.ERC20, balance: BigNumber.from('1000000000000000000'), formattedBalance: '1.0', - token: { - name: 'ERC20', - symbol: 'ERC20', - decimals: 18, - address: '0xERC20', - }, + token: tokenInfo, }, + isFee: true, }); }); - it('should return sufficient false if requirements not met for NATIVE', async () => { + it('should return sufficient false if requirements not met for NATIVE', () => { const itemRequirement: NativeItem = { type: ItemType.NATIVE, amount: BigNumber.from('1000000000000000000'), + isFee: false, }; const balances: ItemBalance[] = [ { @@ -294,10 +391,12 @@ describe('balanceRequirement', () => { }, ]; - const result = await getTokenBalanceRequirement( + const tokenInfo: TokenInfo = ZKEVM_NATIVE_TOKEN; + + const result = getTokenBalanceRequirement( itemRequirement, balances, - mockProvider, + tokenInfo, ); expect(result).toEqual({ sufficient: false, @@ -310,31 +409,25 @@ describe('balanceRequirement', () => { type: ItemType.NATIVE, balance: BigNumber.from('1000000000000000000'), formattedBalance: '1.0', - token: { - name: 'IMX', - symbol: 'IMX', - decimals: 18, - }, + token: tokenInfo, }, current: { type: ItemType.NATIVE, balance: BigNumber.from('10000000000'), formattedBalance: '0.000001', - token: { - name: 'IMX', - symbol: 'IMX', - decimals: 18, - }, + token: tokenInfo, }, + isFee: false, }); }); - it('should return sufficient false if requirements not met for ERC20', async () => { + it('should return sufficient false if requirements not met for ERC20', () => { const itemRequirement: ERC20Item = { type: ItemType.ERC20, tokenAddress: '0xERC20', amount: BigNumber.from('1000000000000000000'), spenderAddress: '0xSEAPORT', + isFee: false, }; const balances: ItemBalance[] = [ { @@ -359,11 +452,17 @@ describe('balanceRequirement', () => { }, }, ]; + const tokenInfo: TokenInfo = { + name: 'ERC20', + symbol: 'ERC20', + decimals: 18, + address: '0xERC20', + }; - const result = await getTokenBalanceRequirement( + const result = getTokenBalanceRequirement( itemRequirement, balances, - mockProvider, + tokenInfo, ); expect(result).toEqual({ sufficient: false, @@ -376,24 +475,15 @@ describe('balanceRequirement', () => { type: ItemType.ERC20, balance: BigNumber.from('1000000000000000000'), formattedBalance: '1.0', - token: { - name: 'ERC20', - symbol: 'ERC20', - decimals: 18, - address: '0xERC20', - }, + token: tokenInfo, }, current: { type: ItemType.ERC20, balance: BigNumber.from('10000000000'), formattedBalance: '0.000001', - token: { - name: 'ERC20', - symbol: 'ERC20', - decimals: 18, - address: '0xERC20', - }, + token: tokenInfo, }, + isFee: false, }); }); }); diff --git a/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceRequirement.ts b/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceRequirement.ts index ba4089effd..f43597ff45 100644 --- a/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceRequirement.ts +++ b/packages/checkout/sdk/src/smartCheckout/balanceCheck/balanceRequirement.ts @@ -96,84 +96,125 @@ export const getERC721BalanceRequirement = ( balance: BigNumber.from(1), formattedBalance: '1', }, + isFee: false, }; }; -/** - * Gets the balance requirement for a NATIVE or ERC20 requirement. - */ -export const getTokenBalanceRequirement = async ( +export const getTokenFromBalances = ( itemRequirement: ERC20Item | NativeItem, balances: ItemBalance[], - provider: Web3Provider, -): Promise => { - let itemBalanceResult: ItemBalance | undefined; - - // Get the requirements related balance +): TokenBalance | undefined => { if (itemRequirement.type === ItemType.ERC20) { - itemBalanceResult = balances.find((balance) => { + return balances.find((balance) => { return isMatchingAddress( (balance as TokenBalance).token?.address, itemRequirement.tokenAddress, ); - }); - } else if (itemRequirement.type === ItemType.NATIVE) { - itemBalanceResult = balances.find((balance) => { - return isNativeToken((balance as TokenBalance).token?.address); - }); + }) as TokenBalance; } - // Calculate the balance delta + return balances.find((balance) => { + return isNativeToken((balance as TokenBalance).token?.address); + }) as TokenBalance; +}; + +type TokensInfoMap = { + [key: string]: TokenInfo; +}; + +export const getTokensInfo = async ( + itemRequirements: Array, + balances: ItemBalance[], + provider: Web3Provider, +): Promise => { + const tokensInfo: TokensInfoMap = {}; + + for (const itemRequirement of itemRequirements) { + if (itemRequirement.type === ItemType.NATIVE) { + tokensInfo[NATIVE] = ZKEVM_NATIVE_TOKEN; + continue; + } + + const tokenBalance = getTokenFromBalances(itemRequirement, balances); + + let address = tokenBalance?.token.address ?? ''; + let name = tokenBalance?.token.name ?? ''; + let symbol = tokenBalance?.token.symbol ?? ''; + let decimals = tokenBalance?.token.decimals ?? DEFAULT_TOKEN_DECIMALS; + + if (!tokenBalance && itemRequirement.type === ItemType.ERC20) { + address = itemRequirement.tokenAddress; + + if (address.toLowerCase() in tokensInfo) { + continue; + } + + // Missing item balance so we need to query contract + try { + const contract = new Contract( + itemRequirement.tokenAddress, + JSON.stringify(ERC20ABI), + provider, + ); + // eslint-disable-next-line no-await-in-loop + const [contractName, contractSymbol, contractDecimals] = await Promise.all([ + contract.name(), + contract.symbol(), + contract.decimals(), + ]); + address = itemRequirement.tokenAddress; + decimals = contractDecimals; + name = contractName; + symbol = contractSymbol; + } catch (error) { + // eslint-disable-next-line no-console + console.error( + 'Failed to query contract information', + itemRequirement.tokenAddress, + ); + } + } + + tokensInfo[address.toLowerCase()] = { + address, + name, + symbol, + decimals, + }; + } + + return tokensInfo; +}; + +/** + * Gets the balance requirement for a NATIVE or ERC20 requirement. + */ +export const getTokenBalanceRequirement = ( + itemRequirement: ERC20Item | NativeItem, + balances: ItemBalance[], + token: TokenInfo, +): BalanceNativeRequirement | BalanceERC20Requirement => { + let tokenBalance = getTokenFromBalances(itemRequirement, balances); + const requiredBalance: BigNumber = itemRequirement.amount; + + // Calculate the balance delta const sufficient = requiredBalance.isNegative() || requiredBalance.isZero() - || (itemBalanceResult?.balance.gte(requiredBalance) ?? false); + || (tokenBalance?.balance.gte(requiredBalance) ?? false); const delta = requiredBalance.sub( - itemBalanceResult?.balance ?? BigNumber.from(0), + tokenBalance?.balance ?? BigNumber.from(0), ); - let name = ''; - let symbol = ''; - let decimals = DEFAULT_TOKEN_DECIMALS; - if (itemBalanceResult) { - 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; if (itemRequirement.type === ItemType.NATIVE) { // No token balance so mark as zero native - if (!tokenBalanceResult) { - tokenBalanceResult = { + if (!tokenBalance) { + tokenBalance = { type: ItemType.NATIVE, balance: BigNumber.from(0), formattedBalance: '0', - token: ZKEVM_NATIVE_TOKEN, + token, }; } @@ -182,33 +223,31 @@ export const getTokenBalanceRequirement = async ( type: ItemType.NATIVE, delta: { balance: delta, - formattedBalance: utils.formatUnits(delta, decimals), + formattedBalance: utils.formatUnits(delta, token.decimals), }, current: { - ...tokenBalanceResult, + ...tokenBalance, type: ItemType.NATIVE, + token, }, required: { - ...tokenBalanceResult, + ...tokenBalance, type: ItemType.NATIVE, balance: BigNumber.from(itemRequirement.amount), - formattedBalance: utils.formatUnits(itemRequirement.amount, decimals), + formattedBalance: utils.formatUnits(itemRequirement.amount, token.decimals), + token, }, + isFee: itemRequirement.isFee, }; } // No token balance so mark as zero - if (!tokenBalanceResult) { - tokenBalanceResult = { + if (!tokenBalance) { + tokenBalance = { type: itemRequirement.type, balance: BigNumber.from(0), formattedBalance: '0', - token: { - name, - symbol, - address: itemRequirement.tokenAddress, - decimals, - }, + token, }; } @@ -217,27 +256,18 @@ export const getTokenBalanceRequirement = async ( type: ItemType.ERC20, delta: { balance: delta, - formattedBalance: utils.formatUnits(delta, decimals), + formattedBalance: utils.formatUnits(delta, token.decimals), }, current: { - ...tokenBalanceResult, - token: { - address: itemRequirement.tokenAddress, - name, - symbol, - decimals, - }, + ...tokenBalance, + token, }, required: { - ...tokenBalanceResult, - token: { - address: itemRequirement.tokenAddress, - name, - symbol, - decimals, - }, + ...tokenBalance, + token, balance: BigNumber.from(itemRequirement.amount), - formattedBalance: utils.formatUnits(itemRequirement.amount, decimals), + formattedBalance: utils.formatUnits(itemRequirement.amount, token.decimals), }, + isFee: itemRequirement.isFee, }; }; diff --git a/packages/checkout/sdk/src/smartCheckout/balanceCheck/types.ts b/packages/checkout/sdk/src/smartCheckout/balanceCheck/types.ts index e6ff21a454..9c9176ffc7 100644 --- a/packages/checkout/sdk/src/smartCheckout/balanceCheck/types.ts +++ b/packages/checkout/sdk/src/smartCheckout/balanceCheck/types.ts @@ -8,6 +8,7 @@ export type BalanceNativeRequirement = { delta: BalanceDelta, current: TokenBalance, required: TokenBalance, + isFee: boolean, }; export type BalanceERC20Requirement = { @@ -16,6 +17,7 @@ export type BalanceERC20Requirement = { delta: BalanceDelta, current: TokenBalance, required: TokenBalance, + isFee: boolean, }; export type BalanceERC721Requirement = { @@ -24,6 +26,7 @@ export type BalanceERC721Requirement = { delta: BalanceDelta, current: ERC721Balance, required: ERC721Balance, + isFee: boolean, }; export type BalanceRequirement = BalanceNativeRequirement | BalanceERC721Requirement | BalanceERC20Requirement; diff --git a/packages/checkout/sdk/src/smartCheckout/buy/buy.test.ts b/packages/checkout/sdk/src/smartCheckout/buy/buy.test.ts index e0b696bd69..25061a4091 100644 --- a/packages/checkout/sdk/src/smartCheckout/buy/buy.test.ts +++ b/packages/checkout/sdk/src/smartCheckout/buy/buy.test.ts @@ -89,6 +89,7 @@ describe('buy', () => { balance: BigNumber.from(0), formattedBalance: '0', }, + isFee: false, }], }; const fulfillOrderMock = jest.fn().mockReturnValue({ @@ -151,6 +152,7 @@ describe('buy', () => { { type: ItemType.NATIVE, amount: BigNumber.from('2000000000000000000'), + isFee: false, }, ]; @@ -219,6 +221,7 @@ describe('buy', () => { balance: BigNumber.from(0), formattedBalance: '0', }, + isFee: false, }, { type: ItemType.ERC20, @@ -247,6 +250,7 @@ describe('buy', () => { balance: BigNumber.from(0), formattedBalance: '0', }, + isFee: false, }], }; const fulfillOrderMock = jest.fn().mockReturnValue({ @@ -325,6 +329,7 @@ describe('buy', () => { amount: BigNumber.from('2000000000000000000'), tokenAddress: '0xCONTRACTADDRESS', spenderAddress: '0xSEAPORT', + isFee: false, }, ]; @@ -395,6 +400,7 @@ describe('buy', () => { balance: BigNumber.from(0), formattedBalance: '0', }, + isFee: false, }], }; const fulfillOrderMock = jest.fn().mockReturnValue({ @@ -457,6 +463,7 @@ describe('buy', () => { { type: ItemType.NATIVE, amount: BigNumber.from('10010000000000000000'), // 101e16 + isFee: false, }, ]; @@ -525,6 +532,7 @@ describe('buy', () => { balance: BigNumber.from(0), formattedBalance: '0', }, + isFee: false, }], }; const fulfillOrderMock = jest.fn().mockReturnValue({ @@ -588,6 +596,7 @@ describe('buy', () => { { type: ItemType.NATIVE, amount: BigNumber.from('5005000000000000000'), // 5005e15 + isFee: false, }, ]; @@ -655,6 +664,7 @@ describe('buy', () => { balance: BigNumber.from(0), formattedBalance: '0', }, + isFee: false, }, { type: ItemType.ERC20, @@ -683,6 +693,7 @@ describe('buy', () => { balance: BigNumber.from(0), formattedBalance: '0', }, + isFee: false, }], }; const fulfillOrderMock = jest.fn().mockReturnValue({ @@ -761,6 +772,7 @@ describe('buy', () => { amount: BigNumber.from('2000000000000000000'), tokenAddress: '0xCONTRACTADDRESS', spenderAddress: '0xSEAPORT', + isFee: false, }, ]; @@ -838,6 +850,7 @@ describe('buy', () => { balance: BigNumber.from(0), formattedBalance: '0', }, + isFee: false, }, { type: ItemType.ERC20, @@ -866,6 +879,7 @@ describe('buy', () => { balance: BigNumber.from(0), formattedBalance: '0', }, + isFee: false, }], }; const fulfillOrderMock = jest.fn().mockReturnValue({ @@ -943,6 +957,7 @@ describe('buy', () => { amount: BigNumber.from('2000000000000000000'), tokenAddress: '0xCONTRACTADDRESS', spenderAddress: '0xSEAPORT', + isFee: false, }, ]; @@ -1022,6 +1037,7 @@ describe('buy', () => { { type: ItemType.NATIVE, amount: BigNumber.from('2000000000000000000'), + isFee: false, }, ]; const gasAmount: GasAmount = { @@ -1130,6 +1146,7 @@ describe('buy', () => { amount: BigNumber.from('2000000000000000000'), tokenAddress: '0x123', spenderAddress: seaportContractAddress, + isFee: false, }, ]; const gasAmount: GasAmount = { @@ -1238,6 +1255,7 @@ describe('buy', () => { { type: ItemType.NATIVE, amount: BigNumber.from('2'), + isFee: false, }, ]; const fulfillmentTransaction: FulfillmentTransaction = { @@ -1356,6 +1374,7 @@ describe('buy', () => { { type: ItemType.NATIVE, amount: BigNumber.from('2'), + isFee: false, }, ]; const fulfillmentTransaction: FulfillmentTransaction = { @@ -1414,6 +1433,7 @@ describe('buy', () => { balance: BigNumber.from(0), formattedBalance: '0', }, + isFee: false, }], }; (smartCheckout as jest.Mock).mockResolvedValue(smartCheckoutResult); @@ -1478,6 +1498,7 @@ describe('buy', () => { { type: ItemType.NATIVE, amount: BigNumber.from('2'), + isFee: false, }, ]; const fulfillmentTransaction: FulfillmentTransaction = { @@ -1847,6 +1868,7 @@ describe('buy', () => { balance: BigNumber.from(0), formattedBalance: '0', }, + isFee: false, }], }; const fulfillOrderMock = jest.fn().mockReturnValue({ @@ -1903,6 +1925,7 @@ describe('buy', () => { { type: ItemType.NATIVE, amount: BigNumber.from('2000000000000000000'), + isFee: false, }, ]; const fulfillmentTransaction: FulfillmentTransaction = { @@ -2055,6 +2078,7 @@ describe('buy', () => { formattedBalance: '0', }, }], + isFee: false, }; const fulfillOrderMock = jest.fn().mockReturnValue({ actions: [ @@ -2120,6 +2144,7 @@ describe('buy', () => { amount: BigNumber.from('2000000'), tokenAddress: '0xCONTRACTADDRESS', spenderAddress: '0xSEAPORT', + isFee: false, }, ]; const fulfillmentTransaction: FulfillmentTransaction = { @@ -2161,6 +2186,7 @@ describe('buy', () => { expect(result).toEqual({ type, amount, + isFee: false, }); }); @@ -2174,6 +2200,7 @@ describe('buy', () => { amount, tokenAddress, spenderAddress: seaportContractAddress, + isFee: false, }); }); @@ -2185,6 +2212,7 @@ describe('buy', () => { expect(result).toEqual({ type: ItemType.NATIVE, amount, + isFee: false, }); }); @@ -2195,6 +2223,7 @@ describe('buy', () => { expect(result).toEqual({ type: ItemType.NATIVE, amount, + isFee: false, }); }); }); diff --git a/packages/checkout/sdk/src/smartCheckout/buy/buy.ts b/packages/checkout/sdk/src/smartCheckout/buy/buy.ts index 4f52e5a30e..17a425114c 100644 --- a/packages/checkout/sdk/src/smartCheckout/buy/buy.ts +++ b/packages/checkout/sdk/src/smartCheckout/buy/buy.ts @@ -47,6 +47,7 @@ export const getItemRequirement = ( tokenAddress: string, amount: BigNumber, spenderAddress: string, + isFee: boolean = false, ): ItemRequirement => { switch (type) { case ItemType.ERC20: @@ -55,12 +56,14 @@ export const getItemRequirement = ( amount, tokenAddress, spenderAddress, + isFee, }; case ItemType.NATIVE: default: return { type: ItemType.NATIVE, amount, + isFee, }; } }; diff --git a/packages/checkout/sdk/src/smartCheckout/gas/gasCalculator.test.ts b/packages/checkout/sdk/src/smartCheckout/gas/gasCalculator.test.ts index 7dd9392d6d..e340e46cf5 100644 --- a/packages/checkout/sdk/src/smartCheckout/gas/gasCalculator.test.ts +++ b/packages/checkout/sdk/src/smartCheckout/gas/gasCalculator.test.ts @@ -27,6 +27,7 @@ describe('gasCalculator', () => { expect(item).toEqual({ type: ItemType.NATIVE, amount: BigNumber.from(100000), + isFee: true, }); }); @@ -47,6 +48,7 @@ describe('gasCalculator', () => { tokenAddress: '0xERC20', amount: BigNumber.from(100000), spenderAddress: '0xSEAPORT', + isFee: false, }, approvalTransaction: { from: '0xADDRESS', data: '0xDATA', to: '0xSEAPORT' }, }, @@ -73,6 +75,7 @@ describe('gasCalculator', () => { expect(item).toEqual({ type: ItemType.NATIVE, amount: BigNumber.from(300000), + isFee: true, }); }); @@ -99,6 +102,7 @@ describe('gasCalculator', () => { tokenAddress: '0xERC20', amount: BigNumber.from(100000), spenderAddress: '0xSEAPORT', + isFee: false, }, approvalTransaction: { from: '0xADDRESS', data: '0xDATA', to: '0xSEAPORT' }, }, @@ -126,6 +130,7 @@ describe('gasCalculator', () => { expect(item).toEqual({ type: ItemType.NATIVE, amount: BigNumber.from(400000), + isFee: true, }); }); @@ -152,6 +157,7 @@ describe('gasCalculator', () => { tokenAddress: '0xERC20', amount: BigNumber.from(100000), spenderAddress: '0xSEAPORT', + isFee: false, }, approvalTransaction: { from: '0xADDRESS', data: '0xDATA', to: '0xSEAPORT' }, }, @@ -182,6 +188,7 @@ describe('gasCalculator', () => { tokenAddress: '0xERC20', amount: BigNumber.from(400000), spenderAddress: '', + isFee: true, }); }); @@ -261,6 +268,7 @@ describe('gasCalculator', () => { expect(item).toEqual({ type: ItemType.NATIVE, amount: BigNumber.from(100000), + isFee: true, }); }); @@ -279,6 +287,7 @@ describe('gasCalculator', () => { expect(item).toEqual({ type: ItemType.NATIVE, amount: BigNumber.from(100000), + isFee: true, }); }); @@ -300,6 +309,7 @@ describe('gasCalculator', () => { amount: BigNumber.from(100000), tokenAddress: '0xERC20', spenderAddress: '', + isFee: true, }); }); }); diff --git a/packages/checkout/sdk/src/smartCheckout/gas/gasCalculator.ts b/packages/checkout/sdk/src/smartCheckout/gas/gasCalculator.ts index 3bc46d39d9..74192b12c8 100644 --- a/packages/checkout/sdk/src/smartCheckout/gas/gasCalculator.ts +++ b/packages/checkout/sdk/src/smartCheckout/gas/gasCalculator.ts @@ -31,6 +31,7 @@ export const getGasItemRequirement = ( return { type: ItemType.NATIVE, amount: gas, + isFee: true, }; } @@ -39,6 +40,7 @@ export const getGasItemRequirement = ( amount: gas, tokenAddress: transactionOrGas.gasToken.tokenAddress, spenderAddress: '', + isFee: true, }; }; diff --git a/packages/checkout/sdk/src/smartCheckout/routing/bridgeAndSwap/bridgeAndSwapRoute.test.ts b/packages/checkout/sdk/src/smartCheckout/routing/bridgeAndSwap/bridgeAndSwapRoute.test.ts index b47a296328..649af66c9f 100644 --- a/packages/checkout/sdk/src/smartCheckout/routing/bridgeAndSwap/bridgeAndSwapRoute.test.ts +++ b/packages/checkout/sdk/src/smartCheckout/routing/bridgeAndSwap/bridgeAndSwapRoute.test.ts @@ -397,6 +397,7 @@ describe('bridgeAndSwapRoute', () => { symbol: 'YEET', } as TokenInfo, }, + isFee: false, }; const bridgeableTokens: string[] = [INDEXER_ETH_ROOT_CONTRACT_ADDRESS, '0xIMXL1']; @@ -629,6 +630,7 @@ describe('bridgeAndSwapRoute', () => { symbol: 'YEET', } as TokenInfo, }, + isFee: false, }; const bridgeableTokens: string[] = []; diff --git a/packages/checkout/sdk/src/smartCheckout/routing/routingCalculator.test.ts b/packages/checkout/sdk/src/smartCheckout/routing/routingCalculator.test.ts index e70b1e781c..c63ad7f5ca 100644 --- a/packages/checkout/sdk/src/smartCheckout/routing/routingCalculator.test.ts +++ b/packages/checkout/sdk/src/smartCheckout/routing/routingCalculator.test.ts @@ -290,6 +290,7 @@ describe('routingCalculator', () => { const balanceERC20Requirement: BalanceERC20Requirement = { type: ItemType.ERC20, sufficient: false, + isFee: false, delta: { balance: BigNumber.from(10), formattedBalance: '10', @@ -1147,6 +1148,7 @@ describe('routingCalculator', () => { const balanceERC20Requirement: BalanceERC20Requirement = { type: ItemType.ERC20, sufficient: false, + isFee: false, delta: { balance: BigNumber.from(10), formattedBalance: '10', @@ -1272,6 +1274,7 @@ describe('routingCalculator', () => { const balanceERC20Requirement: BalanceERC20Requirement = { type: ItemType.ERC20, sufficient: false, + isFee: false, delta: { balance: BigNumber.from(10), formattedBalance: '10', diff --git a/packages/checkout/sdk/src/smartCheckout/routing/swap/swapRoute.test.ts b/packages/checkout/sdk/src/smartCheckout/routing/swap/swapRoute.test.ts index d0b9dffae9..9070f5934c 100644 --- a/packages/checkout/sdk/src/smartCheckout/routing/swap/swapRoute.test.ts +++ b/packages/checkout/sdk/src/smartCheckout/routing/swap/swapRoute.test.ts @@ -1283,6 +1283,7 @@ describe('swapRoute', () => { balance: BigNumber.from(1), formattedBalance: '1', }, + isFee: false, }; const requiredToken = getRequiredToken(balanceRequirement); @@ -1324,6 +1325,7 @@ describe('swapRoute', () => { balance: BigNumber.from(1), formattedBalance: '1', }, + isFee: false, }; const requiredToken = getRequiredToken(balanceRequirement); diff --git a/packages/checkout/sdk/src/smartCheckout/smartCheckout.test.ts b/packages/checkout/sdk/src/smartCheckout/smartCheckout.test.ts index abb639139b..f52c0a096e 100644 --- a/packages/checkout/sdk/src/smartCheckout/smartCheckout.test.ts +++ b/packages/checkout/sdk/src/smartCheckout/smartCheckout.test.ts @@ -157,6 +157,7 @@ describe('smartCheckout', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, ]; @@ -361,6 +362,7 @@ describe('smartCheckout', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, ]; @@ -565,6 +567,7 @@ describe('smartCheckout', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, ]; @@ -776,6 +779,7 @@ describe('smartCheckout', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, ]; @@ -995,6 +999,7 @@ describe('smartCheckout', () => { { type: ItemType.NATIVE, amount: BigNumber.from(1), + isFee: false, }, ]; @@ -1169,6 +1174,7 @@ describe('smartCheckout', () => { tokenAddress: '0xERC20', amount: BigNumber.from(1), spenderAddress: '0x1', + isFee: false, }, ]; @@ -1289,6 +1295,7 @@ describe('smartCheckout', () => { tokenAddress: '0xERC20', amount: BigNumber.from(1), spenderAddress: '0x1', + isFee: false, }, ]; @@ -1384,6 +1391,7 @@ describe('smartCheckout', () => { }, type: ItemType.NATIVE, }, + isFee: false, }, { sufficient: true, @@ -1414,6 +1422,7 @@ describe('smartCheckout', () => { decimals: 6, }, }, + isFee: false, }, ], }; @@ -1477,6 +1486,7 @@ describe('smartCheckout', () => { tokenAddress: '0xERC20', amount: BigNumber.from(10), spenderAddress: '0x1', + isFee: false, }, ]; diff --git a/packages/checkout/sdk/src/types/smartCheckout.ts b/packages/checkout/sdk/src/types/smartCheckout.ts index 2d4a995ac5..26e757adc3 100644 --- a/packages/checkout/sdk/src/types/smartCheckout.ts +++ b/packages/checkout/sdk/src/types/smartCheckout.ts @@ -488,6 +488,8 @@ export type NativeItem = { type: ItemType.NATIVE; /** The amount of the item. */ amount: BigNumber; + /** Flag to indicate if the item is a transaction fee */ + isFee: boolean; }; /** @@ -506,6 +508,8 @@ export type ERC20Item = { amount: BigNumber; /** The contract address of the approver. */ spenderAddress: string; + /** Flag to indicate if the item is a transaction fee */ + isFee: boolean; }; /** @@ -934,13 +938,15 @@ export type TransactionRequirement = { current: ItemBalance; /** The delta between the required and current balances. */ delta: BalanceDelta; + /** Flags if the requirement is needed for transaction fees */ + isFee: boolean; }; /** * Represents the balance for either a native or ERC20 token. * @property {ItemType.NATIVE | ItemType.ERC20} type * @property {BigNumber} balance - * @property {string} formattedBalanc + * @property {string} formattedBalance * @property {TokenInfo} token */ export type TokenBalance = { diff --git a/packages/checkout/widgets-lib/src/widgets/sale/components/OrderFees.tsx b/packages/checkout/widgets-lib/src/widgets/sale/components/OrderFees.tsx index d4233780ec..86e1d335bb 100644 --- a/packages/checkout/widgets-lib/src/widgets/sale/components/OrderFees.tsx +++ b/packages/checkout/widgets-lib/src/widgets/sale/components/OrderFees.tsx @@ -11,17 +11,17 @@ export type FeesDisplay = { }; export type OrderFeesProps = { - swapFees: FeesDisplay; + fees: FeesDisplay; onFeesClick?: () => void; sx?: SxProps; }; -export function OrderFees({ sx, swapFees, onFeesClick }: OrderFeesProps) { +export function OrderFees({ sx, fees, onFeesClick }: OrderFeesProps) { return ( void; onProceedToBuy: (fundingBalance: FundingBalance) => void; onPayWithCard?: (paymentType: SalePaymentTypes) => void; + gasFees?: TokenBalance; }; export function OrderReview({ @@ -51,6 +53,7 @@ export function OrderReview({ onBackButtonClick, onPayWithCard, onProceedToBuy, + gasFees, }: OrderReviewProps) { const { eventTargetState: { eventTarget }, @@ -68,7 +71,7 @@ export function OrderReview({ const [showCoinsDrawer, setShowCoinsDrawer] = useState(false); const [selectedCurrencyIndex, setSelectedCurrencyIndex] = useState(0); - const [swapFees, setSwapFees] = useState({ + const [transactionFees, setTransactionFees] = useState({ token: undefined, amount: '', fiatAmount: '', @@ -117,7 +120,7 @@ export function OrderReview({ return; } - setSwapFees({ + setTransactionFees({ token: fee.token, amount: fee.formattedAmount, fiatAmount: calculateCryptoToFiat( @@ -133,6 +136,37 @@ export function OrderReview({ }); }, [fundingBalance, conversions, provider]); + useEffect(() => { + if (fundingBalance.type === FundingStepType.SWAP) { + return; + } + + if (!gasFees || gasFees.balance.lte(0)) { + return; + } + + const fiatAmount = calculateCryptoToFiat( + gasFees.formattedBalance, + gasFees.token.symbol, + conversions, + ); + + setTransactionFees({ + token: gasFees.token, + amount: gasFees.formattedBalance, + fiatAmount, + formattedFees: [ + { + label: t('drawers.feesBreakdown.fees.gasFeeMove.label'), + fiatAmount: `≈ ${t('drawers.feesBreakdown.fees.fiatPricePrefix')}${fiatAmount}`, + amount: `${tokenValueFormat(gasFees.formattedBalance)}`, + prefix: '~ ', + token: gasFees.token, + }, + ], + }); + }, [gasFees, fundingBalance, conversions]); + // Trigger page loaded event useMount( () => { @@ -155,7 +189,7 @@ export function OrderReview({ ); const multiple = items.length > 1; - const withFees = !loadingBalances && fundingBalance.type === FundingStepType.SWAP; + const withFees = transactionFees.formattedFees.length > 0; return ( {!multiple && withFees && ( {multiple && withFees && ( 'token' in balance && balance.token !== undefined; + export type FundingBalanceParams = { provider: Web3Provider; checkout: Checkout; @@ -30,6 +34,7 @@ export type FundingBalanceParams = { fundingItemRequirement: TransactionRequirement ) => void; onComplete?: (balances: FundingBalance[]) => void; + onUpdateGasFees?: (fees: TokenBalance) => void; }; export const fetchFundingBalances = async ( @@ -45,6 +50,7 @@ export const fetchFundingBalances = async ( getIsGasless, onComplete, onFundingRequirement, + onUpdateGasFees, } = params; const signer = provider?.getSigner(); @@ -106,7 +112,7 @@ export const fetchFundingBalances = async ( }) .filter(Boolean) as Promise[]; - const results = await wrapPromisesWithOnResolve( + return await wrapPromisesWithOnResolve( balancePromises, ({ currency, smartCheckoutResult }) => { if (isBaseCurrency(currency.name)) { @@ -118,9 +124,13 @@ export const fetchFundingBalances = async ( updateFundingBalances( getFundingBalances(smartCheckoutResult, environment), ); + + const feeRequirement = smartCheckoutResult.transactionRequirements.find((requirement) => requirement.isFee); + + if (feeRequirement && isTokenFee(feeRequirement.required) && onUpdateGasFees) { + onUpdateGasFees(feeRequirement.required); + } } }, ); - - return results; }; diff --git a/packages/checkout/widgets-lib/src/widgets/sale/functions/getTopUpViewData.test.ts b/packages/checkout/widgets-lib/src/widgets/sale/functions/getTopUpViewData.test.ts index 1866d6204c..4ae5e17817 100644 --- a/packages/checkout/widgets-lib/src/widgets/sale/functions/getTopUpViewData.test.ts +++ b/packages/checkout/widgets-lib/src/widgets/sale/functions/getTopUpViewData.test.ts @@ -47,6 +47,7 @@ describe('getTopUpViewData', () => { balance: BigNumber.from(50), formattedBalance: '50', }, + isFee: false, }; const insufficientERC20: TransactionRequirement = { @@ -63,6 +64,7 @@ describe('getTopUpViewData', () => { balance: BigNumber.from(100), formattedBalance: '100', }, + isFee: false, }; const sufficientNative: TransactionRequirement = { @@ -79,6 +81,7 @@ describe('getTopUpViewData', () => { balance: BigNumber.from(30), formattedBalance: '30', }, + isFee: false, }; const insufficientNative: TransactionRequirement = { @@ -95,6 +98,7 @@ describe('getTopUpViewData', () => { balance: BigNumber.from(20), formattedBalance: '20', }, + isFee: false, }; it('should return correct data when both NATIVE and ERC20 are insufficient', () => { @@ -185,6 +189,7 @@ describe('getTopUpViewData', () => { balance: BigNumber.from(30), formattedBalance: '30', }, + isFee: true, }, { type: ItemType.ERC20, @@ -204,6 +209,7 @@ describe('getTopUpViewData', () => { balance: BigNumber.from(50), formattedBalance: '50', }, + isFee: false, }, ]; diff --git a/packages/checkout/widgets-lib/src/widgets/sale/hooks/useFundingBalances.ts b/packages/checkout/widgets-lib/src/widgets/sale/hooks/useFundingBalances.ts index 5ef6b92a9d..d4d55458a3 100644 --- a/packages/checkout/widgets-lib/src/widgets/sale/hooks/useFundingBalances.ts +++ b/packages/checkout/widgets-lib/src/widgets/sale/hooks/useFundingBalances.ts @@ -1,5 +1,5 @@ import { useContext, useRef, useState } from 'react'; -import { TransactionRequirement } from '@imtbl/checkout-sdk'; +import { TokenBalance, TransactionRequirement } from '@imtbl/checkout-sdk'; import { CryptoFiatContext } from '../../../context/crypto-fiat-context/CryptoFiatContext'; import { fetchFundingBalances } from '../functions/fetchFundingBalances'; import { FundingBalance, FundingBalanceResult } from '../types'; @@ -24,6 +24,7 @@ export const useFundingBalances = () => { FundingBalanceResult[] >([]); const [loadingBalances, setLoadingBalances] = useState(false); + const [gasFees, setGasFees] = useState(); const queryFundingBalances = () => { if ( @@ -60,6 +61,9 @@ export const useFundingBalances = () => { onFundingRequirement: (requirement) => { setTransactionRequirement(requirement); }, + onUpdateGasFees: (fees) => { + setGasFees(fees); + }, }); setFundingBalancesResult(results); @@ -76,6 +80,7 @@ export const useFundingBalances = () => { loadingBalances, fundingBalancesResult, transactionRequirement, + gasFees, queryFundingBalances, }; }; diff --git a/packages/checkout/widgets-lib/src/widgets/sale/views/OrderSummary.tsx b/packages/checkout/widgets-lib/src/widgets/sale/views/OrderSummary.tsx index 539fc9988f..9be358c497 100644 --- a/packages/checkout/widgets-lib/src/widgets/sale/views/OrderSummary.tsx +++ b/packages/checkout/widgets-lib/src/widgets/sale/views/OrderSummary.tsx @@ -126,6 +126,7 @@ export function OrderSummary({ subView }: OrderSummaryProps) { loadingBalances, fundingBalancesResult, transactionRequirement, + gasFees, queryFundingBalances, } = useFundingBalances(); @@ -228,6 +229,7 @@ export function OrderSummary({ subView }: OrderSummaryProps) { onProceedToBuy={onProceedToBuy} transactionRequirement={transactionRequirement} onPayWithCard={onPayWithCard} + gasFees={gasFees} /> )} {subView === OrderSummarySubViews.EXECUTE_FUNDING_ROUTE && (