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 support for the new alchemy paymaster endpoint #14

Merged
merged 1 commit into from
Jun 2, 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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,17 @@ 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.

### 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:

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.

## Contributing

1. clone the repo
Expand Down
18 changes: 9 additions & 9 deletions packages/core/src/__tests__/simple-account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
SimpleSmartContractAccount,
type SimpleSmartAccountOwner,
} from "../account/simple.js";
import { alchemyPaymasterAndDataMiddleware } from "../middleware/alchemy-paymaster.js";
import { withAlchemyGasManager } from "../middleware/alchemy-paymaster.js";
import { SmartAccountProvider } from "../provider/base.js";
import type { BatchUserOperationCallData } from "../types.js";

Expand Down Expand Up @@ -98,13 +98,11 @@ describe("Simple Account Tests", () => {
// 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.withPaymasterMiddleware(
alchemyPaymasterAndDataMiddleware({
provider: signer.rpcClient,
policyId: PAYMASTER_POLICY_ID,
entryPoint: ENTRYPOINT_ADDRESS,
})
);
const newSigner = withAlchemyGasManager(signer, {
provider: signer.rpcClient,
policyId: PAYMASTER_POLICY_ID,
entryPoint: ENTRYPOINT_ADDRESS,
});

const result = newSigner.sendUserOperation({
target: await newSigner.getAddress(),
Expand All @@ -127,6 +125,8 @@ describe("Simple Account Tests", () => {
},
] satisfies BatchUserOperationCallData;

expect(await account.encodeBatchExecute(data)).toMatchInlineSnapshot('"0x18dfb3c7000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef0000000000000000000000008ba1f109551bd432803012645ac136ddd64dba720000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004deadbeef000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004cafebabe00000000000000000000000000000000000000000000000000000000"');
expect(await account.encodeBatchExecute(data)).toMatchInlineSnapshot(
'"0x18dfb3c7000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef0000000000000000000000008ba1f109551bd432803012645ac136ddd64dba720000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000004deadbeef000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004cafebabe00000000000000000000000000000000000000000000000000000000"'
);
});
});
5 changes: 4 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ export {
} from "./client/create-client.js";
export type * from "./client/types.js";

export { alchemyPaymasterAndDataMiddleware } from "./middleware/alchemy-paymaster.js";
export {
alchemyPaymasterAndDataMiddleware,
withAlchemyGasManager,
} from "./middleware/alchemy-paymaster.js";
export type { AlchemyPaymasterConfig } from "./middleware/alchemy-paymaster.js";

export { SmartAccountProvider, noOpMiddleware } from "./provider/base.js";
Expand Down
88 changes: 84 additions & 4 deletions packages/core/src/middleware/alchemy-paymaster.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { Address, Hex } from "viem";
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 { ISmartAccountProvider } from "../provider/types.js";
import type { UserOperationRequest, UserOperationStruct } from "../types.js";
import { deepHexlify, resolveProperties } from "../utils.js";
import type { ISmartAccountProvider } from "../provider/types.js";

type ClientWithAlchemyMethod = PublicErc4337Client & {
type ClientWithAlchemyMethods = PublicErc4337Client & {
request: PublicErc4337Client["request"] &
{
request(args: {
Expand All @@ -17,6 +19,24 @@ type ClientWithAlchemyMethod = PublicErc4337Client & {
}
];
}): Promise<{ paymasterAndData: Hex }>;
request(args: {
method: "alchemy_requestGasAndPaymasterAndData";
params: [
{
policyId: string;
entryPoint: Address;
userOperation: UserOperationRequest;
dummySignature: Hex;
}
];
}): Promise<{
paymasterAndData: Hex;
callGasLimit: Hex;
verificationGasLimit: Hex;
preVerificationGas: Hex;
maxFeePerGas: Hex;
maxPriorityFeePerGas: Hex;
}>;
}["request"];
};

Expand All @@ -26,6 +46,66 @@ export interface AlchemyPaymasterConfig {
provider: PublicErc4337Client;
}

/**
* This uses the alchemy RPC method: `alchemy_requestGasAndPaymasterAndData` to get all of the gas estimates + paymaster data
* in one RPC call. It will no-op the gas estimator and fee data getter middleware and set a custom middleware that makes the RPC call
*
* @param provider - the smart account provider to override to use the alchemy paymaster
* @param config - the alchemy paymaster configuration
* @returns the provider augmented to use the alchemy paymaster
*/
export const withAlchemyGasManager = <
T extends Transport,
Provider extends SmartAccountProvider<T> & {
account: BaseSmartContractAccount<T>;
}
>(
provider: Provider,
config: AlchemyPaymasterConfig
): Provider => {
return (
provider
// no-op gas estimator
.withGasEstimator(async () => ({
callGasLimit: 0n,
preVerificationGas: 0n,
verificationGasLimit: 0n,
}))
// no-op gas manager because the alchemy api will do it
.withFeeDataGetter(async () => ({
maxFeePerGas: 0n,
maxPriorityFeePerGas: 0n,
}))
.withCustomMiddleware(async (struct) => {
const result = await (
config.provider as ClientWithAlchemyMethods
).request({
method: "alchemy_requestGasAndPaymasterAndData",
params: [
{
policyId: config.policyId,
entryPoint: config.entryPoint,
userOperation: deepHexlify(await resolveProperties(struct)),
dummySignature: provider.account.getDummySignature(),
},
],
});

return {
...struct,
...result,
};
})
);
};

