@ilalv3/cli 0.1.0

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.
@@ -0,0 +1,164 @@
1
+ /**
2
+ * status.ts — `ilal status`
3
+ *
4
+ * Dashboard: credential validity, hook config, pool policy — all in one view.
5
+ */
6
+ import { createPublicClient, http, isAddress } from "viem";
7
+ import { base, baseSepolia } from "viem/chains";
8
+ import { fmt, log, header, Spinner, dieOnContract } from "../ui.js";
9
+ import { withConfig } from "../config.js";
10
+ const CHAINS = { "8453": base, "84532": baseSepolia };
11
+ const CNF_ABI = [
12
+ { name: "isValid", type: "function", stateMutability: "view", inputs: [{ name: "wallet", type: "address" }], outputs: [{ type: "bool" }] },
13
+ { name: "credentialOf", type: "function", stateMutability: "view", inputs: [{ name: "wallet", type: "address" }], outputs: [{ name: "tokenId", type: "uint256" }] },
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
+ { name: "merkleRoot", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "uint256" }] },
16
+ { name: "zkVerifier", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "address" }] },
17
+ ];
18
+ const HOOK_ABI = [
19
+ { name: "issuer", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "address" }] },
20
+ ];
21
+ const REGISTRY_ABI = [
22
+ { 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
+ function daysUntil(unixSec) {
25
+ return Math.floor((unixSec * 1000 - Date.now()) / 86_400_000);
26
+ }
27
+ export async function status(opts) {
28
+ const cfg = withConfig(opts);
29
+ const chain = CHAINS[cfg.chain ?? "84532"] ?? baseSepolia;
30
+ const transport = cfg.rpc ? http(cfg.rpc) : http();
31
+ const client = createPublicClient({ chain, transport });
32
+ const poolId = cfg.pool ?? cfg.poolId;
33
+ header("ILAL Status", chain.name);
34
+ let credentialReady;
35
+ let issuerReady;
36
+ let policyReady;
37
+ // ── Credential ──────────────────────────────────────────────────────────────
38
+ if (cfg.wallet && cfg.issuer) {
39
+ if (!isAddress(cfg.wallet)) {
40
+ log.warn("Invalid wallet address");
41
+ }
42
+ else if (!isAddress(cfg.issuer)) {
43
+ log.warn("Invalid issuer address");
44
+ }
45
+ else {
46
+ const spin = new Spinner("Fetching credential…").start();
47
+ try {
48
+ const [valid, tokenId] = await Promise.all([
49
+ client.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "isValid", args: [cfg.wallet] }),
50
+ client.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "credentialOf", args: [cfg.wallet] }),
51
+ ]);
52
+ spin.stop();
53
+ log.section("Credential");
54
+ log.kv("wallet", fmt.cyan(cfg.wallet));
55
+ log.kv("issuer", fmt.cyan(cfg.issuer));
56
+ if (tokenId === 0n) {
57
+ log.kv("status", fmt.badge("missing", "red"));
58
+ log.command("ilal credential prove --wallet " + cfg.wallet + " --update-root");
59
+ credentialReady = false;
60
+ }
61
+ else {
62
+ const cred = await client.readContract({
63
+ address: cfg.issuer, abi: CNF_ABI,
64
+ functionName: "getCredential", args: [tokenId],
65
+ });
66
+ const days = daysUntil(Number(cred.expiresAt));
67
+ const expiryStr = new Date(Number(cred.expiresAt) * 1000).toISOString().split("T")[0];
68
+ const daysLabel = days > 0
69
+ ? fmt.gray(`(${days}d remaining)`)
70
+ : fmt.red("(EXPIRED)");
71
+ log.kv("token ID", fmt.cyan(`#${tokenId}`));
72
+ log.kv("issued", fmt.gray(new Date(Number(cred.issuedAt) * 1000).toISOString().split("T")[0]));
73
+ log.kv("expires", `${fmt.cyan(expiryStr)} ${daysLabel}`);
74
+ log.kv("revoked", cred.revoked ? fmt.badge("yes", "red") : fmt.badge("no", "gray"));
75
+ log.kv("status", valid ? fmt.badge("valid", "green") + " can trade" : fmt.badge("invalid", "red"));
76
+ credentialReady = valid;
77
+ }
78
+ }
79
+ catch (e) {
80
+ spin.stop();
81
+ dieOnContract(e);
82
+ }
83
+ log.line();
84
+ }
85
+ }
86
+ // ── Issuer config ────────────────────────────────────────────────────────────
87
+ if (cfg.issuer && isAddress(cfg.issuer)) {
88
+ const spin = new Spinner("Fetching issuer config…").start();
89
+ try {
90
+ const [root, verifier] = await Promise.all([
91
+ client.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "merkleRoot" }),
92
+ client.readContract({ address: cfg.issuer, abi: CNF_ABI, functionName: "zkVerifier" }),
93
+ ]);
94
+ spin.stop();
95
+ log.section("Issuer");
96
+ log.kv("address", fmt.cyan(cfg.issuer));
97
+ log.kv("zkVerifier", verifier === "0x0000000000000000000000000000000000000000"
98
+ ? fmt.badge("not set", "red")
99
+ : fmt.green(fmt.addr(verifier)));
100
+ log.kv("merkleRoot", root === 0n
101
+ ? fmt.badge("not set", "red")
102
+ : fmt.gray(root.toString().slice(0, 20) + "…"));
103
+ issuerReady = root !== 0n && verifier !== "0x0000000000000000000000000000000000000000";
104
+ }
105
+ catch (e) {
106
+ spin.stop();
107
+ log.warn(`Could not fetch issuer config: ${e instanceof Error ? e.message.split("\n")[0] : String(e)}`);
108
+ }
109
+ log.line();
110
+ }
111
+ // ── Pool policy ──────────────────────────────────────────────────────────────
112
+ if (cfg.registry && poolId && isAddress(cfg.registry)) {
113
+ const spin = new Spinner("Fetching pool policy…").start();
114
+ try {
115
+ const policy = await client.readContract({
116
+ address: cfg.registry, abi: REGISTRY_ABI,
117
+ functionName: "getPolicy", args: [poolId],
118
+ });
119
+ spin.stop();
120
+ const configured = policy.enabled && policy.cnfIssuer !== "0x0000000000000000000000000000000000000000";
121
+ log.section("Pool Policy");
122
+ log.kv("pool", fmt.hash(poolId));
123
+ log.kv("registry", fmt.cyan(cfg.registry));
124
+ if (configured) {
125
+ log.kv("issuer", fmt.addr(policy.cnfIssuer));
126
+ log.kv("schema", fmt.hash(policy.requiredCredentialType));
127
+ log.kv("status", fmt.badge("configured", "green"));
128
+ }
129
+ else {
130
+ log.kv("status", fmt.badge("missing", "red"));
131
+ log.command("ilal pool policy set --pool " + poolId);
132
+ }
133
+ policyReady = configured;
134
+ }
135
+ catch (e) {
136
+ spin.stop();
137
+ log.warn(`Could not fetch policy: ${e instanceof Error ? e.message.split("\n")[0] : String(e)}`);
138
+ }
139
+ log.line();
140
+ }
141
+ // ── Hint if nothing was shown ────────────────────────────────────────────────
142
+ if (!cfg.wallet && !cfg.issuer && !cfg.registry) {
143
+ log.info("Pass --wallet and --issuer to check credential status.");
144
+ log.info(`Or run ${fmt.cyan("ilal init")} to save your config.`);
145
+ console.log();
146
+ }
147
+ else {
148
+ const checks = [credentialReady, issuerReady, policyReady].filter((v) => v !== undefined);
149
+ if (checks.length > 0) {
150
+ const passed = checks.filter(Boolean).length;
151
+ const readiness = Math.round((passed / checks.length) * 100);
152
+ const tone = readiness >= 85 ? "green" : readiness >= 60 ? "yellow" : "red";
153
+ log.section("Access Verdict");
154
+ log.progress("readiness", readiness, tone);
155
+ if (readiness >= 85) {
156
+ log.callout("Wallet can use ILAL", "credential and pool policy are aligned for hook-gated execution", "green");
157
+ }
158
+ else {
159
+ log.callout("Wallet is not ready", "fix the failing credential, issuer, or policy check above", tone);
160
+ }
161
+ console.log();
162
+ }
163
+ }
164
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * swap.ts — `ilal swap`
3
+ *
4
+ * Execute a compliant token swap through the ILAL channel.
5
+ *
6
+ * Signs a fresh SessionToken internally, then calls ILALRouter.swap()
7
+ * on-chain. The ComplianceHook verifies the session + CNF credential
8
+ * before the swap is executed.
9
+ *
10
+ * Usage:
11
+ * ilal swap \
12
+ * --amount-in 100 \
13
+ * --token-in 0xTOKA \
14
+ * --zero-for-one \
15
+ * --router 0xROUTER \
16
+ * --hook 0xHOOK \
17
+ * --issuer 0xISSUER \
18
+ * --pool-id 0xPOOLID \
19
+ * --chain 84532
20
+ */
21
+ export declare function swap(opts: {
22
+ amountIn: string;
23
+ tokenIn?: string;
24
+ zeroForOne?: boolean;
25
+ poolId?: string;
26
+ router?: string;
27
+ hook?: string;
28
+ issuer?: string;
29
+ tokenA?: string;
30
+ tokenB?: string;
31
+ fee?: string;
32
+ tickSpacing?: string;
33
+ chain?: string;
34
+ rpc?: string;
35
+ privateKey?: string;
36
+ ttl?: string;
37
+ simulate?: boolean;
38
+ }): Promise<void>;
@@ -0,0 +1,284 @@
1
+ /**
2
+ * swap.ts — `ilal swap`
3
+ *
4
+ * Execute a compliant token swap through the ILAL channel.
5
+ *
6
+ * Signs a fresh SessionToken internally, then calls ILALRouter.swap()
7
+ * on-chain. The ComplianceHook verifies the session + CNF credential
8
+ * before the swap is executed.
9
+ *
10
+ * Usage:
11
+ * ilal swap \
12
+ * --amount-in 100 \
13
+ * --token-in 0xTOKA \
14
+ * --zero-for-one \
15
+ * --router 0xROUTER \
16
+ * --hook 0xHOOK \
17
+ * --issuer 0xISSUER \
18
+ * --pool-id 0xPOOLID \
19
+ * --chain 84532
20
+ */
21
+ import { createPublicClient, createWalletClient, encodeAbiParameters, http, isAddress, isHex, parseAbiParameters, parseUnits, } from "viem";
22
+ import { privateKeyToAccount } from "viem/accounts";
23
+ import { base, baseSepolia } from "viem/chains";
24
+ import { fmt, log, header, Spinner, die, dieOnContract } from "../ui.js";
25
+ import { withConfig } from "../config.js";
26
+ const CHAINS = { "8453": base, "84532": baseSepolia };
27
+ // ─── ABIs ─────────────────────────────────────────────────────────────────────
28
+ const ERC20_ABI = [
29
+ { name: "decimals", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "uint8" }] },
30
+ { name: "symbol", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "string" }] },
31
+ { name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "owner", type: "address" }], outputs: [{ type: "uint256" }] },
32
+ { name: "allowance", type: "function", stateMutability: "view", inputs: [{ name: "owner", type: "address" }, { name: "spender", type: "address" }], outputs: [{ type: "uint256" }] },
33
+ { name: "approve", type: "function", stateMutability: "nonpayable", inputs: [{ name: "spender", type: "address" }, { name: "amount", type: "uint256" }], outputs: [{ type: "bool" }] },
34
+ ];
35
+ const ROUTER_ABI = [
36
+ { name: "protocolFeePips", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "uint24" }] },
37
+ { name: "treasury", type: "function", stateMutability: "view", inputs: [], outputs: [{ type: "address" }] },
38
+ {
39
+ name: "swap", type: "function", stateMutability: "payable",
40
+ inputs: [
41
+ { name: "key", type: "tuple", components: [
42
+ { name: "currency0", type: "address" },
43
+ { name: "currency1", type: "address" },
44
+ { name: "fee", type: "uint24" },
45
+ { name: "tickSpacing", type: "int24" },
46
+ { name: "hooks", type: "address" },
47
+ ] },
48
+ { name: "params", type: "tuple", components: [
49
+ { name: "zeroForOne", type: "bool" },
50
+ { name: "amountSpecified", type: "int256" },
51
+ { name: "sqrtPriceLimitX96", type: "uint160" },
52
+ ] },
53
+ { name: "hookData", type: "bytes" },
54
+ ],
55
+ outputs: [{ name: "delta", type: "int256" }],
56
+ },
57
+ ];
58
+ // ─── Session helpers ──────────────────────────────────────────────────────────
59
+ const SESSION_TOKEN_TYPE = [
60
+ { name: "user", type: "address" },
61
+ { name: "authorizedCaller", type: "address" },
62
+ { name: "cnfIssuer", type: "address" },
63
+ { name: "chainId", type: "uint256" },
64
+ { name: "verifyingHook", type: "address" },
65
+ { name: "poolId", type: "bytes32" },
66
+ { name: "action", type: "uint8" },
67
+ { name: "deadline", type: "uint64" },
68
+ { name: "nonce", type: "bytes32" },
69
+ ];
70
+ const HOOK_DATA_ABI = parseAbiParameters([
71
+ "(address user, address authorizedCaller, address cnfIssuer, uint256 chainId, address verifyingHook, bytes32 poolId, uint8 action, uint64 deadline, bytes32 nonce) token",
72
+ "bytes signature",
73
+ ]);
74
+ // sqrtPriceLimitX96 — use min/max to let the swap fill fully
75
+ const MIN_SQRT_PRICE = 4295128740n; // TickMath.MIN_SQRT_PRICE + 1
76
+ const MAX_SQRT_PRICE = 1461446703485210103287273052203988822378723970341n; // MAX - 1
77
+ const DYNAMIC_FEE_FLAG = 8388608;
78
+ const PIPS_DENOMINATOR = 1000000n;
79
+ function txUrl(chain, hash) {
80
+ const baseUrl = chain.blockExplorers?.default?.url;
81
+ return baseUrl ? `${baseUrl}/tx/${hash}` : undefined;
82
+ }
83
+ function feeLabel(fee) {
84
+ if (fee === DYNAMIC_FEE_FLAG)
85
+ return `${fmt.badge("fair flow", "green")} verified swap fee 0.05%`;
86
+ return `${fmt.badge("static", "gray")} ${(fee / 10_000).toFixed(4).replace(/0+$/, "").replace(/\.$/, "")}%`;
87
+ }
88
+ function pipsToPercent(pips) {
89
+ return `${(pips / 10_000).toFixed(4).replace(/0+$/, "").replace(/\.$/, "")}%`;
90
+ }
91
+ function poolFeePercent(fee) {
92
+ return fee === DYNAMIC_FEE_FLAG
93
+ ? "0.05%"
94
+ : `${(fee / 10_000).toFixed(4).replace(/0+$/, "").replace(/\.$/, "")}%`;
95
+ }
96
+ // ─── Main export ──────────────────────────────────────────────────────────────
97
+ export async function swap(opts) {
98
+ const cfg = withConfig(opts);
99
+ const rawKey = cfg.privateKey ?? process.env["PRIVATE_KEY"];
100
+ if (!rawKey)
101
+ die("Private key required. Use --private-key or set PRIVATE_KEY env var.");
102
+ if (!cfg.router)
103
+ die("ILALRouter address required. Use --router or set in .ilal.json");
104
+ if (!cfg.hook)
105
+ die("ComplianceHook address required. Use --hook or set in .ilal.json");
106
+ if (!cfg.issuer)
107
+ die("CNFIssuer address required. Use --issuer or set in .ilal.json");
108
+ if (!cfg.poolId)
109
+ die("Pool ID required. Use --pool-id or set in .ilal.json");
110
+ if (!isAddress(cfg.router))
111
+ die(`Invalid router address: ${cfg.router}`);
112
+ if (!isAddress(cfg.hook))
113
+ die(`Invalid hook address: ${cfg.hook}`);
114
+ if (!isAddress(cfg.issuer))
115
+ die(`Invalid issuer address: ${cfg.issuer}`);
116
+ if (!isHex(cfg.poolId) || cfg.poolId.length !== 66)
117
+ die("poolId must be 0x + 64 hex chars");
118
+ const chain = CHAINS[cfg.chain ?? "84532"] ?? baseSepolia;
119
+ const account = privateKeyToAccount(rawKey);
120
+ const transport = cfg.rpc ? http(cfg.rpc) : http();
121
+ const pubClient = createPublicClient({ chain, transport });
122
+ const walClient = createWalletClient({ account, chain, transport });
123
+ // Determine token order
124
+ const tokenA = (cfg.tokenA ?? opts.tokenA);
125
+ const tokenB = (cfg.tokenB ?? opts.tokenB);
126
+ if (!tokenA || !tokenB)
127
+ die("Token addresses required. Use --token-a/--token-b or set in .ilal.json");
128
+ // currency0 < currency1
129
+ const c0 = tokenA.toLowerCase() < tokenB.toLowerCase() ? tokenA : tokenB;
130
+ const c1 = tokenA.toLowerCase() < tokenB.toLowerCase() ? tokenB : tokenA;
131
+ const tokenIn = (opts.tokenIn ?? tokenA);
132
+ if (!isAddress(tokenIn))
133
+ die(`Invalid token-in address: ${tokenIn}`);
134
+ const zeroForOne = tokenIn.toLowerCase() === c0.toLowerCase();
135
+ header("ILAL Swap", chain.name);
136
+ log.kv("router", fmt.cyan(cfg.router));
137
+ log.kv("hook", fmt.cyan(cfg.hook));
138
+ log.kv("pool", fmt.gray(cfg.poolId.slice(0, 18) + "…"));
139
+ log.kv("tokenIn", fmt.cyan(tokenIn));
140
+ log.kv("direction", zeroForOne ? "currency0 → currency1" : "currency1 → currency0");
141
+ log.line();
142
+ // Fetch token decimals + symbol
143
+ const spin = new Spinner("Fetching token info…").start();
144
+ const [decimals, symbol] = await Promise.all([
145
+ pubClient.readContract({ address: tokenIn, abi: ERC20_ABI, functionName: "decimals" }),
146
+ pubClient.readContract({ address: tokenIn, abi: ERC20_ABI, functionName: "symbol" }),
147
+ ]);
148
+ spin.stop();
149
+ const amountIn = parseUnits(opts.amountIn, decimals);
150
+ log.kv("amount", `${opts.amountIn} ${fmt.cyan(symbol)} (${amountIn.toString()} wei)`);
151
+ let protocolFeePips = 0;
152
+ let treasury;
153
+ try {
154
+ [protocolFeePips, treasury] = await Promise.all([
155
+ pubClient.readContract({ address: cfg.router, abi: ROUTER_ABI, functionName: "protocolFeePips" }),
156
+ pubClient.readContract({ address: cfg.router, abi: ROUTER_ABI, functionName: "treasury" }),
157
+ ]);
158
+ }
159
+ catch {
160
+ protocolFeePips = 0;
161
+ }
162
+ const protocolFeeAmount = amountIn * BigInt(protocolFeePips) / PIPS_DENOMINATOR;
163
+ const totalDebit = amountIn + protocolFeeAmount;
164
+ log.deal([
165
+ { label: "verified input", value: `${opts.amountIn} ${symbol}`, note: "exact-in swap", tone: "cyan" },
166
+ { label: "LP fee", value: poolFeePercent(parseInt(cfg.fee ?? "3000")), note: "hook-priced flow", tone: "green" },
167
+ { label: "ILAL fee", value: protocolFeePips > 0 ? pipsToPercent(protocolFeePips) : "off", note: protocolFeePips > 0 ? "protocol revenue" : "legacy router", tone: protocolFeePips > 0 ? "cyan" : "gray" },
168
+ ]);
169
+ log.line();
170
+ // Check allowance — approve if needed
171
+ const approveSpin = new Spinner("Checking allowance…").start();
172
+ const allowed = await pubClient.readContract({
173
+ address: tokenIn,
174
+ abi: ERC20_ABI,
175
+ functionName: "allowance",
176
+ args: [account.address, cfg.router],
177
+ });
178
+ if (allowed < totalDebit) {
179
+ approveSpin.update(`Approving ${symbol} for ILALRouter…`);
180
+ const approveHash = await walClient.writeContract({
181
+ address: tokenIn,
182
+ abi: ERC20_ABI,
183
+ functionName: "approve",
184
+ args: [cfg.router, totalDebit * 10n], // approve 10× for future swaps
185
+ });
186
+ await pubClient.waitForTransactionReceipt({ hash: approveHash });
187
+ approveSpin.succeed(`Approved ${symbol} ${fmt.gray(fmt.hash(approveHash))}`);
188
+ }
189
+ else {
190
+ approveSpin.succeed(`Allowance ok (${fmt.gray(allowed.toString())} wei)`);
191
+ }
192
+ // Sign session token
193
+ const signSpin = new Spinner("Signing session token…").start();
194
+ const ttl = parseInt(opts.ttl ?? "600");
195
+ const deadline = BigInt(Math.floor(Date.now() / 1000) + ttl);
196
+ const nonce = `0x${Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString("hex")}`;
197
+ const token = {
198
+ user: account.address,
199
+ authorizedCaller: cfg.router,
200
+ cnfIssuer: cfg.issuer,
201
+ chainId: BigInt(chain.id),
202
+ verifyingHook: cfg.hook,
203
+ poolId: cfg.poolId,
204
+ action: 1, // ACTION_SWAP
205
+ deadline,
206
+ nonce,
207
+ };
208
+ const signature = await walClient.signTypedData({
209
+ account,
210
+ domain: {
211
+ name: "ILAL ComplianceHook",
212
+ version: "1",
213
+ chainId: BigInt(chain.id),
214
+ verifyingContract: cfg.hook,
215
+ },
216
+ types: { SessionToken: SESSION_TOKEN_TYPE },
217
+ primaryType: "SessionToken",
218
+ message: token,
219
+ });
220
+ const hookData = encodeAbiParameters(HOOK_DATA_ABI, [token, signature]);
221
+ signSpin.succeed(`Session signed (expires in ${ttl}s)`);
222
+ const fee = parseInt(cfg.fee ?? "3000");
223
+ const tickSpacing = parseInt(cfg.tickSpacing ?? "60");
224
+ log.section("Gate Checks");
225
+ log.kv("credential", `${fmt.badge("required", "cyan")} issuer ${fmt.addr(cfg.issuer)}`);
226
+ log.kv("caller", `${fmt.badge("bound", "green")} ${fmt.addr(cfg.router)}`);
227
+ log.kv("nonce", `${fmt.badge("fresh", "green")} ${fmt.hash(nonce)}`);
228
+ log.kv("fee", feeLabel(fee));
229
+ if (protocolFeePips > 0) {
230
+ log.kv("protocol fee", `${fmt.badge("ILAL", "cyan")} ${pipsToPercent(protocolFeePips)} to ${treasury ? fmt.addr(treasury) : "treasury"}`);
231
+ log.kv("total debit", `${totalDebit.toString()} wei (${symbol} input + ILAL fee)`);
232
+ }
233
+ log.line();
234
+ if (opts.simulate) {
235
+ log.ok("Simulation mode — skipping on-chain tx");
236
+ log.kv("hookData", hookData.slice(0, 22) + "…");
237
+ console.log();
238
+ return;
239
+ }
240
+ // Build PoolKey
241
+ const poolKey = {
242
+ currency0: c0,
243
+ currency1: c1,
244
+ fee,
245
+ tickSpacing,
246
+ hooks: cfg.hook,
247
+ };
248
+ const swapParams = {
249
+ zeroForOne,
250
+ amountSpecified: -amountIn, // negative = exactIn
251
+ sqrtPriceLimitX96: zeroForOne ? MIN_SQRT_PRICE : MAX_SQRT_PRICE,
252
+ };
253
+ // Execute swap
254
+ const txSpin = new Spinner("Sending swap tx…").start();
255
+ let txHash;
256
+ try {
257
+ txHash = await walClient.writeContract({
258
+ address: cfg.router,
259
+ abi: ROUTER_ABI,
260
+ functionName: "swap",
261
+ args: [poolKey, swapParams, hookData],
262
+ value: 0n,
263
+ });
264
+ txSpin.update(`Confirming ${fmt.gray(fmt.hash(txHash))}…`);
265
+ const receipt = await pubClient.waitForTransactionReceipt({ hash: txHash });
266
+ if (receipt.status !== "success") {
267
+ txSpin.fail("Transaction reverted");
268
+ die(`Tx failed: ${txHash}`);
269
+ }
270
+ txSpin.succeed(fmt.bold(fmt.green(`Swap executed via ILAL channel ✓`)));
271
+ }
272
+ catch (e) {
273
+ txSpin.fail("Swap failed");
274
+ dieOnContract(e);
275
+ }
276
+ log.line();
277
+ log.callout("Hook-enforced swap", "credential, session, caller binding, and nonce all passed on-chain", "green");
278
+ log.kv("tx", fmt.gray(txHash));
279
+ log.kv("block", fmt.gray((await pubClient.getTransactionReceipt({ hash: txHash })).blockNumber.toString()));
280
+ const explorer = txUrl(chain, txHash);
281
+ if (explorer)
282
+ log.kv("explorer", fmt.cyan(explorer));
283
+ console.log();
284
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * config.ts — ILAL CLI project configuration.
3
+ *
4
+ * Loads from (highest priority first):
5
+ * 1. CLI flags (handled by Commander, passed directly)
6
+ * 2. Environment variables (ILAL_ISSUER, ILAL_CHAIN, etc.)
7
+ * 3. .ilal.json in current dir or any parent dir
8
+ */
9
+ export interface ILALConfig {
10
+ issuer?: string;
11
+ hook?: string;
12
+ registry?: string;
13
+ router?: string;
14
+ treasury?: string;
15
+ tokenA?: string;
16
+ tokenB?: string;
17
+ poolId?: string;
18
+ fee?: string;
19
+ tickSpacing?: string;
20
+ chain?: string;
21
+ rpc?: string;
22
+ circuitDir?: string;
23
+ outDir?: string;
24
+ }
25
+ export declare function loadConfig(): ILALConfig;
26
+ /** Merge CLI flags over config (undefined flags don't override) */
27
+ export declare function withConfig<T extends Partial<ILALConfig>>(flags: T): T & ILALConfig;
28
+ export declare function writeConfig(config: ILALConfig, dir?: string): string;
29
+ export declare function configFilePath(): string | null;
package/dist/config.js ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * config.ts — ILAL CLI project configuration.
3
+ *
4
+ * Loads from (highest priority first):
5
+ * 1. CLI flags (handled by Commander, passed directly)
6
+ * 2. Environment variables (ILAL_ISSUER, ILAL_CHAIN, etc.)
7
+ * 3. .ilal.json in current dir or any parent dir
8
+ */
9
+ import { readFileSync, writeFileSync, existsSync } from "fs";
10
+ import { resolve, dirname } from "path";
11
+ const CONFIG_FILE = ".ilal.json";
12
+ // ─── Find config file ─────────────────────────────────────────────────────────
13
+ function findConfigFile(startDir = process.cwd()) {
14
+ let dir = startDir;
15
+ for (let i = 0; i < 6; i++) {
16
+ const candidate = resolve(dir, CONFIG_FILE);
17
+ if (existsSync(candidate))
18
+ return candidate;
19
+ const parent = dirname(dir);
20
+ if (parent === dir)
21
+ break;
22
+ dir = parent;
23
+ }
24
+ return null;
25
+ }
26
+ // ─── Load config ──────────────────────────────────────────────────────────────
27
+ let _config = null;
28
+ export function loadConfig() {
29
+ if (_config)
30
+ return _config;
31
+ // Start with file config
32
+ const filePath = findConfigFile();
33
+ let fileConfig = {};
34
+ if (filePath) {
35
+ try {
36
+ fileConfig = JSON.parse(readFileSync(filePath, "utf8"));
37
+ }
38
+ catch { /* malformed, ignore */ }
39
+ }
40
+ // Overlay env vars
41
+ _config = {
42
+ issuer: process.env["ILAL_ISSUER"] ?? fileConfig.issuer,
43
+ hook: process.env["ILAL_HOOK"] ?? fileConfig.hook,
44
+ registry: process.env["ILAL_REGISTRY"] ?? fileConfig.registry,
45
+ router: process.env["ILAL_ROUTER"] ?? fileConfig.router,
46
+ treasury: process.env["ILAL_TREASURY"] ?? fileConfig.treasury,
47
+ tokenA: process.env["ILAL_TOKEN_A"] ?? fileConfig.tokenA,
48
+ tokenB: process.env["ILAL_TOKEN_B"] ?? fileConfig.tokenB,
49
+ poolId: process.env["ILAL_POOL_ID"] ?? fileConfig.poolId,
50
+ fee: process.env["ILAL_FEE"] ?? fileConfig.fee,
51
+ tickSpacing: process.env["ILAL_TICK_SPACING"] ?? fileConfig.tickSpacing,
52
+ chain: process.env["ILAL_CHAIN"] ?? fileConfig.chain,
53
+ rpc: process.env["ILAL_RPC"] ?? fileConfig.rpc,
54
+ circuitDir: process.env["ILAL_CIRCUIT_DIR"] ?? fileConfig.circuitDir,
55
+ outDir: process.env["ILAL_OUT_DIR"] ?? fileConfig.outDir,
56
+ };
57
+ return _config;
58
+ }
59
+ /** Merge CLI flags over config (undefined flags don't override) */
60
+ export function withConfig(flags) {
61
+ const cfg = loadConfig();
62
+ return {
63
+ ...cfg,
64
+ ...Object.fromEntries(Object.entries(flags).filter(([, v]) => v !== undefined)),
65
+ };
66
+ }
67
+ // ─── Write config ─────────────────────────────────────────────────────────────
68
+ export function writeConfig(config, dir = process.cwd()) {
69
+ const path = resolve(dir, CONFIG_FILE);
70
+ writeFileSync(path, JSON.stringify(config, null, 2) + "\n");
71
+ return path;
72
+ }
73
+ export function configFilePath() {
74
+ return findConfigFile();
75
+ }
@@ -0,0 +1,5 @@
1
+ export declare const EAS_ADDRESSES: Record<number, `0x${string}`>;
2
+ export declare const COINBASE_ATTESTER: "0x357458739F90461b99789350868CD7CF330Dd7EE";
3
+ export declare const COINBASE_SCHEMA_UID: "0xf8b05c79f090979bf4a80270aba232dff11a10d9ca55c4f88de95317970f0de9";
4
+ export declare const COINBASE_COUNTRY_SCHEMA: "0x1801901fabd0e6189356b4fb52bb0ab855276d84f7ec140839fbd1f6801ca065";
5
+ export declare const DEFAULT_LIFETIME_DAYS = 90;
@@ -0,0 +1,14 @@
1
+ // EAS is an OP Stack predeploy — same address on Base mainnet and Base Sepolia
2
+ // Source: github.com/coinbase/verifications, docs.attest.org/docs/quick--start/contracts
3
+ export const EAS_ADDRESSES = {
4
+ 8453: "0x4200000000000000000000000000000000000021", // Base mainnet
5
+ 84532: "0x4200000000000000000000000000000000000021", // Base Sepolia
6
+ };
7
+ // Coinbase Verifications — Base mainnet only (no Sepolia equivalent)
8
+ // Source: github.com/coinbase/verifications
9
+ export const COINBASE_ATTESTER = "0x357458739F90461b99789350868CD7CF330Dd7EE";
10
+ // Verified Account (primary KYC schema)
11
+ export const COINBASE_SCHEMA_UID = "0xf8b05c79f090979bf4a80270aba232dff11a10d9ca55c4f88de95317970f0de9";
12
+ // Verified Country (jurisdiction schema — for Phase 3 policy extensions)
13
+ export const COINBASE_COUNTRY_SCHEMA = "0x1801901fabd0e6189356b4fb52bb0ab855276d84f7ec140839fbd1f6801ca065";
14
+ export const DEFAULT_LIFETIME_DAYS = 90;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};