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,4 +1,5 @@
1
- import type { AgentMeta } from "../settings.js";
1
+ import type { AgentMeta, AgentSource, AnyAgentMeta, ArcusAgentMeta, OndoAgentMeta, QfexAgentMeta } from "../settings.js";
2
+ import type { Network } from "../exchanges/venue-types.js";
2
3
  /**
3
4
  * Attempt to acquire the lockfile for an exchange's agent-approve operation.
4
5
  * Returns void on success. Throws LOCK_HELD if another live process holds it.
@@ -9,11 +10,27 @@ import type { AgentMeta } from "../settings.js";
9
10
  */
10
11
  export declare function acquireLock(exchange: string): void;
11
12
  export declare function releaseLock(exchange: string): void;
13
+ export declare function getAgent(exchange: string, agentName?: string): AgentMeta | null;
12
14
  /**
13
- * Returns the specific named agent, or the first non-partial agent when
14
- * agentName is omitted.
15
+ * Network a legacy-shaped agent is registered on, or null when it cannot be
16
+ * classified (an untagged Lighter-family record, whose approve always honored -n).
15
17
  */
16
- export declare function getAgent(exchange: string, agentName?: string): AgentMeta | null;
18
+ export declare function recordedAgentNetworkOf(exchange: string, meta: AgentMeta): Network | null;
19
+ /**
20
+ * How an agent record got here (AgentMeta.source). Records written before the field existed were
21
+ * all created by `wallet agent approve` (master-signed on this machine), except Ondo keys, which
22
+ * are always created in the Ondo Perps web app.
23
+ */
24
+ export declare function agentSourceOf(exchange: string, meta: AnyAgentMeta): AgentSource;
25
+ /**
26
+ * Network a legacy-shaped agent is registered on. Untagged Lighter-family
27
+ * records (whose approve always honored -n) cannot be classified and throw.
28
+ */
29
+ export declare function agentNetworkOf(exchange: string, meta: AgentMeta): Network;
30
+ /** Throws AGENT_NOT_REGISTERED when `meta` was registered on a different network than `network`. */
31
+ export declare function assertAgentNetwork(exchange: string, meta: AgentMeta, network: Network): void;
32
+ /** First non-partial agent of `exchange` registered on `network`, or null. */
33
+ export declare function getAgentForNetwork(exchange: string, network: Network): AgentMeta | null;
17
34
  /**
18
35
  * Persist an agent meta entry for the given exchange.
19
36
  * Acquires a lockfile before writing to prevent concurrent races (AC-14).
@@ -35,3 +52,19 @@ export declare function listAgents(exchange?: string): Array<{
35
52
  exchange: string;
36
53
  meta: AgentMeta;
37
54
  }>;
55
+ type VenueAgentMap = {
56
+ ondo: OndoAgentMeta;
57
+ arcus: ArcusAgentMeta;
58
+ qfex: QfexAgentMeta;
59
+ };
60
+ /** Named agent, or the only/first registered one when agentName is omitted. */
61
+ export declare function getVenueAgent<V extends keyof VenueAgentMap>(venue: V, agentName?: string): VenueAgentMap[V] | null;
62
+ /** First Ondo/Arcus agent registered on `network` (meta.network), or null — never another network's key. */
63
+ export declare function getVenueAgentForNetwork<V extends keyof VenueAgentMap>(venue: V, network: Network): VenueAgentMap[V] | null;
64
+ export declare function setVenueAgent<V extends keyof VenueAgentMap>(venue: V, meta: VenueAgentMap[V]): void;
65
+ export declare function deleteVenueAgent(venue: keyof VenueAgentMap, agentName: string): boolean;
66
+ export declare function listVenueAgents(): Array<{
67
+ exchange: keyof VenueAgentMap;
68
+ meta: OndoAgentMeta | ArcusAgentMeta | QfexAgentMeta;
69
+ }>;
70
+ export {};
@@ -90,7 +90,15 @@ function isPidAlive(pid) {
90
90
  * Returns the specific named agent, or the first non-partial agent when
91
91
  * agentName is omitted.
92
92
  */
93
+ /** Venues whose agents use a non-legacy meta shape (see getVenueAgent). */
94
+ const NON_LEGACY_AGENT_VENUES = new Set(["ondo", "arcus", "qfex"]);
95
+ function assertLegacyAgentVenue(exchange) {
96
+ if (NON_LEGACY_AGENT_VENUES.has(exchange)) {
97
+ throw new PerpError("INVALID_PARAMS", `${exchange} agents use a venue-specific meta; call getVenueAgent("${exchange}")`);
98
+ }
99
+ }
93
100
  export function getAgent(exchange, agentName) {
101
+ assertLegacyAgentVenue(exchange);
94
102
  const settings = loadSettings();
95
103
  const byExchange = settings.agents?.[exchange];
96
104
  if (!byExchange)
@@ -106,6 +114,69 @@ export function getAgent(exchange, agentName) {
106
114
  }
107
115
  return null;
108
116
  }
117
+ /**
118
+ * Venues whose agent approve/rotate was hardwired to mainnet endpoints before
119
+ * agent records carried `network` — an untagged record of these venues was
120
+ * provably registered on mainnet.
121
+ */
122
+ const PRE_TAG_MAINNET_ONLY_VENUES = new Set(["aster", "hyperliquid", "pacifica"]);
123
+ /**
124
+ * Network a legacy-shaped agent is registered on, or null when it cannot be
125
+ * classified (an untagged Lighter-family record, whose approve always honored -n).
126
+ */
127
+ export function recordedAgentNetworkOf(exchange, meta) {
128
+ if (meta.network !== undefined)
129
+ return meta.network;
130
+ if (PRE_TAG_MAINNET_ONLY_VENUES.has(exchange))
131
+ return "mainnet";
132
+ return null;
133
+ }
134
+ /**
135
+ * How an agent record got here (AgentMeta.source). Records written before the field existed were
136
+ * all created by `wallet agent approve` (master-signed on this machine), except Ondo keys, which
137
+ * are always created in the Ondo Perps web app.
138
+ */
139
+ export function agentSourceOf(exchange, meta) {
140
+ if (meta.source !== undefined)
141
+ return meta.source;
142
+ return exchange === "ondo" || exchange === "qfex" ? "imported" : "approved";
143
+ }
144
+ /**
145
+ * Network a legacy-shaped agent is registered on. Untagged Lighter-family
146
+ * records (whose approve always honored -n) cannot be classified and throw.
147
+ */
148
+ export function agentNetworkOf(exchange, meta) {
149
+ const recorded = recordedAgentNetworkOf(exchange, meta);
150
+ if (recorded !== null)
151
+ return recorded;
152
+ throw new PerpError("AGENT_NOT_REGISTERED", `${exchange} agent "${meta.agentName}" has no recorded network (registered before network tagging)`, {
153
+ remediation: `Set "network": "mainnet" or "testnet" on settings.agents["${exchange}"]["${meta.agentName}"] in ~/.perp/settings.json, or re-register it: perp [-n testnet] wallet agent rotate ${exchange} ${meta.agentName}`,
154
+ });
155
+ }
156
+ /** Throws AGENT_NOT_REGISTERED when `meta` was registered on a different network than `network`. */
157
+ export function assertAgentNetwork(exchange, meta, network) {
158
+ const registered = agentNetworkOf(exchange, meta);
159
+ if (registered !== network) {
160
+ throw new PerpError("AGENT_NOT_REGISTERED", `${exchange} agent "${meta.agentName}" is registered on ${registered}, not ${network}`, {
161
+ remediation: registered === "mainnet"
162
+ ? `Run without -n testnet to use or manage it, or register a testnet agent: perp -n testnet wallet agent approve ${exchange}`
163
+ : `Run with -n testnet to use or manage it, or register a mainnet agent: perp wallet agent approve ${exchange}`,
164
+ });
165
+ }
166
+ }
167
+ /** First non-partial agent of `exchange` registered on `network`, or null. */
168
+ export function getAgentForNetwork(exchange, network) {
169
+ assertLegacyAgentVenue(exchange);
170
+ const settings = loadSettings();
171
+ const byExchange = settings.agents?.[exchange];
172
+ if (!byExchange)
173
+ return null;
174
+ for (const meta of Object.values(byExchange)) {
175
+ if (meta.status !== "partial" && agentNetworkOf(exchange, meta) === network)
176
+ return meta;
177
+ }
178
+ return null;
179
+ }
109
180
  /**
110
181
  * Persist an agent meta entry for the given exchange.
111
182
  * Acquires a lockfile before writing to prevent concurrent races (AC-14).
@@ -115,6 +186,7 @@ export function getAgent(exchange, agentName) {
115
186
  * double-acquire or release.
116
187
  */
117
188
  export function setAgent(exchange, meta) {
189
+ assertLegacyAgentVenue(exchange);
118
190
  const alreadyLocked = callerHoldsLock(exchange);
119
191
  if (!alreadyLocked) {
120
192
  acquireLock(exchange);
@@ -162,6 +234,8 @@ export function listAgents(exchange) {
162
234
  for (const [ex, agents] of Object.entries(settings.agents)) {
163
235
  if (exchange !== undefined && ex !== exchange)
164
236
  continue;
237
+ if (NON_LEGACY_AGENT_VENUES.has(ex))
238
+ continue; // listed by listVenueAgents()
165
239
  if (agents && typeof agents === "object") {
166
240
  for (const meta of Object.values(agents)) {
167
241
  result.push({ exchange: ex, meta });
@@ -170,3 +244,61 @@ export function listAgents(exchange) {
170
244
  }
171
245
  return result;
172
246
  }
247
+ /** Named agent, or the only/first registered one when agentName is omitted. */
248
+ export function getVenueAgent(venue, agentName) {
249
+ const settings = loadSettings();
250
+ const byVenue = settings.agents?.[venue];
251
+ if (!byVenue)
252
+ return null;
253
+ if (agentName !== undefined)
254
+ return byVenue[agentName] ?? null;
255
+ const first = Object.values(byVenue)[0];
256
+ return first === undefined ? null : first;
257
+ }
258
+ /** First Ondo/Arcus agent registered on `network` (meta.network), or null — never another network's key. */
259
+ export function getVenueAgentForNetwork(venue, network) {
260
+ const settings = loadSettings();
261
+ const byVenue = settings.agents?.[venue];
262
+ if (!byVenue)
263
+ return null;
264
+ const match = Object.values(byVenue).find(m => m.network === network);
265
+ return match === undefined ? null : match;
266
+ }
267
+ export function setVenueAgent(venue, meta) {
268
+ const alreadyLocked = callerHoldsLock(venue);
269
+ if (!alreadyLocked)
270
+ acquireLock(venue);
271
+ try {
272
+ const settings = loadSettings();
273
+ if (!settings.agents)
274
+ settings.agents = {};
275
+ const map = (settings.agents[venue] ?? {});
276
+ map[meta.agentName] = meta;
277
+ settings.agents[venue] = map;
278
+ saveSettings(settings);
279
+ }
280
+ finally {
281
+ if (!alreadyLocked)
282
+ releaseLock(venue);
283
+ }
284
+ }
285
+ export function deleteVenueAgent(venue, agentName) {
286
+ const settings = loadSettings();
287
+ const byVenue = settings.agents?.[venue];
288
+ if (!byVenue || !(agentName in byVenue))
289
+ return false;
290
+ delete byVenue[agentName];
291
+ saveSettings(settings);
292
+ return true;
293
+ }
294
+ export function listVenueAgents() {
295
+ const settings = loadSettings();
296
+ const out = [];
297
+ for (const v of ["ondo", "arcus", "qfex"]) {
298
+ const m = settings.agents?.[v];
299
+ if (m)
300
+ for (const meta of Object.values(m))
301
+ out.push({ exchange: v, meta });
302
+ }
303
+ return out;
304
+ }
@@ -0,0 +1,14 @@
1
+ export declare function venueKeystoreDir(dir: string): string;
2
+ export declare function venueKeystorePath(dir: string, name: string): string;
3
+ /** Encrypt `plaintext` and write it with mode 0600. Returns the absolute path. */
4
+ export declare function saveVenueSecret(dir: string, name: string, plaintext: string, passphrase: string): string;
5
+ /**
6
+ * Atomic keystore write: tmp file (0600, exclusive create) in the same directory, fsync,
7
+ * then rename over the target. A crash or full disk never leaves a truncated keystore
8
+ * where a valid one (or none) was. Throws on any failure after removing the tmp file.
9
+ */
10
+ export declare function writeKeystoreAtomic(path: string, contents: string): void;
11
+ /** Read + decrypt a stored secret. `remediation` is surfaced on every failure. */
12
+ export declare function loadVenueSecret(dir: string, name: string, passphrase: string, remediation: string): string;
13
+ /** Idempotent delete. Returns true if a file was removed. */
14
+ export declare function deleteVenueSecret(dir: string, name: string): boolean;
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Generic per-venue encrypted secret store (SSOT Rule #3).
3
+ *
4
+ * `~/.perp/<dir>/<name>.json`, AES-256-GCM with a scrypt-derived key — the same
5
+ * scheme as lighter-keystore.ts / OWS. Used for secrets that are not an
6
+ * OWS-native curve key: Lighter-family L2 slot keys (`lighter-rh-agents`) and
7
+ * Ondo HMAC API secrets (`ondo-agents`).
8
+ *
9
+ * No fallbacks (Rule #2): missing file / wrong passphrase / corrupt file throw.
10
+ */
11
+ import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeSync } from "fs";
12
+ import { resolve } from "path";
13
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
14
+ import { PerpError } from "../errors.js";
15
+ const KDF = { dklen: 32, n: 65536, p: 1, r: 8 };
16
+ function assertSafeSegment(s, what) {
17
+ if (!/^[A-Za-z0-9._-]+$/.test(s) || s === "." || s === "..") {
18
+ throw new PerpError("INVALID_PARAMS", `invalid keystore ${what}: "${s}"`);
19
+ }
20
+ }
21
+ export function venueKeystoreDir(dir) {
22
+ assertSafeSegment(dir, "directory");
23
+ const home = process.env["HOME"];
24
+ if (!home)
25
+ throw new PerpError("INVALID_PARAMS", "HOME is not set; cannot locate ~/.perp keystore");
26
+ return resolve(home, ".perp", dir);
27
+ }
28
+ export function venueKeystorePath(dir, name) {
29
+ assertSafeSegment(name, "name");
30
+ return resolve(venueKeystoreDir(dir), `${name}.json`);
31
+ }
32
+ /** Encrypt `plaintext` and write it with mode 0600. Returns the absolute path. */
33
+ export function saveVenueSecret(dir, name, plaintext, passphrase) {
34
+ const d = venueKeystoreDir(dir);
35
+ if (!existsSync(d))
36
+ mkdirSync(d, { recursive: true, mode: 0o700 });
37
+ chmodSync(d, 0o700);
38
+ const path = venueKeystorePath(dir, name);
39
+ const salt = randomBytes(32);
40
+ const iv = randomBytes(12);
41
+ const dk = scryptSync(Buffer.from(passphrase, "utf-8"), salt, KDF.dklen, { N: KDF.n, r: KDF.r, p: KDF.p, maxmem: 128 * 1024 * 1024 });
42
+ const cipher = createCipheriv("aes-256-gcm", dk, iv);
43
+ const ciphertext = Buffer.concat([cipher.update(Buffer.from(plaintext, "utf-8")), cipher.final()]);
44
+ const file = {
45
+ version: 1,
46
+ kind: "venue-secret",
47
+ dir,
48
+ name,
49
+ crypto: {
50
+ cipher: "aes-256-gcm",
51
+ cipherparams: { iv: iv.toString("hex") },
52
+ ciphertext: ciphertext.toString("hex"),
53
+ auth_tag: cipher.getAuthTag().toString("hex"),
54
+ kdf: "scrypt",
55
+ kdfparams: { dklen: KDF.dklen, n: KDF.n, p: KDF.p, r: KDF.r, salt: salt.toString("hex") },
56
+ },
57
+ createdAt: new Date().toISOString(),
58
+ };
59
+ writeKeystoreAtomic(path, JSON.stringify(file, null, 2));
60
+ return path;
61
+ }
62
+ /**
63
+ * Atomic keystore write: tmp file (0600, exclusive create) in the same directory, fsync,
64
+ * then rename over the target. A crash or full disk never leaves a truncated keystore
65
+ * where a valid one (or none) was. Throws on any failure after removing the tmp file.
66
+ */
67
+ export function writeKeystoreAtomic(path, contents) {
68
+ const tmp = `${path}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
69
+ try {
70
+ const fd = openSync(tmp, "wx", 0o600);
71
+ try {
72
+ writeSync(fd, contents);
73
+ fsyncSync(fd);
74
+ }
75
+ finally {
76
+ closeSync(fd);
77
+ }
78
+ chmodSync(tmp, 0o600);
79
+ renameSync(tmp, path);
80
+ }
81
+ catch (e) {
82
+ if (existsSync(tmp))
83
+ unlinkSync(tmp);
84
+ throw e;
85
+ }
86
+ }
87
+ /** Read + decrypt a stored secret. `remediation` is surfaced on every failure. */
88
+ export function loadVenueSecret(dir, name, passphrase, remediation) {
89
+ const path = venueKeystorePath(dir, name);
90
+ if (!existsSync(path)) {
91
+ throw new PerpError("KEY_NOT_FOUND", `Keystore not found: ${path}`, { remediation });
92
+ }
93
+ let parsed;
94
+ try {
95
+ parsed = JSON.parse(readFileSync(path, "utf-8"));
96
+ }
97
+ catch (e) {
98
+ throw new PerpError("SIGNATURE_FAILED", `Keystore corrupt (${path}): ${e instanceof Error ? e.message : String(e)}`, { remediation });
99
+ }
100
+ if (parsed.version !== 1 || parsed.crypto?.cipher !== "aes-256-gcm" || parsed.crypto?.kdf !== "scrypt") {
101
+ throw new PerpError("SIGNATURE_FAILED", `Keystore has unsupported version/cipher: ${path}`, { remediation });
102
+ }
103
+ const kp = parsed.crypto.kdfparams;
104
+ const dk = scryptSync(Buffer.from(passphrase, "utf-8"), Buffer.from(kp.salt, "hex"), kp.dklen, { N: kp.n, r: kp.r, p: kp.p, maxmem: 128 * 1024 * 1024 });
105
+ try {
106
+ const decipher = createDecipheriv("aes-256-gcm", dk, Buffer.from(parsed.crypto.cipherparams.iv, "hex"));
107
+ decipher.setAuthTag(Buffer.from(parsed.crypto.auth_tag, "hex"));
108
+ return Buffer.concat([decipher.update(Buffer.from(parsed.crypto.ciphertext, "hex")), decipher.final()]).toString("utf-8");
109
+ }
110
+ catch (e) {
111
+ throw new PerpError("SIGNATURE_FAILED", `Keystore decrypt failed (${path}): ${e instanceof Error ? e.message : String(e)}`, { remediation });
112
+ }
113
+ }
114
+ /** Idempotent delete. Returns true if a file was removed. */
115
+ export function deleteVenueSecret(dir, name) {
116
+ const path = venueKeystorePath(dir, name);
117
+ if (!existsSync(path))
118
+ return false;
119
+ unlinkSync(path);
120
+ return true;
121
+ }
@@ -1,4 +1,4 @@
1
- import { HYPERLIQUID_API_URL } from "./urls.js";
1
+ import { HYPERLIQUID_API_URL, sharedApiNetwork } from "./urls.js";
2
2
  import { assertOk } from "./_http.js";
3
3
  import { withCache, TTL_MARKET } from "../../cache.js";
4
4
  // ── Internal ──
@@ -16,7 +16,7 @@ async function hlPost(type) {
16
16
  export function fetchHyperliquidMeta() {
17
17
  // SSOT rule #2: error must propagate. Row-level skipping handles missing
18
18
  // markPx / funding so a 0 doesn't reach downstream comparisons.
19
- return withCache("pub:hl:metaAndAssetCtxs", TTL_MARKET, async () => {
19
+ return withCache(`pub:hl:${sharedApiNetwork()}:metaAndAssetCtxs`, TTL_MARKET, async () => {
20
20
  const json = await hlPost("metaAndAssetCtxs");
21
21
  const universe = (json[0] ?? {}).universe ?? [];
22
22
  const ctxs = (json[1] ?? []);
@@ -38,7 +38,7 @@ export function fetchHyperliquidMetaRaw() {
38
38
  // `.catch(() => null)` so a network failure surfaces as a rejected Promise.
39
39
  // Callers should wrap with Promise.allSettled to keep multi-DEX comparisons
40
40
  // running when one DEX is down.
41
- return withCache("pub:hl:metaAndAssetCtxs:raw", TTL_MARKET, () => hlPost("metaAndAssetCtxs"));
41
+ return withCache(`pub:hl:${sharedApiNetwork()}:metaAndAssetCtxs:raw`, TTL_MARKET, () => hlPost("metaAndAssetCtxs"));
42
42
  }
43
43
  export function parseHyperliquidMetaRaw(raw) {
44
44
  const rates = new Map();
@@ -65,7 +65,7 @@ export function parseHyperliquidMetaRaw(raw) {
65
65
  }
66
66
  export function fetchHyperliquidAllMidsRaw() {
67
67
  // SSOT rule #2: error must propagate; see fetchHyperliquidMetaRaw.
68
- return withCache("pub:hl:allMids:raw", TTL_MARKET, () => hlPost("allMids"));
68
+ return withCache(`pub:hl:${sharedApiNetwork()}:allMids:raw`, TTL_MARKET, () => hlPost("allMids"));
69
69
  }
70
70
  // ── Health check ──
71
71
  export async function pingHyperliquid() {
@@ -1,4 +1,4 @@
1
- import { LIGHTER_API_URL } from "./urls.js";
1
+ import { LIGHTER_API_URL, sharedApiNetwork } from "./urls.js";
2
2
  import { assertOk } from "./_http.js";
3
3
  import { withCache, TTL_MARKET } from "../../cache.js";
4
4
  // ── Fetchers ──
@@ -7,7 +7,7 @@ export function fetchLighterOrderBookDetails() {
7
7
  // returned no markets" from "API down" otherwise. Row-level skipping is
8
8
  // applied for missing/invalid last_trade_price so we never publish a
9
9
  // 0 price downstream.
10
- return withCache("pub:lt:orderBookDetails", TTL_MARKET, async () => {
10
+ return withCache(`pub:lt:${sharedApiNetwork()}:orderBookDetails`, TTL_MARKET, async () => {
11
11
  const res = await fetch(`${LIGHTER_API_URL}/api/v1/orderBookDetails`);
12
12
  await assertOk(res, "Lighter orderBookDetails");
13
13
  const json = await res.json();
@@ -31,7 +31,7 @@ export function fetchLighterOrderBookDetailsRaw() {
31
31
  // rejected Promise instead of being indistinguishable from "API returned no
32
32
  // markets". Non-2xx responses are also rejected so a 5xx JSON body cannot
33
33
  // fulfill as if it were valid data.
34
- return withCache("pub:lt:orderBookDetails:raw", TTL_MARKET, async () => {
34
+ return withCache(`pub:lt:${sharedApiNetwork()}:orderBookDetails:raw`, TTL_MARKET, async () => {
35
35
  const res = await fetch(`${LIGHTER_API_URL}/api/v1/orderBookDetails`);
36
36
  await assertOk(res, "Lighter orderBookDetails");
37
37
  return res.json();
@@ -44,7 +44,7 @@ export function fetchLighterFundingRates() {
44
44
  // documented as intermittent on Lighter's side — when missing, surface as
45
45
  // null so the consumer can apply the orderBookDetails fallback (a
46
46
  // documented price-source preference, NOT an error fallback).
47
- return withCache("pub:lt:fundingRates", TTL_MARKET, async () => {
47
+ return withCache(`pub:lt:${sharedApiNetwork()}:fundingRates`, TTL_MARKET, async () => {
48
48
  const res = await fetch(`${LIGHTER_API_URL}/api/v1/funding-rates`);
49
49
  await assertOk(res, "Lighter funding-rates");
50
50
  const json = await res.json();
@@ -75,7 +75,7 @@ export function fetchLighterFundingRates() {
75
75
  }
76
76
  export function fetchLighterFundingRatesRaw() {
77
77
  // SSOT rule #2: error must propagate; see fetchLighterOrderBookDetailsRaw.
78
- return withCache("pub:lt:fundingRates:raw", TTL_MARKET, async () => {
78
+ return withCache(`pub:lt:${sharedApiNetwork()}:fundingRates:raw`, TTL_MARKET, async () => {
79
79
  const res = await fetch(`${LIGHTER_API_URL}/api/v1/funding-rates`);
80
80
  await assertOk(res, "Lighter funding-rates");
81
81
  return res.json();
@@ -2,7 +2,6 @@ interface PacificaAsset {
2
2
  symbol: string;
3
3
  funding: number;
4
4
  mark: number;
5
- nextFunding?: number;
6
5
  }
7
6
  export declare function fetchPacificaPrices(): Promise<PacificaAsset[]>;
8
7
  export declare function fetchPacificaPricesRaw(): Promise<unknown>;
@@ -1,11 +1,11 @@
1
- import { PACIFICA_API_URL } from "./urls.js";
1
+ import { PACIFICA_API_URL, sharedApiNetwork } from "./urls.js";
2
2
  import { assertOk } from "./_http.js";
3
3
  import { withCache, TTL_MARKET } from "../../cache.js";
4
4
  // ── Fetchers ──
5
5
  export function fetchPacificaPrices() {
6
6
  // SSOT rule #2: error must propagate. Row-level skipping is applied for
7
7
  // missing/invalid mark or funding so a 0 doesn't reach downstream paths.
8
- return withCache("pub:pac:prices", TTL_MARKET, async () => {
8
+ return withCache(`pub:pac:${sharedApiNetwork()}:prices`, TTL_MARKET, async () => {
9
9
  const res = await fetch(PACIFICA_API_URL);
10
10
  await assertOk(res, "Pacifica prices");
11
11
  const json = await res.json();
@@ -21,16 +21,7 @@ export function fetchPacificaPrices() {
21
21
  if (!symbol || fundingRaw === undefined || !Number.isFinite(funding) || !Number.isFinite(mark) || mark <= 0) {
22
22
  continue;
23
23
  }
24
- const nextFundingRaw = p.next_funding;
25
- const nextFundingNumber = Number(nextFundingRaw);
26
- out.push({
27
- symbol,
28
- funding,
29
- mark,
30
- nextFunding: nextFundingRaw !== undefined && Number.isFinite(nextFundingNumber)
31
- ? nextFundingNumber
32
- : undefined,
33
- });
24
+ out.push({ symbol, funding, mark });
34
25
  }
35
26
  return out;
36
27
  });
@@ -41,7 +32,7 @@ export function fetchPacificaPricesRaw() {
41
32
  // Callers should wrap with Promise.allSettled to keep multi-DEX comparisons
42
33
  // running when one DEX is down. Non-2xx responses are also rejected so a
43
34
  // 5xx JSON body cannot fulfill as if it were valid data.
44
- return withCache("pub:pac:prices:raw", TTL_MARKET, async () => {
35
+ return withCache(`pub:pac:${sharedApiNetwork()}:prices:raw`, TTL_MARKET, async () => {
45
36
  const res = await fetch(PACIFICA_API_URL);
46
37
  await assertOk(res, "Pacifica prices");
47
38
  return res.json();
@@ -1,12 +1,20 @@
1
1
  /**
2
2
  * Shared API URL management for all exchanges.
3
- * URLs are mutable to support --network testnet switching.
3
+ *
4
+ * `legacyPublicBaseUrl(venue, net)` is the network-aware source for the public
5
+ * data path (funding/legacy-public.ts). The mutable `*_API_URL` globals remain
6
+ * for the older fetchers below and are switched by setSharedApiNetwork().
4
7
  */
8
+ type LegacyPublicVenue = "pacifica" | "hyperliquid" | "aster";
9
+ export declare function legacyPublicBaseUrl(venue: LegacyPublicVenue, net: "mainnet" | "testnet"): string;
5
10
  export declare let PACIFICA_API_URL: string;
6
11
  export declare let HYPERLIQUID_API_URL: string;
7
12
  export declare let LIGHTER_API_URL: string;
13
+ /** Network selected by the CLI entry point (setSharedApiNetwork). */
14
+ export declare function sharedApiNetwork(): "mainnet" | "testnet";
8
15
  /**
9
16
  * Switch shared API URLs between mainnet and testnet.
10
17
  * Call from CLI entry point after parsing --network flag.
11
18
  */
12
19
  export declare function setSharedApiNetwork(network: "mainnet" | "testnet"): void;
20
+ export {};
@@ -1,33 +1,55 @@
1
1
  /**
2
2
  * Shared API URL management for all exchanges.
3
- * URLs are mutable to support --network testnet switching.
3
+ *
4
+ * `legacyPublicBaseUrl(venue, net)` is the network-aware source for the public
5
+ * data path (funding/legacy-public.ts). The mutable `*_API_URL` globals remain
6
+ * for the older fetchers below and are switched by setSharedApiNetwork().
4
7
  */
8
+ import { PerpError } from "../../errors.js";
9
+ import { asterVenue } from "../../exchanges/venues/aster.js";
10
+ import { MAINNET_REST as PAC_MAINNET_REST, TESTNET_REST as PAC_TESTNET_REST } from "../../pacifica/constants.js";
11
+ /** REST base per legacy venue and network. null = no public testnet API known. */
12
+ const LEGACY_PUBLIC_BASES = {
13
+ pacifica: { mainnet: PAC_MAINNET_REST, testnet: PAC_TESTNET_REST },
14
+ hyperliquid: { mainnet: "https://api.hyperliquid.xyz", testnet: "https://api.hyperliquid-testnet.xyz" },
15
+ // SSOT: the Aster venue definition (venues/aster.ts) — same hosts AsterAdapter uses.
16
+ aster: { mainnet: asterVenue.networks.mainnet.restUrl, testnet: asterVenue.networks.testnet === null ? null : asterVenue.networks.testnet.restUrl },
17
+ };
18
+ export function legacyPublicBaseUrl(venue, net) {
19
+ const url = LEGACY_PUBLIC_BASES[venue][net];
20
+ if (url === null) {
21
+ throw new PerpError("NOT_IMPLEMENTED", `${venue} has no public ${net} market-data API`, {
22
+ remediation: `Run without --testnet for ${venue} market data`,
23
+ });
24
+ }
25
+ return url;
26
+ }
5
27
  const MAINNET_URLS = {
6
- pacifica: "https://api.pacifica.fi/api/v1/info/prices",
28
+ pacifica: `${PAC_MAINNET_REST}/info/prices`,
7
29
  hyperliquid: "https://api.hyperliquid.xyz/info",
8
30
  lighter: "https://mainnet.zklighter.elliot.ai",
9
31
  };
10
32
  const TESTNET_URLS = {
11
- pacifica: null, // no public testnet API
33
+ pacifica: `${PAC_TESTNET_REST}/info/prices`,
12
34
  hyperliquid: "https://api.hyperliquid-testnet.xyz/info",
13
35
  lighter: "https://testnet.zklighter.elliot.ai",
14
36
  };
15
37
  export let PACIFICA_API_URL = MAINNET_URLS.pacifica;
16
38
  export let HYPERLIQUID_API_URL = MAINNET_URLS.hyperliquid;
17
39
  export let LIGHTER_API_URL = MAINNET_URLS.lighter;
40
+ let _sharedNetwork = "mainnet";
41
+ /** Network selected by the CLI entry point (setSharedApiNetwork). */
42
+ export function sharedApiNetwork() {
43
+ return _sharedNetwork;
44
+ }
18
45
  /**
19
46
  * Switch shared API URLs between mainnet and testnet.
20
47
  * Call from CLI entry point after parsing --network flag.
21
48
  */
22
49
  export function setSharedApiNetwork(network) {
23
- if (network === "testnet") {
24
- PACIFICA_API_URL = TESTNET_URLS.pacifica ?? MAINNET_URLS.pacifica;
25
- HYPERLIQUID_API_URL = TESTNET_URLS.hyperliquid ?? MAINNET_URLS.hyperliquid;
26
- LIGHTER_API_URL = TESTNET_URLS.lighter ?? MAINNET_URLS.lighter;
27
- }
28
- else {
29
- PACIFICA_API_URL = MAINNET_URLS.pacifica;
30
- HYPERLIQUID_API_URL = MAINNET_URLS.hyperliquid;
31
- LIGHTER_API_URL = MAINNET_URLS.lighter;
32
- }
50
+ _sharedNetwork = network;
51
+ const urls = network === "testnet" ? TESTNET_URLS : MAINNET_URLS;
52
+ PACIFICA_API_URL = urls.pacifica;
53
+ HYPERLIQUID_API_URL = urls.hyperliquid;
54
+ LIGHTER_API_URL = urls.lighter;
33
55
  }
@@ -0,0 +1,11 @@
1
+ /** How long to wait for another process's lock, and when a lock left by a dead process is stale. */
2
+ export declare const FILE_LOCK_WAIT_MS = 5000;
3
+ export declare const FILE_LOCK_STALE_MS = 30000;
4
+ /**
5
+ * Run `fn` holding the advisory lock file `<path>.lock` (O_EXCL create) — for a load → modify →
6
+ * save of a state file shared by several perp processes (bots, daemons, CLI), so one process
7
+ * never overwrites another's concurrent write. A lock older than FILE_LOCK_STALE_MS (its holder
8
+ * died) is removed; a live one not released within FILE_LOCK_WAIT_MS → LOCK_HELD (nothing is
9
+ * written on top of an unknown concurrent write).
10
+ */
11
+ export declare function withFileLock<T>(path: string, what: string, fn: () => T): T;
@@ -0,0 +1,74 @@
1
+ import { closeSync, existsSync, mkdirSync, openSync, statSync, unlinkSync } from "fs";
2
+ import { resolve } from "path";
3
+ import { PerpError } from "../errors.js";
4
+ /** How long to wait for another process's lock, and when a lock left by a dead process is stale. */
5
+ export const FILE_LOCK_WAIT_MS = 5_000;
6
+ export const FILE_LOCK_STALE_MS = 30_000;
7
+ /** Locks this process holds (path → depth): a nested load-modify-save in the same process never self-deadlocks. */
8
+ const held = new Map();
9
+ /**
10
+ * Run `fn` holding the advisory lock file `<path>.lock` (O_EXCL create) — for a load → modify →
11
+ * save of a state file shared by several perp processes (bots, daemons, CLI), so one process
12
+ * never overwrites another's concurrent write. A lock older than FILE_LOCK_STALE_MS (its holder
13
+ * died) is removed; a live one not released within FILE_LOCK_WAIT_MS → LOCK_HELD (nothing is
14
+ * written on top of an unknown concurrent write).
15
+ */
16
+ export function withFileLock(path, what, fn) {
17
+ const lock = `${path}.lock`;
18
+ const depth = held.get(lock) ?? 0;
19
+ if (depth > 0) {
20
+ held.set(lock, depth + 1);
21
+ try {
22
+ return fn();
23
+ }
24
+ finally {
25
+ held.set(lock, depth);
26
+ }
27
+ }
28
+ const dir = resolve(path, "..");
29
+ if (!existsSync(dir))
30
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
31
+ const deadline = Date.now() + FILE_LOCK_WAIT_MS;
32
+ const sleeper = new Int32Array(new SharedArrayBuffer(4));
33
+ for (;;) {
34
+ try {
35
+ closeSync(openSync(lock, "wx", 0o600));
36
+ break;
37
+ }
38
+ catch (e) {
39
+ if (e.code !== "EEXIST")
40
+ throw e;
41
+ let stale = false;
42
+ try {
43
+ stale = Date.now() - statSync(lock).mtimeMs > FILE_LOCK_STALE_MS;
44
+ }
45
+ catch {
46
+ continue; // released between the open and the stat: try again
47
+ }
48
+ if (stale) {
49
+ try {
50
+ unlinkSync(lock);
51
+ }
52
+ catch { /* another process removed it */ }
53
+ continue;
54
+ }
55
+ if (Date.now() > deadline) {
56
+ throw new PerpError("LOCK_HELD", `${what} is locked by another perp process (${lock})`, {
57
+ remediation: `Retry shortly; if no other perp process is running, delete ${lock}`,
58
+ });
59
+ }
60
+ Atomics.wait(sleeper, 0, 0, 10);
61
+ }
62
+ }
63
+ held.set(lock, 1);
64
+ try {
65
+ return fn();
66
+ }
67
+ finally {
68
+ held.delete(lock);
69
+ try {
70
+ unlinkSync(lock);
71
+ }
72
+ catch { /* already gone */ }
73
+ }
74
+ }