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: eip 7702 support and demo #1268

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ site/.vitepress/cache/**/*

/examples/*
!/examples/ui-demo
/examples/ui-demo/contracts
/examples/ui-demo/.next/*

**/.turbo/*
Expand Down
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,9 @@
[submodule "examples/embedded-accounts-quickstart"]
path = examples/embedded-accounts-quickstart
url = https://github.com/alchemyplatform/embedded-accounts-quickstart
[submodule "examples/ui-demo/contracts/lib/openzeppelin-contracts"]
path = examples/ui-demo/contracts/lib/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
[submodule "examples/ui-demo/contracts/lib/forge-std"]
path = examples/ui-demo/contracts/lib/forge-std
url = https://github.com/foundry-rs/forge-std
2 changes: 1 addition & 1 deletion .vitest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"prool": "^0.0.15",
"tar": "^7.4.1",
"typescript-template": "*",
"viem": "2.20.0"
"viem": "2.22.6"
},
"dependencies": {
"@aa-sdk/core": "^4.0.0-alpha.8"
Expand Down
2 changes: 1 addition & 1 deletion aa-sdk/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"zod": "^3.22.4"
},
"peerDependencies": {
"viem": "^2.20.0"
"viem": "^2.22.6"
},
"repository": {
"type": "git",
Expand Down
5 changes: 4 additions & 1 deletion aa-sdk/core/src/account/smartContractAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,10 @@ export type ToSmartContractAccountParams<
// if not provided, will default to just using signMessage over the Hex
signUserOperationHash?: (uoHash: Hex) => Promise<Hex>;
encodeUpgradeToAndCall?: (params: UpgradeToAndCallParams) => Promise<Hex>;
} & Omit<CustomSource, "signTransaction" | "address">;
} & Omit<
CustomSource,
"signTransaction" | "address" | "experimental_signAuthorization"
>;
// [!endregion ToSmartContractAccountParams]

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,17 @@ export async function _sendUserOperation<
overrides,
});

// TODO: check if this is the place to add the auth tuple, if not, remove
// if (
// request.signature.startsWith(
// // TODO: put this in a constant
// "0x00000000000000000000000000000000000000000000000001ff00"
// ) &&
// account.signAuthorization
// ) {
// request.authorizationTuple = await account.signAuthorization();
// }

return {
hash: await client.sendRawUserOperation(request, entryPoint.address),
request,
Expand Down
5 changes: 4 additions & 1 deletion aa-sdk/core/src/client/decorators/bundlerClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ export type BundlerActions = {
estimateUserOperationGas<
TEntryPointVersion extends EntryPointVersion = EntryPointVersion
>(
request: UserOperationRequest<TEntryPointVersion>,
request: Extract<
UserOperationRequest<TEntryPointVersion>,
{ authorizationContract?: Address }
>,
entryPoint: Address,
stateOverride?: StateOverride
): Promise<UserOperationEstimateGasResponse<TEntryPointVersion>>;
Expand Down
2 changes: 1 addition & 1 deletion aa-sdk/core/src/entrypoint/0.7.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const packUserOperation = (request: UserOperationRequest<"0.7.0">): Hex => {
{ type: "bytes32" },
],
[
request.sender as Address,
request.sender,
hexToBigInt(request.nonce),
keccak256(initCode),
keccak256(request.callData),
Expand Down
2 changes: 2 additions & 0 deletions aa-sdk/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ export {
} from "./errors/useroperation.js";
export { LogLevel, Logger } from "./logger.js";
export { middlewareActions } from "./middleware/actions.js";
export { default7702UserOpSigner } from "./middleware/defaults/7702signer.js";
export { default7702GasEstimator } from "./middleware/defaults/7702gasEstimator.js";
export { defaultFeeEstimator } from "./middleware/defaults/feeEstimator.js";
export { defaultGasEstimator } from "./middleware/defaults/gasEstimator.js";
export { defaultPaymasterAndData } from "./middleware/defaults/paymasterAndData.js";
Expand Down
44 changes: 44 additions & 0 deletions aa-sdk/core/src/middleware/defaults/7702gasEstimator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { AccountNotFoundError } from "../../errors/account.js";
import type { UserOperationStruct } from "../../types.js";
import type { ClientMiddlewareFn } from "../types";
import { defaultGasEstimator } from "./gasEstimator.js";

/**
* Asynchronously processes a given struct and parameters, ensuring the account and entry point compatibility before modifying the struct with an authorization contract and estimating gas.
*
* @param {UserOperationStruct} struct The user operation structure to estimate gas over
* @param {*} params The parameters containing an account or a client with an account.
* @throws {AccountNotFoundError} If no account is found in the parameters.
* @throws {Error} If the account's entry point version is not 0.7.0.
* @returns {Promise<UserOperationStruct>} A promise that resolves after estimating gas with the modified struct.
*/
export const default7702GasEstimator: ClientMiddlewareFn = async (
struct,
params
) => {
const account = params.account ?? params.client.account;
if (!account) {
throw new AccountNotFoundError();
}

const entryPoint = account.getEntryPoint();
if (entryPoint.version !== "0.7.0") {
throw new Error(
"This middleware is only compatible with EntryPoint v0.7.0"
);
}

// todo: this is currently overloading the meaning of the getImplementationAddress method, replace with a dedicated method or clarify intention in docs
const implementationAddress = await account.getImplementationAddress();

// todo: do we need to omit this from estimation if the account is already 7702 delegated? Not omitting for now.

(struct as UserOperationStruct<"0.7.0">).authorizationContract =
implementationAddress;

const estimatedUO = await defaultGasEstimator(params.client)(struct, params);

estimatedUO.authorizationContract = undefined; // Strip out authorizationContract after estimation.

return estimatedUO;
};
79 changes: 79 additions & 0 deletions aa-sdk/core/src/middleware/defaults/7702signer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { toHex } from "viem";
import { isSmartAccountWithSigner } from "../../account/smartContractAccount.js";
import { AccountNotFoundError } from "../../errors/account.js";
import { ChainNotFoundError } from "../../errors/client.js";
import type { ClientMiddlewareFn } from "../types";
import { defaultUserOpSigner } from "./userOpSigner.js";

