@ilalv3/cli 0.2.1 → 0.2.3

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
@@ -18,19 +18,42 @@ npx @ilalv3/cli <command>
18
18
 
19
19
  ## Quick start (Base Sepolia demo)
20
20
 
21
+ `ilal init` points at the current seeded Base Sepolia demo stack. The demo issuer
22
+ uses MockEAS, so reviewers can verify the full path without waiting for a real
23
+ Coinbase attestation. The seeded reviewer wallet already has CNF + TOKA/TOKB;
24
+ for your own wallet, deploy a mock stack with `ilal deploy --mock`.
25
+
21
26
  ```bash
22
27
  # 1. Point CLI at the live demo deployment
23
28
  ilal init
24
29
 
25
- # 2. Check credential + pool status
26
- ilal status
30
+ # 2. Check credential + pool status for the seeded reviewer wallet
31
+ ilal status --wallet 0xc0807D4778a9E5FE15ad68A8500e64d65BA78D58
27
32
 
28
- # 3. Mint a CNF via ZK proof
29
- # Requires the issuer root to already include your wallet.
30
- PRIVATE_KEY=0x... ilal credential prove --wallet 0xYourWallet
33
+ # 3. Full readiness verdict
34
+ ilal demo check --wallet 0xc0807D4778a9E5FE15ad68A8500e64d65BA78D58
35
+
36
+ # 4. Execute a compliant swap with the seeded reviewer key
37
+ PRIVATE_KEY=0x... ilal swap --amount-in 1 --token-in 0x582362E608F36850F6f641510d5D19C1EaB4cb27 --min-amount-out 0
38
+ ```
39
+
40
+ For a fully seeded local/testnet demo, deploy mock EAS + demo pool pieces:
31
41
 
32
- # 4. Execute a compliant swap
33
- PRIVATE_KEY=0x... ilal swap --amount-in 0.001 --token-in 0x2E0dEd1CF4ec6106079df4eF1200959c2a454f3A --min-amount-out 0
42
+ ```bash
43
+ PRIVATE_KEY=0x... ilal deploy \
44
+ --chain 84532 \
45
+ --mock \
46
+ --wallet-to-seed 0xYourWallet \
47
+ --broadcast
48
+
49
+ # Then mint the seeded CNF from the printed AttestationUID:
50
+ PRIVATE_KEY=0x... ilal credential mint \
51
+ --issuer <CNFIssuer> \
52
+ --attestation <AttestationUID> \
53
+ --chain 84532
54
+
55
+ # If the wallet needs more demo tokens:
56
+ PRIVATE_KEY=0x... ilal demo faucet --wallet 0xYourWallet
34
57
  ```
35
58
 
36
59
  ## Getting a CNF credential
@@ -75,6 +98,8 @@ Generates a Groth16 proof locally (~5s), verifies it on-chain, and mints/renews
75
98
  | `ilal oracle activate-verifier` | Operator flow: activate the pending ZK verifier after timelock |
76
99
  | `ilal session sign` | Sign a standalone SessionToken |
77
100
  | `ilal proof mint` | Mint CNF from existing proof.json + public.json |
101
+ | `ilal deploy --mock` | Deploy a seeded testnet demo stack with MockEAS, tokens, router, hook, and policy |
102
+ | `ilal demo faucet` | Mint mock demo TOKA/TOKB to a wallet |
78
103
  | `ilal deploy` | Deploy full ILAL contract stack |
79
104
 
80
105
  ## Configuration
@@ -83,10 +108,10 @@ The CLI reads `.ilal.json` in the current directory. Run `ilal init` to create i
83
108
 
84
109
  ```bash
85
110
  ilal swap \
86
- --router 0xd0aF4D1EFF36CB2a1E88017eA398dCaDe1Ac0040 \
87
- --hook 0x6C57b50Ef9286b132066012B19b291FB120ACa80 \
88
- --issuer 0xB13AE2498Df62A85768a4b783109C05fCf5A264a \
89
- --pool-id 0x16b3e7a5c52216925f705673b3ab25db5e6025da530cf53b3bcb5affeb18d95f \
111
+ --router 0x7727F0f3EBe99A558487394D001950ee6B33BB86 \
112
+ --hook 0xF5066ad9c25F3f54cfb19609A60187C48C184A80 \
113
+ --issuer 0xc4E032A7574016bd0e3d1a5BbFdE886af09CeD9A \
114
+ --pool-id 0xc1c8f29d6f03b5cd18bf2b862d48f45cc338022a154945b89c4bcb0a3e11e87f \
90
115
  --amount-in 0.001
91
116
  ```
92
117
 
@@ -94,10 +119,19 @@ ilal swap \
94
119
 
95
120
  | Contract | Address |
96
121
  |---|---|
97
- | CNFIssuer | `0xB13AE2498Df62A85768a4b783109C05fCf5A264a` |
98
- | ComplianceHook | `0x6C57b50Ef9286b132066012B19b291FB120ACa80` |
99
- | ILALRouter | `0xd0aF4D1EFF36CB2a1E88017eA398dCaDe1Ac0040` |
100
- | PolicyRegistry | `0x19fD4eCF4359fCc8d5E79916691a28c24A22a9B4` |
122
+ | CNFIssuer | `0xc4E032A7574016bd0e3d1a5BbFdE886af09CeD9A` |
123
+ | ComplianceHook | `0xF5066ad9c25F3f54cfb19609A60187C48C184A80` |
124
+ | ILALRouter | `0x7727F0f3EBe99A558487394D001950ee6B33BB86` |
125
+ | PolicyRegistry | `0x910a3efDc426f3216738106dd0DC6EA696477233` |
126
+ | TokenA / TOKA | `0x582362E608F36850F6f641510d5D19C1EaB4cb27` |
127
+ | TokenB / TOKB | `0x6eBBdAC70EC422C512727B25c7F0D9120ed101Ff` |
128
+ | Pool ID | `0xc1c8f29d6f03b5cd18bf2b862d48f45cc338022a154945b89c4bcb0a3e11e87f` |
129
+
130
+ Live proof:
131
+
132
+ - CNF mint tx: `0x676ca67698eb8fed6c905c2b3a9536d4d056e89c199c41c44085a29db8b4d462`
133
+ - Add liquidity tx: `0x531fac3678878e4855471318b8ea39b2b2f3ced3d890d9d7c40721af296084ca`
134
+ - Swap tx: `0xdaf4136d305e546d6936715cc0101efb4dc88abcb779add9ee03591fdf555a5a`
101
135
 
