@ilalv3/cli 0.1.2 → 0.2.1

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
@@ -25,11 +25,12 @@ ilal init
25
25
  # 2. Check credential + pool status
26
26
  ilal status
27
27
 
28
- # 3. Mint a CNF via ZK proof (adds your wallet to the Merkle tree)
29
- PRIVATE_KEY=0x... ilal credential prove --wallet 0xYourWallet --update-root
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
30
31
 
31
32
  # 4. Execute a compliant swap
32
- PRIVATE_KEY=0x... ilal swap --amount-in 0.001 --token-in 0x60Fa08963dD59724a188A37C7239fA89F97DB17D
33
+ PRIVATE_KEY=0x... ilal swap --amount-in 0.001 --token-in 0x2E0dEd1CF4ec6106079df4eF1200959c2a454f3A --min-amount-out 0
33
34
  ```
34
35
 
35
36
  ## Getting a CNF credential
@@ -49,10 +50,10 @@ PRIVATE_KEY=0x... ilal swap --amount-in 0.001 --token-in 0x60Fa08963dD59724a188A
49
50
  ### Path B — ZK proof (privacy-preserving)
50
51
 
51
52
  ```bash
52
- PRIVATE_KEY=0x... ilal credential prove --wallet 0xYourWallet --update-root
53
+ PRIVATE_KEY=0x... ilal credential prove --wallet 0xYourWallet
53
54
  ```
54
55
 
55
- Generates a Groth16 proof locally (~5s), verifies it on-chain, and mints/renews your CNF without revealing identity.
56
+ Generates a Groth16 proof locally (~5s), verifies it on-chain, and mints/renews your CNF without revealing identity. If the Merkle root does not match, the issuer/operator must queue the updated root with `ilal oracle propose-root --root <newRoot>` and activate it after the timelock.
56
57
 
57
58
  ## Command reference
58
59
 
@@ -60,14 +61,18 @@ Generates a Groth16 proof locally (~5s), verifies it on-chain, and mints/renews
60
61
  |---|---|
61
62
  | `ilal init` | Create `.ilal.json` with contract addresses |
62
63
  | `ilal status` | Dashboard: credential · issuer config · pool policy |
63
- | `ilal credential prove` | ZK proof → mint or renew CNF (all-in-one) |
64
+ | `ilal credential prove` | Trader flow: local ZK proof → mint or renew CNF |
64
65
  | `ilal credential mint` | Mint CNF via Coinbase EAS attestation |
65
66
  | `ilal credential renew` | Renew CNF via EAS attestation |
66
- | `ilal swap` | Compliant swap via ILALRouter |
67
+ | `ilal swap` | Compliant swap via ILALRouter with optional `--min-amount-out` |
67
68
  | `ilal pool add-liquidity` | Add liquidity to a compliant pool |
68
69
  | `ilal pool remove-liquidity` | Remove liquidity from a compliant pool |
69
70
  | `ilal pool policy set` | Register compliance policy for a pool |
70
71
  | `ilal pool policy get` | Read pool compliance policy |
72
+ | `ilal oracle propose-root` | Operator flow: queue a new Merkle root behind the 48h timelock |
73
+ | `ilal oracle activate-root` | Operator flow: activate the pending Merkle root after timelock |
74
+ | `ilal oracle propose-verifier` | Operator flow: queue a new ZK verifier behind the 72h timelock |
75
+ | `ilal oracle activate-verifier` | Operator flow: activate the pending ZK verifier after timelock |
71
76
  | `ilal session sign` | Sign a standalone SessionToken |
72
77
  | `ilal proof mint` | Mint CNF from existing proof.json + public.json |
73
78
  | `ilal deploy` | Deploy full ILAL contract stack |
@@ -78,10 +83,10 @@ The CLI reads `.ilal.json` in the current directory. Run `ilal init` to create i
78
83
 
79
84
  ```bash
80
85
  ilal swap \
81
- --router 0x35fE5eE12C102e78f5AbfD24cfe803Ad5824ca7F \
82
- --hook 0x6a1e3d7441fE8610fB5e2d2066912326457e8A80 \
83
- --issuer 0x319c0F1cb46c85B42E051251c4db04BA6BD265a2 \
84
- --pool-id 0xab4f3b0242cd9c33e6564b8a63d21eec62b570e7df9e5ce01e88d26b8223fb59 \
86
+ --router 0xd0aF4D1EFF36CB2a1E88017eA398dCaDe1Ac0040 \
87
+ --hook 0x6C57b50Ef9286b132066012B19b291FB120ACa80 \
88
+ --issuer 0xB13AE2498Df62A85768a4b783109C05fCf5A264a \
89
+ --pool-id 0x16b3e7a5c52216925f705673b3ab25db5e6025da530cf53b3bcb5affeb18d95f \
85
90
  --amount-in 0.001
