@ilalv3/cli 0.2.2 → 0.2.4

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.
package/README.md CHANGED
@@ -52,6 +52,10 @@ PRIVATE_KEY=0x... ilal credential mint \
52
52
  --attestation <AttestationUID> \
53
53
  --chain 84532
54
54
 
55
+ # Or, with the MockEAS owner key, create a fresh test attestation:
56
+ PRIVATE_KEY=0x... ilal demo attest --wallet 0xYourWallet
57
+ PRIVATE_KEY=0xYourWalletKey ilal credential mint --attestation <uid>
58
+
55
59
  # If the wallet needs more demo tokens:
56
60
  PRIVATE_KEY=0x... ilal demo faucet --wallet 0xYourWallet
57
61
  ```
@@ -99,9 +103,12 @@ Generates a Groth16 proof locally (~5s), verifies it on-chain, and mints/renews
99
103
  | `ilal session sign` | Sign a standalone SessionToken |
100
104
  | `ilal proof mint` | Mint CNF from existing proof.json + public.json |
101
105
  | `ilal deploy --mock` | Deploy a seeded testnet demo stack with MockEAS, tokens, router, hook, and policy |
106
+ | `ilal demo attest` | Create a MockEAS test attestation so a wallet can mint CNF |
102
107
  | `ilal demo faucet` | Mint mock demo TOKA/TOKB to a wallet |
103
108
  | `ilal deploy` | Deploy full ILAL contract stack |
104
109
 
110
+ Session note: ILAL hookData is a one-time EIP-712 authorization with a deadline and nonce. The expensive compliance step is the CNF issuance or renewal; swaps do not verify a fresh ZK proof. Use `ilal session sign` to export hookData, and `ilal swap --hook-data <hex>` to execute with an externally signed authorization.
111
+
105
112
  ## Configuration
106
113
 
107
114
  The CLI reads `.ilal.json` in the current directory. Run `ilal init` to create it, or pass flags directly:
@@ -10,3 +10,8 @@ export declare function demoFaucet(opts: {
10
10
  amount?: string;
11
11
  privateKey?: string;
12
12
  }): Promise<void>;
13
+ export declare function demoAttest(opts: {
14
+ wallet: string;
15
+ privateKey?: string;
16
+ expiresInDays?: string;
17
+ }): Promise<void>;
@@ -1,4 +1,4 @@
1
- import { createPublicClient, createWalletClient, formatUnits, http, isAddress, isHex, parseUnits, } from "viem";
1
+ import { createPublicClient, createWalletClient, decodeEventLog, formatUnits, http, isAddress, isHex, parseUnits, } from "viem";
2
2
  import { privateKeyToAccount } from "viem/accounts";
3
3
  import { base, baseSepolia } from "viem/chains";
4
4
  import { loadConfig } from "../config.js";
@@ -39,6 +39,30 @@ const ERC20_ABI = [
39
39
  { name: "allowance", type: "function", stateMutability: "view", inputs: [{ name: "owner", type: "address" }, { name: "spender", type: "address" }], outputs: [{ type: "uint256" }] },
40
40
  { name: "mint", type: "function", stateMutability: "nonpayable", inputs: [{ name: "to", type: "address" }, { name: "amount", type: "uint256" }], outputs: [] },
41
41
  ];
42
+ const MOCK_EAS_ABI = [
43
+ {
44
+ type: "event",
45
+ name: "AttestationCreated",
46
+ inputs: [
47
+ { name: "uid", type: "bytes32", indexed: true },
48
+ { name: "recipient", type: "address", indexed: true },
49
+ { name: "attester", type: "address", indexed: true },
50
+ ],
51
+ },
52
+ {
53
+ name: "attest",
54
+ type: "function",
55
+ stateMutability: "nonpayable",
56
+ inputs: [
57
+ { name: "schema", type: "bytes32" },
58
+ { name: "recipient", type: "address" },
59
+ { name: "attester", type: "address" },
60
+ { name: "expirationTime", type: "uint64" },
61
+ { name: "data", type: "bytes" },
62
+ ],
63
+ outputs: [{ type: "bytes32" }],
64
+ },
65
+ ];
42
66
  function stage(n, title, subtitle) {
43
67
  console.log();
44
68
  console.log(` ${fmt.badge(`step ${n}`, "cyan")} ${fmt.bold(title)}`);
@@ -148,6 +172,15 @@ export async function demoCheck(opts) {
148
172
  log.line();
149
173
  let score = 0;
150
174
  let total = 0;
175
+ let networkReady = false;
176
+ let configReady = true;
177
+ let codeReady = true;
178
+ let economicsReady = false;
179
+ let issuerPathReady = false;
180
+ let credentialReady = false;
181
+ let policyReady = false;
182
+ let walletBalancesReady = false;
183
+ const walletBalanceChecks = [];
151
184
  const pass = (condition) => {
152
185
  total++;
153
186
  if (condition)
@@ -158,6 +191,7 @@ export async function demoCheck(opts) {
158
191
  try {
159
192
  const block = await client.getBlockNumber();
160
193
  ok("latest block", block.toString());
194
+ networkReady = true;
161
195
  pass(true);
162
196
  }
163
197
  catch (e) {
@@ -190,18 +224,25 @@ export async function demoCheck(opts) {
190
224
  : value;
191
225
  ok(label, display);
192
226
  }
193
- else
227
+ else {
194
228
  bad(label, fmt.badge("missing", "red"));
229
+ configReady = false;
230
+ }
195
231
  pass(valid);
196
232
  }
197
233
  log.line();
198
234
  log.section("Contract Code", "must exist on-chain");
199
- pass(await hasCode(client, "CNFIssuer", cfg.issuer));
200
- pass(await hasCode(client, "ComplianceHook", cfg.hook));
201
- pass(await hasCode(client, "PolicyRegistry", cfg.registry));
202
- pass(await hasCode(client, "ILALRouter", cfg.router));
203
- pass(await hasCode(client, "currency0", cfg.tokenA));
204
- pass(await hasCode(client, "currency1", cfg.tokenB));
235
+ const codeChecks = [
236
+ await hasCode(client, "CNFIssuer", cfg.issuer),
237
+ await hasCode(client, "ComplianceHook", cfg.hook),
238
+ await hasCode(client, "PolicyRegistry", cfg.registry),
239
+ await hasCode(client, "ILALRouter", cfg.router),
240
+ await hasCode(client, "currency0", cfg.tokenA),
241
+ await hasCode(client, "currency1", cfg.tokenB),
242
+ ];
243
+ for (const check of codeChecks)
244
+ pass(check);
245
+ codeReady = codeChecks.every(Boolean);
205
246
  log.line();
206
247
  if (cfg.router && isAddress(cfg.router)) {
207
248
  log.section("Verified Flow Economics");
@@ -213,10 +254,12 @@ export async function demoCheck(opts) {
213
254
  ok("LP fee", cfg.fee === "8388608" ? `${fmt.badge("dynamic", "green")} verified 0.05%` : "pool fee tier");
214
255
  ok("ILAL fee", protocolFeePips > 0 ? `${fmt.badge("protocol", "cyan")} ${pipsToPercent(protocolFeePips)}` : fmt.badge("off", "yellow"));
215
256
  ok("treasury", fmt.addr(treasury));
257
+ economicsReady = true;
216
258
  pass(true);
217
259
  }
218
260
  catch {
219
261
  warn("protocol fee", fmt.badge("legacy router", "yellow"));
262
+ economicsReady = true;
220
263
  pass(true);
221
264
  }
222
265
  log.line();
@@ -237,6 +280,7 @@ export async function demoCheck(opts) {
237
280
  ok("issuance path", fmt.badge("ZK", "green"));
238
281
  else
239
282
  warn("issuance path", fmt.badge("not ready", "yellow"));
283
+ issuerPathReady = hasEASPath || hasZKPath;
240
284
  if (root === 0n)
241
285
  warn("merkleRoot", fmt.badge("not set", "yellow"));
242
286
  else
@@ -245,7 +289,7 @@ export async function demoCheck(opts) {
245
289
  warn("zkVerifier", fmt.badge("not set", "yellow"));
246
290
  else
247
291
  ok("zkVerifier", fmt.addr(verifier));
248
- pass(hasEASPath || hasZKPath);
292
+ pass(issuerPathReady);
249
293
  }
250
294
  catch (e) {
251
295
  bad("issuer reads", e instanceof Error ? e.message.split("\n")[0] : String(e));
@@ -261,6 +305,7 @@ export async function demoCheck(opts) {
261
305
  ok("credential", `${fmt.badge("valid", "green")} token #${tokenId}`);
