Skip to content
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ui-sdk",
"version": "0.6.0",
"version": "0.6.1",
"main": "./dist/src/index.js",
"license": "MIT",
"files": [
Expand Down
145 changes: 144 additions & 1 deletion src/BaseAccountAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
UserOperationStruct
} from '@account-abstraction/contracts'

import { TransactionDetailsForUserOp } from './TransactionDetailsForUserOp'
import { TransactionDetailsForUserOp, BatchTransactionDetailsForUserOp } from './TransactionDetailsForUserOp'
import { resolveProperties } from 'ethers/lib/utils'
import { PaymasterAPI } from './PaymasterAPI'
import { getUserOpHash, NotPromise, packUserOp } from '@account-abstraction/utils'
Expand Down Expand Up @@ -101,6 +101,14 @@ export abstract class BaseAccountAPI {
*/
abstract encodeExecute (target: string, value: BigNumberish, data: string): Promise<string>

/**
* encode a batch of method calls from entryPoint through our account to the target contracts.
* @param targets array of target addresses
* @param values array of values
* @param datas array of call data
*/
abstract encodeExecuteBatch (targets: string[], values: BigNumberish[], datas: string[]): Promise<string>

/**
* sign a userOp's hash (userOpHash).
* @param userOpHash
Expand Down Expand Up @@ -403,4 +411,139 @@ export abstract class BaseAccountAPI {
* This should be implemented by derived classes
*/
abstract getSignatureMode(): SignatureMode

/**
* encode batch operations for user operation call data and estimate gas
*/
async encodeBatchUserOpCallDataAndGasLimit (detailsForUserOp: BatchTransactionDetailsForUserOp): Promise<{ callData: string, callGasLimit: BigNumber }> {
const { targets, values, datas } = detailsForUserOp

// Validate arrays have same length
if (targets.length !== values.length || targets.length !== datas.length) {
throw new Error('Batch operation arrays must have the same length')
}

const callData = await this.encodeExecuteBatch(targets, values, datas)

debug('Starting batch estimation for %d operations', targets.length)

// 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 for batch operations...')
const initCode = await this.getInitCode()
const verificationGasLimit = await this.getVerificationGasLimit()

debug('Preparing bundler estimation for batch: %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 for batch operation')
}

debug('Bundler estimation details for batch: %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: BigNumber.from(bundlerEstimation.callGasLimit)
}
}

/**
* create a batch UserOperation, filling all details (except signature)
*/
async createUnsignedBatchUserOp (info: BatchTransactionDetailsForUserOp): Promise<UserOperationStruct> {
const {
callData,
callGasLimit
} = await this.encodeBatchUserOpCallDataAndGasLimit(info)
const initCode = await this.getInitCode()

const initGas = await this.estimateCreationGas(initCode)
const verificationGasLimit = BigNumber.from(await this.getVerificationGasLimit())
.add(initGas)

let {
maxFeePerGas,
maxPriorityFeePerGas
} = info
if (maxFeePerGas == null || maxPriorityFeePerGas == null) {
const feeData = await this.provider.getFeeData()
if (maxFeePerGas == null) {
maxFeePerGas = feeData.maxFeePerGas ?? undefined
}
if (maxPriorityFeePerGas == null) {
maxPriorityFeePerGas = feeData.maxPriorityFeePerGas ?? undefined
}
}

const partialUserOp: any = {
sender: await this.getAccountAddress(),
nonce: info.nonce ?? await this.getNonce(),
initCode,
callData,
callGasLimit,
verificationGasLimit,
maxFeePerGas,
maxPriorityFeePerGas,
paymasterAndData: '0x'
}

let paymasterAndData: string | undefined
if (this.paymasterAPI != null) {
// fill (partial) preVerificationGas (all except the cost of the generated paymasterAndData)
const userOpForPm = {
...partialUserOp,
preVerificationGas: await this.getPreVerificationGas(partialUserOp)
}
paymasterAndData = await this.paymasterAPI.getPaymasterAndData(userOpForPm)
}
partialUserOp.paymasterAndData = paymasterAndData ?? '0x'
return {
...partialUserOp,
preVerificationGas: await this.getPreVerificationGas(partialUserOp),
signature: ''
}
}

/**
* helper method: create and sign a batch user operation.
*/
async createSignedBatchUserOp (info: BatchTransactionDetailsForUserOp): Promise<UserOperationStruct> {
return await this.signUserOp(await this.createUnsignedBatchUserOp(info))
}
}
60 changes: 59 additions & 1 deletion src/ERC4337EthersSigner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Deferrable, defineReadOnly } from '@ethersproject/properties'
import { Provider, TransactionRequest, TransactionResponse } from '@ethersproject/providers'
import { Signer } from '@ethersproject/abstract-signer'

