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,127 @@
1
+ /**
2
+ * Aster on-chain deposit + API withdraw helpers (mainnet only).
3
+ *
4
+ * Deposit (github.com/asterdex/api-docs demo/aster-deposit-withdrawal.md): "Call the `depositFor`
5
+ * method of the vault contract … the deposited asset is credited to the `forAddress` account";
6
+ * broker: "pass `1000` to deposit to the spot account; any other value deposits to the futures account".
7
+ *
8
+ * ERC-20 approve(vault, exact amount) → vault.depositFor(address currency, address forAddress, uint256 amount, uint256 broker)
9
+ *
10
+ * with forAddress = the depositor itself and broker = 1 (futures account). NEVER a plain token transfer. Vault / token / chain id are
11
+ * PINNED in the venue definition (venues/aster.ts ASTER_FUNDS_CHAINS). Before anything is signed,
12
+ * read-only checks on the pinned RPC: eth_chainId, on-chain decimals(), supportToken(token)
13
+ * non-zero, paused() false, token balance ≥ amount, native gas > 0, allowance. The deposit tx is
14
+ * simulated from the depositor (eth_estimateGas) after the approve; its receipt must carry
15
+ * Deposit(account = depositor, currency = token, broker = 1); the venue credit is then polled
16
+ * (POST /fapi/v3/aster/deposit-withdraw-history) — a timeout is ORDER_PENDING with the tx hash,
17
+ * never "failed".
18
+ *
19
+ * Withdraw: POST /fapi/v3/aster/user-withdraw — see AsterAdapter.withdraw; this module holds the
20
+ * pure checks (fee / max from user-withdraw-info, signature recovery).
21
+ */
22
+ import { Interface } from "ethers";
23
+ import { type TxSigner } from "../evm-master-tx.js";
24
+ import { buildWithdrawTypedData } from "./aster-typed-data.js";
25
+ import type { Network } from "./venue-types.js";
26
+ import { type AsterFundsAsset, type AsterFundsChain } from "./venues/aster.js";
27
+ export declare const ASTER_VAULT: Interface;
28
+ /** Venue credit polling (deposit-withdraw-history). */
29
+ export declare const ASTER_DEPOSIT_CREDIT_TIMEOUT_MS = 180000;
30
+ export declare const ASTER_DEPOSIT_CREDIT_POLL_MS = 5000;
31
+ /** Deposit / withdraw exist on mainnet only; testnet → uniform CAPABILITY_UNSUPPORTED. */
32
+ export declare function requireAsterFundsNetwork(net: Network, feature: "deposit" | "withdraw"): void;
33
+ export declare function asterFundsChain(raw: string): AsterFundsChain;
34
+ export declare function asterFundsAsset(raw: string): AsterFundsAsset;
35
+ export interface AsterDepositPlan {
36
+ network: Network;
37
+ chain: AsterFundsChain;
38
+ chainId: number;
39
+ chainLabel: string;
40
+ rpcUrl: string;
41
+ vault: string;
42
+ asset: AsterFundsAsset;
43
+ token: string;
44
+ /** Read on-chain (decimals()). */
45
+ decimals: number;
46
+ amount: string;
47
+ amountRaw: bigint;
48
+ depositor: string;
49
+ tokenBalance: bigint;
50
+ allowance: bigint;
51
+ needsApprove: boolean;
52
+ broker: bigint;
53
+ }
54
+ export interface AsterDepositResult {
55
+ approveTxHash: string | null;
56
+ depositTxHash: string;
57
+ /** Amount of the vault's Deposit event (token base units). */
58
+ depositedRaw: string;
59
+ }
60
+ /** Read-only pre-checks on the pinned chain. Throws before anything is signed. */
61
+ export declare function planAsterDeposit(p: {
62
+ network: Network;
63
+ chain: AsterFundsChain;
64
+ asset: AsterFundsAsset;
65
+ amount: string;
66
+ depositor: string;
67
+ }): Promise<AsterDepositPlan>;
68
+ export declare function asterDepositCalldata(plan: Pick<AsterDepositPlan, "token" | "depositor" | "amountRaw" | "broker">): string;
69
+ interface ReceiptLog {
70
+ address: string;
71
+ topics: readonly string[];
72
+ data: string;
73
+ }
74
+ /**
75
+ * The vault's Deposit event for this deposit: account = depositor, currency = token, broker = 1.
76
+ * Returns the event amount; null when the receipt carries no such event.
77
+ */
78
+ export declare function findAsterDepositEvent(logs: readonly ReceiptLog[], plan: Pick<AsterDepositPlan, "vault" | "depositor" | "token" | "broker">): bigint | null;
79
+ /** Sends approve (only when the allowance is short — exact amount) then deposit; verifies the Deposit event. */
80
+ export declare function executeAsterDeposit(plan: AsterDepositPlan, sign: TxSigner): Promise<AsterDepositResult>;
81
+ export interface AsterHistoryRow {
82
+ type: string;
83
+ state: string;
84
+ txHash: string;
85
+ asset: string;
86
+ amount: string;
87
+ accountType: string | null;
88
+ }
89
+ /** Rows of POST /fapi/v3/aster/deposit-withdraw-history (an array; anything else is a venue error). */
90
+ export declare function parseAsterHistory(raw: unknown): AsterHistoryRow[];
91
+ /**
92
+ * Poll the venue's deposit history until this tx is SUCCESS. FAILED → EXCHANGE_ERROR (definite,
93
+ * venue-reported). Not listed / PROCESSING / unreadable until the deadline → ORDER_PENDING with
94
+ * the tx hash (the on-chain deposit is confirmed; only the venue credit is unknown).
95
+ * The history endpoint itself answers -5050 DEPOSIT_REQUIRED until the first deposit is credited —
96
+ * that is "not credited yet", so polling continues.
97
+ */
98
+ export declare function awaitAsterDepositCredit(readHistory: () => Promise<unknown>, txHash: string, timing: {
99
+ timeoutMs: number;
100
+ pollMs: number;
101
+ sleep: (ms: number) => Promise<void>;
102
+ now: () => number;
103
+ }): Promise<AsterHistoryRow>;
104
+ export interface AsterWithdrawLimits {
105
+ /** balances.<asset>.chainBalances.<chainId>.perpMaxWithdrawAmount */
106
+ max: string;
107
+ /** balances.<asset>.chainBalances.<chainId>.withdrawFee */
108
+ fee: string;
109
+ }
110
+ /** Fee + max of POST /fapi/v3/aster/user-withdraw-info for (asset, chain). Missing → refusal, never a default. */
111
+ export declare function parseAsterWithdrawLimits(raw: unknown, asset: string, chainId: number): AsterWithdrawLimits;
112
+ /** amount > fee and amount ≤ max (exact decimal comparison). */
113
+ export declare function assertAsterWithdrawAmount(amount: string, limits: AsterWithdrawLimits, asset: string): void;
114
+ /** The withdraw `userSignature` must recover to the account owner before the request is sent. */
115
+ export declare function assertAsterWithdrawSignature(typed: ReturnType<typeof buildWithdrawTypedData>, signature: string, user: string): void;
116
+ export interface AsterWithdrawResult {
117
+ withdrawId: string;
118
+ /** Digest of the user signature (venue response `hash`). */
119
+ hash: string;
120
+ chain: AsterFundsChain;
121
+ chainId: number;
122
+ asset: string;
123
+ amount: string;
124
+ fee: string;
125
+ receiver: string;
126
+ }
127
+ export {};
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Aster on-chain deposit + API withdraw helpers (mainnet only).
3
+ *
4
+ * Deposit (github.com/asterdex/api-docs demo/aster-deposit-withdrawal.md): "Call the `depositFor`
5
+ * method of the vault contract … the deposited asset is credited to the `forAddress` account";
6
+ * broker: "pass `1000` to deposit to the spot account; any other value deposits to the futures account".
7
+ *
8
+ * ERC-20 approve(vault, exact amount) → vault.depositFor(address currency, address forAddress, uint256 amount, uint256 broker)
9
+ *
10
+ * with forAddress = the depositor itself and broker = 1 (futures account). NEVER a plain token transfer. Vault / token / chain id are
11
+ * PINNED in the venue definition (venues/aster.ts ASTER_FUNDS_CHAINS). Before anything is signed,
12
+ * read-only checks on the pinned RPC: eth_chainId, on-chain decimals(), supportToken(token)
13
+ * non-zero, paused() false, token balance ≥ amount, native gas > 0, allowance. The deposit tx is
14
+ * simulated from the depositor (eth_estimateGas) after the approve; its receipt must carry
15
+ * Deposit(account = depositor, currency = token, broker = 1); the venue credit is then polled
16
+ * (POST /fapi/v3/aster/deposit-withdraw-history) — a timeout is ORDER_PENDING with the tx hash,
17
+ * never "failed".
18
+ *
19
+ * Withdraw: POST /fapi/v3/aster/user-withdraw — see AsterAdapter.withdraw; this module holds the
20
+ * pure checks (fee / max from user-withdraw-info, signature recovery).
21
+ */
22
+ import { Interface, formatUnits, parseUnits, verifyTypedData, zeroPadValue } from "ethers";
23
+ import { venueRateString } from "../utils/numeric.js";
24
+ import { PerpError } from "../errors.js";
25
+ import { ERC20, EVM_ADDRESS_RE, assertRpcChainId, erc20Allowance, erc20Balance, erc20Decimals, evmProvider, sameAddress, sendMasterTx, txOutcomeUnknown, } from "../evm-master-tx.js";
26
+ import { unsupported } from "./trading-v2.js";
27
+ import { ASTER_DEPOSIT_BROKER_PERP, ASTER_FUNDS_CHAINS, ASTER_FUNDS_TESTNET_UNSUPPORTED, } from "./venues/aster.js";
28
+ export const ASTER_VAULT = new Interface([
29
+ "function depositFor(address currency, address forAddress, uint256 amount, uint256 broker) payable",
30
+ "function paused() view returns (bool)",
31
+ "function supportToken(address) view returns (address currency)",
32
+ "event Deposit(address indexed account, address indexed currency, bool isNative, uint256 amount, uint256 broker)",
33
+ ]);
34
+ const DEPOSIT_TOPIC = ASTER_VAULT.getEvent("Deposit").topicHash;
35
+ /** Venue credit polling (deposit-withdraw-history). */
36
+ export const ASTER_DEPOSIT_CREDIT_TIMEOUT_MS = 180_000;
37
+ export const ASTER_DEPOSIT_CREDIT_POLL_MS = 5_000;
38
+ /** Deposit / withdraw exist on mainnet only; testnet → uniform CAPABILITY_UNSUPPORTED. */
39
+ export function requireAsterFundsNetwork(net, feature) {
40
+ if (net === "testnet") {
41
+ throw unsupported("Aster", feature, ASTER_FUNDS_TESTNET_UNSUPPORTED.reason, ASTER_FUNDS_TESTNET_UNSUPPORTED.remediation);
42
+ }
43
+ }
44
+ export function asterFundsChain(raw) {
45
+ const c = raw.toLowerCase();
46
+ if (c === "bsc" || c === "arbitrum")
47
+ return c;
48
+ throw new PerpError("INVALID_PARAMS", `Unsupported Aster chain "${raw}"`, { remediation: "Use --chain bsc or --chain arbitrum" });
49
+ }
50
+ export function asterFundsAsset(raw) {
51
+ const a = raw.toUpperCase();
52
+ if (a === "USDT" || a === "USDC")
53
+ return a;
54
+ throw new PerpError("INVALID_PARAMS", `Unsupported Aster deposit asset "${raw}"`, { remediation: "Use --asset USDT or --asset USDC" });
55
+ }
56
+ function requireDecimal(amount, what) {
57
+ if (!/^\d+(\.\d+)?$/.test(amount) || !(Number(amount) > 0)) {
58
+ throw new PerpError("INVALID_PARAMS", `${what} must be a positive decimal (got "${amount}")`);
59
+ }
60
+ }
61
+ /** Read-only pre-checks on the pinned chain. Throws before anything is signed. */
62
+ export async function planAsterDeposit(p) {
63
+ requireAsterFundsNetwork(p.network, "deposit");
64
+ if (!EVM_ADDRESS_RE.test(p.depositor)) {
65
+ throw new PerpError("NO_SIGNER_AVAILABLE", "Aster deposit needs the master wallet address", {
66
+ remediation: "Run with the master wallet: perp --ows <wallet> funds deposit aster <amount>",
67
+ });
68
+ }
69
+ requireDecimal(p.amount, "Deposit amount");
70
+ const pins = ASTER_FUNDS_CHAINS[p.chain];
71
+ const token = pins.tokens[p.asset];
72
+ const rpc = evmProvider(pins.rpcUrl, pins.chainId);
73
+ await assertRpcChainId(rpc, pins.rpcUrl, pins.chainId, "aster");
74
+ const decimals = await erc20Decimals(rpc, token);
75
+ let amountRaw;
76
+ try {
77
+ amountRaw = parseUnits(p.amount, decimals);
78
+ }
79
+ catch {
80
+ throw new PerpError("INVALID_PARAMS", `Deposit amount ${p.amount} has more than ${decimals} decimals (${p.asset} on ${pins.label})`);
81
+ }
82
+ const [supportRaw, pausedRaw] = await Promise.all([
83
+ rpc.call({ to: pins.vault, data: ASTER_VAULT.encodeFunctionData("supportToken", [token]) }),
84
+ rpc.call({ to: pins.vault, data: ASTER_VAULT.encodeFunctionData("paused", []) }),
85
+ ]);
86
+ // supportToken(address) returns the Token struct; its first word (currency) is zero for an unsupported token.
87
+ if (supportRaw.length < 66 || BigInt(supportRaw.slice(0, 66)) === 0n) {
88
+ throw new PerpError("EXCHANGE_ERROR", `aster: vault ${pins.vault} on ${pins.label} does not list ${p.asset} (${token}) as a supported deposit token — nothing was sent`, { exchange: "aster" });
89
+ }
90
+ if (ASTER_VAULT.decodeFunctionResult("paused", pausedRaw)[0] === true) {
91
+ throw new PerpError("EXCHANGE_ERROR", `aster: vault ${pins.vault} on ${pins.label} is paused — nothing was sent`, {
92
+ exchange: "aster", remediation: "Wait until Aster resumes the vault (status: asterdex.com), then retry",
93
+ });
94
+ }
95
+ const [tokenBalance, allowance, gas] = await Promise.all([
96
+ erc20Balance(rpc, token, p.depositor),
97
+ erc20Allowance(rpc, token, p.depositor, pins.vault),
98
+ rpc.getBalance(p.depositor),
99
+ ]);
100
+ if (tokenBalance < amountRaw) {
101
+ throw new PerpError("INSUFFICIENT_BALANCE", `Wallet ${p.depositor} holds ${formatUnits(tokenBalance, decimals)} ${p.asset} on ${pins.label}; the deposit needs ${p.amount}`, {
102
+ exchange: "aster", remediation: `Fund ${p.depositor} with ${p.asset} on ${pins.label} (chainId ${pins.chainId}) first — e.g. perp funds bridge send --to ${p.chain} --to-asset ${p.asset}`,
103
+ });
104
+ }
105
+ if (gas === 0n) {
106
+ throw new PerpError("INSUFFICIENT_BALANCE", `Wallet ${p.depositor} has no native gas token on ${pins.label}`, {
107
+ exchange: "aster", remediation: `Fund ${p.depositor} with ${p.chain === "bsc" ? "BNB" : "ETH"} on ${pins.label} (chainId ${pins.chainId}) for gas`,
108
+ });
109
+ }
110
+ return {
111
+ network: p.network, chain: p.chain, chainId: pins.chainId, chainLabel: pins.label, rpcUrl: pins.rpcUrl,
112
+ vault: pins.vault, asset: p.asset, token, decimals, amount: p.amount, amountRaw, depositor: p.depositor,
113
+ tokenBalance, allowance, needsApprove: allowance < amountRaw, broker: ASTER_DEPOSIT_BROKER_PERP,
114
+ };
115
+ }
116
+ export function asterDepositCalldata(plan) {
117
+ return ASTER_VAULT.encodeFunctionData("depositFor", [plan.token, plan.depositor, plan.amountRaw, plan.broker]);
118
+ }
119
+ /**
120
+ * The vault's Deposit event for this deposit: account = depositor, currency = token, broker = 1.
121
+ * Returns the event amount; null when the receipt carries no such event.
122
+ */
123
+ export function findAsterDepositEvent(logs, plan) {
124
+ const account = zeroPadValue(plan.depositor, 32).toLowerCase();
125
+ const currency = zeroPadValue(plan.token, 32).toLowerCase();
126
+ for (const log of logs) {
127
+ if (!sameAddress(log.address, plan.vault) || log.topics.length !== 3)
128
+ continue;
129
+ if (log.topics[0] !== DEPOSIT_TOPIC || log.topics[1].toLowerCase() !== account || log.topics[2].toLowerCase() !== currency)
130
+ continue;
131
+ const [isNative, amount, broker] = ASTER_VAULT.decodeEventLog("Deposit", log.data, [...log.topics]).slice(2);
132
+ if (isNative || broker !== plan.broker)
133
+ continue;
134
+ return amount;
135
+ }
136
+ return null;
137
+ }
138
+ /** Sends approve (only when the allowance is short — exact amount) then deposit; verifies the Deposit event. */
139
+ export async function executeAsterDeposit(plan, sign) {
140
+ const rpc = evmProvider(plan.rpcUrl, plan.chainId);
141
+ const base = { venue: "aster", chainLabel: plan.chainLabel, chainId: plan.chainId, from: plan.depositor, value: 0n };
142
+ let approveTxHash = null;
143
+ if (plan.needsApprove) {
144
+ approveTxHash = (await sendMasterTx(rpc, {
145
+ ...base, to: plan.token, data: ERC20.encodeFunctionData("approve", [plan.vault, plan.amountRaw]), label: `${plan.asset} approve`,
146
+ }, sign)).txHash;
147
+ }
148
+ const depositTx = { ...base, to: plan.vault, data: asterDepositCalldata(plan), label: "Aster vault deposit" };
149
+ const { txHash, receipt } = await sendMasterTx(rpc, depositTx, sign);
150
+ const deposited = findAsterDepositEvent(receipt.logs, plan);
151
+ if (deposited === null) {
152
+ throw txOutcomeUnknown(depositTx, txHash, `it was mined, but its receipt has no Deposit(account ${plan.depositor}, currency ${plan.token}, broker ${plan.broker}) event from vault ${plan.vault}`);
153
+ }
154
+ return { approveTxHash, depositTxHash: txHash, depositedRaw: deposited.toString() };
155
+ }
156
+ /** Rows of POST /fapi/v3/aster/deposit-withdraw-history (an array; anything else is a venue error). */
157
+ export function parseAsterHistory(raw) {
158
+ if (!Array.isArray(raw)) {
159
+ throw new PerpError("EXCHANGE_ERROR", `aster: unrecognized deposit-withdraw-history response ${JSON.stringify(raw).slice(0, 160)}`, { exchange: "aster" });
160
+ }
161
+ return raw.map((r) => {
162
+ const row = (r !== null && typeof r === "object" ? r : {});
163
+ return {
164
+ type: String(row.type), state: String(row.state), txHash: String(row.txHash), asset: String(row.asset), amount: String(row.amount),
165
+ accountType: typeof row.accountType === "string" ? row.accountType : null,
166
+ };
167
+ });
168
+ }
169
+ /**
170
+ * Poll the venue's deposit history until this tx is SUCCESS. FAILED → EXCHANGE_ERROR (definite,
171
+ * venue-reported). Not listed / PROCESSING / unreadable until the deadline → ORDER_PENDING with
172
+ * the tx hash (the on-chain deposit is confirmed; only the venue credit is unknown).
173
+ * The history endpoint itself answers -5050 DEPOSIT_REQUIRED until the first deposit is credited —
174
+ * that is "not credited yet", so polling continues.
175
+ */
176
+ export async function awaitAsterDepositCredit(readHistory, txHash, timing) {
177
+ const deadline = timing.now() + timing.timeoutMs;
178
+ let last = "not listed yet";
179
+ for (;;) {
180
+ try {
181
+ const row = parseAsterHistory(await readHistory()).find(r => r.type === "DEPOSIT" && r.txHash.toLowerCase() === txHash.toLowerCase());
182
+ if (row !== undefined) {
183
+ if (row.state === "SUCCESS")
184
+ return row;
185
+ if (row.state === "FAILED") {
186
+ throw new PerpError("EXCHANGE_ERROR", `aster: the venue reports deposit ${txHash} as FAILED — the tokens are in the vault but were not credited`, {
187
+ exchange: "aster", txHash, remediation: "Contact Aster support with the transaction hash; do NOT deposit again until it is resolved",
188
+ });
189
+ }
190
+ last = `state ${row.state}`;
191
+ }
192
+ }
193
+ catch (e) {
194
+ if (e instanceof PerpError && e.structured.code === "EXCHANGE_ERROR" && e.structured.details?.txHash === txHash)
195
+ throw e;
196
+ last = e instanceof Error ? e.message : String(e);
197
+ }
198
+ if (timing.now() + timing.pollMs > deadline)
199
+ break;
200
+ await timing.sleep(timing.pollMs);
201
+ }
202
+ throw new PerpError("ORDER_PENDING", `aster: deposit ${txHash} is confirmed on-chain but not yet shown as credited by the venue after ${timing.timeoutMs / 1000}s (${last})`, {
203
+ exchange: "aster", txHash,
204
+ remediation: `Do NOT deposit again — the vault holds the funds and the credit usually follows. Check: perp -e aster account balance (or the Aster app → Assets → History)`,
205
+ });
206
+ }
207
+ /** Fee + max of POST /fapi/v3/aster/user-withdraw-info for (asset, chain). Missing → refusal, never a default. */
208
+ export function parseAsterWithdrawLimits(raw, asset, chainId) {
209
+ const rec = (v) => (v !== null && typeof v === "object" && !Array.isArray(v) ? v : null);
210
+ const chain = rec(rec(rec(rec(rec(raw)?.balances)?.[asset])?.chainBalances)?.[String(chainId)]);
211
+ if (chain === null) {
212
+ throw new PerpError("INSUFFICIENT_BALANCE", `Aster reports no withdrawable ${asset} balance on chain ${chainId}`, {
213
+ exchange: "aster", remediation: "Check the perp balance (perp -e aster account balance) and that the asset can be withdrawn on that chain",
214
+ });
215
+ }
216
+ // The venue sends these as JSON numbers (live 2026-10-13: `"perpMaxWithdrawAmount":8.7451191,
217
+ // "withdrawFee":0.51`) or numeric strings; either becomes a plain decimal string.
218
+ const decimal = (v) => {
219
+ const str = typeof v === "number" && Number.isFinite(v) && v >= 0 ? venueRateString(v, `aster ${asset} withdraw limit`) : typeof v === "string" ? v : null;
220
+ return str !== null && /^\d+(\.\d+)?$/.test(str) ? str : null;
221
+ };
222
+ const max = decimal(chain.perpMaxWithdrawAmount);
223
+ const fee = decimal(chain.withdrawFee);
224
+ if (max === null || fee === null) {
225
+ throw new PerpError("EXCHANGE_ERROR", `aster: user-withdraw-info has no readable perpMaxWithdrawAmount / withdrawFee for ${asset} on chain ${chainId}`, { exchange: "aster" });
226
+ }
227
+ return { max, fee };
228
+ }
229
+ /** amount > fee and amount ≤ max (exact decimal comparison). */
230
+ export function assertAsterWithdrawAmount(amount, limits, asset) {
231
+ requireDecimal(amount, "Withdraw amount");
232
+ const u = (x) => parseUnits(x, 18);
233
+ if (u(amount) <= u(limits.fee)) {
234
+ throw new PerpError("SIZE_TOO_SMALL", `Aster withdraw amount ${amount} ${asset} does not exceed the withdrawal fee ${limits.fee} ${asset}`, { exchange: "aster" });
235
+ }
236
+ if (u(amount) > u(limits.max)) {
237
+ throw new PerpError("INSUFFICIENT_BALANCE", `Aster withdraw amount ${amount} ${asset} exceeds the withdrawable perp balance ${limits.max} ${asset} on that chain`, { exchange: "aster" });
238
+ }
239
+ }
240
+ /** The withdraw `userSignature` must recover to the account owner before the request is sent. */
241
+ export function assertAsterWithdrawSignature(typed, signature, user) {
242
+ let recovered;
243
+ try {
244
+ recovered = verifyTypedData(typed.domain, typed.types, typed.message, signature);
245
+ }
246
+ catch (e) {
247
+ throw new PerpError("SIGNATURE_FAILED", `Aster withdraw signature is unreadable (${e instanceof Error ? e.message : String(e)}) — nothing was sent`, { exchange: "aster" });
248
+ }
249
+ if (!sameAddress(recovered, user)) {
250
+ throw new PerpError("SIGNATURE_FAILED", `Aster withdraw signature recovers to ${recovered}, not the account owner ${user} — nothing was sent`, { exchange: "aster" });
251
+ }
252
+ }
@@ -0,0 +1,2 @@
1
+ /** Next nonce (µs, decimal string) for `signer`: strictly greater than the last one issued, across processes. */
2
+ export declare function nextAsterNonce(signer: string, nowMs?: number): string;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Monotonic Aster V3 request nonce per signer.
3
+ *
4
+ * Aster requires a microsecond `nonce` within ±10 s of server time that is
5
+ * never reused for the same signer. `Date.now() * 1000 + counter % 1000`
6
+ * repeats across processes (two CLI calls in the same millisecond) and after a
7
+ * clock step backwards. This allocator returns max(now µs, last + 1) and
8
+ * persists `last` per signer in `~/.perp/aster-nonces.json` (public state, no
9
+ * secrets), so successive processes never go backwards or collide.
10
+ */
11
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
12
+ import { resolve } from "path";
13
+ import { PerpError } from "../errors.js";
14
+ function noncePath() {
15
+ return resolve(process.env.HOME || "~", ".perp", "aster-nonces.json");
16
+ }
17
+ function readNonces(path) {
18
+ if (!existsSync(path))
19
+ return {};
20
+ const raw = readFileSync(path, "utf-8");
21
+ let parsed;
22
+ try {
23
+ parsed = JSON.parse(raw);
24
+ }
25
+ catch {
26
+ parsed = null;
27
+ }
28
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)
29
+ || Object.values(parsed).some(v => typeof v !== "string" || !/^\d+$/.test(v))) {
30
+ throw new PerpError("INVALID_PARAMS", `Aster nonce state ${path} is corrupt`, {
31
+ remediation: `Delete ${path} (it only stores the last request nonce per signer) and retry.`,
32
+ });
33
+ }
34
+ return parsed;
35
+ }
36
+ /** How long to wait for another process's nonce lock, and when a lock left by a dead process is stale. */
37
+ const LOCK_WAIT_MS = 2_000;
38
+ const LOCK_STALE_MS = 10_000;
39
+ /**
40
+ * Run `fn` holding the exclusive lock file `<path>.lock` (O_EXCL create). The read → +1 → write
41
+ * of the nonce file is not atomic by itself: two processes reading the same `last` would issue
42
+ * the same nonce. A lock older than LOCK_STALE_MS (its holder died) is removed; a live one not
43
+ * released within LOCK_WAIT_MS → LOCK_HELD (nothing is signed with an unreserved nonce).
44
+ */
45
+ function withNonceLock(path, fn) {
46
+ const lock = `${path}.lock`;
47
+ const deadline = Date.now() + LOCK_WAIT_MS;
48
+ const sleeper = new Int32Array(new SharedArrayBuffer(4));
49
+ for (;;) {
50
+ try {
51
+ closeSync(openSync(lock, "wx", 0o600));
52
+ break;
53
+ }
54
+ catch (e) {
55
+ if (e.code !== "EEXIST")
56
+ throw e;
57
+ let stale = false;
58
+ try {
59
+ stale = Date.now() - statSync(lock).mtimeMs > LOCK_STALE_MS;
60
+ }
61
+ catch {
62
+ continue; // released between the open and the stat: try again
63
+ }
64
+ if (stale) {
65
+ try {
66
+ unlinkSync(lock);
67
+ }
68
+ catch { /* another process removed it */ }
69
+ continue;
70
+ }
71
+ if (Date.now() > deadline) {
72
+ throw new PerpError("LOCK_HELD", `Aster nonce state is locked by another perp process (${lock})`, {
73
+ remediation: `Retry shortly; if no other perp process is running, delete ${lock}`,
74
+ });
75
+ }
76
+ Atomics.wait(sleeper, 0, 0, 5);
77
+ }
78
+ }
79
+ try {
80
+ return fn();
81
+ }
82
+ finally {
83
+ try {
84
+ unlinkSync(lock);
85
+ }
86
+ catch { /* already gone */ }
87
+ }
88
+ }
89
+ /** Next nonce (µs, decimal string) for `signer`: strictly greater than the last one issued, across processes. */
90
+ export function nextAsterNonce(signer, nowMs = Date.now()) {
91
+ const path = noncePath();
92
+ const key = signer.toLowerCase();
93
+ const dir = resolve(path, "..");
94
+ if (!existsSync(dir))
95
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
96
+ return withNonceLock(path, () => {
97
+ const nonces = readNonces(path);
98
+ const now = BigInt(Math.floor(nowMs)) * 1000n;
99
+ const last = nonces[key] !== undefined ? BigInt(nonces[key]) : -1n;
100
+ const next = now > last ? now : last + 1n;
101
+ nonces[key] = next.toString();
102
+ const tmp = `${path}.${process.pid}.tmp`;
103
+ writeFileSync(tmp, JSON.stringify(nonces), { mode: 0o600 });
104
+ renameSync(tmp, path);
105
+ return next.toString();
106
+ });
107
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Aster Spot Adapter (design §12, §12.4 #8).
3
+ *
4
+ * Host `sapi.asterdex.com` (testnet `sapi.asterdex-testnet.com`), paths
5
+ * `/api/v3/*`. Signing is the perp EIP-712 Domain B scheme, reused through
6
+ * `AsterAdapter.signedRequest()` (same agent signer + nonce counter).
7
+ *
8
+ * Account model is separate: spot balances live in `/api/v3/account`; funds
9
+ * move with `POST /api/v3/asset/wallet/transfer` (FUTURE_SPOT / SPOT_FUTURE).
10
+ *
11
+ * Venue rules enforced locally (official spot V3 docs):
12
+ * - TIF GTC | IOC | GTX (post-only). FOK is rejected by the venue (-1115).
13
+ * - LIMIT price must be on PRICE_FILTER.tickSize (never rounded).
14
+ * - Market orders are IOC LIMIT at a protection price (design §13.2): bookTicker
15
+ * mid ± slippage, aligned toward crossing on tickSize, inside PRICE_FILTER
16
+ * min/max and PERCENT_PRICE (venue native MARKET is never sent).
17
+ * - quantity on LOT_SIZE step, min, max (MARKET_LOT_SIZE only for native MARKET).
18
+ * - MIN_NOTIONAL / NOTIONAL minimum when a price is known.
19
+ *
20
+ * Permission gate: when the active signer is an agent whose stored meta lacks
21
+ * `permissions.canSpotTrade === true`, trading calls throw POLICY_DENIED. Venue
22
+ * permission rejections (-1002 / -2015) are mapped to the same error.
23
+ */
24
+ import type { SpotAdapter, SpotMarketInfo, SpotBalance, SpotOrderResult, SpotTransferCapable, SpotOpenOrdersCapable, SpotCancelAllCapable } from "./spot-interface.js";
25
+ import type { ResolvedSlippage } from "./slippage.js";
26
+ import { type AsterAdapter } from "./aster.js";
27
+ export declare class AsterSpotAdapter implements SpotAdapter, SpotTransferCapable, SpotOpenOrdersCapable, SpotCancelAllCapable {
28
+ readonly name = "aster";
29
+ private _perp;
30
+ /** "BASE/QUOTE" → market */
31
+ private _markets;
32
+ /** wire "BTCUSDT" → market */
33
+ private _byWire;
34
+ private _initialized;
35
+ private _tranCounter;
36
+ constructor(perp: AsterAdapter);
37
+ init(): Promise<void>;
38
+ getSpotMarkets(): Promise<SpotMarketInfo[]>;
39
+ getSpotOrderbook(symbol: string): Promise<{
40
+ bids: [string, string][];
41
+ asks: [string, string][];
42
+ }>;
43
+ getSpotBalances(): Promise<SpotBalance[]>;
44
+ spotMarketOrder(symbol: string, side: "buy" | "sell", size: string, opts: {
45
+ slippage: ResolvedSlippage;
46
+ }): Promise<SpotOrderResult>;
47
+ /**
48
+ * Declared protection-price reference for spot: the bookTicker mid
49
+ * ((bidPrice + askPrice) / 2, GET /api/v3/ticker/bookTicker). Spot has no mark price;
50
+ * no runtime substitute — a missing / one-sided book → PRICE_STALE.
51
+ */
52
+ referencePrice(symbol: string): Promise<string>;
53
+ /**
54
+ * Protection price a spot market order would be sent with now: mid ± slippage.bps,
55
+ * aligned toward crossing on PRICE_FILTER.tickSize; outside PRICE_FILTER min/max or
56
+ * PERCENT_PRICE (checked against the mid) → INVALID_PARAMS, never clamped.
57
+ */
58
+ computeProtectionPrice(symbol: string, side: "buy" | "sell", slippage: ResolvedSlippage): Promise<{
59
+ referencePrice: string;
60
+ protectionPrice: string;
61
+ appliedBps: number;
62
+ }>;
63
+ /**
64
+ * Definite IOC outcome (./ioc-outcome.ts contract) of what spotMarketOrder /
65
+ * spotLimitOrder returned — a SpotOrderResult (its venueRaw is read) or a raw venue row.
66
+ */
67
+ iocOutcomeOf(raw: unknown): {
68
+ filledSize: string;
69
+ final: true;
70
+ } | null;
71
+ spotLimitOrder(symbol: string, side: "buy" | "sell", price: string, size: string, opts?: {
72
+ tif?: string;
73
+ }): Promise<SpotOrderResult>;
74
+ spotCancelOrder(symbol: string, orderId: string): Promise<SpotOrderResult>;
75
+ getSpotOpenOrders(symbol: string | null): Promise<Array<{
76
+ orderId: string;
77
+ symbol: string;
78
+ side: "buy" | "sell";
79
+ price: string;
80
+ size: string;
81
+ filled: string;
82
+ }>>;
83
+ /** Venue requires a symbol; null cancels per symbol that has open orders. */
84
+ spotCancelAllOrders(symbol: string | null): Promise<unknown>;
85
+ transferToSpot(amount: string, asset: string): Promise<unknown>;
86
+ transferToPerp(amount: string, asset: string): Promise<unknown>;
87
+ private _transfer;
88
+ private _loadExchangeInfo;
89
+ /** bare base → BASE/USDT; "BASE/QUOTE" as given. */
90
+ private _resolve;
91
+ private _resolveTrading;
92
+ private _mapTif;
93
+ /** Floor to LOT_SIZE (+ MARKET_LOT_SIZE) step and check min/max of each applicable filter. */
94
+ private _sizeFor;
95
+ /** Effective minimum notional, or null when no filter applies. */
96
+ private _minNotional;
97
+ private _checkNotional;
98
+ /** §12.4 #8: gate only when the active signer is an agent lacking canSpotTrade. */
99
+ private _requireSpotPermission;
100
+ private _signed;
101
+ private _publicGet;
102
+ /**
103
+ * POST /api/v3/order. Outcome-unknown failures — HTTP 5xx (the gateway may have
104
+ * forwarded the order), a network / timeout error, or a 2xx with an unreadable
105
+ * body — become a "pending" result,
106
+ * never a thrown (retryable) EXCHANGE_ERROR: a resubmit could place a second order.
107
+ * 4xx / venue error envelopes are definite rejections and still throw.
108
+ */
109
+ private _submitOrder;
110
+ /**
111
+ * Result of an order the venue already accepted (HTTP success). A response that
112
+ * cannot be interpreted must not become a thrown error — a retry would place a
113
+ * second order — so it is reported as finality "pending" with the order id.
114
+ */
115
+ private _acceptedResult;
116
+ private _toResult;
117
+ }