262
306
  else
263
307
  warn("credential", tokenId === 0n ? fmt.badge("missing", "yellow") : fmt.badge("invalid", "yellow"));
308
+ credentialReady = valid;
264
309
  pass(valid);
265
310
  }
266
311
  catch (e) {
@@ -285,6 +330,7 @@ export async function demoCheck(opts) {
285
330
  const issuerMatches = policy.cnfIssuer.toLowerCase() === (cfg.issuer ?? "").toLowerCase();
286
331
  const ready = policy.enabled && issuerMatches;
287
332
  (ready ? ok : warn)("policy", `${policy.enabled ? fmt.badge("enabled", "green") : fmt.badge("disabled", "yellow")} issuer ${fmt.addr(policy.cnfIssuer)}`);
333
+ policyReady = ready;
288
334
  pass(ready);
289
335
  }
290
336
  catch (e) {
@@ -308,37 +354,61 @@ export async function demoCheck(opts) {
308
354
  const balanceText = `${formatUnits(balance, decimals)} ${symbol}`;
309
355
  const allowanceText = allowance > 0n ? fmt.badge("approved", "green") : fmt.badge("needs approval", "yellow");
310
356
  (balance > 0n ? ok : warn)(label, `${balanceText} ${allowanceText}`);
357
+ walletBalanceChecks.push(balance > 0n);
311
358
  pass(balance > 0n);
312
359
  }
313
360
  catch (e) {
314
361
  bad(label, e instanceof Error ? e.message.split("\n")[0] : String(e));
362
+ walletBalanceChecks.push(false);
315
363
  pass(false);
316
364
  }
317
365
  }
366
+ walletBalancesReady = walletBalanceChecks.length === 2 && walletBalanceChecks.every(Boolean);
318
367
  log.line();
319
368
  }
