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 @@ -85,6 +85,7 @@ export type CreateMAV2BaseParams<
signer: TSigner;
signerEntity?: SignerEntity;
accountAddress: Address;
deferredAction?: Hex;
};

export type CreateMAV2BaseReturnType<
Expand All @@ -108,6 +109,7 @@ export async function createMAv2Base<
entityId = DEFAULT_OWNER_ENTITY_ID,
} = {},
accountAddress,
deferredAction,
...remainingToSmartContractAccountParams
} = config;

Expand All @@ -120,6 +122,39 @@ export async function createMAv2Base<
chain,
});

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

let useDeferredAction: boolean = false;
let nonce: bigint = 0n;
let deferredActionData: Hex = "0x";
let hasAssociatedExecHooks: boolean = false;

if (deferredAction) {
if (deferredAction.slice(3, 5) !== "00") {
throw new Error("Invalid deferred action version");
}
nonce = BigInt(`0x${deferredAction.slice(6, 70)}`);
// Set these values if the deferred action has not been consumed. We check this with the EP
const nextNonceForDeferredActionNonce: bigint =
(await entryPointContract.read.getNonce([
accountAddress,
nonce >> 64n,
])) as bigint;

// we only add the deferred action in if the nonce has not been consumed
if (nonce === nextNonceForDeferredActionNonce) {
useDeferredAction = true;
deferredActionData = `0x${deferredAction.slice(70)}`;
hasAssociatedExecHooks = deferredAction[6] === "1";
} else if (nonce > nextNonceForDeferredActionNonce) {
throw new Error("Deferred action nonce invalid");
}
}

const encodeExecute: (tx: AccountOp) => Promise<Hex> = async ({
target,
data,
Expand Down Expand Up @@ -150,18 +185,16 @@ export async function createMAv2Base<

const isAccountDeployed: () => Promise<boolean> = async () =>
!!(await client.getCode({ address: accountAddress }));
// TODO: add deferred action flag

const getNonce = async (nonceKey: bigint = 0n): Promise<bigint> => {
if (useDeferredAction && deferredAction) {
return nonce;
}

if (nonceKey > maxUint152) {
throw new InvalidNonceKeyError(nonceKey);
}

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

const fullNonceKey: bigint =
(nonceKey << 40n) +
(BigInt(entityId) << 8n) +
Expand Down Expand Up @@ -216,7 +249,8 @@ export async function createMAv2Base<
entityId: Number(entityId),
});

return validationData.executionHooks.length
return (useDeferredAction && hasAssociatedExecHooks) ||
validationData.executionHooks.length
? concatHex([executeUserOpSelector, callData])
: callData;
};
Expand All @@ -231,8 +265,14 @@ export async function createMAv2Base<
encodeBatchExecute,
getNonce,
...(entityId === DEFAULT_OWNER_ENTITY_ID
? nativeSMASigner(signer, chain, accountAddress)
: singleSignerMessageSigner(signer, chain, accountAddress, entityId)),
? nativeSMASigner(signer, chain, accountAddress, deferredActionData)
: singleSignerMessageSigner(
signer,
chain,
accountAddress,
entityId,
deferredActionData
)),
});

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export type CreateModularAccountV2Params<
> & {
signer: TSigner;
entryPoint?: EntryPointDef<"0.7.0", Chain>;
deferredAction?: Hex;
signerEntity?: SignerEntity;
}) &
(
Expand Down Expand Up @@ -103,6 +104,7 @@ export async function createModularAccountV2(
entityId: DEFAULT_OWNER_ENTITY_ID,
},
signerEntity: { entityId = DEFAULT_OWNER_ENTITY_ID } = {},
deferredAction,
} = config;

const client = createBundlerClient({
Expand Down Expand Up @@ -183,6 +185,7 @@ export async function createModularAccountV2(
signer,
entryPoint,
signerEntity,
deferredAction,
...accountFunctions,
});
}
Expand Down
34 changes: 20 additions & 14 deletions account-kit/smart-contracts/src/ma-v2/account/nativeSMASigner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type Chain,
type Address,
concat,
concatHex,
} from "viem";