102
136
  ## License
103
137
 
@@ -5,3 +5,8 @@ export declare function demoCheck(opts: {
5
5
  wallet?: string;
6
6
  privateKey?: string;
7
7
  }): Promise<void>;
8
+ export declare function demoFaucet(opts: {
9
+ wallet?: string;
10
+ amount?: string;
11
+ privateKey?: string;
12
+ }): Promise<void>;
@@ -1,19 +1,19 @@
1
- import { createPublicClient, formatUnits, http, isAddress, isHex, } from "viem";
1
+ import { createPublicClient, createWalletClient, 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";
5
- import { fmt, header, log } from "../ui.js";
5
+ import { die, fmt, header, log, Spinner } from "../ui.js";
6
6
  const CHAINS = { "8453": base, "84532": baseSepolia };
7
7
  const POOL_MANAGER = {
8
8
  "84532": "0x05E73354cFDd6745C338b50BcFDfA3Aa6fA03408",
9
9
  "8453": "0x498581fF718922c3f8e6A244956aF099B2652b2b",
10
10
  };
11
11
  const SAMPLE = {
12
- wallet: "0xF40493ACDd33cC4a841fCD69577A66218381C2fC",
13
- issuer: "0xB13AE2498Df62A85768a4b783109C05fCf5A264a",
14
- hook: "0x6C57b50Ef9286b132066012B19b291FB120ACa80",
15
- router: "0xd0aF4D1EFF36CB2a1E88017eA398dCaDe1Ac0040",
16
- pool: "0x16b3e7a5c52216925f705673b3ab25db5e6025da530cf53b3bcb5affeb18d95f",
12
+ wallet: "0xc0807D4778a9E5FE15ad68A8500e64d65BA78D58",
13
+ issuer: "0xc4E032A7574016bd0e3d1a5BbFdE886af09CeD9A",
14
+ hook: "0xF5066ad9c25F3f54cfb19609A60187C48C184A80",
15
+ router: "0x7727F0f3EBe99A558487394D001950ee6B33BB86",
16
+ pool: "0xc1c8f29d6f03b5cd18bf2b862d48f45cc338022a154945b89c4bcb0a3e11e87f",
17
17
  proof: "0x91f2b8a0c43e902f7f1a8c0d",
18
18
  session: "0x6b84eac5e0db21f8d5d43b7a",
19
19
  };
@@ -23,6 +23,7 @@ const CNF_ABI = [
23
23
  { name: "credentialOf", type: "function", stateMutability: "view", inputs: [{ name: "wallet", type: "address" }], outputs: [{ type: "uint256" }] },
24
24
  { name: "merkleRoot", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "uint256" }] },
25
25
  { name: "zkVerifier", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "address" }] },
26
+ { name: "eas", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "address" }] },
26
27
  ];
27
28
  const REGISTRY_ABI = [
28
29
  { name: "getPolicy", type: "function", stateMutability: "view", inputs: [{ name: "poolId", type: "bytes32" }], outputs: [{ type: "tuple", components: [{ name: "cnfIssuer", type: "address" }, { name: "requiredCredentialType", type: "bytes32" }, { name: "enabled", type: "bool" }] }] },
@@ -36,6 +37,7 @@ const ERC20_ABI = [
36
37
  { name: "decimals", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "uint8" }] },
37
38
  { name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "owner", type: "address" }], outputs: [{ type: "uint256" }] },
38
39
  { name: "allowance", type: "function", stateMutability: "view", inputs: [{ name: "owner", type: "address" }, { name: "spender", type: "address" }], outputs: [{ type: "uint256" }] },
40
+ { name: "mint", type: "function", stateMutability: "nonpayable", inputs: [{ name: "to", type: "address" }, { name: "amount", type: "uint256" }], outputs: [] },
39
41
  ];