369
+ const infrastructureChecks = [networkReady, configReady, codeReady, economicsReady, issuerPathReady, policyReady];
370
+ const infrastructureReady = infrastructureChecks.every(Boolean);
371
+ const walletSelected = !!wallet && isAddress(wallet);
372
+ const walletReady = walletSelected && credentialReady && walletBalancesReady;
373
+ const realTxReady = infrastructureReady && walletReady;
320
374
  const readiness = total === 0 ? 0 : Math.round((score / total) * 100);
375
+ const infraScore = Math.round((infrastructureChecks.filter(Boolean).length / infrastructureChecks.length) * 100);
376
+ const walletScore = walletSelected
377
+ ? Math.round(([credentialReady, walletBalancesReady].filter(Boolean).length / 2) * 100)
378
+ : 0;
321
379
  log.section("Readiness");
322
- const tone = readiness >= 85 ? "green" : readiness >= 60 ? "yellow" : "red";
323
- log.progress("score", readiness, tone);
380
+ const tone = realTxReady ? "green" : infrastructureReady ? "yellow" : readiness >= 60 ? "yellow" : "red";
381
+ log.progress("overall", readiness, tone);
382
+ log.progress("infrastructure", infraScore, infrastructureReady ? "green" : "yellow");
383
+ log.progress("wallet", walletScore, walletReady ? "green" : "yellow");
324
384
  log.metrics([
325
- { label: "credential", value: wallet ? "ready" : "missing", tone: wallet ? "green" : "yellow" },
326
- { label: "policy", value: cfg.poolId ? "enabled" : "missing", tone: cfg.poolId ? "green" : "yellow" },
385
+ { label: "infra", value: infrastructureReady ? "ready" : "incomplete", tone: infrastructureReady ? "green" : "yellow" },
386
+ { label: "wallet", value: walletReady ? "ready" : "not ready", tone: walletReady ? "green" : "yellow" },
387
+ { label: "tx", value: realTxReady ? "ready" : "blocked", tone: realTxReady ? "green" : "yellow" },
388
+ ]);
389
+ log.metrics([
390
+ { label: "credential", value: credentialReady ? "valid" : "missing", tone: credentialReady ? "green" : "yellow" },
391
+ { label: "balances", value: walletBalancesReady ? "funded" : "missing", tone: walletBalancesReady ? "green" : "yellow" },
392
+ { label: "policy", value: policyReady ? "enabled" : "missing", tone: policyReady ? "green" : "yellow" },
327
393
  { label: "deal", value: cfg.fee === "8388608" ? "better" : "standard", tone: cfg.fee === "8388608" ? "green" : "gray" },
328
394
  ]);
