perp-cli 0.4.8 → 0.4.10

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.
Files changed (45) hide show
  1. package/README.md +44 -5
  2. package/dist/arb/reconcile.d.ts +28 -0
  3. package/dist/arb/reconcile.js +162 -0
  4. package/dist/commands/account.d.ts +1 -1
  5. package/dist/commands/account.js +366 -62
  6. package/dist/commands/agent.js +9 -5
  7. package/dist/commands/arb/index.js +243 -49
  8. package/dist/commands/arb-auto.js +67 -19
  9. package/dist/commands/arb.d.ts +2 -1
  10. package/dist/commands/arb.js +535 -131
  11. package/dist/commands/bot.js +207 -0
  12. package/dist/commands/funds.d.ts +4 -0
  13. package/dist/commands/funds.js +741 -0
  14. package/dist/commands/history.d.ts +2 -1
  15. package/dist/commands/history.js +508 -5
  16. package/dist/commands/manage.js +5 -3
  17. package/dist/commands/market.js +34 -1
  18. package/dist/commands/pnl.js +1 -1
  19. package/dist/commands/risk.d.ts +2 -0
  20. package/dist/commands/risk.js +56 -0
  21. package/dist/commands/settings.js +147 -0
  22. package/dist/commands/trade.d.ts +1 -1
  23. package/dist/commands/trade.js +241 -259
  24. package/dist/commands/wallet.d.ts +4 -0
  25. package/dist/commands/wallet.js +10 -0
  26. package/dist/config.d.ts +2 -2
  27. package/dist/config.js +11 -3
  28. package/dist/exchanges/hyperliquid-spot.d.ts +1 -0
  29. package/dist/exchanges/hyperliquid-spot.js +13 -21
  30. package/dist/exchanges/lighter-spot.js +13 -15
  31. package/dist/exchanges/lighter.js +82 -21
  32. package/dist/exchanges/pacifica.js +12 -0
  33. package/dist/execution-log.d.ts +1 -1
  34. package/dist/index.js +39 -123
  35. package/dist/mcp-server.js +43 -96
  36. package/dist/strategies/split-order.d.ts +49 -0
  37. package/dist/strategies/split-order.js +138 -0
  38. package/package.json +1 -1
  39. package/skills/perp-cli/SKILL.md +13 -26
  40. package/skills/perp-cli/references/agent-operations.md +19 -9
  41. package/skills/perp-cli/references/commands.md +38 -31
  42. package/skills/perp-cli/references/strategies.md +26 -16
  43. package/skills/perp-cli/scripts/funding-analysis.sh +2 -2
  44. package/skills/perp-cli/scripts/preflight.sh +1 -1
  45. package/skills/perp-cli/scripts/spot-perp-scan.sh +1 -1
