Skip to content

Commit

Permalink
feat: aa-core smart account provider fee options to handle more syste…
Browse files Browse the repository at this point in the history
…matic fee options for userops
  • Loading branch information
denniswon committed Nov 24, 2023
1 parent 76caba2 commit dc77bad
Show file tree
Hide file tree
Showing 14 changed files with 541 additions and 145 deletions.
2 changes: 1 addition & 1 deletion packages/alchemy/src/middleware/gas-fees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@ export const withAlchemyGasFeeEstimator = <P extends AlchemyProvider>(
maxFeePerGas: baseFeeIncrease + prioFeeIncrease,
maxPriorityFeePerGas: prioFeeIncrease,
};
});
}, provider.feeOptions);
return provider;
};
56 changes: 43 additions & 13 deletions packages/alchemy/src/middleware/gas-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,35 +13,65 @@ export interface AlchemyGasManagerConfig {
/**
* This middleware wraps the Alchemy Gas Manager APIs to provide more flexible UserOperation gas sponsorship.
*
* If `estimateGas` is true, it will use `alchemy_requestGasAndPaymasterAndData` to get all of the gas estimates + paymaster data
* If `delegateGasEstimation` is true, it will use `alchemy_requestGasAndPaymasterAndData` to get all of the gas estimates + paymaster data
* in one RPC call.
*
* Otherwise, it will use `alchemy_requestPaymasterAndData` to get only paymaster data, allowing you
* to customize the gas and fee estimation middleware.
*
* @param provider - the smart account provider to override to use the alchemy gas manager
* @param config - the alchemy gas manager configuration
* @param estimateGas - if true, this will use `alchemy_requestGasAndPaymasterAndData` else will use `alchemy_requestPaymasterAndData`
* @param delegateGasEstimation - if true, this will use `alchemy_requestGasAndPaymasterAndData` else will use `alchemy_requestPaymasterAndData`
* @returns the provider augmented to use the alchemy gas manager
*/
export const withAlchemyGasManager = <P extends AlchemyProvider>(
provider: P,
config: AlchemyGasManagerConfig,
estimateGas: boolean = true
delegateGasEstimation: boolean = true
): P => {
return estimateGas
const fallbackGasEstimator = provider.gasEstimator;
const fallbackFeeDataGetter = provider.feeDataGetter;

return delegateGasEstimation
? provider
// no-op gas estimator
.withGasEstimator(async () => ({
callGasLimit: 0n,
preVerificationGas: 0n,
verificationGasLimit: 0n,
}))
.withGasEstimator(async (struct, overrides) => {
// but if user is bypassing paymaster to fallback to having the account to pay the gas (one-off override),
// we cannot delegate gas estimation to the bundler because paymaster middleware will not be called
if (overrides?.paymasterAndData !== undefined) {
const result = await fallbackGasEstimator(struct, overrides);
return {
callGasLimit: (await result.callGasLimit) ?? 0n,
preVerificationGas: (await result.preVerificationGas) ?? 0n,
verificationGasLimit: (await result.verificationGasLimit) ?? 0n,
};
} else {
return {
callGasLimit: 0n,
preVerificationGas: 0n,
verificationGasLimit: 0n,
};
}
})
// no-op fee because the alchemy api will do it
.withFeeDataGetter(async (struct) => ({
maxFeePerGas: (await struct.maxFeePerGas) ?? 0n,
maxPriorityFeePerGas: (await struct.maxPriorityFeePerGas) ?? 0n,
}))
.withFeeDataGetter(async (struct, overrides) => {
let maxFeePerGas = (await struct.maxFeePerGas) ?? 0n;
let maxPriorityFeePerGas = (await struct.maxPriorityFeePerGas) ?? 0n;

// but if user is bypassing paymaster to fallback to having the account to pay the gas (one-off override),
// we cannot delegate gas estimation to the bundler because paymaster middleware will not be called
if (overrides?.paymasterAndData !== undefined) {
const result = await fallbackFeeDataGetter(struct, overrides);
maxFeePerGas = (await result.maxFeePerGas) ?? maxFeePerGas;
maxPriorityFeePerGas =
(await result.maxPriorityFeePerGas) ?? maxPriorityFeePerGas;
}

return {
maxFeePerGas,
maxPriorityFeePerGas,
};
})
.withPaymasterMiddleware(
withAlchemyGasAndPaymasterAndDataMiddleware(provider, config)
)
Expand Down
76 changes: 75 additions & 1 deletion packages/core/e2e-tests/simple-account.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import { isAddress, type Address, type Chain, type Hash } from "viem";
import {
fromHex,
isAddress,
type Address,
type Chain,
type Hash,
type Hex,
} from "viem";
import { generatePrivateKey } from "viem/accounts";
import { polygonMumbai } from "viem/chains";
import { SimpleSmartContractAccount } from "../src/account/simple.js";
import {
getDefaultSimpleAccountFactoryAddress,
type SmartAccountSigner,
type UserOperationFeeOptions,
} from "../src/index.js";
import { SmartAccountProvider } from "../src/provider/base.js";
import { LocalAccountSigner } from "../src/signer/local-account.js";
Expand Down Expand Up @@ -58,20 +66,86 @@ describe("Simple Account Tests", () => {
await expect(address).resolves.not.toThrowError();
expect(isAddress(await address)).toBe(true);
});

it("should correctly handle provider feeOptions set during init", async () => {
const signer = givenConnectedProvider({
owner,
chain,
});

const structPromise = signer.buildUserOperation({
target: await signer.getAddress(),
data: "0x",
});
await expect(structPromise).resolves.not.toThrowError();

const signerWithFeeOptions = givenConnectedProvider({
owner,
chain,
feeOptions: {
preVerificationGas: { percentage: 100 },
},
});

const structWithFeeOptionsPromise = signerWithFeeOptions.buildUserOperation(
{
target: await signer.getAddress(),
data: "0x",
}
);
await expect(structWithFeeOptionsPromise).resolves.not.toThrowError();

const [struct, structWithFeeOptions] = await Promise.all([
structPromise,
structWithFeeOptionsPromise,
]);

const preVerificationGas =
typeof struct.preVerificationGas === "string"
? fromHex(struct.preVerificationGas as Hex, "bigint")
: struct.preVerificationGas;
const preVerificationGasWithFeeOptions =
typeof structWithFeeOptions.preVerificationGas === "string"
? fromHex(structWithFeeOptions.preVerificationGas as Hex, "bigint")
: structWithFeeOptions.preVerificationGas;

expect(preVerificationGasWithFeeOptions).toBeGreaterThan(
preVerificationGas!
);
}, 60000);

it("should correctly handle percentage overrides for sendUserOperation", async () => {
const signer = givenConnectedProvider({
owner,
chain,
feeOptions: {
preVerificationGas: { percentage: 100 },
},
});

const struct = signer.sendUserOperation({
target: await signer.getAddress(),
data: "0x",
});
await expect(struct).resolves.not.toThrowError();
}, 60000);
});

const givenConnectedProvider = ({
owner,
chain,
accountAddress,
feeOptions,
}: {
owner: SmartAccountSigner;
chain: Chain;
accountAddress?: Address;
feeOptions?: UserOperationFeeOptions;
}) => {
const provider = new SmartAccountProvider({
rpcProvider: `${chain.rpcUrls.alchemy.http[0]}/${API_KEY}`,
chain,
opts: { feeOptions },
});
const feeDataGetter = async () => ({
maxFeePerGas: 100_000_000_000n,
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/account/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,12 +256,12 @@ export abstract class BaseSmartContractAccount<
try {
await this.entryPoint.simulate.getSenderAddress([initCode]);
} catch (err: any) {
Logger.debug(
"[BaseSmartContractAccount](getAddress) entrypoint.getSenderAddress result: ",
err
);
if (err.cause?.data?.errorName === "SenderAddressResult") {
this.accountAddress = err.cause.data.args[0] as Address;
Logger.debug(
"[BaseSmartContractAccount](getAddress) entrypoint.getSenderAddress result:",
this.accountAddress
);
return this.accountAddress;
}
}
Expand Down
Loading

0 comments on commit dc77bad

Please sign in to comment.