dexterm-cli 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/LICENSE +661 -0
  2. package/dist/packages/agents/src/audit.js +122 -0
  3. package/dist/packages/agents/src/bus.js +379 -0
  4. package/dist/packages/agents/src/executor.js +177 -0
  5. package/dist/packages/agents/src/gate.js +176 -0
  6. package/dist/packages/agents/src/identity.js +179 -0
  7. package/dist/packages/agents/src/index.js +15 -0
  8. package/dist/packages/agents/src/proposals.js +153 -0
  9. package/dist/packages/agents/src/schema.js +87 -0
  10. package/dist/packages/agents/src/sdk.js +140 -0
  11. package/dist/packages/agents/src/tiers.js +114 -0
  12. package/dist/packages/agents/src/tools.js +69 -0
  13. package/dist/packages/agents/src/ws.js +158 -0
  14. package/dist/packages/cli/assets/claude/analyst-system.md +76 -0
  15. package/dist/packages/cli/assets/claude/plugin/.claude-plugin/plugin.json +5 -0
  16. package/dist/packages/cli/assets/claude/plugin/skills/REGISTRY.json +16 -0
  17. package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +67 -0
  18. package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +64 -0
  19. package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +63 -0
  20. package/dist/packages/cli/assets/claude/plugin/skills/trench-scan/SKILL.md +44 -0
  21. package/dist/packages/cli/assets/claude/schemas/larp.schema.json +117 -0
  22. package/dist/packages/cli/assets/claude/schemas/trench-scan.schema.json +71 -0
  23. package/dist/packages/cli/assets/claude/schemas/verdict.schema.json +115 -0
  24. package/dist/packages/cli/bin/dexterm.mjs +11 -0
  25. package/dist/packages/cli/src/agent-cmd.js +303 -0
  26. package/dist/packages/cli/src/agent-roster.js +26 -0
  27. package/dist/packages/cli/src/analyst/backend.js +526 -0
  28. package/dist/packages/cli/src/analyst/results.js +38 -0
  29. package/dist/packages/cli/src/analyst/session.js +319 -0
  30. package/dist/packages/cli/src/audit.js +51 -0
  31. package/dist/packages/cli/src/backtest-cmd.js +149 -0
  32. package/dist/packages/cli/src/boot-state.js +62 -0
  33. package/dist/packages/cli/src/custom.js +261 -0
  34. package/dist/packages/cli/src/display.js +191 -0
  35. package/dist/packages/cli/src/doctor.js +361 -0
  36. package/dist/packages/cli/src/feed.js +634 -0
  37. package/dist/packages/cli/src/fixtures.js +275 -0
  38. package/dist/packages/cli/src/follow-cmd.js +221 -0
  39. package/dist/packages/cli/src/images-config.js +59 -0
  40. package/dist/packages/cli/src/index.js +31 -0
  41. package/dist/packages/cli/src/kb-skill-cmd.js +240 -0
  42. package/dist/packages/cli/src/larp-record.js +21 -0
  43. package/dist/packages/cli/src/live/copy.js +80 -0
  44. package/dist/packages/cli/src/live/flow.js +233 -0
  45. package/dist/packages/cli/src/live/index.js +509 -0
  46. package/dist/packages/cli/src/live/intel.js +227 -0
  47. package/dist/packages/cli/src/live/newpairs.js +334 -0
  48. package/dist/packages/cli/src/live/screens/newpairs-live.js +219 -0
  49. package/dist/packages/cli/src/live/screens/token-live.js +459 -0
  50. package/dist/packages/cli/src/live/screens/watchlist-live.js +149 -0
  51. package/dist/packages/cli/src/live/token.js +585 -0
  52. package/dist/packages/cli/src/live/trade.js +929 -0
  53. package/dist/packages/cli/src/live/watchlist.js +178 -0
  54. package/dist/packages/cli/src/main.js +5583 -0
  55. package/dist/packages/cli/src/narrative-cmd.js +76 -0
  56. package/dist/packages/cli/src/pack-cmd.js +323 -0
  57. package/dist/packages/cli/src/plan-cmd.js +278 -0
  58. package/dist/packages/cli/src/popup.js +810 -0
  59. package/dist/packages/cli/src/rules-cmd.js +131 -0
  60. package/dist/packages/cli/src/rules.js +339 -0
  61. package/dist/packages/cli/src/scan/command.js +57 -0
  62. package/dist/packages/cli/src/scan/engine.js +419 -0
  63. package/dist/packages/cli/src/scan/observe.js +64 -0
  64. package/dist/packages/cli/src/scan/ui.js +98 -0
  65. package/dist/packages/cli/src/screener/filter.js +241 -0
  66. package/dist/packages/cli/src/screener/presets.js +80 -0
  67. package/dist/packages/cli/src/screener/rows.js +146 -0
  68. package/dist/packages/cli/src/screener/rug.js +50 -0
  69. package/dist/packages/cli/src/screener/sort.js +52 -0
  70. package/dist/packages/cli/src/screener/view.js +183 -0
  71. package/dist/packages/cli/src/screens/analyst-session.js +343 -0
  72. package/dist/packages/cli/src/screens/analyst.js +326 -0
  73. package/dist/packages/cli/src/screens/boot.js +74 -0
  74. package/dist/packages/cli/src/screens/bubbles.js +255 -0
  75. package/dist/packages/cli/src/screens/coin.js +33 -0
  76. package/dist/packages/cli/src/screens/follows.js +201 -0
  77. package/dist/packages/cli/src/screens/kol.js +90 -0
  78. package/dist/packages/cli/src/screens/narrative.js +90 -0
  79. package/dist/packages/cli/src/screens/newpairs.js +150 -0
  80. package/dist/packages/cli/src/screens/overlay.js +257 -0
  81. package/dist/packages/cli/src/screens/plans.js +126 -0
  82. package/dist/packages/cli/src/screens/positions.js +176 -0
  83. package/dist/packages/cli/src/screens/rules.js +170 -0
  84. package/dist/packages/cli/src/screens/screener.js +459 -0
  85. package/dist/packages/cli/src/screens/social-panel.js +107 -0
  86. package/dist/packages/cli/src/screens/ticket.js +342 -0
  87. package/dist/packages/cli/src/screens/token.js +312 -0
  88. package/dist/packages/cli/src/screens/trending.js +181 -0
  89. package/dist/packages/cli/src/screens/watchlist.js +104 -0
  90. package/dist/packages/cli/src/social/demo.js +96 -0
  91. package/dist/packages/cli/src/social/view.js +250 -0
  92. package/dist/packages/cli/src/state.js +463 -0
  93. package/dist/packages/cli/src/theme-cmd.js +126 -0
  94. package/dist/packages/cli/src/trade.js +319 -0
  95. package/dist/packages/cli/src/voice-hud.js +42 -0
  96. package/dist/packages/cli/src/wallet-cmd.js +440 -0
  97. package/dist/packages/cli/src/wallet-command.js +31 -0
  98. package/dist/packages/cli/src/widgets.js +208 -0
  99. package/dist/packages/cli/src/workspace-cmd.js +68 -0
  100. package/dist/packages/cli/src/x-cmd.js +154 -0
  101. package/dist/packages/custom/src/config.js +213 -0
  102. package/dist/packages/custom/src/index.js +10 -0
  103. package/dist/packages/custom/src/keymap.js +107 -0
  104. package/dist/packages/custom/src/layout.js +235 -0
  105. package/dist/packages/custom/src/packs.js +633 -0
  106. package/dist/packages/custom/src/rules.js +704 -0
  107. package/dist/packages/custom/src/schema.js +515 -0
  108. package/dist/packages/custom/src/themes.js +176 -0
  109. package/dist/packages/custom/src/widget-html.js +222 -0
  110. package/dist/packages/custom/src/workspace.js +141 -0
  111. package/dist/packages/data/src/analyst-actions.js +133 -0
  112. package/dist/packages/data/src/budget.js +88 -0
  113. package/dist/packages/data/src/cache.js +142 -0
  114. package/dist/packages/data/src/candles.js +168 -0
  115. package/dist/packages/data/src/clock.js +64 -0
  116. package/dist/packages/data/src/credits.js +137 -0
  117. package/dist/packages/data/src/detail.js +97 -0
  118. package/dist/packages/data/src/fast-lane.js +321 -0
  119. package/dist/packages/data/src/fixtures.js +135 -0
  120. package/dist/packages/data/src/health.js +245 -0
  121. package/dist/packages/data/src/http.js +277 -0
  122. package/dist/packages/data/src/images/cache.js +141 -0
  123. package/dist/packages/data/src/images/decode-worker.js +28 -0
  124. package/dist/packages/data/src/images/decode.js +81 -0
  125. package/dist/packages/data/src/images/fetch.js +162 -0
  126. package/dist/packages/data/src/images/gif.js +214 -0
  127. package/dist/packages/data/src/images/index.js +10 -0
  128. package/dist/packages/data/src/images/jpeg.js +544 -0
  129. package/dist/packages/data/src/images/png.js +310 -0
  130. package/dist/packages/data/src/images/service.js +334 -0
  131. package/dist/packages/data/src/images/types.js +27 -0
  132. package/dist/packages/data/src/index.js +208 -0
  133. package/dist/packages/data/src/intel/helius-history.js +59 -0
  134. package/dist/packages/data/src/intel/holder-class.js +125 -0
  135. package/dist/packages/data/src/intel/rules.js +421 -0
  136. package/dist/packages/data/src/intel/scan.js +170 -0
  137. package/dist/packages/data/src/larp/evidence.js +288 -0
  138. package/dist/packages/data/src/larp/links.js +137 -0
  139. package/dist/packages/data/src/larp/robots.js +90 -0
  140. package/dist/packages/data/src/larp/safe-fetch.js +389 -0
  141. package/dist/packages/data/src/limiter.js +341 -0
  142. package/dist/packages/data/src/model.js +12 -0
  143. package/dist/packages/data/src/paper.js +331 -0
  144. package/dist/packages/data/src/rpc-config.js +126 -0
  145. package/dist/packages/data/src/rpc-pool.js +166 -0
  146. package/dist/packages/data/src/social/chain.js +83 -0
  147. package/dist/packages/data/src/social/dedupe.js +205 -0
  148. package/dist/packages/data/src/social/feed.js +462 -0
  149. package/dist/packages/data/src/social/firewall.js +110 -0
  150. package/dist/packages/data/src/social/index.js +11 -0
  151. package/dist/packages/data/src/social/kol.js +269 -0
  152. package/dist/packages/data/src/social/model.js +11 -0
  153. package/dist/packages/data/src/social/rank.js +184 -0
  154. package/dist/packages/data/src/social/rss.js +113 -0
  155. package/dist/packages/data/src/social/summary.js +104 -0
  156. package/dist/packages/data/src/social/venues.js +186 -0
  157. package/dist/packages/data/src/sources/dexscreener.js +170 -0
  158. package/dist/packages/data/src/sources/geckoterminal.js +133 -0
  159. package/dist/packages/data/src/sources/goplus.js +161 -0
  160. package/dist/packages/data/src/sources/jupiter-swap.js +236 -0
  161. package/dist/packages/data/src/sources/jupiter.js +198 -0
  162. package/dist/packages/data/src/sources/pda.js +113 -0
  163. package/dist/packages/data/src/sources/pump-curve.js +58 -0
  164. package/dist/packages/data/src/sources/pumpportal.js +247 -0
  165. package/dist/packages/data/src/sources/rugcheck.js +71 -0
  166. package/dist/packages/data/src/sources/solana-rpc.js +299 -0
  167. package/dist/packages/data/src/sources/solana-ws.js +287 -0
  168. package/dist/packages/data/src/store.js +289 -0
  169. package/dist/packages/data/src/trade/bridge.js +464 -0
  170. package/dist/packages/data/src/trade/caps.js +323 -0
  171. package/dist/packages/data/src/trade/copy/auto.js +385 -0
  172. package/dist/packages/data/src/trade/copy/follows.js +103 -0
  173. package/dist/packages/data/src/trade/copy/index.js +9 -0
  174. package/dist/packages/data/src/trade/copy/mirror.js +91 -0
  175. package/dist/packages/data/src/trade/copy/stats.js +120 -0
  176. package/dist/packages/data/src/trade/copy/tracker.js +158 -0
  177. package/dist/packages/data/src/trade/copy/trades.js +87 -0
  178. package/dist/packages/data/src/trade/copy/types.js +6 -0
  179. package/dist/packages/data/src/trade/executor.js +933 -0
  180. package/dist/packages/data/src/trade/kill-switch.js +47 -0
  181. package/dist/packages/data/src/trade/link-store.js +52 -0
  182. package/dist/packages/data/src/trade/plans/confirm.js +157 -0
  183. package/dist/packages/data/src/trade/plans/engine.js +784 -0
  184. package/dist/packages/data/src/trade/plans/executor-live.js +220 -0
  185. package/dist/packages/data/src/trade/plans/executor-paper.js +44 -0
  186. package/dist/packages/data/src/trade/plans/executor.js +11 -0
  187. package/dist/packages/data/src/trade/plans/index.js +12 -0
  188. package/dist/packages/data/src/trade/plans/journal.js +77 -0
  189. package/dist/packages/data/src/trade/plans/mandate.js +129 -0
  190. package/dist/packages/data/src/trade/plans/plan.js +179 -0
  191. package/dist/packages/data/src/trade/plans/service.js +168 -0
  192. package/dist/packages/data/src/trade/plans/types.js +6 -0
  193. package/dist/packages/data/src/trade/rpc-exec.js +229 -0
  194. package/dist/packages/data/src/trade/signer/client.js +197 -0
  195. package/dist/packages/data/src/trade/signer/fast-wallet.js +154 -0
  196. package/dist/packages/data/src/trade/signer/host.js +97 -0
  197. package/dist/packages/data/src/trade/signer/main.js +121 -0
  198. package/dist/packages/data/src/trade/signer/probe-builder.js +85 -0
  199. package/dist/packages/data/src/trade/signer/protocol.js +92 -0
  200. package/dist/packages/data/src/trade/signer/service.js +400 -0
  201. package/dist/packages/data/src/trade/signer/sol-usd-feed.js +98 -0
  202. package/dist/packages/data/src/trade/sol-usd.js +86 -0
  203. package/dist/packages/data/src/trade/swap-build.js +108 -0
  204. package/dist/packages/data/src/trade/token-delta.js +201 -0
  205. package/dist/packages/data/src/trade/tx-decode.js +632 -0
  206. package/dist/packages/data/src/util.js +24 -0
  207. package/dist/packages/data/src/watchlist.js +176 -0
  208. package/dist/packages/data/src/x/api.js +256 -0
  209. package/dist/packages/data/src/x/budget.js +66 -0
  210. package/dist/packages/data/src/x/config.js +49 -0
  211. package/dist/packages/data/src/x/index.js +10 -0
  212. package/dist/packages/data/src/x/mock-server.js +217 -0
  213. package/dist/packages/data/src/x/oauth.js +242 -0
  214. package/dist/packages/data/src/x/pkce.js +21 -0
  215. package/dist/packages/data/src/x/source.js +285 -0
  216. package/dist/packages/data/src/x/token-store.js +134 -0
  217. package/dist/packages/data/src/x/transport.js +71 -0
  218. package/dist/packages/data/vendor/solana-web3/LICENSE +20 -0
  219. package/dist/packages/data/vendor/solana-web3/NOTICE.md +13 -0
  220. package/dist/packages/data/vendor/solana-web3/index.iife.min.js +20 -0
  221. package/dist/packages/mcp/assets/cases.json +1194 -0
  222. package/dist/packages/mcp/assets/kb/entries/cabal-and-coordinated-launches.md +28 -0
  223. package/dist/packages/mcp/assets/kb/entries/common-rug-patterns.md +40 -0
  224. package/dist/packages/mcp/assets/kb/entries/dbc-mechanics.md +30 -0
  225. package/dist/packages/mcp/assets/kb/entries/dev-wallet-behaviour.md +33 -0
  226. package/dist/packages/mcp/assets/kb/entries/glossary.md +35 -0
  227. package/dist/packages/mcp/assets/kb/entries/holder-concentration.md +30 -0
  228. package/dist/packages/mcp/assets/kb/entries/jito-bundles.md +29 -0
  229. package/dist/packages/mcp/assets/kb/entries/launchlab-curve.md +31 -0
  230. package/dist/packages/mcp/assets/kb/entries/lp-burn-vs-lock.md +31 -0
  231. package/dist/packages/mcp/assets/kb/entries/metadata-and-copycats.md +29 -0
  232. package/dist/packages/mcp/assets/kb/entries/mint-freeze-authority.md +31 -0
  233. package/dist/packages/mcp/assets/kb/entries/narrative-lifecycle.md +32 -0
  234. package/dist/packages/mcp/assets/kb/entries/pump-bonding-curve.md +33 -0
  235. package/dist/packages/mcp/assets/kb/entries/pumpswap-migration.md +29 -0
  236. package/dist/packages/mcp/assets/kb/entries/sniping-and-snipers.md +29 -0
  237. package/dist/packages/mcp/assets/kb/entries/token2022-extensions-risk.md +32 -0
  238. package/dist/packages/mcp/assets/kb/entries/wash-trading-fake-volume.md +33 -0
  239. package/dist/packages/mcp/assets/skills/rug-check.md +52 -0
  240. package/dist/packages/mcp/assets/skills/token-dossier.md +57 -0
  241. package/dist/packages/mcp/assets/skills/trench-scan.md +40 -0
  242. package/dist/packages/mcp/bin/dexterm-mcp.mjs +16 -0
  243. package/dist/packages/mcp/src/dossier.js +78 -0
  244. package/dist/packages/mcp/src/firewall.js +21 -0
  245. package/dist/packages/mcp/src/index.js +22 -0
  246. package/dist/packages/mcp/src/server.js +849 -0
  247. package/dist/packages/mcp/src/user-content.js +434 -0
  248. package/dist/packages/packs/src/backtest.js +183 -0
  249. package/dist/packages/packs/src/digest.js +95 -0
  250. package/dist/packages/packs/src/firewall.js +57 -0
  251. package/dist/packages/packs/src/index.js +19 -0
  252. package/dist/packages/packs/src/larp-eval.js +100 -0
  253. package/dist/packages/packs/src/narrative.js +309 -0
  254. package/dist/packages/packs/src/reason.js +135 -0
  255. package/dist/packages/packs/src/rug-model.js +137 -0
  256. package/dist/packages/packs/src/schema.js +54 -0
  257. package/dist/packages/packs/src/token-dossier.js +174 -0
  258. package/dist/packages/packs/src/verdicts.js +17 -0
  259. package/dist/packages/packs/src/wallet-dossier.js +105 -0
  260. package/dist/packages/popup/src/config.js +71 -0
  261. package/dist/packages/popup/src/dock.js +271 -0
  262. package/dist/packages/popup/src/host.js +210 -0
  263. package/dist/packages/popup/src/index.js +12 -0
  264. package/dist/packages/popup/src/loopback.js +53 -0
  265. package/dist/packages/popup/src/open.js +183 -0
  266. package/dist/packages/popup/src/pages/bubbles-lib.js +160 -0
  267. package/dist/packages/popup/src/pages/bubbles.css +23 -0
  268. package/dist/packages/popup/src/pages/bubbles.html +36 -0
  269. package/dist/packages/popup/src/pages/bubbles.js +214 -0
  270. package/dist/packages/popup/src/pages/chart-lib.js +262 -0
  271. package/dist/packages/popup/src/pages/chart.css +50 -0
  272. package/dist/packages/popup/src/pages/chart.html +56 -0
  273. package/dist/packages/popup/src/pages/chart.js +554 -0
  274. package/dist/packages/popup/src/pages/common.css +34 -0
  275. package/dist/packages/popup/src/pages/common.js +187 -0
  276. package/dist/packages/popup/src/pages/feeds.html +20 -0
  277. package/dist/packages/popup/src/pages/feeds.js +77 -0
  278. package/dist/packages/popup/src/pages/narrative.html +20 -0
  279. package/dist/packages/popup/src/pages/narrative.js +61 -0
  280. package/dist/packages/popup/src/pages/plans.css +30 -0
  281. package/dist/packages/popup/src/pages/plans.html +26 -0
  282. package/dist/packages/popup/src/pages/plans.js +82 -0
  283. package/dist/packages/popup/src/pages/roster.css +41 -0
  284. package/dist/packages/popup/src/pages/roster.html +25 -0
  285. package/dist/packages/popup/src/pages/roster.js +75 -0
  286. package/dist/packages/popup/src/pages/token.html +20 -0
  287. package/dist/packages/popup/src/pages/token.js +112 -0
  288. package/dist/packages/popup/src/pages/widget-sdk.js +224 -0
  289. package/dist/packages/popup/src/pages/widget.css +5 -0
  290. package/dist/packages/popup/src/pages/widget.html +21 -0
  291. package/dist/packages/popup/src/pages/widget.js +85 -0
  292. package/dist/packages/popup/src/protocol.js +31 -0
  293. package/dist/packages/popup/src/selftest.js +156 -0
  294. package/dist/packages/popup/src/server.js +582 -0
  295. package/dist/packages/popup/src/sinkproxy.js +167 -0
  296. package/dist/packages/popup/src/widget-html.js +231 -0
  297. package/dist/packages/popup/vendor/lightweight-charts/LICENSE +201 -0
  298. package/dist/packages/popup/vendor/lightweight-charts/NOTICE +2 -0
  299. package/dist/packages/popup/vendor/lightweight-charts/README.md +30 -0
  300. package/dist/packages/popup/vendor/lightweight-charts/VERSION +1 -0
  301. package/dist/packages/popup/vendor/lightweight-charts/lightweight-charts.standalone.production.js +7 -0
  302. package/dist/packages/recorder/bin/dexterm-recorder.mjs +11 -0
  303. package/dist/packages/recorder/src/cli.js +113 -0
  304. package/dist/packages/recorder/src/export.js +71 -0
  305. package/dist/packages/recorder/src/index.js +20 -0
  306. package/dist/packages/recorder/src/labels.js +151 -0
  307. package/dist/packages/recorder/src/open.js +29 -0
  308. package/dist/packages/recorder/src/recorder.js +553 -0
  309. package/dist/packages/recorder/src/schedule.js +43 -0
  310. package/dist/packages/recorder/src/schema.js +135 -0
  311. package/dist/packages/recorder/src/snapshot.js +191 -0
  312. package/dist/packages/recorder/src/sweep.js +52 -0
  313. package/dist/packages/recorder/src/verdicts.js +52 -0
  314. package/dist/packages/tui/src/bubbles.js +415 -0
  315. package/dist/packages/tui/src/canvas.js +370 -0
  316. package/dist/packages/tui/src/chrome.js +85 -0
  317. package/dist/packages/tui/src/color.js +133 -0
  318. package/dist/packages/tui/src/flush.js +62 -0
  319. package/dist/packages/tui/src/fmt.js +188 -0
  320. package/dist/packages/tui/src/fx-trade.js +278 -0
  321. package/dist/packages/tui/src/fx.js +527 -0
  322. package/dist/packages/tui/src/images/blocks.js +141 -0
  323. package/dist/packages/tui/src/images/coin.js +22 -0
  324. package/dist/packages/tui/src/images/detect.js +140 -0
  325. package/dist/packages/tui/src/images/index.js +7 -0
  326. package/dist/packages/tui/src/images/layer.js +146 -0
  327. package/dist/packages/tui/src/images/pixels.js +157 -0
  328. package/dist/packages/tui/src/images/protocols.js +87 -0
  329. package/dist/packages/tui/src/index.js +15 -0
  330. package/dist/packages/tui/src/keymap.js +355 -0
  331. package/dist/packages/tui/src/layout.js +11 -0
  332. package/dist/packages/tui/src/sound.js +177 -0
  333. package/dist/packages/tui/src/theme.js +182 -0
  334. package/dist/packages/tui/src/widgets.js +598 -0
  335. package/dist/packages/voice/src/controller.js +395 -0
  336. package/dist/packages/voice/src/doctor.js +63 -0
  337. package/dist/packages/voice/src/engines.js +44 -0
  338. package/dist/packages/voice/src/index.js +16 -0
  339. package/dist/packages/voice/src/router.js +502 -0
  340. package/dist/packages/voice/src/stt/apple-helper.swift.js +103 -0
  341. package/dist/packages/voice/src/stt/apple.js +179 -0
  342. package/dist/packages/voice/src/stt/fake.js +34 -0
  343. package/dist/packages/voice/src/stt/whisper.js +102 -0
  344. package/dist/packages/voice/src/summary.js +44 -0
  345. package/dist/packages/voice/src/tts/fake.js +30 -0
  346. package/dist/packages/voice/src/tts/say.js +96 -0
  347. package/dist/packages/voice/src/types.js +3 -0
  348. package/package.json +42 -4
  349. package/README.md +0 -3
