diff --git a/.changeset/fluffy-dolls-mate.md b/.changeset/fluffy-dolls-mate.md new file mode 100644 index 00000000..4fcc9ff5 --- /dev/null +++ b/.changeset/fluffy-dolls-mate.md @@ -0,0 +1,5 @@ +--- +"permissionless": patch +--- + +Added support for SimpleAccount management diff --git a/biome.json b/biome.json index 0eba5238..9cc93617 100644 --- a/biome.json +++ b/biome.json @@ -32,8 +32,8 @@ "formatter": { "enabled": true, "formatWithErrors": true, - "lineWidth": 120, - "indentSize": 4, + "lineWidth": 80, + "indentWidth": 4, "indentStyle": "space" }, "javascript": { diff --git a/bun.lockb b/bun.lockb index 7bff1d13..8cbf439d 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 63db78cc..b7b54b5f 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "@changesets/cli": "^2.26.2", "@size-limit/esbuild-why": "^9.0.0", "@size-limit/preset-small-lib": "^9.0.0", - "bun-types": "^1.0.3", + "bun-types": "^1.0.7", "rimraf": "^5.0.1", "simple-git-hooks": "^2.9.0", "size-limit": "^9.0.0", diff --git a/src/accounts/index.ts b/src/accounts/index.ts new file mode 100644 index 00000000..dec18329 --- /dev/null +++ b/src/accounts/index.ts @@ -0,0 +1,14 @@ +import { + type PrivateKeySimpleSmartAccount, + SignTransactionNotSupportedBySmartAccount, + privateKeyToSimpleSmartAccount +} from "./privateKeyToSimpleSmartAccount.js" + +import { type SmartAccount } from "./types.js" + +export { + SignTransactionNotSupportedBySmartAccount, + type PrivateKeySimpleSmartAccount, + privateKeyToSimpleSmartAccount, + type SmartAccount +} diff --git a/src/accounts/privateKeyToSimpleSmartAccount.ts b/src/accounts/privateKeyToSimpleSmartAccount.ts new file mode 100644 index 00000000..758ad72e --- /dev/null +++ b/src/accounts/privateKeyToSimpleSmartAccount.ts @@ -0,0 +1,220 @@ +import { + type Address, + BaseError, + type Chain, + type Client, + type Hex, + type Transport, + concatHex, + encodeFunctionData +} from "viem" +import { privateKeyToAccount, toAccount } from "viem/accounts" +import { getBytecode, getChainId } from "viem/actions" +import { getAccountNonce } from "../actions/public/getAccountNonce.js" +import { getSenderAddress } from "../actions/public/getSenderAddress.js" +import { getUserOperationHash } from "../utils/getUserOperationHash.js" +import { type SmartAccount } from "./types.js" + +export class SignTransactionNotSupportedBySmartAccount extends BaseError { + override name = "SignTransactionNotSupportedBySmartAccount" + constructor({ docsPath }: { docsPath?: string } = {}) { + super( + [ + "A smart account cannot sign or send transaction, it can only sign message or userOperation.", + "Please send user operation instead." + ].join("\n"), + { + docsPath, + docsSlug: "account" + } + ) + } +} + +export type PrivateKeySimpleSmartAccount< + transport extends Transport = Transport, + chain extends Chain | undefined = Chain | undefined +> = SmartAccount<"privateKeySimpleSmartAccount", transport, chain> + +const getAccountInitCode = async ( + factoryAddress: Address, + owner: Address, + index = 0n +): Promise => { + if (!owner) throw new Error("Owner account not found") + + return concatHex([ + factoryAddress, + encodeFunctionData({ + abi: [ + { + inputs: [ + { + internalType: "address", + name: "owner", + type: "address" + }, + { + internalType: "uint256", + name: "salt", + type: "uint256" + } + ], + name: "createAccount", + outputs: [ + { + internalType: "contract SimpleAccount", + name: "ret", + type: "address" + } + ], + stateMutability: "nonpayable", + type: "function" + } + ], + functionName: "createAccount", + args: [owner, index] + }) as Hex + ]) +} + +const getAccountAddress = async < + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined +>({ + client, + factoryAddress, + entryPoint, + owner +}: { + client: Client + factoryAddress: Address + owner: Address + entryPoint: Address +}): Promise
=> { + const initCode = await getAccountInitCode(factoryAddress, owner) + + return getSenderAddress(client, { + initCode, + entryPoint + }) +} + +/** + * @description Creates an Simple Account from a private key. + * + * @returns A Private Key Simple Account. + */ +export async function privateKeyToSimpleSmartAccount< + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined +>( + client: Client, + { + privateKey, + factoryAddress, + entryPoint + }: { + privateKey: Hex + factoryAddress: Address + entryPoint: Address + } +): Promise> { + const privateKeyAccount = privateKeyToAccount(privateKey) + + const [accountAddress, chainId] = await Promise.all([ + getAccountAddress({ + client, + factoryAddress, + entryPoint, + owner: privateKeyAccount.address + }), + getChainId(client) + ]) + + if (!accountAddress) throw new Error("Account address not found") + + const account = toAccount({ + address: accountAddress, + async signMessage({ message }) { + return privateKeyAccount.signMessage({ message }) + }, + async signTransaction(_, __) { + throw new SignTransactionNotSupportedBySmartAccount() + }, + async signTypedData(typedData) { + return privateKeyAccount.signTypedData({ ...typedData, privateKey }) + } + }) + + return { + ...account, + client: client, + publicKey: accountAddress, + entryPoint: entryPoint, + source: "privateKeySimpleSmartAccount", + async getNonce() { + return getAccountNonce(client, { + sender: accountAddress, + entryPoint: entryPoint + }) + }, + async signUserOperation(userOperation) { + return account.signMessage({ + message: { + raw: getUserOperationHash({ + userOperation, + entryPoint: entryPoint, + chainId: chainId + }) + } + }) + }, + async getInitCode() { + const contractCode = await getBytecode(client, { + address: accountAddress + }) + + if ((contractCode?.length ?? 0) > 2) return "0x" + + return getAccountInitCode(factoryAddress, privateKeyAccount.address) + }, + async encodeDeployCallData(_) { + throw new Error("Simple account doesn't support account deployment") + }, + async encodeCallData({ to, value, data }) { + return encodeFunctionData({ + abi: [ + { + inputs: [ + { + internalType: "address", + name: "dest", + type: "address" + }, + { + internalType: "uint256", + name: "value", + type: "uint256" + }, + { + internalType: "bytes", + name: "func", + type: "bytes" + } + ], + name: "execute", + outputs: [], + stateMutability: "nonpayable", + type: "function" + } + ], + functionName: "execute", + args: [to, value, data] + }) + }, + async getDummySignature() { + return "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c" + } + } +} diff --git a/src/accounts/types.ts b/src/accounts/types.ts new file mode 100644 index 00000000..c7de6d15 --- /dev/null +++ b/src/accounts/types.ts @@ -0,0 +1,33 @@ +import type { + Abi, + Address, + Client, + GetConstructorArgs, + Hex, + LocalAccount +} from "viem" +import type { Chain, Transport } from "viem" +import { type UserOperation } from "../types/index.js" + +export type SmartAccount< + Name extends string = string, + transport extends Transport = Transport, + chain extends Chain | undefined = Chain | undefined +> = LocalAccount & { + client: Client + entryPoint: Address + getNonce: () => Promise + getInitCode: () => Promise + encodeCallData: ({ + to, + value, + data + }: { to: Address; value: bigint; data: Hex }) => Promise + getDummySignature(): Promise + encodeDeployCallData: ({ + abi, + args, + bytecode + }: { abi: TAbi; bytecode: Hex } & GetConstructorArgs) => Promise + signUserOperation: (UserOperation: UserOperation) => Promise +} diff --git a/src/actions/bundler/chainId.ts b/src/actions/bundler/chainId.ts index 3d0ab0e4..370c6a70 100644 --- a/src/actions/bundler/chainId.ts +++ b/src/actions/bundler/chainId.ts @@ -1,4 +1,4 @@ -import type { BundlerClient } from "../../clients/bundler.js" +import type { BundlerClient } from "../../clients/createBundlerClient.js" /** * Returns the supported chain id by the bundler service diff --git a/src/actions/bundler/estimateUserOperationGas.ts b/src/actions/bundler/estimateUserOperationGas.ts index da625303..88944ac3 100644 --- a/src/actions/bundler/estimateUserOperationGas.ts +++ b/src/actions/bundler/estimateUserOperationGas.ts @@ -1,12 +1,15 @@ import type { Address } from "viem" import type { PartialBy } from "viem/types/utils" -import type { BundlerClient } from "../../clients/bundler.js" +import type { BundlerClient } from "../../clients/createBundlerClient.js" import type { UserOperation } from "../../types/userOperation.js" import type { UserOperationWithBigIntAsHex } from "../../types/userOperation.js" import { deepHexlify } from "../../utils/deepHexlify.js" export type EstimateUserOperationGasParameters = { - userOperation: PartialBy + userOperation: PartialBy< + UserOperation, + "callGasLimit" | "preVerificationGas" | "verificationGasLimit" + > entryPoint: Address } @@ -51,7 +54,10 @@ export const estimateUserOperationGas = async ( const response = await client.request({ method: "eth_estimateUserOperationGas", - params: [deepHexlify(userOperation) as UserOperationWithBigIntAsHex, entryPoint as Address] + params: [ + deepHexlify(userOperation) as UserOperationWithBigIntAsHex, + entryPoint as Address + ] }) return { diff --git a/src/actions/bundler/getUserOperationByHash.ts b/src/actions/bundler/getUserOperationByHash.ts index 6279bc56..e3898b8a 100644 --- a/src/actions/bundler/getUserOperationByHash.ts +++ b/src/actions/bundler/getUserOperationByHash.ts @@ -1,5 +1,5 @@ import type { Address, Hash } from "viem" -import type { BundlerClient } from "../../clients/bundler.js" +import type { BundlerClient } from "../../clients/createBundlerClient.js" import type { UserOperation } from "../../types/userOperation.js" export type GetUserOperationByHashParameters = { @@ -49,7 +49,13 @@ export const getUserOperationByHash = async ( if (!response) return null - const { userOperation, entryPoint, transactionHash, blockHash, blockNumber } = response + const { + userOperation, + entryPoint, + transactionHash, + blockHash, + blockNumber + } = response return { userOperation: { diff --git a/src/actions/bundler/getUserOperationReceipt.ts b/src/actions/bundler/getUserOperationReceipt.ts index 1a3f128e..8b43e503 100644 --- a/src/actions/bundler/getUserOperationReceipt.ts +++ b/src/actions/bundler/getUserOperationReceipt.ts @@ -1,5 +1,5 @@ import type { Address, Hash, Hex } from "viem" -import type { BundlerClient } from "../../clients/bundler.js" +import type { BundlerClient } from "../../clients/createBundlerClient.js" import type { TStatus } from "../../types/userOperation.js" import { transactionReceiptStatus } from "../../utils/deepHexlify.js" diff --git a/src/actions/bundler/sendUserOperation.ts b/src/actions/bundler/sendUserOperation.ts index 8cd8feff..e396d1d5 100644 --- a/src/actions/bundler/sendUserOperation.ts +++ b/src/actions/bundler/sendUserOperation.ts @@ -1,6 +1,9 @@ import type { Address, Hash } from "viem" -import type { BundlerClient } from "../../clients/bundler.js" -import type { UserOperation, UserOperationWithBigIntAsHex } from "../../types/userOperation.js" +import type { BundlerClient } from "../../clients/createBundlerClient.js" +import type { + UserOperation, + UserOperationWithBigIntAsHex +} from "../../types/userOperation.js" import { deepHexlify } from "../../utils/deepHexlify.js" export type SendUserOperationParameters = { @@ -17,7 +20,6 @@ export type SendUserOperationParameters = { * @param args {@link SendUserOperationParameters}. * @returns UserOpHash that you can use to track user operation as {@link Hash}. * - * * @example * import { createClient } from "viem" * import { sendUserOperation } from "permissionless/actions" @@ -33,13 +35,18 @@ export type SendUserOperationParameters = { * }) * * // Return '0xe9fad2cd67f9ca1d0b7a6513b2a42066784c8df938518da2b51bb8cc9a89ea34' - * */ -export const sendUserOperation = async (client: BundlerClient, args: SendUserOperationParameters): Promise => { +export const sendUserOperation = async ( + client: BundlerClient, + args: SendUserOperationParameters +): Promise => { const { userOperation, entryPoint } = args return client.request({ method: "eth_sendUserOperation", - params: [deepHexlify(userOperation) as UserOperationWithBigIntAsHex, entryPoint as Address] + params: [ + deepHexlify(userOperation) as UserOperationWithBigIntAsHex, + entryPoint as Address + ] }) } diff --git a/src/actions/bundler/supportedEntryPoints.ts b/src/actions/bundler/supportedEntryPoints.ts index be44a76f..adc5f8d4 100644 --- a/src/actions/bundler/supportedEntryPoints.ts +++ b/src/actions/bundler/supportedEntryPoints.ts @@ -1,5 +1,5 @@ import type { Address } from "viem" -import type { BundlerClient } from "../../clients/bundler.js" +import type { BundlerClient } from "../../clients/createBundlerClient.js" /** * Returns the supported entrypoints by the bundler service @@ -23,7 +23,9 @@ import type { BundlerClient } from "../../clients/bundler.js" * // Return ['0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789'] * */ -export const supportedEntryPoints = async (client: BundlerClient): Promise => { +export const supportedEntryPoints = async ( + client: BundlerClient +): Promise => { return client.request({ method: "eth_supportedEntryPoints", params: [] diff --git a/src/actions/bundler/waitForUserOperationReceipt.ts b/src/actions/bundler/waitForUserOperationReceipt.ts index 3fddf546..5e8bcd19 100644 --- a/src/actions/bundler/waitForUserOperationReceipt.ts +++ b/src/actions/bundler/waitForUserOperationReceipt.ts @@ -1,12 +1,18 @@ import { BaseError, type Chain, type Hash, stringify } from "viem" -import type { BundlerClient } from "../../clients/bundler.js" +import type { BundlerClient } from "../../clients/createBundlerClient.js" +import { getAction } from "../../utils/getAction.js" import { observe } from "../../utils/observe.js" -import { type GetUserOperationReceiptReturnType, getUserOperationReceipt } from "./getUserOperationReceipt.js" +import { + type GetUserOperationReceiptReturnType, + getUserOperationReceipt +} from "./getUserOperationReceipt.js" export class WaitForUserOperationReceiptTimeoutError extends BaseError { override name = "WaitForUserOperationReceiptTimeoutError" constructor({ hash }: { hash: Hash }) { - super(`Timed out while waiting for transaction with hash "${hash}" to be confirmed.`) + super( + `Timed out while waiting for transaction with hash "${hash}" to be confirmed.` + ) } } @@ -45,36 +51,57 @@ export type WaitForUserOperationReceiptParameters = { */ export const waitForUserOperationReceipt = ( bundlerClient: BundlerClient, - { hash, pollingInterval = bundlerClient.pollingInterval, timeout }: WaitForUserOperationReceiptParameters + { + hash, + pollingInterval = bundlerClient.pollingInterval, + timeout + }: WaitForUserOperationReceiptParameters ): Promise => { - const observerId = stringify(["waitForUserOperationReceipt", bundlerClient.uid, hash]) + const observerId = stringify([ + "waitForUserOperationReceipt", + bundlerClient.uid, + hash + ]) let userOperationReceipt: GetUserOperationReceiptReturnType return new Promise((resolve, reject) => { if (timeout) { - setTimeout(() => reject(new WaitForUserOperationReceiptTimeoutError({ hash })), timeout) + setTimeout( + () => + reject( + new WaitForUserOperationReceiptTimeoutError({ hash }) + ), + timeout + ) } - const _unobserve = observe(observerId, { resolve, reject }, async (emit) => { - const _removeInterval = setInterval(async () => { - const done = (fn: () => void) => { - clearInterval(_removeInterval) - fn() - _unobserve() - } + const _unobserve = observe( + observerId, + { resolve, reject }, + async (emit) => { + const _removeInterval = setInterval(async () => { + const done = (fn: () => void) => { + clearInterval(_removeInterval) + fn() + _unobserve() + } - const _userOperationReceipt = await getUserOperationReceipt(bundlerClient, { hash }) + const _userOperationReceipt = await getAction( + bundlerClient, + getUserOperationReceipt + )({ hash }) - if (_userOperationReceipt !== null) { - userOperationReceipt = _userOperationReceipt - } + if (_userOperationReceipt !== null) { + userOperationReceipt = _userOperationReceipt + } - if (userOperationReceipt) { - done(() => emit.resolve(userOperationReceipt)) - return - } - }, pollingInterval) - }) + if (userOperationReceipt) { + done(() => emit.resolve(userOperationReceipt)) + return + } + }, pollingInterval) + } + ) }) } diff --git a/src/actions/index.ts b/src/actions/index.ts index 5d30baf8..a2f0737b 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -11,7 +11,10 @@ import type { import type { SendUserOperationParameters } from "./bundler/sendUserOperation.js" import type { GetSenderAddressParams } from "./public/getSenderAddress.js" -import { InvalidEntryPointError, getSenderAddress } from "./public/getSenderAddress.js" +import { + InvalidEntryPointError, + getSenderAddress +} from "./public/getSenderAddress.js" import { chainId } from "./bundler/chainId.js" import { estimateUserOperationGas } from "./bundler/estimateUserOperationGas.js" diff --git a/src/actions/pimlico.ts b/src/actions/pimlico.ts index e5d994dd..3ccaf459 100644 --- a/src/actions/pimlico.ts +++ b/src/actions/pimlico.ts @@ -13,8 +13,14 @@ import { sponsorUserOperation } from "./pimlico/sponsorUserOperation.js" -import type { PimlicoBundlerActions, PimlicoPaymasterClientActions } from "../clients/decorators/pimlico.js" -import { pimlicoBundlerActions, pimlicoPaymasterActions } from "../clients/decorators/pimlico.js" +import type { + PimlicoBundlerActions, + PimlicoPaymasterClientActions +} from "../clients/decorators/pimlico.js" +import { + pimlicoBundlerActions, + pimlicoPaymasterActions +} from "../clients/decorators/pimlico.js" export type { GetUserOperationGasPriceReturnType, diff --git a/src/actions/pimlico/getUserOperationGasPrice.ts b/src/actions/pimlico/getUserOperationGasPrice.ts index 07bc4b60..b12094c9 100644 --- a/src/actions/pimlico/getUserOperationGasPrice.ts +++ b/src/actions/pimlico/getUserOperationGasPrice.ts @@ -1,4 +1,5 @@ -import type { PimlicoBundlerClient } from "../../clients/pimlico.js" +import type { Account, Chain, Client, Transport } from "viem" +import type { PimlicoBundlerRpcSchema } from "../../types/pimlico.js" export type GetUserOperationGasPriceReturnType = { slow: { @@ -20,7 +21,7 @@ export type GetUserOperationGasPriceReturnType = { * * - Docs: https://docs.pimlico.io/permissionless/reference/pimlico-bundler-actions/getUserOperationGasPrice * - * @param client {@link PimlicoBundlerClient} that you created using viem's createClient whose transport url is pointing to the Pimlico's bundler. + * @param client that you created using viem's createClient whose transport url is pointing to the Pimlico's bundler. * @returns slow, standard & fast values for maxFeePerGas & maxPriorityFeePerGas * * @@ -36,8 +37,12 @@ export type GetUserOperationGasPriceReturnType = { * await getUserOperationGasPrice(bundlerClient) * */ -export const getUserOperationGasPrice = async ( - client: PimlicoBundlerClient +export const getUserOperationGasPrice = async < + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TAccount extends Account | undefined = Account | undefined +>( + client: Client ): Promise => { const gasPrices = await client.request({ method: "pimlico_getUserOperationGasPrice", @@ -51,7 +56,9 @@ export const getUserOperationGasPrice = async ( }, standard: { maxFeePerGas: BigInt(gasPrices.standard.maxFeePerGas), - maxPriorityFeePerGas: BigInt(gasPrices.standard.maxPriorityFeePerGas) + maxPriorityFeePerGas: BigInt( + gasPrices.standard.maxPriorityFeePerGas + ) }, fast: { maxFeePerGas: BigInt(gasPrices.fast.maxFeePerGas), diff --git a/src/actions/pimlico/getUserOperationStatus.ts b/src/actions/pimlico/getUserOperationStatus.ts index 457ffc83..e68a8d60 100644 --- a/src/actions/pimlico/getUserOperationStatus.ts +++ b/src/actions/pimlico/getUserOperationStatus.ts @@ -1,6 +1,9 @@ -import type { Hash } from "viem" +import type { Account, Chain, Client, Hash, Transport } from "viem" import type { PimlicoBundlerClient } from "../../clients/pimlico.js" -import type { PimlicoUserOperationStatus } from "../../types/pimlico.js" +import type { + PimlicoBundlerRpcSchema, + PimlicoUserOperationStatus +} from "../../types/pimlico.js" export type GetUserOperationStatusParameters = { hash: Hash @@ -30,8 +33,12 @@ export type GetUserOperationStatusReturnType = PimlicoUserOperationStatus * await getUserOperationStatus(bundlerClient, { hash: userOpHash }) * */ -export const getUserOperationStatus = async ( - client: PimlicoBundlerClient, +export const getUserOperationStatus = async < + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TAccount extends Account | undefined = Account | undefined +>( + client: Client, { hash }: GetUserOperationStatusParameters ): Promise => { return client.request({ diff --git a/src/actions/pimlico/sponsorUserOperation.ts b/src/actions/pimlico/sponsorUserOperation.ts index 045d8386..7e9fe42f 100644 --- a/src/actions/pimlico/sponsorUserOperation.ts +++ b/src/actions/pimlico/sponsorUserOperation.ts @@ -1,13 +1,19 @@ -import type { Address, Hex } from "viem" +import type { Account, Address, Chain, Client, Hex, Transport } from "viem" import type { PartialBy } from "viem/types/utils" -import type { PimlicoPaymasterClient } from "../../clients/pimlico.js" -import type { UserOperation, UserOperationWithBigIntAsHex } from "../../types/userOperation.js" +import type { PimlicoPaymasterRpcSchema } from "../../types/pimlico.js" +import type { + UserOperation, + UserOperationWithBigIntAsHex +} from "../../types/userOperation.js" import { deepHexlify } from "../../utils/deepHexlify.js" export type SponsorUserOperationParameters = { userOperation: PartialBy< UserOperation, - "callGasLimit" | "preVerificationGas" | "verificationGasLimit" | "paymasterAndData" + | "callGasLimit" + | "preVerificationGas" + | "verificationGasLimit" + | "paymasterAndData" > entryPoint: Address } @@ -44,13 +50,20 @@ export type SponsorUserOperationReturnType = { * }}) * */ -export const sponsorUserOperation = async ( - client: PimlicoPaymasterClient, +export const sponsorUserOperation = async < + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TAccount extends Account | undefined = Account | undefined +>( + client: Client, args: SponsorUserOperationParameters ): Promise => { const response = await client.request({ method: "pm_sponsorUserOperation", - params: [deepHexlify(args.userOperation) as UserOperationWithBigIntAsHex, args.entryPoint] + params: [ + deepHexlify(args.userOperation) as UserOperationWithBigIntAsHex, + args.entryPoint + ] }) return { diff --git a/src/actions/public/getAccountNonce.ts b/src/actions/public/getAccountNonce.ts index 2759a596..af50bb43 100644 --- a/src/actions/public/getAccountNonce.ts +++ b/src/actions/public/getAccountNonce.ts @@ -1,13 +1,19 @@ -import type { Address, PublicClient } from "viem" +import type { Address, Chain, Client, Transport } from "viem" +import { readContract } from "viem/actions" +import { getAction } from "../../utils/getAction.js" -export type GetAccountNonceParams = { sender: Address; entryPoint: Address; key?: bigint } +export type GetAccountNonceParams = { + sender: Address + entryPoint: Address + key?: bigint +} /** * Returns the nonce of the account with the entry point. * * - Docs: https://docs.pimlico.io/permissionless/reference/public-actions/getAccountNonce * - * @param publicClient {@link PublicClient} that you created using viem's createPublicClient. + * @param client {@link client} that you created using viem's createPublicClient. * @param args {@link GetAccountNonceParams} address, entryPoint & key * @returns bigint nonce * @@ -15,12 +21,12 @@ export type GetAccountNonceParams = { sender: Address; entryPoint: Address; key? * import { createPublicClient } from "viem" * import { getAccountNonce } from "permissionless/actions" * - * const publicClient = createPublicClient({ + * const client = createPublicClient({ * chain: goerli, * transport: http("https://goerli.infura.io/v3/your-infura-key") * }) * - * const nonce = await getAccountNonce(publicClient, { + * const nonce = await getAccountNonce(client, { * address, * entryPoint, * key @@ -28,11 +34,17 @@ export type GetAccountNonceParams = { sender: Address; entryPoint: Address; key? * * // Return 0n */ -export const getAccountNonce = async ( - publicClient: PublicClient, +export const getAccountNonce = async < + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined +>( + client: Client, { sender, entryPoint, key = BigInt(0) }: GetAccountNonceParams ): Promise => { - return await publicClient.readContract({ + return await getAction( + client, + readContract + )({ address: entryPoint, abi: [ { diff --git a/src/actions/public/getSenderAddress.ts b/src/actions/public/getSenderAddress.ts index 7c6af6a9..42f639bf 100644 --- a/src/actions/public/getSenderAddress.ts +++ b/src/actions/public/getSenderAddress.ts @@ -1,18 +1,26 @@ import { type Address, BaseError, + type Chain, + type Client, type ContractFunctionExecutionErrorType, type ContractFunctionRevertedErrorType, type Hex, - type PublicClient + type Transport } from "viem" +import { simulateContract } from "viem/actions" +import { getAction } from "../../utils/getAction.js" + export type GetSenderAddressParams = { initCode: Hex; entryPoint: Address } export class InvalidEntryPointError extends BaseError { override name = "InvalidEntryPointError" - constructor({ cause, entryPoint }: { cause?: BaseError; entryPoint?: Address } = {}) { + constructor({ + cause, + entryPoint + }: { cause?: BaseError; entryPoint?: Address } = {}) { super( `The entry point address (\`entryPoint\`${ entryPoint ? ` = ${entryPoint}` : "" @@ -29,8 +37,7 @@ export class InvalidEntryPointError extends BaseError { * * - Docs: https://docs.pimlico.io/permissionless/reference/public-actions/getSenderAddress * - * - * @param publicClient {@link PublicClient} that you created using viem's createPublicClient. + * @param client {@link Client} that you created using viem's createPublicClient. * @param args {@link GetSenderAddressParams} initCode & entryPoint * @returns Sender's Address * @@ -50,12 +57,18 @@ export class InvalidEntryPointError extends BaseError { * * // Return '0x7a88a206ba40b37a8c07a2b5688cf8b287318b63' */ -export const getSenderAddress = async ( - publicClient: PublicClient, +export const getSenderAddress = async < + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined +>( + client: Client, { initCode, entryPoint }: GetSenderAddressParams ): Promise
=> { try { - await publicClient.simulateContract({ + await getAction( + client, + simulateContract + )({ address: entryPoint, abi: [ { @@ -92,7 +105,11 @@ export const getSenderAddress = async ( if (err.cause.name === "ContractFunctionRevertedError") { const revertError = err.cause as ContractFunctionRevertedErrorType const errorName = revertError.data?.errorName ?? "" - if (errorName === "SenderAddressResult" && revertError.data?.args && revertError.data?.args[0]) { + if ( + errorName === "SenderAddressResult" && + revertError.data?.args && + revertError.data?.args[0] + ) { return revertError.data?.args[0] as Address } } diff --git a/src/actions/smartAccount.ts b/src/actions/smartAccount.ts new file mode 100644 index 00000000..e7c54020 --- /dev/null +++ b/src/actions/smartAccount.ts @@ -0,0 +1,42 @@ +import { + type DeployContractParametersWithPaymaster, + deployContract +} from "./smartAccount/deployContract.js" + +import { + type PrepareUserOperationRequestParameters, + type PrepareUserOperationRequestReturnType, + type SponsorUserOperationMiddleware, + prepareUserOperationRequest +} from "./smartAccount/prepareUserOperationRequest.js" + +import { + type SendTransactionWithPaymasterParameters, + sendTransaction +} from "./smartAccount/sendTransaction.js" + +import { + type SendUserOperationParameters, + type SendUserOperationReturnType, + sendUserOperation +} from "./smartAccount/sendUserOperation.js" + +import { signMessage } from "./smartAccount/signMessage.js" + +import { signTypedData } from "./smartAccount/signTypedData.js" + +export { + deployContract, + type DeployContractParametersWithPaymaster, + prepareUserOperationRequest, + type PrepareUserOperationRequestParameters, + type PrepareUserOperationRequestReturnType, + sendTransaction, + sendUserOperation, + type SendUserOperationParameters, + type SendUserOperationReturnType, + signMessage, + signTypedData, + type SendTransactionWithPaymasterParameters, + type SponsorUserOperationMiddleware +} diff --git a/src/actions/smartAccount/deployContract.ts b/src/actions/smartAccount/deployContract.ts new file mode 100644 index 00000000..48bab17f --- /dev/null +++ b/src/actions/smartAccount/deployContract.ts @@ -0,0 +1,110 @@ +import { + type Abi, + type Chain, + type Client, + type DeployContractParameters, + type DeployContractReturnType, + type Transport +} from "viem" +import type { SmartAccount } from "../../accounts/types.js" +import { getAction } from "../../utils/getAction.js" +import { parseAccount } from "../../utils/index.js" +import { AccountOrClientNotFoundError } from "../../utils/signUserOperationHashWithECDSA.js" +import { waitForUserOperationReceipt } from "../bundler/waitForUserOperationReceipt.js" +import { type SponsorUserOperationMiddleware } from "./prepareUserOperationRequest.js" +import { sendUserOperation } from "./sendUserOperation.js" + +export type DeployContractParametersWithPaymaster< + TAbi extends Abi | readonly unknown[] = Abi | readonly unknown[], + TChain extends Chain | undefined = Chain | undefined, + TAccount extends SmartAccount | undefined = SmartAccount | undefined, + TChainOverride extends Chain | undefined = Chain | undefined +> = DeployContractParameters & + SponsorUserOperationMiddleware + +/** + * Deploys a contract to the network, given bytecode and constructor arguments. + * This function also allows you to sponsor this transaction if sender is a smartAccount + * + * - Docs: https://viem.sh/docs/contract/deployContract.html + * - Examples: https://stackblitz.com/github/wagmi-dev/viem/tree/main/examples/contracts/deploying-contracts + * + * @param client - Client to use + * @param parameters - {@link DeployContractParameters} + * @returns The [Transaction](https://viem.sh/docs/glossary/terms.html#transaction) hash. {@link DeployContractReturnType} + * + * @example + * import { createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { mainnet } from 'viem/chains' + * import { deployContract } from 'viem/contract' + * + * const client = createWalletClient({ + * account: privateKeyToAccount('0x…'), + * chain: mainnet, + * transport: http(), + * }) + * const hash = await deployContract(client, { + * abi: [], + * account: '0x…, + * bytecode: '0x608060405260405161083e38038061083e833981016040819052610...', + * }) + */ +export async function deployContract< + const TAbi extends Abi | readonly unknown[], + TChain extends Chain | undefined, + TAccount extends SmartAccount | undefined, + TChainOverride extends Chain | undefined +>( + client: Client, + { + abi, + args, + bytecode, + sponsorUserOperation, + ...request + }: DeployContractParametersWithPaymaster +): Promise { + const { account: account_ = client.account } = request + + if (!account_) { + throw new AccountOrClientNotFoundError({ + docsPath: "/docs/actions/wallet/sendTransaction" + }) + } + + const account = parseAccount(account_) as SmartAccount + + const userOpHash = await getAction( + client, + sendUserOperation + )({ + userOperation: { + sender: account.address, + paymasterAndData: "0x", + maxFeePerGas: request.maxFeePerGas || 0n, + maxPriorityFeePerGas: request.maxPriorityFeePerGas || 0n, + callData: await account.encodeDeployCallData({ + abi, + args, + bytecode + } as unknown as DeployContractParameters< + TAbi, + TChain, + TAccount, + TChainOverride + >) + }, + account: account, + sponsorUserOperation + }) + + const userOperationReceipt = await getAction( + client, + waitForUserOperationReceipt + )({ + hash: userOpHash + }) + + return userOperationReceipt?.receipt.transactionHash +} diff --git a/src/actions/smartAccount/prepareUserOperationRequest.ts b/src/actions/smartAccount/prepareUserOperationRequest.ts new file mode 100644 index 00000000..3f49a0e1 --- /dev/null +++ b/src/actions/smartAccount/prepareUserOperationRequest.ts @@ -0,0 +1,138 @@ +import type { Address, Chain, Client, Hex, Transport } from "viem" +import { estimateFeesPerGas } from "viem/actions" +import type { SmartAccount } from "../../accounts/types.js" +import type { + GetAccountParameter, + PartialBy, + UserOperation +} from "../../types/index.js" +import { getAction } from "../../utils/getAction.js" +import { + AccountOrClientNotFoundError, + parseAccount +} from "../../utils/index.js" +import { estimateUserOperationGas } from "../bundler/estimateUserOperationGas.js" + +export type SponsorUserOperationMiddleware = { + sponsorUserOperation?: (args: { + userOperation: UserOperation + entryPoint: Address + }) => Promise<{ + paymasterAndData: Hex + preVerificationGas: bigint + verificationGasLimit: bigint + callGasLimit: bigint + }> +} + +export type PrepareUserOperationRequestParameters< + TAccount extends SmartAccount | undefined = SmartAccount | undefined, +> = { + userOperation: PartialBy< + UserOperation, + | "nonce" + | "sender" + | "initCode" + | "callGasLimit" + | "verificationGasLimit" + | "preVerificationGas" + | "maxFeePerGas" + | "maxPriorityFeePerGas" + | "paymasterAndData" + | "signature" + > +} & GetAccountParameter & + SponsorUserOperationMiddleware + +export type PrepareUserOperationRequestReturnType = UserOperation + +export async function prepareUserOperationRequest< + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TAccount extends SmartAccount | undefined = SmartAccount | undefined +>( + client: Client, + args: PrepareUserOperationRequestParameters +): Promise { + const { + account: account_ = client.account, + userOperation: partialUserOperation, + sponsorUserOperation + } = args + if (!account_) throw new AccountOrClientNotFoundError() + + const account = parseAccount(account_) as SmartAccount + + const [sender, nonce, initCode, signature, callData, gasEstimation] = + await Promise.all([ + partialUserOperation.sender || account.address, + partialUserOperation.nonce || account.getNonce(), + partialUserOperation.initCode || account.getInitCode(), + partialUserOperation.signature || account.getDummySignature(), + partialUserOperation.callData, + !partialUserOperation.maxFeePerGas || + !partialUserOperation.maxPriorityFeePerGas + ? estimateFeesPerGas(account.client) + : undefined + ]) + + const userOperation: UserOperation = { + sender, + nonce, + initCode, + signature, + callData, + paymasterAndData: "0x", + maxFeePerGas: + partialUserOperation.maxFeePerGas || + gasEstimation?.maxFeePerGas || + 0n, + maxPriorityFeePerGas: + partialUserOperation.maxPriorityFeePerGas || + gasEstimation?.maxPriorityFeePerGas || + 0n, + callGasLimit: partialUserOperation.callGasLimit || 0n, + verificationGasLimit: partialUserOperation.verificationGasLimit || 0n, + preVerificationGas: partialUserOperation.preVerificationGas || 0n + } + + if (sponsorUserOperation) { + const { + callGasLimit, + verificationGasLimit, + preVerificationGas, + paymasterAndData + } = await sponsorUserOperation({ + userOperation, + entryPoint: account.entryPoint + }) + userOperation.paymasterAndData = paymasterAndData + userOperation.callGasLimit = userOperation.callGasLimit || callGasLimit + userOperation.verificationGasLimit = + userOperation.verificationGasLimit || verificationGasLimit + userOperation.preVerificationGas = + userOperation.preVerificationGas || preVerificationGas + } else if ( + !userOperation.callGasLimit || + !userOperation.verificationGasLimit || + !userOperation.preVerificationGas + ) { + const gasParameters = await getAction( + client, + estimateUserOperationGas + )({ + userOperation, + entryPoint: account.entryPoint + }) + + userOperation.callGasLimit = + userOperation.callGasLimit || gasParameters.callGasLimit + userOperation.verificationGasLimit = + userOperation.verificationGasLimit || + gasParameters.verificationGasLimit + userOperation.preVerificationGas = + userOperation.preVerificationGas || gasParameters.preVerificationGas + } + + return userOperation +} diff --git a/src/actions/smartAccount/sendTransaction.ts b/src/actions/smartAccount/sendTransaction.ts new file mode 100644 index 00000000..abfc22d1 --- /dev/null +++ b/src/actions/smartAccount/sendTransaction.ts @@ -0,0 +1,136 @@ +import type { + Chain, + Client, + SendTransactionParameters, + SendTransactionReturnType, + Transport +} from "viem" +import { type SmartAccount } from "../../accounts/types.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 SendTransactionWithPaymasterParameters< + TChain extends Chain | undefined = Chain | undefined, + TAccount extends SmartAccount | undefined = SmartAccount | undefined, + TChainOverride extends Chain | undefined = Chain | undefined +> = SendTransactionParameters & + SponsorUserOperationMiddleware + +/** + * 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 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, + * }) + * + * @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 sendTransaction(client, { + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + * value: 1000000000000000000n, + * }) + */ +export async function sendTransaction< + TChain extends Chain | undefined, + TAccount extends SmartAccount | undefined, + TChainOverride extends Chain | undefined +>( + client: Client, + args: SendTransactionWithPaymasterParameters< + TChain, + TAccount, + TChainOverride + > +): Promise { + const { + account: account_ = client.account, + data, + maxFeePerGas, + maxPriorityFeePerGas, + to, + value, + sponsorUserOperation + } = args + + if (!account_) { + throw new AccountOrClientNotFoundError({ + docsPath: "/docs/actions/wallet/sendTransaction" + }) + } + + const account = parseAccount(account_) as SmartAccount + + if (!to) throw new Error("Missing to address") + + if (account.type !== "local") { + throw new Error("RPC account type not supported") + } + + const callData = await account.encodeCallData({ + 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/actions/smartAccount/sendUserOperation.ts b/src/actions/smartAccount/sendUserOperation.ts new file mode 100644 index 00000000..b9c5d05f --- /dev/null +++ b/src/actions/smartAccount/sendUserOperation.ts @@ -0,0 +1,70 @@ +import type { Chain, Client, Hex, Transport } from "viem" +import type { SmartAccount } from "../../accounts/types.js" +import type { + GetAccountParameter, + PartialBy, + UserOperation +} from "../../types/index.js" +import { getAction } from "../../utils/getAction.js" +import { + AccountOrClientNotFoundError, + parseAccount +} from "../../utils/index.js" +import { sendUserOperation as sendUserOperationBundler } from "../bundler/sendUserOperation.js" +import { + type SponsorUserOperationMiddleware, + prepareUserOperationRequest +} from "./prepareUserOperationRequest.js" + +export type SendUserOperationParameters< + TAccount extends SmartAccount | undefined = SmartAccount | undefined +> = { + userOperation: PartialBy< + UserOperation, + | "nonce" + | "sender" + | "initCode" + | "signature" + | "callGasLimit" + | "maxFeePerGas" + | "maxPriorityFeePerGas" + | "preVerificationGas" + | "verificationGasLimit" + | "paymasterAndData" + > +} & GetAccountParameter & + SponsorUserOperationMiddleware + +export type SendUserOperationReturnType = Hex + +export async function sendUserOperation< + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TAccount extends SmartAccount | undefined = SmartAccount | undefined +>( + client: Client, + args: SendUserOperationParameters +): Promise { + const { account: account_ = client.account } = args + if (!account_) throw new AccountOrClientNotFoundError() + + const account = parseAccount(account_) as SmartAccount + + const userOperation = await getAction( + client, + prepareUserOperationRequest + )(args) + + userOperation.signature = await account.signUserOperation(userOperation) + + const userOpHash = await getAction( + client, + sendUserOperationBundler, + "sendUserOperation" + )({ + userOperation: userOperation, + entryPoint: account.entryPoint + }) + + return userOpHash +} diff --git a/src/actions/smartAccount/signMessage.ts b/src/actions/smartAccount/signMessage.ts new file mode 100644 index 00000000..4146d7ab --- /dev/null +++ b/src/actions/smartAccount/signMessage.ts @@ -0,0 +1,79 @@ +import type { + Chain, + Client, + SignMessageParameters, + SignMessageReturnType, + Transport +} from "viem" +import { type SmartAccount } from "../../accounts/types.js" +import { + AccountOrClientNotFoundError, + parseAccount +} from "../../utils/index.js" + +/** + * Calculates an Ethereum-specific signature in [EIP-191 format](https://eips.ethereum.org/EIPS/eip-191): `keccak256("\x19Ethereum Signed Message:\n" + len(message) + message))`. + * + * - Docs: https://viem.sh/docs/actions/wallet/signMessage.html + * - JSON-RPC Methods: + * - JSON-RPC Accounts: [`personal_sign`](https://docs.metamask.io/guide/signing-data.html#personal-sign) + * - Local Accounts: Signs locally. No JSON-RPC request. + * + * With the calculated signature, you can: + * - use [`verifyMessage`](https://viem.sh/docs/utilities/verifyMessage.html) to verify the signature, + * - use [`recoverMessageAddress`](https://viem.sh/docs/utilities/recoverMessageAddress.html) to recover the signing address from a signature. + * + * @param client - Client to use + * @param parameters - {@link SignMessageParameters} + * @returns The signed message. {@link SignMessageReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * import { signMessage } from 'viem/wallet' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const signature = await signMessage(client, { + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * message: 'hello world', + * }) + * + * @example + * // Account Hoisting + * import { createWalletClient, custom } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { mainnet } from 'viem/chains' + * import { signMessage } from 'viem/wallet' + * + * const client = createWalletClient({ + * account: privateKeyToAccount('0x…'), + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const signature = await signMessage(client, { + * message: 'hello world', + * }) + */ +export async function signMessage< + TChain extends Chain | undefined, + TAccount extends SmartAccount | undefined +>( + client: Client, + { + account: account_ = client.account, + message + }: SignMessageParameters +): Promise { + if (!account_) + throw new AccountOrClientNotFoundError({ + docsPath: "/docs/actions/wallet/signMessage" + }) + + const account = parseAccount(account_) + if (account.type === "local") return account.signMessage({ message }) + + throw new Error("Sign message is not supported by this account") +} diff --git a/src/actions/smartAccount/signTypedData.ts b/src/actions/smartAccount/signTypedData.ts new file mode 100644 index 00000000..0ddfe27c --- /dev/null +++ b/src/actions/smartAccount/signTypedData.ts @@ -0,0 +1,161 @@ +import { + type Chain, + type Client, + type SignTypedDataParameters, + type SignTypedDataReturnType, + type Transport, + type TypedData, + type TypedDataDefinition, + getTypesForEIP712Domain, + validateTypedData +} from "viem" +import { type SmartAccount } from "../../accounts/types.js" +import { + AccountOrClientNotFoundError, + parseAccount +} from "../../utils/index.js" + +/** + * Signs typed data and calculates an Ethereum-specific signature in [https://eips.ethereum.org/EIPS/eip-712](https://eips.ethereum.org/EIPS/eip-712): `sign(keccak256("\x19\x01" ‖ domainSeparator ‖ hashStruct(message)))` + * + * - Docs: https://viem.sh/docs/actions/wallet/signTypedData.html + * - JSON-RPC Methods: + * - JSON-RPC Accounts: [`eth_signTypedData_v4`](https://docs.metamask.io/guide/signing-data.html#signtypeddata-v4) + * - Local Accounts: Signs locally. No JSON-RPC request. + * + * @param client - Client to use + * @param parameters - {@link SignTypedDataParameters} + * @returns The signed data. {@link SignTypedDataReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * import { signTypedData } from 'viem/wallet' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const signature = await signTypedData(client, { + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * domain: { + * name: 'Ether Mail', + * version: '1', + * chainId: 1, + * verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + * }, + * types: { + * Person: [ + * { name: 'name', type: 'string' }, + * { name: 'wallet', type: 'address' }, + * ], + * Mail: [ + * { name: 'from', type: 'Person' }, + * { name: 'to', type: 'Person' }, + * { name: 'contents', type: 'string' }, + * ], + * }, + * primaryType: 'Mail', + * message: { + * from: { + * name: 'Cow', + * wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + * }, + * to: { + * name: 'Bob', + * wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + * }, + * contents: 'Hello, Bob!', + * }, + * }) + * + * @example + * // Account Hoisting + * import { createWalletClient, http } from 'viem' + * import { privateKeyToAccount } from 'viem/accounts' + * import { mainnet } from 'viem/chains' + * import { signTypedData } from 'viem/wallet' + * + * const client = createWalletClient({ + * account: privateKeyToAccount('0x…'), + * chain: mainnet, + * transport: http(), + * }) + * const signature = await signTypedData(client, { + * domain: { + * name: 'Ether Mail', + * version: '1', + * chainId: 1, + * verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + * }, + * types: { + * Person: [ + * { name: 'name', type: 'string' }, + * { name: 'wallet', type: 'address' }, + * ], + * Mail: [ + * { name: 'from', type: 'Person' }, + * { name: 'to', type: 'Person' }, + * { name: 'contents', type: 'string' }, + * ], + * }, + * primaryType: 'Mail', + * message: { + * from: { + * name: 'Cow', + * wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + * }, + * to: { + * name: 'Bob', + * wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + * }, + * contents: 'Hello, Bob!', + * }, + * }) + */ +export async function signTypedData< + const TTypedData extends TypedData | { [key: string]: unknown }, + TPrimaryType extends string, + TChain extends Chain | undefined, + TAccount extends SmartAccount | undefined +>( + client: Client, + { + account: account_ = client.account, + domain, + message, + primaryType, + types: types_ + }: SignTypedDataParameters +): Promise { + if (!account_) { + throw new AccountOrClientNotFoundError({ + docsPath: "/docs/actions/wallet/signMessage" + }) + } + + const account = parseAccount(account_) + + const types = { + EIP712Domain: getTypesForEIP712Domain({ domain }), + ...(types_ as TTypedData) + } + + validateTypedData({ + domain, + message, + primaryType, + types + } as TypedDataDefinition) + + if (account.type === "local") { + return account.signTypedData({ + domain, + primaryType, + types, + message + } as TypedDataDefinition) + } + + throw new Error("Sign type message is not supported by this account") +} diff --git a/src/actions/smartAccount/writeContract.ts b/src/actions/smartAccount/writeContract.ts new file mode 100644 index 00000000..f8e1cd90 --- /dev/null +++ b/src/actions/smartAccount/writeContract.ts @@ -0,0 +1,126 @@ +import { + type Abi, + type Chain, + type Client, + type EncodeFunctionDataParameters, + type Transport, + type WriteContractParameters, + type WriteContractReturnType, + encodeFunctionData +} from "viem" +import { type SmartAccount } from "../../accounts/types.js" +import { getAction } from "../../utils/getAction.js" +import { type SponsorUserOperationMiddleware } from "./prepareUserOperationRequest.js" +import { + type SendTransactionWithPaymasterParameters, + sendTransaction +} from "./sendTransaction.js" + +/** + * Executes a write function on a contract. + * This function also allows you to sponsor this transaction if sender is a smartAccount + * + * - Docs: https://viem.sh/docs/contract/writeContract.html + * - Examples: https://stackblitz.com/github/wagmi-dev/viem/tree/main/examples/contracts/writing-to-contracts + * + * A "write" function on a Solidity contract modifies the state of the blockchain. These types of functions require gas to be executed, and hence a [Transaction](https://viem.sh/docs/glossary/terms.html) is needed to be broadcast in order to change the state. + * + * Internally, uses a [Wallet Client](https://viem.sh/docs/clients/wallet.html) to call the [`sendTransaction` action](https://viem.sh/docs/actions/wallet/sendTransaction.html) with [ABI-encoded `data`](https://viem.sh/docs/contract/encodeFunctionData.html). + * + * __Warning: The `write` internally sends a transaction – it does not validate if the contract write will succeed (the contract may throw an error). It is highly recommended to [simulate the contract write with `contract.simulate`](https://viem.sh/docs/contract/writeContract.html#usage) before you execute it.__ + * + * @param client - Client to use + * @param parameters - {@link WriteContractParameters} + * @returns A [Transaction Hash](https://viem.sh/docs/glossary/terms.html#hash). {@link WriteContractReturnType} + * + * @example + * import { createWalletClient, custom, parseAbi } from 'viem' + * import { mainnet } from 'viem/chains' + * import { writeContract } from 'viem/contract' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const hash = await writeContract(client, { + * address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + * abi: parseAbi(['function mint(uint32 tokenId) nonpayable']), + * functionName: 'mint', + * args: [69420], + * }) + * + * @example + * // With Validation + * import { createWalletClient, http, parseAbi } from 'viem' + * import { mainnet } from 'viem/chains' + * import { simulateContract, writeContract } from 'viem/contract' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: http(), + * }) + * const { request } = await simulateContract(client, { + * address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + * abi: parseAbi(['function mint(uint32 tokenId) nonpayable']), + * functionName: 'mint', + * args: [69420], + * } + * const hash = await writeContract(client, request) + */ +export type WriteContractWithPaymasterParameters< + TChain extends Chain | undefined = Chain | undefined, + TAccount extends SmartAccount | undefined = SmartAccount | undefined, + TAbi extends Abi | readonly unknown[] = Abi | readonly unknown[], + TFunctionName extends string = string, + TChainOverride extends Chain | undefined = undefined +> = WriteContractParameters< + TAbi, + TFunctionName, + TChain, + TAccount, + TChainOverride +> & + SponsorUserOperationMiddleware + +export async function writeContract< + TChain extends Chain | undefined, + TAccount extends SmartAccount | undefined, + const TAbi extends Abi | readonly unknown[], + TFunctionName extends string, + TChainOverride extends Chain | undefined = undefined +>( + client: Client, + { + abi, + address, + args, + dataSuffix, + functionName, + ...request + }: WriteContractWithPaymasterParameters< + TChain, + TAccount, + TAbi, + TFunctionName, + TChainOverride + > +): Promise { + const data = encodeFunctionData({ + abi, + args, + functionName + } as unknown as EncodeFunctionDataParameters) + const hash = await getAction( + client, + sendTransaction + )({ + data: `${data}${dataSuffix ? dataSuffix.replace("0x", "") : ""}`, + to: address, + ...request + } as unknown as SendTransactionWithPaymasterParameters< + TChain, + TAccount, + TChainOverride + >) + return hash +} diff --git a/src/actions/stackup/sponsorUserOperation.ts b/src/actions/stackup/sponsorUserOperation.ts index 4f48d78f..6c26d4f6 100644 --- a/src/actions/stackup/sponsorUserOperation.ts +++ b/src/actions/stackup/sponsorUserOperation.ts @@ -2,13 +2,19 @@ import type { Address, Hex } from "viem" import type { PartialBy } from "viem/types/utils" import { type StackupPaymasterClient } from "../../clients/stackup.js" import type { StackupPaymasterContext } from "../../types/stackup.js" -import type { UserOperation, UserOperationWithBigIntAsHex } from "../../types/userOperation.js" +import type { + UserOperation, + UserOperationWithBigIntAsHex +} from "../../types/userOperation.js" import { deepHexlify } from "../../utils/deepHexlify.js" export type SponsorUserOperationParameters = { userOperation: PartialBy< UserOperation, - "callGasLimit" | "preVerificationGas" | "verificationGasLimit" | "paymasterAndData" + | "callGasLimit" + | "preVerificationGas" + | "verificationGasLimit" + | "paymasterAndData" > entryPoint: Address context: StackupPaymasterContext @@ -52,7 +58,11 @@ export const sponsorUserOperation = async ( ): Promise => { const response = await client.request({ method: "pm_sponsorUserOperation", - params: [deepHexlify(args.userOperation) as UserOperationWithBigIntAsHex, args.entryPoint, args.context] + params: [ + deepHexlify(args.userOperation) as UserOperationWithBigIntAsHex, + args.entryPoint, + args.context + ] }) return { diff --git a/src/clients/bundler.ts b/src/clients/createBundlerClient.ts similarity index 81% rename from src/clients/bundler.ts rename to src/clients/createBundlerClient.ts index 75264197..4bbd9af0 100644 --- a/src/clients/bundler.ts +++ b/src/clients/createBundlerClient.ts @@ -1,9 +1,17 @@ -import type { Account, Chain, Client, PublicClientConfig, Transport } from "viem" +import type { + Account, + Chain, + Client, + PublicClientConfig, + Transport +} from "viem" import { createClient } from "viem" import type { BundlerRpcSchema } from "../types/bundler.js" import { type BundlerActions, bundlerActions } from "./decorators/bundler.js" -export type BundlerClient = Client< +export type BundlerClient< + TChain extends Chain | undefined = Chain | undefined +> = Client< Transport, TChain, Account | undefined, @@ -29,7 +37,10 @@ export type BundlerClient * transport: http(BUNDLER_URL), * }) */ -export const createBundlerClient = ( +export const createBundlerClient = < + transport extends Transport, + chain extends Chain | undefined = undefined +>( parameters: PublicClientConfig ): BundlerClient => { const { key = "public", name = "Bundler Client" } = parameters diff --git a/src/clients/createSmartAccountClient.ts b/src/clients/createSmartAccountClient.ts new file mode 100644 index 00000000..b970418b --- /dev/null +++ b/src/clients/createSmartAccountClient.ts @@ -0,0 +1,95 @@ +import type { + Chain, + Client, + ClientConfig, + ParseAccount, + Transport, + WalletClientConfig +} from "viem" +import { createClient } from "viem" +import { type SmartAccount } from "../accounts/types.js" +import { type SponsorUserOperationMiddleware } from "../actions/smartAccount/prepareUserOperationRequest.js" +import { type BundlerRpcSchema } from "../types/bundler.js" +import type { Prettify } from "../types/index.js" +import { + type SmartAccountActions, + smartAccountActions +} from "./decorators/smartAccount.js" + +export type SmartAccountClient< + transport extends Transport = Transport, + chain extends Chain | undefined = Chain | undefined, + account extends SmartAccount | undefined = SmartAccount | undefined +> = Prettify< + Client< + transport, + chain, + account, + BundlerRpcSchema, + SmartAccountActions + > +> + +export type SmartAccountClientConfig< + transport extends Transport = Transport, + chain extends Chain | undefined = Chain | undefined, + TAccount extends SmartAccount | undefined = SmartAccount | undefined +> = Prettify< + Pick< + ClientConfig, + | "account" + | "cacheTime" + | "chain" + | "key" + | "name" + | "pollingInterval" + | "transport" + > +> + +/** + * Creates a EIP-4337 compliant Bundler Client with a given [Transport](https://viem.sh/docs/clients/intro.html) configured for a [Chain](https://viem.sh/docs/clients/chains.html). + * + * - Docs: https://docs.pimlico.io/permissionless/reference/clients/smartAccountClient + * + * A Bundler Client is an interface to "erc 4337" [JSON-RPC API](https://eips.ethereum.org/EIPS/eip-4337#rpc-methods-eth-namespace) methods such as sending user operation, estimating gas for a user operation, get user operation receipt, etc through Bundler Actions. + * + * @param config - {@link WalletClientConfig} + * @returns A Bundler Client. {@link SmartAccountClient} + * + * @example + * import { createPublicClient, http } from 'viem' + * import { mainnet } from 'viem/chains' + * + * const smartAccountClient = createSmartAccountClient({ + * chain: mainnet, + * transport: http(BUNDLER_URL), + * }) + */ +export const createSmartAccountClient = < + TTransport extends Transport, + TChain extends Chain | undefined = undefined, + TSmartAccount extends SmartAccount | undefined = undefined +>( + parameters: SmartAccountClientConfig & + SponsorUserOperationMiddleware +): SmartAccountClient> => { + const { + key = "Account", + name = "Smart Account Client", + transport + } = parameters + const client = createClient({ + ...parameters, + key, + name, + transport: (opts) => transport({ ...opts, retryCount: 0 }), + type: "smartAccountClient" + }) + + return client.extend( + smartAccountActions({ + sponsorUserOperation: parameters.sponsorUserOperation + }) + ) +} diff --git a/src/clients/decorators/bundler.ts b/src/clients/decorators/bundler.ts index 0f9b6701..92539eaf 100644 --- a/src/clients/decorators/bundler.ts +++ b/src/clients/decorators/bundler.ts @@ -16,13 +16,16 @@ import { type GetUserOperationReceiptReturnType, getUserOperationReceipt } from "../../actions/bundler/getUserOperationReceipt.js" -import { type SendUserOperationParameters, sendUserOperation } from "../../actions/bundler/sendUserOperation.js" +import { + type SendUserOperationParameters, + sendUserOperation +} from "../../actions/bundler/sendUserOperation.js" import { supportedEntryPoints } from "../../actions/bundler/supportedEntryPoints.js" import { type WaitForUserOperationReceiptParameters, waitForUserOperationReceipt } from "../../actions/bundler/waitForUserOperationReceipt.js" -import type { BundlerClient } from "../bundler.js" +import type { BundlerClient } from "../createBundlerClient.js" export type BundlerActions = { /** @@ -76,7 +79,9 @@ export type BundlerActions = { * * // Return {preVerificationGas: 43492n, verificationGasLimit: 59436n, callGasLimit: 9000n} */ - estimateUserOperationGas: (args: EstimateUserOperationGasParameters) => Promise + estimateUserOperationGas: ( + args: EstimateUserOperationGasParameters + ) => Promise /** * * Returns the supported entrypoints by the bundler service @@ -141,7 +146,9 @@ export type BundlerActions = { * await bundlerClient.getUserOperationByHash(userOpHash) * */ - getUserOperationByHash: (args: GetUserOperationByHashParameters) => Promise + getUserOperationByHash: ( + args: GetUserOperationByHashParameters + ) => Promise /** * * Returns the user operation receipt from userOpHash @@ -194,18 +201,21 @@ export type BundlerActions = { } const bundlerActions = (client: Client): BundlerActions => ({ - sendUserOperation: async (args: SendUserOperationParameters): Promise => - sendUserOperation(client as BundlerClient, args), + sendUserOperation: async ( + args: SendUserOperationParameters + ): Promise => sendUserOperation(client as BundlerClient, args), estimateUserOperationGas: (args: EstimateUserOperationGasParameters) => estimateUserOperationGas(client as BundlerClient, args), - supportedEntryPoints: (): Promise => supportedEntryPoints(client as BundlerClient), + supportedEntryPoints: (): Promise => + supportedEntryPoints(client as BundlerClient), chainId: () => chainId(client as BundlerClient), getUserOperationByHash: (args: GetUserOperationByHashParameters) => getUserOperationByHash(client as BundlerClient, args), getUserOperationReceipt: (args: GetUserOperationReceiptParameters) => getUserOperationReceipt(client as BundlerClient, args), - waitForUserOperationReceipt: (args: WaitForUserOperationReceiptParameters) => - waitForUserOperationReceipt(client as BundlerClient, args) + waitForUserOperationReceipt: ( + args: WaitForUserOperationReceiptParameters + ) => waitForUserOperationReceipt(client as BundlerClient, args) }) export { bundlerActions } diff --git a/src/clients/decorators/pimlico.ts b/src/clients/decorators/pimlico.ts index 3f76193b..03d91c96 100644 --- a/src/clients/decorators/pimlico.ts +++ b/src/clients/decorators/pimlico.ts @@ -13,7 +13,10 @@ import { type SponsorUserOperationReturnType, sponsorUserOperation } from "../../actions/pimlico/sponsorUserOperation.js" -import type { PimlicoBundlerClient, PimlicoPaymasterClient } from "../pimlico.js" +import type { + PimlicoBundlerClient, + PimlicoPaymasterClient +} from "../pimlico.js" export type PimlicoBundlerActions = { /** @@ -55,11 +58,16 @@ export type PimlicoBundlerActions = { * * await bundlerClient.getUserOperationStatus({ hash: userOpHash }) */ - getUserOperationStatus: (args: GetUserOperationStatusParameters) => Promise + getUserOperationStatus: ( + args: GetUserOperationStatusParameters + ) => Promise } -export const pimlicoBundlerActions = (client: Client): PimlicoBundlerActions => ({ - getUserOperationGasPrice: async () => getUserOperationGasPrice(client as PimlicoBundlerClient), +export const pimlicoBundlerActions = ( + client: Client +): PimlicoBundlerActions => ({ + getUserOperationGasPrice: async () => + getUserOperationGasPrice(client as PimlicoBundlerClient), getUserOperationStatus: async (args: GetUserOperationStatusParameters) => getUserOperationStatus(client as PimlicoBundlerClient, args) }) @@ -88,10 +96,14 @@ export type PimlicoPaymasterClientActions = { * }}) * */ - sponsorUserOperation: (args: SponsorUserOperationParameters) => Promise + sponsorUserOperation: ( + args: SponsorUserOperationParameters + ) => Promise } -export const pimlicoPaymasterActions = (client: Client): PimlicoPaymasterClientActions => ({ +export const pimlicoPaymasterActions = ( + client: Client +): PimlicoPaymasterClientActions => ({ sponsorUserOperation: async (args: SponsorUserOperationParameters) => sponsorUserOperation(client as PimlicoPaymasterClient, args) }) diff --git a/src/clients/decorators/smartAccount.ts b/src/clients/decorators/smartAccount.ts new file mode 100644 index 00000000..0399a098 --- /dev/null +++ b/src/clients/decorators/smartAccount.ts @@ -0,0 +1,390 @@ +import type { + Abi, + Chain, + Client, + DeployContractParameters, + SendTransactionParameters, + Transport, + TypedData, + WriteContractParameters +} from "viem" +import type { SmartAccount } from "../../accounts/types.js" +import { + type DeployContractParametersWithPaymaster, + deployContract +} from "../../actions/smartAccount/deployContract.js" +import { + type PrepareUserOperationRequestReturnType, + type SponsorUserOperationMiddleware, + prepareUserOperationRequest +} from "../../actions/smartAccount/prepareUserOperationRequest.js" +import { + type SendTransactionWithPaymasterParameters, + sendTransaction +} from "../../actions/smartAccount/sendTransaction.js" +import { signMessage } from "../../actions/smartAccount/signMessage.js" +import { signTypedData } from "../../actions/smartAccount/signTypedData.js" +import { + type WriteContractWithPaymasterParameters, + writeContract +} from "../../actions/smartAccount/writeContract.js" + +export type SmartAccountActions< + TChain extends Chain | undefined = Chain | undefined, + TSmartAccount extends SmartAccount | undefined = SmartAccount | undefined +> = { + /** + * 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, + * }) + * + * @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, + * }) + */ + sendTransaction: ( + args: SendTransactionParameters + ) => ReturnType< + typeof sendTransaction + > + /** + * Calculates an Ethereum-specific signature in [EIP-191 format](https://eips.ethereum.org/EIPS/eip-191): `keccak256("\x19Ethereum Signed Message:\n" + len(message) + message))`. + * + * - Docs: https://viem.sh/docs/actions/wallet/signMessage.html + * - JSON-RPC Methods: + * - JSON-RPC Accounts: [`personal_sign`](https://docs.metamask.io/guide/signing-data.html#personal-sign) + * - Local Accounts: Signs locally. No JSON-RPC request. + * + * With the calculated signature, you can: + * - use [`verifyMessage`](https://viem.sh/docs/utilities/verifyMessage.html) to verify the signature, + * - use [`recoverMessageAddress`](https://viem.sh/docs/utilities/recoverMessageAddress.html) to recover the signing address from a signature. + * + * @param args - {@link SignMessageParameters} + * @returns The signed message. {@link SignMessageReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const signature = await client.signMessage({ + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * message: 'hello world', + * }) + * + * @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 signature = await client.signMessage({ + * message: 'hello world', + * }) + */ + signMessage: ( + args: Parameters>[1] + ) => ReturnType> + /** + * Signs typed data and calculates an Ethereum-specific signature in [EIP-191 format](https://eips.ethereum.org/EIPS/eip-191): `keccak256("\x19Ethereum Signed Message:\n" + len(message) + message))`. + * + * - Docs: https://viem.sh/docs/actions/wallet/signTypedData.html + * - JSON-RPC Methods: + * - JSON-RPC Accounts: [`eth_signTypedData_v4`](https://docs.metamask.io/guide/signing-data.html#signtypeddata-v4) + * - Local Accounts: Signs locally. No JSON-RPC request. + * + * @param client - Client to use + * @param args - {@link SignTypedDataParameters} + * @returns The signed data. {@link SignTypedDataReturnType} + * + * @example + * import { createWalletClient, custom } from 'viem' + * import { mainnet } from 'viem/chains' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const signature = await client.signTypedData({ + * account: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e', + * domain: { + * name: 'Ether Mail', + * version: '1', + * chainId: 1, + * verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + * }, + * types: { + * Person: [ + * { name: 'name', type: 'string' }, + * { name: 'wallet', type: 'address' }, + * ], + * Mail: [ + * { name: 'from', type: 'Person' }, + * { name: 'to', type: 'Person' }, + * { name: 'contents', type: 'string' }, + * ], + * }, + * primaryType: 'Mail', + * message: { + * from: { + * name: 'Cow', + * wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + * }, + * to: { + * name: 'Bob', + * wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + * }, + * contents: 'Hello, Bob!', + * }, + * }) + * + * @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 signature = await client.signTypedData({ + * domain: { + * name: 'Ether Mail', + * version: '1', + * chainId: 1, + * verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + * }, + * types: { + * Person: [ + * { name: 'name', type: 'string' }, + * { name: 'wallet', type: 'address' }, + * ], + * Mail: [ + * { name: 'from', type: 'Person' }, + * { name: 'to', type: 'Person' }, + * { name: 'contents', type: 'string' }, + * ], + * }, + * primaryType: 'Mail', + * message: { + * from: { + * name: 'Cow', + * wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + * }, + * to: { + * name: 'Bob', + * wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + * }, + * contents: 'Hello, Bob!', + * }, + * }) + */ + signTypedData: < + const TTypedData extends TypedData | { [key: string]: unknown }, + TPrimaryType extends string + >( + args: Parameters< + typeof signTypedData< + TTypedData, + TPrimaryType, + TChain, + TSmartAccount + > + >[1] + ) => ReturnType< + typeof signTypedData + > + /** + * Deploys a contract to the network, given bytecode and constructor arguments. + * This function also allows you to sponsor this transaction if sender is a smartAccount + * + * - Docs: https://viem.sh/docs/contract/deployContract.html + * - Examples: https://stackblitz.com/github/wagmi-dev/viem/tree/main/examples/contracts/deploying-contracts + * + * @param args - {@link DeployContractParameters} + * @returns The [Transaction](https://viem.sh/docs/glossary/terms.html#transaction) hash. {@link DeployContractReturnType} + * + * @example + * 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.deployContract({ + * abi: [], + * account: '0x…, + * bytecode: '0x608060405260405161083e38038061083e833981016040819052610...', + * }) + */ + deployContract: < + const TAbi extends Abi | readonly unknown[], + TChainOverride extends Chain | undefined = undefined + >( + args: DeployContractParameters< + TAbi, + TChain, + TSmartAccount, + TChainOverride + > + ) => ReturnType< + typeof deployContract + > + /** + * Executes a write function on a contract. + * This function also allows you to sponsor this transaction if sender is a smartAccount + * + * - Docs: https://viem.sh/docs/contract/writeContract.html + * - Examples: https://stackblitz.com/github/wagmi-dev/viem/tree/main/examples/contracts/writing-to-contracts + * + * A "write" function on a Solidity contract modifies the state of the blockchain. These types of functions require gas to be executed, and hence a [Transaction](https://viem.sh/docs/glossary/terms.html) is needed to be broadcast in order to change the state. + * + * Internally, uses a [Wallet Client](https://viem.sh/docs/clients/wallet.html) to call the [`sendTransaction` action](https://viem.sh/docs/actions/wallet/sendTransaction.html) with [ABI-encoded `data`](https://viem.sh/docs/contract/encodeFunctionData.html). + * + * __Warning: The `write` internally sends a transaction – it does not validate if the contract write will succeed (the contract may throw an error). It is highly recommended to [simulate the contract write with `contract.simulate`](https://viem.sh/docs/contract/writeContract.html#usage) before you execute it.__ + * + * @param args - {@link WriteContractParameters} + * @returns A [Transaction Hash](https://viem.sh/docs/glossary/terms.html#hash). {@link WriteContractReturnType} + * + * @example + * import { createWalletClient, custom, parseAbi } from 'viem' + * import { mainnet } from 'viem/chains' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const hash = await client.writeContract({ + * address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + * abi: parseAbi(['function mint(uint32 tokenId) nonpayable']), + * functionName: 'mint', + * args: [69420], + * }) + * + * @example + * // With Validation + * import { createWalletClient, custom, parseAbi } from 'viem' + * import { mainnet } from 'viem/chains' + * + * const client = createWalletClient({ + * chain: mainnet, + * transport: custom(window.ethereum), + * }) + * const { request } = await client.simulateContract({ + * address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + * abi: parseAbi(['function mint(uint32 tokenId) nonpayable']), + * functionName: 'mint', + * args: [69420], + * } + * const hash = await client.writeContract(request) + */ + writeContract: < + const TAbi extends Abi | readonly unknown[], + TFunctionName extends string, + TChainOverride extends Chain | undefined = undefined + >( + args: WriteContractParameters< + TAbi, + TFunctionName, + TChain, + TSmartAccount, + TChainOverride + > + ) => ReturnType< + typeof writeContract< + TChain, + TSmartAccount, + TAbi, + TFunctionName, + TChainOverride + > + > + prepareUserOperationRequest: ( + args: Parameters< + typeof prepareUserOperationRequest< + TTransport, + TChain, + TSmartAccount + > + >[1] + ) => Promise +} + +export const smartAccountActions = + ({ sponsorUserOperation }: SponsorUserOperationMiddleware) => + < + TTransport extends Transport, + TChain extends Chain | undefined = Chain | undefined, + TSmartAccount extends SmartAccount | undefined = + | SmartAccount + | undefined + >( + client: Client + ): SmartAccountActions => ({ + prepareUserOperationRequest: (args) => + prepareUserOperationRequest(client, args), + deployContract: (args) => + deployContract(client, { + ...args, + sponsorUserOperation + } as DeployContractParametersWithPaymaster), + sendTransaction: (args) => + sendTransaction(client, { + ...args, + sponsorUserOperation + } as SendTransactionWithPaymasterParameters), + signMessage: (args) => signMessage(client, args), + signTypedData: (args) => signTypedData(client, args), + writeContract: (args) => + writeContract(client, { + ...args, + sponsorUserOperation + } as WriteContractWithPaymasterParameters) + }) diff --git a/src/clients/decorators/stackup.ts b/src/clients/decorators/stackup.ts index e50001c4..27e0620c 100644 --- a/src/clients/decorators/stackup.ts +++ b/src/clients/decorators/stackup.ts @@ -1,5 +1,8 @@ import type { Address, Client } from "viem" -import { type AccountsParameters, accounts } from "../../actions/stackup/accounts.js" +import { + type AccountsParameters, + accounts +} from "../../actions/stackup/accounts.js" import { type SponsorUserOperationParameters, type SponsorUserOperationReturnType, @@ -31,7 +34,9 @@ export type StackupPaymasterClientActions = { * }}) * */ - sponsorUserOperation: (args: SponsorUserOperationParameters) => Promise + sponsorUserOperation: ( + args: SponsorUserOperationParameters + ) => Promise /** * Returns all the Paymaster addresses associated with an EntryPoint that’s owned by this service. @@ -58,8 +63,11 @@ export type StackupPaymasterClientActions = { accounts: (args: AccountsParameters) => Promise } -export const stackupPaymasterActions = (client: Client): StackupPaymasterClientActions => ({ +export const stackupPaymasterActions = ( + client: Client +): StackupPaymasterClientActions => ({ sponsorUserOperation: async (args: SponsorUserOperationParameters) => sponsorUserOperation(client as StackupPaymasterClient, args), - accounts: async (args: AccountsParameters) => accounts(client as StackupPaymasterClient, args) + accounts: async (args: AccountsParameters) => + accounts(client as StackupPaymasterClient, args) }) diff --git a/src/clients/pimlico.ts b/src/clients/pimlico.ts index 99dff902..a935ab67 100644 --- a/src/clients/pimlico.ts +++ b/src/clients/pimlico.ts @@ -1,6 +1,15 @@ -import type { Account, Chain, Client, PublicClientConfig, Transport } from "viem" +import type { + Account, + Chain, + Client, + PublicClientConfig, + Transport +} from "viem" import { createClient } from "viem" -import type { PimlicoBundlerRpcSchema, PimlicoPaymasterRpcSchema } from "../types/pimlico.js" +import type { + PimlicoBundlerRpcSchema, + PimlicoPaymasterRpcSchema +} from "../types/pimlico.js" import { type BundlerActions, bundlerActions } from "./decorators/bundler.js" import { type PimlicoBundlerActions, @@ -44,7 +53,10 @@ export type PimlicoPaymasterClient = Client< * transport: http("https://api.pimlico.io/v1/goerli/rpc?apikey=YOUR_API_KEY_HERE"), * }) */ -export const createPimlicoBundlerClient = ( +export const createPimlicoBundlerClient = < + transport extends Transport, + chain extends Chain | undefined = undefined +>( parameters: PublicClientConfig ): PimlicoBundlerClient => { const { key = "public", name = "Pimlico Bundler Client" } = parameters @@ -76,7 +88,10 @@ export const createPimlicoBundlerClient = ( +export const createPimlicoPaymasterClient = < + transport extends Transport, + chain extends Chain | undefined = undefined +>( parameters: PublicClientConfig ): PimlicoPaymasterClient => { const { key = "public", name = "Pimlico Paymaster Client" } = parameters diff --git a/src/clients/stackup.ts b/src/clients/stackup.ts index 949e9da9..05469e05 100644 --- a/src/clients/stackup.ts +++ b/src/clients/stackup.ts @@ -1,7 +1,17 @@ -import { type Account, type Chain, type Client, type PublicClientConfig, type Transport, createClient } from "viem" +import { + type Account, + type Chain, + type Client, + type PublicClientConfig, + type Transport, + createClient +} from "viem" import type { StackupPaymasterRpcSchema } from "../types/stackup.js" import { type BundlerActions, bundlerActions } from "./decorators/bundler.js" -import { type StackupPaymasterClientActions, stackupPaymasterActions } from "./decorators/stackup.js" +import { + type StackupPaymasterClientActions, + stackupPaymasterActions +} from "./decorators/stackup.js" export type StackupPaymasterClient = Client< Transport, @@ -30,7 +40,10 @@ export type StackupPaymasterClient = Client< * transport: http("https://api.stackup.sh/v1/paymaster/YOUR_API_KEY_HERE"), * }) */ -export const createStackupPaymasterClient = ( +export const createStackupPaymasterClient = < + transport extends Transport, + chain extends Chain | undefined = undefined +>( parameters: PublicClientConfig ): StackupPaymasterClient => { const { key = "public", name = "Stackup Paymaster Client" } = parameters diff --git a/src/index.ts b/src/index.ts index 310dcf6a..fe1d1d14 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,9 +26,21 @@ import { } from "./actions/bundler/waitForUserOperationReceipt.js" import type { GetAccountNonceParams } from "./actions/public/getAccountNonce.js" import { getAccountNonce } from "./actions/public/getAccountNonce.js" -import { type BundlerClient, createBundlerClient } from "./clients/bundler.js" +import { + type BundlerClient, + createBundlerClient +} from "./clients/createBundlerClient.js" +import { createSmartAccountClient } from "./clients/createSmartAccountClient.js" +import { + type SmartAccountClient, + type SmartAccountClientConfig +} from "./clients/createSmartAccountClient.js" import type { BundlerActions } from "./clients/decorators/bundler.js" import { bundlerActions } from "./clients/decorators/bundler.js" +import { + type SmartAccountActions, + smartAccountActions +} from "./clients/decorators/smartAccount.js" export type { SendUserOperationParameters, @@ -42,7 +54,10 @@ export type { GetAccountNonceParams, BundlerClient, BundlerActions, - WaitForUserOperationReceiptParameters + WaitForUserOperationReceiptParameters, + SmartAccountClient, + SmartAccountClientConfig, + SmartAccountActions } export { @@ -57,7 +72,9 @@ export { waitForUserOperationReceipt, createBundlerClient, bundlerActions, - WaitForUserOperationReceiptTimeoutError + WaitForUserOperationReceiptTimeoutError, + createSmartAccountClient, + smartAccountActions } import type { UserOperation } from "./types/userOperation.js" diff --git a/src/package.json b/src/package.json index aee63ca1..a60cf257 100644 --- a/src/package.json +++ b/src/package.json @@ -9,13 +9,7 @@ "type": "module", "sideEffects": false, "description": "A utility library for working with ERC-4337", - "keywords": [ - "ethereum", - "erc-4337", - "eip-4337", - "paymaster", - "bundler" - ], + "keywords": ["ethereum", "erc-4337", "eip-4337", "paymaster", "bundler"], "license": "MIT", "exports": { ".": { @@ -23,6 +17,11 @@ "import": "./_esm/index.js", "default": "./_cjs/index.js" }, + "./accounts": { + "types": "./_types/accounts/index.d.ts", + "import": "./_esm/accounts/index.js", + "default": "./_cjs/accounts/index.js" + }, "./actions": { "types": "./_types/actions/index.d.ts", "import": "./_esm/actions/index.js", @@ -38,6 +37,12 @@ "import": "./_esm/actions/stackup.js", "default": "./_cjs/actions/stackup.js" }, + + "./actions/smartAccount": { + "types": "./_types/actions/smartAccount.d.ts", + "import": "./_esm/actions/smartAccount.js", + "default": "./_cjs/actions/smartAccount.js" + }, "./clients": { "types": "./_types/clients/index.d.ts", "import": "./_esm/clients/index.js", diff --git a/src/types/bundler.ts b/src/types/bundler.ts index fa2b92ac..ca1a599b 100644 --- a/src/types/bundler.ts +++ b/src/types/bundler.ts @@ -5,7 +5,10 @@ import type { UserOperationWithBigIntAsHex } from "./userOperation.js" export type BundlerRpcSchema = [ { Method: "eth_sendUserOperation" - Parameters: [userOperation: UserOperationWithBigIntAsHex, entryPoint: Address] + Parameters: [ + userOperation: UserOperationWithBigIntAsHex, + entryPoint: Address + ] ReturnType: Hash }, { diff --git a/src/types/index.ts b/src/types/index.ts index 5387c10c..1a558cbc 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,27 @@ +import type { Account, Chain, Client, Transport } from "viem" +import type { SmartAccount } from "../accounts/types.js" import type { UserOperation } from "./userOperation.js" export type { UserOperation } + +type IsUndefined = [undefined] extends [T] ? true : false + +export type GetAccountParameterWithClient< + TTransport extends Transport = Transport, + TChain extends Chain | undefined = Chain | undefined, + TAccount extends Account | undefined = Account | undefined +> = IsUndefined extends true + ? { account: Account; client?: Client } + : { client: Client; account?: Account } + +export type GetAccountParameter< + TAccount extends SmartAccount | undefined = SmartAccount | undefined +> = IsUndefined extends true + ? { account: SmartAccount } + : { account?: SmartAccount } + +export type Prettify = { + [K in keyof T]: T[K] +} & {} + +export type PartialBy = Omit & Partial> diff --git a/src/types/pimlico.ts b/src/types/pimlico.ts index fd5dd4a3..cc921189 100644 --- a/src/types/pimlico.ts +++ b/src/types/pimlico.ts @@ -18,7 +18,14 @@ type PimlicoUserOperationGasPriceWithBigIntAsHex = { } export type PimlicoUserOperationStatus = { - status: "not_found" | "not_submitted" | "submitted" | "rejected" | "reverted" | "included" | "failed" + status: + | "not_found" + | "not_submitted" + | "submitted" + | "rejected" + | "reverted" + | "included" + | "failed" transactionHash: Hash | null } @@ -41,7 +48,10 @@ export type PimlicoPaymasterRpcSchema = [ Parameters: [ userOperation: PartialBy< UserOperationWithBigIntAsHex, - "callGasLimit" | "preVerificationGas" | "verificationGasLimit" | "paymasterAndData" + | "callGasLimit" + | "preVerificationGas" + | "verificationGasLimit" + | "paymasterAndData" >, entryPoint: Address ] diff --git a/src/types/stackup.ts b/src/types/stackup.ts index 1cc0290f..52bc3695 100644 --- a/src/types/stackup.ts +++ b/src/types/stackup.ts @@ -16,7 +16,10 @@ export type StackupPaymasterRpcSchema = [ Parameters: [ userOperation: PartialBy< UserOperationWithBigIntAsHex, - "callGasLimit" | "preVerificationGas" | "verificationGasLimit" | "paymasterAndData" + | "callGasLimit" + | "preVerificationGas" + | "verificationGasLimit" + | "paymasterAndData" >, entryPoint: Address, context: StackupPaymasterContext diff --git a/src/utils/deepHexlify.test.ts b/src/utils/deepHexlify.test.ts index 1796ca3b..e57dd201 100644 --- a/src/utils/deepHexlify.test.ts +++ b/src/utils/deepHexlify.test.ts @@ -1,16 +1,22 @@ +import { beforeAll, expect, test } from "bun:test" import dotenv from "dotenv" import { deepHexlify } from "./deepHexlify.js" -import { beforeAll, expect, test } from "bun:test" dotenv.config() beforeAll(() => { - if (!process.env.PIMLICO_API_KEY) throw new Error("PIMLICO_API_KEY environment variable not set") - if (!process.env.STACKUP_API_KEY) throw new Error("STACKUP_API_KEY environment variable not set") - if (!process.env.FACTORY_ADDRESS) throw new Error("FACTORY_ADDRESS environment variable not set") - if (!process.env.TEST_PRIVATE_KEY) throw new Error("TEST_PRIVATE_KEY environment variable not set") - if (!process.env.RPC_URL) throw new Error("RPC_URL environment variable not set") - if (!process.env.ENTRYPOINT_ADDRESS) throw new Error("ENTRYPOINT_ADDRESS environment variable not set") + if (!process.env.PIMLICO_API_KEY) + throw new Error("PIMLICO_API_KEY environment variable not set") + if (!process.env.STACKUP_API_KEY) + throw new Error("STACKUP_API_KEY environment variable not set") + if (!process.env.FACTORY_ADDRESS) + throw new Error("FACTORY_ADDRESS environment variable not set") + if (!process.env.TEST_PRIVATE_KEY) + throw new Error("TEST_PRIVATE_KEY environment variable not set") + if (!process.env.RPC_URL) + throw new Error("RPC_URL environment variable not set") + if (!process.env.ENTRYPOINT_ADDRESS) + throw new Error("ENTRYPOINT_ADDRESS environment variable not set") }) test("Test deep Hexlify", async () => { diff --git a/src/utils/deepHexlify.ts b/src/utils/deepHexlify.ts index 13bfa3bc..e250f210 100644 --- a/src/utils/deepHexlify.ts +++ b/src/utils/deepHexlify.ts @@ -21,10 +21,11 @@ export function deepHexlify(obj: any): any { return obj.map((member) => deepHexlify(member)) } return Object.keys(obj).reduce( - (set, key) => ({ - ...set, - [key]: deepHexlify(obj[key]) - }), + // biome-ignore lint/suspicious/noExplicitAny: it's a recursive function, so it's hard to type + (set: any, key: string) => { + set[key] = deepHexlify(obj[key]) + return set + }, {} ) } diff --git a/src/utils/getAction.ts b/src/utils/getAction.ts new file mode 100644 index 00000000..9022a162 --- /dev/null +++ b/src/utils/getAction.ts @@ -0,0 +1,15 @@ +import type { Client } from "viem" + +export function getAction( + client: Client, + // biome-ignore lint/suspicious/noExplicitAny: it's a recursive function, so it's hard to type + action: (_: any, params: params) => returnType, + actionName: string = action.name +) { + return (params: params): returnType => + ( + client as Client & { + [key: string]: (params: params) => returnType + } + )[actionName]?.(params) ?? action(client, params) +} diff --git a/src/utils/getUserOperationHash.ts b/src/utils/getUserOperationHash.ts index 5e7352f8..3bd141e2 100644 --- a/src/utils/getUserOperationHash.ts +++ b/src/utils/getUserOperationHash.ts @@ -35,7 +35,11 @@ function packUserOp({ userOperation }: { userOperation: UserOperation }): Hex { ) } -export type GetUserOperationHashParams = { userOperation: UserOperation; entryPoint: Address; chainId: number } +export type GetUserOperationHashParams = { + userOperation: UserOperation + entryPoint: Address + chainId: number +} /** * @@ -58,7 +62,11 @@ export type GetUserOperationHashParams = { userOperation: UserOperation; entryPo * // Returns "0xe9fad2cd67f9ca1d0b7a6513b2a42066784c8df938518da2b51bb8cc9a89ea34" * */ -export const getUserOperationHash = ({ userOperation, entryPoint, chainId }: GetUserOperationHashParams): Hash => { +export const getUserOperationHash = ({ + userOperation, + entryPoint, + chainId +}: GetUserOperationHashParams): Hash => { const encoded = encodeAbiParameters( [{ type: "bytes32" }, { type: "address" }, { type: "uint256" }], [keccak256(packUserOp({ userOperation })), entryPoint, BigInt(chainId)] diff --git a/src/utils/index.ts b/src/utils/index.ts index dda63cb3..250ebc45 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,18 @@ -import { type GetUserOperationHashParams, getUserOperationHash } from "./getUserOperationHash.js" -import { AccountOrClientNotFoundError, signUserOperationHashWithECDSA } from "./signUserOperationHashWithECDSA.js" +import type { Account, Address } from "viem" +import { + type GetUserOperationHashParams, + getUserOperationHash +} from "./getUserOperationHash.js" +import { + AccountOrClientNotFoundError, + signUserOperationHashWithECDSA +} from "./signUserOperationHashWithECDSA.js" + +export function parseAccount(account: Address | Account): Account { + if (typeof account === "string") + return { address: account, type: "json-rpc" } + return account +} export { getUserOperationHash, diff --git a/src/utils/observe.ts b/src/utils/observe.ts index 60368417..b4b719a8 100644 --- a/src/utils/observe.ts +++ b/src/utils/observe.ts @@ -4,10 +4,16 @@ import type { MaybePromise } from "viem/types/utils" type Callback = ((...args: any[]) => any) | undefined type Callbacks = Record -export const listenersCache = /*#__PURE__*/ new Map() +export const listenersCache = /*#__PURE__*/ new Map< + string, + { id: number; fns: Callbacks }[] +>() export const cleanupCache = /*#__PURE__*/ new Map void>() -type EmitFunction = (emit: TCallbacks) => MaybePromise void)> +type EmitFunction = ( + emit: TCallbacks + // biome-ignore lint/suspicious/noConfusingVoidType: +) => MaybePromise void)> let callbackCount = 0 @@ -41,16 +47,23 @@ export function observe( } const listeners = getListeners() - listenersCache.set(observerId, [...listeners, { id: callbackId, fns: callbacks }]) + listenersCache.set(observerId, [ + ...listeners, + { id: callbackId, fns: callbacks } + ]) if (listeners && listeners.length > 0) return unwatch const emit: TCallbacks = {} as TCallbacks for (const key in callbacks) { - emit[key] = ((...args: Parameters>) => { + emit[key] = (( + ...args: Parameters> + ) => { const listeners = getListeners() if (listeners.length === 0) return - listeners.forEach((listener) => listener.fns[key]?.(...args)) + for (const listener of listeners) { + listener.fns[key]?.(...args) + } }) as TCallbacks[Extract] } diff --git a/src/utils/signUserOperationHashWithECDSA.ts b/src/utils/signUserOperationHashWithECDSA.ts index 45367afd..a77896bb 100644 --- a/src/utils/signUserOperationHashWithECDSA.ts +++ b/src/utils/signUserOperationHashWithECDSA.ts @@ -8,29 +8,16 @@ import { type Hex, type Transport } from "viem" +import { type GetAccountParameterWithClient } from "../types/index.js" import type { UserOperation } from "../types/userOperation.js" import { getUserOperationHash } from "./getUserOperationHash.js" - -function parseAccount(account: Address | Account): Account { - if (typeof account === "string") return { address: account, type: "json-rpc" } - return account -} - -type IsUndefined = [undefined] extends [T] ? true : false - -type GetAccountParameter< - TTransport extends Transport = Transport, - TChain extends Chain | undefined = Chain | undefined, - TAccount extends Account | undefined = Account | undefined -> = IsUndefined extends true - ? { account: Account; client?: undefined } - : { client: Client; account?: undefined } +import { parseAccount } from "./index.js" export type signUserOperationHashWithECDSAParams< TTransport extends Transport = Transport, TChain extends Chain | undefined = Chain | undefined, TAccount extends Account | undefined = Account | undefined -> = GetAccountParameter & +> = GetAccountParameterWithClient & ( | { hash: Hash @@ -96,10 +83,15 @@ export const signUserOperationHashWithECDSA = async < userOperation, chainId, entryPoint -}: signUserOperationHashWithECDSAParams): Promise => { +}: signUserOperationHashWithECDSAParams< + TTransport, + TChain, + TAccount +>): Promise => { if (!account_) throw new AccountOrClientNotFoundError({ - docsPath: "/permissionless/reference/utils/signUserOperationHashWithECDSA" + docsPath: + "/permissionless/reference/utils/signUserOperationHashWithECDSA" }) let userOperationHash: Hash @@ -107,7 +99,11 @@ export const signUserOperationHashWithECDSA = async < if (hash) { userOperationHash = hash } else { - userOperationHash = getUserOperationHash({ userOperation, chainId, entryPoint }) + userOperationHash = getUserOperationHash({ + userOperation, + chainId, + entryPoint + }) } const account = parseAccount(account_) @@ -121,7 +117,8 @@ export const signUserOperationHashWithECDSA = async < if (!client) throw new AccountOrClientNotFoundError({ - docsPath: "/permissionless/reference/utils/signUserOperationHashWithECDSA" + docsPath: + "/permissionless/reference/utils/signUserOperationHashWithECDSA" }) return client.request({ diff --git a/test/abis/Greeter.ts b/test/abis/Greeter.ts new file mode 100644 index 00000000..756e69db --- /dev/null +++ b/test/abis/Greeter.ts @@ -0,0 +1,96 @@ +export const GreeterBytecode = + "0x60806040523480156200001157600080fd5b50620000776040518060600160405280602281526020016200133f602291396040518060400160405280600581526020017f48656c6c6f000000000000000000000000000000000000000000000000000000815250620000c460201b620003cc1760201c565b6040518060400160405280600581526020017f48656c6c6f00000000000000000000000000000000000000000000000000000081525060009081620000bd91906200040d565b50620005be565b620001668282604051602401620000dd92919062000583565b6040516020818303038152906040527f4b5c4277000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506200016a60201b60201c565b5050565b60008151905060006a636f6e736f6c652e6c6f679050602083016000808483855afa5050505050565b600081519050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806200021557607f821691505b6020821081036200022b576200022a620001cd565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b600060088302620002957fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8262000256565b620002a1868362000256565b95508019841693508086168417925050509392505050565b6000819050919050565b6000819050919050565b6000620002ee620002e8620002e284620002b9565b620002c3565b620002b9565b9050919050565b6000819050919050565b6200030a83620002cd565b620003226200031982620002f5565b84845462000263565b825550505050565b600090565b620003396200032a565b62000346818484620002ff565b505050565b5b818110156200036e57620003626000826200032f565b6001810190506200034c565b5050565b601f821115620003bd57620003878162000231565b620003928462000246565b81016020851015620003a2578190505b620003ba620003b18562000246565b8301826200034b565b50505b505050565b600082821c905092915050565b6000620003e260001984600802620003c2565b1980831691505092915050565b6000620003fd8383620003cf565b9150826002028217905092915050565b620004188262000193565b67ffffffffffffffff8111156200043457620004336200019e565b5b620004408254620001fc565b6200044d82828562000372565b600060209050601f83116001811462000485576000841562000470578287015190505b6200047c8582620003ef565b865550620004ec565b601f198416620004958662000231565b60005b82811015620004bf5784890151825560018201915060208501945060208101905062000498565b86831015620004df5784890151620004db601f891682620003cf565b8355505b6001600288020188555050505b505050505050565b600082825260208201905092915050565b60005b838110156200052557808201518184015260208101905062000508565b60008484015250505050565b6000601f19601f8301169050919050565b60006200054f8262000193565b6200055b8185620004f4565b93506200056d81856020860162000505565b620005788162000531565b840191505092915050565b600060408201905081810360008301526200059f818562000542565b90508181036020830152620005b5818462000542565b90509392505050565b610d7180620005ce6000396000f3fe608060405234801561001057600080fd5b50600436106100575760003560e01c80636250ce311461005c578063a413686214610078578063c3023ff414610094578063cd20c713146100b0578063cfae3217146100e0575b600080fd5b61007660048036038101906100719190610671565b6100fe565b005b610092600480360381019061008d91906107f7565b610183565b005b6100ae60048036038101906100a99190610840565b610243565b005b6100ca60048036038101906100c59190610840565b610315565b6040516100d7919061088f565b60405180910390f35b6100e861033a565b6040516100f59190610929565b60405180910390f35b80600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055505050565b610230604051806060016040528060238152602001610d1960239139600080546101ac9061097a565b80601f01602080910402602001604051908101604052809291908181526020018280546101d89061097a565b80156102255780601f106101fa57610100808354040283529160200191610225565b820191906000526020600020905b81548152906001019060200180831161020857829003601f168201915b505050505083610468565b806000908161023f9190610b57565b5050565b6000600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490506102cd81610507565b60008114610310576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161030790610c75565b60405180910390fd5b505050565b6001602052816000526040600020602052806000526040600020600091509150505481565b6060600080546103499061097a565b80601f01602080910402602001604051908101604052809291908181526020018280546103759061097a565b80156103c25780601f10610397576101008083540402835291602001916103c2565b820191906000526020600020905b8154815290600101906020018083116103a557829003601f168201915b5050505050905090565b61046482826040516024016103e2929190610c95565b6040516020818303038152906040527f4b5c4277000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506105a0565b5050565b61050283838360405160240161048093929190610ccc565b6040516020818303038152906040527f2ced7cef000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506105a0565b505050565b61059d8160405160240161051b919061088f565b6040516020818303038152906040527ff82c50f1000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff83818316178352505050506105a0565b50565b60008151905060006a636f6e736f6c652e6c6f679050602083016000808483855afa5050505050565b6000604051905090565b600080fd5b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610608826105dd565b9050919050565b610618816105fd565b811461062357600080fd5b50565b6000813590506106358161060f565b92915050565b6000819050919050565b61064e8161063b565b811461065957600080fd5b50565b60008135905061066b81610645565b92915050565b60008060408385031215610688576106876105d3565b5b600061069685828601610626565b92505060206106a78582860161065c565b9150509250929050565b600080fd5b600080fd5b6000601f19601f8301169050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b610704826106bb565b810181811067ffffffffffffffff82111715610723576107226106cc565b5b80604052505050565b60006107366105c9565b905061074282826106fb565b919050565b600067ffffffffffffffff821115610762576107616106cc565b5b61076b826106bb565b9050602081019050919050565b82818337600083830152505050565b600061079a61079584610747565b61072c565b9050828152602081018484840111156107b6576107b56106b6565b5b6107c1848285610778565b509392505050565b600082601f8301126107de576107dd6106b1565b5b81356107ee848260208601610787565b91505092915050565b60006020828403121561080d5761080c6105d3565b5b600082013567ffffffffffffffff81111561082b5761082a6105d8565b5b610837848285016107c9565b91505092915050565b60008060408385031215610857576108566105d3565b5b600061086585828601610626565b925050602061087685828601610626565b9150509250929050565b6108898161063b565b82525050565b60006020820190506108a46000830184610880565b92915050565b600081519050919050565b600082825260208201905092915050565b60005b838110156108e45780820151818401526020810190506108c9565b60008484015250505050565b60006108fb826108aa565b61090581856108b5565b93506109158185602086016108c6565b61091e816106bb565b840191505092915050565b6000602082019050818103600083015261094381846108f0565b905092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b6000600282049050600182168061099257607f821691505b6020821081036109a5576109a461094b565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b600060088302610a0d7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff826109d0565b610a1786836109d0565b95508019841693508086168417925050509392505050565b6000819050919050565b6000610a54610a4f610a4a8461063b565b610a2f565b61063b565b9050919050565b6000819050919050565b610a6e83610a39565b610a82610a7a82610a5b565b8484546109dd565b825550505050565b600090565b610a97610a8a565b610aa2818484610a65565b505050565b5b81811015610ac657610abb600082610a8f565b600181019050610aa8565b5050565b601f821115610b0b57610adc816109ab565b610ae5846109c0565b81016020851015610af4578190505b610b08610b00856109c0565b830182610aa7565b50505b505050565b600082821c905092915050565b6000610b2e60001984600802610b10565b1980831691505092915050565b6000610b478383610b1d565b9150826002028217905092915050565b610b60826108aa565b67ffffffffffffffff811115610b7957610b786106cc565b5b610b83825461097a565b610b8e828285610aca565b600060209050601f831160018114610bc15760008415610baf578287015190505b610bb98582610b3b565b865550610c21565b601f198416610bcf866109ab565b60005b82811015610bf757848901518255600182019150602085019450602081019050610bd2565b86831015610c145784890151610c10601f891682610b1d565b8355505b6001600288020188555050505b505050505050565b7f4e6f20476173206c696d69740000000000000000000000000000000000000000600082015250565b6000610c5f600c836108b5565b9150610c6a82610c29565b602082019050919050565b60006020820190508181036000830152610c8e81610c52565b9050919050565b60006040820190508181036000830152610caf81856108f0565b90508181036020830152610cc381846108f0565b90509392505050565b60006060820190508181036000830152610ce681866108f0565b90508181036020830152610cfa81856108f0565b90508181036040830152610d0e81846108f0565b905094935050505056fe4368616e67696e67206772656574696e672066726f6d202725732720746f2027257327a26469706673582212208a9d4f617c5917088dbed912c8d8cb47f8edd6cf21b2c4643b7ead8977183a5964736f6c634300081200334465706c6f79696e67206120477265657465722077697468206772656574696e673a" + +export const GreeterAbi = [ + { + inputs: [], + stateMutability: "nonpayable", + type: "constructor" + }, + { + inputs: [ + { + internalType: "address", + name: "", + type: "address" + }, + { + internalType: "address", + name: "", + type: "address" + } + ], + name: "approvedGasLimit", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256" + } + ], + stateMutability: "view", + type: "function" + }, + { + inputs: [ + { + internalType: "address", + name: "safe", + type: "address" + }, + { + internalType: "address", + name: "token", + type: "address" + } + ], + name: "getApprovedGasLimit", + outputs: [], + stateMutability: "view", + type: "function" + }, + { + inputs: [], + name: "greet", + outputs: [ + { + internalType: "string", + name: "", + type: "string" + } + ], + stateMutability: "view", + type: "function" + }, + { + inputs: [ + { + internalType: "address", + name: "token", + type: "address" + }, + { + internalType: "uint256", + name: "gasLimit", + type: "uint256" + } + ], + name: "setApprovedGasLimit", + outputs: [], + stateMutability: "nonpayable", + type: "function" + }, + { + inputs: [ + { + internalType: "string", + name: "_greeting", + type: "string" + } + ], + name: "setGreeting", + outputs: [], + stateMutability: "nonpayable", + type: "function" + } +] diff --git a/test/bundlerActions.test.ts b/test/bundlerActions.test.ts index 17065406..586bd952 100644 --- a/test/bundlerActions.test.ts +++ b/test/bundlerActions.test.ts @@ -1,20 +1,35 @@ +import { beforeAll, beforeEach, describe, expect, test } from "bun:test" import dotenv from "dotenv" -import { BundlerClient, WaitForUserOperationReceiptTimeoutError } from "permissionless" +import { + BundlerClient, + WaitForUserOperationReceiptTimeoutError +} from "permissionless" import { getUserOperationHash } from "permissionless/utils" import { http, Address, Hex } from "viem" -import { buildUserOp } from "./userOp" -import { getBundlerClient, getEntryPoint, getEoaWalletClient, getPublicClient, getTestingChain } from "./utils" -import { beforeAll, beforeEach, describe, expect, test } from "bun:test" +import { buildUserOp } from "./userOp.js" +import { + getBundlerClient, + getEntryPoint, + getEoaWalletClient, + getPublicClient, + getTestingChain +} from "./utils.js" dotenv.config() beforeAll(() => { - if (!process.env.PIMLICO_API_KEY) throw new Error("PIMLICO_API_KEY environment variable not set") - if (!process.env.STACKUP_API_KEY) throw new Error("STACKUP_API_KEY environment variable not set") - if (!process.env.FACTORY_ADDRESS) throw new Error("FACTORY_ADDRESS environment variable not set") - if (!process.env.TEST_PRIVATE_KEY) throw new Error("TEST_PRIVATE_KEY environment variable not set") - if (!process.env.RPC_URL) throw new Error("RPC_URL environment variable not set") - if (!process.env.ENTRYPOINT_ADDRESS) throw new Error("ENTRYPOINT_ADDRESS environment variable not set") + if (!process.env.PIMLICO_API_KEY) + throw new Error("PIMLICO_API_KEY environment variable not set") + if (!process.env.STACKUP_API_KEY) + throw new Error("STACKUP_API_KEY environment variable not set") + if (!process.env.FACTORY_ADDRESS) + throw new Error("FACTORY_ADDRESS environment variable not set") + if (!process.env.TEST_PRIVATE_KEY) + throw new Error("TEST_PRIVATE_KEY environment variable not set") + if (!process.env.RPC_URL) + throw new Error("RPC_URL environment variable not set") + if (!process.env.ENTRYPOINT_ADDRESS) + throw new Error("ENTRYPOINT_ADDRESS environment variable not set") }) describe("BUNDLER ACTIONS", () => { @@ -43,17 +58,8 @@ describe("BUNDLER ACTIONS", () => { test("Estimate user operation gas", async () => { const eoaWalletClient = getEoaWalletClient() - const publicClient = await getPublicClient() - const { maxFeePerGas, maxPriorityFeePerGas } = await publicClient.estimateFeesPerGas() - - const userOperation = { - ...(await buildUserOp(eoaWalletClient)), - maxFeePerGas: maxFeePerGas || 0n, - maxPriorityFeePerGas: maxPriorityFeePerGas || 0n, - callGasLimit: 0n, - verificationGasLimit: 0n, - preVerificationGas: 0n - } + + const userOperation = await buildUserOp(eoaWalletClient) const gasParameters = await bundlerClient.estimateUserOperationGas({ userOperation, @@ -67,17 +73,7 @@ describe("BUNDLER ACTIONS", () => { test("Sending user operation", async () => { const eoaWalletClient = getEoaWalletClient() - const publicClient = await getPublicClient() - const { maxFeePerGas, maxPriorityFeePerGas } = await publicClient.estimateFeesPerGas() - - const userOperation = { - ...(await buildUserOp(eoaWalletClient)), - maxFeePerGas: maxFeePerGas || 0n, - maxPriorityFeePerGas: maxPriorityFeePerGas || 0n, - callGasLimit: 0n, - verificationGasLimit: 0n, - preVerificationGas: 0n - } + const userOperation = await buildUserOp(eoaWalletClient) const entryPoint = getEntryPoint() const chain = getTestingChain() @@ -93,7 +89,13 @@ describe("BUNDLER ACTIONS", () => { userOperation.signature = await eoaWalletClient.signMessage({ account: eoaWalletClient.account, - message: { raw: getUserOperationHash({ userOperation, entryPoint, chainId: chain.id }) } + message: { + raw: getUserOperationHash({ + userOperation, + entryPoint, + chainId: chain.id + }) + } }) const userOpHash = await bundlerClient.sendUserOperation({ @@ -104,38 +106,38 @@ describe("BUNDLER ACTIONS", () => { expect(userOpHash).toBeString() expect(userOpHash).toStartWith("0x") - const userOperationReceipt = await bundlerClient.waitForUserOperationReceipt({ hash: userOpHash }) + const userOperationReceipt = + await bundlerClient.waitForUserOperationReceipt({ + hash: userOpHash + }) expect(userOperationReceipt).not.toBeNull() expect(userOperationReceipt?.userOpHash).toBe(userOpHash) expect(userOperationReceipt?.receipt.transactionHash).not.toBeEmpty() expect(userOperationReceipt?.receipt.transactionHash).not.toBeNull() - expect(userOperationReceipt?.receipt.transactionHash).not.toBeUndefined() + expect( + userOperationReceipt?.receipt.transactionHash + ).not.toBeUndefined() - const userOperationFromUserOpHash = await bundlerClient.getUserOperationByHash({ hash: userOpHash }) + const userOperationFromUserOpHash = + await bundlerClient.getUserOperationByHash({ hash: userOpHash }) expect(userOperationFromUserOpHash).not.toBeNull() expect(userOperationFromUserOpHash?.entryPoint).toBe(entryPoint) - expect(userOperationFromUserOpHash?.transactionHash).toBe(userOperationReceipt?.receipt.transactionHash) + expect(userOperationFromUserOpHash?.transactionHash).toBe( + userOperationReceipt?.receipt.transactionHash + ) for (const key in userOperationFromUserOpHash?.userOperation) { - expect(userOperationFromUserOpHash?.userOperation[key]).toBe(userOperation[key]) + expect(userOperationFromUserOpHash?.userOperation[key]).toBe( + userOperation[key] + ) } }, 100000) test("wait for user operation receipt fail", async () => { const eoaWalletClient = getEoaWalletClient() - const publicClient = await getPublicClient() - const { maxFeePerGas, maxPriorityFeePerGas } = await publicClient.estimateFeesPerGas() - - const userOperation = { - ...(await buildUserOp(eoaWalletClient)), - maxFeePerGas: maxFeePerGas || 0n, - maxPriorityFeePerGas: maxPriorityFeePerGas || 0n, - callGasLimit: 0n, - verificationGasLimit: 0n, - preVerificationGas: 0n - } + const userOperation = await buildUserOp(eoaWalletClient) const entryPoint = getEntryPoint() const chain = getTestingChain() @@ -149,10 +151,19 @@ describe("BUNDLER ACTIONS", () => { userOperation.verificationGasLimit = gasParameters.verificationGasLimit userOperation.preVerificationGas = gasParameters.preVerificationGas - const userOpHash = getUserOperationHash({ userOperation, entryPoint, chainId: chain.id }) + const userOpHash = getUserOperationHash({ + userOperation, + entryPoint, + chainId: chain.id + }) - await expect(async () => { - await bundlerClient.waitForUserOperationReceipt({ hash: userOpHash, timeout: 100 }) - }).toThrow(new WaitForUserOperationReceiptTimeoutError({ hash: userOpHash })) + expect(async () => { + await bundlerClient.waitForUserOperationReceipt({ + hash: userOpHash, + timeout: 100 + }) + }).toThrow( + new WaitForUserOperationReceiptTimeoutError({ hash: userOpHash }) + ) }) }) diff --git a/test/index.test.ts b/test/index.test.ts index bbaf83b1..0099a9bb 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,9 +1,12 @@ +import { beforeAll, describe, expect, test } from "bun:test" import dotenv from "dotenv" -import { UserOperation, createBundlerClient, getSenderAddress, getUserOperationHash } from "permissionless" -import { signUserOperationHashWithECDSA } from "permissionless" -import { InvalidEntryPointError } from "permissionless/actions" -import { http } from "viem" -import { buildUserOp, getAccountInitCode } from "./userOp" +import { + UserOperation, + getSenderAddress, + getUserOperationHash +} from "permissionless" +import { signUserOperationHashWithECDSA } from "permissionless/utils" +import { buildUserOp, getAccountInitCode } from "./userOp.js" import { getBundlerClient, getEntryPoint, @@ -12,19 +15,24 @@ import { getPrivateKeyAccount, getPublicClient, getTestingChain -} from "./utils" -import { beforeAll, describe, expect, test } from "bun:test" +} from "./utils.js" dotenv.config() const pimlicoApiKey = process.env.PIMLICO_API_KEY beforeAll(() => { - if (!process.env.PIMLICO_API_KEY) throw new Error("PIMLICO_API_KEY environment variable not set") - if (!process.env.STACKUP_API_KEY) throw new Error("STACKUP_API_KEY environment variable not set") - if (!process.env.FACTORY_ADDRESS) throw new Error("FACTORY_ADDRESS environment variable not set") - if (!process.env.TEST_PRIVATE_KEY) throw new Error("TEST_PRIVATE_KEY environment variable not set") - if (!process.env.RPC_URL) throw new Error("RPC_URL environment variable not set") - if (!process.env.ENTRYPOINT_ADDRESS) throw new Error("ENTRYPOINT_ADDRESS environment variable not set") + if (!process.env.PIMLICO_API_KEY) + throw new Error("PIMLICO_API_KEY environment variable not set") + if (!process.env.STACKUP_API_KEY) + throw new Error("STACKUP_API_KEY environment variable not set") + if (!process.env.FACTORY_ADDRESS) + throw new Error("FACTORY_ADDRESS environment variable not set") + if (!process.env.TEST_PRIVATE_KEY) + throw new Error("TEST_PRIVATE_KEY environment variable not set") + if (!process.env.RPC_URL) + throw new Error("RPC_URL environment variable not set") + if (!process.env.ENTRYPOINT_ADDRESS) + throw new Error("ENTRYPOINT_ADDRESS environment variable not set") }) describe("test public actions and utils", () => { @@ -32,7 +40,10 @@ describe("test public actions and utils", () => { const eoaWalletClient = getEoaWalletClient() const factoryAddress = getFactoryAddress() - const initCode = await getAccountInitCode(factoryAddress, eoaWalletClient) + const initCode = await getAccountInitCode( + factoryAddress, + eoaWalletClient + ) const publicClient = await getPublicClient() const entryPoint = getEntryPoint() @@ -51,7 +62,10 @@ describe("test public actions and utils", () => { const eoaWalletClient = getEoaWalletClient() const factoryAddress = getFactoryAddress() - const initCode = await getAccountInitCode(factoryAddress, eoaWalletClient) + const initCode = await getAccountInitCode( + factoryAddress, + eoaWalletClient + ) const publicClient = await getPublicClient() const entryPoint = "0x0000000" @@ -65,21 +79,10 @@ describe("test public actions and utils", () => { test("getUserOperationHash", async () => { const eoaWalletClient = getEoaWalletClient() - const publicClient = await getPublicClient() const chain = getTestingChain() const entryPoint = getEntryPoint() const bundlerClient = getBundlerClient() - - const { maxFeePerGas, maxPriorityFeePerGas } = await publicClient.estimateFeesPerGas() - - const userOperation = { - ...(await buildUserOp(eoaWalletClient)), - maxFeePerGas: maxFeePerGas || 0n, - maxPriorityFeePerGas: maxPriorityFeePerGas || 0n, - callGasLimit: 0n, - verificationGasLimit: 0n, - preVerificationGas: 0n - } + const userOperation = await buildUserOp(eoaWalletClient) const gasParameters = await bundlerClient.estimateUserOperationGas({ userOperation, @@ -90,7 +93,11 @@ describe("test public actions and utils", () => { userOperation.verificationGasLimit = gasParameters.verificationGasLimit userOperation.preVerificationGas = gasParameters.preVerificationGas - const userOpHash = getUserOperationHash({ userOperation, entryPoint, chainId: chain.id }) + const userOpHash = getUserOperationHash({ + userOperation, + entryPoint, + chainId: chain.id + }) expect(userOpHash).toBeString() expect(userOpHash).toStartWith("0x") @@ -99,17 +106,7 @@ describe("test public actions and utils", () => { test("signUserOperationHashWithECDSA", async () => { const bundlerClient = getBundlerClient() const eoaWalletClient = getEoaWalletClient() - const publicClient = await getPublicClient() - const { maxFeePerGas, maxPriorityFeePerGas } = await publicClient.estimateFeesPerGas() - - const userOperation: UserOperation = { - ...(await buildUserOp(eoaWalletClient)), - maxFeePerGas: maxFeePerGas || 0n, - maxPriorityFeePerGas: maxPriorityFeePerGas || 0n, - callGasLimit: 0n, - verificationGasLimit: 0n, - preVerificationGas: 0n - } + const userOperation = await buildUserOp(eoaWalletClient) const entryPoint = getEntryPoint() const chain = getTestingChain() @@ -123,7 +120,11 @@ describe("test public actions and utils", () => { userOperation.verificationGasLimit = gasParameters.verificationGasLimit userOperation.preVerificationGas = gasParameters.preVerificationGas - const userOpHash = getUserOperationHash({ userOperation, entryPoint, chainId: chain.id }) + const userOpHash = getUserOperationHash({ + userOperation, + entryPoint, + chainId: chain.id + }) userOperation.signature = await signUserOperationHashWithECDSA({ client: eoaWalletClient, @@ -136,9 +137,15 @@ describe("test public actions and utils", () => { expect(userOperation.signature).toBeString() expect(userOperation.signature).toStartWith("0x") - const signature = await signUserOperationHashWithECDSA({ client: eoaWalletClient, hash: userOpHash }) + const signature = await signUserOperationHashWithECDSA({ + client: eoaWalletClient, + hash: userOpHash + }) - await signUserOperationHashWithECDSA({ account: eoaWalletClient.account, hash: userOpHash }) + await signUserOperationHashWithECDSA({ + account: eoaWalletClient.account, + hash: userOpHash + }) await signUserOperationHashWithECDSA({ account: eoaWalletClient.account, diff --git a/test/package.json b/test/package.json index 2b4356cb..df7cd408 100644 --- a/test/package.json +++ b/test/package.json @@ -2,8 +2,11 @@ "name": "test", "private": true, "type": "module", + "devDependencies": { + "bun-types": "^1.0.7" + }, "dependencies": { "dotenv": "^16.3.1", - "viem": "1.14.0" + "viem": "^1.14.0" } } diff --git a/test/pimlicoActions.test.ts b/test/pimlicoActions.test.ts index 4b61fc85..0f5bafe9 100644 --- a/test/pimlicoActions.test.ts +++ b/test/pimlicoActions.test.ts @@ -1,3 +1,4 @@ +import { beforeAll, beforeEach, describe, expect, test } from "bun:test" import dotenv from "dotenv" import { PimlicoBundlerClient, @@ -5,9 +6,10 @@ import { createPimlicoBundlerClient, createPimlicoPaymasterClient } from "permissionless/clients/pimlico" +import { UserOperation } from "permissionless/index.js" import { getUserOperationHash } from "permissionless/utils" import { http } from "viem" -import { buildUserOp } from "./userOp" +import { buildUserOp } from "./userOp.js" import { getEntryPoint, getEoaWalletClient, @@ -15,18 +17,23 @@ import { getPimlicoPaymasterClient, getPublicClient, getTestingChain -} from "./utils" -import { beforeAll, beforeEach, describe, expect, test } from "bun:test" +} from "./utils.js" dotenv.config() beforeAll(() => { - if (!process.env.PIMLICO_API_KEY) throw new Error("PIMLICO_API_KEY environment variable not set") - if (!process.env.STACKUP_API_KEY) throw new Error("STACKUP_API_KEY environment variable not set") - if (!process.env.FACTORY_ADDRESS) throw new Error("FACTORY_ADDRESS environment variable not set") - if (!process.env.TEST_PRIVATE_KEY) throw new Error("TEST_PRIVATE_KEY environment variable not set") - if (!process.env.RPC_URL) throw new Error("RPC_URL environment variable not set") - if (!process.env.ENTRYPOINT_ADDRESS) throw new Error("ENTRYPOINT_ADDRESS environment variable not set") + if (!process.env.PIMLICO_API_KEY) + throw new Error("PIMLICO_API_KEY environment variable not set") + if (!process.env.STACKUP_API_KEY) + throw new Error("STACKUP_API_KEY environment variable not set") + if (!process.env.FACTORY_ADDRESS) + throw new Error("FACTORY_ADDRESS environment variable not set") + if (!process.env.TEST_PRIVATE_KEY) + throw new Error("TEST_PRIVATE_KEY environment variable not set") + if (!process.env.RPC_URL) + throw new Error("RPC_URL environment variable not set") + if (!process.env.ENTRYPOINT_ADDRESS) + throw new Error("ENTRYPOINT_ADDRESS environment variable not set") }) const pimlicoApiKey = process.env.PIMLICO_API_KEY @@ -42,7 +49,8 @@ describe("Pimlico Actions tests", () => { describe("Pimlico Bundler actions", () => { test("fetch gas price", async () => { - const gasPrice = await pimlicoBundlerClient.getUserOperationGasPrice() + const gasPrice = + await pimlicoBundlerClient.getUserOperationGasPrice() expect(gasPrice).not.toBeUndefined() expect(gasPrice).not.toBeNull() @@ -60,19 +68,25 @@ describe("Pimlico Actions tests", () => { expect(gasPrice.slow.maxFeePerGas).toBeGreaterThan(BigInt(0)) expect(typeof gasPrice.slow.maxPriorityFeePerGas).toBe("bigint") - expect(gasPrice.slow.maxPriorityFeePerGas).toBeGreaterThan(BigInt(0)) + expect(gasPrice.slow.maxPriorityFeePerGas).toBeGreaterThan( + BigInt(0) + ) expect(typeof gasPrice.standard.maxFeePerGas).toBe("bigint") expect(gasPrice.standard.maxFeePerGas).toBeGreaterThan(BigInt(0)) expect(typeof gasPrice.standard.maxPriorityFeePerGas).toBe("bigint") - expect(gasPrice.standard.maxPriorityFeePerGas).toBeGreaterThan(BigInt(0)) + expect(gasPrice.standard.maxPriorityFeePerGas).toBeGreaterThan( + BigInt(0) + ) expect(typeof gasPrice.fast.maxFeePerGas).toBe("bigint") expect(gasPrice.fast.maxFeePerGas).toBeGreaterThan(BigInt(0)) expect(typeof gasPrice.fast.maxPriorityFeePerGas).toBe("bigint") - expect(gasPrice.fast.maxPriorityFeePerGas).toBeGreaterThan(BigInt(0)) + expect(gasPrice.fast.maxPriorityFeePerGas).toBeGreaterThan( + BigInt(0) + ) }) test("fetch user operation status", async () => {}) @@ -82,10 +96,13 @@ describe("Pimlico Actions tests", () => { test("Fetching paymaster and data", async () => { const eoaWalletClient = getEoaWalletClient() const publicClient = await getPublicClient() - const { maxFeePerGas, maxPriorityFeePerGas } = await publicClient.estimateFeesPerGas() + const { maxFeePerGas, maxPriorityFeePerGas } = + await publicClient.estimateFeesPerGas() - const userOperation = { - ...(await buildUserOp(eoaWalletClient)), + const partialUserOp = await buildUserOp(eoaWalletClient) + + const userOperation: UserOperation = { + ...partialUserOp, maxFeePerGas: maxFeePerGas || 0n, maxPriorityFeePerGas: maxPriorityFeePerGas || 0n, callGasLimit: 0n, @@ -95,57 +112,73 @@ describe("Pimlico Actions tests", () => { const entryPoint = getEntryPoint() - const sponsorUserOperationPaymasterAndData = await pimlicoPaymasterClient.sponsorUserOperation({ - userOperation: userOperation, - entryPoint: entryPoint - }) + const sponsorUserOperationPaymasterAndData = + await pimlicoPaymasterClient.sponsorUserOperation({ + userOperation: userOperation, + entryPoint: entryPoint + }) expect(sponsorUserOperationPaymasterAndData).not.toBeNull() expect(sponsorUserOperationPaymasterAndData).not.toBeUndefined() expect(sponsorUserOperationPaymasterAndData).not.toBeUndefined() - expect(typeof sponsorUserOperationPaymasterAndData.callGasLimit).toBe("bigint") - expect(sponsorUserOperationPaymasterAndData.callGasLimit).toBeGreaterThan(BigInt(0)) - - expect(typeof sponsorUserOperationPaymasterAndData.preVerificationGas).toBe("bigint") - expect(sponsorUserOperationPaymasterAndData.preVerificationGas).toBeGreaterThan(BigInt(0)) - - expect(typeof sponsorUserOperationPaymasterAndData.verificationGasLimit).toBe("bigint") - expect(sponsorUserOperationPaymasterAndData.verificationGasLimit).toBeGreaterThan(BigInt(0)) - - expect(sponsorUserOperationPaymasterAndData.paymasterAndData).not.toBeEmpty() - expect(sponsorUserOperationPaymasterAndData.paymasterAndData).toStartWith("0x") - }) + expect( + typeof sponsorUserOperationPaymasterAndData.callGasLimit + ).toBe("bigint") + expect( + sponsorUserOperationPaymasterAndData.callGasLimit + ).toBeGreaterThan(BigInt(0)) + + expect( + typeof sponsorUserOperationPaymasterAndData.preVerificationGas + ).toBe("bigint") + expect( + sponsorUserOperationPaymasterAndData.preVerificationGas + ).toBeGreaterThan(BigInt(0)) + + expect( + typeof sponsorUserOperationPaymasterAndData.verificationGasLimit + ).toBe("bigint") + expect( + sponsorUserOperationPaymasterAndData.verificationGasLimit + ).toBeGreaterThan(BigInt(0)) + + expect( + sponsorUserOperationPaymasterAndData.paymasterAndData + ).not.toBeEmpty() + expect( + sponsorUserOperationPaymasterAndData.paymasterAndData + ).toStartWith("0x") + }, 100000) test("Sending user op with paymaster and data", async () => { const entryPoint = getEntryPoint() const eoaWalletClient = getEoaWalletClient() const chain = getTestingChain() - const publicClient = await getPublicClient() - const { maxFeePerGas, maxPriorityFeePerGas } = await publicClient.estimateFeesPerGas() - - const userOperation = { - ...(await buildUserOp(eoaWalletClient)), - maxFeePerGas: maxFeePerGas || 0n, - maxPriorityFeePerGas: maxPriorityFeePerGas || 0n, - callGasLimit: 0n, - verificationGasLimit: 0n, - preVerificationGas: 0n - } - - const sponsorUserOperationPaymasterAndData = await pimlicoPaymasterClient.sponsorUserOperation({ - userOperation: userOperation, - entryPoint: entryPoint + const userOperation = await buildUserOp(eoaWalletClient) + + const sponsorUserOperationPaymasterAndData = + await pimlicoPaymasterClient.sponsorUserOperation({ + userOperation: userOperation, + entryPoint: entryPoint + }) + + userOperation.paymasterAndData = + sponsorUserOperationPaymasterAndData.paymasterAndData + userOperation.callGasLimit = + sponsorUserOperationPaymasterAndData.callGasLimit + userOperation.verificationGasLimit = + sponsorUserOperationPaymasterAndData.verificationGasLimit + userOperation.preVerificationGas = + sponsorUserOperationPaymasterAndData.preVerificationGas + + const userOperationHash = getUserOperationHash({ + userOperation, + entryPoint, + chainId: chain.id }) - userOperation.paymasterAndData = sponsorUserOperationPaymasterAndData.paymasterAndData - userOperation.callGasLimit = sponsorUserOperationPaymasterAndData.callGasLimit - userOperation.verificationGasLimit = sponsorUserOperationPaymasterAndData.verificationGasLimit - userOperation.preVerificationGas = sponsorUserOperationPaymasterAndData.preVerificationGas - - const userOperationHash = getUserOperationHash({ userOperation, entryPoint, chainId: chain.id }) - userOperation.signature = await eoaWalletClient.signMessage({ account: eoaWalletClient.account, message: { raw: userOperationHash } @@ -159,35 +192,48 @@ describe("Pimlico Actions tests", () => { expect(userOpHash).toBeString() expect(userOpHash).toStartWith("0x") - const userOperationReceipt = await pimlicoBundlerClient.waitForUserOperationReceipt({ - hash: userOpHash - }) + const userOperationReceipt = + await pimlicoBundlerClient.waitForUserOperationReceipt({ + hash: userOpHash + }) expect(userOperationReceipt).not.toBeNull() expect(userOperationReceipt?.userOpHash).toBe(userOpHash) - expect(userOperationReceipt?.receipt.transactionHash).not.toBeEmpty() + expect( + userOperationReceipt?.receipt.transactionHash + ).not.toBeEmpty() expect(userOperationReceipt?.receipt.transactionHash).not.toBeNull() - expect(userOperationReceipt?.receipt.transactionHash).not.toBeUndefined() + expect( + userOperationReceipt?.receipt.transactionHash + ).not.toBeUndefined() - const userOperationFromUserOpHash = await pimlicoBundlerClient.getUserOperationByHash({ hash: userOpHash }) + const userOperationFromUserOpHash = + await pimlicoBundlerClient.getUserOperationByHash({ + hash: userOpHash + }) expect(userOperationFromUserOpHash).not.toBeNull() expect(userOperationFromUserOpHash?.entryPoint).toBe(entryPoint) - expect(userOperationFromUserOpHash?.transactionHash).toBe(userOperationReceipt?.receipt.transactionHash) + expect(userOperationFromUserOpHash?.transactionHash).toBe( + userOperationReceipt?.receipt.transactionHash + ) // for (const key in userOperationFromUserOpHash?.userOperation) { // expect(userOperationFromUserOpHash?.userOperation[key]).toBe(userOperation[key]) // } - const userOperationStatus = await pimlicoBundlerClient.getUserOperationStatus({ - hash: userOpHash - }) + const userOperationStatus = + await pimlicoBundlerClient.getUserOperationStatus({ + hash: userOpHash + }) expect(userOperationStatus).not.toBeNull() expect(userOperationStatus).not.toBeUndefined() expect(userOperationStatus.status).toBe("included") - expect(userOperationStatus.transactionHash).toBe(userOperationReceipt?.receipt.transactionHash) + expect(userOperationStatus.transactionHash).toBe( + userOperationReceipt?.receipt.transactionHash + ) }, 100000) }) }) diff --git a/test/simpleAccount.test.ts b/test/simpleAccount.test.ts new file mode 100644 index 00000000..d6561502 --- /dev/null +++ b/test/simpleAccount.test.ts @@ -0,0 +1,221 @@ +import { beforeAll, describe, expect, test } from "bun:test" +import dotenv from "dotenv" +import { SignTransactionNotSupportedBySmartAccount } from "permissionless/accounts" +import { UserOperation } from "permissionless/index.js" +import { + Address, + Client, + Hex, + Transport, + decodeEventLog, + getContract, + zeroAddress +} from "viem" +import { EntryPointAbi } from "./abis/EntryPoint.js" +import { GreeterAbi, GreeterBytecode } from "./abis/Greeter.js" +import { + getBundlerClient, + getEntryPoint, + getPimlicoPaymasterClient, + getPrivateKeyToSimpleSmartAccount, + getPublicClient, + getSmartAccountClient, + getTestingChain +} from "./utils.js" + +dotenv.config() + +let testPrivateKey: Hex +let factoryAddress: Address + +beforeAll(() => { + if (!process.env.PIMLICO_API_KEY) { + throw new Error("PIMLICO_API_KEY environment variable not set") + } + if (!process.env.STACKUP_API_KEY) { + throw new Error("STACKUP_API_KEY environment variable not set") + } + if (!process.env.FACTORY_ADDRESS) { + throw new Error("FACTORY_ADDRESS environment variable not set") + } + if (!process.env.TEST_PRIVATE_KEY) { + throw new Error("TEST_PRIVATE_KEY environment variable not set") + } + if (!process.env.RPC_URL) { + throw new Error("RPC_URL environment variable not set") + } + if (!process.env.ENTRYPOINT_ADDRESS) { + throw new Error("ENTRYPOINT_ADDRESS environment variable not set") + } + + if (!process.env.GREETER_ADDRESS) { + throw new Error("ENTRYPOINT_ADDRESS environment variable not set") + } + testPrivateKey = process.env.TEST_PRIVATE_KEY as Hex + factoryAddress = process.env.FACTORY_ADDRESS as Address +}) + +describe("Simple Account", () => { + test("Simple Account address", async () => { + const simpleSmartAccount = await getPrivateKeyToSimpleSmartAccount() + + expect(simpleSmartAccount.address).toBeString() + expect(simpleSmartAccount.address).toHaveLength(42) + expect(simpleSmartAccount.address).toMatch(/^0x[0-9a-fA-F]{40}$/) + + expect(async () => { + await simpleSmartAccount.signTransaction({ + to: zeroAddress, + value: 0n, + data: "0x" + }) + }).toThrow(new SignTransactionNotSupportedBySmartAccount()) + }) + + test("Smart account client signMessage", async () => { + const smartAccountClient = await getSmartAccountClient() + + const response = await smartAccountClient.signMessage({ + message: "hello world" + }) + + expect(response).toBeString() + expect(response).toHaveLength(132) + expect(response).toMatch(/^0x[0-9a-fA-F]{130}$/) + }) + + test("Smart account client signTypedData", async () => { + const smartAccountClient = await getSmartAccountClient() + + const response = await smartAccountClient.signTypedData({ + domain: { + chainId: 1, + name: "Test", + verifyingContract: zeroAddress + }, + primaryType: "Test", + types: { + Test: [ + { + name: "test", + type: "string" + } + ] + }, + message: { + test: "hello world" + } + }) + + expect(response).toBeString() + expect(response).toHaveLength(132) + expect(response).toMatch(/^0x[0-9a-fA-F]{130}$/) + }) + + test("smart account client deploy contract", async () => { + const smartAccountClient = await getSmartAccountClient() + + expect(async () => { + await smartAccountClient.deployContract({ + abi: GreeterAbi, + bytecode: GreeterBytecode + }) + }).toThrow("Simple account doesn't support account deployment") + }) + + test("Smart account write contract", async () => { + const greeterContract = getContract({ + abi: GreeterAbi, + address: process.env.GREETER_ADDRESS as Address, + publicClient: await getPublicClient(), + walletClient: await getSmartAccountClient() + }) + + const oldGreet = await greeterContract.read.greet() + + expect(oldGreet).toBeString() + + const txHash = await greeterContract.write.setGreeting(["hello world"]) + + expect(txHash).toBeString() + expect(txHash).toHaveLength(66) + + const newGreet = await greeterContract.read.greet() + + expect(newGreet).toBeString() + expect(newGreet).toEqual("hello world") + }, 1000000) + + test("Smart account client send transaction", async () => { + const smartAccountClient = await getSmartAccountClient() + const response = await smartAccountClient.sendTransaction({ + to: zeroAddress, + value: 0n, + data: "0x" + }) + expect(response).toBeString() + expect(response).toHaveLength(66) + expect(response).toMatch(/^0x[0-9a-fA-F]{64}$/) + }, 1000000) + + test("smart account client send Transaction 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.sendTransaction({ + to: zeroAddress, + value: 0n, + data: "0x" + }) + + 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() + }, 1000000) +}) diff --git a/test/stackupActions.test.ts b/test/stackupActions.test.ts index 11eeef85..2d60e05d 100644 --- a/test/stackupActions.test.ts +++ b/test/stackupActions.test.ts @@ -2,51 +2,51 @@ import { BundlerClient, UserOperation } from "permissionless" import { StackupPaymasterClient } from "permissionless/clients/stackup" import { getUserOperationHash } from "permissionless/utils" import { Address } from "viem" -import { buildUserOp } from "./userOp" -import { getEntryPoint, getEoaWalletClient, getPublicClient, getTestingChain } from "./utils" +import { buildUserOp } from "./userOp.js" +import { + getEntryPoint, + getEoaWalletClient, + getPublicClient, + getTestingChain +} from "./utils.js" -export const testStackupBundlerActions = async (stackupBundlerClient: StackupPaymasterClient) => { +export const testStackupBundlerActions = async ( + stackupBundlerClient: StackupPaymasterClient +) => { const entryPoint = getEntryPoint() const chain = getTestingChain() - console.log("STACKUP ACTIONS:: ======= TESTING STACKUP PAYMASTER ACTIONS =======") - const supportedPaymasters = await stackupBundlerClient.accounts({ entryPoint }) - console.log("PAYMASTER ADDRESSES: ", supportedPaymasters) + const supportedPaymasters = await stackupBundlerClient.accounts({ + entryPoint + }) const eoaWalletClient = getEoaWalletClient() - const publicClient = await getPublicClient() + const userOperation = await buildUserOp(eoaWalletClient) - const { maxFeePerGas, maxPriorityFeePerGas } = await publicClient.estimateFeesPerGas() + const sponsorUserOperationPaymasterAndData = + await stackupBundlerClient.sponsorUserOperation({ + userOperation: userOperation, + entryPoint: entryPoint as Address, + context: { + type: "payg" + } + }) - const userOperation: UserOperation = { - ...(await buildUserOp(eoaWalletClient)), - maxFeePerGas: maxFeePerGas || 0n, - maxPriorityFeePerGas: maxPriorityFeePerGas || 0n, - paymasterAndData: "0x", - callGasLimit: 0n, - verificationGasLimit: 0n, - preVerificationGas: 0n - } + userOperation.paymasterAndData = + sponsorUserOperationPaymasterAndData.paymasterAndData + userOperation.callGasLimit = + sponsorUserOperationPaymasterAndData.callGasLimit + userOperation.verificationGasLimit = + sponsorUserOperationPaymasterAndData.verificationGasLimit + userOperation.preVerificationGas = + sponsorUserOperationPaymasterAndData.preVerificationGas - const sponsorUserOperationPaymasterAndData = await stackupBundlerClient.sponsorUserOperation({ - userOperation: userOperation, - entryPoint: entryPoint as Address, - context: { - type: "payg" - } + const userOperationHash = getUserOperationHash({ + userOperation, + entryPoint, + chainId: chain.id }) - userOperation.paymasterAndData = sponsorUserOperationPaymasterAndData.paymasterAndData - userOperation.callGasLimit = sponsorUserOperationPaymasterAndData.callGasLimit - userOperation.verificationGasLimit = sponsorUserOperationPaymasterAndData.verificationGasLimit - userOperation.preVerificationGas = sponsorUserOperationPaymasterAndData.preVerificationGas - - console.log(userOperation, "STACKUP ACTIONS:: ============= USER OPERATION =============") - - const userOperationHash = getUserOperationHash({ userOperation, entryPoint, chainId: chain.id }) - - console.log(userOperationHash, "STACKUP ACTIONS:: ============= USER OPERATION HASH =============") - const signedUserOperation: UserOperation = { ...userOperation, signature: await eoaWalletClient.signMessage({ @@ -54,14 +54,12 @@ export const testStackupBundlerActions = async (stackupBundlerClient: StackupPay message: { raw: userOperationHash } }) } - console.log(signedUserOperation, "STACKUP ACTIONS:: ============= SIGNED USER OPERATION HASH =============") const userOpHash = await stackupBundlerClient.sendUserOperation({ userOperation: signedUserOperation, entryPoint: entryPoint as Address }) - console.log("userOpHash", userOpHash) await stackupBundlerClient.waitForUserOperationReceipt({ hash: userOpHash }) diff --git a/test/userOp.ts b/test/userOp.ts index 2135b614..475727f6 100644 --- a/test/userOp.ts +++ b/test/userOp.ts @@ -1,9 +1,26 @@ -import { UserOperation, getAccountNonce, getSenderAddress } from "permissionless" -import { Address, Hex, WalletClient, concatHex, encodeFunctionData, zeroAddress } from "viem" +import { + UserOperation, + getAccountNonce, + getSenderAddress +} from "permissionless" +import { + Address, + Hex, + WalletClient, + concatHex, + encodeFunctionData, + zeroAddress +} from "viem" import { PartialBy } from "viem/types/utils" -import { SimpleAccountAbi } from "./abis/SimpleAccount" -import { SimpleAccountFactoryAbi } from "./abis/SimpleAccountFactory" -import { getDummySignature, getEntryPoint, getFactoryAddress, getPublicClient, isAccountDeployed } from "./utils" +import { SimpleAccountAbi } from "./abis/SimpleAccount.js" +import { SimpleAccountFactoryAbi } from "./abis/SimpleAccountFactory.js" +import { + getDummySignature, + getEntryPoint, + getFactoryAddress, + getPublicClient, + isAccountDeployed +} from "./utils.js" const getInitCode = async (factoryAddress: Address, owner: WalletClient) => { const accountAddress = await getAccountAddress(factoryAddress, owner) @@ -14,7 +31,11 @@ const getInitCode = async (factoryAddress: Address, owner: WalletClient) => { return getAccountInitCode(factoryAddress, owner) } -export const getAccountInitCode = async (factoryAddress: Address, owner: WalletClient, index = 0n): Promise => { +export const getAccountInitCode = async ( + factoryAddress: Address, + owner: WalletClient, + index = 0n +): Promise => { if (!owner.account) throw new Error("Owner account not found") return concatHex([ factoryAddress, @@ -26,7 +47,10 @@ export const getAccountInitCode = async (factoryAddress: Address, owner: WalletC ]) } -const getAccountAddress = async (factoryAddress: Address, owner: WalletClient): Promise
=> { +const getAccountAddress = async ( + factoryAddress: Address, + owner: WalletClient +): Promise
=> { const initCode = await getAccountInitCode(factoryAddress, owner) const publicClient = await getPublicClient() const entryPoint = getEntryPoint() @@ -37,7 +61,11 @@ const getAccountAddress = async (factoryAddress: Address, owner: WalletClient): }) } -const encodeExecute = async (target: Hex, value: bigint, data: Hex): Promise<`0x${string}`> => { +const encodeExecute = async ( + target: Hex, + value: bigint, + data: Hex +): Promise<`0x${string}`> => { return encodeFunctionData({ abi: SimpleAccountAbi, functionName: "execute", @@ -45,7 +73,9 @@ const encodeExecute = async (target: Hex, value: bigint, data: Hex): Promise<`0x }) } -export const buildUserOp = async (eoaWalletClient: WalletClient) => { +export const buildUserOp = async ( + eoaWalletClient: WalletClient +): Promise => { await new Promise((resolve) => { setTimeout(() => { // wait for prev user op to be added to make sure ew get correct nonce @@ -57,7 +87,10 @@ export const buildUserOp = async (eoaWalletClient: WalletClient) => { const publicClient = await getPublicClient() const entryPoint = getEntryPoint() - const accountAddress = await getAccountAddress(factoryAddress, eoaWalletClient) + const accountAddress = await getAccountAddress( + factoryAddress, + eoaWalletClient + ) if (!accountAddress) throw new Error("Account address not found") @@ -66,16 +99,28 @@ export const buildUserOp = async (eoaWalletClient: WalletClient) => { entryPoint: entryPoint }) + const { maxFeePerGas, maxPriorityFeePerGas } = + await publicClient.estimateFeesPerGas() + const userOperation: PartialBy< UserOperation, - "maxFeePerGas" | "maxPriorityFeePerGas" | "callGasLimit" | "verificationGasLimit" | "preVerificationGas" + | "maxFeePerGas" + | "maxPriorityFeePerGas" + | "callGasLimit" + | "verificationGasLimit" + | "preVerificationGas" > = { sender: accountAddress, nonce: nonce, initCode: await getInitCode(factoryAddress, eoaWalletClient), callData: await encodeExecute(zeroAddress as Hex, 0n, "0x" as Hex), paymasterAndData: "0x" as Hex, - signature: getDummySignature() + signature: getDummySignature(), + maxFeePerGas: maxFeePerGas || 0n, + maxPriorityFeePerGas: maxPriorityFeePerGas || 0n, + callGasLimit: 0n, + verificationGasLimit: 0n, + preVerificationGas: 0n } return userOperation diff --git a/test/utils.ts b/test/utils.ts index 4481770a..792803ed 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,17 +1,30 @@ -import { createBundlerClient } from "permissionless" -import { createPimlicoBundlerClient, createPimlicoPaymasterClient } from "permissionless/clients/pimlico" -import { http, Address, Hex, createPublicClient, createWalletClient } from "viem" +import { createBundlerClient, createSmartAccountClient } from "permissionless" +import { privateKeyToSimpleSmartAccount } from "permissionless/accounts" +import { SponsorUserOperationMiddleware } from "permissionless/actions/smartAccount" +import { + createPimlicoBundlerClient, + createPimlicoPaymasterClient +} from "permissionless/clients/pimlico" +import { + http, + Address, + Hex, + createPublicClient, + createWalletClient +} from "viem" import { privateKeyToAccount } from "viem/accounts" import { goerli } from "viem/chains" export const getFactoryAddress = () => { - if (!process.env.FACTORY_ADDRESS) throw new Error("FACTORY_ADDRESS environment variable not set") + if (!process.env.FACTORY_ADDRESS) + throw new Error("FACTORY_ADDRESS environment variable not set") const factoryAddress = process.env.FACTORY_ADDRESS as Address return factoryAddress } export const getPrivateKeyAccount = () => { - if (!process.env.TEST_PRIVATE_KEY) throw new Error("TEST_PRIVATE_KEY environment variable not set") + if (!process.env.TEST_PRIVATE_KEY) + throw new Error("TEST_PRIVATE_KEY environment variable not set") return privateKeyToAccount(process.env.TEST_PRIVATE_KEY as Hex) } @@ -19,6 +32,41 @@ export const getTestingChain = () => { return goerli } +export const getPrivateKeyToSimpleSmartAccount = async () => { + if (!process.env.TEST_PRIVATE_KEY) + throw new Error("TEST_PRIVATE_KEY environment variable not set") + + const publicClient = await getPublicClient() + + return await privateKeyToSimpleSmartAccount(publicClient, { + entryPoint: getEntryPoint(), + factoryAddress: getFactoryAddress(), + privateKey: process.env.TEST_PRIVATE_KEY as Hex + }) +} + +export const getSmartAccountClient = async ({ + sponsorUserOperation +}: SponsorUserOperationMiddleware = {}) => { + if (!process.env.PIMLICO_API_KEY) + throw new Error("PIMLICO_API_KEY environment variable not set") + if (!process.env.PIMLICO_BUNDLER_RPC_HOST) + throw new Error("PIMLICO_BUNDLER_RPC_HOST environment variable not set") + const pimlicoApiKey = process.env.PIMLICO_API_KEY + const chain = getTestingChain() + + return createSmartAccountClient({ + account: await getPrivateKeyToSimpleSmartAccount(), + chain, + transport: http( + `${ + process.env.PIMLICO_BUNDLER_RPC_HOST + }/v1/${chain.name.toLowerCase()}/rpc?apikey=${pimlicoApiKey}` + ), + sponsorUserOperation + }) +} + export const getEoaWalletClient = () => { return createWalletClient({ account: getPrivateKeyAccount(), @@ -28,26 +76,32 @@ export const getEoaWalletClient = () => { } export const getEntryPoint = () => { - if (!process.env.ENTRYPOINT_ADDRESS) throw new Error("ENTRYPOINT_ADDRESS environment variable not set") + if (!process.env.ENTRYPOINT_ADDRESS) + throw new Error("ENTRYPOINT_ADDRESS environment variable not set") return process.env.ENTRYPOINT_ADDRESS as Address } export const getPublicClient = async () => { - if (!process.env.RPC_URL) throw new Error("RPC_URL environment variable not set") + if (!process.env.RPC_URL) + throw new Error("RPC_URL environment variable not set") + const publicClient = createPublicClient({ transport: http(process.env.RPC_URL as string) }) const chainId = await publicClient.getChainId() - if (chainId !== getTestingChain().id) throw new Error("Testing Chain ID not supported by RPC URL") + if (chainId !== getTestingChain().id) + throw new Error("Testing Chain ID not supported by RPC URL") return publicClient } export const getBundlerClient = () => { - if (!process.env.PIMLICO_API_KEY) throw new Error("PIMLICO_API_KEY environment variable not set") - if (!process.env.PIMLICO_BUNDLER_RPC_HOST) throw new Error("PIMLICO_BUNDLER_RPC_HOST environment variable not set") + if (!process.env.PIMLICO_API_KEY) + throw new Error("PIMLICO_API_KEY environment variable not set") + if (!process.env.PIMLICO_BUNDLER_RPC_HOST) + throw new Error("PIMLICO_BUNDLER_RPC_HOST environment variable not set") const pimlicoApiKey = process.env.PIMLICO_API_KEY const chain = getTestingChain() @@ -55,14 +109,18 @@ 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 + }/v1/${chain.name.toLowerCase()}/rpc?apikey=${pimlicoApiKey}` ) }) } export const getPimlicoBundlerClient = () => { - if (!process.env.PIMLICO_BUNDLER_RPC_HOST) throw new Error("PIMLICO_BUNDLER_RPC_HOST environment variable not set") - if (!process.env.PIMLICO_API_KEY) throw new Error("PIMLICO_API_KEY environment variable not set") + if (!process.env.PIMLICO_BUNDLER_RPC_HOST) + throw new Error("PIMLICO_BUNDLER_RPC_HOST environment variable not set") + if (!process.env.PIMLICO_API_KEY) + throw new Error("PIMLICO_API_KEY environment variable not set") const pimlicoApiKey = process.env.PIMLICO_API_KEY const chain = getTestingChain() @@ -70,14 +128,18 @@ 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 + }/v1/${chain.name.toLowerCase()}/rpc?apikey=${pimlicoApiKey}` ) }) } export const getPimlicoPaymasterClient = () => { - if (!process.env.PIMLICO_BUNDLER_RPC_HOST) throw new Error("PIMLICO_BUNDLER_RPC_HOST environment variable not set") - if (!process.env.PIMLICO_API_KEY) throw new Error("PIMLICO_API_KEY environment variable not set") + if (!process.env.PIMLICO_BUNDLER_RPC_HOST) + throw new Error("PIMLICO_BUNDLER_RPC_HOST environment variable not set") + if (!process.env.PIMLICO_API_KEY) + throw new Error("PIMLICO_API_KEY environment variable not set") const pimlicoApiKey = process.env.PIMLICO_API_KEY const chain = getTestingChain() @@ -85,7 +147,9 @@ 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_BUNDLER_RPC_HOST + }/v2/${chain.name.toLowerCase()}/rpc?apikey=${pimlicoApiKey}` ) }) } diff --git a/tsconfig.base.json b/tsconfig.base.json index e74910f3..0ecd2a72 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -35,6 +35,7 @@ "DOM" // We are adding `DOM` here to get the `fetch`, etc. types. This should be removed once these types are available via DefinitelyTyped. ], // Skip type checking for node modules - "skipLibCheck": true + "skipLibCheck": true, + "types": ["bun-types"] } } \ No newline at end of file