Skip to content
This repository was archived by the owner on Oct 14, 2025. It is now read-only.

Commit 16df361

Browse files
committed
feat: session key and account recovery
1 parent be8b8ab commit 16df361

8 files changed

Lines changed: 208 additions & 40 deletions

File tree

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@ node_modules/
55
.vscode/
66

77
# Build output
8-
dist/
8+
bin/
9+
10+
# .env
11+
.env

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Openfort 7702 CLI
22

33
Get authorization hash, sign it, send a 7702 transaction to delegate Openfort Smart Account to your EOA.
4-
Enjoy all 4337 feature from the comfort of your EOA (gas sponsoring with Paymaster, session keys and account recovery).
4+
Enjoy 4337 features from the comfort of your EOA (batching, gas sponsoring with Paymaster, session keys).
55

66
## Requirements
77

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"commander": "^12.1.0",
88
"dotenv": "^16.4.5",
99
"figlet": "^1.8.0",
10-
"viem": "^2.21.19"
10+
"viem": "^2.21.25"
1111
},
1212
"bin": {
1313
"openfort-7702": "bin/index.js"

src/clients.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
import { createPublicClient, createWalletClient, http } from "viem";
2-
import { eip7702Actions } from "viem/experimental";
32
import { createBundlerClient } from "viem/account-abstraction";
4-
import { authority } from "./account";
5-
import { anvil } from "viem/chains";
3+
import { network } from "./constants";
64

75
export const publicClient = createPublicClient({
8-
chain: anvil,
6+
chain: network,
97
transport: http(),
108
});
119

1210
export const walletClient = createWalletClient({
13-
account: authority,
14-
chain: anvil,
11+
chain: network,
1512
transport: http(),
16-
}).extend(eip7702Actions());
13+
});
1714