40
42
  function stage(n, title, subtitle) {
41
43
  console.log();
@@ -146,6 +148,15 @@ export async function demoCheck(opts) {
146
148
  log.line();
147
149
  let score = 0;
148
150
  let total = 0;
151
+ let networkReady = false;
152
+ let configReady = true;
153
+ let codeReady = true;
154
+ let economicsReady = false;
155
+ let issuerPathReady = false;
156
+ let credentialReady = false;
157
+ let policyReady = false;
158
+ let walletBalancesReady = false;
159
+ const walletBalanceChecks = [];
149
160
  const pass = (condition) => {
150
161
  total++;
151
162
  if (condition)
@@ -156,6 +167,7 @@ export async function demoCheck(opts) {
156
167
  try {
157
168
  const block = await client.getBlockNumber();
158
169
  ok("latest block", block.toString());
170
+ networkReady = true;
159
171
  pass(true);
160
172
  }
161
173
  catch (e) {
@@ -188,18 +200,25 @@ export async function demoCheck(opts) {
188
200
  : value;
189
201
  ok(label, display);
190
202
  }
191
- else
203
+ else {
192
204
  bad(label, fmt.badge("missing", "red"));
205
+ configReady = false;
206
+ }
193
207
  pass(valid);
194
208
  }
195
209
  log.line();
196
210
  log.section("Contract Code", "must exist on-chain");
197
- pass(await hasCode(client, "CNFIssuer", cfg.issuer));
198
- pass(await hasCode(client, "ComplianceHook", cfg.hook));
199
- pass(await hasCode(client, "PolicyRegistry", cfg.registry));
200
- pass(await hasCode(client, "ILALRouter", cfg.router));
201
- pass(await hasCode(client, "currency0", cfg.tokenA));
202
- pass(await hasCode(client, "currency1", cfg.tokenB));
211
+ const codeChecks = [
212
+ await hasCode(client, "CNFIssuer", cfg.issuer),
213
+ await hasCode(client, "ComplianceHook", cfg.hook),
214
+ await hasCode(client, "PolicyRegistry", cfg.registry),
215
+ await hasCode(client, "ILALRouter", cfg.router),
216
+ await hasCode(client, "currency0", cfg.tokenA),
217
+ await hasCode(client, "currency1", cfg.tokenB),
218
+ ];
219
+ for (const check of codeChecks)
220
+ pass(check);
221
+ codeReady = codeChecks.every(Boolean);
203
222
  log.line();
204
223
  if (cfg.router && isAddress(cfg.router)) {
205
224
  log.section("Verified Flow Economics");
@@ -211,10 +230,12 @@ export async function demoCheck(opts) {
211
230
  ok("LP fee", cfg.fee === "8388608" ? `${fmt.badge("dynamic", "green")} verified 0.05%` : "pool fee tier");
212
231
  ok("ILAL fee", protocolFeePips > 0 ? `${fmt.badge("protocol", "cyan")} ${pipsToPercent(protocolFeePips)}` : fmt.badge("off", "yellow"));
213
232
  ok("treasury", fmt.addr(treasury));
233
+ economicsReady = true;
214
234
  pass(true);
215
235
  }
216
236
  catch {
217
237
  warn("protocol fee", fmt.badge("legacy router", "yellow"));
238
+ economicsReady = true;
218
239
  pass(true);
219
240
  }
220
241
  log.line();
@@ -222,10 +243,20 @@ export async function demoCheck(opts) {
222
243
  if (cfg.issuer && isAddress(cfg.issuer)) {
223
244
  log.section("Issuer State");
224
245
  try {
225
- const [root, verifier] = await Promise.all([
246
+ const [root, verifier, eas] = await Promise.all([
226
247
  client.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "merkleRoot" }),
227
248
  client.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "zkVerifier" }),
249
+ client.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "eas" }),
228
250
  ]);
251
+ const hasEASPath = eas !== ZERO;
252
+ const hasZKPath = root !== 0n && verifier !== ZERO;
253
+ if (hasEASPath)
254
+ ok("issuance path", `${fmt.badge("EAS/mock", "green")} ${fmt.addr(eas)}`);
255
+ else if (hasZKPath)
256
+ ok("issuance path", fmt.badge("ZK", "green"));
257
+ else
258
+ warn("issuance path", fmt.badge("not ready", "yellow"));
259
+ issuerPathReady = hasEASPath || hasZKPath;
229
260
  if (root === 0n)
230
261
  warn("merkleRoot", fmt.badge("not set", "yellow"));
231
262
  else
@@ -234,9 +265,11 @@ export async function demoCheck(opts) {
234
265
  warn("zkVerifier", fmt.badge("not set", "yellow"));
235
266
  else
236
267
  ok("zkVerifier", fmt.addr(verifier));
268
+ pass(issuerPathReady);
237
269
  }
238
270
  catch (e) {
239
271
  bad("issuer reads", e instanceof Error ? e.message.split("\n")[0] : String(e));
272
+ pass(false);
240
273
  }
241
274
  if (wallet && isAddress(wallet)) {
242
275
  try {
@@ -248,6 +281,7 @@ export async function demoCheck(opts) {
248
281
  ok("credential", `${fmt.badge("valid", "green")} token #${tokenId}`);
249
282
  else
250
283
  warn("credential", tokenId === 0n ? fmt.badge("missing", "yellow") : fmt.badge("invalid", "yellow"));
284
+ credentialReady = valid;
251
285
  pass(valid);
252
286
  }
253
287
  catch (e) {
@@ -272,6 +306,7 @@ export async function demoCheck(opts) {
272
306
  const issuerMatches = policy.cnfIssuer.toLowerCase() === (cfg.issuer ?? "").toLowerCase();
273
307
  const ready = policy.enabled && issuerMatches;
274
308
  (ready ? ok : warn)("policy", `${policy.enabled ? fmt.badge("enabled", "green") : fmt.badge("disabled", "yellow")} issuer ${fmt.addr(policy.cnfIssuer)}`);
309
+ policyReady = ready;
275
310
  pass(ready);
276
311
  }
277
312
  catch (e) {
@@ -295,37 +330,61 @@ export async function demoCheck(opts) {
295
330
  const balanceText = `${formatUnits(balance, decimals)} ${symbol}`;
296
331
  const allowanceText = allowance > 0n ? fmt.badge("approved", "green") : fmt.badge("needs approval", "yellow");
297
332
  (balance > 0n ? ok : warn)(label, `${balanceText} ${allowanceText}`);
333
+ walletBalanceChecks.push(balance > 0n);
298
334
  pass(balance > 0n);
299
335
  }
300
336
  catch (e) {
301
337
  bad(label, e instanceof Error ? e.message.split("\n")[0] : String(e));
338
+ walletBalanceChecks.push(false);
302
339
  pass(false);
303
340
  }
304
341
  }
342
+ walletBalancesReady = walletBalanceChecks.length === 2 && walletBalanceChecks.every(Boolean);
305
343
  log.line();
306
344
  }
345
+ const infrastructureChecks = [networkReady, configReady, codeReady, economicsReady, issuerPathReady, policyReady];
346
+ const infrastructureReady = infrastructureChecks.every(Boolean);
347
+ const walletSelected = !!wallet && isAddress(wallet);
348
+ const walletReady = walletSelected && credentialReady && walletBalancesReady;
349
+ const realTxReady = infrastructureReady && walletReady;
307
350
  const readiness = total === 0 ? 0 : Math.round((score / total) * 100);
351
+ const infraScore = Math.round((infrastructureChecks.filter(Boolean).length / infrastructureChecks.length) * 100);
352
+ const walletScore = walletSelected
353
+ ? Math.round(([credentialReady, walletBalancesReady].filter(Boolean).length / 2) * 100)
354
+ : 0;
308
355
  log.section("Readiness");
309
- const tone = readiness >= 85 ? "green" : readiness >= 60 ? "yellow" : "red";
310
- log.progress("score", readiness, tone);
356
+ const tone = realTxReady ? "green" : infrastructureReady ? "yellow" : readiness >= 60 ? "yellow" : "red";
357
+ log.progress("overall", readiness, tone);
358
+ log.progress("infrastructure", infraScore, infrastructureReady ? "green" : "yellow");
359
+ log.progress("wallet", walletScore, walletReady ? "green" : "yellow");
311
360
  log.metrics([
312
- { label: "credential", value: wallet ? "ready" : "missing", tone: wallet ? "green" : "yellow" },
313
- { label: "policy", value: cfg.poolId ? "enabled" : "missing", tone: cfg.poolId ? "green" : "yellow" },
361
+ { label: "infra", value: infrastructureReady ? "ready" : "incomplete", tone: infrastructureReady ? "green" : "yellow" },
362
+ { label: "wallet", value: walletReady ? "ready" : "not ready", tone: walletReady ? "green" : "yellow" },
363
+ { label: "tx", value: realTxReady ? "ready" : "blocked", tone: realTxReady ? "green" : "yellow" },
364
+ ]);
365
+ log.metrics([
366
+ { label: "credential", value: credentialReady ? "valid" : "missing", tone: credentialReady ? "green" : "yellow" },
367
+ { label: "balances", value: walletBalancesReady ? "funded" : "missing", tone: walletBalancesReady ? "green" : "yellow" },
368
+ { label: "policy", value: policyReady ? "enabled" : "missing", tone: policyReady ? "green" : "yellow" },
314
369
  { label: "deal", value: cfg.fee === "8388608" ? "better" : "standard", tone: cfg.fee === "8388608" ? "green" : "gray" },
315
370
  ]);
316
- if (readiness >= 85) {
371
+ if (realTxReady) {
317
372
  log.callout("Live demo ready", "credential, policy, hook, router, pool, and balances are aligned", "green");
318
373
  }
374
+ else if (infrastructureReady) {
375
+ log.callout("Demo infrastructure ready", "wallet is not ready yet: mint CNF and fund demo tokens before real tx", "yellow");
376
+ }
319
377
  else {
320
378
  log.callout("Live demo not ready", "fill the missing config/state first", tone);
321
379
  }
322
380
  log.line();
323
- log.section("Live Path", readiness >= 85 ? "what the judge is about to see" : "target flow");
324
- flowStep("credential", wallet ? `${fmt.addr(wallet)} holds a valid CNF` : "wallet not selected", wallet ? "green" : "yellow");
381
+ log.section("Live Path", realTxReady ? "what the judge is about to see" : "target flow");
382
+ 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");
325
383
  flowStep("session", `local EIP-712 binds user + router + pool + action`, "green");
326
384
  flowStep("hook", `${cfg.hook ? fmt.addr(cfg.hook) : fmt.badge("missing", "red")} gates swap/liquidity`, cfg.hook ? "green" : "red");
327
- flowStep("pool", cfg.poolId ? `${fmt.hash(cfg.poolId)} policy enabled` : fmt.badge("missing", "red"), cfg.poolId ? "green" : "red");
328
- flowStep("result", readiness >= 85 ? fmt.badge("ready for real tx", "green") : fmt.badge("preflight incomplete", "yellow"), readiness >= 85 ? "green" : "yellow");
385
+ flowStep("pool", policyReady && cfg.poolId ? `${fmt.hash(cfg.poolId)} policy enabled` : fmt.badge("policy not ready", "yellow"), policyReady ? "green" : "yellow");
386
+ flowStep("balances", walletBalancesReady ? fmt.badge("funded", "green") : fmt.badge("missing demo tokens", "yellow"), walletBalancesReady ? "green" : "yellow");
387
+ flowStep("result", realTxReady ? fmt.badge("ready for real tx", "green") : fmt.badge("wallet not ready for real tx", "yellow"), realTxReady ? "green" : "yellow");
329
388
  log.line();
330
389
  log.section("Next Commands");
331
390
  if (!cfg.router || !cfg.tokenA || !cfg.tokenB || !cfg.poolId) {
@@ -344,3 +403,41 @@ export async function demoCheck(opts) {
344
403
  log.command(`ilal swap --amount-in 0.001 --token-in ${suggestedTokenIn}`);
345
404
  console.log();
346
405
  }
406
+ export async function demoFaucet(opts) {
407
+ const cfg = loadConfig();
408
+ const chain = CHAINS[cfg.chain ?? "84532"] ?? baseSepolia;
409
+ const rawKey = opts.privateKey ?? process.env["PRIVATE_KEY"];
410
+ if (!rawKey || !isHex(rawKey) || rawKey.length !== 66) {
411
+ die("Private key required. Use --private-key or set PRIVATE_KEY env var.");
412
+ }
413
+ if (!cfg.tokenA || !cfg.tokenB || !isAddress(cfg.tokenA) || !isAddress(cfg.tokenB)) {
414
+ die("tokenA/tokenB required. Run `ilal init` with demo token addresses first.");
415
+ }
416
+ const account = privateKeyToAccount(rawKey);
417
+ const wallet = opts.wallet ?? account.address;
418
+ if (!isAddress(wallet))
419
+ die(`Invalid wallet address: ${wallet}`);
420
+ const client = createPublicClient({ chain, transport: http(cfg.rpc) });
421
+ const walletClient = createWalletClient({ account, chain, transport: http(cfg.rpc) });
422
+ header("ILAL Demo Faucet", chain.name);
423
+ log.kv("recipient", fmt.addr(wallet));
424
+ log.line();
425
+ for (const token of [cfg.tokenA, cfg.tokenB]) {
426
+ const [symbol, decimals] = await Promise.all([
427
+ client.readContract({ address: token, abi: ERC20_ABI, functionName: "symbol" }),
428
+ client.readContract({ address: token, abi: ERC20_ABI, functionName: "decimals" }),
429
+ ]);
430
+ const amount = parseUnits(opts.amount ?? "10000", decimals);
431
+ const spin = new Spinner(`Minting ${opts.amount ?? "10000"} ${symbol}…`).start();
432
+ const hash = await walletClient.writeContract({
433
+ address: token,
434
+ abi: ERC20_ABI,
435
+ functionName: "mint",
436
+ args: [wallet, amount],
437
+ });
438
+ await client.waitForTransactionReceipt({ hash });
439
+ spin.succeed(`Minted ${symbol} ${fmt.hash(hash)}`);
440
+ }
441
+ log.callout("Demo tokens ready", "wallet can now pass token-balance preflight checks", "green");
442
+ console.log();
443
+ }
@@ -58,13 +58,15 @@ export async function deploy(opts) {
58
58
  };
59
59
  if (isMock) {
60
60
  env["WALLET_TO_SEED"] = opts.walletToSeed;
61
+ env["WALLET"] = opts.walletToSeed;
62
+ env["MOCK_EAS"] = "true";
61
63
  }
62
64
  else {
63
65
  env["EAS_ADDRESS"] = easAddress;
64
66
  env["SCHEMA_UID"] = COINBASE_SCHEMA_UID;
65
67
  env["TRUSTED_ATTESTER"] = COINBASE_ATTESTER;
66
68
  }
67
- const script = isMock ? "script/DeployMock.s.sol" : "script/Deploy.s.sol";
69
+ const script = isMock ? "script/DeployDemo.s.sol" : "script/Deploy.s.sol";
68
70
  const flags = [
69
71
  `--rpc-url ${rpc}`,
70
72
  opts.broadcast ? "--broadcast" : "",
@@ -10,14 +10,14 @@ import { fmt, log, header, die } from "../ui.js";
10
10
  // Known testnet / mainnet addresses for quick init
11
11
  const PRESETS = {
12
12
  "84532": {
13
- issuer: "0xB13AE2498Df62A85768a4b783109C05fCf5A264a",
14
- hook: "0x6C57b50Ef9286b132066012B19b291FB120ACa80",
15
- registry: "0x19fD4eCF4359fCc8d5E79916691a28c24A22a9B4",
16
- router: "0xd0aF4D1EFF36CB2a1E88017eA398dCaDe1Ac0040",
17
- treasury: "0xF40493ACDd33cC4a841fCD69577A66218381C2fC",
18
- tokenA: "0x2E0dEd1CF4ec6106079df4eF1200959c2a454f3A",
19
- tokenB: "0x6dFCC8c373fBC3ecdda0F2b27590f12EeE9fF204",
20
- poolId: "0x16b3e7a5c52216925f705673b3ab25db5e6025da530cf53b3bcb5affeb18d95f",
13
+ issuer: "0xc4E032A7574016bd0e3d1a5BbFdE886af09CeD9A",
14
+ hook: "0xF5066ad9c25F3f54cfb19609A60187C48C184A80",
15
+ registry: "0x910a3efDc426f3216738106dd0DC6EA696477233",
16
+ router: "0x7727F0f3EBe99A558487394D001950ee6B33BB86",
17
+ treasury: "0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38",
18
+ tokenA: "0x582362E608F36850F6f641510d5D19C1EaB4cb27",
19
+ tokenB: "0x6eBBdAC70EC422C512727B25c7F0D9120ed101Ff",
20
+ poolId: "0xc1c8f29d6f03b5cd18bf2b862d48f45cc338022a154945b89c4bcb0a3e11e87f",
21
21
  fee: "8388608",
22
22
  tickSpacing: "60",
23
23
  rpc: "https://sepolia.base.org",
@@ -23,9 +23,17 @@ const CHAINS = { "8453": base, "84532": baseSepolia };
23
23
  const ERC20_ABI = [
24
24
  { name: "symbol", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "string" }] },
25
25
  { name: "decimals", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "uint8" }] },
26
+ { name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "owner", type: "address" }], outputs: [{ type: "uint256" }] },
26
27
  { name: "allowance", type: "function", stateMutability: "view", inputs: [{ name: "owner", type: "address" }, { name: "spender", type: "address" }], outputs: [{ type: "uint256" }] },
27
28
  { name: "approve", type: "function", stateMutability: "nonpayable", inputs: [{ name: "spender", type: "address" }, { name: "amount", type: "uint256" }], outputs: [{ type: "bool" }] },
28
29
  ];
30
+ const CNF_ABI = [
31
+ { name: "isValid", type: "function", stateMutability: "view", inputs: [{ name: "wallet", type: "address" }], outputs: [{ type: "bool" }] },
32
+ { name: "credentialOf", type: "function", stateMutability: "view", inputs: [{ name: "wallet", type: "address" }], outputs: [{ type: "uint256" }] },
33
+ { name: "merkleRoot", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "uint256" }] },
34
+ { name: "zkVerifier", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "address" }] },
35
+ { name: "eas", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "address" }] },
36
+ ];
29
37
  const ROUTER_LIQUIDITY_ABI = [
30
38
  {
31
39
  name: "addLiquidity", type: "function", stateMutability: "payable",
@@ -94,6 +102,7 @@ function txUrl(chain, hash) {
94
102
  const baseUrl = chain.blockExplorers?.default?.url;
95
103
  return baseUrl ? `${baseUrl}/tx/${hash}` : undefined;
96
104
  }
105
+ const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
97
106
  // ─── Shared core ──────────────────────────────────────────────────────────────
98
107
  async function executeLiquidity(action, opts) {
99
108
  const cfg = withConfig(opts);
@@ -143,11 +152,51 @@ async function executeLiquidity(action, opts) {
143
152
  log.kv("tickUpper", tickUpper.toString());
144
153
  log.kv("liquidity", liquidity.toString());
145
154
  log.line();
155
+ if (liquidity <= 0n) {
156
+ die("liquidity must be greater than 0. No approval or liquidity transaction was sent.");
157
+ }
158
+ const preflightSpin = new Spinner("Running preflight checks…").start();
159
+ const [root, verifier, eas, valid, tokenId, sym0, sym1, bal0, bal1] = await Promise.all([
160
+ pubClient.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "merkleRoot" }),
161
+ pubClient.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "zkVerifier" }),
162
+ pubClient.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "eas" }),
163
+ pubClient.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "isValid", args: [account.address] }),
164
+ pubClient.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "credentialOf", args: [account.address] }),
165
+ pubClient.readContract({ address: c0, abi: ERC20_ABI, functionName: "symbol" }),
166
+ pubClient.readContract({ address: c1, abi: ERC20_ABI, functionName: "symbol" }),
167
+ pubClient.readContract({ address: c0, abi: ERC20_ABI, functionName: "balanceOf", args: [account.address] }),
168
+ pubClient.readContract({ address: c1, abi: ERC20_ABI, functionName: "balanceOf", args: [account.address] }),
169
+ ]);
170
+ preflightSpin.stop();
171
+ const preflightErrors = [];
172
+ const hasEASPath = eas !== ZERO_ADDRESS;
173
+ const hasZKPath = verifier !== ZERO_ADDRESS && root !== 0n;
174
+ if (tokenId === 0n) {
175
+ preflightErrors.push("wallet has no CNF credential; mint one before changing liquidity.");
176
+ if (hasEASPath)
177
+ preflightErrors.push("issuer supports EAS/mock attestation minting: run `ilal credential mint --attestation <uid>`.");
178
+ else if (hasZKPath)
179
+ preflightErrors.push(`issuer supports ZK minting: run \`ilal credential prove --wallet ${account.address}\`.`);
180
+ else
181
+ preflightErrors.push("issuer has no active issuance path: EAS is unset and ZK verifier/root are not both configured.");
182
+ }
183
+ else if (!valid)
184
+ preflightErrors.push("wallet CNF credential exists but is not valid.");
185
+ if (action === "add" && (bal0 === 0n || bal1 === 0n)) {
186
+ preflightErrors.push(`token balances are not ready for adding liquidity: ${sym0}=${bal0.toString()} wei, ${sym1}=${bal1.toString()} wei.`);
187
+ }
188
+ if (preflightErrors.length > 0) {
189
+ log.section("Preflight Failed");
190
+ for (const error of preflightErrors)
191
+ log.warn(error);
192
+ console.log();
193
+ die(`${verb} liquidity not sent. Fix the preflight issues above.`);
194
+ }
146
195
  // Approve both tokens if adding liquidity
