perp-cli 0.12.1 → 0.12.3

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.
@@ -1157,6 +1157,56 @@ export function registerWalletAgentCommands(walletCmd, isJson) {
1157
1157
  }
1158
1158
  });
1159
1159
  }
1160
+ // ── Pre-venue rollback helpers ───────────────────────────────────────────────
1161
+ //
1162
+ // When `wallet agent approve <dex>` fails BEFORE the venue accepted the agent
1163
+ // (HTTP error, body-code rejection, parse error, network drop), the locally
1164
+ // generated `agent-<dex>-<master>` OWS wallet is orphaned in `~/.ows/wallets/`.
1165
+ // Retry collides with "wallet name already exists". These helpers auto-delete
1166
+ // the orphan and produce an actionable remediation string.
1167
+ //
1168
+ // Post-venue (persist-only) failures must NOT use these — those leave a
1169
+ // status:"partial" record so the user can still revoke venue-side state.
1170
+ /**
1171
+ * Best-effort delete of an orphan local OWS wallet. Returns true on success,
1172
+ * false on failure (also logs the failure to stderr per SSOT Rule #2: rollback
1173
+ * failures must not silently mask the original error). The caller threads the
1174
+ * boolean into the user-visible remediation.
1175
+ */
1176
+ function tryDeleteOrphanWallet(ows, walletName) {
1177
+ if (!walletName)
1178
+ return false;
1179
+ try {
1180
+ ows.deleteWallet(walletName);
1181
+ return true;
1182
+ }
1183
+ catch (cleanupErr) {
1184
+ // Don't swallow — surface the rollback failure on stderr so the user can
1185
+ // see exactly what's stuck. Original error still propagates from caller.
1186
+ process.stderr.write(`[warn] failed to clean up orphan wallet "${walletName}": ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}\n`);
1187
+ return false;
1188
+ }
1189
+ }
1190
+ /**
1191
+ * Build an actionable remediation message for pre-venue approve failures.
1192
+ * On clean rollback: tells the user to deposit + retry the canonical
1193
+ * `wallet agent approve <dex> --master <master>` command (no manual steps).
1194
+ * On rollback failure: tells the user to manually delete the orphan wallet
1195
+ * before retrying. The agent address is appended for diagnostic purposes
1196
+ * (some users may need it for the venue UI).
1197
+ */
1198
+ function buildPreVenueRemediation(opts) {
1199
+ const { cleanupOk, dex, masterName, agentAddress, hint } = opts;
1200
+ const retry = `perp wallet agent approve ${dex} --master ${masterName}`;
1201
+ const head = cleanupOk
1202
+ ? `Local agent wallet cleaned up. Deposit funds and retry: ${retry}`
1203
+ : `Manual cleanup required — local agent wallet may still exist. Run: perp wallet remove agent-${dex === "hyperliquid" ? "hl" : dex === "pacifica" ? "pac" : "aster"}-${masterName}, then retry: ${retry}`;
1204
+ const parts = [head];
1205
+ if (hint)
1206
+ parts.push(hint);
1207
+ parts.push(`Agent address (diagnostic): ${agentAddress}`);
1208
+ return parts.join(" | ");
1209
+ }
1160
1210
  /**
1161
1211
  * Core approve flow: Steps 2-12 (OWS wallet + policy + api-key + EIP-712 + Aster POST + persist).
1162
1212
  * Caller MUST hold the exchange lock before calling this function.
@@ -1233,13 +1283,15 @@ async function runApproveFlow(opts) {
1233
1283
  });
1234
1284
  if (!builderRes.ok) {
1235
1285
  const errText = await builderRes.text().catch(() => "");
1236
- // Aster builder POST failed — rollback api key (no Aster agent registered yet)
1286
+ // Aster builder POST failed — pre-venue (no Aster agent registered yet).
1287
+ // Rollback both: api key + orphan local OWS wallet.
1237
1288
  if (owsApiKeyId) {
1238
1289
  try {
1239
1290
  ows.revokeApiKey(owsApiKeyId);
1240
1291
  }
1241
1292
  catch { /* best effort */ }
1242
1293
  }
