perp-cli 0.12.17 → 0.14.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 (361) 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 +286 -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 +286 -0
  101. package/dist/commands/funds.d.ts +25 -0
  102. package/dist/commands/funds.js +939 -245
  103. package/dist/commands/history.js +72 -45
  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 +316 -337
  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 +219 -6
  166. package/dist/exchanges/aster.js +1023 -153
  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 +166 -0
  200. package/dist/exchanges/ondo/adapter.js +835 -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 +122 -0
  206. package/dist/exchanges/ondo/funds.js +290 -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 +288 -4
  218. package/dist/exchanges/pacifica.js +1013 -76
  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 +1383 -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 +34 -0
  270. package/dist/exchanges/venues/ondo.js +140 -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 +28 -0
  274. package/dist/exchanges/venues/qfex.js +132 -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 +820 -402
  290. package/dist/mcp-trade-guard.d.ts +16 -0
  291. package/dist/mcp-trade-guard.js +59 -0
  292. package/dist/pacifica/client.d.ts +41 -10
  293. package/dist/pacifica/client.js +167 -63
  294. package/dist/pacifica/constants.d.ts +0 -1
  295. package/dist/pacifica/constants.js +0 -5
  296. package/dist/pacifica/deposit.d.ts +30 -3
  297. package/dist/pacifica/deposit.js +119 -2
  298. package/dist/pacifica/faucet.d.ts +29 -0
  299. package/dist/pacifica/faucet.js +100 -0
  300. package/dist/pacifica/sign-tx.d.ts +15 -0
  301. package/dist/pacifica/sign-tx.js +41 -0
  302. package/dist/pacifica/types/account.d.ts +28 -4
  303. package/dist/pacifica/types/lake.d.ts +20 -5
  304. package/dist/pacifica/types/lake.js +4 -0
  305. package/dist/pacifica/types/market.d.ts +5 -0
  306. package/dist/pacifica/types/order.d.ts +4 -1
  307. package/dist/pacifica/types/position.d.ts +9 -1
  308. package/dist/plan-executor.d.ts +10 -5
  309. package/dist/plan-executor.js +165 -54
  310. package/dist/position-history.js +13 -0
  311. package/dist/rebalance.d.ts +26 -2
  312. package/dist/rebalance.js +86 -7
  313. package/dist/relay-swap.d.ts +232 -0
  314. package/dist/relay-swap.js +630 -0
  315. package/dist/risk.js +5 -1
  316. package/dist/secret-env.d.ts +15 -0
  317. package/dist/secret-env.js +36 -0
  318. package/dist/settings.d.ts +169 -6
  319. package/dist/settings.js +219 -38
  320. package/dist/signer/ows-evm.d.ts +13 -0
  321. package/dist/signer/ows-evm.js +22 -0
  322. package/dist/smart-order.d.ts +9 -0
  323. package/dist/smart-order.js +53 -8
  324. package/dist/strategies/dca.d.ts +6 -2
  325. package/dist/strategies/dca.js +66 -26
  326. package/dist/strategies/funding-arb.d.ts +11 -1
  327. package/dist/strategies/funding-arb.js +202 -89
  328. package/dist/strategies/grid.d.ts +31 -0
  329. package/dist/strategies/grid.js +89 -11
  330. package/dist/strategies/pending-stop.d.ts +8 -0
  331. package/dist/strategies/pending-stop.js +12 -0
  332. package/dist/strategies/split-order.d.ts +45 -17
  333. package/dist/strategies/split-order.js +157 -91
  334. package/dist/strategies/trailing-stop.d.ts +2 -1
  335. package/dist/strategies/trailing-stop.js +52 -6
  336. package/dist/strategies/twap.d.ts +6 -1
  337. package/dist/strategies/twap.js +39 -11
  338. package/dist/trade-validator.d.ts +22 -0
  339. package/dist/trade-validator.js +119 -15
  340. package/dist/utils/numeric.d.ts +46 -0
  341. package/dist/utils/numeric.js +79 -0
  342. package/dist/utils.d.ts +19 -0
  343. package/dist/utils.js +96 -19
  344. package/dist/withdraw-gate.d.ts +49 -0
  345. package/dist/withdraw-gate.js +131 -0
  346. package/package.json +15 -6
  347. package/skills/perp-cli/SKILL.md +142 -55
  348. package/skills/perp-cli/references/agent-operations.md +110 -56
  349. package/skills/perp-cli/references/commands.md +266 -71
  350. package/skills/perp-cli/references/strategies.md +34 -31
  351. package/skills/perp-cli/scripts/arb-monitor.sh +10 -5
  352. package/skills/perp-cli/scripts/funding-analysis.sh +14 -9
  353. package/skills/perp-cli/scripts/preflight.sh +21 -14
  354. package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
  355. package/skills/perp-cli/scripts/validate-arb.sh +31 -15
  356. package/vendor/lighter-signer/SHA256SUMS +2 -0
  357. package/vendor/lighter-signer/VERSION +8 -0
  358. package/vendor/lighter-signer/lighter-signer.wasm +0 -0
  359. package/vendor/lighter-signer/wasm_exec.js +575 -0
  360. package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
  361. package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Ondo Perps funding (mainnet only) + withdrawal address book.
