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