From 7389cdc33e26bf14dcb18d94c28eaf2661e31414 Mon Sep 17 00:00:00 2001 From: plusminushalf Date: Tue, 21 Nov 2023 01:19:30 +0530 Subject: [PATCH 1/5] Enable multiple transactions support --- src/accounts/privateKeyToSafeSmartAccount.ts | 130 +++++++++---- .../privateKeyToSimpleSmartAccount.ts | 43 ++++- src/accounts/types.ts | 18 +- src/actions/smartAccount.ts | 9 +- src/actions/smartAccount/sendTransactions.ts | 174 ++++++++++++++++++ src/clients/decorators/smartAccount.ts | 74 ++++++++ src/types/index.ts | 5 + test/bundlerActions.test.ts | 4 +- test/pimlicoActions.test.ts | 5 +- test/safeSmartAccount.test.ts | 113 +++++++++++- test/simpleAccount.test.ts | 102 +++++++++- test/utils.ts | 6 + 12 files changed, 635 insertions(+), 48 deletions(-) create mode 100644 src/actions/smartAccount/sendTransactions.ts diff --git a/src/accounts/privateKeyToSafeSmartAccount.ts b/src/accounts/privateKeyToSafeSmartAccount.ts index 68ec86f7..c5802f7f 100644 --- a/src/accounts/privateKeyToSafeSmartAccount.ts +++ b/src/accounts/privateKeyToSafeSmartAccount.ts @@ -40,35 +40,23 @@ export const EIP712_SAFE_OPERATION_TYPE = { ] } -const SAFE_ADDRESSES_MAP: { +const SAFE_VERSION_TO_ADDRESSES_MAP: { [key in SafeVersion]: { - [chainId: string]: { - ADD_MODULES_LIB_ADDRESS: Address - SAFE_4337_MODULE_ADDRESS: Address - SAFE_PROXY_FACTORY_ADDRESS: Address - SAFE_SINGLETON_ADDRESS: Address - } + ADD_MODULES_LIB_ADDRESS: Address + SAFE_4337_MODULE_ADDRESS: Address + SAFE_PROXY_FACTORY_ADDRESS: Address + SAFE_SINGLETON_ADDRESS: Address + MULTI_SEND_CALL_ONLY_ADDRESS: Address } } = { "1.4.1": { - "11155111": { - ADD_MODULES_LIB_ADDRESS: - "0x191EFDC03615B575922289DC339F4c70aC5C30Af", - SAFE_4337_MODULE_ADDRESS: - "0x39E54Bb2b3Aa444b4B39DEe15De3b7809c36Fc38", - SAFE_PROXY_FACTORY_ADDRESS: - "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - SAFE_SINGLETON_ADDRESS: "0x41675C099F32341bf84BFc5382aF534df5C7461a" - }, - "5": { - ADD_MODULES_LIB_ADDRESS: - "0x191EFDC03615B575922289DC339F4c70aC5C30Af", - SAFE_4337_MODULE_ADDRESS: - "0x39E54Bb2b3Aa444b4B39DEe15De3b7809c36Fc38", - SAFE_PROXY_FACTORY_ADDRESS: - "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", - SAFE_SINGLETON_ADDRESS: "0x41675C099F32341bf84BFc5382aF534df5C7461a" - } + ADD_MODULES_LIB_ADDRESS: "0x191EFDC03615B575922289DC339F4c70aC5C30Af", + SAFE_4337_MODULE_ADDRESS: "0x39E54Bb2b3Aa444b4B39DEe15De3b7809c36Fc38", + SAFE_PROXY_FACTORY_ADDRESS: + "0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67", + SAFE_SINGLETON_ADDRESS: "0x41675C099F32341bf84BFc5382aF534df5C7461a", + MULTI_SEND_CALL_ONLY_ADDRESS: + "0x9641d764fc13c8B624c04430C7356C1C7C8102e2" } } @@ -371,36 +359,39 @@ export async function privateKeyToSafeSmartAccount< safe4337ModuleAddress: _safe4337ModuleAddress, safeProxyFactoryAddress: _safeProxyFactoryAddress, safeSingletonAddress: _safeSingletonAddress, + multiSendCallOnlyAddress: _multiSendCallOnlyAddress, saltNonce = 0n }: { - privateKey: Hex safeVersion: SafeVersion + privateKey: Hex entryPoint: Address addModuleLibAddress?: Address safe4337ModuleAddress?: Address safeProxyFactoryAddress?: Address safeSingletonAddress?: Address + multiSendCallOnlyAddress?: Address saltNonce?: bigint } ): Promise> { const privateKeyAccount = privateKeyToAccount(privateKey) const chainId = await getChainId(client) - const chainIdString: string = chainId.toString() - const addModuleLibAddress: Address = + const addModuleLibAddress = _addModuleLibAddress ?? - SAFE_ADDRESSES_MAP[safeVersion][chainIdString].ADD_MODULES_LIB_ADDRESS - const safe4337ModuleAddress: Address = + SAFE_VERSION_TO_ADDRESSES_MAP[safeVersion].ADD_MODULES_LIB_ADDRESS + const safe4337ModuleAddress = _safe4337ModuleAddress ?? - SAFE_ADDRESSES_MAP[safeVersion][chainIdString].SAFE_4337_MODULE_ADDRESS - const safeProxyFactoryAddress: Address = + SAFE_VERSION_TO_ADDRESSES_MAP[safeVersion].SAFE_4337_MODULE_ADDRESS + const safeProxyFactoryAddress = _safeProxyFactoryAddress ?? - SAFE_ADDRESSES_MAP[safeVersion][chainIdString] - .SAFE_PROXY_FACTORY_ADDRESS - const safeSingletonAddress: Address = + SAFE_VERSION_TO_ADDRESSES_MAP[safeVersion].SAFE_PROXY_FACTORY_ADDRESS + const safeSingletonAddress = _safeSingletonAddress ?? - SAFE_ADDRESSES_MAP[safeVersion][chainIdString].SAFE_SINGLETON_ADDRESS + SAFE_VERSION_TO_ADDRESSES_MAP[safeVersion].SAFE_SINGLETON_ADDRESS + const multiSendCallOnlyAddress = + _multiSendCallOnlyAddress ?? + SAFE_VERSION_TO_ADDRESSES_MAP[safeVersion].MULTI_SEND_CALL_ONLY_ADDRESS const accountAddress = await getAccountAddress({ client, @@ -544,7 +535,72 @@ export async function privateKeyToSafeSmartAccount< async encodeDeployCallData(_) { throw new Error("Safe account doesn't support account deployment") }, - async encodeCallData({ to, value, data }) { + async encodeCallData(args) { + let to: Address + let value: bigint + let data: Hex + + if (Array.isArray(args)) { + const argsArray = args as { + to: Address + value: bigint + data: Hex + }[] + + to = multiSendCallOnlyAddress + value = 0n + data = encodeFunctionData({ + abi: [ + { + inputs: [ + { + internalType: "bytes", + name: "transactions", + type: "bytes" + } + ], + name: "multiSend", + outputs: [], + stateMutability: "payable", + type: "function" + } + ], + functionName: "multiSend", + args: [ + `0x${argsArray + .map(({ to, value, data }) => { + const datBytes = toBytes(data) + return encodePacked( + [ + "uint8", + "address", + "uint256", + "uint256", + "bytes" + ], + [ + 0, + to, + value, + BigInt(datBytes.length), + data + ] + ).slice(2) + }) + .join("")}` + ] + }) + } else { + const singleTransaction = args as { + to: Address + value: bigint + data: Hex + } + to = singleTransaction.to + data = singleTransaction.data + value = singleTransaction.value + } + return encodeFunctionData({ abi: [ { diff --git a/src/accounts/privateKeyToSimpleSmartAccount.ts b/src/accounts/privateKeyToSimpleSmartAccount.ts index fd162215..cc983032 100644 --- a/src/accounts/privateKeyToSimpleSmartAccount.ts +++ b/src/accounts/privateKeyToSimpleSmartAccount.ts @@ -191,7 +191,48 @@ export async function privateKeyToSimpleSmartAccount< async encodeDeployCallData(_) { throw new Error("Simple account doesn't support account deployment") }, - async encodeCallData({ to, value, data }) { + async encodeCallData(args) { + if (Array.isArray(args)) { + const argsArray = args as { + to: Address + value: bigint + data: Hex + }[] + return encodeFunctionData({ + abi: [ + { + inputs: [ + { + internalType: "address[]", + name: "dest", + type: "address[]" + }, + { + internalType: "bytes[]", + name: "func", + type: "bytes[]" + } + ], + name: "executeBatch", + outputs: [], + stateMutability: "nonpayable", + type: "function" + } + ], + functionName: "executeBatch", + args: [ + argsArray.map((a) => a.to), + argsArray.map((a) => a.data) + ] + }) + } + + const { to, value, data } = args as { + to: Address + value: bigint + data: Hex + } + return encodeFunctionData({ abi: [ { diff --git a/src/accounts/types.ts b/src/accounts/types.ts index c7de6d15..6ebd9f6c 100644 --- a/src/accounts/types.ts +++ b/src/accounts/types.ts @@ -18,11 +18,19 @@ export type SmartAccount< entryPoint: Address getNonce: () => Promise getInitCode: () => Promise - encodeCallData: ({ - to, - value, - data - }: { to: Address; value: bigint; data: Hex }) => Promise + encodeCallData: ( + args: + | { + to: Address + value: bigint + data: Hex + } + | { + to: Address + value: bigint + data: Hex + }[] + ) => Promise getDummySignature(): Promise encodeDeployCallData: ({ abi, diff --git a/src/actions/smartAccount.ts b/src/actions/smartAccount.ts index e7c54020..77087b4c 100644 --- a/src/actions/smartAccount.ts +++ b/src/actions/smartAccount.ts @@ -25,6 +25,11 @@ import { signMessage } from "./smartAccount/signMessage.js" import { signTypedData } from "./smartAccount/signTypedData.js" +import { + type SendTransactionsWithPaymasterParameters, + sendTransactions +} from "./smartAccount/sendTransactions.js" + export { deployContract, type DeployContractParametersWithPaymaster, @@ -38,5 +43,7 @@ export { signMessage, signTypedData, type SendTransactionWithPaymasterParameters, - type SponsorUserOperationMiddleware + type SponsorUserOperationMiddleware, + sendTransactions, + type SendTransactionsWithPaymasterParameters } diff --git a/src/actions/smartAccount/sendTransactions.ts b/src/actions/smartAccount/sendTransactions.ts new file mode 100644 index 00000000..ccf2c790 --- /dev/null +++ b/src/actions/smartAccount/sendTransactions.ts @@ -0,0 +1,174 @@ +import type { + Chain, + Client, + FormattedTransactionRequest, + GetChain, + SendTransactionParameters, + SendTransactionReturnType, + Transport +} from "viem" +import { type SmartAccount } from "../../accounts/types.js" +import type { GetAccountParameter, UnionOmit } from "../../types/index.js" +import { getAction } from "../../utils/getAction.js" +import { + AccountOrClientNotFoundError, + parseAccount +} from "../../utils/index.js" +import { waitForUserOperationReceipt } from "../bundler/waitForUserOperationReceipt.js" +import { type SponsorUserOperationMiddleware } from "./prepareUserOperationRequest.js" +import { sendUserOperation } from "./sendUserOperation.js" + +export type SendTransactionsWithPaymasterParameters< + TChain extends Chain | undefined = Chain | undefined, + TAccount extends SmartAccount | undefined = SmartAccount | undefined, + TChainOverride extends Chain | undefined = Chain | undefined +> = { + transactions: (UnionOmit< + FormattedTransactionRequest< + TChainOverride extends Chain ? TChainOverride : TChain + >, + "from" + > & + GetChain)[] +} & GetAccountParameter & + SponsorUserOperationMiddleware + +/** + * Creates, signs, and sends a new transactions to the network. + * This function also allows you to sponsor this transaction if sender is a smartAccount + * + * @param client - Client to use + * @param parameters - {@link SendTransactionParameters} + * @returns The [Transaction](https://viem.sh/docs/glossary/terms.html#transaction) hash. {@link SendTransactionReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * import { sendTransaction } from 'viem/wallet' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const hash = await sendTransaction(client, [{ + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * value: 1000000000000000000n, + * }, { + * to: '0x61897970c51812dc3a010c7d01b50e0d17dc1234', + * value: 10000000000000000n, + * }]) + * + * @example + * // Account Hoisting + * import { createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { mainnet } from 'viem/chains' + * import { sendTransaction } from 'viem/wallet' + * + * const client = createWalletClient({ + * account: privateKeyToAccount('0x…'), + * chain: mainnet, + * transport: http(), + * }) + * const hash = await sendTransactions(client, [{ + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * value: 1000000000000000000n, + * }, { + * to: '0x61897970c51812dc3a010c7d01b50e0d17dc1234', + * value: 10000000000000000n, + * }]) + */ +export async function sendTransactions< + TChain extends Chain | undefined, + TAccount extends SmartAccount | undefined, + TChainOverride extends Chain | undefined +>( + client: Client, + args: SendTransactionsWithPaymasterParameters< + TChain, + TAccount, + TChainOverride + > +): Promise { + const { + account: account_ = client.account, + transactions, + sponsorUserOperation + } = args + + if (!account_) { + throw new AccountOrClientNotFoundError({ + docsPath: "/docs/actions/wallet/sendTransaction" + }) + } + + const account = parseAccount(account_) as SmartAccount + + if (account.type !== "local") { + throw new Error("RPC account type not supported") + } + + let maxFeePerGas: bigint | undefined + let maxPriorityFeePerGas: bigint | undefined + + const callData = await account.encodeCallData( + transactions.map( + ({ + to, + value, + data, + maxFeePerGas: txMaxFeePerGas, + maxPriorityFeePerGas: txMaxPriorityFeePerGas + }) => { + if (!to) throw new Error("Missing to address") + + if (txMaxFeePerGas) { + maxFeePerGas = maxFeePerGas + ? maxFeePerGas > txMaxFeePerGas + ? maxFeePerGas + : txMaxFeePerGas + : txMaxFeePerGas + } + + if (txMaxPriorityFeePerGas) { + maxPriorityFeePerGas = maxPriorityFeePerGas + ? maxPriorityFeePerGas > txMaxPriorityFeePerGas + ? maxPriorityFeePerGas + : txMaxPriorityFeePerGas + : txMaxPriorityFeePerGas + } + + return { + to, + value: value || 0n, + data: data || "0x" + } + } + ) + ) + + const userOpHash = await getAction( + client, + sendUserOperation + )({ + userOperation: { + sender: account.address, + paymasterAndData: "0x", + maxFeePerGas: maxFeePerGas || 0n, + maxPriorityFeePerGas: maxPriorityFeePerGas || 0n, + callData: callData + }, + account: account, + sponsorUserOperation + }) + + const userOperationReceipt = await getAction( + client, + waitForUserOperationReceipt + )({ + hash: userOpHash + }) + + return userOperationReceipt?.receipt.transactionHash +} diff --git a/src/clients/decorators/smartAccount.ts b/src/clients/decorators/smartAccount.ts index 0399a098..3e5cad13 100644 --- a/src/clients/decorators/smartAccount.ts +++ b/src/clients/decorators/smartAccount.ts @@ -3,12 +3,18 @@ import type { Chain, Client, DeployContractParameters, + FormattedTransactionRequest, + GetChain, SendTransactionParameters, Transport, TypedData, WriteContractParameters } from "viem" import type { SmartAccount } from "../../accounts/types.js" +import { + type SendTransactionsWithPaymasterParameters, + sendTransactions +} from "../../actions/smartAccount.js" import { type DeployContractParametersWithPaymaster, deployContract @@ -28,6 +34,7 @@ import { type WriteContractWithPaymasterParameters, writeContract } from "../../actions/smartAccount/writeContract.js" +import type { GetAccountParameter, UnionOmit } from "../../types/index.js" export type SmartAccountActions< TChain extends Chain | undefined = Chain | undefined, @@ -355,6 +362,68 @@ export type SmartAccountActions< > >[1] ) => Promise + /** + * Creates, signs, and sends a new transaction to the network. + * This function also allows you to sponsor this transaction if sender is a smartAccount + * + * - Docs: https://viem.sh/docs/actions/wallet/sendTransaction.html + * - Examples: https://stackblitz.com/github/wagmi-dev/viem/tree/main/examples/transactions/sending-transactions + * - JSON-RPC Methods: + * - JSON-RPC Accounts: [`eth_sendTransaction`](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendtransaction) + * - Local Accounts: [`eth_sendRawTransaction`](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendrawtransaction) + * + * @param args - {@link SendTransactionParameters} + * @returns The [Transaction](https://viem.sh/docs/glossary/terms.html#transaction) hash. {@link SendTransactionReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const hash = await client.sendTransaction([{ + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * value: 1000000000000000000n + * }, { + * to: '0x61897970c51812dc3a010c7d01b50e0d17dc1234', + * value: 10000000000000000n + * }) + * + * @example + * // Account Hoisting + * import { createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { mainnet } from 'viem/chains' + * + * const client = createWalletClient({ + * account: privateKeyToAccount('0x…'), + * chain: mainnet, + * transport: http(), + * }) + * const hash = await client.sendTransaction([{ + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * value: 1000000000000000000n + * }, { + * to: '0x61897970c51812dc3a010c7d01b50e0d17dc1234', + * value: 10000000000000000n + * }]) + */ + sendTransactions: ( + args: { + transactions: (UnionOmit< + FormattedTransactionRequest< + TChainOverride extends Chain ? TChainOverride : TChain + >, + "from" + > & + GetChain)[] + } & GetAccountParameter + ) => ReturnType< + typeof sendTransactions + > } export const smartAccountActions = @@ -380,6 +449,11 @@ export const smartAccountActions = ...args, sponsorUserOperation } as SendTransactionWithPaymasterParameters), + sendTransactions: (args) => + sendTransactions(client, { + ...args, + sponsorUserOperation + } as SendTransactionsWithPaymasterParameters), signMessage: (args) => signMessage(client, args), signTypedData: (args) => signTypedData(client, args), writeContract: (args) => diff --git a/src/types/index.ts b/src/types/index.ts index 1a558cbc..e8efa656 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -25,3 +25,8 @@ export type Prettify = { } & {} export type PartialBy = Omit & Partial> + +// biome-ignore lint/suspicious/noExplicitAny: generic type +export type UnionOmit = T extends any + ? Omit + : never diff --git a/test/bundlerActions.test.ts b/test/bundlerActions.test.ts index 586bd952..8eaeedc1 100644 --- a/test/bundlerActions.test.ts +++ b/test/bundlerActions.test.ts @@ -12,7 +12,8 @@ import { getEntryPoint, getEoaWalletClient, getPublicClient, - getTestingChain + getTestingChain, + waitForNonceUpdate } from "./utils.js" dotenv.config() @@ -133,6 +134,7 @@ describe("BUNDLER ACTIONS", () => { userOperation[key] ) } + await waitForNonceUpdate() }, 100000) test("wait for user operation receipt fail", async () => { diff --git a/test/pimlicoActions.test.ts b/test/pimlicoActions.test.ts index 0f5bafe9..afb7a777 100644 --- a/test/pimlicoActions.test.ts +++ b/test/pimlicoActions.test.ts @@ -16,7 +16,8 @@ import { getPimlicoBundlerClient, getPimlicoPaymasterClient, getPublicClient, - getTestingChain + getTestingChain, + waitForNonceUpdate } from "./utils.js" dotenv.config() @@ -149,6 +150,7 @@ describe("Pimlico Actions tests", () => { expect( sponsorUserOperationPaymasterAndData.paymasterAndData ).toStartWith("0x") + await waitForNonceUpdate() }, 100000) test("Sending user op with paymaster and data", async () => { @@ -234,6 +236,7 @@ describe("Pimlico Actions tests", () => { expect(userOperationStatus.transactionHash).toBe( userOperationReceipt?.receipt.transactionHash ) + await waitForNonceUpdate() }, 100000) }) }) diff --git a/test/safeSmartAccount.test.ts b/test/safeSmartAccount.test.ts index a7840a0c..549969a9 100644 --- a/test/safeSmartAccount.test.ts +++ b/test/safeSmartAccount.test.ts @@ -20,7 +20,8 @@ import { getPimlicoPaymasterClient, getPrivateKeyToSafeSmartAccount, getPublicClient, - getSmartAccountClient + getSmartAccountClient, + waitForNonceUpdate } from "./utils.js" dotenv.config() @@ -294,6 +295,32 @@ describe("Safe Account", () => { expect(newGreet).toBeString() expect(newGreet).toEqual("hello world") + await waitForNonceUpdate() + }, 1000000) + + test("Smart account client send multiple transactions", async () => { + const smartAccountClient = await getSmartAccountClient({ + account: await getPrivateKeyToSafeSmartAccount() + }) + const response = await smartAccountClient.sendTransactions({ + transactions: [ + { + to: zeroAddress, + value: 0n, + data: "0x" + }, + { + to: zeroAddress, + value: 0n, + data: "0x" + } + ] + }) + console.log(response) + expect(response).toBeString() + expect(response).toHaveLength(66) + expect(response).toMatch(/^0x[0-9a-fA-F]{64}$/) + await waitForNonceUpdate() }, 1000000) test("Smart account client send transaction", async () => { @@ -308,6 +335,11 @@ describe("Safe Account", () => { expect(response).toBeString() expect(response).toHaveLength(66) expect(response).toMatch(/^0x[0-9a-fA-F]{64}$/) + + await new Promise((res) => { + setTimeout(res, 1000) + }) + await waitForNonceUpdate() }, 1000000) test("smart account client send Transaction with paymaster", async () => { @@ -375,5 +407,84 @@ describe("Safe Account", () => { } expect(eventFound).toBeTrue() + await waitForNonceUpdate() + }, 1000000) + + test("smart account client send Transaction with paymaster", async () => { + const publicClient = await getPublicClient() + + const bundlerClient = getBundlerClient() + + const smartAccountClient = await getSmartAccountClient({ + account: await getPrivateKeyToSafeSmartAccount(), + sponsorUserOperation: async ({ + entryPoint: _entryPoint, + userOperation + }): Promise<{ + paymasterAndData: Hex + preVerificationGas: bigint + verificationGasLimit: bigint + callGasLimit: bigint + }> => { + const pimlicoPaymaster = getPimlicoPaymasterClient() + return pimlicoPaymaster.sponsorUserOperation({ + userOperation, + entryPoint: getEntryPoint() + }) + } + }) + + const response = await smartAccountClient.sendTransactions({ + transactions: [ + { + to: zeroAddress, + value: 0n, + data: "0x" + }, + { + to: zeroAddress, + value: 0n, + data: "0x" + } + ] + }) + console.log(response) + + expect(response).toBeString() + expect(response).toHaveLength(66) + expect(response).toMatch(/^0x[0-9a-fA-F]{64}$/) + + const transactionReceipt = await publicClient.waitForTransactionReceipt( + { + hash: response + } + ) + + let eventFound = false + + for (const log of transactionReceipt.logs) { + try { + const event = decodeEventLog({ + abi: EntryPointAbi, + ...log + }) + if (event.eventName === "UserOperationEvent") { + eventFound = true + const userOperation = + await bundlerClient.getUserOperationByHash({ + hash: event.args.userOpHash + }) + expect( + userOperation?.userOperation.paymasterAndData + ).not.toBe("0x") + } + } catch (e) { + const error = e as BaseError + if (error.name !== "AbiEventSignatureNotFoundError") throw e + } + } + + expect(eventFound).toBeTrue() + await waitForNonceUpdate() }, 1000000) }) diff --git a/test/simpleAccount.test.ts b/test/simpleAccount.test.ts index d6561502..3db9449e 100644 --- a/test/simpleAccount.test.ts +++ b/test/simpleAccount.test.ts @@ -20,7 +20,8 @@ import { getPrivateKeyToSimpleSmartAccount, getPublicClient, getSmartAccountClient, - getTestingChain + getTestingChain, + waitForNonceUpdate } from "./utils.js" dotenv.config() @@ -123,6 +124,29 @@ describe("Simple Account", () => { }).toThrow("Simple account doesn't support account deployment") }) + test("Smart account client send multiple transactions", async () => { + const smartAccountClient = await getSmartAccountClient() + const response = await smartAccountClient.sendTransactions({ + transactions: [ + { + to: zeroAddress, + value: 0n, + data: "0x" + }, + { + to: zeroAddress, + value: 0n, + data: "0x" + } + ] + }) + console.log(response) + expect(response).toBeString() + expect(response).toHaveLength(66) + expect(response).toMatch(/^0x[0-9a-fA-F]{64}$/) + await waitForNonceUpdate() + }, 1000000) + test("Smart account write contract", async () => { const greeterContract = getContract({ abi: GreeterAbi, @@ -144,6 +168,7 @@ describe("Simple Account", () => { expect(newGreet).toBeString() expect(newGreet).toEqual("hello world") + await waitForNonceUpdate() }, 1000000) test("Smart account client send transaction", async () => { @@ -156,6 +181,7 @@ describe("Simple Account", () => { expect(response).toBeString() expect(response).toHaveLength(66) expect(response).toMatch(/^0x[0-9a-fA-F]{64}$/) + await waitForNonceUpdate() }, 1000000) test("smart account client send Transaction with paymaster", async () => { @@ -217,5 +243,79 @@ describe("Simple Account", () => { } expect(eventFound).toBeTrue() + await waitForNonceUpdate() + }, 1000000) + + test("smart account client send multiple Transactions with paymaster", async () => { + const publicClient = await getPublicClient() + + const bundlerClient = getBundlerClient() + + const smartAccountClient = await getSmartAccountClient({ + sponsorUserOperation: async ({ + entryPoint: _entryPoint, + userOperation + }): Promise<{ + paymasterAndData: Hex + preVerificationGas: bigint + verificationGasLimit: bigint + callGasLimit: bigint + }> => { + const pimlicoPaymaster = getPimlicoPaymasterClient() + return pimlicoPaymaster.sponsorUserOperation({ + userOperation, + entryPoint: getEntryPoint() + }) + } + }) + + const response = await smartAccountClient.sendTransactions({ + transactions: [ + { + to: zeroAddress, + value: 0n, + data: "0x" + }, + { + to: zeroAddress, + value: 0n, + data: "0x" + } + ] + }) + + console.log(response) + + expect(response).toBeString() + expect(response).toHaveLength(66) + expect(response).toMatch(/^0x[0-9a-fA-F]{64}$/) + + const transactionReceipt = await publicClient.waitForTransactionReceipt( + { + hash: response + } + ) + + let eventFound = false + + for (const log of transactionReceipt.logs) { + const event = decodeEventLog({ + abi: EntryPointAbi, + ...log + }) + if (event.eventName === "UserOperationEvent") { + eventFound = true + const userOperation = + await bundlerClient.getUserOperationByHash({ + hash: event.args.userOpHash + }) + expect(userOperation?.userOperation.paymasterAndData).not.toBe( + "0x" + ) + } + } + + expect(eventFound).toBeTrue() + await waitForNonceUpdate() }, 1000000) }) diff --git a/test/utils.ts b/test/utils.ts index 8090d551..65864f2f 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -192,3 +192,9 @@ export const getDummySignature = (): Hex => { export const getOldUserOpHash = (): Hex => { return "0xe9fad2cd67f9ca1d0b7a6513b2a42066784c8df938518da2b51bb8cc9a89ea34" } + +export const waitForNonceUpdate = async () => { + return new Promise((res) => { + setTimeout(res, 1000) + }) +} From 0da0f7e454145768f6f45ff57276e6b81e831d11 Mon Sep 17 00:00:00 2001 From: plusminushalf Date: Tue, 21 Nov 2023 14:15:05 +0530 Subject: [PATCH 2/5] Fix call gas estimation --- src/actions/bundler/chainId.ts | 10 +++++++++- .../bundler/estimateUserOperationGas.ts | 11 ++++++++--- src/actions/bundler/getUserOperationByHash.ts | 11 ++++++++--- .../bundler/getUserOperationReceipt.ts | 19 ++++++++++++++++--- src/actions/bundler/sendUserOperation.ts | 11 ++++++++--- src/actions/bundler/supportedEntryPoints.ts | 11 ++++++++--- .../bundler/waitForUserOperationReceipt.ts | 19 +++++++++++++++---- .../prepareUserOperationRequest.ts | 6 +++++- test/utils.ts | 2 +- 9 files changed, 78 insertions(+), 22 deletions(-) diff --git a/src/actions/bundler/chainId.ts b/src/actions/bundler/chainId.ts index 370c6a70..37a2ea0c 100644 --- a/src/actions/bundler/chainId.ts +++ b/src/actions/bundler/chainId.ts @@ -1,4 +1,6 @@ +import type { Account, Chain, Client, Transport } from "viem" import type { BundlerClient } from "../../clients/createBundlerClient.js" +import type { BundlerRpcSchema } from "../../types/bundler.js" /** * Returns the supported chain id by the bundler service @@ -22,7 +24,13 @@ import type { BundlerClient } from "../../clients/createBundlerClient.js" * // Return 5n for Goerli * */ -export const chainId = async (client: BundlerClient) => { +export const chainId = async < + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TAccount extends Account | undefined = Account | undefined +>( + client: Client +) => { return Number( await client.request({ method: "eth_chainId", diff --git a/src/actions/bundler/estimateUserOperationGas.ts b/src/actions/bundler/estimateUserOperationGas.ts index 88944ac3..4d5dba61 100644 --- a/src/actions/bundler/estimateUserOperationGas.ts +++ b/src/actions/bundler/estimateUserOperationGas.ts @@ -1,6 +1,7 @@ -import type { Address } from "viem" +import type { Account, Address, Chain, Client, Transport } from "viem" import type { PartialBy } from "viem/types/utils" import type { BundlerClient } from "../../clients/createBundlerClient.js" +import type { BundlerRpcSchema } from "../../types/bundler.js" import type { UserOperation } from "../../types/userOperation.js" import type { UserOperationWithBigIntAsHex } from "../../types/userOperation.js" import { deepHexlify } from "../../utils/deepHexlify.js" @@ -46,8 +47,12 @@ export type EstimateUserOperationGasReturnType = { * // Return {preVerificationGas: 43492n, verificationGasLimit: 59436n, callGasLimit: 9000n} * */ -export const estimateUserOperationGas = async ( - client: BundlerClient, +export const estimateUserOperationGas = async < + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TAccount extends Account | undefined = Account | undefined +>( + client: Client, args: EstimateUserOperationGasParameters ): Promise => { const { userOperation, entryPoint } = args diff --git a/src/actions/bundler/getUserOperationByHash.ts b/src/actions/bundler/getUserOperationByHash.ts index e3898b8a..206037c1 100644 --- a/src/actions/bundler/getUserOperationByHash.ts +++ b/src/actions/bundler/getUserOperationByHash.ts @@ -1,5 +1,6 @@ -import type { Address, Hash } from "viem" +import type { Account, Address, Chain, Client, Hash, Transport } from "viem" import type { BundlerClient } from "../../clients/createBundlerClient.js" +import type { BundlerRpcSchema } from "../../types/bundler.js" import type { UserOperation } from "../../types/userOperation.js" export type GetUserOperationByHashParameters = { @@ -36,8 +37,12 @@ export type GetUserOperationByHashReturnType = { * getUserOperationByHash(bundlerClient, {hash: userOpHash}) * */ -export const getUserOperationByHash = async ( - client: BundlerClient, +export const getUserOperationByHash = async < + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TAccount extends Account | undefined = Account | undefined +>( + client: Client, { hash }: GetUserOperationByHashParameters ): Promise => { const params: [Hash] = [hash] diff --git a/src/actions/bundler/getUserOperationReceipt.ts b/src/actions/bundler/getUserOperationReceipt.ts index 8b43e503..3bc2db12 100644 --- a/src/actions/bundler/getUserOperationReceipt.ts +++ b/src/actions/bundler/getUserOperationReceipt.ts @@ -1,5 +1,14 @@ -import type { Address, Hash, Hex } from "viem" +import type { + Account, + Address, + Chain, + Client, + Hash, + Hex, + Transport +} from "viem" import type { BundlerClient } from "../../clients/createBundlerClient.js" +import type { BundlerRpcSchema } from "../../types/bundler.js" import type { TStatus } from "../../types/userOperation.js" import { transactionReceiptStatus } from "../../utils/deepHexlify.js" @@ -62,8 +71,12 @@ export type GetUserOperationReceiptReturnType = { * getUserOperationReceipt(bundlerClient, {hash: userOpHash}) * */ -export const getUserOperationReceipt = async ( - client: BundlerClient, +export const getUserOperationReceipt = async < + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TAccount extends Account | undefined = Account | undefined +>( + client: Client, { hash }: GetUserOperationReceiptParameters ): Promise => { const params: [Hash] = [hash] diff --git a/src/actions/bundler/sendUserOperation.ts b/src/actions/bundler/sendUserOperation.ts index e396d1d5..5cf6072d 100644 --- a/src/actions/bundler/sendUserOperation.ts +++ b/src/actions/bundler/sendUserOperation.ts @@ -1,5 +1,6 @@ -import type { Address, Hash } from "viem" +import type { Account, Address, Chain, Client, Hash, Transport } from "viem" import type { BundlerClient } from "../../clients/createBundlerClient.js" +import type { BundlerRpcSchema } from "../../types/bundler.js" import type { UserOperation, UserOperationWithBigIntAsHex @@ -36,8 +37,12 @@ export type SendUserOperationParameters = { * * // Return '0xe9fad2cd67f9ca1d0b7a6513b2a42066784c8df938518da2b51bb8cc9a89ea34' */ -export const sendUserOperation = async ( - client: BundlerClient, +export const sendUserOperation = async < + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TAccount extends Account | undefined = Account | undefined +>( + client: Client, args: SendUserOperationParameters ): Promise => { const { userOperation, entryPoint } = args diff --git a/src/actions/bundler/supportedEntryPoints.ts b/src/actions/bundler/supportedEntryPoints.ts index adc5f8d4..617ad002 100644 --- a/src/actions/bundler/supportedEntryPoints.ts +++ b/src/actions/bundler/supportedEntryPoints.ts @@ -1,5 +1,6 @@ -import type { Address } from "viem" +import type { Account, Address, Chain, Client, Transport } from "viem" import type { BundlerClient } from "../../clients/createBundlerClient.js" +import type { BundlerRpcSchema } from "../../types/bundler.js" /** * Returns the supported entrypoints by the bundler service @@ -23,8 +24,12 @@ import type { BundlerClient } from "../../clients/createBundlerClient.js" * // Return ['0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789'] * */ -export const supportedEntryPoints = async ( - client: BundlerClient +export const supportedEntryPoints = async < + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TAccount extends Account | undefined = Account | undefined +>( + client: Client ): Promise => { return client.request({ method: "eth_supportedEntryPoints", diff --git a/src/actions/bundler/waitForUserOperationReceipt.ts b/src/actions/bundler/waitForUserOperationReceipt.ts index 5e8bcd19..edaa2173 100644 --- a/src/actions/bundler/waitForUserOperationReceipt.ts +++ b/src/actions/bundler/waitForUserOperationReceipt.ts @@ -1,5 +1,12 @@ -import { BaseError, type Chain, type Hash, stringify } from "viem" -import type { BundlerClient } from "../../clients/createBundlerClient.js" +import { + type Account, + BaseError, + type Chain, + type Client, + type Hash, + type Transport, + stringify +} from "viem" import { getAction } from "../../utils/getAction.js" import { observe } from "../../utils/observe.js" import { @@ -49,8 +56,12 @@ export type WaitForUserOperationReceiptParameters = { * hash: '0x4ca7ee652d57678f26e887c149ab0735f41de37bcad58c9f6d3ed5824f15b74d', * }) */ -export const waitForUserOperationReceipt = ( - bundlerClient: BundlerClient, +export const waitForUserOperationReceipt = < + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TAccount extends Account | undefined = Account | undefined +>( + bundlerClient: Client, { hash, pollingInterval = bundlerClient.pollingInterval, diff --git a/src/actions/smartAccount/prepareUserOperationRequest.ts b/src/actions/smartAccount/prepareUserOperationRequest.ts index 3f49a0e1..f0d61c3e 100644 --- a/src/actions/smartAccount/prepareUserOperationRequest.ts +++ b/src/actions/smartAccount/prepareUserOperationRequest.ts @@ -121,7 +121,11 @@ export async function prepareUserOperationRequest< client, estimateUserOperationGas )({ - userOperation, + userOperation: { + ...userOperation, + maxFeePerGas: 1n, + maxPriorityFeePerGas: 1n + }, entryPoint: account.entryPoint }) diff --git a/test/utils.ts b/test/utils.ts index 65864f2f..c466c171 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -195,6 +195,6 @@ export const getOldUserOpHash = (): Hex => { export const waitForNonceUpdate = async () => { return new Promise((res) => { - setTimeout(res, 1000) + setTimeout(res, 10000) }) } From 06e524ad1a644627bef5d95dc466b684f4e4fe90 Mon Sep 17 00:00:00 2001 From: plusminushalf Date: Tue, 21 Nov 2023 14:18:16 +0530 Subject: [PATCH 3/5] Remove console logs --- test/safeSmartAccount.test.ts | 2 -- test/simpleAccount.test.ts | 3 --- 2 files changed, 5 deletions(-) diff --git a/test/safeSmartAccount.test.ts b/test/safeSmartAccount.test.ts index 549969a9..aeb2d07c 100644 --- a/test/safeSmartAccount.test.ts +++ b/test/safeSmartAccount.test.ts @@ -316,7 +316,6 @@ describe("Safe Account", () => { } ] }) - console.log(response) expect(response).toBeString() expect(response).toHaveLength(66) expect(response).toMatch(/^0x[0-9a-fA-F]{64}$/) @@ -448,7 +447,6 @@ describe("Safe Account", () => { } ] }) - console.log(response) expect(response).toBeString() expect(response).toHaveLength(66) diff --git a/test/simpleAccount.test.ts b/test/simpleAccount.test.ts index 3db9449e..28f0b197 100644 --- a/test/simpleAccount.test.ts +++ b/test/simpleAccount.test.ts @@ -140,7 +140,6 @@ describe("Simple Account", () => { } ] }) - console.log(response) expect(response).toBeString() expect(response).toHaveLength(66) expect(response).toMatch(/^0x[0-9a-fA-F]{64}$/) @@ -284,8 +283,6 @@ describe("Simple Account", () => { ] }) - console.log(response) - expect(response).toBeString() expect(response).toHaveLength(66) expect(response).toMatch(/^0x[0-9a-fA-F]{64}$/) From f6274eda5bab3a04bb06315f1e42dcb94d89121c Mon Sep 17 00:00:00 2001 From: plusminushalf Date: Wed, 22 Nov 2023 15:17:48 +0530 Subject: [PATCH 4/5] revert setting maxFeePerGas to 1n --- .../prepareUserOperationRequest.ts | 4 +--- test/safeSmartAccount.test.ts | 19 +++---------------- test/utils.ts | 16 ++++------------ 3 files changed, 8 insertions(+), 31 deletions(-) diff --git a/src/actions/smartAccount/prepareUserOperationRequest.ts b/src/actions/smartAccount/prepareUserOperationRequest.ts index f0d61c3e..7c1d3b4e 100644 --- a/src/actions/smartAccount/prepareUserOperationRequest.ts +++ b/src/actions/smartAccount/prepareUserOperationRequest.ts @@ -122,9 +122,7 @@ export async function prepareUserOperationRequest< estimateUserOperationGas )({ userOperation: { - ...userOperation, - maxFeePerGas: 1n, - maxPriorityFeePerGas: 1n + ...userOperation }, entryPoint: account.entryPoint }) diff --git a/test/safeSmartAccount.test.ts b/test/safeSmartAccount.test.ts index aeb2d07c..a50b7f1e 100644 --- a/test/safeSmartAccount.test.ts +++ b/test/safeSmartAccount.test.ts @@ -344,25 +344,11 @@ describe("Safe Account", () => { test("smart account client send Transaction with paymaster", async () => { const publicClient = await getPublicClient() - const bundlerClient = getBundlerClient() + const pimlicoPaymaster = getPimlicoPaymasterClient() const smartAccountClient = await getSmartAccountClient({ account: await getPrivateKeyToSafeSmartAccount(), - sponsorUserOperation: async ({ - entryPoint: _entryPoint, - userOperation - }): Promise<{ - paymasterAndData: Hex - preVerificationGas: bigint - verificationGasLimit: bigint - callGasLimit: bigint - }> => { - const pimlicoPaymaster = getPimlicoPaymasterClient() - return pimlicoPaymaster.sponsorUserOperation({ - userOperation, - entryPoint: getEntryPoint() - }) - } + sponsorUserOperation: pimlicoPaymaster.sponsorUserOperation }) const response = await smartAccountClient.sendTransaction({ @@ -383,6 +369,7 @@ describe("Safe Account", () => { let eventFound = false + const bundlerClient = getBundlerClient() for (const log of transactionReceipt.logs) { try { const event = decodeEventLog({ diff --git a/test/utils.ts b/test/utils.ts index c466c171..df18a1af 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -78,9 +78,7 @@ export const getSmartAccountClient = async ({ account: account ?? (await getPrivateKeyToSimpleSmartAccount()), chain, transport: http( - `${ - process.env.PIMLICO_BUNDLER_RPC_HOST - }/v1/${chain.name.toLowerCase()}/rpc?apikey=${pimlicoApiKey}` + `${process.env.PIMLICO_BUNDLER_RPC_HOST}?apikey=${pimlicoApiKey}` ), sponsorUserOperation }) @@ -128,9 +126,7 @@ export const getBundlerClient = () => { return createBundlerClient({ chain: chain, transport: http( - `${ - process.env.PIMLICO_BUNDLER_RPC_HOST - }/v1/${chain.name.toLowerCase()}/rpc?apikey=${pimlicoApiKey}` + `${process.env.PIMLICO_BUNDLER_RPC_HOST}?apikey=${pimlicoApiKey}` ) }) } @@ -147,9 +143,7 @@ export const getPimlicoBundlerClient = () => { return createPimlicoBundlerClient({ chain: chain, transport: http( - `${ - process.env.PIMLICO_BUNDLER_RPC_HOST - }/v1/${chain.name.toLowerCase()}/rpc?apikey=${pimlicoApiKey}` + `${process.env.PIMLICO_BUNDLER_RPC_HOST}/rpc?apikey=${pimlicoApiKey}` ) }) } @@ -166,9 +160,7 @@ export const getPimlicoPaymasterClient = () => { return createPimlicoPaymasterClient({ chain: chain, transport: http( - `${ - process.env.PIMLICO_BUNDLER_RPC_HOST - }/v2/${chain.name.toLowerCase()}/rpc?apikey=${pimlicoApiKey}` + `${process.env.PIMLICO_PAYMASTER_RPC_HOST}?apikey=${pimlicoApiKey}` ) }) } From 0254c2451d2ca18f8dd8ec7cee02ff68e1f4f706 Mon Sep 17 00:00:00 2001 From: Kristof Gazso Date: Wed, 22 Nov 2023 12:24:01 +0100 Subject: [PATCH 5/5] Create honest-taxis-smile.md --- .changeset/honest-taxis-smile.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/honest-taxis-smile.md diff --git a/.changeset/honest-taxis-smile.md b/.changeset/honest-taxis-smile.md new file mode 100644 index 00000000..ca63a241 --- /dev/null +++ b/.changeset/honest-taxis-smile.md @@ -0,0 +1,5 @@ +--- +"permissionless": patch +--- + +Enable batch calls for Safe account