perp-cli 0.14.2 → 0.15.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.
Files changed (313) hide show
  1. package/README.md +152 -13
  2. package/dist/agent-wallet/store.d.ts +3 -2
  3. package/dist/agent-wallet/store.js +2 -2
  4. package/dist/api/public/hyperliquid.js +4 -0
  5. package/dist/api/public/pacifica.js +83 -13
  6. package/dist/api/public/urls.d.ts +2 -0
  7. package/dist/api/public/urls.js +3 -0
  8. package/dist/arb/utils.d.ts +1 -2
  9. package/dist/arb/utils.js +1 -2
  10. package/dist/bot/apex/pulse.js +3 -0
  11. package/dist/bot/apex/radar.js +2 -1
  12. package/dist/bot/engine.d.ts +10 -1
  13. package/dist/bot/engine.js +14 -6
  14. package/dist/bot/indicators.d.ts +7 -0
  15. package/dist/bot/indicators.js +16 -0
  16. package/dist/bot/perp-order.d.ts +4 -4
  17. package/dist/bot/perp-order.js +5 -5
  18. package/dist/bot/quarantine.d.ts +1 -1
  19. package/dist/bot/quarantine.js +1 -1
  20. package/dist/bot/strategies/apex-strategy.js +3 -2
  21. package/dist/bot/strategies/engine-mm.js +6 -5
  22. package/dist/bot/strategies/funding-arb-strategy.js +9 -9
  23. package/dist/bot/strategies/funding-arb-utils.d.ts +15 -2
  24. package/dist/bot/strategies/funding-arb-utils.js +43 -24
  25. package/dist/bot/strategies/funding-auto.js +2 -2
  26. package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
  27. package/dist/bot/strategies/liquidation-mm.js +8 -2
  28. package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
  29. package/dist/bot/strategies/momentum-breakout.js +11 -3
  30. package/dist/bot/strategies/spot-perp-arb-strategy.js +23 -12
  31. package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
  32. package/dist/bot/strategies/spot-perp-executor.js +1 -1
  33. package/dist/bot/strategy-types.d.ts +2 -1
  34. package/dist/bot/tui/BotDashboard.d.ts +1 -1
  35. package/dist/bot/tui/BotDashboard.js +1 -1
  36. package/dist/cache.js +12 -6
  37. package/dist/cli-program.d.ts +15 -0
  38. package/dist/cli-program.js +125 -5
  39. package/dist/commands/agent-import.d.ts +7 -0
  40. package/dist/commands/agent-import.js +30 -1
  41. package/dist/commands/agent.d.ts +6 -3
  42. package/dist/commands/agent.js +160 -12
  43. package/dist/commands/arb/index.js +4 -4
  44. package/dist/commands/arb-auto.js +21 -17
  45. package/dist/commands/arb.d.ts +12 -0
  46. package/dist/commands/arb.js +61 -143
  47. package/dist/commands/bot.js +1 -1
  48. package/dist/commands/builder.js +1 -1
  49. package/dist/commands/funds.js +113 -3
  50. package/dist/commands/init.d.ts +1 -1
  51. package/dist/commands/init.js +27 -7
  52. package/dist/commands/manage.js +23 -21
  53. package/dist/commands/market.js +168 -36
  54. package/dist/commands/options.d.ts +9 -0
  55. package/dist/commands/options.js +134 -0
  56. package/dist/commands/pacifica-earn.d.ts +5 -0
  57. package/dist/commands/pacifica-earn.js +468 -0
  58. package/dist/commands/pacifica-orders.d.ts +14 -0
  59. package/dist/commands/pacifica-orders.js +142 -0
  60. package/dist/commands/portfolio.js +32 -47
  61. package/dist/commands/rebalance.d.ts +1 -0
  62. package/dist/commands/rebalance.js +15 -0
  63. package/dist/commands/risk.d.ts +11 -2
  64. package/dist/commands/risk.js +39 -44
  65. package/dist/commands/trade.d.ts +1 -0
  66. package/dist/commands/trade.js +63 -58
  67. package/dist/commands/wallet.js +1 -1
  68. package/dist/config.d.ts +2 -2
  69. package/dist/config.js +15 -4
  70. package/dist/constants.js +6 -0
  71. package/dist/dashboard/server.js +1 -1
  72. package/dist/dashboard/ui.js +1 -1
  73. package/dist/errors.d.ts +24 -1
  74. package/dist/errors.js +25 -3
  75. package/dist/exchanges/arcus/adapter.d.ts +7 -7
  76. package/dist/exchanges/arcus/adapter.js +12 -12
  77. package/dist/exchanges/arcus/agent.d.ts +1 -1
  78. package/dist/exchanges/arcus/agent.js +1 -1
  79. package/dist/exchanges/arcus/client.js +1 -1
  80. package/dist/exchanges/arcus/signing.d.ts +2 -3
  81. package/dist/exchanges/arcus/signing.js +1 -1
  82. package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
  83. package/dist/exchanges/arcus/spot-chain.js +1 -1
  84. package/dist/exchanges/arcus/spot-router.d.ts +1 -1
  85. package/dist/exchanges/arcus/spot-router.js +2 -2
  86. package/dist/exchanges/arcus/spot.d.ts +2 -3
  87. package/dist/exchanges/arcus/spot.js +2 -3
  88. package/dist/exchanges/aster-spot.d.ts +3 -3
  89. package/dist/exchanges/aster-spot.js +6 -9
  90. package/dist/exchanges/aster-typed-data.d.ts +1 -1
  91. package/dist/exchanges/aster-typed-data.js +1 -1
  92. package/dist/exchanges/aster.d.ts +9 -4
  93. package/dist/exchanges/aster.js +125 -55
  94. package/dist/exchanges/builder-fee.d.ts +1 -1
  95. package/dist/exchanges/builder-fee.js +13 -2
  96. package/dist/exchanges/canonical-symbols.d.ts +1 -2
  97. package/dist/exchanges/canonical-symbols.js +26 -2
  98. package/dist/exchanges/derive/client.d.ts +37 -0
  99. package/dist/exchanges/derive/client.js +135 -0
  100. package/dist/exchanges/derive/options.d.ts +68 -0
  101. package/dist/exchanges/derive/options.js +360 -0
  102. package/dist/exchanges/extended/adapter.d.ts +82 -0
  103. package/dist/exchanges/extended/adapter.js +179 -0
  104. package/dist/exchanges/extended/client.d.ts +38 -0
  105. package/dist/exchanges/extended/client.js +88 -0
  106. package/dist/exchanges/extended/public.d.ts +130 -0
  107. package/dist/exchanges/extended/public.js +429 -0
  108. package/dist/exchanges/grvt/adapter.d.ts +86 -0
  109. package/dist/exchanges/grvt/adapter.js +206 -0
  110. package/dist/exchanges/grvt/client.d.ts +25 -0
  111. package/dist/exchanges/grvt/client.js +86 -0
  112. package/dist/exchanges/grvt/public.d.ts +151 -0
  113. package/dist/exchanges/grvt/public.js +438 -0
  114. package/dist/exchanges/hyperliquid-spot.d.ts +1 -1
  115. package/dist/exchanges/hyperliquid-spot.js +5 -5
  116. package/dist/exchanges/hyperliquid.d.ts +22 -16
  117. package/dist/exchanges/hyperliquid.js +80 -55
  118. package/dist/exchanges/index.d.ts +1 -1
  119. package/dist/exchanges/index.js +1 -1
  120. package/dist/exchanges/interface.d.ts +97 -8
  121. package/dist/exchanges/interface.js +19 -2
  122. package/dist/exchanges/ioc-outcome.d.ts +1 -1
  123. package/dist/exchanges/ioc-outcome.js +1 -1
  124. package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
  125. package/dist/exchanges/legacy-trading-v2.js +7 -2
  126. package/dist/exchanges/lighter-public.d.ts +2 -2
  127. package/dist/exchanges/lighter-public.js +2 -2
  128. package/dist/exchanges/lighter-signer.d.ts +1 -1
  129. package/dist/exchanges/lighter-signer.js +1 -1
  130. package/dist/exchanges/lighter-spot.d.ts +3 -4
  131. package/dist/exchanges/lighter-spot.js +6 -7
  132. package/dist/exchanges/lighter.d.ts +11 -11
  133. package/dist/exchanges/lighter.js +28 -14
  134. package/dist/exchanges/ondo/adapter.d.ts +5 -6
  135. package/dist/exchanges/ondo/adapter.js +16 -14
  136. package/dist/exchanges/ondo/agent.d.ts +1 -1
  137. package/dist/exchanges/ondo/agent.js +1 -1
  138. package/dist/exchanges/ondo/client.d.ts +1 -1
  139. package/dist/exchanges/ondo/client.js +2 -2
  140. package/dist/exchanges/ondo/public.d.ts +8 -1
  141. package/dist/exchanges/ondo/public.js +13 -2
  142. package/dist/exchanges/ondo/spot.d.ts +2 -2
  143. package/dist/exchanges/ondo/spot.js +3 -3
  144. package/dist/exchanges/options-interface.d.ts +158 -0
  145. package/dist/exchanges/options-interface.js +17 -0
  146. package/dist/exchanges/options-venues.d.ts +12 -0
  147. package/dist/exchanges/options-venues.js +35 -0
  148. package/dist/exchanges/pacifica-atomic.d.ts +211 -0
  149. package/dist/exchanges/pacifica-atomic.js +440 -0
  150. package/dist/exchanges/pacifica-decimal.d.ts +10 -0
  151. package/dist/exchanges/pacifica-decimal.js +51 -0
  152. package/dist/exchanges/pacifica-presigned.d.ts +53 -0
  153. package/dist/exchanges/pacifica-presigned.js +141 -0
  154. package/dist/exchanges/pacifica-print.d.ts +200 -0
  155. package/dist/exchanges/pacifica-print.js +417 -0
  156. package/dist/exchanges/pacifica-spot.d.ts +2 -2
  157. package/dist/exchanges/pacifica-spot.js +3 -3
  158. package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
  159. package/dist/exchanges/pacifica-typed-data.js +1 -1
  160. package/dist/exchanges/pacifica-vaults.d.ts +121 -0
  161. package/dist/exchanges/pacifica-vaults.js +264 -0
  162. package/dist/exchanges/pacifica.d.ts +32 -5
  163. package/dist/exchanges/pacifica.js +114 -48
  164. package/dist/exchanges/precision.d.ts +3 -1
  165. package/dist/exchanges/precision.js +9 -2
  166. package/dist/exchanges/qfex/adapter.d.ts +18 -5
  167. package/dist/exchanges/qfex/adapter.js +32 -12
  168. package/dist/exchanges/qfex/agent.d.ts +1 -1
  169. package/dist/exchanges/qfex/agent.js +1 -1
  170. package/dist/exchanges/qfex/client.d.ts +1 -1
  171. package/dist/exchanges/qfex/client.js +1 -1
  172. package/dist/exchanges/qfex/errors.d.ts +1 -1
  173. package/dist/exchanges/qfex/errors.js +2 -2
  174. package/dist/exchanges/qfex/public.d.ts +3 -3
  175. package/dist/exchanges/qfex/public.js +3 -3
  176. package/dist/exchanges/qfex/signing.d.ts +2 -2
  177. package/dist/exchanges/qfex/signing.js +4 -5
  178. package/dist/exchanges/qfex/ws-transport.d.ts +2 -2
  179. package/dist/exchanges/qfex/ws-transport.js +3 -3
  180. package/dist/exchanges/rate-limiter.d.ts +1 -1
  181. package/dist/exchanges/rate-limiter.js +5 -2
  182. package/dist/exchanges/registry.d.ts +1 -1
  183. package/dist/exchanges/registry.js +1 -1
  184. package/dist/exchanges/risex/adapter.d.ts +148 -0
  185. package/dist/exchanges/risex/adapter.js +796 -0
  186. package/dist/exchanges/risex/agent.d.ts +154 -0
  187. package/dist/exchanges/risex/agent.js +384 -0
  188. package/dist/exchanges/risex/client.d.ts +47 -0
  189. package/dist/exchanges/risex/client.js +174 -0
  190. package/dist/exchanges/risex/nonce.d.ts +43 -0
  191. package/dist/exchanges/risex/nonce.js +212 -0
  192. package/dist/exchanges/risex/public.d.ts +64 -0
  193. package/dist/exchanges/risex/public.js +150 -0
  194. package/dist/exchanges/risex/session.d.ts +71 -0
  195. package/dist/exchanges/risex/session.js +171 -0
  196. package/dist/exchanges/risex/typed-data.d.ts +173 -0
  197. package/dist/exchanges/risex/typed-data.js +240 -0
  198. package/dist/exchanges/risex/units.d.ts +29 -0
  199. package/dist/exchanges/risex/units.js +104 -0
  200. package/dist/exchanges/slippage.d.ts +1 -1
  201. package/dist/exchanges/slippage.js +1 -1
  202. package/dist/exchanges/spot-factory.d.ts +3 -3
  203. package/dist/exchanges/spot-factory.js +7 -7
  204. package/dist/exchanges/spot-interface.d.ts +5 -5
  205. package/dist/exchanges/spot-interface.js +2 -2
  206. package/dist/exchanges/spot-result.d.ts +3 -3
  207. package/dist/exchanges/spot-result.js +3 -3
  208. package/dist/exchanges/trading-v2.d.ts +3 -3
  209. package/dist/exchanges/trading-v2.js +9 -6
  210. package/dist/exchanges/txflow/adapter.d.ts +54 -0
  211. package/dist/exchanges/txflow/adapter.js +118 -0
  212. package/dist/exchanges/txflow/client.d.ts +47 -0
  213. package/dist/exchanges/txflow/client.js +153 -0
  214. package/dist/exchanges/txflow/public.d.ts +76 -0
  215. package/dist/exchanges/txflow/public.js +284 -0
  216. package/dist/exchanges/unknown-symbol.d.ts +29 -0
  217. package/dist/exchanges/unknown-symbol.js +51 -0
  218. package/dist/exchanges/variational/adapter.d.ts +71 -0
  219. package/dist/exchanges/variational/adapter.js +137 -0
  220. package/dist/exchanges/variational/client.d.ts +21 -0
  221. package/dist/exchanges/variational/client.js +66 -0
  222. package/dist/exchanges/variational/public.d.ts +126 -0
  223. package/dist/exchanges/variational/public.js +430 -0
  224. package/dist/exchanges/venue-types.d.ts +6 -7
  225. package/dist/exchanges/venue-types.js +2 -2
  226. package/dist/exchanges/venue.d.ts +51 -8
  227. package/dist/exchanges/venue.js +54 -4
  228. package/dist/exchanges/venues/arcus.d.ts +1 -2
  229. package/dist/exchanges/venues/arcus.js +5 -4
  230. package/dist/exchanges/venues/aster.d.ts +2 -2
  231. package/dist/exchanges/venues/aster.js +6 -4
  232. package/dist/exchanges/venues/create-helpers.d.ts +1 -1
  233. package/dist/exchanges/venues/create-helpers.js +3 -3
  234. package/dist/exchanges/venues/extended.d.ts +21 -0
  235. package/dist/exchanges/venues/extended.js +96 -0
  236. package/dist/exchanges/venues/grvt.d.ts +30 -0
  237. package/dist/exchanges/venues/grvt.js +98 -0
  238. package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
  239. package/dist/exchanges/venues/hyperliquid.js +10 -8
  240. package/dist/exchanges/venues/index.d.ts +7 -2
  241. package/dist/exchanges/venues/index.js +8 -2
  242. package/dist/exchanges/venues/lighter-family.d.ts +7 -9
  243. package/dist/exchanges/venues/lighter-family.js +9 -11
  244. package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
  245. package/dist/exchanges/venues/lighter-rh.js +7 -5
  246. package/dist/exchanges/venues/lighter.d.ts +1 -1
  247. package/dist/exchanges/venues/lighter.js +5 -3
  248. package/dist/exchanges/venues/ondo.d.ts +1 -1
  249. package/dist/exchanges/venues/ondo.js +5 -3
  250. package/dist/exchanges/venues/pacifica.d.ts +2 -2
  251. package/dist/exchanges/venues/pacifica.js +6 -4
  252. package/dist/exchanges/venues/qfex.d.ts +1 -1
  253. package/dist/exchanges/venues/qfex.js +4 -2
  254. package/dist/exchanges/venues/risex-release.d.ts +14 -0
  255. package/dist/exchanges/venues/risex-release.js +14 -0
  256. package/dist/exchanges/venues/risex.d.ts +31 -0
  257. package/dist/exchanges/venues/risex.js +116 -0
  258. package/dist/exchanges/venues/txflow.d.ts +18 -0
  259. package/dist/exchanges/venues/txflow.js +77 -0
  260. package/dist/exchanges/venues/variational.d.ts +21 -0
  261. package/dist/exchanges/venues/variational.js +85 -0
  262. package/dist/execution-log.d.ts +1 -1
  263. package/dist/funding/legacy-public.d.ts +1 -1
  264. package/dist/funding/legacy-public.js +9 -4
  265. package/dist/funding/normalize.d.ts +1 -1
  266. package/dist/funding/normalize.js +1 -1
  267. package/dist/funding/rates.d.ts +44 -4
  268. package/dist/funding/rates.js +56 -12
  269. package/dist/index.js +7 -6
  270. package/dist/jobs.d.ts +1 -0
  271. package/dist/jobs.js +25 -1
  272. package/dist/landing.d.ts +2 -0
  273. package/dist/landing.js +1 -1
  274. package/dist/mcp-pacifica-earn.d.ts +9 -0
  275. package/dist/mcp-pacifica-earn.js +58 -0
  276. package/dist/mcp-server.js +113 -59
  277. package/dist/pacifica/client.d.ts +67 -4
  278. package/dist/pacifica/client.js +170 -8
  279. package/dist/pacifica/rate-limit.d.ts +144 -0
  280. package/dist/pacifica/rate-limit.js +267 -0
  281. package/dist/pacifica/types/order.d.ts +2 -1
  282. package/dist/plan-executor.js +7 -2
  283. package/dist/rebalance.js +1 -1
  284. package/dist/risk.d.ts +2 -1
  285. package/dist/risk.js +2 -4
  286. package/dist/settings.d.ts +32 -3
  287. package/dist/signer/ows-evm.d.ts +1 -1
  288. package/dist/signer/ows-evm.js +1 -1
  289. package/dist/smart-order.d.ts +1 -1
  290. package/dist/smart-order.js +1 -1
  291. package/dist/strategies/funding-arb.d.ts +1 -1
  292. package/dist/strategies/funding-arb.js +1 -1
  293. package/dist/trade-validator.d.ts +1 -1
  294. package/dist/trade-validator.js +9 -4
  295. package/dist/utils.d.ts +12 -3
  296. package/dist/utils.js +11 -3
  297. package/dist/venue-health.d.ts +40 -0
  298. package/dist/venue-health.js +75 -0
  299. package/dist/withdraw-gate.d.ts +1 -1
  300. package/dist/withdraw-gate.js +1 -1
  301. package/dist/ws/index.d.ts +959 -0
  302. package/dist/ws/index.js +4295 -0
  303. package/dist/ws/node.d.ts +975 -0
  304. package/dist/ws/node.js +4366 -0
  305. package/dist/ws/pacifica-instrument.d.ts +8 -0
  306. package/dist/ws/pacifica-instrument.js +8 -0
  307. package/dist/ws-cli/credentials.d.ts +62 -0
  308. package/dist/ws-cli/credentials.js +126 -0
  309. package/package.json +58 -10
  310. package/skills/perp-cli/SKILL.md +62 -9
  311. package/skills/perp-cli/references/agent-operations.md +7 -0
  312. package/skills/perp-cli/references/commands.md +76 -5
  313. package/skills/perp-cli/references/ws-library.md +492 -0
