@ilalv3/cli 0.2.0 → 0.2.2

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: "0x1b869CaC69Df23Ad9D727932496AEb3605538c8D",
13
- issuer: "0x319c0F1cb46c85B42E051251c4db04BA6BD265a2",
14
- hook: "0xdFF2ebBAc963f5Ed0B0EBCf021aB5EA16d57ea94",
15
- router: "0x4A1F7E7d9D2D1f2A0c4A2F4A8C1A0B3E9E5d1111",
16
- pool: "0x7ef1c0ffee00000000000000000000000000000000000000000000000000bEEF",
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();
@@ -222,10 +224,19 @@ export async function demoCheck(opts) {
222
224
  if (cfg.issuer && isAddress(cfg.issuer)) {
223
225
  log.section("Issuer State");
224
226
  try {
225
- const [root, verifier] = await Promise.all([
227
+ const [root, verifier, eas] = await Promise.all([
226
228
  client.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "merkleRoot" }),
227
229
  client.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "zkVerifier" }),
230
+ client.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "eas" }),
228
231
  ]);
232
+ const hasEASPath = eas !== ZERO;
233
+ const hasZKPath = root !== 0n && verifier !== ZERO;
234
+ if (hasEASPath)
235
+ ok("issuance path", `${fmt.badge("EAS/mock", "green")} ${fmt.addr(eas)}`);
236
+ else if (hasZKPath)
237
+ ok("issuance path", fmt.badge("ZK", "green"));
238
+ else
239
+ warn("issuance path", fmt.badge("not ready", "yellow"));
229
240
  if (root === 0n)
230
241
  warn("merkleRoot", fmt.badge("not set", "yellow"));
231
242
  else
@@ -234,9 +245,11 @@ export async function demoCheck(opts) {
234
245
  warn("zkVerifier", fmt.badge("not set", "yellow"));
235
246
  else
236
247
  ok("zkVerifier", fmt.addr(verifier));
248
+ pass(hasEASPath || hasZKPath);
237
249
  }
238
250
  catch (e) {
239
251
  bad("issuer reads", e instanceof Error ? e.message.split("\n")[0] : String(e));
252
+ pass(false);
240
253
  }
241
254
  if (wallet && isAddress(wallet)) {
242
255
  try {
@@ -344,3 +357,41 @@ export async function demoCheck(opts) {
344
357
  log.command(`ilal swap --amount-in 0.001 --token-in ${suggestedTokenIn}`);
345
358
  console.log();
346
359
  }
360
+ export async function demoFaucet(opts) {
361
+ const cfg = loadConfig();
362
+ const chain = CHAINS[cfg.chain ?? "84532"] ?? baseSepolia;
363
+ const rawKey = opts.privateKey ?? process.env["PRIVATE_KEY"];
364
+ if (!rawKey || !isHex(rawKey) || rawKey.length !== 66) {
365
+ die("Private key required. Use --private-key or set PRIVATE_KEY env var.");
366
+ }
367
+ if (!cfg.tokenA || !cfg.tokenB || !isAddress(cfg.tokenA) || !isAddress(cfg.tokenB)) {
368
+ die("tokenA/tokenB required. Run `ilal init` with demo token addresses first.");
369
+ }
370
+ const account = privateKeyToAccount(rawKey);
371
+ const wallet = opts.wallet ?? account.address;
372
+ if (!isAddress(wallet))
373
+ die(`Invalid wallet address: ${wallet}`);
374
+ const client = createPublicClient({ chain, transport: http(cfg.rpc) });
375
+ const walletClient = createWalletClient({ account, chain, transport: http(cfg.rpc) });
376
+ header("ILAL Demo Faucet", chain.name);
377
+ log.kv("recipient", fmt.addr(wallet));
378
+ log.line();
379
+ for (const token of [cfg.tokenA, cfg.tokenB]) {
380
+ const [symbol, decimals] = await Promise.all([
381
+ client.readContract({ address: token, abi: ERC20_ABI, functionName: "symbol" }),
382
+ client.readContract({ address: token, abi: ERC20_ABI, functionName: "decimals" }),
383
+ ]);
384
+ const amount = parseUnits(opts.amount ?? "10000", decimals);
385
+ const spin = new Spinner(`Minting ${opts.amount ?? "10000"} ${symbol}…`).start();
386
+ const hash = await walletClient.writeContract({
387
+ address: token,
388
+ abi: ERC20_ABI,
389
+ functionName: "mint",
390
+ args: [wallet, amount],
391
+ });
392
+ await client.waitForTransactionReceipt({ hash });
393
+ spin.succeed(`Minted ${symbol} ${fmt.hash(hash)}`);
394
+ }
395
+ log.callout("Demo tokens ready", "wallet can now pass token-balance preflight checks", "green");
396
+ console.log();
397
+ }
@@ -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,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: "0xc4E032A7574016bd0e3d1a5BbFdE886af09CeD9A",
14
+ hook: "0xF5066ad9c25F3f54cfb19609A60187C48C184A80",
15
+ registry: "0x910a3efDc426f3216738106dd0DC6EA696477233",
16
+ router: "0x7727F0f3EBe99A558487394D001950ee6B33BB86",
17
+ treasury: "0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38",
18
+ tokenA: "0x582362E608F36850F6f641510d5D19C1EaB4cb27",
19
+ tokenB: "0x6eBBdAC70EC422C512727B25c7F0D9120ed101Ff",
20
+ poolId: "0xc1c8f29d6f03b5cd18bf2b862d48f45cc338022a154945b89c4bcb0a3e11e87f",
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
  };
@@ -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 0x319c0...
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 0x319c0...
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 0x319c0...
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 0x319c0...
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 0x319c0... \
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 0x319c0... \
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.1.0")
22
+ .version("0.2.2")
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.0",
3
+ "version": "0.2.2",
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
  },