Skip to content

Commit

Permalink
feat: add alchemy sub-package (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
moldy530 committed Jun 12, 2023
1 parent 4946408 commit e7fc1aa
Show file tree
Hide file tree
Showing 16 changed files with 584 additions and 68 deletions.
81 changes: 80 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 .",
Expand Down
65 changes: 65 additions & 0 deletions packages/alchemy/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
131 changes: 131 additions & 0 deletions packages/alchemy/src/__tests__/simple-account.test.ts
Original file line number Diff line number Diff line change
@@ -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"'
);
});
});
50 changes: 50 additions & 0 deletions packages/alchemy/src/chains.ts
Original file line number Diff line number Diff line change
@@ -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<number, Chain>([
[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<number, GasFeeMode> = 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),
]);
11 changes: 11 additions & 0 deletions packages/alchemy/src/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Loading

0 comments on commit e7fc1aa

Please sign in to comment.