From b9a3940dec411cafd2add806ccde681cb566eada Mon Sep 17 00:00:00 2001 From: yundi <2666544+yundifu@users.noreply.github.com> Date: Tue, 31 Oct 2023 09:24:33 +1100 Subject: [PATCH] feat: only send 1 meta transaction when sponsored [ID-1152] (#1080) --- .../sdk/src/zkEvm/sendTransaction.test.ts | 49 +++++++++++ .../passport/sdk/src/zkEvm/sendTransaction.ts | 82 ++++++++++++------- 2 files changed, 101 insertions(+), 30 deletions(-) diff --git a/packages/passport/sdk/src/zkEvm/sendTransaction.test.ts b/packages/passport/sdk/src/zkEvm/sendTransaction.test.ts index 373b8c29f8..d8b3181b8b 100644 --- a/packages/passport/sdk/src/zkEvm/sendTransaction.test.ts +++ b/packages/passport/sdk/src/zkEvm/sendTransaction.test.ts @@ -87,6 +87,55 @@ describe('sendTransaction', () => { ); }); + it('calls relayerClient.ethSendTransaction with sponsored meta transaction', async () => { + (retryWithDelay as jest.Mock).mockResolvedValue({ + status: RelayerTransactionStatus.SUCCESSFUL, + hash: transactionHash, + } as RelayerTransaction); + + const mockImxFeeOption = { + tokenPrice: '0', + tokenSymbol: 'IMX', + tokenDecimals: 18, + tokenAddress: '0x1337', + recipientAddress: '0x7331', + }; + + relayerClient.imGetFeeOptions.mockResolvedValue([mockImxFeeOption]); + + const result = await sendTransaction({ + params: [transactionRequest], + magicProvider, + jsonRpcProvider: jsonRpcProvider as JsonRpcProvider, + relayerClient: relayerClient as unknown as RelayerClient, + user: mockUserZkEvm, + guardianClient: guardianClient as unknown as GuardianClient, + }); + + expect(result).toEqual(transactionHash); + expect(guardianClient.validateEVMTransaction).toHaveBeenCalledWith( + { + chainId: chainIdEip155, + nonce, + user: mockUserZkEvm, + metaTransactions: [ + { + data: transactionRequest.data, + revertOnError: true, + to: mockUserZkEvm.zkEvm.ethAddress, + value: '0x00', + nonce, + }, + ], + }, + ); + + expect(relayerClient.ethSendTransaction).toHaveBeenCalledWith( + mockUserZkEvm.zkEvm.ethAddress, + signedTransactions, + ); + }); + it('calls guardian.evaluateTransaction with the correct arguments', async () => { (retryWithDelay as jest.Mock).mockResolvedValue({ status: RelayerTransactionStatus.SUCCESSFUL, diff --git a/packages/passport/sdk/src/zkEvm/sendTransaction.ts b/packages/passport/sdk/src/zkEvm/sendTransaction.ts index 0872e7bc19..0505993b93 100644 --- a/packages/passport/sdk/src/zkEvm/sendTransaction.ts +++ b/packages/passport/sdk/src/zkEvm/sendTransaction.ts @@ -1,7 +1,7 @@ import { - ExternalProvider, JsonRpcProvider, TransactionRequest, Web3Provider, + ExternalProvider, JsonRpcProvider, JsonRpcSigner, TransactionRequest, Web3Provider, } from '@ethersproject/providers'; -import { BigNumber } from 'ethers'; +import { BigNumber, BigNumberish } from 'ethers'; import { getEip155ChainId, getNonce, getSignedMetaTransactions } from './walletHelpers'; import { MetaTransaction, RelayerTransactionStatus } from './types'; import { JsonRpcError, RpcErrorCode } from './JsonRpcError'; @@ -22,6 +22,51 @@ export type EthSendTransactionParams = { params: Array; }; +const getMetaTransactions = async ( + metaTransaction: MetaTransaction, + nonce: BigNumberish, + chainId: BigNumber, + walletAddress: string, + signer: JsonRpcSigner, + relayerClient: RelayerClient, +): Promise => { + // NOTE: We sign the transaction before getting the fee options because + // accurate estimation of a transaction gas cost is only possible if the smart + // wallet contract can actually execute it (in a simulated environment) - and + // it can only execute signed transactions. + const signedTransaction = await getSignedMetaTransactions( + [metaTransaction], + nonce, + chainId, + walletAddress, + signer, + ); + + // TODO: ID-698 Add support for non-native gas payments (e.g ERC20, feeTransaction initialisation must change) + + // NOTE: "Fee Options" represent the multiple ways we could pay for the gas + // used in this transaction. Each fee option has a "recipientAddress" we + // should transfer the payment to, an amount and a currency. We choose one + // option and build a transaction that sends the expected currency amount for + // that option to the specified address. + const feeOptions = await relayerClient.imGetFeeOptions(walletAddress, signedTransaction); + const imxFeeOption = feeOptions.find((feeOption) => feeOption.tokenSymbol === 'IMX'); + if (!imxFeeOption) { + throw new Error('Failed to retrieve fees for IMX token'); + } + + const feeMetaTransaction: MetaTransaction = { + nonce, + to: imxFeeOption.recipientAddress, + value: imxFeeOption.tokenPrice, + revertOnError: true, + }; + if (BigNumber.from(feeMetaTransaction.value).isZero()) { + return [metaTransaction]; + } + return [metaTransaction, feeMetaTransaction]; +}; + export const sendTransaction = ({ params, magicProvider, @@ -50,49 +95,26 @@ export const sendTransaction = ({ revertOnError: true, }; - // NOTE: We sign the transaction before getting the fee options because - // accurate estimation of a transaction gas cost is only possible if the smart - // wallet contract can actually execute it (in a simulated environment) - and - // it can only execute signed transactions. - const signedTransaction = await getSignedMetaTransactions( - [metaTransaction], + const metaTransactions = await getMetaTransactions( + metaTransaction, nonce, chainIdBigNumber, user.zkEvm.ethAddress, signer, + relayerClient, ); - // TODO: ID-698 Add support for non-native gas payments (e.g ERC20, feeTransaction initialisation must change) - - // NOTE: "Fee Options" represent the multiple ways we could pay for the gas - // used in this transaction. Each fee option has a "recipientAddress" we - // should transfer the payment to, an amount and a currency. We choose one - // option and build a transaction that sends the expected currency amount for - // that option to the specified address. - const feeOptions = await relayerClient.imGetFeeOptions(user.zkEvm.ethAddress, signedTransaction); - const imxFeeOption = feeOptions.find((feeOption) => feeOption.tokenSymbol === 'IMX'); - if (!imxFeeOption) { - throw new Error('Failed to retrieve fees for IMX token'); - } - - const feeMetaTransaction: MetaTransaction = { - nonce, - to: imxFeeOption.recipientAddress, - value: imxFeeOption.tokenPrice, - revertOnError: true, - }; - await guardianClient.validateEVMTransaction({ chainId: getEip155ChainId(chainId), nonce: convertBigNumberishToString(nonce), user, - metaTransactions: [metaTransaction, feeMetaTransaction], + metaTransactions, }); // NOTE: We sign again because we now are adding the fee transaction, so the // whole payload is different and needs a new signature. const signedTransactions = await getSignedMetaTransactions( - [metaTransaction, feeMetaTransaction], + metaTransactions, nonce, chainIdBigNumber, user.zkEvm.ethAddress,