147
196
  if (action === "add") {
148
197
  const MAX = 2n ** 256n - 1n;
149
198
  for (const token of [c0, c1]) {
150
- const sym = await pubClient.readContract({ address: token, abi: ERC20_ABI, functionName: "symbol" });
199
+ const sym = token.toLowerCase() === c0.toLowerCase() ? sym0 : sym1;
151
200
  const allowed = await pubClient.readContract({
152
201
  address: token, abi: ERC20_ABI, functionName: "allowance",
153
202
  args: [account.address, cfg.router],
@@ -26,7 +26,11 @@ const CNF_ISSUER_ABI = [
26
26
  inputs: [{ name: "wallet", type: "address" }],
27
27
  outputs: [{ type: "bool" }],
28
28
  },
29
+ { name: "eas", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "address" }] },
30
+ { name: "schemaUID", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "bytes32" }] },
31
+ { name: "trustedAttester", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "address" }] },
29
32
  ];
33
+ const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
30
34
  async function sendMintTx(mode, opts) {
31
35
  const rawKey = opts.privateKey ?? process.env["PRIVATE_KEY"];
32
36
  if (!rawKey)
@@ -54,9 +58,14 @@ async function sendMintTx(mode, opts) {
54
58
  log.line();
55
59
  // Verify EAS attestation exists on-chain before sending tx
56
60
  log.step("Verifying attestation on EAS…");
57
- const easAddress = EAS_ADDRESSES[chain.id];
61
+ const [issuerEAS, issuerSchema, issuerAttester] = await Promise.all([
62
+ publicClient.readContract({ address: opts.issuer, abi: CNF_ISSUER_ABI, functionName: "eas" }),
63
+ publicClient.readContract({ address: opts.issuer, abi: CNF_ISSUER_ABI, functionName: "schemaUID" }),
64
+ publicClient.readContract({ address: opts.issuer, abi: CNF_ISSUER_ABI, functionName: "trustedAttester" }),
65
+ ]);
66
+ const easAddress = issuerEAS !== ZERO_ADDRESS ? issuerEAS : EAS_ADDRESSES[chain.id];
58
67
  if (!easAddress)
59
- die(`No EAS contract known for chain ${chain.id}. Use --rpc with a custom setup.`);
68
+ die(`No EAS contract known for chain ${chain.id}. Use an issuer with eas() configured.`);
60
69
  const EAS_ABI = [
61
70
  {
62
71
  name: "getAttestation",
@@ -95,18 +104,24 @@ async function sendMintTx(mode, opts) {
95
104
  if (attestation.recipient.toLowerCase() !== account.address.toLowerCase())
96
105
  die(`Attestation recipient (${attestation.recipient}) does not match your wallet (${account.address}).`);
97
106
  log.ok(`Attester: ${attestation.attester}`);
98
- if (attestation.attester.toLowerCase() === COINBASE_ATTESTER.toLowerCase()) {
107
+ if (attestation.attester.toLowerCase() === issuerAttester.toLowerCase()) {
108
+ log.ok("Issuer trusted attester confirmed");
109
+ }
110
+ else if (attestation.attester.toLowerCase() === COINBASE_ATTESTER.toLowerCase()) {
99
111
  log.ok("Coinbase Verifications attester confirmed");
100
112
  }
101
113
  else {
102
- log.warn(`Unknown attesternot Coinbase Verifications (${COINBASE_ATTESTER})`);
114
+ log.warn(`Attester mismatchissuer expects ${issuerAttester}`);
103
115
  }
104
- if (attestation.schema.toLowerCase() !== COINBASE_SCHEMA_UID.toLowerCase()) {
105
- log.warn(`Schema mismatch — expected Coinbase schema, got ${attestation.schema}`);
116
+ if (attestation.schema.toLowerCase() !== issuerSchema.toLowerCase()) {
117
+ log.warn(`Schema mismatch — issuer expects ${issuerSchema}, got ${attestation.schema}`);
106
118
  }
107
- else {
119
+ else if (attestation.schema.toLowerCase() === COINBASE_SCHEMA_UID.toLowerCase()) {
108
120
  log.ok("Coinbase Account Verification schema confirmed");
109
121
  }
122
+ else {
123
+ log.ok("Issuer schema confirmed");
124
+ }
110
125
  log.line();
111
126
  if (opts.simulate) {
112
127
  log.ok("Simulation complete — attestation valid, tx would succeed");
@@ -12,11 +12,11 @@
12
12
  * # Step 1 — queue a new root (requires owner key, executes immediately)
13
13
  * PRIVATE_KEY=0x... ilal oracle propose-root \
14
14
  * --root 0xDEADBEEF... \
15
- * --issuer 0xB13AE2...
15
+ * --issuer 0xc4E032...
16
16
  *
17
17
  * # Step 2 — after ROOT_DELAY (48 h) has elapsed, activate it
18
18
  * PRIVATE_KEY=0x... ilal oracle activate-root \
19
- * --issuer 0xB13AE2...
19
+ * --issuer 0xc4E032...
20
20
  *
21
21
  * # Same pattern for the ZK verifier (VERIFIER_DELAY = 72 h)
22
22
  * PRIVATE_KEY=0x... ilal oracle propose-verifier --verifier 0x... --issuer 0x...
@@ -12,11 +12,11 @@
12
12
  * # Step 1 — queue a new root (requires owner key, executes immediately)
13
13
  * PRIVATE_KEY=0x... ilal oracle propose-root \
14
14
  * --root 0xDEADBEEF... \
15
- * --issuer 0xB13AE2...
15
+ * --issuer 0xc4E032...
16
16
  *
17
17
  * # Step 2 — after ROOT_DELAY (48 h) has elapsed, activate it
18
18
  * PRIVATE_KEY=0x... ilal oracle activate-root \
19
- * --issuer 0xB13AE2...
19
+ * --issuer 0xc4E032...
20
20
  *
21
21
  * # Same pattern for the ZK verifier (VERIFIER_DELAY = 72 h)
22
22
  * PRIVATE_KEY=0x... ilal oracle propose-verifier --verifier 0x... --issuer 0x...
@@ -7,7 +7,7 @@
7
7
  * Usage:
8
8
  * ilal credential prove \
9
9
  * --wallet 0x1b869... \
10
- * --issuer 0xB13AE2... \
10
+ * --issuer 0xc4E032... \
11
11
  * --chain 84532 \
12
12
  * --action mint # or renew (default: auto-detect)
13
13
  * --circuit-dir ./circuits/build
@@ -7,7 +7,7 @@
7
7
  * Usage:
8
8
  * ilal credential prove \
9
9
  * --wallet 0x1b869... \
10
- * --issuer 0xB13AE2... \
10
+ * --issuer 0xc4E032... \
11
11
  * --chain 84532 \
12
12
  * --action mint # or renew (default: auto-detect)
13
13
  * --circuit-dir ./circuits/build
@@ -14,6 +14,7 @@ const CNF_ABI = [
14
14
  { name: "getCredential", type: "function", stateMutability: "view", inputs: [{ name: "tokenId", type: "uint256" }], outputs: [{ type: "tuple", components: [{ name: "holder", type: "address" }, { name: "issuer", type: "address" }, { name: "credentialType", type: "bytes32" }, { name: "issuedAt", type: "uint64" }, { name: "expiresAt", type: "uint64" }, { name: "revoked", type: "bool" }] }] },
15
15
  { name: "merkleRoot", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "uint256" }] },
16
16
  { name: "zkVerifier", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "address" }] },
17
+ { name: "eas", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "address" }] },
17
18
  ];
18
19
  const HOOK_ABI = [
19
20
  { name: "issuer", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "address" }] },
@@ -21,6 +22,7 @@ const HOOK_ABI = [
21
22
  const REGISTRY_ABI = [
22
23
  { name: "getPolicy", type: "function", stateMutability: "view", inputs: [{ name: "poolId", type: "bytes32" }], outputs: [{ type: "tuple", components: [{ name: "cnfIssuer", type: "address" }, { name: "requiredCredentialType", type: "bytes32" }, { name: "enabled", type: "bool" }] }] },
23
24
  ];
25
+ const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
24
26
  function daysUntil(unixSec) {
25
27
  return Math.floor((unixSec * 1000 - Date.now()) / 86_400_000);
26
28
  }
@@ -88,20 +90,28 @@ export async function status(opts) {
88
90
  if (cfg.issuer && isAddress(cfg.issuer)) {
89
91
  const spin = new Spinner("Fetching issuer config…").start();
90
92
  try {
91
- const [root, verifier] = await Promise.all([
93
+ const [root, verifier, eas] = await Promise.all([
92
94
  client.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "merkleRoot" }),
93
95
  client.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "zkVerifier" }),
96
+ client.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "eas" }),
94
97
  ]);
95
98
  spin.stop();
99
+ const hasEASPath = eas !== ZERO_ADDRESS;
100
+ const hasZKPath = root !== 0n && verifier !== ZERO_ADDRESS;
96
101
  log.section("Issuer");
97
102
  log.kv("address", fmt.cyan(cfg.issuer));
98
- log.kv("zkVerifier", verifier === "0x0000000000000000000000000000000000000000"
103
+ log.kv("issuance", hasEASPath
104
+ ? `${fmt.badge("EAS", "green")} ${fmt.addr(eas)}`
105
+ : hasZKPath
106
+ ? fmt.badge("ZK", "green")
107
+ : fmt.badge("not ready", "red"));
108
+ log.kv("zkVerifier", verifier === ZERO_ADDRESS
99
109
  ? fmt.badge("not set", "red")
100
110
  : fmt.green(fmt.addr(verifier)));
101
111
  log.kv("merkleRoot", root === 0n
102
112
  ? fmt.badge("not set", "red")
103
113
  : fmt.gray(root.toString().slice(0, 20) + "…"));
104
- issuerReady = root !== 0n && verifier !== "0x0000000000000000000000000000000000000000";
114
+ issuerReady = hasEASPath || hasZKPath;
105
115
  }
106
116
  catch (e) {
107
117
  spin.stop();
@@ -56,6 +56,13 @@ const ROUTER_ABI = [
56
56
  outputs: [{ name: "delta", type: "int256" }],
57
57
  },
58
58
  ];
59
+ const CNF_ABI = [
60
+ { name: "isValid", type: "function", stateMutability: "view", inputs: [{ name: "wallet", type: "address" }], outputs: [{ type: "bool" }] },
61
+ { name: "credentialOf", type: "function", stateMutability: "view", inputs: [{ name: "wallet", type: "address" }], outputs: [{ type: "uint256" }] },
62
+ { name: "merkleRoot", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "uint256" }] },
63
+ { name: "zkVerifier", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "address" }] },
64
+ { name: "eas", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "address" }] },
65
+ ];
59
66
  // ─── Session helpers ──────────────────────────────────────────────────────────
