@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.
- package/LICENSE +201 -0
- package/README.md +99 -0
- package/dist/commands/credential.d.ts +6 -0
- package/dist/commands/credential.js +78 -0
- package/dist/commands/demo.d.ts +7 -0
- package/dist/commands/demo.js +346 -0
- package/dist/commands/deploy.d.ts +10 -0
- package/dist/commands/deploy.js +94 -0
- package/dist/commands/init.d.ts +22 -0
- package/dist/commands/init.js +88 -0
- package/dist/commands/liquidity.d.ts +51 -0
- package/dist/commands/liquidity.js +249 -0
- package/dist/commands/mint.d.ts +11 -0
- package/dist/commands/mint.js +146 -0
- package/dist/commands/pool.d.ts +15 -0
- package/dist/commands/pool.js +115 -0
- package/dist/commands/proof.d.ts +16 -0
- package/dist/commands/proof.js +132 -0
- package/dist/commands/prove.d.ts +26 -0
- package/dist/commands/prove.js +292 -0
- package/dist/commands/session.d.ts +11 -0
- package/dist/commands/session.js +105 -0
- package/dist/commands/status.d.ts +14 -0
- package/dist/commands/status.js +164 -0
- package/dist/commands/swap.d.ts +38 -0
- package/dist/commands/swap.js +284 -0
- package/dist/config.d.ts +29 -0
- package/dist/config.js +75 -0
- package/dist/constants.d.ts +5 -0
- package/dist/constants.js +14 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +278 -0
- package/dist/ui.d.ts +59 -0
- package/dist/ui.js +218 -0
- package/package.json +48 -0
|
@@ -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
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -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;
|
package/dist/index.d.ts
ADDED