86
91
  ```
87
92
 
@@ -89,10 +94,10 @@ ilal swap \
89
94
 
90
95
  | Contract | Address |
91
96
  |---|---|
92
- | CNFIssuer | `0x319c0F1cb46c85B42E051251c4db04BA6BD265a2` |
93
- | ComplianceHook | `0x6a1e3d7441fE8610fB5e2d2066912326457e8A80` |
94
- | ILALRouter | `0x35fE5eE12C102e78f5AbfD24cfe803Ad5824ca7F` |
95
- | PolicyRegistry | `0x72A425672c1D0FA95C75F5073e6DAf72194A1E0F` |
97
+ | CNFIssuer | `0xB13AE2498Df62A85768a4b783109C05fCf5A264a` |
98
+ | ComplianceHook | `0x6C57b50Ef9286b132066012B19b291FB120ACa80` |
99
+ | ILALRouter | `0xd0aF4D1EFF36CB2a1E88017eA398dCaDe1Ac0040` |
100
+ | PolicyRegistry | `0x19fD4eCF4359fCc8d5E79916691a28c24A22a9B4` |
96
101
 
97
102
  ## License
98
103
 
@@ -46,8 +46,10 @@ export async function credentialStatus(opts) {
46
46
  console.log(` ${fmt.gray("3.")} Run: ${fmt.cyan("ilal credential mint --attestation <uid>")}`);
47
47
  console.log();
48
48
  console.log(fmt.bold(" Path B — ZK proof (privacy-preserving, no KYC data on-chain)"));
49
- console.log(` ${fmt.gray("1.")} Operator adds wallet to the attestation Merkle tree`);
50
- console.log(` ${fmt.gray("2.")} Run: ${fmt.cyan("ilal credential prove --wallet " + opts.wallet + " --update-root")}`);
49
+ console.log(` ${fmt.gray("1.")} Issuer/operator adds wallet to the Merkle tree`);
50
+ console.log(` ${fmt.gray("2.")} Operator queues root: ${fmt.cyan("ilal oracle propose-root --root <newMerkleRoot>")}`);
51
+ console.log(` ${fmt.gray("3.")} After timelock, operator activates: ${fmt.cyan("ilal oracle activate-root")}`);
52
+ console.log(` ${fmt.gray("4.")} Trader runs: ${fmt.cyan("ilal credential prove --wallet " + opts.wallet)}`);
51
53
  console.log();
52
54
  return;
53
55
  }
@@ -9,11 +9,11 @@ const POOL_MANAGER = {
9
9
  "8453": "0x498581fF718922c3f8e6A244956aF099B2652b2b",
10
10
  };
11
11
  const SAMPLE = {
12
- wallet: "0x1b869CaC69Df23Ad9D727932496AEb3605538c8D",
13
- issuer: "0x319c0F1cb46c85B42E051251c4db04BA6BD265a2",
14
- hook: "0xdFF2ebBAc963f5Ed0B0EBCf021aB5EA16d57ea94",
15
- router: "0x4A1F7E7d9D2D1f2A0c4A2F4A8C1A0B3E9E5d1111",
16
- pool: "0x7ef1c0ffee00000000000000000000000000000000000000000000000000bEEF",
12
+ wallet: "0xF40493ACDd33cC4a841fCD69577A66218381C2fC",
13
+ issuer: "0xB13AE2498Df62A85768a4b783109C05fCf5A264a",
14
+ hook: "0x6C57b50Ef9286b132066012B19b291FB120ACa80",
15
+ router: "0xd0aF4D1EFF36CB2a1E88017eA398dCaDe1Ac0040",
16
+ pool: "0x16b3e7a5c52216925f705673b3ab25db5e6025da530cf53b3bcb5affeb18d95f",
17
17
  proof: "0x91f2b8a0c43e902f7f1a8c0d",
18
18
  session: "0x6b84eac5e0db21f8d5d43b7a",
19
19
  };
@@ -92,9 +92,9 @@ export async function demo(opts) {
92
92
  console.log();
93
93
  log.section("Live Demo Commands");
94
94
  log.command("ilal status --wallet <wallet>");
95
- log.command("ilal credential prove --wallet <wallet> --update-root");
95
+ log.command("ilal credential prove --wallet <wallet>");
96
96
  log.command("ilal session sign --pool <poolId> --action swap --hook <hook> --issuer <issuer> --caller <router>");
97
- log.command("ilal swap --amount-in 100 --token-in <token> --pool-id <poolId>");
97
+ log.command("ilal swap --amount-in 100 --token-in <token> --pool-id <poolId> --min-amount-out 0");
98
98
  }
99
99
  console.log();
100
100
  }
@@ -10,9 +10,16 @@ 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: "0x319c0F1cb46c85B42E051251c4db04BA6BD265a2",
14
- hook: "0xdFF2ebBAc963f5Ed0B0EBCf021aB5EA16d57ea94",
15
- registry: "0x72A425672c1D0FA95C75F5073e6DAf72194A1E0F",
13
+ issuer: "0xB13AE2498Df62A85768a4b783109C05fCf5A264a",
14
+ hook: "0x6C57b50Ef9286b132066012B19b291FB120ACa80",
15
+ registry: "0x19fD4eCF4359fCc8d5E79916691a28c24A22a9B4",
16
+ router: "0xd0aF4D1EFF36CB2a1E88017eA398dCaDe1Ac0040",
17
+ treasury: "0xF40493ACDd33cC4a841fCD69577A66218381C2fC",
18
+ tokenA: "0x2E0dEd1CF4ec6106079df4eF1200959c2a454f3A",
19
+ tokenB: "0x6dFCC8c373fBC3ecdda0F2b27590f12EeE9fF204",
20
+ poolId: "0x16b3e7a5c52216925f705673b3ab25db5e6025da530cf53b3bcb5affeb18d95f",
21
+ fee: "8388608",
22
+ tickSpacing: "60",
16
23
  rpc: "https://sepolia.base.org",
17
24
  },
18
25
  "8453": {
@@ -35,13 +42,13 @@ export async function init(opts) {
35
42
  issuer: opts.issuer ?? preset["issuer"],
36
43
  hook: opts.hook ?? preset["hook"],
37
44
  registry: opts.registry ?? preset["registry"],
38
- router: opts.router,
39
- treasury: opts.treasury,
40
- tokenA: opts.tokenA,
41
- tokenB: opts.tokenB,
42
- poolId: opts.poolId,
43
- fee: opts.fee,
44
- tickSpacing: opts.tickSpacing,
45
+ router: opts.router ?? preset["router"],
46
+ treasury: opts.treasury ?? preset["treasury"],
47
+ tokenA: opts.tokenA ?? preset["tokenA"],
48
+ tokenB: opts.tokenB ?? preset["tokenB"],
49
+ poolId: opts.poolId ?? preset["poolId"],
50
+ fee: opts.fee ?? preset["fee"],
51
+ tickSpacing: opts.tickSpacing ?? preset["tickSpacing"],
45
52
  rpc: opts.rpc ?? preset["rpc"],
46
53
  ...(opts.circuitDir ? { circuitDir: opts.circuitDir } : {}),
47
54
  };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * oracle.ts — `ilal oracle`
3
+ *
4
+ * Operator-only commands for managing the CNFIssuer Merkle root and
5
+ * ZK verifier via the timelock mechanism.
6
+ *
7
+ * Merkle root and ZK verifier changes are protected by a 2-step propose → activate
8
+ * timelock (ROOT_DELAY = 48 h, VERIFIER_DELAY = 72 h). Only the contract
9
+ * owner can call these.
10
+ *
11
+ * Usage:
12
+ * # Step 1 — queue a new root (requires owner key, executes immediately)
13
+ * PRIVATE_KEY=0x... ilal oracle propose-root \
14
+ * --root 0xDEADBEEF... \
15
+ * --issuer 0xB13AE2...
16
+ *
17
+ * # Step 2 — after ROOT_DELAY (48 h) has elapsed, activate it
18
+ * PRIVATE_KEY=0x... ilal oracle activate-root \
19
+ * --issuer 0xB13AE2...
20
+ *
21
+ * # Same pattern for the ZK verifier (VERIFIER_DELAY = 72 h)
22
+ * PRIVATE_KEY=0x... ilal oracle propose-verifier --verifier 0x... --issuer 0x...
23
+ * PRIVATE_KEY=0x... ilal oracle activate-verifier --issuer 0x...
24
+ */
25
+ export declare function oracleProposeRoot(opts: {
26
+ root?: string;
27
+ issuer?: string;
28
+ chain?: string;
29
+ rpc?: string;
30
+ privateKey?: string;
31
+ }): Promise<void>;
32
+ export declare function oracleActivateRoot(opts: {
33
+ issuer?: string;
34
+ chain?: string;
35
+ rpc?: string;
36
+ privateKey?: string;
37
+ }): Promise<void>;
38
+ export declare function oracleProposeVerifier(opts: {
39
+ verifier?: string;
40
+ issuer?: string;
41
+ chain?: string;
42
+ rpc?: string;
43
+ privateKey?: string;
44
+ }): Promise<void>;
45
+ export declare function oracleActivateVerifier(opts: {
46
+ issuer?: string;
47
+ chain?: string;
48
+ rpc?: string;
49
+ privateKey?: string;
50
+ }): Promise<void>;
@@ -0,0 +1,281 @@
1
+ /**
2
+ * oracle.ts — `ilal oracle`
3
+ *
4
+ * Operator-only commands for managing the CNFIssuer Merkle root and
5
+ * ZK verifier via the timelock mechanism.
6
+ *
7
+ * Merkle root and ZK verifier changes are protected by a 2-step propose → activate
8
+ * timelock (ROOT_DELAY = 48 h, VERIFIER_DELAY = 72 h). Only the contract
9
+ * owner can call these.
10
+ *
11
+ * Usage:
12
+ * # Step 1 — queue a new root (requires owner key, executes immediately)
13
+ * PRIVATE_KEY=0x... ilal oracle propose-root \
14
+ * --root 0xDEADBEEF... \
15
+ * --issuer 0xB13AE2...
16
+ *
17
+ * # Step 2 — after ROOT_DELAY (48 h) has elapsed, activate it
18
+ * PRIVATE_KEY=0x... ilal oracle activate-root \
19
+ * --issuer 0xB13AE2...
20
+ *
21
+ * # Same pattern for the ZK verifier (VERIFIER_DELAY = 72 h)
22
+ * PRIVATE_KEY=0x... ilal oracle propose-verifier --verifier 0x... --issuer 0x...
23
+ * PRIVATE_KEY=0x... ilal oracle activate-verifier --issuer 0x...
24
+ */
25
+ import { createPublicClient, createWalletClient, http, isAddress, } from "viem";
26
+ import { privateKeyToAccount } from "viem/accounts";
27
+ import { base, baseSepolia } from "viem/chains";
28
+ import { fmt, log, header, Spinner, die, dieOnContract } from "../ui.js";
29
+ import { withConfig } from "../config.js";
30
+ const CHAINS = { "8453": base, "84532": baseSepolia };
31
+ const ORACLE_ABI = [
32
+ {
33
+ name: "proposeMerkleRoot", type: "function", stateMutability: "nonpayable",
34
+ inputs: [{ name: "_root", type: "uint256" }], outputs: [],
35
+ },
36
+ {
37
+ name: "activateMerkleRoot", type: "function", stateMutability: "nonpayable",
38
+ inputs: [], outputs: [],
39
+ },
40
+ {
41
+ name: "proposeZKVerifier", type: "function", stateMutability: "nonpayable",
42
+ inputs: [{ name: "_verifier", type: "address" }], outputs: [],
43
+ },
44
+ {
45
+ name: "activateZKVerifier", type: "function", stateMutability: "nonpayable",
46
+ inputs: [], outputs: [],
47
+ },
48
+ {
49
+ name: "pendingRoot", type: "function", stateMutability: "view",
50
+ inputs: [], outputs: [{ type: "uint256" }],
51
+ },
52
+ {
53
+ name: "pendingRootActivatesAt", type: "function", stateMutability: "view",
54
+ inputs: [], outputs: [{ type: "uint256" }],
55
+ },
56
+ {
57
+ name: "pendingZKVerifier", type: "function", stateMutability: "view",
58
+ inputs: [], outputs: [{ type: "address" }],
59
+ },
60
+ {
61
+ name: "pendingVerifierActivatesAt", type: "function", stateMutability: "view",
62
+ inputs: [], outputs: [{ type: "uint256" }],
63
+ },
64
+ {
65
+ name: "ROOT_DELAY", type: "function", stateMutability: "view",
66
+ inputs: [], outputs: [{ type: "uint256" }],
67
+ },
68
+ {
69
+ name: "VERIFIER_DELAY", type: "function", stateMutability: "view",
70
+ inputs: [], outputs: [{ type: "uint256" }],
71
+ },
72
+ {
73
+ name: "merkleRoot", type: "function", stateMutability: "view",
74
+ inputs: [], outputs: [{ type: "uint256" }],
75
+ },
76
+ ];
77
+ function txUrl(chain, hash) {
78
+ const baseUrl = chain.blockExplorers?.default?.url;
79
+ return baseUrl ? `${baseUrl}/tx/${hash}` : undefined;
80
+ }
81
+ function makeClients(cfg, opts) {
82
+ const rawKey = opts.privateKey ?? process.env["PRIVATE_KEY"];
83
+ if (!rawKey)
84
+ die("Private key required. Use --private-key or set PRIVATE_KEY env var.");
85
+ const chain = CHAINS[opts.chain ?? "84532"] ?? baseSepolia;
86
+ const transport = opts.rpc ? http(opts.rpc) : http();
87
+ const account = privateKeyToAccount(rawKey);
88
+ return {
89
+ chain,
90
+ account,
91
+ pubClient: createPublicClient({ chain, transport }),
92
+ walClient: createWalletClient({ account, chain, transport }),
93
+ };
94
+ }
95
+ // ─── propose-root ─────────────────────────────────────────────────────────────
96
+ export async function oracleProposeRoot(opts) {
97
+ const cfg = withConfig(opts);
98
+ if (!cfg.issuer)
99
+ die("CNFIssuer address required. Use --issuer or set in .ilal.json");
100
+ if (!isAddress(cfg.issuer))
101
+ die(`Invalid issuer address: ${cfg.issuer}`);
102
+ if (!opts.root)
103
+ die("--root <uint256> required");
104
+ const root = BigInt(opts.root);
105
+ const { chain, account, pubClient, walClient } = makeClients(cfg, opts);
106
+ header("ILAL Oracle — Propose Merkle Root", chain.name);
107
+ log.kv("issuer", fmt.cyan(cfg.issuer));
108
+ log.kv("new root", fmt.gray(root.toString().slice(0, 22) + "…"));
109
+ // Show current root and delay
110
+ const [currentRoot, delay] = await Promise.all([
111
+ pubClient.readContract({ address: cfg.issuer, abi: ORACLE_ABI, functionName: "merkleRoot" }),
112
+ pubClient.readContract({ address: cfg.issuer, abi: ORACLE_ABI, functionName: "ROOT_DELAY" }),
113
+ ]);
114
+ log.kv("current root", fmt.gray(currentRoot.toString().slice(0, 22) + "…"));
115
+ log.kv("timelock delay", `${Number(delay) / 3600} hours`);
116
+ log.line();
117
+ const spin = new Spinner("Proposing new Merkle root…").start();
118
+ let txHash;
119
+ try {
120
+ txHash = await walClient.writeContract({
121
+ account,
122
+ address: cfg.issuer,
123
+ abi: ORACLE_ABI,
124
+ functionName: "proposeMerkleRoot",
125
+ args: [root],
126
+ });
127
+ await pubClient.waitForTransactionReceipt({ hash: txHash });
128
+ spin.succeed(`Merkle root proposed ${fmt.gray(fmt.hash(txHash))}`);
129
+ }
130
+ catch (e) {
131
+ spin.fail("proposeMerkleRoot failed");
132
+ dieOnContract(e);
133
+ return;
134
+ }
135
+ const activatesAt = new Date((Date.now() + Number(delay) * 1000));
136
+ log.line();
137
+ log.callout("Root queued", `Activate after ${activatesAt.toISOString()}\n Run: ilal oracle activate-root --issuer ${cfg.issuer}`, "cyan");
138
+ const explorer = txUrl(chain, txHash);
139
+ if (explorer)
140
+ log.kv("explorer", fmt.cyan(explorer));
141
+ console.log();
142
+ }
143
+ // ─── activate-root ────────────────────────────────────────────────────────────
144
+ export async function oracleActivateRoot(opts) {
145
+ const cfg = withConfig(opts);
146
+ if (!cfg.issuer)
147
+ die("CNFIssuer address required. Use --issuer or set in .ilal.json");
148
+ if (!isAddress(cfg.issuer))
149
+ die(`Invalid issuer address: ${cfg.issuer}`);
150
+ const { chain, account, pubClient, walClient } = makeClients(cfg, opts);
151
+ header("ILAL Oracle — Activate Merkle Root", chain.name);
152
+ log.kv("issuer", fmt.cyan(cfg.issuer));
153
+ const [pendingRoot, activatesAt] = await Promise.all([
154
+ pubClient.readContract({ address: cfg.issuer, abi: ORACLE_ABI, functionName: "pendingRoot" }),
155
+ pubClient.readContract({ address: cfg.issuer, abi: ORACLE_ABI, functionName: "pendingRootActivatesAt" }),
156
+ ]);
157
+ if (pendingRoot === 0n)
158
+ die("No pending root — run `ilal oracle propose-root` first");
159
+ const now = BigInt(Math.floor(Date.now() / 1000));
160
+ if (activatesAt > now) {
161
+ const remaining = Number(activatesAt - now);
162
+ die(`Timelock not elapsed. Activate in ${Math.ceil(remaining / 3600)} hour(s) (${new Date(Number(activatesAt) * 1000).toISOString()})`);
163
+ }
164
+ log.kv("pending root", fmt.cyan(pendingRoot.toString().slice(0, 22) + "…"));
165
+ log.line();
166
+ const spin = new Spinner("Activating Merkle root…").start();
167
+ let txHash;
168
+ try {
169
+ txHash = await walClient.writeContract({
170
+ account,
171
+ address: cfg.issuer,
172
+ abi: ORACLE_ABI,
173
+ functionName: "activateMerkleRoot",
174
+ args: [],
175
+ });
176
+ await pubClient.waitForTransactionReceipt({ hash: txHash });
177
+ spin.succeed(`Merkle root activated ${fmt.gray(fmt.hash(txHash))}`);
178
+ }
179
+ catch (e) {
180
+ spin.fail("activateMerkleRoot failed");
181
+ dieOnContract(e);
182
+ return;
183
+ }
184
+ log.line();
185
+ log.callout("Root live", "New Merkle root is now enforced on all ZK proof verifications", "green");
186
+ const explorer = txUrl(chain, txHash);
187
+ if (explorer)
188
+ log.kv("explorer", fmt.cyan(explorer));
189
+ console.log();
190
+ }
191
+ // ─── propose-verifier ─────────────────────────────────────────────────────────
192
+ export async function oracleProposeVerifier(opts) {
193
+ const cfg = withConfig(opts);
194
+ if (!cfg.issuer)
195
+ die("CNFIssuer address required. Use --issuer or set in .ilal.json");
196
+ if (!isAddress(cfg.issuer))
197
+ die(`Invalid issuer address: ${cfg.issuer}`);
198
+ if (!opts.verifier || !isAddress(opts.verifier))
199
+ die("--verifier <address> required");
200
+ const { chain, account, pubClient, walClient } = makeClients(cfg, opts);
201
+ header("ILAL Oracle — Propose ZK Verifier", chain.name);
202
+ log.kv("issuer", fmt.cyan(cfg.issuer));
203
+ log.kv("new verifier", fmt.cyan(opts.verifier));
204
+ const delay = await pubClient.readContract({
205
+ address: cfg.issuer, abi: ORACLE_ABI, functionName: "VERIFIER_DELAY",
206
+ });
207
+ log.kv("timelock delay", `${Number(delay) / 3600} hours`);
208
+ log.line();
209
+ const spin = new Spinner("Proposing new ZK verifier…").start();
210
+ let txHash;
211
+ try {
212
+ txHash = await walClient.writeContract({
213
+ account,
214
+ address: cfg.issuer,
215
+ abi: ORACLE_ABI,
216
+ functionName: "proposeZKVerifier",
217
+ args: [opts.verifier],
218
+ });
219
+ await pubClient.waitForTransactionReceipt({ hash: txHash });
220
+ spin.succeed(`ZK verifier proposed ${fmt.gray(fmt.hash(txHash))}`);
221
+ }
222
+ catch (e) {
223
+ spin.fail("proposeZKVerifier failed");
224
+ dieOnContract(e);
225
+ return;
226
+ }
227
+ const activatesAt = new Date((Date.now() + Number(delay) * 1000));
228
+ log.line();
229
+ log.callout("Verifier queued", `Activate after ${activatesAt.toISOString()}\n Run: ilal oracle activate-verifier --issuer ${cfg.issuer}`, "cyan");
230
+ const explorer = txUrl(chain, txHash);
231
+ if (explorer)
232
+ log.kv("explorer", fmt.cyan(explorer));
233
+ console.log();
234
+ }
235
+ // ─── activate-verifier ────────────────────────────────────────────────────────
236
+ export async function oracleActivateVerifier(opts) {
237
+ const cfg = withConfig(opts);
238
+ if (!cfg.issuer)
239
+ die("CNFIssuer address required. Use --issuer or set in .ilal.json");
240
+ if (!isAddress(cfg.issuer))
241
+ die(`Invalid issuer address: ${cfg.issuer}`);
242
+ const { chain, account, pubClient, walClient } = makeClients(cfg, opts);
243
+ header("ILAL Oracle — Activate ZK Verifier", chain.name);
244
+ const [pending, activatesAt] = await Promise.all([
245
+ pubClient.readContract({ address: cfg.issuer, abi: ORACLE_ABI, functionName: "pendingZKVerifier" }),
246
+ pubClient.readContract({ address: cfg.issuer, abi: ORACLE_ABI, functionName: "pendingVerifierActivatesAt" }),
247
+ ]);
248
+ if (pending === "0x0000000000000000000000000000000000000000")
249
+ die("No pending verifier — run `ilal oracle propose-verifier` first");
250
+ const now = BigInt(Math.floor(Date.now() / 1000));
251
+ if (activatesAt > now) {
252
+ const remaining = Number(activatesAt - now);
253
+ die(`Timelock not elapsed. Activate in ${Math.ceil(remaining / 3600)} hour(s)`);
254
+ }
255
+ log.kv("pending verifier", fmt.cyan(pending));
256
+ log.line();
257
+ const spin = new Spinner("Activating ZK verifier…").start();
258
+ let txHash;
259
+ try {
260
+ txHash = await walClient.writeContract({
261
+ account,
262
+ address: cfg.issuer,
263
+ abi: ORACLE_ABI,
264
+ functionName: "activateZKVerifier",
265
+ args: [],
266
+ });
267
+ await pubClient.waitForTransactionReceipt({ hash: txHash });
268
+ spin.succeed(`ZK verifier activated ${fmt.gray(fmt.hash(txHash))}`);
269
+ }
270
+ catch (e) {
271
+ spin.fail("activateZKVerifier failed");
272
+ dieOnContract(e);
273
+ return;
274
+ }
275
+ log.line();
276
+ log.callout("Verifier live", "New ZK verifier is now active for all proof verifications", "green");
277
+ const explorer = txUrl(chain, txHash);
278
+ if (explorer)
279
+ log.kv("explorer", fmt.cyan(explorer));
280
+ console.log();
281
+ }
@@ -1,24 +1,25 @@
1
1
  /**
2
2
  * prove.ts — `ilal credential prove`
3
3
  *
4
- * All-in-one command: builds Merkle tree, generates Groth16 ZK proof,
5
- * (optionally) updates the on-chain merkleRoot, then mints or renews the CNF.
4
+ * Trader command: builds a local Merkle proof, generates a Groth16 ZK proof,
5
+ * then mints or renews the CNF if the issuer's active root already includes it.
6
6
  *
7
7
  * Usage:
8
8
  * ilal credential prove \
9
9
  * --wallet 0x1b869... \
10
- * --issuer 0x319c0... \
10
+ * --issuer 0xB13AE2... \
11
11
  * --chain 84532 \
12
12
  * --action mint # or renew (default: auto-detect)
13
- * --update-root # call setMerkleRoot before minting
14
13
  * --circuit-dir ./circuits/build
14
+ *
15
+ * Operator root changes live under `ilal oracle propose-root` and
16
+ * `ilal oracle activate-root`.
15
17
  */
16
18
  export declare function credentialProve(opts: {
17
19
  wallet?: string;
18
20
  issuer?: string;
19
21
  chain?: string;
20
22
  action?: string;
21
- updateRoot: boolean;
22
23
  circuitDir?: string;
23
24
  outDir?: string;
24
25
  rpc?: string;
@@ -1,17 +1,19 @@
1
1
  /**
2
2
  * prove.ts — `ilal credential prove`
3
3
  *
4
- * All-in-one command: builds Merkle tree, generates Groth16 ZK proof,
5
- * (optionally) updates the on-chain merkleRoot, then mints or renews the CNF.
4
+ * Trader command: builds a local Merkle proof, generates a Groth16 ZK proof,
5
+ * then mints or renews the CNF if the issuer's active root already includes it.
6
6
  *
7
7
  * Usage:
8
8
  * ilal credential prove \
9
9
  * --wallet 0x1b869... \
10
- * --issuer 0x319c0... \
10
+ * --issuer 0xB13AE2... \
11
11
  * --chain 84532 \
12
12
  * --action mint # or renew (default: auto-detect)
13
- * --update-root # call setMerkleRoot before minting
14
13
  * --circuit-dir ./circuits/build
14
+ *
15
+ * Operator root changes live under `ilal oracle propose-root` and
16
+ * `ilal oracle activate-root`.
15
17
  */
16
18
  import { execSync } from "child_process";
17
19
  import { mkdirSync, writeFileSync, readFileSync } from "fs";
@@ -42,11 +44,6 @@ const CNF_ABI = [
42
44
  inputs: [{ name: "proof", type: "bytes" }, { name: "publicInputs", type: "uint256[]" }],
43
45
  outputs: [],
44
46
  },
45
- {
46
- name: "setMerkleRoot", type: "function", stateMutability: "nonpayable",
47
- inputs: [{ name: "_root", type: "uint256" }],
48
- outputs: [],
49
- },
50
47
  {
51
48
  name: "isValid", type: "function", stateMutability: "view",
52
49
  inputs: [{ name: "wallet", type: "address" }],
@@ -236,24 +233,6 @@ export async function credentialProve(opts) {
236
233
  log.kv("expiresAt", fmt.cyan(new Date((Date.now() + 90 * 24 * 3600 * 1000)).toISOString().split("T")[0]));
237
234
  log.kv("merkleRoot", fmt.gray(proofResult.merkleRoot.toString().slice(0, 22) + "…"));
238
235
  log.line();
239
- // ── Update merkle root on-chain ────────────────────────────────────────────
240
- if (opts.updateRoot) {
241
- const rootSpin = new Spinner("Updating merkleRoot on-chain…").start();
242
- try {
243
- const rootHash = await walClient.writeContract({
244
- address: cfg.issuer,
245
- abi: CNF_ABI,
246
- functionName: "setMerkleRoot",
247
- args: [proofResult.merkleRoot],
248
- });
249
- await pubClient.waitForTransactionReceipt({ hash: rootHash });
250
- rootSpin.succeed(`merkleRoot updated ${fmt.gray(fmt.hash(rootHash))}`);
251
- }
252
- catch (e) {
253
- rootSpin.fail("setMerkleRoot failed");
254
- dieOnContract(e);
255
- }
256
- }
257
236
  // ── Send mint / renew tx ───────────────────────────────────────────────────
258
237
  const { proofBytes, publicInputs } = encodeProof(proofResult.proofJson, proofResult.publicJson);
259
238
  const fnName = action === "mint" ? "mintWithProof" : "renewWithProof";
@@ -55,7 +55,8 @@ export async function status(opts) {
55
55
  log.kv("issuer", fmt.cyan(cfg.issuer));
56
56
  if (tokenId === 0n) {
57
57
  log.kv("status", fmt.badge("missing", "red"));
58
- log.command("ilal credential prove --wallet " + cfg.wallet + " --update-root");
58
+ log.command("ilal credential prove --wallet " + cfg.wallet);
59
+ console.log(fmt.gray("If root mismatch occurs, ask the issuer to queue and activate the updated root via `ilal oracle`."));
59
60
  credentialReady = false;
60
61
  }
61
62
  else {
@@ -20,6 +20,7 @@
20
20
  */
21
21
  export declare function swap(opts: {
22
22
  amountIn: string;
23
+ minAmountOut?: string;
23
24
  tokenIn?: string;
24
25
  zeroForOne?: boolean;
25
26
  poolId?: string;
@@ -50,6 +50,7 @@ const ROUTER_ABI = [
50
50
  { name: "amountSpecified", type: "int256" },
51
51
  { name: "sqrtPriceLimitX96", type: "uint160" },
52
52
  ] },
53
+ { name: "minAmountOut", type: "uint256" },
53
54
  { name: "hookData", type: "bytes" },
54
55
  ],
55
56
  outputs: [{ name: "delta", type: "int256" }],
@@ -250,6 +251,13 @@ export async function swap(opts) {
250
251
  amountSpecified: -amountIn, // negative = exactIn
251
252
  sqrtPriceLimitX96: zeroForOne ? MIN_SQRT_PRICE : MAX_SQRT_PRICE,
252
253
  };
254
+ // Slippage: parse --min-amount-out if provided (0 = disabled)
255
+ // We don't know the tokenOut decimals here without another RPC call,
256
+ // so we accept wei (raw bigint) from the flag. The CLI documents this.
257
+ const minAmountOut = opts.minAmountOut ? BigInt(opts.minAmountOut) : 0n;
258
+ if (minAmountOut > 0n) {
259
+ log.kv("min-amount-out", `${fmt.cyan(minAmountOut.toString())} wei (slippage protection on)`);
260
+ }
253
261
  // Execute swap
254
262
  const txSpin = new Spinner("Sending swap tx…").start();
255
263
  let txHash;
@@ -258,7 +266,7 @@ export async function swap(opts) {
258
266
  address: cfg.router,
259
267
  abi: ROUTER_ABI,
260
268
  functionName: "swap",
261
- args: [poolKey, swapParams, hookData],
269
+ args: [poolKey, swapParams, minAmountOut, hookData],
262
270
  value: 0n,
263
271
  });
264
272
  txSpin.update(`Confirming ${fmt.gray(fmt.hash(txHash))}…`);
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { Command } from "commander";
3
3
  import { credentialStatus } from "./commands/credential.js";
4
4
  import { credentialProve } from "./commands/prove.js";
5
+ import { oracleProposeRoot, oracleActivateRoot, oracleProposeVerifier, oracleActivateVerifier } from "./commands/oracle.js";
5
6
  import { mintCredential, renewCredential } from "./commands/mint.js";
6
7
  import { proofMint, proofRenew } from "./commands/proof.js";
7
8
  import { sessionSign } from "./commands/session.js";
@@ -18,7 +19,7 @@ const program = new Command();
18
19
  program
19
20
  .name("ilal")
20
21
  .description("ILAL Protocol CLI — Uniswap v4 compliance hook toolkit")
21
- .version("0.1.0")
22
+ .version("0.2.1")
22
23
  .addHelpText("before", `\n ${fmt.bold(fmt.cyan("◆"))} ${fmt.bold("ILAL Protocol")} ${fmt.gray("Uniswap v4 Compliance Hook")}\n`);
23
24
  // ─── init ─────────────────────────────────────────────────────────────────────
24
25
  program
@@ -92,7 +93,6 @@ credential
92
93
  .option("-w, --wallet <address>", "Wallet address to prove eligibility for")
93
94
  .option("-i, --issuer <address>", "CNFIssuer contract address (or set in .ilal.json)")
94
95
  .option("-a, --action <action>", "mint or renew (default: auto-detect)")
95
- .option("--update-root", "Call setMerkleRoot on-chain before minting (requires owner key)", false)
96
96
  .option("--circuit-dir <path>", "Path to circuits/build directory (auto-detected by default)")
97
97
  .option("--out-dir <path>", "Directory to write proof/witness files")
98
98
  .option("-c, --chain <chainId>", "Chain ID (8453=Base, 84532=Base Sepolia)", "84532")
@@ -243,6 +243,7 @@ program
243
243
  .command("swap")
244
244
  .description("Execute a compliant token swap through the ILAL channel")
245
245
  .requiredOption("--amount-in <amount>", "Input amount in human-readable units (e.g. 100)")
246
+ .option("--min-amount-out <wei>", "Minimum output amount in wei — reverts if pool gives less (default: 0 = off)")
246
247
  .option("--token-in <address>", "Token to sell (defaults to tokenA from config)")
247
248
  .option("--token-a <address>", "currency0 token address (or set in .ilal.json)")
248
249
  .option("--token-b <address>", "currency1 token address (or set in .ilal.json)")
@@ -260,6 +261,55 @@ program
260
261
  .action(async (opts) => {
261
262
  await swap(opts).catch(err);
262
263
  });
264
+ // ─── oracle ───────────────────────────────────────────────────────────────────
265
+ // Operator-only commands — require owner key.
266
+ // Merkle root and ZK verifier changes are timelocked:
267
+ // ROOT_DELAY = 48h, VERIFIER_DELAY = 72h.
268
+ const oracle = program
269
+ .command("oracle")
270
+ .description("Operator commands for managing timelocked Merkle root and ZK verifier");
271
+ oracle
272
+ .command("propose-root")
273
+ .description("Queue a new Merkle root (step 1 of 2 — owner only, ROOT_DELAY = 48 h timelock)")
274
+ .requiredOption("--root <uint256>", "New Merkle root value (decimal or 0x hex)")
275
+ .option("-i, --issuer <address>", "CNFIssuer contract address (or set in .ilal.json)")
276
+ .option("-c, --chain <chainId>", "Chain ID (8453=Base, 84532=Base Sepolia)", "84532")
277
+ .option("-r, --rpc <url>", "Custom RPC URL")
278
+ .option("-k, --private-key <hex>", "Private key (or set PRIVATE_KEY env var)")
279
+ .action(async (opts) => {
280
+ await oracleProposeRoot(opts).catch(err);
281
+ });
282
+ oracle
283
+ .command("activate-root")
284
+ .description("Activate the pending Merkle root after the 48-hour timelock has elapsed (step 2 of 2)")
285
+ .option("-i, --issuer <address>", "CNFIssuer contract address (or set in .ilal.json)")
286
+ .option("-c, --chain <chainId>", "Chain ID (8453=Base, 84532=Base Sepolia)", "84532")
287
+ .option("-r, --rpc <url>", "Custom RPC URL")
288
+ .option("-k, --private-key <hex>", "Private key (or set PRIVATE_KEY env var)")
289
+ .action(async (opts) => {
290
+ await oracleActivateRoot(opts).catch(err);
291
+ });
292
+ oracle
293
+ .command("propose-verifier")
294
+ .description("Queue a new ZK verifier contract (step 1 of 2 — owner only, VERIFIER_DELAY = 72 h)")
295
+ .requiredOption("--verifier <address>", "New IGroth16Verifier contract address")
296
+ .option("-i, --issuer <address>", "CNFIssuer contract address (or set in .ilal.json)")
297
+ .option("-c, --chain <chainId>", "Chain ID", "84532")
298
+ .option("-r, --rpc <url>", "Custom RPC URL")
299
+ .option("-k, --private-key <hex>", "Private key (or set PRIVATE_KEY env var)")
300
+ .action(async (opts) => {
301
+ await oracleProposeVerifier(opts).catch(err);
302
+ });
303
+ oracle
304
+ .command("activate-verifier")
305
+ .description("Activate the pending ZK verifier after the 72-hour timelock has elapsed (step 2 of 2)")
306
+ .option("-i, --issuer <address>", "CNFIssuer contract address (or set in .ilal.json)")
307
+ .option("-c, --chain <chainId>", "Chain ID", "84532")
308
+ .option("-r, --rpc <url>", "Custom RPC URL")
309
+ .option("-k, --private-key <hex>", "Private key (or set PRIVATE_KEY env var)")
310
+ .action(async (opts) => {
311
+ await oracleActivateVerifier(opts).catch(err);
312
+ });
263
313
  // ─── deploy ───────────────────────────────────────────────────────────────────
264
314
  program
265
315
  .command("deploy")
package/dist/ui.js CHANGED
@@ -169,8 +169,8 @@ const CONTRACT_ERRORS = {
169
169
  "0x724efe91": "Credential already exists — use `ilal credential prove` to renew",
170
170
  "0xe6567cc4": "ZK verifier not set on CNFIssuer — contact the issuer admin",
171
171
  "0xd611c318": "ZK proof verification failed — regenerate your proof",
172
- "0x9dd854d3": "Invalid Merkle root — re-run with --update-root",
173
- "0x0432f01c": "Merkle root mismatch — re-run with --update-root",
172
+ "0x9dd854d3": "Invalid Merkle root — issuer must queue and activate the updated root with `ilal oracle`",
173
+ "0x0432f01c": "Merkle root mismatch — issuer must queue and activate the updated root with `ilal oracle`",
174
174
  "0xddefae28": "Credential already minted for this wallet",
175
175
  "0x30cd7471": "Not the contract owner",
176
176
  "0xf6f992e7": "Credential has expired — renew it",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilalv3/cli",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "ILAL Protocol CLI — compliant swaps and credential management for Uniswap v4",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,9 +23,9 @@
23
23
  ],
24
24
  "repository": {
25
25
  "type": "git",
26
- "url": "git+https://github.com/ilal-protocol/ilal.git"
26
+ "url": "git+https://github.com/rpnny/ilal-cli.git"
27
27
  },
28
- "homepage": "https://github.com/ilal-protocol/ilal#readme",
28
+ "homepage": "https://github.com/rpnny/ilal-cli#readme",
29
29
  "publishConfig": {
30
30
  "access": "public"
31
31
  },