@ilalv3/cli 0.2.6 → 0.2.8

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
@@ -34,7 +34,7 @@ ilal status --wallet 0xc0807D4778a9E5FE15ad68A8500e64d65BA78D58
34
34
  ilal demo check --wallet 0xc0807D4778a9E5FE15ad68A8500e64d65BA78D58
35
35
 
36
36
  # 4. Execute a compliant swap with the seeded reviewer key
37
- PRIVATE_KEY=0x... ilal swap --amount-in 1 --token-in 0x3a7d58fAc623B4C30D7735B01DcE036EfF46e079 --min-amount-out 0
37
+ PRIVATE_KEY=0x... ilal swap --amount-in 1 --token-in 0x3d5b92a8Cea5BBe1c6f63b73D69DA6457e6436E2 --min-amount-out 0
38
38
  ```
39
39
 
40
40
  For a fully seeded local/testnet demo, deploy mock EAS + demo pool pieces:
@@ -130,10 +130,10 @@ The CLI reads `.ilal.json` in the current directory. Run `ilal init` to create i
130
130
 
131
131
  ```bash
132
132
  ilal swap \
133
- --router 0xEfB2F179F6Ce44d7af66d3e3FF792563033C9b7e \
134
- --hook 0xaCD0fccDDd96471f7De9b3f015C5ebFaADe70a80 \
135
- --issuer 0x108fA8db11616d73ccB67725B44C535Ddcaac5a9 \
136
- --pool-id 0x0decaeb998563be8faf6e6b66d4a0c32025a166e35bae97b8ec62ded1b04be1b \
133
+ --router 0x88125331f169aF4Dc81ADa6E8A189110566E457a \
134
+ --hook 0x5f1de4376C7a59b5BBC5E5cd766D40995E9e4A80 \
135
+ --issuer 0x33541301e35d33eDf554c4DFba1e04d04FCc52F4 \
136
+ --pool-id 0x8b6d21e53673584f192bdad8b65e2002e9e8eea730c62adad5ac1f4a084164a4 \
137
137
  --amount-in 0.001