import { Bytes } from 'ethers'
import { Bytes, BigNumber, BigNumberish } from 'ethers'
import { ERC4337EthersProvider } from './ERC4337EthersProvider'
import { ClientConfig } from './ClientConfig'
import { HttpRpcClient } from './HttpRpcClient'
Expand All @@ -12,6 +12,15 @@ import Debug from 'debug'

const debug = Debug('aa.signer')

export interface BatchTransactionRequest {
targets: string[]
datas: string[]
values: BigNumberish[]
gasLimit?: BigNumberish
maxFeePerGas?: BigNumberish
maxPriorityFeePerGas?: BigNumberish
}

export class ERC4337EthersSigner extends Signer {
// TODO: we have 'erc4337provider', remove shared dependencies or avoid two-way reference
constructor (
Expand Down Expand Up @@ -99,6 +108,55 @@ export class ERC4337EthersSigner extends Signer {
}
}

async verifyAllNecessaryBatchFields (batchRequest: BatchTransactionRequest): Promise<void> {
if (batchRequest.targets.length === 0) {
throw new Error('Empty batch request')
}
if (batchRequest.targets.length !== batchRequest.datas.length || batchRequest.targets.length !== batchRequest.values.length) {
throw new Error('Batch arrays must have the same length')
}
for (const target of batchRequest.targets) {
if (target == null) {
throw new Error('Missing call target in batch')
}
}
}

/**
* Send a batch of transactions
*/
async sendBatchTransaction(batchRequest: BatchTransactionRequest): Promise<TransactionResponse> {
await this.verifyAllNecessaryBatchFields(batchRequest)

// Convert values to BigNumber and ensure they're in the correct format
const convertedRequest = {
targets: batchRequest.targets,
datas: batchRequest.datas.map(d => d || '0x'),
values: batchRequest.values.map(v => BigNumber.from(v || 0)),
gasLimit: batchRequest.gasLimit ? BigNumber.from(batchRequest.gasLimit) : undefined,
maxFeePerGas: batchRequest.maxFeePerGas ? BigNumber.from(batchRequest.maxFeePerGas) : undefined,
maxPriorityFeePerGas: batchRequest.maxPriorityFeePerGas ? BigNumber.from(batchRequest.maxPriorityFeePerGas) : undefined
}

const userOpDetails = {
targets: convertedRequest.targets,
datas: convertedRequest.datas,
values: convertedRequest.values,
...(batchRequest.gasLimit !== undefined && { gasLimit: convertedRequest.gasLimit }),
maxFeePerGas: convertedRequest.maxFeePerGas,
maxPriorityFeePerGas: convertedRequest.maxPriorityFeePerGas
}

const userOperation = await this.smartAccountAPI.createSignedBatchUserOp(userOpDetails)
const transactionResponse = await this.erc4337provider.constructUserOpTransactionResponse(userOperation)
try {
await this.httpRpcClient.sendUserOpToBundler(userOperation)
} catch (error: any) {
throw this.unwrapError(error)
}
return transactionResponse
}

connect (provider: Provider): Signer {
throw new Error('changing providers is not supported')
}
Expand Down
13 changes: 13 additions & 0 deletions src/SimpleAccountAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,19 @@ export class SimpleAccountAPI extends BaseAccountAPI {
])
}

/**
* encode a batch of method calls from entryPoint to our contract
* @param targets array of target addresses
* @param values array of values
* @param datas array of call data
*/
async encodeExecuteBatch (targets: string[], values: BigNumberish[], datas: string[]): Promise<string> {
const iface = new Interface([
'function executeBatch(address[] calldata dest, uint256[] calldata value, bytes[] calldata func) external'
])
return iface.encodeFunctionData('executeBatch', [targets, values, datas])
}

async signUserOpHash (userOpHash: string): Promise<string> {
const signedMessage = await this.owner.signMessage(arrayify(userOpHash))
const versionBytes = zeroPad(
Expand Down
10 changes: 10 additions & 0 deletions src/TransactionDetailsForUserOp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,13 @@ export interface TransactionDetailsForUserOp {
maxPriorityFeePerGas?: BigNumberish
nonce?: BigNumberish
}

export interface BatchTransactionDetailsForUserOp {
targets: string[]
datas: string[]
values: BigNumberish[]
gasLimit?: BigNumberish
maxFeePerGas?: BigNumberish
maxPriorityFeePerGas?: BigNumberish
nonce?: BigNumberish
}
Loading