perp-cli 0.14.3 → 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 (311) 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 +2 -2
  23. package/dist/bot/strategies/funding-arb-utils.d.ts +1 -1
  24. package/dist/bot/strategies/funding-arb-utils.js +5 -5
  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 +2 -2
  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 +167 -35
  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/errors.d.ts +24 -1
  73. package/dist/errors.js +25 -3
  74. package/dist/exchanges/arcus/adapter.d.ts +7 -7
  75. package/dist/exchanges/arcus/adapter.js +12 -12
  76. package/dist/exchanges/arcus/agent.d.ts +1 -1
  77. package/dist/exchanges/arcus/agent.js +1 -1
  78. package/dist/exchanges/arcus/client.js +1 -1
  79. package/dist/exchanges/arcus/signing.d.ts +2 -3
  80. package/dist/exchanges/arcus/signing.js +1 -1
  81. package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
  82. package/dist/exchanges/arcus/spot-chain.js +1 -1
  83. package/dist/exchanges/arcus/spot-router.d.ts +1 -1
  84. package/dist/exchanges/arcus/spot-router.js +2 -2
  85. package/dist/exchanges/arcus/spot.d.ts +2 -3
  86. package/dist/exchanges/arcus/spot.js +2 -3
  87. package/dist/exchanges/aster-spot.d.ts +3 -3
  88. package/dist/exchanges/aster-spot.js +6 -9
  89. package/dist/exchanges/aster-typed-data.d.ts +1 -1
  90. package/dist/exchanges/aster-typed-data.js +1 -1
  91. package/dist/exchanges/aster.d.ts +4 -4
  92. package/dist/exchanges/aster.js +31 -13
  93. package/dist/exchanges/builder-fee.d.ts +1 -1
  94. package/dist/exchanges/builder-fee.js +13 -2
  95. package/dist/exchanges/canonical-symbols.d.ts +1 -2
  96. package/dist/exchanges/canonical-symbols.js +26 -2
  97. package/dist/exchanges/derive/client.d.ts +37 -0
  98. package/dist/exchanges/derive/client.js +135 -0
  99. package/dist/exchanges/derive/options.d.ts +68 -0
  100. package/dist/exchanges/derive/options.js +360 -0
  101. package/dist/exchanges/extended/adapter.d.ts +82 -0
  102. package/dist/exchanges/extended/adapter.js +179 -0
  103. package/dist/exchanges/extended/client.d.ts +38 -0
  104. package/dist/exchanges/extended/client.js +88 -0
  105. package/dist/exchanges/extended/public.d.ts +130 -0
  106. package/dist/exchanges/extended/public.js +429 -0
  107. package/dist/exchanges/grvt/adapter.d.ts +86 -0
  108. package/dist/exchanges/grvt/adapter.js +206 -0
  109. package/dist/exchanges/grvt/client.d.ts +25 -0
  110. package/dist/exchanges/grvt/client.js +86 -0
  111. package/dist/exchanges/grvt/public.d.ts +151 -0
  112. package/dist/exchanges/grvt/public.js +438 -0
  113. package/dist/exchanges/hyperliquid-spot.d.ts +1 -1
  114. package/dist/exchanges/hyperliquid-spot.js +5 -5
  115. package/dist/exchanges/hyperliquid.d.ts +22 -12
  116. package/dist/exchanges/hyperliquid.js +80 -34
  117. package/dist/exchanges/index.d.ts +1 -1
  118. package/dist/exchanges/index.js +1 -1
  119. package/dist/exchanges/interface.d.ts +94 -6
  120. package/dist/exchanges/interface.js +16 -0
  121. package/dist/exchanges/ioc-outcome.d.ts +1 -1
  122. package/dist/exchanges/ioc-outcome.js +1 -1
  123. package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
  124. package/dist/exchanges/legacy-trading-v2.js +7 -2
  125. package/dist/exchanges/lighter-public.d.ts +2 -2
  126. package/dist/exchanges/lighter-public.js +2 -2
  127. package/dist/exchanges/lighter-signer.d.ts +1 -1
  128. package/dist/exchanges/lighter-signer.js +1 -1
  129. package/dist/exchanges/lighter-spot.d.ts +3 -4
  130. package/dist/exchanges/lighter-spot.js +6 -7
  131. package/dist/exchanges/lighter.d.ts +11 -11
  132. package/dist/exchanges/lighter.js +28 -14
  133. package/dist/exchanges/ondo/adapter.d.ts +5 -6
  134. package/dist/exchanges/ondo/adapter.js +16 -14
  135. package/dist/exchanges/ondo/agent.d.ts +1 -1
  136. package/dist/exchanges/ondo/agent.js +1 -1
  137. package/dist/exchanges/ondo/client.d.ts +1 -1
  138. package/dist/exchanges/ondo/client.js +2 -2
  139. package/dist/exchanges/ondo/public.d.ts +8 -1
  140. package/dist/exchanges/ondo/public.js +13 -2
  141. package/dist/exchanges/ondo/spot.d.ts +2 -2
  142. package/dist/exchanges/ondo/spot.js +3 -3
  143. package/dist/exchanges/options-interface.d.ts +158 -0
  144. package/dist/exchanges/options-interface.js +17 -0
  145. package/dist/exchanges/options-venues.d.ts +12 -0
  146. package/dist/exchanges/options-venues.js +35 -0
  147. package/dist/exchanges/pacifica-atomic.d.ts +211 -0
  148. package/dist/exchanges/pacifica-atomic.js +440 -0
  149. package/dist/exchanges/pacifica-decimal.d.ts +10 -0
  150. package/dist/exchanges/pacifica-decimal.js +51 -0
  151. package/dist/exchanges/pacifica-presigned.d.ts +53 -0
  152. package/dist/exchanges/pacifica-presigned.js +141 -0
  153. package/dist/exchanges/pacifica-print.d.ts +200 -0
  154. package/dist/exchanges/pacifica-print.js +417 -0
  155. package/dist/exchanges/pacifica-spot.d.ts +2 -2
  156. package/dist/exchanges/pacifica-spot.js +3 -3
  157. package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
  158. package/dist/exchanges/pacifica-typed-data.js +1 -1
  159. package/dist/exchanges/pacifica-vaults.d.ts +121 -0
  160. package/dist/exchanges/pacifica-vaults.js +264 -0
  161. package/dist/exchanges/pacifica.d.ts +32 -5
  162. package/dist/exchanges/pacifica.js +114 -48
  163. package/dist/exchanges/precision.d.ts +3 -1
  164. package/dist/exchanges/precision.js +9 -2
  165. package/dist/exchanges/qfex/adapter.d.ts +18 -5
  166. package/dist/exchanges/qfex/adapter.js +32 -12
  167. package/dist/exchanges/qfex/agent.d.ts +1 -1
  168. package/dist/exchanges/qfex/agent.js +1 -1
  169. package/dist/exchanges/qfex/client.d.ts +1 -1
  170. package/dist/exchanges/qfex/client.js +1 -1
  171. package/dist/exchanges/qfex/errors.d.ts +1 -1
  172. package/dist/exchanges/qfex/errors.js +2 -2
  173. package/dist/exchanges/qfex/public.d.ts +3 -3
  174. package/dist/exchanges/qfex/public.js +3 -3
  175. package/dist/exchanges/qfex/signing.d.ts +2 -2
  176. package/dist/exchanges/qfex/signing.js +4 -5
  177. package/dist/exchanges/qfex/ws-transport.d.ts +2 -2
  178. package/dist/exchanges/qfex/ws-transport.js +3 -3
  179. package/dist/exchanges/rate-limiter.d.ts +1 -1
  180. package/dist/exchanges/rate-limiter.js +5 -2
  181. package/dist/exchanges/registry.d.ts +1 -1
  182. package/dist/exchanges/registry.js +1 -1
  183. package/dist/exchanges/risex/adapter.d.ts +148 -0
  184. package/dist/exchanges/risex/adapter.js +796 -0
  185. package/dist/exchanges/risex/agent.d.ts +154 -0
  186. package/dist/exchanges/risex/agent.js +384 -0
  187. package/dist/exchanges/risex/client.d.ts +47 -0
  188. package/dist/exchanges/risex/client.js +174 -0
  189. package/dist/exchanges/risex/nonce.d.ts +43 -0
  190. package/dist/exchanges/risex/nonce.js +212 -0
  191. package/dist/exchanges/risex/public.d.ts +64 -0
  192. package/dist/exchanges/risex/public.js +150 -0
  193. package/dist/exchanges/risex/session.d.ts +71 -0
  194. package/dist/exchanges/risex/session.js +171 -0
  195. package/dist/exchanges/risex/typed-data.d.ts +173 -0
  196. package/dist/exchanges/risex/typed-data.js +240 -0
  197. package/dist/exchanges/risex/units.d.ts +29 -0
  198. package/dist/exchanges/risex/units.js +104 -0
  199. package/dist/exchanges/slippage.d.ts +1 -1
  200. package/dist/exchanges/slippage.js +1 -1
  201. package/dist/exchanges/spot-factory.d.ts +3 -3
  202. package/dist/exchanges/spot-factory.js +7 -7
  203. package/dist/exchanges/spot-interface.d.ts +5 -5
  204. package/dist/exchanges/spot-interface.js +2 -2
  205. package/dist/exchanges/spot-result.d.ts +3 -3
  206. package/dist/exchanges/spot-result.js +3 -3
  207. package/dist/exchanges/trading-v2.d.ts +3 -3
  208. package/dist/exchanges/trading-v2.js +9 -6
  209. package/dist/exchanges/txflow/adapter.d.ts +54 -0
  210. package/dist/exchanges/txflow/adapter.js +118 -0
  211. package/dist/exchanges/txflow/client.d.ts +47 -0
  212. package/dist/exchanges/txflow/client.js +153 -0
  213. package/dist/exchanges/txflow/public.d.ts +76 -0
  214. package/dist/exchanges/txflow/public.js +284 -0
  215. package/dist/exchanges/unknown-symbol.d.ts +29 -0
  216. package/dist/exchanges/unknown-symbol.js +51 -0
  217. package/dist/exchanges/variational/adapter.d.ts +71 -0
  218. package/dist/exchanges/variational/adapter.js +137 -0
  219. package/dist/exchanges/variational/client.d.ts +21 -0
  220. package/dist/exchanges/variational/client.js +66 -0
  221. package/dist/exchanges/variational/public.d.ts +126 -0
  222. package/dist/exchanges/variational/public.js +430 -0
  223. package/dist/exchanges/venue-types.d.ts +6 -7
  224. package/dist/exchanges/venue-types.js +2 -2
  225. package/dist/exchanges/venue.d.ts +51 -8
  226. package/dist/exchanges/venue.js +54 -4
  227. package/dist/exchanges/venues/arcus.d.ts +1 -2
  228. package/dist/exchanges/venues/arcus.js +5 -4
  229. package/dist/exchanges/venues/aster.d.ts +2 -2
  230. package/dist/exchanges/venues/aster.js +6 -4
  231. package/dist/exchanges/venues/create-helpers.d.ts +1 -1
  232. package/dist/exchanges/venues/create-helpers.js +3 -3
  233. package/dist/exchanges/venues/extended.d.ts +21 -0
  234. package/dist/exchanges/venues/extended.js +96 -0
  235. package/dist/exchanges/venues/grvt.d.ts +30 -0
  236. package/dist/exchanges/venues/grvt.js +98 -0
  237. package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
  238. package/dist/exchanges/venues/hyperliquid.js +10 -8
  239. package/dist/exchanges/venues/index.d.ts +7 -2
  240. package/dist/exchanges/venues/index.js +8 -2
  241. package/dist/exchanges/venues/lighter-family.d.ts +7 -9
  242. package/dist/exchanges/venues/lighter-family.js +9 -11
  243. package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
  244. package/dist/exchanges/venues/lighter-rh.js +7 -5
  245. package/dist/exchanges/venues/lighter.d.ts +1 -1
  246. package/dist/exchanges/venues/lighter.js +5 -3
  247. package/dist/exchanges/venues/ondo.d.ts +1 -1
  248. package/dist/exchanges/venues/ondo.js +5 -3
  249. package/dist/exchanges/venues/pacifica.d.ts +2 -2
  250. package/dist/exchanges/venues/pacifica.js +6 -4
  251. package/dist/exchanges/venues/qfex.d.ts +1 -1
  252. package/dist/exchanges/venues/qfex.js +4 -2
  253. package/dist/exchanges/venues/risex-release.d.ts +14 -0
  254. package/dist/exchanges/venues/risex-release.js +14 -0
  255. package/dist/exchanges/venues/risex.d.ts +31 -0
  256. package/dist/exchanges/venues/risex.js +116 -0
  257. package/dist/exchanges/venues/txflow.d.ts +18 -0
  258. package/dist/exchanges/venues/txflow.js +77 -0
  259. package/dist/exchanges/venues/variational.d.ts +21 -0
  260. package/dist/exchanges/venues/variational.js +85 -0
  261. package/dist/execution-log.d.ts +1 -1
  262. package/dist/funding/legacy-public.d.ts +1 -1
  263. package/dist/funding/legacy-public.js +9 -4
  264. package/dist/funding/normalize.d.ts +1 -1
  265. package/dist/funding/normalize.js +1 -1
  266. package/dist/funding/rates.d.ts +44 -4
  267. package/dist/funding/rates.js +56 -12
  268. package/dist/index.js +7 -6
  269. package/dist/jobs.d.ts +1 -0
  270. package/dist/jobs.js +25 -1
  271. package/dist/landing.d.ts +2 -0
  272. package/dist/landing.js +1 -1
  273. package/dist/mcp-pacifica-earn.d.ts +9 -0
  274. package/dist/mcp-pacifica-earn.js +58 -0
  275. package/dist/mcp-server.js +113 -59
  276. package/dist/pacifica/client.d.ts +67 -4
  277. package/dist/pacifica/client.js +170 -8
  278. package/dist/pacifica/rate-limit.d.ts +144 -0
  279. package/dist/pacifica/rate-limit.js +267 -0
  280. package/dist/pacifica/types/order.d.ts +2 -1
  281. package/dist/plan-executor.js +7 -2
  282. package/dist/rebalance.js +1 -1
  283. package/dist/risk.d.ts +2 -1
  284. package/dist/risk.js +2 -4
  285. package/dist/settings.d.ts +32 -3
  286. package/dist/signer/ows-evm.d.ts +1 -1
  287. package/dist/signer/ows-evm.js +1 -1
  288. package/dist/smart-order.d.ts +1 -1
  289. package/dist/smart-order.js +1 -1
  290. package/dist/strategies/funding-arb.d.ts +1 -1
  291. package/dist/strategies/funding-arb.js +1 -1
  292. package/dist/trade-validator.js +2 -2
  293. package/dist/utils.d.ts +12 -3
  294. package/dist/utils.js +11 -3
  295. package/dist/venue-health.d.ts +40 -0
  296. package/dist/venue-health.js +75 -0
  297. package/dist/withdraw-gate.d.ts +1 -1
  298. package/dist/withdraw-gate.js +1 -1
  299. package/dist/ws/index.d.ts +959 -0
  300. package/dist/ws/index.js +4295 -0
  301. package/dist/ws/node.d.ts +975 -0
  302. package/dist/ws/node.js +4366 -0
  303. package/dist/ws/pacifica-instrument.d.ts +8 -0
  304. package/dist/ws/pacifica-instrument.js +8 -0
  305. package/dist/ws-cli/credentials.d.ts +62 -0
  306. package/dist/ws-cli/credentials.js +126 -0
  307. package/package.json +58 -10
  308. package/skills/perp-cli/SKILL.md +62 -9
  309. package/skills/perp-cli/references/agent-operations.md +7 -0
  310. package/skills/perp-cli/references/commands.md +76 -5
  311. package/skills/perp-cli/references/ws-library.md +492 -0
