perp-cli 0.7.8 → 0.7.9

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.
@@ -1,5 +1,5 @@
1
1
  import { createRequire } from "node:module";
2
- import { printJson, jsonOk } from "../utils.js";
2
+ import { printJson, jsonOk, jsonError } from "../utils.js";
3
3
  import { ERROR_CODES } from "../errors.js";
4
4
  import chalk from "chalk";
5
5
  const _require = createRequire(import.meta.url);
@@ -274,7 +274,7 @@ export function registerAgentCommands(program, getAdapter, isJson) {
274
274
  }
275
275
  catch (err) {
276
276
  const msg = err instanceof Error ? err.message : String(err);
277
- console.error(JSON.stringify({ error: msg }));
277
+ console.error(JSON.stringify(jsonError("EXEC_FAILED", msg)));
278
278
  process.exit(1);
279
279
  }
280
280
  });
@@ -1,5 +1,5 @@
1
1
  import chalk from "chalk";
2
- import { printJson, jsonOk } from "../utils.js";
2
+ import { printJson, jsonOk, jsonError } from "../utils.js";
3
3
  import { loadBotConfig, parseStrategy, quickGridConfig, quickDCAConfig, runBot, PRESETS, getPreset, getPresetsByStrategy } from "../bot/index.js";
4
4
  import { updateJobState } from "../jobs.js";
5
5
  import { runTWAP, runFundingArb, runGrid, runDCA, runTrailingStop } from "../strategies/index.js";
@@ -444,6 +444,10 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
444
444
  "grid-mm", "liquidation-mm", "regime-mm",
445
445
  ];
