perp-cli 0.14.3 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (363) hide show
  1. package/README.ja.md +243 -0
  2. package/README.ko.md +243 -0
  3. package/README.md +138 -405
  4. package/README.zh.md +243 -0
  5. package/dist/agent-wallet/store.d.ts +3 -2
  6. package/dist/agent-wallet/store.js +2 -2
  7. package/dist/api/public/hyperliquid.js +4 -0
  8. package/dist/api/public/lighter.d.ts +5 -0
  9. package/dist/api/public/lighter.js +5 -0
  10. package/dist/api/public/pacifica.js +85 -15
  11. package/dist/api/public/urls.d.ts +2 -0
  12. package/dist/api/public/urls.js +3 -0
  13. package/dist/arb/sizing.js +4 -2
  14. package/dist/arb/state.d.ts +6 -0
  15. package/dist/arb/utils.d.ts +1 -2
  16. package/dist/arb/utils.js +1 -2
  17. package/dist/balance-by-address.d.ts +39 -0
  18. package/dist/balance-by-address.js +44 -0
  19. package/dist/bot/apex/pulse.js +3 -0
  20. package/dist/bot/apex/radar.js +2 -1
  21. package/dist/bot/engine.d.ts +10 -1
  22. package/dist/bot/engine.js +14 -6
  23. package/dist/bot/indicators.d.ts +7 -0
  24. package/dist/bot/indicators.js +16 -0
  25. package/dist/bot/perp-order.d.ts +4 -4
  26. package/dist/bot/perp-order.js +5 -5
  27. package/dist/bot/quarantine.d.ts +1 -1
  28. package/dist/bot/quarantine.js +1 -1
  29. package/dist/bot/quote-rules.js +1 -1
  30. package/dist/bot/strategies/apex-strategy.js +3 -2
  31. package/dist/bot/strategies/engine-mm.js +6 -5
  32. package/dist/bot/strategies/funding-arb-strategy.js +12 -5
  33. package/dist/bot/strategies/funding-arb-utils.d.ts +15 -3
  34. package/dist/bot/strategies/funding-arb-utils.js +53 -20
  35. package/dist/bot/strategies/funding-auto.js +2 -2
  36. package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
  37. package/dist/bot/strategies/liquidation-mm.js +8 -2
  38. package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
  39. package/dist/bot/strategies/momentum-breakout.js +11 -3
  40. package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
  41. package/dist/bot/strategies/spot-perp-arb-strategy.js +6 -4
  42. package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
  43. package/dist/bot/strategies/spot-perp-executor.js +3 -3
  44. package/dist/bot/strategy-types.d.ts +2 -1
  45. package/dist/bot/tui/BotDashboard.d.ts +1 -1
  46. package/dist/bot/tui/BotDashboard.js +1 -1
  47. package/dist/cache.js +12 -6
  48. package/dist/cli-program.d.ts +15 -0
  49. package/dist/cli-program.js +126 -6
  50. package/dist/commands/account.js +168 -24
  51. package/dist/commands/agent-import.d.ts +7 -0
  52. package/dist/commands/agent-import.js +30 -1
  53. package/dist/commands/agent.d.ts +6 -3
  54. package/dist/commands/agent.js +160 -12
  55. package/dist/commands/arb/index.js +23 -81
  56. package/dist/commands/arb-auto.d.ts +24 -0
  57. package/dist/commands/arb-auto.js +175 -97
  58. package/dist/commands/arb.d.ts +20 -0
  59. package/dist/commands/arb.js +107 -163
  60. package/dist/commands/backtest.js +23 -21
  61. package/dist/commands/bot.js +1 -1
  62. package/dist/commands/builder.js +1 -1
  63. package/dist/commands/funds.js +113 -3
  64. package/dist/commands/init.d.ts +1 -1
  65. package/dist/commands/init.js +27 -7
  66. package/dist/commands/manage.js +23 -21
  67. package/dist/commands/market.d.ts +48 -0
  68. package/dist/commands/market.js +395 -100
  69. package/dist/commands/options.d.ts +14 -0
  70. package/dist/commands/options.js +151 -0
  71. package/dist/commands/pacifica-earn.d.ts +5 -0
  72. package/dist/commands/pacifica-earn.js +455 -0
  73. package/dist/commands/pacifica-orders.d.ts +14 -0
  74. package/dist/commands/pacifica-orders.js +142 -0
  75. package/dist/commands/portfolio.d.ts +16 -0
  76. package/dist/commands/portfolio.js +83 -55
  77. package/dist/commands/rebalance.d.ts +1 -0
  78. package/dist/commands/rebalance.js +15 -0
  79. package/dist/commands/risk.d.ts +11 -2
  80. package/dist/commands/risk.js +39 -44
  81. package/dist/commands/trade.d.ts +1 -0
  82. package/dist/commands/trade.js +67 -62
  83. package/dist/commands/wallet.js +1 -1
  84. package/dist/config.d.ts +2 -2
  85. package/dist/config.js +15 -4
  86. package/dist/constants.js +6 -0
  87. package/dist/cross-chain-margin.d.ts +3 -1
  88. package/dist/cross-chain-margin.js +4 -2
  89. package/dist/dashboard/server.js +3 -3
  90. package/dist/dashboard/ui.js +9 -6
  91. package/dist/errors.d.ts +29 -1
  92. package/dist/errors.js +35 -3
  93. package/dist/exchanges/address.d.ts +8 -0
  94. package/dist/exchanges/address.js +47 -0
  95. package/dist/exchanges/arcus/adapter.d.ts +16 -12
  96. package/dist/exchanges/arcus/adapter.js +45 -18
  97. package/dist/exchanges/arcus/agent.d.ts +1 -1
  98. package/dist/exchanges/arcus/agent.js +1 -1
  99. package/dist/exchanges/arcus/client.js +1 -1
  100. package/dist/exchanges/arcus/public.js +3 -2
  101. package/dist/exchanges/arcus/signing.d.ts +2 -3
  102. package/dist/exchanges/arcus/signing.js +1 -1
  103. package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
  104. package/dist/exchanges/arcus/spot-chain.js +1 -1
  105. package/dist/exchanges/arcus/spot-router.d.ts +1 -1
  106. package/dist/exchanges/arcus/spot-router.js +2 -2
  107. package/dist/exchanges/arcus/spot.d.ts +2 -3
  108. package/dist/exchanges/arcus/spot.js +2 -3
  109. package/dist/exchanges/aster-spot.d.ts +3 -3
  110. package/dist/exchanges/aster-spot.js +6 -9
  111. package/dist/exchanges/aster-typed-data.d.ts +1 -1
  112. package/dist/exchanges/aster-typed-data.js +1 -1
  113. package/dist/exchanges/aster.d.ts +30 -10
  114. package/dist/exchanges/aster.js +153 -53
  115. package/dist/exchanges/builder-fee.d.ts +1 -1
  116. package/dist/exchanges/builder-fee.js +13 -2
  117. package/dist/exchanges/canonical-symbols.d.ts +8 -2
  118. package/dist/exchanges/canonical-symbols.js +46 -2
  119. package/dist/exchanges/capabilities.d.ts +2 -0
  120. package/dist/exchanges/capabilities.js +2 -1
  121. package/dist/exchanges/derive/client.d.ts +37 -0
  122. package/dist/exchanges/derive/client.js +135 -0
  123. package/dist/exchanges/derive/options.d.ts +73 -0
  124. package/dist/exchanges/derive/options.js +379 -0
  125. package/dist/exchanges/extended/adapter.d.ts +96 -0
  126. package/dist/exchanges/extended/adapter.js +241 -0
  127. package/dist/exchanges/extended/client.d.ts +38 -0
  128. package/dist/exchanges/extended/client.js +88 -0
  129. package/dist/exchanges/extended/public.d.ts +133 -0
  130. package/dist/exchanges/extended/public.js +436 -0
  131. package/dist/exchanges/grvt/adapter.d.ts +100 -0
  132. package/dist/exchanges/grvt/adapter.js +283 -0
  133. package/dist/exchanges/grvt/client.d.ts +25 -0
  134. package/dist/exchanges/grvt/client.js +86 -0
  135. package/dist/exchanges/grvt/public.d.ts +151 -0
  136. package/dist/exchanges/grvt/public.js +438 -0
  137. package/dist/exchanges/hyperliquid-outcome.d.ts +2 -0
  138. package/dist/exchanges/hyperliquid-outcome.js +29 -1
  139. package/dist/exchanges/hyperliquid-spot.d.ts +9 -1
  140. package/dist/exchanges/hyperliquid-spot.js +24 -16
  141. package/dist/exchanges/hyperliquid.d.ts +199 -22
  142. package/dist/exchanges/hyperliquid.js +550 -202
  143. package/dist/exchanges/index.d.ts +1 -1
  144. package/dist/exchanges/index.js +1 -1
  145. package/dist/exchanges/interface.d.ts +157 -13
  146. package/dist/exchanges/interface.js +47 -0
  147. package/dist/exchanges/ioc-outcome.d.ts +1 -1
  148. package/dist/exchanges/ioc-outcome.js +1 -1
  149. package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
  150. package/dist/exchanges/legacy-trading-v2.js +7 -2
  151. package/dist/exchanges/lighter-public.d.ts +67 -12
  152. package/dist/exchanges/lighter-public.js +250 -82
  153. package/dist/exchanges/lighter-signer.d.ts +1 -1
  154. package/dist/exchanges/lighter-signer.js +1 -1
  155. package/dist/exchanges/lighter-spot.d.ts +10 -5
  156. package/dist/exchanges/lighter-spot.js +105 -97
  157. package/dist/exchanges/lighter.d.ts +80 -21
  158. package/dist/exchanges/lighter.js +312 -105
  159. package/dist/exchanges/ondo/adapter.d.ts +38 -13
  160. package/dist/exchanges/ondo/adapter.js +86 -27
  161. package/dist/exchanges/ondo/agent.d.ts +1 -1
  162. package/dist/exchanges/ondo/agent.js +1 -1
  163. package/dist/exchanges/ondo/client.d.ts +1 -1
  164. package/dist/exchanges/ondo/client.js +2 -2
  165. package/dist/exchanges/ondo/public.d.ts +35 -3
  166. package/dist/exchanges/ondo/public.js +75 -32
  167. package/dist/exchanges/ondo/spot.d.ts +2 -2
  168. package/dist/exchanges/ondo/spot.js +3 -3
  169. package/dist/exchanges/options-interface.d.ts +158 -0
  170. package/dist/exchanges/options-interface.js +17 -0
  171. package/dist/exchanges/options-venues.d.ts +12 -0
  172. package/dist/exchanges/options-venues.js +35 -0
  173. package/dist/exchanges/outcome-interface.d.ts +4 -0
  174. package/dist/exchanges/pacifica-atomic.d.ts +211 -0
  175. package/dist/exchanges/pacifica-atomic.js +440 -0
  176. package/dist/exchanges/pacifica-decimal.d.ts +10 -0
  177. package/dist/exchanges/pacifica-decimal.js +51 -0
  178. package/dist/exchanges/pacifica-presigned.d.ts +53 -0
  179. package/dist/exchanges/pacifica-presigned.js +141 -0
  180. package/dist/exchanges/pacifica-print.d.ts +200 -0
  181. package/dist/exchanges/pacifica-print.js +417 -0
  182. package/dist/exchanges/pacifica-spot.d.ts +8 -2
  183. package/dist/exchanges/pacifica-spot.js +26 -18
  184. package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
  185. package/dist/exchanges/pacifica-typed-data.js +1 -1
  186. package/dist/exchanges/pacifica-vaults.d.ts +130 -0
  187. package/dist/exchanges/pacifica-vaults.js +277 -0
  188. package/dist/exchanges/pacifica.d.ts +108 -10
  189. package/dist/exchanges/pacifica.js +428 -171
  190. package/dist/exchanges/precision.d.ts +3 -1
  191. package/dist/exchanges/precision.js +9 -2
  192. package/dist/exchanges/public-balances.d.ts +25 -0
  193. package/dist/exchanges/public-balances.js +158 -0
  194. package/dist/exchanges/public-positions.d.ts +43 -0
  195. package/dist/exchanges/public-positions.js +154 -0
  196. package/dist/exchanges/qfex/adapter.d.ts +69 -13
  197. package/dist/exchanges/qfex/adapter.js +102 -17
  198. package/dist/exchanges/qfex/agent.d.ts +1 -1
  199. package/dist/exchanges/qfex/agent.js +1 -1
  200. package/dist/exchanges/qfex/client.d.ts +1 -1
  201. package/dist/exchanges/qfex/client.js +1 -1
  202. package/dist/exchanges/qfex/errors.d.ts +1 -1
  203. package/dist/exchanges/qfex/errors.js +2 -2
  204. package/dist/exchanges/qfex/public.d.ts +65 -12
  205. package/dist/exchanges/qfex/public.js +131 -25
  206. package/dist/exchanges/qfex/signing.d.ts +2 -2
  207. package/dist/exchanges/qfex/signing.js +4 -5
  208. package/dist/exchanges/qfex/ws-transport.d.ts +3 -3
  209. package/dist/exchanges/qfex/ws-transport.js +3 -3
  210. package/dist/exchanges/rate-limiter.d.ts +1 -1
  211. package/dist/exchanges/rate-limiter.js +5 -2
  212. package/dist/exchanges/registry.d.ts +1 -1
  213. package/dist/exchanges/registry.js +1 -1
  214. package/dist/exchanges/risex/adapter.d.ts +160 -0
  215. package/dist/exchanges/risex/adapter.js +842 -0
  216. package/dist/exchanges/risex/agent.d.ts +154 -0
  217. package/dist/exchanges/risex/agent.js +384 -0
  218. package/dist/exchanges/risex/client.d.ts +47 -0
  219. package/dist/exchanges/risex/client.js +174 -0
  220. package/dist/exchanges/risex/nonce.d.ts +43 -0
  221. package/dist/exchanges/risex/nonce.js +212 -0
  222. package/dist/exchanges/risex/public.d.ts +64 -0
  223. package/dist/exchanges/risex/public.js +150 -0
  224. package/dist/exchanges/risex/session.d.ts +71 -0
  225. package/dist/exchanges/risex/session.js +171 -0
  226. package/dist/exchanges/risex/typed-data.d.ts +173 -0
  227. package/dist/exchanges/risex/typed-data.js +240 -0
  228. package/dist/exchanges/risex/units.d.ts +29 -0
  229. package/dist/exchanges/risex/units.js +104 -0
  230. package/dist/exchanges/slippage.d.ts +1 -1
  231. package/dist/exchanges/slippage.js +1 -1
  232. package/dist/exchanges/spot-factory.d.ts +3 -3
  233. package/dist/exchanges/spot-factory.js +8 -8
  234. package/dist/exchanges/spot-interface.d.ts +5 -5
  235. package/dist/exchanges/spot-interface.js +2 -2
  236. package/dist/exchanges/spot-result.d.ts +3 -3
  237. package/dist/exchanges/spot-result.js +3 -3
  238. package/dist/exchanges/trading-v2.d.ts +3 -3
  239. package/dist/exchanges/trading-v2.js +9 -6
  240. package/dist/exchanges/txflow/adapter.d.ts +61 -0
  241. package/dist/exchanges/txflow/adapter.js +150 -0
  242. package/dist/exchanges/txflow/client.d.ts +47 -0
  243. package/dist/exchanges/txflow/client.js +155 -0
  244. package/dist/exchanges/txflow/public.d.ts +87 -0
  245. package/dist/exchanges/txflow/public.js +355 -0
  246. package/dist/exchanges/unknown-symbol.d.ts +34 -0
  247. package/dist/exchanges/unknown-symbol.js +56 -0
  248. package/dist/exchanges/variational/adapter.d.ts +67 -0
  249. package/dist/exchanges/variational/adapter.js +137 -0
  250. package/dist/exchanges/variational/client.d.ts +21 -0
  251. package/dist/exchanges/variational/client.js +66 -0
  252. package/dist/exchanges/variational/public.d.ts +126 -0
  253. package/dist/exchanges/variational/public.js +430 -0
  254. package/dist/exchanges/venue-types.d.ts +52 -10
  255. package/dist/exchanges/venue-types.js +82 -11
  256. package/dist/exchanges/venue.d.ts +150 -11
  257. package/dist/exchanges/venue.js +126 -4
  258. package/dist/exchanges/venues/arcus.d.ts +1 -2
  259. package/dist/exchanges/venues/arcus.js +5 -4
  260. package/dist/exchanges/venues/aster.d.ts +2 -2
  261. package/dist/exchanges/venues/aster.js +6 -4
  262. package/dist/exchanges/venues/create-helpers.d.ts +1 -1
  263. package/dist/exchanges/venues/create-helpers.js +3 -3
  264. package/dist/exchanges/venues/extended.d.ts +21 -0
  265. package/dist/exchanges/venues/extended.js +96 -0
  266. package/dist/exchanges/venues/grvt.d.ts +30 -0
  267. package/dist/exchanges/venues/grvt.js +98 -0
  268. package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
  269. package/dist/exchanges/venues/hyperliquid.js +19 -8
  270. package/dist/exchanges/venues/index.d.ts +7 -2
  271. package/dist/exchanges/venues/index.js +8 -2
  272. package/dist/exchanges/venues/lighter-family.d.ts +7 -9
  273. package/dist/exchanges/venues/lighter-family.js +9 -11
  274. package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
  275. package/dist/exchanges/venues/lighter-rh.js +20 -8
  276. package/dist/exchanges/venues/lighter.d.ts +1 -1
  277. package/dist/exchanges/venues/lighter.js +18 -6
  278. package/dist/exchanges/venues/ondo.d.ts +1 -1
  279. package/dist/exchanges/venues/ondo.js +5 -3
  280. package/dist/exchanges/venues/pacifica.d.ts +2 -2
  281. package/dist/exchanges/venues/pacifica.js +15 -4
  282. package/dist/exchanges/venues/qfex.d.ts +1 -1
  283. package/dist/exchanges/venues/qfex.js +4 -2
  284. package/dist/exchanges/venues/risex-release.d.ts +14 -0
  285. package/dist/exchanges/venues/risex-release.js +14 -0
  286. package/dist/exchanges/venues/risex.d.ts +31 -0
  287. package/dist/exchanges/venues/risex.js +131 -0
  288. package/dist/exchanges/venues/txflow.d.ts +18 -0
  289. package/dist/exchanges/venues/txflow.js +77 -0
  290. package/dist/exchanges/venues/variational.d.ts +21 -0
  291. package/dist/exchanges/venues/variational.js +85 -0
  292. package/dist/execution-log.d.ts +1 -1
  293. package/dist/funding/legacy-public.d.ts +6 -4
  294. package/dist/funding/legacy-public.js +42 -41
  295. package/dist/funding/normalize.d.ts +1 -1
  296. package/dist/funding/normalize.js +1 -1
  297. package/dist/funding/rates.d.ts +95 -6
  298. package/dist/funding/rates.js +99 -19
  299. package/dist/index.js +7 -6
  300. package/dist/jobs.d.ts +1 -0
  301. package/dist/jobs.js +25 -1
  302. package/dist/landing.d.ts +2 -0
  303. package/dist/landing.js +1 -1
  304. package/dist/liquidity.d.ts +3 -1
  305. package/dist/liquidity.js +8 -4
  306. package/dist/mcp-pacifica-earn.d.ts +9 -0
  307. package/dist/mcp-pacifica-earn.js +64 -0
  308. package/dist/mcp-server.js +211 -68
  309. package/dist/pacifica/client.d.ts +69 -5
  310. package/dist/pacifica/client.js +181 -8
  311. package/dist/pacifica/rate-limit.d.ts +144 -0
  312. package/dist/pacifica/rate-limit.js +267 -0
  313. package/dist/pacifica/types/order.d.ts +2 -1
  314. package/dist/plan-executor.js +7 -2
  315. package/dist/positions-by-address.d.ts +34 -0
  316. package/dist/positions-by-address.js +46 -0
  317. package/dist/rebalance.js +1 -1
  318. package/dist/risk.d.ts +2 -1
  319. package/dist/risk.js +2 -4
  320. package/dist/settings.d.ts +32 -3
  321. package/dist/signer/ows-evm.d.ts +1 -1
  322. package/dist/signer/ows-evm.js +1 -1
  323. package/dist/smart-order.d.ts +1 -1
  324. package/dist/smart-order.js +1 -1
  325. package/dist/strategies/funding-arb.d.ts +1 -1
  326. package/dist/strategies/funding-arb.js +36 -63
  327. package/dist/trade-validator.js +2 -2
  328. package/dist/utils.d.ts +22 -3
  329. package/dist/utils.js +11 -3
  330. package/dist/venue-health.d.ts +40 -0
  331. package/dist/venue-health.js +75 -0
  332. package/dist/withdraw-gate.d.ts +1 -1
  333. package/dist/withdraw-gate.js +1 -1
  334. package/dist/ws/arcus-market.d.ts +9 -0
  335. package/dist/ws/arcus-market.js +9 -0
  336. package/dist/ws/aster-market.d.ts +33 -0
  337. package/dist/ws/aster-market.js +30 -0
  338. package/dist/ws/index.d.ts +980 -0
  339. package/dist/ws/index.js +4460 -0
  340. package/dist/ws/lighter-market.d.ts +14 -0
  341. package/dist/ws/lighter-market.js +13 -0
  342. package/dist/ws/node.d.ts +996 -0
  343. package/dist/ws/node.js +4554 -0
  344. package/dist/ws/pacifica-instrument.d.ts +8 -0
  345. package/dist/ws/pacifica-instrument.js +8 -0
  346. package/dist/ws/unclassified.d.ts +16 -0
  347. package/dist/ws/unclassified.js +43 -0
  348. package/dist/ws-cli/credentials.d.ts +62 -0
  349. package/dist/ws-cli/credentials.js +126 -0
  350. package/package.json +71 -10
  351. package/skills/perp-cli/SKILL.md +153 -36
  352. package/skills/perp-cli/package.json +15 -2
  353. package/skills/perp-cli/references/agent-operations.md +22 -12
  354. package/skills/perp-cli/references/commands.md +143 -17
  355. package/skills/perp-cli/references/strategies.md +38 -3
  356. package/skills/perp-cli/references/ws-library.md +523 -0
  357. package/skills/perp-cli/scripts/arb-monitor.sh +13 -10
  358. package/skills/perp-cli/scripts/funding-analysis.sh +57 -29
  359. package/skills/perp-cli/scripts/preflight.sh +27 -19
  360. package/skills/perp-cli/scripts/spot-perp-scan.sh +58 -38
  361. package/skills/perp-cli/scripts/validate-arb.sh +56 -39
  362. package/dist/cli-spec.d.ts +0 -46
  363. package/dist/cli-spec.js +0 -81