1294
+ tryDeleteOrphanWallet(ows, agentWalletName);
1243
1295
  throw new PerpError("EXCHANGE_ERROR", `Aster approveBuilder failed (${builderRes.status}): ${errText.slice(0, 200)}`, {
1244
1296
  remediation: "Check builder address and max-fee-rate parameters",
1245
1297
  });
@@ -1253,6 +1305,7 @@ async function runApproveFlow(opts) {
1253
1305
  }
1254
1306
  catch { /* best effort */ }
1255
1307
  }
1308
+ tryDeleteOrphanWallet(ows, agentWalletName);
1256
1309
  const errMsg = builderResp.msg ?? builderResp.message ?? String(builderResp.code);
1257
1310
  throw new PerpError("EXCHANGE_ERROR", `Aster approveBuilder failed: ${errMsg}`, {
1258
1311
  remediation: "Check builder address and max-fee-rate parameters",
@@ -1307,30 +1360,37 @@ async function runApproveFlow(opts) {
1307
1360
  asterPostSucceeded = true;
1308
1361
  }
1309
1362
  else {
1310
- // Aster POST body indicated failure — rollback api key (Aster did not register)
1363
+ // Aster POST body indicated failure — pre-venue (Aster did not register).
1364
+ // Rollback both api key + orphan local OWS wallet.
1311
1365
  if (owsApiKeyId) {
1312
1366
  try {
1313
1367
  ows.revokeApiKey(owsApiKeyId);
1314
1368
  }
1315
1369
  catch { /* best effort */ }
1316
1370
  }
1371
+ const cleanupOk = tryDeleteOrphanWallet(ows, agentWalletName);
1317
1372
  const errMsg = resp.msg ?? resp.message ?? String(resp.code);
1318
1373
  throw new PerpError("APPROVE_PARTIAL", `Aster approveAgent failed: ${errMsg}`, {
1319
- remediation: `Agent address for cleanup: ${agentEvmAddress}`,
1374
+ remediation: buildPreVenueRemediation({
1375
+ cleanupOk, dex: "aster", masterName, agentAddress: agentEvmAddress,
1376
+ }),
1320
1377
  });
1321
1378
  }
1322
1379
  }
1323
1380
  else {
1324
- // HTTP error — rollback api key (Aster did not register)
1381
+ // HTTP error — pre-venue (Aster did not register). Rollback both.
1325
1382
  if (owsApiKeyId) {
1326
1383
  try {
1327
1384
  ows.revokeApiKey(owsApiKeyId);
1328
1385
  }
1329
1386
  catch { /* best effort */ }
1330
1387
  }
1388
+ const cleanupOk = tryDeleteOrphanWallet(ows, agentWalletName);
1331
1389
  const errText = await agentRes.text().catch(() => "");
1332
1390
  throw new PerpError("APPROVE_PARTIAL", `Aster approveAgent failed (${agentRes.status}): ${errText.slice(0, 200)}`, {
1333
- remediation: `Agent address for cleanup: ${agentEvmAddress}`,
1391
+ remediation: buildPreVenueRemediation({
1392
+ cleanupOk, dex: "aster", masterName, agentAddress: agentEvmAddress,
1393
+ }),
1334
1394
  });
1335
1395
  }
1336
1396
  // Step 12: Persist agent meta
@@ -1551,13 +1611,26 @@ async function runHlApproveFlow(opts) {
1551
1611
  resp = JSON.parse(text);
1552
1612
  }
1553
1613
  catch {
1614
+ // Pre-venue parse failure — Hyperliquid did NOT register the agent.
1615
+ // Roll back the orphan local OWS wallet so retry doesn't collide with
1616
+ // "wallet name already exists". Errors during cleanup must NOT mask the
1617
+ // original network/parse error (SSOT Rule #2).
1618
+ const cleanupOk = tryDeleteOrphanWallet(ows, agentWalletName);
1554
1619
  throw new PerpError("EXCHANGE_ERROR", `HL exchange API parse error (${agentRes.status}): ${text.slice(0, 200)}`, {
1555
- remediation: "Check Hyperliquid API status",
1620
+ remediation: buildPreVenueRemediation({
1621
+ cleanupOk, dex: "hyperliquid", masterName, agentAddress: agentEvmAddress,
1622
+ hint: "Check Hyperliquid API status",
1623
+ }),
1556
1624
  });
1557
1625
  }
1558
1626
  if (resp?.status === "err") {
1627
+ // Pre-venue rejection — Hyperliquid did NOT register the agent. Roll
1628
+ // back the orphan local OWS wallet so retry can proceed cleanly.
1629
+ const cleanupOk = tryDeleteOrphanWallet(ows, agentWalletName);
1559
1630
  throw new PerpError("APPROVE_PARTIAL", `HL approveAgent failed: ${typeof resp.response === "string" ? resp.response : JSON.stringify(resp)}`, {
1560
- remediation: `Agent address for cleanup: ${agentEvmAddress}`,
1631
+ remediation: buildPreVenueRemediation({
1632
+ cleanupOk, dex: "hyperliquid", masterName, agentAddress: agentEvmAddress,
1633
+ }),
1561
1634
  });
1562
1635
  }
1563
1636
  // Step 6: Persist
@@ -1682,24 +1755,36 @@ async function runPacApproveFlow(opts) {
1682
1755
  // Pacifica REST envelope: success === true OR explicit data field
1683
1756
  if (resp.success === false || resp.error) {
1684
1757
  const errMsg = typeof resp.error === "string" ? resp.error : JSON.stringify(resp);
1758
+ // Pre-venue rejection — Pacifica did NOT register the agent.
1759
+ const cleanupOk = tryDeleteOrphanWallet(ows, agentWalletName);
1685
1760
  throw new PerpError("APPROVE_PARTIAL", `Pacifica bind_agent_wallet failed: ${errMsg}`, {
1686
- remediation: `Agent address for cleanup: ${agentSolanaAddress}`,
1761
+ remediation: buildPreVenueRemediation({
1762
+ cleanupOk, dex: "pacifica", masterName, agentAddress: agentSolanaAddress,
1763
+ }),
1687
1764
  });
1688
1765
  }
1689
1766
  pacPostSucceeded = true;
1690
1767
  }
1691
1768
  else {
1692
1769
  const errText = await httpRes.text().catch(() => "");
1770
+ // Pre-venue rejection — Pacifica did NOT register the agent.
1771
+ const cleanupOk = tryDeleteOrphanWallet(ows, agentWalletName);
1693
1772
  throw new PerpError("APPROVE_PARTIAL", `Pacifica bind_agent_wallet failed (${httpRes.status}): ${errText.slice(0, 200)}`, {
1694
- remediation: `Agent address for cleanup: ${agentSolanaAddress}`,
1773
+ remediation: buildPreVenueRemediation({
1774
+ cleanupOk, dex: "pacifica", masterName, agentAddress: agentSolanaAddress,
1775
+ }),
1695
1776
  });
1696
1777
  }
1697
1778
  }
