perp-cli 0.12.17 → 0.14.1

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 (363) hide show
  1. package/README.md +247 -27
  2. package/dist/_stderr-filter.js +0 -1
  3. package/dist/agent-wallet/lighter-keystore.d.ts +11 -0
  4. package/dist/agent-wallet/lighter-keystore.js +92 -10
  5. package/dist/agent-wallet/passphrase.d.ts +1 -1
  6. package/dist/agent-wallet/passphrase.js +16 -3
  7. package/dist/agent-wallet/store.d.ts +37 -4
  8. package/dist/agent-wallet/store.js +132 -0
  9. package/dist/agent-wallet/venue-keystore.d.ts +14 -0
  10. package/dist/agent-wallet/venue-keystore.js +121 -0
  11. package/dist/api/public/hyperliquid.js +4 -4
  12. package/dist/api/public/lighter.js +5 -5
  13. package/dist/api/public/pacifica.d.ts +0 -1
  14. package/dist/api/public/pacifica.js +4 -13
  15. package/dist/api/public/urls.d.ts +9 -1
  16. package/dist/api/public/urls.js +35 -13
  17. package/dist/arb/file-lock.d.ts +11 -0
  18. package/dist/arb/file-lock.js +74 -0
  19. package/dist/arb/index.d.ts +2 -2
  20. package/dist/arb/index.js +2 -2
  21. package/dist/arb/pending-entry.d.ts +32 -0
  22. package/dist/arb/pending-entry.js +77 -0
  23. package/dist/arb/sizing.d.ts +6 -13
  24. package/dist/arb/sizing.js +12 -51
  25. package/dist/arb/state.d.ts +45 -12
  26. package/dist/arb/state.js +214 -42
  27. package/dist/arb/utils.d.ts +20 -6
  28. package/dist/arb/utils.js +43 -39
  29. package/dist/bot/conditions.d.ts +4 -4
  30. package/dist/bot/conditions.js +11 -6
  31. package/dist/bot/config.d.ts +17 -1
  32. package/dist/bot/config.js +48 -12
  33. package/dist/bot/engine.d.ts +61 -2
  34. package/dist/bot/engine.js +610 -144
  35. package/dist/bot/fill-size.d.ts +7 -0
  36. package/dist/bot/fill-size.js +18 -0
  37. package/dist/bot/index.d.ts +1 -1
  38. package/dist/bot/index.js +1 -1
  39. package/dist/bot/perp-order.d.ts +99 -0
  40. package/dist/bot/perp-order.js +299 -0
  41. package/dist/bot/presets.d.ts +7 -0
  42. package/dist/bot/presets.js +17 -0
  43. package/dist/bot/quarantine.d.ts +75 -0
  44. package/dist/bot/quarantine.js +190 -0
  45. package/dist/bot/quote-rules.d.ts +23 -0
  46. package/dist/bot/quote-rules.js +69 -0
  47. package/dist/bot/strategies/avellaneda-mm.d.ts +32 -2
  48. package/dist/bot/strategies/avellaneda-mm.js +72 -70
  49. package/dist/bot/strategies/basis-arb.js +2 -0
  50. package/dist/bot/strategies/dca-strategy.d.ts +2 -0
  51. package/dist/bot/strategies/dca-strategy.js +5 -0
  52. package/dist/bot/strategies/funding-arb-strategy.d.ts +43 -3
  53. package/dist/bot/strategies/funding-arb-strategy.js +571 -380
  54. package/dist/bot/strategies/funding-arb-utils.d.ts +48 -23
  55. package/dist/bot/strategies/funding-arb-utils.js +159 -195
  56. package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
  57. package/dist/bot/strategies/funding-arb-v2.js +258 -47
  58. package/dist/bot/strategies/funding-auto.js +215 -194
  59. package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +14 -0
  60. package/dist/bot/strategies/spot-perp-arb-strategy.js +217 -50
  61. package/dist/bot/strategies/spot-perp-executor.d.ts +73 -19
  62. package/dist/bot/strategies/spot-perp-executor.js +319 -120
  63. package/dist/bot/strategies/spot-perp-state.d.ts +43 -0
  64. package/dist/bot/strategies/spot-perp-state.js +70 -0
  65. package/dist/bot/strategy-types.d.ts +6 -0
  66. package/dist/bot/tui/BotDashboard.d.ts +7 -2
  67. package/dist/bot/tui/BotDashboard.js +6 -3
  68. package/dist/bridge-engine.d.ts +120 -19
  69. package/dist/bridge-engine.js +710 -422
  70. package/dist/bridge-signers.d.ts +54 -0
  71. package/dist/bridge-signers.js +161 -0
  72. package/dist/cache.d.ts +4 -1
  73. package/dist/cache.js +11 -0
  74. package/dist/cli-flags.d.ts +50 -0
  75. package/dist/cli-flags.js +104 -0
  76. package/dist/cli-program.d.ts +35 -0
  77. package/dist/cli-program.js +303 -0
  78. package/dist/cli-spec.js +1 -1
  79. package/dist/client-id-tracker.js +11 -2
  80. package/dist/commands/account.d.ts +7 -1
  81. package/dist/commands/account.js +70 -92
  82. package/dist/commands/agent-import.d.ts +114 -0
  83. package/dist/commands/agent-import.js +794 -0
  84. package/dist/commands/agent.d.ts +232 -2
  85. package/dist/commands/agent.js +1858 -955
  86. package/dist/commands/alerts.d.ts +1 -0
  87. package/dist/commands/alerts.js +31 -28
  88. package/dist/commands/arb/index.d.ts +60 -1
  89. package/dist/commands/arb/index.js +381 -384
  90. package/dist/commands/arb-auto.d.ts +132 -4
  91. package/dist/commands/arb-auto.js +1474 -967
  92. package/dist/commands/arb.d.ts +3 -1
  93. package/dist/commands/arb.js +62 -30
  94. package/dist/commands/bot.js +127 -47
  95. package/dist/commands/bridge.d.ts +2 -1
  96. package/dist/commands/bridge.js +339 -192
  97. package/dist/commands/builder.d.ts +55 -0
  98. package/dist/commands/builder.js +387 -0
  99. package/dist/commands/funds-onchain.d.ts +32 -0
  100. package/dist/commands/funds-onchain.js +248 -0
  101. package/dist/commands/funds.d.ts +25 -0
  102. package/dist/commands/funds.js +925 -245
  103. package/dist/commands/history.js +102 -54
  104. package/dist/commands/init.d.ts +107 -15
  105. package/dist/commands/init.js +1116 -249
  106. package/dist/commands/jobs.d.ts +2 -1
  107. package/dist/commands/jobs.js +77 -15
  108. package/dist/commands/manage.js +136 -91
  109. package/dist/commands/market.d.ts +4 -1
  110. package/dist/commands/market.js +35 -32
  111. package/dist/commands/outcome.d.ts +10 -0
  112. package/dist/commands/outcome.js +396 -0
  113. package/dist/commands/plan.js +52 -12
  114. package/dist/commands/portfolio.d.ts +1 -0
  115. package/dist/commands/portfolio.js +60 -72
  116. package/dist/commands/rebalance.d.ts +2 -1
  117. package/dist/commands/rebalance.js +33 -43
  118. package/dist/commands/risk.js +46 -19
  119. package/dist/commands/settings.d.ts +1 -0
  120. package/dist/commands/settings.js +246 -51
  121. package/dist/commands/trade.d.ts +50 -0
  122. package/dist/commands/trade.js +1943 -695
  123. package/dist/commands/wallet.d.ts +30 -1
  124. package/dist/commands/wallet.js +357 -459
  125. package/dist/config.d.ts +60 -9
  126. package/dist/config.js +252 -82
  127. package/dist/constants.d.ts +29 -4
  128. package/dist/constants.js +62 -20
  129. package/dist/dashboard/server.d.ts +2 -0
  130. package/dist/dashboard/server.js +9 -6
  131. package/dist/dashboard/ui.js +6 -4
  132. package/dist/dashboard/ws-feeds.js +20 -8
  133. package/dist/dex-asset-map.js +5 -3
  134. package/dist/errors.d.ts +122 -0
  135. package/dist/errors.js +194 -0
  136. package/dist/event-stream.js +75 -19
  137. package/dist/evm-master-tx.d.ts +59 -0
  138. package/dist/evm-master-tx.js +154 -0
  139. package/dist/exchanges/adapter-network.d.ts +10 -0
  140. package/dist/exchanges/adapter-network.js +10 -0
  141. package/dist/exchanges/arcus/adapter.d.ts +196 -0
  142. package/dist/exchanges/arcus/adapter.js +1092 -0
  143. package/dist/exchanges/arcus/agent.d.ts +55 -0
  144. package/dist/exchanges/arcus/agent.js +188 -0
  145. package/dist/exchanges/arcus/client.d.ts +63 -0
  146. package/dist/exchanges/arcus/client.js +285 -0
  147. package/dist/exchanges/arcus/public.d.ts +65 -0
  148. package/dist/exchanges/arcus/public.js +138 -0
  149. package/dist/exchanges/arcus/signing.d.ts +176 -0
  150. package/dist/exchanges/arcus/signing.js +288 -0
  151. package/dist/exchanges/arcus/spot-chain.d.ts +60 -0
  152. package/dist/exchanges/arcus/spot-chain.js +241 -0
  153. package/dist/exchanges/arcus/spot-router.d.ts +177 -0
  154. package/dist/exchanges/arcus/spot-router.js +183 -0
  155. package/dist/exchanges/arcus/spot.d.ts +109 -0
  156. package/dist/exchanges/arcus/spot.js +645 -0
  157. package/dist/exchanges/aster-funds.d.ts +127 -0
  158. package/dist/exchanges/aster-funds.js +252 -0
  159. package/dist/exchanges/aster-nonce.d.ts +2 -0
  160. package/dist/exchanges/aster-nonce.js +107 -0
  161. package/dist/exchanges/aster-spot.d.ts +117 -0
  162. package/dist/exchanges/aster-spot.js +686 -0
  163. package/dist/exchanges/aster-typed-data.d.ts +133 -7
  164. package/dist/exchanges/aster-typed-data.js +121 -7
  165. package/dist/exchanges/aster.d.ts +240 -6
  166. package/dist/exchanges/aster.js +1139 -195
  167. package/dist/exchanges/builder-fee.d.ts +254 -0
  168. package/dist/exchanges/builder-fee.js +421 -0
  169. package/dist/exchanges/canonical-symbols.d.ts +32 -0
  170. package/dist/exchanges/canonical-symbols.js +96 -0
  171. package/dist/exchanges/capabilities.d.ts +7 -1
  172. package/dist/exchanges/capabilities.js +7 -0
  173. package/dist/exchanges/hl-signing.d.ts +165 -0
  174. package/dist/exchanges/hl-signing.js +158 -0
  175. package/dist/exchanges/hyperliquid-outcome.d.ts +211 -0
  176. package/dist/exchanges/hyperliquid-outcome.js +646 -0
  177. package/dist/exchanges/hyperliquid-spot.d.ts +44 -5
  178. package/dist/exchanges/hyperliquid-spot.js +308 -117
  179. package/dist/exchanges/hyperliquid.d.ts +462 -57
  180. package/dist/exchanges/hyperliquid.js +1575 -449
  181. package/dist/exchanges/index.d.ts +6 -0
  182. package/dist/exchanges/index.js +4 -0
  183. package/dist/exchanges/interface.d.ts +63 -10
  184. package/dist/exchanges/interface.js +18 -3
  185. package/dist/exchanges/ioc-outcome.d.ts +44 -0
  186. package/dist/exchanges/ioc-outcome.js +57 -0
  187. package/dist/exchanges/legacy-trading-v2.d.ts +39 -0
  188. package/dist/exchanges/legacy-trading-v2.js +940 -0
  189. package/dist/exchanges/lighter-public.d.ts +43 -0
  190. package/dist/exchanges/lighter-public.js +276 -0
  191. package/dist/exchanges/lighter-rh-deposit.d.ts +62 -0
  192. package/dist/exchanges/lighter-rh-deposit.js +185 -0
  193. package/dist/exchanges/lighter-signer.d.ts +123 -0
  194. package/dist/exchanges/lighter-signer.js +215 -0
  195. package/dist/exchanges/lighter-spot.d.ts +91 -13
  196. package/dist/exchanges/lighter-spot.js +595 -233
  197. package/dist/exchanges/lighter.d.ts +611 -39
  198. package/dist/exchanges/lighter.js +1890 -649
  199. package/dist/exchanges/ondo/adapter.d.ts +152 -0
  200. package/dist/exchanges/ondo/adapter.js +811 -0
  201. package/dist/exchanges/ondo/agent.d.ts +31 -0
  202. package/dist/exchanges/ondo/agent.js +101 -0
  203. package/dist/exchanges/ondo/client.d.ts +60 -0
  204. package/dist/exchanges/ondo/client.js +297 -0
  205. package/dist/exchanges/ondo/funds.d.ts +95 -0
  206. package/dist/exchanges/ondo/funds.js +198 -0
  207. package/dist/exchanges/ondo/public.d.ts +95 -0
  208. package/dist/exchanges/ondo/public.js +214 -0
  209. package/dist/exchanges/ondo/spot.d.ts +127 -0
  210. package/dist/exchanges/ondo/spot.js +392 -0
  211. package/dist/exchanges/outcome-interface.d.ts +140 -0
  212. package/dist/exchanges/outcome-interface.js +16 -0
  213. package/dist/exchanges/pacifica-spot.d.ts +83 -0
  214. package/dist/exchanges/pacifica-spot.js +324 -0
  215. package/dist/exchanges/pacifica-typed-data.d.ts +30 -13
  216. package/dist/exchanges/pacifica-typed-data.js +57 -9
  217. package/dist/exchanges/pacifica.d.ts +293 -4
  218. package/dist/exchanges/pacifica.js +1044 -90
  219. package/dist/exchanges/precision.d.ts +47 -0
  220. package/dist/exchanges/precision.js +209 -0
  221. package/dist/exchanges/qfex/adapter.d.ts +341 -0
  222. package/dist/exchanges/qfex/adapter.js +1386 -0
  223. package/dist/exchanges/qfex/agent.d.ts +58 -0
  224. package/dist/exchanges/qfex/agent.js +188 -0
  225. package/dist/exchanges/qfex/client.d.ts +54 -0
  226. package/dist/exchanges/qfex/client.js +138 -0
  227. package/dist/exchanges/qfex/errors.d.ts +70 -0
  228. package/dist/exchanges/qfex/errors.js +236 -0
  229. package/dist/exchanges/qfex/public.d.ts +122 -0
  230. package/dist/exchanges/qfex/public.js +268 -0
  231. package/dist/exchanges/qfex/signing.d.ts +160 -0
  232. package/dist/exchanges/qfex/signing.js +283 -0
  233. package/dist/exchanges/qfex/ws-transport.d.ts +294 -0
  234. package/dist/exchanges/qfex/ws-transport.js +798 -0
  235. package/dist/exchanges/rate-limiter.d.ts +12 -0
  236. package/dist/exchanges/rate-limiter.js +64 -0
  237. package/dist/exchanges/registry.d.ts +14 -2
  238. package/dist/exchanges/registry.js +20 -32
  239. package/dist/exchanges/slippage.d.ts +37 -0
  240. package/dist/exchanges/slippage.js +70 -0
  241. package/dist/exchanges/spot-factory.d.ts +166 -0
  242. package/dist/exchanges/spot-factory.js +431 -0
  243. package/dist/exchanges/spot-interface.d.ts +98 -4
  244. package/dist/exchanges/spot-interface.js +27 -1
  245. package/dist/exchanges/spot-result.d.ts +79 -0
  246. package/dist/exchanges/spot-result.js +273 -0
  247. package/dist/exchanges/trading-v2.d.ts +117 -0
  248. package/dist/exchanges/trading-v2.js +301 -0
  249. package/dist/exchanges/venue-types.d.ts +231 -0
  250. package/dist/exchanges/venue-types.js +87 -0
  251. package/dist/exchanges/venue.d.ts +211 -0
  252. package/dist/exchanges/venue.js +132 -0
  253. package/dist/exchanges/venues/arcus.d.ts +8 -0
  254. package/dist/exchanges/venues/arcus.js +109 -0
  255. package/dist/exchanges/venues/aster.d.ts +49 -0
  256. package/dist/exchanges/venues/aster.js +176 -0
  257. package/dist/exchanges/venues/create-helpers.d.ts +18 -0
  258. package/dist/exchanges/venues/create-helpers.js +199 -0
  259. package/dist/exchanges/venues/hyperliquid.d.ts +23 -0
  260. package/dist/exchanges/venues/hyperliquid.js +126 -0
  261. package/dist/exchanges/venues/index.d.ts +16 -0
  262. package/dist/exchanges/venues/index.js +26 -0
  263. package/dist/exchanges/venues/lighter-family.d.ts +92 -0
  264. package/dist/exchanges/venues/lighter-family.js +166 -0
  265. package/dist/exchanges/venues/lighter-rh.d.ts +7 -0
  266. package/dist/exchanges/venues/lighter-rh.js +58 -0
  267. package/dist/exchanges/venues/lighter.d.ts +6 -0
  268. package/dist/exchanges/venues/lighter.js +55 -0
  269. package/dist/exchanges/venues/ondo.d.ts +29 -0
  270. package/dist/exchanges/venues/ondo.js +136 -0
  271. package/dist/exchanges/venues/pacifica.d.ts +8 -0
  272. package/dist/exchanges/venues/pacifica.js +95 -0
  273. package/dist/exchanges/venues/qfex.d.ts +39 -0
  274. package/dist/exchanges/venues/qfex.js +140 -0
  275. package/dist/execution-log.js +10 -2
  276. package/dist/funding/legacy-public.d.ts +31 -0
  277. package/dist/funding/legacy-public.js +369 -0
  278. package/dist/funding/normalize.d.ts +28 -16
  279. package/dist/funding/normalize.js +61 -29
  280. package/dist/funding/rates.d.ts +46 -11
  281. package/dist/funding/rates.js +228 -277
  282. package/dist/guardrail/perp-guardrail.js +0 -0
  283. package/dist/index.js +156 -684
  284. package/dist/jobs.d.ts +84 -6
  285. package/dist/jobs.js +239 -41
  286. package/dist/landing.d.ts +7 -2
  287. package/dist/landing.js +19 -3
  288. package/dist/mcp-server.d.ts +14 -4
  289. package/dist/mcp-server.js +821 -403
  290. package/dist/mcp-trade-guard.d.ts +16 -0
  291. package/dist/mcp-trade-guard.js +59 -0
  292. package/dist/onchain-balances.d.ts +40 -0
  293. package/dist/onchain-balances.js +173 -0
  294. package/dist/pacifica/client.d.ts +41 -10
  295. package/dist/pacifica/client.js +167 -63
  296. package/dist/pacifica/constants.d.ts +0 -1
  297. package/dist/pacifica/constants.js +0 -5
  298. package/dist/pacifica/deposit.d.ts +30 -3
  299. package/dist/pacifica/deposit.js +119 -2
  300. package/dist/pacifica/faucet.d.ts +29 -0
  301. package/dist/pacifica/faucet.js +100 -0
  302. package/dist/pacifica/sign-tx.d.ts +15 -0
  303. package/dist/pacifica/sign-tx.js +41 -0
  304. package/dist/pacifica/types/account.d.ts +28 -4
  305. package/dist/pacifica/types/lake.d.ts +20 -5
  306. package/dist/pacifica/types/lake.js +4 -0
  307. package/dist/pacifica/types/market.d.ts +5 -0
  308. package/dist/pacifica/types/order.d.ts +4 -1
  309. package/dist/pacifica/types/position.d.ts +9 -1
  310. package/dist/plan-executor.d.ts +10 -5
  311. package/dist/plan-executor.js +165 -54
  312. package/dist/position-history.js +13 -0
  313. package/dist/rebalance.d.ts +26 -2
  314. package/dist/rebalance.js +86 -7
  315. package/dist/relay-swap.d.ts +232 -0
  316. package/dist/relay-swap.js +630 -0
  317. package/dist/risk.js +5 -1
  318. package/dist/secret-env.d.ts +15 -0
  319. package/dist/secret-env.js +36 -0
  320. package/dist/settings.d.ts +169 -6
  321. package/dist/settings.js +219 -38
  322. package/dist/signer/ows-evm.d.ts +13 -0
  323. package/dist/signer/ows-evm.js +22 -0
  324. package/dist/smart-order.d.ts +9 -0
  325. package/dist/smart-order.js +53 -8
  326. package/dist/strategies/dca.d.ts +6 -2
  327. package/dist/strategies/dca.js +66 -26
  328. package/dist/strategies/funding-arb.d.ts +11 -1
  329. package/dist/strategies/funding-arb.js +202 -89
  330. package/dist/strategies/grid.d.ts +31 -0
  331. package/dist/strategies/grid.js +89 -11
  332. package/dist/strategies/pending-stop.d.ts +8 -0
  333. package/dist/strategies/pending-stop.js +12 -0
  334. package/dist/strategies/split-order.d.ts +45 -17
  335. package/dist/strategies/split-order.js +157 -91
  336. package/dist/strategies/trailing-stop.d.ts +2 -1
  337. package/dist/strategies/trailing-stop.js +52 -6
  338. package/dist/strategies/twap.d.ts +6 -1
  339. package/dist/strategies/twap.js +39 -11
  340. package/dist/trade-validator.d.ts +22 -0
  341. package/dist/trade-validator.js +119 -15
  342. package/dist/utils/numeric.d.ts +46 -0
  343. package/dist/utils/numeric.js +79 -0
  344. package/dist/utils.d.ts +19 -0
  345. package/dist/utils.js +96 -19
  346. package/dist/withdraw-gate.d.ts +49 -0
  347. package/dist/withdraw-gate.js +131 -0
  348. package/package.json +15 -6
  349. package/skills/perp-cli/SKILL.md +159 -55
  350. package/skills/perp-cli/references/agent-operations.md +112 -58
  351. package/skills/perp-cli/references/commands.md +269 -73
  352. package/skills/perp-cli/references/strategies.md +34 -31
  353. package/skills/perp-cli/scripts/arb-monitor.sh +10 -5
  354. package/skills/perp-cli/scripts/funding-analysis.sh +14 -9
  355. package/skills/perp-cli/scripts/preflight.sh +21 -14
  356. package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
  357. package/skills/perp-cli/scripts/validate-arb.sh +31 -15
  358. package/vendor/lighter-signer/SHA256SUMS +2 -0
  359. package/vendor/lighter-signer/VERSION +8 -0
  360. package/vendor/lighter-signer/lighter-signer.wasm +0 -0
  361. package/vendor/lighter-signer/wasm_exec.js +575 -0
  362. package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
  363. package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
