perp-cli 0.14.3 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (363) hide show
  1. package/README.ja.md +243 -0
  2. package/README.ko.md +243 -0
  3. package/README.md +138 -405
  4. package/README.zh.md +243 -0
  5. package/dist/agent-wallet/store.d.ts +3 -2
  6. package/dist/agent-wallet/store.js +2 -2
  7. package/dist/api/public/hyperliquid.js +4 -0
  8. package/dist/api/public/lighter.d.ts +5 -0
  9. package/dist/api/public/lighter.js +5 -0
  10. package/dist/api/public/pacifica.js +85 -15
  11. package/dist/api/public/urls.d.ts +2 -0
  12. package/dist/api/public/urls.js +3 -0
  13. package/dist/arb/sizing.js +4 -2
  14. package/dist/arb/state.d.ts +6 -0
  15. package/dist/arb/utils.d.ts +1 -2
  16. package/dist/arb/utils.js +1 -2
  17. package/dist/balance-by-address.d.ts +39 -0
  18. package/dist/balance-by-address.js +44 -0
  19. package/dist/bot/apex/pulse.js +3 -0
  20. package/dist/bot/apex/radar.js +2 -1
  21. package/dist/bot/engine.d.ts +10 -1
  22. package/dist/bot/engine.js +14 -6
  23. package/dist/bot/indicators.d.ts +7 -0
  24. package/dist/bot/indicators.js +16 -0
  25. package/dist/bot/perp-order.d.ts +4 -4
  26. package/dist/bot/perp-order.js +5 -5
  27. package/dist/bot/quarantine.d.ts +1 -1
  28. package/dist/bot/quarantine.js +1 -1
  29. package/dist/bot/quote-rules.js +1 -1
  30. package/dist/bot/strategies/apex-strategy.js +3 -2
  31. package/dist/bot/strategies/engine-mm.js +6 -5
  32. package/dist/bot/strategies/funding-arb-strategy.js +12 -5
  33. package/dist/bot/strategies/funding-arb-utils.d.ts +15 -3
  34. package/dist/bot/strategies/funding-arb-utils.js +53 -20
  35. package/dist/bot/strategies/funding-auto.js +2 -2
  36. package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
  37. package/dist/bot/strategies/liquidation-mm.js +8 -2
  38. package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
  39. package/dist/bot/strategies/momentum-breakout.js +11 -3
  40. package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
  41. package/dist/bot/strategies/spot-perp-arb-strategy.js +6 -4
  42. package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
  43. package/dist/bot/strategies/spot-perp-executor.js +3 -3
  44. package/dist/bot/strategy-types.d.ts +2 -1
  45. package/dist/bot/tui/BotDashboard.d.ts +1 -1
  46. package/dist/bot/tui/BotDashboard.js +1 -1
  47. package/dist/cache.js +12 -6
  48. package/dist/cli-program.d.ts +15 -0
  49. package/dist/cli-program.js +126 -6
  50. package/dist/commands/account.js +168 -24
  51. package/dist/commands/agent-import.d.ts +7 -0
  52. package/dist/commands/agent-import.js +30 -1
  53. package/dist/commands/agent.d.ts +6 -3
  54. package/dist/commands/agent.js +160 -12
  55. package/dist/commands/arb/index.js +23 -81
  56. package/dist/commands/arb-auto.d.ts +24 -0
  57. package/dist/commands/arb-auto.js +175 -97
  58. package/dist/commands/arb.d.ts +20 -0
  59. package/dist/commands/arb.js +107 -163
  60. package/dist/commands/backtest.js +23 -21
  61. package/dist/commands/bot.js +1 -1
  62. package/dist/commands/builder.js +1 -1
  63. package/dist/commands/funds.js +113 -3
  64. package/dist/commands/init.d.ts +1 -1
  65. package/dist/commands/init.js +27 -7
  66. package/dist/commands/manage.js +23 -21
  67. package/dist/commands/market.d.ts +48 -0
  68. package/dist/commands/market.js +395 -100
  69. package/dist/commands/options.d.ts +14 -0
  70. package/dist/commands/options.js +151 -0
  71. package/dist/commands/pacifica-earn.d.ts +5 -0
  72. package/dist/commands/pacifica-earn.js +455 -0
  73. package/dist/commands/pacifica-orders.d.ts +14 -0
  74. package/dist/commands/pacifica-orders.js +142 -0
  75. package/dist/commands/portfolio.d.ts +16 -0
  76. package/dist/commands/portfolio.js +83 -55
  77. package/dist/commands/rebalance.d.ts +1 -0
  78. package/dist/commands/rebalance.js +15 -0
  79. package/dist/commands/risk.d.ts +11 -2
  80. package/dist/commands/risk.js +39 -44
  81. package/dist/commands/trade.d.ts +1 -0
  82. package/dist/commands/trade.js +67 -62
  83. package/dist/commands/wallet.js +1 -1
  84. package/dist/config.d.ts +2 -2
  85. package/dist/config.js +15 -4
  86. package/dist/constants.js +6 -0
  87. package/dist/cross-chain-margin.d.ts +3 -1
  88. package/dist/cross-chain-margin.js +4 -2
  89. package/dist/dashboard/server.js +3 -3
  90. package/dist/dashboard/ui.js +9 -6
  91. package/dist/errors.d.ts +29 -1
  92. package/dist/errors.js +35 -3
  93. package/dist/exchanges/address.d.ts +8 -0
  94. package/dist/exchanges/address.js +47 -0
  95. package/dist/exchanges/arcus/adapter.d.ts +16 -12
  96. package/dist/exchanges/arcus/adapter.js +45 -18
  97. package/dist/exchanges/arcus/agent.d.ts +1 -1
  98. package/dist/exchanges/arcus/agent.js +1 -1
  99. package/dist/exchanges/arcus/client.js +1 -1
  100. package/dist/exchanges/arcus/public.js +3 -2
  101. package/dist/exchanges/arcus/signing.d.ts +2 -3
  102. package/dist/exchanges/arcus/signing.js +1 -1
  103. package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
  104. package/dist/exchanges/arcus/spot-chain.js +1 -1
  105. package/dist/exchanges/arcus/spot-router.d.ts +1 -1
  106. package/dist/exchanges/arcus/spot-router.js +2 -2
  107. package/dist/exchanges/arcus/spot.d.ts +2 -3
  108. package/dist/exchanges/arcus/spot.js +2 -3
  109. package/dist/exchanges/aster-spot.d.ts +3 -3
  110. package/dist/exchanges/aster-spot.js +6 -9
  111. package/dist/exchanges/aster-typed-data.d.ts +1 -1
  112. package/dist/exchanges/aster-typed-data.js +1 -1
  113. package/dist/exchanges/aster.d.ts +30 -10
  114. package/dist/exchanges/aster.js +153 -53
  115. package/dist/exchanges/builder-fee.d.ts +1 -1
  116. package/dist/exchanges/builder-fee.js +13 -2
  117. package/dist/exchanges/canonical-symbols.d.ts +8 -2
  118. package/dist/exchanges/canonical-symbols.js +46 -2
  119. package/dist/exchanges/capabilities.d.ts +2 -0
  120. package/dist/exchanges/capabilities.js +2 -1
  121. package/dist/exchanges/derive/client.d.ts +37 -0
  122. package/dist/exchanges/derive/client.js +135 -0
  123. package/dist/exchanges/derive/options.d.ts +73 -0
  124. package/dist/exchanges/derive/options.js +379 -0
  125. package/dist/exchanges/extended/adapter.d.ts +96 -0
  126. package/dist/exchanges/extended/adapter.js +241 -0
  127. package/dist/exchanges/extended/client.d.ts +38 -0
  128. package/dist/exchanges/extended/client.js +88 -0
  129. package/dist/exchanges/extended/public.d.ts +133 -0
  130. package/dist/exchanges/extended/public.js +436 -0
  131. package/dist/exchanges/grvt/adapter.d.ts +100 -0
  132. package/dist/exchanges/grvt/adapter.js +283 -0
  133. package/dist/exchanges/grvt/client.d.ts +25 -0
  134. package/dist/exchanges/grvt/client.js +86 -0
  135. package/dist/exchanges/grvt/public.d.ts +151 -0
  136. package/dist/exchanges/grvt/public.js +438 -0
  137. package/dist/exchanges/hyperliquid-outcome.d.ts +2 -0
  138. package/dist/exchanges/hyperliquid-outcome.js +29 -1
  139. package/dist/exchanges/hyperliquid-spot.d.ts +9 -1
  140. package/dist/exchanges/hyperliquid-spot.js +24 -16
  141. package/dist/exchanges/hyperliquid.d.ts +199 -22
  142. package/dist/exchanges/hyperliquid.js +550 -202
  143. package/dist/exchanges/index.d.ts +1 -1
  144. package/dist/exchanges/index.js +1 -1
  145. package/dist/exchanges/interface.d.ts +157 -13
  146. package/dist/exchanges/interface.js +47 -0
  147. package/dist/exchanges/ioc-outcome.d.ts +1 -1
  148. package/dist/exchanges/ioc-outcome.js +1 -1
  149. package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
  150. package/dist/exchanges/legacy-trading-v2.js +7 -2
  151. package/dist/exchanges/lighter-public.d.ts +67 -12
  152. package/dist/exchanges/lighter-public.js +250 -82
  153. package/dist/exchanges/lighter-signer.d.ts +1 -1
  154. package/dist/exchanges/lighter-signer.js +1 -1
  155. package/dist/exchanges/lighter-spot.d.ts +10 -5
  156. package/dist/exchanges/lighter-spot.js +105 -97
  157. package/dist/exchanges/lighter.d.ts +80 -21
  158. package/dist/exchanges/lighter.js +312 -105
  159. package/dist/exchanges/ondo/adapter.d.ts +38 -13
  160. package/dist/exchanges/ondo/adapter.js +86 -27
  161. package/dist/exchanges/ondo/agent.d.ts +1 -1
  162. package/dist/exchanges/ondo/agent.js +1 -1
  163. package/dist/exchanges/ondo/client.d.ts +1 -1
  164. package/dist/exchanges/ondo/client.js +2 -2
  165. package/dist/exchanges/ondo/public.d.ts +35 -3
  166. package/dist/exchanges/ondo/public.js +75 -32
  167. package/dist/exchanges/ondo/spot.d.ts +2 -2
  168. package/dist/exchanges/ondo/spot.js +3 -3
  169. package/dist/exchanges/options-interface.d.ts +158 -0
  170. package/dist/exchanges/options-interface.js +17 -0
  171. package/dist/exchanges/options-venues.d.ts +12 -0
  172. package/dist/exchanges/options-venues.js +35 -0
  173. package/dist/exchanges/outcome-interface.d.ts +4 -0
  174. package/dist/exchanges/pacifica-atomic.d.ts +211 -0
  175. package/dist/exchanges/pacifica-atomic.js +440 -0
  176. package/dist/exchanges/pacifica-decimal.d.ts +10 -0
  177. package/dist/exchanges/pacifica-decimal.js +51 -0
  178. package/dist/exchanges/pacifica-presigned.d.ts +53 -0
  179. package/dist/exchanges/pacifica-presigned.js +141 -0
  180. package/dist/exchanges/pacifica-print.d.ts +200 -0
  181. package/dist/exchanges/pacifica-print.js +417 -0
  182. package/dist/exchanges/pacifica-spot.d.ts +8 -2
  183. package/dist/exchanges/pacifica-spot.js +26 -18
  184. package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
  185. package/dist/exchanges/pacifica-typed-data.js +1 -1
  186. package/dist/exchanges/pacifica-vaults.d.ts +130 -0
  187. package/dist/exchanges/pacifica-vaults.js +277 -0
  188. package/dist/exchanges/pacifica.d.ts +108 -10
  189. package/dist/exchanges/pacifica.js +428 -171
  190. package/dist/exchanges/precision.d.ts +3 -1
  191. package/dist/exchanges/precision.js +9 -2
  192. package/dist/exchanges/public-balances.d.ts +25 -0
  193. package/dist/exchanges/public-balances.js +158 -0
  194. package/dist/exchanges/public-positions.d.ts +43 -0
  195. package/dist/exchanges/public-positions.js +154 -0
  196. package/dist/exchanges/qfex/adapter.d.ts +69 -13
  197. package/dist/exchanges/qfex/adapter.js +102 -17
  198. package/dist/exchanges/qfex/agent.d.ts +1 -1
  199. package/dist/exchanges/qfex/agent.js +1 -1
  200. package/dist/exchanges/qfex/client.d.ts +1 -1
  201. package/dist/exchanges/qfex/client.js +1 -1
  202. package/dist/exchanges/qfex/errors.d.ts +1 -1
  203. package/dist/exchanges/qfex/errors.js +2 -2
  204. package/dist/exchanges/qfex/public.d.ts +65 -12
  205. package/dist/exchanges/qfex/public.js +131 -25
  206. package/dist/exchanges/qfex/signing.d.ts +2 -2
  207. package/dist/exchanges/qfex/signing.js +4 -5
  208. package/dist/exchanges/qfex/ws-transport.d.ts +3 -3
  209. package/dist/exchanges/qfex/ws-transport.js +3 -3
  210. package/dist/exchanges/rate-limiter.d.ts +1 -1
  211. package/dist/exchanges/rate-limiter.js +5 -2
  212. package/dist/exchanges/registry.d.ts +1 -1
  213. package/dist/exchanges/registry.js +1 -1
  214. package/dist/exchanges/risex/adapter.d.ts +160 -0
  215. package/dist/exchanges/risex/adapter.js +842 -0
  216. package/dist/exchanges/risex/agent.d.ts +154 -0
  217. package/dist/exchanges/risex/agent.js +384 -0
  218. package/dist/exchanges/risex/client.d.ts +47 -0
  219. package/dist/exchanges/risex/client.js +174 -0
  220. package/dist/exchanges/risex/nonce.d.ts +43 -0
  221. package/dist/exchanges/risex/nonce.js +212 -0
  222. package/dist/exchanges/risex/public.d.ts +64 -0
  223. package/dist/exchanges/risex/public.js +150 -0
  224. package/dist/exchanges/risex/session.d.ts +71 -0
  225. package/dist/exchanges/risex/session.js +171 -0
  226. package/dist/exchanges/risex/typed-data.d.ts +173 -0
  227. package/dist/exchanges/risex/typed-data.js +240 -0
  228. package/dist/exchanges/risex/units.d.ts +29 -0
  229. package/dist/exchanges/risex/units.js +104 -0
  230. package/dist/exchanges/slippage.d.ts +1 -1
  231. package/dist/exchanges/slippage.js +1 -1
  232. package/dist/exchanges/spot-factory.d.ts +3 -3
  233. package/dist/exchanges/spot-factory.js +8 -8
  234. package/dist/exchanges/spot-interface.d.ts +5 -5
  235. package/dist/exchanges/spot-interface.js +2 -2
  236. package/dist/exchanges/spot-result.d.ts +3 -3
  237. package/dist/exchanges/spot-result.js +3 -3
  238. package/dist/exchanges/trading-v2.d.ts +3 -3
  239. package/dist/exchanges/trading-v2.js +9 -6
  240. package/dist/exchanges/txflow/adapter.d.ts +61 -0
  241. package/dist/exchanges/txflow/adapter.js +150 -0
  242. package/dist/exchanges/txflow/client.d.ts +47 -0
  243. package/dist/exchanges/txflow/client.js +155 -0
  244. package/dist/exchanges/txflow/public.d.ts +87 -0
  245. package/dist/exchanges/txflow/public.js +355 -0
  246. package/dist/exchanges/unknown-symbol.d.ts +34 -0
  247. package/dist/exchanges/unknown-symbol.js +56 -0
  248. package/dist/exchanges/variational/adapter.d.ts +67 -0
  249. package/dist/exchanges/variational/adapter.js +137 -0
  250. package/dist/exchanges/variational/client.d.ts +21 -0
  251. package/dist/exchanges/variational/client.js +66 -0
  252. package/dist/exchanges/variational/public.d.ts +126 -0
  253. package/dist/exchanges/variational/public.js +430 -0
  254. package/dist/exchanges/venue-types.d.ts +52 -10
  255. package/dist/exchanges/venue-types.js +82 -11
  256. package/dist/exchanges/venue.d.ts +150 -11
  257. package/dist/exchanges/venue.js +126 -4
  258. package/dist/exchanges/venues/arcus.d.ts +1 -2
  259. package/dist/exchanges/venues/arcus.js +5 -4
  260. package/dist/exchanges/venues/aster.d.ts +2 -2
  261. package/dist/exchanges/venues/aster.js +6 -4
  262. package/dist/exchanges/venues/create-helpers.d.ts +1 -1
  263. package/dist/exchanges/venues/create-helpers.js +3 -3
  264. package/dist/exchanges/venues/extended.d.ts +21 -0
  265. package/dist/exchanges/venues/extended.js +96 -0
  266. package/dist/exchanges/venues/grvt.d.ts +30 -0
  267. package/dist/exchanges/venues/grvt.js +98 -0
  268. package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
  269. package/dist/exchanges/venues/hyperliquid.js +19 -8
  270. package/dist/exchanges/venues/index.d.ts +7 -2
  271. package/dist/exchanges/venues/index.js +8 -2
  272. package/dist/exchanges/venues/lighter-family.d.ts +7 -9
  273. package/dist/exchanges/venues/lighter-family.js +9 -11
  274. package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
  275. package/dist/exchanges/venues/lighter-rh.js +20 -8
  276. package/dist/exchanges/venues/lighter.d.ts +1 -1
  277. package/dist/exchanges/venues/lighter.js +18 -6
  278. package/dist/exchanges/venues/ondo.d.ts +1 -1
  279. package/dist/exchanges/venues/ondo.js +5 -3
  280. package/dist/exchanges/venues/pacifica.d.ts +2 -2
  281. package/dist/exchanges/venues/pacifica.js +15 -4
  282. package/dist/exchanges/venues/qfex.d.ts +1 -1
  283. package/dist/exchanges/venues/qfex.js +4 -2
  284. package/dist/exchanges/venues/risex-release.d.ts +14 -0
  285. package/dist/exchanges/venues/risex-release.js +14 -0
  286. package/dist/exchanges/venues/risex.d.ts +31 -0
  287. package/dist/exchanges/venues/risex.js +131 -0
  288. package/dist/exchanges/venues/txflow.d.ts +18 -0
  289. package/dist/exchanges/venues/txflow.js +77 -0
  290. package/dist/exchanges/venues/variational.d.ts +21 -0
  291. package/dist/exchanges/venues/variational.js +85 -0
  292. package/dist/execution-log.d.ts +1 -1
  293. package/dist/funding/legacy-public.d.ts +6 -4
  294. package/dist/funding/legacy-public.js +42 -41
  295. package/dist/funding/normalize.d.ts +1 -1
  296. package/dist/funding/normalize.js +1 -1
  297. package/dist/funding/rates.d.ts +95 -6
  298. package/dist/funding/rates.js +99 -19
  299. package/dist/index.js +7 -6
  300. package/dist/jobs.d.ts +1 -0
  301. package/dist/jobs.js +25 -1
  302. package/dist/landing.d.ts +2 -0
  303. package/dist/landing.js +1 -1
  304. package/dist/liquidity.d.ts +3 -1
  305. package/dist/liquidity.js +8 -4
  306. package/dist/mcp-pacifica-earn.d.ts +9 -0
  307. package/dist/mcp-pacifica-earn.js +64 -0
  308. package/dist/mcp-server.js +211 -68
  309. package/dist/pacifica/client.d.ts +69 -5
  310. package/dist/pacifica/client.js +181 -8
  311. package/dist/pacifica/rate-limit.d.ts +144 -0
  312. package/dist/pacifica/rate-limit.js +267 -0
  313. package/dist/pacifica/types/order.d.ts +2 -1
  314. package/dist/plan-executor.js +7 -2
  315. package/dist/positions-by-address.d.ts +34 -0
  316. package/dist/positions-by-address.js +46 -0
  317. package/dist/rebalance.js +1 -1
  318. package/dist/risk.d.ts +2 -1
  319. package/dist/risk.js +2 -4
  320. package/dist/settings.d.ts +32 -3
  321. package/dist/signer/ows-evm.d.ts +1 -1
  322. package/dist/signer/ows-evm.js +1 -1
  323. package/dist/smart-order.d.ts +1 -1
  324. package/dist/smart-order.js +1 -1
  325. package/dist/strategies/funding-arb.d.ts +1 -1
  326. package/dist/strategies/funding-arb.js +36 -63
  327. package/dist/trade-validator.js +2 -2
  328. package/dist/utils.d.ts +22 -3
  329. package/dist/utils.js +11 -3
  330. package/dist/venue-health.d.ts +40 -0
  331. package/dist/venue-health.js +75 -0
  332. package/dist/withdraw-gate.d.ts +1 -1
  333. package/dist/withdraw-gate.js +1 -1
  334. package/dist/ws/arcus-market.d.ts +9 -0
  335. package/dist/ws/arcus-market.js +9 -0
  336. package/dist/ws/aster-market.d.ts +33 -0
  337. package/dist/ws/aster-market.js +30 -0
  338. package/dist/ws/index.d.ts +980 -0
  339. package/dist/ws/index.js +4460 -0
  340. package/dist/ws/lighter-market.d.ts +14 -0
  341. package/dist/ws/lighter-market.js +13 -0
  342. package/dist/ws/node.d.ts +996 -0
  343. package/dist/ws/node.js +4554 -0
  344. package/dist/ws/pacifica-instrument.d.ts +8 -0
  345. package/dist/ws/pacifica-instrument.js +8 -0
  346. package/dist/ws/unclassified.d.ts +16 -0
  347. package/dist/ws/unclassified.js +43 -0
  348. package/dist/ws-cli/credentials.d.ts +62 -0
  349. package/dist/ws-cli/credentials.js +126 -0
  350. package/package.json +71 -10
  351. package/skills/perp-cli/SKILL.md +153 -36
  352. package/skills/perp-cli/package.json +15 -2
  353. package/skills/perp-cli/references/agent-operations.md +22 -12
  354. package/skills/perp-cli/references/commands.md +143 -17
  355. package/skills/perp-cli/references/strategies.md +38 -3
  356. package/skills/perp-cli/references/ws-library.md +523 -0
  357. package/skills/perp-cli/scripts/arb-monitor.sh +13 -10
  358. package/skills/perp-cli/scripts/funding-analysis.sh +57 -29
  359. package/skills/perp-cli/scripts/preflight.sh +27 -19
  360. package/skills/perp-cli/scripts/spot-perp-scan.sh +58 -38
  361. package/skills/perp-cli/scripts/validate-arb.sh +56 -39
  362. package/dist/cli-spec.d.ts +0 -46
  363. package/dist/cli-spec.js +0 -81
