Skip to content

Commit 2a615bd

Browse files
committed
fix: test
1 parent a9f2c7c commit 2a615bd

File tree

3 files changed

+238
-65
lines changed

3 files changed

+238
-65
lines changed

account-kit/smart-contracts/src/ma-v2/account/common/modularAccountV2Base.ts

Lines changed: 57 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,20 @@ import {
2121
getContract,
2222
concatHex,
2323
maxUint152,
24-
hexToBigInt,
2524
hexToNumber,
2625
decodeFunctionData,
26+
createCursor,
27+
toFunctionSelector,
28+
hexToBytes,
29+
decodeAbiParameters,
30+
AbiConstructorNotFoundError,
31+
type DecodeFunctionDataParameters,
2732
} from "viem";
2833
import { modularAccountAbi } from "../../abis/modularAccountAbi.js";
2934
import { serializeModuleEntity } from "../../actions/common/utils.js";
3035
import { nativeSMASigner } from "../nativeSMASigner.js";
3136
import { singleSignerMessageSigner } from "../../modules/single-signer-validation/signer.js";
37+
import { formatAbiItem } from "viem/utils";
3238

3339
export const executeUserOpSelector: Hex = "0x8DD7712F";
3440

@@ -55,17 +61,10 @@ export type ValidationDataParams =
5561
| {
5662
validationModuleAddress: Address;
5763
entityId?: never;
58-
deferredActionDigest?: never;
5964
}
6065
| {
6166
validationModuleAddress?: never;
6267
entityId: number;
63-
deferredActionDigest?: never;
64-
}
65-
| {
66-
deferredActionDigest: Hex;
67-
validationModuleAddress?: never;
68-
entityId?: never;
6968
};
7069

7170
export type ModularAccountV2<
@@ -95,8 +94,16 @@ export type CreateMAV2BaseParams<
9594
signer: TSigner;
9695
signerEntity?: SignerEntity;
9796
accountAddress: Address;
98-
deferredActionDigest?: Hex;
99-
};
97+
} & (
98+
| {
99+
deferredActionDigest: Hex;
100+
nonce: bigint;
101+
}
102+
| {
103+
deferredActionDigest?: never;
104+
nonce?: never;
105+
}
106+
);
100107

101108
export type CreateMAV2BaseReturnType<
102109
TSigner extends SmartAccountSigner = SmartAccountSigner
@@ -120,6 +127,7 @@ export async function createMAv2Base<
120127
} = {},
121128
accountAddress,
122129
deferredActionDigest,
130+
nonce,
123131
...remainingToSmartContractAccountParams
124132
} = config;
125133

@@ -140,61 +148,54 @@ export async function createMAv2Base<
140148

141149
let deferredAction:
142150
| undefined
143-
| { nonce: bigint; data: Hex; hasAssociatedExecHooks: boolean };
151+
| { data: Hex; hasAssociatedExecHooks: boolean };
144152