60
67
  const SESSION_TOKEN_TYPE = [
61
68
  { name: "user", type: "address" },
@@ -77,6 +84,7 @@ const MIN_SQRT_PRICE = 4295128740n; // TickMath.MIN_SQRT_PRICE + 1
77
84
  const MAX_SQRT_PRICE = 1461446703485210103287273052203988822378723970341n; // MAX - 1
78
85
  const DYNAMIC_FEE_FLAG = 8388608;
79
86
  const PIPS_DENOMINATOR = 1000000n;
87
+ const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
80
88
  function txUrl(chain, hash) {
81
89
  const baseUrl = chain.blockExplorers?.default?.url;
82
90
  return baseUrl ? `${baseUrl}/tx/${hash}` : undefined;
@@ -148,6 +156,9 @@ export async function swap(opts) {
148
156
  ]);
149
157
  spin.stop();
150
158
  const amountIn = parseUnits(opts.amountIn, decimals);
159
+ if (amountIn <= 0n) {
160
+ die("amount-in must be greater than 0. Use `ilal swap --simulate` for a dry run.");
161
+ }
151
162
  log.kv("amount", `${opts.amountIn} ${fmt.cyan(symbol)} (${amountIn.toString()} wei)`);
152
163
  let protocolFeePips = 0;
153
164
  let treasury;
@@ -162,34 +173,47 @@ export async function swap(opts) {
162
173
  }
163
174
  const protocolFeeAmount = amountIn * BigInt(protocolFeePips) / PIPS_DENOMINATOR;
164
175
  const totalDebit = amountIn + protocolFeeAmount;
176
+ const preflightSpin = new Spinner("Running preflight checks…").start();
177
+ const [root, verifier, eas, valid, tokenId, balance] = await Promise.all([
178
+ pubClient.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "merkleRoot" }),
179
+ pubClient.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "zkVerifier" }),
180
+ pubClient.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "eas" }),
181
+ pubClient.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "isValid", args: [account.address] }),
182
+ pubClient.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "credentialOf", args: [account.address] }),
183
+ pubClient.readContract({ address: tokenIn, abi: ERC20_ABI, functionName: "balanceOf", args: [account.address] }),
184
+ ]);
185
+ preflightSpin.stop();
186
+ const preflightErrors = [];
187
+ const hasEASPath = eas !== ZERO_ADDRESS;
188
+ const hasZKPath = verifier !== ZERO_ADDRESS && root !== 0n;
189
+ if (tokenId === 0n) {
190
+ preflightErrors.push(`wallet has no CNF credential; mint one before trading.`);
191
+ if (hasEASPath)
192
+ preflightErrors.push("issuer supports EAS/mock attestation minting: run `ilal credential mint --attestation <uid>`.");
193
+ else if (hasZKPath)
194
+ preflightErrors.push(`issuer supports ZK minting: run \`ilal credential prove --wallet ${account.address}\`.`);
195
+ else
196
+ preflightErrors.push("issuer has no active issuance path: EAS is unset and ZK verifier/root are not both configured.");
197
+ }
198
+ else if (!valid)
199
+ preflightErrors.push("wallet CNF credential exists but is not valid.");
200
+ if (balance < totalDebit)
201
+ preflightErrors.push(`insufficient ${symbol} balance: need ${totalDebit.toString()} wei including ILAL fee, have ${balance.toString()} wei.`);
202
+ if (preflightErrors.length > 0) {
203
+ log.section("Preflight Failed");
204
+ for (const error of preflightErrors)
205
+ log.warn(error);
206
+ console.log();
207
+ if (!opts.simulate) {
208
+ die("Swap not sent. Fix the preflight issues above, or use --simulate to inspect session/hookData only.");
209
+ }
210
+ }
165
211
  log.deal([
166
212
  { label: "verified input", value: `${opts.amountIn} ${symbol}`, note: "exact-in swap", tone: "cyan" },
167
213
  { label: "LP fee", value: poolFeePercent(parseInt(cfg.fee ?? "3000")), note: "hook-priced flow", tone: "green" },
168
214
  { label: "ILAL fee", value: protocolFeePips > 0 ? pipsToPercent(protocolFeePips) : "off", note: protocolFeePips > 0 ? "protocol revenue" : "legacy router", tone: protocolFeePips > 0 ? "cyan" : "gray" },
169
215
  ]);