1815
export const bundlerClient = createBundlerClient({
1916
client: publicClient,

src/constants.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import dotenv from "dotenv";
22
import type { Hex, Address } from "viem";
3+
import { odysseyTestnet, anvil } from "viem/chains";
34

45
dotenv.config();
56

@@ -13,3 +14,11 @@ export const openfortSmartAccountImplementation = process.env
1314
.OPENFORT_SMART_ACCOUNT_IMPLEMENTATION as Address;
1415
// Guardian Address
1516
export const guardianAddress = process.env.GUARDIAN_ADDRESS as Address;
17+
export const guardianPrivateKey = process.env.GUARDIAN_PRIVATE_KEY as Hex;
18+
19+
export const network = process.env.NETWORK === "anvil" ? anvil : odysseyTestnet;
20+
21+
export const recoveryPeriod = BigInt(process.env.RECOVERY_PERIOD ?? "172800"); // default 2 days in seconds
22+
export const securityPeriod = BigInt(process.env.SECURITY_PERIOD ?? "129600"); // default 1.5 days in seconds
23+
export const securityWindow = BigInt(process.env.SECURITY_WINDOW ?? "43200"); // 0.5 days in seconds
24+
export const lockPeriod = BigInt(process.env.LOCK_PERIOD ?? "432000"); // default 5 days in seconds

src/index.ts

Lines changed: 183 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { publicClient, bundlerClient } from "./clients";
1+
import { publicClient, walletClient, bundlerClient } from "./clients";
22
import {
33
hashAuthorization,
44
recoverAuthorizationAddress,
@@ -8,16 +8,26 @@ import { Command } from "commander";
88
import { authority } from "./account";
99
import { encodeFunctionData, parseAbi, parseSignature } from "viem";
1010
import { sendTransaction } from "viem/actions";
11-
import { anvil } from "viem/chains";
11+
import { network } from "./constants";
1212

1313
import {
1414
guardianAddress,
15+
guardianPrivateKey,
1516
openfortSmartAccountImplementation,
1617
openfortSmartAccountProxy,
18+
recoveryPeriod,
19+
securityPeriod,
20+
securityWindow,
21+
lockPeriod,
1722
} from "./constants";
1823
import assert = require("node:assert");
1924
import { getAccount } from "./openfortSmartAccount";
20-
import { entryPoint06Address } from "viem/account-abstraction";
25+
import {
26+
entryPoint06Address,
27+
getUserOperationHash,
28+
} from "viem/account-abstraction";
29+
import { privateKeyToAccount } from "viem/accounts";
30+
import { signTypedData } from "viem/accounts";
2131

2232
const figlet = require("figlet");
2333
const program = new Command();
@@ -33,6 +43,7 @@ program
3343
.command("get-nonce")
3444
.description("get authority account transaction count")
3545
.action(async () => {
46+
console.log("authority address:", authority.address);
3647
const EOAnonce = await publicClient.getTransactionCount({
3748
address: authority.address,
3849
});
@@ -57,7 +68,7 @@ program
5768

5869
const authorization = hashAuthorization({
5970
contractAddress: delegationDesignator,
60-
chainId: anvil.id,
71+
chainId: network.id,
6172
nonce: nonce + 1,
6273
});
6374
console.log("Authorization hash:", authorization);
@@ -90,7 +101,7 @@ program
90101

91102
const authorization: SignedAuthorization = {
92103
contractAddress: delegationDesignator,
93-
chainId: anvil.id,
104+
chainId: network.id,
94105
nonce: nonce + 1,
95106
...parseSignature(signature),
96107
};
@@ -111,10 +122,10 @@ program
111122
args: [
112123
openfortSmartAccountImplementation,
113124
entryPoint06Address,
114-
172800n,
115-
129600n,
116-
43200n,
117-
432000n,
125+
recoveryPeriod,
126+
securityPeriod,
127+
securityWindow,
128+
lockPeriod,
118129
guardianAddress,
119130
],
120131
});
@@ -129,29 +140,177 @@ program
129140
});
130141

131142
program
132-
.command("test-send-batch")
143+
.command("send-batch")
133144
.description("send 4337 wei to alice and bob within the same UserOperation")
134-
.action(async () => {
145+
.option("-s, --signer <signer>", "signer")
146+
.action(async ({ signer }) => {
135147
console.log(
136-
`Sending 4337 wei to alice & bob with EOA ${authority.address} in one UserOperation!`,
148+
`Sending 4337 wei to alice & bob from EOA ${authority.address} in one UserOperation!`,
137149
);
138150
const alice = "0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f";
139151
const bob = "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720";
140-
const openfortSmartAccount = await getAccount(authority);
141-
const userOp = await bundlerClient.sendUserOperation({
142-
account: openfortSmartAccount,
143-
calls: [
144-
{
145-
to: alice,
146-
value: 4337n,
147-
},
148-
{
149-
to: bob,
150-
value: 4337n,
152+
const openfortAccount = await getAccount(authority);
153+
// If session key is provided, we sign the UserOperation with the session key
154+
if (signer) {
155+
const signerAccount = privateKeyToAccount(signer);
156+
console.log("Signing UserOperation with signer", signerAccount.address);
157+
const unsignedUserOp = await bundlerClient.prepareUserOperation({
158+
account: openfortAccount,
159+
calls: [
160+
{
161+
to: alice,
162+
value: 4337n,
163+
},
164+
{
165+
to: bob,
166+
value: 4337n,
167+
},
168+
],
169+
});
170+
const userOpHash = await getUserOperationHash({
171+
chainId: network.id,
172+
entryPointAddress: entryPoint06Address,
173+
entryPointVersion: "0.6",
174+
userOperation: {
175+
...(unsignedUserOp as any),
176+
sender: openfortAccount.address,
151177
},
178+
});
179+
const signature = await signerAccount.signMessage({
180+
message: { raw: userOpHash },
181+
});
182+
const hash = await bundlerClient.sendUserOperation({
183+
...unsignedUserOp,
184+
signature,
185+
});
186+
console.log("User operation hash:", hash);
187+
}
188+
189+
// If no session key is provided, we sign the UserOperation with authority EOA
190+
// owner of the smart accoubt
191+
else {
192+
console.log("Signing UserOperation with smart account owner (authority)");
193+
const userOp = await bundlerClient.sendUserOperation({
194+
account: openfortAccount,
195+
calls: [
196+
{
197+
to: alice,
198+
value: 4337n,
199+
},
200+
{
201+
to: bob,
202+
value: 4337n,
203+
},
204+
],
205+
});
206+
console.log("User operation hash:", userOp);
207+
}
208+
});
209+
210+
program
211+
.command("recover-account")
212+
.description("recover a 7702-delegated EOA")
213+
.requiredOption("-n, --new-owner <new-owner>", "new owner account address")
214+
.action(async ({ newOwner }) => {
215+
const guardian = privateKeyToAccount(guardianPrivateKey);
216+
const openfortAccount = await getAccount(authority);
217+
218+
// Start the recovery process
219+
const hash = await walletClient.sendTransaction({
220+
account: guardian,
221+
to: openfortAccount.address,
222+
data: encodeFunctionData({
223+
abi: parseAbi(["function startRecovery(address)"]),
224+
args: [newOwner],
225+
}),
226+
});
227+
228+
console.log("Recovery Started:", hash);
229+
// Get EIP712 domain
230+
const domainData = await publicClient.readContract({
231+
address: openfortAccount.address,
232+
abi: parseAbi([
233+
"function eip712Domain() view returns (bytes1, string, string, uint256, address, bytes32, uint256[])",
234+
]),
235+
functionName: "eip712Domain",
236+
});
237+
238+
const [, name, version, chainId, verifyingContract] = domainData;
239+
240+
console.log("name", name);
241+
console.log("version", version);
242+
console.log("chainId", chainId);
243+
console.log("verifyingContract", verifyingContract);
244+
245+
// Get recovery details
246+
const recoveryDetails = await publicClient.readContract({
247+
address: openfortAccount.address,
248+
abi: parseAbi([
249+
"function recoveryDetails() view returns (address, uint256, uint256)",
250+
]),
251+
functionName: "recoveryDetails",
252+
});
253+
254+
const [, executeAfter, guardiansRequired] = recoveryDetails;
255+
256+
console.log("executeAfter", executeAfter);
257+
console.log("guardiansRequired", guardiansRequired);
258+
259+
// Use the defined type for the domain object
260+
const domain: Record<string, any> = {
261+
name,
262+
version,
263+
chainId,
264+
verifyingContract,
265+
};
266+
267+
const types = {
268+
EIP712Domain: [
269+
{ name: "name", type: "string" },
270+
{ name: "version", type: "string" },
271+
{ name: "chainId", type: "uint256" },
272+
{ name: "verifyingContract", type: "address" },
273+
],
274+
Recover: [
275+
{ name: "recoveryAddress", type: "address" },
276+
{ name: "executeAfter", type: "uint64" },
277+
{ name: "guardiansRequired", type: "uint32" },
152278
],
279+
};
280+
281+
const message = {
282+
recoveryAddress: newOwner,
283+
executeAfter,
284+
guardiansRequired,
285+
};
286+
287+
const signature = await signTypedData({
288+
privateKey: guardianPrivateKey,
289+
domain,
290+
types,
291+
primaryType: "Recover",
292+
message,
293+
});
294+
295+
console.log("Signature:", signature);
296+
console.log(`Mining ${recoveryPeriod} blocks to pass the recovery period`);
297+
// Mine blocks to pass the recovery period
298+
for (let i = 0; i < Number(recoveryPeriod); i++) {
299+
await publicClient.request({
300+
method: "evm_mine" as any,
301+
params: undefined,
302+
});
303+
}
304+
305+
const confirmRecoveryHash = await walletClient.sendTransaction({
306+
account: guardian,
307+
to: openfortAccount.address,
308+
data: encodeFunctionData({
309+
abi: parseAbi(["function completeRecovery(bytes[])"]),
310+
args: [[signature]],
311+
}),
153312
});
154-
console.log("User operation hash:", userOp);
313+
console.log("Recovery Confirmed:", confirmRecoveryHash);
155314
});
156315

157316
program.parse();

src/openfortSmartAccount.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from "viem/account-abstraction";
99
import type { UserOperation } from "viem/account-abstraction";
1010
import { toSmartAccount } from "viem/account-abstraction";
11-
import { anvil } from "viem/chains";
11+
import { network } from "./constants";
1212

1313
// Authority EOA behaves like a Smart Account
1414

@@ -98,7 +98,7 @@ export async function getAccount(authority: Account) {
9898
if (!authority || !authority.signMessage)
9999
throw new Error("Authority does not have signMessage method");
100100
const hash = getUserOperationHash({
101-
chainId: anvil.id,
101+
chainId: network.id,
102102
entryPointAddress: entryPoint06Address,
103103
entryPointVersion: "0.6",
104104
userOperation: {

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -428,10 +428,10 @@ undici-types@~6.19.2:
428428
resolved "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz"
429429
integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
430430

431-
viem@^2.21.19:
432-
version "2.21.19"
433-
resolved "https://registry.npmjs.org/viem/-/viem-2.21.19.tgz"
434-
integrity sha512-FdlkN+UI1IU5sYOmzvygkxsUNjDRD5YHht3gZFu2X9xFv6Z3h9pXq9ycrYQ3F17lNfb41O2Ot4/aqbUkwOv9dA==
431+
viem@^2.21.25:
432+
version "2.21.29"
433+
resolved "https://registry.yarnpkg.com/viem/-/viem-2.21.29.tgz#a86225b41968b89f23c9b792eb0745be6bb03cef"
434+
integrity sha512-n9LoCJjmI1XsE33nl+M4p3Wy5hczv7YC682RpX4Qk9cw8s9HJU+hUi5eDcNDPBcAwIHGCPKsf8yFBEYnE2XYVg==
435435
dependencies:
436436
"@adraffy/ens-normalize" "1.11.0"
437437
"@noble/curves" "1.6.0"

0 commit comments

Comments
 (0)