Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ export async function _initUserOperation<

const signature = account.getDummySignature();

const nonce = account.getAccountNonce(overrides?.nonceKey);
const nonce =
overrides?.nonce ?? account.getAccountNonce(overrides?.nonceKey);

const struct =
entryPoint.version === "0.6.0"
Expand Down
24 changes: 15 additions & 9 deletions aa-sdk/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,6 @@ export type UserOperationOverrides<
verificationGasLimit:
| UserOperationStruct<TEntryPointVersion>["verificationGasLimit"]
| Multiplier;
/**
* This can be used to override the key used when calling `entryPoint.getNonce`
* It is useful when you want to use parallel nonces for user operations
*
* NOTE: not all bundlers fully support this feature and it could be that your bundler will still only include
* one user operation for your account in a bundle
*/
nonceKey: bigint;

/**
* The same state overrides for
Expand All @@ -132,7 +124,21 @@ export type UserOperationOverrides<
*/
stateOverride: StateOverride;
} & UserOperationPaymasterOverrides<TEntryPointVersion>
>;
> &
/**
* This can be used to override the nonce or nonce key used when calling `entryPoint.getNonce`
* It is useful when you want to use parallel nonces for user operations
*
* NOTE: not all bundlers fully support this feature and it could be that your bundler will still only include
* one user operation for your account in a bundle
*/
Partial<
| {
nonceKey: bigint;
nonce: never;
}
| { nonceKey: never; nonce: bigint }
>;
// [!endregion UserOperationOverrides]

// [!region UserOperationRequest_v6]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import {
type TypedDataDefinition,
type Chain,
type Address,
concat,
} from "viem";

