From 1e9b20b8bdba55d243278646958c13b4d92b7e7a Mon Sep 17 00:00:00 2001 From: Jon <24399271+Jon-Alonso@users.noreply.github.com> Date: Thu, 12 Sep 2024 12:25:02 +1000 Subject: [PATCH] feat: Add a new Passport config option to automatically request a transaction when SCW is not deployed (#2149) Co-authored-by: Hayden Fowler --- .../src/context/ImmutableProvider.tsx | 1 + packages/passport/sdk/src/config/config.ts | 4 + packages/passport/sdk/src/types.ts | 9 + ...ndDeployTransactionAndPersonalSign.test.ts | 164 ++++++++++ .../sendDeployTransactionAndPersonalSign.ts | 44 +++ .../sdk/src/zkEvm/sendTransaction.test.ts | 260 ++++------------ .../passport/sdk/src/zkEvm/sendTransaction.ts | 174 +---------- .../sdk/src/zkEvm/transactionHelpers.test.ts | 290 ++++++++++++++++++ .../sdk/src/zkEvm/transactionHelpers.ts | 214 +++++++++++++ .../passport/sdk/src/zkEvm/zkEvmProvider.ts | 20 ++ 10 files changed, 819 insertions(+), 361 deletions(-) create mode 100644 packages/passport/sdk/src/zkEvm/sendDeployTransactionAndPersonalSign.test.ts create mode 100644 packages/passport/sdk/src/zkEvm/sendDeployTransactionAndPersonalSign.ts create mode 100644 packages/passport/sdk/src/zkEvm/transactionHelpers.test.ts create mode 100644 packages/passport/sdk/src/zkEvm/transactionHelpers.ts diff --git a/packages/passport/sdk-sample-app/src/context/ImmutableProvider.tsx b/packages/passport/sdk-sample-app/src/context/ImmutableProvider.tsx index 7b10084440..61ef4ebc2c 100644 --- a/packages/passport/sdk-sample-app/src/context/ImmutableProvider.tsx +++ b/packages/passport/sdk-sample-app/src/context/ImmutableProvider.tsx @@ -94,6 +94,7 @@ const getPassportConfig = (environment: EnvironmentNames): PassportModuleConfigu logoutRedirectUri: LOGOUT_MODE === 'silent' ? SILENT_LOGOUT_REDIRECT_URI : LOGOUT_REDIRECT_URI, + forceScwDeployBeforeMessageSignature: true, }; switch (environment) { diff --git a/packages/passport/sdk/src/config/config.ts b/packages/passport/sdk/src/config/config.ts index 4d5e1fed60..771a39fccb 100644 --- a/packages/passport/sdk/src/config/config.ts +++ b/packages/passport/sdk/src/config/config.ts @@ -50,12 +50,15 @@ export class PassportConfiguration { readonly crossSdkBridgeEnabled: boolean; + readonly forceScwDeployBeforeMessageSignature: boolean; + readonly popupOverlayOptions: PopupOverlayOptions; constructor({ baseConfig, overrides, crossSdkBridgeEnabled, + forceScwDeployBeforeMessageSignature, popupOverlayOptions, ...oidcConfiguration }: PassportModuleConfiguration) { @@ -66,6 +69,7 @@ export class PassportConfiguration { this.oidcConfiguration = oidcConfiguration; this.baseConfig = baseConfig; this.crossSdkBridgeEnabled = crossSdkBridgeEnabled || false; + this.forceScwDeployBeforeMessageSignature = forceScwDeployBeforeMessageSignature || false; this.popupOverlayOptions = popupOverlayOptions || { disableGenericPopupOverlay: false, disableBlockedPopupOverlay: false, diff --git a/packages/passport/sdk/src/types.ts b/packages/passport/sdk/src/types.ts index a425ff0873..068e414515 100644 --- a/packages/passport/sdk/src/types.ts +++ b/packages/passport/sdk/src/types.ts @@ -91,10 +91,19 @@ export interface PassportModuleConfiguration * and not directly on the web. */ crossSdkBridgeEnabled?: boolean; + /** * Options for disabling the Passport popup overlays. */ popupOverlayOptions?: PopupOverlayOptions; + + /** + * This flag controls whether a deploy transaction is sent before signing an ERC191 message. + * + * @default false - By default, this behavior is disabled and the user will not be asked + * to approve a deploy transaction before signing. + */ + forceScwDeployBeforeMessageSignature?: boolean; } type WithRequired = T & { [P in K]-?: T[P] }; diff --git a/packages/passport/sdk/src/zkEvm/sendDeployTransactionAndPersonalSign.test.ts b/packages/passport/sdk/src/zkEvm/sendDeployTransactionAndPersonalSign.test.ts new file mode 100644 index 0000000000..7ec8edafdf --- /dev/null +++ b/packages/passport/sdk/src/zkEvm/sendDeployTransactionAndPersonalSign.test.ts @@ -0,0 +1,164 @@ +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import { Signer } from '@ethersproject/abstract-signer'; +import { Flow } from '@imtbl/metrics'; +import { BigNumber } from 'ethers'; +import { sendDeployTransactionAndPersonalSign } from './sendDeployTransactionAndPersonalSign'; +import { mockUserZkEvm } from '../test/mocks'; +import { RelayerClient } from './relayerClient'; +import GuardianClient from '../guardian'; +import * as transactionHelpers from './transactionHelpers'; +import * as personalSign from './personalSign'; + +jest.mock('./transactionHelpers'); +jest.mock('./personalSign'); + +describe('sendDeployTransactionAndPersonalSign', () => { + const signedTransactions = 'signedTransactions123'; + const relayerTransactionId = 'relayerTransactionId123'; + const transactionHash = 'transactionHash123'; + const signedMessage = 'signedMessage123'; + + const nonce = BigNumber.from(5); + + const params = ['message to sign']; + const rpcProvider = { + detectNetwork: jest.fn(), + }; + const relayerClient = { + imGetFeeOptions: jest.fn(), + ethSendTransaction: jest.fn(), + imGetTransactionByHash: jest.fn(), + }; + const guardianClient = { + validateEVMTransaction: jest.fn(), + withConfirmationScreen: jest.fn(), + }; + const ethSigner = { + getAddress: jest.fn(), + } as Partial as Signer; + const flow = { + addEvent: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + (transactionHelpers.prepareAndSignTransaction as jest.Mock).mockResolvedValue({ + signedTransactions, + relayerId: relayerTransactionId, + nonce, + }); + (transactionHelpers.pollRelayerTransaction as jest.Mock).mockResolvedValue({ + hash: transactionHash, + }); + (personalSign.personalSign as jest.Mock).mockResolvedValue(signedMessage); + (guardianClient.withConfirmationScreen as jest.Mock) + .mockImplementation(() => (task: () => void) => task()); + }); + + it('calls prepareAndSignTransaction with the correct arguments', async () => { + await sendDeployTransactionAndPersonalSign({ + params, + ethSigner, + rpcProvider: rpcProvider as unknown as StaticJsonRpcProvider, + relayerClient: relayerClient as unknown as RelayerClient, + zkEvmAddress: mockUserZkEvm.zkEvm.ethAddress, + guardianClient: guardianClient as unknown as GuardianClient, + flow: flow as unknown as Flow, + }); + + expect(transactionHelpers.prepareAndSignTransaction).toHaveBeenCalledWith({ + transactionRequest: { to: mockUserZkEvm.zkEvm.ethAddress, value: 0 }, + ethSigner, + rpcProvider: rpcProvider as unknown as StaticJsonRpcProvider, + relayerClient: relayerClient as unknown as RelayerClient, + guardianClient: guardianClient as unknown as GuardianClient, + zkEvmAddress: mockUserZkEvm.zkEvm.ethAddress, + flow: flow as unknown as Flow, + }); + }); + + it('calls personalSign with the correct arguments', async () => { + await sendDeployTransactionAndPersonalSign({ + params, + ethSigner, + rpcProvider: rpcProvider as unknown as StaticJsonRpcProvider, + relayerClient: relayerClient as unknown as RelayerClient, + zkEvmAddress: mockUserZkEvm.zkEvm.ethAddress, + guardianClient: guardianClient as unknown as GuardianClient, + flow: flow as unknown as Flow, + }); + + expect(personalSign.personalSign).toHaveBeenCalledWith({ + params, + ethSigner, + zkEvmAddress: mockUserZkEvm.zkEvm.ethAddress, + rpcProvider: rpcProvider as unknown as StaticJsonRpcProvider, + guardianClient: guardianClient as unknown as GuardianClient, + relayerClient: relayerClient as unknown as RelayerClient, + flow: flow as unknown as Flow, + }); + }); + + it('calls pollRelayerTransaction with the correct arguments', async () => { + await sendDeployTransactionAndPersonalSign({ + params, + ethSigner, + rpcProvider: rpcProvider as unknown as StaticJsonRpcProvider, + relayerClient: relayerClient as unknown as RelayerClient, + zkEvmAddress: mockUserZkEvm.zkEvm.ethAddress, + guardianClient: guardianClient as unknown as GuardianClient, + flow: flow as unknown as Flow, + }); + + expect(transactionHelpers.pollRelayerTransaction).toHaveBeenCalledWith( + relayerClient as unknown as RelayerClient, + relayerTransactionId, + flow as unknown as Flow, + ); + }); + + it('returns the signed message', async () => { + const result = await sendDeployTransactionAndPersonalSign({ + params, + ethSigner, + rpcProvider: rpcProvider as unknown as StaticJsonRpcProvider, + relayerClient: relayerClient as unknown as RelayerClient, + zkEvmAddress: mockUserZkEvm.zkEvm.ethAddress, + guardianClient: guardianClient as unknown as GuardianClient, + flow: flow as unknown as Flow, + }); + + expect(result).toEqual(signedMessage); + }); + + it('calls guardianClient.withConfirmationScreen with the correct arguments', async () => { + await sendDeployTransactionAndPersonalSign({ + params, + ethSigner, + rpcProvider: rpcProvider as unknown as StaticJsonRpcProvider, + relayerClient: relayerClient as unknown as RelayerClient, + zkEvmAddress: mockUserZkEvm.zkEvm.ethAddress, + guardianClient: guardianClient as unknown as GuardianClient, + flow: flow as unknown as Flow, + }); + + expect(guardianClient.withConfirmationScreen).toHaveBeenCalled(); + }); + + it('throws an error if any step fails', async () => { + const error = new Error('Something went wrong'); + (transactionHelpers.prepareAndSignTransaction as jest.Mock).mockRejectedValue(error); + + await expect( + sendDeployTransactionAndPersonalSign({ + params, + ethSigner, + rpcProvider: rpcProvider as unknown as StaticJsonRpcProvider, + relayerClient: relayerClient as unknown as RelayerClient, + zkEvmAddress: mockUserZkEvm.zkEvm.ethAddress, + guardianClient: guardianClient as unknown as GuardianClient, + flow: flow as unknown as Flow, + }), + ).rejects.toThrow(error); + }); +}); diff --git a/packages/passport/sdk/src/zkEvm/sendDeployTransactionAndPersonalSign.ts b/packages/passport/sdk/src/zkEvm/sendDeployTransactionAndPersonalSign.ts new file mode 100644 index 0000000000..41da69ad86 --- /dev/null +++ b/packages/passport/sdk/src/zkEvm/sendDeployTransactionAndPersonalSign.ts @@ -0,0 +1,44 @@ +import { prepareAndSignTransaction, pollRelayerTransaction, TransactionParams } from './transactionHelpers'; +import { personalSign } from './personalSign'; + +type EthSendDeployTransactionParams = TransactionParams & { + params: Array; +}; + +export const sendDeployTransactionAndPersonalSign = async ({ + params, + ethSigner, + rpcProvider, + relayerClient, + guardianClient, + zkEvmAddress, + flow, +}: EthSendDeployTransactionParams): Promise => { + const deployTransaction = { to: zkEvmAddress, value: 0 }; + + const { relayerId } = await prepareAndSignTransaction({ + transactionRequest: deployTransaction, + ethSigner, + rpcProvider, + guardianClient, + relayerClient, + zkEvmAddress, + flow, + }); + + return guardianClient.withConfirmationScreen()(async () => { + const signedMessage = await personalSign({ + params, + ethSigner, + zkEvmAddress, + rpcProvider, + guardianClient, + relayerClient, + flow, + }); + + await pollRelayerTransaction(relayerClient, relayerId, flow); + + return signedMessage; + }); +}; diff --git a/packages/passport/sdk/src/zkEvm/sendTransaction.test.ts b/packages/passport/sdk/src/zkEvm/sendTransaction.test.ts index f992c3c2ef..857d65c181 100644 --- a/packages/passport/sdk/src/zkEvm/sendTransaction.test.ts +++ b/packages/passport/sdk/src/zkEvm/sendTransaction.test.ts @@ -2,21 +2,13 @@ import { StaticJsonRpcProvider, TransactionRequest } from '@ethersproject/provid import { Signer } from '@ethersproject/abstract-signer'; import { Flow } from '@imtbl/metrics'; import { BigNumber } from 'ethers'; -import { - getEip155ChainId, - getNonce, - signMetaTransactions, -} from './walletHelpers'; import { sendTransaction } from './sendTransaction'; -import { chainId, chainIdEip155, mockUserZkEvm } from '../test/mocks'; +import { mockUserZkEvm } from '../test/mocks'; import { RelayerClient } from './relayerClient'; -import { retryWithDelay } from '../network/retry'; -import { FeeOption, RelayerTransaction, RelayerTransactionStatus } from './types'; -import { JsonRpcError, RpcErrorCode } from './JsonRpcError'; import GuardianClient from '../guardian'; -import * as walletHelpers from './walletHelpers'; +import * as transactionHelpers from './transactionHelpers'; -jest.mock('./walletHelpers'); +jest.mock('./transactionHelpers'); jest.mock('../network/retry'); describe('sendTransaction', () => { @@ -24,7 +16,7 @@ describe('sendTransaction', () => { const relayerTransactionId = 'relayerTransactionId123'; const transactionHash = 'transactionHash123'; - const nonce = '5'; + const nonce = BigNumber.from(5); const transactionRequest: TransactionRequest = { to: mockUserZkEvm.zkEvm.ethAddress, @@ -49,120 +41,78 @@ describe('sendTransaction', () => { addEvent: jest.fn(), }; - const imxFeeOption: FeeOption = { - tokenPrice: '0x1', - tokenSymbol: 'IMX', - tokenDecimals: 18, - tokenAddress: '0x1337', - recipientAddress: '0x7331', - }; - beforeEach(() => { jest.resetAllMocks(); - relayerClient.imGetFeeOptions.mockResolvedValue([imxFeeOption]); - (getNonce as jest.Mock).mockResolvedValueOnce(nonce); - (getEip155ChainId as jest.Mock).mockReturnValue(chainIdEip155); - (signMetaTransactions as jest.Mock).mockResolvedValueOnce( + (transactionHelpers.prepareAndSignTransaction as jest.Mock).mockResolvedValue({ signedTransactions, - ); - relayerClient.ethSendTransaction.mockResolvedValue(relayerTransactionId); - rpcProvider.detectNetwork.mockResolvedValue({ chainId }); - guardianClient.validateEVMTransaction.mockResolvedValue(undefined); + relayerId: relayerTransactionId, + nonce, + }); + (transactionHelpers.pollRelayerTransaction as jest.Mock).mockResolvedValue({ + hash: transactionHash, + }); }); - describe('when the relayer returns a transaction with a "SUCCESSFUL" status', () => { - beforeEach(() => { - (retryWithDelay as jest.Mock).mockResolvedValue({ - status: RelayerTransactionStatus.SUCCESSFUL, - hash: transactionHash, - } as RelayerTransaction); + it('calls prepareAndSignTransaction with the correct arguments', async () => { + await sendTransaction({ + params: [transactionRequest], + ethSigner, + rpcProvider: rpcProvider as unknown as StaticJsonRpcProvider, + relayerClient: relayerClient as unknown as RelayerClient, + zkEvmAddress: mockUserZkEvm.zkEvm.ethAddress, + guardianClient: guardianClient as unknown as GuardianClient, + flow: flow as unknown as Flow, }); - it('calls relayerClient.imGetFeeOptions with the correct arguments', async () => { - (walletHelpers.encodedTransactions as jest.Mock).mockReturnValue('encodedTransactions123'); - - await sendTransaction({ - params: [transactionRequest], - ethSigner, - rpcProvider: rpcProvider as unknown as StaticJsonRpcProvider, - relayerClient: relayerClient as unknown as RelayerClient, - zkEvmAddress: mockUserZkEvm.zkEvm.ethAddress, - guardianClient: guardianClient as unknown as GuardianClient, - flow: flow as unknown as Flow, - }); - - expect(relayerClient.imGetFeeOptions).toHaveBeenCalledWith( - mockUserZkEvm.zkEvm.ethAddress, - 'encodedTransactions123', - ); + expect(transactionHelpers.prepareAndSignTransaction).toHaveBeenCalledWith({ + transactionRequest, + ethSigner, + rpcProvider: rpcProvider as unknown as StaticJsonRpcProvider, + relayerClient: relayerClient as unknown as RelayerClient, + guardianClient: guardianClient as unknown as GuardianClient, + zkEvmAddress: mockUserZkEvm.zkEvm.ethAddress, + flow: flow as unknown as Flow, }); + }); - it('calls relayerClient.ethSendTransaction with the correct arguments', async () => { - const result = await sendTransaction({ - params: [transactionRequest], - ethSigner, - rpcProvider: rpcProvider as unknown as StaticJsonRpcProvider, - relayerClient: relayerClient as unknown as RelayerClient, - zkEvmAddress: mockUserZkEvm.zkEvm.ethAddress, - guardianClient: guardianClient as unknown as GuardianClient, - flow: flow as unknown as Flow, - }); - - expect(result).toEqual(transactionHash); - expect(relayerClient.ethSendTransaction).toHaveBeenCalledWith( - mockUserZkEvm.zkEvm.ethAddress, - signedTransactions, - ); + it('calls pollRelayerTransaction with the correct arguments', async () => { + await sendTransaction({ + params: [transactionRequest], + ethSigner, + rpcProvider: rpcProvider as unknown as StaticJsonRpcProvider, + relayerClient: relayerClient as unknown as RelayerClient, + zkEvmAddress: mockUserZkEvm.zkEvm.ethAddress, + guardianClient: guardianClient as unknown as GuardianClient, + flow: flow as unknown as Flow, }); - it('calls relayerClient.ethSendTransaction with sponsored meta transaction', async () => { - const mockImxFeeOption = { - tokenPrice: '0', - tokenSymbol: 'IMX', - tokenDecimals: 18, - tokenAddress: '0x1337', - recipientAddress: '0x7331', - }; - - relayerClient.imGetFeeOptions.mockResolvedValue([mockImxFeeOption]); - - const result = await sendTransaction({ - params: [transactionRequest], - ethSigner, - rpcProvider: rpcProvider as unknown as StaticJsonRpcProvider, - relayerClient: relayerClient as unknown as RelayerClient, - zkEvmAddress: mockUserZkEvm.zkEvm.ethAddress, - guardianClient: guardianClient as unknown as GuardianClient, - flow: flow as unknown as Flow, - }); - - expect(result).toEqual(transactionHash); - expect(guardianClient.validateEVMTransaction).toHaveBeenCalledWith( - { - chainId: chainIdEip155, - nonce, - metaTransactions: [ - { - data: transactionRequest.data, - revertOnError: true, - to: mockUserZkEvm.zkEvm.ethAddress, - value: '0x00', - nonce, - }, - ], - }, - ); + expect(transactionHelpers.pollRelayerTransaction).toHaveBeenCalledWith( + relayerClient as unknown as RelayerClient, + relayerTransactionId, + flow as unknown as Flow, + ); + }); - expect(relayerClient.ethSendTransaction).toHaveBeenCalledWith( - mockUserZkEvm.zkEvm.ethAddress, - signedTransactions, - ); + it('returns the transaction hash', async () => { + const result = await sendTransaction({ + params: [transactionRequest], + ethSigner, + rpcProvider: rpcProvider as unknown as StaticJsonRpcProvider, + relayerClient: relayerClient as unknown as RelayerClient, + zkEvmAddress: mockUserZkEvm.zkEvm.ethAddress, + guardianClient: guardianClient as unknown as GuardianClient, + flow: flow as unknown as Flow, }); - it('calls guardian.evaluateTransaction with the correct arguments', async () => { - (getEip155ChainId as jest.Mock).mockReturnValue(`eip155:${chainId}`); + expect(result).toEqual(transactionHash); + }); + + it('throws an error if pollRelayerTransaction fails', async () => { + const error = new Error('Transaction failed'); + (transactionHelpers.pollRelayerTransaction as jest.Mock).mockRejectedValue(error); - const result = await sendTransaction({ + await expect( + sendTransaction({ params: [transactionRequest], ethSigner, rpcProvider: rpcProvider as unknown as StaticJsonRpcProvider, @@ -170,91 +120,7 @@ describe('sendTransaction', () => { zkEvmAddress: mockUserZkEvm.zkEvm.ethAddress, guardianClient: guardianClient as unknown as GuardianClient, flow: flow as unknown as Flow, - }); - - expect(result).toEqual(transactionHash); - expect(getEip155ChainId).toHaveBeenCalledWith(chainId); - expect(guardianClient.validateEVMTransaction).toHaveBeenCalledWith( - { - chainId: chainIdEip155, - nonce, - metaTransactions: [ - { - data: transactionRequest.data, - revertOnError: true, - to: mockUserZkEvm.zkEvm.ethAddress, - value: '0x00', - nonce, - }, - { - revertOnError: true, - to: imxFeeOption.recipientAddress, - value: BigNumber.from(imxFeeOption.tokenPrice), - nonce, - }, - ], - }, - ); - expect(relayerClient.ethSendTransaction).toHaveBeenCalledWith( - mockUserZkEvm.zkEvm.ethAddress, - signedTransactions, - ); - }); - }); - - describe('when the relayer returns a transaction with a "FAILED" status', () => { - beforeEach(() => { - (retryWithDelay as jest.Mock).mockResolvedValue({ - status: RelayerTransactionStatus.FAILED, - statusMessage: 'Unable to complete transaction', - } as RelayerTransaction); - }); - - it('returns and surfaces an error', async () => { - await expect( - sendTransaction({ - params: [transactionRequest], - ethSigner, - rpcProvider: rpcProvider as unknown as StaticJsonRpcProvider, - relayerClient: relayerClient as unknown as RelayerClient, - zkEvmAddress: mockUserZkEvm.zkEvm.ethAddress, - guardianClient: guardianClient as unknown as GuardianClient, - flow: flow as unknown as Flow, - }), - ).rejects.toThrow( - new JsonRpcError( - RpcErrorCode.INTERNAL_ERROR, - 'Transaction failed to submit with status FAILED. Error message: Unable to complete transaction', - ), - ); - }); - }); - - describe('when the relayer returns a transaction with a "CANCELLED" status', () => { - beforeEach(() => { - (retryWithDelay as jest.Mock).mockResolvedValue({ - status: RelayerTransactionStatus.CANCELLED, - statusMessage: 'Transaction cancelled', - } as RelayerTransaction); - }); - - it('returns and surfaces an error', async () => { - await expect( - sendTransaction({ - params: [transactionRequest], - ethSigner, - rpcProvider: rpcProvider as unknown as StaticJsonRpcProvider, - relayerClient: relayerClient as unknown as RelayerClient, - zkEvmAddress: mockUserZkEvm.zkEvm.ethAddress, - guardianClient: guardianClient as unknown as GuardianClient, - flow: flow as unknown as Flow, - }), - ).rejects.toThrow( - new JsonRpcError( - RpcErrorCode.INTERNAL_ERROR, - 'Transaction failed to submit with status CANCELLED. Error message: Transaction cancelled', - ), - ); - }); + }), + ).rejects.toThrow(error); }); }); diff --git a/packages/passport/sdk/src/zkEvm/sendTransaction.ts b/packages/passport/sdk/src/zkEvm/sendTransaction.ts index 1d636914a5..73546e1bb7 100644 --- a/packages/passport/sdk/src/zkEvm/sendTransaction.ts +++ b/packages/passport/sdk/src/zkEvm/sendTransaction.ts @@ -1,96 +1,7 @@ -import { BigNumber } from 'ethers'; -import { StaticJsonRpcProvider, TransactionRequest } from '@ethersproject/providers'; -import { Flow } from '@imtbl/metrics'; -import { Signer } from '@ethersproject/abstract-signer'; -import { - encodedTransactions, - getEip155ChainId, - getNonce, - getNormalisedTransactions, - signMetaTransactions, -} from './walletHelpers'; -import { FeeOption, MetaTransaction, RelayerTransactionStatus } from './types'; -import { JsonRpcError, RpcErrorCode } from './JsonRpcError'; -import { retryWithDelay } from '../network/retry'; -import { RelayerClient } from './relayerClient'; -import GuardianClient, { convertBigNumberishToString } from '../guardian'; +import { prepareAndSignTransaction, pollRelayerTransaction, TransactionParams } from './transactionHelpers'; -const MAX_TRANSACTION_HASH_RETRIEVAL_RETRIES = 30; -const TRANSACTION_HASH_RETRIEVAL_WAIT = 1000; - -type EthSendTransactionParams = { - ethSigner: Signer; - rpcProvider: StaticJsonRpcProvider; - guardianClient: GuardianClient; - relayerClient: RelayerClient; - zkEvmAddress: string, +type EthSendTransactionParams = TransactionParams & { params: Array; - flow: Flow; -}; - -const getFeeOption = async ( - metaTransaction: MetaTransaction, - walletAddress: string, - relayerClient: RelayerClient, -): Promise => { - const normalisedMetaTransaction = getNormalisedTransactions([metaTransaction]); - const transactions = encodedTransactions(normalisedMetaTransaction); - const feeOptions = await relayerClient.imGetFeeOptions(walletAddress, transactions); - - const imxFeeOption = feeOptions.find((feeOption) => feeOption.tokenSymbol === 'IMX'); - if (!imxFeeOption) { - throw new Error('Failed to retrieve fees for IMX token'); - } - - return imxFeeOption; -}; - -/** - * Prepares the meta transactions array to be signed by estimating the fee and - * getting the nonce from the smart wallet. - * - */ -const buildMetaTransactions = async ( - transactionRequest: TransactionRequest, - rpcProvider: StaticJsonRpcProvider, - relayerClient: RelayerClient, - zkevmAddress: string, -): Promise<[MetaTransaction, ...MetaTransaction[]]> => { - if (!transactionRequest.to) { - throw new JsonRpcError(RpcErrorCode.INVALID_PARAMS, 'eth_sendTransaction requires a "to" field'); - } - - const metaTransaction: MetaTransaction = { - to: transactionRequest.to, - data: transactionRequest.data, - nonce: BigNumber.from(0), // NOTE: We don't need a valid nonce to estimate the fee - value: transactionRequest.value, - revertOnError: true, - }; - - // Estimate the fee and get the nonce from the smart wallet - const [nonce, feeOption] = await Promise.all([ - getNonce(rpcProvider, zkevmAddress), - getFeeOption(metaTransaction, zkevmAddress, relayerClient), - ]); - - // Build the meta transactions array with a valid nonce and fee transaction - const metaTransactions: [MetaTransaction, ...MetaTransaction[]] = [{ - ...metaTransaction, - nonce, - }]; - // Add a fee transaction if the fee is non-zero - const feeValue = BigNumber.from(feeOption.tokenPrice); - if (!feeValue.isZero()) { - metaTransactions.push({ - nonce, - to: feeOption.recipientAddress, - value: feeValue, - revertOnError: true, - }); - } - - return metaTransactions; }; export const sendTransaction = async ({ @@ -102,83 +13,18 @@ export const sendTransaction = async ({ zkEvmAddress, flow, }: EthSendTransactionParams): Promise => { - const { chainId } = await rpcProvider.detectNetwork(); - const chainIdBigNumber = BigNumber.from(chainId); - flow.addEvent('endDetectNetwork'); + const transactionRequest = params[0]; - // Prepare the meta transactions by adding an optional fee transaction - const metaTransactions = await buildMetaTransactions( - params[0], + const { relayerId } = await prepareAndSignTransaction({ + transactionRequest, + ethSigner, rpcProvider, + guardianClient, relayerClient, zkEvmAddress, - ); - flow.addEvent('endBuildMetaTransactions'); - - const { nonce } = metaTransactions[0]; - if (!nonce) { - throw new Error('Failed to retrieve nonce from the smart wallet'); - } - - // Parallelize the validation and signing of the transaction - const validateEVMTransactionPromise = guardianClient.validateEVMTransaction({ - chainId: getEip155ChainId(chainId), - nonce: convertBigNumberishToString(nonce), - metaTransactions, + flow, }); - validateEVMTransactionPromise.then(() => flow.addEvent('endValidateEVMTransaction')); - - // NOTE: We sign again because we now are adding the fee transaction, so the - // whole payload is different and needs a new signature. - const getSignedMetaTransactionsPromise = signMetaTransactions( - metaTransactions, - nonce, - chainIdBigNumber, - zkEvmAddress, - ethSigner, - ); - getSignedMetaTransactionsPromise.then(() => flow.addEvent('endGetSignedMetaTransactions')); - - const [, signedTransactions] = await Promise.all([ - validateEVMTransactionPromise, - getSignedMetaTransactionsPromise, - ]); - - const relayerId = await relayerClient.ethSendTransaction(zkEvmAddress, signedTransactions); - flow.addEvent('endRelayerSendTransaction'); - - const retrieveRelayerTransaction = async () => { - const tx = await relayerClient.imGetTransactionByHash(relayerId); - // NOTE: The transaction hash is only available from the Relayer once the - // transaction is actually submitted onchain. Hence we need to poll the - // Relayer get transaction endpoint until the status transitions to one that - // has the hash available. - if (tx.status === RelayerTransactionStatus.PENDING) { - throw new Error(); - } - return tx; - }; - - const relayerTransaction = await retryWithDelay(retrieveRelayerTransaction, { - retries: MAX_TRANSACTION_HASH_RETRIEVAL_RETRIES, - interval: TRANSACTION_HASH_RETRIEVAL_WAIT, - finalErr: new JsonRpcError(RpcErrorCode.RPC_SERVER_ERROR, 'transaction hash not generated in time'), - }); - flow.addEvent('endRetrieveRelayerTransaction'); - - if (![ - RelayerTransactionStatus.SUBMITTED, - RelayerTransactionStatus.SUCCESSFUL, - ].includes(relayerTransaction.status)) { - let errorMessage = `Transaction failed to submit with status ${relayerTransaction.status}.`; - if (relayerTransaction.statusMessage) { - errorMessage += ` Error message: ${relayerTransaction.statusMessage}`; - } - throw new JsonRpcError( - RpcErrorCode.RPC_SERVER_ERROR, - errorMessage, - ); - } - return relayerTransaction.hash; + const { hash } = await pollRelayerTransaction(relayerClient, relayerId, flow); + return hash; }; diff --git a/packages/passport/sdk/src/zkEvm/transactionHelpers.test.ts b/packages/passport/sdk/src/zkEvm/transactionHelpers.test.ts new file mode 100644 index 0000000000..f152f4fb47 --- /dev/null +++ b/packages/passport/sdk/src/zkEvm/transactionHelpers.test.ts @@ -0,0 +1,290 @@ +import { StaticJsonRpcProvider } from '@ethersproject/providers'; +import { Signer } from '@ethersproject/abstract-signer'; +import { Flow } from '@imtbl/metrics'; +import { BigNumber } from 'ethers'; +import { RelayerClient } from './relayerClient'; +import GuardianClient from '../guardian'; +import { FeeOption, MetaTransaction, RelayerTransactionStatus } from './types'; +import { JsonRpcError, RpcErrorCode } from './JsonRpcError'; +import { pollRelayerTransaction, prepareAndSignTransaction } from './transactionHelpers'; +import * as walletHelpers from './walletHelpers'; +import { retryWithDelay } from '../network/retry'; + +jest.mock('./walletHelpers'); +jest.mock('../network/retry'); + +describe('transactionHelpers', () => { + const flow = { addEvent: jest.fn() } as unknown as Flow; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('pollRelayerTransaction', () => { + const relayerId = 'relayerId123'; + const transactionHash = 'transactionHash123'; + const relayerClient = { + imGetFeeOptions: jest.fn(), + ethSendTransaction: jest.fn(), + imGetTransactionByHash: jest.fn(), + } as unknown as RelayerClient; + + it('returns the transaction when successful', async () => { + const successfulTx = { status: RelayerTransactionStatus.SUCCESSFUL, hash: transactionHash }; + (retryWithDelay as jest.Mock).mockResolvedValue(successfulTx); + + const result = await pollRelayerTransaction(relayerClient, relayerId, flow); + + expect(result).toEqual(successfulTx); + expect(flow.addEvent).toHaveBeenCalledWith('endRetrieveRelayerTransaction'); + }); + + it('throws an error for failed transactions', async () => { + const failedTx = { status: RelayerTransactionStatus.FAILED, statusMessage: 'Transaction failed' }; + (retryWithDelay as jest.Mock).mockResolvedValue(failedTx); + + await expect(pollRelayerTransaction(relayerClient, relayerId, flow)) + .rejects.toThrow(new JsonRpcError( + RpcErrorCode.RPC_SERVER_ERROR, + 'Transaction failed to submit with status FAILED. Error message: Transaction failed', + )); + }); + + it('throws an error for cancelled transactions', async () => { + const cancelledTx = { status: RelayerTransactionStatus.CANCELLED, statusMessage: 'Transaction cancelled' }; + (retryWithDelay as jest.Mock).mockResolvedValue(cancelledTx); + + await expect(pollRelayerTransaction(relayerClient, relayerId, flow)) + .rejects.toThrow(new JsonRpcError( + RpcErrorCode.RPC_SERVER_ERROR, + 'Transaction failed to submit with status CANCELLED. Error message: Transaction cancelled', + )); + }); + }); + + describe('prepareAndSignTransaction', () => { + const chainId = 123; + const nonce = BigNumber.from(5); + const zkEvmAddress = '0x1234567890123456789012345678901234567890'; + const transactionRequest = { + to: '0x1234567890123456789012345678901234567890', + data: '0x456', + value: '0x00', + }; + + const metaTransactions: MetaTransaction[] = [ + { + to: transactionRequest.to, + data: transactionRequest.data, + nonce, + value: transactionRequest.value, + revertOnError: true, + }, + ]; + + const signedTransactions = 'signedTransactions123'; + const relayerId = 'relayerId123'; + + const imxFeeOption: FeeOption = { + tokenPrice: '0x1', + tokenSymbol: 'IMX', + tokenDecimals: 18, + tokenAddress: '0x1337', + recipientAddress: '0x7331', + }; + + const rpcProvider = { + detectNetwork: jest.fn().mockResolvedValue({ chainId }), + } as unknown as StaticJsonRpcProvider; + + const relayerClient = { + imGetFeeOptions: jest.fn().mockResolvedValue([imxFeeOption]), + ethSendTransaction: jest.fn().mockResolvedValue(relayerId), + } as unknown as RelayerClient; + + const guardianClient = { + validateEVMTransaction: jest.fn().mockResolvedValue(undefined), + } as unknown as GuardianClient; + + const ethSigner = {} as Signer; + + beforeEach(() => { + jest.resetAllMocks(); + (walletHelpers.getEip155ChainId as jest.Mock).mockReturnValue(`eip155:${chainId}`); + (walletHelpers.signMetaTransactions as jest.Mock).mockResolvedValue(signedTransactions); + (walletHelpers.getNonce as jest.Mock).mockResolvedValue(nonce); + (walletHelpers.getNormalisedTransactions as jest.Mock).mockReturnValue(metaTransactions); + (walletHelpers.encodedTransactions as jest.Mock).mockReturnValue('encodedTransactions123'); + (rpcProvider.detectNetwork as jest.Mock).mockResolvedValue({ chainId }); + (relayerClient.imGetFeeOptions as jest.Mock).mockResolvedValue([imxFeeOption]); + (relayerClient.ethSendTransaction as jest.Mock).mockResolvedValue(relayerId); + (guardianClient.validateEVMTransaction as jest.Mock).mockResolvedValue(undefined); + }); + + it('prepares and signs transaction correctly', async () => { + const result = await prepareAndSignTransaction({ + transactionRequest, + ethSigner, + rpcProvider, + guardianClient, + relayerClient, + zkEvmAddress, + flow, + }); + + expect(result).toEqual({ + signedTransactions, + relayerId, + nonce, + }); + + expect(rpcProvider.detectNetwork).toHaveBeenCalled(); + expect(guardianClient.validateEVMTransaction).toHaveBeenCalled(); + expect(walletHelpers.signMetaTransactions).toHaveBeenCalled(); + expect(relayerClient.ethSendTransaction).toHaveBeenCalledWith(zkEvmAddress, signedTransactions); + expect(flow.addEvent).toHaveBeenCalledWith('endDetectNetwork'); + expect(flow.addEvent).toHaveBeenCalledWith('endBuildMetaTransactions'); + expect(flow.addEvent).toHaveBeenCalledWith('endValidateEVMTransaction'); + expect(flow.addEvent).toHaveBeenCalledWith('endGetSignedMetaTransactions'); + expect(flow.addEvent).toHaveBeenCalledWith('endRelayerSendTransaction'); + }); + + it('handles sponsored transactions correctly', async () => { + const sponsoredFeeOption = { ...imxFeeOption, tokenPrice: '0' }; + (relayerClient.imGetFeeOptions as jest.Mock).mockResolvedValue([sponsoredFeeOption]); + + await prepareAndSignTransaction({ + transactionRequest, + ethSigner, + rpcProvider, + guardianClient, + relayerClient, + zkEvmAddress, + flow, + }); + + expect(guardianClient.validateEVMTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + metaTransactions: expect.arrayContaining([ + expect.objectContaining({ + data: transactionRequest.data, + revertOnError: true, + to: transactionRequest.to, + value: '0x00', + nonce: expect.any(BigNumber), + }), + ]), + }), + ); + }); + + it('handles non-sponsored transactions correctly', async () => { + const nonSponsoredFeeOption: FeeOption = { + tokenPrice: '0x1', // Non-zero value in hex + tokenSymbol: 'IMX', + tokenDecimals: 18, + tokenAddress: '0x1337', + recipientAddress: '0x7331', + }; + (relayerClient.imGetFeeOptions as jest.Mock).mockResolvedValue([nonSponsoredFeeOption]); + + await prepareAndSignTransaction({ + transactionRequest, + ethSigner, + rpcProvider, + guardianClient, + relayerClient, + zkEvmAddress, + flow, + }); + + expect(guardianClient.validateEVMTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + metaTransactions: expect.arrayContaining([ + expect.objectContaining({ + data: transactionRequest.data, + revertOnError: true, + to: transactionRequest.to, + value: '0x00', + nonce: expect.any(BigNumber), + }), + expect.objectContaining({ + to: '0x7331', + value: expect.any(BigNumber), + revertOnError: true, + nonce: expect.any(BigNumber), + }), + ]), + }), + ); + + expect(walletHelpers.signMetaTransactions).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + data: transactionRequest.data, + revertOnError: true, + to: transactionRequest.to, + value: '0x00', + nonce: expect.any(BigNumber), + }), + expect.objectContaining({ + to: '0x7331', + value: expect.any(BigNumber), + revertOnError: true, + nonce: expect.any(BigNumber), + }), + ]), + expect.any(BigNumber), + expect.any(BigNumber), + zkEvmAddress, + ethSigner, + ); + }); + + it('throws an error when validateEVMTransaction fails', async () => { + (guardianClient.validateEVMTransaction as jest.Mock).mockRejectedValue(new Error('Validation failed')); + + await expect(prepareAndSignTransaction({ + transactionRequest, + ethSigner, + rpcProvider, + guardianClient, + relayerClient, + zkEvmAddress, + flow, + })).rejects.toThrow('Validation failed'); + + expect(guardianClient.validateEVMTransaction).toHaveBeenCalled(); + expect(walletHelpers.signMetaTransactions).toHaveBeenCalled(); // This will be called due to parallelization + expect(relayerClient.ethSendTransaction).not.toHaveBeenCalled(); + }); + + it('throws an error when signMetaTransactions fails', async () => { + (walletHelpers.signMetaTransactions as jest.Mock).mockRejectedValue(new Error('Signing failed')); + + await expect(prepareAndSignTransaction({ + transactionRequest, + ethSigner, + rpcProvider, + guardianClient, + relayerClient, + zkEvmAddress, + flow, + })).rejects.toThrow('Signing failed'); + }); + + it('throws an error when ethSendTransaction fails', async () => { + (relayerClient.ethSendTransaction as jest.Mock).mockRejectedValue(new Error('Transaction send failed')); + + await expect(prepareAndSignTransaction({ + transactionRequest, + ethSigner, + rpcProvider, + guardianClient, + relayerClient, + zkEvmAddress, + flow, + })).rejects.toThrow('Transaction send failed'); + }); + }); +}); diff --git a/packages/passport/sdk/src/zkEvm/transactionHelpers.ts b/packages/passport/sdk/src/zkEvm/transactionHelpers.ts new file mode 100644 index 0000000000..21c0af21f5 --- /dev/null +++ b/packages/passport/sdk/src/zkEvm/transactionHelpers.ts @@ -0,0 +1,214 @@ +import { BigNumber } from 'ethers'; +import { + StaticJsonRpcProvider, + TransactionRequest, +} from '@ethersproject/providers'; +import { Flow } from '@imtbl/metrics'; +import { Signer } from '@ethersproject/abstract-signer'; +import { + getEip155ChainId, + signMetaTransactions, + encodedTransactions, + getNormalisedTransactions, + getNonce, +} from './walletHelpers'; +import { RelayerClient } from './relayerClient'; +import GuardianClient, { convertBigNumberishToString } from '../guardian'; +import { FeeOption, MetaTransaction, RelayerTransactionStatus } from './types'; +import { JsonRpcError, RpcErrorCode } from './JsonRpcError'; +import { retryWithDelay } from '../network/retry'; + +const MAX_TRANSACTION_HASH_RETRIEVAL_RETRIES = 30; +const TRANSACTION_HASH_RETRIEVAL_WAIT = 1000; + +export type TransactionParams = { + ethSigner: Signer; + rpcProvider: StaticJsonRpcProvider; + guardianClient: GuardianClient; + relayerClient: RelayerClient; + zkEvmAddress: string; + flow: Flow; +}; + +const getFeeOption = async ( + metaTransaction: MetaTransaction, + walletAddress: string, + relayerClient: RelayerClient, +): Promise => { + const normalisedMetaTransaction = getNormalisedTransactions([ + metaTransaction, + ]); + const transactions = encodedTransactions(normalisedMetaTransaction); + const feeOptions = await relayerClient.imGetFeeOptions( + walletAddress, + transactions, + ); + + const imxFeeOption = feeOptions.find( + (feeOption) => feeOption.tokenSymbol === 'IMX', + ); + if (!imxFeeOption) { + throw new Error('Failed to retrieve fees for IMX token'); + } + + return imxFeeOption; +}; + +/** + * Prepares the meta transactions array to be signed by estimating the fee and + * getting the nonce from the smart wallet. + * + */ +const buildMetaTransactions = async ( + transactionRequest: TransactionRequest, + rpcProvider: StaticJsonRpcProvider, + relayerClient: RelayerClient, + zkevmAddress: string, +): Promise<[MetaTransaction, ...MetaTransaction[]]> => { + if (!transactionRequest.to) { + throw new JsonRpcError( + RpcErrorCode.INVALID_PARAMS, + 'eth_sendTransaction requires a "to" field', + ); + } + + const metaTransaction: MetaTransaction = { + to: transactionRequest.to, + data: transactionRequest.data, + nonce: BigNumber.from(0), // NOTE: We don't need a valid nonce to estimate the fee + value: transactionRequest.value, + revertOnError: true, + }; + + // Estimate the fee and get the nonce from the smart wallet + const [nonce, feeOption] = await Promise.all([ + getNonce(rpcProvider, zkevmAddress), + getFeeOption(metaTransaction, zkevmAddress, relayerClient), + ]); + + // Build the meta transactions array with a valid nonce and fee transaction + const metaTransactions: [MetaTransaction, ...MetaTransaction[]] = [ + { + ...metaTransaction, + nonce, + }, + ]; + + // Add a fee transaction if the fee is non-zero + const feeValue = BigNumber.from(feeOption.tokenPrice); + if (!feeValue.isZero()) { + metaTransactions.push({ + nonce, + to: feeOption.recipientAddress, + value: feeValue, + revertOnError: true, + }); + } + + return metaTransactions; +}; + +export const pollRelayerTransaction = async ( + relayerClient: RelayerClient, + relayerId: string, + flow: Flow, +) => { + const retrieveRelayerTransaction = async () => { + const tx = await relayerClient.imGetTransactionByHash(relayerId); + // NOTE: The transaction hash is only available from the Relayer once the + // transaction is actually submitted onchain. Hence we need to poll the + // Relayer get transaction endpoint until the status transitions to one that + // has the hash available. + if (tx.status === RelayerTransactionStatus.PENDING) { + throw new Error(); + } + return tx; + }; + + const relayerTransaction = await retryWithDelay(retrieveRelayerTransaction, { + retries: MAX_TRANSACTION_HASH_RETRIEVAL_RETRIES, + interval: TRANSACTION_HASH_RETRIEVAL_WAIT, + finalErr: new JsonRpcError( + RpcErrorCode.RPC_SERVER_ERROR, + 'transaction hash not generated in time', + ), + }); + flow.addEvent('endRetrieveRelayerTransaction'); + + if ( + ![ + RelayerTransactionStatus.SUBMITTED, + RelayerTransactionStatus.SUCCESSFUL, + ].includes(relayerTransaction.status) + ) { + let errorMessage = `Transaction failed to submit with status ${relayerTransaction.status}.`; + if (relayerTransaction.statusMessage) { + errorMessage += ` Error message: ${relayerTransaction.statusMessage}`; + } + throw new JsonRpcError(RpcErrorCode.RPC_SERVER_ERROR, errorMessage); + } + + return relayerTransaction; +}; + +export const prepareAndSignTransaction = async ({ + transactionRequest, + ethSigner, + rpcProvider, + guardianClient, + relayerClient, + zkEvmAddress, + flow, +}: TransactionParams & { transactionRequest: TransactionRequest }) => { + const { chainId } = await rpcProvider.detectNetwork(); + const chainIdBigNumber = BigNumber.from(chainId); + flow.addEvent('endDetectNetwork'); + + const metaTransactions = await buildMetaTransactions( + transactionRequest, + rpcProvider, + relayerClient, + zkEvmAddress, + ); + flow.addEvent('endBuildMetaTransactions'); + + const { nonce } = metaTransactions[0]; + if (!nonce) { + throw new Error('Failed to retrieve nonce from the smart wallet'); + } + + // Parallelize the validation and signing of the transaction + // without waiting for the validation to complete + const validateTransaction = async () => { + await guardianClient.validateEVMTransaction({ + chainId: getEip155ChainId(chainId), + nonce: convertBigNumberishToString(nonce), + metaTransactions, + }); + flow.addEvent('endValidateEVMTransaction'); + }; + + // NOTE: We sign again because we now are adding the fee transaction, so the + // whole payload is different and needs a new signature. + const signTransaction = async () => { + const signed = await signMetaTransactions( + metaTransactions, + nonce, + chainIdBigNumber, + zkEvmAddress, + ethSigner, + ); + flow.addEvent('endGetSignedMetaTransactions'); + return signed; + }; + + const [, signedTransactions] = await Promise.all([ + validateTransaction(), + signTransaction(), + ]); + + const relayerId = await relayerClient.ethSendTransaction(zkEvmAddress, signedTransactions); + flow.addEvent('endRelayerSendTransaction'); + + return { signedTransactions, relayerId, nonce }; +}; diff --git a/packages/passport/sdk/src/zkEvm/zkEvmProvider.ts b/packages/passport/sdk/src/zkEvm/zkEvmProvider.ts index 6df8959df5..f4c9f760cf 100644 --- a/packages/passport/sdk/src/zkEvm/zkEvmProvider.ts +++ b/packages/passport/sdk/src/zkEvm/zkEvmProvider.ts @@ -29,6 +29,8 @@ import GuardianClient from '../guardian'; import { signTypedDataV4 } from './signTypedDataV4'; import { personalSign } from './personalSign'; import { trackSessionActivity } from './sessionActivity/sessionActivity'; +import { getNonce } from './walletHelpers'; +import { sendDeployTransactionAndPersonalSign } from './sendDeployTransactionAndPersonalSign'; export type ZkEvmProviderInput = { authManager: AuthManager; @@ -332,6 +334,24 @@ export class ZkEvmProvider implements Provider { const ethSigner = await this.#getSigner(); flow.addEvent('endGetSigner'); + if (this.#config.forceScwDeployBeforeMessageSignature) { + // Check if the smart contract wallet has been deployed + const nonce = await getNonce(this.#rpcProvider, zkEvmAddress); + if (!nonce.gt(0)) { + // If the smart contract wallet has not been deployed, + // submit a transaction before signing the message + return await sendDeployTransactionAndPersonalSign({ + params: request.params || [], + ethSigner, + zkEvmAddress, + rpcProvider: this.#rpcProvider, + guardianClient: this.#guardianClient, + relayerClient: this.#relayerClient, + flow, + }); + } + } + return await personalSign({ params: request.params || [], ethSigner,