import {
Expand All @@ -23,7 +24,6 @@ import { SignatureType } from "../modules/utils.js";
* @example
* ```ts
* import { nativeSMASigner } from "@account-kit/smart-contracts";

* import { LocalAccountSigner } from "@aa-sdk/core";
*
* const MNEMONIC = "...":
Expand All @@ -38,31 +38,37 @@ import { SignatureType } from "../modules/utils.js";
* @param {SmartAccountSigner} signer Signer to use for signing operations
* @param {Chain} chain Chain object for the signer
* @param {Address} accountAddress address of the smart account using this signer
* @param {Hex} deferredActionData optional deferred action data to prepend to the uo signatures
* @returns {object} an object with methods for signing operations and managing signatures
*/
export const nativeSMASigner = (
signer: SmartAccountSigner,
chain: Chain,
accountAddress: Address
accountAddress: Address,
deferredActionData?: Hex
) => {
return {
getDummySignature: (): Hex => {
const dummyEcdsaSignature =
"0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c";

return packUOSignature({
const sig = packUOSignature({
// orderedHookData: [],
validationSignature: dummyEcdsaSignature,
validationSignature:
"0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c",
});

return deferredActionData ? concatHex([deferredActionData, sig]) : sig;
},

signUserOperationHash: (uoHash: Hex): Promise<Hex> => {
return signer.signMessage({ raw: uoHash }).then((signature: Hex) =>
packUOSignature({
// orderedHookData: [],
validationSignature: signature,
})
);
signUserOperationHash: async (uoHash: Hex): Promise<Hex> => {
const sig = await signer
.signMessage({ raw: uoHash })
.then((signature: Hex) =>
packUOSignature({
// orderedHookData: [],
validationSignature: signature,
})
);

return deferredActionData ? concatHex([deferredActionData, sig]) : sig;
},

// we apply the expected 1271 packing here since the account contract will expect it
Expand Down
157 changes: 157 additions & 0 deletions account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import {
erc7677Middleware,
LocalAccountSigner,
type SmartAccountSigner,
} from "@aa-sdk/core";
import {
custom,
parseEther,
publicActions,
testActions,
toHex,
type TestActions,
} from "viem";
import { installValidationActions } from "@account-kit/smart-contracts/experimental";
import {
// createModularAccountV2Client,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// createModularAccountV2Client,

type SignerEntity,
} from "@account-kit/smart-contracts";
import { local070Instance } from "~test/instances.js";
import { setBalance } from "viem/actions";
import { accounts } from "~test/constants.js";
import { alchemyGasAndPaymasterAndDataMiddleware } from "@account-kit/infra";
import { deferralActions } from "./deferralActions.js";
import { PermissionBuilder, PermissionType } from "../permissionBuilder.js";
import { createModularAccountV2Client } from "../client/client.js";

// Note: These tests maintain a shared state to not break the local-running rundler by desyncing the chain.
describe("MA v2 deferral actions tests", async () => {
const instance = local070Instance;

let client: ReturnType<typeof instance.getClient> &

Check warning on line 31 in account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts

View workflow job for this annotation

GitHub Actions / Lint

'client' is assigned a value but never used
ReturnType<typeof publicActions> &
TestActions;
Comment on lines +31 to +33
Copy link
Collaborator

Choose a reason for hiding this comment

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

⚠️ [eslint] <@typescript-eslint/no-unused-vars> reported by reviewdog 🐶
'client' is assigned a value but never used.


beforeAll(async () => {
client = instance
.getClient()
.extend(publicActions)
.extend(testActions({ mode: "anvil" }));
});

const signer: SmartAccountSigner = new LocalAccountSigner(
accounts.fundedAccountOwner
);

const target = "0x000000000000000000000000000000000000dEaD";
const sendAmount = parseEther("1");

it("tests the full deferred actions flow", async () => {
const provider = (await givenConnectedProvider({ signer }))
.extend(deferralActions)
.extend(installValidationActions);

await setBalance(instance.getClient(), {
address: provider.getAddress(),
value: parseEther("2"),
});

const sessionKeyEntityId = 1;

Check warning on line 59 in account-kit/smart-contracts/src/ma-v2/actions/deferralActions.test.ts

View workflow job for this annotation

GitHub Actions / Lint

'sessionKeyEntityId' is assigned a value but never used
Copy link
Collaborator

Choose a reason for hiding this comment

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

⚠️ [eslint] <@typescript-eslint/no-unused-vars> reported by reviewdog 🐶
'sessionKeyEntityId' is assigned a value but never used.

const sessionKey: SmartAccountSigner = new LocalAccountSigner(
accounts.unfundedAccountOwner
);

// these can be default values or from call arguments
const { entityId, nonce } = await provider.getEntityIdAndNonce({
entityId: 0,
nonceKey: 0n,
isGlobalValidation: true,
});

// TODO: remove nonce override here
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// TODO: remove nonce override here

const { typedData, hasAssociatedExecHooks } = await new PermissionBuilder(
provider
)
.configure({
key: {
publicKey: await sessionKey.getAddress(),
type: "secp256k1",
},
entityId,
nonceKeyOverride: 0n, // TODO: add nonce override here
})
.addPermission({
permission: {
type: PermissionType.ROOT,
},
})
.compile_deferred({
deadline: 0,
uoValidationEntityId: entityId,
uoIsGlobalValidation: true,
});

const sig = await provider.account.signTypedData(typedData);

const deferredActionDigest = await provider.buildDeferredActionDigest({
typedData,
sig,
});

// TODO: need nonce, orig account address, orig account initcode
const sessionKeyClient = await createModularAccountV2Client({
transport: custom(instance.getClient()),
chain: instance.chain,
accountAddress: provider.getAddress(),
signer: sessionKey,
initCode: provider.account.getInitCode(),
deferredAction: `0x00${hasAssociatedExecHooks ? "01" : "00"}${toHex(
nonce,
{
size: 32,
}
).slice(2)}${deferredActionDigest.slice(2)}`,
});

const uoResult = await sessionKeyClient.sendUserOperation({
uo: {
target: target,
value: sendAmount,
data: "0x",
},
});

await provider.waitForUserOperationTransaction(uoResult);
});

const givenConnectedProvider = async ({
signer,
signerEntity,
accountAddress,
paymasterMiddleware,
salt = 0n,
}: {
signer: SmartAccountSigner;
signerEntity?: SignerEntity;
accountAddress?: `0x${string}`;
paymasterMiddleware?: "alchemyGasAndPaymasterAndData" | "erc7677";
salt?: bigint;
}) =>
createModularAccountV2Client({
chain: instance.chain,
signer,
accountAddress,
signerEntity,
transport: custom(instance.getClient()),
...(paymasterMiddleware === "alchemyGasAndPaymasterAndData"
? alchemyGasAndPaymasterAndDataMiddleware({
policyId: "FAKE_POLICY_ID",
// @ts-ignore (expects an alchemy transport, but we're using a custom transport for mocking)
transport: custom(instance.getClient()),
})
: paymasterMiddleware === "erc7677"
? erc7677Middleware()
: {}),
salt,
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export const deferralActions: (
[validationLocator, typedData.message.deadline, typedData.message.call]
);

// TODO: encoded call data as input
const encodedDataLength = size(encodedCallData);
const sigLength = size(sig);

Expand Down
3 changes: 0 additions & 3 deletions account-kit/smart-contracts/src/ma-v2/client/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -595,9 +595,6 @@ describe("MA v2 Tests", async () => {
uoIsGlobalValidation: isGlobalValidation, // UO validation is global
});

console.log(typedData);
console.log("Deferred: %s, \n\n Nonce: %s", typedData, nonceOverride);

// Sign the typed data using the first session key
const deferredValidationSig = await sessionKeyClient.account.signTypedData(
typedData
Expand Down
Loading
Loading