@@ -0,0 +1,468 @@
1
+ /**
2
+ * `perp print …` (Pacifica Print) and `perp vault …` (Pacifica vaults / "lakes").
3
+ *
4
+ * Pacifica-only products: the commands always talk to Pacifica (an explicit `-e <other venue>` is
5
+ * refused). Reads are unauthenticated GETs and never sign; `--account <address>` reads any
6
+ * account without loading a key or building the adapter.
7
+ *
8
+ * Signer policy (perp-cli rule, stricter than the venue, which accepts `agent_wallet` on all four):
9
+ * - `vault deposit` (deposit_to_lake) is MASTER-ONLY, like `funds withdraw pacifica`: a leaked agent
10
+ * key must not be able to move the whole balance into a vault an attacker manages;
11
+ * - `vault withdraw` (pays only into the own account), `print deposit` and `print cancel` are
12
+ * signed by the active signer (agent when approved, else master).
13
+ * Every write supports `--dry-run` (exact canonical message + body, never signs, never sends). An
14
+ * unknown send outcome is ORDER_PENDING: never resubmitted except with the same --idempotency-key.
15
+ */
16
+ import { randomUUID } from "node:crypto";
17
+ import chalk from "chalk";
18
+ import { isDryRun, networkOf, supportsDryRun } from "../cli-flags.js";
19
+ import { PerpError } from "../errors.js";
20
+ import { PacificaAdapter } from "../exchanges/pacifica.js";
21
+ import { resolveVenueId } from "../exchanges/venue.js";
22
+ import { PacificaClient } from "../pacifica/client.js";
23
+ import { logExecution } from "../execution-log.js";
24
+ import { jsonOk, makeTable, printJson } from "../utils.js";
25
+ import { mulDecimal } from "../exchanges/precision.js";
26
+ import { cmpDecimal, isPlainDecimal, trimDecimal } from "../exchanges/pacifica-decimal.js";
27
+ import { fundsFailStatus } from "./funds.js";
28
+ import { requireIdempotencyKey, restampPresigned, sendPresigned, signPresigned } from "../exchanges/pacifica-presigned.js";
29
+ import { buildPrintDepositRequest, buildPrintEndRequest, daysUntil, fetchPrintEvents, fetchPrintGames, fetchPrintHistory, fetchPrintMarket, fetchPrintPositions, findPrintGame, quotePrintOrder, requireEndable, } from "../exchanges/pacifica-print.js";
30
+ import { buildVaultDepositRequest, buildVaultWithdrawRequest, DEPOSIT_WHITELIST_API_STATUS, fetchDepositWhitelist, fetchVaults, findVault, heldShares, planVaultDeposit, planVaultWithdraw, withdrawWindowAt, } from "../exchanges/pacifica-vaults.js";
31
+ const ts = (ms) => (ms === null ? "—" : new Date(ms).toISOString().replace("T", " ").slice(0, 16) + " UTC");
32
+ export function registerPacificaEarnCommands(program, getAdapterForExchange, isJson) {
33
+ /** Pacifica-only: an explicit -e naming another venue is refused (never silently ignored). */
34
+ function assertPacifica() {
35
+ if (program.getOptionValueSource("exchange") !== "cli")
36
+ return;
37
+ const raw = program.opts().exchange;
38
+ if (raw !== undefined && resolveVenueId(raw) !== "pacifica") {
39
+ throw new PerpError("INVALID_PARAMS", `Print and vaults are Pacifica products; -e ${raw} has neither`, { remediation: "Run with -e pacifica (or omit -e)" });
40
+ }
41
+ }
42
+ const publicClient = () => new PacificaClient({ network: networkOf(program) });
43
+ async function pacificaAdapter() {
44
+ const a = await getAdapterForExchange("pacifica");
45
+ if (!(a instanceof PacificaAdapter))
46
+ throw new Error("pacifica venue did not produce a PacificaAdapter");
47
+ return a;
48
+ }
49
+ /** Account to read: --account, else the configured wallet's (master) address. Signs nothing. */
50
+ async function readAccount(opt) {
51
+ if (opt !== undefined)
52
+ return requireAddress(opt);
53
+ const a = await pacificaAdapter();
54
+ if (a.isReadOnly) {
55
+ throw new PerpError("NO_SIGNER_AVAILABLE", "No Pacifica wallet configured to read", { remediation: "Pass --account <solana address>, or configure a Pacifica wallet (perp setup)" });
56
+ }
57
+ return a.publicKey;
58
+ }
59
+ /** Identity for a write. --account only plans (dry-run, no key loaded); a live write signs as the configured wallet. */
60
+ async function signing(accountOpt, dryRun) {
61
+ if (accountOpt !== undefined) {
62
+ if (!dryRun) {
63
+ throw new PerpError("INVALID_PARAMS", "--account only plans with --dry-run; a live write signs as the configured Pacifica wallet", { remediation: "Drop --account (or add --dry-run)" });
64
+ }
65
+ return { identity: { account: requireAddress(accountOpt), agentWallet: null }, tier: null, sign: null, client: publicClient() };
66
+ }
67
+ const a = await pacificaAdapter();
68
+ const id = a.signingIdentity();
69
+ return { identity: { account: id.account, agentWallet: id.agentWallet }, tier: id.tier, sign: id.signMessage, client: a.sdk };
70
+ }
71
+ /**
72
+ * Master-only identity (vault deposit): the master Solana signer, never the agent — even when an
73
+ * agent is approved. No master on this machine → NO_SIGNER_AVAILABLE. Mirrors PacificaAdapter.withdraw.
74
+ */
75
+ async function masterSigning(accountOpt, dryRun) {
76
+ if (accountOpt !== undefined) {
77
+ const plan = await signing(accountOpt, dryRun);
78
+ return { ...plan, tier: null };
79
+ }
80
+ const a = await pacificaAdapter();
81
+ const refuse = () => new PerpError("NO_SIGNER_AVAILABLE", "A Pacifica vault deposit must be signed by the account's master wallet (an agent key cannot fund a vault), and it is not loaded", {
82
+ exchange: "pacifica",
83
+ remediation: "Run with the master wallet: perp -e pacifica --ows <wallet> vault deposit … (or deposit on the Pacifica website)",
84
+ });
85
+ let master;
86
+ try {
87
+ master = a.masterSolanaSigner();
88
+ }
89
+ catch {
90
+ throw refuse();
91
+ }
92
+ if (master.getPublicKeyBase58() !== a.publicKey)
93
+ throw refuse();
94
+ return { identity: { account: a.publicKey, agentWallet: null }, tier: "master", sign: (m) => master.signMessage(m), client: a.sdk };
95
+ }
96
+ /** Dry-run: the exact request a live run would sign and send (no signature). */
97
+ function printDryRun(plan, req, tier, extra = {}) {
98
+ const request = { path: `POST /api/v1${req.path}`, type: req.type, signer: tier ?? "not loaded (--account plan)", signedMessage: req.message, body: { ...req.body, signature: null } };
99
+ if (isJson())
100
+ return printJson(jsonOk({ dryRun: true, sent: false, ...extra, plan, request }));
101
+ if (extra.apiStatus === "unofficial")
102
+ console.log(chalk.yellow(` ! ${req.path} is UNOFFICIAL (not in the Pacifica API docs; shape captured from app.pacifica.fi)`));
103
+ console.log(chalk.yellow(`\n [dry-run] ${req.type} — nothing signed, nothing sent`));
104
+ console.log(` POST /api/v1${req.path} signer: ${request.signer}`);
105
+ console.log(chalk.gray(" message to sign:"));
106
+ console.log(` ${req.message}`);
107
+ console.log(chalk.gray(" body (signature added at send):"));
108
+ console.log(` ${JSON.stringify(request.body)}\n`);
109
+ }
110
+ /** Sign + send; log every outcome incl. a signing failure (unknown outcome → "unverified", ORDER_PENDING). */
111
+ async function send(s, req, o) {
112
+ if (s.sign === null)
113
+ throw new Error("send without a signer");
114
+ const base = { type: o.logType, exchange: "pacifica", symbol: o.symbol, side: req.type, size: o.size, dryRun: false, meta: { ...o.meta, signer: s.tier, idempotencyKey: req.idempotencyKey, before: o.before } };
115
+ try {
116
+ // Credit wait BEFORE signing (a post-sign wait would eat the 5 s expiry_window). The body's
117
+ // timestamp is re-stamped after the wait so the signature window starts now.
118
+ await s.client.awaitPresignedCapacity(req.type);
119
+ req = restampPresigned(req, Date.now());
120
+ const body = await signPresigned(req, s.sign);
121
+ const data = await sendPresigned(s.client, req, body, { what: o.what, check: o.check, before: o.before });
122
+ logExecution({ ...base, status: "success", meta: { ...base.meta, response: data } });
123
+ return data;
124
+ }
125
+ catch (err) {
126
+ logExecution({ ...base, status: fundsFailStatus(err), error: err instanceof Error ? err.message : String(err) });
127
+ throw err;
128
+ }
129
+ }
130
+ const keyOf = (raw) => (raw === undefined ? randomUUID() : requireIdempotencyKey(raw));
131
+ // ═══════════════════════════ PRINT ═══════════════════════════
132
+ const print = program.command("print").description("Pacifica Print — strike orders that earn a premium every cycle while they wait (reads, quote, deposit/cancel with --dry-run)");
133
+ print.command("games").alias("products")
134
+ .description("Print products: market, cycle, max leverage, deposit caps, allowed target range")
135
+ .action(async () => {
136
+ assertPacifica();
137
+ const games = await fetchPrintGames(publicClient());
138
+ if (isJson())
139
+ return printJson(jsonOk(games));
140
+ console.log(chalk.white.bold("\n Pacifica Print products"));
141
+ console.log(makeTable(["game", "asset", "cycle", "max lev", "target from mark", "product cap (total)", "pool cap (total)"], games.map((g) => [g.game, g.asset, g.cycleDays === null ? "?" : `${g.cycleDays}d`, g.maxLeverage === null ? "?" : `${g.maxLeverage}x`,
142
+ g.targetDistancePct.max === null ? "?" : `${g.targetDistancePct.min}%–${g.targetDistancePct.max}%`, g.productDepositCap ?? "—", g.poolDepositCap ?? "—"])));
143
+ console.log(chalk.gray(` Min deposit ${games[0]?.minDeposit ?? "10"} USDC · while resting, fill/liquidation only at each cycle's checkpoint · after a fill: ordinary perp position · cancel settles at the cycle's end\n`));
144
+ });
145
+ const orderArgs = (c) => c
146
+ .argument("<game>", "Print product, e.g. BTC_24H, BTC_7D, BTC_1M (perp print games)")
147
+ .argument("<direction>", "long (target below mark) | short (target above mark)")
148
+ .argument("<deposit>", "USDC deposit (locked margin), min 10")
149
+ .requiredOption("--strike <price>", "Target price (on the market tick)")
150
+ .requiredOption("--leverage <n>", "Whole-number leverage, 1 … the product max")
151
+ .option("--cycles <n>", "Cycles to run, 1 … 1000 (default: roll until cancelled)");
152
+ async function quoteFrom(gameArg, dirArg, deposit, o) {
153
+ const direction = parseDirection(dirArg);
154
+ const cycles = o.cycles === undefined ? null : parseWhole(o.cycles, "--cycles");
155
+ const c = publicClient();
156
+ const game = findPrintGame(await fetchPrintGames(c), gameArg);
157
+ const market = await fetchPrintMarket(c, game.asset);
158
+ return quotePrintOrder(game, { game: game.game, direction, deposit, strikePrice: o.strike, leverage: o.leverage, cycles }, market);
159
+ }
160
+ orderArgs(print.command("quote"))
161
+ .description("Validate a Print order and preview size, liquidation price, max loss and lock period (read-only; the premium itself is quoted only by the venue)")
162
+ .action(async (gameArg, dirArg, deposit, o) => {
163
+ assertPacifica();
164
+ const q = await quoteFrom(gameArg, dirArg, deposit, o);
165
+ if (isJson())
166
+ return printJson(jsonOk(q));
167
+ printQuote(q);
168
+ });
169
+ print.command("positions")
170
+ .description("Current Print orders of the account (printing / cancelling / just settled)")
171
+ .option("--account <address>", "Read this account instead of the configured wallet")
172
+ .action(async (o) => {
173
+ assertPacifica();
174
+ const account = await readAccount(o.account);
175
+ const rows = await fetchPrintPositions(publicClient(), account);
176
+ if (isJson())
177
+ return printJson(jsonOk({ account, positions: rows }));
178
+ printPositions(account, rows);
179
+ });
180
+ print.command("history")
181
+ .description("Every Print order of the account, newest first (paginated)")
182
+ .option("--account <address>", "Read this account instead of the configured wallet")
183
+ .option("--limit <n>", "Rows per page")
184
+ .option("--cursor <cursor>", "next_cursor of the previous page")
185
+ .action(async (o) => {
186
+ assertPacifica();
187
+ const account = await readAccount(o.account);
188
+ const page = await fetchPrintHistory(publicClient(), account, { limit: o.limit === undefined ? null : parseWhole(o.limit, "--limit"), cursor: o.cursor ?? null });
189
+ if (isJson())
190
+ return printJson(jsonOk({ account, positions: page.items, hasMore: page.hasMore, nextCursor: page.nextCursor }));
191
+ printPositions(account, page.items);
192
+ if (page.hasMore === true)
193
+ console.log(chalk.gray(` More: --cursor ${page.nextCursor ?? "?"}\n`));
194
+ });
195
+ print.command("events <printAddress>")
196
+ .description("Event log of one Print order: created, premium, end_requested, filled, liquidated, …")
197
+ .option("--limit <n>", "Rows per page")
198
+ .option("--cursor <cursor>", "next_cursor of the previous page")
199
+ .action(async (addr, o) => {
200
+ assertPacifica();
201
+ const page = await fetchPrintEvents(publicClient(), requireAddress(addr), { limit: o.limit === undefined ? null : parseWhole(o.limit, "--limit"), cursor: o.cursor ?? null });
202
+ if (isJson())
203
+ return printJson(jsonOk({ printAddress: addr, events: page.items, hasMore: page.hasMore, nextCursor: page.nextCursor }));
204
+ console.log("\n" + makeTable(["time", "event", "amount", "strike", "mark", "cycle"], page.items.map((e) => [ts(e.time), e.event, e.amount ?? "—", e.strikePrice ?? "—", e.markPrice ?? "—", e.cycleStartedAt === null ? "—" : `${ts(e.cycleStartedAt)} → ${ts(e.cycleEndedAt)}`])) + "\n");
205
+ });
206
+ supportsDryRun(orderArgs(print.command("deposit").alias("open")))
207
+ .description("Place a Print order (POST /game/deposit, deposit_to_game) — locks the deposit until it fills or a cancel settles at a cycle's end; supports --dry-run")
208
+ .option("--account <address>", "With --dry-run: plan for this account without loading a key")
209
+ .option("--idempotency-key <uuid>", "Re-send an earlier attempt with ITS key (only safe retry after ORDER_PENDING; default: a fresh key)")
210
+ .action(async (gameArg, dirArg, deposit, o, cmd) => {
211
+ assertPacifica();
212
+ const dryRun = isDryRun(cmd);
213
+ const idempotencyKey = keyOf(o.idempotencyKey);
214
+ const q = await quoteFrom(gameArg, dirArg, deposit, o);
215
+ const s = await signing(o.account, dryRun);
216
+ const ordersBefore = (await fetchPrintPositions(publicClient(), s.identity.account)).length;
217
+ const available = availableOf(await publicClient().getAccount(s.identity.account), s.identity.account);
218
+ if (cmpDecimal(available, q.deposit) < 0) {
219
+ throw new PerpError("INSUFFICIENT_BALANCE", `Print deposit ${q.deposit} USDC exceeds available_to_withdraw ${available}`, { exchange: "pacifica", remediation: "Deposit less, or free margin first (perp -e pacifica portfolio)" });
220
+ }
221
+ const req = buildPrintDepositRequest(q, s.identity, { timestamp: Date.now(), idempotencyKey });
222
+ if (!isJson())
223
+ printQuote(q);
224
+ if (dryRun)
225
+ return printDryRun(q, req, s.tier);
226
+ const data = await send(s, req, {
227
+ what: `Print deposit ${q.deposit} USDC ${q.game} ${q.direction} @ ${q.strikePrice}`,
228
+ check: `perp -e pacifica print positions — look for a new ${q.game} ${q.direction} order with strikePrice ${q.strikePrice} and deposit ${q.deposit} (${ordersBefore} current order(s) before this request)`,
229
+ logType: "print_deposit", symbol: q.game, size: q.deposit, before: { printOrders: ordersBefore },
230
+ meta: { game: q.game, direction: q.direction, strike: q.strikePrice, leverage: q.leverage, cycles: q.cycles },
231
+ });
232
+ const printAddress = typeof data.game_account_address === "string" ? data.game_account_address : null;
233
+ if (isJson())
234
+ return printJson(jsonOk({ placed: true, printAddress, quote: q, idempotencyKey: req.idempotencyKey, response: data }));
235
+ console.log(chalk.green(`\n Print order placed: ${printAddress ?? "(no address in response)"}`));
236
+ console.log(chalk.gray(" Track it: perp -e pacifica print positions\n"));
237
+ });
238
+ supportsDryRun(print.command("cancel <printAddress>").alias("end"))
239
+ .description("[UNOFFICIAL endpoint] End a running Print order (POST /game/end, end_game — not in the API docs; shape captured from app.pacifica.fi). It still runs to the end of the current cycle, then fills or returns the deposit + premiums. Live run needs --unverified-endpoint; supports --dry-run")
240
+ .option("--account <address>", "With --dry-run: plan for this account without loading a key")
241
+ .option("--unverified-endpoint", "Acknowledge that /game/end is undocumented (required for a live run)")
242
+ .action(async (addr, o, cmd) => {
243
+ assertPacifica();
244
+ const dryRun = isDryRun(cmd);
245
+ if (!dryRun && o.unverifiedEndpoint !== true) {
246
+ throw new PerpError("INVALID_PARAMS", "print cancel uses POST /game/end (end_game), which is not in the Pacifica API docs (apiStatus: unofficial)", {
247
+ exchange: "pacifica", apiStatus: "unofficial",
248
+ remediation: "Check it first with --dry-run, then re-run with --unverified-endpoint — or end the order on app.pacifica.fi",
249
+ });
250
+ }
251
+ const s = await signing(o.account, dryRun);
252
+ const p = requireEndable(await fetchPrintPositions(publicClient(), s.identity.account), requireAddress(addr));
253
+ const req = buildPrintEndRequest(p.address, s.identity, { timestamp: Date.now() });
254
+ const plan = { printAddress: p.address, game: p.game, settlesAt: p.currentCycleEndAt, settlesInDays: daysUntil(p.currentCycleEndAt, Date.now()), lockedNow: p.maxLoss };
255
+ if (!isJson())
256
+ console.log(chalk.cyan(`\n End ${p.address} (${p.game}): it keeps running until ${ts(p.currentCycleEndAt)}, then fills or returns ${p.maxLoss ?? p.deposit} USDC`));
257
+ if (dryRun)
258
+ return printDryRun(plan, req, s.tier, { apiStatus: "unofficial" });
259
+ const data = await send(s, req, {
260
+ what: `Print end ${p.address}`,
261
+ check: `perp -e pacifica print positions — order ${p.address} shows status "cancelling" if the end was accepted (it was "${p.status}" before)`,
262
+ logType: "print_end", symbol: p.game, size: p.deposit, before: { status: p.status }, meta: { printAddress: p.address, game: p.game, apiStatus: "unofficial" },
263
+ });
264
+ const at = typeof data.scheduled_end_at_ms === "number" ? data.scheduled_end_at_ms : null;
265
+ if (isJson())
266
+ return printJson(jsonOk({ endRequested: true, apiStatus: "unofficial", printAddress: p.address, scheduledEndAt: at, response: data }));
267
+ console.log(chalk.green(`\n End requested for ${p.address} — settles at ${ts(at)} (the order runs until then).\n`));
268
+ });
269
+ // ═══════════════════════════ VAULT ═══════════════════════════
270
+ const vault = program.command("vault").alias("lake").description("Pacifica vaults (API: lakes) — list, info, positions, deposit/withdraw with --dry-run");
271
+ vault.command("list")
272
+ .description("Vaults by TVL with NAV per share, fees, lock-up and withdrawal window")
273
+ .option("--account <address>", "Include this account's LP shares")
274
+ .option("--limit <n>", "Rows shown (human output; --json returns all)", "25")
275
+ .action(async (o) => {
276
+ assertPacifica();
277
+ const vaults = sortByTvl(await fetchVaults(publicClient(), o.account === undefined ? null : requireAddress(o.account)));
278
+ if (isJson())
279
+ return printJson(jsonOk(vaults));
280
+ const n = parseWhole(o.limit, "--limit");
281
+ console.log("\n" + makeTable(["vault", "address", "TVL", "NAV/LP share", "fee", "lock-up", "withdraw window"], vaults.slice(0, n).map((v) => [v.nickname ?? "—", v.address, v.tvl, v.navPerLpShare ?? "1 (empty)", v.config.managerProfitShare ?? "—",
282
+ v.config.depositMinDurationMs === null ? "—" : `${(v.config.depositMinDurationMs / 3_600_000).toFixed(1)}h`, windowLabel(v)])));
283
+ console.log(chalk.gray(` ${Math.min(n, vaults.length)} of ${vaults.length} vaults · APY is not published by Pacifica\n`));
284
+ });
285
+ vault.command("info <vault>")
286
+ .description("One vault (address or nickname): balances, NAV, config, live account equity, positions, withdrawal window now")
287
+ .action(async (ref) => {
288
+ assertPacifica();
289
+ const c = publicClient();
290
+ const v = findVault(await fetchVaults(c, null), ref);
291
+ const [acct, positions, whitelist] = await Promise.all([c.getAccount(v.address), c.getPositions(v.address), whitelistOrNull(c, v.address)]);
292
+ const out = {
293
+ ...v,
294
+ live: { accountEquity: acct.account_equity ?? null, availableToWithdraw: acct.available_to_withdraw ?? null, positions: positions.map((p) => ({ symbol: p.symbol, side: p.side, amount: p.amount, entryPrice: p.entry_price })) },
295
+ depositWhitelist: { apiStatus: DEPOSIT_WHITELIST_API_STATUS, addresses: whitelist },
296
+ withdrawWindow: withdrawWindowAt(v.config, Date.now()),
297
+ };
298
+ if (isJson())
299
+ return printJson(jsonOk(out));
300
+ console.log(chalk.white.bold(`\n ${v.nickname ?? v.address}`));
301
+ for (const [k, val] of Object.entries({
302
+ address: v.address, manager: v.manager ?? "none (fresh vault)", creator: v.creator ?? "—", TVL: v.tvl, "NAV / LP share": v.navPerLpShare ?? "1 (LP class empty)",
303
+ "NAV / manager share": v.navPerManagerShare ?? "—", "high-water mark": v.highWatermark ?? "—", "live equity": out.live.accountEquity ?? "—",
304
+ "vault free balance": out.live.availableToWithdraw ?? "—", "performance fee": v.config.managerProfitShare ?? "none", "deposit cap": v.config.depositCap ?? "none",
305
+ "lock-up after deposit": v.config.depositMinDurationMs === null ? "none" : `${(v.config.depositMinDurationMs / 3_600_000).toFixed(2)} h (reset by every deposit)`,
306
+ "withdrawal window": `${windowLabel(v)}${out.withdrawWindow.restricted ? ` — now ${out.withdrawWindow.open ? "OPEN until" : "closed, opens"} ${ts(out.withdrawWindow.nextChangeAt)}` : ""}`,
307
+ "deposit whitelist": `${whitelist === null ? "unknown" : whitelist.length === 0 ? "none (anyone)" : `${whitelist.length} address(es)`} (unofficial endpoint; the venue decides)`, APY: "not published",
308
+ }))
309
+ console.log(` ${k.padEnd(22)} ${val}`);
310
+ if (out.live.positions.length > 0)
311
+ console.log("\n" + makeTable(["symbol", "side", "amount", "entry"], out.live.positions.map((p) => [p.symbol, p.side, p.amount, p.entryPrice])));
312
+ console.log("");
313
+ });
314
+ vault.command("positions")
315
+ .description("Vaults where the account holds shares (LP shares, or the manager's own)")
316
+ .option("--account <address>", "Read this account instead of the configured wallet")
317
+ .action(async (o) => {
318
+ assertPacifica();
319
+ const account = await readAccount(o.account);
320
+ const rows = (await fetchVaults(publicClient(), account)).map((v) => ({ v, h: heldShares(v, account) }))
321
+ .filter(({ h }) => h.shares !== null && Number(h.shares) > 0)
322
+ .map(({ v, h }) => ({ schema: 1, vault: v.address, nickname: v.nickname, role: h.role, shares: h.shares, navPerShare: h.nav, value: h.nav === null ? null : mul(h.shares, h.nav), withdrawWindow: withdrawWindowAt(v.config, Date.now()), lockMs: v.config.depositMinDurationMs }));
323
+ if (isJson())
324
+ return printJson(jsonOk({ account, positions: rows }));
325
+ if (rows.length === 0) {
326
+ console.log(chalk.gray(`\n ${account} holds no vault shares.\n`));
327
+ return;
328
+ }
329
+ console.log("\n" + makeTable(["vault", "role", "shares", "NAV/share", "value ≈ USDC", "withdraw now"], rows.map((r) => [r.nickname ?? r.vault, r.role, r.shares, r.navPerShare ?? "—", r.value ?? "—", r.withdrawWindow.open ? "window open" : `closed → ${ts(r.withdrawWindow.nextChangeAt)}`])) + "\n");
330
+ });
331
+ supportsDryRun(vault.command("deposit <vault> <amount>"))
332
+ .description("Deposit USDC from the trading balance into a vault (POST /lake/deposit, deposit_to_lake) for shares at NAV; supports --dry-run")
333
+ .option("--account <address>", "With --dry-run: plan for this account without loading a key")
334
+ .option("--idempotency-key <uuid>", "Re-send an earlier attempt with ITS key (only safe retry after ORDER_PENDING; default: a fresh key)")
335
+ .action(async (ref, amount, o, cmd) => {
336
+ assertPacifica();
337
+ const dryRun = isDryRun(cmd);
338
+ const idempotencyKey = keyOf(o.idempotencyKey);
339
+ const c = publicClient();
340
+ const s = await masterSigning(o.account, dryRun);
341
+ const v = findVault(await fetchVaults(c, s.identity.account), ref);
342
+ const [info, whitelist] = await Promise.all([c.getAccount(s.identity.account), whitelistOrNull(c, v.address)]);
343
+ const plan = planVaultDeposit(v, s.identity.account, amount, { availableToWithdraw: availableOf(info, s.identity.account), whitelist });
344
+ const sharesBefore = heldShares(v, s.identity.account).shares ?? "0";
345
+ const req = buildVaultDepositRequest(v.address, plan.amount, s.identity, { timestamp: Date.now(), idempotencyKey });
346
+ if (!isJson())
347
+ printWarnings(`Depositing ${plan.amount} USDC into vault ${v.address} "${v.nickname ?? ""}" (manager ${v.manager ?? "none"}) — ≈ ${plan.estimatedShares ?? "?"} shares; signer: master`, plan.warnings);
348
+ if (dryRun)
349
+ return printDryRun(plan, req, s.tier === null ? null : "master");
350
+ const data = await send(s, req, {
351
+ what: `vault deposit ${plan.amount} USDC into ${v.address}`,
352
+ check: `perp -e pacifica vault positions — shares in vault ${v.address} (${sharesBefore} before this request)`,
353
+ logType: "vault_deposit", symbol: "USDC", size: plan.amount, before: { shares: sharesBefore }, meta: { vault: v.address, manager: v.manager },
354
+ });
355
+ const minted = typeof data.shares_minted === "string" ? data.shares_minted : null;
356
+ if (isJson())
357
+ return printJson(jsonOk({ deposited: true, vault: v.address, amount: plan.amount, sharesMinted: minted, idempotencyKey: req.idempotencyKey, response: data }));
358
+ console.log(chalk.green(`\n Deposited ${plan.amount} USDC — shares minted: ${minted ?? "(not in response)"}\n`));
359
+ });
360
+ supportsDryRun(vault.command("withdraw <vault> [shares]"))
361
+ .description("Redeem vault shares back into YOUR OWN trading balance (POST /lake/withdraw, withdraw_from_lake; no destination exists); supports --dry-run")
362
+ .option("--all", "Redeem every share the account holds")
363
+ .option("--account <address>", "With --dry-run: plan for this account without loading a key")
364
+ .option("--idempotency-key <uuid>", "Re-send an earlier attempt with ITS key (only safe retry after ORDER_PENDING; default: a fresh key)")
365
+ .action(async (ref, shares, o, cmd) => {
366
+ assertPacifica();
367
+ if ((shares === undefined) === (o.all !== true)) {
368
+ throw new PerpError("INVALID_PARAMS", "vault withdraw: give exactly one of <shares> or --all", { remediation: "e.g. perp -e pacifica vault withdraw <vault> 50 | --all" });
369
+ }
370
+ const dryRun = isDryRun(cmd);
371
+ const idempotencyKey = keyOf(o.idempotencyKey);
372
+ const c = publicClient();
373
+ const s = await signing(o.account, dryRun);
374
+ const v = findVault(await fetchVaults(c, s.identity.account), ref);
375
+ const lake = await c.getAccount(v.address);
376
+ const plan = planVaultWithdraw(v, s.identity.account, o.all === true ? "all" : shares, { nowMs: Date.now(), vaultAvailableToWithdraw: isPlainDecimal(lake.available_to_withdraw) ? lake.available_to_withdraw : null });
377
+ const req = buildVaultWithdrawRequest(v.address, plan.shares, s.identity, { timestamp: Date.now(), idempotencyKey });
378
+ if (req.body.account !== s.identity.account)
379
+ throw new Error("vault withdraw body account mismatch");
380
+ if (!isJson())
381
+ printWarnings(`Redeeming ${plan.shares} shares (≈ ${plan.estimatedAmount ?? "?"} USDC) from vault ${v.address} "${v.nickname ?? ""}" (manager ${v.manager ?? "none"}) to ${plan.destination}`, plan.warnings);
382
+ if (dryRun)
383
+ return printDryRun(plan, req, s.tier);
384
+ const data = await send(s, req, {
385
+ what: `vault withdraw ${plan.shares} shares from ${v.address}`,
386
+ check: `perp -e pacifica vault positions — shares in vault ${v.address} (${plan.heldShares} before this request)`,
387
+ logType: "vault_withdraw", symbol: "USDC", size: plan.shares, before: { shares: plan.heldShares }, meta: { vault: v.address, estimatedAmount: plan.estimatedAmount },
388
+ });
389
+ const amt = typeof data.amount_withdrawn === "string" ? data.amount_withdrawn : null;
390
+ if (isJson())
391
+ return printJson(jsonOk({ withdrawn: true, vault: v.address, shares: plan.shares, amountWithdrawn: amt, destination: plan.destination, idempotencyKey: req.idempotencyKey, response: data }));
392
+ console.log(chalk.green(`\n Redeemed ${plan.shares} shares → ${amt ?? "(amount not in response)"} USDC credited to ${plan.destination}\n`));
393
+ });
394
+ }
395
+ // ── helpers ──────────────────────────────────────────────────────────────
396
+ /** Unofficial whitelist read: null when unreadable (advisory only — the venue's refusal is final). */
397
+ async function whitelistOrNull(c, vault) {
398
+ try {
399
+ return await fetchDepositWhitelist(c, vault);
400
+ }
401
+ catch {
402
+ return null;
403
+ }
404
+ }
405
+ function requireAddress(s) {
406
+ const a = s.trim();
407
+ if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(a)) {
408
+ throw new PerpError("INVALID_PARAMS", `Not a Solana (base58) address: "${s}"`, { remediation: "Pass a base58 Solana address" });
409
+ }
410
+ return a;
411
+ }
412
+ function parseDirection(s) {
413
+ const d = s.trim().toLowerCase();
414
+ if (d === "long" || d === "short")
415
+ return d;
416
+ throw new PerpError("INVALID_PARAMS", `direction must be long or short (got "${s}")`, { remediation: "long = target below mark, short = target above mark" });
417
+ }
418
+ function parseWhole(s, what) {
419
+ if (!/^[1-9]\d*$/.test(s.trim()))
420
+ throw new PerpError("INVALID_PARAMS", `${what} must be a positive whole number (got "${s}")`);
421
+ return Number(s.trim());
422
+ }
423
+ function mul(a, b) {
424
+ return trimDecimal(mulDecimal(a, b));
425
+ }
426
+ /** available_to_withdraw of GET /account; unreadable → EXCHANGE_ERROR (never treated as 0). */
427
+ function availableOf(info, account) {
428
+ const v = info.available_to_withdraw;
429
+ if (!isPlainDecimal(v))
430
+ throw new PerpError("EXCHANGE_ERROR", `Pacifica /account for ${account} has no readable available_to_withdraw`, { exchange: "pacifica" });
431
+ return v;
432
+ }
433
+ function sortByTvl(v) {
434
+ return [...v].sort((a, b) => Number(b.tvl) - Number(a.tvl));
435
+ }
436
+ function windowLabel(v) {
437
+ const { withdrawWindowS: w, withdrawDurationS: d } = v.config;
438
+ return w === null || d === null ? "always" : `${(d / 3600).toFixed(1)}h every ${(w / 3600).toFixed(1)}h`;
439
+ }
440
+ function printWarnings(title, warnings) {
441
+ console.log(chalk.cyan(`\n ${title}`));
442
+ for (const w of warnings)
443
+ console.log(chalk.yellow(` ! ${w}`));
444
+ }
445
+ function printQuote(q) {
446
+ console.log(chalk.white.bold(`\n Print ${q.direction.toUpperCase()} ${q.game} — preview`));
447
+ for (const [k, v] of Object.entries({
448
+ "deposit (locked)": `${q.deposit} USDC`,
449
+ leverage: `${q.leverage}x → notional ${q.notional} USDC`,
450
+ "target / mark": `${q.strikePrice} / ${q.markPrice} (${q.targetDistancePct}% away; allowed ${q.allowedTargetDistancePct.min}%–${q.allowedTargetDistancePct.max}%)`,
451
+ "size on fill": `${q.size} ${q.asset} at exactly ${q.strikePrice}`,
452
+ "liquidation": `${q.docsLiquidationPrice ?? "unknown (docs formula gives ≤ 0)"} — docs-formula outer bound only; observed earlier in one live case, venue trigger unpublished`,
453
+ "after a fill": "ordinary position in your main account: continuous mark-price liquidation, nets with an existing position, may be cross-margined (perp -e pacifica account positions)",
454
+ "MAX LOSS": `${q.maxLoss} USDC deposit + every premium credited — never more`,
455
+ cycles: q.cycles === null ? `rolls every ${q.cycleDays}d until cancelled` : `${q.cycles} × ${q.cycleDays}d (up to ${q.maxDurationDays} days)`,
456
+ "cancel settles": `at the end of the running cycle (up to ${q.cancelSettlesWithinDays}d)`,
457
+ premium: "set by the venue each cycle (not quoted here)",
458
+ }))
459
+ console.log(` ${k.padEnd(18)} ${v}`);
460
+ console.log("");
461
+ }
462
+ function printPositions(account, rows) {
463
+ if (rows.length === 0) {
464
+ console.log(chalk.gray(`\n No Print orders for ${account}.\n`));
465
+ return;
466
+ }
467
+ console.log("\n" + makeTable(["address", "game", "dir", "deposit", "lev", "strike", "premium", "liq (docs, outer)", "status", "cycle ends"], rows.map((p) => [p.address, p.game, p.direction, p.deposit, `${p.leverage}x`, p.strikePrice, p.premiumPaid ?? "—", p.docsLiquidationPrice ?? "—", p.status, ts(p.currentCycleEndAt ?? p.scheduledEndAt)])) + "\n");
468
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Pacifica-specific order types exposed through `perp -e pacifica trade …`.
3
+ *
4
+ * Of Pacifica's strategy order types (docs trading-on-pacifica/order-types/strategies) only Atomic
5
+ * has an API path: it is a batch of ordinary market orders (POST /orders/batch). Chase, OCO,
6
+ * Trailing Stop, Swarm, Trigger Swarm and Conditional Market / Limit have no documented API / SDK
7
+ * path (as of 2026-10-19): no REST endpoint, no signing operation type (api/signing/operation-types),
8
+ * no batch action type and no order-type code; the official python-sdk and Pacifica MCP server
9
+ * expose none. Nearest perp-cli equivalents: `trade twap` (≈ Swarm: a split market execution) and
10
+ * `trade tpsl` (≈ OCO bracket exit: position take-profit + stop-loss).
11
+ */
12
+ import type { Command } from "commander";
13
+ import type { ExchangeAdapter } from "../exchanges/interface.js";
14
+ export declare function registerPacificaOrderCommands(trade: Command, getAdapter: () => Promise<ExchangeAdapter>, isJson: () => boolean, isDryRun: () => boolean): void;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Pacifica-specific order types exposed through `perp -e pacifica trade …`.
3
+ *
4
+ * Of Pacifica's strategy order types (docs trading-on-pacifica/order-types/strategies) only Atomic
5
+ * has an API path: it is a batch of ordinary market orders (POST /orders/batch). Chase, OCO,
6
+ * Trailing Stop, Swarm, Trigger Swarm and Conditional Market / Limit have no documented API / SDK
7
+ * path (as of 2026-10-19): no REST endpoint, no signing operation type (api/signing/operation-types),
8
+ * no batch action type and no order-type code; the official python-sdk and Pacifica MCP server
9
+ * expose none. Nearest perp-cli equivalents: `trade twap` (≈ Swarm: a split market execution) and
10
+ * `trade tpsl` (≈ OCO bracket exit: position take-profit + stop-loss).
11
+ */
12
+ import chalk from "chalk";
13
+ import { supportsDryRun } from "../cli-flags.js";
14
+ import { PerpError, extractErrorMessage, parsePositiveDecimal } from "../errors.js";
15
+ import { atomicBasketRiskCheck, atomicDryRunPlan, atomicIncompleteError, atomicLegClientOrderId, atomicLegPayload, atomicMarketsOf, executeAtomic, parseAtomicLegs, requireAtomicReduceOnly, sizeAtomicLegs, } from "../exchanges/pacifica-atomic.js";
16
+ import { resolveSlippage } from "../exchanges/slippage.js";
17
+ import { parseSlippageBpsFlag } from "./trade.js";
18
+ import { reduceOnlyGateOf, unsupported } from "../exchanges/trading-v2.js";
19
+ import { getVenue } from "../exchanges/venue.js";
20
+ import { quarantineEntryFor, quarantineSymbol } from "../bot/quarantine.js";
21
+ import { enforceOrderRisk } from "../trade-validator.js";
22
+ import { assessRisk, loadRiskLimits } from "../risk.js";
23
+ import { logExecution } from "../execution-log.js";
24
+ import { invalidateCache } from "../cache.js";
25
+ import { printJson, jsonOk } from "../utils.js";
26
+ export function registerPacificaOrderCommands(trade, getAdapter, isJson, isDryRun) {
27
+ supportsDryRun(trade.command("atomic"))
28
+ .description("Pacifica only: weighted basket of market orders across markets, sent in ONE batch request. Each leg fills or fails on its own (not all-or-nothing). "
29
+ + "An unconfirmed leg → ORDER_PENDING and its symbol is quarantined; a batch failure other than HTTP 400 / 429 (timeout, 5xx, unreadable answer) quarantines EVERY leg. "
30
+ + "Pacifica Chase / OCO / Trailing / Swarm have no documented API (as of 2026-10-19) — nearest here: trade twap (≈ Swarm), trade tpsl (≈ OCO)")
31
+ .requiredOption("--usd <total>", "Total basket notional in USD, split across the legs by weight at each mark price")
32
+ .option("--long <legs...>", "Buy legs: SYMBOL[:WEIGHT] (up to 5; no weight = 1)")
33
+ .option("--short <legs...>", "Sell legs: SYMBOL[:WEIGHT] (up to 5; no weight = 1)")
34
+ .option("--reduce-only", "Every leg reduce-only: --long legs must close shorts, --short legs must close longs")
35
+ .option("--slippage-bps <n>", "Per-leg slippage bound in bps (default: settings slippageBps.pacifica → defaultSlippageBps)")
36
+ .option("--force", "Bypass local risk-limit checks (maxPosition / total exposure) for every leg")
37
+ .action(async (opts) => {
38
+ const adapter = await getAdapter();
39
+ if (adapter.name !== "pacifica") {
40
+ const def = getVenue(adapter.name);
41
+ throw unsupported(def.displayName, "atomicOrder", "Atomic is a Pacifica basket order (POST /orders/batch of market orders)", "Run it on Pacifica (perp -e pacifica trade atomic …), or use perp trade multi for a cross-venue multi-leg order");
42
+ }
43
+ const pac = adapter;
44
+ const specs = parseAtomicLegs(opts.long ?? [], opts.short ?? []);
45
+ const usd = parsePositiveDecimal(opts.usd, "--usd");
46
+ const slippage = resolveSlippage("pacifica", parseSlippageBpsFlag(opts.slippageBps));
47
+ const reduceOnly = opts.reduceOnly === true;
48
+ const [info, prices] = await Promise.all([pac.sdk.getInfo(), pac.sdk.getPrices()]);
49
+ const legs = sizeAtomicLegs(specs, usd, atomicMarketsOf(specs, info, prices), slippage.bps);
50
+ // Pre-send gates (read only), before the dry-run return so a dry run reports the same refusal.
51
+ if (reduceOnly) {
52
+ const gate = reduceOnlyGateOf(adapter);
53
+ if (gate !== null)
54
+ throw gate;
55
+ invalidateCache("acct");
56
+ requireAtomicReduceOnly(legs, await adapter.getPositions());
57
+ }
58
+ else {
59
+ for (const l of legs) {
60
+ const q = quarantineEntryFor(l.symbol, (m) => { if (!isJson())
61
+ console.error(m); });
62
+ if (q !== null) {
63
+ throw new PerpError("RISK_VIOLATION", `${l.symbol} is quarantined by ${q.strategy}: ${q.reason}`, {
64
+ exchange: "pacifica",
65
+ remediation: `${q.remediation} — only reduce-only orders are accepted until a human clears it (perp strategy quarantine-clear ${l.symbol})`,
66
+ });
67
+ }
68
+ await enforceOrderRisk(adapter, { symbol: l.symbol, size: Number(l.size), price: Number(l.mark), force: opts.force });
69
+ }
70
+ // The basket as one order: total notional vs total exposure, new markets vs max positions.
71
+ if (opts.force !== true) {
72
+ const [balance, positions] = await Promise.all([adapter.getBalance(), adapter.getPositions()]);
73
+ const assessment = assessRisk([{ exchange: adapter.name, balance }], positions.map(position => ({ exchange: adapter.name, position })), loadRiskLimits());
74
+ atomicBasketRiskCheck(assessment, legs, positions);
75
+ }
76
+ }
77
+ const builder = await pac.builderDecision();
78
+ const builderCode = builder.applied === null ? null : builder.applied.identity;
79
+ const payloads = legs.map(l => atomicLegPayload(l, { reduceOnly, slippageBps: slippage.bps, clientOrderId: atomicLegClientOrderId(), builderCode }));
80
+ const legView = legs.map((l, i) => ({
81
+ symbol: l.symbol, side: l.side, weight: l.weight, share: Number(l.share.toFixed(6)), mark: l.mark, lotSize: l.lotSize,
82
+ size: l.size, notionalUsd: Number(l.notionalUsd.toFixed(2)), clientOrderId: payloads[i].client_order_id,
83
+ }));
84
+ const summary = { exchange: "pacifica", totalUsd: usd, reduceOnly, slippageBps: slippage.bps, legs: legView, builderFee: builder.applied, ...(builder.note !== null ? { builderNote: builder.note } : {}) };
85
+ if (isDryRun()) {
86
+ const tier = pac.activeSignerTier;
87
+ const plan = atomicDryRunPlan(payloads, {
88
+ account: pac.isReadOnly ? "<no signer configured: a live run needs a Pacifica key>" : pac.publicKey,
89
+ agentWallet: tier === "agent" ? "<agent wallet public key>" : null,
90
+ timestamp: Date.now(),
91
+ });
92
+ for (const l of legs) {
93
+ logExecution({ type: "multi_leg", exchange: "pacifica", symbol: l.symbol, side: l.side, size: l.size, status: "simulated", dryRun: true, meta: { atomic: true, reduceOnly } });
94
+ }
95
+ if (isJson())
96
+ return printJson(jsonOk({ dryRun: true, sent: false, action: "atomic_order", ...summary, request: plan }));
97
+ console.log(chalk.yellow("\n [DRY RUN] Would send a Pacifica Atomic basket (nothing signed or sent):"));
98
+ for (const l of legView)
99
+ console.log(chalk.gray(` ${l.side.toUpperCase().padEnd(4)} ${l.size} ${l.symbol} ≈ $${l.notionalUsd} (weight ${l.weight}, mark ${l.mark})`));
100
+ console.log(chalk.gray(` reduce-only: ${reduceOnly} · slippage: ${slippage.bps} bps per leg · builder: ${builder.applied === null ? "none" : builder.applied.identity}`));
101
+ console.log(chalk.gray(` ${plan.endpoint}\n${JSON.stringify(plan.body, null, 2).split("\n").map(s => ` ${s}`).join("\n")}`));
102
+ console.log(chalk.gray(` signed messages (create_market_order, one per leg):\n${plan.signedMessages.map(s => ` ${s}`).join("\n")}\n`));
103
+ return;
104
+ }
105
+ let outcomes;
106
+ let venueRaw;
107
+ try {
108
+ ({ outcomes, venueRaw } = await executeAtomic(pac, legs, payloads));
109
+ }
110
+ catch (e) {
111
+ for (const l of legs) {
112
+ logExecution({ type: "multi_leg", exchange: "pacifica", symbol: l.symbol, side: l.side, size: l.size, status: e instanceof PerpError && e.structured.code === "ORDER_PENDING" ? "unverified" : "failed", dryRun: false, error: extractErrorMessage(e), meta: { atomic: true, reduceOnly } });
113
+ if (!reduceOnly && e instanceof PerpError && e.structured.code === "ORDER_PENDING")
114
+ quarantineAtomicLeg(l.symbol, extractErrorMessage(e), isJson);
115
+ }
116
+ throw e;
117
+ }
118
+ for (const o of outcomes) {
119
+ logExecution({
120
+ type: "multi_leg", exchange: "pacifica", symbol: o.symbol, side: o.side, size: o.size,
121
+ status: o.status === "unknown" ? "unverified" : o.status === "filled" || o.status === "partiallyFilled" ? "success" : "failed",
122
+ dryRun: false, ...(o.detail !== null && o.status !== "filled" ? { error: o.detail } : {}),
123
+ meta: { atomic: true, reduceOnly, orderId: o.orderId, clientOrderId: o.clientOrderId, filledSize: o.filledSize },
124
+ });
125
+ if (o.status === "unknown" && !reduceOnly)
126
+ quarantineAtomicLeg(o.symbol, o.detail ?? "outcome unknown", isJson);
127
+ }
128
+ const incomplete = atomicIncompleteError(outcomes);
129
+ if (incomplete !== null)
130
+ throw incomplete;
131
+ if (isJson())
132
+ return printJson(jsonOk({ ...summary, outcomes, venueRaw }));
133
+ console.log(chalk.green(`\n Atomic basket filled on Pacifica ($${usd}):`));
134
+ for (const o of outcomes)
135
+ console.log(` ${chalk.green("FILLED")} ${o.side.toUpperCase().padEnd(4)} ${o.filledSize ?? o.size} ${o.symbol} order ${o.orderId ?? "-"}`);
136
+ console.log();
137
+ });
138
+ }
139
+ function quarantineAtomicLeg(symbol, reason, isJson) {
140
+ quarantineSymbol("trade-atomic", symbol, `atomic leg on ${symbol}: outcome unknown (${reason})`, `Check the position and fills: perp -e pacifica account positions / trade fills ${symbol}`, (m) => { if (!isJson())
141
+ console.error(m); });
142
+ }