Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 88 additions & 7 deletions src/BaseAccountAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,19 @@ 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
entryPointAddress: string
accountAddress?: string
overheads?: Partial<GasOverheads>
paymasterAPI?: PaymasterAPI
httpRpcClient: HttpRpcClient
}

export interface UserOpResult {
Expand All @@ -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<GasOverheads>
entryPointAddress: string
Expand All @@ -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)
Expand Down Expand Up @@ -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<BigNumberish> {
return 500000
const signatureMode = this.getSignatureMode()
// Passkey signatures require more verification gas
return signatureMode === SignatureMode.PASSKEY ? 500000 : 100000
}

/**
Expand All @@ -159,7 +170,12 @@ export abstract class BaseAccountAPI {
*/
async getPreVerificationGas (userOp: Partial<UserOperationStruct>): Promise<number> {
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
})
}

/**
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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
}
28 changes: 26 additions & 2 deletions src/ERC4337EthersSigner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,12 +30,33 @@ export class ERC4337EthersSigner extends Signer {
async sendTransaction (transaction: Deferrable<TransactionRequest>): Promise<TransactionResponse> {
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)
Expand Down
46 changes: 39 additions & 7 deletions src/HttpRpcClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,45 @@ export class HttpRpcClient {
* @param userOp1
* @returns latest gas suggestions made by the bundler.
*/
async estimateUserOpGas (userOp1: Partial<UserOperationStruct>): 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<UserOperationStruct>): 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<void> {
Expand Down
8 changes: 5 additions & 3 deletions src/Provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 4 additions & 0 deletions src/SignatureMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum SignatureMode {
EOA = 'EOA',
PASSKEY = 'PASSKEY'
}
15 changes: 9 additions & 6 deletions src/SimpleAccountAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
}
}
Loading
Loading