From 278b4f0ef59da53053acb8a95bd45200abd9b9d9 Mon Sep 17 00:00:00 2001 From: enis-incentiv Date: Mon, 30 Dec 2024 23:00:50 +0100 Subject: [PATCH] Implement dynamic gas estimation with bundler simulation and signature-aware overhead calculations --- src/BaseAccountAPI.ts | 95 +++++++++++++++++-- src/ERC4337EthersSigner.ts | 28 +++++- src/HttpRpcClient.ts | 46 +++++++-- src/Provider.ts | 8 +- src/SignatureMode.ts | 4 + src/SimpleAccountAPI.ts | 15 +-- src/calcPreVerificationGas.ts | 59 +++++++++--- src/index.ts | 2 + src/utils.ts | 16 ++++ test/0-deterministicDeployer.test.ts | 26 ------ test/1-SimpleAccountAPI.test.ts | 129 -------------------------- test/2-ERC4337EthersProvider.test.ts | 20 ---- test/3-ERC4337EthersSigner.test.ts | 99 -------------------- test/4-calcPreVerificationGas.test.ts | 35 ------- 14 files changed, 236 insertions(+), 346 deletions(-) create mode 100644 src/SignatureMode.ts create mode 100644 src/utils.ts delete mode 100644 test/0-deterministicDeployer.test.ts delete mode 100644 test/1-SimpleAccountAPI.test.ts delete mode 100644 test/2-ERC4337EthersProvider.test.ts delete mode 100644 test/3-ERC4337EthersSigner.test.ts delete mode 100644 test/4-calcPreVerificationGas.test.ts diff --git a/src/BaseAccountAPI.ts b/src/BaseAccountAPI.ts index a924f61..9c5cea1 100644 --- a/src/BaseAccountAPI.ts +++ b/src/BaseAccountAPI.ts @@ -10,6 +10,11 @@ import { resolveProperties } from 'ethers/lib/utils' import { PaymasterAPI } from './PaymasterAPI' import { getUserOpHash, NotPromise, packUserOp } from '@account-abstraction/utils' import { calcPreVerificationGas, GasOverheads } from './calcPreVerificationGas' +import { SignatureMode } from './SignatureMode' +import { HttpRpcClient } from './HttpRpcClient' +import Debug from 'debug' + +const debug = Debug('aa.base') export interface BaseApiParams { provider: Provider @@ -17,6 +22,7 @@ export interface BaseApiParams { accountAddress?: string overheads?: Partial paymasterAPI?: PaymasterAPI + httpRpcClient: HttpRpcClient } export interface UserOpResult { @@ -43,6 +49,8 @@ export abstract class BaseAccountAPI { // entryPoint connected to "zero" address. allowed to make static calls (e.g. to getSenderAddress) private readonly entryPointView: EntryPoint + readonly httpRpcClient: HttpRpcClient + provider: Provider overheads?: Partial entryPointAddress: string @@ -59,6 +67,7 @@ export abstract class BaseAccountAPI { this.entryPointAddress = params.entryPointAddress this.accountAddress = params.accountAddress this.paymasterAPI = params.paymasterAPI + this.httpRpcClient = params.httpRpcClient // factory "connect" define the contract address. the contract "connect" defines the "from" address. this.entryPointView = EntryPoint__factory.connect(params.entryPointAddress, params.provider).connect(ethers.constants.AddressZero) @@ -150,7 +159,9 @@ export abstract class BaseAccountAPI { * NOTE: createUnsignedUserOp will add to this value the cost of creation, if the contract is not yet created. */ async getVerificationGasLimit (): Promise { - return 500000 + const signatureMode = this.getSignatureMode() + // Passkey signatures require more verification gas + return signatureMode === SignatureMode.PASSKEY ? 500000 : 100000 } /** @@ -159,7 +170,12 @@ export abstract class BaseAccountAPI { */ async getPreVerificationGas (userOp: Partial): Promise { const p = await resolveProperties(userOp) - return calcPreVerificationGas(p, this.overheads) + const signatureMode = this.getSignatureMode() + debug('PreVerificationGas using signature mode: %s', signatureMode) + return calcPreVerificationGas(p, { + ...this.overheads, + signatureMode + }) } /** @@ -178,15 +194,74 @@ export abstract class BaseAccountAPI { const value = parseNumber(detailsForUserOp.value) ?? BigNumber.from(0) const callData = await this.encodeExecute(detailsForUserOp.target, value, detailsForUserOp.data) - const callGasLimit = parseNumber(detailsForUserOp.gasLimit) ?? await this.provider.estimateGas({ - from: this.entryPointAddress, - to: this.getAccountAddress(), - data: callData + debug('Starting estimation for: %o', { + type: !detailsForUserOp.data || detailsForUserOp.data === '0x' ? 'Simple Transfer' : 'Contract Interaction', + target: detailsForUserOp.target, + value: value.toString(), + hasCallData: !!detailsForUserOp.data && detailsForUserOp.data !== '0x' + }) + + // Log transaction details in a cleaner format + debug('Transaction details: %o', { + target: detailsForUserOp.target, + value: value.toString(), + data: detailsForUserOp.data === '0x' ? 'none' : 'present', + signatureMode: this.getSignatureMode() + }) + + // If user provided gas limit, use it + if (detailsForUserOp.gasLimit) { + debug('Using provided gas limit: %s', detailsForUserOp.gasLimit.toString()) + return { + callData, + callGasLimit: BigNumber.from(detailsForUserOp.gasLimit) + } + } + + debug('Using bundler estimation...') + const initCode = await this.getInitCode() + const verificationGasLimit = await this.getVerificationGasLimit() + + debug('Preparing bundler estimation: %o', { + signatureMode: this.getSignatureMode(), + requestedVerificationGas: verificationGasLimit.toString() + }) + + const partialOp = { + sender: await this.getAccountAddress(), + nonce: await this.getNonce(), + initCode, + callData, + callGasLimit: 0, + verificationGasLimit, + maxFeePerGas: 0, + maxPriorityFeePerGas: 0, + paymasterAndData: '0x', + signature: '0x' + } + + const bundlerEstimation = await this.httpRpcClient.estimateUserOpGas(partialOp) + + if (!bundlerEstimation.success) { + throw new Error('Bundler gas estimation failed') + } + + debug('Bundler estimation details: %o', { + signatureMode: this.getSignatureMode(), + requestedVerificationGas: verificationGasLimit.toString(), + bundlerVerificationGas: bundlerEstimation.verificationGas.toString(), + callGasLimit: bundlerEstimation.callGasLimit.toString(), + preVerificationGas: bundlerEstimation.preVerificationGas.toString(), + totalGas: ( + Number(bundlerEstimation.verificationGas) + + Number(bundlerEstimation.callGasLimit) + + Number(bundlerEstimation.preVerificationGas) + ).toString() }) return { callData, - callGasLimit + callGasLimit: BigNumber.from(bundlerEstimation.callGasLimit) } } @@ -322,4 +397,10 @@ export abstract class BaseAccountAPI { } return null } + + /** + * Get the signature mode for the current account + * This should be implemented by derived classes + */ + abstract getSignatureMode(): SignatureMode } diff --git a/src/ERC4337EthersSigner.ts b/src/ERC4337EthersSigner.ts index 4653e2e..a9242a3 100644 --- a/src/ERC4337EthersSigner.ts +++ b/src/ERC4337EthersSigner.ts @@ -8,6 +8,9 @@ import { ClientConfig } from './ClientConfig' import { HttpRpcClient } from './HttpRpcClient' import { UserOperationStruct } from '@account-abstraction/contracts' import { BaseAccountAPI } from './BaseAccountAPI' +import Debug from 'debug' + +const debug = Debug('aa.signer') export class ERC4337EthersSigner extends Signer { // TODO: we have 'erc4337provider', remove shared dependencies or avoid two-way reference @@ -27,12 +30,33 @@ export class ERC4337EthersSigner extends Signer { async sendTransaction (transaction: Deferrable): Promise { const tx: TransactionRequest = await this.populateTransaction(transaction) await this.verifyAllNecessaryFields(tx) - const userOperation = await this.smartAccountAPI.createSignedUserOp({ + + // Check if gasLimit was explicitly set in the original transaction + const originalGasLimit = (transaction as any).gasLimit + const isGasLimitExplicit = originalGasLimit !== undefined && originalGasLimit !== null + + debug('Gas limit details: %o', { + originalGasLimit: originalGasLimit?.toString() ?? 'not set', + populatedGasLimit: tx.gasLimit?.toString() ?? 'not set', + isExplicitlySet: isGasLimitExplicit + }) + + // Only pass gasLimit if it was explicitly set in the original transaction + const userOpDetails = { target: tx.to ?? '', data: tx.data?.toString() ?? '', value: tx.value, - gasLimit: tx.gasLimit + ...(isGasLimitExplicit && { gasLimit: tx.gasLimit }) + } + + debug('Creating UserOp with details: %o', { + target: userOpDetails.target, + hasData: !!userOpDetails.data && userOpDetails.data !== '0x', + hasValue: !!userOpDetails.value && userOpDetails.value !== '0x', + gasLimitIncluded: 'gasLimit' in userOpDetails }) + + const userOperation = await this.smartAccountAPI.createSignedUserOp(userOpDetails) const transactionResponse = await this.erc4337provider.constructUserOpTransactionResponse(userOperation) try { await this.httpRpcClient.sendUserOpToBundler(userOperation) diff --git a/src/HttpRpcClient.ts b/src/HttpRpcClient.ts index 28b4822..667c614 100644 --- a/src/HttpRpcClient.ts +++ b/src/HttpRpcClient.ts @@ -53,13 +53,45 @@ export class HttpRpcClient { * @param userOp1 * @returns latest gas suggestions made by the bundler. */ - async estimateUserOpGas (userOp1: Partial): Promise<{callGasLimit: number, preVerificationGas: number, verificationGas: number}> { - await this.initializing - const hexifiedUserOp = deepHexlify(await resolveProperties(userOp1)) - const jsonRequestData: [UserOperationStruct, string] = [hexifiedUserOp, this.entryPointAddress] - await this.printUserOperation('eth_estimateUserOperationGas', jsonRequestData) - return await this.userOpJsonRpcProvider - .send('eth_estimateUserOperationGas', [hexifiedUserOp, this.entryPointAddress]) + async estimateUserOpGas(userOp1: Partial): Promise<{ + callGasLimit: number + preVerificationGas: number + verificationGas: number + success: boolean + }> { + try { + await this.initializing + const hexifiedUserOp = deepHexlify(await resolveProperties(userOp1)) + + debug('Sending estimation request: %o', { + sender: hexifiedUserOp.sender, + nonce: hexifiedUserOp.nonce, + initCode: hexifiedUserOp.initCode?.length > 2 ? 'present' : 'none', + callData: hexifiedUserOp.callData?.length > 2 ? 'present' : 'none' + }) + + const result = await this.userOpJsonRpcProvider + .send('eth_estimateUserOperationGas', [hexifiedUserOp, this.entryPointAddress]) + .catch(error => { + debug('Estimation failed: %s', error instanceof Error ? error.message : 'Unknown error') + throw error + }) + + debug('Estimation response: %o', result) + return { + ...result, + success: true + } + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error' + debug('Estimation error: %s', errorMessage) + return { + callGasLimit: 0, + preVerificationGas: 0, + verificationGas: 0, + success: false + } + } } private async printUserOperation (method: string, [userOp1, entryPointAddress]: [UserOperationStruct, string]): Promise { diff --git a/src/Provider.ts b/src/Provider.ts index d6a84d4..423ff94 100644 --- a/src/Provider.ts +++ b/src/Provider.ts @@ -57,18 +57,20 @@ export async function wrapProvider ( } } + const chainId = await originalProvider.getNetwork().then(net => net.chainId) + const httpRpcClient = new HttpRpcClient(config.bundlerUrl, config.entryPointAddress, chainId) + // Initialize SimpleAccountAPI with the resolved factoryAddress const smartAccountAPI = new SimpleAccountAPI({ provider: originalProvider, entryPointAddress: entryPoint.address, owner: originalSigner, factoryAddress, // Use resolved factoryAddress - paymasterAPI: config.paymasterAPI + paymasterAPI: config.paymasterAPI, + httpRpcClient // Add httpRpcClient }) debug('config=', config) - const chainId = await originalProvider.getNetwork().then(net => net.chainId) - const httpRpcClient = new HttpRpcClient(config.bundlerUrl, config.entryPointAddress, chainId) // Return the initialized ERC4337EthersProvider return await new ERC4337EthersProvider( diff --git a/src/SignatureMode.ts b/src/SignatureMode.ts new file mode 100644 index 0000000..47043d5 --- /dev/null +++ b/src/SignatureMode.ts @@ -0,0 +1,4 @@ +export enum SignatureMode { + EOA = 'EOA', + PASSKEY = 'PASSKEY' +} \ No newline at end of file diff --git a/src/SimpleAccountAPI.ts b/src/SimpleAccountAPI.ts index 78d8f9e..3c1663a 100644 --- a/src/SimpleAccountAPI.ts +++ b/src/SimpleAccountAPI.ts @@ -9,12 +9,8 @@ import { import { arrayify, hexlify, zeroPad, hexConcat, Interface } from 'ethers/lib/utils' import { Signer } from '@ethersproject/abstract-signer' import { BaseApiParams, BaseAccountAPI } from './BaseAccountAPI' - -function hasPublicKey(owner: any): owner is { publicKey: { x: string; y: string } } { - return owner && owner.publicKey && - typeof owner.publicKey.x === 'string' && - typeof owner.publicKey.y === 'string' -} +import { SignatureMode } from './SignatureMode' +import { hasPublicKey } from './utils' /** * constructor params, added no top of base params: @@ -149,4 +145,11 @@ export class SimpleAccountAPI extends BaseAccountAPI { ) return hexConcat([versionBytes, signedMessage]) } + + /** + * Get the signature mode based on the owner type + */ + getSignatureMode(): SignatureMode { + return hasPublicKey(this.owner) ? SignatureMode.PASSKEY : SignatureMode.EOA + } } diff --git a/src/calcPreVerificationGas.ts b/src/calcPreVerificationGas.ts index d2d51d4..b4b7725 100644 --- a/src/calcPreVerificationGas.ts +++ b/src/calcPreVerificationGas.ts @@ -1,6 +1,16 @@ import { UserOperationStruct } from '@account-abstraction/contracts' import { NotPromise, packUserOp } from '@account-abstraction/utils' import { arrayify, hexlify } from 'ethers/lib/utils' +import { SignatureMode } from './SignatureMode' +import Debug from 'debug' + +const debug = Debug('aa.gas') + +// Signature sizes for different types +export const SignatureSizes = { + EOA: 65, // r,s,v signature (32 + 32 + 1) + PASSKEY: 536 // Passkey signature size +} export interface GasOverheads { /** @@ -36,19 +46,19 @@ export interface GasOverheads { bundleSize: number /** - * expected length of the userOp signature. + * signature mode (EOA or PASSKEY) */ - sigSize: number + signatureMode?: SignatureMode } export const DefaultGasOverheads: GasOverheads = { fixed: 21000, - perUserOp: 18300, + perUserOp: 26000, perUserOpWord: 4, zeroByte: 4, nonZeroByte: 16, bundleSize: 1, - sigSize: 536 + signatureMode: SignatureMode.EOA } /** @@ -58,23 +68,48 @@ export const DefaultGasOverheads: GasOverheads = { * @param userOp filled userOp to calculate. The only possible missing fields can be the signature and preVerificationGas itself * @param overheads gas overheads to use, to override the default values */ -export function calcPreVerificationGas (userOp: Partial>, overheads?: Partial): number { +export function calcPreVerificationGas(userOp: Partial>, overheads?: Partial): number { + debug('Calculating preVerificationGas...') + const ov = { ...DefaultGasOverheads, ...(overheads ?? {}) } + + // Determine signature size based on signature mode + const sigSize = ov.signatureMode === SignatureMode.PASSKEY ? + SignatureSizes.PASSKEY : + SignatureSizes.EOA + + debug('Using signature mode:', ov.signatureMode, 'with size:', sigSize) + const p: NotPromise = { - // dummy values, in case the UserOp is incomplete. - preVerificationGas: 21000, // dummy value, just for calldata cost - signature: hexlify(Buffer.alloc(ov.sigSize, 1)), // dummy signature + preVerificationGas: 21000, + signature: hexlify(Buffer.alloc(sigSize, 1)), ...userOp } as any const packed = arrayify(packUserOp(p, false)) const lengthInWord = (packed.length + 31) / 32 const callDataCost = packed.map(x => x === 0 ? ov.zeroByte : ov.nonZeroByte).reduce((sum, x) => sum + x) + + const baseGas = ov.fixed / ov.bundleSize + const userOpGas = ov.perUserOp + const wordGas = ov.perUserOpWord * lengthInWord + const ret = Math.round( callDataCost + - ov.fixed / ov.bundleSize + - ov.perUserOp + - ov.perUserOpWord * lengthInWord + baseGas + + userOpGas + + wordGas ) - return ret + + debug('Gas calculation:', { + sigSize, + signatureMode: ov.signatureMode, + baseGas, + userOpGas, + wordGas, + callDataCost, + total: ret + }) + + return Math.max(ret, 49024) // Ensure we never return less than the bundler minimum } diff --git a/src/index.ts b/src/index.ts index eabae18..f447aaf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,3 +8,5 @@ export { ClientConfig } from './ClientConfig' export { HttpRpcClient } from './HttpRpcClient' export { DeterministicDeployer } from './DeterministicDeployer' export * from './calcPreVerificationGas' +export { SignatureMode } from './SignatureMode' +export { hasPublicKey } from './utils' diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..43004fe --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,16 @@ +/** + * Type guard to check if an object has a publicKey property with x and y coordinates + */ +export function hasPublicKey(owner: any): owner is { publicKey: { x: string; y: string } } { + return ( + owner != null && + typeof owner === 'object' && + 'publicKey' in owner && + typeof owner.publicKey === 'object' && + owner.publicKey != null && + 'x' in owner.publicKey && + 'y' in owner.publicKey && + typeof owner.publicKey.x === 'string' && + typeof owner.publicKey.y === 'string' + ) +} \ No newline at end of file diff --git a/test/0-deterministicDeployer.test.ts b/test/0-deterministicDeployer.test.ts deleted file mode 100644 index 3fca331..0000000 --- a/test/0-deterministicDeployer.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { expect } from 'chai' -import { SampleRecipient__factory } from '@account-abstraction/utils/dist/src/types' -import { ethers } from 'hardhat' -import { hexValue } from 'ethers/lib/utils' -import { DeterministicDeployer } from '../src/DeterministicDeployer' - -const deployer = new DeterministicDeployer(ethers.provider) - -describe('#deterministicDeployer', () => { - it('deploy deployer', async () => { - expect(await deployer.isDeployerDeployed()).to.equal(false) - await deployer.deployFactory() - expect(await deployer.isDeployerDeployed()).to.equal(true) - }) - it('should ignore deploy again of deployer', async () => { - await deployer.deployFactory() - }) - it('should deploy at given address', async () => { - const ctr = hexValue(new SampleRecipient__factory(ethers.provider.getSigner()).getDeployTransaction().data!) - DeterministicDeployer.init(ethers.provider) - const addr = await DeterministicDeployer.getAddress(ctr) - expect(await deployer.isContractDeployed(addr)).to.equal(false) - await DeterministicDeployer.deploy(ctr) - expect(await deployer.isContractDeployed(addr)).to.equal(true) - }) -}) diff --git a/test/1-SimpleAccountAPI.test.ts b/test/1-SimpleAccountAPI.test.ts deleted file mode 100644 index b42244c..0000000 --- a/test/1-SimpleAccountAPI.test.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { - EntryPoint, - EntryPoint__factory, - SimpleAccountFactory__factory, - UserOperationStruct -} from '@account-abstraction/contracts' -import { Wallet } from 'ethers' -import { parseEther } from 'ethers/lib/utils' -import { expect } from 'chai' -import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs' -import { ethers } from 'hardhat' -import { DeterministicDeployer, SimpleAccountAPI } from '../src' -import { SampleRecipient, SampleRecipient__factory } from '@account-abstraction/utils/dist/src/types' -import { rethrowError } from '@account-abstraction/utils' - -const provider = ethers.provider -const signer = provider.getSigner() - -describe('SimpleAccountAPI', () => { - let owner: Wallet - let api: SimpleAccountAPI - let entryPoint: EntryPoint - let beneficiary: string - let recipient: SampleRecipient - let accountAddress: string - let accountDeployed = false - - before('init', async () => { - entryPoint = await new EntryPoint__factory(signer).deploy() - beneficiary = await signer.getAddress() - - recipient = await new SampleRecipient__factory(signer).deploy() - owner = Wallet.createRandom() - DeterministicDeployer.init(ethers.provider) - const factoryAddress = await DeterministicDeployer.deploy(new SimpleAccountFactory__factory(), 0, [entryPoint.address]) - api = new SimpleAccountAPI({ - provider, - entryPointAddress: entryPoint.address, - owner, - factoryAddress - }) - }) - - it('#getUserOpHash should match entryPoint.getUserOpHash', async function () { - const userOp: UserOperationStruct = { - sender: '0x'.padEnd(42, '1'), - nonce: 2, - initCode: '0x3333', - callData: '0x4444', - callGasLimit: 5, - verificationGasLimit: 6, - preVerificationGas: 7, - maxFeePerGas: 8, - maxPriorityFeePerGas: 9, - paymasterAndData: '0xaaaaaa', - signature: '0xbbbb' - } - const hash = await api.getUserOpHash(userOp) - const epHash = await entryPoint.getUserOpHash(userOp) - expect(hash).to.equal(epHash) - }) - - it('should deploy to counterfactual address', async () => { - accountAddress = await api.getAccountAddress() - expect(await provider.getCode(accountAddress).then(code => code.length)).to.equal(2) - - await signer.sendTransaction({ - to: accountAddress, - value: parseEther('0.1') - }) - const op = await api.createSignedUserOp({ - target: recipient.address, - data: recipient.interface.encodeFunctionData('something', ['hello']) - }) - - await expect(entryPoint.handleOps([op], beneficiary)).to.emit(recipient, 'Sender') - .withArgs(anyValue, accountAddress, 'hello') - expect(await provider.getCode(accountAddress).then(code => code.length)).to.greaterThan(1000) - accountDeployed = true - }) - - context('#rethrowError', () => { - let userOp: UserOperationStruct - before(async () => { - userOp = await api.createUnsignedUserOp({ - target: ethers.constants.AddressZero, - data: '0x' - }) - // expect FailedOp "invalid signature length" - userOp.signature = '0x11' - }) - it('should parse FailedOp error', async () => { - await expect( - entryPoint.handleOps([userOp], beneficiary) - .catch(rethrowError)) - .to.revertedWith('FailedOp: AA23 reverted: ECDSA: invalid signature length') - }) - it('should parse Error(message) error', async () => { - await expect( - entryPoint.addStake(0) - ).to.revertedWith('must specify unstake delay') - }) - it('should parse revert with no description', async () => { - // use wrong signature for contract.. - const wrongContract = entryPoint.attach(recipient.address) - await expect( - wrongContract.addStake(0) - ).to.revertedWithoutReason() - }) - }) - - it('should use account API after creation without a factory', async function () { - if (!accountDeployed) { - this.skip() - } - const api1 = new SimpleAccountAPI({ - provider, - entryPointAddress: entryPoint.address, - accountAddress, - owner - }) - const op1 = await api1.createSignedUserOp({ - target: recipient.address, - data: recipient.interface.encodeFunctionData('something', ['world']) - }) - await expect(entryPoint.handleOps([op1], beneficiary)).to.emit(recipient, 'Sender') - .withArgs(anyValue, accountAddress, 'world') - }) -}) diff --git a/test/2-ERC4337EthersProvider.test.ts b/test/2-ERC4337EthersProvider.test.ts deleted file mode 100644 index c468221..0000000 --- a/test/2-ERC4337EthersProvider.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -// import { expect } from 'chai' -// import hre from 'hardhat' -// import { time } from '@nomicfoundation/hardhat-network-helpers' -// -// describe('Lock', function () { -// it('Should set the right unlockTime', async function () { -// const lockedAmount = 1_000_000_000 -// const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60 -// const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS -// -// // deploy a lock contract where funds can be withdrawn -// // one year in the future -// const Lock = await hre.ethers.getContractFactory('Lock') -// const lock = await Lock.deploy(unlockTime, { value: lockedAmount }) -// -// // assert that the value is correct -// expect(await lock.unlockTime()).to.equal(unlockTime) -// }) -// }) -// should throw timeout exception if user operation is not mined after x time diff --git a/test/3-ERC4337EthersSigner.test.ts b/test/3-ERC4337EthersSigner.test.ts deleted file mode 100644 index 2203703..0000000 --- a/test/3-ERC4337EthersSigner.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { SampleRecipient, SampleRecipient__factory } from '@account-abstraction/utils/dist/src/types' -import { ethers } from 'hardhat' -import { ClientConfig, ERC4337EthersProvider, wrapProvider } from '../src' -import { EntryPoint, EntryPoint__factory, SimpleAccountFactory__factory } from '@account-abstraction/contracts' -import { expect } from 'chai' -import { parseEther } from 'ethers/lib/utils' -import { Wallet } from 'ethers' -import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs' -import { DeterministicDeployer } from '../src/DeterministicDeployer' - -const provider = ethers.provider -const signer = provider.getSigner() - -describe('ERC4337EthersSigner, Provider', function () { - let recipient: SampleRecipient - let aaProvider: ERC4337EthersProvider - let entryPoint: EntryPoint - let factoryAddress: string - - before('init', async () => { - // Deploy the SampleRecipient contract - const deployRecipient = await new SampleRecipient__factory(signer).deploy() - - // Deploy the EntryPoint contract - entryPoint = await new EntryPoint__factory(signer).deploy() - - // Deploy the SimpleAccountFactory using DeterministicDeployer - const detDeployer = new DeterministicDeployer(provider) - factoryAddress = await detDeployer.deterministicDeploy(new SimpleAccountFactory__factory(signer), 0, [entryPoint.address]) - - // Set up the ClientConfig with the deployed factoryAddress - const config: ClientConfig = { - entryPointAddress: entryPoint.address, - factoryAddress, - bundlerUrl: '' - } - - // Create a random wallet for the test signer - const aasigner = Wallet.createRandom() - - // Wrap the provider with the ERC4337 abstraction - aaProvider = await wrapProvider(provider, config, aasigner) - - const beneficiary = await provider.getSigner().getAddress() - - // Bypass sending through a bundler, and send directly to our entrypoint - aaProvider.httpRpcClient.sendUserOpToBundler = async (userOp) => { - try { - await entryPoint.handleOps([userOp], beneficiary) - } catch (e: any) { - // Doesn't report error unless called with callStatic - await entryPoint.callStatic.handleOps([userOp], beneficiary).catch((e: any) => { - const message = e.errorArgs != null ? `${e.errorName}(${e.errorArgs.join(',')})` : e.message - throw new Error(message) - }) - } - return '' - } - - // Connect the recipient contract with the aaProvider's signer - recipient = deployRecipient.connect(aaProvider.getSigner()) - }) - - it('should fail to send before funding', async () => { - try { - await recipient.something('hello', { gasLimit: 1e6 }) - throw new Error('should revert') - } catch (e: any) { - expect(e.message).to.eq('FailedOp(0,AA21 didn\'t pay prefund)') - } - }) - - it('should use ERC-4337 Signer and Provider to send the UserOperation to the bundler', async function () { - const accountAddress = await aaProvider.getSigner().getAddress() - - // Fund the account with some ether - await signer.sendTransaction({ - to: accountAddress, - value: parseEther('0.1') - }) - - // Send the 'something' transaction and expect an event - const ret = await recipient.something('hello') - await expect(ret).to.emit(recipient, 'Sender') - .withArgs(anyValue, accountAddress, 'hello') - }) - - it('should revert if on-chain userOp execution reverts', async function () { - // Send a transaction that should revert - const ret = await recipient.reverting({ gasLimit: 10000 }) - - try { - await ret.wait() - throw new Error('expected to revert') - } catch (e: any) { - expect(e.message).to.match(/test revert/) - } - }) -}) diff --git a/test/4-calcPreVerificationGas.test.ts b/test/4-calcPreVerificationGas.test.ts deleted file mode 100644 index 759bd41..0000000 --- a/test/4-calcPreVerificationGas.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { expect } from 'chai' -import { hexlify } from 'ethers/lib/utils' -import { calcPreVerificationGas } from '../src/calcPreVerificationGas' - -describe('#calcPreVerificationGas', () => { - const userOp = { - sender: '0x'.padEnd(42, '1'), - nonce: 0, - initCode: '0x3333', - callData: '0x4444', - callGasLimit: 5, - verificationGasLimit: 6, - maxFeePerGas: 8, - maxPriorityFeePerGas: 9, - paymasterAndData: '0xaaaaaa' - } - - it('returns a gas value proportional to sigSize', async () => { - const pvg1 = calcPreVerificationGas(userOp, { sigSize: 0 }) - const pvg2 = calcPreVerificationGas(userOp, { sigSize: 65 }) - - expect(pvg2).to.be.greaterThan(pvg1) - }) - - it('returns a gas value that ignores sigSize if userOp already signed', async () => { - const userOpWithSig = { - ...userOp, - signature: hexlify(Buffer.alloc(65, 1)) - } - - const pvg1 = calcPreVerificationGas(userOpWithSig, { sigSize: 0 }) - const pvg2 = calcPreVerificationGas(userOpWithSig) - expect(pvg2).to.equal(pvg1) - }) -})