170
216
  log.line();
171
- // Check allowance — approve if needed
172
- const approveSpin = new Spinner("Checking allowance…").start();
173
- const allowed = await pubClient.readContract({
174
- address: tokenIn,
175
- abi: ERC20_ABI,
176
- functionName: "allowance",
177
- args: [account.address, cfg.router],
178
- });
179
- if (allowed < totalDebit) {
180
- approveSpin.update(`Approving ${symbol} for ILALRouter…`);
181
- const approveHash = await walClient.writeContract({
182
- address: tokenIn,
183
- abi: ERC20_ABI,
184
- functionName: "approve",
185
- args: [cfg.router, totalDebit * 10n], // approve 10× for future swaps
186
- });
187
- await pubClient.waitForTransactionReceipt({ hash: approveHash });
188
- approveSpin.succeed(`Approved ${symbol} ${fmt.gray(fmt.hash(approveHash))}`);
189
- }
190
- else {
191
- approveSpin.succeed(`Allowance ok (${fmt.gray(allowed.toString())} wei)`);
192
- }
193
217
  // Sign session token
194
218
  const signSpin = new Spinner("Signing session token…").start();
195
219
  const ttl = parseInt(opts.ttl ?? "600");
@@ -233,11 +257,33 @@ export async function swap(opts) {
233
257
  }
