diff --git a/packages/passport/sdk/src/zkEvm/sendTransaction.ts b/packages/passport/sdk/src/zkEvm/sendTransaction.ts index 73546e1bb7..2146f35795 100644 --- a/packages/passport/sdk/src/zkEvm/sendTransaction.ts +++ b/packages/passport/sdk/src/zkEvm/sendTransaction.ts @@ -12,6 +12,7 @@ export const sendTransaction = async ({ guardianClient, zkEvmAddress, flow, + nonceSpace, }: EthSendTransactionParams): Promise => { const transactionRequest = params[0]; @@ -23,6 +24,7 @@ export const sendTransaction = async ({ relayerClient, zkEvmAddress, flow, + nonceSpace, }); const { hash } = await pollRelayerTransaction(relayerClient, relayerId, flow); diff --git a/packages/passport/sdk/src/zkEvm/transactionHelpers.ts b/packages/passport/sdk/src/zkEvm/transactionHelpers.ts index 5cf3381a63..d8cc405fa1 100644 --- a/packages/passport/sdk/src/zkEvm/transactionHelpers.ts +++ b/packages/passport/sdk/src/zkEvm/transactionHelpers.ts @@ -28,6 +28,7 @@ export type TransactionParams = { relayerClient: RelayerClient; zkEvmAddress: string; flow: Flow; + nonceSpace?: BigNumber; }; export type EjectionTransactionParams = Pick; @@ -64,13 +65,13 @@ const getFeeOption = async ( /** * 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, + nonceSpace?: BigNumber, ): Promise<[MetaTransaction, ...MetaTransaction[]]> => { if (!transactionRequest.to) { throw new JsonRpcError( @@ -89,7 +90,7 @@ const buildMetaTransactions = async ( // Estimate the fee and get the nonce from the smart wallet const [nonce, feeOption] = await Promise.all([ - getNonce(rpcProvider, zkevmAddress), + getNonce(rpcProvider, zkevmAddress, nonceSpace), getFeeOption(metaTransaction, zkevmAddress, relayerClient), ]); @@ -166,6 +167,7 @@ export const prepareAndSignTransaction = async ({ relayerClient, zkEvmAddress, flow, + nonceSpace, }: TransactionParams & { transactionRequest: TransactionRequest }) => { const { chainId } = await rpcProvider.detectNetwork(); const chainIdBigNumber = BigNumber.from(chainId); @@ -176,6 +178,7 @@ export const prepareAndSignTransaction = async ({ rpcProvider, relayerClient, zkEvmAddress, + nonceSpace, ); flow.addEvent('endBuildMetaTransactions'); diff --git a/packages/passport/sdk/src/zkEvm/walletHelpers.test.ts b/packages/passport/sdk/src/zkEvm/walletHelpers.test.ts index 9aea97ae63..ecaf4560be 100644 --- a/packages/passport/sdk/src/zkEvm/walletHelpers.test.ts +++ b/packages/passport/sdk/src/zkEvm/walletHelpers.test.ts @@ -4,6 +4,8 @@ import { import { StaticJsonRpcProvider } from '@ethersproject/providers'; import { getNonce, signMetaTransactions, signAndPackTypedData, packSignatures, + coerceNonceSpace, + encodeNonce, } from './walletHelpers'; import { TypedDataPayload } from './types'; @@ -121,6 +123,34 @@ describe('signAndPackTypedData', () => { }); }); +describe('coerceNonceSpace', () => { + describe('with no space', () => { + it('should default to 0', () => { + expect(coerceNonceSpace()).toEqual(BigNumber.from(0)); + }); + }); + + describe('with space', () => { + it('should return the space', () => { + expect(coerceNonceSpace(BigNumber.from(12345))).toEqual(BigNumber.from(12345)); + }); + }); +}); + +describe('encodeNonce', () => { + describe('with no space', () => { + it('should not left shift the nonce', () => { + expect(encodeNonce(BigNumber.from(0), BigNumber.from(1))).toEqual(BigNumber.from(1)); + }); + }); + + describe('with space', () => { + it('should left shift the nonce by 96 bits', () => { + expect(encodeNonce(BigNumber.from(1), BigNumber.from(0))).toEqual(BigNumber.from('0x01000000000000000000000000')); + }); + }); +}); + describe('getNonce', () => { const rpcProvider = {} as StaticJsonRpcProvider; const nonceMock = jest.fn(); @@ -129,6 +159,7 @@ describe('getNonce', () => { jest.resetAllMocks(); (Contract as unknown as jest.Mock).mockImplementation(() => ({ nonce: nonceMock, + readNonce: nonceMock, })); }); diff --git a/packages/passport/sdk/src/zkEvm/walletHelpers.ts b/packages/passport/sdk/src/zkEvm/walletHelpers.ts index 41ea853c9e..564ccbd1bc 100644 --- a/packages/passport/sdk/src/zkEvm/walletHelpers.ts +++ b/packages/passport/sdk/src/zkEvm/walletHelpers.ts @@ -5,6 +5,7 @@ import { constants, utils, errors, + ethers, } from 'ethers'; import { walletContracts } from '@0xsequence/abi'; import { StaticJsonRpcProvider } from '@ethersproject/providers'; @@ -55,9 +56,36 @@ export const encodedTransactions = ( [normalisedTransactions], ); +/** + * This helper function is used to coerce the type to BigNumber for the + * getNonce function above. + * @param {BigNumber} nonceSpace - An unsigned 256 bit value that can be used to encode a nonce into a distinct space. + * @returns {BigNumber} The passed in nonceSpace or instead initialises the nonce to 0. + */ +export const coerceNonceSpace = (nonceSpace?: BigNumber): BigNumber => nonceSpace || BigNumber.from(0); + +/** + * This helper function is used to encode the nonce into a 256 bit value where the space is encoded into + * the first 160 bits, and the nonce the remaining 96 bits. + * @param {BigNumber} nonceSpace - An unsigned 256 bit value that can be used to encode a nonce into a distinct space. + * @param nonce {BigNumber} nonce - Sequential number starting at 0, and incrementing in single steps e.g. 0,1,2,... + * @returns {BigNumber} The encoded value where the space is left shifted 96 bits, and the nonce is in the first 96 bits. + */ +export const encodeNonce = (nonceSpace: BigNumber, nonce: BigNumber): BigNumber => { + const shiftedSpace = BigNumber.from(nonceSpace).mul(ethers.constants.Two.pow(96)); + return BigNumber.from(nonce).add(shiftedSpace); +}; + +/** + * When we retrieve a nonce for a smart contract wallet we can retrieve the nonce in a given 256 bit space. + * Nonces in each 256 bit space need to be sequential per wallet address. Nonces across 256 bit spaces per + * wallet address do not. This function overload can be used to invoke transactions in parallel per smart + * contract wallet if required. + */ export const getNonce = async ( rpcProvider: StaticJsonRpcProvider, smartContractWalletAddress: string, + nonceSpace?: BigNumber, ): Promise => { try { const contract = new Contract( @@ -65,9 +93,10 @@ export const getNonce = async ( walletContracts.mainModule.abi, rpcProvider, ); - const result = await contract.nonce(); + const space: BigNumber = coerceNonceSpace(nonceSpace); // Default nonce space is 0 + const result = await contract.readNonce(space); if (result instanceof BigNumber) { - return result; + return encodeNonce(space, result); } } catch (error) { if (error instanceof Error diff --git a/packages/passport/sdk/src/zkEvm/zkEvmProvider.ts b/packages/passport/sdk/src/zkEvm/zkEvmProvider.ts index 7cd2f4802f..eb1f4b4b79 100644 --- a/packages/passport/sdk/src/zkEvm/zkEvmProvider.ts +++ b/packages/passport/sdk/src/zkEvm/zkEvmProvider.ts @@ -1,7 +1,7 @@ import { StaticJsonRpcProvider, Web3Provider } from '@ethersproject/providers'; import { MultiRollupApiClients } from '@imtbl/generated-clients'; import { Signer } from '@ethersproject/abstract-signer'; -import { utils } from 'ethers'; +import { BigNumber, utils } from 'ethers'; import { Flow, identify, trackError, trackFlow, } from '@imtbl/metrics'; @@ -193,6 +193,11 @@ export class ZkEvmProvider implements Provider { } async #callSessionActivity(zkEvmAddress: string, clientId?: string) { + // SessionActivity requests are processed in nonce space 1, where as all + // other sendTransaction requests are processed in nonce space 0. This means + // we can submit a session activity request per SCW in parallel without a SCW + // INVALID_NONCE error. + const nonceSpace: BigNumber = BigNumber.from(1); const sendTransactionClosure = async (params: Array, flow: Flow) => { const ethSigner = await this.#getSigner(); return await sendTransaction({ @@ -203,6 +208,7 @@ export class ZkEvmProvider implements Provider { relayerClient: this.#relayerClient, zkEvmAddress, flow, + nonceSpace, }); }; this.#passportEventEmitter.emit(PassportEvents.ACCOUNTS_REQUESTED, {