138
138
  ```
139
139
 
@@ -141,20 +141,22 @@ ilal swap \
141
141
 
142
142
  | Contract | Address |
143
143
  |---|---|
144
- | CNFIssuer | `0x108fA8db11616d73ccB67725B44C535Ddcaac5a9` |
145
- | MockEAS | `0xE46d87960b8740585010ae5158193D67da7dd807` |
146
- | ZKVerifierAdapter | `0xb77BB4566d5D1e81370E159bb0251467e4a2fcfa` |
147
- | ComplianceHook | `0xaCD0fccDDd96471f7De9b3f015C5ebFaADe70a80` |
148
- | ILALRouter | `0xEfB2F179F6Ce44d7af66d3e3FF792563033C9b7e` |
149
- | PolicyRegistry | `0xC2Be4887aF9218b4B617F7125924737413292160` |
150
- | Currency0 / TOKA | `0x3a7d58fAc623B4C30D7735B01DcE036EfF46e079` |
151
- | Currency1 / TOKB | `0x7BC67f7Fd3892fBE6AcC4F10bc3df95b64c2eD80` |
152
- | Pool ID | `0x0decaeb998563be8faf6e6b66d4a0c32025a166e35bae97b8ec62ded1b04be1b` |
144
+ | CNFIssuer | `0x33541301e35d33eDf554c4DFba1e04d04FCc52F4` |
145
+ | MockEAS | `0x6A98096DF6F54DBF40498dC5525d84AEA840663A` |
146
+ | ZKVerifierAdapter | `0x9467ED8d962221e3C1865a387481B862B1b5bE95` |
147
+ | ComplianceHook | `0x5f1de4376C7a59b5BBC5E5cd766D40995E9e4A80` |
148
+ | ILALRouter | `0x88125331f169aF4Dc81ADa6E8A189110566E457a` |
149
+ | PolicyRegistry | `0x83d8111B415E97bA91eaAe717c2D9Ae6f0DD19d4` |
150
+ | Currency0 / TOKA | `0x3d5b92a8Cea5BBe1c6f63b73D69DA6457e6436E2` |
151
+ | Currency1 / TOKB | `0x6145F81e3691d991a4D2033FE25BeB140487B7Ee` |
152
+ | Pool ID | `0x8b6d21e53673584f192bdad8b65e2002e9e8eea730c62adad5ac1f4a084164a4` |
153
153
 
154
154
  Live proof:
155
155
 
156
- - CNF ZK mint tx: `0x8c0ca35cb666d839b7070ed8103d12379b12ccb399283fcacaf5caa8b86e4542`
157
- - Current-stack add liquidity / swap: pending re-run after local RPC/TLS instability clears; router happy path is covered by the Solidity integration tests.
156
+ - CNF ZK mint tx: `0xb9aa16c9604a575c8b2281cbfe9ba24fedbf205283a7b05638fbc413ed78de41`
157
+ - Add liquidity tx: `0xc3dba6d488933e1568541ece17ce43307fb173eb747dff303f3631456eccb16a`
158
+ - Swap tx: `0x360461d2a3c19acdc3ba125e55689679fcf809946d8a5092e833eb9e94b0f52f`
159
+ - Router bypass fix verified: `ComplianceHook.authorizedRouter()` returns `0x88125331f169aF4Dc81ADa6E8A189110566E457a`
158
160
 
159
161
  ## License
160
162
 
@@ -10,10 +10,10 @@ const POOL_MANAGER = {
10
10
  };
11
11
  const SAMPLE = {
12
12
  wallet: "0xc0807D4778a9E5FE15ad68A8500e64d65BA78D58",
13
- issuer: "0x108fA8db11616d73ccB67725B44C535Ddcaac5a9",
14
- hook: "0xaCD0fccDDd96471f7De9b3f015C5ebFaADe70a80",
15
- router: "0xEfB2F179F6Ce44d7af66d3e3FF792563033C9b7e",
16
- pool: "0x0decaeb998563be8faf6e6b66d4a0c32025a166e35bae97b8ec62ded1b04be1b",
13
+ issuer: "0x33541301e35d33eDf554c4DFba1e04d04FCc52F4",
14
+ hook: "0x5f1de4376C7a59b5BBC5E5cd766D40995E9e4A80",
15
+ router: "0x88125331f169aF4Dc81ADa6E8A189110566E457a",
16
+ pool: "0x8b6d21e53673584f192bdad8b65e2002e9e8eea730c62adad5ac1f4a084164a4",
17
17
  proof: "0x91f2b8a0c43e902f7f1a8c0d",
18
18
  session: "0x6b84eac5e0db21f8d5d43b7a",
19
19
  };
@@ -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: "0x108fA8db11616d73ccB67725B44C535Ddcaac5a9",
14
- hook: "0xaCD0fccDDd96471f7De9b3f015C5ebFaADe70a80",
15
- registry: "0xC2Be4887aF9218b4B617F7125924737413292160",
16
- router: "0xEfB2F179F6Ce44d7af66d3e3FF792563033C9b7e",
13
+ issuer: "0x33541301e35d33eDf554c4DFba1e04d04FCc52F4",
14
+ hook: "0x5f1de4376C7a59b5BBC5E5cd766D40995E9e4A80",
15
+ registry: "0x83d8111B415E97bA91eaAe717c2D9Ae6f0DD19d4",
16
+ router: "0x88125331f169aF4Dc81ADa6E8A189110566E457a",
17
17
  treasury: "0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38",
18
- tokenA: "0x3a7d58fAc623B4C30D7735B01DcE036EfF46e079",
19
- tokenB: "0x7BC67f7Fd3892fBE6AcC4F10bc3df95b64c2eD80",
20
- poolId: "0x0decaeb998563be8faf6e6b66d4a0c32025a166e35bae97b8ec62ded1b04be1b",
18
+ tokenA: "0x3d5b92a8Cea5BBe1c6f63b73D69DA6457e6436E2",
19
+ tokenB: "0x6145F81e3691d991a4D2033FE25BeB140487B7Ee",
20
+ poolId: "0x8b6d21e53673584f192bdad8b65e2002e9e8eea730c62adad5ac1f4a084164a4",
21
21
  fee: "8388608",
22
22
  tickSpacing: "60",
23
23
  rpc: "https://sepolia.base.org",
@@ -13,7 +13,7 @@
13
13
  * --router 0xROUTER --hook 0xHOOK --issuer 0xISSUER \
14
14
  * --pool-id 0xPOOLID --token-a 0xTOKA --token-b 0xTOKB
15
15
  */
16
- import { createPublicClient, createWalletClient, encodeAbiParameters, http, isAddress, isHex, parseAbiParameters, } from "viem";
16
+ import { createPublicClient, createWalletClient, encodeAbiParameters, formatEther, formatUnits, http, isAddress, isHex, parseAbiParameters, } from "viem";
17
17
  import { privateKeyToAccount } from "viem/accounts";
18
18
  import { base, baseSepolia } from "viem/chains";
19
19
  import { fmt, log, header, Spinner, die, dieOnContract } from "../ui.js";
@@ -103,6 +103,25 @@ function txUrl(chain, hash) {
103
103
  return baseUrl ? `${baseUrl}/tx/${hash}` : undefined;
104
104
  }
105
105
  const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
106
+ const MAX_UINT256 = 2n ** 256n - 1n;
107
+ function trimDecimals(value, places = 8) {
108
+ const [whole, frac] = value.split(".");
109
+ if (!frac)
110
+ return whole ?? value;
111
+ const trimmed = frac.slice(0, places).replace(/0+$/, "");
112
+ return trimmed ? `${whole}.${trimmed}` : whole ?? value;
113
+ }
114
+ function tokenAmount(raw, decimals, symbol, places = 8) {
115
+ return `${trimDecimals(formatUnits(raw, decimals), places)} ${symbol}`;
116
+ }
117
+ function allowanceLabel(raw, decimals, symbol) {
118
+ if (raw >= MAX_UINT256 / 2n)
119
+ return "unlimited (MAX)";
120
+ return `${tokenAmount(raw, decimals, symbol)} (${raw.toString()} wei)`;
121
+ }
122
+ function secondsSince(startMs) {
123
+ return `${((Date.now() - startMs) / 1000).toFixed(1)}s`;
124
+ }
106
125
  // ─── Shared core ──────────────────────────────────────────────────────────────
107
126
  async function executeLiquidity(action, opts) {
108
127
  const cfg = withConfig(opts);
@@ -156,7 +175,7 @@ async function executeLiquidity(action, opts) {
156
175
  die("liquidity must be greater than 0. No approval or liquidity transaction was sent.");
157
176
  }
158
177
  const preflightSpin = new Spinner("Running preflight checks…").start();
159
- const [root, verifier, eas, valid, tokenId, sym0, sym1, bal0, bal1] = await Promise.all([
178
+ const [root, verifier, eas, valid, tokenId, sym0, sym1, dec0, dec1, bal0, bal1] = await Promise.all([
160
179
  pubClient.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "merkleRoot" }),
161
180
  pubClient.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "zkVerifier" }),
162
181
  pubClient.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "eas" }),
@@ -164,6 +183,8 @@ async function executeLiquidity(action, opts) {
164
183
  pubClient.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "credentialOf", args: [account.address] }),
165
184
  pubClient.readContract({ address: c0, abi: ERC20_ABI, functionName: "symbol" }),
166
185
  pubClient.readContract({ address: c1, abi: ERC20_ABI, functionName: "symbol" }),
186
+ pubClient.readContract({ address: c0, abi: ERC20_ABI, functionName: "decimals" }),
187
+ pubClient.readContract({ address: c1, abi: ERC20_ABI, functionName: "decimals" }),
167
188
  pubClient.readContract({ address: c0, abi: ERC20_ABI, functionName: "balanceOf", args: [account.address] }),
168
189
  pubClient.readContract({ address: c1, abi: ERC20_ABI, functionName: "balanceOf", args: [account.address] }),
169
190
  ]);
@@ -183,8 +204,24 @@ async function executeLiquidity(action, opts) {
183
204
  else if (!valid)
184
205
  preflightErrors.push("wallet CNF credential exists but is not valid.");
185
206
  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.`);