@@ -0,0 +1,741 @@
1
+ import chalk from "chalk";
2
+ import { printJson, formatUsd, jsonOk } from "../utils.js";
3
+ import { PacificaAdapter } from "../exchanges/pacifica.js";
4
+ import { HyperliquidAdapter } from "../exchanges/hyperliquid.js";
5
+ import { logExecution } from "../execution-log.js";
6
+ const DEFAULT_RELAYER = "http://localhost:3100";
7
+ function getRelayerUrl() {
8
+ return process.env.PERP_RELAYER_URL || DEFAULT_RELAYER;
9
+ }
10
+ async function relayerAvailable() {
11
+ try {
12
+ const res = await fetch(`${getRelayerUrl()}/health`, { signal: AbortSignal.timeout(2000) });
13
+ return res.ok;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }
19
+ export function registerFundsCommands(program, getAdapter, isJson, getNetwork) {
20
+ const funds = program.command("funds").description("Deposit, withdraw, bridge & transfer funds");
21
+ // ═══════════════════════════════════════════════════════
22
+ // DEPOSIT
23
+ // ═══════════════════════════════════════════════════════
24
+ const deposit = funds.command("deposit").description("Deposit funds into exchange accounts");
25
+ // ── Pacifica (Solana) ──
26
+ deposit
27
+ .command("pacifica <amount>")
28
+ .description("Deposit USDC into Pacifica")
29
+ .option("--no-relay", "Skip relayer, pay gas yourself")
30
+ .action(async (amount, opts) => {
31
+ const amountNum = parseFloat(amount);
32
+ if (isNaN(amountNum) || amountNum <= 0)
33
+ throw new Error("Invalid amount");
34
+ if (amountNum < 10)
35
+ throw new Error("Minimum deposit is 10 USDC");
36
+ // Try relayer first (gas-sponsored)
37
+ if (opts.relay && await relayerAvailable()) {
38
+ if (!isJson())
39
+ console.log(chalk.cyan(`\n Depositing $${formatUsd(amountNum)} via relayer (gasless)...\n`));
40
+ const adapter = await getAdapter();
41
+ if (!(adapter instanceof PacificaAdapter))
42
+ throw new Error("Requires --exchange pacifica");
43
+ try {
44
+ const buildRes = await fetch(`${getRelayerUrl()}/deposit/pacifica/build`, {
45
+ method: "POST",
46
+ headers: { "Content-Type": "application/json" },
47
+ body: JSON.stringify({
48
+ userPubkey: adapter.keypair.publicKey.toBase58(),
49
+ amount: amountNum,
50
+ testnet: getNetwork() === "testnet",
51
+ }),
52
+ });
53
+ if (!buildRes.ok) {
54
+ const err = await buildRes.json();
55
+ throw new Error(err.error || "Relayer build failed");
56
+ }
57
+ const { transaction, fee, netAmount } = (await buildRes.json());
58
+ if (!isJson()) {
59
+ console.log(` Fee: $${formatUsd(fee)} (gas sponsored)`);
60
+ console.log(` Net: $${formatUsd(netAmount)} to Pacifica\n`);
61
+ }
62
+ const { Transaction } = await import("@solana/web3.js");
63
+ const tx = Transaction.from(Buffer.from(transaction, "base64"));
64
+ tx.partialSign(adapter.keypair);
65
+ const signed = tx.serialize().toString("base64");
66
+ const submitRes = await fetch(`${getRelayerUrl()}/deposit/pacifica/submit`, {
67
+ method: "POST",
68
+ headers: { "Content-Type": "application/json" },
69
+ body: JSON.stringify({
70
+ signedTransaction: signed,
71
+ testnet: getNetwork() === "testnet",
72
+ }),
73
+ });
74
+ if (!submitRes.ok) {
75
+ const err = await submitRes.json();
76
+ throw new Error(err.error || "Submit failed");
77
+ }
78
+ const result = (await submitRes.json());
79
+ logExecution({
80
+ type: "bridge", exchange: "pacifica", symbol: "USDC", side: "deposit",
81
+ size: String(amountNum), status: "success", dryRun: false,
82
+ meta: { action: "deposit", method: "relay", signature: result.signature },
83
+ });
84
+ if (isJson())
85
+ return printJson(jsonOk({ ...result, fee, netAmount, method: "relay" }));
86
+ console.log(chalk.green(` Deposit confirmed!`));
87
+ console.log(` Signature: ${result.signature}`);
88
+ console.log(chalk.gray(`\n Gas: FREE (relayer sponsored)\n`));
89
+ return;
90
+ }
91
+ catch (err) {
92
+ logExecution({
93
+ type: "bridge", exchange: "pacifica", symbol: "USDC", side: "deposit",
94
+ size: String(amountNum), status: "failed", dryRun: false,
95
+ error: err instanceof Error ? err.message : String(err),
96
+ meta: { action: "deposit", method: "relay" },
97
+ });
98
+ throw err;
99
+ }
100
+ }
101
+ // Fallback: direct deposit (user pays gas)
102
+ if (!isJson()) {
103
+ console.log(chalk.cyan(`\n Depositing $${formatUsd(amountNum)} USDC into Pacifica...\n`));
104
+ console.log(chalk.gray(" Requires: SOL for gas (~0.005 SOL) + USDC on Solana\n"));
105
+ }
106
+ const adapter = await getAdapter();
107
+ if (!(adapter instanceof PacificaAdapter))
108
+ throw new Error("Requires --exchange pacifica");
109
+ try {
110
+ const { Connection, Transaction } = await import("@solana/web3.js");
111
+ const { buildDepositInstruction } = await import("../pacifica/deposit.js");
112
+ const network = getNetwork();
113
+ const rpcUrl = network === "testnet" ? "https://api.devnet.solana.com" : "https://api.mainnet-beta.solana.com";
114
+ const connection = new Connection(rpcUrl, "confirmed");
115
+ const solBalance = await connection.getBalance(adapter.keypair.publicKey);
116
+ if (solBalance < 5_000_000) {
117
+ console.error(chalk.red(" Insufficient SOL for gas. Need ~0.005 SOL."));
118
+ console.error(chalk.gray(" Tip: Use relayer for gasless deposits (start relayer server)\n"));
119
+ process.exit(1);
120
+ }
121
+ const { PublicKey } = await import("@solana/web3.js");
122
+ const USDC_MINT = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
123
+ const tokenAccounts = await connection.getParsedTokenAccountsByOwner(adapter.keypair.publicKey, { mint: USDC_MINT });
124
+ const usdcBalance = tokenAccounts.value.length > 0
125
+ ? tokenAccounts.value[0].account.data.parsed.info.tokenAmount.uiAmount ?? 0
126
+ : 0;
127
+ if (usdcBalance < amountNum) {
128
+ console.error(chalk.red(` Insufficient USDC on Solana. Have: $${formatUsd(usdcBalance)}, need: $${formatUsd(amountNum)}`));
129
+ process.exit(1);
130
+ }
131
+ const ix = await buildDepositInstruction(adapter.keypair.publicKey, amountNum, network);
132
+ const tx = new Transaction().add(ix);
133
+ tx.feePayer = adapter.keypair.publicKey;
134
+ tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
135
+ tx.sign(adapter.keypair);
136
+ const sig = await connection.sendRawTransaction(tx.serialize());
137
+ await connection.confirmTransaction(sig, "confirmed");
138
+ logExecution({
139
+ type: "bridge", exchange: "pacifica", symbol: "USDC", side: "deposit",
140
+ size: String(amountNum), status: "success", dryRun: false,
141
+ meta: { action: "deposit", method: "direct", signature: sig },
142
+ });
143
+ if (isJson())
144
+ return printJson(jsonOk({ signature: sig, amount: amountNum, method: "direct" }));
145
+ console.log(chalk.green(` Deposit confirmed!`));
146
+ console.log(` Amount: $${formatUsd(amountNum)} USDC`);
147
+ console.log(` Signature: ${sig}\n`);
148
+ }
149
+ catch (err) {
150
+ logExecution({
151
+ type: "bridge", exchange: "pacifica", symbol: "USDC", side: "deposit",
152
+ size: String(amountNum), status: "failed", dryRun: false,
153
+ error: err instanceof Error ? err.message : String(err),
154
+ meta: { action: "deposit", method: "direct" },
155
+ });
156
+ throw err;
157
+ }
158
+ });
159
+ // ── Hyperliquid (Arbitrum — USDC transfer to Bridge2) ──
160
+ deposit
161
+ .command("hyperliquid <amount>")
162
+ .description("Deposit USDC into Hyperliquid (Arbitrum)")
163
+ .option("--no-relay", "Skip relayer, pay gas yourself")
164
+ .action(async (amount, opts) => {
165
+ const amountNum = parseFloat(amount);
166
+ if (isNaN(amountNum) || amountNum <= 0)
167
+ throw new Error("Invalid amount");
168
+ if (amountNum < 5)
169
+ throw new Error("Minimum deposit is $5 for Hyperliquid");
170
+ // Try relayer
171
+ if (opts.relay && await relayerAvailable()) {
172
+ if (!isJson())
173
+ console.log(chalk.cyan(`\n Depositing $${formatUsd(amountNum)} via relayer (gasless)...\n`));
174
+ const { ethers } = await import("ethers");
175
+ const { loadPrivateKey } = await import("../config.js");
176
+ const pk = await loadPrivateKey("hyperliquid");
177
+ const wallet = new ethers.Wallet(pk);
178
+ try {
179
+ const res = await fetch(`${getRelayerUrl()}/deposit/hyperliquid`, {
180
+ method: "POST",
181
+ headers: { "Content-Type": "application/json" },
182
+ body: JSON.stringify({ userAddress: wallet.address, amount: amountNum }),
183
+ });
184
+ const result = (await res.json());
185
+ if (!res.ok)
186
+ throw new Error(String(result.error || "Relayer failed"));
187
+ logExecution({
188
+ type: "bridge", exchange: "hyperliquid", symbol: "USDC", side: "deposit",
189
+ size: String(amountNum), status: "success", dryRun: false,
190
+ meta: { action: "deposit", method: "relay", txHash: String(result.txHash) },
191
+ });
192
+ if (isJson())
193
+ return printJson(jsonOk({ ...result, method: "relay" }));
194
+ console.log(chalk.green(` Deposit confirmed!`));
195
+ console.log(` TX Hash: ${result.txHash}`);
196
+ console.log(` Fee: $${formatUsd(Number(result.fee))}`);
197
+ console.log(` Net: $${formatUsd(Number(result.netAmount))}`);
198
+ console.log(chalk.gray(`\n Gas: FREE (relayer sponsored)\n`));
199
+ return;
200
+ }
201
+ catch (err) {
202
+ logExecution({
203
+ type: "bridge", exchange: "hyperliquid", symbol: "USDC", side: "deposit",
204
+ size: String(amountNum), status: "failed", dryRun: false,
205
+ error: err instanceof Error ? err.message : String(err),
206
+ meta: { action: "deposit", method: "relay" },
207
+ });
208
+ throw err;
209
+ }
210
+ }
211
+ // Direct: USDC.transfer() to HL Bridge2
212
+ if (!isJson()) {
213
+ console.log(chalk.cyan(`\n Depositing $${formatUsd(amountNum)} USDC into Hyperliquid...\n`));
214
+ console.log(chalk.gray(" Requires: ETH on Arbitrum for gas (~0.0001 ETH) + USDC on Arbitrum\n"));
215
+ }
216
+ const { ethers } = await import("ethers");
217
+ const { loadPrivateKey } = await import("../config.js");
218
+ const pk = await loadPrivateKey("hyperliquid");
219
+ const provider = new ethers.JsonRpcProvider("https://arb1.arbitrum.io/rpc");
220
+ const wallet = new ethers.Wallet(pk, provider);
221
+ const HL_BRIDGE = "0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7";
222
+ const USDC_ARB = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831";
223
+ const ethBal = await provider.getBalance(wallet.address);
224
+ if (ethBal < ethers.parseEther("0.0001")) {
225
+ console.error(chalk.red(" Insufficient ETH on Arbitrum for gas."));
226
+ process.exit(1);
227
+ }
228
+ const usdc = new ethers.Contract(USDC_ARB, [
229
+ "function transfer(address to, uint256 amount) returns (bool)",
230
+ "function balanceOf(address) view returns (uint256)",
231
+ ], wallet);
232
+ const amountRaw = ethers.parseUnits(amount, 6);
233
+ const usdcBal = await usdc.balanceOf(wallet.address);
234
+ if (usdcBal < amountRaw) {
235
+ console.error(chalk.red(` Insufficient USDC. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
236
+ process.exit(1);
237
+ }
238
+ try {
239
+ if (!isJson())
240
+ console.log(chalk.gray(" Sending USDC to Hyperliquid Bridge2..."));
241
+ const tx = await usdc.transfer(HL_BRIDGE, amountRaw);
242
+ const receipt = await tx.wait();
243
+ logExecution({
244
+ type: "bridge", exchange: "hyperliquid", symbol: "USDC", side: "deposit",
245
+ size: String(amountNum), status: "success", dryRun: false,
246
+ meta: { action: "deposit", method: "direct", txHash: receipt.hash },
247
+ });
248
+ if (isJson())
249
+ return printJson(jsonOk({ txHash: receipt.hash, amount: amountNum, method: "direct" }));
250
+ console.log(chalk.green(` Deposit confirmed!`));
251
+ console.log(` Amount: $${formatUsd(amountNum)} USDC`);
252
+ console.log(` TX Hash: ${receipt.hash}`);
253
+ console.log(chalk.gray(`\n Funds appear in ~1-3 minutes.\n`));
254
+ }
255
+ catch (err) {
256
+ logExecution({
257
+ type: "bridge", exchange: "hyperliquid", symbol: "USDC", side: "deposit",
258
+ size: String(amountNum), status: "failed", dryRun: false,
259
+ error: err instanceof Error ? err.message : String(err),
260
+ meta: { action: "deposit", method: "direct" },
261
+ });
262
+ throw err;
263
+ }
264
+ });
265
+ // ── Lighter ──
266
+ const lighterDeposit = deposit.command("lighter").description("Deposit USDC into Lighter");
267
+ lighterDeposit
268
+ .command("ethereum <amount>")
269
+ .description("Deposit USDC via Ethereum L1 (min 1 USDC, gas: $3-10+)")
270
+ .option("--asset-id <id>", "Asset index (default: 2 = USDC)", "2")
271
+ .option("--route <type>", "Route: 0=perps, 1=spot (default: 0)", "0")
272
+ .action(async (amount, opts) => {
273
+ const amountNum = parseFloat(amount);
274
+ if (isNaN(amountNum) || amountNum <= 0)
275
+ throw new Error("Invalid amount");
276
+ if (amountNum < 1)
277
+ throw new Error("Minimum deposit via Ethereum is 1 USDC");
278
+ if (!isJson()) {
279
+ console.log(chalk.cyan(`\n Depositing $${formatUsd(amountNum)} USDC into Lighter via Ethereum L1...\n`));
280
+ console.log(chalk.yellow(" Warning: Ethereum L1 gas fees may be high ($3-10+)\n"));
281
+ }
282
+ const { ethers } = await import("ethers");
283
+ const { loadPrivateKey } = await import("../config.js");
284
+ const pk = await loadPrivateKey("lighter");
285
+ const provider = new ethers.JsonRpcProvider("https://eth.llamarpc.com");
286
+ const wallet = new ethers.Wallet(pk, provider);
287
+ const LIGHTER_CONTRACT = "0x3B4D794a66304f130a4db8f2551b0070dfcf5ca7";
288
+ const USDC_ETH = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
289
+ const usdc = new ethers.Contract(USDC_ETH, [
290
+ "function approve(address spender, uint256 amount) returns (bool)",
291
+ "function allowance(address owner, address spender) view returns (uint256)",
292
+ "function balanceOf(address) view returns (uint256)",
293
+ ], wallet);
294
+ const lighter = new ethers.Contract(LIGHTER_CONTRACT, [
295
+ "function deposit(address _to, uint8 _assetIndex, uint8 _routeType, uint256 _amount) payable",
296
+ ], wallet);
297
+ const amountRaw = ethers.parseUnits(amount, 6);
298
+ const ethBal = await provider.getBalance(wallet.address);
299
+ if (ethBal < ethers.parseEther("0.003")) {
300
+ console.error(chalk.red(" Insufficient ETH on Ethereum for gas. Need ~0.003 ETH ($3+)."));
301
+ process.exit(1);
302
+ }
303
+ const usdcBal = await usdc.balanceOf(wallet.address);
304
+ if (usdcBal < amountRaw) {
305
+ console.error(chalk.red(` Insufficient USDC on Ethereum. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
306
+ process.exit(1);
307
+ }
308
+ const allowance = await usdc.allowance(wallet.address, LIGHTER_CONTRACT);
309
+ if (allowance < amountRaw) {
310
+ if (!isJson())
311
+ console.log(chalk.gray(" Approving USDC for Lighter..."));
312
+ const approveTx = await usdc.approve(LIGHTER_CONTRACT, amountRaw);
313
+ await approveTx.wait();
314
+ if (!isJson())
315
+ console.log(chalk.gray(" Approved."));
316
+ }
317
+ if (!isJson())
318
+ console.log(chalk.gray(" Calling deposit()..."));
319
+ try {
320
+ const tx = await lighter.deposit(wallet.address, parseInt(opts.assetId), parseInt(opts.route), amountRaw);
321
+ const receipt = await tx.wait();
322
+ logExecution({
323
+ type: "bridge", exchange: "lighter", symbol: "USDC", side: "deposit",
324
+ size: String(amountNum), status: "success", dryRun: false,
325
+ meta: { action: "deposit", method: "ethereum", txHash: receipt.hash },
326
+ });
327
+ if (isJson())
328
+ return printJson(jsonOk({ txHash: receipt.hash, amount: amountNum, chain: "ethereum", method: "direct" }));
329
+ console.log(chalk.green(`\n Deposit confirmed!`));
330
+ console.log(` Amount: $${formatUsd(amountNum)} USDC`);
331
+ console.log(` TX Hash: ${receipt.hash}`);
332
+ console.log(chalk.gray(`\n Funds appear in your Lighter perps account shortly.\n`));
333
+ }
334
+ catch (err) {
335
+ logExecution({
336
+ type: "bridge", exchange: "lighter", symbol: "USDC", side: "deposit",
337
+ size: String(amountNum), status: "failed", dryRun: false,
338
+ error: err instanceof Error ? err.message : String(err),
339
+ meta: { action: "deposit", method: "ethereum" },
340
+ });
341
+ throw err;
342
+ }
343
+ });
344
+ const CCTP_CHAINS = {
345
+ arbitrum: { chainId: 42161, rpc: "https://arb1.arbitrum.io/rpc", usdc: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", name: "Arbitrum" },
346
+ base: { chainId: 8453, rpc: "https://mainnet.base.org", usdc: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", name: "Base" },
347
+ avalanche: { chainId: 43114, rpc: "https://api.avax.network/ext/bc/C/rpc", usdc: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", name: "Avalanche" },
348
+ };
349
+ lighterDeposit
350
+ .command("cctp <chain> <amount>")
351
+ .description("Deposit USDC via CCTP (arbitrum, base, avalanche — min 5 USDC)")
352
+ .action(async (chain, amount) => {
353
+ const amountNum = parseFloat(amount);
354
+ if (isNaN(amountNum) || amountNum <= 0)
355
+ throw new Error("Invalid amount");
356
+ if (amountNum < 5)
357
+ throw new Error("Minimum CCTP deposit is 5 USDC");
358
+ const chainKey = chain.toLowerCase();
359
+ const chainInfo = CCTP_CHAINS[chainKey];
360
+ if (!chainInfo)
361
+ throw new Error(`Unsupported chain: ${chain}. Use: arbitrum, base, avalanche`);
362
+ if (!isJson())
363
+ console.log(chalk.cyan(`\n Depositing $${formatUsd(amountNum)} USDC into Lighter via CCTP (${chainInfo.name})...\n`));
364
+ const { ethers } = await import("ethers");
365
+ const { loadPrivateKey } = await import("../config.js");
366
+ const pk = await loadPrivateKey("lighter");
367
+ const provider = new ethers.JsonRpcProvider(chainInfo.rpc);
368
+ const wallet = new ethers.Wallet(pk, provider);
369
+ if (!isJson())
370
+ console.log(chalk.gray(" Creating intent address..."));
371
+ const intentRes = await fetch("https://mainnet.zklighter.elliot.ai/api/v1/createIntentAddress", {
372
+ method: "POST",
373
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
374
+ body: new URLSearchParams({
375
+ chain_id: String(chainInfo.chainId),
376
+ from_addr: wallet.address,
377
+ amount: "0",
378
+ is_external_deposit: "true",
379
+ }),
380
+ });
381
+ if (!intentRes.ok) {
382
+ const err = await intentRes.text();
383
+ throw new Error(`Failed to create intent address: ${err}`);
384
+ }
385
+ const intentData = (await intentRes.json());
386
+ const intentAddress = String(intentData.intent_address || intentData.address || "");
387
+ if (!intentAddress)
388
+ throw new Error("No intent address returned");
389
+ if (!isJson())
390
+ console.log(` Intent address: ${intentAddress}`);
391
+ const gasBal = await provider.getBalance(wallet.address);
392
+ if (gasBal < ethers.parseEther("0.00005")) {
393
+ console.error(chalk.red(` Insufficient gas token on ${chainInfo.name}. Need native token for gas (~$0.01).`));
394
+ process.exit(1);
395
+ }
396
+ const usdc = new ethers.Contract(chainInfo.usdc, [
397
+ "function transfer(address to, uint256 amount) returns (bool)",
398
+ "function balanceOf(address) view returns (uint256)",
399
+ ], wallet);
400
+ const amountRaw = ethers.parseUnits(amount, 6);
401
+ const usdcBal = await usdc.balanceOf(wallet.address);
402
+ if (usdcBal < amountRaw) {
403
+ console.error(chalk.red(` Insufficient USDC on ${chainInfo.name}. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
404
+ process.exit(1);
405
+ }
406
+ if (!isJson())
407
+ console.log(chalk.gray(` Sending USDC to intent address on ${chainInfo.name}...`));
408
+ try {
409
+ const tx = await usdc.transfer(intentAddress, amountRaw);
410
+ const receipt = await tx.wait();
411
+ logExecution({
412
+ type: "bridge", exchange: "lighter", symbol: "USDC", side: "deposit",
413
+ size: String(amountNum), status: "success", dryRun: false,
414
+ meta: { action: "deposit", method: "cctp", chain: chainKey, txHash: receipt.hash },
415
+ });
416
+ if (isJson())
417
+ return printJson(jsonOk({ txHash: receipt.hash, amount: amountNum, chain: chainKey, intentAddress, method: "cctp" }));
418
+ console.log(chalk.green(`\n USDC sent to intent address!`));
419
+ console.log(` Amount: $${formatUsd(amountNum)} USDC`);
420
+ console.log(` TX Hash: ${receipt.hash}`);
421
+ console.log(` Chain: ${chainInfo.name}`);
422
+ console.log(chalk.gray(`\n CCTP bridging takes ~1-3 minutes. Funds will appear in Lighter automatically.\n`));
423
+ }
424
+ catch (err) {
425
+ logExecution({
426
+ type: "bridge", exchange: "lighter", symbol: "USDC", side: "deposit",
427
+ size: String(amountNum), status: "failed", dryRun: false,
428
+ error: err instanceof Error ? err.message : String(err),
429
+ meta: { action: "deposit", method: "cctp", chain: chainKey },
430
+ });
431
+ throw err;
432
+ }
433
+ });
434
+ lighterDeposit
435
+ .command("info")
436
+ .description("Show Lighter deposit routes & minimums")
437
+ .action(async () => {
438
+ if (isJson()) {
439
+ return printJson(jsonOk({
440
+ routes: {
441
+ ethereum: { min: "1 USDC", gas: "$3-10+", method: "deposit() on L1 contract" },
442
+ arbitrum: { min: "5 USDC", gas: "~$0.01", method: "CCTP via intent address" },
443
+ base: { min: "5 USDC", gas: "~$0.01", method: "CCTP via intent address" },
444
+ avalanche: { min: "5 USDC", gas: "~$0.01", method: "CCTP via intent address" },
445
+ },
446
+ contract: "0x3B4D794a66304f130a4db8f2551b0070dfcf5ca7",
447
+ }));
448
+ }
449
+ console.log(chalk.cyan.bold("\n Lighter Deposit Routes\n"));
450
+ console.log(chalk.white.bold(" Ethereum L1") + chalk.gray(" (direct)"));
451
+ console.log(` Min: 1 USDC | Gas: $3-10+`);
452
+ console.log(` Command: ${chalk.green("perp funds deposit lighter ethereum <amount>")}`);
453
+ console.log(chalk.white.bold("\n Arbitrum") + chalk.gray(" (CCTP)"));
454
+ console.log(` Min: 5 USDC | Gas: ~$0.01`);
455
+ console.log(` Command: ${chalk.green("perp funds deposit lighter cctp arbitrum <amount>")}`);
456
+ console.log(chalk.white.bold("\n Base") + chalk.gray(" (CCTP)"));
457
+ console.log(` Min: 5 USDC | Gas: ~$0.01`);
458
+ console.log(` Command: ${chalk.green("perp funds deposit lighter cctp base <amount>")}`);
459
+ console.log(chalk.white.bold("\n Avalanche") + chalk.gray(" (CCTP)"));
460
+ console.log(` Min: 5 USDC | Gas: ~$0.01`);
461
+ console.log(` Command: ${chalk.green("perp funds deposit lighter cctp avalanche <amount>")}`);
462
+ console.log();
463
+ });
464
+ // ═══════════════════════════════════════════════════════
465
+ // WITHDRAW
466
+ // ═══════════════════════════════════════════════════════
467
+ const withdraw = funds.command("withdraw").description("Withdraw funds from exchange accounts");
468
+ // ── Pacifica ──
469
+ withdraw
470
+ .command("pacifica <amount>")
471
+ .description("Withdraw USDC from Pacifica to your Solana wallet")
472
+ .option("--to <address>", "Destination Solana address (default: your wallet)")
473
+ .action(async (amount, opts) => {
474
+ const amountNum = parseFloat(amount);
475
+ if (isNaN(amountNum) || amountNum <= 0)
476
+ throw new Error("Invalid amount");
477
+ const adapter = await getAdapter();
478
+ if (!(adapter instanceof PacificaAdapter))
479
+ throw new Error("Requires --exchange pacifica");
480
+ const dest = opts.to || adapter.publicKey;
481
+ if (!isJson()) {
482
+ console.log(chalk.cyan(`\n Withdrawing $${formatUsd(amountNum)} USDC from Pacifica...\n`));
483
+ console.log(` Destination: ${dest}`);
484
+ }
485
+ try {
486
+ const result = await adapter.sdk.withdraw({ amount: String(amountNum), dest_address: dest }, adapter.publicKey, adapter.signer);
487
+ logExecution({
488
+ type: "bridge", exchange: "pacifica", symbol: "USDC", side: "withdraw",
489
+ size: String(amountNum), status: "success", dryRun: false,
490
+ meta: { action: "withdraw", destination: dest },
491
+ });
492
+ if (isJson())
493
+ return printJson(jsonOk(result));
494
+ console.log(chalk.green(`\n Withdrawal submitted!`));
495
+ console.log(` Amount: $${formatUsd(amountNum)} USDC`);
496
+ console.log(chalk.gray(`\n Funds arrive in your Solana wallet shortly.\n`));
497
+ }
498
+ catch (err) {
499
+ logExecution({
500
+ type: "bridge", exchange: "pacifica", symbol: "USDC", side: "withdraw",
501
+ size: String(amountNum), status: "failed", dryRun: false,
502
+ error: err instanceof Error ? err.message : String(err),
503
+ meta: { action: "withdraw", destination: dest },
504
+ });
505
+ throw err;
506
+ }
507
+ });
508
+ // ── Hyperliquid ──
509
+ withdraw
510
+ .command("hyperliquid <amount>")
511
+ .description("Withdraw USDC from Hyperliquid to your Arbitrum wallet")
512
+ .option("--to <address>", "Destination EVM address (default: your wallet)")
513
+ .action(async (amount, opts) => {
514
+ const amountNum = parseFloat(amount);
515
+ if (isNaN(amountNum) || amountNum <= 0)
516
+ throw new Error("Invalid amount");
517
+ const adapter = await getAdapter();
518
+ if (!(adapter instanceof HyperliquidAdapter))
519
+ throw new Error("Requires --exchange hyperliquid");
520
+ const dest = opts.to || adapter.address;
521
+ if (!isJson()) {
522
+ console.log(chalk.cyan(`\n Withdrawing $${formatUsd(amountNum)} USDC from Hyperliquid...\n`));
523
+ console.log(` Destination: ${dest}`);
524
+ }
525
+ const balance = await adapter.getBalance();
526
+ const available = Number(balance.available);
527
+ if (amountNum > available) {
528
+ console.error(chalk.red(` Insufficient withdrawable balance. Available: $${formatUsd(available)}`));
529
+ process.exit(1);
530
+ }
531
+ try {
532
+ const result = await adapter.withdraw(String(amountNum), dest);
533
+ logExecution({
534
+ type: "bridge", exchange: "hyperliquid", symbol: "USDC", side: "withdraw",
535
+ size: String(amountNum), status: "success", dryRun: false,
536
+ meta: { action: "withdraw", destination: dest },
537
+ });
538
+ if (isJson())
539
+ return printJson(jsonOk(result));
540
+ console.log(chalk.green(`\n Withdrawal submitted!`));
541
+ console.log(` Amount: $${formatUsd(amountNum)} USDC`);
542
+ console.log(chalk.gray(`\n Processing may take a few minutes.\n`));
543
+ }
544
+ catch (err) {
545
+ logExecution({
546
+ type: "bridge", exchange: "hyperliquid", symbol: "USDC", side: "withdraw",
547
+ size: String(amountNum), status: "failed", dryRun: false,
548
+ error: err instanceof Error ? err.message : String(err),
549
+ meta: { action: "withdraw", destination: dest },
550
+ });
551
+ throw err;
552
+ }
553
+ });
554
+ // ── Lighter ──
555
+ withdraw
556
+ .command("lighter <amount>")
557
+ .description("Withdraw USDC from Lighter to your Ethereum L1 wallet")
558
+ .option("--asset-id <id>", "Asset ID (default: 2 = USDC)", "2")
559
+ .option("--route <type>", "Route type: 0=perp, 1=spot (default: 0)", "0")
560
+ .action(async (amount, opts) => {
561
+ const amountNum = parseFloat(amount);
562
+ if (isNaN(amountNum) || amountNum <= 0)
563
+ throw new Error("Invalid amount");
564
+ const adapter = await getAdapter();
565
+ const { LighterAdapter } = await import("../exchanges/lighter.js");
566
+ if (!(adapter instanceof LighterAdapter))
567
+ throw new Error("Requires --exchange lighter");
568
+ if (!isJson()) {
569
+ console.log(chalk.cyan(`\n Withdrawing $${formatUsd(amountNum)} USDC from Lighter...\n`));
570
+ console.log(` Account Index: ${adapter.accountIndex}`);
571
+ console.log(` Address: ${adapter.address}`);
572
+ }
573
+ try {
574
+ const result = await adapter.withdraw(amountNum, parseInt(opts.assetId), parseInt(opts.route));
575
+ logExecution({
576
+ type: "bridge", exchange: "lighter", symbol: "USDC", side: "withdraw",
577
+ size: String(amountNum), status: "success", dryRun: false,
578
+ meta: { action: "withdraw" },
579
+ });
580
+ if (isJson())
581
+ return printJson(jsonOk(result));
582
+ console.log(chalk.green(`\n Withdrawal submitted!`));
583
+ console.log(` Amount: $${formatUsd(amountNum)} USDC`);
584
+ console.log(chalk.gray(`\n Standard withdrawal takes ~12 hours. Use Lighter web for fast withdrawal.\n`));
585
+ }
586
+ catch (err) {
587
+ logExecution({
588
+ type: "bridge", exchange: "lighter", symbol: "USDC", side: "withdraw",
589
+ size: String(amountNum), status: "failed", dryRun: false,
590
+ error: err instanceof Error ? err.message : String(err),
591
+ meta: { action: "withdraw" },
592
+ });
593
+ throw err;
594
+ }
595
+ });
596
+ // ═══════════════════════════════════════════════════════
597
+ // TRANSFER (HL internal)
598
+ // ═══════════════════════════════════════════════════════
599
+ funds
600
+ .command("transfer <amount> <destination>")
601
+ .description("Transfer USDC between Hyperliquid accounts (internal, instant)")
602
+ .action(async (amount, destination) => {
603
+ const amountNum = parseFloat(amount);
604
+ if (isNaN(amountNum) || amountNum <= 0)
605
+ throw new Error("Invalid amount");
606
+ const adapter = await getAdapter();
607
+ if (!(adapter instanceof HyperliquidAdapter))
608
+ throw new Error("Requires --exchange hyperliquid");
609
+ if (!isJson())
610
+ console.log(chalk.cyan(`\n Transferring $${formatUsd(amountNum)} USDC to ${destination}...\n`));
611
+ const result = await adapter.usdTransfer(amountNum, destination);
612
+ if (isJson())
613
+ return printJson(jsonOk(result));
614
+ console.log(chalk.green(` Transfer complete!`));
615
+ console.log(` Amount: $${formatUsd(amountNum)} USDC`);
616
+ console.log(` To: ${destination}\n`);
617
+ });
618
+ // ═══════════════════════════════════════════════════════
619
+ // BRIDGE (CCTP)
620
+ // ═══════════════════════════════════════════════════════
621
+ funds
622
+ .command("bridge")
623
+ .description("Bridge USDC between chains via CCTP V2")
624
+ .requiredOption("--from <chain>", "Source chain (arbitrum, ethereum)")
625
+ .requiredOption("--to <chain>", "Destination chain (arbitrum, ethereum, solana)")
626
+ .requiredOption("--amount <amount>", "USDC amount")
627
+ .requiredOption("--recipient <address>", "Recipient address on destination chain")
628
+ .action(async (opts) => {
629
+ const amountNum = parseFloat(opts.amount);
630
+ if (isNaN(amountNum) || amountNum <= 0)
631
+ throw new Error("Invalid amount");
632
+ if (await relayerAvailable()) {
633
+ if (!isJson())
634
+ console.log(chalk.cyan(`\n Bridging $${formatUsd(amountNum)} USDC via CCTP V2 (${opts.from} → ${opts.to})...\n`));
635
+ const res = await fetch(`${getRelayerUrl()}/bridge/cctp`, {
636
+ method: "POST",
637
+ headers: { "Content-Type": "application/json" },
638
+ body: JSON.stringify({
639
+ fromChain: opts.from,
640
+ toChain: opts.to,
641
+ amount: amountNum,
642
+ recipient: opts.recipient,
643
+ }),
644
+ });
645
+ const result = (await res.json());
646
+ if (!res.ok)
647
+ throw new Error(String(result.error || "CCTP bridge failed"));
648
+ if (isJson())
649
+ return printJson(jsonOk(result));
650
+ console.log(chalk.green(` Burn TX submitted!`));
651
+ console.log(` TX Hash: ${result.txHash}`);
652
+ console.log(` Message Hash: ${result.messageHash}`);
653
+ console.log(chalk.gray(`\n Waiting for Circle attestation (~1-3 min)...`));
654
+ console.log(chalk.gray(` Check: perp funds bridge-status --hash ${result.messageHash}\n`));
655
+ }
656
+ else {
657
+ console.error(chalk.red("\n CCTP bridge requires relayer server."));
658
+ console.error(chalk.gray(" Start: cd packages/relayer && pnpm start\n"));
659
+ }
660
+ });
661
+ funds
662
+ .command("bridge-status")
663
+ .description("Check CCTP bridge status")
664
+ .requiredOption("--hash <messageHash>", "Message hash from bridge TX")
665
+ .action(async (opts) => {
666
+ const res = await fetch(`${getRelayerUrl()}/bridge/cctp/status/${opts.hash}`);
667
+ const result = (await res.json());
668
+ if (isJson())
669
+ return printJson(jsonOk(result));
670
+ console.log(chalk.cyan.bold("\n CCTP Bridge Status\n"));
671
+ console.log(` Status: ${result.status === "complete" ? chalk.green("complete") : chalk.yellow(String(result.status))}`);
672
+ if (result.attestation)
673
+ console.log(` Attestation: ${chalk.gray("received")}`);
674
+ console.log();
675
+ });
676
+ // ═══════════════════════════════════════════════════════
677
+ // INFO (combined deposit + withdraw)
678
+ // ═══════════════════════════════════════════════════════
679
+ funds
680
+ .command("info")
681
+ .description("Show deposit & withdrawal info, routes, and limits")
682
+ .action(async () => {
683
+ const hasRelay = await relayerAvailable();
684
+ if (isJson()) {
685
+ return printJson(jsonOk({
686
+ relayer: hasRelay ? getRelayerUrl() : null,
687
+ deposit: {
688
+ pacifica: { chain: "Solana", token: "USDC", min: 10, gas: "SOL ~0.005", method: "on-chain program" },
689
+ hyperliquid: { chain: "Arbitrum", token: "USDC", min: 5, gas: "ETH ~0.0001", method: "USDC transfer to Bridge2" },
690
+ lighter: { chain: "Ethereum L1 / Arbitrum / Base / Avalanche", token: "USDC", min: "1 (L1) or 5 (CCTP)", gas: "$3-10 (L1), ~$0.01 (CCTP)", method: "deposit() or CCTP intent" },
691
+ },
692
+ withdraw: {
693
+ pacifica: { chain: "Solana", token: "USDC", speed: "~10s", fee: "none" },
694
+ hyperliquid: { chain: "Arbitrum", token: "USDC", speed: "minutes", fee: "$1" },
695
+ lighter: { chain: "Ethereum L1", token: "USDC", speed: "~12 hours (standard), minutes (fast)", fee: "varies" },
696
+ },
697
+ }));
698
+ }
699
+ console.log(chalk.cyan.bold("\n Funds — Deposit & Withdrawal Info\n"));
700
+ if (hasRelay) {
701
+ console.log(chalk.green(" ✓ Relayer available — gas-free deposits!\n"));
702
+ }
703
+ else {
704
+ console.log(chalk.yellow(" ⚠ Relayer offline — deposits will cost gas.\n"));
705
+ console.log(chalk.gray(" Start relayer: cd packages/relayer && pnpm start\n"));
706
+ }
707
+ // Deposit info
708
+ console.log(chalk.white.bold(" ── Deposit ──\n"));
709
+ console.log(chalk.white.bold(" Pacifica") + chalk.gray(" (Solana)"));
710
+ console.log(` Token: USDC | Min: $10 | Gas: ${hasRelay ? chalk.green("FREE") : "SOL ~0.005"}`);
711
+ console.log(` Command: ${chalk.green("perp funds deposit pacifica <amount>")}`);
712
+ console.log(chalk.white.bold("\n Hyperliquid") + chalk.gray(" (Arbitrum)"));
713
+ console.log(` Token: USDC | Min: $5 | Gas: ${hasRelay ? chalk.green("FREE") : "ETH ~0.0001"}`);
714
+ console.log(` Command: ${chalk.green("perp funds deposit hyperliquid <amount>")}`);
715
+ console.log(chalk.white.bold("\n Lighter") + chalk.gray(" (Ethereum L1 / CCTP)"));
716
+ console.log(` Token: USDC | Min: 1 (L1), 5 (CCTP)`);
717
+ console.log(` L1: ${chalk.green("perp funds deposit lighter ethereum <amount>")} — gas $3-10+`);
718
+ console.log(` CCTP: ${chalk.green("perp funds deposit lighter cctp <chain> <amount>")} — gas ~$0.01`);
719
+ console.log(` Chains: arbitrum, base, avalanche`);
720
+ // Withdraw info
721
+ console.log(chalk.white.bold("\n ── Withdraw ──\n"));
722
+ console.log(chalk.white.bold(" Pacifica") + chalk.gray(" → Solana"));
723
+ console.log(` Speed: ~10s | Fee: none`);
724
+ console.log(` Command: ${chalk.green("perp funds withdraw pacifica <amount>")}`);
725
+ console.log(chalk.white.bold("\n Hyperliquid") + chalk.gray(" → Arbitrum"));
726
+ console.log(` Speed: minutes | Fee: ~$1`);
727
+ console.log(` Command: ${chalk.green("perp -e hl funds withdraw hyperliquid <amount>")}`);
728
+ console.log(chalk.white.bold("\n Lighter") + chalk.gray(" → Ethereum L1"));
729
+ console.log(` Speed: ~12h (standard) | Fee: varies`);
730
+ console.log(` Command: ${chalk.green("perp -e lt funds withdraw lighter <amount>")}`);
731
+ // Transfer + Bridge
732
+ console.log(chalk.white.bold("\n ── Transfer & Bridge ──\n"));
733
+ console.log(chalk.white.bold(" Internal Transfer") + chalk.gray(" (Hyperliquid → Hyperliquid)"));
734
+ console.log(` Speed: instant | Fee: none`);
735
+ console.log(` Command: ${chalk.green("perp -e hl funds transfer <amount> <address>")}`);
736
+ console.log(chalk.white.bold("\n CCTP Bridge") + chalk.gray(" (Cross-chain USDC)"));
737
+ console.log(` Routes: Arbitrum ↔ Ethereum ↔ Solana`);
738
+ console.log(` Command: ${chalk.green("perp funds bridge --from arbitrum --to solana --amount 100 --recipient <addr>")}`);
739
+ console.log(chalk.gray("\n Use --no-relay to skip relayer and pay gas yourself.\n"));
740
+ });
741
+ }