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

fix: make "mode" optional in useSmartAccountClient #1334

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a5aea17
feat: sma 7702
adamegyed Dec 18, 2024
960aa52
feat: adds 7702 support to alchemy signer (#1269)
linnall Jan 9, 2025
57f2c27
feat: 7702 progress
adamegyed Jan 9, 2025
8e3ced7
feat: update viem and debug 7702 auth
adamegyed Jan 13, 2025
31c486d
feat: add deterministically deployed demo nft
adamegyed Jan 13, 2025
969e7a4
fix: don't re-sign delegations
adamegyed Jan 14, 2025
caeea07
feat: update 7702 middleware to higher order function
adamegyed Jan 23, 2025
425c630
chore: merge fixes
adamegyed Jan 23, 2025
f5cebc3
fix: remove unnecessary console log
adamegyed Jan 23, 2025
9c81658
fix: correctly await client actions in infra test
adamegyed Jan 23, 2025
f8cde3a
fix: correctly encode yParity in zero case
adamegyed Jan 23, 2025
6607a64
feat: correctly set validation entity in nonce and separate encode ac…
adamegyed Jan 27, 2025
a421004
feat: consolidate MAv2Base
adamegyed Jan 29, 2025
a86bc3b
feat: add unified ma v2 client (#1309)
howydev Feb 4, 2025
399e257
feat: add ma v2 account to useSmartAccountClient hook (#1314)
howydev Feb 4, 2025
3209c9f
chore: rebaseme
adamegyed Feb 5, 2025
3a3afe9
fix: client type and middleware inclusion
adamegyed Feb 5, 2025
38d6baa
docs: update docs with twoslash
adamegyed Feb 5, 2025
fe6fe6a
feat: update to new json format for auth
adamegyed Feb 5, 2025
f7fa27c
feat: add defaults for useSmartAccountClient to ma v2 (#1328)
howydev Feb 7, 2025
6d6c3db
fix: fix MAv2 React Hook client creation for 7702 (#1329)
jakehobbs Feb 7, 2025
b3acd54
feat: update eip 7702 auth format
adamegyed Feb 7, 2025
1fbd707
fix: make mode optional in useSmartAccountClient
howydev Feb 7, 2025
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
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 @@ -55,7 +55,7 @@
"zod": "^3.22.4"
},
"peerDependencies": {
"viem": "^2.20.0"
"viem": "^2.22.6"
},
"repository": {
"type": "git",
Expand Down
69 changes: 39 additions & 30 deletions aa-sdk/core/src/account/smartContractAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,11 @@ export type ToSmartContractAccountParams<
getDummySignature: () => Hex | Promise<Hex>;
encodeExecute: (tx: AccountOp) => Promise<Hex>;
encodeBatchExecute?: (txs: AccountOp[]) => Promise<Hex>;
getNonce?: (nonceKey?: bigint) => Promise<bigint>;
// if not provided, will default to just using signMessage over the Hex
signUserOperationHash?: (uoHash: Hex) => Promise<Hex>;
encodeUpgradeToAndCall?: (params: UpgradeToAndCallParams) => Promise<Hex>;
getImplementationAddress?: () => Promise<NullAddress | Address>;
} & Omit<CustomSource, "signTransaction" | "address">;
// [!endregion ToSmartContractAccountParams]

Expand Down Expand Up @@ -260,6 +262,7 @@ export async function toSmartContractAccount<
source,
accountAddress,
getAccountInitCode,
getNonce,
signMessage,
signTypedData,
encodeBatchExecute,
Expand Down Expand Up @@ -339,11 +342,13 @@ export async function toSmartContractAccount(
getAccountInitCode,
signMessage,
signTypedData,
encodeBatchExecute,
encodeExecute,
encodeBatchExecute,
getNonce,
getDummySignature,
signUserOperationHash,
encodeUpgradeToAndCall,
getImplementationAddress,
} = params;

const client = createBundlerClient({
Expand Down Expand Up @@ -410,16 +415,18 @@ export async function toSmartContractAccount(
return initCode === "0x";
};

const getNonce = async (nonceKey = 0n): Promise<bigint> => {
if (!(await isAccountDeployed())) {
return 0n;
}

return entryPointContract.read.getNonce([
accountAddress_,
nonceKey,
]) as Promise<bigint>;
};
const getNonce_ =
getNonce ??
(async (nonceKey = 0n): Promise<bigint> => {
if (!(await isAccountDeployed())) {
return 0n;
}

return entryPointContract.read.getNonce([
accountAddress_,
nonceKey,
]) as Promise<bigint>;
});

const account = toAccount({
address: accountAddress_,
Expand Down Expand Up @@ -468,25 +475,27 @@ export async function toSmartContractAccount(
return create6492Signature(isDeployed, signature);
};

const getImplementationAddress = async (): Promise<NullAddress | Address> => {
const storage = await client.getStorageAt({
address: account.address,
// This is the default slot for the implementation address for Proxies
slot: "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
const getImplementationAddress_ =
getImplementationAddress ??
(async () => {
const storage = await client.getStorageAt({
address: account.address,
// This is the default slot for the implementation address for Proxies
slot: "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
});

if (storage == null) {
throw new FailedToGetStorageSlotError(
"0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
"Proxy Implementation Address"
);
}

// The storage slot contains a full bytes32, but we want only the last 20 bytes.
// So, slice off the leading `0x` and the first 12 bytes (24 characters), leaving the last 20 bytes, then prefix with `0x`.
return `0x${storage.slice(26)}`;
});

if (storage == null) {
throw new FailedToGetStorageSlotError(
"0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
"Proxy Implementation Address"
);
}

// The storage slot contains a full bytes32, but we want only the last 20 bytes.
// So, slice off the leading `0x` and the first 12 bytes (24 characters), leaving the last 20 bytes, then prefix with `0x`.
return `0x${storage.slice(26)}`;
};

if (entryPoint.version !== "0.6.0" && entryPoint.version !== "0.7.0") {
throw new InvalidEntryPointError(chain, entryPoint.version);
}
Expand All @@ -510,9 +519,9 @@ export async function toSmartContractAccount(
encodeUpgradeToAndCall: encodeUpgradeToAndCall_,
getEntryPoint: () => entryPoint,
isAccountDeployed,
getAccountNonce: getNonce,
getAccountNonce: getNonce_,
signMessageWith6492,
signTypedDataWith6492,
getImplementationAddress,
getImplementationAddress: getImplementationAddress_,
};
}
14 changes: 14 additions & 0 deletions aa-sdk/core/src/errors/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,17 @@ export class EntityIdOverrideError extends BaseError {
super(`EntityId of 0 is reserved for the owner and cannot be used`);
}
}

/**
* Error class denoting that the provided ma v2 account mode is invalid.
*/
export class InvalidModularAccountV2Mode extends BaseError {
override name = "InvalidModularAccountV2Mode";

/**
* Initializes a new instance of the error message with a default message indicating that the provided ma v2 account mode is invalid.
*/
constructor() {
super(`The provided account mode is invalid for ModularAccount V2`);
}
}
3 changes: 3 additions & 0 deletions aa-sdk/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export {
InvalidEntityIdError,
InvalidNonceKeyError,
EntityIdOverrideError,
InvalidModularAccountV2Mode,
} from "./errors/client.js";
export {
EntryPointNotFoundError,
Expand All @@ -92,6 +93,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
77 changes: 77 additions & 0 deletions aa-sdk/core/src/middleware/defaults/7702gasEstimator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { zeroHash } from "viem";
import { AccountNotFoundError } from "../../errors/account.js";
import type { UserOperationStruct } from "../../types.js";
import type { ClientMiddlewareFn } from "../types";
import { defaultGasEstimator } from "./gasEstimator.js";

/**
* A middleware function to estimate the gas usage of a user operation when using an EIP-7702 delegated account. Has an optional custom gas estimator.
* This function is only compatible with accounts using EntryPoint v0.7.0, and the account must have an implementation address defined in `getImplementationAddress()`.
*
* @example
* ```ts twoslash
* import {
* default7702GasEstimator,
* default7702UserOpSigner,
* createSmartAccountClient,
* type SmartAccountClient,
* } from "@aa-sdk/core";
* import {
* createModularAccountV2,
* type CreateModularAccountV2ClientParams,
* } from "@account-kit/smart-contracts";
*
* async function createSMA7702AccountClient(
* config: CreateModularAccountV2ClientParams
* ): Promise<SmartAccountClient> {
* const sma7702Account = await createModularAccountV2({ ...config, mode: "7702" });
*
* return createSmartAccountClient({
* account: sma7702Account,
* gasEstimator: default7702GasEstimator(config.gasEstimator),
* signUserOperation: default7702UserOpSigner(config.signUserOperation),
* ...config,
* });
* }
* ```
*
* @param {ClientMiddlewareFn} [gasEstimator] Optional custom gas estimator function
* @returns {Function} A function that takes user operation struct and parameters, estimates gas usage, and returns the user operation with gas limits.
*/
export const default7702GasEstimator: (
gasEstimator?: ClientMiddlewareFn
) => ClientMiddlewareFn =
(gasEstimator?: ClientMiddlewareFn) => async (struct, params) => {
const gasEstimator_ = gasEstimator ?? defaultGasEstimator(params.client);

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"
);
}

const implementationAddress = await account.getImplementationAddress();

// Note: does not omit the delegation from estimation if the account is already 7702 delegated.

(struct as UserOperationStruct<"0.7.0">).eip7702Auth = {
chainId: "0x0",
nonce: "0x0",
address: implementationAddress,
r: zeroHash, // aka `bytes32(0)`
s: zeroHash,
yParity: "0x0",
};

const estimatedUO = await gasEstimator_(struct, params);

estimatedUO.eip7702Auth = undefined; // Strip out the auth after estimation.

return estimatedUO;
};
105 changes: 105 additions & 0 deletions aa-sdk/core/src/middleware/defaults/7702signer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
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 EIP-7702 delegated accounts.
* If the signer doesn't support `signAuthorization`, then this just runs the provided `signUserOperation` middleware.
* This function is only compatible with accounts using EntryPoint v0.7.0, and the account must have an implementation address defined in `getImplementationAddress()`.
*
* @example
* ```ts twoslash
* import {
* default7702GasEstimator,
* default7702UserOpSigner,
* createSmartAccountClient,
* type SmartAccountClient,
* } from "@aa-sdk/core";
* import {
* createModularAccountV2,
* type CreateModularAccountV2ClientParams,
* } from "@account-kit/smart-contracts";
*
* async function createSMA7702AccountClient(
* config: CreateModularAccountV2ClientParams
* ): Promise<SmartAccountClient> {
* const sma7702Account = await createModularAccountV2({ ...config, mode: "7702" });
*
* return createSmartAccountClient({
* account: sma7702Account,
* gasEstimator: default7702GasEstimator(config.gasEstimator),
* signUserOperation: default7702UserOpSigner(config.signUserOperation),
* ...config,
* });
* }
* ```
*
* @param {ClientMiddlewareFn} [userOpSigner] Optional user operation signer function
* @returns {Function} A middleware function that signs EIP-7702 authorization tuples if necessary, and also uses the provided or default user operation signer to generate the user op signature.
*/
export const default7702UserOpSigner: (
userOpSigner?: ClientMiddlewareFn
) => ClientMiddlewareFn =
(userOpSigner?: ClientMiddlewareFn) => async (struct, params) => {
const userOpSigner_ = userOpSigner ?? defaultUserOpSigner;

const uo = await userOpSigner_(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) {
return uo;
}

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

const code = (await client.getCode({ address: account.address })) ?? "0x";

const implAddress = await account.getImplementationAddress();

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

if (code.toLowerCase() === expectedCode.toLowerCase()) {
// If the delegation already matches the expected, then we don't need to sign and include an authorization tuple.
return uo;
}

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

const authSignature = await signer.signAuthorization({
chainId: client.chain.id,
contractAddress: implAddress,
nonce: accountNonce,
});

const { r, s } = authSignature;

const yParity = authSignature.yParity ?? authSignature.v - 27n;

return {
...uo,
eip7702Auth: {
// deepHexlify doesn't encode number(0) correctly, it returns "0x"
chainId: toHex(client.chain.id),
nonce: toHex(accountNonce),
address: implAddress,
r,
s,
yParity: toHex(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 twoslash
* import { LocalAccountSigner } from "@aa-sdk/core";
* import { generatePrivateKey } from "viem/accounts";
*
* const signer = LocalAccountSigner.privateKeyToAccountSigner(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
Loading