@@ -0,0 +1,440 @@
1
+ /**
2
+ * Pacifica Atomic order: a weighted basket of market orders across several markets, sent in ONE
3
+ * POST /api/v1/orders/batch request.
4
+ *
5
+ * Docs (read 2026-10-19):
6
+ * - trading-on-pacifica/order-types/strategies/atomic: "Each leg is signed as a market order with
7
+ * your Max Slippage, and all legs are sent to the exchange in a single batch request. Each leg
8
+ * can fill or fail independently." Up to 5 legs per side, a market appears once in the basket,
9
+ * weights are relative, the total size is USD converted to a token amount at each leg's mark
10
+ * price, Reduce Only applies to every leg. "Atomic does not guarantee an all-or-nothing fill."
11
+ * - api-documentation/api/rest-api/orders/batch-order: action `type` "CreateMarket"; `data` = the
12
+ * signed request payload of create-market-order (signing type "create_market_order"); max 10
13
+ * actions; "If one action fails, subsequent actions are still attempted"; response
14
+ * `{ success, data: { results: [{ success, order_id, error }] } }`; Status 400 = bad request.
15
+ * - There is no Atomic endpoint, signing type or order type code: legs are ordinary market
16
+ * orders (`/orders` and `account_order_updates` show them as `order_type` / `ot` "market").
17
+ *
18
+ * Money safety: every leg is validated (lot, venue minimum, slippage bound, unique market) before
19
+ * anything is signed; the whole basket is signed before the single POST; an unreadable or
20
+ * ambiguous answer is ORDER_PENDING (never resubmitted); a per-leg fill is read back from
21
+ * history_by_id and never assumed.
22
+ */
23
+ import { randomUUID } from "crypto";
24
+ import { PerpError, extractErrorMessage, orderOutcomeUnknown } from "../errors.js";
25
+ import { prepareMessage } from "../pacifica/signing.js";
26
+ import { DEFAULT_EXPIRY_WINDOW } from "../pacifica/constants.js";
27
+ import { isPacificaPerpetual } from "../ws/pacifica-instrument.js";
28
+ import { floorSize } from "./precision.js";
29
+ import { effectiveLimit } from "../risk.js";
30
+ import { symbolNotFound } from "./unknown-symbol.js";
31
+ /** "Up to 5 markets per side" (atomic docs). */
32
+ export const ATOMIC_MAX_LEGS_PER_SIDE = 5;
33
+ /** "Maximum 10 actions per batch request" (batch-order docs). */
34
+ export const PACIFICA_BATCH_MAX_ACTIONS = 10;
35
+ /** Signing operation type of each leg (create-market-order docs). */
36
+ export const ATOMIC_LEG_SIGNING_TYPE = "create_market_order";
37
+ /** Batch action type of each leg (batch-order docs, case sensitive). */
38
+ export const ATOMIC_LEG_ACTION_TYPE = "CreateMarket";
39
+ /** Placeholder for the signature in a dry-run plan (nothing is signed in a dry run). */
40
+ export const DRY_RUN_SIGNATURE = "<not signed: dry-run>";
41
+ const WEIGHT_RE = /^(\d+(\.\d+)?|\.\d+)$/;
42
+ /** `SYMBOL` or `SYMBOL:WEIGHT` (no weight = 1, i.e. equal weights). */
43
+ function parseLeg(raw, side, flag) {
44
+ const s = raw.trim();
45
+ const i = s.lastIndexOf(":");
46
+ const symbol = (i === -1 ? s : s.slice(0, i)).trim();
47
+ const w = i === -1 ? "1" : s.slice(i + 1).trim();
48
+ if (!/^[A-Za-z0-9]+$/.test(symbol)) {
49
+ throw new PerpError("INVALID_PARAMS", `${flag} leg "${raw}": the symbol must be letters and digits (e.g. BTC or BTC:50)`, {
50
+ remediation: `Write each leg as SYMBOL or SYMBOL:WEIGHT, e.g. ${flag} BTC:50 ETH:50`,
51
+ });
52
+ }
53
+ if (!WEIGHT_RE.test(w) || !(Number(w) > 0)) {
54
+ throw new PerpError("INVALID_PARAMS", `${flag} leg "${raw}": the weight must be a positive number (got "${w}")`, {
55
+ remediation: `Write each leg as SYMBOL or SYMBOL:WEIGHT, e.g. ${flag} BTC:50`,
56
+ });
57
+ }
58
+ return { symbol, side, weight: Number(w) };
59
+ }
60
+ /** --long / --short leg lists → leg specs (≤ 5 per side, ≥ 1 leg, each market once). */
61
+ export function parseAtomicLegs(longs, shorts) {
62
+ for (const [list, flag] of [[longs, "--long"], [shorts, "--short"]]) {
63
+ if (list.length > ATOMIC_MAX_LEGS_PER_SIDE) {
64
+ throw new PerpError("INVALID_PARAMS", `${flag}: ${list.length} legs; Pacifica Atomic takes up to ${ATOMIC_MAX_LEGS_PER_SIDE} markets per side`, {
65
+ remediation: `Use at most ${ATOMIC_MAX_LEGS_PER_SIDE} ${flag} legs`,
66
+ });
67
+ }
68
+ }
69
+ const legs = [...longs.map(l => parseLeg(l, "buy", "--long")), ...shorts.map(l => parseLeg(l, "sell", "--short"))];
70
+ if (legs.length === 0) {
71
+ throw new PerpError("INVALID_PARAMS", "An Atomic order needs at least one leg", { remediation: "Pass --long SYMBOL[:WEIGHT]... and/or --short SYMBOL[:WEIGHT]..." });
72
+ }
73
+ const seen = new Set();
74
+ for (const l of legs) {
75
+ const k = l.symbol.toUpperCase();
76
+ if (seen.has(k)) {
77
+ throw new PerpError("INVALID_PARAMS", `${l.symbol} appears more than once; a market can appear only once in an Atomic basket`, {
78
+ remediation: "List each market once (combine its weights)",
79
+ });
80
+ }
81
+ seen.add(k);
82
+ }
83
+ return legs;
84
+ }
85
+ function requirePositiveField(row, key, endpoint) {
86
+ const v = row[key];
87
+ if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v)) && Number(v) > 0)
88
+ return v.trim();
89
+ throw new PerpError("EXCHANGE_ERROR", `Pacifica ${endpoint} ${String(row.symbol)}: no usable ${key} (got ${JSON.stringify(v)})`, {
90
+ exchange: "pacifica", remediation: "Retry; if it persists the venue's market data is incomplete for this market",
91
+ });
92
+ }
93
+ /**
94
+ * The basket's markets from GET /info + GET /info/prices (public reads). Perp rows only; every
95
+ * requested symbol must be listed, with a positive lot_size / min_order_size and a positive mark
96
+ * (PRICE_STALE otherwise — nothing sent). Resolved to the venue's own spelling (e.g. kBONK).
97
+ */
98
+ export function atomicMarketsOf(specs, info, prices) {
99
+ const perps = info.filter(isPacificaPerpetual);
100
+ const out = new Map();
101
+ for (const s of specs) {
102
+ const row = perps.find(m => m.symbol.toUpperCase() === s.symbol.toUpperCase());
103
+ if (row === undefined)
104
+ throw symbolNotFound("pacifica", s.symbol);
105
+ const r = row;
106
+ const lotSize = requirePositiveField(r, "lot_size", "/info");
107
+ const minOrderUsd = requirePositiveField(r, "min_order_size", "/info");
108
+ const p = prices.find(x => x.symbol === row.symbol);
109
+ const mark = p === undefined ? NaN : Number(p.mark);
110
+ if (p === undefined || !Number.isFinite(mark) || !(mark > 0)) {
111
+ throw new PerpError("PRICE_STALE", `Pacifica ${row.symbol}: no usable mark price to size the Atomic leg; nothing was sent`, {
112
+ exchange: "pacifica", remediation: "Retry in a moment",
113
+ });
114
+ }
115
+ out.set(s.symbol.toUpperCase(), { symbol: row.symbol, lotSize, minOrderUsd, mark: String(p.mark) });
116
+ }
117
+ return out;
118
+ }
119
+ /**
120
+ * Size every leg: total × weight / Σweights in USD → token amount at the mark, floored to the
121
+ * lot. A leg under one lot → SIZE_TOO_SMALL (nothing sent). Its notional at the mark must be at
122
+ * least the market's USD minimum × (1 + slippage bound), so a sell leg still meets the minimum when
123
+ * it fills at the worst price the bound allows. Reduce-only legs are held to it too (no exemption is
124
+ * documented).
125
+ */
126
+ export function sizeAtomicLegs(specs, totalUsd, markets, slippageBps) {
127
+ const total = Number(totalUsd);
128
+ if (!Number.isFinite(total) || !(total > 0))
129
+ throw new PerpError("INVALID_PARAMS", `--usd must be a positive amount (got "${totalUsd}")`);
130
+ const sum = specs.reduce((a, s) => a + s.weight, 0);
131
+ return specs.map((s) => {
132
+ const m = markets.get(s.symbol.toUpperCase());
133
+ if (m === undefined)
134
+ throw symbolNotFound("pacifica", s.symbol);
135
+ const share = s.weight / sum;
136
+ const targetUsd = total * share;
137
+ const raw = (targetUsd / Number(m.mark)).toFixed(12);
138
+ let size;
139
+ try {
140
+ size = floorSize(raw, m.lotSize);
141
+ }
142
+ catch {
143
+ throw new PerpError("SIZE_TOO_SMALL", `Atomic leg ${m.symbol}: $${targetUsd.toFixed(2)} at mark ${m.mark} is ${raw} — below one lot (${m.lotSize}); nothing was sent`, {
144
+ exchange: "pacifica", symbol: m.symbol, lotSize: m.lotSize,
145
+ remediation: "Raise --usd or this leg's weight",
146
+ });
147
+ }
148
+ const notionalUsd = Number(size) * Number(m.mark);
149
+ const requiredUsd = Number(m.minOrderUsd) * (1 + slippageBps / 10_000);
150
+ if (notionalUsd < requiredUsd) {
151
+ throw new PerpError("SIZE_TOO_SMALL", `Atomic leg ${m.symbol}: ${size} (≈ $${notionalUsd.toFixed(2)}) is below the venue minimum order of $${m.minOrderUsd} plus the ${slippageBps} bps slippage bound (≈ $${requiredUsd.toFixed(2)}); nothing was sent`, {
152
+ exchange: "pacifica", symbol: m.symbol, size, minOrderUsd: m.minOrderUsd, requiredUsd,
153
+ remediation: "Raise --usd or this leg's weight",
154
+ });
155
+ }
156
+ return { symbol: m.symbol, side: s.side, weight: s.weight, share, targetUsd, mark: m.mark, lotSize: m.lotSize, size, notionalUsd };
157
+ });
158
+ }
159
+ /**
160
+ * Reduce-only basket check (read only): every leg must reduce an existing position — a buy needs
161
+ * a short, a sell a long — by no more than its size. Otherwise INVALID_PARAMS, nothing sent.
162
+ */
163
+ export function requireAtomicReduceOnly(legs, positions) {
164
+ for (const l of legs) {
165
+ const need = l.side === "buy" ? "short" : "long";
166
+ const pos = positions.find(p => p.symbol.toUpperCase() === l.symbol.toUpperCase() && Number(p.size) !== 0);
167
+ if (pos === undefined || pos.side !== need) {
168
+ throw new PerpError("INVALID_PARAMS", `--reduce-only: the ${l.side} leg on ${l.symbol} needs an open ${need} position (${pos === undefined ? "none open" : `the position is ${pos.side}`}); nothing was sent`, {
169
+ exchange: "pacifica", remediation: "With --reduce-only, put the markets you are short under --long and those you are long under --short",
170
+ });
171
+ }
172
+ if (Number(l.size) > Math.abs(Number(pos.size))) {
173
+ throw new PerpError("INVALID_PARAMS", `--reduce-only: the ${l.side} leg on ${l.symbol} is ${l.size}, more than the ${pos.size.replace(/^-/, "")} ${need} position; nothing was sent`, {
174
+ exchange: "pacifica", remediation: "Lower --usd (or this leg's weight) so that no leg exceeds its position",
175
+ });
176
+ }
177
+ }
178
+ }
179
+ /** A client_order_id for one leg: a UUID v4 outside the reserved TIF-encoding prefix (5ac1f). */
180
+ export function atomicLegClientOrderId() {
181
+ for (;;) {
182
+ const id = randomUUID();
183
+ if (!id.startsWith("5ac1f"))
184
+ return id;
185
+ }
186
+ }
187
+ /**
188
+ * Basis points → Pacifica `slippage_percent` as an exact decimal string (1.1 bps → "0.011", not
189
+ * the float "0.011000000000000001"; 50 → "0.5").
190
+ */
191
+ export function bpsToPercent(bps) {
192
+ const s = String(bps);
193
+ if (!/^\d+(\.\d+)?$/.test(s))
194
+ throw new PerpError("INVALID_PARAMS", `slippage ${s} bps is not a plain decimal`);
195
+ const [int, frac = ""] = s.split(".");
196
+ const digits = (int + frac).replace(/^0+(?=\d)/, "");
197
+ const scale = frac.length + 2;
198
+ const padded = digits.padStart(scale + 1, "0");
199
+ const out = `${padded.slice(0, padded.length - scale)}.${padded.slice(padded.length - scale)}`.replace(/\.?0+$/, "");
200
+ return out === "" ? "0" : out;
201
+ }
202
+ /**
203
+ * Basket-level risk gate (on top of the per-leg enforceOrderRisk, which checks each leg against
204
+ * the max position size): the WHOLE basket's notional against the total-exposure limit and the
205
+ * number of legs that open a new market against max positions — preTradeCheck's rules applied
206
+ * to the basket as one order. Violations → RISK_VIOLATION (nothing sent). --force skips it.
207
+ */
208
+ export function atomicBasketRiskCheck(assessment, legs, positions) {
209
+ const refuse = (reason) => new PerpError("RISK_VIOLATION", `Atomic basket: ${reason}`, {
210
+ exchange: "pacifica", remediation: "Lower --usd / fewer legs, or adjust the limits (perp risk limits …); --force skips the local risk check",
211
+ });
212
+ if (!assessment.canTrade)
213
+ throw refuse("trading suspended: critical risk violation active");
214
+ const lim = assessment.limits;
215
+ const m = assessment.metrics;
216
+ const basket = legs.reduce((a, l) => a + l.notionalUsd, 0);
217
+ const effExposure = effectiveLimit(lim.maxTotalExposureUsd, lim.maxExposurePct, m.totalEquity);
218
+ if (m.totalExposure + basket > effExposure) {
219
+ throw refuse(`would exceed the total exposure limit ($${(m.totalExposure + basket).toFixed(0)} > $${effExposure.toFixed(0)})`);
220
+ }
221
+ const opening = legs.filter(l => !positions.some(p => p.symbol.toUpperCase() === l.symbol.toUpperCase() && Number(p.size) !== 0)).length;
222
+ if (m.positionCount + opening > lim.maxPositions) {
223
+ throw refuse(`would exceed max positions (${m.positionCount} + ${opening} new > ${lim.maxPositions})`);
224
+ }
225
+ }
226
+ /**
227
+ * The create_market_order payload of one leg (create-market-order docs: symbol, amount, side,
228
+ * slippage_percent, reduce_only, client_order_id, optional builder_code). Signed as-is.
229
+ */
230
+ export function atomicLegPayload(leg, o) {
231
+ return {
232
+ symbol: leg.symbol,
233
+ amount: leg.size,
234
+ side: leg.side === "buy" ? "bid" : "ask",
235
+ slippage_percent: bpsToPercent(o.slippageBps),
236
+ reduce_only: o.reduceOnly,
237
+ client_order_id: o.clientOrderId,
238
+ ...(o.builderCode !== null ? { builder_code: o.builderCode } : {}),
239
+ };
240
+ }
241
+ /**
242
+ * Dry-run plan: the exact POST /orders/batch body the live run would send, with the signature
243
+ * replaced by a placeholder, plus each leg's canonical message (recursively key-sorted compact JSON,
244
+ * `{data, expiry_window, timestamp, type}`) that the live run signs. Nothing is signed.
245
+ */
246
+ export function atomicDryRunPlan(payloads, o) {
247
+ const expiry = o.expiryWindow ?? DEFAULT_EXPIRY_WINDOW;
248
+ const header = { type: ATOMIC_LEG_SIGNING_TYPE, timestamp: o.timestamp, expiry_window: expiry };
249
+ return {
250
+ endpoint: "POST /api/v1/orders/batch",
251
+ body: {
252
+ actions: payloads.map((p) => ({
253
+ type: ATOMIC_LEG_ACTION_TYPE,
254
+ data: {
255
+ account: o.account,
256
+ ...(o.agentWallet !== null ? { agent_wallet: o.agentWallet } : {}),
257
+ signature: DRY_RUN_SIGNATURE,
258
+ timestamp: o.timestamp,
259
+ expiry_window: expiry,
260
+ ...p,
261
+ },
262
+ })),
263
+ },
264
+ signedMessages: payloads.map(p => prepareMessage(header, p)),
265
+ };
266
+ }
267
+ const BATCH_REMEDIATION = "Do NOT resubmit — some legs may have been executed. Check positions and fills first: perp -e pacifica account positions / trade fills";
268
+ /**
269
+ * Error thrown by POST /orders/batch → typed. Already-typed errors (ORDER_PENDING after a send,
270
+ * ORDER_REJECTED when nothing could be sent) are kept. HTTP 400 is the documented whole-request
271
+ * refusal (batch-order: "Status 400: Bad request"; "All signatures must be valid for the batch to
272
+ * process") → ORDER_REJECTED. HTTP 429 → RATE_LIMITED, nothing processed. Any other answer →
273
+ * ORDER_PENDING (undocumented for this endpoint: legs may have executed).
274
+ */
275
+ export function atomicBatchError(e) {
276
+ if (e instanceof PerpError)
277
+ return e;
278
+ const msg = extractErrorMessage(e);
279
+ const m = /^POST \/orders\/batch failed \((\d{3})\): ([\s\S]*)$/.exec(msg);
280
+ if (m !== null && m[1] === "400") {
281
+ return new PerpError("ORDER_REJECTED", `Pacifica refused the Atomic batch (HTTP 400, no leg was processed): ${m[2].slice(0, 300)}`, {
282
+ exchange: "pacifica", httpStatus: 400, remediation: "Fix the parameters named in the message and retry",
283
+ });
284
+ }
285
+ if (m !== null && m[1] === "429") {
286
+ return new PerpError("RATE_LIMITED", `Pacifica rate-limited the Atomic batch (HTTP 429, no leg was processed): ${m[2].slice(0, 300)}`, {
287
+ exchange: "pacifica", httpStatus: 429, sent: false, remediation: "Wait for the rate-limit window to reset, then retry",
288
+ });
289
+ }
290
+ return orderOutcomeUnknown("pacifica", "Atomic batch", e, BATCH_REMEDIATION);
291
+ }
292
+ /**
293
+ * Per-leg acks of a 200 batch response, in leg order. The body must carry `data.results` with one
294
+ * row per action; otherwise the batch's effect is unknown → ORDER_PENDING. A row with
295
+ * `success: true` was accepted (its `order_id` may be null — the docs' own example has one); a row
296
+ * with `success: false` and an error string was refused; anything else is unknown.
297
+ */
298
+ export function parseAtomicBatchResponse(raw, legCount) {
299
+ const pending = (why) => orderOutcomeUnknown("pacifica", "Atomic batch", new Error(why), BATCH_REMEDIATION);
300
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
301
+ throw pending(`unreadable batch response ${JSON.stringify(raw)?.slice(0, 200)}`);
302
+ const r = raw;
303
+ const data = r.data;
304
+ const results = data !== null && typeof data === "object" ? data.results : undefined;
305
+ if (!Array.isArray(results)) {
306
+ throw pending(`batch response without data.results (success ${JSON.stringify(r.success)}, error ${JSON.stringify(r.error)})`);
307
+ }
308
+ if (results.length !== legCount) {
309
+ throw pending(`batch response has ${results.length} results for ${legCount} legs`);
310
+ }
311
+ return results.map((row) => {
312
+ if (row === null || typeof row !== "object")
313
+ return { kind: "unknown", reason: `unreadable result ${JSON.stringify(row)}` };
314
+ const x = row;
315
+ if (x.success === true) {
316
+ const id = typeof x.order_id === "number" || (typeof x.order_id === "string" && /^\d+$/.test(x.order_id)) ? String(x.order_id) : null;
317
+ return { kind: "accepted", orderId: id };
318
+ }
319
+ if (x.success === false && typeof x.error === "string" && x.error !== "")
320
+ return { kind: "rejected", error: x.error };
321
+ return { kind: "unknown", reason: `result without a definite success/error: ${JSON.stringify(x).slice(0, 160)}` };
322
+ });
323
+ }
324
+ /**
325
+ * Leg outcome from its ack, the order id it was traced to (the ack's, or one resolved from the
326
+ * leg's client_order_id; null = never traced) and that order's history_by_id outcome (null = not
327
+ * resolved).
328
+ */
329
+ export function atomicLegOutcome(leg, clientOrderId, ack, orderId, history) {
330
+ const base = { symbol: leg.symbol, side: leg.side, size: leg.size, clientOrderId };
331
+ if (ack.kind === "rejected")
332
+ return { ...base, orderId: null, status: "rejected", filledSize: "0", detail: ack.error };
333
+ if (ack.kind === "unknown")
334
+ return { ...base, orderId: null, status: "unknown", filledSize: null, detail: ack.reason };
335
+ if (orderId === null) {
336
+ return { ...base, orderId: null, status: "unknown", filledSize: null, detail: `accepted without an order_id, and client_order_id ${clientOrderId} was not found in open orders / order history` };
337
+ }
338
+ if (history === null)
339
+ return { ...base, orderId, status: "unknown", filledSize: null, detail: "accepted; fill not confirmed (history_by_id)" };
340
+ const map = {
341
+ filled: "filled", partiallyFilled: "partiallyFilled", cancelled: "notFilled", rejected: "rejected", open: "unknown",
342
+ };
343
+ return { ...base, orderId, status: map[history.status], filledSize: history.filledSize, detail: `history_by_id ${history.venueStatus}` };
344
+ }
345
+ /**
346
+ * The failure an Atomic basket reports unless every leg filled completely: any unknown leg →
347
+ * ORDER_PENDING (do not resubmit); nothing filled at all → ORDER_REJECTED; otherwise
348
+ * ORDER_PARTIAL. details carry every leg.
349
+ */
350
+ export function atomicIncompleteError(legs) {
351
+ const count = (s) => legs.filter(l => l.status === s).length;
352
+ if (count("filled") === legs.length)
353
+ return null;
354
+ const summary = `${count("filled")} filled, ${count("partiallyFilled")} partly filled, ${count("notFilled") + count("rejected")} not filled, ${count("unknown")} unknown`;
355
+ const details = { exchange: "pacifica", legs };
356
+ if (count("unknown") > 0) {
357
+ return new PerpError("ORDER_PENDING", `Pacifica Atomic basket: the outcome of ${count("unknown")} leg(s) is unknown (${summary})`, { ...details, remediation: BATCH_REMEDIATION });
358
+ }
359
+ if (count("filled") === 0 && count("partiallyFilled") === 0) {
360
+ return new PerpError("ORDER_REJECTED", `Pacifica Atomic basket: no leg filled — ${legs.map(l => `${l.symbol}: ${l.detail ?? l.status}`).join("; ")}`, details);
361
+ }
362
+ return new PerpError("ORDER_PARTIAL", `Pacifica Atomic basket: ${summary}`, {
363
+ ...details, remediation: "The basket is unbalanced. Inspect: perp -e pacifica account positions — then adjust or close the filled legs",
364
+ });
365
+ }
366
+ /** Safety margin under the signature expiry: the batch is not sent when less than this is left. */
367
+ export const ATOMIC_EXPIRY_MARGIN_MS = 1_500;
368
+ /** client_order_id → order_id lookups for a leg accepted without an order_id. */
369
+ export const ATOMIC_CLOID_POLLS = 6;
370
+ export const ATOMIC_CLOID_POLL_MS = 500;
371
+ const REAL_DEPS = { now: () => Date.now(), sleep: (ms) => new Promise(r => setTimeout(r, ms)) };
372
+ /**
373
+ * order_id of a leg accepted without one (the batch docs' CreateMarket result carries
374
+ * `order_id: null`), found by its client_order_id in open orders / order history
375
+ * (PacificaAdapter.orderByClientId). Bounded: ATOMIC_CLOID_POLLS reads ATOMIC_CLOID_POLL_MS apart;
376
+ * read errors poll again. null = never found (the leg stays unknown).
377
+ */
378
+ export async function resolveAtomicLegOrderId(adapter, clientOrderId, deps = REAL_DEPS) {
379
+ for (let attempt = 0; attempt < ATOMIC_CLOID_POLLS; attempt++) {
380
+ await deps.sleep(ATOMIC_CLOID_POLL_MS);
381
+ try {
382
+ const hit = await adapter.orderByClientId(clientOrderId);
383
+ if (hit !== null)
384
+ return hit.orderId;
385
+ }
386
+ catch {
387
+ // A failed read proves nothing either way: poll again; never found → the leg stays unknown.
388
+ }
389
+ }
390
+ return null;
391
+ }
392
+ /**
393
+ * Wait for rate-limit capacity, sign every leg (create_market_order) with ONE shared timestamp,
394
+ * send them in ONE batch (the post-sign throttle wait bounded by the signature deadline), then
395
+ * read each accepted leg's fill back (in parallel — reads only). Signing failures and a signature
396
+ * that would expire within ATOMIC_EXPIRY_MARGIN_MS (a slow signer) stop before anything is sent
397
+ * (ORDER_REJECTED). Returns the per-leg outcomes; the caller decides with atomicIncompleteError.
398
+ */
399
+ export async function executeAtomic(adapter, legs, payloads, deps = REAL_DEPS) {
400
+ if (legs.length !== payloads.length || legs.length === 0 || legs.length > PACIFICA_BATCH_MAX_ACTIONS) {
401
+ throw new PerpError("INVALID_PARAMS", `Atomic basket must have 1..${PACIFICA_BATCH_MAX_ACTIONS} legs (got ${legs.length})`);
402
+ }
403
+ adapter.prepareSigning();
404
+ // Rate-limit wait BEFORE signing (up to 15 s; RATE_LIMITED = nothing signed or sent), for the
405
+ // whole batch's per-action cost — a wait after signing would eat the signature window.
406
+ await adapter.sdk.awaitBatchCapacity(payloads.map(() => ATOMIC_LEG_ACTION_TYPE));
407
+ const header = { timestamp: deps.now(), expiry_window: DEFAULT_EXPIRY_WINDOW };
408
+ const actions = [];
409
+ for (const p of payloads) {
410
+ actions.push({ type: ATOMIC_LEG_ACTION_TYPE, data: await adapter.sdk.signBatchAction(ATOMIC_LEG_SIGNING_TYPE, p, adapter.publicKey, adapter.signer, header) });
411
+ }
412
+ const elapsed = deps.now() - header.timestamp;
413
+ if (elapsed >= header.expiry_window - ATOMIC_EXPIRY_MARGIN_MS) {
414
+ throw new PerpError("ORDER_REJECTED", `Pacifica Atomic batch not sent: signing ${legs.length} legs took ${elapsed} ms, leaving less than ${ATOMIC_EXPIRY_MARGIN_MS} ms of the ${header.expiry_window} ms signature window`, {
415
+ exchange: "pacifica", sent: false, remediation: "Nothing was sent. Retry; if the signer is slow (hardware / remote wallet), use fewer legs",
416
+ });
417
+ }
418
+ // The post-sign throttle may wait only for what is left before the guard margin: a longer wait
419
+ // fails fast (RATE_LIMITED, sent: false) instead of sending a batch past its expiry.
420
+ const leftMs = header.expiry_window - ATOMIC_EXPIRY_MARGIN_MS - elapsed;
421
+ let raw;
422
+ try {
423
+ raw = await adapter.sdk.batchOrders(actions, { maxWaitMs: leftMs });
424
+ }
425
+ catch (e) {
426
+ throw atomicBatchError(e);
427
+ }
428
+ const acks = parseAtomicBatchResponse(raw, legs.length);
429
+ const outcomes = await Promise.all(legs.map(async (leg, i) => {
430
+ const ack = acks[i];
431
+ const cloid = String(payloads[i].client_order_id);
432
+ if (ack.kind !== "accepted")
433
+ return atomicLegOutcome(leg, cloid, ack, null, null);
434
+ const orderId = ack.orderId !== null ? ack.orderId : await resolveAtomicLegOrderId(adapter, cloid, deps);
435
+ const history = orderId === null ? null
436
+ : await adapter.resolveOrderOutcome(orderId, `atomic ${leg.side} ${leg.size} ${leg.symbol}`, { terminalOnly: true });
437
+ return atomicLegOutcome(leg, cloid, ack, orderId, history);
438
+ }));
439
+ return { outcomes, venueRaw: raw };
440
+ }
@@ -0,0 +1,10 @@
1
+ /** True when `s` is a plain decimal string ("12", "-0.5"). */
2
+ export declare function isPlainDecimal(s: unknown): s is string;
3
+ /** Sign of a − b: -1, 0, 1. */
4
+ export declare function cmpDecimal(a: string, b: string): -1 | 0 | 1;
5
+ /** Number of fractional digits written ("1.50" → 2). */
6
+ export declare function fractionDigits(s: string): number;
7
+ /** a ÷ b truncated toward zero at `scale` fractional digits, trailing zeros dropped. b = 0 → null. */
8
+ export declare function divDecimal(a: string, b: string, scale?: number): string | null;
9
+ /** Trailing fractional zeros dropped ("20.190000000000" → "20.19", "10.000" → "10"). */
10
+ export declare function trimDecimal(s: string): string;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Exact decimal-string helpers for Print / vault math (scaled BigInt, no float drift). Venue
3
+ * values carry up to ~28 fractional digits (lake balances), so Number() is not enough.
4
+ */
5
+ import { PerpError } from "../errors.js";
6
+ import { subDecimal } from "./precision.js";
7
+ const PLAIN = /^-?\d+(\.\d+)?$/;
8
+ function scaled(s) {
9
+ if (!PLAIN.test(s))
10
+ throw new PerpError("INVALID_PARAMS", `Not a plain decimal number: "${s}"`);
11
+ const neg = s.startsWith("-");
12
+ const [ip, fp = ""] = (neg ? s.slice(1) : s).split(".");
13
+ return { int: BigInt(ip + fp) * (neg ? -1n : 1n), scale: fp.length };
14
+ }
15
+ /** True when `s` is a plain decimal string ("12", "-0.5"). */
16
+ export function isPlainDecimal(s) {
17
+ return typeof s === "string" && PLAIN.test(s);
18
+ }
19
+ /** Sign of a − b: -1, 0, 1. */
20
+ export function cmpDecimal(a, b) {
21
+ const d = subDecimal(a, b);
22
+ if (/^-?0(\.0+)?$/.test(d))
23
+ return 0;
24
+ return d.startsWith("-") ? -1 : 1;
25
+ }
26
+ /** Number of fractional digits written ("1.50" → 2). */
27
+ export function fractionDigits(s) {
28
+ return scaled(s).scale;
29
+ }
30
+ /** a ÷ b truncated toward zero at `scale` fractional digits, trailing zeros dropped. b = 0 → null. */
31
+ export function divDecimal(a, b, scale = 18) {
32
+ const x = scaled(a);
33
+ const y = scaled(b);
34
+ if (y.int === 0n)
35
+ return null;
36
+ // (x.int / 10^xs) / (y.int / 10^ys) = x.int * 10^(ys + scale - xs) / y.int, at `scale` digits
37
+ const shift = y.scale + scale - x.scale;
38
+ const num = shift >= 0 ? x.int * 10n ** BigInt(shift) : x.int;
39
+ const den = shift >= 0 ? y.int : y.int * 10n ** BigInt(-shift);
40
+ const q = num / den;
41
+ const neg = q < 0n;
42
+ let s = (neg ? -q : q).toString().padStart(scale + 1, "0");
43
+ if (scale > 0)
44
+ s = `${s.slice(0, s.length - scale)}.${s.slice(s.length - scale)}`.replace(/\.?0+$/, "");
45
+ return (neg && s !== "0" ? "-" : "") + s;
46
+ }
47
+ /** Trailing fractional zeros dropped ("20.190000000000" → "20.19", "10.000" → "10"). */
48
+ export function trimDecimal(s) {
49
+ scaled(s);
50
+ return s.includes(".") ? s.replace(/\.?0+$/, "") : s;
51
+ }
@@ -0,0 +1,53 @@
1
+ import type { PacificaClient } from "../pacifica/client.js";
2
+ export type PresignedPath = "/game/deposit" | "/game/end" | "/lake/deposit" | "/lake/withdraw";
3
+ export interface PresignedRequest {
4
+ /** Signing operation type (header `type`). */
5
+ type: string;
6
+ path: PresignedPath;
7
+ /** Canonical JSON the signer signs (UTF-8 bytes). */
8
+ message: string;
9
+ /** Request body without `signature`. */
10
+ body: Record<string, unknown>;
11
+ /** `idempotency_key` of the payload, or null when the operation has none. */
12
+ idempotencyKey: string | null;
13
+ }
14
+ export interface PresignedIdentity {
15
+ /** Master account address (`account`). */
16
+ account: string;
17
+ /** Agent wallet pubkey when an agent key signs, else null (master signs). */
18
+ agentWallet: string | null;
19
+ }
20
+ /** Build the canonical message and unsigned body. Undefined payload values are refused (they would be dropped by JSON but not by the signer). */
21
+ export declare function buildPresigned(p: {
22
+ type: string;
23
+ path: PresignedPath;
24
+ payload: Record<string, unknown>;
25
+ identity: PresignedIdentity;
26
+ timestamp: number;
27
+ expiryWindow?: number;
28
+ }): PresignedRequest;
29
+ /**
30
+ * The same request with a new timestamp (message + body re-derived; payload, identity, expiry
31
+ * window and idempotency key unchanged) — used after the pre-sign credit wait so the signature's
32
+ * expiry window starts when it is signed.
33
+ */
34
+ export declare function restampPresigned(req: PresignedRequest, timestamp: number): PresignedRequest;
35
+ /** Sign the canonical message; returns the full body (`signature` added). A signing failure is "not sent". */
36
+ export declare function signPresigned(req: PresignedRequest, signMessage: (msg: Uint8Array) => Promise<Uint8Array>): Promise<Record<string, unknown>>;
37
+ /**
38
+ * Send a signed body and return the response `data` object. `what` names the action in errors
39
+ * ("Print deposit"); `check` is the read command that settles an unknown outcome.
40
+ */
41
+ export declare function sendPresigned(client: Pick<PacificaClient, "postPresigned">, req: PresignedRequest, signedBody: Record<string, unknown>, ctx: {
42
+ what: string;
43
+ check: string;
44
+ before?: Record<string, unknown>;
45
+ }): Promise<Record<string, unknown>>;
46
+ /**
47
+ * ORDER_PENDING remediation. With an idempotency key, the ONLY safe retry is the same request with
48
+ * the same key (`--idempotency-key`): the venue answers a duplicate with HTTP 409 instead of
49
+ * executing it twice. How long Pacifica remembers a key is not documented (unverified).
50
+ */
51
+ export declare function pendingRemediation(what: string, check: string, idempotencyKey: string | null): string;
52
+ /** `--idempotency-key`: a full UUID string (API docs), lowercased; refused otherwise. */
53
+ export declare function requireIdempotencyKey(raw: string): string;