@ilalv3/cli 0.2.9 → 0.2.11

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
@@ -94,7 +94,35 @@ PRIVATE_KEY=0x... ilal credential prove \
94
94
  --expires-at 1800000000
95
95
  ```
96
96
 
97
- 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.
97
+ The CLI automatically prepares proving artifacts:
98
+
99
+ 1. Use `--circuit-dir` when a local `circuits/build` directory exists.
100
+ 2. Otherwise use the local cache at `~/.ilal/artifacts/ilal-v1`.
101
+ 3. If the cache is empty, download hosted artifacts from the ILAL release CDN.
102
+
103
+ No institution needs to compile Circom in its backend. The flow 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.
104
+
105
+ Advanced artifact controls:
106
+
107
+ ```bash
108
+ # Use a custom enterprise artifact mirror
109
+ PRIVATE_KEY=0x... ilal credential prove \
110
+ --wallet 0xYourWallet \
111
+ --artifact-url https://zk-artifacts.yourdomain.example/ilal-v1
112
+
113
+ # Pre-seeded/offline mode
114
+ PRIVATE_KEY=0x... ilal credential prove \
115
+ --wallet 0xYourWallet \
116
+ --artifact-cache /opt/ilal/artifacts/ilal-v1 \
117
+ --offline
118
+ ```
119
+
120
+ Equivalent environment variables:
121
+
122
+ ```bash
123
+ ILAL_ARTIFACT_BASE_URL=https://zk-artifacts.yourdomain.example/ilal-v1
124
+ ILAL_ARTIFACT_CACHE=/opt/ilal/artifacts/ilal-v1
125
+ ```
98
126
 
99
127
  ## Command reference
100
128
 
@@ -103,7 +131,7 @@ Generates a Groth16 proof locally (~5s), verifies it on-chain, and mints/renews
103
131
  | `ilal init` | Create `.ilal.json` with contract addresses |
104
132
  | `ilal status` | Dashboard: credential · issuer config · pool policy |
105
133
  | `ilal credential zk-root` | Operator helper: compute the ZK Merkle root for a demo wallet/expiry |
106
- | `ilal credential prove` | Trader flow: local ZK proof → mint or renew CNF |
134
+ | `ilal credential prove` | Trader flow: hosted/cached ZK artifacts → local proof → mint or renew CNF |
107
135
  | `ilal credential mint` | Mint CNF via Coinbase EAS attestation |
108
136
  | `ilal credential renew` | Renew CNF via EAS attestation |
109
137
  | `ilal swap` | Compliant swap via ILALRouter with optional `--min-amount-out` |
@@ -1,6 +1,6 @@
1
1
  export declare function credentialStatus(opts: {
2
2
  wallet: string;
3
- issuer: string;
3
+ issuer?: string;
4
4
  rpc?: string;
5
- chain: string;
5
+ chain?: string;
6
6
  }): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  import { createPublicClient, http, isAddress } from "viem";
2
2
  import { base, baseSepolia } from "viem/chains";
3
3
  import { fmt, log, die } from "../ui.js";
4
+ import { withConfig } from "../config.js";
4
5
  const CNF_ABI = [
5
6
  { name: "isValid", type: "function", stateMutability: "view", inputs: [{ name: "wallet", type: "address" }], outputs: [{ type: "bool" }] },
6
7
  { name: "credentialOf", type: "function", stateMutability: "view", inputs: [{ name: "wallet", type: "address" }], outputs: [{ name: "tokenId", type: "uint256" }] },
@@ -11,30 +12,39 @@ const CHAINS = {
11
12
  "84532": baseSepolia,
12
13
  };
13
14
  export async function credentialStatus(opts) {
14
- if (!isAddress(opts.wallet))
15
- die(`Invalid wallet address: ${opts.wallet}`);
16
- if (!isAddress(opts.issuer))
17
- die(`Invalid issuer address: ${opts.issuer}`);
18
- const chain = CHAINS[opts.chain] ?? baseSepolia;
19
- const transport = opts.rpc ? http(opts.rpc) : http();
15
+ const cfg = withConfig(opts);
16
+ if (!isAddress(cfg.wallet))
17
+ die(`Invalid wallet address: ${cfg.wallet}`);
18
+ if (!cfg.issuer)
19
+ die("CNFIssuer address required. Use --issuer or run `ilal init`.");
20
+ if (!isAddress(cfg.issuer))
21
+ die(`Invalid issuer address: ${cfg.issuer}`);
22
+ const chain = CHAINS[cfg.chain ?? "84532"] ?? baseSepolia;
23
+ const transport = cfg.rpc ? http(cfg.rpc) : http();
20
24
  const client = createPublicClient({ chain, transport });
21
25
  console.log();
22
26
  console.log(fmt.bold(" ILAL Credential Status"));
23
27
  log.line();
24
- log.kv("wallet", opts.wallet);
25
- log.kv("issuer", opts.issuer);
28
+ log.kv("wallet", cfg.wallet);
29
+ log.kv("issuer", cfg.issuer);
26
30
  log.kv("chain", chain.name);
27
31
  log.line();
28
32
  log.step("Querying CNFIssuer on-chain…");
29
33
  const [valid, tokenId] = await Promise.all([
30
- client.readContract({ address: opts.issuer, abi: CNF_ABI, functionName: "isValid", args: [opts.wallet] }),
31
- client.readContract({ address: opts.issuer, abi: CNF_ABI, functionName: "credentialOf", args: [opts.wallet] }),
34
+ client.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "isValid", args: [cfg.wallet] }),
35
+ client.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "credentialOf", args: [cfg.wallet] }),
32
36
  ]);
33
37
  if (tokenId === 0n) {
34
38
  log.fail("No credential found for this wallet");
35
39
  console.log();
36
40
  console.log(fmt.bold(" How to get a CNF credential:"));
37
41
  console.log();
42
+ console.log(fmt.bold(" Demo path — issuer-created MockEAS attestation"));
43
+ console.log(` ${fmt.gray("1.")} Ask the issuer/operator to run:`);
44
+ console.log(` ${fmt.cyan("PRIVATE_KEY=<issuer-owner> ilal demo attest --wallet " + cfg.wallet)}`);
45
+ console.log(` ${fmt.gray("2.")} Then mint with your wallet key:`);
46
+ console.log(` ${fmt.cyan("PRIVATE_KEY=<wallet-key> ilal credential mint --attestation <uid>")}`);
47
+ console.log();
38
48
  console.log(fmt.bold(" Path A — Coinbase Verifications (EAS)"));
39
49
  console.log(` ${fmt.gray("1.")} Complete KYC at ${fmt.cyan("https://coinbase.com/onchain-verify")}`);
40
50
  console.log(` ${fmt.gray("2.")} Find your attestation UID on EAS Explorer:`);
@@ -49,12 +59,12 @@ export async function credentialStatus(opts) {
49
59
  console.log(` ${fmt.gray("1.")} Issuer/operator adds wallet to the Merkle tree`);
50
60
  console.log(` ${fmt.gray("2.")} Operator queues root: ${fmt.cyan("ilal oracle propose-root --root <newMerkleRoot>")}`);
51
61
  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)}`);