145153
// deferred action format:
146-
// 32 bytes nonce | 4 bytes len | 21 bytes valLocator | 8 bytes deadline |
147-
// variable bytes calldata | 4 bytes sig length | variable bytes sig
154+
// 4 bytes len | 21 bytes valLocator | 6 bytes deadline | variable bytes calldata |
155+
// 4 bytes sig length | variable bytes sig
148156
if (deferredActionDigest) {
149-
// we always infer entityId and isGlobalValidation from the deferred action case
150-
// this number here is in the range of [0, 7]
151-
if (Number(deferredActionDigest[116]) >= 4) {
152-
// TODO: implement > 4 case which is direct validation
153-
throw new Error("Direct call validation not supported yet");
154-
} else {
155-
// 1st bit is isGlobalValidation, 2nd bit is isDeferredAction
156-
signerEntity.isGlobalValidation =
157-
Number(deferredActionDigest[116]) % 2 === 0 ? false : true;
158-
signerEntity.entityId = hexToNumber(
159-
`0x${deferredActionDigest.slice(98, 116)}`
160-
);
161-
}
162-
163157
// Set these values if the deferred action has not been consumed. We check this with the EP
164-
const deferredActionNonce = hexToBigInt(
165-
`0x${deferredActionDigest.slice(2, 66)}`
166-
);
167-
const nextNonceForDeferredActionNonceKey: bigint =
158+
const nextNonceForDeferredActionNonce: bigint =
168159
(await entryPointContract.read.getNonce([
169160
accountAddress,
170-
deferredActionNonce >> 64n,
161+
nonce >> 64n,
171162
])) as bigint;
172-
if (deferredActionNonce === nextNonceForDeferredActionNonceKey) {
173-
// parse deferred action to get
163+
164+
if (nonce === nextNonceForDeferredActionNonce) {
165+
// parse deferred action to get length. -21 for val locator, -6 for timestamp
166+
174167
const callBytesLength =
175-
hexToNumber(`0x${deferredActionDigest.slice(66, 74)}`) - 29;
168+
hexToNumber(`0x${deferredActionDigest.slice(2, 10)}`) - 27;
176169
if (callBytesLength < 4) {
177170
throw new Error("Invalid deferred action calldata length");
178171
}
179-
const deferredActionCall = decodeFunctionData({
180-
abi: modularAccountAbi,
181-
data: `0x${deferredActionDigest.slice(132, 132 + callBytesLength * 2)}`,
182-
});
183-
184-
const hooks =
185-
deferredActionCall.functionName !== "installValidation"
186-
? []
187-
: deferredActionCall.args[3];
188-
189-
deferredAction = {
190-
nonce: deferredActionNonce,
191-
data: `0x${deferredActionDigest.slice(66)}`,
192-
// get the 25th byte of each hook, execution hooks have the 1st bit empty and val have it set.
193-
// for a string, this is 2 (0x) + 2 * 25 = 52
194-
// we can just get the single character since we are just checking a single bit
195-
hasAssociatedExecHooks: hooks.map((h) => Number(h[52]) % 2).includes(0),
196-
};
197-
} else if (nextNonceForDeferredActionNonceKey < deferredActionNonce) {
172+
try {
173+
const bytes2: Hex = `0x${deferredActionDigest.slice(
174+
64,
175+
64 + callBytesLength * 2
176+
)}`;
177+
178+
const { functionName, args } = decodeFunctionData({
179+
abi: modularAccountAbi,
180+
data: bytes2 as `0x${string}`,
181+
});
182+
183+
if (functionName === "installValidation") {
184+
deferredAction = {
185+
data: `0x${deferredActionDigest.slice(2)}`,
186+
// get the 25th byte of each hook, execution hooks have the 1st bit empty and val have it set.
187+
// for a string, this is 2 (0x) + 2 * 25 = 52
188+
// we can just get the single character since we are just checking a single bit
189+
hasAssociatedExecHooks: args[3]
190+
.map((h) => Number(h[52]) % 2)
191+
.includes(0),
192+
};
193+
}
194+
} catch {
195+
// if the decode fails, it could be because the function is an non-native selector that has been installed
196+
// in these cases, we don't do anything.
197+
}
198+
} else if (nextNonceForDeferredActionNonce < nonce) {
198199
throw new Error("Deferred action nonce invalid");
199200
}
200201
}
@@ -231,8 +232,8 @@ export async function createMAv2Base<
231232
!!(await client.getCode({ address: accountAddress }));
232233

233234
const getNonce = async (nonceKey: bigint = 0n): Promise<bigint> => {
234-
if (deferredAction) {
235-
return deferredAction.nonce;
235+
if (deferredActionDigest) {
236+
return nonce;
236237
}
237238

238239
if (nonceKey > maxUint152) {

account-kit/smart-contracts/src/ma-v2/account/modularAccountV2.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ export type CreateModularAccountV2Params<
3636
> & {
3737
signer: TSigner;
3838
entryPoint?: EntryPointDef<"0.7.0", Chain>;
39+
deferredActionDigest?: Hex;
3940
signerEntity?: SignerEntity;
41+
nonce?: bigint;
4042
}) &
4143
(
4244
| {
@@ -103,6 +105,8 @@ export async function createModularAccountV2(
103105
entityId: DEFAULT_OWNER_ENTITY_ID,
104106
},
105107
signerEntity: { entityId = DEFAULT_OWNER_ENTITY_ID } = {},
108+
deferredActionDigest,
109+
nonce,
106110
} = config;
107111

108112
const client = createBundlerClient({
@@ -176,15 +180,27 @@ export async function createModularAccountV2(
176180
}
177181
})();
178182

179-
return createMAv2Base({
180-
source: "ModularAccountV2",
181-
transport,
182-
chain,
183-
signer,
184-
entryPoint,
185-
signerEntity,
186-
...accountFunctions,
187-
});
183+
return nonce && deferredActionDigest
184+
? createMAv2Base({
185+
source: "ModularAccountV2",
186+
transport,
187+
chain,
188+
signer,
189+
entryPoint,
190+
signerEntity,
191+
nonce,
192+
deferredActionDigest,
193+
...accountFunctions,
194+
})
195+
: createMAv2Base({
196+
source: "ModularAccountV2",
197+
transport,
198+
chain,
199+
signer,
200+
entryPoint,
201+
signerEntity,
202+
...accountFunctions,
203+
});
188204
}
189205

190206
// If we add more valid modes, the switch case branch's mode will no longer be `never`, which will cause a compile time error here and ensure we handle the new type.
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import {
2+
erc7677Middleware,
3+
LocalAccountSigner,
4+
type SmartAccountSigner,
5+
} from "@aa-sdk/core";
6+
import {
7+
custom,
8+
parseEther,
9+
publicActions,
10+
testActions,
11+
type TestActions,
12+
} from "viem";
13+
import { installValidationActions } from "@account-kit/smart-contracts/experimental";
14+
import {
15+
// createModularAccountV2Client,
16+
type SignerEntity,
17+
} from "@account-kit/smart-contracts";
18+
import { local070Instance } from "~test/instances.js";
19+
import { setBalance } from "viem/actions";
20+
import { accounts } from "~test/constants.js";
21+
import { alchemyGasAndPaymasterAndDataMiddleware } from "@account-kit/infra";
22+
import { deferralActions } from "./deferralActions.js";
23+
import { PermissionBuilder, PermissionType } from "../permissionBuilder.js";
24+
import { createModularAccountV2Client } from "../client/client.js";
25+
26+
// Note: These tests maintain a shared state to not break the local-running rundler by desyncing the chain.
27+
describe("MA v2 deferral actions tests", async () => {
28+
const instance = local070Instance;
29+
30+
let client: ReturnType<typeof instance.getClient> &
31+
ReturnType<typeof publicActions> &
32+
TestActions;
33+
34+
beforeAll(async () => {
35+
client = instance
36+
.getClient()
37+
.extend(publicActions)
38+
.extend(testActions({ mode: "anvil" }));
39+
});
40+
41+
const getTargetBalance = async (): Promise<bigint> =>
42+
client.getBalance({
43+
address: target,
44+
});
45+
46+
const signer: SmartAccountSigner = new LocalAccountSigner(
47+
accounts.fundedAccountOwner
48+
);
49+
50+
const target = "0x000000000000000000000000000000000000dEaD";
51+
const sendAmount = parseEther("1");
52+
53+
it("tests the full deferred actions flow", async () => {
54+
const provider = (await givenConnectedProvider({ signer }))
55+
.extend(deferralActions)
56+
.extend(installValidationActions);
57+
58+
await setBalance(instance.getClient(), {
59+
address: provider.getAddress(),
60+
value: parseEther("2"),
61+
});
62+
63+
const sessionKeyEntityId = 1;
64+
const sessionKey: SmartAccountSigner = new LocalAccountSigner(
65+
accounts.unfundedAccountOwner
66+
);
67+
68+
// these can be default values or from call arguments
69+
const { entityId, nonce } = await provider.getEntityIdAndNonce({
70+
entityId: 0,
71+
nonceKey: 0n,
72+
isGlobalValidation: true,
73+
});
74+
75+
// TODO: remove nonce override here
76+
const { typedData } = await new PermissionBuilder(provider)
77+
.configure({
78+
key: {
79+
publicKey: await sessionKey.getAddress(),
80+
type: "secp256k1",
81+
},
82+
entityId: sessionKeyEntityId,
83+
nonceKeyOverride: 0n, // TODO: add nonce override here
84+
})
85+
.addPermission({
86+
permission: {
87+
type: PermissionType.ROOT,
88+
},
89+
})
90+
.compile_deferred({
91+
deadline: 0,
92+
uoValidationEntityId: entityId,
93+
uoIsGlobalValidation: true,
94+
});
95+
96+
const sig = await provider.account.signTypedData(typedData);
97+
98+
const deferredActionDigest = await provider.buildDeferredActionDigest({
99+
typedData,
100+
sig,
101+
});
102+
103+
// TODO: need nonce, orig account address, orig account initcode, signer entity
104+
const sessionKeyClient = await createModularAccountV2Client({
105+
transport: custom(instance.getClient()),
106+
chain: instance.chain,
107+
accountAddress: provider.getAddress(),
108+
signer: sessionKey,
109+
signerEntity: { isGlobalValidation: false, entityId: sessionKeyEntityId },
110+
initCode: provider.account.getInitCode(),
111+
nonce,
112+
deferredActionDigest,
113+
});
114+
115+
const uoResult = await sessionKeyClient.sendUserOperation({
116+
uo: {
117+
target: target,
118+
value: sendAmount,
119+
data: "0x",
120+
},
121+
});
122+
123+
await provider.waitForUserOperationTransaction(uoResult);
124+
});
125+
126+
const givenConnectedProvider = async ({
127+
signer,
128+
signerEntity,
129+
accountAddress,
130+
paymasterMiddleware,
131+
salt = 0n,
132+
}: {
133+
signer: SmartAccountSigner;
134+
signerEntity?: SignerEntity;
135+
accountAddress?: `0x${string}`;
136+
paymasterMiddleware?: "alchemyGasAndPaymasterAndData" | "erc7677";
137+
salt?: bigint;
138+
}) =>
139+
createModularAccountV2Client({
140+
chain: instance.chain,
141+
signer,
142+
accountAddress,
143+
signerEntity,
144+
transport: custom(instance.getClient()),
145+
...(paymasterMiddleware === "alchemyGasAndPaymasterAndData"
146+
? alchemyGasAndPaymasterAndDataMiddleware({
147+
policyId: "FAKE_POLICY_ID",
148+
// @ts-ignore (expects an alchemy transport, but we're using a custom transport for mocking)
149+
transport: custom(instance.getClient()),
150+
})
151+
: paymasterMiddleware === "erc7677"
152+
? erc7677Middleware()
153+
: {}),
154+
salt,
155+
});
156+
});

0 commit comments

Comments
 (0)