import {
packUOSignature,
pack1271Signature,
DEFAULT_OWNER_ENTITY_ID,
} from "../utils.js";
import { SignatureType } from "../modules/utils.js";
/**
* Creates an object with methods for generating a dummy signature, signing user operation hashes, signing messages, and signing typed data.
*
Expand Down Expand Up @@ -99,7 +101,10 @@ export const nativeSMASigner = (
typedDataDefinition?.domain?.verifyingContract === accountAddress;

return isDeferredAction
? signer.signTypedData(typedDataDefinition)
? concat([
SignatureType.EOA,
await signer.signTypedData(typedDataDefinition),
])
: pack1271Signature({
validationSignature: await signer.signTypedData({
domain: {
Expand Down
232 changes: 232 additions & 0 deletions account-kit/smart-contracts/src/ma-v2/actions/DeferralActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import {
AccountNotFoundError,
InvalidNonceKeyError,
EntryPointNotFoundError,
type UserOperationCallData,
type BatchUserOperationCallData,
type UserOperationRequest_v7,
} from "@aa-sdk/core";
import {
type Address,
type Hex,
concatHex,
maxUint152,
getContract,
encodePacked,
size,
toHex,
} from "viem";
import type { ModularAccountV2Client } from "../client/client.js";

export type DeferredActionTypedData = {
domain: {
chainId: number;
verifyingContract: Address;
};
types: {
DeferredAction: [
{ name: "nonce"; type: "uint256" },
{ name: "deadline"; type: "uint48" },
{ name: "call"; type: "bytes" }
];
};
primaryType: "DeferredAction";
message: {
nonce: bigint;
deadline: number;
call: Hex;
};
};

export type DeferredActionReturnData = {
typedData: DeferredActionTypedData;
nonceOverride: bigint;
};

export type CreateDeferredActionTypedDataParams = {
callData: Hex;
deadline: number;
entityId: number;
isGlobalValidation: boolean;
nonceKeyOverride?: bigint;
};

export type BuildDeferredActionDigestParams = {
typedData: DeferredActionTypedData;
sig: Hex;
};

export type BuildUserOperationWithDeferredActionParams = {
uo: UserOperationCallData | BatchUserOperationCallData;
signaturePrepend: Hex;
nonceOverride: bigint;
};

export type DeferralActions = {
createDeferredActionTypedDataObject: (
args: CreateDeferredActionTypedDataParams
) => Promise<DeferredActionReturnData>;
buildDeferredActionDigest: (args: BuildDeferredActionDigestParams) => Hex;
buildUserOperationWithDeferredAction: (
args: BuildUserOperationWithDeferredActionParams
) => Promise<UserOperationRequest_v7>;
};

/**
* Provides deferred action functionalities for a MA v2 client, ensuring compatibility with `SmartAccountClient`.
*
* @param {ModularAccountV2Client} client - The client instance which provides account and sendUserOperation functionality.
* @returns {object} - An object containing three methods: `createDeferredActionTypedDataObject`, `buildDeferredActionDigest`, and `buildUserOperationWithDeferredAction`.
*/
export const deferralActions: (
client: ModularAccountV2Client
) => DeferralActions = (client: ModularAccountV2Client): DeferralActions => {
const createDeferredActionTypedDataObject = async ({
callData,
deadline,
entityId,
isGlobalValidation,
nonceKeyOverride,
}: CreateDeferredActionTypedDataParams): Promise<DeferredActionReturnData> => {
if (!client.account) {
throw new AccountNotFoundError();
}

const baseNonceKey = nonceKeyOverride || 0n;
if (baseNonceKey > maxUint152) {
throw new InvalidNonceKeyError(baseNonceKey);
}

const entryPoint = client.account.getEntryPoint();
if (entryPoint === undefined) {
throw new EntryPointNotFoundError(client.chain, "0.7.0");
}

const entryPointContract = getContract({
address: entryPoint.address,
abi: entryPoint.abi,
client: client,
});

// 2 = deferred action flags 0b10
// 1 = isGlobal validation flag 0b01
const fullNonceKey: bigint =
((baseNonceKey << 40n) + (BigInt(entityId) << 8n)) |
2n |
(isGlobalValidation ? 1n : 0n);

const nonceOverride = (await entryPointContract.read.getNonce([
client.account.address,
fullNonceKey,
])) as bigint;

return {
typedData: {
domain: {
chainId: await client.getChainId(),
verifyingContract: client.account.address,
},
types: {
DeferredAction: [
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint48" },
{ name: "call", type: "bytes" },
],
},
primaryType: "DeferredAction",
message: {
nonce: nonceOverride,
deadline: deadline,
call: callData,
},
},
nonceOverride: nonceOverride,
};
};

/**
* Creates the digest which must be prepended to the userOp signature.
*
* Assumption: The client this extends is used to sign the typed data.
*
* @param {object} args The argument object containing the following:
* @param {DeferredActionTypedData} args.typedData The typed data object for the deferred action
* @param {Hex} args.sig The signature to include in the digest
* @returns {Hex} The encoded digest to be prepended to the userOp signature
*/
const buildDeferredActionDigest = ({
typedData,
sig,
}: BuildDeferredActionDigestParams): Hex => {
const signerEntity = client.account.signerEntity;
const validationLocator =
(BigInt(signerEntity.entityId) << 8n) |
(signerEntity.isGlobalValidation ? 1n : 0n);

let encodedData = encodePacked(
["uint168", "uint48", "bytes"],
[validationLocator, typedData.message.deadline, typedData.message.call]
);

const encodedDataLength = size(encodedData);
const sigLength = size(sig);

encodedData = concatHex([
toHex(encodedDataLength, { size: 4 }),
encodedData,
toHex(sigLength, { size: 4 }),
sig,
]);

return encodedData;
};

/**
* Builds a user operation with a deferred action by wrapping buildUserOperation() with a dummy signature override.
*
* @param {object} args The argument object containing the following:
* @param {UserOperationCallData | BatchUserOperationCallData} args.uo The user operation call data to build
* @param {Hex} args.signaturePrepend The signature data to prepend to the dummy signature
* @param {bigint} args.nonceOverride The nonce to override in the user operation, generally given from the typed data builder
* @returns {Promise<UserOperationRequest_v7>} The unsigned user operation request with the deferred action
*/
const buildUserOperationWithDeferredAction = async ({
uo,
signaturePrepend,
nonceOverride,
}: BuildUserOperationWithDeferredActionParams): Promise<UserOperationRequest_v7> => {
// Check if client.account is defined
if (client.account === undefined) {
throw new AccountNotFoundError();
}

// Pre-fetch the dummy sig so we can override `client.account.getDummySignature()`
const dummySig = await client.account.getDummySignature();

// Cache the previous dummy signature getter
const previousDummySigGetter = client.account.getDummySignature;

// Override client.account.getDummySignature() so `client.buildUserOperation()` uses the prepended hex and the dummy signature during gas estimation
client.account.getDummySignature = () => {
return concatHex([signaturePrepend, dummySig as Hex]);
};

const unsignedUo = (await client.buildUserOperation({
uo: uo,
overrides: {
nonce: nonceOverride,
},
})) as UserOperationRequest_v7;

// Restore the dummy signature getter
client.account.getDummySignature = previousDummySigGetter;

return unsignedUo;
};

return {
createDeferredActionTypedDataObject,
buildDeferredActionDigest,
buildUserOperationWithDeferredAction,
};
};
Loading