@ilalv3/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,132 @@
1
+ import { readFileSync } from "fs";
2
+ import { createPublicClient, createWalletClient, encodeAbiParameters, http, isAddress, } from "viem";
3
+ import { privateKeyToAccount } from "viem/accounts";
4
+ import { base, baseSepolia } from "viem/chains";
5
+ import { fmt, log, die } from "../ui.js";
6
+ const CHAINS = { "8453": base, "84532": baseSepolia };
7
+ const CNF_ISSUER_ABI = [
8
+ {
9
+ name: "mintWithProof",
10
+ type: "function",
11
+ stateMutability: "nonpayable",
12
+ inputs: [
13
+ { name: "proof", type: "bytes" },
14
+ { name: "publicInputs", type: "uint256[]" },
15
+ ],
16
+ outputs: [{ name: "tokenId", type: "uint256" }],
17
+ },
18
+ {
19
+ name: "renewWithProof",
20
+ type: "function",
21
+ stateMutability: "nonpayable",
22
+ inputs: [
23
+ { name: "proof", type: "bytes" },
24
+ { name: "publicInputs", type: "uint256[]" },
25
+ ],
26
+ outputs: [],
27
+ },
28
+ {
29
+ name: "isValid",
30
+ type: "function",
31
+ stateMutability: "view",
32
+ inputs: [{ name: "wallet", type: "address" }],
33
+ outputs: [{ type: "bool" }],
34
+ },
35
+ ];
36
+ function loadSnarkjsProof(proofPath, publicPath) {
37
+ let rawProof;
38
+ let rawPublic;
39
+ try {
40
+ rawProof = JSON.parse(readFileSync(proofPath, "utf8"));
41
+ }
42
+ catch {
43
+ die(`Cannot read proof file: ${proofPath}`);
44
+ }
45
+ try {
46
+ rawPublic = JSON.parse(readFileSync(publicPath, "utf8"));
47
+ }
48
+ catch {
49
+ die(`Cannot read public inputs file: ${publicPath}`);
50
+ }
51
+ if (rawProof.protocol !== "groth16") {
52
+ log.warn(`Unexpected proof protocol: ${rawProof.protocol} (expected groth16)`);
53
+ }
54
+ // snarkjs bn128 proofs store G2 points in reversed coordinate order
55
+ const a = [BigInt(rawProof.pi_a[0]), BigInt(rawProof.pi_a[1])];
56
+ const b = [
57
+ [BigInt(rawProof.pi_b[0][1]), BigInt(rawProof.pi_b[0][0])],
58
+ [BigInt(rawProof.pi_b[1][1]), BigInt(rawProof.pi_b[1][0])],
59
+ ];
60
+ const c = [BigInt(rawProof.pi_c[0]), BigInt(rawProof.pi_c[1])];
61
+ const proofBytes = encodeAbiParameters([
62
+ { type: "uint256[2]" },
63
+ { type: "uint256[2][2]" },
64
+ { type: "uint256[2]" },
65
+ ], [a, b, c]);
66
+ const publicInputs = rawPublic.map((x) => BigInt(x));
67
+ return { proofBytes, publicInputs };
68
+ }
69
+ export async function proofMint(opts) {
70
+ await sendProofTx("mint", opts);
71
+ }
72
+ export async function proofRenew(opts) {
73
+ await sendProofTx("renew", opts);
74
+ }
75
+ async function sendProofTx(mode, opts) {
76
+ const rawKey = opts.privateKey ?? process.env["PRIVATE_KEY"];
77
+ if (!rawKey)
78
+ die("Private key required. Use --private-key or set PRIVATE_KEY env var.");
79
+ if (!isAddress(opts.issuer))
80
+ die(`Invalid issuer address: ${opts.issuer}`);
81
+ const chain = CHAINS[opts.chain] ?? baseSepolia;
82
+ const account = privateKeyToAccount(rawKey);
83
+ const transport = opts.rpc ? http(opts.rpc) : http();
84
+ const publicClient = createPublicClient({ chain, transport });
85
+ const walletClient = createWalletClient({ account, chain, transport });
86
+ console.log();
87
+ console.log(fmt.bold(` ILAL Credential ZK ${mode === "mint" ? "Mint" : "Renew"}`));
88
+ log.line();
89
+ log.kv("wallet", account.address);
90
+ log.kv("issuer", opts.issuer);
91
+ log.kv("chain", chain.name);
92
+ log.kv("proof", opts.proof);
93
+ log.kv("public", opts.public);
94
+ log.line();
95
+ log.step("Loading snarkjs proof…");
96
+ const { proofBytes, publicInputs } = loadSnarkjsProof(opts.proof, opts.public);
97
+ log.ok(`Proof loaded — ${publicInputs.length} public input(s)`);
98
+ // Show key public input fields (PI_WALLET_HASH=0, PI_EXPIRES_AT=3)
99
+ if (publicInputs.length > 0)
100
+ log.kv("walletHash (PI[0])", publicInputs[0].toString(16).slice(0, 16) + "…");
101
+ if (publicInputs.length > 3) {
102
+ const expiresAt = Number(publicInputs[3]);
103
+ log.kv("expiresAt (PI[3])", new Date(expiresAt * 1000).toISOString());
104
+ }
105
+ log.line();
106
+ log.step(`Sending ${mode === "mint" ? "mintWithProof" : "renewWithProof"} transaction…`);
107
+ const hash = await walletClient.writeContract({
108
+ address: opts.issuer,
109
+ abi: CNF_ISSUER_ABI,
110
+ functionName: mode === "mint" ? "mintWithProof" : "renewWithProof",
111
+ args: [proofBytes, publicInputs],
112
+ });
113
+ log.step(`Tx sent: ${hash}`);
114
+ log.step("Waiting for confirmation…");
115
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
116
+ if (receipt.status === "success") {
117
+ log.ok(fmt.bold(fmt.green(`CNF ${mode === "mint" ? "minted" : "renewed"} via ZK proof`)));
118
+ log.kv("tx hash", hash);
119
+ log.kv("block", receipt.blockNumber.toString());
120
+ const valid = await publicClient.readContract({
121
+ address: opts.issuer,
122
+ abi: CNF_ISSUER_ABI,
123
+ functionName: "isValid",
124
+ args: [account.address],
125
+ });
126
+ log.kv("isValid()", valid ? fmt.green("true") : fmt.red("false"));
127
+ }
128
+ else {
129
+ die(`Transaction reverted. Hash: ${hash}`);
130
+ }
131
+ console.log();
132
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * prove.ts — `ilal credential prove`
3
+ *
4
+ * All-in-one command: builds Merkle tree, generates Groth16 ZK proof,
5
+ * (optionally) updates the on-chain merkleRoot, then mints or renews the CNF.
6
+ *
7
+ * Usage:
8
+ * ilal credential prove \
9
+ * --wallet 0x1b869... \
10
+ * --issuer 0x319c0... \
11
+ * --chain 84532 \
12
+ * --action mint # or renew (default: auto-detect)
13
+ * --update-root # call setMerkleRoot before minting
14
+ * --circuit-dir ./circuits/build
15
+ */
16
+ export declare function credentialProve(opts: {
17
+ wallet?: string;
18
+ issuer?: string;
19
+ chain?: string;
20
+ action?: string;
21
+ updateRoot: boolean;
22
+ circuitDir?: string;
23
+ outDir?: string;
24
+ rpc?: string;
25
+ privateKey?: string;
26
+ }): Promise<void>;
@@ -0,0 +1,292 @@
1
+ /**
2
+ * prove.ts — `ilal credential prove`
3
+ *
4
+ * All-in-one command: builds Merkle tree, generates Groth16 ZK proof,
5
+ * (optionally) updates the on-chain merkleRoot, then mints or renews the CNF.
6
+ *
7
+ * Usage:
8
+ * ilal credential prove \
9
+ * --wallet 0x1b869... \
10
+ * --issuer 0x319c0... \
11
+ * --chain 84532 \
12
+ * --action mint # or renew (default: auto-detect)
13
+ * --update-root # call setMerkleRoot before minting
14
+ * --circuit-dir ./circuits/build
15
+ */
16
+ import { execSync } from "child_process";
17
+ import { mkdirSync, writeFileSync, readFileSync } from "fs";
18
+ import { resolve, dirname } from "path";
19
+ import { fileURLToPath } from "url";
20
+ import { createPublicClient, createWalletClient, encodeAbiParameters, http, isAddress, keccak256, } from "viem";
21
+ import { privateKeyToAccount } from "viem/accounts";
22
+ import { base, baseSepolia } from "viem/chains";
23
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
24
+ // @ts-ignore — no bundled types for this package
25
+ import { IncrementalMerkleTree } from "@zk-kit/incremental-merkle-tree";
26
+ import { poseidon2, poseidon4 } from "poseidon-lite";
27
+ import { fmt, log, header, Spinner, die, dieOnContract } from "../ui.js";
28
+ import { withConfig } from "../config.js";
29
+ import { COINBASE_SCHEMA_UID } from "../constants.js";
30
+ const __dirname = dirname(fileURLToPath(import.meta.url));
31
+ const CHAINS = { "8453": base, "84532": baseSepolia };
32
+ const DEPTH = 20;
33
+ // ─── ABI ──────────────────────────────────────────────────────────────────────
34
+ const CNF_ABI = [
35
+ {
36
+ name: "mintWithProof", type: "function", stateMutability: "nonpayable",
37
+ inputs: [{ name: "proof", type: "bytes" }, { name: "publicInputs", type: "uint256[]" }],
38
+ outputs: [{ name: "tokenId", type: "uint256" }],
39
+ },
40
+ {
41
+ name: "renewWithProof", type: "function", stateMutability: "nonpayable",
42
+ inputs: [{ name: "proof", type: "bytes" }, { name: "publicInputs", type: "uint256[]" }],
43
+ outputs: [],
44
+ },
45
+ {
46
+ name: "setMerkleRoot", type: "function", stateMutability: "nonpayable",
47
+ inputs: [{ name: "_root", type: "uint256" }],
48
+ outputs: [],
49
+ },
50
+ {
51
+ name: "isValid", type: "function", stateMutability: "view",
52
+ inputs: [{ name: "wallet", type: "address" }],
53
+ outputs: [{ type: "bool" }],
54
+ },
55
+ {
56
+ name: "credentialOf", type: "function", stateMutability: "view",
57
+ inputs: [{ name: "wallet", type: "address" }],
58
+ outputs: [{ name: "tokenId", type: "uint256" }],
59
+ },
60
+ ];
61
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
62
+ function addressToField(addr) {
63
+ return BigInt(addr.toLowerCase());
64
+ }
65
+ function addressToBitsLSBFirst(addr) {
66
+ const field = addressToField(addr);
67
+ const bits = [];
68
+ for (let i = 0; i < 160; i++)
69
+ bits.push(Number((field >> BigInt(i)) & 1n));
70
+ return bits;
71
+ }
72
+ /** keccak256(wallet_20_bytes) >> 4 — matches the circuit constraint. */
73
+ function computeWalletHash(walletAddr) {
74
+ const checksummed = walletAddr;
75
+ // viem's keccak256 takes hex bytes — the address as 20 raw bytes
76
+ const hash = keccak256(checksummed);
77
+ return BigInt(hash) >> 4n;
78
+ }
79
+ function poseidonField(value) {
80
+ return poseidon2([value, 0n]);
81
+ }
82
+ function schemaHash(schemaUID) {
83
+ const schemaHex = schemaUID.replace("0x", "").padStart(64, "0");
84
+ const schemaLo = BigInt("0x" + schemaHex.slice(32));
85
+ const schemaHi = BigInt("0x" + schemaHex.slice(0, 32));
86
+ return poseidon2([schemaLo, schemaHi]);
87
+ }
88
+ function findCircuitDir(override) {
89
+ if (override)
90
+ return resolve(override);
91
+ // Look relative to the CLI package root (cli/ → circuits/build)
92
+ const candidates = [
93
+ resolve(__dirname, "../../../../circuits/build"), // dev: cli/src/commands → circuits/build
94
+ resolve(__dirname, "../../../circuits/build"),
95
+ resolve(process.cwd(), "circuits/build"),
96
+ resolve(process.cwd(), "build"),
97
+ ];
98
+ for (const p of candidates) {
99
+ try {
100
+ readFileSync(resolve(p, "ilal.zkey"));
101
+ return p;
102
+ }
103
+ catch { /* not found */ }
104
+ }
105
+ die("Circuit build directory not found.\n" +
106
+ " Run: bash circuits/scripts/compile.sh\n" +
107
+ " Or pass: --circuit-dir <path/to/circuits/build>");
108
+ }
109
+ function generateProof(opts) {
110
+ const { walletAddr, issuerAddr, circuitDir, outDir } = opts;
111
+ mkdirSync(outDir, { recursive: true });
112
+ const walletField = addressToField(walletAddr);
113
+ const walletBits = addressToBitsLSBFirst(walletAddr);
114
+ const walletHash = computeWalletHash(walletAddr);
115
+ const issuerHash = poseidonField(addressToField(issuerAddr));
116
+ const schemaHashValue = schemaHash(COINBASE_SCHEMA_UID);
117
+ const expiresAt = BigInt(Math.floor(Date.now() / 1000) + 90 * 24 * 3600); // +90 days
118
+ // Build single-leaf Poseidon Merkle tree
119
+ const leaf = poseidon4([walletField, 2n, 840n, expiresAt]);
120
+ const tree = new IncrementalMerkleTree(poseidon2, DEPTH, 0n, 2);
121
+ tree.insert(leaf);
122
+ const merkleProof = tree.createProof(0);
123
+ const merkleRoot = tree.root;
124
+ // Build input.json
125
+ const input = {
126
+ walletField: walletField.toString(),
127
+ walletBits: walletBits.map(String),
128
+ kycLevel: "2",
129
+ countryCode: "840",
130
+ merklePathElements: merkleProof.siblings.map((s) => s[0].toString()),
131
+ merklePathIndices: merkleProof.pathIndices.map(String),
132
+ walletHash: walletHash.toString(),
133
+ issuerHash: issuerHash.toString(),
134
+ schemaHash: schemaHashValue.toString(),
135
+ expiresAt: expiresAt.toString(),
136
+ revealFlags: "0",
137
+ merkleRoot: merkleRoot.toString(),
138
+ };
139
+ const inputPath = resolve(outDir, "input.json");
140
+ const wtnsPath = resolve(outDir, "witness.wtns");
141
+ const proofPath = resolve(outDir, "proof.json");
142
+ const publicPath = resolve(outDir, "public.json");
143
+ const wasmPath = resolve(circuitDir, "ilal_js/ilal.wasm");
144
+ const witnessJs = resolve(circuitDir, "ilal_js/generate_witness.js");
145
+ const zkeyPath = resolve(circuitDir, "ilal.zkey");
146
+ writeFileSync(inputPath, JSON.stringify(input, null, 2));
147
+ // Generate witness
148
+ execSync(`node "${witnessJs}" "${wasmPath}" "${inputPath}" "${wtnsPath}"`, {
149
+ stdio: "pipe",
150
+ cwd: dirname(witnessJs),
151
+ });
152
+ // Generate proof
153
+ execSync(`npx snarkjs groth16 prove "${zkeyPath}" "${wtnsPath}" "${proofPath}" "${publicPath}"`, {
154
+ stdio: "pipe",
155
+ });
156
+ // Verify locally
157
+ const vkeyPath = resolve(circuitDir, "ilal_vkey.json");
158
+ execSync(`npx snarkjs groth16 verify "${vkeyPath}" "${publicPath}" "${proofPath}"`, {
159
+ stdio: "pipe",
160
+ });
161
+ const publicJson = JSON.parse(readFileSync(publicPath, "utf8"));
162
+ // Read merkleRoot from circuit output (public.json[5]) — guaranteed to match
163
+ // what we'll pass to the contract, eliminating any JS/circuit hash discrepancy.
164
+ const circuitMerkleRoot = BigInt(publicJson[5]);
165
+ return {
166
+ proofJson: JSON.parse(readFileSync(proofPath, "utf8")),
167
+ publicJson,
168
+ merkleRoot: circuitMerkleRoot,
169
+ };
170
+ }
171
+ function encodeProof(proofJson, publicJson) {
172
+ const p = proofJson;
173
+ const a = [BigInt(p.pi_a[0]), BigInt(p.pi_a[1])];
174
+ const b = [
175
+ [BigInt(p.pi_b[0][1]), BigInt(p.pi_b[0][0])],
176
+ [BigInt(p.pi_b[1][1]), BigInt(p.pi_b[1][0])],
177
+ ];
178
+ const c = [BigInt(p.pi_c[0]), BigInt(p.pi_c[1])];
179
+ const proofBytes = encodeAbiParameters([{ type: "uint256[2]" }, { type: "uint256[2][2]" }, { type: "uint256[2]" }], [a, b, c]);
180
+ return { proofBytes, publicInputs: publicJson.map((x) => BigInt(x)) };
181
+ }
182
+ // ─── Main export ──────────────────────────────────────────────────────────────
183
+ export async function credentialProve(opts) {
184
+ const cfg = withConfig(opts);
185
+ const rawKey = cfg.privateKey ?? process.env["PRIVATE_KEY"];
186
+ if (!rawKey)
187
+ die("Private key required. Use --private-key or set PRIVATE_KEY env var.");
188
+ if (!cfg.wallet)
189
+ die("Wallet address required. Use --wallet or set issuer in .ilal.json");
190
+ if (!cfg.issuer)
191
+ die("Issuer address required. Use --issuer or run `ilal init`");
192
+ if (!isAddress(cfg.wallet))
193
+ die(`Invalid wallet address: ${cfg.wallet}`);
194
+ if (!isAddress(cfg.issuer))
195
+ die(`Invalid issuer address: ${cfg.issuer}`);
196
+ const chain = CHAINS[cfg.chain ?? "84532"] ?? baseSepolia;
197
+ const account = privateKeyToAccount(rawKey);
198
+ const transport = cfg.rpc ? http(cfg.rpc) : http();
199
+ const pubClient = createPublicClient({ chain, transport });
200
+ const walClient = createWalletClient({ account, chain, transport });
201
+ header("ILAL Credential ZK Prove", chain.name);
202
+ log.kv("wallet", fmt.cyan(cfg.wallet));
203
+ log.kv("issuer", fmt.cyan(cfg.issuer));
204
+ log.line();
205
+ // ── Auto-detect mint vs renew ──────────────────────────────────────────────
206
+ let action = opts.action;
207
+ if (!action) {
208
+ const spin = new Spinner("Checking existing credential…").start();
209
+ const tokenId = await pubClient.readContract({
210
+ address: cfg.issuer,
211
+ abi: CNF_ABI,
212
+ functionName: "credentialOf",
213
+ args: [cfg.wallet],
214
+ });
215
+ action = tokenId === 0n ? "mint" : "renew";
216
+ spin.succeed(`Action: ${fmt.cyan(action)}${tokenId > 0n ? fmt.gray(` (token #${tokenId})`) : ""}`);
217
+ }
218
+ // ── Find circuit build dir ─────────────────────────────────────────────────
219
+ const circuitDir = findCircuitDir(cfg.circuitDir);
220
+ const outDir = cfg.outDir
221
+ ? resolve(cfg.outDir)
222
+ : resolve(circuitDir, "../../outputs");
223
+ log.line();
224
+ // ── Generate proof ─────────────────────────────────────────────────────────
225
+ const spin = new Spinner("Building Merkle tree & generating ZK proof…").start();
226
+ let proofResult;
227
+ try {
228
+ spin.update("Generating ZK witness…");
229
+ proofResult = generateProof({ walletAddr: cfg.wallet, issuerAddr: cfg.issuer, circuitDir, outDir });
230
+ spin.succeed(`Proof generated & verified locally`);
231
+ }
232
+ catch (e) {
233
+ spin.fail("Proof generation failed");
234
+ die(e instanceof Error ? e.message.split("\n")[0] : String(e));
235
+ }
236
+ log.kv("expiresAt", fmt.cyan(new Date((Date.now() + 90 * 24 * 3600 * 1000)).toISOString().split("T")[0]));
237
+ log.kv("merkleRoot", fmt.gray(proofResult.merkleRoot.toString().slice(0, 22) + "…"));
238
+ log.line();
239
+ // ── Update merkle root on-chain ────────────────────────────────────────────
240
+ if (opts.updateRoot) {
241
+ const rootSpin = new Spinner("Updating merkleRoot on-chain…").start();
242
+ try {
243
+ const rootHash = await walClient.writeContract({
244
+ address: cfg.issuer,
245
+ abi: CNF_ABI,
246
+ functionName: "setMerkleRoot",
247
+ args: [proofResult.merkleRoot],
248
+ });
249
+ await pubClient.waitForTransactionReceipt({ hash: rootHash });
250
+ rootSpin.succeed(`merkleRoot updated ${fmt.gray(fmt.hash(rootHash))}`);
251
+ }
252
+ catch (e) {
253
+ rootSpin.fail("setMerkleRoot failed");
254
+ dieOnContract(e);
255
+ }
256
+ }
257
+ // ── Send mint / renew tx ───────────────────────────────────────────────────
258
+ const { proofBytes, publicInputs } = encodeProof(proofResult.proofJson, proofResult.publicJson);
259
+ const fnName = action === "mint" ? "mintWithProof" : "renewWithProof";
260
+ const txSpin = new Spinner(`Sending ${fnName}…`).start();
261
+ let txHash;
262
+ try {
263
+ txHash = await walClient.writeContract({
264
+ address: cfg.issuer,
265
+ abi: CNF_ABI,
266
+ functionName: fnName,
267
+ args: [proofBytes, publicInputs],
268
+ });
269
+ txSpin.update(`Confirming ${fmt.gray(fmt.hash(txHash))}…`);
270
+ const receipt = await pubClient.waitForTransactionReceipt({ hash: txHash });
271
+ if (receipt.status !== "success") {
272
+ txSpin.fail("Transaction reverted");
273
+ die(`Tx failed: ${txHash}`);
274
+ }
275
+ txSpin.succeed(fmt.bold(fmt.green(`CNF ${action === "mint" ? "minted" : "renewed"} via ZK proof`)));
276
+ }
277
+ catch (e) {
278
+ txSpin.fail(`${fnName} failed`);
279
+ dieOnContract(e);
280
+ }
281
+ log.line();
282
+ log.kv("tx", fmt.gray(txHash));
283
+ log.kv("block", fmt.gray((await pubClient.getTransactionReceipt({ hash: txHash })).blockNumber.toString()));
284
+ const valid = await pubClient.readContract({
285
+ address: cfg.issuer,
286
+ abi: CNF_ABI,
287
+ functionName: "isValid",
288
+ args: [cfg.wallet],
289
+ });
290
+ log.kv("isValid()", valid ? fmt.green("✓ true") : fmt.red("✗ false"));
291
+ console.log();
292
+ }
@@ -0,0 +1,11 @@
1
+ export declare function sessionSign(opts: {
2
+ user?: string;
3
+ pool: string;
4
+ action: string;
5
+ hook: string;
6
+ issuer: string;
7
+ caller?: string;
8
+ chain: string;
9
+ ttl: number;
10
+ privateKey?: string;
11
+ }): Promise<void>;
@@ -0,0 +1,105 @@
1
+ import { createWalletClient, encodeAbiParameters, http, isAddress, isHex, parseAbiParameters, } from "viem";
2
+ import { privateKeyToAccount } from "viem/accounts";
3
+ import { base, baseSepolia } from "viem/chains";
4
+ import { fmt, log, header, die } from "../ui.js";
5
+ const CHAINS = {
6
+ "8453": base,
7
+ "84532": baseSepolia,
8
+ };
9
+ const ACTIONS = {
10
+ swap: 1,
11
+ addliquidity: 2,
12
+ removeliquidity: 3,
13
+ };
14
+ const SESSION_TOKEN_TYPE = [
15
+ { name: "user", type: "address" },
16
+ { name: "authorizedCaller", type: "address" },
17
+ { name: "cnfIssuer", type: "address" },
18
+ { name: "chainId", type: "uint256" },
19
+ { name: "verifyingHook", type: "address" },
20
+ { name: "poolId", type: "bytes32" },
21
+ { name: "action", type: "uint8" },
22
+ { name: "deadline", type: "uint64" },
23
+ { name: "nonce", type: "bytes32" },
24
+ ];
25
+ const HOOK_DATA_ABI = parseAbiParameters([
26
+ "(address user, address authorizedCaller, address cnfIssuer, uint256 chainId, address verifyingHook, bytes32 poolId, uint8 action, uint64 deadline, bytes32 nonce) token",
27
+ "bytes signature",
28
+ ]);
29
+ export async function sessionSign(opts) {
30
+ // Resolve private key
31
+ const rawKey = opts.privateKey ?? process.env["PRIVATE_KEY"];
32
+ if (!rawKey)
33
+ die("Private key required. Use --private-key or set PRIVATE_KEY env var.");
34
+ if (!isHex(rawKey) || rawKey.length !== 66)
35
+ die("Invalid private key format (expected 0x + 32 bytes).");
36
+ const account = privateKeyToAccount(rawKey);
37
+ const user = (opts.user ?? account.address);
38
+ const authorizedCaller = (opts.caller ?? user);
39
+ if (!isAddress(user))
40
+ die(`Invalid user address: ${user}`);
41
+ if (!isAddress(authorizedCaller))
42
+ die(`Invalid authorized caller address: ${authorizedCaller}`);
43
+ if (!isAddress(opts.hook))
44
+ die(`Invalid hook address: ${opts.hook}`);
45
+ if (!isAddress(opts.issuer))
46
+ die(`Invalid issuer address: ${opts.issuer}`);
47
+ if (!isHex(opts.pool) || opts.pool.length !== 66)
48
+ die("poolId must be a 32-byte hex string (0x + 64 chars).");
49
+ const actionKey = opts.action.toLowerCase().replace(/[^a-z]/g, "");
50
+ const actionCode = ACTIONS[actionKey];
51
+ if (actionCode === undefined)
52
+ die(`Unknown action "${opts.action}". Use: swap | addLiquidity | removeLiquidity`);
53
+ const chain = CHAINS[opts.chain] ?? baseSepolia;
54
+ const walletClient = createWalletClient({ account, chain, transport: http() });
55
+ const deadline = BigInt(Math.floor(Date.now() / 1000) + opts.ttl);
56
+ const nonce = `0x${Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("hex")}`;
57
+ const token = {
58
+ user,
59
+ authorizedCaller,
60
+ cnfIssuer: opts.issuer,
61
+ chainId: BigInt(chain.id),
62
+ verifyingHook: opts.hook,
63
+ poolId: opts.pool,
64
+ action: actionCode,
65
+ deadline,
66
+ nonce: nonce,
67
+ };
68
+ header("Session Sign", chain.name);
69
+ log.section("Session");
70
+ log.kv("user", fmt.addr(user));
71
+ log.kv("caller", fmt.addr(authorizedCaller));
72
+ log.kv("chain", chain.name);
73
+ log.kv("pool", fmt.hash(opts.pool));
74
+ log.kv("action", opts.action);
75
+ log.kv("hook", fmt.addr(opts.hook));
76
+ log.kv("issuer", fmt.addr(opts.issuer));
77
+ log.kv("deadline", new Date(Number(deadline) * 1000).toISOString());
78
+ log.line();
79
+ log.step("Signing EIP-712 session token locally…");
80
+ log.step(fmt.gray("(no ILAL API call — pure local operation)"));
81
+ const signature = await walletClient.signTypedData({
82
+ account,
83
+ domain: {
84
+ name: "ILAL ComplianceHook",
85
+ version: "1",
86
+ chainId: BigInt(chain.id),
87
+ verifyingContract: opts.hook,
88
+ },
89
+ types: { SessionToken: SESSION_TOKEN_TYPE },
90
+ primaryType: "SessionToken",
91
+ message: token,
92
+ });
93
+ const hookData = encodeAbiParameters(HOOK_DATA_ABI, [token, signature]);
94
+ log.result("Session signed", fmt.badge("local", "green"), "green");
95
+ log.section("Output");
96
+ log.kv("signature", fmt.hash(signature));
97
+ log.kv("hookData", fmt.hash(hookData));
98
+ log.line();
99
+ console.log(` ${fmt.bold("Full hookData")}`);
100
+ console.log();
101
+ console.log(` ${fmt.cyan(hookData)}`);
102
+ console.log();
103
+ console.log(fmt.gray(" verifies: caller, deadline, chainId, hook, pool, action, sig, CNF"));
104
+ console.log();
105
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * status.ts — `ilal status`
3
+ *
4
+ * Dashboard: credential validity, hook config, pool policy — all in one view.
5
+ */
6
+ export declare function status(opts: {
7
+ wallet?: string;
8
+ issuer?: string;
9
+ hook?: string;
10
+ registry?: string;
11
+ pool?: string;
12
+ chain?: string;
13
+ rpc?: string;
14
+ }): Promise<void>;