207
+ preflightErrors.push(`token balances are not ready for adding liquidity: ${sym0}=${tokenAmount(bal0, dec0, sym0)}, ${sym1}=${tokenAmount(bal1, dec1, sym1)}.`);
187
208
  }
209
+ log.section("Preflight Checks");
210
+ if (tokenId !== 0n && valid)
211
+ log.ok(`CNF credential token #${tokenId.toString()}`);
212
+ else
213
+ log.fail("CNF credential missing or invalid");
214
+ log.ok(`Issuer config (${hasEASPath ? "EAS" : "no EAS"}${hasZKPath ? " + ZK" : ""})`);
215
+ if (action !== "add" || bal0 > 0n)
216
+ log.ok(`${sym0} balance ${tokenAmount(bal0, dec0, sym0)}`);
217
+ else
218
+ log.fail(`${sym0} balance ${tokenAmount(bal0, dec0, sym0)}`);
219
+ if (action !== "add" || bal1 > 0n)
220
+ log.ok(`${sym1} balance ${tokenAmount(bal1, dec1, sym1)}`);
221
+ else
222
+ log.fail(`${sym1} balance ${tokenAmount(bal1, dec1, sym1)}`);
223
+ log.ok(`Route bound to router ${fmt.addr(cfg.router)} and hook ${fmt.addr(cfg.hook)}`);
224
+ log.line();
188
225
  if (preflightErrors.length > 0) {
189
226
  log.section("Preflight Failed");
190
227
  for (const error of preflightErrors)
@@ -194,9 +231,10 @@ async function executeLiquidity(action, opts) {
194
231
  }
195
232
  // Approve both tokens if adding liquidity
196
233
  if (action === "add") {
197
- const MAX = 2n ** 256n - 1n;
234
+ const MAX = MAX_UINT256;
198
235
  for (const token of [c0, c1]) {
199
236
  const sym = token.toLowerCase() === c0.toLowerCase() ? sym0 : sym1;
237
+ const decimals = token.toLowerCase() === c0.toLowerCase() ? dec0 : dec1;
200
238
  const allowed = await pubClient.readContract({
201
239
  address: token, abi: ERC20_ABI, functionName: "allowance",
202
240
  args: [account.address, cfg.router],
@@ -210,6 +248,9 @@ async function executeLiquidity(action, opts) {
210
248
  await pubClient.waitForTransactionReceipt({ hash: h });
211
249
  appSpin.succeed(`Approved ${sym} ${fmt.gray(fmt.hash(h))}`);
212
250
  }
251
+ else {
252
+ log.ok(`${sym} allowance: ${allowanceLabel(allowed, decimals, sym)}`);
253
+ }
213
254
  }
214
255
  }
215
256
  // Sign session token
@@ -243,7 +284,7 @@ async function executeLiquidity(action, opts) {
243
284
  message: token,
244
285
  });
245
286
  const hookData = encodeAbiParameters(HOOK_DATA_ABI, [token, signature]);
246
- signSpin.succeed(`Session signed (expires in ${ttl}s)`);
287
+ signSpin.succeed(`Session authorization signed (expires in ${ttl}s, one-time nonce)`);
247
288
  log.section("Gate Checks");
248
289
  log.kv("credential", `${fmt.badge("required", "cyan")} issuer ${fmt.addr(cfg.issuer)}`);
249
290
  log.kv("caller", `${fmt.badge("bound", "green")} ${fmt.addr(cfg.router)}`);
@@ -261,20 +302,30 @@ async function executeLiquidity(action, opts) {
261
302
  const liquidityParams = { tickLower, tickUpper, liquidityDelta, salt };
262
303
  const fnName = action === "add" ? "addLiquidity" : "removeLiquidity";
263
304
  // Execute
264
- const txSpin = new Spinner(`Sending ${fnName}…`).start();
305
+ const txSpin = new Spinner(`Submitting ${fnName} tx…`).start();
265
306
  let txHash;
307
+ let receipt;
266
308
  try {
309
+ const startMs = Date.now();
267
310
  const baseArgs = [poolKey, liquidityParams, hookData];
268
311
  txHash = await (action === "add"
269
312
  ? walClient.writeContract({ address: cfg.router, abi: ROUTER_LIQUIDITY_ABI, functionName: "addLiquidity", args: baseArgs, value: 0n })
270
313
  : walClient.writeContract({ address: cfg.router, abi: ROUTER_LIQUIDITY_ABI, functionName: "removeLiquidity", args: baseArgs }));
271
- txSpin.update(`Confirming ${fmt.gray(fmt.hash(txHash))}…`);
272
- const receipt = await pubClient.waitForTransactionReceipt({ hash: txHash });
314
+ txSpin.succeed(`Submitted to mempool ${fmt.gray(fmt.hash(txHash))}`);
315
+ const confirmSpin = new Spinner(`Confirming ${fmt.gray(fmt.hash(txHash))}…`).start();
316
+ receipt = await pubClient.waitForTransactionReceipt({ hash: txHash });
273
317
  if (receipt.status !== "success") {
274
- txSpin.fail("Transaction reverted");
318
+ confirmSpin.fail("Transaction reverted");
275
319
  die(`Tx failed: ${txHash}`);
276
320
  }
277
- txSpin.succeed(fmt.bold(fmt.green(`Liquidity ${action === "add" ? "added" : "removed"} via ILAL channel ✓`)));
321
+ confirmSpin.succeed(`Confirmed in block ${receipt.blockNumber.toString()}`);
322
+ const effectiveGasPrice = receipt.effectiveGasPrice;
323
+ log.metrics([
324
+ { label: "finality", value: secondsSince(startMs), tone: "green" },
325
+ { label: "gas used", value: receipt.gasUsed.toString(), tone: "cyan" },
326
+ ...(effectiveGasPrice ? [{ label: "gas cost", value: `${trimDecimals(formatEther(receipt.gasUsed * effectiveGasPrice), 8)} ETH`, tone: "cyan" }] : []),
327
+ ]);
328
+ log.ok(fmt.bold(fmt.green(`Liquidity ${action === "add" ? "added" : "removed"} via ILAL channel`)));
278
329
  }
279
330
  catch (e) {
280
331
  txSpin.fail(`${fnName} failed`);
@@ -283,7 +334,7 @@ async function executeLiquidity(action, opts) {
283
334
  log.line();
284
335
  log.callout(action === "add" ? "Hook-enforced liquidity add" : "Hook-enforced liquidity removal", "pool policy, credential type, session binding, and nonce all passed on-chain", "green");
285
336
  log.kv("tx", fmt.gray(txHash));
286
- log.kv("block", fmt.gray((await pubClient.getTransactionReceipt({ hash: txHash })).blockNumber.toString()));
337
+ log.kv("block", fmt.gray((receipt ?? await pubClient.getTransactionReceipt({ hash: txHash })).blockNumber.toString()));
287
338
  const explorer = txUrl(chain, txHash);
288
339
  if (explorer)
289
340
  log.kv("explorer", fmt.cyan(explorer));
@@ -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 0x108fA8...
15
+ * --issuer 0x335413...
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 0x108fA8...
19
+ * --issuer 0x335413...
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 0x108fA8...
15
+ * --issuer 0x335413...
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 0x108fA8...
19
+ * --issuer 0x335413...
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 0x108fA8... \
10
+ * --issuer 0x335413... \
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 0x108fA8... \
10
+ * --issuer 0x335413... \
11
11
  * --chain 84532 \
12
12
  * --action mint # or renew (default: auto-detect)
13
13
  * --circuit-dir ./circuits/build
@@ -37,4 +37,5 @@ export declare function swap(opts: {
37
37
  ttl?: string;
38
38
  hookData?: string;
39
39
  simulate?: boolean;
40
+ explain?: boolean;
40
41
  }): Promise<void>;
@@ -18,7 +18,7 @@
18
18
  * --pool-id 0xPOOLID \
19
19
  * --chain 84532
20
20
  */
21
- import { createPublicClient, createWalletClient, decodeAbiParameters, encodeAbiParameters, http, isAddress, isHex, parseAbiParameters, parseUnits, } from "viem";
21
+ import { createPublicClient, createWalletClient, decodeAbiParameters, encodeAbiParameters, formatEther, formatUnits, 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";
@@ -85,13 +85,14 @@ const MAX_SQRT_PRICE = 1461446703485210103287273052203988822378723970341n; // MA
85
85
  const DYNAMIC_FEE_FLAG = 8388608;
86
86
  const PIPS_DENOMINATOR = 1000000n;
87
87
  const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
88
+ const MAX_UINT256 = 2n ** 256n - 1n;
88
89
  function txUrl(chain, hash) {
89
90
  const baseUrl = chain.blockExplorers?.default?.url;
90
91
  return baseUrl ? `${baseUrl}/tx/${hash}` : undefined;
91
92
  }
92
93
  function feeLabel(fee) {
93
94
  if (fee === DYNAMIC_FEE_FLAG)
94
- return `${fmt.badge("fair flow", "green")} verified swap fee 0.05%`;
95
+ return `${fmt.badge("verified", "green")} 0.05% (vs 0.30% standard pool)`;
95
96
  return `${fmt.badge("static", "gray")} ${(fee / 10_000).toFixed(4).replace(/0+$/, "").replace(/\.$/, "")}%`;
96
97
  }
97
98
  function pipsToPercent(pips) {
@@ -102,6 +103,27 @@ function poolFeePercent(fee) {
102
103
  ? "0.05%"
103
104
  : `${(fee / 10_000).toFixed(4).replace(/0+$/, "").replace(/\.$/, "")}%`;
104
105
  }
106
+ function trimDecimals(value, places = 8) {
107
+ const [whole, frac] = value.split(".");
108
+ if (!frac)
109
+ return whole ?? value;
110
+ const trimmed = frac.slice(0, places).replace(/0+$/, "");
111
+ return trimmed ? `${whole}.${trimmed}` : whole ?? value;
112
+ }
113
+ function tokenAmount(raw, decimals, symbol, places = 8) {
114
+ return `${trimDecimals(formatUnits(raw, decimals), places)} ${symbol}`;
115
+ }
116
+ function tokenAmountWithWei(raw, decimals, symbol, places = 8) {
117
+ return `${tokenAmount(raw, decimals, symbol, places)} (${raw.toString()} wei)`;
118
+ }
119
+ function allowanceLabel(raw, decimals, symbol) {
120
+ if (raw >= MAX_UINT256 / 2n)
121
+ return "unlimited (MAX)";
122
+ return tokenAmountWithWei(raw, decimals, symbol);
123
+ }
124
+ function secondsSince(startMs) {
125
+ return `${((Date.now() - startMs) / 1000).toFixed(1)}s`;
126
+ }
105
127
  // ─── Main export ──────────────────────────────────────────────────────────────
106
128
  export async function swap(opts) {
107
129
  const cfg = withConfig(opts);
@@ -159,7 +181,7 @@ export async function swap(opts) {
159
181
  if (amountIn <= 0n) {
160
182
  die("amount-in must be greater than 0. Use `ilal swap --simulate` for a dry run.");
161
183
  }
162
- log.kv("amount", `${opts.amountIn} ${fmt.cyan(symbol)} (${amountIn.toString()} wei)`);
184
+ log.kv("amount", `${fmt.cyan(tokenAmountWithWei(amountIn, decimals, symbol))}`);
163
185
  let protocolFeePips = 0;
164
186
  let treasury;
165
187
  try {
@@ -198,7 +220,23 @@ export async function swap(opts) {
198
220
  else if (!valid)
199
221
  preflightErrors.push("wallet CNF credential exists but is not valid.");
200
222
  if (balance < totalDebit)
201
- preflightErrors.push(`insufficient ${symbol} balance: need ${totalDebit.toString()} wei including ILAL fee, have ${balance.toString()} wei.`);
223
+ preflightErrors.push(`insufficient ${symbol} balance: need ${tokenAmountWithWei(totalDebit, decimals, symbol)} including ILAL fee, have ${tokenAmountWithWei(balance, decimals, symbol)}.`);
224
+ log.section("Preflight Checks");
225
+ if (tokenId !== 0n && valid)
226
+ log.ok(`CNF credential token #${tokenId.toString()}`);
227
+ else
228
+ log.fail("CNF credential missing or invalid");
229
+ log.ok(`Issuer config (${hasEASPath ? "EAS" : "no EAS"}${hasZKPath ? " + ZK" : ""})`);
230
+ if (balance >= totalDebit)
231
+ log.ok(`Wallet balance ${tokenAmount(balance, decimals, symbol)}`);
232
+ else
233
+ log.fail(`Wallet balance ${tokenAmount(balance, decimals, symbol)}`);
234
+ log.ok(`Route bound to router ${fmt.addr(cfg.router)} and hook ${fmt.addr(cfg.hook)}`);
235
+ if (opts.explain) {
236
+ log.info("CNF proves this wallet is allowed to access the pool without revealing identity data.");
237
+ log.info("Caller binding means the signed authorization can only be used through the ILALRouter.");
238
+ }
239
+ log.line();
202
240
  if (preflightErrors.length > 0) {
203
241
  log.section("Preflight Failed");
204
242
  for (const error of preflightErrors)
@@ -210,7 +248,7 @@ export async function swap(opts) {
210
248
  }
211
249
  log.deal([
212
250
  { label: "verified input", value: `${opts.amountIn} ${symbol}`, note: "exact-in swap", tone: "cyan" },
213
- { label: "LP fee", value: poolFeePercent(parseInt(cfg.fee ?? "3000")), note: "hook-priced flow", tone: "green" },
251
+ { label: "LP fee", value: poolFeePercent(parseInt(cfg.fee ?? "3000")), note: parseInt(cfg.fee ?? "3000") === DYNAMIC_FEE_FLAG ? "6× cheaper than 0.30% standard" : "pool fee", tone: "green" },
214
252
  { label: "ILAL fee", value: protocolFeePips > 0 ? pipsToPercent(protocolFeePips) : "off", note: protocolFeePips > 0 ? "protocol revenue" : "legacy router", tone: protocolFeePips > 0 ? "cyan" : "gray" },
215
253
  ]);
216
254
  log.line();
@@ -286,10 +324,12 @@ export async function swap(opts) {
286
324
  log.kv("credential", `${fmt.badge("required", "cyan")} issuer ${fmt.addr(cfg.issuer)}`);
287
325
  log.kv("caller", `${fmt.badge("bound", "green")} ${fmt.addr(cfg.router)}`);
288
326
  log.kv("nonce", `${opts.hookData ? fmt.badge("external", "cyan") : fmt.badge("fresh", "green")} ${fmt.hash(sessionNonce)}`);
327
+ if (opts.explain)
328
+ log.kvdim("", "↳ unique one-time session ID; prevents replay attacks");
289
329
  log.kv("fee", feeLabel(fee));
290
330
  if (protocolFeePips > 0) {
291
331
  log.kv("protocol fee", `${fmt.badge("ILAL", "cyan")} ${pipsToPercent(protocolFeePips)} to ${treasury ? fmt.addr(treasury) : "treasury"}`);
292
- log.kv("total debit", `${totalDebit.toString()} wei (${symbol} input + ILAL fee)`);
332
+ log.kv("total debit", `${tokenAmountWithWei(totalDebit, decimals, symbol)} input + ILAL fee`);
293
333
  }
294
334
  log.line();
295
335
  if (opts.simulate) {
@@ -315,10 +355,10 @@ export async function swap(opts) {
315
355
  args: [cfg.router, totalDebit * 10n], // approve 10× for future swaps
316
356
  });
317
357
  await pubClient.waitForTransactionReceipt({ hash: approveHash });
318
- approveSpin.succeed(`Approved ${symbol} ${fmt.gray(fmt.hash(approveHash))}`);
358
+ approveSpin.succeed(`Approved ${tokenAmount(totalDebit * 10n, decimals, symbol)} ${fmt.gray(fmt.hash(approveHash))}`);
319
359
  }
320
360
  else {
321
- approveSpin.succeed(`Allowance ok (${fmt.gray(allowed.toString())} wei)`);
361
+ approveSpin.succeed(`Allowance: ${allowanceLabel(allowed, decimals, symbol)}`);
322
362
  }
323
363
  // Build PoolKey
324
364
  const poolKey = {
@@ -341,9 +381,11 @@ export async function swap(opts) {
341
381
  log.kv("min-amount-out", `${fmt.cyan(minAmountOut.toString())} wei (slippage protection on)`);
342
382
  }
343
383
  // Execute swap
344
- const txSpin = new Spinner("Sending swap tx…").start();
384
+ const txSpin = new Spinner("Submitting swap tx…").start();
345
385
  let txHash;
386
+ let receipt;
346
387
  try {
388
+ const startMs = Date.now();
347
389
  txHash = await walClient.writeContract({
348
390
  address: cfg.router,
349
391
  abi: ROUTER_ABI,
@@ -351,13 +393,21 @@ export async function swap(opts) {
351
393
  args: [poolKey, swapParams, minAmountOut, hookData],
352
394
  value: 0n,
353
395
  });
354
- txSpin.update(`Confirming ${fmt.gray(fmt.hash(txHash))}…`);
355
- const receipt = await pubClient.waitForTransactionReceipt({ hash: txHash });
396
+ txSpin.succeed(`Submitted to mempool ${fmt.gray(fmt.hash(txHash))}`);
397
+ const confirmSpin = new Spinner(`Confirming ${fmt.gray(fmt.hash(txHash))}…`).start();
398
+ receipt = await pubClient.waitForTransactionReceipt({ hash: txHash });
356
399
  if (receipt.status !== "success") {
357
- txSpin.fail("Transaction reverted");
400
+ confirmSpin.fail("Transaction reverted");
358
401
  die(`Tx failed: ${txHash}`);
359
402
  }
360
- txSpin.succeed(fmt.bold(fmt.green(`Swap executed via ILAL channel ✓`)));
403
+ confirmSpin.succeed(`Confirmed in block ${receipt.blockNumber.toString()}`);
404
+ const effectiveGasPrice = receipt.effectiveGasPrice;
405
+ log.metrics([
406
+ { label: "finality", value: secondsSince(startMs), tone: "green" },
407
+ { label: "gas used", value: receipt.gasUsed.toString(), tone: "cyan" },
408
+ ...(effectiveGasPrice ? [{ label: "gas cost", value: `${trimDecimals(formatEther(receipt.gasUsed * effectiveGasPrice), 8)} ETH`, tone: "cyan" }] : []),
409
+ ]);
410
+ log.ok(fmt.bold(fmt.green("Swap executed via ILAL channel")));
361
411
  }
362
412
  catch (e) {
363
413
  txSpin.fail("Swap failed");
@@ -366,7 +416,7 @@ export async function swap(opts) {
366
416
  log.line();
367
417
  log.callout("Hook-enforced swap", "credential, session, caller binding, and nonce all passed on-chain", "green");
368
418
  log.kv("tx", fmt.gray(txHash));
369
- log.kv("block", fmt.gray((await pubClient.getTransactionReceipt({ hash: txHash })).blockNumber.toString()));
419
+ log.kv("block", fmt.gray((receipt ?? await pubClient.getTransactionReceipt({ hash: txHash })).blockNumber.toString()));
370
420
  const explorer = txUrl(chain, txHash);
371
421
  if (explorer)
372
422
  log.kv("explorer", fmt.cyan(explorer));
package/dist/index.js CHANGED
@@ -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.6")
22
+ .version("0.2.8")
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
@@ -286,6 +286,7 @@ program
286
286
  .option("-k, --private-key <hex>", "Private key (or set PRIVATE_KEY env var)")
287
287
  .option("--ttl <seconds>", "Session token lifetime in seconds", "600")
288
288
  .option("--hook-data <hex>", "Use externally signed one-time hookData instead of signing inside swap")
289
+ .option("--explain", "Show inline explanations for gate checks and session fields", false)
289
290
  .option("--simulate", "Sign session without sending tx", false)
290
291
  .action(async (opts) => {
291
292
  await swap(opts).catch(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilalv3/cli",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "ILAL Protocol CLI — compliant swaps and credential management for Uniswap v4",
5
5
  "type": "module",
6
6
  "bin": {