@@ -1,7 +1,337 @@
1
1
  import { PacificaClient } from "../pacifica/index.js";
2
2
  import { LocalSolanaSigner } from "../signer/index.js";
3
3
  import { isExpired } from "../agent-wallet/expiry.js";
4
- import { PerpError } from "../errors.js";
4
+ import { randomUUID } from "crypto";
5
+ import { PerpError, extractErrorMessage, isDefiniteOrderFailure, orderNotSent, orderOutcomeUnknown } from "../errors.js";
6
+ import { parseFiniteVenueNumber, venueRateField } from "../utils/numeric.js";
7
+ import { unsupported, validateLegacyTif } from "./trading-v2.js";
8
+ import { floorSize, requireOnTick, subDecimal } from "./precision.js";
9
+ import { requireCapabilities } from "./venue.js";
10
+ import { PACIFICA_UUID_RE, pacificaVenue } from "./venues/pacifica.js";
11
+ import { approvalFromCeiling, builderGateOf, rateToBps, requireFeeBps, NO_BUILDER } from "./builder-fee.js";
12
+ /**
13
+ * CLI TIF → Pacifica TIF (design §13.3). GTC / IOC / ALO / TOB are native; venue
14
+ * aliases GTT → GTC and GTX → ALO; no TIF → GTC (uniform default). FOK (never
15
+ * emulated) and unknown values → uniform CAPABILITY_UNSUPPORTED.
16
+ */
17
+ export function pacificaTif(tif) {
18
+ if (tif === undefined || tif.trim() === "")
19
+ return "GTC";
20
+ return validateLegacyTif(pacificaVenue, requireCapabilities(pacificaVenue), tif);
21
+ }
22
+ /**
23
+ * Generated client_order_id carrying the order's TIF (design §13.4, §13.10 #10, like Ondo's
24
+ * `pcli-<p|n>-…`): Pacifica takes a full UUID and never returns the TIF (GET /orders has no
25
+ * tif field), so amend recovers it from the id. Layout: a UUID v4 whose first 6 hex digits are
26
+ * `5ac1f` + TIF code (1 GTC, 2 IOC, 3 ALO, 4 TOB). The native edit keeps the client_order_id.
27
+ */
28
+ const PACIFICA_TIF_CODE = { GTC: "1", IOC: "2", ALO: "3", TOB: "4" };
29
+ const PACIFICA_CLOID_RE = /^5ac1f([1-4])[0-9a-f]{2}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
30
+ export function pacificaClientOrderId(tif) {
31
+ return `5ac1f${PACIFICA_TIF_CODE[tif]}${randomUUID().slice(6)}`;
32
+ }
33
+ /**
34
+ * A caller's client order id: Pacifica takes a "Full UUID string" (docs.pacifica.fi rest-api
35
+ * orders/create-limit-order + create-market-order: `client_order_id` | Full UUID string | optional
36
+ * | "Client-defined order ID" | f47ac10b-58cc-4372-a567-0e02b2c3d479). Anything else →
37
+ * INVALID_PARAMS before sending. An id in the reserved generated layout (`5ac1f` + TIF code) must
38
+ * state the order's own TIF `tif` (amend trusts it); null `tif` = the order has none (market).
39
+ */
40
+ export function requirePacificaClientOrderId(id, tif) {
41
+ if (!PACIFICA_UUID_RE.test(id)) {
42
+ throw new PerpError("INVALID_PARAMS", `Client order id "${id}" is not valid on Pacifica (a full UUID, e.g. f47ac10b-58cc-4372-a567-0e02b2c3d479)`, {
43
+ exchange: "pacifica", remediation: "Pass a UUID as --client-id, or use --auto-id (a UUID is generated and reported)",
44
+ });
45
+ }
46
+ const encoded = pacificaTifOf(id.toLowerCase());
47
+ if (encoded !== null && encoded !== tif) {
48
+ throw new PerpError("INVALID_PARAMS", `Client order id "${id}" uses the reserved 5ac1f prefix that encodes TIF ${encoded}, but the order is ${tif === null ? "a market order" : tif}`, {
49
+ exchange: "pacifica", remediation: "Use a UUID that does not start with 5ac1f, or --auto-id",
50
+ });
51
+ }
52
+ return id;
53
+ }
54
+ /** The raw create result with the client_order_id that was SENT attached (read back by LegacyTradingV2.result). */
55
+ function withSentClientOrderId(raw, clientOrderId) {
56
+ if (clientOrderId === null || raw === null || typeof raw !== "object" || Array.isArray(raw))
57
+ return raw;
58
+ return { ...raw, clientOrderId };
59
+ }
60
+ /** TIF encoded in a generated client_order_id; null = not generated here (attributes unknown). */
61
+ export function pacificaTifOf(clientOrderId) {
62
+ if (typeof clientOrderId !== "string")
63
+ return null;
64
+ const m = PACIFICA_CLOID_RE.exec(clientOrderId);
65
+ if (m === null)
66
+ return null;
67
+ const hit = Object.entries(PACIFICA_TIF_CODE).find(([, c]) => c === m[1]);
68
+ return hit === undefined ? null : hit[0];
69
+ }
70
+ /** Pacifica POST error that is a venue answer (HTTP 4xx): the request was refused, nothing changed. */
71
+ const PACIFICA_TERMINAL_STATUSES = new Set(["filled", "cancelled", "canceled", "rejected"]);
72
+ /**
73
+ * The order's current `/orders/history_by_id` row. The array position is not a documented
74
+ * ordering and is never used: the row with the largest `created_at` wins (a timestamp tie →
75
+ * the single terminal row among them; several terminal rows tied → null). When rows lack a
76
+ * numeric `created_at`, only an explicit terminal row (filled / cancelled / rejected) for
77
+ * that order id is taken, and only when it is the only one. null = not proven (the caller
78
+ * polls again or reports pending). Rows of another order id are ignored.
79
+ */
80
+ export function pacificaLatestHistoryRow(rows, orderId) {
81
+ if (!Array.isArray(rows))
82
+ return null;
83
+ const mine = rows.filter((r) => r !== null && typeof r === "object" && (r.order_id === undefined || r.order_id === null || String(r.order_id) === orderId));
84
+ if (mine.length === 0)
85
+ return null;
86
+ const ts = (r) => {
87
+ const v = r.created_at;
88
+ if (typeof v === "number" && Number.isFinite(v))
89
+ return v;
90
+ if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v)))
91
+ return Number(v);
92
+ return null;
93
+ };
94
+ const terminal = (rs) => rs.filter(r => PACIFICA_TERMINAL_STATUSES.has(String(r.order_status ?? "")));
95
+ if (mine.every(r => ts(r) !== null)) {
96
+ const max = Math.max(...mine.map(r => ts(r)));
97
+ const top = mine.filter(r => ts(r) === max);
98
+ if (top.length === 1)
99
+ return top[0];
100
+ const t = terminal(top);
101
+ if (t.length === 1)
102
+ return t[0];
103
+ return t.length === 0 ? top[0] : null;
104
+ }
105
+ const t = terminal(mine);
106
+ return t.length === 1 ? t[0] : null;
107
+ }
108
+ const DECIMAL_RE = /^\d+(\.\d+)?$/;
109
+ /**
110
+ * The order's outcome from its history_by_id rows (pacificaLatestHistoryRow), or null when not
111
+ * proven: no row for `orderId`, an unknown order_status, or no decimal filled_amount.
112
+ * `terminalOnly` (IOC): an open / partially_filled row is not an outcome (the order never rests).
113
+ * `expect` (price / size sent): a row whose price or initial_amount differ is not this
114
+ * placement / edit yet → null.
115
+ */
116
+ export function pacificaOrderOutcomeOf(rows, orderId, opts = {}) {
117
+ const row = pacificaLatestHistoryRow(rows, orderId);
118
+ if (row === null || row.order_id === undefined || row.order_id === null)
119
+ return null;
120
+ const st = String(row.order_status ?? "");
121
+ const filled = String(row.filled_amount ?? "").trim();
122
+ if (!DECIMAL_RE.test(filled))
123
+ return null;
124
+ const price = typeof row.price === "string" || typeof row.price === "number" ? String(row.price) : null;
125
+ const size = typeof row.initial_amount === "string" || typeof row.initial_amount === "number" ? String(row.initial_amount) : null;
126
+ const ex = opts.expect;
127
+ if (ex !== undefined) {
128
+ if (ex.price !== null && (price === null || Number(price) !== Number(ex.price)))
129
+ return null;
130
+ if (ex.size !== null && (size === null || Number(size) !== Number(ex.size)))
131
+ return null;
132
+ }
133
+ const hasFill = Number(filled) > 0;
134
+ let status;
135
+ if (st === "open")
136
+ status = hasFill ? "partiallyFilled" : "open";
137
+ else if (st === "partially_filled")
138
+ status = "partiallyFilled";
139
+ else if (st === "filled")
140
+ status = "filled";
141
+ else if (st === "cancelled" || st === "canceled")
142
+ status = hasFill ? "partiallyFilled" : "cancelled";
143
+ else if (st === "rejected")
144
+ status = "rejected";
145
+ else
146
+ return null;
147
+ if (opts.terminalOnly === true && (st === "open" || st === "partially_filled"))
148
+ return null;
149
+ return {
150
+ status, final: true, venueStatus: st, eventType: typeof row.event_type === "string" ? row.event_type : null,
151
+ orderId, filledSize: filled, price, size, source: "history_by_id",
152
+ };
153
+ }
154
+ /** order_id of a create / edit response ({ success, data: { order_id } }), or null. */
155
+ function pacificaCreatedId(raw) {
156
+ if (raw === null || typeof raw !== "object")
157
+ return null;
158
+ const r = raw;
159
+ const data = (r.data !== null && typeof r.data === "object" ? r.data : r);
160
+ return typeof data.order_id === "number" || typeof data.order_id === "string" ? String(data.order_id) : null;
161
+ }
162
+ /**
163
+ * A create-order POST the venue refused with HTTP 422 (Pacifica "Business Logic Error", error-codes
164
+ * doc) is a definite pre-trade rejection — no order was created. Live testnet 2026-10-02:
165
+ * POST /orders/create_market 422 {"success":false,"error":"No reasonable price found for SOL-USDC",
166
+ * "code":0,"error_id":"no_reasonable_price"} (an empty book). → ORDER_REJECTED carrying the venue's
167
+ * error / code / error_id — but ONLY for the documented validation codes
168
+ * (PACIFICA_DEFINITE_CREATE_CODES / _ERROR_IDS). Internal or unknown codes → ORDER_PENDING (the
169
+ * order may exist). Anything else (other status, unparseable body, not a create path, already a
170
+ * PerpError) → `e` unchanged.
171
+ */
172
+ export function pacificaCreateRejection(e, what) {
173
+ if (e instanceof PerpError)
174
+ return e;
175
+ const m = /^POST (\/orders\/(?:stop\/)?create\S*) failed \(422\): ([\s\S]*)$/.exec(extractErrorMessage(e));
176
+ if (m === null)
177
+ return e;
178
+ let body;
179
+ try {
180
+ const parsed = JSON.parse(m[2]);
181
+ if (parsed === null || typeof parsed !== "object")
182
+ return e;
183
+ body = parsed;
184
+ }
185
+ catch {
186
+ return e;
187
+ }
188
+ if (body.success !== false || typeof body.error !== "string" || body.error === "")
189
+ return e;
190
+ const details = { exchange: "pacifica", venueCode: body.code ?? null, errorId: typeof body.error_id === "string" ? body.error_id : null, path: m[1] };
191
+ // Business-logic codes per docs.pacifica.fi api error-codes (422): 4 INVALID_TICK_LEVEL,
192
+ // 5 INSUFFICIENT_BALANCE, 7 ORDER_AMOUNT_TOO_LOW, 8 ORDER_AMOUNT_TOO_HIGH. All are definite: no order was created.
193
+ const byCode = {
194
+ 4: "INVALID_TICK", 5: "INSUFFICIENT_BALANCE", 7: "SIZE_TOO_SMALL", 8: "SIZE_TOO_LARGE",
195
+ };
196
+ const mapped = typeof body.code === "number" ? byCode[body.code] : undefined;
197
+ if (mapped !== undefined)
198
+ return new PerpError(mapped, `Pacifica ${what} refused (no order was created): ${body.error}`, details);
199
+ const errorId = typeof body.error_id === "string" ? body.error_id.toLowerCase() : null;
200
+ const definite = (typeof body.code === "number" && PACIFICA_DEFINITE_CREATE_CODES.has(body.code))
201
+ || (errorId !== null && PACIFICA_DEFINITE_CREATE_ERROR_IDS.has(errorId));
202
+ if (definite)
203
+ return new PerpError("ORDER_REJECTED", `Pacifica ${what} refused (no order was created): ${body.error}`, details);
204
+ // 0 INTERNAL, 14 DATABASE_ERROR, 26 CHANNEL_CLOSED, 27 RESPONSE_DROPPED, or a code this CLI does
205
+ // not know: the venue did not say the order was refused — it may exist.
206
+ return orderOutcomeUnknown("pacifica", what, new Error(`HTTP 422 code ${String(body.code)}${errorId !== null ? ` (${errorId})` : ""}: ${body.error}`), "Do NOT resubmit — the order may have been created. Check: perp account orders -e pacifica / perp account positions -e pacifica");
207
+ }
208
+ /**
209
+ * 422 business codes that say a create was validated and REFUSED (docs.pacifica.fi api
210
+ * error-codes, read 2026-10-05): 1 ACCOUNT_NOT_FOUND, 3 BOOK_NOT_FOUND, 4 INVALID_TICK_LEVEL,
211
+ * 5 INSUFFICIENT_BALANCE, 7/8 ORDER_AMOUNT_TOO_LOW/HIGH, 10 OPEN_ORDER_LIMIT_REACHED,
212
+ * 13 POSITION_NOT_FOUND, 16–23 stop / order-type / reduce-only validation, 25 NO_REASONABLE_PRICE,
213
+ * 28 IMMEDIATE_LIQUIDATION, 30 PRICE_TOO_FAR_FROM_MARK, 36 DUPLICATE_CLIENT_ORDER_ID,
214
+ * 38 TRADING_DISABLED, 41 OPEN_INTEREST_LIMIT_EXCEEDED, 49 POSITION_TPSL_LIMIT_EXCEEDED,
215
+ * 50 INVALID_BUILDER_CODE, 51 UNSUPPORTED_OPERATION, 52 INVALID_TICK_SIZE,
216
+ * 53 ORDER_BLOCKED_BY_LOAN_POOL_STRESS, 55/56 ASSET_NOT_FOUND/NOT_ACTIVE, 59 INVALID_AMOUNT,
217
+ * 62 INSUFFICIENT_SPOT_BALANCE, 63 MISSING_MARK_PRICE, 106–108 spot restrictions,
218
+ * 119 ORACLE_NOT_AVAILABLE. NOT listed (outcome unknown): 0 INTERNAL, 14 DATABASE_ERROR,
219
+ * 26 CHANNEL_CLOSED, 27 RESPONSE_DROPPED and every undocumented code.
220
+ */
221
+ export const PACIFICA_DEFINITE_CREATE_CODES = new Set([
222
+ 1, 3, 4, 5, 7, 8, 10, 13, 16, 17, 18, 19, 20, 21, 22, 23, 25, 28, 30, 36, 38, 41, 49, 50, 51, 52, 53, 55, 56, 59, 62, 63, 106, 107, 108, 119,
223
+ ]);
224
+ /**
225
+ * `error_id` values that are definite on their own: the venue sends some validation refusals
226
+ * under code 0 with a specific error_id (live testnet 2026-10-02: an empty book →
227
+ * {"code":0,"error_id":"no_reasonable_price"}).
228
+ * Live mainnet 2026-10-05: a reduce-only limit with no position →
229
+ * {"code":0,"error_id":"no_position_for_reduce_only_order"} ("No position found for reduce-only order").
230
+ */
231
+ export const PACIFICA_DEFINITE_CREATE_ERROR_IDS = new Set(["no_reasonable_price", "no_position_for_reduce_only_order"]);
232
+ /**
233
+ * Pacifica's documented USDC withdrawal minimum (docs.pacifica.fi "Deposits & Withdrawals":
234
+ * "Minimum withdrawal: $1. Fees: $1 per withdrawal (gas)"). Not the whole rule: testnet refused
235
+ * exactly 1 USDC on 2026-10-02 with 422 code 29 WITHDRAW_AMOUNT_TOO_LOW, so amounts at or just
236
+ * above it are left to the venue (pacificaWithdrawRejection maps the refusal).
237
+ */
238
+ export const PACIFICA_WITHDRAW_MIN_USDC = "1";
239
+ /**
240
+ * A withdraw POST the venue refused with HTTP 422 (Business Logic Error — nothing was withdrawn):
241
+ * code 29 WITHDRAW_AMOUNT_TOO_LOW → INVALID_PARAMS, code 9 OVER_WITHDRAWAL → INSUFFICIENT_BALANCE,
242
+ * any other explicit refusal → ACTION_REJECTED (codes: docs.pacifica.fi api error-codes). Anything
243
+ * else (other status, unparseable body, already a PerpError) → `e` unchanged (outcome unknown).
244
+ */
245
+ export function pacificaWithdrawRejection(e, amount) {
246
+ if (e instanceof PerpError)
247
+ return e;
248
+ const m = /^POST \/account\/withdraw failed \(422\): ([\s\S]*)$/.exec(extractErrorMessage(e));
249
+ if (m === null)
250
+ return e;
251
+ let body;
252
+ try {
253
+ const parsed = JSON.parse(m[1]);
254
+ if (parsed === null || typeof parsed !== "object")
255
+ return e;
256
+ body = parsed;
257
+ }
258
+ catch {
259
+ return e;
260
+ }
261
+ if (body.success !== false || typeof body.error !== "string" || body.error === "")
262
+ return e;
263
+ const details = { exchange: "pacifica", venueCode: body.code ?? null, errorId: typeof body.error_id === "string" ? body.error_id : null };
264
+ if (body.code === 29) {
265
+ return new PerpError("INVALID_PARAMS", `Pacifica withdraw ${amount} USDC refused (nothing was withdrawn): ${body.error}`, {
266
+ ...details,
267
+ remediation: `Withdraw a larger amount — Pacifica documents a ${PACIFICA_WITHDRAW_MIN_USDC} USDC minimum plus a $1 fee per withdrawal, and refused ${amount} USDC`,
268
+ });
269
+ }
270
+ if (body.code === 9) {
271
+ return new PerpError("INSUFFICIENT_BALANCE", `Pacifica withdraw ${amount} USDC refused (nothing was withdrawn): ${body.error}`, {
272
+ ...details, remediation: "Check available_to_withdraw: perp -e pacifica portfolio",
273
+ });
274
+ }
275
+ return new PerpError("ACTION_REJECTED", `Pacifica withdraw ${amount} USDC refused (nothing was withdrawn): ${body.error}`, details);
276
+ }
277
+ export function pacificaRefused(e) {
278
+ return !(e instanceof PerpError) && /^POST \S+ failed \(4\d\d\)/.test(extractErrorMessage(e));
279
+ }
280
+ /**
281
+ * What a refused cancel (POST /orders/cancel 4xx, e.g. 422 {"error":"Failed to cancel order","code":5}
282
+ * for an already-filled market order — live testnet 2026-10-02) means for the order, from its
283
+ * history_by_id read (`history` = the read's data, or the read's error):
284
+ * - terminal row (filled / cancelled / rejected) → failed: nothing was cancelled by this request;
285
+ * - GET 404 "Order history not found" → failed: the venue has no such order;
286
+ * - a still-open row, no row, or any other read failure → pending (not proven either way).
287
+ * Never "cancelled".
288
+ */
289
+ export function pacificaCancelRefusalOf(orderId, refusal, history) {
290
+ if ("error" in history) {
291
+ const msg = extractErrorMessage(history.error);
292
+ if (/^GET \S*history_by_id failed \(404\)/.test(msg)) {
293
+ return { kind: "failed", error: `order ${orderId} does not exist on Pacifica (history_by_id 404); nothing was cancelled — venue refusal: ${refusal}` };
294
+ }
295
+ return { kind: "pending", reason: `the venue refused the cancel (${refusal}) and the order's state could not be read (${msg})` };
296
+ }
297
+ const out = pacificaOrderOutcomeOf(history.data, orderId);
298
+ if (out === null)
299
+ return { kind: "pending", reason: `the venue refused the cancel (${refusal}) and history_by_id shows no settled row for order ${orderId}` };
300
+ if (out.venueStatus === "open" || out.venueStatus === "partially_filled") {
301
+ return { kind: "pending", reason: `the venue refused the cancel (${refusal}) but order ${orderId} is still ${out.venueStatus} (filled ${out.filledSize})` };
302
+ }
303
+ return { kind: "failed", error: `order ${orderId} is already ${out.venueStatus} (filled ${out.filledSize}, history_by_id); nothing was cancelled — venue refusal: ${refusal}` };
304
+ }
305
+ /** `{ success: false }` / `{ error }` body = explicit venue refusal. */
306
+ function pacificaBodyError(r) {
307
+ if (r === null || typeof r !== "object")
308
+ return null;
309
+ const o = r;
310
+ if (o.success === false || (typeof o.error === "string" && o.error !== ""))
311
+ return String(o.error ?? "request rejected");
312
+ return null;
313
+ }
314
+ /** /info rows must carry instrument_type — a row without it cannot be classified perp vs spot. */
315
+ function requireInstrumentType(rows) {
316
+ for (const m of rows) {
317
+ if (typeof m.instrument_type !== "string") {
318
+ throw new PerpError("EXCHANGE_ERROR", `Pacifica /info row ${String(m.symbol)} has no instrument_type`, { exchange: "pacifica" });
319
+ }
320
+ }
321
+ return rows;
322
+ }
323
+ /** Perp markets of /info (spot rows such as SOL-USDC excluded, design §12). */
324
+ function perpInfoRows(rows) {
325
+ return requireInstrumentType(rows).filter((m) => m.instrument_type !== "spot");
326
+ }
327
+ /** Wire symbols of /info spot markets. */
328
+ function spotInfoSymbols(rows) {
329
+ return new Set(requireInstrumentType(rows).filter((m) => m.instrument_type === "spot").map((m) => m.symbol));
330
+ }
331
+ /** Pacifica order_type of a stop / TP / SL order (stop_market, stop_limit, take_profit_*, stop_loss_*). */
332
+ export function isPacificaStopType(type) {
333
+ return /^(stop|take_profit|stop_loss)_/.test(type);
334
+ }
5
335
  export class PacificaAdapter {
6
336
  name = "pacifica";
7
337
  chain = "solana";
@@ -26,18 +356,35 @@ export class PacificaAdapter {
26
356
  _marketsCache = null;
27
357
  _marketsCacheTime = 0;
28
358
  static CACHE_TTL = 30_000;
29
- constructor(keypair, network = "mainnet", builderCode, hasRealKey = true) {
359
+ /** Venue network — part of every account cache key (the same key exists on mainnet and testnet). */
360
+ _network;
361
+ /**
362
+ * Builder-fee gate (settings builder.pacifica + confirmed approval); null = not configured.
363
+ * Set by createPacifica (venues/create-helpers.ts) — the adapter never reads settings.
364
+ */
365
+ builderFee = null;
366
+ constructor(keypair, network = "mainnet", hasRealKey = true) {
367
+ this._network = network;
30
368
  this._solanaSigner = new LocalSolanaSigner(keypair);
31
369
  this._hasRealKey = hasRealKey;
32
370
  this.account = this._solanaSigner.getPublicKeyBase58();
33
- this.client = new PacificaClient({ network, builderCode });
371
+ this.client = new PacificaClient({ network });
34
372
  // Hot-path methods always go through `_resolveSigner()` so the bound
35
373
  // `signMessage` reflects the highest-priority active tier.
36
374
  this.signMessage = (msg) => this._resolveSigner().signer.signMessage(msg);
37
375
  }
38
376
  ensureSigner() {
39
377
  // Resolves across all tiers; throws PerpError if none available.
40
- this._resolveSigner();
378
+ const r = this._resolveSigner();
379
+ // Sync the client's agent_wallet for the active tier. Agent-signed requests
380
+ // (account = master, signature = agent key) MUST carry agent_wallet, or
381
+ // Pacifica verifies the signature against the master key and rejects every
382
+ // order. Master/PK signers clear it so they self-sign as before.
383
+ this.client.setRequestAgentWallet(r.tier === "agent" ? r.address : undefined);
384
+ }
385
+ /** Resolve the active signer tier and sync agent_wallet on the client (used by PacificaSpotAdapter). */
386
+ prepareSigning() {
387
+ this.ensureSigner();
41
388
  }
42
389
  /**
43
390
  * Inject an external Solana signer (Tier 2 — OWS master).
@@ -83,20 +430,16 @@ export class PacificaAdapter {
83
430
  }
84
431
  /** Resolve the active signer across three tiers. Mirrors AsterAdapter._resolveSigner. */
85
432
  _resolveSigner() {
86
- // Tier 1: agent (when registered, not expired, --no-agent NOT set)
433
+ // Tier 1: agent (when registered, --no-agent NOT set). An expired agent record is an
434
+ // explicit error — never a silent switch to the master signer; --no-agent is the opt-in.
87
435
  if (!this._useNoAgent && this._agentSigner && this._agentMeta) {
88
436
  if (isExpired(this._agentMeta)) {
89
- // Skip Tier 1 only if Tier 2/3 available; otherwise throw AGENT_EXPIRED
90
- if (!this._masterSigner && !this._hasRealKey) {
91
- throw new PerpError("AGENT_EXPIRED", "Agent wallet has expired", {
92
- remediation: "perp wallet agent approve pacifica --rotate",
93
- });
94
- }
95
- // fall through to Tier 2/3
96
- }
97
- else {
98
- return { tier: "agent", signer: this._agentSigner, address: this._agentSigner.getPublicKeyBase58() };
437
+ throw new PerpError("AGENT_EXPIRED", `Pacifica agent wallet ${this._agentMeta.agentName} expired at ${this._agentMeta.expiresAt}`, {
438
+ exchange: "pacifica",
439
+ remediation: "Re-approve the agent: perp wallet agent approve pacifica — or sign with the master wallet for this command: --no-agent",
440
+ });
99
441
  }
442
+ return { tier: "agent", signer: this._agentSigner, address: this._agentSigner.getPublicKeyBase58() };
100
443
  }
101
444
  // Tier 2: OWS master
102
445
  if (this._masterSigner) {
@@ -118,6 +461,19 @@ export class PacificaAdapter {
118
461
  remediation: "Run one of: (a) perp wallet agent approve pacifica --master <wallet>; (b) perp wallet generate && perp -e pacifica --ows <wallet>; (c) export PACIFICA_PRIVATE_KEY=...",
119
462
  });
120
463
  }
464
+ /**
465
+ * The owner (master) Solana signer for on-chain transactions (deposit, test-token
466
+ * mint) — never the agent, which cannot move funds.
467
+ */
468
+ masterSolanaSigner() {
469
+ if (this._masterSigner)
470
+ return this._masterSigner;
471
+ if (this._hasRealKey && this._solanaSigner)
472
+ return this._solanaSigner;
473
+ throw new PerpError("NO_SIGNER_AVAILABLE", "No Pacifica master wallet on this computer for an on-chain transaction", {
474
+ remediation: "Select the master wallet (--ows <wallet> or perp wallet use <wallet>), or do it on the Pacifica website",
475
+ });
476
+ }
121
477
  /** Access the underlying Keypair (only available with LocalSolanaSigner). */
122
478
  get keypair() {
123
479
  if (this._solanaSigner instanceof LocalSolanaSigner) {
@@ -127,15 +483,44 @@ export class PacificaAdapter {
127
483
  }
128
484
  async _getPrices() {
129
485
  const { withCache, TTL_ACCOUNT } = await import("../cache.js");
130
- return withCache(`acct:pac:prices:${this.account.slice(0, 8)}`, TTL_ACCOUNT, () => this.client.getPrices());
486
+ return withCache(`acct:pac:${this._network}:prices:${this.account}`, TTL_ACCOUNT, () => this.client.getPrices());
131
487
  }
132
488
  async _getPositions() {
133
489
  const { withCache, TTL_ACCOUNT } = await import("../cache.js");
134
- return withCache(`acct:pac:positions:${this.account.slice(0, 8)}`, TTL_ACCOUNT, () => this.client.getPositions(this.account));
490
+ return withCache(`acct:pac:${this._network}:positions:${this.account}`, TTL_ACCOUNT, () => this.client.getPositions(this.account));
135
491
  }
136
492
  get publicKey() {
137
493
  return this.account;
138
494
  }
495
+ /** Builder decision for the next perp order (cached approval read; never throws). */
496
+ async builderDecision() {
497
+ const gate = builderGateOf(this);
498
+ return gate === null ? NO_BUILDER : gate.decide({ hip3: false });
499
+ }
500
+ /** builder_code to send with the next perp order, or null (not configured / not approved). */
501
+ async builderCode() {
502
+ const d = await this.builderDecision();
503
+ return d.applied === null ? null : d.applied.identity;
504
+ }
505
+ /**
506
+ * Approval of `target.identity` on this account (GET /account/builder_codes/approvals).
507
+ * Pacifica's builder sets its own rate up to the approved max_fee_rate, so the code is
508
+ * attached only while that max ≤ target.feeBps (the user's consent); a higher approved max
509
+ * is "aboveConsent" (not attached, real max reported).
510
+ */
511
+ async builderApprovalOf(target) {
512
+ const rows = await this.client.getBuilderApprovals(this.account);
513
+ if (!Array.isArray(rows)) {
514
+ return { status: "unknown", reason: `unexpected approvals response ${JSON.stringify(rows).slice(0, 120)}` };
515
+ }
516
+ const row = rows.find(r => r !== null && typeof r === "object" && r.builder_code === target.identity);
517
+ if (row === undefined)
518
+ return approvalFromCeiling(null, requireFeeBps(target));
519
+ const max = rateToBps(row.max_fee_rate);
520
+ if (max === null)
521
+ return { status: "unknown", reason: `unreadable max_fee_rate ${JSON.stringify(row.max_fee_rate)}` };
522
+ return approvalFromCeiling(max, requireFeeBps(target));
523
+ }
139
524
  get sdk() {
140
525
  return this.client;
141
526
  }
@@ -150,21 +535,26 @@ export class PacificaAdapter {
150
535
  this._getPrices(),
151
536
  ]);
152
537
  const priceMap = new Map(prices.map((p) => [p.symbol, p]));
153
- const result = markets.map((m) => {
538
+ // /info also lists spot markets (e.g. SOL-USDC); those are not perps (§12).
539
+ const result = perpInfoRows(markets).map((m) => {
154
540
  const p = priceMap.get(m.symbol);
155
541
  // Derive sizeDecimals from lot_size (e.g. "0.001" → 3, "1" → 0)
156
- const lotSize = parseFloat(m.lot_size ?? "0");
157
- const sizeDecimals = lotSize > 0 ? Math.max(0, -Math.floor(Math.log10(lotSize))) : undefined;
542
+ const lotSize = m.lot_size != null ? parseFloat(m.lot_size) : null;
543
+ const validLot = lotSize !== null && Number.isFinite(lotSize) && lotSize > 0;
544
+ const sizeDecimals = validLot ? Math.max(0, -Math.floor(Math.log10(lotSize))) : null;
158
545
  return {
159
546
  symbol: m.symbol,
160
547
  markPrice: p?.mark ?? "-",
161
- indexPrice: p?.oracle ?? "-",
548
+ indexPrice: p?.oracle ?? null,
162
549
  fundingRate: m.next_funding_rate ?? m.funding_rate,
163
- volume24h: p?.volume_24h ?? "-",
164
- openInterest: p?.open_interest ?? "-",
550
+ volume24h: p?.volume_24h ?? null,
551
+ openInterest: p?.open_interest ?? null,
165
552
  maxLeverage: m.max_leverage,
166
553
  sizeDecimals,
167
- stepSize: lotSize > 0 ? m.lot_size : undefined,
554
+ stepSize: validLot ? m.lot_size : null,
555
+ // Pacifica publishes neither a per-market funding interval nor margin tiers.
556
+ fundingHours: null,
557
+ marginTiers: null,
168
558
  };
169
559
  });
170
560
  this._marketsCache = result;
@@ -191,22 +581,34 @@ export class PacificaAdapter {
191
581
  // API often returns unrealized_pnl=0 — compute from price delta when needed
192
582
  let totalPnl = 0;
193
583
  for (const p of positions) {
194
- let upnl = Number(p.unrealized_pnl ?? 0);
195
- if (upnl === 0 && Number(p.amount) > 0) {
196
- const mark = Number(priceMap.get(p.symbol)?.mark ?? p.mark_price ?? 0);
197
- const entry = Number(p.entry_price);
584
+ let upnl = parseFiniteVenueNumber(p.unrealized_pnl, "position.unrealized_pnl", "pacifica");
585
+ const amount = parseFiniteVenueNumber(p.amount, "position.amount", "pacifica");
586
+ if (upnl === 0 && amount > 0) {
587
+ const mark = parseFiniteVenueNumber(priceMap.get(p.symbol)?.mark ?? p.mark_price, "position.mark", "pacifica");
588
+ const entry = parseFiniteVenueNumber(p.entry_price, "position.entry_price", "pacifica");
198
589
  const dir = p.side === "bid" ? 1 : -1;
199
590
  if (mark > 0 && entry > 0)
200
- upnl = (mark - entry) * Number(p.amount) * dir;
591
+ upnl = (mark - entry) * amount * dir;
201
592
  }
202
593
  totalPnl += upnl;
203
594
  }
204
- return {
595
+ // Unified margin: a negative USDC `balance` is a loan; `pending_interest` is accrued,
596
+ // unpaid loan interest (docs get-account-info). Surfaced, not folded into equity.
597
+ const usdcBalance = info.balance;
598
+ if (typeof usdcBalance !== "string") {
599
+ throw new PerpError("EXCHANGE_ERROR", "Pacifica account info has no USDC `balance`", { exchange: "pacifica", venueRaw: raw });
600
+ }
601
+ const balance = {
205
602
  equity: info.account_equity,
206
603
  available: info.available_to_spend,
207
604
  marginUsed: String(raw.total_margin_used ?? raw.margin_used ?? "0"),
208
605
  unrealizedPnl: totalPnl.toFixed(4),
606
+ usdcBalance,
607
+ borrowedUsdc: usdcBalance.startsWith("-") ? usdcBalance.slice(1) : "0",
608
+ pendingInterest: typeof info.pending_interest === "string" ? info.pending_interest : null,
609
+ availableToWithdraw: info.available_to_withdraw,
209
610
  };
611
+ return balance;
210
612
  }
211
613
  async getPositions() {
212
614
  if (!this._hasRealKey)
@@ -216,45 +618,109 @@ export class PacificaAdapter {
216
618
  this._getPrices(),
217
619
  ]);
218
620
  const priceMap = new Map(prices.map((p) => [p.symbol, p]));
219
- let levMap = new Map();
220
- try {
221
- const settings = await this.client.getAccountSettings(this.account);
222
- if (Array.isArray(settings)) {
223
- levMap = new Map(settings.map((s) => [s.symbol, s.leverage]));
224
- }
225
- }
226
- catch {
227
- // Settings API may not be available
228
- }
229
- return positions.map((p) => {
621
+ const mapped = positions.map((p) => {
230
622
  const mark = priceMap.get(p.symbol)?.mark ?? p.mark_price ?? "0";
231
623
  const side = p.side === "bid" ? "long" : "short";
232
- const size = Number(p.amount);
233
- const entry = Number(p.entry_price);
234
- const markNum = Number(mark);
624
+ const size = parseFiniteVenueNumber(p.amount, "position.amount", "pacifica");
625
+ const entry = parseFiniteVenueNumber(p.entry_price, "position.entry_price", "pacifica");
626
+ const markNum = parseFiniteVenueNumber(mark, "position.mark", "pacifica");
235
627
  // API often returns unrealized_pnl=0 — compute from price delta when needed
236
- let upnl = Number(p.unrealized_pnl ?? 0);
628
+ let upnl = parseFiniteVenueNumber(p.unrealized_pnl, "position.unrealized_pnl", "pacifica");
237
629
  if (upnl === 0 && size > 0 && entry > 0 && markNum > 0) {
238
630
  const dir = side === "long" ? 1 : -1;
239
631
  upnl = (markNum - entry) * size * dir;
240
632
  }
633
+ // Leverage, in order: (1) the API's own `leverage` when present; (2) for an ISOLATED
634
+ // position, entry-notional / margin — the docs define `margin` as "Amount of margin
635
+ // allocated to an isolated position (only shown when isolated)", so a cross position
636
+ // reports "0" here; (3)/(4) resolved below from GET /account/settings, then the market max.
637
+ const marginNum = parseFiniteVenueNumber(p.margin ?? p.margin_used, "position.margin", "pacifica", { defaultValue: 0 });
638
+ const apiLev = typeof p.leverage === "number" && p.leverage > 0 ? p.leverage : 0;
639
+ const entryNotional = size * entry;
640
+ const derivedLev = marginNum > 0 && entryNotional > 0 ? Math.round(entryNotional / marginNum) : 0;
241
641
  return {
242
642
  symbol: p.symbol,
243
643
  side: side,
244
644
  size: String(p.amount),
245
645
  entryPrice: String(p.entry_price),
246
646
  markPrice: mark,
247
- liquidationPrice: String(p.liquidation_price ?? "N/A"),
647
+ liquidationPrice: p.liquidation_price != null ? String(p.liquidation_price) : null,
248
648
  unrealizedPnl: upnl.toFixed(4),
249
- leverage: p.leverage ?? levMap.get(String(p.symbol)) ?? 1,
649
+ // 0 = unresolved; resolved from the account settings / market max below.
650
+ leverage: apiLev > 0 ? apiLev : derivedLev,
250
651
  };
251
652
  });
653
+ // Unresolved (cross) positions: (3) the symbol's entry in GET /account/settings
654
+ // `margin_settings` — "Current leverage set by the user"; (4) no entry = the symbol is at its
655
+ // default, and the docs (get-account-settings) say "all markets have margin settings default
656
+ // to cross margin and leverage default to max ... will return blank" → the market's max
657
+ // leverage. Neither known → EXCHANGE_ERROR (never a fabricated value). Both reads happen ONLY
658
+ // when something is unresolved; a failure of either propagates.
659
+ if (mapped.some((m) => m.leverage <= 0)) {
660
+ const settings = await this.client.getAccountSettings(this.account);
661
+ if (settings === null || typeof settings !== "object" || !Array.isArray(settings.margin_settings)) {
662
+ throw new PerpError("EXCHANGE_ERROR", `Pacifica /account/settings returned no margin_settings list (got ${JSON.stringify(settings).slice(0, 160)})`, { exchange: "pacifica" });
663
+ }
664
+ const setLev = new Map();
665
+ for (const s of settings.margin_settings) {
666
+ if (typeof s?.symbol !== "string" || typeof s.leverage !== "number" || !(s.leverage > 0)) {
667
+ throw new PerpError("EXCHANGE_ERROR", `Pacifica /account/settings: malformed margin_settings entry ${JSON.stringify(s)}`, { exchange: "pacifica" });
668
+ }
669
+ setLev.set(s.symbol, s.leverage);
670
+ }
671
+ let maxLevMap = null;
672
+ for (const m of mapped) {
673
+ if (m.leverage > 0)
674
+ continue;
675
+ const configured = setLev.get(m.symbol);
676
+ if (configured !== undefined) {
677
+ m.leverage = configured;
678
+ continue;
679
+ }
680
+ if (maxLevMap === null)
681
+ maxLevMap = new Map((await this.getMarkets()).map((mk) => [mk.symbol, mk.maxLeverage]));
682
+ const max = maxLevMap.get(m.symbol);
683
+ if (typeof max !== "number" || !(max > 0)) {
684
+ throw new PerpError("EXCHANGE_ERROR", `Pacifica ${m.symbol}: the position's leverage cannot be determined (no leverage on the position, no /account/settings entry, and no max leverage for the market)`, {
685
+ exchange: "pacifica", remediation: "Check the market: perp -e pacifica market info " + m.symbol,
686
+ });
687
+ }
688
+ m.leverage = max;
689
+ }
690
+ }
691
+ return mapped;
692
+ }
693
+ /**
694
+ * The leverage this account applies to `symbol`: its GET /account/settings `margin_settings`
695
+ * entry ("Current leverage set by the user"); no entry → the market's max leverage (docs
696
+ * get-account-settings: "leverage default to max ... will return blank"). Unreadable → EXCHANGE_ERROR.
697
+ */
698
+ async marketLeverage(symbol) {
699
+ const sym = symbol.toUpperCase();
700
+ const settings = await this.client.getAccountSettings(this.account);
701
+ if (settings === null || typeof settings !== "object" || !Array.isArray(settings.margin_settings)) {
702
+ throw new PerpError("EXCHANGE_ERROR", `Pacifica /account/settings returned no margin_settings list (got ${JSON.stringify(settings).slice(0, 160)})`, { exchange: "pacifica" });
703
+ }
704
+ const entry = settings.margin_settings.find(e => typeof e?.symbol === "string" && e.symbol.toUpperCase() === sym);
705
+ if (entry !== undefined) {
706
+ if (typeof entry.leverage !== "number" || !(entry.leverage >= 1)) {
707
+ throw new PerpError("EXCHANGE_ERROR", `Pacifica /account/settings: malformed margin_settings entry ${JSON.stringify(entry)}`, { exchange: "pacifica" });
708
+ }
709
+ return entry.leverage;
710
+ }
711
+ const max = (await this.getMarkets()).find(m => m.symbol.toUpperCase() === sym)?.maxLeverage;
712
+ if (typeof max !== "number" || !(max >= 1)) {
713
+ throw new PerpError("EXCHANGE_ERROR", `Pacifica ${sym}: no /account/settings entry and no max leverage for the market`, { exchange: "pacifica" });
714
+ }
715
+ return max;
252
716
  }
253
717
  async getOpenOrders() {
254
718
  if (!this._hasRealKey)
255
719
  throw new Error("No private key configured — account data unavailable. Run: perp setup");
256
- const orders = await this.client.getOrders(this.account);
257
- return orders.map((o) => {
720
+ // GET /orders covers every market; spot orders (SOL-USDC) belong to the spot adapter.
721
+ const [orders, info] = await Promise.all([this.client.getOrders(this.account), this.client.getInfo()]);
722
+ const spotSymbols = spotInfoSymbols(info);
723
+ return orders.filter((o) => !spotSymbols.has(String(o.symbol))).map((o) => {
258
724
  const raw = o;
259
725
  return {
260
726
  orderId: String(raw.order_id),
@@ -265,36 +731,65 @@ export class PacificaAdapter {
265
731
  filled: String(raw.filled_amount ?? raw.filled ?? "0"),
266
732
  status: String(raw.order_status ?? "open"),
267
733
  type: String(raw.order_type ?? ""),
734
+ // Stop / TP / SL orders: stop_price is the trigger (a position-level TP/SL lists price 0, amount 0).
735
+ triggerPrice: raw.stop_price !== null && raw.stop_price !== undefined && Number(raw.stop_price) > 0 ? String(raw.stop_price) : null,
268
736
  };
269
737
  });
270
738
  }
271
739
  async marketOrder(symbol, side, size, opts) {
272
740
  this.ensureSigner();
273
- const result = await this.client.createMarketOrder({ symbol, amount: size, side: side === "buy" ? "bid" : "ask", reduce_only: opts?.reduceOnly ?? false, slippage_percent: "1" }, this.account, this.signMessage);
741
+ // A caller's client order id is sent as client_order_id (validated UUID); none → none sent.
742
+ const cloid = opts.clientOrderId === undefined ? null : requirePacificaClientOrderId(opts.clientOrderId, null);
743
+ const created = await this.client.createMarketOrder({
744
+ symbol, amount: size, side: side === "buy" ? "bid" : "ask", reduce_only: opts.reduceOnly, slippage_percent: String(opts.slippage.bps / 100),
745
+ ...(cloid !== null ? { client_order_id: cloid } : {}),
746
+ }, this.account, this.signMessage, await this.builderCode()).catch((e) => { throw pacificaCreateRejection(e, `market ${side} ${size} ${symbol}`); });
747
+ const result = withSentClientOrderId(created, cloid);
274
748
  // Validate response
275
749
  const r = result;
276
750
  const data = (r.data ?? r);
277
751
  if (r.success === false || r.error) {
278
- throw new Error(`Market ${side} ${symbol}: ${r.error ?? 'order rejected'}`);
752
+ // Explicit venue rejection in the response body: definitely not placed.
753
+ throw new PerpError("ORDER_REJECTED", `Market ${side} ${symbol}: ${r.error ?? 'order rejected'}`, { exchange: "pacifica" });
279
754
  }
280
755
  // Get order_id from response
281
756
  const orderId = data.order_id ? String(data.order_id) : null;
282
757
  if (orderId && !(opts?.reduceOnly ?? false)) {
283
758
  // Poll order status via history_by_id
284
759
  let lastPollError = null;
760
+ let verdict = null;
285
761
  for (let attempt = 0; attempt < 10; attempt++) {
286
762
  await new Promise(r => setTimeout(r, 500));
287
763
  try {
288
764
  const history = await this.client.getOrderHistoryById(Number(orderId));
289
- const histData = (history.data ?? history);
290
- if (Array.isArray(histData) && histData.length > 0) {
291
- const latest = histData[histData.length - 1];
765
+ const histData = history.data ?? history;
766
+ const latest = pacificaLatestHistoryRow(histData, orderId);
767
+ if (latest !== null) {
292
768
  const status = String(latest.order_status ?? "");
293
769
  const filled = parseFloat(String(latest.filled_amount ?? "0"));
294
- if (status === "filled" || filled > 0)
295
- return result; // confirmed fill
770
+ if (status === "filled" || filled > 0) {
771
+ // Terminal row (filled / cancelled remainder) with a reported filled_amount = the
772
+ // order's final fill (design §13.10 #4). A non-terminal partial row is not final.
773
+ const reported = latest.filled_amount;
774
+ const terminal = status === "filled" || status === "cancelled" || status === "rejected";
775
+ if (terminal && reported !== undefined && reported !== null && String(reported).trim() !== ""
776
+ && Number.isFinite(Number(reported)) && result !== null && typeof result === "object") {
777
+ // The terminal row rides along with the raw result (read back by iocOutcomeOf).
778
+ const fillReport = { orderId, orderStatus: status, filledAmount: String(reported).trim() };
779
+ return { ...result, fillReport };
780
+ }
781
+ return result; // confirmed fill (no final report)
782
+ }
296
783
  if (status === "cancelled" || status === "rejected") {
297
- throw new Error(`Market ${side} ${symbol}: order ${orderId} ${status}`);
784
+ // Definite no-fill ONLY when the venue reports filled_amount and it is 0. A
785
+ // terminal row without filled_amount may be a partially-filled IOC whose
786
+ // remainder was cancelled — leave it untyped so the caller measures the fill.
787
+ if (latest.filled_amount !== undefined && latest.filled_amount !== null && String(latest.filled_amount).trim() !== "" && Number(latest.filled_amount) === 0) {
788
+ throw new PerpError("ORDER_REJECTED", `Market ${side} ${symbol}: order ${orderId} ${status}`, { exchange: "pacifica", orderId });
789
+ }
790
+ // Sent and terminal, but the fill amount is not reported: a partially filled IOC
791
+ // whose remainder was cancelled looks the same — outcome unknown, never resubmit.
792
+ verdict = orderOutcomeUnknown("pacifica", `market ${side} ${size} ${symbol}`, new Error(`order ${orderId} ${status} without a reported filled_amount`), `Do NOT resubmit — order ${orderId} may have partially filled. Check positions and fills first: perp -e pacifica account positions / trades`);
298
793
  }
299
794
  // Still processing, retry
300
795
  }
@@ -304,42 +799,442 @@ export class PacificaAdapter {
304
799
  throw e;
305
800
  lastPollError = e;
306
801
  }
802
+ if (verdict !== null)
803
+ throw verdict;
307
804
  }
308
- // Order was accepted but verification timed out — warn, don't throw
805
+ // Accepted but not confirmed within the poll window: returned without a fill report, so the
806
+ // caller (iocOutcomeOf → null) reports it unconfirmed — never assumed executed.
309
807
  const errDetail = lastPollError instanceof Error ? ` (poll error: ${lastPollError.message})` : "";
310
- console.error(`[pacifica] Warning: market ${side} ${symbol} order ${orderId} not confirmed after 5s${errDetail} — assuming executed`);
808
+ console.error(`[pacifica] Warning: market ${side} ${symbol} order ${orderId} not confirmed after 5s${errDetail} — outcome unconfirmed; check positions before retrying: perp -e pacifica account positions`);
311
809
  }
312
- if (!(opts?.reduceOnly ?? false)) {
810
+ if (!orderId && !(opts?.reduceOnly ?? false)) {
313
811
  console.error(`[pacifica] Warning: market ${side} ${symbol} accepted but no order_id in response — skipping fill verification`);
314
812
  }
315
813
  return result;
316
814
  }
815
+ /**
816
+ * Venue-final outcome of a raw marketOrder() result (IocOutcomeCapable, design §13.10 #4),
817
+ * parsed from the result's content (so copies of it work): its `fillReport` — the terminal
818
+ * history_by_id row (filled / cancelled / rejected) with a reported filled_amount — for the
819
+ * same order id as the create response. null otherwise — reduce-only orders (not polled),
820
+ * a poll that timed out or saw only non-terminal rows, any other raw value. Never guesses.
821
+ */
822
+ iocOutcomeOf(raw) {
823
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
824
+ return null;
825
+ const r = raw;
826
+ const fr = r.fillReport;
827
+ if (fr === null || typeof fr !== "object")
828
+ return null;
829
+ const data = (r.data !== null && typeof r.data === "object" ? r.data : r);
830
+ const createdId = typeof data.order_id === "number" || typeof data.order_id === "string" ? String(data.order_id) : null;
831
+ if (createdId === null || fr.orderId !== createdId)
832
+ return null;
833
+ if (fr.orderStatus !== "filled" && fr.orderStatus !== "cancelled" && fr.orderStatus !== "rejected")
834
+ return null;
835
+ const filled = fr.filledAmount;
836
+ if (typeof filled !== "string" || !/^\d+(\.\d+)?$/.test(filled))
837
+ return null;
838
+ return { filledSize: filled, final: true };
839
+ }
840
+ /**
841
+ * Venue-final outcome of a placed / edited order: polls `/orders/history_by_id` (bounded,
842
+ * 6 × 500 ms) until pacificaOrderOutcomeOf proves one. null = still unknown (the caller
843
+ * reports pending). Read failures poll again; the last one is logged when nothing resolved.
844
+ */
845
+ async resolveOrderOutcome(orderId, what, opts = {}) {
846
+ let lastErr = null;
847
+ for (let attempt = 0; attempt < 6; attempt++) {
848
+ await new Promise((r) => setTimeout(r, 500));
849
+ try {
850
+ const history = await this.client.getOrderHistoryById(Number(orderId));
851
+ const out = pacificaOrderOutcomeOf(history.data ?? history, orderId, opts);
852
+ if (out !== null)
853
+ return out;
854
+ }
855
+ catch (e) {
856
+ lastErr = extractErrorMessage(e);
857
+ }
858
+ }
859
+ console.error(`[pacifica] Warning: ${what}: order ${orderId} outcome not confirmed${lastErr !== null ? ` (last read error: ${lastErr})` : ""} — verify with: perp account orders -e pacifica`);
860
+ return null;
861
+ }
862
+ /** The raw create result with its resolved `orderOutcome` attached (unchanged when there is no order id / a body error / no outcome). */
863
+ async withOrderOutcome(raw, what, opts) {
864
+ const id = pacificaCreatedId(raw);
865
+ if (id === null || pacificaBodyError(raw) !== null || raw === null || typeof raw !== "object")
866
+ return raw;
867
+ const orderOutcome = await this.resolveOrderOutcome(id, what, opts);
868
+ return orderOutcome === null ? raw : { ...raw, orderOutcome };
869
+ }
870
+ /**
871
+ * Limit order. The create response is only an ack ({ order_id }); the placement outcome
872
+ * (resting / filled / cancelled / rejected) is read back from history_by_id and attached as
873
+ * `orderOutcome` (IOC: terminal rows only). Unresolved → no `orderOutcome` (pending).
874
+ */
317
875
  async limitOrder(symbol, side, price, size, opts) {
318
876
  this.ensureSigner();
319
- return this.client.createLimitOrder({ symbol, price, amount: size, side: side === "buy" ? "bid" : "ask", reduce_only: opts?.reduceOnly ?? false, tif: (opts?.tif ?? "GTC") }, this.account, this.signMessage);
877
+ const tif = pacificaTif(opts?.tif); // §13.3 common mapping; FOK → CAPABILITY_UNSUPPORTED
878
+ // The caller's client order id (validated UUID) is sent as given; without one a generated
879
+ // client_order_id records the TIF for a later TIF-preserving amend. Either way the id sent
880
+ // rides on the result (`clientOrderId`).
881
+ const cloid = opts?.clientOrderId !== undefined ? requirePacificaClientOrderId(opts.clientOrderId, tif) : pacificaClientOrderId(tif);
882
+ const raw = await this.client.createLimitOrder({ symbol, price, amount: size, side: side === "buy" ? "bid" : "ask", reduce_only: opts?.reduceOnly ?? false, tif, client_order_id: cloid }, this.account, this.signMessage, await this.builderCode()).catch((e) => { throw pacificaCreateRejection(e, `limit ${tif} ${side} ${size} ${symbol} @ ${price}`); });
883
+ return withSentClientOrderId(await this.withOrderOutcome(raw, `limit ${tif} ${side} ${size} ${symbol} @ ${price}`, { terminalOnly: tif === "IOC", expect: { price, size } }), cloid);
320
884
  }
321
885
  async cancelOrder(symbol, orderId) {
322
886
  this.ensureSigner();
323
887
  return this.client.cancelOrder({ symbol, order_id: Number(orderId) }, this.account, this.signMessage);
324
888
  }
889
+ /**
890
+ * The account's order carrying `clientOrderId`, from the venue's own listings — both document
891
+ * the field: GET /orders and GET /orders/history rows carry `client_order_id` ("CLOID of order
892
+ * if assigned by user", docs.pacifica.fi rest-api orders/get-open-orders, get-order-history).
893
+ * Open orders first, then the latest order-history page (the venue's default page, 100 rows).
894
+ * null = not among them. history_by_id itself takes only `order_id`, so this resolves the id.
895
+ */
896
+ async orderByClientId(clientOrderId) {
897
+ if (!this._hasRealKey)
898
+ throw new Error("No private key configured — account data unavailable. Run: perp setup");
899
+ const wanted = requirePacificaClientOrderId(clientOrderId, pacificaTifOf(clientOrderId.toLowerCase())).toLowerCase();
900
+ const pick = (rows, open) => {
901
+ if (!Array.isArray(rows)) {
902
+ throw new PerpError("EXCHANGE_ERROR", `Pacifica ${open ? "/orders" : "/orders/history"} returned no order list`, { exchange: "pacifica" });
903
+ }
904
+ const row = rows.find((o) => typeof o?.client_order_id === "string" && o.client_order_id.toLowerCase() === wanted);
905
+ if (row === undefined)
906
+ return null;
907
+ if (typeof row.order_id !== "number" && typeof row.order_id !== "string") {
908
+ throw new PerpError("EXCHANGE_ERROR", `Pacifica order with client id ${clientOrderId} has no order_id`, { exchange: "pacifica" });
909
+ }
910
+ return { orderId: String(row.order_id), symbol: String(row.symbol), type: String(row.order_type), open };
911
+ };
912
+ const open = pick(await this.client.getOrders(this.account), true);
913
+ if (open !== null)
914
+ return open;
915
+ return pick(await this.client.getOrderHistory(this.account), false);
916
+ }
917
+ /**
918
+ * Venue-final state of one order from a single `/orders/history_by_id` read (`trade status`).
919
+ * null outcome = the rows prove no state yet (reported pending). A 404 → ORDER_NOT_FOUND.
920
+ */
921
+ async orderStatusById(orderId) {
922
+ if (!/^\d+$/.test(orderId))
923
+ throw new PerpError("INVALID_PARAMS", `Pacifica order id must be numeric (got "${orderId}")`);
924
+ let history;
925
+ try {
926
+ history = await this.client.getOrderHistoryById(Number(orderId));
927
+ }
928
+ catch (e) {
929
+ if (/^GET \S*history_by_id failed \(404\)/.test(extractErrorMessage(e))) {
930
+ throw new PerpError("ORDER_NOT_FOUND", `Pacifica order ${orderId} not found`, { exchange: "pacifica" });
931
+ }
932
+ throw e;
933
+ }
934
+ const rows = (history !== null && typeof history === "object" && !Array.isArray(history) && "data" in history) ? history.data : history;
935
+ const row = pacificaLatestHistoryRow(rows, orderId);
936
+ if (row === null && (!Array.isArray(rows) || rows.length === 0)) {
937
+ throw new PerpError("ORDER_NOT_FOUND", `Pacifica order ${orderId} not found`, { exchange: "pacifica" });
938
+ }
939
+ return {
940
+ outcome: pacificaOrderOutcomeOf(rows, orderId),
941
+ clientOrderId: row !== null && typeof row.client_order_id === "string" && row.client_order_id !== "" ? row.client_order_id : null,
942
+ venueRaw: rows,
943
+ };
944
+ }
945
+ /** A refused cancel of `orderId` (pacificaRefused error) settled from one history_by_id read (pacificaCancelRefusalOf). */
946
+ async cancelRefusalOutcome(orderId, refusal) {
947
+ const msg = extractErrorMessage(refusal);
948
+ let history;
949
+ try {
950
+ const h = await this.client.getOrderHistoryById(Number(orderId));
951
+ history = { data: h?.data ?? h };
952
+ }
953
+ catch (e) {
954
+ history = { error: e };
955
+ }
956
+ return pacificaCancelRefusalOf(orderId, msg, history);
957
+ }
325
958
  async cancelAllOrders(symbol) {
326
959
  this.ensureSigner();
327
- if (symbol) {
328
- // Cancel only orders for this symbol: fetch open orders, filter, cancel individually
329
- const orders = await this.getOpenOrders();
330
- const filtered = orders.filter(o => o.symbol.toUpperCase() === symbol.toUpperCase());
331
- const results = [];
332
- for (const o of filtered) {
333
- results.push(await this.cancelOrder(o.symbol, o.orderId));
960
+ // Never `cancel_all` with all_symbols:true — it would also cancel spot orders.
961
+ // getOpenOrders() already excludes spot symbols; cancel the perp orders one by one.
962
+ let orders;
963
+ try { // pre-send: the open-orders listing — nothing is cancelled when it fails
964
+ orders = await this.getOpenOrders();
965
+ }
966
+ catch (e) {
967
+ throw orderNotSent("pacifica", `cancel-all${symbol ? ` ${symbol.toUpperCase()}` : ""}`, e);
968
+ }
969
+ const targets = symbol ? orders.filter(o => o.symbol.toUpperCase() === symbol.toUpperCase()) : orders;
970
+ // Every target is attempted (like lighter.ts cancelAllOrders): one refused / unknown cancel
971
+ // must not leave the orders after it untouched and unreported. All cancelled → the raw
972
+ // results; any failure → one error carrying the per-order outcome.
973
+ const results = [];
974
+ const perOrder = [];
975
+ for (const o of targets) {
976
+ try {
977
+ const r = await this._cancelListed(o);
978
+ const bodyErr = pacificaBodyError(r);
979
+ if (bodyErr !== null)
980
+ throw new PerpError("ORDER_REJECTED", `Pacifica cancel of ${o.orderId} refused: ${bodyErr}`, { exchange: "pacifica" });
981
+ results.push(r);
982
+ perOrder.push({ orderId: o.orderId, symbol: o.symbol, cancelled: true, definite: true, error: null });
983
+ }
984
+ catch (e) {
985
+ // A 4xx refusal / explicit body refusal / pre-send refusal is definite (that order was not
986
+ // cancelled by this request); a transport failure, 5xx or anything else leaves it unknown.
987
+ const definite = pacificaRefused(e) || isDefiniteOrderFailure(e);
988
+ perOrder.push({ orderId: o.orderId, symbol: o.symbol, cancelled: false, definite, error: extractErrorMessage(e) });
334
989
  }
335
- return results;
336
990
  }
337
- return this.client.cancelAllOrders({ all_symbols: true, exclude_reduce_only: false }, this.account, this.signMessage);
991
+ const failed = perOrder.filter(r => !r.cancelled);
992
+ if (failed.length === 0)
993
+ return results;
994
+ const cancelled = perOrder.length - failed.length;
995
+ const code = cancelled > 0 ? "ORDER_PARTIAL" : failed.every(r => r.definite) ? "ORDER_REJECTED" : "ORDER_PENDING";
996
+ throw new PerpError(code, `pacifica cancel-all: ${cancelled} of ${perOrder.length} order(s) cancelled; failed: ${failed.map(r => `${r.orderId} ${r.symbol} (${r.error})`).join("; ")}`, {
997
+ exchange: "pacifica",
998
+ results: perOrder,
999
+ remediation: "Check which orders are still open (perp account orders -e pacifica) and cancel the remaining ones individually",
1000
+ });
338
1001
  }
1002
+ /** Stop / TP / SL rows (incl. position-level TP/SL) go to /orders/stop/cancel; /orders/cancel refuses them (422). */
1003
+ _cancelListed(o) {
1004
+ return isPacificaStopType(o.type) ? this.cancelStopOrder(o.symbol, o.orderId) : this.cancelOrder(o.symbol, o.orderId);
1005
+ }
1006
+ /**
1007
+ * Native edit (POST /orders/edit). Venue semantics (docs rest-api/orders/edit-order,
1008
+ * read 2026-10-02): the edit cancels the original and creates a new order with the same
1009
+ * side, reduce-only and client_order_id, a new order_id, and TIF = ALO (post-only) —
1010
+ * whatever the original TIF was. Post-only is therefore always kept (an ALO original
1011
+ * stays ALO); a GTC / TOB original becomes ALO and is rejected instead of crossing.
1012
+ * The original TIF is not readable (GET /orders has no tif field), so the conversion
1013
+ * cannot be detected per order; callers report it (execution tifSent "ALO").
1014
+ */
339
1015
  async editOrder(symbol, orderId, price, size) {
340
1016
  this.ensureSigner();
341
1017
  return this.client.editOrder({ symbol, order_id: Number(orderId), price, amount: size }, this.account, this.signMessage);
342
1018
  }
1019
+ /**
1020
+ * Amend that keeps the order's TIF (design §13.4, §13.10 #10). `size` = new open size.
1021
+ * TIF = `tif` when the caller gives one, else the TIF encoded in the order's generated
1022
+ * client_order_id; unknown → uniform CAPABILITY_UNSUPPORTED (never guessed).
1023
+ * - ALO: native edit (the venue re-creates the order as ALO, keeping its client_order_id).
1024
+ * - GTC / TOB: cancel → read the fill at cancel → replace with
1025
+ * size − (filledAtCancel − filledAtRead) at the same TIF / side / reduce-only (≤ 0 or below
1026
+ * the lot → no replacement, reported). Cancel outcome unknown → ORDER_PENDING (no replace);
1027
+ * cancel ok but the fill at cancel unreadable or the replacement refused → ORDER_PARTIAL
1028
+ * { cancelled, placed: null }; replacement outcome unknown → ORDER_PENDING.
1029
+ */
1030
+ async amendKeepingTif(symbol, orderId, price, size, tif, forceTif = false) {
1031
+ this.ensureSigner();
1032
+ if (!/^\d+$/.test(orderId))
1033
+ throw new PerpError("INVALID_PARAMS", `Pacifica order id must be numeric (got "${orderId}")`);
1034
+ if (!(Number(price) > 0) || !(Number(size) > 0))
1035
+ throw new PerpError("INVALID_PARAMS", `Pacifica amend needs a positive price and size (got ${price} x ${size})`);
1036
+ const sym = symbol.toUpperCase();
1037
+ const open = await this.client.getOrders(this.account);
1038
+ const original = open.find((o) => String(o.order_id) === orderId);
1039
+ if (original === undefined) {
1040
+ throw new PerpError("ORDER_NOT_FOUND", `Pacifica order ${orderId} is not among the open orders`, { exchange: "pacifica" });
1041
+ }
1042
+ if (String(original.symbol).toUpperCase() !== sym) {
1043
+ throw new PerpError("INVALID_PARAMS", `Order ${orderId} is on ${original.symbol}, not ${sym}`);
1044
+ }
1045
+ if (original.order_type !== "limit") {
1046
+ throw unsupported("Pacifica", "amend", `only open limit orders can be amended (order is ${original.order_type})`, `Cancel it (perp trade cancel ${sym} ${orderId} -e pacifica) and place a new order`);
1047
+ }
1048
+ const encoded = pacificaTifOf(original.client_order_id);
1049
+ if (tif !== null && encoded !== null && (tif === "ALO") !== (encoded === "ALO") && !forceTif) {
1050
+ throw new PerpError("INVALID_PARAMS", `--tif ${tif} would make order ${orderId} ${encoded === "ALO" ? "no longer post-only" : "post-only"} (its client id encodes TIF ${encoded})`, {
1051
+ remediation: "Omit --tif to keep the order's own TIF, or pass --force-tif to change it",
1052
+ });
1053
+ }
1054
+ const keep = tif ?? encoded;
1055
+ if (keep === null) {
1056
+ throw unsupported("Pacifica", "amend", "original order attributes unknown", `pass --tif: perp trade edit ${sym} ${orderId} <price> <size> --tif <GTC|ALO|TOB> -e pacifica`);
1057
+ }
1058
+ const tifNote = tif !== null ? `TIF ${keep} (from --tif)` : `TIF ${keep} kept (from the order's client id)`;
1059
+ if (keep === "ALO") {
1060
+ const raw = await this.editOrder(sym, orderId, price, size);
1061
+ const err = pacificaBodyError(raw);
1062
+ if (err !== null)
1063
+ throw new PerpError("ORDER_REJECTED", `Pacifica edit of ${orderId} refused: ${err}`, { exchange: "pacifica" });
1064
+ const data = (raw !== null && typeof raw === "object" ? (raw.data ?? raw) : null);
1065
+ const newId = data !== null && (typeof data.order_id === "number" || typeof data.order_id === "string") ? String(data.order_id) : null;
1066
+ // The edited order (new id) read back with the new price / size verified.
1067
+ const outcome = newId === null ? null : await this.resolveOrderOutcome(newId, `edit of ${orderId}`, { expect: { price, size } });
1068
+ return {
1069
+ method: "nativeAmend", tif: "ALO", orderId: newId, clientOrderId: original.client_order_id ?? null,
1070
+ filledBeforeCancel: null, replacedSize: null, filledCompletely: false, outcome,
1071
+ notes: [tifNote, "native edit (the venue re-creates the order as ALO with a new order id)"], venueRaw: raw,
1072
+ };
1073
+ }
1074
+ if (keep === "IOC") {
1075
+ throw unsupported("Pacifica", "amend", "an IOC order never rests, so there is nothing to amend", "Place a new order");
1076
+ }
1077
+ // cancel → replace. Validate the replacement before cancelling (bad size/price never loses the original).
1078
+ const market = (await this.getMarkets()).find((m) => m.symbol.toUpperCase() === sym);
1079
+ if (market === undefined || market.stepSize === null) {
1080
+ throw new PerpError("SYMBOL_NOT_FOUND", `Pacifica ${sym}: no lot size published; cannot size a replacement`, { exchange: "pacifica" });
1081
+ }
1082
+ const lot = market.stepSize;
1083
+ floorSize(size, lot);
1084
+ // The replacement price must sit on the venue tick BEFORE the original is cancelled — an
1085
+ // off-tick price would be refused only after the original is already gone.
1086
+ const info = perpInfoRows(await this.client.getInfo()).find((m) => m.symbol.toUpperCase() === sym);
1087
+ if (info === undefined || typeof info.tick_size !== "string" || !(Number(info.tick_size) > 0)) {
1088
+ throw new PerpError("SYMBOL_NOT_FOUND", `Pacifica ${sym}: no tick size published; cannot validate the replacement price`, { exchange: "pacifica" });
1089
+ }
1090
+ requireOnTick(price, info.tick_size);
1091
+ // The replacement size is derived from filled_amount (size − fills raced in during the cancel):
1092
+ // an unreadable one is refused HERE, before the cancel — after it, the original would be gone
1093
+ // with no computable replacement.
1094
+ const filledAtReadRaw = original.filled_amount;
1095
+ if ((typeof filledAtReadRaw !== "string" && typeof filledAtReadRaw !== "number") || !/^\d+(\.\d+)?$/.test(String(filledAtReadRaw).trim())) {
1096
+ throw new PerpError("EXCHANGE_ERROR", `Pacifica order ${orderId} has no readable filled_amount (got ${JSON.stringify(filledAtReadRaw ?? null)}); nothing was cancelled`, {
1097
+ exchange: "pacifica", sent: false,
1098
+ remediation: `Retry the amend, or cancel (perp trade cancel ${sym} ${orderId} -e pacifica) and place a new order`,
1099
+ });
1100
+ }
1101
+ const filledAtRead = String(filledAtReadRaw).trim();
1102
+ const cancelWhat = `amend: cancel of ${orderId}`;
1103
+ let cancelRaw;
1104
+ try {
1105
+ cancelRaw = await this.client.cancelOrder({ symbol: sym, order_id: Number(orderId) }, this.account, this.signMessage);
1106
+ }
1107
+ catch (e) {
1108
+ if (pacificaRefused(e)) {
1109
+ throw new PerpError("ORDER_REJECTED", `Pacifica ${cancelWhat} refused (${extractErrorMessage(e)}); the original order is untouched, nothing replaced`, { exchange: "pacifica" });
1110
+ }
1111
+ throw orderOutcomeUnknown("pacifica", cancelWhat, e, `No replacement was placed. Check the original: perp account orders -e pacifica`);
1112
+ }
1113
+ const cancelErr = pacificaBodyError(cancelRaw);
1114
+ if (cancelErr !== null) {
1115
+ throw new PerpError("ORDER_REJECTED", `Pacifica ${cancelWhat} refused: ${cancelErr}; the original order is untouched, nothing replaced`, { exchange: "pacifica" });
1116
+ }
1117
+ // Fill at cancel: the order's terminal history_by_id row with a reported filled_amount.
1118
+ let filledAtCancel = null;
1119
+ let terminalStatus = null;
1120
+ let lastReadError = null;
1121
+ for (let attempt = 0; attempt < 6 && filledAtCancel === null; attempt++) {
1122
+ await new Promise((r) => setTimeout(r, 500));
1123
+ try {
1124
+ const history = await this.client.getOrderHistoryById(Number(orderId));
1125
+ const latest = pacificaLatestHistoryRow(history.data ?? history, orderId);
1126
+ const st = latest === null ? "" : String(latest.order_status ?? "");
1127
+ const fa = latest?.filled_amount;
1128
+ if ((st === "cancelled" || st === "filled") && fa !== undefined && fa !== null && /^\d+(\.\d+)?$/.test(String(fa).trim())) {
1129
+ filledAtCancel = String(fa).trim();
1130
+ terminalStatus = st;
1131
+ }
1132
+ }
1133
+ catch (e) {
1134
+ lastReadError = extractErrorMessage(e); // retried; still unknown after the loop → ORDER_PARTIAL below
1135
+ }
1136
+ }
1137
+ const notes = [tifNote, `cancel→replace: filledAtRead ${filledAtRead}, filledAtCancel ${filledAtCancel ?? "unknown"}`];
1138
+ if (filledAtCancel === null) {
1139
+ throw new PerpError("ORDER_PARTIAL", `Pacifica amend: order ${orderId} was CANCELLED but its fill at cancel could not be read${lastReadError !== null ? ` (${lastReadError})` : ""}; no replacement was placed`, {
1140
+ exchange: "pacifica", cancelled: { orderId, venueRaw: cancelRaw }, placed: null,
1141
+ remediation: "The original order is gone; check the position (perp account positions -e pacifica) and place a new order manually",
1142
+ });
1143
+ }
1144
+ const base = { tif: keep, orderId: null, clientOrderId: null, filledBeforeCancel: filledAtCancel, filledCompletely: false, outcome: null };
1145
+ if (terminalStatus === "filled") {
1146
+ return { ...base, filledCompletely: true, method: "cancelReplace", replacedSize: null, notes: [...notes, `order ${orderId} filled completely before the cancel — no replacement placed`], venueRaw: { cancelled: cancelRaw, placed: null } };
1147
+ }
1148
+ const raced = subDecimal(filledAtCancel, filledAtRead);
1149
+ const replace = subDecimal(size, raced.startsWith("-") ? "0" : raced);
1150
+ let replaceSize = null;
1151
+ if (!replace.startsWith("-") && Number(replace) > 0) {
1152
+ try {
1153
+ replaceSize = floorSize(replace, lot);
1154
+ }
1155
+ catch (e) {
1156
+ if (!(e instanceof PerpError) || e.structured.code !== "SIZE_TOO_SMALL")
1157
+ throw e;
1158
+ }
1159
+ }
1160
+ if (replaceSize === null) {
1161
+ return { ...base, method: "cancelReplace", replacedSize: null, notes: [...notes, `${raced} filled between read and cancel; replacement ${replace} is not above the lot ${lot} — no replacement placed`], venueRaw: { cancelled: cancelRaw, placed: null } };
1162
+ }
1163
+ const cloid = pacificaClientOrderId(keep);
1164
+ let placed;
1165
+ try {
1166
+ placed = await this.client.createLimitOrder({ symbol: sym, price, amount: replaceSize, side: original.side, reduce_only: original.reduce_only === true, tif: keep, client_order_id: cloid }, this.account, this.signMessage, await this.builderCode());
1167
+ }
1168
+ catch (e) {
1169
+ if (pacificaRefused(e)) {
1170
+ throw new PerpError("ORDER_PARTIAL", `Pacifica amend: original order ${orderId} was CANCELLED but the replacement was refused: ${extractErrorMessage(e)}`, {
1171
+ exchange: "pacifica", cancelled: { orderId, venueRaw: cancelRaw }, placed: null,
1172
+ remediation: "The original order is gone; place a new order manually",
1173
+ });
1174
+ }
1175
+ throw new PerpError("ORDER_PENDING", `Pacifica amend: original order ${orderId} was CANCELLED; the replacement outcome is unknown: ${extractErrorMessage(e)}`, {
1176
+ exchange: "pacifica", cancelled: { orderId, venueRaw: cancelRaw }, replacementClientOrderId: cloid,
1177
+ remediation: "Do NOT resubmit. Check open orders: perp account orders -e pacifica",
1178
+ });
1179
+ }
1180
+ const placeErr = pacificaBodyError(placed);
1181
+ if (placeErr !== null) {
1182
+ throw new PerpError("ORDER_PARTIAL", `Pacifica amend: original order ${orderId} was CANCELLED but the replacement was refused: ${placeErr}`, {
1183
+ exchange: "pacifica", cancelled: { orderId, venueRaw: cancelRaw }, placed: null,
1184
+ remediation: "The original order is gone; place a new order manually",
1185
+ });
1186
+ }
1187
+ const pd = (placed !== null && typeof placed === "object" ? (placed.data ?? placed) : null);
1188
+ const newId = pd !== null && (typeof pd.order_id === "number" || typeof pd.order_id === "string") ? String(pd.order_id) : null;
1189
+ const extra = subDecimal(size, replaceSize) === "0" ? [] : [`replacement size ${replaceSize} (requested open size ${size})`];
1190
+ const outcome = newId === null ? null : await this.resolveOrderOutcome(newId, `amend: replacement of ${orderId}`, { expect: { price, size: replaceSize } });
1191
+ return {
1192
+ ...base, method: "cancelReplace", orderId: newId, clientOrderId: cloid, replacedSize: replaceSize, outcome,
1193
+ notes: [...notes, ...extra], venueRaw: { cancelled: cancelRaw, placed },
1194
+ };
1195
+ }
1196
+ /** Cancel a stop / TP / SL order (POST /orders/stop/cancel). An explicit venue refusal → ORDER_REJECTED. */
1197
+ async cancelStopOrder(symbol, orderId) {
1198
+ this.ensureSigner();
1199
+ if (!/^\d+$/.test(orderId))
1200
+ throw new PerpError("INVALID_PARAMS", `Pacifica stop order id must be numeric (got "${orderId}")`);
1201
+ const raw = await this.client.cancelStopOrder({ symbol: symbol.toUpperCase(), order_id: Number(orderId) }, this.account, this.signMessage);
1202
+ const err = pacificaBodyError(raw);
1203
+ if (err !== null)
1204
+ throw new PerpError("ORDER_REJECTED", `Pacifica cancel of stop order ${orderId} refused: ${err}`, { exchange: "pacifica" });
1205
+ return raw;
1206
+ }
1207
+ /** Open position-level TP/SL orders (order_type take_profit_* / stop_loss_*) on `symbol`. */
1208
+ async positionTpSlOrders(symbol) {
1209
+ const sym = symbol.toUpperCase();
1210
+ const open = await this.client.getOrders(this.account);
1211
+ return open
1212
+ .filter((o) => String(o.symbol).toUpperCase() === sym && /^(take_profit|stop_loss)_/.test(String(o.order_type)))
1213
+ .map((o) => ({ orderId: String(o.order_id), type: String(o.order_type), stopPrice: o.stop_price === null ? null : String(o.stop_price) }));
1214
+ }
1215
+ /**
1216
+ * Position-level TP/SL (POST /positions/tpsl). `positionSide` = the position's side; the
1217
+ * venue's `side` field is the TP/SL orders' (CLOSING) side: long → "ask", short → "bid"
1218
+ * (pacifica-fi/python-sdk rest/create_position_tpsl.py: a BTC long sends side "ask"; the
1219
+ * position side was refused live on testnet 2026-10-02 with 422 "Invalid stop order side").
1220
+ * Covers the whole position. An explicit venue refusal → ORDER_REJECTED.
1221
+ */
1222
+ async setPositionTpSl(symbol, positionSide, tpsl) {
1223
+ this.ensureSigner();
1224
+ const params = {
1225
+ symbol: symbol.toUpperCase(),
1226
+ side: positionSide === "long" ? "ask" : "bid",
1227
+ };
1228
+ if (tpsl.tp !== null)
1229
+ params.take_profit = tpsl.tpLimit !== null ? { stop_price: tpsl.tp, limit_price: tpsl.tpLimit } : { stop_price: tpsl.tp };
1230
+ if (tpsl.sl !== null)
1231
+ params.stop_loss = { stop_price: tpsl.sl };
1232
+ const raw = await this.client.setTPSL(params, this.account, this.signMessage, await this.builderCode());
1233
+ const err = pacificaBodyError(raw);
1234
+ if (err !== null)
1235
+ throw new PerpError("ORDER_REJECTED", `Pacifica position TP/SL on ${params.symbol} refused: ${err}`, { exchange: "pacifica" });
1236
+ return raw;
1237
+ }
343
1238
  async setLeverage(symbol, leverage, marginMode = "cross") {
344
1239
  this.ensureSigner();
345
1240
  await this.client.updateLeverage({ symbol, leverage }, this.account, this.signMessage);
@@ -348,18 +1243,59 @@ export class PacificaAdapter {
348
1243
  }
349
1244
  return { symbol, leverage, marginMode };
350
1245
  }
1246
+ /** CLI trigger-type → Pacifica `trigger_price_type` value. Omitted ⇒ venue default (mark_price). */
1247
+ static TRIGGER_TYPE = {
1248
+ mark: "mark_price",
1249
+ last: "last_trade_price",
1250
+ mid: "mid_price",
1251
+ };
351
1252
  async stopOrder(symbol, side, size, triggerPrice, opts) {
352
1253
  this.ensureSigner();
353
- return this.client.createStopOrder({
1254
+ const triggerPriceType = opts?.triggerType ? PacificaAdapter.TRIGGER_TYPE[opts.triggerType] : undefined;
1255
+ const raw = await this.client.createStopOrder({
354
1256
  symbol,
355
1257
  side: side === "buy" ? "bid" : "ask",
356
1258
  reduce_only: opts?.reduceOnly ?? false,
357
- stop_order: { stop_price: triggerPrice, amount: size, limit_price: opts?.limitPrice },
358
- }, this.account, this.signMessage);
1259
+ stop_order: { stop_price: triggerPrice, amount: size, limit_price: opts?.limitPrice, trigger_price_type: triggerPriceType },
1260
+ }, this.account, this.signMessage, await this.builderCode()).catch((e) => { throw pacificaCreateRejection(e, `stop ${side} ${size} ${symbol} @ ${triggerPrice}`); });
1261
+ // The ack carries only the order id: read the untriggered stop's state back (history_by_id).
1262
+ return this.withOrderOutcome(raw, `stop ${side} ${size} ${symbol} @ ${triggerPrice}`, { expect: { price: null, size } });
359
1263
  }
360
1264
  async withdraw(amount, destination, _opts) {
361
- this.ensureSigner();
362
- return this.client.withdraw({ amount, dest_address: destination }, this.account, this.signMessage);
1265
+ // A withdrawal moves funds: it is signed by the MASTER wallet only — never by the agent
1266
+ // key, even when one is registered (an agent key must not be able to withdraw).
1267
+ const master = this._masterSigner ?? (this._hasRealKey && this._solanaSigner ? this._solanaSigner : null);
1268
+ if (master === null || master.getPublicKeyBase58() !== this.account) {
1269
+ throw new PerpError("NO_SIGNER_AVAILABLE", "Pacifica withdraw must be signed by the account's master wallet (an agent wallet cannot withdraw), and it is not loaded", {
1270
+ exchange: "pacifica",
1271
+ remediation: "Run with the master wallet: perp -e pacifica --ows <wallet> funds withdraw ... (or withdraw on the Pacifica website)",
1272
+ });
1273
+ }
1274
+ // Pacifica withdrawals ALWAYS go to the account owner's own connected wallet —
1275
+ // the REST API has no destination field. Refuse a mismatched destination rather
1276
+ // than silently sending to the own wallet (Rule #2: no misdirection / false
1277
+ // affordance). An empty destination or the own address is accepted.
1278
+ if (destination && destination !== this.account) {
1279
+ throw new PerpError("INVALID_PARAMS", `Pacifica withdrawals only go to the account owner's own wallet (${this.account}); the API has no destination field, so withdrawing to ${destination} is not possible.`, { exchange: "pacifica" });
1280
+ }
1281
+ if (subDecimal(amount, PACIFICA_WITHDRAW_MIN_USDC).startsWith("-")) {
1282
+ throw new PerpError("INVALID_PARAMS", `Pacifica withdraw ${amount} USDC is below the documented minimum of ${PACIFICA_WITHDRAW_MIN_USDC} USDC`, {
1283
+ exchange: "pacifica",
1284
+ remediation: `Withdraw more than ${PACIFICA_WITHDRAW_MIN_USDC} USDC (a $1 fee is charged per withdrawal)`,
1285
+ });
1286
+ }
1287
+ // Cap by the venue's available_to_withdraw (equity minus margin in use / loans) before signing.
1288
+ const info = await this.client.getAccount(this.account);
1289
+ const headroom = subDecimal(info.available_to_withdraw, amount);
1290
+ if (headroom.startsWith("-")) {
1291
+ throw new PerpError("INSUFFICIENT_BALANCE", `Pacifica withdraw ${amount} USDC exceeds available_to_withdraw ${info.available_to_withdraw}`, {
1292
+ exchange: "pacifica",
1293
+ remediation: `Withdraw at most ${info.available_to_withdraw} USDC, or free margin first (close positions / cancel orders / repay a negative USDC balance)`,
1294
+ });
1295
+ }
1296
+ this.client.setRequestAgentWallet(undefined); // master-signed: no agent_wallet field
1297
+ return this.client.withdraw({ amount }, this.account, (msg) => master.signMessage(msg))
1298
+ .catch((e) => { throw pacificaWithdrawRejection(e, amount); });
363
1299
  }
364
1300
  async getRecentTrades(symbol, _limit = 20) {
365
1301
  const trades = await this.client.getTrades(symbol);
@@ -388,7 +1324,7 @@ export class PacificaAdapter {
388
1324
  return [];
389
1325
  return data.map((h) => ({
390
1326
  time: Number(h.created_at ?? 0),
391
- rate: String(h.funding_rate ?? "0"),
1327
+ rate: venueRateField(h.funding_rate, "funding history funding_rate", "pacifica"),
392
1328
  price: String(h.oracle_price ?? "0"),
393
1329
  }));
394
1330
  }
@@ -404,23 +1340,41 @@ export class PacificaAdapter {
404
1340
  trades: k.n,
405
1341
  }));
406
1342
  }
1343
+ /**
1344
+ * Recent orders, newest first (GET /orders/history; PacificaClient.get already unwraps the
1345
+ * `{success, data}` envelope, so the list is the result itself). A non-list or a row without a
1346
+ * readable field → EXCHANGE_ERROR, never an empty list or a placeholder value.
1347
+ */
407
1348
  async getOrderHistory(limit = 30) {
408
1349
  if (!this._hasRealKey)
409
1350
  throw new Error("No private key configured — account data unavailable. Run: perp setup");
410
- const result = await this.client.getOrderHistory(this.account);
411
- const data = (result.data ?? result.orders ?? []);
412
- if (!Array.isArray(data))
413
- return [];
414
- return data.slice(0, limit).map((o) => ({
415
- orderId: String(o.order_id ?? ""),
416
- symbol: String(o.symbol ?? ""),
417
- side: String(o.side) === "bid" ? "buy" : "sell",
418
- price: String(o.initial_price ?? o.price ?? "0"),
419
- size: String(o.amount ?? o.initial_amount ?? ""),
420
- filled: String(o.filled_amount ?? "0"),
421
- status: String(o.order_status ?? "done"),
422
- type: String(o.order_type ?? ""),
423
- }));
1351
+ const rows = await this.client.getOrderHistory(this.account, { limit });
1352
+ if (!Array.isArray(rows)) {
1353
+ throw new PerpError("EXCHANGE_ERROR", `Pacifica /orders/history returned no order list (${JSON.stringify(rows).slice(0, 120)})`, { exchange: "pacifica" });
1354
+ }
1355
+ const field = (o, k) => {
1356
+ const v = o[k];
1357
+ if ((typeof v === "string" && v !== "") || typeof v === "number")
1358
+ return String(v);
1359
+ throw new PerpError("EXCHANGE_ERROR", `Pacifica order-history row without a readable ${k}: ${JSON.stringify(o).slice(0, 160)}`, { exchange: "pacifica" });
1360
+ };
1361
+ return rows.slice(0, limit).map((o) => {
1362
+ const side = field(o, "side");
1363
+ if (side !== "bid" && side !== "ask") {
1364
+ throw new PerpError("EXCHANGE_ERROR", `Pacifica order-history row with unknown side "${side}"`, { exchange: "pacifica" });
1365
+ }
1366
+ return {
1367
+ orderId: field(o, "order_id"),
1368
+ symbol: field(o, "symbol"),
1369
+ side: side === "bid" ? "buy" : "sell",
1370
+ price: field(o, "initial_price"),
1371
+ size: field(o, "amount"),
1372
+ filled: field(o, "filled_amount"),
1373
+ status: field(o, "order_status"),
1374
+ type: field(o, "order_type"),
1375
+ triggerPrice: Number(o.stop_price) > 0 ? String(o.stop_price) : null,
1376
+ };
1377
+ });
424
1378
  }
425
1379
  async getTradeHistory(limit = 30) {
426
1380
  if (!this._hasRealKey)