From a609d15bdf7cff4d8ed6244faa44d78e4ae5212d Mon Sep 17 00:00:00 2001 From: moldy Date: Mon, 12 Jun 2023 14:33:10 -0400 Subject: [PATCH] feat: add alchemy sub-package --- README.md | 81 ++++++++++- package.json | 1 + packages/alchemy/package.json | 65 +++++++++ .../src/__tests__/simple-account.test.ts | 131 ++++++++++++++++++ packages/alchemy/src/chains.ts | 50 +++++++ packages/alchemy/src/index.ts | 11 ++ packages/alchemy/src/middleware/gas-fees.ts | 55 ++++++++ .../src/middleware/gas-manager.ts} | 65 +++++++-- packages/alchemy/src/provider.ts | 96 +++++++++++++ packages/alchemy/tsconfig.build.json | 8 ++ packages/alchemy/tsconfig.json | 3 + packages/alchemy/vitest.config.ts | 9 ++ .../core/src/__tests__/simple-account.test.ts | 23 +-- packages/core/src/index.ts | 8 +- packages/core/src/provider/base.ts | 18 ++- .../src/__tests__/simple-account.test.ts | 28 +--- 16 files changed, 584 insertions(+), 68 deletions(-) create mode 100644 packages/alchemy/package.json create mode 100644 packages/alchemy/src/__tests__/simple-account.test.ts create mode 100644 packages/alchemy/src/chains.ts create mode 100644 packages/alchemy/src/index.ts create mode 100644 packages/alchemy/src/middleware/gas-fees.ts rename packages/{core/src/middleware/alchemy-paymaster.ts => alchemy/src/middleware/gas-manager.ts} (56%) create mode 100644 packages/alchemy/src/provider.ts create mode 100644 packages/alchemy/tsconfig.build.json create mode 100644 packages/alchemy/tsconfig.json create mode 100644 packages/alchemy/vitest.config.ts diff --git a/README.md b/README.md index dee4ff57c9..6fd8be2211 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,20 @@ via `npm`: npm i -s @alchemy/aa-core viem ``` +If you are using Alchemy APIs for Account Abstraction, then you can also add the `@alchemy/aa-alchemy` package: + +via `yarn`: + +```bash +yarn add @alchemy/aa-alchemy +``` + +via `npm`: + +```bash +npm i -s @alchemy/aa-alchemy +``` + If you are using `ethers` and want to use an `ethers` compatible `Provider` and `Signer` you can also add the the `@alchemy/aa-ethers` library (the above packages are required still). via `yarn`: @@ -89,6 +103,65 @@ const { hash } = provider.sendUserOperation({ }); ``` +### via `aa-alchemy` + +```ts +import { + SimpleSmartContractAccount, + type SimpleSmartAccountOwner, +} from "@alchemy/aa-core"; +import { toHex } from "viem"; +import { mnemonicToAccount } from "viem/accounts"; +import { polygonMumbai } from "viem/chains"; +import { AlchemyProvider } from "@alchemy/aa-alchemy"; + +const SIMPLE_ACCOUNT_FACTORY_ADDRESS = + "0x9406Cc6185a346906296840746125a0E44976454"; + +// 1. define the EOA owner of the Smart Account +// This is just one exapmle of how to interact with EOAs, feel free to use any other interface +const ownerAccount = mnemonicToAccount(MNEMONIC); +// All that is important for defining an owner is that it provides a `signMessage` and `getAddress` function +const owner: SimpleSmartAccountOwner = { + // this should sign a message according to ERC-191 + signMessage: async (msg) => + ownerAccount.signMessage({ + message: toHex(msg), + }), + getAddress: async () => ownerAccount.address, +}; + +// 2. initialize the provider and connect it to the account +let provider = new AlchemyProvider({ + apiKey: API_KEY, + chain, + entryPointAddress: ENTRYPOINT_ADDRESS, +}).connect( + (rpcClient) => + new SimpleSmartContractAccount({ + entryPointAddress: ENTRYPOINT_ADDRESS, + chain: polygonMumbai, // ether a viem Chain or chainId that supports account abstraction at Alchemy + owner, + factoryAddress: SIMPLE_ACCOUNT_FACTORY_ADDRESS, + rpcClient, + }) +); + +// [OPTIONAL] Use Alchemy Gas Manager +prpvider = provider.withAlchemyGasManager({ + provider: provider.rpcClient, + policyId: PAYMASTER_POLICY_ID, + entryPoint: ENTRYPOINT_ADDRESS, +}); + +// 3. send a UserOperation +const { hash } = provider.sendUserOperation({ + target: "0xTargetAddress", + data: "0xcallData", + value: 0n, // value: bigint or undefined +}); +``` + ### via `aa-ethers` ```ts @@ -157,13 +230,19 @@ If you want to add support for your own `SmartAccounts` then you will need to pr 3. `signMessage` -- this should return an ERC-191 compliant message and is used to sign UO Hashes 4. `getAccountInitCode` -- this should return the init code that will be used to create an account if one does not exist. Usually this is the concatenation of the account's factory address and the abi encoded function data of the account factory's `createAccount` method. +### Paymaster Middleware + +You can use `provider.withPaymasterMiddleware` to add middleware to the stack which will set the `paymasterAndData` field during `sendUserOperation` calls. The `withPaymasterMiddleware` method has two overrides. One of the overrides takes a `dummyPaymasterData` generator function. This `dummyPaymasterData` is needed to estimate gas correctly when using a paymaster and is specific to the paymaster you're using. The second override is the actually `paymasterAndData` generator function. This function is called after gas estimation and fee estimation and is used to set the `paymasterAndData` field. The default `dummyPaymasterData` generator function returns `0x` for both the `paymasterAndData` fields. The default `paymasterAndData` generator function returns `0x` for both the `paymasterAndData` fields. + +Both of the override methods can return new gas estimates. This allows for paymaster RPC urls that handle gas estimation for you. It's important to note that if you're using an ERC-20 paymaster and your RPC endpoint does not return estimates, you should add an additional 75k gas to the gas estimate for `verificationGasLimit`. + ### Alchemy Gas Manager Middleware Alchemy has two separate RPC methods for interacting with our Gas Manager services. The first is `alchemy_requestPaymasterAndData` and the second is `alchemy_requestGasAndPaymasterAndData`. The former is useful if you want to do your own gas estimation + fee estimation (or you're happy using the default middlewares for gas and fee estimation), but want to use the Alchemy Gas Manager service. The latter is will handle gas + fee estimation and return `paymasterAndData` in a single request. -We provide two utility methods in `aa-sdk/core` for interacting with these RPC methods: +We provide two utility methods in `@alchemy/aa-alchemy` for interacting with these RPC methods: 1. `alchemyPaymasterAndDataMiddleware` which is used in conjunction with `withPaymasterMiddleware` to add the `alchemy_requestPaymasterAndData` RPC method to the middleware stack. 2. `withAlchemyGasManager` which wraps a connected `SmartAccountProvider` with the middleware overrides to use `alchemy_requestGasAndPaymasterAndData` RPC method. diff --git a/package.json b/package.json index 01566c7533..a8e311640f 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ ], "scripts": { "build": "lerna run build", + "build:packages": "lerna run build --ignore=alchemy-daapp", "clean": "lerna run clean", "test": "vitest run", "lint:write": "eslint . --fix && prettier --write --ignore-unknown .", diff --git a/packages/alchemy/package.json b/packages/alchemy/package.json new file mode 100644 index 0000000000..a7b6f64088 --- /dev/null +++ b/packages/alchemy/package.json @@ -0,0 +1,65 @@ +{ + "name": "@alchemy/aa-alchemy", + "version": "0.1.0-alpha.1", + "description": "adapters for @alchemy/aa-core for interacting with alchemy services", + "author": "Alchemy", + "license": "MIT", + "private": false, + "type": "module", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/types/index.d.ts", + "typings": "./dist/types/index.d.ts", + "sideEffects": false, + "files": [ + "dist", + "src/**/*.ts", + "!dist/**/*.tsbuildinfo", + "!vitest.config.ts", + "!.env", + "!src/**/*.test.ts", + "!src/__tests__/**/*" + ], + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "import": "./dist/esm/index.js", + "default": "./dist/cjs/index.js" + }, + "./package.json": "./package.json" + }, + "scripts": { + "build": "yarn clean && yarn build:cjs && yarn build:esm && yarn build:types", + "build:cjs": "tsc --project tsconfig.build.json --module commonjs --outDir ./dist/cjs --removeComments --verbatimModuleSyntax false && echo > ./dist/cjs/package.json '{\"type\":\"commonjs\"}'", + "build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir ./dist/esm --removeComments && echo > ./dist/esm/package.json '{\"type\":\"module\"}'", + "build:types": "tsc --project tsconfig.build.json --module esnext --declarationDir ./dist/types --emitDeclarationOnly --declaration --declarationMap", + "clean": "rm -rf ./dist", + "test": "vitest", + "test:run": "vitest run" + }, + "devDependencies": { + "@alchemy/aa-core": "^0.1.0-alpha.1", + "typescript": "^5.0.4", + "typescript-template": "*", + "viem": "^0.3.50", + "vitest": "^0.31.0" + }, + "dependencies": {}, + "peerDependencies": { + "@alchemy/aa-core": "^0.1.0-alpha.1", + "viem": "^0.3.50" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/alchemyplatform/aa-sdk.git" + }, + "bugs": { + "url": "https://github.com/alchemyplatform/aa-sdk/issues" + }, + "homepage": "https://github.com/alchemyplatform/aa-sdk#readme", + "gitHead": "b7e4cd3253f6d93032419a9a559ea16d2a4f71d8" +} diff --git a/packages/alchemy/src/__tests__/simple-account.test.ts b/packages/alchemy/src/__tests__/simple-account.test.ts new file mode 100644 index 0000000000..442442820f --- /dev/null +++ b/packages/alchemy/src/__tests__/simple-account.test.ts @@ -0,0 +1,131 @@ +import { + SimpleSmartContractAccount, + type BatchUserOperationCallData, + type SimpleSmartAccountOwner, +} from "@alchemy/aa-core"; +import { toHex } from "viem"; +import { mnemonicToAccount } from "viem/accounts"; +import { polygonMumbai } from "viem/chains"; +import { AlchemyProvider } from "../provider"; + +const ENTRYPOINT_ADDRESS = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"; +const API_KEY = process.env.API_KEY!; +const OWNER_MNEMONIC = process.env.OWNER_MNEMONIC!; +const PAYMASTER_POLICY_ID = process.env.PAYMASTER_POLICY_ID!; +const SIMPLE_ACCOUNT_FACTORY_ADDRESS = + "0x9406Cc6185a346906296840746125a0E44976454"; + +describe("Simple Account Tests", () => { + const ownerAccount = mnemonicToAccount(OWNER_MNEMONIC); + const owner: SimpleSmartAccountOwner = { + signMessage: async (msg) => + ownerAccount.signMessage({ + message: { raw: toHex(msg) }, + }), + getAddress: async () => ownerAccount.address, + }; + const chain = polygonMumbai; + const signer = new AlchemyProvider({ + apiKey: API_KEY, + chain, + entryPointAddress: ENTRYPOINT_ADDRESS, + }).connect( + (provider) => + new SimpleSmartContractAccount({ + entryPointAddress: ENTRYPOINT_ADDRESS, + chain, + owner, + factoryAddress: SIMPLE_ACCOUNT_FACTORY_ADDRESS, + rpcClient: provider, + }) + ); + + it("should succesfully get counterfactual address", async () => { + expect(await signer.getAddress()).toMatchInlineSnapshot( + `"0xb856DBD4fA1A79a46D426f537455e7d3E79ab7c4"` + ); + }); + + it("should correctly sign the message", async () => { + expect( + // TODO: expose sign message on the provider too + await signer.account.signMessage( + "0xa70d0af2ebb03a44dcd0714a8724f622e3ab876d0aa312f0ee04823285d6fb1b" + ) + ).toBe( + "0xd16f93b584fbfdc03a5ee85914a1f29aa35c44fea5144c387ee1040a3c1678252bf323b7e9c3e9b4dfd91cca841fc522f4d3160a1e803f2bf14eb5fa037aae4a1b" + ); + }); + + it("should execute successfully", async () => { + const result = signer.sendUserOperation({ + target: await signer.getAddress(), + data: "0x", + }); + + await expect(result).resolves.not.toThrowError(); + }); + + it("should fail to execute if account address is not deployed and not correct", async () => { + const accountAddress = "0xc33AbD9621834CA7c6Fc9f9CC3c47b9c17B03f9F"; + const newSigner = new AlchemyProvider({ + apiKey: API_KEY, + chain, + entryPointAddress: ENTRYPOINT_ADDRESS, + }).connect( + (provider) => + new SimpleSmartContractAccount({ + entryPointAddress: ENTRYPOINT_ADDRESS, + chain, + owner, + factoryAddress: SIMPLE_ACCOUNT_FACTORY_ADDRESS, + rpcClient: provider, + accountAddress, + }) + ); + + const result = newSigner.sendUserOperation({ + target: await newSigner.getAddress(), + data: "0x", + }); + + await expect(result).rejects.toThrowError(); + }); + + it("should successfully execute with alchemy paymaster info", async () => { + // TODO: this is super hacky right now + // we have to wait for the test above to run and be confirmed so that this one submits successfully using the correct nonce + // one way we could do this is by batching the two UOs together + await new Promise((resolve) => setTimeout(resolve, 7500)); + const newSigner = signer.withAlchemyGasManager({ + provider: signer.rpcClient, + policyId: PAYMASTER_POLICY_ID, + entryPoint: ENTRYPOINT_ADDRESS, + }); + + const result = newSigner.sendUserOperation({ + target: await newSigner.getAddress(), + data: "0x", + }); + + await expect(result).resolves.not.toThrowError(); + }, 10000); + + it("should correctly encode batch transaction data", async () => { + const account = signer.account; + const data = [ + { + target: "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + data: "0xdeadbeef", + }, + { + target: "0x8ba1f109551bd432803012645ac136ddd64dba72", + data: "0xcafebabe", + }, + ] satisfies BatchUserOperationCallData; + + expect(await account.encodeBatchExecute(data)).toMatchInlineSnapshot( + '"0x18dfb3c7000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef0000000000000000000000008ba1f109551bd432803012645ac136ddd64dba720000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004deadbeef000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004cafebabe00000000000000000000000000000000000000000000000000000000"' + ); + }); +}); diff --git a/packages/alchemy/src/chains.ts b/packages/alchemy/src/chains.ts new file mode 100644 index 0000000000..a49b6fbc62 --- /dev/null +++ b/packages/alchemy/src/chains.ts @@ -0,0 +1,50 @@ +import type { Chain } from "viem"; +import { + arbitrum, + arbitrumGoerli, + goerli, + mainnet, + optimism, + optimismGoerli, + polygon, + polygonMumbai, + sepolia, +} from "viem/chains"; +import { GasFeeStrategy, type GasFeeMode } from "./middleware/gas-fees"; + +export const SupportedChains = new Map([ + [polygonMumbai.id, polygonMumbai], + [polygon.id, polygon], + [mainnet.id, mainnet], + [sepolia.id, sepolia], + [goerli.id, goerli], + [arbitrumGoerli.id, arbitrumGoerli], + [arbitrum.id, arbitrum], + [optimism.id, optimism], + [optimismGoerli.id, optimismGoerli], +]); + +const defineChainStrategy = ( + chainId: number, + strategy: GasFeeStrategy, + value: GasFeeMode["value"] +): [number, GasFeeMode] => { + return [chainId, { strategy, value }]; +}; + +export const ChainFeeStrategies: Map = new Map< + number, + GasFeeMode +>([ + // testnets + defineChainStrategy(goerli.id, GasFeeStrategy.FIXED, 0n), + defineChainStrategy(sepolia.id, GasFeeStrategy.FIXED, 0n), + defineChainStrategy(polygonMumbai.id, GasFeeStrategy.FIXED, 0n), + defineChainStrategy(optimismGoerli.id, GasFeeStrategy.FIXED, 0n), + defineChainStrategy(arbitrumGoerli.id, GasFeeStrategy.FIXED, 0n), + // mainnets + defineChainStrategy(mainnet.id, GasFeeStrategy.PRIORITY_FEE_PERCENTAGE, 25n), + defineChainStrategy(polygon.id, GasFeeStrategy.PRIORITY_FEE_PERCENTAGE, 25n), + defineChainStrategy(optimism.id, GasFeeStrategy.BASE_FEE_PERCENTAGE, 5n), + defineChainStrategy(arbitrum.id, GasFeeStrategy.BASE_FEE_PERCENTAGE, 5n), +]); diff --git a/packages/alchemy/src/index.ts b/packages/alchemy/src/index.ts new file mode 100644 index 0000000000..c998fd8018 --- /dev/null +++ b/packages/alchemy/src/index.ts @@ -0,0 +1,11 @@ +export { + GasFeeStrategy, + withAlchemyGasFeeEstimator, +} from "./middleware/gas-fees"; +export type { GasFeeMode } from "./middleware/gas-fees"; + +export { withAlchemyGasManager } from "./middleware/gas-manager"; + +export { SupportedChains } from "./chains"; +export { AlchemyProvider } from "./provider"; +export type { AlchemyProviderConfig } from "./provider"; diff --git a/packages/alchemy/src/middleware/gas-fees.ts b/packages/alchemy/src/middleware/gas-fees.ts new file mode 100644 index 0000000000..c9249c77e1 --- /dev/null +++ b/packages/alchemy/src/middleware/gas-fees.ts @@ -0,0 +1,55 @@ +import type { AlchemyProvider } from "../provider"; + +export enum GasFeeStrategy { + DEFAULT = "DEFAULT", + FIXED = "FIXED", + BASE_FEE_PERCENTAGE = "BASE_FEE_PERCENTAGE", + PRIORITY_FEE_PERCENTAGE = "PRIORITY_FEE_PERCENTAGE", +} + +export interface GasFeeMode { + strategy: GasFeeStrategy; + value: bigint; +} + +export const withAlchemyGasFeeEstimator = ( + provider: AlchemyProvider, + feeMode: GasFeeMode +): AlchemyProvider => { + if (feeMode.strategy === GasFeeStrategy.DEFAULT) { + return provider; + } + + provider.withFeeDataGetter(async () => { + const block = await provider.rpcClient.getBlock({ blockTag: "latest" }); + const baseFeePerGas = block.baseFeePerGas; + if (baseFeePerGas == null) { + throw new Error("baseFeePerGas is null"); + } + const maxPriorityFeePerGas = BigInt( + await provider.rpcClient.getMaxPriorityFeePerGas() + ); + // add 25% overhead to ensure mine + const baseFeeScaled = (baseFeePerGas * 5n) / 4n; + + const prioFee = ((): bigint => { + switch (feeMode.strategy) { + case GasFeeStrategy.FIXED: + return maxPriorityFeePerGas + feeMode.value; + case GasFeeStrategy.BASE_FEE_PERCENTAGE: + return (baseFeeScaled * feeMode.value) / 100n; + case GasFeeStrategy.PRIORITY_FEE_PERCENTAGE: + // add 10% to required priority fee to ensure mine + return (maxPriorityFeePerGas * (110n + feeMode.value)) / 100n; + default: + throw new Error("fee mode not supported"); + } + })(); + + return { + maxPriorityFeePerGas, + maxFeePerGas: baseFeeScaled + prioFee, + }; + }); + return provider; +}; diff --git a/packages/core/src/middleware/alchemy-paymaster.ts b/packages/alchemy/src/middleware/gas-manager.ts similarity index 56% rename from packages/core/src/middleware/alchemy-paymaster.ts rename to packages/alchemy/src/middleware/gas-manager.ts index ed11bfdd35..20f8170c0b 100644 --- a/packages/core/src/middleware/alchemy-paymaster.ts +++ b/packages/alchemy/src/middleware/gas-manager.ts @@ -1,9 +1,11 @@ +import { + deepHexlify, + resolveProperties, + type ConnectedSmartAccountProvider, + type PublicErc4337Client, + type UserOperationRequest, +} from "@alchemy/aa-core"; import type { Address, Hex, Transport } from "viem"; -import type { BaseSmartContractAccount } from "../account/base.js"; -import type { PublicErc4337Client } from "../client/types.js"; -import type { SmartAccountProvider } from "../provider/base.js"; -import type { UserOperationRequest } from "../types.js"; -import { deepHexlify, resolveProperties } from "../utils.js"; type ClientWithAlchemyMethods = PublicErc4337Client & { request: PublicErc4337Client["request"] & @@ -39,7 +41,7 @@ type ClientWithAlchemyMethods = PublicErc4337Client & { }["request"]; }; -export interface AlchemyPaymasterConfig { +export interface AlchemyGasManagerConfig { policyId: string; entryPoint: Address; provider: PublicErc4337Client; @@ -55,12 +57,10 @@ export interface AlchemyPaymasterConfig { */ export const withAlchemyGasManager = < T extends Transport, - Provider extends SmartAccountProvider & { - account: BaseSmartContractAccount; - } + Provider extends ConnectedSmartAccountProvider >( provider: Provider, - config: AlchemyPaymasterConfig + config: AlchemyGasManagerConfig ): Provider => { return ( provider @@ -96,3 +96,48 @@ export const withAlchemyGasManager = < }) ); }; + +/** + * This is the middleware for calling the alchemy paymaster API which does not estimate gas. It's recommend to use + * {@link withAlchemyGasManager} instead which handles estimating gas + getting paymaster data in one go. + * + * @param config {@link AlchemyPaymasterConfig} + * @returns middleware overrides for paymaster middlewares + */ +export const alchemyPaymasterAndDataMiddleware = ( + config: AlchemyGasManagerConfig +): Parameters< + ConnectedSmartAccountProvider["withPaymasterMiddleware"] +>["0"] => ({ + dummyPaymasterDataMiddleware: async (_struct) => { + switch (config.provider.chain.id) { + case 1: + case 137: + case 42161: + return { + paymasterAndData: + "0x4Fd9098af9ddcB41DA48A1d78F91F1398965addcfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c", + }; + default: + return { + paymasterAndData: + "0xc03aac639bb21233e0139381970328db8bceeb67fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c", + }; + } + }, + paymasterDataMiddleware: async (struct) => { + const { paymasterAndData } = await ( + config.provider as ClientWithAlchemyMethods + ).request({ + method: "alchemy_requestPaymasterAndData", + params: [ + { + policyId: config.policyId, + entryPoint: config.entryPoint, + userOperation: deepHexlify(await resolveProperties(struct)), + }, + ], + }); + return { paymasterAndData }; + }, +}); diff --git a/packages/alchemy/src/provider.ts b/packages/alchemy/src/provider.ts new file mode 100644 index 0000000000..61df881eea --- /dev/null +++ b/packages/alchemy/src/provider.ts @@ -0,0 +1,96 @@ +import { + BaseSmartContractAccount, + SmartAccountProvider, + deepHexlify, + resolveProperties, + type AccountMiddlewareFn, + type SmartAccountProviderOpts, +} from "@alchemy/aa-core"; +import type { Address, Chain, HttpTransport } from "viem"; +import { + arbitrum, + arbitrumGoerli, + optimism, + optimismGoerli, +} from "viem/chains"; +import { ChainFeeStrategies, SupportedChains } from "./chains"; +import { + GasFeeStrategy, + withAlchemyGasFeeEstimator, +} from "./middleware/gas-fees"; +import { + withAlchemyGasManager, + type AlchemyGasManagerConfig, +} from "./middleware/gas-manager"; + +export type AlchemyProviderConfig = { + apiKey: string; + chain: Chain | number; + entryPointAddress: Address; + account?: BaseSmartContractAccount; + opts?: SmartAccountProviderOpts; +}; + +export class AlchemyProvider extends SmartAccountProvider { + constructor({ + apiKey, + chain, + entryPointAddress, + account, + opts, + }: AlchemyProviderConfig) { + const _chain = + typeof chain === "number" ? SupportedChains.get(chain) : chain; + if (!_chain || !_chain.rpcUrls["alchemy"]) { + throw new Error(`AlchemyProvider: chain (${chain}) not supported`); + } + + const rpcUrl = `${_chain.rpcUrls.alchemy.http[0]}/${apiKey}`; + super(rpcUrl, entryPointAddress, _chain, account, opts); + + withAlchemyGasFeeEstimator( + this, + ChainFeeStrategies.get(_chain.id) ?? { + strategy: GasFeeStrategy.DEFAULT, + value: 0n, + } + ); + } + + gasEstimator: AccountMiddlewareFn = async (struct) => { + const request = deepHexlify(await resolveProperties(struct)); + const estimates = await this.rpcClient.estimateUserOperationGas( + request, + this.entryPointAddress + ); + + // On Arbitrum and Optimism, we need to increase the preVerificationGas by 10% + // to ensure the transaction is mined + if ( + new Set([ + arbitrum.id, + arbitrumGoerli.id, + optimism.id, + optimismGoerli.id, + ]).has(this.chain.id) + ) { + estimates.preVerificationGas = + (BigInt(estimates.preVerificationGas) * 110n) / 100n; + } + + return { + ...struct, + ...estimates, + }; + }; + + withAlchemyGasManager(config: AlchemyGasManagerConfig) { + if (!this.isConnected()) { + throw new Error( + "AlchemyProvider: account is not set, did you call `connect` first?" + ); + } + + return withAlchemyGasManager(this, config); + } +} diff --git a/packages/alchemy/tsconfig.build.json b/packages/alchemy/tsconfig.build.json new file mode 100644 index 0000000000..345f8f3fab --- /dev/null +++ b/packages/alchemy/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "typescript-template/build.json", + "exclude": ["node_modules", "**/*/__tests__", "vitest.config.ts"], + "include": ["src"], + "compilerOptions": { + "sourceMap": true + } +} diff --git a/packages/alchemy/tsconfig.json b/packages/alchemy/tsconfig.json new file mode 100644 index 0000000000..748018d6a6 --- /dev/null +++ b/packages/alchemy/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "typescript-template/base.json" +} diff --git a/packages/alchemy/vitest.config.ts b/packages/alchemy/vitest.config.ts new file mode 100644 index 0000000000..0d61b56b29 --- /dev/null +++ b/packages/alchemy/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineProject } from "vitest/config"; + +export default defineProject({ + test: { + globals: true, + setupFiles: ["../../.vitest/setupTests.ts"], + name: "core", + }, +}); diff --git a/packages/core/src/__tests__/simple-account.test.ts b/packages/core/src/__tests__/simple-account.test.ts index c4b2d9d9e4..e9289a69ff 100644 --- a/packages/core/src/__tests__/simple-account.test.ts +++ b/packages/core/src/__tests__/simple-account.test.ts @@ -5,14 +5,12 @@ import { SimpleSmartContractAccount, type SimpleSmartAccountOwner, } from "../account/simple.js"; -import { withAlchemyGasManager } from "../middleware/alchemy-paymaster.js"; import { SmartAccountProvider } from "../provider/base.js"; import type { BatchUserOperationCallData } from "../types.js"; const ENTRYPOINT_ADDRESS = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"; const API_KEY = process.env.API_KEY!; const OWNER_MNEMONIC = process.env.OWNER_MNEMONIC!; -const PAYMASTER_POLICY_ID = process.env.PAYMASTER_POLICY_ID!; const SIMPLE_ACCOUNT_FACTORY_ADDRESS = "0x9406Cc6185a346906296840746125a0E44976454"; @@ -21,7 +19,7 @@ describe("Simple Account Tests", () => { const owner: SimpleSmartAccountOwner = { signMessage: async (msg) => ownerAccount.signMessage({ - message: toHex(msg), + message: { raw: toHex(msg) }, }), getAddress: async () => ownerAccount.address, }; @@ -93,25 +91,6 @@ describe("Simple Account Tests", () => { await expect(result).rejects.toThrowError(); }); - it("should successfully execute with alchemy paymaster info", async () => { - // TODO: this is super hacky right now - // we have to wait for the test above to run and be confirmed so that this one submits successfully using the correct nonce - // one way we could do this is by batching the two UOs together - await new Promise((resolve) => setTimeout(resolve, 7500)); - const newSigner = withAlchemyGasManager(signer, { - provider: signer.rpcClient, - policyId: PAYMASTER_POLICY_ID, - entryPoint: ENTRYPOINT_ADDRESS, - }); - - const result = newSigner.sendUserOperation({ - target: await newSigner.getAddress(), - data: "0x", - }); - - await expect(result).resolves.not.toThrowError(); - }, 10000); - it("should correctly encode batch transaction data", async () => { const account = signer.account; const data = [ diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e421c2fa78..cf33015df6 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -21,11 +21,11 @@ export { } from "./client/create-client.js"; export type * from "./client/types.js"; -export { withAlchemyGasManager } from "./middleware/alchemy-paymaster.js"; -export type { AlchemyPaymasterConfig } from "./middleware/alchemy-paymaster.js"; - export { SmartAccountProvider, noOpMiddleware } from "./provider/base.js"; -export type { SmartAccountProviderOpts } from "./provider/base.js"; +export type { + ConnectedSmartAccountProvider, + SmartAccountProviderOpts, +} from "./provider/base.js"; export type * from "./provider/types.js"; export type * from "./types.js"; diff --git a/packages/core/src/provider/base.ts b/packages/core/src/provider/base.ts index 70e88e9858..c31a9b35ce 100644 --- a/packages/core/src/provider/base.ts +++ b/packages/core/src/provider/base.ts @@ -74,20 +74,26 @@ const minPriorityFeePerBidDefaults = new Map([ [arbitrumGoerli.id, 10_000_000n], ]); +export type ConnectedSmartAccountProvider< + TTransport extends SupportedTransports = Transport +> = SmartAccountProvider & { + account: BaseSmartContractAccount; +}; + export class SmartAccountProvider< TTransport extends SupportedTransports = Transport > implements ISmartAccountProvider { private txMaxRetries: number; private txRetryIntervalMs: number; - private minPriorityFeePerBid: bigint; + minPriorityFeePerBid: bigint; rpcClient: PublicErc4337Client; constructor( rpcProvider: string | PublicErc4337Client, - private entryPointAddress: Address, - private chain: Chain, - readonly account?: BaseSmartContractAccount, + protected entryPointAddress: Address, + protected chain: Chain, + readonly account?: BaseSmartContractAccount, opts?: SmartAccountProviderOpts ) { this.txMaxRetries = opts?.txMaxRetries ?? 5; @@ -361,6 +367,10 @@ export class SmartAccountProvider< return this as this & { account: typeof account }; } + isConnected(): this is ConnectedSmartAccountProvider { + return this.account !== undefined; + } + private overrideMiddlewareFunction = ( override: AccountMiddlewareOverrideFn ): AccountMiddlewareFn => { diff --git a/packages/ethers/src/__tests__/simple-account.test.ts b/packages/ethers/src/__tests__/simple-account.test.ts index 23ea4725f7..6113c1d16f 100644 --- a/packages/ethers/src/__tests__/simple-account.test.ts +++ b/packages/ethers/src/__tests__/simple-account.test.ts @@ -1,8 +1,4 @@ -import { - alchemyPaymasterAndDataMiddleware, - getChain, - SimpleSmartContractAccount, -} from "@alchemy/aa-core"; +import { getChain, SimpleSmartContractAccount } from "@alchemy/aa-core"; import { Wallet } from "@ethersproject/wallet"; import { Alchemy, Network } from "alchemy-sdk"; import { EthersProviderAdapter } from "../provider-adapter.js"; @@ -11,7 +7,6 @@ import { convertWalletToAccountSigner } from "../utils.js"; const ENTRYPOINT_ADDRESS = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"; const API_KEY = process.env.API_KEY!; const OWNER_MNEMONIC = process.env.OWNER_MNEMONIC!; -const PAYMASTER_POLICY_ID = process.env.PAYMASTER_POLICY_ID!; const SIMPLE_ACCOUNT_FACTORY_ADDRESS = "0x9406Cc6185a346906296840746125a0E44976454"; @@ -85,25 +80,4 @@ describe("Simple Account Tests", async () => { await expect(result).rejects.toThrowError(); }); - - it("should successfully execute with alchemy paymaster info", async () => { - // TODO: this is super hacky right now - // we have to wait for the test above to run and be confirmed so that this one submits successfully using the correct nonce - // one way we could do this is by batching the two UOs together - await new Promise((resolve) => setTimeout(resolve, 7500)); - signer.withPaymasterMiddleware( - alchemyPaymasterAndDataMiddleware({ - provider: signer.getPublicErc4337Client(), - policyId: PAYMASTER_POLICY_ID, - entryPoint: ENTRYPOINT_ADDRESS, - }) - ); - - const result = signer.sendUserOperation({ - target: (await signer.getAddress()) as `0x${string}`, - data: "0x", - }); - - await expect(result).resolves.not.toThrowError(); - }, 10000); });