3
+ *
4
+ * Deposit = a plain ERC-20 transfer of USDC to the account's own funding address
5
+ * (docs.ondoperps.xyz/funding-ondo-perps, api-reference/integration_guide "Deposit USDC").
6
+ * - The address comes from POST /v1/wallet/deposit_address/list {coins:["USDC"]}: the FIRST
7
+ * listed row for (USDC, chosen network, wallet "margin"), whose depositDestination.id must be
8
+ * this account's id (GET /v1/account accountID) — anything else is refused.
9
+ * - POST /v1/provision_address creates ANOTHER address on every call, so it is called only when
10
+ * the list has no address for that network.
11
+ * - USDC token addresses / chain ids / RPCs are pinned in the venue definition
12
+ * (venues/ondo.ts ONDO_DEPOSIT_CHAINS); decimals are read on-chain.
13
+ * - With a probe amount the transfer is two-step: probe → wait until GET /v1/wallet/deposits
14
+ * shows it confirmed → remainder. A probe that is not credited within the window stops the
15
+ * flow (ORDER_PENDING with the probe tx hash); the remainder is NOT sent.
16
+ *
17
+ * Address book: withdrawals go only to addresses on the account's address book. An address is
18
+ * added by signing a SIWE challenge with the account's wallet
19
+ * (POST /v1/auth/erc-4361/address_book/get_challenge → personal_sign → …/complete_challenge).
20
+ */
21
+ import { formatUnits, parseUnits, verifyMessage } from "ethers";
22
+ import { PerpError } from "../../errors.js";
23
+ import { ERC20, EVM_ADDRESS_RE, assertRpcChainId, erc20Balance, erc20Decimals, evmProvider, sameAddress, sendMasterTx, } from "../../evm-master-tx.js";
24
+ import { unsupported } from "../trading-v2.js";
25
+ import { ONDO_DEPOSIT_CHAINS } from "../venues/ondo.js";
26
+ export const ONDO_DEPOSIT_CREDIT_TIMEOUT_MS = 600_000;
27
+ export const ONDO_DEPOSIT_CREDIT_POLL_MS = 10_000;
28
+ function venueError(msg) {
29
+ return new PerpError("EXCHANGE_ERROR", `ondo: ${msg}`, { exchange: "ondo" });
30
+ }
31
+ /** Funding by on-chain transfer exists on mainnet only (the sandbox is funded with demo funds). */
32
+ export function requireOndoDepositNetwork(net) {
33
+ if (net === "testnet") {
34
+ throw unsupported("Ondo Perps", "deposit", "the sandbox is funded with demo funds, not on-chain transfers", "Fund the sandbox account in the sandbox app (app.ondoperps-sandbox.xyz)");
35
+ }
36
+ }
37
+ export function ondoDepositChain(raw) {
38
+ const c = raw.toLowerCase();
39
+ if (c === "arbitrum" || c === "ethereum")
40
+ return c;
41
+ throw new PerpError("INVALID_PARAMS", `Unsupported Ondo deposit chain "${raw}"`, { remediation: "Use --chain arbitrum or --chain ethereum (USDC only)" });
42
+ }
43
+ // ── Funding address ──
44
+ /**
45
+ * The first listed USDC funding address of `network` for the margin wallet. Returns null when the
46
+ * list has none for that network (the caller may then provision one). Refuses when the first
47
+ * listed address does not belong to `accountId`.
48
+ */
49
+ export function selectOndoDepositAddress(rows, network, accountId) {
50
+ if (!Array.isArray(rows))
51
+ throw venueError(`deposit_address/list returned ${JSON.stringify(rows).slice(0, 120)}, expected a list`);
52
+ for (const r of rows) {
53
+ const row = (r !== null && typeof r === "object" ? r : {});
54
+ if (row.coin !== "USDC" || row.network !== network)
55
+ continue;
56
+ const dest = row.depositDestination;
57
+ if (dest === null || dest === undefined || dest.wallet !== "margin")
58
+ continue;
59
+ if (String(dest.id) !== accountId) {
60
+ throw venueError(`the first listed USDC ${network} funding address belongs to account ${String(dest.id)}, not this account ${accountId} — refusing to send`);
61
+ }
62
+ if (typeof row.address !== "string" || !EVM_ADDRESS_RE.test(row.address)) {
63
+ throw venueError(`the listed USDC ${network} funding address is not an EVM address (${JSON.stringify(row.address)})`);
64
+ }
65
+ return row.address;
66
+ }
67
+ return null;
68
+ }
69
+ /** Resolve (or, only when none exists, provision) the account's USDC funding address on `network`. */
70
+ export async function resolveOndoDepositAddress(client, network) {
71
+ const account = await client.get("/v1/account", null);
72
+ if (typeof account.accountID !== "string" || account.accountID === "")
73
+ throw venueError("GET /v1/account returned no accountID");
74
+ const accountId = account.accountID;
75
+ const list = () => client.post("/v1/wallet/deposit_address/list", { coins: ["USDC"] });
76
+ const existing = selectOndoDepositAddress(await list(), network, accountId);
77
+ if (existing !== null)
78
+ return { accountId, address: existing, provisioned: false };
79
+ const created = await client.post("/v1/provision_address", {
80
+ network, symbol: "USDC", deposit_destination: { id: accountId, wallet: "margin" },
81
+ });
82
+ if (String(created.accountId) !== accountId) {
83
+ throw venueError(`provision_address answered for account ${String(created.accountId)}, not ${accountId} — refusing to send`);
84
+ }
85
+ const listed = selectOndoDepositAddress(await list(), network, accountId);
86
+ if (listed === null || typeof created.address !== "string" || !sameAddress(listed, created.address)) {
87
+ throw venueError(`the provisioned USDC ${network} address ${String(created.address)} is not the one the address list shows (${String(listed)}) — refusing to send`);
88
+ }
89
+ return { accountId, address: listed, provisioned: true };
90
+ }
91
+ /** Read-only checks on the pinned chain. Throws before anything is signed. */
92
+ export async function planOndoDeposit(p) {
93
+ requireOndoDepositNetwork(p.network);
94
+ if (!EVM_ADDRESS_RE.test(p.depositor)) {
95
+ throw new PerpError("NO_SIGNER_AVAILABLE", "Ondo deposit needs the master wallet address", {
96
+ remediation: "Run with the master wallet: perp --ows <wallet> funds deposit ondo <amount>",
97
+ });
98
+ }
99
+ const pins = ONDO_DEPOSIT_CHAINS[p.chain];
100
+ const rpc = evmProvider(pins.rpcUrl, pins.chainId);
101
+ await assertRpcChainId(rpc, pins.rpcUrl, pins.chainId, "ondo");
102
+ const decimals = await erc20Decimals(rpc, pins.usdc);
103
+ const raw = (x, what) => {
104
+ if (!/^\d+(\.\d+)?$/.test(x) || !(Number(x) > 0))
105
+ throw new PerpError("INVALID_PARAMS", `${what} must be a positive decimal (got "${x}")`);
106
+ try {
107
+ return parseUnits(x, decimals);
108
+ }
109
+ catch {
110
+ throw new PerpError("INVALID_PARAMS", `${what} ${x} has more than ${decimals} decimals (USDC on ${pins.label})`);
111
+ }
112
+ };
113
+ const amountRaw = raw(p.amount, "Deposit amount");
114
+ const probeRaw = p.probe === null ? null : raw(p.probe, "--probe");
115
+ if (probeRaw !== null && probeRaw >= amountRaw) {
116
+ throw new PerpError("INVALID_PARAMS", `--probe ${String(p.probe)} must be smaller than the deposit amount ${p.amount} (the remainder is sent after the probe is credited)`);
117
+ }
118
+ const [tokenBalance, gas] = await Promise.all([erc20Balance(rpc, pins.usdc, p.depositor), rpc.getBalance(p.depositor)]);
119
+ if (tokenBalance < amountRaw) {
120
+ throw new PerpError("INSUFFICIENT_BALANCE", `Wallet ${p.depositor} holds ${formatUnits(tokenBalance, decimals)} USDC on ${pins.label}; the deposit needs ${p.amount}`, {
121
+ exchange: "ondo", remediation: `Fund ${p.depositor} with USDC on ${pins.label} (chainId ${pins.chainId}) first — e.g. perp funds bridge send --to ${p.chain} --to-asset USDC`,
122
+ });
123
+ }
124
+ if (gas === 0n) {
125
+ throw new PerpError("INSUFFICIENT_BALANCE", `Wallet ${p.depositor} has no ETH for gas on ${pins.label}`, {
126
+ exchange: "ondo", remediation: `Fund ${p.depositor} with ETH on ${pins.label} (chainId ${pins.chainId})`,
127
+ });
128
+ }
129
+ return {
130
+ network: p.network, chain: p.chain, chainId: pins.chainId, chainLabel: pins.label, rpcUrl: pins.rpcUrl, token: pins.usdc, decimals,
131
+ amount: p.amount, amountRaw, probe: p.probe, probeRaw, depositor: p.depositor,
132
+ depositAddress: p.funding.address, accountId: p.funding.accountId, tokenBalance,
133
+ };
134
+ }
135
+ /**
136
+ * Wait until GET /v1/wallet/deposits lists this transaction as `confirmed`. Not listed / pending /
137
+ * unreadable until the deadline → ORDER_PENDING with the tx hash (the transfer itself is mined).
138
+ */
139
+ export async function awaitOndoDepositCredit(readDeposits, txHash, what, timing) {
140
+ const deadline = timing.now() + timing.timeoutMs;
141
+ let last = "not listed yet";
142
+ for (;;) {
143
+ try {
144
+ const rows = await readDeposits();
145
+ if (!Array.isArray(rows))
146
+ throw venueError(`GET /v1/wallet/deposits returned ${JSON.stringify(rows).slice(0, 120)}, expected a list`);
147
+ const row = rows.find(r => typeof r?.txid === "string" && r.txid.toLowerCase() === txHash.toLowerCase());
148
+ if (row !== undefined) {
149
+ if (row.status === "confirmed")
150
+ return;
151
+ last = `status ${String(row.status)}`;
152
+ }
153
+ }
154
+ catch (e) {
155
+ last = e instanceof Error ? e.message : String(e);
156
+ }
157
+ if (timing.now() + timing.pollMs > deadline)
158
+ break;
159
+ await timing.sleep(timing.pollMs);
160
+ }
161
+ throw new PerpError("ORDER_PENDING", `ondo: ${what} ${txHash} is mined but not yet credited by Ondo Perps after ${timing.timeoutMs / 1000}s (${last})`, {
162
+ exchange: "ondo", txHash,
163
+ remediation: "Do NOT send again — the transfer is on-chain and deposits can take longer to confirm. Check: perp -e ondo account balance (or the Ondo Perps app → deposits)",
164
+ });
165
+ }
166
+ /**
167
+ * Send the USDC transfer(s). With a probe: probe → credited → remainder → credited; the
168
+ * remainder is never sent while the probe is uncredited (the ORDER_PENDING carries `sent`).
169
+ */
170
+ export async function executeOndoDeposit(plan, sign, readDeposits, timing) {
171
+ const rpc = evmProvider(plan.rpcUrl, plan.chainId);
172
+ const transfers = [];
173
+ const send = async (step, raw) => {
174
+ const amount = formatUnits(raw, plan.decimals);
175
+ const { txHash } = await sendMasterTx(rpc, {
176
+ venue: "ondo", chainLabel: plan.chainLabel, chainId: plan.chainId, from: plan.depositor, to: plan.token, value: 0n,
177
+ data: ERC20.encodeFunctionData("transfer", [plan.depositAddress, raw]), label: `USDC ${step} transfer (${amount})`,
178
+ }, sign);
179
+ transfers.push({ step, txHash, amount });
180
+ try {
181
+ await awaitOndoDepositCredit(readDeposits, txHash, `${step} transfer`, timing);
182
+ }
183
+ catch (e) {
184
+ if (e instanceof PerpError && e.structured.code === "ORDER_PENDING") {
185
+ const notSent = step === "probe" ? ` The remaining ${formatUnits(plan.amountRaw - raw, plan.decimals)} USDC was NOT sent.` : "";
186
+ throw new PerpError("ORDER_PENDING", `${e.message}.${notSent}`, {
187
+ exchange: "ondo", txHash, sent: transfers, depositAddress: plan.depositAddress,
188
+ remediation: e.structured.remediation,
189
+ });
190
+ }
191
+ throw e;
192
+ }
193
+ };
194
+ if (plan.probeRaw !== null) {
195
+ await send("probe", plan.probeRaw);
196
+ await send("deposit", plan.amountRaw - plan.probeRaw);
197
+ }
198
+ else {
199
+ await send("deposit", plan.amountRaw);
200
+ }
201
+ return { transfers, depositAddress: plan.depositAddress };
202
+ }
203
+ // ── Address book ──
204
+ const ADDRESS_BOOK_UI = "add the address in the Ondo Perps web app (Withdraw → address book)";
205
+ /** Withdrawal addresses on the account's address book (GET /v1/wallet/address_book). */
206
+ export async function ondoAddressBook(client) {
207
+ const res = await client.get("/v1/wallet/address_book", null);
208
+ if (!Array.isArray(res.addressBook))
209
+ throw venueError("GET /v1/wallet/address_book returned no addressBook list");
210
+ return res.addressBook.map(e => {
211
+ if (typeof e?.withdrawalAddress !== "string")
212
+ throw venueError("address book entry has no withdrawalAddress");
213
+ return e.withdrawalAddress;
214
+ });
215
+ }
216
+ /** Refusal for a withdraw destination that is not on the address book (nothing was sent). */
217
+ export function ondoAddressNotOnBook(destination, book) {
218
+ return new PerpError("INVALID_PARAMS", `${destination} is not on the Ondo Perps withdrawal address book (${book.length === 0 ? "the book is empty" : `listed: ${book.join(", ")}`}) — nothing was sent`, {
219
+ exchange: "ondo",
220
+ remediation: `Ondo only withdraws to address-book entries. An entry is added by signing a SIWE challenge with the account's wallet (api-reference/auth/get-siwe-address-book-challenge + complete). For the master wallet's own address run: perp funds address-book add ondo ${destination} — otherwise ${ADDRESS_BOOK_UI}`,
221
+ });
222
+ }
223
+ /** A challenge the master key will sign must be about this address book entry — never an arbitrary message. */
224
+ export function assertOndoAddressBookChallenge(message, wallet, withdrawalAddress, restUrl) {
225
+ const refuse = (why) => {
226
+ throw new PerpError("POLICY_DENIED", `Ondo address-book challenge refused: ${why}. Nothing was signed.`, {
227
+ exchange: "ondo", challenge: message.slice(0, 600), remediation: `If this message is expected, ${ADDRESS_BOOK_UI}`,
228
+ });
229
+ };
230
+ if (!message.toLowerCase().includes(withdrawalAddress.toLowerCase()))
231
+ refuse(`the message does not name the withdrawal address ${withdrawalAddress}`);
232
+ // The endpoint is POST /v1/auth/erc-4361/address_book/get_challenge — "Get a Sign-In with Ethereum
233
+ // challenge for adding a withdrawal address" (Ondo REST spec). ERC-4361 shape:
234
+ // "<domain> wants you to sign in with your Ethereum account:\n<address>\n…". Any other message
235
+ // is refused: the master key never signs free-form text.
236
+ const siwe = /^(\S+) wants you to sign in with your Ethereum account:\n(0x[0-9a-fA-F]{40})\n/.exec(message);
237
+ if (siwe === null)
238
+ return refuse("the message is not a Sign-In with Ethereum (ERC-4361) message");
239
+ const venueHost = new URL(restUrl).hostname.split(".").slice(-2).join(".");
240
+ const domain = siwe[1].replace(/^[a-z]+:\/\//, "").split(":")[0];
241
+ if (domain !== venueHost && !domain.endsWith(`.${venueHost}`))
242
+ refuse(`its domain "${siwe[1]}" is not ${venueHost}`);
243
+ if (!sameAddress(siwe[2], wallet))
244
+ refuse(`it is addressed to ${siwe[2]}, not the signing wallet ${wallet}`);
245
+ }
246
+ /** A venue refusal of the SIWE address-book endpoints, re-raised with the web-app route. */
247
+ function addressBookAuthError(e, step) {
248
+ if (!(e instanceof PerpError) || e.structured.code === "ORDER_PENDING")
249
+ return e;
250
+ return new PerpError(e.structured.code, `${e.message} — Ondo refused the address-book ${step} with API-key auth`, {
251
+ ...e.structured.details, exchange: "ondo",
252
+ remediation: `These endpoints may need a wallet (SIWE/JWT) session instead of an API key: ${ADDRESS_BOOK_UI}`,
253
+ });
254
+ }
255
+ /**
256
+ * Add the signing wallet's own address to the address book: get_challenge → personal_sign
257
+ * (EIP-191) with the wallet → complete_challenge. Challenge chainId "1" (enum: 1 | 43114).
258
+ */
259
+ export async function addOndoAddressBookEntry(client, p) {
260
+ const before = await ondoAddressBook(client);
261
+ if (before.some(a => sameAddress(a, p.wallet)))
262
+ return { address: p.wallet, alreadyListed: true, listed: true, challengeId: null };
263
+ let challenge;
264
+ try {
265
+ challenge = await client.post("/v1/auth/erc-4361/address_book/get_challenge", {
266
+ walletAddress: p.wallet, chainId: "1", withdrawalAddress: p.wallet,
267
+ });
268
+ }
269
+ catch (e) {
270
+ throw addressBookAuthError(e, "challenge");
271
+ }
272
+ if (typeof challenge.id !== "string" || typeof challenge.message !== "string" || challenge.message === "") {
273
+ throw venueError("address_book/get_challenge returned no id / message");
274
+ }
275
+ assertOndoAddressBookChallenge(challenge.message, p.wallet, p.wallet, p.restUrl);
276
+ const signature = await p.signMessage(challenge.message);
277
+ if (!sameAddress(verifyMessage(challenge.message, signature), p.wallet)) {
278
+ throw new PerpError("SIGNATURE_FAILED", `The address-book challenge signature does not recover to ${p.wallet} — nothing was submitted`, { exchange: "ondo" });
279
+ }
280
+ try {
281
+ await client.postNoResult("/v1/auth/erc-4361/address_book/complete_challenge", {
282
+ id: challenge.id, signature, ...(p.label !== null ? { addressLabel: p.label } : {}),
283
+ });
284
+ }
285
+ catch (e) {
286
+ throw addressBookAuthError(e, "completion");
287
+ }
288
+ const after = await ondoAddressBook(client);
289
+ return { address: p.wallet, alreadyListed: false, listed: after.some(a => sameAddress(a, p.wallet)), challengeId: challenge.id };
290
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Ondo Perps public market data (design §10.6).
3
+ *
4
+ * Sources (all public, live-verified 2026-09-30):
5
+ * GET /v1/markets result.perps.tradingPairs[] — increments, margin
6
+ * brackets, tags (asset class), trading schedule
7
+ * GET /v1/perps/contracts result[] — ticker, funding, OI, volume, status
8
+ * GET /v1/perps/mark_prices result{market: MarkPrice} — mark price and the
9
+ * weekend discovery band (discoveryLower/Upper,
10
+ * inPriceDiscovery; not in the OpenAPI spec but
11
+ * returned live)
12
+ *
13
+ * Funding is hourly: `fundingRate` is a 1h rate (docs funding-rates), so the
14
+ * basis is perHours 1 and settlement is every hour.
15
+ *
16
+ * Symbols: venue symbol "TSLA-USD.P" → CLI symbol "TSLA". Spot pairs
17
+ * (result.spot) are not perp markets; OndoSpotAdapter reads them via
18
+ * fetchOndoSpotTradingPairs.
19
+ */
20
+ import type { VenuePublicData } from "../venue.js";
21
+ import type { AssetClass, FundingRateBasis, Network } from "../venue-types.js";
22
+ import { OndoClient } from "./client.js";
23
+ export declare const ONDO_PERP_SUFFIX = "-USD.P";
24
+ export declare const ONDO_FUNDING_BASIS: FundingRateBasis;
25
+ export declare const ONDO_SETTLEMENT_HOURS = 1;
26
+ export interface OndoMarginBracket {
27
+ positionBracketUsd: string;
28
+ maxLeverage: string;
29
+ maintenanceMarginRate: string;
30
+ maintenanceAmount: string;
31
+ }
32
+ export interface OndoTradingPair {
33
+ market: string;
34
+ pair: {
35
+ base: string;
36
+ quote: string;
37
+ };
38
+ baseIncrement: string;
39
+ quoteIncrement: string;
40
+ marginInfo: OndoMarginBracket[];
41
+ tags: string[] | null;
42
+ /** Present on listings with a trading schedule (non-crypto); absent for 24/7 crypto. */
43
+ schedule?: unknown;
44
+ /** Present (true) on delisted / paused listings. */
45
+ disabled?: boolean;
46
+ }
47
+ /** Spot pair from /v1/markets result.spot.tradingPairs (live; not in the OpenAPI spec). */
48
+ export interface OndoSpotTradingPair {
49
+ market: string;
50
+ pair: {
51
+ base: string;
52
+ quote: string;
53
+ };
54
+ longName?: string;
55
+ baseIncrement: string;
56
+ quoteIncrement: string;
57
+ makerFee: string;
58
+ takerFee: string;
59
+ tags: string[] | null;
60
+ /** Present (true) on pairs that are listed but not tradable. */
61
+ disabled?: boolean;
62
+ }
63
+ export interface OndoContract {
64
+ market: string;
65
+ disabled: boolean;
66
+ lastPrice?: string;
67
+ usdVolume?: string;
68
+ openInterest?: string;
69
+ indexPrice?: string;
70
+ fundingRate?: string;
71
+ nextFundingRateTimestamp?: string;
72
+ isClosed?: boolean;
73
+ reduceOnly?: boolean;
74
+ tags?: string[];
75
+ }
76
+ export interface OndoMarkPrice {
77
+ market: string;
78
+ markPrice: string;
79
+ oraclePrice: string;
80
+ discoveryLower?: string;
81
+ discoveryUpper?: string;
82
+ inPriceDiscovery?: boolean;
83
+ }
84
+ export declare function ondoAssetClass(market: string, tags: readonly string[] | null | undefined): AssetClass;
85
+ /** "TSLA-USD.P" → "TSLA". Throws for non-perp symbols. */
86
+ export declare function ondoPlainSymbol(venueSymbol: string): string;
87
+ /** CLI symbol ("tsla", "TSLA", "TSLA-USD.P") → venue symbol "TSLA-USD.P". */
88
+ export declare function ondoVenueSymbol(symbol: string): string;
89
+ /** Empty string / absent → null. Ondo omits or blanks fields it does not publish. */
90
+ export declare function strOrNull(v: string | undefined | null): string | null;
91
+ export declare function parseOndoTime(iso: string, what: string): number;
92
+ export declare function ondoPublicClient(net: Network): OndoClient;
93
+ export declare function fetchOndoTradingPairs(client: OndoClient): Promise<OndoTradingPair[]>;
94
+ export declare function fetchOndoSpotTradingPairs(client: OndoClient): Promise<OndoSpotTradingPair[]>;
95
+ export declare const ondoPublicData: VenuePublicData;
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Ondo Perps public market data (design §10.6).
3
+ *
4
+ * Sources (all public, live-verified 2026-09-30):
5
+ * GET /v1/markets result.perps.tradingPairs[] — increments, margin
6
+ * brackets, tags (asset class), trading schedule
7
+ * GET /v1/perps/contracts result[] — ticker, funding, OI, volume, status
8
+ * GET /v1/perps/mark_prices result{market: MarkPrice} — mark price and the
9
+ * weekend discovery band (discoveryLower/Upper,
10
+ * inPriceDiscovery; not in the OpenAPI spec but
11
+ * returned live)
12
+ *
13
+ * Funding is hourly: `fundingRate` is a 1h rate (docs funding-rates), so the
14
+ * basis is perHours 1 and settlement is every hour.
15
+ *
16
+ * Symbols: venue symbol "TSLA-USD.P" → CLI symbol "TSLA". Spot pairs
17
+ * (result.spot) are not perp markets; OndoSpotAdapter reads them via
18
+ * fetchOndoSpotTradingPairs.
19
+ */
20
+ import { PerpError } from "../../errors.js";
21
+ import { canonicalFor } from "../canonical-symbols.js";
22
+ import { networkConfig } from "../venue.js";
23
+ import { ondoVenue } from "../venues/ondo.js";
24
+ import { OndoClient } from "./client.js";
25
+ export const ONDO_PERP_SUFFIX = "-USD.P";
26
+ export const ONDO_FUNDING_BASIS = { kind: "perHours", hours: 1 };
27
+ export const ONDO_SETTLEMENT_HOURS = 1;
28
+ // ── Helpers ──
29
+ const TAG_TO_ASSET_CLASS = {
30
+ Crypto: "crypto",
31
+ Stock: "equity",
32
+ ETF: "etf",
33
+ Index: "index",
34
+ Commodity: "commodity",
35
+ FX: "fx",
36
+ };
37
+ export function ondoAssetClass(market, tags) {
38
+ if (tags === null || tags === undefined) {
39
+ throw new PerpError("EXCHANGE_ERROR", `Ondo Perps: ${market} has no tags; cannot classify asset class`);
40
+ }
41
+ const known = tags.map(t => TAG_TO_ASSET_CLASS[t]).filter((c) => c !== undefined);
42
+ if (known.length !== 1) {
43
+ throw new PerpError("EXCHANGE_ERROR", `Ondo Perps: cannot classify ${market} from tags ${JSON.stringify(tags)}`, {
44
+ remediation: "Update TAG_TO_ASSET_CLASS in src/exchanges/ondo/public.ts for the new venue tag",
45
+ });
46
+ }
47
+ return known[0];
48
+ }
49
+ /** "TSLA-USD.P" → "TSLA". Throws for non-perp symbols. */
50
+ export function ondoPlainSymbol(venueSymbol) {
51
+ if (!venueSymbol.endsWith(ONDO_PERP_SUFFIX)) {
52
+ throw new PerpError("EXCHANGE_ERROR", `Ondo Perps: unexpected perp market symbol "${venueSymbol}"`);
53
+ }
54
+ return venueSymbol.slice(0, -ONDO_PERP_SUFFIX.length);
55
+ }
56
+ /** CLI symbol ("tsla", "TSLA", "TSLA-USD.P") → venue symbol "TSLA-USD.P". */
57
+ export function ondoVenueSymbol(symbol) {
58
+ const up = symbol.trim().toUpperCase();
59
+ if (up.length === 0)
60
+ throw new PerpError("INVALID_PARAMS", "symbol is empty");
61
+ return up.endsWith(ONDO_PERP_SUFFIX) ? up : `${up}${ONDO_PERP_SUFFIX}`;
62
+ }
63
+ /** Empty string / absent → null. Ondo omits or blanks fields it does not publish. */
64
+ export function strOrNull(v) {
65
+ return v === undefined || v === null || v === "" ? null : v;
66
+ }
67
+ export function parseOndoTime(iso, what) {
68
+ const t = Date.parse(iso);
69
+ if (!Number.isFinite(t))
70
+ throw new PerpError("EXCHANGE_ERROR", `Ondo Perps: invalid ${what} timestamp "${iso}"`);
71
+ return t;
72
+ }
73
+ function marginTiersOf(pair) {
74
+ if (pair.marginInfo.length === 0) {
75
+ throw new PerpError("EXCHANGE_ERROR", `Ondo Perps: ${pair.market} has no marginInfo brackets`);
76
+ }
77
+ // Brackets are ascending by positionBracketUsd (the bracket's upper bound).
78
+ const tiers = [];
79
+ let lower = 0;
80
+ for (const b of pair.marginInfo) {
81
+ const lev = Number(b.maxLeverage);
82
+ const upper = Number(b.positionBracketUsd);
83
+ if (!Number.isFinite(lev) || lev <= 0 || !Number.isFinite(upper)) {
84
+ throw new PerpError("EXCHANGE_ERROR", `Ondo Perps: ${pair.market} invalid margin bracket ${JSON.stringify(b)}`);
85
+ }
86
+ tiers.push({ lowerBound: lower, maxLeverage: lev });
87
+ lower = upper;
88
+ }
89
+ return { maxLeverage: Math.max(...tiers.map(t => t.maxLeverage)), tiers };
90
+ }
91
+ /**
92
+ * Weekend/holiday discovery band (docs weekend-trading). Only non-crypto
93
+ * listings carry a schedule; for 24/7 crypto the session is null.
94
+ * `inPriceDiscovery` = external feeds unavailable (underlying closed) and the
95
+ * mark is bounded by [discoveryLower, discoveryUpper].
96
+ */
97
+ function sessionOf(pair, mark) {
98
+ if (pair.schedule === undefined || pair.schedule === null)
99
+ return null;
100
+ if (typeof mark.inPriceDiscovery !== "boolean")
101
+ return null;
102
+ if (!mark.inPriceDiscovery)
103
+ return { underlyingOpen: true, priceBand: null, marginMultiplier: null };
104
+ const lower = strOrNull(mark.discoveryLower);
105
+ const upper = strOrNull(mark.discoveryUpper);
106
+ return {
107
+ underlyingOpen: false,
108
+ priceBand: lower !== null && upper !== null ? { lower, upper } : null,
109
+ marginMultiplier: null,
110
+ };
111
+ }
112
+ // ── Fetchers ──
113
+ export function ondoPublicClient(net) {
114
+ return new OndoClient({
115
+ restUrl: networkConfig(ondoVenue, net).restUrl,
116
+ network: net,
117
+ rateLimit: ondoVenue.rateLimit,
118
+ credentials: null,
119
+ });
120
+ }
121
+ export async function fetchOndoTradingPairs(client) {
122
+ const r = await client.publicGet("/v1/markets", null);
123
+ return r.perps.tradingPairs;
124
+ }
125
+ export async function fetchOndoSpotTradingPairs(client) {
126
+ const r = await client.publicGet("/v1/markets", null);
127
+ if (r.spot === undefined || !Array.isArray(r.spot.tradingPairs)) {
128
+ throw new PerpError("EXCHANGE_ERROR", "Ondo Perps: /v1/markets has no result.spot.tradingPairs");
129
+ }
130
+ return r.spot.tradingPairs;
131
+ }
132
+ function byMarket(rows) {
133
+ return new Map(rows.map(r => [r.market, r]));
134
+ }
135
+ function requireRow(m, market, source) {
136
+ const row = m.get(market);
137
+ if (row === undefined) {
138
+ throw new PerpError("EXCHANGE_ERROR", `Ondo Perps: ${market} is missing from ${source}`);
139
+ }
140
+ return row;
141
+ }
142
+ async function fetchMarkets(net) {
143
+ const client = ondoPublicClient(net);
144
+ const pairs = await fetchOndoTradingPairs(client);
145
+ const contracts = byMarket(await client.publicGet("/v1/perps/contracts", null));
146
+ const marks = await client.publicGet("/v1/perps/mark_prices", null);
147
+ // A listing with no mark price has no price feed (seen on the sandbox, e.g.
148
+ // SHEIN/COPPER/NATGAS with lastPrice 0). It cannot be priced or traded, so it
149
+ // is excluded with a warning rather than given a made-up mark.
150
+ const noMark = pairs.filter(p => marks[p.market] === undefined).map(p => p.market);
151
+ if (noMark.length > 0) {
152
+ console.warn(`[ondo] excluded ${noMark.length} market(s) without a mark price: ${noMark.join(", ")}`);
153
+ }
154
+ return pairs.filter(p => marks[p.market] !== undefined).map((pair) => {
155
+ const contract = requireRow(contracts, pair.market, "/v1/perps/contracts");
156
+ const mark = marks[pair.market];
157
+ const symbol = ondoPlainSymbol(pair.market);
158
+ const assetClass = ondoAssetClass(pair.market, pair.tags);
159
+ const { maxLeverage, tiers } = marginTiersOf(pair);
160
+ return {
161
+ venue: "ondo",
162
+ venueSymbol: pair.market,
163
+ symbol,
164
+ canonical: canonicalFor("ondo", symbol, assetClass),
165
+ assetClass,
166
+ settle: "USDC",
167
+ quoteAsset: pair.pair.quote,
168
+ markPrice: mark.markPrice,
169
+ indexPrice: strOrNull(contract.indexPrice),
170
+ fundingRate: strOrNull(contract.fundingRate),
171
+ fundingRateBasis: ONDO_FUNDING_BASIS,
172
+ settlementIntervalHours: ONDO_SETTLEMENT_HOURS,
173
+ openInterest: strOrNull(contract.openInterest),
174
+ volume24h: strOrNull(contract.usdVolume),
175
+ tickSize: pair.quoteIncrement,
176
+ stepSize: pair.baseIncrement,
177
+ minSize: null,
178
+ minNotional: null,
179
+ maxLeverage,
180
+ marginTiers: tiers,
181
+ session: sessionOf(pair, mark),
182
+ tradable: pair.disabled !== true && !contract.disabled && contract.isClosed !== true && contract.reduceOnly !== true,
183
+ };
184
+ });
185
+ }
186
+ async function fetchFundingRates(net) {
187
+ const client = ondoPublicClient(net);
188
+ const contracts = await client.publicGet("/v1/perps/contracts", null);
189
+ const marks = await client.publicGet("/v1/perps/mark_prices", null);
190
+ const out = [];
191
+ for (const c of contracts) {
192
+ if (c.disabled)
193
+ continue; // delisted/paused listings carry a stale rate and a zero last price
194
+ const rate = strOrNull(c.fundingRate);
195
+ if (rate === null) {
196
+ throw new PerpError("EXCHANGE_ERROR", `Ondo Perps: ${c.market} has no fundingRate in /v1/perps/contracts`);
197
+ }
198
+ const symbol = ondoPlainSymbol(c.market);
199
+ const next = strOrNull(c.nextFundingRateTimestamp);
200
+ const mark = marks[c.market];
201
+ out.push({
202
+ venue: "ondo",
203
+ venueSymbol: c.market,
204
+ symbol,
205
+ canonical: canonicalFor("ondo", symbol, ondoAssetClass(c.market, c.tags)),
206
+ rate,
207
+ basis: ONDO_FUNDING_BASIS,
208
+ markPrice: mark === undefined ? null : mark.markPrice,
209
+ nextFundingTime: next === null ? null : parseOndoTime(next, "nextFundingRateTimestamp"),
210
+ });
211
+ }
212
+ return out;
213
+ }
214
+ export const ondoPublicData = { fetchMarkets, fetchFundingRates };