329
- if (readiness >= 85) {
395
+ if (realTxReady) {
330
396
  log.callout("Live demo ready", "credential, policy, hook, router, pool, and balances are aligned", "green");
331
397
  }
398
+ else if (infrastructureReady) {
399
+ log.callout("Demo infrastructure ready", "wallet is not ready yet: mint CNF and fund demo tokens before real tx", "yellow");
400
+ }
332
401
  else {
333
402
  log.callout("Live demo not ready", "fill the missing config/state first", tone);
334
403
  }
335
404
  log.line();
336
- log.section("Live Path", readiness >= 85 ? "what the judge is about to see" : "target flow");
337
- flowStep("credential", wallet ? `${fmt.addr(wallet)} holds a valid CNF` : "wallet not selected", wallet ? "green" : "yellow");
405
+ log.section("Live Path", realTxReady ? "what the judge is about to see" : "target flow");
406
+ flowStep("credential", credentialReady && wallet ? `${fmt.addr(wallet)} holds a valid CNF` : wallet ? `${fmt.addr(wallet)} has no valid CNF` : "wallet not selected", credentialReady ? "green" : "yellow");
338
407
  flowStep("session", `local EIP-712 binds user + router + pool + action`, "green");
339
408
  flowStep("hook", `${cfg.hook ? fmt.addr(cfg.hook) : fmt.badge("missing", "red")} gates swap/liquidity`, cfg.hook ? "green" : "red");
340
- flowStep("pool", cfg.poolId ? `${fmt.hash(cfg.poolId)} policy enabled` : fmt.badge("missing", "red"), cfg.poolId ? "green" : "red");
341
- flowStep("result", readiness >= 85 ? fmt.badge("ready for real tx", "green") : fmt.badge("preflight incomplete", "yellow"), readiness >= 85 ? "green" : "yellow");
409
+ flowStep("pool", policyReady && cfg.poolId ? `${fmt.hash(cfg.poolId)} policy enabled` : fmt.badge("policy not ready", "yellow"), policyReady ? "green" : "yellow");
410
+ flowStep("balances", walletBalancesReady ? fmt.badge("funded", "green") : fmt.badge("missing demo tokens", "yellow"), walletBalancesReady ? "green" : "yellow");
411
+ flowStep("result", realTxReady ? fmt.badge("ready for real tx", "green") : fmt.badge("wallet not ready for real tx", "yellow"), realTxReady ? "green" : "yellow");
342
412
  log.line();
343
413
  log.section("Next Commands");
344
414
  if (!cfg.router || !cfg.tokenA || !cfg.tokenB || !cfg.poolId) {
@@ -395,3 +465,75 @@ export async function demoFaucet(opts) {
395
465
  log.callout("Demo tokens ready", "wallet can now pass token-balance preflight checks", "green");
396
466
  console.log();
397
467
  }
468
+ export async function demoAttest(opts) {
469
+ const cfg = loadConfig();
470
+ const chain = CHAINS[cfg.chain ?? "84532"] ?? baseSepolia;
471
+ const rawKey = opts.privateKey ?? process.env["PRIVATE_KEY"];
472
+ if (!rawKey || !isHex(rawKey) || rawKey.length !== 66) {
473
+ die("Private key required. Use --private-key or set PRIVATE_KEY env var.");
474
+ }
475
+ if (!cfg.issuer || !isAddress(cfg.issuer))
476
+ die("CNFIssuer required. Run `ilal init` first.");
477
+ if (!isAddress(opts.wallet))
478
+ die(`Invalid wallet address: ${opts.wallet}`);
479
+ const account = privateKeyToAccount(rawKey);
480
+ const client = createPublicClient({ chain, transport: http(cfg.rpc) });
481
+ const walletClient = createWalletClient({ account, chain, transport: http(cfg.rpc) });
482
+ const [eas, schemaUID, trustedAttester] = await Promise.all([
483
+ client.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "eas" }),
484
+ client.readContract({ address: cfg.issuer, abi: [
485
+ { name: "schemaUID", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "bytes32" }] },
486
+ ], functionName: "schemaUID" }),
487
+ client.readContract({ address: cfg.issuer, abi: [
488
+ { name: "trustedAttester", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "address" }] },
489
+ ], functionName: "trustedAttester" }),
490
+ ]);
491
+ if (eas === ZERO)
492
+ die("Configured issuer has no EAS/MockEAS path. Use a mock demo issuer or Coinbase EAS attestation.");
493
+ const days = BigInt(parseInt(opts.expiresInDays ?? "90", 10));
494
+ const expiration = BigInt(Math.floor(Date.now() / 1000)) + days * 24n * 60n * 60n;
495
+ header("ILAL Demo Attestation", chain.name);
496
+ log.kv("mockEAS", fmt.addr(eas));
497
+ log.kv("issuer", fmt.addr(cfg.issuer));
498
+ log.kv("recipient", fmt.addr(opts.wallet));
499
+ log.kv("attester", fmt.addr(trustedAttester));
500
+ log.line();
501
+ const spin = new Spinner("Creating MockEAS attestation…").start();
502
+ const hash = await walletClient.writeContract({
503
+ address: eas,
504
+ abi: MOCK_EAS_ABI,
505
+ functionName: "attest",
506
+ args: [
507
+ schemaUID,
508
+ opts.wallet,
509
+ trustedAttester,
510
+ expiration,
511
+ "0x",
512
+ ],
513
+ });
514
+ const receipt = await client.waitForTransactionReceipt({ hash });
515
+ spin.succeed(`Attestation created ${fmt.gray(fmt.hash(hash))}`);
516
+ let uid;
517
+ for (const logItem of receipt.logs) {
518
+ if (logItem.address.toLowerCase() !== eas.toLowerCase())
519
+ continue;
520
+ try {
521
+ const decoded = decodeEventLog({ abi: MOCK_EAS_ABI, data: logItem.data, topics: logItem.topics });
522
+ if (decoded.eventName === "AttestationCreated") {
523
+ uid = decoded.args.uid;
524
+ break;
525
+ }
526
+ }
527
+ catch { }
528
+ }
529
+ log.line();
530
+ if (uid)
531
+ log.kv("attestation", fmt.cyan(uid));
532
+ log.kv("tx", fmt.gray(hash));
533
+ log.callout("CNF mint path ready", "the recipient wallet can now run `ilal credential mint --attestation <uid>`", "green");
534
+ if (uid) {
535
+ console.log();
536
+ log.command(`PRIVATE_KEY=0x... ilal credential mint --issuer ${cfg.issuer} --attestation ${uid} --chain ${chain.id}`);
537
+ }
538
+ console.log();
539
+ }
@@ -1,11 +1,11 @@
1
1
  export declare function sessionSign(opts: {
2
2
  user?: string;
3
- pool: string;
3
+ pool?: string;
4
4
  action: string;
5
- hook: string;
6
- issuer: string;
5
+ hook?: string;
6
+ issuer?: string;
7
7
  caller?: string;
8
- chain: string;
8
+ chain?: string;
9
9
  ttl: number;
10
10
  privateKey?: string;
11
11
  }): Promise<void>;