1698
1779
  catch (httpErr) {
1699
1780
  if (httpErr instanceof PerpError)
1700
1781
  throw httpErr;
1782
+ // Network-level failure — Pacifica did NOT register the agent.
1783
+ const cleanupOk = tryDeleteOrphanWallet(ows, agentWalletName);
1701
1784
  throw new PerpError("APPROVE_PARTIAL", `Pacifica bind_agent_wallet network error: ${httpErr instanceof Error ? httpErr.message : String(httpErr)}`, {
1702
- remediation: `Agent address for cleanup: ${agentSolanaAddress}`,
1785
+ remediation: buildPreVenueRemediation({
1786
+ cleanupOk, dex: "pacifica", masterName, agentAddress: agentSolanaAddress,
1787
+ }),
1703
1788
  });
1704
1789
  }
1705
1790
  // Step 5: Persist
@@ -14,3 +14,22 @@ export declare function validateKey(chain: "solana" | "evm" | "apikey", key: str
14
14
  address: string;
15
15
  }>;
16
16
  export declare function registerInitCommand(program: Command): void;
17
+ /**
18
+ * Drive `perp setup` end-to-end without TTY. Required:
19
+ * - --passphrase or OWS_PASSPHRASE env (3-path resolver)
20
+ * Optional:
21
+ * - --wallet-name (default: "main")
22
+ * - --default-exchange (must be one of pacifica/hyperliquid/lighter/aster)
23
+ *
24
+ * Side effects:
25
+ * 1. Creates an OWS wallet via createWallet(name, passphrase)
26
+ * 2. Sets settings.owsActiveWallet (if unset) + settings.defaultExchange (if provided)
27
+ *
28
+ * Errors are returned as INVALID_PARAMS / PASSPHRASE_REQUIRED — never via
29
+ * readline. JSON envelope is emitted on stdout when --json is present.
30
+ */
31
+ export declare function runNonInteractiveSetup(opts: {
32
+ walletName?: string;
33
+ passphrase?: string;
34
+ defaultExchange?: string;
35
+ }): Promise<void>;
@@ -3,6 +3,10 @@ import { createInterface } from "readline";
3
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
4
4
  import { resolve } from "path";
