Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: aa-core smart account provider fee options to handle more systematic fee options for userops #276

Merged
merged 8 commits into from
Nov 30, 2023
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import { AlchemyProvider } from "@alchemy/aa-alchemy";
import { LocalAccountSigner, type SmartAccountSigner } from "@alchemy/aa-core";
import {
LocalAccountSigner,
Logger,
LogLevel,
type SmartAccountSigner,
type UserOperationFeeOptions,
} from "@alchemy/aa-core";
import {
isAddress,
type Address,
type Chain,
type HDAccount,
type Hash,
type HDAccount,
} from "viem";
import { generatePrivateKey } from "viem/accounts";
import { sepolia } from "viem/chains";
import {
LightSmartContractAccount,
getDefaultLightAccountFactoryAddress,
LightSmartContractAccount,
} from "../../index.js";
import {
API_KEY,
Expand All @@ -22,6 +28,8 @@ import {

const chain = sepolia;

Logger.setLogLevel(LogLevel.DEBUG);

describe("Light Account Tests", () => {
const owner: SmartAccountSigner<HDAccount> =
LocalAccountSigner.mnemonicToAccountSigner(LIGHT_ACCOUNT_OWNER_MNEMONIC);
Expand Down Expand Up @@ -107,7 +115,7 @@ describe("Light Account Tests", () => {
);

await expect(txnHash).resolves.not.toThrowError();
}, 50000);
}, 100000);

it("should fail to execute if account address is not deployed and not correct", async () => {
const accountAddress = "0xc33AbD9621834CA7c6Fc9f9CC3c47b9c17B03f9F";
Expand Down Expand Up @@ -147,15 +155,6 @@ describe("Light Account Tests", () => {
});

it("should transfer ownership successfully", async () => {
const provider = givenConnectedProvider({
owner,
chain,
feeOpts: {
baseFeeBufferPercent: 50n,
maxPriorityFeeBufferPercent: 50n,
preVerificationGasBufferPercent: 50n,
},
});
// create a throwaway address
const throwawayOwner = LocalAccountSigner.privateKeyToAccountSigner(
generatePrivateKey()
Expand All @@ -165,17 +164,6 @@ describe("Light Account Tests", () => {
chain,
});

// fund the throwaway address
denniswon marked this conversation as resolved.
Show resolved Hide resolved
const fundThrowawayResult = await provider.sendUserOperation({
target: await throwawayProvider.getAddress(),
data: "0x",
value: 10000000000000n,
});
const fundThrowawayTxnHash = provider.waitForUserOperationTransaction(
fundThrowawayResult.hash
);
await expect(fundThrowawayTxnHash).resolves.not.toThrowError();

// create new owner and transfer ownership
const newThrowawayOwner = LocalAccountSigner.privateKeyToAccountSigner(
generatePrivateKey()
Expand All @@ -184,6 +172,7 @@ describe("Light Account Tests", () => {
throwawayProvider,
newThrowawayOwner
);

const txnHash = throwawayProvider.waitForUserOperationTransaction(result);
await expect(txnHash).resolves.not.toThrowError();

Expand All @@ -200,21 +189,24 @@ const givenConnectedProvider = ({
owner,
chain,
accountAddress,
feeOpts,
feeOptions,
}: {
owner: SmartAccountSigner;
chain: Chain;
accountAddress?: Address;
feeOpts?: {
baseFeeBufferPercent?: bigint;
maxPriorityFeeBufferPercent?: bigint;
preVerificationGasBufferPercent?: bigint;
};
feeOptions?: UserOperationFeeOptions;
}) => {
const provider = new AlchemyProvider({
apiKey: API_KEY!,
chain,
feeOpts,
opts: {
txMaxRetries: 10,
feeOptions: {
maxFeePerGas: { percentage: 100 },
maxPriorityFeePerGas: { percentage: 100 },
...feeOptions,
},
},
}).connect(
(rpcClient) =>
new LightSmartContractAccount({
Expand Down
108 changes: 62 additions & 46 deletions packages/alchemy/e2e-tests/simple-account.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import type {
PublicErc4337Client,
UserOperationOverrides,
} from "@alchemy/aa-core";
import {
LogLevel,
Logger,
SimpleSmartContractAccount,
getDefaultSimpleAccountFactoryAddress,
type SmartAccountSigner,
type UserOperationFeeOptions,
} from "@alchemy/aa-core";
import { Alchemy, Network } from "alchemy-sdk";
import {
Expand All @@ -13,12 +20,15 @@ import {
} from "viem";
import { mnemonicToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";
import type { SimulateUserOperationAssetChangesResponse } from "../src/middleware/types/index.js";
import { AlchemyProvider } from "../src/provider.js";
import { API_KEY, OWNER_MNEMONIC, PAYMASTER_POLICY_ID } from "./constants.js";

const chain = sepolia;
const network = Network.ETH_SEPOLIA;

Logger.setLogLevel(LogLevel.DEBUG);

describe("Simple Account Tests", () => {
const ownerAccount = mnemonicToAccount(OWNER_MNEMONIC);
const owner: SmartAccountSigner<HDAccount> = {
Expand Down Expand Up @@ -50,7 +60,7 @@ describe("Simple Account Tests", () => {
);

await expect(txnHash).resolves.not.toThrowError();
}, 50000);
}, 100000);

it("should fail to execute if account address is not deployed and not correct", async () => {
const accountAddress = "0xc33AbD9621834CA7c6Fc9f9CC3c47b9c17B03f9F";
Expand Down Expand Up @@ -81,10 +91,14 @@ describe("Simple Account Tests", () => {
);

await expect(txnHash).resolves.not.toThrowError();
}, 50000);
}, 100000);

it("should successfully override fees in alchemy paymaster", async () => {
const provider = givenConnectedProvider({ owner, chain })
const provider = givenConnectedProvider({
owner,
chain,
feeOptions: { maxFeePerGas: undefined, maxPriorityFeePerGas: undefined },
denniswon marked this conversation as resolved.
Show resolved Hide resolved
})
.withAlchemyGasManager({
policyId: PAYMASTER_POLICY_ID,
})
Expand All @@ -101,7 +115,7 @@ describe("Simple Account Tests", () => {
data: "0x",
})
).rejects.toThrow();
}, 50000);
}, 100000);

it("should support overrides for buildUserOperation", async () => {
const signer = givenConnectedProvider({
Expand All @@ -111,11 +125,12 @@ describe("Simple Account Tests", () => {
policyId: PAYMASTER_POLICY_ID,
});

const overrides = {
const overrides: UserOperationOverrides = {
maxFeePerGas: 100000000n,
maxPriorityFeePerGas: 100000000n,
paymasterAndData: "0x",
};

const uoStruct = await signer.buildUserOperation(
{
target: await signer.getAddress(),
Expand All @@ -129,16 +144,16 @@ describe("Simple Account Tests", () => {
overrides.maxPriorityFeePerGas
);
expect(uoStruct.paymasterAndData).toEqual(overrides.paymasterAndData);
}, 50000);
}, 100000);

it("should successfully use paymaster with fee opts", async () => {
const provider = givenConnectedProvider({
owner,
chain,
feeOpts: {
baseFeeBufferPercent: 50n,
maxPriorityFeeBufferPercent: 50n,
preVerificationGasBufferPercent: 50n,
feeOptions: {
maxFeePerGas: { percentage: 50 },
maxPriorityFeePerGas: { percentage: 50 },
preVerificationGas: { percentage: 50 },
},
});

Expand All @@ -151,7 +166,7 @@ describe("Simple Account Tests", () => {
);

await expect(txnHash).resolves.not.toThrowError();
}, 50000);
}, 100000);

it("should execute successfully via drop and replace", async () => {
const provider = givenConnectedProvider({
Expand All @@ -171,7 +186,7 @@ describe("Simple Account Tests", () => {
replacedResult.hash
);
await expect(txnHash).resolves.not.toThrowError();
}, 50000);
}, 100000);

it("should execute successfully via drop and replace when using paymaster", async () => {
const provider = givenConnectedProvider({
Expand All @@ -193,7 +208,7 @@ describe("Simple Account Tests", () => {
replacedResult.hash
);
await expect(txnHash).resolves.not.toThrowError();
}, 50000);
}, 100000);

it("should get token balances for the smart account", async () => {
const alchemy = new Alchemy({
Expand All @@ -212,7 +227,7 @@ describe("Simple Account Tests", () => {
const address = await provider.getAddress();
const balances = await provider.core.getTokenBalances(address);
expect(balances.tokenBalances.length).toMatchInlineSnapshot(`4`);
}, 50000);
}, 100000);

it("should get owned nfts for the smart account", async () => {
const alchemy = new Alchemy({
Expand All @@ -231,43 +246,44 @@ describe("Simple Account Tests", () => {
const address = await provider.getAddress();
const nfts = await provider.nft.getNftsForOwner(address);
expect(nfts.ownedNfts).toMatchInlineSnapshot("[]");
}, 50000);
}, 100000);

it("should correctly simulate asset changes for the user operation", async () => {
const provider = givenConnectedProvider({
owner,
chain,
});

const simulatedAssetChanges =
const simulatedAssetChanges: SimulateUserOperationAssetChangesResponse =
denniswon marked this conversation as resolved.
Show resolved Hide resolved
await provider.simulateUserOperationAssetChanges({
target: provider.getEntryPointAddress(),
data: "0x",
value: 1n,
});

expect(simulatedAssetChanges).toMatchInlineSnapshot(`
expect(simulatedAssetChanges.changes.length).toEqual(2);
expect(
simulatedAssetChanges.changes.map((change) => ({
assertType: change.assetType,
changeType: change.changeType,
from: change.from.toLowerCase(),
to: change.to.toLowerCase(),
}))
).toEqual([
{
assertType: "NATIVE",
changeType: "TRANSFER",
from: (await provider.getAddress()).toLowerCase(),
to: provider.getEntryPointAddress().toLowerCase(),
},
{
"changes": [
{
"amount": "0.000000000000000001",
"assetType": "NATIVE",
"changeType": "TRANSFER",
"contractAddress": null,
"decimals": 18,
"from": "0xb856dbd4fa1a79a46d426f537455e7d3e79ab7c4",
"logo": "https://static.alchemyapi.io/images/network-assets/eth.png",
"name": "Ethereum",
"rawAmount": "1",
"symbol": "ETH",
"to": "0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789",
"tokenId": null,
},
],
"error": null,
}
`);
}, 50000);
assertType: "NATIVE",
changeType: "TRANSFER",
from: (await provider.getAddress()).toLowerCase(),
to: provider.getEntryPointAddress().toLowerCase(),
},
]);
}, 100000);

it("should simulate as part of middleware stack when added to provider", async () => {
const provider = givenConnectedProvider({
Expand All @@ -284,33 +300,33 @@ describe("Simple Account Tests", () => {
});

expect(spy).toHaveBeenCalledOnce();
}, 50000);
}, 100000);
});

const givenConnectedProvider = ({
owner,
chain,
accountAddress,
feeOpts,
feeOptions,
}: {
owner: SmartAccountSigner;
chain: Chain;
accountAddress?: Address;
feeOpts?: {
baseFeeBufferPercent?: bigint;
maxPriorityFeeBufferPercent?: bigint;
preVerificationGasBufferPercent?: bigint;
};
feeOptions?: UserOperationFeeOptions;
}) =>
new AlchemyProvider({
apiKey: API_KEY!,
chain,
feeOpts,
opts: {
txMaxRetries: 10,
feeOptions: {
...feeOptions,
maxFeePerGas: { percentage: 50 },
maxPriorityFeePerGas: { percentage: 50 },
},
},
}).connect(
(provider) =>
(provider: PublicErc4337Client) =>
denniswon marked this conversation as resolved.
Show resolved Hide resolved
new SimpleSmartContractAccount({
chain,
owner,
Expand Down
Loading
Loading