/**
* 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: AlchemyPaymasterConfig
): Parameters<ISmartAccountProvider["withPaymasterMiddleware"]>["0"] => ({
Expand All @@ -37,7 +117,7 @@ export const alchemyPaymasterAndDataMiddleware = (
},
paymasterDataMiddleware: async (struct: UserOperationStruct) => {
const { paymasterAndData } = await (
config.provider as ClientWithAlchemyMethod
config.provider as ClientWithAlchemyMethods
).request({
method: "alchemy_requestPaymasterAndData",
params: [
Expand Down
11 changes: 10 additions & 1 deletion packages/core/src/provider/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@ export class SmartAccountProvider<
this.dummyPaymasterDataMiddleware,
this.gasEstimator,
this.feeDataGetter,
this.paymasterDataMiddleware
this.paymasterDataMiddleware,
this.customMiddleware ?? noOpMiddleware
)({
initCode,
sender: this.getAddress(),
Expand Down Expand Up @@ -299,6 +300,8 @@ export class SmartAccountProvider<
return struct;
};

readonly customMiddleware?: AccountMiddlewareFn | undefined = undefined;

withPaymasterMiddleware = (overrides: {
dummyPaymasterDataMiddleware?: PaymasterAndDataMiddleware;
paymasterDataMiddleware?: PaymasterAndDataMiddleware;
Expand Down Expand Up @@ -334,6 +337,12 @@ export class SmartAccountProvider<
return this;
};

withCustomMiddleware = (override: AccountMiddlewareFn): this => {
defineReadOnly(this, "customMiddleware", override);

return this;
};

connect(
fn: (provider: PublicErc4337Client<TTransport>) => BaseSmartContractAccount
): this & { account: BaseSmartContractAccount } {
Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/provider/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface ISmartAccountProvider<
readonly paymasterDataMiddleware: AccountMiddlewareFn;
readonly gasEstimator: AccountMiddlewareFn;
readonly feeDataGetter: AccountMiddlewareFn;
readonly customMiddleware?: AccountMiddlewareFn;

readonly account?: BaseSmartContractAccount;

Expand Down Expand Up @@ -136,6 +137,16 @@ export interface ISmartAccountProvider<
*/
withFeeDataGetter: (override: FeeDataMiddleware) => this;

/**
* This adds a final middleware step to the middleware stack that runs right before signature verification.
* This can be used if you have an RPC that does most of the functions of the other middlewares for you and
* you want to delegate that work to that RPC instead of chaining together multiple RPC calls via the default middlwares.
*
* @param override - the UO transform function to run
* @returns
*/
withCustomMiddleware: (override: AccountMiddlewareFn) => this;

/**
* Sets the current account to the account returned by the given function. The function parameter
* provides the public rpc client that is used by this provider so the account can make RPC calls.
Expand Down
6 changes: 6 additions & 0 deletions packages/ethers/src/account-signer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
BaseSmartContractAccount,
resolveProperties,
type AccountMiddlewareFn,
type FeeDataMiddleware,
type GasEstimatorMiddleware,
type PaymasterAndDataMiddleware,
Expand Down Expand Up @@ -74,6 +75,11 @@ export class AccountSigner extends Signer {
return this;
};

withCustomMiddleware = (override: AccountMiddlewareFn): this => {
this.provider.withCustomMiddleware(override);
return this;
};

async sendTransaction(
transaction: Deferrable<TransactionRequest>
): Promise<TransactionResponse> {
Expand Down
12 changes: 9 additions & 3 deletions packages/ethers/src/provider-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import {
BaseSmartContractAccount,
SmartAccountProvider,
getChain,
type AccountMiddlewareFn,
type Address,
type HttpTransport,
type PublicErc4337Client,
type PaymasterAndDataMiddleware,
type FeeDataMiddleware,
type GasEstimatorMiddleware,
type HttpTransport,
type PaymasterAndDataMiddleware,
type PublicErc4337Client,
} from "@alchemy/aa-core";
import { defineReadOnly } from "@ethersproject/properties";
import { JsonRpcProvider } from "@ethersproject/providers";
Expand Down Expand Up @@ -76,6 +77,11 @@ export class EthersProviderAdapter extends JsonRpcProvider {
return this;
};

withCustomMiddleware = (override: AccountMiddlewareFn): this => {
this.accountProvider.withCustomMiddleware(override);
return this;
};

getPublicErc4337Client(): PublicErc4337Client {
return this.accountProvider.rpcClient;
}
Expand Down