5
5
  import { loadSettings, saveSettings } from "../settings.js";
6
+ import { PerpError } from "../errors.js";
7
+ import { jsonOk, jsonError, printJson } from "../utils.js";
8
+ import { resolvePassphrase } from "../agent-wallet/passphrase.js";
9
+ import { loadOws } from "../signer/ows-loader.js";
6
10
  const PERP_DIR = resolve(process.env.HOME || "~", ".perp");
7
11
  const ENV_FILE = resolve(PERP_DIR, ".env");
8
12
  // ── .env helpers ──────────────────────────────────────────────
@@ -109,12 +113,23 @@ export async function validateKey(chain, key) {
109
113
  }
110
114
  // ── Commands ──────────────────────────────────────────────────
111
115
  export function registerInitCommand(program) {
112
- // ── perp setup (interactive) ──
116
+ // ── perp setup (interactive + non-interactive) ──
113
117
  program
114
118
  .command("setup")
115
119
  .alias("init")
116
- .description("Interactive setup wizard — configure keys & default exchange")
117
- .action(async () => {
120
+ .description("Setup wizard — interactive by default, scriptable with --non-interactive")
121
+ .option("--non-interactive", "Run without prompts; requires --passphrase or OWS_PASSPHRASE env")
122
+ .option("--wallet-name <name>", "OWS wallet alias to create (default: 'main')")
123
+ .option("--passphrase <pp>", "OWS vault passphrase (fallback: OWS_PASSPHRASE env / stdin)")
124
+ .option("--default-exchange <name>", "Default exchange (pacifica/hyperliquid/lighter/aster)")
125
+ .action(async (opts) => {
126
+ // Non-interactive mode: skip all readline prompts, drive everything from
127
+ // flags + OWS_PASSPHRASE env. This is the agent-friendly onboarding path
128
+ // (CI / Docker / scripted init) — see plan v3.0 "agent-friendly five
129
+ // guarantees" P1 (no-prompt) and P3 (passphrase 3-path).
130
+ if (opts.nonInteractive) {
131
+ return runNonInteractiveSetup(opts);
132
+ }
118
133
  const rl = createInterface({ input: process.stdin, output: process.stdout });
119
134
  try {
120
135
  console.log(chalk.cyan.bold("\n perp-cli Setup Wizard\n"));
@@ -304,3 +319,109 @@ function parseExchangeChoice(input) {
304
319
  }
305
320
  return [...new Set(exchanges)];
306
321
  }
322
+ // ── Non-interactive setup (agent / CI / Docker) ──────────────────────────────
323
+ const VALID_EXCHANGES = ["pacifica", "hyperliquid", "lighter", "aster"];
324
+ /**
325
+ * Test seam: emit JSON envelopes when the global --json flag is present.
326
+ * Mirrors the inline pattern used elsewhere (see src/index.ts:87) instead of
327
+ * wiring isJson() through registerInitCommand for one call site.
328
+ */
329
+ function setupIsJson() {
330
+ return process.argv.includes("--json");
331
+ }
332
+ /**
333
+ * Drive `perp setup` end-to-end without TTY. Required:
334
+ * - --passphrase or OWS_PASSPHRASE env (3-path resolver)
335
+ * Optional:
336
+ * - --wallet-name (default: "main")
337
+ * - --default-exchange (must be one of pacifica/hyperliquid/lighter/aster)
338
+ *
339
+ * Side effects:
340
+ * 1. Creates an OWS wallet via createWallet(name, passphrase)
341
+ * 2. Sets settings.owsActiveWallet (if unset) + settings.defaultExchange (if provided)
342
+ *
343
+ * Errors are returned as INVALID_PARAMS / PASSPHRASE_REQUIRED — never via
344
+ * readline. JSON envelope is emitted on stdout when --json is present.
345
+ */
346
+ export async function runNonInteractiveSetup(opts) {
347
+ const isJson = setupIsJson();
348
+ try {
349
+ // Validate --default-exchange shape early so users hit a precise error
350
+ // before passphrase resolution.
351
+ if (opts.defaultExchange !== undefined &&
352
+ !VALID_EXCHANGES.includes(opts.defaultExchange)) {
353
+ throw new PerpError("INVALID_PARAMS", `Invalid --default-exchange "${opts.defaultExchange}"`, {
354
+ remediation: `Use one of: ${VALID_EXCHANGES.join(", ")}`,
355
+ });
356
+ }
357
+ // Resolve passphrase. resolvePassphrase already throws PASSPHRASE_REQUIRED
358
+ // when stdin is non-TTY and nothing was provided. In TTY mode it returns
359
+ // null — non-interactive mode must NOT fall back to a prompt.
360
+ const resolved = await resolvePassphrase({ flag: opts.passphrase });
361
+ if (resolved === null) {
362
+ throw new PerpError("PASSPHRASE_REQUIRED", "Non-interactive setup requires a passphrase", {
363
+ remediation: "Provide --passphrase <pp> or set OWS_PASSPHRASE env var",
364
+ });
365
+ }
366
+ const pp = resolved;
367
+ const walletName = opts.walletName ?? "main";
368
+ // Create OWS wallet
369
+ const ows = loadOws();
370
+ const w = ows.createWallet(walletName, pp);
371
+ // Update settings
372
+ const settings = loadSettings();
373
+ if (!settings.owsActiveWallet) {
374
+ settings.owsActiveWallet = walletName;
375
+ }
376
+ if (opts.defaultExchange) {
377
+ settings.defaultExchange = opts.defaultExchange;
378
+ }
379
+ saveSettings(settings);
380
+ if (isJson) {
381
+ printJson(jsonOk({
382
+ walletName: w.name,
383
+ walletId: w.id,
384
+ accounts: w.accounts,
385
+ owsActiveWallet: settings.owsActiveWallet,
386
+ defaultExchange: settings.defaultExchange ?? null,
387
+ mode: "non-interactive",
388
+ }));
389
+ return;
390
+ }
391
+ console.log(chalk.cyan.bold("\n perp-cli Setup (non-interactive) — Done\n"));
392
+ console.log(` Wallet: ${chalk.white.bold(w.name)}`);
393
+ console.log(` Active wallet: ${chalk.cyan(settings.owsActiveWallet)}`);
394
+ if (settings.defaultExchange) {
395
+ console.log(` Default exchange: ${chalk.cyan(settings.defaultExchange)}`);
396
+ }
397
+ console.log(chalk.gray(`\n Vault: ~/.ows/ (AES-256-GCM encrypted)\n`));
398
+ }
399
+ catch (err) {
400
+ if (isJson) {
401
+ if (err instanceof PerpError) {
402
+ printJson(jsonError(err.structured.code, err.structured.message, {
403
+ ...(err.structured.remediation
404
+ ? { details: { remediation: err.structured.remediation } }
405
+ : {}),
406
+ }));
407
+ }
408
+ else {
409
+ const msg = err instanceof Error ? err.message : String(err);
410
+ printJson(jsonError("UNKNOWN", msg));
411
+ }
412
+ process.exitCode = 1;
413
+ return;
414
+ }
415
+ if (err instanceof PerpError) {
416
+ console.error(chalk.red(`\n Error [${err.structured.code}]: ${err.structured.message}`));
417
+ if (err.structured.remediation) {
418
+ console.error(chalk.gray(` Remediation: ${err.structured.remediation}\n`));
419
+ }
420
+ }
421
+ else {
422
+ const msg = err instanceof Error ? err.message : String(err);
423
+ console.error(chalk.red(`\n Error: ${msg}\n`));
424
+ }
425
+ process.exitCode = 1;
426
+ }
427
+ }
@@ -324,6 +324,7 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
324
324
  .description("Generate a new wallet (OWS encrypted vault, multi-chain)")
325
325
  .option("--words <count>", "Mnemonic word count (12 or 24)", "12")
326
326
  .option("--show-mnemonic", "Display the mnemonic phrase (CAUTION)")
327
+ .option("--passphrase <pp>", "Master OWS passphrase (fallback: OWS_PASSPHRASE env / stdin)")
327
328
  .option("--legacy <chain>", "Legacy mode: generate solana or evm key (unencrypted)")
328
329
  .action(async (name, opts) => {
329
330
  // Legacy mode for backward compat
@@ -332,8 +333,14 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
332
333
  }
333
334
  const walletName = name || "default";
334
335
  try {
336
+ // Resolve vault passphrase via the standard 3-path resolver (--passphrase
337
+ // > OWS_PASSPHRASE env > stdin pipe). When stdin is a TTY and nothing was
338
+ // provided, the resolver returns null; fall back to "" to preserve the
339
+ // historical "no passphrase" default for interactive use.
340
+ const { resolvePassphrase } = await import("../agent-wallet/passphrase.js");
341
+ const pp = (await resolvePassphrase({ flag: opts.passphrase })) ?? "";
335
342
  const ows = loadOws();
336
- const w = ows.createWallet(walletName, "", parseInt(opts.words));
343
+ const w = ows.createWallet(walletName, pp, parseInt(opts.words));
337
344
  // Set as active wallet
338
345
  const { loadSettings: ls, saveSettings: ss } = await import("../settings.js");
339
346
  const settings = ls();
@@ -393,7 +400,7 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
393
400
  .option("--evm <hex>", "EVM (secp256k1) private key — use with --solana to import both curves")
394
401
  .option("--solana <key>", "Solana (ed25519) private key — accepts hex (64), base58 (Phantom export), or JSON byte array. Use with --evm to import both curves")
395
402
  .option("--mnemonic", "Import as mnemonic phrase instead of private key")
396
- .option("--passphrase <pp>", "OWS vault encryption passphrase (default: empty / OWS_PASSPHRASE env)")
403
+ .option("--passphrase <pp>", "Master OWS passphrase (fallback: OWS_PASSPHRASE env / stdin)")
397
404
  .option("--legacy <chain>", "Legacy mode: import to wallets.json (solana or evm)")
398
405
  .action(async (privateKey, opts) => {
399
406
  // Legacy mode (positional only)
@@ -475,9 +482,12 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
475
482
  throw new Error(`Invalid Solana key — expected hex (64 chars), base58 (Phantom export), or JSON byte array. Inner: ${msg}`);
476
483
  }
477
484
  };
478
- // Resolve vault passphrase (precedence: --passphrase > OWS_PASSPHRASE env > "")
479
- const pp = opts.passphrase
480
- ?? (("OWS_PASSPHRASE" in process.env) ? (process.env["OWS_PASSPHRASE"] ?? "") : "");
485
+ // Resolve vault passphrase via the standard 3-path resolver (--passphrase
486
+ // > OWS_PASSPHRASE env > stdin pipe). When stdin is a TTY and nothing was
487
+ // provided, the resolver returns null; fall back to "" to preserve the
488
+ // historical "no passphrase" default for interactive use.
489
+ const { resolvePassphrase } = await import("../agent-wallet/passphrase.js");
490
+ const pp = (await resolvePassphrase({ flag: opts.passphrase })) ?? "";
481
491
  let w;
482
492
  if (opts.mnemonic) {
483
493
  w = ows.importWalletMnemonic(opts.name, privateKey, pp);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.12.1",
3
+ "version": "0.12.3",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter, Aster",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",
@@ -59,7 +59,7 @@
59
59
  "ink": "^6.8.0",
60
60
  "ink-spinner": "^5.0.0",
61
61
  "ink-table": "^3.1.0",
62
- "lighter-ts-sdk": "^1.0.10",
62
+ "lighter-ts-sdk": "1.0.10",
63
63
  "react": "^19.2.4",
64
64
  "tweetnacl": "^1.0.3",
65
65
  "ws": "^8.19.0",