@@ -0,0 +1,179 @@
1
+ // task-1355 — ORDER PLAN schema, validation, canonical hash, read-back (SPEC §6.13.1).
2
+ //
3
+ // A plan is a PROPOSAL until a confirmation proof arms it. Voice, Claude, popups and batch builders can
4
+ // only produce the input shape below; nothing here carries an `armed`/`status`/`caps` field from the
5
+ // proposer — those are stripped, never trusted. Amounts are USD (entry) and fractions of the ORIGINAL
6
+ // filled position (exits). Market-cap triggers are stored WITH the supply snapshot they were proposed
7
+ // against, so the read-back can say "300k = $0.0003 at 1.00B supply" and a later supply change is visible.
8
+ //
9
+ // No signing, no wallet, no network. `node:crypto` is used only for the SHA-256 plan hash.
10
+ // Ported from the task-1292 reference simulation (research/task-1292/sim/plan.mjs), typed and frozen.
11
+ import { createHash } from 'node:crypto';
12
+ const MINT_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
13
+ export const PLAN_SCHEMA_VERSION = 1;
14
+ /** Which fields are "material": a change to any of them changes the hash and voids every confirmation. */
15
+ export const MATERIAL_FIELDS = [
16
+ 'v', 'mint', 'decimals', 'side', 'sizeUsd', 'entry', 'exits', 'moonBagFraction', 'stop',
17
+ 'slippageBps', 'maxPriceImpactPct', 'supplyAtProposal', 'expiresAt', 'chunkUsd',
18
+ ];
19
+ export class PlanError extends Error {
20
+ reason;
21
+ constructor(reason, message) {
22
+ super(message);
23
+ this.name = 'PlanError';
24
+ this.reason = reason;
25
+ }
26
+ }
27
+ function num(x, name, { min = 0, max = Infinity, integer = false } = {}) {
28
+ if (typeof x !== 'number' || !Number.isFinite(x))
29
+ throw new PlanError('bad-request', `${name} must be a finite number`);
30
+ if (x < min || x > max)
31
+ throw new PlanError('bad-request', `${name} must be in [${min}, ${max}]`);
32
+ if (integer && !Number.isInteger(x))
33
+ throw new PlanError('bad-request', `${name} must be an integer`);
34
+ return x;
35
+ }
36
+ function trigger(t, name) {
37
+ if (t === null || typeof t !== 'object')
38
+ throw new PlanError('bad-request', `${name} must be an object`);
39
+ const kind = t.kind;
40
+ if (kind === 'immediate')
41
+ return { kind: 'immediate' };
42
+ if (kind !== 'mc-below' && kind !== 'mc-above' && kind !== 'price-below' && kind !== 'price-above') {
43
+ throw new PlanError('bad-request', `${name}.kind unknown`);
44
+ }
45
+ return { kind, value: num(t.value, `${name}.value`, { min: 1e-12 }) };
46
+ }
47
+ /** Sanitize untrusted display text (token symbol / name): printable ASCII, short. Never used in policy. */
48
+ export function displayText(s, max = 16) {
49
+ return typeof s === 'string' ? s.replace(/[^\x20-\x7e]/g, '?').slice(0, max) : '';
50
+ }
51
+ /**
52
+ * Validate an untrusted proposal input and return a frozen plan (status 'proposed'). `ctx` is what the
53
+ * TUI knows and the proposer does not control: the supply snapshot, the clock, and the provenance.
54
+ */
55
+ export function proposePlan(input, ctx) {
56
+ if (input === null || typeof input !== 'object')
57
+ throw new PlanError('bad-request', 'plan must be an object');
58
+ const x = input;
59
+ if (typeof x.mint !== 'string' || !MINT_RE.test(x.mint))
60
+ throw new PlanError('bad-request', 'mint must be a base58 address');
61
+ const side = x.side === 'sell' ? 'sell' : x.side === 'buy' ? 'buy' : null;
62
+ if (!side)
63
+ throw new PlanError('bad-request', 'side must be buy or sell');
64
+ const decimals = num(x.decimals, 'decimals', { min: 0, max: 18, integer: true });
65
+ const sizeUsd = num(x.sizeUsd, 'sizeUsd', { min: 0.01, max: 1e7 });
66
+ const entry = trigger(x.entry ?? { kind: 'immediate' }, 'entry');
67
+ const exitsIn = Array.isArray(x.exits) ? x.exits : [];
68
+ if (exitsIn.length > 8)
69
+ throw new PlanError('bad-request', 'at most 8 exit legs');
70
+ let sum = 0;
71
+ const cleanExits = exitsIn.map((e, i) => {
72
+ const at = trigger(e?.at, `exits[${i}].at`);
73
+ if (at.kind !== 'mc-above' && at.kind !== 'price-above')
74
+ throw new PlanError('bad-request', `exits[${i}] must be an above-trigger (take profit)`);
75
+ const fraction = num(e?.fraction, `exits[${i}].fraction`, { min: 0.01, max: 1 });
76
+ sum += fraction;
77
+ return { at, fraction };
78
+ });
79
+ // exits must be ascending so the ladder is unambiguous ("sell half at 600k, a quarter at 1.2M")
80
+ for (let i = 1; i < cleanExits.length; i++) {
81
+ const cur = cleanExits[i].at;
82
+ const prev = cleanExits[i - 1].at;
83
+ const curVal = cur.kind === 'immediate' ? 0 : cur.value;
84
+ const prevVal = prev.kind === 'immediate' ? 0 : prev.value;
85
+ if (curVal <= prevVal)
86
+ throw new PlanError('bad-request', 'exit legs must be ascending');
87
+ }
88
+ if (sum > 1 + 1e-9)
89
+ throw new PlanError('bad-request', 'exit fractions sum above 100%');
90
+ const moonBagFraction = Math.round((1 - sum) * 1e6) / 1e6;
91
+ const stop = x.stop == null ? null : trigger(x.stop, 'stop');
92
+ if (stop && stop.kind !== 'mc-below' && stop.kind !== 'price-below')
93
+ throw new PlanError('bad-request', 'stop must be a below-trigger');
94
+ if (stop && entry.kind === 'mc-below' && stop.kind === 'mc-below' && stop.value >= entry.value)
95
+ throw new PlanError('bad-request', 'stop must be below the entry');
96
+ const slippageBps = num(x.slippageBps ?? 300, 'slippageBps', { min: 0, max: 1000, integer: true });
97
+ const maxPriceImpactPct = num(x.maxPriceImpactPct ?? 3, 'maxPriceImpactPct', { min: 0.01, max: 3 });
98
+ const ttlMs = num(x.ttlMs ?? 4 * 3600_000, 'ttlMs', { min: 10_000, max: 7 * 86_400_000, integer: true });
99
+ const chunkUsd = x.chunkUsd == null ? null : num(x.chunkUsd, 'chunkUsd', { min: 1 });
100
+ if (typeof ctx?.supply !== 'number' || !(ctx.supply > 0))
101
+ throw new PlanError('bad-request', 'ctx.supply (token supply snapshot) is required');
102
+ if (!ctx.clock)
103
+ throw new PlanError('bad-request', 'ctx.clock is required');
104
+ const createdAt = ctx.clock.now();
105
+ const plan = {
106
+ v: PLAN_SCHEMA_VERSION,
107
+ id: ctx.id ?? `plan_${createdAt.toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
108
+ mint: x.mint, symbol: displayText(x.symbol) || x.mint.slice(0, 4), decimals, side, sizeUsd,
109
+ entry, exits: cleanExits, moonBagFraction, stop, slippageBps, maxPriceImpactPct,
110
+ supplyAtProposal: ctx.supply, chunkUsd,
111
+ createdAt, expiresAt: createdAt + ttlMs,
112
+ // provenance (NOT material; the proposer cannot influence policy through it)
113
+ source: { by: ctx.by ?? 'user', ref: typeof ctx.ref === 'string' ? ctx.ref.slice(0, 80) : null, batchId: ctx.batchId ?? null },
114
+ status: 'proposed',
115
+ hash: '',
116
+ };
117
+ plan.hash = planHash(plan);
118
+ return Object.freeze(plan);
119
+ }
120
+ /** Canonical JSON of the material fields only (sorted keys, fixed order). */
121
+ export function canonicalPlan(plan) {
122
+ const pick = {};
123
+ const rec = plan;
124
+ for (const k of MATERIAL_FIELDS)
125
+ pick[k] = rec[k] === undefined ? null : rec[k];
126
+ return JSON.stringify(pick, (_k, v) => (v && typeof v === 'object' && !Array.isArray(v))
127
+ ? Object.fromEntries(Object.keys(v).sort().map((kk) => [kk, v[kk]]))
128
+ : v);
129
+ }
130
+ export function planHash(plan) {
131
+ return createHash('sha256').update(canonicalPlan(plan)).digest('hex');
132
+ }
133
+ /** Return a NEW plan with a material field changed (a mutated plan is a different plan: new hash, new status). */
134
+ export function amendPlan(plan, patch, ctx) {
135
+ const input = {
136
+ mint: plan.mint, symbol: plan.symbol, decimals: plan.decimals, side: plan.side, sizeUsd: plan.sizeUsd,
137
+ entry: plan.entry, exits: plan.exits, stop: plan.stop, slippageBps: plan.slippageBps,
138
+ maxPriceImpactPct: plan.maxPriceImpactPct, ttlMs: plan.expiresAt - plan.createdAt, chunkUsd: plan.chunkUsd,
139
+ ...patch,
140
+ };
141
+ return proposePlan(input, {
142
+ supply: plan.supplyAtProposal, clock: ctx.clock,
143
+ by: ctx.by ?? plan.source.by, ref: ctx.ref ?? plan.source.ref, batchId: plan.source.batchId,
144
+ });
145
+ }
146
+ export function fmtUsd(n) {
147
+ if (n >= 1e9)
148
+ return `$${(n / 1e9).toFixed(2)}B`;
149
+ if (n >= 1e6)
150
+ return `$${(n / 1e6).toFixed(2)}M`;
151
+ if (n >= 1e3)
152
+ return `$${(n / 1e3).toFixed(0)}k`;
153
+ return `$${n.toFixed(2)}`;
154
+ }
155
+ /** The price a market-cap trigger means at the proposal's supply snapshot. */
156
+ export function priceForMc(mcUsd, supply) { return mcUsd / supply; }
157
+ function describeTrigger(t, supply) {
158
+ switch (t.kind) {
159
+ case 'immediate': return 'now';
160
+ case 'mc-below': return `if market cap dips to ${fmtUsd(t.value)} (price $${priceForMc(t.value, supply).toPrecision(3)} at ${(supply / 1e9).toFixed(2)}B supply)`;
161
+ case 'mc-above': return `at ${fmtUsd(t.value)} market cap`;
162
+ case 'price-below': return `if price dips to $${t.value}`;
163
+ case 'price-above': return `at $${t.value}`;
164
+ default: return '?';
165
+ }
166
+ }
167
+ /** The spoken/rendered read-back. The challenge code is appended by the confirm gate, not here. */
168
+ export function readBack(plan) {
169
+ const s = plan.supplyAtProposal;
170
+ const parts = [`${plan.side === 'buy' ? 'Buy' : 'Sell'} ${fmtUsd(plan.sizeUsd)} of ${plan.symbol} (mint ${plan.mint.slice(0, 4)}…${plan.mint.slice(-4)}) ${describeTrigger(plan.entry, s)}`];
171
+ for (const e of plan.exits)
172
+ parts.push(`sell ${Math.round(e.fraction * 100)}% ${describeTrigger(e.at, s)}`);
173
+ if (plan.moonBagFraction > 0)
174
+ parts.push(`keep ${Math.round(plan.moonBagFraction * 100)}% as a moon bag`);
175
+ if (plan.stop)
176
+ parts.push(`stop ${describeTrigger(plan.stop, s).replace(/^if /, '')}`);
177
+ parts.push(`slippage ${(plan.slippageBps / 100).toFixed(1)}%`, `expires in ${Math.round((plan.expiresAt - plan.createdAt) / 60_000)} min`);
178
+ return parts.join('; ') + '.';
179
+ }
@@ -0,0 +1,168 @@
1
+ // task-1355 — PlanService: the app-facing facade over the plan engine.
2
+ //
3
+ // Owns the engine + hash-chained journal (persisted to ~/.dexterm/plans.journal) + the paper fire path +
4
+ // the confirm gate + the gate-off settings. It adds persistence (recover on load), untrusted-string
5
+ // parsing for typed commands (`mc-below:300k`, `mc-above:600k:50%`), and list/describe views the CLI,
6
+ // TUI and popup render. It adds NO new way to arm or sign: arming still requires a signer-issued
7
+ // keypress/Phantom gesture proof routed through the engine; voice/agents can only propose.
8
+ import { existsSync } from 'node:fs';
9
+ import { homedir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import { systemClock } from "../../clock.js";
12
+ import { Journal } from "./journal.js";
13
+ import { ConfirmGate, Settings, HOLD_MS } from "./confirm.js";
14
+ import { PlanEngine } from "./engine.js";
15
+ import { PaperPlanExecutor } from "./executor-paper.js";
16
+ import { ConfirmError } from "./confirm.js";
17
+ import { PolicyRefusal } from "./engine.js";
18
+ import { readBack, fmtUsd, PlanError } from "./plan.js";
19
+ /** Where the plan/mandate journal (audit log + recovery source) lives, shared by every dexterm process. */
20
+ export function defaultPlansJournalPath(env = process.env) {
21
+ const home = env.DEXTERM_HOME && env.DEXTERM_HOME.length ? env.DEXTERM_HOME : join(homedir(), '.dexterm');
22
+ return join(home, 'plans.journal');
23
+ }
24
+ /** Parse a memecoin-style amount: 300k / 1.2m / 2b / 600000. */
25
+ export function parseAmount(s) {
26
+ const m = /^\s*\$?([0-9]*\.?[0-9]+)\s*([kmb])?\s*$/i.exec(s);
27
+ if (!m)
28
+ throw new PlanError('bad-request', `cannot parse amount "${s}"`);
29
+ const mult = m[2] ? { k: 1e3, m: 1e6, b: 1e9 }[m[2].toLowerCase()] : 1;
30
+ return Number(m[1]) * mult;
31
+ }
32
+ /** Parse a trigger spec: `immediate` | `mc-below:300k` | `price-above:0.0005`. */
33
+ export function parseTrigger(s) {
34
+ const t = s.trim();
35
+ if (t === 'immediate' || t === 'now')
36
+ return { kind: 'immediate' };
37
+ const m = /^(mc-below|mc-above|price-below|price-above):(.+)$/.exec(t);
38
+ if (!m)
39
+ throw new PlanError('bad-request', `cannot parse trigger "${s}" (expected kind:value, e.g. mc-below:300k)`);
40
+ return { kind: m[1], value: parseAmount(m[2]) };
41
+ }
42
+ /** Parse an exit spec: `mc-above:600k:0.5` or `mc-above:600k:50%`. */
43
+ export function parseExit(s) {
44
+ const parts = s.split(':');
45
+ if (parts.length < 3)
46
+ throw new PlanError('bad-request', `cannot parse exit "${s}" (expected kind:value:fraction, e.g. mc-above:600k:50%)`);
47
+ const fracStr = parts[parts.length - 1].trim();
48
+ const at = parseTrigger(parts.slice(0, -1).join(':'));
49
+ const fraction = fracStr.endsWith('%') ? Number(fracStr.slice(0, -1)) / 100 : Number(fracStr);
50
+ if (!(fraction > 0 && fraction <= 1))
51
+ throw new PlanError('bad-request', `exit fraction "${fracStr}" must be in (0, 1] or a percent`);
52
+ return { at, fraction };
53
+ }
54
+ export class PlanService {
55
+ clock;
56
+ journal;
57
+ settings;
58
+ gate;
59
+ engine;
60
+ paper;
61
+ constructor(opts = {}) {
62
+ this.clock = opts.clock ?? systemClock;
63
+ const path = opts.journalPath ?? null;
64
+ this.journal = new Journal({ path, clock: this.clock });
65
+ this.settings = new Settings(this.clock, this.journal);
66
+ this.gate = new ConfirmGate({ clock: this.clock, journal: this.journal, settings: this.settings, rng: opts.rng });
67
+ this.paper = opts.executor ?? new PaperPlanExecutor({ clock: this.clock, solUsd: opts.solUsd });
68
+ const deps = { clock: this.clock, journal: this.journal, executor: this.paper, gate: this.gate, safety: opts.safety, priceMaxAgeMs: opts.priceMaxAgeMs, debounceTicks: opts.debounceTicks };
69
+ // Recover from an existing journal (replay + settle in-flight) or start fresh. The fire path always starts
70
+ // PAPER (task-1363): live is a per-run keypress opt-in, never recovered from the journal.
71
+ this.engine = path && existsSync(path) ? PlanEngine.recover(deps) : new PlanEngine(deps);
72
+ }
73
+ /** The executor the engine fires through right now (paper by default; the live adapter after useLive). */
74
+ get executor() { return this.engine.executor; }
75
+ /** 'paper' unless a live executor is installed. Surfaces that cannot prove 'paper' must render LIVE. */
76
+ mode() { return this.engine.executor.kind === 'live' ? 'live' : 'paper'; }
77
+ // ── the fire path (task-1363). Switching to LIVE is a physical keypress on the plans screen (never voice,
78
+ // an agent, a pipe or the popup): the gesture is required here AND the signer only fills from a fast wallet
79
+ // that was itself created by a TTY keypress. A switch while plans are armed/filled would strand their legs on
80
+ // the other executor's ledger, so both directions refuse until those plans are cancelled or finished. ──
81
+ useLive(executor, gesture) {
82
+ if (gesture?.gesture !== 'keypress')
83
+ throw new ConfirmError('live-needs-keypress', 'switching plan fills to LIVE needs a physical keypress');
84
+ if (executor.kind !== 'live')
85
+ throw new PlanError('bad-request', 'useLive needs a live executor');
86
+ const open = this.engine.openPlans();
87
+ if (open.length)
88
+ throw new PolicyRefusal('open-plans', `cancel or finish ${open.length} open plan(s) before switching the fire path`);
89
+ this.engine.replaceExecutor(executor);
90
+ this.journal.append('settings.executor', { data: { kind: 'live', network: 'devnet', by: 'keypress', label: executor.describe?.() ?? 'live' } });
91
+ }
92
+ usePaper() {
93
+ if (this.engine.executor === this.paper)
94
+ return;
95
+ const open = this.engine.openPlans();
96
+ if (open.length)
97
+ throw new PolicyRefusal('open-plans', `cancel or finish ${open.length} open plan(s) before switching the fire path`);
98
+ this.engine.replaceExecutor(this.paper);
99
+ this.journal.append('settings.executor', { data: { kind: 'paper' } });
100
+ }
101
+ // ── proposals (voice/claude/user/batch all land here; a proposal is inert until armed) ──
102
+ propose(input, ctx) {
103
+ return this.engine.propose(input, { supply: ctx.supply, by: ctx.by ?? 'user', ref: ctx.ref ?? null });
104
+ }
105
+ readBack(planId) { const p = this.engine.plan(planId); if (!p)
106
+ throw new PlanError('unknown-plan', 'no such plan'); return readBack(p); }
107
+ // ── arming (keypress/Phantom only; the CLI/TUI must supply a real gesture) ──
108
+ challenge(planId, tier = 'keypress') { return this.engine.challenge(planId, tier); }
109
+ confirm(challengeId, evidence) { return this.engine.confirm(challengeId, evidence); }
110
+ armPlan(planId, proof, mandateId) { return this.engine.armPlan(planId, proof, mandateId); }
111
+ cancelPlan(planId, reason = 'user') { return this.engine.cancelPlan(planId, reason); }
112
+ /** Arm a plan from a physical keypress gesture the host already captured (hold >= HOLD_MS). */
113
+ armPlanByKeypress(planId, mandateId, holdMs = HOLD_MS + 50) {
114
+ const ch = this.challenge(planId, 'keypress');
115
+ const proof = this.confirm(ch.id, { kind: 'keypress', holdMs });
116
+ return this.armPlan(planId, proof, mandateId);
117
+ }
118
+ // ── mandates (drafted by anyone; armed by keypress/Phantom only) ──
119
+ draftMandate(input) { return this.engine.draftMandate(input); }
120
+ armMandateByKeypress(mandateId, gesture = 'keypress') {
121
+ return this.engine.armMandate(mandateId, this.engine.issueMandateProof(mandateId, gesture));
122
+ }
123
+ revokeMandate(mandateId, reason = 'user') { this.engine.revokeMandate(mandateId, reason); }
124
+ // ── kill switch ──
125
+ panic(reason = 'panic key') { this.engine.panic(reason); }
126
+ // ── the "less safe" hands-free opt-in (Tier 4). The keypress gate is ON by default: voice/agents can only
127
+ // ever leave an inert proposal, and arming is a physical gesture. Turning the gate OFF enables hands-free
128
+ // (spoken) confirmation; it is accepted ONLY by a real keypress on the risk popup, journaled, auto-expires
129
+ // in 24h (RISK_ACCEPT_TTL_MS), and is reversible at any time. These thin methods keep every surface off the
130
+ // Settings object so the keypress-only rule lives in exactly one place. ──
131
+ riskStatus() {
132
+ const r = this.settings.risk;
133
+ return { gateOff: this.settings.gateOff, acceptedAt: r?.acceptedAt ?? null, expiresAt: r?.expiresAt ?? null, active: this.settings.handsFreeAllowed(this.clock.now()) };
134
+ }
135
+ acceptRisk(gesture) { this.settings.acceptRiskPopup(gesture); }
136
+ restoreGate() { this.settings.restoreGate(); }
137
+ // ── ticks (untrusted price hints; the engine re-confirms via its own quote) ──
138
+ tick(t) { this.engine.tick(t); }
139
+ // ── views ──
140
+ listPlans() {
141
+ return [...this.engine.recs.values()].map((r) => ({
142
+ id: r.plan.id, mint: r.plan.mint, symbol: r.plan.symbol, side: r.plan.side, sizeUsd: r.plan.sizeUsd,
143
+ status: r.status, mandateId: r.mandateId, tier: r.tier, readBack: readBack(r.plan), paused: r.paused !== null,
144
+ }));
145
+ }
146
+ listMandates() {
147
+ return [...this.engine.mandates.values()].map((x) => {
148
+ const L = this.engine.ledger(x.m.id);
149
+ return {
150
+ id: x.m.id, status: x.m.status, wallet: x.m.wallet, totalUsd: x.m.budget.totalUsd, perTradeUsd: x.m.budget.perTradeUsd,
151
+ perCoinUsd: x.m.budget.perCoinUsd, maxTier: x.m.maxTier, reservedUsd: L.reservedUsd, spentUsd: L.spentUsd,
152
+ realizedUsd: L.realizedUsd, lossUsd: L.lossUsd, expiresAt: x.m.expiresAt,
153
+ };
154
+ });
155
+ }
156
+ // ── audit log ──
157
+ verifyJournal() { return this.journal.verify(); }
158
+ exportJournal() { return this.journal.events.map((e) => JSON.stringify(e)).join('\n'); }
159
+ summary(planId) { return this.engine.summary(planId); }
160
+ position(planId) { return this.engine.position(planId); }
161
+ ledger(mandateId) { return this.engine.ledger(mandateId); }
162
+ }
163
+ /** One-line label for a plan status (CLI/TUI). */
164
+ export function planStatusLabel(row) {
165
+ const tier = row.tier ? ` · ${row.tier}` : '';
166
+ const paused = row.paused ? ' · PAUSED' : '';
167
+ return `${row.symbol} ${fmtUsd(row.sizeUsd)} ${row.side} — ${row.status}${tier}${paused}`;
168
+ }
@@ -0,0 +1,6 @@
1
+ // task-1355 — shared types for the order-plan + mandate engine (SPEC §6.13).
2
+ //
3
+ // These are the money-safe object shapes. Material plan fields are hashed (SHA-256 over canonical
4
+ // JSON); any change to a material field is a new plan with a new hash and voids every outstanding
5
+ // confirmation. Display/provenance fields are never read by policy.
6
+ export {};
@@ -0,0 +1,229 @@
1
+ // dexterm T1 — the ONLY place that can submit a transaction (SPEC §6.1 steps 2/5). Kept out of the
2
+ // read-only `sources/solana-rpc.ts` (which refuses non-read methods) so the broadcast path is small,
3
+ // explicit and auditable. It simulates against the user's RPC, submits an already-signed transaction
4
+ // (dexterm never signs), and confirms it. It holds no keys.
5
+ //
6
+ // DEVNET is the default RPC. Mainnet is only used when the caps allow it (--mainnet).
7
+ import { systemClock } from "../clock.js";
8
+ export const DEVNET_RPC = 'https://api.devnet.solana.com';
9
+ export const MAINNET_RPC = 'https://api.mainnet-beta.solana.com';
10
+ /**
11
+ * Hostnames that are devnet/testnet by construction. EXACT matches only (round 2, re-verify MEDIUM #5):
12
+ * the old substring test took `https://mainnet.helius-rpc.com/?api-key=devnet-team-key` for devnet and
13
+ * dropped every USD cap. Any other host — mainnet, a vendor URL, localhost, an unparseable string — is
14
+ * classified MAINNET so the caps apply by default. A deployment that really runs a private devnet can
15
+ * say so explicitly via `TradeRpc({ network: 'devnet' })`.
16
+ */
17
+ export const DEVNET_HOSTS = new Set([
18
+ 'api.devnet.solana.com',
19
+ 'api.testnet.solana.com',
20
+ 'devnet.helius-rpc.com',
21
+ ]);
22
+ /**
23
+ * DEFECT 4 / round 2: classify a trade from the RPC endpoint it will actually talk to, NOT from a
24
+ * free-text "network" label and NOT from a substring. The URL is PARSED and only an exact known
25
+ * devnet/testnet hostname is devnet. Everything else (including an empty or malformed URL) is mainnet.
26
+ */
27
+ export function rpcNetwork(url, explicit) {
28
+ if (explicit === 'devnet' || explicit === 'mainnet')
29
+ return explicit;
30
+ if (typeof url !== 'string' || url.length === 0)
31
+ return 'mainnet';
32
+ let host;
33
+ try {
34
+ const u = new URL(url);
35
+ if (u.protocol !== 'https:' && u.protocol !== 'http:' && u.protocol !== 'wss:' && u.protocol !== 'ws:')
36
+ return 'mainnet';
37
+ host = u.hostname.toLowerCase();
38
+ }
39
+ catch {
40
+ return 'mainnet';
41
+ }
42
+ return DEVNET_HOSTS.has(host) ? 'devnet' : 'mainnet';
43
+ }
44
+ /**
45
+ * Only these JSON-RPC methods are reachable; `simulateTransaction` is the gate before any submit.
46
+ * task-1337 adds the two READ methods the token-balance diff needs (the wallet's token accounts before
47
+ * the trade, and address-lookup-table contents); still nothing that moves funds besides `sendTransaction`.
48
+ */
49
+ const ALLOWED = new Set(['simulateTransaction', 'sendTransaction', 'getLatestBlockhash', 'getSignatureStatuses', 'getBalance', 'getMultipleAccounts', 'getTokenAccountsByOwner', 'requestAirdrop']);
50
+ /** The node's `[base64, 'base64']` (or a bare base64 string) account data → bytes; null when absent/odd. */
51
+ export function decodeAccountData(data) {
52
+ const s = Array.isArray(data) ? data[0] : data;
53
+ if (typeof s !== 'string')
54
+ return null;
55
+ if (Array.isArray(data) && data[1] !== undefined && data[1] !== 'base64')
56
+ return null;
57
+ return Buffer.from(s, 'base64');
58
+ }
59
+ /**
60
+ * The node ANSWERED with a JSON-RPC error. For `sendTransaction` this is a DEFINITE pre-broadcast
61
+ * rejection (preflight failure, bad blockhash, malformed tx): nothing reached the network, so the
62
+ * executor may refund the daily reservation. Any other failure (timeout, reset, 5xx) is a transport
63
+ * error whose outcome is UNKNOWN and must not be refunded (round 2, re-verify MEDIUM #4).
64
+ */
65
+ export class RpcRejectedError extends Error {
66
+ method;
67
+ code;
68
+ data;
69
+ constructor(method, message, code, data) {
70
+ super(`rpc ${method}: ${message}`);
71
+ this.name = 'RpcRejectedError';
72
+ this.method = method;
73
+ this.code = code;
74
+ this.data = data;
75
+ }
76
+ }
77
+ export class TradeRpc {
78
+ http;
79
+ url;
80
+ /** The classified network of `url` (or the explicit config). The executor caps by THIS. */
81
+ network;
82
+ meta = { source: 'solana-rpc', bucket: 'solana-rpc' };
83
+ clock;
84
+ constructor(http, opts = {}) {
85
+ this.http = http;
86
+ this.url = opts.url ?? DEVNET_RPC;
87
+ this.network = rpcNetwork(this.url, opts.network ?? null);
88
+ this.clock = opts.clock ?? systemClock;
89
+ }
90
+ async call(method, params) {
91
+ if (!ALLOWED.has(method))
92
+ throw new Error(`refusing RPC method ${method}`);
93
+ const r = await this.http.postJson(this.url, { jsonrpc: '2.0', id: 1, method, params }, this.meta);
94
+ if (r.data.error)
95
+ throw new RpcRejectedError(method, r.data.error.message ?? 'error', typeof r.data.error.code === 'number' ? r.data.error.code : null, r.data.error.data);
96
+ if (r.data.result === undefined)
97
+ throw new Error(`rpc ${method}: no result`);
98
+ return r.data.result;
99
+ }
100
+ /**
101
+ * Simulate a (possibly unsigned) base64 transaction with `replaceRecentBlockhash` + `sigVerify:false`
102
+ * so the plan can be checked before the user signs. A non-null `err` means the trade must be refused.
103
+ * `opts.accounts` asks the node for the POST-simulation state of those addresses (their lamports are
104
+ * what the executor compares against the pre-balance to learn how much SOL the tx really moves).
105
+ */
106
+ async simulate(txBase64, opts = {}) {
107
+ const cfg = { encoding: 'base64', replaceRecentBlockhash: true, sigVerify: false, commitment: 'confirmed' };
108
+ const want = opts.accounts ?? [];
109
+ if (want.length > 0)
110
+ cfg.accounts = { encoding: 'base64', addresses: want };
111
+ const res = await this.call('simulateTransaction', [txBase64, cfg]);
112
+ const v = res.value;
113
+ let accounts = null;
114
+ let accountsData = null;
115
+ if (want.length > 0) {
116
+ accounts = want.map((_, i) => {
117
+ const a = v.accounts?.[i];
118
+ return a && typeof a.lamports === 'number' && Number.isFinite(a.lamports) ? BigInt(Math.trunc(a.lamports)) : null;
119
+ });
120
+ if (Array.isArray(v.accounts))
121
+ accountsData = want.map((_, i) => decodeAccountData(v.accounts?.[i]?.data));
122
+ }
123
+ return { ok: v.err === null || v.err === undefined, err: v.err ?? null, logs: v.logs ?? null, unitsConsumed: typeof v.unitsConsumed === 'number' ? v.unitsConsumed : null, accounts, accountsData };
124
+ }
125
+ /**
126
+ * task-1363: devnet faucet (`requestAirdrop`) for the capped fast wallet — refused by classification on any
127
+ * non-devnet endpoint (mainnet nodes reject it anyway). Returns the airdrop signature.
128
+ */
129
+ async airdrop(pubkey, lamports) {
130
+ if (this.network !== 'devnet')
131
+ throw new Error('requestAirdrop runs only against a devnet RPC endpoint');
132
+ if (!(lamports > 0n))
133
+ throw new Error('airdrop needs a positive lamport amount');
134
+ return this.call('requestAirdrop', [pubkey, Number(lamports)]);
135
+ }
136
+ /** Lamport balance of an account (read-only `getBalance`). Used for the pre-simulation balance. */
137
+ async balance(pubkey) {
138
+ const res = await this.call('getBalance', [pubkey, { commitment: 'confirmed' }]);
139
+ if (typeof res.value !== 'number' || !Number.isFinite(res.value))
140
+ throw new Error('rpc getBalance: bad value');
141
+ return BigInt(Math.trunc(res.value));
142
+ }
143
+ /**
144
+ * task-1337: read-only snapshots of several accounts (`getMultipleAccounts`, base64). null per entry
145
+ * when the account does not exist. Used for the token-account PRE-balances the simulation diff
146
+ * compares against, and to read address-lookup tables so every account a v0 transaction can touch is
147
+ * known. Throws on a malformed answer (the executor fails closed).
148
+ */
149
+ async accountsInfo(pubkeys) {
150
+ if (pubkeys.length === 0)
151
+ return [];
152
+ const out = [];
153
+ for (let i = 0; i < pubkeys.length; i += 100) { // the node caps one call at 100 accounts
154
+ const chunk = pubkeys.slice(i, i + 100);
155
+ const res = await this.call('getMultipleAccounts', [chunk, { encoding: 'base64', commitment: 'confirmed' }]);
156
+ if (!Array.isArray(res.value) || res.value.length !== chunk.length)
157
+ throw new Error('rpc getMultipleAccounts: bad value');
158
+ for (const a of res.value) {
159
+ if (a === null) {
160
+ out.push(null);
161
+ continue;
162
+ }
163
+ const data = decodeAccountData(a.data);
164
+ if (typeof a.lamports !== 'number' || typeof a.owner !== 'string' || data === null)
165
+ throw new Error('rpc getMultipleAccounts: malformed account');
166
+ out.push({ lamports: BigInt(Math.trunc(a.lamports)), owner: a.owner, data });
167
+ }
168
+ }
169
+ return out;
170
+ }
171
+ /**
172
+ * task-1337: every token account `owner` holds under `programId` (`getTokenAccountsByOwner`, base64).
173
+ * These are the accounts a hostile route could drain; the executor watches their balances through the
174
+ * simulation. Throws on a malformed answer (fail closed).
175
+ */
176
+ async tokenAccountsByOwner(owner, programId) {
177
+ const res = await this.call('getTokenAccountsByOwner', [owner, { programId }, { encoding: 'base64', commitment: 'confirmed' }]);
178
+ if (!Array.isArray(res.value))
179
+ throw new Error('rpc getTokenAccountsByOwner: bad value');
180
+ return res.value.map((e) => {
181
+ const data = decodeAccountData(e.account?.data);
182
+ if (typeof e.pubkey !== 'string' || data === null)
183
+ throw new Error('rpc getTokenAccountsByOwner: malformed entry');
184
+ return { pubkey: e.pubkey, data };
185
+ });
186
+ }
187
+ /** A recent blockhash for a transaction dexterm builds itself (the devnet probe). Read-only. */
188
+ async latestBlockhash() {
189
+ const res = await this.call('getLatestBlockhash', [{ commitment: 'confirmed' }]);
190
+ const v = res.value;
191
+ if (!v || typeof v.blockhash !== 'string' || !/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(v.blockhash))
192
+ throw new Error('rpc getLatestBlockhash: bad value');
193
+ return { blockhash: v.blockhash, lastValidBlockHeight: typeof v.lastValidBlockHeight === 'number' ? v.lastValidBlockHeight : 0 };
194
+ }
195
+ /** Broadcast an already-signed base64 transaction. Returns the signature. */
196
+ async submit(signedTxBase64) {
197
+ return this.call('sendTransaction', [signedTxBase64, { encoding: 'base64', skipPreflight: false, maxRetries: 3, preflightCommitment: 'confirmed' }]);
198
+ }
199
+ /**
200
+ * One status lookup for a signature we already know (from the signed bytes). Returns null when the
201
+ * node has not seen it. Used after a transport error on submit to learn whether the send landed.
202
+ */
203
+ async signatureStatus(signature) {
204
+ const res = await this.call('getSignatureStatuses', [[signature], { searchTransactionHistory: true }]);
205
+ const st = res.value?.[0];
206
+ if (!st)
207
+ return null;
208
+ return { confirmationStatus: st.confirmationStatus ?? null, err: st.err ?? null, slot: st.slot ?? null };
209
+ }
210
+ /** Poll signature status until confirmed/finalized, a chain error, or the bound elapses. */
211
+ async confirm(signature, opts = {}) {
212
+ const deadline = this.clock.now() + (opts.timeoutMs ?? 60_000);
213
+ const interval = opts.intervalMs ?? 2_000;
214
+ for (;;) {
215
+ const res = await this.call('getSignatureStatuses', [[signature], { searchTransactionHistory: false }]);
216
+ const st = res.value[0];
217
+ if (st) {
218
+ if (st.err)
219
+ return { signature, confirmed: false, err: st.err, slot: st.slot ?? null };
220
+ if (st.confirmationStatus === 'confirmed' || st.confirmationStatus === 'finalized') {
221
+ return { signature, confirmed: true, err: null, slot: st.slot ?? null };
222
+ }
223
+ }
224
+ if (this.clock.now() >= deadline)
225
+ return { signature, confirmed: false, err: 'confirmation timed out', slot: null };
226
+ await this.clock.sleep(interval);
227
+ }
228
+ }
229
+ }