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: add alchemy sub-package #22

Merged
merged 1 commit into from
Jun 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this depends on the specific value of the private key in the env var? If so, we could use a test key for this purpose (like the output of a string's hash, similar to foundry's makeAddrAndKey).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea it does depend on a specific key in the env var.

I can make that change to test signing, but will still need some live key with funds in it for testing the send user op methods

);
});

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