446
446
  if (sym === "ALL" && symbolRequiredStrategies.includes(strategyName)) {
447
+ if (isJson()) {
448
+ console.error(JSON.stringify(jsonError("INVALID_PARAMS", `The '${strategyName}' strategy requires a <symbol> argument`)));
449
+ return process.exit(1);
450
+ }
447
451
  console.error(chalk.red(`\n Error: The '${strategyName}' strategy requires a <symbol> argument.`));
448
452
  console.error(chalk.gray(` Usage: perp bot run ${strategyName} <symbol>\n`));
449
453
  return process.exit(1);
@@ -499,6 +503,10 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
499
503
  const { getStrategy, listStrategies: listStrats } = await import("../bot/strategy-registry.js");
500
504
  if (!getStrategy(strategyName)) {
501
505
  const available = listStrats();
506
+ if (isJson()) {
507
+ console.error(JSON.stringify(jsonError("INVALID_PARAMS", `Unknown strategy: "${strategyName}"`, { details: { available } })));
508
+ return process.exit(1);
509
+ }
502
510
  console.error(chalk.red(`\n Unknown strategy: "${strategyName}"`));
503
511
  console.error(chalk.gray(`\n Available strategies:`));
504
512
  for (const s of available)
@@ -201,7 +201,7 @@ export function registerBridgeCommands(program, isJson) {
201
201
  if (!gasCheck.ok) {
202
202
  const msg = gasCheck.errors.map(e => ` ✗ ${e}`).join("\n");
203
203
  if (isJson())
204
- return printJson({ ok: false, error: "Insufficient gas", details: gasCheck.errors });
204
+ return printJson({ ok: false, error: { code: "INSUFFICIENT_BALANCE", message: "Insufficient gas for transaction", details: gasCheck.errors }, meta: { timestamp: new Date().toISOString() } });
205
205
  console.log(chalk.red("\n Insufficient gas for bridge:\n"));
206
206
  console.log(chalk.red(msg));
207
207
  if (needsDstGas) {
@@ -343,7 +343,7 @@ export function registerBridgeCommands(program, isJson) {
343
343
  if (!gasCheck.ok) {
344
344
  const msg = gasCheck.errors.map(e => ` ✗ ${e}`).join("\n");
345
345
  if (isJson())
346
- return printJson({ ok: false, error: "Insufficient gas", details: gasCheck.errors });
346
+ return printJson({ ok: false, error: { code: "INSUFFICIENT_BALANCE", message: "Insufficient gas for transaction", details: gasCheck.errors }, meta: { timestamp: new Date().toISOString() } });
347
347
  console.log(chalk.red("\n Insufficient gas for bridge:\n"));
348
348
  console.log(chalk.red(msg));
349
349
  if (needsDstGas) {
@@ -117,8 +117,13 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork) {
117
117
  const connection = new Connection(rpcUrl, "confirmed");
118
118
  const solBalance = await connection.getBalance(pacDirect.keypair.publicKey);
119
119
  if (solBalance < 5_000_000) {
120
- console.error(chalk.red(" Insufficient SOL for gas. Need ~0.005 SOL."));
121
- console.error(chalk.gray(" Tip: Use relayer for gasless deposits (start relayer server)\n"));
120
+ if (isJson()) {
121
+ printJson(jsonError("INSUFFICIENT_BALANCE", "Insufficient SOL for gas. Need ~0.005 SOL."));
122
+ }
123
+ else {
124
+ console.error(chalk.red(" Insufficient SOL for gas. Need ~0.005 SOL."));
125
+ console.error(chalk.gray(" Tip: Use relayer for gasless deposits (start relayer server)\n"));
126
+ }
122
127
  process.exit(1);
123
128
  }
124
129
  const { PublicKey } = await import("@solana/web3.js");
@@ -128,7 +133,12 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork) {
128
133
  ? tokenAccounts.value[0].account.data.parsed.info.tokenAmount.uiAmount ?? 0
129
134
  : 0;
130
135
  if (usdcBalance < amountNum) {
131
- console.error(chalk.red(` Insufficient USDC on Solana. Have: $${formatUsd(usdcBalance)}, need: $${formatUsd(amountNum)}`));
136
+ if (isJson()) {
137
+ printJson(jsonError("INSUFFICIENT_BALANCE", `Insufficient USDC on Solana. Have: $${formatUsd(usdcBalance)}, need: $${formatUsd(amountNum)}`));
138
+ }
139
+ else {
140
+ console.error(chalk.red(` Insufficient USDC on Solana. Have: $${formatUsd(usdcBalance)}, need: $${formatUsd(amountNum)}`));
141
+ }
132
142
  process.exit(1);
133
143
  }
134
144
  const ix = await buildDepositInstruction(pacDirect.keypair.publicKey, amountNum, network);
@@ -231,7 +241,12 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork) {
231
241
  const USDC_ARB = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831";
232
242
  const ethBal = await provider.getBalance(wallet.address);
233
243
  if (ethBal < ethers.parseEther("0.0001")) {
234
- console.error(chalk.red(" Insufficient ETH on Arbitrum for gas."));
244
+ if (isJson()) {
245
+ printJson(jsonError("INSUFFICIENT_BALANCE", "Insufficient ETH on Arbitrum for gas."));
246
+ }
247
+ else {
248
+ console.error(chalk.red(" Insufficient ETH on Arbitrum for gas."));
249
+ }
235
250
  process.exit(1);
236
251
  }
237
252
  const usdc = new ethers.Contract(USDC_ARB, [
@@ -241,7 +256,12 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork) {
241
256
  const amountRaw = ethers.parseUnits(amount, 6);
242
257
  const usdcBal = await usdc.balanceOf(wallet.address);
243
258
  if (usdcBal < amountRaw) {
244
- console.error(chalk.red(` Insufficient USDC. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
259
+ if (isJson()) {
260
+ printJson(jsonError("INSUFFICIENT_BALANCE", `Insufficient USDC. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
261
+ }
262
+ else {
263
+ console.error(chalk.red(` Insufficient USDC. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
264
+ }
245
265
  process.exit(1);
246
266
  }
247
267
  try {
@@ -306,12 +326,22 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork) {
306
326
  const amountRaw = ethers.parseUnits(amount, 6);
307
327
  const ethBal = await provider.getBalance(wallet.address);
308
328
  if (ethBal < ethers.parseEther("0.003")) {
309
- console.error(chalk.red(" Insufficient ETH on Ethereum for gas. Need ~0.003 ETH ($3+)."));
329
+ if (isJson()) {
330
+ printJson(jsonError("INSUFFICIENT_BALANCE", "Insufficient ETH on Ethereum for gas. Need ~0.003 ETH ($3+)."));
331
+ }
332
+ else {
333
+ console.error(chalk.red(" Insufficient ETH on Ethereum for gas. Need ~0.003 ETH ($3+)."));
334
+ }
310
335
  process.exit(1);
311
336
  }
312
337
  const usdcBal = await usdc.balanceOf(wallet.address);
313
338
  if (usdcBal < amountRaw) {
314
- console.error(chalk.red(` Insufficient USDC on Ethereum. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
339
+ if (isJson()) {
340
+ printJson(jsonError("INSUFFICIENT_BALANCE", `Insufficient USDC on Ethereum. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
341
+ }
342
+ else {
343
+ console.error(chalk.red(` Insufficient USDC on Ethereum. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
344
+ }
315
345
  process.exit(1);
316
346
  }
317
347
  const allowance = await usdc.allowance(wallet.address, LIGHTER_CONTRACT);
@@ -399,7 +429,12 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork) {
399
429
  console.log(` Intent address: ${intentAddress}`);
400
430
  const gasBal = await provider.getBalance(wallet.address);
401
431
  if (gasBal < ethers.parseEther("0.00005")) {
402
- console.error(chalk.red(` Insufficient gas token on ${chainInfo.name}. Need native token for gas (~$0.01).`));
432
+ if (isJson()) {
433
+ printJson(jsonError("INSUFFICIENT_BALANCE", `Insufficient gas token on ${chainInfo.name}. Need native token for gas (~$0.01).`));
434
+ }
435
+ else {
436
+ console.error(chalk.red(` Insufficient gas token on ${chainInfo.name}. Need native token for gas (~$0.01).`));
437
+ }
403
438
  process.exit(1);
404
439
  }
405
440
  const usdc = new ethers.Contract(chainInfo.usdc, [
@@ -409,7 +444,12 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork) {
409
444
  const amountRaw = ethers.parseUnits(amount, 6);
410
445
  const usdcBal = await usdc.balanceOf(wallet.address);
411
446
  if (usdcBal < amountRaw) {
412
- console.error(chalk.red(` Insufficient USDC on ${chainInfo.name}. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
447
+ if (isJson()) {
448
+ printJson(jsonError("INSUFFICIENT_BALANCE", `Insufficient USDC on ${chainInfo.name}. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
449
+ }
450
+ else {
451
+ console.error(chalk.red(` Insufficient USDC on ${chainInfo.name}. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
452
+ }
413
453
  process.exit(1);
414
454
  }
415
455
  if (!isJson())
@@ -534,7 +574,12 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork) {
534
574
  const balance = await adapter.getBalance();
535
575
  const available = Number(balance.available);
536
576
  if (amountNum > available) {
537
- console.error(chalk.red(` Insufficient withdrawable balance. Available: $${formatUsd(available)}`));
577
+ if (isJson()) {
578
+ printJson(jsonError("INSUFFICIENT_BALANCE", `Insufficient withdrawable balance. Available: $${formatUsd(available)}`));
579
+ }
580
+ else {
581
+ console.error(chalk.red(` Insufficient withdrawable balance. Available: $${formatUsd(available)}`));
582
+ }
538
583
  process.exit(1);
539
584
  }
540
585
  try {
@@ -5,6 +5,7 @@ declare function loadEnvFile(): Record<string, string>;
5
5
  declare function setEnvVar(key: string, value: string): void;
6
6
  export declare const EXCHANGE_ENV_MAP: Record<string, {
7
7
  envKey: string;
8
+ envKeySecret?: string;
8
9
  chain: "solana" | "evm" | "apikey";
9
10
  label: string;
10
11
  }>;
@@ -70,7 +70,7 @@ export const EXCHANGE_ENV_MAP = {
70
70
  pacifica: { envKey: "PACIFICA_PRIVATE_KEY", chain: "solana", label: "Pacifica (Solana)" },
71
71
  hyperliquid: { envKey: "HL_PRIVATE_KEY", chain: "evm", label: "Hyperliquid (EVM)" },
72
72
  lighter: { envKey: "LIGHTER_PRIVATE_KEY", chain: "evm", label: "Lighter (EVM)" },
73
- aster: { envKey: "ASTER_API_KEY", chain: "apikey", label: "Aster (BNB Chain)" },
73
+ aster: { envKey: "ASTER_API_KEY", envKeySecret: "ASTER_API_SECRET", chain: "apikey", label: "Aster (BNB Chain)" },
74
74
  };
75
75
  // ── Validate keys ─────────────────────────────────────────────
76
76
  export async function validateKey(chain, key) {
@@ -146,8 +146,9 @@ export function registerInitCommand(program) {
146
146
  console.log(` 1) ${chalk.cyan("Pacifica")} ${chalk.gray("— Solana perps (USDC on Solana)")}`);
147
147
  console.log(` 2) ${chalk.cyan("Hyperliquid")} ${chalk.gray("— EVM perps (USDC on Arbitrum)")}`);
148
148
  console.log(` 3) ${chalk.cyan("Lighter")} ${chalk.gray("— EVM perps (USDC on Ethereum/Arb)")}`);
149
+ console.log(` 4) ${chalk.cyan("Aster")} ${chalk.gray("— BNB Chain perps (API key + secret)")}`);
149
150
  console.log();
150
- const exchangeChoice = await ask(rl, " Which exchange(s)? (1,2,3 or 'all'): ");
151
+ const exchangeChoice = await ask(rl, " Which exchange(s)? (1,2,3,4 or 'all'): ");
151
152
  const selected = parseExchangeChoice(exchangeChoice);
152
153
  if (selected.length === 0) {
153
154
  console.log(chalk.red("\n No exchanges selected.\n"));
@@ -158,6 +159,7 @@ export function registerInitCommand(program) {
158
159
  // EVM exchanges can share the same key
159
160
  const evmExchanges = selected.filter((e) => EXCHANGE_ENV_MAP[e].chain === "evm");
160
161
  const solExchanges = selected.filter((e) => EXCHANGE_ENV_MAP[e].chain === "solana");
162
+ const apikeyExchanges = selected.filter((e) => EXCHANGE_ENV_MAP[e].chain === "apikey");
161
163
  const newEnv = { ...env };
162
164
  if (evmExchanges.length > 0) {
163
165
  console.log(chalk.cyan.bold("\n EVM Private Key") + chalk.gray(` (for ${evmExchanges.join(", ")})`));
@@ -213,6 +215,34 @@ export function registerInitCommand(program) {
213
215
  }
214
216
  }
215
217
  }
218
+ if (apikeyExchanges.length > 0) {
219
+ for (const ex of apikeyExchanges) {
220
+ const info = EXCHANGE_ENV_MAP[ex];
221
+ console.log(chalk.cyan.bold(`\n ${info.label} API Credentials`));
222
+ const apiKey = await ask(rl, ` ${info.label} API key: `);
223
+ if (!apiKey) {
224
+ console.log(chalk.gray(" Skipped."));
225
+ continue;
226
+ }
227
+ const { valid, address } = await validateKey("apikey", apiKey);
228
+ if (!valid) {
229
+ console.log(chalk.red(" Invalid API key (must be at least 16 chars), skipped."));
230
+ continue;
231
+ }
232
+ newEnv[info.envKey] = apiKey;
233
+ console.log(` ${chalk.green("OK")} ${chalk.gray(address)}`);
234
+ if (info.envKeySecret) {
235
+ const secret = await ask(rl, ` ${info.label} API secret: `);
236
+ if (secret) {
237
+ newEnv[info.envKeySecret] = secret;
238
+ console.log(` ${chalk.green("OK")} secret saved`);
239
+ }
240
+ else {
241
+ console.log(chalk.yellow(" No secret provided — some operations may fail."));
242
+ }
243
+ }
244
+ }
245
+ }
216
246
  // Default exchange
217
247
  const settings = loadSettings();
218
248
  if (selected.length === 1) {
@@ -256,7 +286,9 @@ export function registerInitCommand(program) {
256
286
  }
257
287
  function parseExchangeChoice(input) {
258
288
  const lower = input.toLowerCase().trim();
259
- if (lower === "all" || lower === "1,2,3")
289
+ if (lower === "all" || lower === "1,2,3,4")
290
+ return ["pacifica", "hyperliquid", "lighter", "aster"];
291
+ if (lower === "1,2,3")
260
292
  return ["pacifica", "hyperliquid", "lighter"];
261
293
  const exchanges = [];
262
294
  const parts = lower.split(/[,\s]+/);
@@ -267,6 +299,8 @@ function parseExchangeChoice(input) {
267
299
  exchanges.push("hyperliquid");
268
300
  else if (p === "3" || p.startsWith("lig") || p === "lt")
269
301
  exchanges.push("lighter");
302
+ else if (p === "4" || p.startsWith("ast"))
303
+ exchanges.push("aster");
270
304
  }
271
305
  return [...new Set(exchanges)];
272
306
  }
@@ -1,4 +1,4 @@
1
- import { printJson, jsonOk } from "../utils.js";
1
+ import { printJson, jsonOk, jsonError } from "../utils.js";
2
2
  import { setEnvVar } from "./init.js";
3
3
  import chalk from "chalk";
4
4
  import { hasPacificaSdk, hasLighterAccount } from "../exchanges/capabilities.js";
@@ -19,6 +19,10 @@ export function registerManageCommands(program, getAdapter, isJson, getPacificaA
19
19
  .action(async (symbol, mode) => {
20
20
  const m = mode.toLowerCase();
21
21
  if (m !== "cross" && m !== "isolated") {
22
+ if (isJson()) {
23
+ console.error(JSON.stringify(jsonError("INVALID_PARAMS", "Mode must be cross or isolated")));
24
+ process.exit(1);
25
+ }
22
26
  console.error(chalk.red("Mode must be cross or isolated"));
23
27
  process.exit(1);
24
28
  }
package/dist/config.js CHANGED
@@ -51,7 +51,7 @@ export async function loadPrivateKey(exchange, pkOverride, walletName) {
51
51
  catch {
52
52
  // wallet module not available, skip
53
53
  }
54
- // 5. Legacy key file (~/.perp/<exchange>.key)
54
+ // 6. Legacy key file (~/.perp/<exchange>.key)
55
55
  const keyFile = resolve(process.env.HOME || "~", ".perp", `${exchange}.key`);
56
56
  if (existsSync(keyFile)) {
57
57
  return readFileSync(keyFile, "utf-8").trim();
@@ -12,7 +12,7 @@ export declare const TESTNET_PACIFICA_VAULT = "5SDFdHZGTZbyRYu54CgmRkCGnPHC5pYaN
12
12
  export declare const TESTNET_USDC_MINT = "USDPqRbLidFGufty2s3oizmDEKdqx7ePTqzDMbf5ZKM";
13
13
  export declare const DEFAULT_EXPIRY_WINDOW = 5000;
14
14
  export declare const USDC_DECIMALS = 6;
15
- export declare const BUILDER_CODE: string;
15
+ export declare const BUILDER_CODE = "PERPCLI";
16
16
  export type Network = "mainnet" | "testnet";
17
17
  export declare function getNetworkConfig(network?: Network): {
18
18
  restUrl: string;
@@ -20,7 +20,10 @@ export const DEFAULT_EXPIRY_WINDOW = 5000; // 5 seconds
20
20
  // USDC decimals
21
21
  export const USDC_DECIMALS = 6;
22
22
  // Builder code — 0% fee, used for Pacifica hackathon tracking
23
- export const BUILDER_CODE = process.env.PACIFICA_BUILDER_CODE || "PERPCLI";
23
+ // The actual 3-way fallback (env → settings.referralCodes.pacifica → "PERPCLI") is
24
+ // resolved in index.ts before constructing PacificaAdapter. This constant is only
25
+ // a last-resort default for direct PacificaClient usage without a builderCode arg.
26
+ export const BUILDER_CODE = "PERPCLI";
24
27
  export function getNetworkConfig(network = "mainnet") {
25
28
  if (network === "testnet") {
26
29
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.7.8",
3
+ "version": "0.7.9",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter, Aster",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",