@@ -0,0 +1,47 @@
1
+ /**
2
+ * TxFlow transport — UNOFFICIAL API (see venues/txflow.ts).
3
+ *
4
+ * - REST: `POST <rest>/info` with an HL-style `{type, ...}` body. Errors come back three ways and
5
+ * are all checked: a non-2xx status (403 "This action is not allowed." = the read is not on the
6
+ * venue's allowlist), HTTP 200 with a `{message}` body, or a body of an unexpected shape.
7
+ * There is no fallback for a refused read (no HL-endpoint substitution, no guessing).
8
+ * - WS one-shot: open → subscribe `allAssetCtxs` → first data frame → close (bulk mark / funding
9
+ * snapshot). The socket factory is injectable for tests.
10
+ */
11
+ import { PerpError } from "../../errors.js";
12
+ import type { RateLimitPolicy } from "../venue.js";
13
+ export declare const TXFLOW_HTTP_TIMEOUT_MS = 15000;
14
+ export declare const TXFLOW_WS_SNAPSHOT_TIMEOUT_MS = 10000;
15
+ /** Shape drift in the unofficial API: an explicit EXCHANGE_ERROR, never a guessed value. */
16
+ export declare function txflowShapeError(what: string): PerpError;
17
+ export declare class TxflowClient {
18
+ private readonly restUrl;
19
+ private readonly rateLimit;
20
+ constructor(restUrl: string, rateLimit: RateLimitPolicy);
21
+ /** POST /info. Returns the parsed body; every error path is a PerpError. */
22
+ info<T = unknown>(body: {
23
+ type: string;
24
+ } & Record<string, unknown>): Promise<T>;
25
+ }
26
+ /** The subset of a `ws` WebSocket the one-shot reader uses (a fake in tests). */
27
+ export interface WsLike {
28
+ on(event: "open", cb: () => void): unknown;
29
+ on(event: "message", cb: (data: unknown) => void): unknown;
30
+ on(event: "error", cb: (err: unknown) => void): unknown;
31
+ on(event: "close", cb: (code: number) => void): unknown;
32
+ send(data: string): void;
33
+ close(): void;
34
+ }
35
+ export type WsFactory = (url: string) => WsLike;
36
+ export declare const defaultWsFactory: WsFactory;
37
+ /** One raw `allAssetCtxs` entry: `coin` is the perpMeta `index` as a decimal string. */
38
+ export interface TxflowRawAssetCtx {
39
+ coin: string;
40
+ ctx: Record<string, unknown>;
41
+ }
42
+ /**
43
+ * Open the socket, subscribe to `allAssetCtxs`, resolve with the first data frame's `ctxs`, close.
44
+ * No frame within the timeout (or a socket error / close first) → EXCHANGE_UNREACHABLE; a frame
45
+ * of an unexpected shape → EXCHANGE_ERROR (shape drift).
46
+ */
47
+ export declare function fetchAllAssetCtxsOnce(wsUrl: string, factory?: WsFactory, timeoutMs?: number): Promise<TxflowRawAssetCtx[]>;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * TxFlow transport — UNOFFICIAL API (see venues/txflow.ts).
3
+ *
4
+ * - REST: `POST <rest>/info` with an HL-style `{type, ...}` body. Errors come back three ways and
5
+ * are all checked: a non-2xx status (403 "This action is not allowed." = the read is not on the
6
+ * venue's allowlist), HTTP 200 with a `{message}` body, or a body of an unexpected shape.
7
+ * There is no fallback for a refused read (no HL-endpoint substitution, no guessing).
8
+ * - WS one-shot: open → subscribe `allAssetCtxs` → first data frame → close (bulk mark / funding
9
+ * snapshot). The socket factory is injectable for tests.
10
+ */
11
+ import WebSocket from "ws";
12
+ import { PerpError, extractErrorMessage } from "../../errors.js";
13
+ import { acquireRate, rateLimitedError } from "../rate-limiter.js";
14
+ export const TXFLOW_HTTP_TIMEOUT_MS = 15_000;
15
+ export const TXFLOW_WS_SNAPSHOT_TIMEOUT_MS = 10_000;
16
+ const UNOFFICIAL_REMEDIATION = (type) => `TxFlow's unofficial API refused "${type}"; perp-cli cannot read this until it is updated (TxFlow has no official API yet)`;
17
+ /** Shape drift in the unofficial API: an explicit EXCHANGE_ERROR, never a guessed value. */
18
+ export function txflowShapeError(what) {
19
+ return new PerpError("EXCHANGE_ERROR", `TxFlow unofficial API shape changed: ${what}`, {
20
+ exchange: "txflow",
21
+ remediation: "TxFlow's undocumented API changed; perp-cli needs an update to read it. TxFlow data is unavailable until then",
22
+ });
23
+ }
24
+ function isMessageOnly(body) {
25
+ if (body === null || typeof body !== "object" || Array.isArray(body))
26
+ return false;
27
+ const keys = Object.keys(body);
28
+ return keys.length === 1 && keys[0] === "message" && typeof body.message === "string";
29
+ }
30
+ export class TxflowClient {
31
+ restUrl;
32
+ rateLimit;
33
+ constructor(restUrl, rateLimit) {
34
+ this.restUrl = restUrl;
35
+ this.rateLimit = rateLimit;
36
+ }
37
+ /** POST /info. Returns the parsed body; every error path is a PerpError. */
38
+ async info(body) {
39
+ await acquireRate("txflow", this.rateLimit, body.type);
40
+ let res;
41
+ try {
42
+ res = await fetch(`${this.restUrl}/info`, {
43
+ method: "POST",
44
+ headers: { "content-type": "application/json" },
45
+ body: JSON.stringify(body),
46
+ signal: AbortSignal.timeout(TXFLOW_HTTP_TIMEOUT_MS),
47
+ });
48
+ }
49
+ catch (e) {
50
+ throw new PerpError("EXCHANGE_UNREACHABLE", `TxFlow ${body.type}: ${extractErrorMessage(e)}`, { exchange: "txflow" });
51
+ }
52
+ const text = await res.text().catch((e) => {
53
+ throw new PerpError("EXCHANGE_UNREACHABLE", `TxFlow ${body.type}: response body unreadable (${extractErrorMessage(e)})`, { exchange: "txflow" });
54
+ });
55
+ if (res.status === 429)
56
+ throw rateLimitedError("txflow", res.headers.get("retry-after"), text);
57
+ let parsed;
58
+ try {
59
+ parsed = JSON.parse(text);
60
+ }
61
+ catch {
62
+ parsed = undefined;
63
+ }
64
+ if (res.status === 403) {
65
+ throw new PerpError("EXCHANGE_ERROR", `TxFlow ${body.type}: HTTP 403 ${isMessageOnly(parsed) ? parsed.message : text.slice(0, 200)}`, {
66
+ exchange: "txflow", status: 403, remediation: UNOFFICIAL_REMEDIATION(body.type),
67
+ });
68
+ }
69
+ if (!res.ok) {
70
+ // A 5xx can be a raw Cloudflare HTML page (live: candleSnapshot 1w / 3d → HTTP 500): tags stripped.
71
+ const clean = text.replace(/<(script|style)[\s\S]*?<\/\1>/gi, " ").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 200);
72
+ throw new PerpError("EXCHANGE_ERROR", `TxFlow ${body.type}: HTTP ${res.status} ${clean}`, {
73
+ exchange: "txflow", status: res.status, remediation: UNOFFICIAL_REMEDIATION(body.type),
74
+ });
75
+ }
76
+ if (parsed === undefined)
77
+ throw txflowShapeError(`${body.type} returned a non-JSON body`);
78
+ // HTTP 200 with {message} is an error on this API ("Orderbook for coin '…' not found").
79
+ if (isMessageOnly(parsed)) {
80
+ throw new PerpError("EXCHANGE_ERROR", `TxFlow ${body.type}: ${parsed.message}`, {
81
+ exchange: "txflow", remediation: UNOFFICIAL_REMEDIATION(body.type),
82
+ });
83
+ }
84
+ return parsed;
85
+ }
86
+ }
87
+ export const defaultWsFactory = url => new WebSocket(url, { handshakeTimeout: TXFLOW_WS_SNAPSHOT_TIMEOUT_MS });
88
+ /**
89
+ * Open the socket, subscribe to `allAssetCtxs`, resolve with the first data frame's `ctxs`, close.
90
+ * No frame within the timeout (or a socket error / close first) → EXCHANGE_UNREACHABLE; a frame
91
+ * of an unexpected shape → EXCHANGE_ERROR (shape drift).
92
+ */
93
+ export function fetchAllAssetCtxsOnce(wsUrl, factory = defaultWsFactory, timeoutMs = TXFLOW_WS_SNAPSHOT_TIMEOUT_MS) {
94
+ return new Promise((resolve, reject) => {
95
+ let done = false;
96
+ let ws;
97
+ const finish = (err, value) => {
98
+ if (done)
99
+ return;
100
+ done = true;
101
+ clearTimeout(timer);
102
+ try {
103
+ ws.close();
104
+ }
105
+ catch { /* already closed */ }
106
+ if (err !== null)
107
+ reject(err);
108
+ else
109
+ resolve(value);
110
+ };
111
+ const timer = setTimeout(() => finish(new PerpError("EXCHANGE_UNREACHABLE", `TxFlow allAssetCtxs snapshot: no data within ${timeoutMs / 1000}s`, { exchange: "txflow" })), timeoutMs);
112
+ try {
113
+ ws = factory(wsUrl);
114
+ }
115
+ catch (e) {
116
+ clearTimeout(timer);
117
+ reject(new PerpError("EXCHANGE_UNREACHABLE", `TxFlow WebSocket: ${extractErrorMessage(e)}`, { exchange: "txflow" }));
118
+ return;
119
+ }
120
+ ws.on("open", () => {
121
+ try {
122
+ ws.send(JSON.stringify({ method: "subscribe", type: "allAssetCtxs", subscription: { type: "allAssetCtxs" } }));
123
+ }
124
+ catch (e) {
125
+ finish(new PerpError("EXCHANGE_UNREACHABLE", `TxFlow WebSocket subscribe failed: ${extractErrorMessage(e)}`, { exchange: "txflow" }));
126
+ }
127
+ });
128
+ ws.on("error", e => finish(new PerpError("EXCHANGE_UNREACHABLE", `TxFlow WebSocket: ${extractErrorMessage(e)}`, { exchange: "txflow" })));
129
+ ws.on("close", code => finish(new PerpError("EXCHANGE_UNREACHABLE", `TxFlow WebSocket closed (code ${code}) before a snapshot arrived`, { exchange: "txflow" })));
130
+ ws.on("message", raw => {
131
+ let msg;
132
+ try {
133
+ msg = JSON.parse(String(raw));
134
+ }
135
+ catch {
136
+ return finish(txflowShapeError("WebSocket frame is not JSON"));
137
+ }
138
+ if (msg === null || typeof msg !== "object")
139
+ return finish(txflowShapeError("WebSocket frame is not an object"));
140
+ const m = msg;
141
+ if (m.channel !== "allAssetCtxs")
142
+ return; // pong / subscription acks / other channels
143
+ const ctxs = m.data?.ctxs;
144
+ if (!Array.isArray(ctxs))
145
+ return finish(txflowShapeError("allAssetCtxs frame has no ctxs array"));
146
+ for (const c of ctxs) {
147
+ if (c === null || typeof c !== "object" || typeof c.coin !== "string"
148
+ || c.ctx === null || typeof c.ctx !== "object") {
149
+ return finish(txflowShapeError("allAssetCtxs entry without coin / ctx"));
150
+ }
151
+ }
152
+ finish(null, ctxs);
153
+ });
154
+ });
155
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * TxFlow public market data — UNOFFICIAL API (see venues/txflow.ts).
3
+ *
4
+ * Sources:
5
+ * POST /info {type:"perpMeta", dex:""} market directory: index (numeric coin id, with gaps),
6
+ * name "BTC-USDC", baseCurrency, szDecimals (can be
7
+ * NEGATIVE: lot 10, 100 …), priceTick, maxLeverage,
8
+ * haltTrading, delisted, tagIds
9
+ * POST /info {type:"allMarketTickers"} per instrument: dayNtlVlm (24 h USD notional), prevDayPx …;
10
+ * `instrumentId` == perpMeta `index` (spot rows ≥ 10000),
11
+ * empty fields are "" (→ null). `market list|prices` only.
12
+ * WS allAssetCtxs (one-shot snapshot) per coin: markPx, oraclePx, funding, openInterest,
13
+ * nextFundingTime, prevFundingTime. `coin` == perpMeta
14
+ * `index` (as a string) — joined by id, never by position.
15
+ *
16
+ * Funding: `funding` is PERCENT per hour (settlementIntervalMs 3600000 on every market; 0.00125 is
17
+ * the HL-style 0.00125 %/h baseline). Converted to a fraction per hour by an exact two-place decimal
18
+ * shift (no float maths). The value moves every tick: an accruing / predicted estimate, not a
19
+ * settled print — the venue is fundingVerified false. A market whose next − prev funding time is
20
+ * not one hour gets no funding row.
21
+ */
22
+ import type { VenuePublicData } from "../venue.js";
23
+ import type { AssetClass, FundingRateBasis, Network, VenueFundingRate, VenueMarket } from "../venue-types.js";
24
+ import { TxflowClient, type TxflowRawAssetCtx, type WsFactory } from "./client.js";
25
+ export declare const TXFLOW_FUNDING_BASIS: FundingRateBasis;
26
+ export declare const TXFLOW_SETTLEMENT_MS = 3600000;
27
+ /** perpMeta tag id of the TradFi (equity / commodity / ETF) markets. */
28
+ export declare const TXFLOW_TRADFI_TAG = 5;
29
+ export interface TxflowMarketMeta {
30
+ /** Numeric coin id (perpMeta `index`); its decimal string is the wire id and venueSymbol. */
31
+ index: number;
32
+ name: string;
33
+ symbol: string;
34
+ quote: string;
35
+ tickSize: string;
36
+ stepSize: string;
37
+ maxLeverage: number;
38
+ halted: boolean;
39
+ tagIds: number[];
40
+ assetClass: AssetClass;
41
+ canonical: string | null;
42
+ }
43
+ /** Lot step from szDecimals, exactly: 4 → "0.0001", 0 → "1", -2 → "100". */
44
+ export declare function stepFromSzDecimals(sz: number): string;
45
+ /** Exact percent → fraction (two-place decimal shift on the string; "0.00125" → "0.0000125"). */
46
+ export declare function percentToFraction(pct: string): string;
47
+ export declare function looksTxflowTradFi(symbol: string, tagIds: readonly number[], fullName: string | null, description: string | null): boolean;
48
+ /** Parse one perpMeta row; shape drift is EXCHANGE_ERROR. Returns null for a delisted market. */
49
+ export declare function toTxflowMarketMeta(raw: unknown): TxflowMarketMeta | null;
50
+ export declare function fetchTxflowDirectory(client: TxflowClient): Promise<TxflowMarketMeta[]>;
51
+ export interface TxflowCtx {
52
+ markPx: string;
53
+ oraclePx: string | null;
54
+ /** Percent per hour, as published; null when absent. */
55
+ fundingPct: string | null;
56
+ openInterest: string | null;
57
+ nextFundingTime: number | null;
58
+ prevFundingTime: number | null;
59
+ }
60
+ /**
61
+ * Join snapshot entries to the directory by coin id. An entry whose coin id is not a listed
62
+ * market is dropped with a warning (never matched by position); an entry without a mark price
63
+ * is dropped too (a market row needs one).
64
+ */
65
+ export declare function joinSnapshot(dir: readonly TxflowMarketMeta[], ctxs: readonly TxflowRawAssetCtx[], warn: (msg: string) => void): Map<number, TxflowCtx>;
66
+ /**
67
+ * Hourly funding fraction for one ctx, or null (unknown) when the rate is absent or the one-hour
68
+ * interval cannot be PROVEN from next − prev funding time (a missing time is unknown, never assumed 1h).
69
+ */
70
+ export declare function hourlyFundingOf(ctx: TxflowCtx): string | null;
71
+ /** First spot instrumentId in allMarketTickers (perp ids are the perpMeta index, below it). */
72
+ export declare const TXFLOW_SPOT_ID_BASE = 10000;
73
+ /**
74
+ * 24 h USD notional volume per perp coin id from allMarketTickers. "" (no trades) → null, never
75
+ * "0"; a non-list answer or a row without a numeric instrumentId is EXCHANGE_ERROR.
76
+ */
77
+ export declare function fetchTxflowDayVolumes(client: TxflowClient): Promise<Map<number, string | null>>;
78
+ export declare function toTxflowVenueMarket(m: TxflowMarketMeta, ctx: TxflowCtx, volume24h: string | null): VenueMarket;
79
+ export interface TxflowPublicDeps {
80
+ wsFactory: WsFactory;
81
+ warn: (msg: string) => void;
82
+ }
83
+ export declare function fetchTxflowMarkets(net: Network, deps?: TxflowPublicDeps): Promise<VenueMarket[]>;
84
+ export declare function fetchTxflowFundingRates(net: Network, deps?: TxflowPublicDeps): Promise<VenueFundingRate[]>;
85
+ export declare const txflowPublicData: VenuePublicData;
86
+ /** Resolve a CLI symbol ("btc", "BTC-USDC", "BTC-PERP", or the numeric coin id) against the directory. */
87
+ export declare function resolveTxflowMarket(dir: readonly TxflowMarketMeta[], input: string): TxflowMarketMeta;
@@ -0,0 +1,355 @@
1
+ /**
2
+ * TxFlow public market data — UNOFFICIAL API (see venues/txflow.ts).
3
+ *
4
+ * Sources:
5
+ * POST /info {type:"perpMeta", dex:""} market directory: index (numeric coin id, with gaps),
6
+ * name "BTC-USDC", baseCurrency, szDecimals (can be
7
+ * NEGATIVE: lot 10, 100 …), priceTick, maxLeverage,
8
+ * haltTrading, delisted, tagIds
9
+ * POST /info {type:"allMarketTickers"} per instrument: dayNtlVlm (24 h USD notional), prevDayPx …;
10
+ * `instrumentId` == perpMeta `index` (spot rows ≥ 10000),
11
+ * empty fields are "" (→ null). `market list|prices` only.
12
+ * WS allAssetCtxs (one-shot snapshot) per coin: markPx, oraclePx, funding, openInterest,
13
+ * nextFundingTime, prevFundingTime. `coin` == perpMeta
14
+ * `index` (as a string) — joined by id, never by position.
15
+ *
16
+ * Funding: `funding` is PERCENT per hour (settlementIntervalMs 3600000 on every market; 0.00125 is
17
+ * the HL-style 0.00125 %/h baseline). Converted to a fraction per hour by an exact two-place decimal
18
+ * shift (no float maths). The value moves every tick: an accruing / predicted estimate, not a
19
+ * settled print — the venue is fundingVerified false. A market whose next − prev funding time is
20
+ * not one hour gets no funding row.
21
+ */
22
+ import { PerpError } from "../../errors.js";
23
+ import { canonicalFor } from "../canonical-symbols.js";
24
+ import { networkConfig } from "../venue.js";
25
+ import { txflowVenue } from "../venues/txflow.js";
26
+ import { fetchAllAssetCtxsOnce, txflowShapeError, TxflowClient, defaultWsFactory } from "./client.js";
27
+ export const TXFLOW_FUNDING_BASIS = { kind: "perHours", hours: 1 };
28
+ export const TXFLOW_SETTLEMENT_MS = 3_600_000;
29
+ /** perpMeta tag id of the TradFi (equity / commodity / ETF) markets. */
30
+ export const TXFLOW_TRADFI_TAG = 5;
31
+ /**
32
+ * TradFi markets by class. Equities are matched by ticker identity; commodities / ETFs / pre-IPO only
33
+ * through EXPLICIT_CANONICAL. A TradFi market missing here is listed as equity with NO canonical
34
+ * symbol (never matched across venues) until it is classified.
35
+ */
36
+ const TRADFI_CLASS = {
37
+ XAU: "commodity", XAG: "commodity", XPD: "commodity", XPT: "commodity", CL: "commodity", BZ: "commodity",
38
+ SPY: "etf", QQQ: "etf", EWJ: "etf", SOXL: "etf", URNM: "etf", UVXY: "etf",
39
+ SPCX: "preIpo",
40
+ MSTR: "equity", CRCL: "equity", TSLA: "equity", COIN: "equity", HOOD: "equity", META: "equity", MU: "equity",
41
+ AAPL: "equity", AMZN: "equity", GOOGL: "equity", INTC: "equity", NVDA: "equity", PLTR: "equity", SNDK: "equity",
42
+ TSM: "equity", MSFT: "equity", AMD: "equity", LITE: "equity", AVGO: "equity", CRWV: "equity", QCOM: "equity",
43
+ ARM: "equity", LRCX: "equity", SKHYNIX: "equity", SAMSUNG: "equity", STXX: "equity",
44
+ };
45
+ /** Lot step from szDecimals, exactly: 4 → "0.0001", 0 → "1", -2 → "100". */
46
+ export function stepFromSzDecimals(sz) {
47
+ if (!Number.isInteger(sz))
48
+ throw txflowShapeError(`szDecimals ${sz} is not an integer`);
49
+ if (sz > 0)
50
+ return `0.${"0".repeat(sz - 1)}1`;
51
+ return `1${"0".repeat(-sz)}`;
52
+ }
53
+ /** Exact percent → fraction (two-place decimal shift on the string; "0.00125" → "0.0000125"). */
54
+ export function percentToFraction(pct) {
55
+ const s = pct.trim();
56
+ if (!/^-?\d+(\.\d+)?$/.test(s))
57
+ throw txflowShapeError(`funding "${pct}" is not a plain decimal`);
58
+ const neg = s.startsWith("-");
59
+ const body = neg ? s.slice(1) : s;
60
+ const [ip, fp = ""] = body.split(".");
61
+ const digits = `${ip}${fp}`;
62
+ const scale = fp.length + 2;
63
+ const padded = digits.padStart(scale + 1, "0");
64
+ let out = `${padded.slice(0, padded.length - scale)}.${padded.slice(padded.length - scale)}`;
65
+ out = out.replace(/0+$/, "").replace(/\.$/, "");
66
+ out = out.replace(/^0+(?=\d)/, "");
67
+ if (/^0(\.0*)?$/.test(out) || out === "")
68
+ return "0";
69
+ return neg ? `-${out}` : out;
70
+ }
71
+ /**
72
+ * TradFi markets listed WITHOUT the TradFi tag (live 2026-10-19: ~38 rows with `tagIds: []`, e.g.
73
+ * COPPER, NATGAS, MRNA, GS, PATH, TEAM, TLT, SOXS) were classed as crypto and got a crypto
74
+ * canonical, so arb could pair GS or PATH with same-named crypto tokens. A row is TradFi when it is
75
+ * tagged, its description reads like a share / ETF / commodity / FX / pre-IPO contract, its
76
+ * fullName reads like a company or fund (as Variational's looksTradFi), or it is one of the
77
+ * untagged equities whose perpMeta row has no description (UNTAGGED_TRADFI, from the live listing).
78
+ */
79
+ const TRADFI_DESCRIPTION = /\b(common stock|ordinary shares|shares\b|ETF\b|Pre-IPO|exchange rate|price reference for|priced in US dollars, representing)/i;
80
+ const TRADFI_FULLNAME = /\b(Inc|Corp|Corporation|Company|Holdings?|Ltd|Limited|plc|PBC|N\.V|Group|ETF|Shares|Currency FX|Co\.)\b\.?/i;
81
+ const UNTAGGED_TRADFI = new Set([
82
+ "CRDO", "APP", "ADBE", "COST", "BX", "DKNG", "BRKB", "KO", "IBM", "QNTX", "UNH", "TWST", "CVNA", "MCD", "NKE",
83
+ "AAOI", "ASTS", "VST", "SNPS", "BSP",
84
+ ]);
85
+ /** Crypto tokens whose description reads like a commodity contract (Tether Gold is a token). */
86
+ const CRYPTO_DESPITE_DESCRIPTION = new Set(["XAUT"]);
87
+ export function looksTxflowTradFi(symbol, tagIds, fullName, description) {
88
+ if (tagIds.includes(TXFLOW_TRADFI_TAG))
89
+ return true;
90
+ if (CRYPTO_DESPITE_DESCRIPTION.has(symbol))
91
+ return false;
92
+ if (description !== null && TRADFI_DESCRIPTION.test(description))
93
+ return true;
94
+ if (fullName !== null && TRADFI_FULLNAME.test(fullName))
95
+ return true;
96
+ return UNTAGGED_TRADFI.has(symbol);
97
+ }
98
+ /** Class of an unclassified TradFi row from its listing text (its canonical stays null either way). */
99
+ function textClassOf(fullName, description) {
100
+ const text = `${fullName ?? ""} ${description ?? ""}`;
101
+ if (/exchange rate|Currency FX/i.test(text))
102
+ return "fx";
103
+ if (/Pre-IPO|\bPBC\b|private artificial intelligence company/i.test(text))
104
+ return "preIpo";
105
+ if (/\bETF\b|\bShares\b/.test(text))
106
+ return "etf";
107
+ if (/priced in US dollars, representing/i.test(text))
108
+ return "commodity";
109
+ return "equity";
110
+ }
111
+ function assetClassOf(symbol, tradFi, fullName, description) {
112
+ if (!tradFi)
113
+ return "crypto";
114
+ const known = TRADFI_CLASS[symbol];
115
+ return known !== undefined ? known : textClassOf(fullName, description);
116
+ }
117
+ function canonicalOf(symbol, tradFi, assetClass) {
118
+ // An unclassified TradFi market (tagged or not) is never matched across venues.
119
+ if (tradFi && TRADFI_CLASS[symbol] === undefined)
120
+ return null;
121
+ return canonicalFor("txflow", symbol, assetClass);
122
+ }
123
+ /** Parse one perpMeta row; shape drift is EXCHANGE_ERROR. Returns null for a delisted market. */
124
+ export function toTxflowMarketMeta(raw) {
125
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
126
+ throw txflowShapeError("perpMeta universe row is not an object");
127
+ const r = raw;
128
+ if (typeof r.index !== "number" || !Number.isInteger(r.index) || r.index < 0)
129
+ throw txflowShapeError(`perpMeta row ${String(r.name)} has a non-numeric index`);
130
+ if (typeof r.name !== "string" || typeof r.baseCurrency !== "string" || r.baseCurrency === "")
131
+ throw txflowShapeError(`perpMeta row ${r.index} has no name / baseCurrency`);
132
+ if (typeof r.quoteCurrency !== "string")
133
+ throw txflowShapeError(`perpMeta ${r.name} has no quoteCurrency`);
134
+ if (typeof r.szDecimals !== "number")
135
+ throw txflowShapeError(`perpMeta ${r.name} has no szDecimals`);
136
+ if (typeof r.priceTick !== "string" || !/^\d+(\.\d+)?$/.test(r.priceTick) || !(Number(r.priceTick) > 0))
137
+ throw txflowShapeError(`perpMeta ${r.name} has no valid priceTick`);
138
+ if (typeof r.maxLeverage !== "number" || !(r.maxLeverage > 0))
139
+ throw txflowShapeError(`perpMeta ${r.name} has no maxLeverage`);
140
+ if (typeof r.haltTrading !== "boolean" || typeof r.delisted !== "boolean")
141
+ throw txflowShapeError(`perpMeta ${r.name} has no haltTrading / delisted flags`);
142
+ if (!Array.isArray(r.tagIds) || r.tagIds.some(t => typeof t !== "number"))
143
+ throw txflowShapeError(`perpMeta ${r.name} has no tagIds`);
144
+ if (r.delisted)
145
+ return null;
146
+ const symbol = r.baseCurrency.toUpperCase();
147
+ const tagIds = r.tagIds;
148
+ const fullName = typeof r.fullName === "string" ? r.fullName : null;
149
+ const description = typeof r.description === "string" ? r.description : null;
150
+ const tradFi = looksTxflowTradFi(symbol, tagIds, fullName, description);
151
+ const assetClass = assetClassOf(symbol, tradFi, fullName, description);
152
+ return {
153
+ index: r.index,
154
+ name: r.name,
155
+ symbol,
156
+ quote: r.quoteCurrency,
157
+ tickSize: r.priceTick,
158
+ stepSize: stepFromSzDecimals(r.szDecimals),
159
+ maxLeverage: r.maxLeverage,
160
+ halted: r.haltTrading,
161
+ tagIds,
162
+ assetClass,
163
+ canonical: canonicalOf(symbol, tradFi, assetClass),
164
+ };
165
+ }
166
+ export async function fetchTxflowDirectory(client) {
167
+ const res = await client.info({ type: "perpMeta", dex: "" });
168
+ if (res === null || typeof res !== "object" || !Array.isArray(res.universe))
169
+ throw txflowShapeError("perpMeta has no universe array");
170
+ const out = [];
171
+ const seen = new Set();
172
+ for (const row of res.universe) {
173
+ const m = toTxflowMarketMeta(row);
174
+ if (m === null)
175
+ continue;
176
+ if (seen.has(m.symbol))
177
+ throw txflowShapeError(`perpMeta lists ${m.symbol} twice`);
178
+ seen.add(m.symbol);
179
+ out.push(m);
180
+ }
181
+ return out;
182
+ }
183
+ function decOrNull(v) {
184
+ return typeof v === "string" && /^-?\d+(\.\d+)?$/.test(v) ? v : null;
185
+ }
186
+ function msOrNull(v) {
187
+ return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : null;
188
+ }
189
+ /**
190
+ * Join snapshot entries to the directory by coin id. An entry whose coin id is not a listed
191
+ * market is dropped with a warning (never matched by position); an entry without a mark price
192
+ * is dropped too (a market row needs one).
193
+ */
194
+ export function joinSnapshot(dir, ctxs, warn) {
195
+ const byIndex = new Map(dir.map(m => [String(m.index), m]));
196
+ const out = new Map();
197
+ const unknown = [];
198
+ const unpriced = [];
199
+ for (const { coin, ctx } of ctxs) {
200
+ const meta = byIndex.get(coin);
201
+ if (meta === undefined) {
202
+ unknown.push(coin);
203
+ continue;
204
+ }
205
+ const markPx = decOrNull(ctx.markPx);
206
+ if (markPx === null || !(Number(markPx) > 0)) {
207
+ unpriced.push(meta.symbol);
208
+ continue;
209
+ }
210
+ out.set(meta.index, {
211
+ markPx,
212
+ oraclePx: decOrNull(ctx.oraclePx),
213
+ fundingPct: decOrNull(ctx.funding),
214
+ openInterest: decOrNull(ctx.openInterest),
215
+ nextFundingTime: msOrNull(ctx.nextFundingTime),
216
+ prevFundingTime: msOrNull(ctx.prevFundingTime),
217
+ });
218
+ }
219
+ if (unknown.length > 0)
220
+ warn(`[txflow] dropped ${unknown.length} snapshot entr${unknown.length === 1 ? "y" : "ies"} for unknown coin id(s): ${unknown.join(", ")}`);
221
+ if (unpriced.length > 0)
222
+ warn(`[txflow] dropped ${unpriced.length} snapshot entr${unpriced.length === 1 ? "y" : "ies"} without a mark price: ${unpriced.join(", ")}`);
223
+ return out;
224
+ }
225
+ /**
226
+ * Hourly funding fraction for one ctx, or null (unknown) when the rate is absent or the one-hour
227
+ * interval cannot be PROVEN from next − prev funding time (a missing time is unknown, never assumed 1h).
228
+ */
229
+ export function hourlyFundingOf(ctx) {
230
+ if (ctx.fundingPct === null)
231
+ return null;
232
+ if (ctx.nextFundingTime === null || ctx.prevFundingTime === null)
233
+ return null;
234
+ if (ctx.nextFundingTime - ctx.prevFundingTime !== TXFLOW_SETTLEMENT_MS)
235
+ return null;
236
+ return percentToFraction(ctx.fundingPct);
237
+ }
238
+ // ── Mapping ──
239
+ /** First spot instrumentId in allMarketTickers (perp ids are the perpMeta index, below it). */
240
+ export const TXFLOW_SPOT_ID_BASE = 10_000;
241
+ /**
242
+ * 24 h USD notional volume per perp coin id from allMarketTickers. "" (no trades) → null, never
243
+ * "0"; a non-list answer or a row without a numeric instrumentId is EXCHANGE_ERROR.
244
+ */
245
+ export async function fetchTxflowDayVolumes(client) {
246
+ const res = await client.info({ type: "allMarketTickers" });
247
+ if (!Array.isArray(res))
248
+ throw txflowShapeError("allMarketTickers is not an array");
249
+ const out = new Map();
250
+ for (const r of res) {
251
+ const x = r;
252
+ if (typeof x.instrumentId !== "number" || !Number.isInteger(x.instrumentId))
253
+ throw txflowShapeError("allMarketTickers row without a numeric instrumentId");
254
+ if (x.instrumentId >= TXFLOW_SPOT_ID_BASE)
255
+ continue;
256
+ out.set(x.instrumentId, decOrNull(x.dayNtlVlm));
257
+ }
258
+ return out;
259
+ }
260
+ export function toTxflowVenueMarket(m, ctx, volume24h) {
261
+ return {
262
+ venue: "txflow",
263
+ venueSymbol: String(m.index),
264
+ symbol: m.symbol,
265
+ canonical: m.canonical,
266
+ assetClass: m.assetClass,
267
+ settle: "USDC",
268
+ quoteAsset: m.quote,
269
+ markPrice: ctx.markPx,
270
+ indexPrice: ctx.oraclePx,
271
+ fundingRate: hourlyFundingOf(ctx),
272
+ fundingRateBasis: TXFLOW_FUNDING_BASIS,
273
+ settlementIntervalHours: 1,
274
+ // Base units (as Hyperliquid publishes it) [inferred].
275
+ openInterest: ctx.openInterest,
276
+ volume24h,
277
+ tickSize: m.tickSize,
278
+ stepSize: m.stepSize,
279
+ minSize: null,
280
+ minNotional: null,
281
+ maxLeverage: m.maxLeverage,
282
+ marginTiers: null,
283
+ session: null,
284
+ tradable: !m.halted,
285
+ };
286
+ }
287
+ const DEFAULT_DEPS = {
288
+ wsFactory: defaultWsFactory,
289
+ warn: msg => process.stderr.write(`${msg}\n`),
290
+ };
291
+ function clientFor(net) {
292
+ const cfg = networkConfig(txflowVenue, net); // testnet → NOT_IMPLEMENTED (no testnet)
293
+ if (cfg.wsUrl === null)
294
+ throw new Error("txflow venue definition has no wsUrl");
295
+ return { client: new TxflowClient(cfg.restUrl, txflowVenue.rateLimit), wsUrl: cfg.wsUrl };
296
+ }
297
+ /** Directory + one snapshot, joined; markets missing from the snapshot are dropped with a warning. */
298
+ async function fetchJoined(net, deps) {
299
+ const { client, wsUrl } = clientFor(net);
300
+ const [dir, ctxs] = await Promise.all([fetchTxflowDirectory(client), fetchAllAssetCtxsOnce(wsUrl, deps.wsFactory)]);
301
+ const joined = joinSnapshot(dir, ctxs, deps.warn);
302
+ const out = [];
303
+ const missing = [];
304
+ for (const meta of dir) {
305
+ const ctx = joined.get(meta.index);
306
+ if (ctx === undefined) {
307
+ missing.push(meta.symbol);
308
+ continue;
309
+ }
310
+ out.push({ meta, ctx });
311
+ }
312
+ if (missing.length > 0)
313
+ deps.warn(`[txflow] ${missing.length} listed market(s) had no snapshot entry and are not shown: ${missing.join(", ")}`);
314
+ return out;
315
+ }
316
+ export async function fetchTxflowMarkets(net, deps = DEFAULT_DEPS) {
317
+ const { client } = clientFor(net); // testnet → NOT_IMPLEMENTED before any request
318
+ const [joined, volumes] = await Promise.all([fetchJoined(net, deps), fetchTxflowDayVolumes(client)]);
319
+ // A market absent from allMarketTickers has no published volume → null.
320
+ return joined.map(({ meta, ctx }) => toTxflowVenueMarket(meta, ctx, volumes.has(meta.index) ? volumes.get(meta.index) : null));
321
+ }
322
+ export async function fetchTxflowFundingRates(net, deps = DEFAULT_DEPS) {
323
+ const out = [];
324
+ for (const { meta, ctx } of await fetchJoined(net, deps)) {
325
+ const rate = hourlyFundingOf(ctx);
326
+ if (rate === null)
327
+ continue;
328
+ out.push({
329
+ venue: "txflow",
330
+ venueSymbol: String(meta.index),
331
+ symbol: meta.symbol,
332
+ canonical: meta.canonical,
333
+ rate,
334
+ basis: TXFLOW_FUNDING_BASIS,
335
+ markPrice: ctx.markPx,
336
+ nextFundingTime: ctx.nextFundingTime,
337
+ });
338
+ }
339
+ return out;
340
+ }
341
+ export const txflowPublicData = {
342
+ fetchMarkets: net => fetchTxflowMarkets(net),
343
+ fetchFundingRates: net => fetchTxflowFundingRates(net),
344
+ };
345
+ /** Resolve a CLI symbol ("btc", "BTC-USDC", "BTC-PERP", or the numeric coin id) against the directory. */
346
+ export function resolveTxflowMarket(dir, input) {
347
+ const up = input.trim().toUpperCase().replace(/-PERP$/, "");
348
+ const hit = dir.find(m => m.symbol === up || m.name.toUpperCase() === up || String(m.index) === up);
349
+ if (hit === undefined) {
350
+ throw new PerpError("SYMBOL_NOT_FOUND", `TxFlow has no listed market ${input}`, {
351
+ exchange: "txflow", remediation: "List markets: perp -e txflow market list",
352
+ });
353
+ }
354
+ return hit;
355
+ }