234
258
  log.line();
235
259
  if (opts.simulate) {
236
- log.ok("Simulation mode — skipping on-chain tx");
260
+ log.ok("Simulation mode — skipping approval and on-chain tx");
237
261
  log.kv("hookData", hookData.slice(0, 22) + "…");
238
262
  console.log();
239
263
  return;
240
264
  }
265
+ // Check allowance — approve if needed
266
+ const approveSpin = new Spinner("Checking allowance…").start();
267
+ const allowed = await pubClient.readContract({
268
+ address: tokenIn,
269
+ abi: ERC20_ABI,
270
+ functionName: "allowance",
271
+ args: [account.address, cfg.router],
272
+ });
273
+ if (allowed < totalDebit) {
274
+ approveSpin.update(`Approving ${symbol} for ILALRouter…`);
275
+ const approveHash = await walClient.writeContract({
276
+ address: tokenIn,
277
+ abi: ERC20_ABI,
278
+ functionName: "approve",
279
+ args: [cfg.router, totalDebit * 10n], // approve 10× for future swaps
280
+ });
281
+ await pubClient.waitForTransactionReceipt({ hash: approveHash });
282
+ approveSpin.succeed(`Approved ${symbol} ${fmt.gray(fmt.hash(approveHash))}`);
283
+ }
284
+ else {
285
+ approveSpin.succeed(`Allowance ok (${fmt.gray(allowed.toString())} wei)`);
286
+ }
241
287
  // Build PoolKey
242
288
  const poolKey = {
243
289
  currency0: c0,
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 } from "./commands/demo.js";
11
+ import { demo, demoCheck, demoFaucet } 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.1")
22
+ .version("0.2.3")
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
@@ -72,6 +72,15 @@ demoCommand
72
72
  .action(async (opts) => {
73
73
  await demoCheck(opts).catch(err);
74
74
  });
75
+ demoCommand
76
+ .command("faucet")
77
+ .description("Mint mock demo TOKA/TOKB to a wallet (testnet mock tokens only)")
78
+ .option("-w, --wallet <address>", "Recipient wallet (defaults to PRIVATE_KEY address)")
79
+ .option("--amount <tokens>", "Human token amount to mint for each token", "10000")
80
+ .option("-k, --private-key <hex>", "Private key that pays gas")
81
+ .action(async (opts) => {
82
+ await demoFaucet(opts).catch(err);
83
+ });
75
84
  const err = (e) => {
76
85
  console.error(fmt.red(`\nError: ${e instanceof Error ? e.message : String(e)}\n`));
77
86
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilalv3/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "ILAL Protocol CLI — compliant swaps and credential management for Uniswap v4",
5
5
  "type": "module",
6
6
  "bin": {