From 63662d4bb8518ad557698963234b9d1c080c721f Mon Sep 17 00:00:00 2001 From: Satish Ravi Date: Thu, 9 May 2024 17:09:46 -0700 Subject: [PATCH 1/4] feat(earn): add function for preparing supply transaction --- src/earn/prepareTransactions.test.tsx | 148 ++++++++++++++++++++++++++ src/earn/prepareTransactions.ts | 75 +++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 src/earn/prepareTransactions.test.tsx create mode 100644 src/earn/prepareTransactions.ts diff --git a/src/earn/prepareTransactions.test.tsx b/src/earn/prepareTransactions.test.tsx new file mode 100644 index 00000000000..6064b8220b7 --- /dev/null +++ b/src/earn/prepareTransactions.test.tsx @@ -0,0 +1,148 @@ +import BigNumber from 'bignumber.js' +import aavePool from 'src/abis/AavePoolV3' +import erc20 from 'src/abis/IERC20' +import { prepareSupplyTransactions } from 'src/earn/prepareTransactions' +import { TokenBalance } from 'src/tokens/slice' +import { Network, NetworkId } from 'src/transactions/types' +import { publicClient } from 'src/viem' +import { prepareTransactions } from 'src/viem/prepareTransactions' +import { encodeFunctionData } from 'viem' + +const mockFeeCurrency: TokenBalance = { + address: null, + balance: new BigNumber(100), // 10k units, 100.0 decimals + decimals: 2, + priceUsd: null, + lastKnownPriceUsd: null, + tokenId: 'arbitrum-sepolia:native', + symbol: 'FEE1', + name: 'Fee token 1', + networkId: NetworkId['arbitrum-sepolia'], + isNative: true, +} + +const mockToken: TokenBalance = { + address: '0xusdc', + balance: new BigNumber(10), + decimals: 6, + priceUsd: null, + lastKnownPriceUsd: null, + tokenId: 'arbitrum-sepolia:0xusdc', + symbol: 'USDC', + name: 'USD Coin', + networkId: NetworkId['arbitrum-sepolia'], +} + +jest.mock('src/viem/prepareTransactions') +jest.mock('viem', () => ({ + ...jest.requireActual('viem'), + encodeFunctionData: jest.fn(), +})) + +describe('prepareTransactions', () => { + beforeEach(() => { + jest.clearAllMocks() + jest.mocked(prepareTransactions).mockImplementation(async ({ baseTransactions }) => ({ + transactions: baseTransactions, + type: 'possible', + feeCurrency: mockFeeCurrency, + })) + jest.mocked(encodeFunctionData).mockReturnValue('0xencodedData') + }) + + describe('prepareSupplyTransactions', () => { + it('prepares transactions with approve and supply if not already approved', async () => { + jest.spyOn(publicClient[Network.Arbitrum], 'readContract').mockResolvedValue(BigInt(0)) + + const result = await prepareSupplyTransactions({ + amount: '5', + token: mockToken, + walletAddress: '0x1234', + feeCurrencies: [mockFeeCurrency], + poolContractAddress: '0x5678', + }) + + const expectedTransactions = [ + { + from: '0x1234', + to: '0xusdc', + data: '0xencodedData', + }, + { + from: '0x1234', + to: '0x5678', + data: '0xencodedData', + }, + ] + expect(result).toEqual({ + type: 'possible', + feeCurrency: mockFeeCurrency, + transactions: expectedTransactions, + }) + expect(publicClient[Network.Arbitrum].readContract).toHaveBeenCalledWith({ + address: '0xusdc', + abi: erc20.abi, + functionName: 'allowance', + args: ['0x1234', '0xusdc'], + }) + expect(encodeFunctionData).toHaveBeenNthCalledWith(1, { + abi: erc20.abi, + functionName: 'approve', + args: ['0x5678', BigInt(5e6)], + }) + expect(encodeFunctionData).toHaveBeenNthCalledWith(2, { + abi: aavePool, + functionName: 'supply', + args: ['0xusdc', BigInt(5e6), '0x1234', 0], + }) + expect(prepareTransactions).toHaveBeenCalledWith({ + baseTransactions: expectedTransactions, + feeCurrencies: [mockFeeCurrency], + spendToken: mockToken, + spendTokenAmount: new BigNumber(5), + }) + }) + + it('prepares transactions with supply if already approved', async () => { + jest.spyOn(publicClient[Network.Arbitrum], 'readContract').mockResolvedValue(BigInt(5e6)) + + const result = await prepareSupplyTransactions({ + amount: '5', + token: mockToken, + walletAddress: '0x1234', + feeCurrencies: [mockFeeCurrency], + poolContractAddress: '0x5678', + }) + + const expectedTransactions = [ + { + from: '0x1234', + to: '0x5678', + data: '0xencodedData', + }, + ] + expect(result).toEqual({ + type: 'possible', + feeCurrency: mockFeeCurrency, + transactions: expectedTransactions, + }) + expect(publicClient[Network.Arbitrum].readContract).toHaveBeenCalledWith({ + address: '0xusdc', + abi: erc20.abi, + functionName: 'allowance', + args: ['0x1234', '0xusdc'], + }) + expect(encodeFunctionData).toHaveBeenNthCalledWith(1, { + abi: aavePool, + functionName: 'supply', + args: ['0xusdc', BigInt(5e6), '0x1234', 0], + }) + expect(prepareTransactions).toHaveBeenCalledWith({ + baseTransactions: expectedTransactions, + feeCurrencies: [mockFeeCurrency], + spendToken: mockToken, + spendTokenAmount: new BigNumber(5), + }) + }) + }) +}) diff --git a/src/earn/prepareTransactions.ts b/src/earn/prepareTransactions.ts new file mode 100644 index 00000000000..46d331cebae --- /dev/null +++ b/src/earn/prepareTransactions.ts @@ -0,0 +1,75 @@ +import BigNumber from 'bignumber.js' +import aavePool from 'src/abis/AavePoolV3' +import erc20 from 'src/abis/IERC20' +import { TokenBalance } from 'src/tokens/slice' +import { publicClient } from 'src/viem' +import { TransactionRequest, prepareTransactions } from 'src/viem/prepareTransactions' +import { networkIdToNetwork } from 'src/web3/networkConfig' +import { Address, encodeFunctionData, parseUnits } from 'viem' + +export async function prepareSupplyTransactions({ + amount, + token, + walletAddress, + feeCurrencies, + poolContractAddress, +}: { + amount: string + token: TokenBalance + walletAddress: Address + feeCurrencies: TokenBalance[] + poolContractAddress: Address +}) { + const baseTransactions: TransactionRequest[] = [] + + // amount in smallest unit + const amountToSupply = parseUnits(amount, token.decimals) + + if (!token.address) { + // should never happen + throw new Error('Cannot use a token without address') + } + + const approvedAllowanceForSpender = await publicClient[ + networkIdToNetwork[token.networkId] + ].readContract({ + address: token.address as Address, + abi: erc20.abi, + functionName: 'allowance', + args: [walletAddress, token.address as Address], + }) + + if (approvedAllowanceForSpender < amountToSupply) { + const data = encodeFunctionData({ + abi: erc20.abi, + functionName: 'approve', + args: [poolContractAddress, amountToSupply], + }) + + const approveTx: TransactionRequest = { + from: walletAddress, + to: token.address as Address, + data, + } + baseTransactions.push(approveTx) + } + + const supplyTx: TransactionRequest = { + from: walletAddress, + to: poolContractAddress, + data: encodeFunctionData({ + abi: aavePool, + functionName: 'supply', + args: [token.address as Address, amountToSupply, walletAddress, 0], + }), + } + + baseTransactions.push(supplyTx) + + return prepareTransactions({ + feeCurrencies, + baseTransactions, + spendToken: token, + spendTokenAmount: new BigNumber(amount), + }) +} From c6b13b0ca4c7d3ab072b15ca6e67da6a91c4a736 Mon Sep 17 00:00:00 2001 From: Satish Ravi Date: Mon, 13 May 2024 15:30:08 -0700 Subject: [PATCH 2/4] simulate tx --- ...s.test.tsx => prepareTransactions.test.ts} | 101 +++++++++++++++++- src/earn/prepareTransactions.ts | 46 +++++++- src/web3/networkConfig.ts | 6 ++ 3 files changed, 148 insertions(+), 5 deletions(-) rename src/earn/{prepareTransactions.test.tsx => prepareTransactions.test.ts} (62%) diff --git a/src/earn/prepareTransactions.test.tsx b/src/earn/prepareTransactions.test.ts similarity index 62% rename from src/earn/prepareTransactions.test.tsx rename to src/earn/prepareTransactions.test.ts index 6064b8220b7..f53fac6d5fc 100644 --- a/src/earn/prepareTransactions.test.tsx +++ b/src/earn/prepareTransactions.test.ts @@ -1,4 +1,5 @@ import BigNumber from 'bignumber.js' +import { FetchMock } from 'jest-fetch-mock/types' import aavePool from 'src/abis/AavePoolV3' import erc20 from 'src/abis/IERC20' import { prepareSupplyTransactions } from 'src/earn/prepareTransactions' @@ -47,12 +48,38 @@ describe('prepareTransactions', () => { type: 'possible', feeCurrency: mockFeeCurrency, })) + jest.spyOn(publicClient[Network.Arbitrum], 'readContract').mockResolvedValue(BigInt(0)) jest.mocked(encodeFunctionData).mockReturnValue('0xencodedData') }) describe('prepareSupplyTransactions', () => { + const mockFetch = fetch as FetchMock + beforeEach(() => { + mockFetch.resetMocks() + }) + it('prepares transactions with approve and supply if not already approved', async () => { - jest.spyOn(publicClient[Network.Arbitrum], 'readContract').mockResolvedValue(BigInt(0)) + mockFetch.mockResponseOnce( + JSON.stringify({ + status: 'OK', + simulatedTransactions: [ + { + status: 'success', + blockNumber: '1', + gasNeeded: 3000, + gasUsed: 2800, + gasPrice: '1', + }, + { + status: 'success', + blockNumber: '1', + gasNeeded: 50000, + gasUsed: 49800, + gasPrice: '1', + }, + ], + }) + ) const result = await prepareSupplyTransactions({ amount: '5', @@ -72,6 +99,8 @@ describe('prepareTransactions', () => { from: '0x1234', to: '0x5678', data: '0xencodedData', + gas: BigInt(50000), + _estimatedGasUse: BigInt(49800), }, ] expect(result).toEqual({ @@ -83,7 +112,7 @@ describe('prepareTransactions', () => { address: '0xusdc', abi: erc20.abi, functionName: 'allowance', - args: ['0x1234', '0xusdc'], + args: ['0x1234', '0x5678'], }) expect(encodeFunctionData).toHaveBeenNthCalledWith(1, { abi: erc20.abi, @@ -105,6 +134,20 @@ describe('prepareTransactions', () => { it('prepares transactions with supply if already approved', async () => { jest.spyOn(publicClient[Network.Arbitrum], 'readContract').mockResolvedValue(BigInt(5e6)) + mockFetch.mockResponseOnce( + JSON.stringify({ + status: 'OK', + simulatedTransactions: [ + { + status: 'success', + blockNumber: '1', + gasNeeded: 50000, + gasUsed: 49800, + gasPrice: '1', + }, + ], + }) + ) const result = await prepareSupplyTransactions({ amount: '5', @@ -119,6 +162,8 @@ describe('prepareTransactions', () => { from: '0x1234', to: '0x5678', data: '0xencodedData', + gas: BigInt(50000), + _estimatedGasUse: BigInt(49800), }, ] expect(result).toEqual({ @@ -130,7 +175,7 @@ describe('prepareTransactions', () => { address: '0xusdc', abi: erc20.abi, functionName: 'allowance', - args: ['0x1234', '0xusdc'], + args: ['0x1234', '0x5678'], }) expect(encodeFunctionData).toHaveBeenNthCalledWith(1, { abi: aavePool, @@ -144,5 +189,55 @@ describe('prepareTransactions', () => { spendTokenAmount: new BigNumber(5), }) }) + + it('throws if simulate transactions sends a non 200 response', async () => { + mockFetch.mockResponseOnce( + JSON.stringify({ + status: 'ERROR', + error: 'something went wrong', + }), + { status: 500 } + ) + + await expect( + prepareSupplyTransactions({ + amount: '5', + token: mockToken, + walletAddress: '0x1234', + feeCurrencies: [mockFeeCurrency], + poolContractAddress: '0x5678', + }) + ).rejects.toThrow('Failed to simulate transactions') + }) + + it('throws if supply transaction simulation status is failure', async () => { + mockFetch.mockResponseOnce( + JSON.stringify({ + status: 'OK', + simulatedTransactions: [ + { + status: 'success', + blockNumber: '1', + gasNeeded: 3000, + gasUsed: 2800, + gasPrice: '1', + }, + { + status: 'failure', + }, + ], + }) + ) + + await expect( + prepareSupplyTransactions({ + amount: '5', + token: mockToken, + walletAddress: '0x1234', + feeCurrencies: [mockFeeCurrency], + poolContractAddress: '0x5678', + }) + ).rejects.toThrow('Failed to simulate supply transaction') + }) }) }) diff --git a/src/earn/prepareTransactions.ts b/src/earn/prepareTransactions.ts index 46d331cebae..1a2a769609b 100644 --- a/src/earn/prepareTransactions.ts +++ b/src/earn/prepareTransactions.ts @@ -2,11 +2,23 @@ import BigNumber from 'bignumber.js' import aavePool from 'src/abis/AavePoolV3' import erc20 from 'src/abis/IERC20' import { TokenBalance } from 'src/tokens/slice' +import { fetchWithTimeout } from 'src/utils/fetchWithTimeout' import { publicClient } from 'src/viem' import { TransactionRequest, prepareTransactions } from 'src/viem/prepareTransactions' -import { networkIdToNetwork } from 'src/web3/networkConfig' +import networkConfig, { networkIdToNetwork } from 'src/web3/networkConfig' import { Address, encodeFunctionData, parseUnits } from 'viem' +type SimulatedTransactionResponse = { + status: 'OK' + simulatedTransactions: { + status: 'success' | 'failure' + blockNumber: string + gasNeeded: number + gasUsed: number + gasPrice: string + }[] +} + export async function prepareSupplyTransactions({ amount, token, @@ -36,7 +48,7 @@ export async function prepareSupplyTransactions({ address: token.address as Address, abi: erc20.abi, functionName: 'allowance', - args: [walletAddress, token.address as Address], + args: [walletAddress, poolContractAddress], }) if (approvedAllowanceForSpender < amountToSupply) { @@ -66,6 +78,36 @@ export async function prepareSupplyTransactions({ baseTransactions.push(supplyTx) + const response = await fetchWithTimeout(networkConfig.simulateTransactionsUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + transactions: baseTransactions, + networkId: token.networkId, + }), + }) + + if (!response.ok) { + throw new Error( + `Failed to simulate transactions. status ${response.status}, text: ${await response.text()}` + ) + } + + // extract fee of the supply transaction and set gas fields + const { simulatedTransactions }: SimulatedTransactionResponse = await response.json() + const supplySimulatedTx = simulatedTransactions[simulatedTransactions.length - 1] + + if (supplySimulatedTx.status !== 'success') { + throw new Error( + `Failed to simulate supply transaction. response: ${JSON.stringify(simulatedTransactions)}` + ) + } + + baseTransactions[baseTransactions.length - 1].gas = BigInt(supplySimulatedTx.gasNeeded) + baseTransactions[baseTransactions.length - 1]._estimatedGasUse = BigInt(supplySimulatedTx.gasUsed) + return prepareTransactions({ feeCurrencies, baseTransactions, diff --git a/src/web3/networkConfig.ts b/src/web3/networkConfig.ts index 39c3fea53c1..cde6bc6452b 100644 --- a/src/web3/networkConfig.ts +++ b/src/web3/networkConfig.ts @@ -73,6 +73,7 @@ interface NetworkConfig { getPointsHistoryUrl: string trackPointsEventUrl: string getPointsBalanceUrl: string + simulateTransactionsUrl: string viemChain: { [key in Network]: ViemChain } @@ -272,6 +273,9 @@ const TRACK_POINTS_EVENT_MAINNET = `${CLOUD_FUNCTIONS_MAINNET}/trackPointsEvent` const GET_POINTS_BALANCE_ALFAJORES = `${CLOUD_FUNCTIONS_STAGING}/getPointsBalance` const GET_POINTS_BALANCE_MAINNET = `${CLOUD_FUNCTIONS_MAINNET}/getPointsBalance` +const SIMULATE_TRANSACTIONS_ALFAJORES = `${CLOUD_FUNCTIONS_STAGING}/simulateTransactions` +const SIMULATE_TRANSACTIONS_MAINNET = `${CLOUD_FUNCTIONS_MAINNET}/simulateTransactions` + const networkConfigs: { [testnet: string]: NetworkConfig } = { [Testnets.alfajores]: { networkId: '44787', @@ -333,6 +337,7 @@ const networkConfigs: { [testnet: string]: NetworkConfig } = { getPointsHistoryUrl: GET_POINTS_HISTORY_ALFAJORES, trackPointsEventUrl: TRACK_POINTS_EVENT_ALFAJORES, getPointsBalanceUrl: GET_POINTS_BALANCE_ALFAJORES, + simulateTransactionsUrl: SIMULATE_TRANSACTIONS_ALFAJORES, viemChain: { [Network.Celo]: celoAlfajores, [Network.Ethereum]: ethereumSepolia, @@ -428,6 +433,7 @@ const networkConfigs: { [testnet: string]: NetworkConfig } = { getPointsHistoryUrl: GET_POINTS_HISTORY_MAINNET, trackPointsEventUrl: TRACK_POINTS_EVENT_MAINNET, getPointsBalanceUrl: GET_POINTS_BALANCE_MAINNET, + simulateTransactionsUrl: SIMULATE_TRANSACTIONS_MAINNET, viemChain: { [Network.Celo]: celo, [Network.Ethereum]: ethereum, From 23d8add8dd924ba32adb1217dc1e956b96b3280b Mon Sep 17 00:00:00 2001 From: Satish Ravi Date: Mon, 13 May 2024 15:33:33 -0700 Subject: [PATCH 3/4] drop the as Address cast --- src/earn/prepareTransactions.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/earn/prepareTransactions.ts b/src/earn/prepareTransactions.ts index 1a2a769609b..a162b99d76d 100644 --- a/src/earn/prepareTransactions.ts +++ b/src/earn/prepareTransactions.ts @@ -6,7 +6,7 @@ import { fetchWithTimeout } from 'src/utils/fetchWithTimeout' import { publicClient } from 'src/viem' import { TransactionRequest, prepareTransactions } from 'src/viem/prepareTransactions' import networkConfig, { networkIdToNetwork } from 'src/web3/networkConfig' -import { Address, encodeFunctionData, parseUnits } from 'viem' +import { Address, encodeFunctionData, isAddress, parseUnits } from 'viem' type SimulatedTransactionResponse = { status: 'OK' @@ -37,7 +37,7 @@ export async function prepareSupplyTransactions({ // amount in smallest unit const amountToSupply = parseUnits(amount, token.decimals) - if (!token.address) { + if (!token.address || !isAddress(token.address)) { // should never happen throw new Error('Cannot use a token without address') } @@ -45,7 +45,7 @@ export async function prepareSupplyTransactions({ const approvedAllowanceForSpender = await publicClient[ networkIdToNetwork[token.networkId] ].readContract({ - address: token.address as Address, + address: token.address, abi: erc20.abi, functionName: 'allowance', args: [walletAddress, poolContractAddress], @@ -60,7 +60,7 @@ export async function prepareSupplyTransactions({ const approveTx: TransactionRequest = { from: walletAddress, - to: token.address as Address, + to: token.address, data, } baseTransactions.push(approveTx) @@ -72,7 +72,7 @@ export async function prepareSupplyTransactions({ data: encodeFunctionData({ abi: aavePool, functionName: 'supply', - args: [token.address as Address, amountToSupply, walletAddress, 0], + args: [token.address, amountToSupply, walletAddress, 0], }), } From 794001fca46fadf3140de655a12b6af95c500675 Mon Sep 17 00:00:00 2001 From: Satish Ravi Date: Mon, 13 May 2024 16:22:39 -0700 Subject: [PATCH 4/4] fix tests, feedback --- src/earn/prepareTransactions.test.ts | 45 +++++++++++++++++++++++----- src/earn/prepareTransactions.ts | 9 +++++- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/earn/prepareTransactions.test.ts b/src/earn/prepareTransactions.test.ts index f53fac6d5fc..dddec0295ac 100644 --- a/src/earn/prepareTransactions.test.ts +++ b/src/earn/prepareTransactions.test.ts @@ -7,7 +7,7 @@ import { TokenBalance } from 'src/tokens/slice' import { Network, NetworkId } from 'src/transactions/types' import { publicClient } from 'src/viem' import { prepareTransactions } from 'src/viem/prepareTransactions' -import { encodeFunctionData } from 'viem' +import { Address, encodeFunctionData } from 'viem' const mockFeeCurrency: TokenBalance = { address: null, @@ -22,13 +22,15 @@ const mockFeeCurrency: TokenBalance = { isNative: true, } +const mockTokenAddress: Address = '0x1234567890abcdef1234567890abcdef12345678' + const mockToken: TokenBalance = { - address: '0xusdc', + address: mockTokenAddress, balance: new BigNumber(10), decimals: 6, priceUsd: null, lastKnownPriceUsd: null, - tokenId: 'arbitrum-sepolia:0xusdc', + tokenId: `arbitrum-sepolia:${mockTokenAddress}`, symbol: 'USDC', name: 'USD Coin', networkId: NetworkId['arbitrum-sepolia'], @@ -92,7 +94,7 @@ describe('prepareTransactions', () => { const expectedTransactions = [ { from: '0x1234', - to: '0xusdc', + to: mockTokenAddress, data: '0xencodedData', }, { @@ -109,7 +111,7 @@ describe('prepareTransactions', () => { transactions: expectedTransactions, }) expect(publicClient[Network.Arbitrum].readContract).toHaveBeenCalledWith({ - address: '0xusdc', + address: mockTokenAddress, abi: erc20.abi, functionName: 'allowance', args: ['0x1234', '0x5678'], @@ -122,7 +124,7 @@ describe('prepareTransactions', () => { expect(encodeFunctionData).toHaveBeenNthCalledWith(2, { abi: aavePool, functionName: 'supply', - args: ['0xusdc', BigInt(5e6), '0x1234', 0], + args: [mockTokenAddress, BigInt(5e6), '0x1234', 0], }) expect(prepareTransactions).toHaveBeenCalledWith({ baseTransactions: expectedTransactions, @@ -172,7 +174,7 @@ describe('prepareTransactions', () => { transactions: expectedTransactions, }) expect(publicClient[Network.Arbitrum].readContract).toHaveBeenCalledWith({ - address: '0xusdc', + address: mockTokenAddress, abi: erc20.abi, functionName: 'allowance', args: ['0x1234', '0x5678'], @@ -180,7 +182,7 @@ describe('prepareTransactions', () => { expect(encodeFunctionData).toHaveBeenNthCalledWith(1, { abi: aavePool, functionName: 'supply', - args: ['0xusdc', BigInt(5e6), '0x1234', 0], + args: [mockTokenAddress, BigInt(5e6), '0x1234', 0], }) expect(prepareTransactions).toHaveBeenCalledWith({ baseTransactions: expectedTransactions, @@ -239,5 +241,32 @@ describe('prepareTransactions', () => { }) ).rejects.toThrow('Failed to simulate supply transaction') }) + + it('throws if simulated transactions length does not match base transactions length', async () => { + mockFetch.mockResponseOnce( + JSON.stringify({ + status: 'OK', + simulatedTransactions: [ + { + status: 'success', + blockNumber: '1', + gasNeeded: 3000, + gasUsed: 2800, + gasPrice: '1', + }, + ], + }) + ) + + await expect( + prepareSupplyTransactions({ + amount: '5', + token: mockToken, + walletAddress: '0x1234', + feeCurrencies: [mockFeeCurrency], + poolContractAddress: '0x5678', + }) + ).rejects.toThrow('Expected 2 simulated transactions, got 1') + }) }) }) diff --git a/src/earn/prepareTransactions.ts b/src/earn/prepareTransactions.ts index a162b99d76d..0e22dfefcab 100644 --- a/src/earn/prepareTransactions.ts +++ b/src/earn/prepareTransactions.ts @@ -39,7 +39,7 @@ export async function prepareSupplyTransactions({ if (!token.address || !isAddress(token.address)) { // should never happen - throw new Error('Cannot use a token without address') + throw new Error(`Cannot use a token without address. Token id: ${token.tokenId}`) } const approvedAllowanceForSpender = await publicClient[ @@ -97,6 +97,13 @@ export async function prepareSupplyTransactions({ // extract fee of the supply transaction and set gas fields const { simulatedTransactions }: SimulatedTransactionResponse = await response.json() + + if (simulatedTransactions.length !== baseTransactions.length) { + throw new Error( + `Expected ${baseTransactions.length} simulated transactions, got ${simulatedTransactions.length}, response: ${JSON.stringify(simulatedTransactions)}` + ) + } + const supplySimulatedTx = simulatedTransactions[simulatedTransactions.length - 1] if (supplySimulatedTx.status !== 'success') {