@@ -2,6 +2,7 @@ import { createWalletClient, encodeAbiParameters, http, isAddress, isHex, parseA
2
2
  import { privateKeyToAccount } from "viem/accounts";
3
3
  import { base, baseSepolia } from "viem/chains";
4
4
  import { fmt, log, header, die } from "../ui.js";
5
+ import { withConfig } from "../config.js";
5
6
  const CHAINS = {
6
7
  "8453": base,
7
8
  "84532": baseSepolia,
@@ -27,6 +28,7 @@ const HOOK_DATA_ABI = parseAbiParameters([
27
28
  "bytes signature",
28
29
  ]);
29
30
  export async function sessionSign(opts) {
31
+ const cfg = withConfig({ chain: opts.chain, hook: opts.hook, issuer: opts.issuer });
30
32
  // Resolve private key
31
33
  const rawKey = opts.privateKey ?? process.env["PRIVATE_KEY"];
32
34
  if (!rawKey)
@@ -35,32 +37,36 @@ export async function sessionSign(opts) {
35
37
  die("Invalid private key format (expected 0x + 32 bytes).");
36
38
  const account = privateKeyToAccount(rawKey);
37
39
  const user = (opts.user ?? account.address);
38
- const authorizedCaller = (opts.caller ?? user);
40
+ const pool = opts.pool ?? cfg.poolId;
41
+ const hook = opts.hook ?? cfg.hook;
42
+ const issuer = opts.issuer ?? cfg.issuer;
43
+ const authorizedCaller = (opts.caller ?? cfg.router ?? user);
44
+ const chainId = opts.chain ?? cfg.chain ?? "84532";
39
45
  if (!isAddress(user))
40
46
  die(`Invalid user address: ${user}`);
41
47
  if (!isAddress(authorizedCaller))
42
48
  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
+ if (!hook || !isAddress(hook))
50
+ die(`Invalid hook address: ${hook ?? "<missing>"}. Use --hook or run ilal init.`);
51
+ if (!issuer || !isAddress(issuer))
52
+ die(`Invalid issuer address: ${issuer ?? "<missing>"}. Use --issuer or run ilal init.`);
53
+ if (!pool || !isHex(pool) || pool.length !== 66)
54
+ die("poolId must be a 32-byte hex string. Use --pool or run ilal init.");
49
55
  const actionKey = opts.action.toLowerCase().replace(/[^a-z]/g, "");
50
56
  const actionCode = ACTIONS[actionKey];
51
57
  if (actionCode === undefined)
52
58
  die(`Unknown action "${opts.action}". Use: swap | addLiquidity | removeLiquidity`);
53
- const chain = CHAINS[opts.chain] ?? baseSepolia;
59
+ const chain = CHAINS[chainId] ?? baseSepolia;
54
60
  const walletClient = createWalletClient({ account, chain, transport: http() });
55
61
  const deadline = BigInt(Math.floor(Date.now() / 1000) + opts.ttl);
56
62
  const nonce = `0x${Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("hex")}`;
57
63
  const token = {
58
64
  user,
59
65
  authorizedCaller,
60
- cnfIssuer: opts.issuer,
66
+ cnfIssuer: issuer,
61
67
  chainId: BigInt(chain.id),
62
- verifyingHook: opts.hook,
63
- poolId: opts.pool,
68
+ verifyingHook: hook,
69
+ poolId: pool,
64
70
  action: actionCode,
65
71
  deadline,
66
72
  nonce: nonce,
@@ -69,11 +75,11 @@ export async function sessionSign(opts) {
69
75
  log.section("Session");
70
76
  log.kv("user", fmt.addr(user));
71
77
  log.kv("caller", fmt.addr(authorizedCaller));
72
- log.kv("chain", chain.name);
73
- log.kv("pool", fmt.hash(opts.pool));
78
+ log.kv("chain", `${chain.name} (${chain.id})`);
79
+ log.kv("pool", fmt.hash(pool));
74
80
  log.kv("action", opts.action);
75
- log.kv("hook", fmt.addr(opts.hook));
76
- log.kv("issuer", fmt.addr(opts.issuer));
81
+ log.kv("hook", fmt.addr(hook));
82
+ log.kv("issuer", fmt.addr(issuer));
77
83
  log.kv("deadline", new Date(Number(deadline) * 1000).toISOString());
78
84
  log.line();
79
85
  log.step("Signing EIP-712 session token locally…");
@@ -84,7 +90,7 @@ export async function sessionSign(opts) {
84
90
  name: "ILAL ComplianceHook",
85
91
  version: "1",
86
92
  chainId: BigInt(chain.id),
87
- verifyingContract: opts.hook,
93
+ verifyingContract: hook,
88
94
  },
89
95
  types: { SessionToken: SESSION_TOKEN_TYPE },
90
96
  primaryType: "SessionToken",
@@ -101,5 +107,6 @@ export async function sessionSign(opts) {
101
107
  console.log(` ${fmt.cyan(hookData)}`);
102
108
  console.log();
103
109
  console.log(fmt.gray(" verifies: caller, deadline, chainId, hook, pool, action, sig, CNF"));
110
+ console.log(fmt.gray(" note: this hookData is a one-time authorization; nonce replay is blocked on-chain"));
104
111
  console.log();
105
112
  }
@@ -35,5 +35,6 @@ export declare function swap(opts: {
35
35
  rpc?: string;
36
36
  privateKey?: string;
37
37
  ttl?: string;
38
+ hookData?: string;
38
39
  simulate?: boolean;
39
40
  }): Promise<void>;
@@ -18,7 +18,7 @@
18
18
  * --pool-id 0xPOOLID \
19
19
  * --chain 84532
20
20
  */
21
- import { createPublicClient, createWalletClient, encodeAbiParameters, http, isAddress, isHex, parseAbiParameters, parseUnits, } from "viem";
21
+ import { createPublicClient, createWalletClient, decodeAbiParameters, encodeAbiParameters, http, isAddress, isHex, parseAbiParameters, parseUnits, } from "viem";
22
22
  import { privateKeyToAccount } from "viem/accounts";
23
23
  import { base, baseSepolia } from "viem/chains";
24
24
  import { fmt, log, header, Spinner, die, dieOnContract } from "../ui.js";
@@ -214,42 +214,78 @@ export async function swap(opts) {
214
214
  { label: "ILAL fee", value: protocolFeePips > 0 ? pipsToPercent(protocolFeePips) : "off", note: protocolFeePips > 0 ? "protocol revenue" : "legacy router", tone: protocolFeePips > 0 ? "cyan" : "gray" },
215
215
  ]);
216
216
  log.line();
217
- // Sign session token
218
- const signSpin = new Spinner("Signing session token…").start();
219
217
  const ttl = parseInt(opts.ttl ?? "600");
220
218
  const deadline = BigInt(Math.floor(Date.now() / 1000) + ttl);
221
219
  const nonce = `0x${Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("hex")}`;
222
- const token = {
223
- user: account.address,
224
- authorizedCaller: cfg.router,
225
- cnfIssuer: cfg.issuer,
226
- chainId: BigInt(chain.id),
227
- verifyingHook: cfg.hook,
228
- poolId: cfg.poolId,
229
- action: 1, // ACTION_SWAP
230
- deadline,
231
- nonce,
232
- };
233
- const signature = await walClient.signTypedData({
234
- account,
235
- domain: {
236
- name: "ILAL ComplianceHook",
237
- version: "1",
220
+ let hookData;
221
+ let sessionNonce = nonce;
222
+ if (opts.hookData) {
223
+ if (!isHex(opts.hookData))
224
+ die("--hook-data must be 0x-prefixed ABI-encoded hookData.");
225
+ try {
226
+ const [externalToken] = decodeAbiParameters(HOOK_DATA_ABI, opts.hookData);
227
+ const issues = [];
228
+ if (externalToken.user.toLowerCase() !== account.address.toLowerCase())
229
+ issues.push("user does not match signer wallet");
230
+ if (externalToken.authorizedCaller.toLowerCase() !== cfg.router.toLowerCase())
231
+ issues.push("authorizedCaller does not match router");
232
+ if (externalToken.cnfIssuer.toLowerCase() !== cfg.issuer.toLowerCase())
233
+ issues.push("cnfIssuer does not match config");
234
+ if (externalToken.chainId !== BigInt(chain.id))
235
+ issues.push(`chainId mismatch: hookData=${externalToken.chainId.toString()} config=${chain.id}`);
236
+ if (externalToken.verifyingHook.toLowerCase() !== cfg.hook.toLowerCase())
237
+ issues.push("verifyingHook does not match config");
238
+ if (externalToken.poolId.toLowerCase() !== cfg.poolId.toLowerCase())
239
+ issues.push("poolId does not match config");
240
+ if (externalToken.action !== 1)
241
+ issues.push("action is not swap");
242
+ if (externalToken.deadline < BigInt(Math.floor(Date.now() / 1000)))
243
+ issues.push("session deadline has expired");
244
+ if (issues.length > 0)
245
+ die(`Invalid --hook-data for this swap: ${issues.join("; ")}`);
246
+ sessionNonce = externalToken.nonce;
247
+ hookData = opts.hookData;
248
+ log.ok("Using externally supplied one-time session authorization");
249
+ }
250
+ catch (e) {
251
+ die(`Could not decode --hook-data: ${e instanceof Error ? e.message : String(e)}`);
252
+ }
253
+ }
254
+ else {
255
+ // Sign session token
256
+ const signSpin = new Spinner("Signing one-time session authorization…").start();
257
+ const token = {
258
+ user: account.address,
259
+ authorizedCaller: cfg.router,
260
+ cnfIssuer: cfg.issuer,
238
261
  chainId: BigInt(chain.id),
239
- verifyingContract: cfg.hook,
240
- },
241
- types: { SessionToken: SESSION_TOKEN_TYPE },
242
- primaryType: "SessionToken",
243
- message: token,
244
- });
245
- const hookData = encodeAbiParameters(HOOK_DATA_ABI, [token, signature]);
246
- signSpin.succeed(`Session signed (expires in ${ttl}s)`);
262
+ verifyingHook: cfg.hook,
263
+ poolId: cfg.poolId,
264
+ action: 1, // ACTION_SWAP
265
+ deadline,
266
+ nonce,
267
+ };
268
+ const signature = await walClient.signTypedData({
269
+ account,
270
+ domain: {
271
+ name: "ILAL ComplianceHook",
272
+ version: "1",
273
+ chainId: BigInt(chain.id),
274
+ verifyingContract: cfg.hook,
275
+ },
276
+ types: { SessionToken: SESSION_TOKEN_TYPE },
277
+ primaryType: "SessionToken",
278
+ message: token,
279
+ });
280
+ hookData = encodeAbiParameters(HOOK_DATA_ABI, [token, signature]);
281
+ signSpin.succeed(`Session authorization signed (expires in ${ttl}s, one-time nonce)`);
282
+ }
247
283
  const fee = parseInt(cfg.fee ?? "3000");
248
284
  const tickSpacing = parseInt(cfg.tickSpacing ?? "60");
249
285
  log.section("Gate Checks");
250
286
  log.kv("credential", `${fmt.badge("required", "cyan")} issuer ${fmt.addr(cfg.issuer)}`);
251
287
  log.kv("caller", `${fmt.badge("bound", "green")} ${fmt.addr(cfg.router)}`);
252
- log.kv("nonce", `${fmt.badge("fresh", "green")} ${fmt.hash(nonce)}`);
288
+ log.kv("nonce", `${opts.hookData ? fmt.badge("external", "cyan") : fmt.badge("fresh", "green")} ${fmt.hash(sessionNonce)}`);
253
289
  log.kv("fee", feeLabel(fee));
254
290
  if (protocolFeePips > 0) {
255
291
  log.kv("protocol fee", `${fmt.badge("ILAL", "cyan")} ${pipsToPercent(protocolFeePips)} to ${treasury ? fmt.addr(treasury) : "treasury"}`);
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { proofMint, proofRenew } from "./commands/proof.js";
8
8
  import { sessionSign } from "./commands/session.js";
9
9
  import { poolPolicySet, poolPolicyGet } from "./commands/pool.js";
10
10
  import { deploy } from "./commands/deploy.js";
11
- import { demo, demoCheck, demoFaucet } from "./commands/demo.js";
11
+ import { demo, demoCheck, demoFaucet, demoAttest } from "./commands/demo.js";
12
12
  import { init } from "./commands/init.js";
13
13
  import { status } from "./commands/status.js";
14
14
  import { swap } from "./commands/swap.js";
@@ -19,7 +19,7 @@ const program = new Command();
19
19
  program
20
20
  .name("ilal")
21
21
  .description("ILAL Protocol CLI — Uniswap v4 compliance hook toolkit")
22
- .version("0.2.2")
22
+ .version("0.2.4")
23
23
  .addHelpText("before", `\n ${fmt.bold(fmt.cyan("◆"))} ${fmt.bold("ILAL Protocol")} ${fmt.gray("Uniswap v4 Compliance Hook")}\n`);
24
24
  // ─── init ─────────────────────────────────────────────────────────────────────
25
25
  program
@@ -81,6 +81,15 @@ demoCommand
81
81
  .action(async (opts) => {
82
82
  await demoFaucet(opts).catch(err);
83
83
  });
84
+ demoCommand
85
+ .command("attest")
86
+ .description("Create a MockEAS test attestation for a wallet (demo issuer owner only)")
87
+ .requiredOption("-w, --wallet <address>", "Recipient wallet that will mint the CNF")
88
+ .option("--expires-in-days <days>", "Attestation lifetime in days", "90")
89
+ .option("-k, --private-key <hex>", "MockEAS owner private key")
90
+ .action(async (opts) => {
91
+ await demoAttest(opts).catch(err);
92
+ });
84
93
  const err = (e) => {
85
94
  console.error(fmt.red(`\nError: ${e instanceof Error ? e.message : String(e)}\n`));
86
95
  process.exit(1);
@@ -165,13 +174,13 @@ const session = program.command("session").description("Session token operations
165
174
  session
166
175
  .command("sign")
167
176
  .description("Sign an EIP-712 session token locally — no ILAL API call")
168
- .requiredOption("-p, --pool <bytes32>", "Pool ID (bytes32 hex)")
177
+ .option("-p, --pool <bytes32>", "Pool ID (bytes32 hex, defaults to .ilal.json poolId)")
169
178
  .requiredOption("-a, --action <action>", "Action: swap | addLiquidity | removeLiquidity")
170
- .requiredOption("-H, --hook <address>", "ComplianceHook contract address")
171
- .requiredOption("-i, --issuer <address>", "CNFIssuer contract address")
179
+ .option("-H, --hook <address>", "ComplianceHook contract address (defaults to .ilal.json hook)")
180
+ .option("-i, --issuer <address>", "CNFIssuer contract address (defaults to .ilal.json issuer)")
172
181
  .option("-u, --user <address>", "Trader address (defaults to key's address)")
173
- .option("--caller <address>", "Authorized v4 caller (defaults to user; use ILALRouter address for router flows)")
174
- .option("-c, --chain <chainId>", "Chain ID", "8453")
182
+ .option("--caller <address>", "Authorized v4 caller (defaults to .ilal.json router, then user)")
183
+ .option("-c, --chain <chainId>", "Chain ID (defaults to .ilal.json chain, then 84532)")
175
184
  .option("-t, --ttl <seconds>", "Session lifetime in seconds", "600")
176
185
  .option("-k, --private-key <hex>", "Private key (or set PRIVATE_KEY env var)")
177
186
  .action(async (opts) => {
@@ -266,6 +275,7 @@ program
266
275
  .option("-r, --rpc <url>", "Custom RPC URL")
267
276
  .option("-k, --private-key <hex>", "Private key (or set PRIVATE_KEY env var)")
268
277
  .option("--ttl <seconds>", "Session token lifetime in seconds", "600")
278
+ .option("--hook-data <hex>", "Use externally signed one-time hookData instead of signing inside swap")
269
279
  .option("--simulate", "Sign session without sending tx", false)
270
280
  .action(async (opts) => {
271
281
  await swap(opts).catch(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilalv3/cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "ILAL Protocol CLI — compliant swaps and credential management for Uniswap v4",
5
5
  "type": "module",
6
6
  "bin": {