Skip to content

Commit

Permalink
Merge pull request #305 from bcnmy/feat-paymaster-changes
Browse files Browse the repository at this point in the history
feat paymaster call for buildUserOp
  • Loading branch information
tomarsachin2271 authored Oct 9, 2023
2 parents 92610c8 + 9c8997d commit 0eb2fe8
Show file tree
Hide file tree
Showing 17 changed files with 269 additions and 129 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"test": "concurrently -k --success first 'yarn start:ganache' 'yarn test:run'",
"start:ganache": "ganache -m 'direct buyer cliff train rice spirit census refuse glare expire innocent quote'",
"test:ci": "FORCE_COLOR=1 lerna run test:ci --stream --npm-client=yarn",
"test:coverage": "yarn jest --coverage",
"test:coverage": "concurrently -k --success first 'yarn start:ganache' 'yarn jest --runInBand --coverage'",
"diff": "lerna diff",
"release": "lerna version patch --no-git-tag-version --no-push --conventional-commits --yes"
},
Expand All @@ -53,10 +53,12 @@
]
},
"dependencies": {
"ganache": "^7.9.1",
"node-gyp": "^9.4.0",
"typescript": "^5.2.2"
},
"devDependencies": {
"@types/debug": "^4.1.9",
"@types/jest": "^29.5.4",
"@typescript-eslint/eslint-plugin": "^6.7.0",
"@typescript-eslint/parser": "^6.6.0",
Expand Down
59 changes: 47 additions & 12 deletions packages/account/src/BaseSmartAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import { calcPreVerificationGas, DefaultGasLimits } from "./utils/Preverificaito
import { NotPromise, packUserOp, Logger, RPC_PROVIDER_URLS } from "@biconomy/common";
import { IBundler, UserOpResponse } from "@biconomy/bundler";
import { IPaymaster, PaymasterAndDataResponse } from "@biconomy/paymaster";
import { SponsorUserOperationDto, BiconomyPaymaster, PaymasterMode, IHybridPaymaster } from "@biconomy/paymaster";
import { BaseSmartAccountConfig, Overrides, SendUserOpOptions, TransactionDetailsForUserOp } from "./utils/Types";
import { GasOverheads } from "./utils/Preverificaiton";
import { EntryPoint, EntryPoint__factory } from "@account-abstraction/contracts";
import { DEFAULT_ENTRYPOINT_ADDRESS } from "./utils/Constants";
import { LRUCache } from 'lru-cache'
import { LRUCache } from "lru-cache";

type UserOperationKey = keyof UserOperation;

Expand Down Expand Up @@ -71,7 +72,7 @@ export abstract class BaseSmartAccount implements IBaseSmartAccount {
validateUserOp(userOp: Partial<UserOperation>, requiredFields: UserOperationKey[]): boolean {
for (const field of requiredFields) {
if (!userOp[field]) {
throw new Error(`${field} is missing`);
throw new Error(`${String(field)} is missing in the UserOp`);
}
}
return true;
Expand Down Expand Up @@ -205,10 +206,24 @@ export abstract class BaseSmartAccount implements IBaseSmartAccount {

async calculateUserOpGasValues(userOp: Partial<UserOperation>): Promise<Partial<UserOperation>> {
if (!this.provider) throw new Error("Provider is not present for making rpc calls");
const feeData = await this.provider.getFeeData();
userOp.maxFeePerGas = userOp.maxFeePerGas ?? feeData.maxFeePerGas ?? feeData.gasPrice ?? (await this.provider.getGasPrice());
userOp.maxPriorityFeePerGas =
userOp.maxPriorityFeePerGas ?? feeData.maxPriorityFeePerGas ?? feeData.gasPrice ?? (await this.provider.getGasPrice());
let feeData = null;

if (
userOp.maxFeePerGas === undefined ||
userOp.maxFeePerGas === null ||
userOp.maxPriorityFeePerGas === undefined ||
userOp.maxPriorityFeePerGas === null
) {
feeData = await this.provider.getFeeData();
}

if (userOp.maxFeePerGas === undefined || userOp.maxFeePerGas === null) {
userOp.maxFeePerGas = feeData?.maxFeePerGas ?? feeData?.gasPrice ?? (await this.provider.getGasPrice());
}

if (userOp.maxPriorityFeePerGas === undefined || userOp.maxPriorityFeePerGas === null) {
userOp.maxPriorityFeePerGas = feeData?.maxPriorityFeePerGas ?? feeData?.gasPrice ?? (await this.provider.getGasPrice());
}
if (userOp.initCode) userOp.verificationGasLimit = userOp.verificationGasLimit ?? (await this.getVerificationGasLimit(userOp.initCode));
userOp.callGasLimit =
userOp.callGasLimit ??
Expand All @@ -221,29 +236,48 @@ export abstract class BaseSmartAccount implements IBaseSmartAccount {
return userOp;
}

// TODO // Should make this a Dto
async estimateUserOpGas(
userOp: Partial<UserOperation>,
overrides?: Overrides,
skipBundlerGasEstimation?: boolean,
paymasterServiceData?: SponsorUserOperationDto,
): Promise<Partial<UserOperation>> {
const requiredFields: UserOperationKey[] = ["sender", "nonce", "initCode", "callData"];
this.validateUserOp(userOp, requiredFields);

let finalUserOp = userOp;
const skipBundlerCall = skipBundlerGasEstimation ?? false;
const skipBundlerCall = skipBundlerGasEstimation ?? true;
// Override gas values in userOp if provided in overrides params
if (overrides) {
userOp = { ...userOp, ...overrides };
}

Logger.log("userOp in estimation", userOp);

if (!this.bundler || skipBundlerCall) {
if (!this.provider) throw new Error("Provider is not present for making rpc calls");
// if no bundler url is provided run offchain logic to assign following values of UserOp
// maxFeePerGas, maxPriorityFeePerGas, verificationGasLimit, callGasLimit, preVerificationGas
finalUserOp = await this.calculateUserOpGasValues(userOp);
if (skipBundlerCall) {
// Review: instead of checking mode it could be assumed or just pass gasless flag and use it
// make pmService data locally and pass the object with default values
if (this.paymaster && this.paymaster instanceof BiconomyPaymaster && paymasterServiceData?.mode === PaymasterMode.SPONSORED) {
if (!userOp.maxFeePerGas && !userOp.maxPriorityFeePerGas) {
throw new Error("maxFeePerGas and maxPriorityFeePerGas are required for skipBundlerCall mode");
}
// Making call to paymaster to get gas estimations for userOp
const { callGasLimit, verificationGasLimit, preVerificationGas, paymasterAndData } = await (
this.paymaster as IHybridPaymaster<SponsorUserOperationDto>
).getPaymasterAndData(userOp, paymasterServiceData);
finalUserOp.verificationGasLimit = verificationGasLimit ?? userOp.verificationGasLimit;
finalUserOp.callGasLimit = callGasLimit ?? userOp.callGasLimit;
finalUserOp.preVerificationGas = preVerificationGas ?? userOp.preVerificationGas;
finalUserOp.paymasterAndData = paymasterAndData ?? userOp.paymasterAndData;
} else {
Logger.warn("Skipped paymaster call. If you are using paymasterAndData, generate data externally");
finalUserOp = await this.calculateUserOpGasValues(userOp);
finalUserOp.paymasterAndData = "0x";
}
} else {
if (!this.bundler) throw new Error("Bundler is not provided");
// TODO: is this still needed to delete?
delete userOp.maxFeePerGas;
delete userOp.maxPriorityFeePerGas;
// Making call to bundler to get gas estimations for userOp
Expand All @@ -261,6 +295,7 @@ export abstract class BaseSmartAccount implements IBaseSmartAccount {
finalUserOp.verificationGasLimit = verificationGasLimit ?? userOp.verificationGasLimit;
finalUserOp.callGasLimit = callGasLimit ?? userOp.callGasLimit;
finalUserOp.preVerificationGas = preVerificationGas ?? userOp.preVerificationGas;
finalUserOp.paymasterAndData = "0x";
}
return finalUserOp;
}
Expand Down
89 changes: 48 additions & 41 deletions packages/account/src/BiconomySmartAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,25 +256,59 @@ export class BiconomySmartAccount extends SmartAccount implements IBiconomySmart
return "0x";
}

async buildUserOp(transactions: Transaction[], overrides?: Overrides, skipBundlerGasEstimation?: boolean): Promise<Partial<UserOperation>> {
private async getNonce(): Promise<BigNumber> {
let nonce = BigNumber.from(0);
try {
nonce = await this.nonce();
} catch (error) {
// Not throwing this error as nonce would be 0 if this.nonce() throw exception, which is expected flow for undeployed account
Logger.log("Error while getting nonce for the account. This is expected for undeployed accounts set nonce to 0");
}
return nonce;
}

private async getGasFeeValues(
overrides: Overrides | undefined,
skipBundlerGasEstimation: boolean | undefined,
): Promise<{ maxFeePerGas?: BigNumberish | undefined; maxPriorityFeePerGas?: BigNumberish | undefined }> {
const gasFeeValues = {
maxFeePerGas: overrides?.maxFeePerGas,
maxPriorityFeePerGas: overrides?.maxPriorityFeePerGas,
};
try {
if (this.bundler && !gasFeeValues.maxFeePerGas && !gasFeeValues.maxPriorityFeePerGas && (skipBundlerGasEstimation ?? true)) {
const gasFeeEstimation = await this.bundler.getGasFeeValues();
gasFeeValues.maxFeePerGas = gasFeeEstimation.maxFeePerGas;
gasFeeValues.maxPriorityFeePerGas = gasFeeEstimation.maxPriorityFeePerGas;
}
return gasFeeValues;
} catch (error: any) {
Logger.error("Error while getting gasFeeValues from bundler. Provided bundler might not have getGasFeeValues endpoint", error);
return gasFeeValues;
}
}

async buildUserOp(
transactions: Transaction[],
overrides?: Overrides,
skipBundlerGasEstimation?: boolean,
paymasterServiceData?: SponsorUserOperationDto,
): Promise<Partial<UserOperation>> {
this.isInitialized();
const to = transactions.map((element: Transaction) => element.to);
const data = transactions.map((element: Transaction) => element.data ?? "0x");
const value = transactions.map((element: Transaction) => element.value ?? BigNumber.from("0"));
this.isProxyDefined();

const [nonce, gasFeeValues] = await Promise.all([this.getNonce(), this.getGasFeeValues(overrides, skipBundlerGasEstimation)]);

let callData = "";
if (transactions.length === 1) {
callData = this.getExecuteCallData(to[0], value[0], data[0]);
} else {
callData = this.getExecuteBatchCallData(to, value, data);
}
let nonce = BigNumber.from(0);
try {
nonce = await this.nonce();
} catch (error) {
// Not throwing this error as nonce would be 0 if this.nonce() throw exception, which is expected flow for undeployed account
}

let isDeployed = true;

if (nonce.eq(0)) {
Expand All @@ -286,16 +320,14 @@ export class BiconomySmartAccount extends SmartAccount implements IBiconomySmart
nonce,
initCode: !isDeployed ? this.initCode : "0x",
callData: callData,
maxFeePerGas: gasFeeValues.maxFeePerGas || undefined,
maxPriorityFeePerGas: gasFeeValues.maxPriorityFeePerGas || undefined,
signature: this.getDummySignature(),
};

// for this Smart Account dummy ECDSA signature will be used to estimate gas
userOp.signature = this.getDummySignature();

userOp = await this.estimateUserOpGas(userOp, overrides, skipBundlerGasEstimation);
Logger.log("userOp after estimation ", userOp);

// Do not populate paymasterAndData as part of buildUserOp as it may not have all necessary details
userOp.paymasterAndData = "0x"; // await this.getPaymasterAndData(userOp)
// Note: Can change the default behaviour of calling estimations using bundler/local
userOp = await this.estimateUserOpGas(userOp, overrides, skipBundlerGasEstimation, paymasterServiceData);
Logger.log("UserOp after estimation ", userOp);

return userOp;
}
Expand Down Expand Up @@ -394,36 +426,11 @@ export class BiconomySmartAccount extends SmartAccount implements IBiconomySmart

newCallData = this.getExecuteBatchCallData(batchTo, batchValue, batchData);
}
let finalUserOp: Partial<UserOperation> = {
const finalUserOp: Partial<UserOperation> = {
...userOp,
callData: newCallData,
};

// Requesting to update gas limits again (especially callGasLimit needs to be re-calculated)
try {
delete finalUserOp.callGasLimit;
delete finalUserOp.verificationGasLimit;
delete finalUserOp.preVerificationGas;

// Maybe send paymasterAndData since we know it's for Token paymaster
/*finalUserOp.paymasterAndData =
'0x00000f7365ca6c59a2c93719ad53d567ed49c14c000000000000000000000000000000000000000000000000000000000064e3d3890000000000000000000000000000000000000000000000000000000064e3cc81000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e583100000000000000000000000000000f7748595e46527413574a9327942e744e910000000000000000000000000000000000000000000000000000000063ac7f6c000000000000000000000000000000000000000000000000000000000010c8e07bf61410b71700f943499adfd23e50fb16040d587acb0a5e60ac8576cdbb4c8044f00579a1fc3f294e7dc4a5eb557a7193008343aa36225bddcfbd4fd15646031c'*/

// Review: and handle the case when mock pnd fails with AA31 during simulation.

finalUserOp = await this.estimateUserOpGas(finalUserOp);
const cgl = ethers.BigNumber.from(finalUserOp.callGasLimit);
if (finalUserOp.callGasLimit && cgl.lt(ethers.BigNumber.from("21000"))) {
return {
...userOp,
callData: newCallData,
};
}
Logger.log("userOp after estimation ", finalUserOp);
} catch (error) {
Logger.error("Failed to estimate gas for userOp with updated callData ", error);
Logger.log("sending updated userOp. calculateGasLimit flag should be sent to the paymaster to be able to update callGasLimit");
}
return finalUserOp;
}
} catch (error) {
Expand Down
Loading

0 comments on commit 0eb2fe8

Please sign in to comment.