/**
* Provides a default middleware function for signing user operations with a client account when using ERC-7702 to upgrade local accounts to smart accounts.
* If the SmartAccount doesn't support `signAuthorization`, then this just runs the default UserOpSigner middleware
*
* @param {UserOperationStruct} struct The user operation structure to be signed
* @param {*} params The middleware context containing the client and account information
* @param {Client} params.client The client object, which should include account and chain information
* @param {Account} [params.account] Optional, the account used for signing, defaults to the client's account if not provided
* @returns {Promise<UserOperationStruct>} A promise that resolves to the signed user operation structure
*/
export const default7702UserOpSigner: ClientMiddlewareFn = async (
struct,
params
) => {
const uo = await defaultUserOpSigner(struct, params);
const account = params.account ?? params.client.account;
const { client } = params;

if (!account || !isSmartAccountWithSigner(account)) {
throw new AccountNotFoundError();
}

const signer = account.getSigner();

if (!signer.signAuthorization) {
console.log("account does not support signAuthorization");
return uo;
}

if (!client.chain) {
throw new ChainNotFoundError();
}

const code = (await client.getCode({ address: account.address })) ?? "0x";
// TODO: this isn't the cleanest because now the account implementation HAS to know that it needs to return an impl address
// even if the account is not deployed

const implAddress = await account.getImplementationAddress();

const expectedCode = "0xef0100" + implAddress.slice(2);

if (code.toLowerCase() === expectedCode.toLowerCase()) {
return uo;
}

const accountNonce = await params.client.getTransactionCount({
address: account.address,
});

const {
r,
s,
v,
yParity = v ? v - 27n : undefined,
} = await signer.signAuthorization({
chainId: client.chain.id,
contractAddress: implAddress,
nonce: accountNonce,
});

return {
...uo,
authorizationTuple: {
chainId: client.chain.id,
nonce: toHex(accountNonce), // deepHexlify doesn't encode number(0) correctly, it returns "0x"
address: implAddress,
r,
s,
yParity: Number(yParity),
},
};
};
28 changes: 28 additions & 0 deletions aa-sdk/core/src/signer/local-account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
privateKeyToAccount,
} from "viem/accounts";
import type { SmartAccountSigner } from "./types.js";
import type { Authorization } from "viem/experimental";

/**
* Represents a local account signer and provides methods to sign messages and transactions, as well as static methods to create the signer from mnemonic or private key.
Expand Down Expand Up @@ -95,6 +96,33 @@ export class LocalAccountSigner<
return this.inner.signTypedData(params);
};

/**
* Signs an unsigned authorization using the provided private key account.
*
* @example
* ```ts
* import { LocalAccountSigner } from "@aa-sdk/core";
* import { generatePrivateKey } from "viem";
*
* const signer = LocalAccountSigner.mnemonicToAccountSigner(generatePrivateKey());
* const signedAuthorization = await signer.signAuthorization({
* contractAddress: "0x1234123412341234123412341234123412341234",
* chainId: 1,
* nonce: 3,
* });
* ```
*
* @param {Authorization<number, false>} unsignedAuthorization - The unsigned authorization to be signed.
* @returns {Promise<Authorization<number, true>>} A promise that resolves to the signed authorization.
*/