62
+ console.log(` ${fmt.gray("4.")} Trader runs: ${fmt.cyan("ilal credential prove --wallet " + cfg.wallet)}`);
53
63
  console.log();
54
64
  return;
55
65
  }
56
66
  const cred = await client.readContract({
57
- address: opts.issuer,
67
+ address: cfg.issuer,
58
68
  abi: CNF_ABI,
59
69
  functionName: "getCredential",
60
70
  args: [tokenId],
@@ -2,7 +2,7 @@ import { createPublicClient, createWalletClient, decodeEventLog, formatUnits, ht
2
2
  import { privateKeyToAccount } from "viem/accounts";
3
3
  import { base, baseSepolia } from "viem/chains";
4
4
  import { loadConfig } from "../config.js";
5
- import { die, fmt, header, log, Spinner } from "../ui.js";
5
+ import { die, fmt, header, log, Spinner, requirePrivateKey } from "../ui.js";
6
6
  const CHAINS = { "8453": base, "84532": baseSepolia };
7
7
  const POOL_MANAGER = {
8
8
  "84532": "0x05E73354cFDd6745C338b50BcFDfA3Aa6fA03408",
@@ -442,6 +442,11 @@ export async function demoCheck(opts) {
442
442
  }
443
443
  if (wallet) {
444
444
  log.command(`ilal status --wallet ${wallet}`);
445
+ if (!credentialReady) {
446
+ log.info("Credential missing: the issuer must create an attestation before the wallet can mint CNF.");
447
+ log.command(`PRIVATE_KEY=<issuer-owner> ilal demo attest --wallet ${wallet}`);
448
+ log.command("PRIVATE_KEY=<wallet-key> ilal credential mint --attestation <uid>");
449
+ }
445
450
  log.command(`ilal session sign --pool ${cfg.poolId ?? "<poolId>"} --action swap --hook ${cfg.hook ?? "<hook>"} --issuer ${cfg.issuer ?? "<issuer>"} --caller ${cfg.router ?? "<router>"}`);
446
451
  }
447
452
  else {
@@ -454,10 +459,7 @@ export async function demoCheck(opts) {
454
459
  export async function demoFaucet(opts) {
455
460
  const cfg = loadConfig();
456
461
  const chain = CHAINS[cfg.chain ?? "84532"] ?? baseSepolia;
457
- const rawKey = opts.privateKey ?? process.env["PRIVATE_KEY"];
458
- if (!rawKey || !isHex(rawKey) || rawKey.length !== 66) {
459
- die("Private key required. Use --private-key or set PRIVATE_KEY env var.");
460
- }
462
+ const rawKey = requirePrivateKey(opts.privateKey ?? process.env["PRIVATE_KEY"]);
461
463
  if (!cfg.tokenA || !cfg.tokenB || !isAddress(cfg.tokenA) || !isAddress(cfg.tokenB)) {
462
464
  die("tokenA/tokenB required. Run `ilal init` with demo token addresses first.");
463
465
  }
@@ -492,10 +494,7 @@ export async function demoFaucet(opts) {
492
494
  export async function demoAttest(opts) {
493
495
  const cfg = loadConfig();
494
496
  const chain = CHAINS[cfg.chain ?? "84532"] ?? baseSepolia;
495
- const rawKey = opts.privateKey ?? process.env["PRIVATE_KEY"];
496
- if (!rawKey || !isHex(rawKey) || rawKey.length !== 66) {
497
- die("Private key required. Use --private-key or set PRIVATE_KEY env var.");
498
- }
497
+ const rawKey = requirePrivateKey(opts.privateKey ?? process.env["PRIVATE_KEY"]);
499
498
  if (!cfg.issuer || !isAddress(cfg.issuer))
500
499
  die("CNFIssuer required. Run `ilal init` first.");
501
500
  if (!isAddress(opts.wallet))
@@ -1,7 +1,7 @@
1
1
  import { execSync } from "child_process";
2
2
  import { existsSync } from "fs";
3
3
  import { resolve } from "path";
4
- import { fmt, log, die } from "../ui.js";
4
+ import { fmt, log, die, requirePrivateKey } from "../ui.js";
5
5
  import { EAS_ADDRESSES, COINBASE_ATTESTER, COINBASE_SCHEMA_UID } from "../constants.js";
6
6
  const POOL_MANAGERS = {
7
7
  "8453": "0x498581ff718922c3f8e6a244956af099b2652b2b", // Base mainnet
@@ -12,9 +12,7 @@ const RPC_URLS = {
12
12
  "84532": "https://sepolia.base.org",
13
13
  };
14
14
  export async function deploy(opts) {
15
- const privateKey = opts.privateKey ?? process.env["PRIVATE_KEY"];
16
- if (!privateKey)
17
- die("Private key required. Use --private-key or set PRIVATE_KEY env var.");
15
+ const privateKey = requirePrivateKey(opts.privateKey ?? process.env["PRIVATE_KEY"]);
18
16
  const chainId = opts.chain;
19
17
  const poolManager = POOL_MANAGERS[chainId];
20
18
  if (!poolManager)
@@ -18,5 +18,7 @@ export declare function init(opts: {
18
18
  chain: string;
19
19
  rpc?: string;
20
20
  circuitDir?: string;
21
+ artifactUrl?: string;
22
+ artifactCache?: string;
21
23
  force: boolean;
22
24
  }): Promise<void>;
@@ -51,6 +51,8 @@ export async function init(opts) {
51
51
  tickSpacing: opts.tickSpacing ?? preset["tickSpacing"],
52
52
  rpc: opts.rpc ?? preset["rpc"],
53
53
  ...(opts.circuitDir ? { circuitDir: opts.circuitDir } : {}),
54
+ ...(opts.artifactUrl ? { artifactUrl: opts.artifactUrl } : {}),
55
+ ...(opts.artifactCache ? { artifactCache: opts.artifactCache } : {}),
54
56
  };
55
57
  // Validate addresses
56
58
  for (const [key, val] of Object.entries(config)) {
@@ -86,6 +88,10 @@ export async function init(opts) {
86
88
  log.kv("tickSpacing", config.tickSpacing);
87
89
  if (config.rpc)
88
90
  log.kv("rpc", config.rpc);
91
+ if (config.artifactUrl)
92
+ log.kv("artifactUrl", config.artifactUrl);
93
+ if (config.artifactCache)
94
+ log.kv("artifactCache", config.artifactCache);
89
95
  log.line();
90
96
  console.log(` ${fmt.gray("You can now run commands without --issuer and --chain flags:")}`);
91
97
  console.log();
@@ -16,7 +16,7 @@
16
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
- import { fmt, log, header, Spinner, die, dieOnContract } from "../ui.js";
19
+ import { fmt, log, header, Spinner, die, dieOnContract, requirePrivateKey } from "../ui.js";
20
20
  import { withConfig } from "../config.js";
21
21
  const CHAINS = { "8453": base, "84532": baseSepolia };
22
22
  // ─── ABIs ─────────────────────────────────────────────────────────────────────
@@ -128,9 +128,7 @@ function defaultUserSalt(address) {
128
128
  // ─── Shared core ──────────────────────────────────────────────────────────────
129
129
  async function executeLiquidity(action, opts) {
130
130
  const cfg = withConfig(opts);
131
- const rawKey = cfg.privateKey ?? process.env["PRIVATE_KEY"];
132
- if (!rawKey)
133
- die("Private key required. Use --private-key or set PRIVATE_KEY env var.");
131
+ const rawKey = requirePrivateKey(cfg.privateKey ?? process.env["PRIVATE_KEY"]);
134
132
  if (!cfg.router)
135
133
  die("ILALRouter address required. Use --router or set in .ilal.json");
136
134
  if (!cfg.hook)
@@ -199,7 +197,7 @@ async function executeLiquidity(action, opts) {
199
197
  if (tokenId === 0n) {
200
198
  preflightErrors.push("wallet has no CNF credential; mint one before changing liquidity.");
201
199
  if (hasEASPath)
202
- preflightErrors.push("issuer supports EAS/mock attestation minting: run `ilal credential mint --attestation <uid>`.");
200
+ preflightErrors.push("issuer supports EAS/mock attestation minting: first ask issuer to run `ilal demo attest --wallet <wallet>`, then run `ilal credential mint --attestation <uid>`.");
203
201
  else if (hasZKPath)
204
202
  preflightErrors.push(`issuer supports ZK minting: run \`ilal credential prove --wallet ${account.address}\`.`);
205
203
  else
@@ -1,7 +1,7 @@
1
1
  declare function sendMintTx(mode: "mint" | "renew", opts: {
2
2
  attestation: string;
3
- issuer: string;
4
- chain: string;
3
+ issuer?: string;
4
+ chain?: string;
5
5
  rpc?: string;
6
6
  privateKey?: string;
7
7
  simulate: boolean;
@@ -1,7 +1,8 @@
1
1
  import { createPublicClient, createWalletClient, http, isAddress, isHex, } from "viem";
2
2
  import { privateKeyToAccount } from "viem/accounts";
3
3
  import { base, baseSepolia } from "viem/chains";
4
- import { fmt, log, die } from "../ui.js";
4
+ import { fmt, log, die, Spinner, requirePrivateKey } from "../ui.js";
5
+ import { withConfig } from "../config.js";
5
6
  import { EAS_ADDRESSES, COINBASE_SCHEMA_UID, COINBASE_ATTESTER } from "../constants.js";
6
7
  const CHAINS = { "8453": base, "84532": baseSepolia };
7
8
  const CNF_ISSUER_ABI = [
@@ -32,25 +33,24 @@ const CNF_ISSUER_ABI = [
32
33
  ];
33
34
  const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
34
35
  async function sendMintTx(mode, opts) {
35
- const rawKey = opts.privateKey ?? process.env["PRIVATE_KEY"];
36
- if (!rawKey)
37
- die("Private key required. Use --private-key or set PRIVATE_KEY env var.");
38
- if (!isHex(rawKey) || rawKey.length !== 66)
39
- die("Invalid private key (expected 0x + 32 bytes).");
40
- if (!isAddress(opts.issuer))
41
- die(`Invalid issuer address: ${opts.issuer}`);
36
+ const cfg = withConfig(opts);
37
+ const rawKey = requirePrivateKey(cfg.privateKey ?? process.env["PRIVATE_KEY"]);
38
+ if (!cfg.issuer)
39
+ die("CNFIssuer address required. Use --issuer or run `ilal init`.");
40
+ if (!isAddress(cfg.issuer))
41
+ die(`Invalid issuer address: ${cfg.issuer}`);
42
42
  if (!isHex(opts.attestation) || opts.attestation.length !== 66)
43
43
  die("Attestation UID must be 0x + 32 bytes (64 hex chars).");
44
- const chain = CHAINS[opts.chain] ?? baseSepolia;
44
+ const chain = CHAINS[cfg.chain ?? "84532"] ?? baseSepolia;
45
45
  const account = privateKeyToAccount(rawKey);
46
- const transport = opts.rpc ? http(opts.rpc) : http();
46
+ const transport = cfg.rpc ? http(cfg.rpc) : http();
47
47
  const publicClient = createPublicClient({ chain, transport });
48
48
  const walletClient = createWalletClient({ account, chain, transport });
49
49
  console.log();
50
50
  console.log(fmt.bold(` ILAL Credential ${mode === "mint" ? "Mint" : "Renew"}`));
51
51
  log.line();
52
52
  log.kv("wallet", account.address);
53
- log.kv("issuer", opts.issuer);
53
+ log.kv("issuer", cfg.issuer);
54
54
  log.kv("attestation", opts.attestation);
55
55
  log.kv("chain", chain.name);
56
56
  if (opts.simulate)
@@ -59,9 +59,9 @@ async function sendMintTx(mode, opts) {
59
59
  // Verify EAS attestation exists on-chain before sending tx
60
60
  log.step("Verifying attestation on EAS…");
61
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" }),
62
+ publicClient.readContract({ address: cfg.issuer, abi: CNF_ISSUER_ABI, functionName: "eas" }),
63
+ publicClient.readContract({ address: cfg.issuer, abi: CNF_ISSUER_ABI, functionName: "schemaUID" }),
64
+ publicClient.readContract({ address: cfg.issuer, abi: CNF_ISSUER_ABI, functionName: "trustedAttester" }),
65
65
  ]);
66
66
  const easAddress = issuerEAS !== ZERO_ADDRESS ? issuerEAS : EAS_ADDRESSES[chain.id];
67
67
  if (!easAddress)
@@ -132,7 +132,7 @@ async function sendMintTx(mode, opts) {
132
132
  }
133
133
  log.step(`Sending ${mode === "mint" ? "mintWithEAS" : "renewWithEAS"} transaction…`);
134
134
  const hash = await walletClient.writeContract({
135
- address: opts.issuer,
135
+ address: cfg.issuer,
136
136
  abi: CNF_ISSUER_ABI,
137
137
  functionName: mode === "mint" ? "mintWithEAS" : "renewWithEAS",
138
138
  args: [opts.attestation],
@@ -144,13 +144,26 @@ async function sendMintTx(mode, opts) {
144
144
  log.ok(fmt.bold(fmt.green(`CNF ${mode === "mint" ? "minted" : "renewed"} successfully`)));
145
145
  log.kv("tx hash", hash);
146
146
  log.kv("block", receipt.blockNumber.toString());
147
- const valid = await publicClient.readContract({
148
- address: opts.issuer,
149
- abi: CNF_ISSUER_ABI,
150
- functionName: "isValid",
151
- args: [account.address],
152
- });
153
- log.kv("isValid()", valid ? fmt.green("true ✓") : fmt.red("false"));
147
+ const validSpin = new Spinner("Waiting for credential validity…").start();
148
+ let valid = false;
149
+ for (let attempt = 0; attempt < 6; attempt++) {
150
+ valid = await publicClient.readContract({
151
+ address: cfg.issuer,
152
+ abi: CNF_ISSUER_ABI,
153
+ functionName: "isValid",
154
+ args: [account.address],
155
+ });
156
+ if (valid)
157
+ break;
158
+ await new Promise((resolve) => setTimeout(resolve, 1000));
159
+ }
160
+ if (valid)
161
+ validSpin.succeed("Credential active");
162
+ else {
163
+ validSpin.succeed("Mint confirmed; credential validity may take a few seconds to appear on this RPC");
164
+ log.info("Run `ilal credential status <wallet>` if this RPC still shows stale state.");
165
+ }
166
+ log.kv("isValid()", valid ? fmt.green("true ✓") : fmt.yellow("pending RPC refresh"));
154
167
  }
155
168
  else {
156
169
  die(`Transaction reverted. Hash: ${hash}`);
@@ -25,7 +25,7 @@
25
25
  import { createPublicClient, createWalletClient, http, isAddress, } from "viem";
26
26
  import { privateKeyToAccount } from "viem/accounts";
27
27
  import { base, baseSepolia } from "viem/chains";
28
- import { fmt, log, header, Spinner, die, dieOnContract } from "../ui.js";
28
+ import { fmt, log, header, Spinner, die, dieOnContract, requirePrivateKey } from "../ui.js";
29
29
  import { withConfig } from "../config.js";
30
30
  const CHAINS = { "8453": base, "84532": baseSepolia };
31
31
  const ORACLE_ABI = [
@@ -79,9 +79,7 @@ function txUrl(chain, hash) {
79
79
  return baseUrl ? `${baseUrl}/tx/${hash}` : undefined;
80
80
  }
81
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.");
82
+ const rawKey = requirePrivateKey(opts.privateKey ?? process.env["PRIVATE_KEY"]);
85
83
  const chain = CHAINS[opts.chain ?? "84532"] ?? baseSepolia;
86
84
  const transport = opts.rpc ? http(opts.rpc) : http();
87
85
  const account = privateKeyToAccount(rawKey);
@@ -1,7 +1,7 @@
1
1
  import { createPublicClient, createWalletClient, http, isAddress, isHex, } from "viem";
2
2
  import { privateKeyToAccount } from "viem/accounts";
3
3
  import { base, baseSepolia } from "viem/chains";
4
- import { fmt, log, header, Spinner, die } from "../ui.js";
4
+ import { fmt, log, header, Spinner, die, requirePrivateKey } from "../ui.js";
5
5
  const CHAINS = { "8453": base, "84532": baseSepolia };
6
6
  const REGISTRY_ABI = [
7
7
  {
@@ -38,11 +38,7 @@ const REGISTRY_ABI = [
38
38
  },
39
39
  ];
40
40
  export async function poolPolicySet(opts) {
41
- const rawKey = opts.privateKey ?? process.env["PRIVATE_KEY"];
42
- if (!rawKey)
43
- die("Private key required. Use --private-key or set PRIVATE_KEY env var.");
44
- if (!isHex(rawKey) || rawKey.length !== 66)
45
- die("Invalid private key.");
41
+ const rawKey = requirePrivateKey(opts.privateKey ?? process.env["PRIVATE_KEY"]);
46
42
  if (!isAddress(opts.issuer))
47
43
  die(`Invalid issuer address: ${opts.issuer}`);
48
44
  if (!isAddress(opts.registry))
@@ -2,7 +2,7 @@ import { readFileSync } from "fs";
2
2
  import { createPublicClient, createWalletClient, encodeAbiParameters, http, isAddress, } from "viem";
3
3
  import { privateKeyToAccount } from "viem/accounts";
4
4
  import { base, baseSepolia } from "viem/chains";
5
- import { fmt, log, die } from "../ui.js";
5
+ import { fmt, log, die, requirePrivateKey } from "../ui.js";
6
6
  const CHAINS = { "8453": base, "84532": baseSepolia };
7
7
  const CNF_ISSUER_ABI = [
8
8
  {
@@ -73,9 +73,7 @@ export async function proofRenew(opts) {
73
73
  await sendProofTx("renew", opts);
74
74
  }
75
75
  async function sendProofTx(mode, opts) {
76
- const rawKey = opts.privateKey ?? process.env["PRIVATE_KEY"];
77
- if (!rawKey)
78
- die("Private key required. Use --private-key or set PRIVATE_KEY env var.");
76
+ const rawKey = requirePrivateKey(opts.privateKey ?? process.env["PRIVATE_KEY"]);
79
77
  if (!isAddress(opts.issuer))
80
78
  die(`Invalid issuer address: ${opts.issuer}`);
81
79
  const chain = CHAINS[opts.chain] ?? baseSepolia;
@@ -21,6 +21,9 @@ export declare function credentialProve(opts: {
21
21
  chain?: string;
22
22
  action?: string;
23
23
  circuitDir?: string;
24
+ artifactUrl?: string;
25
+ artifactCache?: string;
26
+ offline?: boolean;
24
27
  outDir?: string;
25
28
  rpc?: string;
26
29
  privateKey?: string;
@@ -16,8 +16,11 @@
16
16
  * `ilal oracle activate-root`.
17
17
  */
18
18
  import { execSync } from "child_process";
19
- import { mkdirSync, writeFileSync, readFileSync } from "fs";
19
+ import { createWriteStream, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
20
+ import { homedir } from "os";
20
21
  import { resolve, dirname } from "path";
22
+ import { Readable } from "stream";
23
+ import { pipeline } from "stream/promises";
21
24
  import { fileURLToPath } from "url";
22
25
  import { createPublicClient, createWalletClient, encodeAbiParameters, http, isAddress, keccak256, } from "viem";
23
26
  import { privateKeyToAccount } from "viem/accounts";
@@ -26,12 +29,21 @@ import { base, baseSepolia } from "viem/chains";
26
29
  // @ts-ignore — no bundled types for this package
27
30
  import { IncrementalMerkleTree } from "@zk-kit/incremental-merkle-tree";
28
31
  import { poseidon2, poseidon4 } from "poseidon-lite";
29
- import { fmt, log, header, Spinner, die, dieOnContract } from "../ui.js";
32
+ import { fmt, log, header, Spinner, die, dieOnContract, requirePrivateKey } from "../ui.js";
30
33
  import { withConfig } from "../config.js";
31
34
  import { COINBASE_SCHEMA_UID } from "../constants.js";
32
35
  const __dirname = dirname(fileURLToPath(import.meta.url));
33
36
  const CHAINS = { "8453": base, "84532": baseSepolia };
34
37
  const DEPTH = 20;
38
+ const DEFAULT_ARTIFACT_URL = "https://unpkg.com/@ilalv3/proving-artifacts@0.1.0";
39
+ const DEFAULT_ARTIFACT_CACHE = resolve(homedir(), ".ilal/artifacts/ilal-v1");
40
+ const PROVING_ARTIFACTS = [
41
+ { asset: "ilal.zkey", rel: "ilal.zkey", minBytes: 50_000_000 },
42
+ { asset: "ilal_vkey.json", rel: "ilal_vkey.json", minBytes: 1_000 },
43
+ { asset: "ilal_js/ilal.wasm", rel: "ilal_js/ilal.wasm", minBytes: 1_000_000 },
44
+ { asset: "ilal_js/generate_witness.js", rel: "ilal_js/generate_witness.js", minBytes: 100 },
45
+ { asset: "ilal_js/witness_calculator.js", rel: "ilal_js/witness_calculator.js", minBytes: 1_000 },
46
+ ];
35
47
  // ─── ABI ──────────────────────────────────────────────────────────────────────
36
48
  const CNF_ABI = [
37
49
  {
@@ -82,26 +94,81 @@ function schemaHash(schemaUID) {
82
94
  const schemaHi = BigInt("0x" + schemaHex.slice(0, 32));
83
95
  return poseidon2([schemaLo, schemaHi]);
84
96
  }
85
- function findCircuitDir(override) {
86
- if (override)
87
- return resolve(override);
97
+ function hasCircuitArtifacts(dir) {
98
+ return PROVING_ARTIFACTS.every((artifact) => {
99
+ const file = resolve(dir, artifact.rel);
100
+ try {
101
+ return existsSync(file) && statSync(file).size >= artifact.minBytes;
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ });
107
+ }
108
+ function requireCircuitArtifacts(dir) {
109
+ const resolved = resolve(dir);
110
+ const missing = PROVING_ARTIFACTS.filter((artifact) => {
111
+ const file = resolve(resolved, artifact.rel);
112
+ try {
113
+ return !existsSync(file) || statSync(file).size < artifact.minBytes;
114
+ }
115
+ catch {
116
+ return true;
117
+ }
118
+ });
119
+ if (missing.length > 0) {
120
+ die(`Proving artifacts are incomplete in ${resolved}.\n` +
121
+ ` Missing: ${missing.map((x) => x.rel).join(", ")}\n` +
122
+ " Pass --artifact-url <base-url> to download them, or use --circuit-dir <path>.");
123
+ }
124
+ return resolved;
125
+ }
126
+ async function downloadFile(url, target) {
127
+ const res = await fetch(url);
128
+ if (!res.ok || !res.body) {
129
+ throw new Error(`download failed ${res.status} ${res.statusText}: ${url}`);
130
+ }
131
+ mkdirSync(dirname(target), { recursive: true });
132
+ await pipeline(Readable.fromWeb(res.body), createWriteStream(target));
133
+ }
134
+ async function ensureHostedArtifacts(opts) {
135
+ const cacheDir = resolve(opts.artifactCache ?? DEFAULT_ARTIFACT_CACHE);
136
+ if (hasCircuitArtifacts(cacheDir))
137
+ return cacheDir;
138
+ if (opts.offline) {
139
+ die("Hosted proving artifacts are not cached locally.\n" +
140
+ ` Cache: ${cacheDir}\n` +
141
+ " Re-run without --offline, or pass --circuit-dir <path>.");
142
+ }
143
+ const baseUrl = (opts.artifactUrl ?? DEFAULT_ARTIFACT_URL).replace(/\/+$/, "");
144
+ for (const artifact of PROVING_ARTIFACTS) {
145
+ const target = resolve(cacheDir, artifact.rel);
146
+ if (existsSync(target) && statSync(target).size >= artifact.minBytes)
147
+ continue;
148
+ await downloadFile(`${baseUrl}/${artifact.asset}`, target);
149
+ }
150
+ return requireCircuitArtifacts(cacheDir);
151
+ }
152
+ async function findCircuitDir(opts) {
153
+ if (opts.override) {
154
+ return { dir: requireCircuitArtifacts(opts.override), source: "local" };
155
+ }
88
156
  // Look relative to the CLI package root (cli/ → circuits/build)
89
157
  const candidates = [
90
158
  resolve(__dirname, "../../../../circuits/build"), // dev: cli/src/commands → circuits/build
91
159
  resolve(__dirname, "../../../circuits/build"),
92
160
  resolve(process.cwd(), "circuits/build"),
93
161
  resolve(process.cwd(), "build"),
162
+ resolve(opts.artifactCache ?? DEFAULT_ARTIFACT_CACHE),
94
163
  ];
95
164
  for (const p of candidates) {
96
- try {
97
- readFileSync(resolve(p, "ilal.zkey"));
98
- return p;
165
+ if (hasCircuitArtifacts(p)) {
166
+ const source = resolve(p) === resolve(opts.artifactCache ?? DEFAULT_ARTIFACT_CACHE) ? "cache" : "local";
167
+ return { dir: p, source };
99
168
  }
100
- catch { /* not found */ }
101
169
  }
102
- die("Circuit build directory not found.\n" +
103
- " Run: bash circuits/scripts/compile.sh\n" +
104
- " Or pass: --circuit-dir <path/to/circuits/build>");
170
+ const dir = await ensureHostedArtifacts(opts);
171
+ return { dir, source: "download" };
105
172
  }
106
173
  function resolveExpiresAt(expiresAt) {
107
174
  if (!expiresAt)
@@ -193,9 +260,7 @@ function encodeProof(proofJson, publicJson) {
193
260
  // ─── Main export ──────────────────────────────────────────────────────────────
194
261
  export async function credentialProve(opts) {
195
262
  const cfg = withConfig(opts);
196
- const rawKey = cfg.privateKey ?? process.env["PRIVATE_KEY"];
197
- if (!rawKey)
198
- die("Private key required. Use --private-key or set PRIVATE_KEY env var.");
263
+ const rawKey = requirePrivateKey(cfg.privateKey ?? process.env["PRIVATE_KEY"]);
199
264
  if (!cfg.wallet)
200
265
  die("Wallet address required. Use --wallet or set issuer in .ilal.json");
201
266
  if (!cfg.issuer)
@@ -226,11 +291,29 @@ export async function credentialProve(opts) {
226
291
  action = tokenId === 0n ? "mint" : "renew";
227
292
  spin.succeed(`Action: ${fmt.cyan(action)}${tokenId > 0n ? fmt.gray(` (token #${tokenId})`) : ""}`);
228
293
  }
229
- // ── Find circuit build dir ─────────────────────────────────────────────────
230
- const circuitDir = findCircuitDir(cfg.circuitDir);
231
- const outDir = cfg.outDir
232
- ? resolve(cfg.outDir)
233
- : resolve(circuitDir, "../../outputs");
294
+ // ── Prepare hosted/local proving artifacts ────────────────────────────────
295
+ const artifactSpin = new Spinner("Preparing proving artifacts…").start();
296
+ let circuitDir;
297
+ try {
298
+ const artifacts = await findCircuitDir({
299
+ override: cfg.circuitDir,
300
+ artifactUrl: cfg.artifactUrl,
301
+ artifactCache: cfg.artifactCache,
302
+ offline: cfg.offline,
303
+ });
304
+ circuitDir = artifacts.dir;
305
+ const sourceLabel = artifacts.source === "download" ? "downloaded to cache" :
306
+ artifacts.source === "cache" ? "cache" :
307
+ "local";
308
+ artifactSpin.succeed(`Proving artifacts ready (${sourceLabel})`);
309
+ }
310
+ catch (e) {
311
+ artifactSpin.fail("Proving artifacts unavailable");
312
+ die(e instanceof Error ? e.message : String(e));
313
+ }
314
+ const outDir = cfg.outDir ? resolve(cfg.outDir) : resolve(process.cwd(), "outputs");
315
+ log.kv("artifacts", fmt.gray(circuitDir));
316
+ log.kv("proofOut", fmt.gray(outDir));
234
317
  log.line();
235
318
  // ── Generate proof ─────────────────────────────────────────────────────────
236
319
  const spin = new Spinner("Building Merkle tree & generating ZK proof…").start();
@@ -1,7 +1,7 @@
1
1
  import { createWalletClient, encodeAbiParameters, http, isAddress, isHex, parseAbiParameters, } from "viem";
2
2
  import { privateKeyToAccount } from "viem/accounts";
3
3
  import { base, baseSepolia } from "viem/chains";
4
- import { fmt, log, header, die } from "../ui.js";
4
+ import { fmt, log, header, die, requirePrivateKey } from "../ui.js";
5
5
  import { withConfig } from "../config.js";
6
6
  const CHAINS = {
7
7
  "8453": base,
@@ -30,11 +30,7 @@ const HOOK_DATA_ABI = parseAbiParameters([
30
30
  export async function sessionSign(opts) {
31
31
  const cfg = withConfig({ chain: opts.chain, hook: opts.hook, issuer: opts.issuer });
32
32
  // Resolve private key
33
- const rawKey = opts.privateKey ?? process.env["PRIVATE_KEY"];
34
- if (!rawKey)
35
- die("Private key required. Use --private-key or set PRIVATE_KEY env var.");
36
- if (!isHex(rawKey) || rawKey.length !== 66)
37
- die("Invalid private key format (expected 0x + 32 bytes).");
33
+ const rawKey = requirePrivateKey(opts.privateKey ?? process.env["PRIVATE_KEY"]);
38
34
  const account = privateKeyToAccount(rawKey);
39
35
  const user = (opts.user ?? account.address);
40
36
  const pool = opts.pool ?? cfg.poolId;
@@ -21,7 +21,7 @@
21
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
- import { fmt, log, header, Spinner, die, dieOnContract } from "../ui.js";
24
+ import { fmt, log, header, Spinner, die, dieOnContract, requirePrivateKey } from "../ui.js";
25
25
  import { withConfig } from "../config.js";
26
26
  const CHAINS = { "8453": base, "84532": baseSepolia };
27
27
  // ─── ABIs ─────────────────────────────────────────────────────────────────────
@@ -127,9 +127,7 @@ function secondsSince(startMs) {
127
127
  // ─── Main export ──────────────────────────────────────────────────────────────
128
128
  export async function swap(opts) {
129
129
  const cfg = withConfig(opts);
130
- const rawKey = cfg.privateKey ?? process.env["PRIVATE_KEY"];
131
- if (!rawKey)
132
- die("Private key required. Use --private-key or set PRIVATE_KEY env var.");
130
+ const rawKey = requirePrivateKey(cfg.privateKey ?? process.env["PRIVATE_KEY"]);
133
131
  if (!cfg.router)
134
132
  die("ILALRouter address required. Use --router or set in .ilal.json");
135
133
  if (!cfg.hook)
@@ -211,7 +209,7 @@ export async function swap(opts) {
211
209
  if (tokenId === 0n) {
212
210
  preflightErrors.push(`wallet has no CNF credential; mint one before trading.`);
213
211
  if (hasEASPath)
214
- preflightErrors.push("issuer supports EAS/mock attestation minting: run `ilal credential mint --attestation <uid>`.");
212
+ preflightErrors.push("issuer supports EAS/mock attestation minting: first ask issuer to run `ilal demo attest --wallet <wallet>`, then run `ilal credential mint --attestation <uid>`.");
215
213
  else if (hasZKPath)
216
214
  preflightErrors.push(`issuer supports ZK minting: run \`ilal credential prove --wallet ${account.address}\`.`);
217
215
  else
package/dist/config.d.ts CHANGED
@@ -20,6 +20,8 @@ export interface ILALConfig {
20
20
  chain?: string;
21
21
  rpc?: string;
22
22
  circuitDir?: string;
23
+ artifactUrl?: string;
24
+ artifactCache?: string;
23
25
  outDir?: string;
24
26
  }
25
27
  export declare function loadConfig(): ILALConfig;
package/dist/config.js CHANGED
@@ -52,6 +52,10 @@ export function loadConfig() {
52
52
  chain: process.env["ILAL_CHAIN"] ?? fileConfig.chain,
53
53
  rpc: process.env["ILAL_RPC"] ?? fileConfig.rpc,
54
54
  circuitDir: process.env["ILAL_CIRCUIT_DIR"] ?? fileConfig.circuitDir,
55
+ artifactUrl: process.env["ILAL_ARTIFACT_BASE_URL"]
56
+ ?? process.env["ILAL_ARTIFACT_URL"]
57
+ ?? fileConfig.artifactUrl,
58
+ artifactCache: process.env["ILAL_ARTIFACT_CACHE"] ?? fileConfig.artifactCache,
55
59
  outDir: process.env["ILAL_OUT_DIR"] ?? fileConfig.outDir,
56
60
  };
57
61
  return _config;
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.9")
22
+ .version("0.2.11")
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
@@ -38,6 +38,8 @@ program
38
38
  .option("-c, --chain <chainId>", "Chain ID (8453=Base, 84532=Base Sepolia)", "84532")
39
39
  .option("-r, --rpc <url>", "Custom RPC URL")
40
40
  .option("--circuit-dir <path>", "Path to circuits/build directory")
41
+ .option("--artifact-url <url>", "Hosted proving artifact base URL")
42
+ .option("--artifact-cache <path>", "Local proving artifact cache directory")
41
43
  .option("-f, --force", "Overwrite existing .ilal.json", false)
42
44
  .action(async (opts) => {
43
45
  await init(opts).catch(err);
@@ -99,8 +101,8 @@ const credential = program.command("credential").description("Manage compliance
99
101
  credential
100
102
  .command("status <wallet>")
101
103
  .description("Check CNF credential status for a wallet")
102
- .requiredOption("-i, --issuer <address>", "CNFIssuer contract address")
103
- .option("-c, --chain <chainId>", "Chain ID (8453=Base, 84532=Base Sepolia)", "8453")
104
+ .option("-i, --issuer <address>", "CNFIssuer contract address (or set in .ilal.json)")
105
+ .option("-c, --chain <chainId>", "Chain ID (8453=Base, 84532=Base Sepolia)", "84532")
104
106
  .option("-r, --rpc <url>", "Custom RPC URL")
105
107
  .action(async (wallet, opts) => {
106
108
  await credentialStatus({ wallet, ...opts }).catch(err);
@@ -111,7 +113,10 @@ credential
111
113
  .option("-w, --wallet <address>", "Wallet address to prove eligibility for")
112
114
  .option("-i, --issuer <address>", "CNFIssuer contract address (or set in .ilal.json)")
113
115
  .option("-a, --action <action>", "mint or renew (default: auto-detect)")
114
- .option("--circuit-dir <path>", "Path to circuits/build directory (auto-detected by default)")
116
+ .option("--circuit-dir <path>", "Path to circuits/build directory (dev/offline override)")
117
+ .option("--artifact-url <url>", "Hosted proving artifact base URL (defaults to ILAL release artifacts)")
118
+ .option("--artifact-cache <path>", "Local proving artifact cache directory (default: ~/.ilal/artifacts/ilal-v1)")
119
+ .option("--offline", "Do not download proving artifacts; require cache or --circuit-dir", false)
115
120
  .option("--out-dir <path>", "Directory to write proof/witness files")
116
121
  .option("-c, --chain <chainId>", "Chain ID (8453=Base, 84532=Base Sepolia)", "84532")
117
122
  .option("-r, --rpc <url>", "Custom RPC URL")
@@ -133,8 +138,8 @@ credential
133
138
  .command("mint")
134
139
  .description("Mint a CNF credential using a Coinbase EAS attestation")
135
140
  .requiredOption("-a, --attestation <uid>", "EAS attestation UID (0x + 64 hex chars)")
136
- .requiredOption("-i, --issuer <address>", "CNFIssuer contract address")
137
- .option("-c, --chain <chainId>", "Chain ID", "8453")
141
+ .option("-i, --issuer <address>", "CNFIssuer contract address (or set in .ilal.json)")
142
+ .option("-c, --chain <chainId>", "Chain ID", "84532")
138
143
  .option("-r, --rpc <url>", "Custom RPC URL")
139
144
  .option("-k, --private-key <hex>", "Private key (or set PRIVATE_KEY env var)")
140
145
  .option("--simulate", "Verify attestation without sending tx", false)
@@ -145,8 +150,8 @@ credential
145
150
  .command("renew")
146
151
  .description("Renew an existing CNF credential with a fresh EAS attestation")
147
152
  .requiredOption("-a, --attestation <uid>", "EAS attestation UID (0x + 64 hex chars)")
148
- .requiredOption("-i, --issuer <address>", "CNFIssuer contract address")
149
- .option("-c, --chain <chainId>", "Chain ID", "8453")
153
+ .option("-i, --issuer <address>", "CNFIssuer contract address (or set in .ilal.json)")
154
+ .option("-c, --chain <chainId>", "Chain ID", "84532")
150
155
  .option("-r, --rpc <url>", "Custom RPC URL")
151
156
  .option("-k, --private-key <hex>", "Private key (or set PRIVATE_KEY env var)")
152
157
  .option("--simulate", "Verify attestation without sending tx", false)
package/dist/ui.d.ts CHANGED
@@ -56,4 +56,5 @@ export declare const log: {
56
56
  };
57
57
  export declare function header(title: string, subtitle?: string): void;
58
58
  export declare function die(msg: string): never;
59
+ export declare function requirePrivateKey(rawKey?: string): `0x${string}`;
59
60
  export declare function dieOnContract(e: unknown): never;
package/dist/ui.js CHANGED
@@ -213,6 +213,19 @@ export function die(msg) {
213
213
  console.error();
214
214
  process.exit(1);
215
215
  }
216
+ export function requirePrivateKey(rawKey) {
217
+ const key = rawKey?.trim();
218
+ if (!key) {
219
+ die("Private key required. Use --private-key or set PRIVATE_KEY env var.");
220
+ }
221
+ if (/^[0-9a-fA-F]{64}$/.test(key)) {
222
+ die("Private key must include the 0x prefix. Example: PRIVATE_KEY=0x...");
223
+ }
224
+ if (!/^0x[0-9a-fA-F]{64}$/.test(key)) {
225
+ die("Private key must be 32-byte hex and include the 0x prefix. Example: PRIVATE_KEY=0x...");
226
+ }
227
+ return key;
228
+ }
216
229
  export function dieOnContract(e) {
217
230
  die(parseViemError(e));
218
231
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ilalv3/cli",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "ILAL Protocol CLI — compliant swaps and credential management for Uniswap v4",
5
5
  "type": "module",
6
6
  "bin": {