@@ -0,0 +1,154 @@
1
+ /**
2
+ * RISEx session signer lifecycle.
3
+ *
4
+ * approve (`perp wallet agent approve risex --master <ows>`):
5
+ * 1. Create OWS wallet `agent-risex-<master>[-testnet]` (EVM key, empty passphrase — the agent
6
+ * wallet pattern of HL / Arcus).
7
+ * 2. Pre-check the master's RISE-chain gas BEFORE registering (the restriction below is an
8
+ * on-chain transaction from the master; a signer that could not be restricted is never left
9
+ * registered on purpose).
10
+ * 3. Master signs RegisterSigner, the signer signs VerifySigner (same nonce pair), POST
11
+ * /v1/auth/register-signer; poll session-key-status until Active.
12
+ * 4. Restrict ON-CHAIN to trading only (owner decision 2026-10-13): for every permission except
13
+ * RISEX_TRADING_PERMISSION the SIGNER signs DisablePermission (the contract only accepts the
14
+ * signer's own signature for giving up a permission — verified by eth_call on testnet
15
+ * 2026-10-14: a master-signed DisablePermission reverts InvalidSignerSignature) and the MASTER
16
+ * wallet sends Authorization.disablePermission(...) as an on-chain transaction (its signature;
17
+ * needs RISE-chain ETH for gas). Re-enabling needs an EnablePermission signed by the ACCOUNT
18
+ * (a signer-signed one reverts), so a leaked signer key cannot undo the restriction.
19
+ * 5. Prove it (assertTradingOnlyOnChain): the reviewed Authorization implementation, no permission
20
+ * id ≥5, and the raw sessionKeys bitmap's permission bits 0..4 equal exactly {2}. Bits 5–31 of a new
21
+ * signer's bitmap (0xffffffff) cannot be cleared (ids ≥5 are rejected at ABI decoding) and map to
22
+ * no permission; the pin + probe refuse trading the moment that changes.
23
+ * A signer that is not Active after polling, or that fails 4–5, is revoked (master-signed, gasless
24
+ * REST) and its key deleted (APPROVE_FAILED). Every exit that KEEPS a key (unknown register outcome,
25
+ * failed revoke) records its meta with permissions.tradingOnly false, so `wallet agent verify /
26
+ * revoke risex` can see it while the adapter refuses to trade with it (POLICY_DENIED).
27
+ *
28
+ * revoke: master signs RevokeSigner; POST /v1/auth/revoke-signer.
29
+ */
30
+ import { type Transaction } from "ethers";
31
+ import type { RisexAgentMeta } from "../../settings.js";
32
+ import type { EvmSigner } from "../../signer/interface.js";
33
+ import type { Network } from "../venue-types.js";
34
+ import { type RisexClient } from "./client.js";
35
+ import { type RisexChainReader, type RisexSigningContext } from "./session.js";
36
+ import { type NonceSlot } from "./typed-data.js";
37
+ export declare const RISEX_SIGNER_MESSAGE = "perp-cli signer";
38
+ export declare const RISEX_DEFAULT_EXPIRY_DAYS = 30;
39
+ export declare function risexAgentWalletName(masterOwsWallet: string, network: Network): string;
40
+ export declare function sessionKeyStatus(client: RisexClient, account: string, signer: string): Promise<number | null>;
41
+ /** Master + signer typed-data signatures and the register request. Unknown outcome → ORDER_PENDING. */
42
+ export declare function registerSigner(p: {
43
+ client: RisexClient;
44
+ network: Network;
45
+ ctx: RisexSigningContext;
46
+ master: EvmSigner;
47
+ signer: EvmSigner;
48
+ expiration: number;
49
+ nonceDir?: string;
50
+ }): Promise<{
51
+ txHash: string | null;
52
+ }>;
53
+ /** Master-signed RevokeSigner over REST (gasless). */
54
+ export declare function revokeSigner(p: {
55
+ client: RisexClient;
56
+ network: Network;
57
+ ctx: RisexSigningContext;
58
+ master: EvmSigner;
59
+ signer: string;
60
+ nonceDir?: string;
61
+ }): Promise<{
62
+ txHash: string | null;
63
+ }>;
64
+ /** Sends one signed disablePermission transaction from the master; resolves with the tx hash once mined successfully. */
65
+ export type DisablePermissionSender = (permission: number, slot: NonceSlot, signature: string) => Promise<string>;
66
+ /**
67
+ * Restrict an ACTIVE signer to the trading permission and prove the final on-chain state.
68
+ * Returns the permissions disabled by this call (already-disabled ones are skipped).
69
+ */
70
+ export declare function restrictSignerToTrading(p: {
71
+ client: RisexClient;
72
+ network: Network;
73
+ ctx: RisexSigningContext;
74
+ account: string;
75
+ signer: EvmSigner;
76
+ chain: RisexChainReader;
77
+ send: DisablePermissionSender;
78
+ nonceDir?: string;
79
+ }): Promise<{
80
+ disabled: Array<{
81
+ permission: number;
82
+ txHash: string;
83
+ }>;
84
+ verifiedAt: string;
85
+ }>;
86
+ /** Master-wallet transaction sender for disablePermission on the pinned Authorization contract. */
87
+ export declare function masterDisablePermissionSender(network: Network, master: EvmSigner & {
88
+ signTransaction(tx: Transaction): Promise<string>;
89
+ }, signer: string): DisablePermissionSender;
90
+ /** Refuse before registering when the master cannot pay for the restriction transactions. */
91
+ export declare function assertRestrictGas(network: Network, account: string): Promise<void>;
92
+ export interface ApproveRisexAgentOpts {
93
+ agentName: string;
94
+ masterOwsWallet: string;
95
+ passphrase: string | null;
96
+ network: Network;
97
+ expiresAt: Date;
98
+ }
99
+ /** OWS operations the approve flow needs (the vault in production, a fake in tests). */
100
+ export interface RisexOwsOps {
101
+ walletExists(name: string): boolean;
102
+ /** Creates an EVM-capable wallet with the empty passphrase; returns its signer. */
103
+ createSigner(name: string): EvmSigner;
104
+ deleteWallet(name: string): void;
105
+ }
106
+ /** Everything approve touches, injectable for tests. */
107
+ export interface RisexApproveDeps {
108
+ master: EvmSigner;
109
+ ows: RisexOwsOps;
110
+ client: RisexClient;
111
+ chain: RisexChainReader;
112
+ /** Sender of the master's disablePermission transactions for `signer`. */
113
+ sender(signer: string): DisablePermissionSender;
114
+ /** Refuses before anything is registered when the master cannot pay the restriction gas. */
115
+ assertGas(account: string): Promise<void>;
116
+ /**
117
+ * Persists the meta of a signer whose key is KEPT after a failure (permissions.tradingOnly false),
118
+ * so `wallet agent verify / revoke risex` can see it. The adapter refuses such a signer.
119
+ */
120
+ saveMeta(meta: RisexAgentMeta): void;
121
+ sleep(ms: number): Promise<void>;
122
+ nonceDir?: string;
123
+ }
124
+ /**
125
+ * Checks that refuse without side effects (signing context pins, maintenance, the master's gas):
126
+ * run before registering — and, for --rotate, before the old signer is revoked.
127
+ */
128
+ export declare function risexApprovePreflight(network: Network, client: RisexClient, chain: RisexChainReader, account: string, assertGas: (a: string) => Promise<void>): Promise<RisexSigningContext>;
129
+ /** --rotate: the side-effect-free checks for the NEW signer, run before the old one is revoked. */
130
+ export declare function risexRotatePreflight(masterOwsWallet: string, passphrase: string | null, network: Network): Promise<void>;
131
+ /** Full approve with OWS master + OWS agent wallet. Returns the meta of a trading-only signer (the caller persists it). */
132
+ export declare function approveRisexAgent(opts: ApproveRisexAgentOpts): Promise<RisexAgentMeta>;
133
+ /** The approve orchestration (see the module doc). */
134
+ export declare function approveRisexAgentWith(opts: ApproveRisexAgentOpts, deps: RisexApproveDeps): Promise<RisexAgentMeta>;
135
+ /** Venue status of a stored signer (1 = Active, 3 = Revoked; anything else = not registered). */
136
+ export declare function risexSignerStatus(meta: RisexAgentMeta): Promise<number | null>;
137
+ /** Revoke a stored signer with the master wallet that owns it. */
138
+ export declare function revokeRisexAgent(opts: {
139
+ meta: RisexAgentMeta;
140
+ masterOwsWallet: string;
141
+ passphrase: string | null;
142
+ }): Promise<{
143
+ txHash: string | null;
144
+ }>;
145
+ /** Verify a stored signer: Active on the venue and trading-only on-chain. */
146
+ export declare function verifyRisexAgent(meta: RisexAgentMeta, chain?: RisexChainReader): Promise<{
147
+ status: number | null;
148
+ active: boolean;
149
+ permissions: Record<number, boolean>;
150
+ bitmap: string | null;
151
+ tradingOnly: boolean;
152
+ reason: string | null;
153
+ expired: boolean;
154
+ }>;
@@ -0,0 +1,384 @@
1
+ /**
2
+ * RISEx session signer lifecycle.
3
+ *
4
+ * approve (`perp wallet agent approve risex --master <ows>`):
5
+ * 1. Create OWS wallet `agent-risex-<master>[-testnet]` (EVM key, empty passphrase — the agent
6
+ * wallet pattern of HL / Arcus).
7
+ * 2. Pre-check the master's RISE-chain gas BEFORE registering (the restriction below is an
8
+ * on-chain transaction from the master; a signer that could not be restricted is never left
9
+ * registered on purpose).
10
+ * 3. Master signs RegisterSigner, the signer signs VerifySigner (same nonce pair), POST
11
+ * /v1/auth/register-signer; poll session-key-status until Active.
12
+ * 4. Restrict ON-CHAIN to trading only (owner decision 2026-10-13): for every permission except
13
+ * RISEX_TRADING_PERMISSION the SIGNER signs DisablePermission (the contract only accepts the
14
+ * signer's own signature for giving up a permission — verified by eth_call on testnet
15
+ * 2026-10-14: a master-signed DisablePermission reverts InvalidSignerSignature) and the MASTER
16
+ * wallet sends Authorization.disablePermission(...) as an on-chain transaction (its signature;
17
+ * needs RISE-chain ETH for gas). Re-enabling needs an EnablePermission signed by the ACCOUNT
18
+ * (a signer-signed one reverts), so a leaked signer key cannot undo the restriction.
19
+ * 5. Prove it (assertTradingOnlyOnChain): the reviewed Authorization implementation, no permission
20
+ * id ≥5, and the raw sessionKeys bitmap's permission bits 0..4 equal exactly {2}. Bits 5–31 of a new
21
+ * signer's bitmap (0xffffffff) cannot be cleared (ids ≥5 are rejected at ABI decoding) and map to
22
+ * no permission; the pin + probe refuse trading the moment that changes.
23
+ * A signer that is not Active after polling, or that fails 4–5, is revoked (master-signed, gasless
24
+ * REST) and its key deleted (APPROVE_FAILED). Every exit that KEEPS a key (unknown register outcome,
25
+ * failed revoke) records its meta with permissions.tradingOnly false, so `wallet agent verify /
26
+ * revoke risex` can see it while the adapter refuses to trade with it (POLICY_DENIED).
27
+ *
28
+ * revoke: master signs RevokeSigner; POST /v1/auth/revoke-signer.
29
+ */
30
+ import { getAddress } from "ethers";
31
+ import { PerpError, extractErrorMessage } from "../../errors.js";
32
+ import { assertRpcChainId, evmProvider, sendMasterTx } from "../../evm-master-tx.js";
33
+ import { RisexSendUnknown } from "./client.js";
34
+ import { risexClient } from "./public.js";
35
+ import { allocateAccountNonce, assertReviewedContracts, assertTradingOnlyOnChain, chainReader, loadSigningContext } from "./session.js";
36
+ import { RISEX_AUTH_IFACE, RISEX_NON_TRADING_PERMISSIONS, RISEX_PERMISSION_IDS, RISEX_PINS, RISEX_TRADING_PERMISSION, signChecked, } from "./typed-data.js";
37
+ export const RISEX_SIGNER_MESSAGE = "perp-cli signer";
38
+ export const RISEX_DEFAULT_EXPIRY_DAYS = 30;
39
+ const STATUS_ACTIVE = 1;
40
+ const CONFIRM_ATTEMPTS = 15;
41
+ const CONFIRM_INTERVAL_MS = 1_000;
42
+ /** Pre-check budget per DisablePermission transaction (gas units); the send re-checks exactly. */
43
+ const RESTRICT_GAS_BUDGET = 300000n;
44
+ export function risexAgentWalletName(masterOwsWallet, network) {
45
+ return network === "testnet" ? `agent-risex-${masterOwsWallet}-testnet` : `agent-risex-${masterOwsWallet}`;
46
+ }
47
+ export async function sessionKeyStatus(client, account, signer) {
48
+ const d = await client.get("/v1/auth/session-key-status", { account: account.toLowerCase(), signer: signer.toLowerCase() });
49
+ return typeof d.status === "number" ? d.status : null;
50
+ }
51
+ /** Master + signer typed-data signatures and the register request. Unknown outcome → ORDER_PENDING. */
52
+ export async function registerSigner(p) {
53
+ const account = getAddress(p.master.getAddress());
54
+ const signer = getAddress(p.signer.getAddress());
55
+ const slot = await allocateAccountNonce(p.client, p.network, account, p.nonceDir);
56
+ const accountSignature = await signChecked(p.master, p.ctx.domain, "RegisterSigner", {
57
+ account, signer, message: RISEX_SIGNER_MESSAGE, expiration: p.expiration, nonceAnchor: slot.anchor, nonceBitmap: slot.bit,
58
+ });
59
+ const signerSignature = await signChecked(p.signer, p.ctx.domain, "VerifySigner", { account, nonceAnchor: slot.anchor, nonceBitmap: slot.bit });
60
+ try {
61
+ const res = await p.client.post("/v1/auth/register-signer", {
62
+ account, signer, message: RISEX_SIGNER_MESSAGE, expiration: String(p.expiration),
63
+ nonce_anchor: slot.anchor.toString(), nonce_bitmap_index: slot.bit,
64
+ account_signature: accountSignature, signer_signature: signerSignature,
65
+ }, "register-signer");
66
+ if (res.success !== true) {
67
+ throw new PerpError("APPROVE_FAILED", `RISEx register-signer answered success=${String(res.success)}`, { exchange: "risex" });
68
+ }
69
+ return { txHash: typeof res.transaction_hash === "string" && res.transaction_hash !== "" ? res.transaction_hash : null };
70
+ }
71
+ catch (e) {
72
+ if (e instanceof RisexSendUnknown) {
73
+ throw new PerpError("ORDER_PENDING", `RISEx register-signer outcome unknown: ${extractErrorMessage(e.cause)}`, {
74
+ exchange: "risex", signer,
75
+ remediation: `Do NOT re-run approve yet. Check perp${p.network === "testnet" ? " -n testnet" : ""} wallet agent verify risex, or the signer list in the RISEx app; revoke ${signer} there if it shows up unrestricted`,
76
+ });
77
+ }
78
+ throw e;
79
+ }
80
+ }
81
+ /** Master-signed RevokeSigner over REST (gasless). */
82
+ export async function revokeSigner(p) {
83
+ const account = getAddress(p.master.getAddress());
84
+ const signer = getAddress(p.signer);
85
+ const slot = await allocateAccountNonce(p.client, p.network, account, p.nonceDir);
86
+ const accountSignature = await signChecked(p.master, p.ctx.domain, "RevokeSigner", { account, signer, nonceAnchor: slot.anchor, nonceBitmap: slot.bit });
87
+ try {
88
+ const res = await p.client.post("/v1/auth/revoke-signer", {
89
+ account, signer, nonce_anchor: slot.anchor.toString(), nonce_bitmap_index: slot.bit, account_signature: accountSignature,
90
+ }, "revoke-signer");
91
+ if (res.success !== true)
92
+ throw new PerpError("ACTION_REJECTED", `RISEx revoke-signer answered success=${String(res.success)}`, { exchange: "risex" });
93
+ return { txHash: typeof res.transaction_hash === "string" && res.transaction_hash !== "" ? res.transaction_hash : null };
94
+ }
95
+ catch (e) {
96
+ if (e instanceof RisexSendUnknown) {
97
+ throw new PerpError("ORDER_PENDING", `RISEx revoke-signer outcome unknown: ${extractErrorMessage(e.cause)}`, {
98
+ exchange: "risex", signer, remediation: "Check the signer status (perp wallet agent verify risex) before retrying",
99
+ });
100
+ }
101
+ throw e;
102
+ }
103
+ }
104
+ /**
105
+ * Restrict an ACTIVE signer to the trading permission and prove the final on-chain state.
106
+ * Returns the permissions disabled by this call (already-disabled ones are skipped).
107
+ */
108
+ export async function restrictSignerToTrading(p) {
109
+ const account = getAddress(p.account);
110
+ const signer = getAddress(p.signer.getAddress());
111
+ const disabled = [];
112
+ for (const permission of RISEX_NON_TRADING_PERMISSIONS) {
113
+ if (!(await p.chain.hasPermission(account, signer, permission)))
114
+ continue;
115
+ const slot = await allocateAccountNonce(p.client, p.network, account, p.nonceDir);
116
+ const signature = await signChecked(p.signer, p.ctx.domain, "DisablePermission", {
117
+ account, signer, permission, nonceAnchor: slot.anchor, nonceBitmap: slot.bit,
118
+ });
119
+ disabled.push({ permission, txHash: await p.send(permission, slot, signature) });
120
+ }
121
+ // The final state is proven from the raw bitmap (plus the implementation pin and the "no id ≥5"
122
+ // probe), not just per-id reads; anything else fails the approve (→ auto-revoke by the caller).
123
+ try {
124
+ await assertTradingOnlyOnChain(p.chain, p.network, account, signer);
125
+ }
126
+ catch (e) {
127
+ if (e instanceof PerpError && e.structured.code === "POLICY_DENIED") {
128
+ throw new PerpError("APPROVE_FAILED", e.message, { ...(e.structured.details ?? {}), exchange: "risex" });
129
+ }
130
+ throw e;
131
+ }
132
+ return { disabled, verifiedAt: new Date().toISOString() };
133
+ }
134
+ /** Master-wallet transaction sender for disablePermission on the pinned Authorization contract. */
135
+ export function masterDisablePermissionSender(network, master, signer) {
136
+ const pin = RISEX_PINS[network];
137
+ if (pin.rpcUrl === null)
138
+ throw new PerpError("NOT_IMPLEMENTED", `RISEx ${network} has no known RPC endpoint`, { exchange: "risex" });
139
+ const rpc = evmProvider(pin.rpcUrl, pin.chainId);
140
+ const account = getAddress(master.getAddress());
141
+ return async (permission, slot, signature) => {
142
+ await assertRpcChainId(rpc, pin.rpcUrl, pin.chainId, "risex");
143
+ const { txHash } = await sendMasterTx(rpc, {
144
+ venue: "risex", chainLabel: `RISE chain (${pin.chainId})`, chainId: pin.chainId, from: account, to: pin.auth,
145
+ data: RISEX_AUTH_IFACE.encodeFunctionData("disablePermission", [account, getAddress(signer), slot.anchor, slot.bit, permission, signature]),
146
+ value: 0n, label: `disable signer permission ${permission}`,
147
+ }, tx => master.signTransaction(tx));
148
+ return txHash;
149
+ };
150
+ }
151
+ /** Refuse before registering when the master cannot pay for the restriction transactions. */
152
+ export async function assertRestrictGas(network, account) {
153
+ const pin = RISEX_PINS[network];
154
+ if (pin.rpcUrl === null)
155
+ throw new PerpError("NOT_IMPLEMENTED", `RISEx ${network} has no known RPC endpoint`, { exchange: "risex" });
156
+ const rpc = evmProvider(pin.rpcUrl, pin.chainId);
157
+ await assertRpcChainId(rpc, pin.rpcUrl, pin.chainId, "risex");
158
+ const fee = await rpc.getFeeData();
159
+ const price = fee.maxFeePerGas ?? fee.gasPrice;
160
+ if (price === null)
161
+ throw new PerpError("EXCHANGE_ERROR", "RISE chain RPC returned no fee data — nothing was signed", { exchange: "risex" });
162
+ const need = price * RESTRICT_GAS_BUDGET * BigInt(RISEX_NON_TRADING_PERMISSIONS.length);
163
+ const have = await rpc.getBalance(account);
164
+ if (have < need) {
165
+ throw new PerpError("INSUFFICIENT_BALANCE", `Restricting a RISEx signer to trading-only needs up to ${need} wei of RISE-chain ETH for gas (${RISEX_NON_TRADING_PERMISSIONS.length} on-chain transactions from ${account}); the wallet has ${have} — nothing was registered`, {
166
+ exchange: "risex",
167
+ remediation: `Send a little ETH on RISE chain (chainId ${pin.chainId}) to ${account}, then retry: perp${network === "testnet" ? " -n testnet" : ""} wallet agent approve risex`,
168
+ });
169
+ }
170
+ }
171
+ /**
172
+ * Checks that refuse without side effects (signing context pins, maintenance, the master's gas):
173
+ * run before registering — and, for --rotate, before the old signer is revoked.
174
+ */
175
+ export async function risexApprovePreflight(network, client, chain, account, assertGas) {
176
+ const ctx = await loadSigningContext(client, network);
177
+ // The reviewed contracts (implementation pins, no permission id ≥5) before anything is registered.
178
+ await assertReviewedContracts(chain, network);
179
+ await assertGas(account);
180
+ return ctx;
181
+ }
182
+ /** --rotate: the side-effect-free checks for the NEW signer, run before the old one is revoked. */
183
+ export async function risexRotatePreflight(masterOwsWallet, passphrase, network) {
184
+ const { OwsEvmSigner } = await import("../../signer/ows-evm.js");
185
+ const account = getAddress(OwsEvmSigner.create(masterOwsWallet, passphrase ?? "").getAddress());
186
+ await risexApprovePreflight(network, risexClient(network), chainReader(network), account, a => assertRestrictGas(network, a));
187
+ }
188
+ /** Full approve with OWS master + OWS agent wallet. Returns the meta of a trading-only signer (the caller persists it). */
189
+ export async function approveRisexAgent(opts) {
190
+ const { OwsEvmSigner } = await import("../../signer/ows-evm.js");
191
+ const { loadOws } = await import("../../signer/ows-loader.js");
192
+ const { setVenueAgent } = await import("../../agent-wallet/store.js");
193
+ const master = OwsEvmSigner.create(opts.masterOwsWallet, opts.passphrase ?? "");
194
+ const ows = loadOws();
195
+ return approveRisexAgentWith(opts, {
196
+ master,
197
+ ows: {
198
+ walletExists: name => ows.listWallets().some((w) => w.name === name),
199
+ createSigner: name => {
200
+ const wallet = ows.createWallet(name, "");
201
+ if (!wallet.accounts.some((a) => a.chainId.startsWith("eip155:"))) {
202
+ try {
203
+ ows.deleteWallet(name);
204
+ }
205
+ catch { /* reported below */ }
206
+ throw new PerpError("INVALID_PARAMS", `Agent wallet "${name}" has no EVM account`, { remediation: "Check OWS vault configuration" });
207
+ }
208
+ return OwsEvmSigner.create(name, "");
209
+ },
210
+ deleteWallet: name => ows.deleteWallet(name),
211
+ },
212
+ client: risexClient(opts.network),
213
+ chain: chainReader(opts.network),
214
+ sender: signer => masterDisablePermissionSender(opts.network, master, signer),
215
+ assertGas: account => assertRestrictGas(opts.network, account),
216
+ saveMeta: meta => setVenueAgent("risex", meta),
217
+ sleep: ms => new Promise(r => setTimeout(r, ms)),
218
+ });
219
+ }
220
+ /** The approve orchestration (see the module doc). */
221
+ export async function approveRisexAgentWith(opts, deps) {
222
+ const { network, expiresAt } = opts;
223
+ const net = network === "testnet" ? " -n testnet" : "";
224
+ const expiration = Math.floor(expiresAt.getTime() / 1000);
225
+ if (!(expiration > Math.floor(Date.now() / 1000) + 60) || expiration > 0xffffffff) {
226
+ throw new PerpError("INVALID_PARAMS", `RISEx signer expiry ${expiresAt.toISOString()} must be in the future (uint32 seconds)`, { remediation: "Use e.g. --expires-in 30d" });
227
+ }
228
+ const { master, ows, client } = deps;
229
+ const account = getAddress(master.getAddress());
230
+ // Everything that can refuse without side effects runs first.
231
+ const ctx = await risexApprovePreflight(network, client, deps.chain, account, deps.assertGas);
232
+ const agentWalletName = risexAgentWalletName(opts.masterOwsWallet, network);
233
+ if (ows.walletExists(agentWalletName)) {
234
+ throw new PerpError("INVALID_PARAMS", `OWS wallet "${agentWalletName}" already exists`, {
235
+ remediation: `Replace the existing signer: perp${net} wallet agent approve risex --rotate (or delete an orphan wallet: ows wallet delete ${agentWalletName})`,
236
+ });
237
+ }
238
+ const signer = ows.createSigner(agentWalletName);
239
+ const signerAddress = getAddress(signer.getAddress());
240
+ const dropWallet = () => { try {
241
+ ows.deleteWallet(agentWalletName);
242
+ }
243
+ catch { /* the remediation names the wallet */ } };
244
+ const metaOf = (tradingOnly, disabled, verifiedAt) => ({
245
+ kind: "risex", agentName: opts.agentName, agentWalletName,
246
+ signerAddress: signerAddress, accountAddress: account,
247
+ expiresAt: new Date(expiration * 1000).toISOString(),
248
+ permissions: { tradingOnly, disabled, verifiedAt },
249
+ network, approvedAt: new Date().toISOString(), source: "approved",
250
+ });
251
+ const verifyHint = `perp${net} wallet agent verify risex ${opts.agentName}`;
252
+ const revokeHint = `perp${net} wallet agent revoke risex ${opts.agentName}`;
253
+ /** A kept key is always recorded (not trading-only → the adapter refuses it). */
254
+ const keep = (disabled) => deps.saveMeta(metaOf(false, disabled, null));
255
+ /** Revoke after a failure; true = proven revoked (key dropped), false = could not (key + meta kept). */
256
+ const revokeAfterFailure = async (disabled) => {
257
+ try {
258
+ await revokeSigner({ client, network, ctx: await loadSigningContext(client, network), master, signer: signerAddress, nonceDir: deps.nonceDir });
259
+ dropWallet();
260
+ return { revoked: true };
261
+ }
262
+ catch (revokeErr) {
263
+ keep(disabled);
264
+ return { revoked: false, why: extractErrorMessage(revokeErr) };
265
+ }
266
+ };
267
+ try {
268
+ await registerSigner({ client, network, ctx, master, signer, expiration, nonceDir: deps.nonceDir });
269
+ }
270
+ catch (e) {
271
+ if (e instanceof PerpError && e.structured.code === "ORDER_PENDING") {
272
+ // Unknown outcome: the signer may be registered — keep its key AND record it.
273
+ keep([]);
274
+ throw new PerpError("ORDER_PENDING", e.message, {
275
+ ...(e.structured.details ?? {}), exchange: "risex", signer: signerAddress,
276
+ remediation: `Do NOT re-run approve yet. Check ${verifyHint}; if it is registered it is NOT restricted (full permissions): revoke it with ${revokeHint}`,
277
+ });
278
+ }
279
+ dropWallet();
280
+ throw e;
281
+ }
282
+ // A failed status read is treated like "not Active": the signer is registered (or may be) with
283
+ // FULL permissions, so it is never left behind unrecorded and unrevoked.
284
+ let status = null;
285
+ let readError = null;
286
+ for (let i = 0; i < CONFIRM_ATTEMPTS; i++) {
287
+ try {
288
+ status = await sessionKeyStatus(client, account, signerAddress);
289
+ readError = null;
290
+ }
291
+ catch (e) {
292
+ status = null;
293
+ readError = extractErrorMessage(e);
294
+ }
295
+ if (status === STATUS_ACTIVE)
296
+ break;
297
+ await deps.sleep(CONFIRM_INTERVAL_MS);
298
+ }
299
+ if (status !== STATUS_ACTIVE) {
300
+ const what = readError !== null ? `its status could not be read (${readError})` : `it did not become Active (status ${String(status)})`;
301
+ // It may still activate with FULL permissions: revoke it now (gasless, master-signed).
302
+ const r = await revokeAfterFailure([]);
303
+ if (r.revoked) {
304
+ throw new PerpError("APPROVE_FAILED", `RISEx signer ${signerAddress}: ${what}; it was revoked and its key deleted`, {
305
+ exchange: "risex", remediation: `Retry: perp${net} wallet agent approve risex`,
306
+ });
307
+ }
308
+ throw new PerpError("APPROVE_PARTIAL", `RISEx signer ${signerAddress}: ${what}, and the automatic revoke failed (${r.why})`, {
309
+ exchange: "risex",
310
+ remediation: `It may activate with FULL permissions. Revoke it: ${revokeHint} (or in the RISEx app). Its key is kept in OWS wallet "${agentWalletName}" and recorded as NOT trading-only`,
311
+ });
312
+ }
313
+ let restricted;
314
+ const disabledSoFar = [];
315
+ try {
316
+ const send = deps.sender(signerAddress);
317
+ restricted = await restrictSignerToTrading({
318
+ client, network, ctx, account, signer, chain: deps.chain, nonceDir: deps.nonceDir,
319
+ send: async (permission, slot, signature) => {
320
+ const h = await send(permission, slot, signature);
321
+ disabledSoFar.push(permission);
322
+ return h;
323
+ },
324
+ });
325
+ }
326
+ catch (e) {
327
+ const why = extractErrorMessage(e);
328
+ const r = await revokeAfterFailure(disabledSoFar);
329
+ if (!r.revoked) {
330
+ throw new PerpError("APPROVE_PARTIAL", `RISEx signer ${signerAddress} was registered but could NOT be restricted to trading-only (${why}) and the automatic revoke failed (${r.why})`, {
331
+ exchange: "risex",
332
+ remediation: `The signer may hold FULL permissions (including fund movement). Revoke it now: ${revokeHint} (or in the RISEx app). Its key is kept in OWS wallet "${agentWalletName}" and recorded as NOT trading-only`,
333
+ });
334
+ }
335
+ throw new PerpError("APPROVE_FAILED", `RISEx signer could not be restricted to trading-only (${why}); it was revoked and its key deleted`, {
336
+ exchange: "risex", cause: e instanceof PerpError ? e.structured : undefined,
337
+ remediation: `Fix the cause above (e.g. RISE-chain ETH for gas on the master wallet), then retry: perp${net} wallet agent approve risex`,
338
+ });
339
+ }
340
+ return metaOf(true, RISEX_NON_TRADING_PERMISSIONS.slice(), restricted.verifiedAt);
341
+ }
342
+ /** Venue status of a stored signer (1 = Active, 3 = Revoked; anything else = not registered). */
343
+ export async function risexSignerStatus(meta) {
344
+ return sessionKeyStatus(risexClient(meta.network), meta.accountAddress, meta.signerAddress);
345
+ }
346
+ /** Revoke a stored signer with the master wallet that owns it. */
347
+ export async function revokeRisexAgent(opts) {
348
+ const { OwsEvmSigner } = await import("../../signer/ows-evm.js");
349
+ const master = OwsEvmSigner.create(opts.masterOwsWallet, opts.passphrase ?? "");
350
+ if (master.getAddress().toLowerCase() !== opts.meta.accountAddress.toLowerCase()) {
351
+ throw new PerpError("INVALID_PARAMS", `Master wallet ${master.getAddress()} does not own RISEx signer ${opts.meta.signerAddress}`, {
352
+ remediation: `Use the OWS wallet for ${opts.meta.accountAddress}`,
353
+ });
354
+ }
355
+ const client = risexClient(opts.meta.network);
356
+ const ctx = await loadSigningContext(client, opts.meta.network);
357
+ return revokeSigner({ client, network: opts.meta.network, ctx, master, signer: opts.meta.signerAddress });
358
+ }
359
+ /** Verify a stored signer: Active on the venue and trading-only on-chain. */
360
+ export async function verifyRisexAgent(meta, chain = chainReader(meta.network)) {
361
+ const client = risexClient(meta.network);
362
+ const status = await sessionKeyStatus(client, meta.accountAddress, meta.signerAddress);
363
+ const permissions = {};
364
+ for (const p of RISEX_PERMISSION_IDS)
365
+ permissions[p] = await chain.hasPermission(meta.accountAddress, meta.signerAddress, p);
366
+ let tradingOnly = true;
367
+ let reason = null;
368
+ let bitmap = null;
369
+ try {
370
+ bitmap = `0x${(await assertTradingOnlyOnChain(chain, meta.network, meta.accountAddress, meta.signerAddress)).bitmap.toString(16)}`;
371
+ }
372
+ catch (e) {
373
+ if (!(e instanceof PerpError && e.structured.code === "POLICY_DENIED"))
374
+ throw e;
375
+ tradingOnly = false;
376
+ reason = e.message;
377
+ const b = e.structured.details?.bitmap;
378
+ bitmap = typeof b === "string" ? b : null;
379
+ }
380
+ // Per-id reads must agree with the bitmap verdict (belt and braces).
381
+ if (!RISEX_PERMISSION_IDS.every(p => permissions[p] === (p === RISEX_TRADING_PERMISSION)))
382
+ tradingOnly = false;
383
+ return { status, active: status === STATUS_ACTIVE, permissions, bitmap, tradingOnly, reason, expired: !(Date.parse(meta.expiresAt) > Date.now()) };
384
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * RISEx REST transport.
3
+ *
4
+ * Every response is `{data, request_id}` on success or `{error:{code, message}, request_id}` on
5
+ * failure, where `code` is a gRPC status name ("InvalidArgument", "FailedPrecondition",
6
+ * "Unauthenticated", "ResourceExhausted", "Internal", "Unavailable", …) and the message may carry a
7
+ * decoded contract revert ("FailedPrecondition: PlaceOrder reverted: SignerNotAuthorized(address)").
8
+ *
9
+ * Reads (GET) map to EXCHANGE_UNREACHABLE / EXCHANGE_ERROR / RATE_LIMITED. Writes (POST) are
10
+ * NEVER retried and classify their outcome:
11
+ * - no connection at all → not sent (definite, `sent: false`)
12
+ * - 4xx gRPC rejection (InvalidArgument, FailedPrecondition, Unauthenticated, NotFound,
13
+ * PermissionDenied, OutOfRange, AlreadyExists) → definite rejection (`sent: false` /
14
+ * ORDER_REJECTED) with the decoded error name
15
+ * - 429 ResourceExhausted → RATE_LIMITED, definite (`sent: false`)
16
+ * - timeout, reset, 5xx (E0701 ChainTransactionFailed, E0702 SequencerUnavailable, …),
17
+ * an unreadable body → outcome UNKNOWN (RisexSendUnknown)
18
+ */
19
+ import type { RateLimitPolicy } from "../venue.js";
20
+ export declare const RISEX_HTTP_TIMEOUT_MS = 30000;
21
+ export interface RisexErrorBody {
22
+ code: string;
23
+ message: string;
24
+ }
25
+ /** Decoded contract error name in a revert message ("… reverted: InvalidNonceIndex(uint8) => (208)" → "InvalidNonceIndex"). */
26
+ export declare function revertNameOf(message: string): string | null;
27
+ /** The send of a write could not be classified: it may have executed. Carries the cause for the caller's reconciliation. */
28
+ export declare class RisexSendUnknown extends Error {
29
+ readonly what: string;
30
+ readonly cause: unknown;
31
+ readonly status: number | null;
32
+ constructor(what: string, cause: unknown, status: number | null);
33
+ }
34
+ export declare class RisexClient {
35
+ readonly restUrl: string;
36
+ private readonly rateLimit;
37
+ private readonly rateKey;
38
+ constructor(restUrl: string, rateLimit: RateLimitPolicy, rateKey: string);
39
+ private url;
40
+ /** GET → `data`. */
41
+ get<T>(path: string, query?: Record<string, string | number | null | undefined>): Promise<T>;
42
+ /**
43
+ * POST a write. Returns `data` on success. Definite refusals throw a PerpError with
44
+ * `details.sent === false`; anything that may have executed throws RisexSendUnknown.
45
+ */
46
+ post<T>(path: string, body: unknown, what: string): Promise<T>;
47
+ }