signAuthorization(
this: LocalAccountSigner<PrivateKeyAccount>,
unsignedAuthorization: Authorization<number, false>
): Promise<Authorization<number, true>> {
return this.inner.experimental_signAuthorization(unsignedAuthorization);
}

/**
* Returns the address of the inner object in a specific hexadecimal format.
*
Expand Down
5 changes: 5 additions & 0 deletions aa-sdk/core/src/signer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
TypedData,
TypedDataDefinition,
} from "viem";
import type { Authorization } from "viem/experimental";

// [!region SmartAccountAuthenticator]
/**
Expand Down Expand Up @@ -42,5 +43,9 @@ export interface SmartAccountSigner<Inner = any> {
>(
params: TypedDataDefinition<TTypedData, TPrimaryType>
) => Promise<Hex>;

signAuthorization?: (
unsignedAuthorization: Authorization<number, false>
) => Promise<Authorization<number, true>>;
}
// [!endregion SmartAccountSigner]
15 changes: 11 additions & 4 deletions aa-sdk/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type StateOverride,
type TransactionReceipt,
} from "viem";
import type { Authorization } from "viem/experimental";
import type { z } from "zod";
import type {
UserOperationFeeOptionsFieldSchema,
Expand Down Expand Up @@ -201,15 +202,20 @@ export interface UserOperationRequest_v7 {
}
// [!endregion UserOperationRequest_v7]

export type Eip7702ExtendedFields =
| { authorizationTuple?: Authorization; authorizationContract?: never }
| { authorizationTuple?: never; authorizationContract?: Address };

// [!region UserOperationRequest]
// Reference: https://eips.ethereum.org/EIPS/eip-4337#definitions
export type UserOperationRequest<
TEntryPointVersion extends EntryPointVersion = EntryPointVersion
> = TEntryPointVersion extends "0.6.0"
> = (TEntryPointVersion extends "0.6.0"
? UserOperationRequest_v6
: TEntryPointVersion extends "0.7.0"
? UserOperationRequest_v7
: never;
: never) &
Eip7702ExtendedFields;

// [!endregion UserOperationRequest]

Expand Down Expand Up @@ -347,9 +353,10 @@ export interface UserOperationStruct_v7 {
// [!region UserOperationStruct]
export type UserOperationStruct<
TEntryPointVersion extends EntryPointVersion = EntryPointVersion
> = TEntryPointVersion extends "0.6.0"
> = (TEntryPointVersion extends "0.6.0"
? UserOperationStruct_v6
: TEntryPointVersion extends "0.7.0"
? UserOperationStruct_v7
: never;
: never) &
Eip7702ExtendedFields;
// [!endregion UserOperationStruct]
2 changes: 1 addition & 1 deletion aa-sdk/ethers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"@ethersproject/wallet": "^5.7.0"
},
"peerDependencies": {
"viem": "^2.20.0"
"viem": "^2.22.6"
},
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion account-kit/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"zustand": "^5.0.0-rc.2"
},
"peerDependencies": {
"viem": "^2.20.0",
"viem": "^2.22.6",
"wagmi": "^2.12.7"
},
"publishConfig": {
Expand Down
2 changes: 1 addition & 1 deletion account-kit/infra/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"zod": "^3.22.4"
},
"peerDependencies": {
"viem": "^2.20.0"
"viem": "^2.22.6"
},
"publishConfig": {
"access": "public",
Expand Down
2 changes: 1 addition & 1 deletion account-kit/plugingen/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,6 @@
"zod": "^3.22.4"
},
"peerDependencies": {
"viem": "^2.20.0"
"viem": "^2.22.6"
}
}
4 changes: 2 additions & 2 deletions account-kit/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,11 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.4.3",
"viem": "^2.20.0",
"viem": "^2.22.6",
"wagmi": "^2.12.7"
},
"resolutions": {
"viem": "2.20.0"
"viem": "2.22.6"
},
"publishConfig": {
"access": "public",
Expand Down
2 changes: 1 addition & 1 deletion account-kit/signer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
"zod": "^3.22.4"
},
"peerDependencies": {
"viem": "^2.20.0"
"viem": "^2.22.6"
},
"publishConfig": {
"access": "public",
Expand Down
Loading