dexterm-cli 0.1.6 → 0.1.7

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 (331) hide show
  1. package/dist/packages/agents/src/schema.js +22 -1
  2. package/dist/packages/agents/src/story.js +11 -2
  3. package/dist/packages/agents/src/tiers.js +27 -2
  4. package/dist/packages/agents/src/tools.js +30 -2
  5. package/dist/packages/autotrade/assets/exit-profile.schema.json +492 -0
  6. package/dist/packages/autotrade/assets/profiles/moonbag.json +46 -0
  7. package/dist/packages/autotrade/assets/profiles/runner.json +88 -0
  8. package/dist/packages/autotrade/assets/profiles/scalp.json +88 -0
  9. package/dist/packages/autotrade/assets/profiles/swing.json +88 -0
  10. package/dist/packages/autotrade/assets/strategies/moonbag.json +101 -0
  11. package/dist/packages/autotrade/assets/strategies/runner.json +101 -0
  12. package/dist/packages/autotrade/assets/strategies/sample-dip-ladder.json +188 -0
  13. package/dist/packages/autotrade/assets/strategies/scalp.json +98 -0
  14. package/dist/packages/autotrade/assets/strategies/swing.json +97 -0
  15. package/dist/packages/autotrade/assets/strategies/yolo.json +157 -0
  16. package/dist/packages/autotrade/assets/tapes/yolo-rehearsal.json +1 -0
  17. package/dist/packages/autotrade/src/account.js +74 -0
  18. package/dist/packages/autotrade/src/backtest.js +357 -0
  19. package/dist/packages/autotrade/src/basis-text.js +32 -0
  20. package/dist/packages/autotrade/src/chat/approvals.js +484 -0
  21. package/dist/packages/autotrade/src/chat/audit.js +140 -0
  22. package/dist/packages/autotrade/src/chat/config.js +93 -0
  23. package/dist/packages/autotrade/src/chat/connector.js +24 -0
  24. package/dist/packages/autotrade/src/chat/control.js +103 -0
  25. package/dist/packages/autotrade/src/chat/fake.js +100 -0
  26. package/dist/packages/autotrade/src/chat/format.js +71 -0
  27. package/dist/packages/autotrade/src/chat/hub.js +187 -0
  28. package/dist/packages/autotrade/src/chat/index.js +18 -0
  29. package/dist/packages/autotrade/src/chat/notify.js +292 -0
  30. package/dist/packages/autotrade/src/chat/pairing.js +46 -0
  31. package/dist/packages/autotrade/src/chat/redact.js +48 -0
  32. package/dist/packages/autotrade/src/chat/secrets.js +99 -0
  33. package/dist/packages/autotrade/src/chat/service.js +248 -0
  34. package/dist/packages/autotrade/src/chat/telegram.js +290 -0
  35. package/dist/packages/autotrade/src/desk-executor.js +188 -0
  36. package/dist/packages/autotrade/src/engine.js +387 -0
  37. package/dist/packages/autotrade/src/execution-port.js +97 -0
  38. package/dist/packages/autotrade/src/fleet-config.js +169 -0
  39. package/dist/packages/autotrade/src/fleet-control.js +276 -0
  40. package/dist/packages/autotrade/src/fleet.js +639 -0
  41. package/dist/packages/autotrade/src/index.js +53 -0
  42. package/dist/packages/autotrade/src/ledger.js +108 -0
  43. package/dist/packages/autotrade/src/live-source.js +182 -0
  44. package/dist/packages/autotrade/src/monitor-daemon.js +715 -0
  45. package/dist/packages/autotrade/src/monitor.js +190 -0
  46. package/dist/packages/autotrade/src/ollama-sessions.js +137 -0
  47. package/dist/packages/autotrade/src/paper-accounts.js +72 -0
  48. package/dist/packages/autotrade/src/replay.js +198 -0
  49. package/dist/packages/autotrade/src/runner-config.js +99 -0
  50. package/dist/packages/autotrade/src/runner.js +872 -0
  51. package/dist/packages/autotrade/src/safety.js +243 -0
  52. package/dist/packages/autotrade/src/session.js +269 -0
  53. package/dist/packages/autotrade/src/sessions.js +503 -0
  54. package/dist/packages/autotrade/src/store.js +250 -0
  55. package/dist/packages/autotrade/src/strategy.js +256 -0
  56. package/dist/packages/autotrade/src/trade-ledger.js +241 -0
  57. package/dist/packages/autotrade/src/yolo.js +140 -0
  58. package/dist/packages/cli/assets/claude/analyst-system.md +4 -4
  59. package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +1 -1
  60. package/dist/packages/cli/src/ai-cmd.js +412 -0
  61. package/dist/packages/cli/src/analyst/backend.js +82 -15
  62. package/dist/packages/cli/src/analyst/codex.js +14 -9
  63. package/dist/packages/cli/src/analyst/ollama.js +263 -0
  64. package/dist/packages/cli/src/analyst/session.js +5 -2
  65. package/dist/packages/cli/src/app-help-gen.js +44 -8
  66. package/dist/packages/cli/src/autotrade-cmd/audit.js +57 -0
  67. package/dist/packages/cli/src/autotrade-cmd/backtest.js +199 -0
  68. package/dist/packages/cli/src/autotrade-cmd/flatten.js +64 -0
  69. package/dist/packages/cli/src/autotrade-cmd/fleet.js +397 -0
  70. package/dist/packages/cli/src/autotrade-cmd/requests.js +79 -0
  71. package/dist/packages/cli/src/autotrade-cmd/run.js +413 -0
  72. package/dist/packages/cli/src/autotrade-cmd/stop.js +76 -0
  73. package/dist/packages/cli/src/autotrade-cmd/strategies.js +198 -0
  74. package/dist/packages/cli/src/autotrade-cmd/strategy-control.js +174 -0
  75. package/dist/packages/cli/src/autotrade-cmd/telegram-host.js +101 -0
  76. package/dist/packages/cli/src/autotrade-cmd.js +168 -0
  77. package/dist/packages/cli/src/display.js +156 -17
  78. package/dist/packages/cli/src/doctor.js +62 -3
  79. package/dist/packages/cli/src/export.js +10 -2
  80. package/dist/packages/cli/src/index.js +1 -1
  81. package/dist/packages/cli/src/ledger-cmd.js +288 -0
  82. package/dist/packages/cli/src/live/chart-fills.js +46 -0
  83. package/dist/packages/cli/src/live/index.js +23 -89
  84. package/dist/packages/cli/src/live/screens/token-live.js +9 -6
  85. package/dist/packages/cli/src/live/trade.js +12 -457
  86. package/dist/packages/cli/src/live/watchlist.js +1 -1
  87. package/dist/packages/cli/src/main.js +147 -190
  88. package/dist/packages/cli/src/mcp-cmd.js +7 -1
  89. package/dist/packages/cli/src/plan-cmd.js +1 -1
  90. package/dist/packages/cli/src/pnl-app.js +31 -21
  91. package/dist/packages/cli/src/pnl-card.js +158 -21
  92. package/dist/packages/cli/src/popup.js +22 -6
  93. package/dist/packages/cli/src/record-cmd.js +141 -20
  94. package/dist/packages/cli/src/screens/agent-hud.js +34 -2
  95. package/dist/packages/cli/src/screens/analyst-session.js +7 -4
  96. package/dist/packages/cli/src/screens/analyst.js +3 -2
  97. package/dist/packages/cli/src/screens/autotrade-keys.js +169 -0
  98. package/dist/packages/cli/src/screens/autotrade-proposals.js +123 -0
  99. package/dist/packages/cli/src/screens/autotrade-screen.js +725 -0
  100. package/dist/packages/cli/src/screens/autotrade-ui.js +640 -0
  101. package/dist/packages/cli/src/screens/autotrade-worker.js +89 -0
  102. package/dist/packages/cli/src/screens/brain.js +37 -11
  103. package/dist/packages/cli/src/screens/overlay.js +5 -2
  104. package/dist/packages/cli/src/screens/plans.js +5 -17
  105. package/dist/packages/cli/src/screens/pnl-chip.js +9 -8
  106. package/dist/packages/cli/src/screens/positions.js +6 -28
  107. package/dist/packages/cli/src/screens/ticket.js +38 -205
  108. package/dist/packages/cli/src/screens/token.js +3 -3
  109. package/dist/packages/cli/src/state.js +25 -19
  110. package/dist/packages/cli/src/telegram-cmd.js +451 -0
  111. package/dist/packages/cli/src/trade.js +3 -128
  112. package/dist/packages/cli/src/version.js +1 -1
  113. package/dist/packages/cli/src/wallet-cmd.js +39 -340
  114. package/dist/packages/cli/src/wallet-command.js +8 -11
  115. package/dist/packages/cli/src/widgets.js +0 -12
  116. package/dist/packages/custom/src/config.js +20 -2
  117. package/dist/packages/custom/src/schema.js +35 -10
  118. package/dist/packages/custom/src/workspace.js +3 -3
  119. package/dist/packages/data/src/basis.js +191 -0
  120. package/dist/packages/data/src/detail.js +53 -2
  121. package/dist/packages/data/src/index.js +0 -14
  122. package/dist/packages/data/src/store.js +14 -5
  123. package/dist/packages/data/src/trade/agent/paper-desk.js +228 -16
  124. package/dist/packages/data/src/trade/agent/rendezvous.js +3 -2
  125. package/dist/packages/data/src/trade/bridge.js +4 -102
  126. package/dist/packages/data/src/trade/ledger/export.js +101 -0
  127. package/dist/packages/data/src/trade/ledger/file.js +135 -0
  128. package/dist/packages/data/src/trade/ledger/fold.js +191 -0
  129. package/dist/packages/data/src/trade/ledger/index.js +9 -0
  130. package/dist/packages/data/src/trade/ledger/schema.js +83 -0
  131. package/dist/packages/data/src/trade/ledger/sources.js +307 -0
  132. package/dist/packages/data/src/trade/ledger/stats.js +123 -0
  133. package/dist/packages/data/src/trade/ledger/tags.js +116 -0
  134. package/dist/packages/data/src/trade/ledger/view.js +51 -0
  135. package/dist/packages/data/src/trade/plans/index.js +0 -1
  136. package/dist/packages/data/src/trade/plans/service.js +3 -27
  137. package/dist/packages/mcp/assets/kb/app-help/autotrade-chat.md +48 -0
  138. package/dist/packages/mcp/assets/kb/app-help/autotrade-parallel.md +42 -0
  139. package/dist/packages/mcp/assets/kb/app-help/autotrade-runner.md +35 -0
  140. package/dist/packages/mcp/assets/kb/app-help/autotrade-stop.md +37 -0
  141. package/dist/packages/mcp/assets/kb/app-help/autotrade-strategies.md +35 -0
  142. package/dist/packages/mcp/assets/kb/app-help/autotrade-telegram.md +40 -0
  143. package/dist/packages/mcp/assets/kb/app-help/chart-trade-markers.md +42 -0
  144. package/dist/packages/mcp/assets/kb/app-help/link-a-wallet.md +4 -10
  145. package/dist/packages/mcp/assets/kb/app-help/live-trading.md +38 -0
  146. package/dist/packages/mcp/assets/kb/app-help/live-yolo.md +34 -0
  147. package/dist/packages/mcp/assets/kb/app-help/order-plans.md +1 -1
  148. package/dist/packages/mcp/assets/kb/app-help/price-or-marketcap.md +5 -2
  149. package/dist/packages/mcp/assets/kb/app-help/screen-analyst.md +3 -0
  150. package/dist/packages/mcp/assets/kb/app-help/screen-autotrade.md +217 -0
  151. package/dist/packages/mcp/assets/kb/app-help/screen-kol.md +3 -0
  152. package/dist/packages/mcp/assets/kb/app-help/screen-newpairs.md +3 -0
  153. package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +3 -0
  154. package/dist/packages/mcp/assets/kb/app-help/screen-screener.md +3 -0
  155. package/dist/packages/mcp/assets/kb/app-help/screen-token.md +3 -0
  156. package/dist/packages/mcp/assets/kb/app-help/screen-trending.md +3 -0
  157. package/dist/packages/mcp/assets/kb/app-help/screen-watchlist.md +3 -0
  158. package/dist/packages/mcp/assets/kb/app-help/standing-mandates.md +1 -1
  159. package/dist/packages/mcp/assets/kb/app-help/trade-ledger.md +54 -0
  160. package/dist/packages/mcp/assets/kb/app-help/use-local-model.md +38 -0
  161. package/dist/packages/mcp/assets/kb/docs/docs-agents-agent-paper-trading.md +4 -1
  162. package/dist/packages/mcp/assets/kb/docs/docs-agents-reading-your-trade-ledger.md +31 -0
  163. package/dist/packages/mcp/assets/kb/docs/docs-agents-untrusted-by-default.md +1 -1
  164. package/dist/packages/mcp/assets/kb/docs/docs-analyst-if-claude-is-missing.md +1 -1
  165. package/dist/packages/mcp/assets/kb/docs/docs-analyst-use-a-local-model-instead.md +27 -0
  166. package/dist/packages/mcp/assets/kb/docs/docs-analyst-what-it-sees.md +2 -0
  167. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-backtests-and-their-limits.md +56 -0
  168. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-change-a-strategy-by-chat.md +49 -0
  169. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-checking-the-record-of-a-run.md +28 -0
  170. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-comparing-them.md +41 -0
  171. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-exits-and-wakes.md +76 -0
  172. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-how-a-stop-reaches-the-runner.md +32 -0
  173. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-on-the-q-screen.md +37 -0
  174. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-one-strategy-or-all-of-them.md +35 -0
  175. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-paper-accounts.md +50 -0
  176. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-presets.md +37 -0
  177. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-running-it-all-day.md +31 -0
  178. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-sessions-and-their-limits.md +31 -0
  179. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-several-strategies-at-once.md +33 -0
  180. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-stopping-it.md +35 -0
  181. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-strategies-and-presets.md +34 -0
  182. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-autotrade-json-file.md +48 -0
  183. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-heartbeat.md +27 -0
  184. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-run-file-and-the-heartbeat.md +34 -0
  185. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-runner-and-candidate-sessions.md +45 -0
  186. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-each-session-shows-first.md +31 -0
  187. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-costs-claude-usage-per-session.md +58 -0
  188. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-is-paper-self-directed.md +43 -0
  189. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-will-not-do.md +31 -0
  190. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-which-account-day-caps-and-the-usage-guard.md +44 -0
  191. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-your-strategies.md +37 -0
  192. package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +22 -0
  193. package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
  194. package/dist/packages/mcp/assets/kb/docs/docs-beginners.md +1 -1
  195. package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-markers.md +30 -0
  196. package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-y-axis.md +1 -1
  197. package/dist/packages/mcp/assets/kb/docs/docs-configuration-environment-variables.md +4 -1
  198. package/dist/packages/mcp/assets/kb/docs/docs-configuration-files-on-disk.md +1 -0
  199. package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -1
  200. package/dist/packages/mcp/assets/kb/docs/docs-configuration-price-display-mode.md +8 -6
  201. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-2-the-one-key-mirror-ticket-a-proposal.md +1 -1
  202. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-3-capped-auto-mirror-default-off-less.md +1 -1
  203. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-what-is-proven-what-is-not.md +1 -1
  204. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
  205. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +36 -0
  206. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-sources.md +1 -0
  207. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-first-run.md +1 -1
  208. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
  209. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +1 -1
  210. package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +1 -1
  211. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-changed-in-0-1-7.md +26 -0
  212. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-stopping.md +28 -0
  213. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-the-short-version.md +34 -0
  214. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-using-a-trading-bot-with-dexterm.md +33 -0
  215. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does-not-do.md +30 -0
  216. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does.md +29 -0
  217. package/dist/packages/mcp/assets/kb/docs/docs-live-trading.md +23 -0
  218. package/dist/packages/mcp/assets/kb/docs/docs-ollama-another-machine.md +28 -0
  219. package/dist/packages/mcp/assets/kb/docs/docs-ollama-autotrade-on-a-local-model.md +29 -0
  220. package/dist/packages/mcp/assets/kb/docs/docs-ollama-check-your-model-dexterm-ai-probe.md +38 -0
  221. package/dist/packages/mcp/assets/kb/docs/docs-ollama-how-we-checked.md +27 -0
  222. package/dist/packages/mcp/assets/kb/docs/docs-ollama-models-without-tool-calling.md +32 -0
  223. package/dist/packages/mcp/assets/kb/docs/docs-ollama-pick-the-engine-globally-or-per-surface.md +43 -0
  224. package/dist/packages/mcp/assets/kb/docs/docs-ollama-restriction-profiles.md +35 -0
  225. package/dist/packages/mcp/assets/kb/docs/docs-ollama-set-it-up-in-two-minutes.md +39 -0
  226. package/dist/packages/mcp/assets/kb/docs/docs-ollama-troubleshooting.md +35 -0
  227. package/dist/packages/mcp/assets/kb/docs/docs-ollama-what-runs-on-a-local-model.md +39 -0
  228. package/dist/packages/mcp/assets/kb/docs/docs-ollama-what-stays-outside-the-model.md +36 -0
  229. package/dist/packages/mcp/assets/kb/docs/docs-ollama-which-model.md +38 -0
  230. package/dist/packages/mcp/assets/kb/docs/docs-ollama-your-own-agent-on-the-local-model.md +28 -0
  231. package/dist/packages/mcp/assets/kb/docs/docs-ollama.md +28 -0
  232. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-letting-an-agent-paper-trade-experimental.md +3 -4
  233. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-link-a-wallet-read-only.md +30 -0
  234. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-order-plans-and-standing-mandates.md +3 -4
  235. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-panic.md +4 -7
  236. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-paper-only-mandates.md +6 -7
  237. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-pnl-cards.md +7 -7
  238. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-safety-model.md +34 -0
  239. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-ticket.md +3 -4
  240. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-trade-ledger.md +42 -0
  241. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-wallet.md +4 -5
  242. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-trade-moments.md +4 -5
  243. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +4 -5
  244. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-buy-and-sell-markers.md +37 -0
  245. package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-what-it-shows.md +2 -1
  246. package/dist/packages/mcp/assets/kb/docs/docs-screens-autotrade-paper.md +26 -0
  247. package/dist/packages/mcp/assets/kb/docs/docs-screens-order-plans.md +1 -1
  248. package/dist/packages/mcp/assets/kb/docs/docs-screens-token-screen.md +2 -0
  249. package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-brain-view-and-the-pnl-chip.md +1 -1
  250. package/dist/packages/mcp/assets/kb/docs/docs-telegram-approve-one-tap-entries.md +50 -0
  251. package/dist/packages/mcp/assets/kb/docs/docs-telegram-control-the-commands.md +43 -0
  252. package/dist/packages/mcp/assets/kb/docs/docs-telegram-if-something-goes-wrong.md +30 -0
  253. package/dist/packages/mcp/assets/kb/docs/docs-telegram-notify-what-reaches-your-phone.md +41 -0
  254. package/dist/packages/mcp/assets/kb/docs/docs-telegram-set-it-up.md +47 -0
  255. package/dist/packages/mcp/assets/kb/docs/docs-telegram-the-record.md +26 -0
  256. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +33 -0
  257. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-will-not-do.md +31 -0
  258. package/dist/packages/mcp/assets/kb/docs/docs-telegram.md +23 -0
  259. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-common-questions.md +1 -1
  260. package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-doctor-lines.md +4 -0
  261. package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +1 -1
  262. package/dist/packages/mcp/assets/kb/docs/docs-voice.md +1 -1
  263. package/dist/packages/mcp/assets/kb/docs/docs-yolo-1-replay-it-on-your-own-history.md +42 -0
  264. package/dist/packages/mcp/assets/kb/docs/docs-yolo-2-run-it-on-paper.md +34 -0
  265. package/dist/packages/mcp/assets/kb/docs/docs-yolo-3-watch-it-and-stop-it.md +32 -0
  266. package/dist/packages/mcp/assets/kb/docs/docs-yolo-the-rules.md +37 -0
  267. package/dist/packages/mcp/assets/kb/docs/docs-yolo-what-it-is.md +34 -0
  268. package/dist/packages/mcp/assets/kb/docs/docs-yolo-what-it-will-not-do.md +31 -0
  269. package/dist/packages/mcp/assets/kb/docs/docs-yolo.md +26 -0
  270. package/dist/packages/mcp/assets/kb/entries/autotrade.md +38 -0
  271. package/dist/packages/mcp/src/app-help.js +1 -1
  272. package/dist/packages/mcp/src/autotrade-tools.js +944 -0
  273. package/dist/packages/mcp/src/backtest-requests.js +71 -0
  274. package/dist/packages/mcp/src/engine/config.js +159 -0
  275. package/dist/packages/mcp/src/engine/index.js +10 -0
  276. package/dist/packages/mcp/src/engine/loop.js +101 -0
  277. package/dist/packages/mcp/src/engine/mcp-client.js +123 -0
  278. package/dist/packages/mcp/src/engine/ollama.js +258 -0
  279. package/dist/packages/mcp/src/engine/probe.js +110 -0
  280. package/dist/packages/mcp/src/engine/reason.js +35 -0
  281. package/dist/packages/mcp/src/engine/restrictions.js +111 -0
  282. package/dist/packages/mcp/src/index.js +7 -1
  283. package/dist/packages/mcp/src/ledger-tools.js +346 -0
  284. package/dist/packages/mcp/src/paper-agent.js +2 -0
  285. package/dist/packages/mcp/src/server.js +127 -19
  286. package/dist/packages/packs/src/backtest.js +3 -1
  287. package/dist/packages/popup/src/pages/chart-lib.js +134 -21
  288. package/dist/packages/popup/src/pages/chart.css +9 -0
  289. package/dist/packages/popup/src/pages/chart.html +3 -1
  290. package/dist/packages/popup/src/pages/chart.js +104 -7
  291. package/dist/packages/popup/src/protocol.js +2 -0
  292. package/dist/packages/popup/src/server.js +4 -0
  293. package/dist/packages/recorder/src/index.js +2 -0
  294. package/dist/packages/recorder/src/recorder.js +63 -6
  295. package/dist/packages/recorder/src/retention.js +100 -0
  296. package/dist/packages/recorder/src/schema.js +34 -1
  297. package/dist/packages/recorder/src/ticks.js +237 -0
  298. package/dist/packages/tui/src/index.js +1 -0
  299. package/dist/packages/tui/src/keymap.js +21 -1
  300. package/dist/packages/tui/src/markers.js +219 -0
  301. package/dist/packages/tui/src/widgets.js +149 -2
  302. package/package.json +2 -1
  303. package/dist/packages/data/src/trade/caps.js +0 -323
  304. package/dist/packages/data/src/trade/executor.js +0 -933
  305. package/dist/packages/data/src/trade/plans/executor-live.js +0 -220
  306. package/dist/packages/data/src/trade/rpc-exec.js +0 -229
  307. package/dist/packages/data/src/trade/signer/client.js +0 -197
  308. package/dist/packages/data/src/trade/signer/fast-wallet.js +0 -154
  309. package/dist/packages/data/src/trade/signer/host.js +0 -97
  310. package/dist/packages/data/src/trade/signer/main.js +0 -121
  311. package/dist/packages/data/src/trade/signer/probe-builder.js +0 -85
  312. package/dist/packages/data/src/trade/signer/protocol.js +0 -92
  313. package/dist/packages/data/src/trade/signer/service.js +0 -400
  314. package/dist/packages/data/src/trade/signer/sol-usd-feed.js +0 -98
  315. package/dist/packages/data/src/trade/sol-usd.js +0 -86
  316. package/dist/packages/data/src/trade/swap-build.js +0 -108
  317. package/dist/packages/data/src/trade/token-delta.js +0 -201
  318. package/dist/packages/data/vendor/solana-web3/LICENSE +0 -20
  319. package/dist/packages/data/vendor/solana-web3/NOTICE.md +0 -13
  320. package/dist/packages/data/vendor/solana-web3/index.iife.min.js +0 -20
  321. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-a-live-ticket.md +0 -42
  322. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-before-you-press-size-vs-caps.md +0 -34
  323. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-devnet-signer-mainnet-token.md +0 -35
  324. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-jupiter-rate-limits.md +0 -33
  325. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-link-a-wallet.md +0 -32
  326. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-live-trading-devnet-first.md +0 -30
  327. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-mainnet.md +0 -31
  328. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-live-fire-path-devnet-the-fast-wallet.md +0 -41
  329. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-signer-process.md +0 -34
  330. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-what-the-live-path-keeps.md +0 -37
  331. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-your-first-live-trade.md +0 -41
@@ -1,933 +0,0 @@
1
- // dexterm T1 — the live (non-custodial) trade executor (SPEC §6.1, §6.2, §6.5). Orchestrates the safe
2
- // pipeline and enforces the invariants the UI/model/voice cannot bypass:
3
- //
4
- // quote -> bind quote to order -> build unsigned tx -> DECODE + §6.2 policy -> simulate with balance
5
- // deltas -> PLAN (+ plan-hash, registered in-process) [UI shows plan, user presses key]
6
- // -> look the plan up by hash (single-use, expiring) -> re-check CAPS + reserve USD atomically
7
- // -> wallet bridge SIGNS -> decode the SIGNED tx: same message, valid Ed25519 signature by the linked
8
- // wallet, same policy -> RE-SIMULATE and compare the SOL delta AND the wallet's TOKEN balances to the
9
- // plan -> submit -> confirm -> record
10
- //
11
- // Refusals (no sign, no submit) when: the quote does not match the order, the tx calls a program off
12
- // the allow-list or carries a forbidden instruction, simulation failed, the simulated SOL out exceeds
13
- // the plan or the SOL cap, a token account of the wallet that the plan does not spend loses tokens in
14
- // the simulation (task-1337, token-delta.ts), price impact / slippage exceed their ceilings, the plan
15
- // is unknown/expired/mutated, the signed bytes differ, or any cap (network / per-trade USD / daily USD)
16
- // is exceeded, or the panic switch is engaged. dexterm never holds a key; the browser approval IS the
17
- // physical confirmation. The AI has no path here — the executor is constructed only inside the
18
- // interactive context and called only from a keypress handler.
19
- import { createHash } from 'node:crypto';
20
- import { systemClock } from "../clock.js";
21
- import { DatabaseSync } from 'node:sqlite';
22
- import { mkdirSync } from 'node:fs';
23
- import { dirname, join } from 'node:path';
24
- import { homedir } from 'node:os';
25
- import { SOL_MINT, computeFill, lamportsToSol, emptyPosition, applyBuy, applySell } from "../paper.js";
26
- import { normalizeSwapQuote } from "../sources/jupiter-swap.js";
27
- import { base58 } from "../sources/solana-rpc.js";
28
- import { base58Decode } from "../sources/pda.js";
29
- import { JupiterSwapBuild } from "./swap-build.js";
30
- import { TradeRpc, RpcRejectedError, rpcNetwork } from "./rpc-exec.js";
31
- import { WalletBridge } from "./bridge.js";
32
- import { checkCaps, effectiveCaps, planHash, CapLedger, SOL_OVERHEAD_ALLOWANCE_LAMPORTS, } from "./caps.js";
33
- import { decodeTransaction, checkTxPolicy, checkTxPolicyWith, checkSwapBinding, compareApprovedMessage, verifyEd25519, ALLOWED_PROGRAMS, WSOL_MINT, LIGHTHOUSE_PROGRAM, SYSTEM_PROGRAM, ASSOCIATED_TOKEN_PROGRAM, COMPUTE_BUDGET_PROGRAM, MEMO_PROGRAM, JUPITER_V6_PROGRAM, JUPITER_ROUTE_DISCRIMINATORS, TOKEN_PROGRAM, TOKEN_2022_PROGRAM, } from "./tx-decode.js";
34
- import { resolveSolUsd } from "./sol-usd.js";
35
- import { KillSwitch } from "./kill-switch.js";
36
- import { buildTokenWatch, referencedAccounts, parseLookupTable, checkTokenDeltas, postBalances, refreshWatch, referencedOverCap, checkWsolBound, MAX_TOKEN_WATCH } from "./token-delta.js";
37
- import { FAST_MAX_SOL_PER_FILL, FAST_MAX_SOL_PER_FILL_LAMPORTS } from "./signer/fast-wallet.js";
38
- /** How long a built plan stays confirmable. A stale quote/tx is refused ('plan-expired'); rebuild. */
39
- export const PLAN_TTL_MS = 90_000;
40
- /** The transaction-policy shape a plan kind must have (a devnet fill IS the probe shape). */
41
- export function policyKind(kind) { return kind === 'swap' ? 'swap' : 'probe'; }
42
- export class LiveExecutor {
43
- d;
44
- clock;
45
- killSwitch;
46
- allowed;
47
- /** DEFECT 5: serializes the check+reserve critical section so concurrent trades can't both pass. */
48
- capGate = Promise.resolve();
49
- /**
50
- * Plans THIS executor built, by hash. A confirm is honoured only for a registered, unused, unexpired
51
- * plan, and the executor works from its OWN frozen copy — an in-process caller that rebinds the
52
- * digest/hash on the object it holds gets 'plan-mismatch' (first verifier's design gap #8).
53
- */
54
- plans = new Map();
55
- constructor(deps) {
56
- this.d = deps;
57
- this.clock = deps.clock ?? systemClock;
58
- this.killSwitch = deps.killSwitch ?? new KillSwitch();
59
- this.allowed = deps.allowedPrograms ?? ALLOWED_PROGRAMS;
60
- }
61
- /** Run `fn` with exclusive access to the daily-cap ledger (promise-chain mutex). */
62
- async runExclusive(fn) {
63
- const prior = this.capGate;
64
- let release;
65
- this.capGate = new Promise((r) => { release = r; });
66
- await prior;
67
- try {
68
- return await fn();
69
- }
70
- finally {
71
- release();
72
- }
73
- }
74
- effectiveNetwork(requested) {
75
- // DEFECT 4: the network that gates USD caps is the MORE DANGEROUS of {requested, actual RPC}. A
76
- // trade is devnet-exempt ONLY when both the requested label AND the configured RPC endpoint are
77
- // devnet; a devnet label over a mainnet RPC is treated as mainnet so caps are enforced on real funds.
78
- const rpcNet = rpcNetworkOf(this.d.rpc);
79
- return (requested === 'mainnet' || rpcNet === 'mainnet') ? 'mainnet' : 'devnet';
80
- }
81
- /**
82
- * Step 1: quote -> bind -> build -> decode/policy -> simulate -> plan. No signing. The plan carries
83
- * its hash so the UI can show it and bind the later confirm to it. Does NOT itself refuse on caps
84
- * (the UI shows the plan even when it will be refused, so the user understands why); refusal happens
85
- * at submit. Throws when live trading is panicked or the quote/tx cannot be parsed (fail closed).
86
- */
87
- async buildPlan(args) {
88
- const panic = this.killSwitch.engaged();
89
- if (panic)
90
- throw new Error(`live trading is PANICKED (${panic.reason}); run \`dexterm wallet resume\` to re-enable`);
91
- const order = args.order;
92
- const user = args.userPublicKey;
93
- if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(user))
94
- throw new Error('buildPlan needs a base58 wallet public key');
95
- const network = this.effectiveNetwork(args.network ?? this.d.caps.network);
96
- const req = {
97
- inputMint: order.side === 'buy' ? SOL_MINT : order.mint,
98
- outputMint: order.side === 'buy' ? order.mint : SOL_MINT,
99
- amount: order.amountRaw,
100
- slippageBps: order.slippageBps,
101
- mode: 'ExactIn',
102
- };
103
- const raw = await this.d.build.quoteRaw(req, args.signal);
104
- const now = this.clock.now();
105
- const quote = normalizeSwapQuote(raw, now); // throws on a non-integer amount (round 2)
106
- // Round 2 HIGH #1: the quote must be the quote for THIS order. A hostile/changed API response that
107
- // reprices the SOL leg or swaps a mint is refused before anything is built.
108
- const bind = bindQuote(quote, req);
109
- const solUsd = resolveSolUsd(this.d.solUsd?.(), now, network);
110
- let txPolicy = bind;
111
- let unsignedTxBase64 = '';
112
- let txDigest = '';
113
- let sim = null;
114
- let solOut = null;
115
- let wsolLost = 0n;
116
- let decodedTx = null;
117
- let prioFee = null;
118
- let tokenWatch = [];
119
- const expect = { user, inputMint: req.inputMint, outputMint: req.outputMint, inAmount: quote.inAmount, outAmount: quote.outAmount, slippageBps: quote.slippageBps };
120
- if (bind.ok) {
121
- const built = await this.d.build.buildSwap({ quote: raw, userPublicKey: user, signal: args.signal });
122
- unsignedTxBase64 = built.swapTransactionBase64;
123
- const decoded = decodeTransaction(Buffer.from(unsignedTxBase64, 'base64')); // throws → fail closed
124
- txDigest = digest(decoded.message);
125
- const pol = checkTxPolicy(decoded, user, this.allowed);
126
- txPolicy = pol.ok ? { ok: true } : { ok: false, reason: pol.reason, message: pol.message };
127
- // task-1328: the one route must BE this plan (amounts, slippage, no fee, the wallet's own ATAs).
128
- if (pol.ok) {
129
- const bound = checkSwapBinding(decoded, expect);
130
- if (!bound.ok)
131
- txPolicy = { ok: false, reason: bound.reason, message: bound.message };
132
- }
133
- if (txPolicy.ok) {
134
- // task-1337: the wallet's token accounts ride along in the simulation; a route that drains one the
135
- // plan does not spend is refused here, before the user ever sees a plan to confirm.
136
- const tw = await this.tokenWatchFor(user, decoded);
137
- if (!tw.ok)
138
- txPolicy = { ok: false, reason: 'token-delta-unknown', message: tw.message };
139
- else {
140
- tokenWatch = tw.watch;
141
- const r = await this.simulateWithDelta(unsignedTxBase64, user, tokenWatch, { side: order.side, mint: order.mint, amountRaw: order.amountRaw });
142
- sim = r.sim;
143
- solOut = r.solOut;
144
- if (r.sim.ok && !r.tokens.ok)
145
- txPolicy = { ok: false, reason: r.tokens.reason, message: r.tokens.message };
146
- else if (r.sim.ok && r.tokens.ok)
147
- wsolLost = r.tokens.wsolLost;
148
- }
149
- }
150
- decodedTx = decoded;
151
- prioFee = built.prioritizationFeeLamports;
152
- }
153
- const solLeg = solLegLamports(order, quote, solOut);
154
- // task-1337 F2: wrapped SOL that left the wallet is SOL-value out; bound it against the plan's SOL leg
155
- // (the per-account token check deliberately lets WSOL through, so this is where a WSOL drain is caught).
156
- if (txPolicy.ok && sim?.ok && wsolLost > 0n) {
157
- const wb = checkWsolBound(order.side, solOut, wsolLost, solLeg, SOL_OVERHEAD_ALLOWANCE_LAMPORTS);
158
- if (!wb.ok)
159
- txPolicy = { ok: false, reason: wb.reason, message: wb.message };
160
- }
161
- const plan = {
162
- network,
163
- mint: order.mint,
164
- symbol: order.symbol,
165
- side: order.side,
166
- amountInRaw: quote.inAmount,
167
- amountOutRaw: quote.outAmount,
168
- slippageBps: quote.slippageBps,
169
- route: Object.freeze([...quote.route]),
170
- priceImpactPct: quote.priceImpactPct,
171
- // DEFECT 3 / round 2: priced LOCALLY from the larger of the planned SOL leg and the SIMULATED SOL
172
- // out, times a resolved multi-source SOL/USD. Never the quote's swapUsdValue.
173
- notionalUsd: notionalUsd(order.side, solLeg, solOut, solUsd),
174
- simulated: sim !== null,
175
- simOk: sim?.ok ?? false,
176
- simError: sim === null ? null : sim.ok ? null : safeErr(sim.err),
177
- solOutLamports: solOut,
178
- solLegLamports: solLeg,
179
- txPolicy,
180
- };
181
- Object.freeze(plan);
182
- const hash = txDigest ? bindHash(plan, txDigest) : bindHash(plan, 'no-tx');
183
- const built = Object.freeze({
184
- kind: 'swap', lane: 'main', ref: null, plan, hash, txDigest, unsignedTxBase64, order, quote, userPublicKey: user, solUsd, builtAt: now,
185
- decoded: decodedTx, expect, instructions: decodedTx ? summarize(decodedTx) : [],
186
- feeLamports: BigInt(5000 * (decodedTx?.numRequiredSignatures ?? 1)) + BigInt(prioFee ?? 0),
187
- prioritizationFeeLamports: prioFee,
188
- tokenWatch: Object.freeze(tokenWatch.map((w) => Object.freeze({ ...w }))),
189
- });
190
- this.plans.set(hash, { built, at: now, used: false });
191
- this.sweep(now);
192
- return built;
193
- }
194
- /**
195
- * task-1328: the devnet PROBE plan — the wrap-and-unwrap shape (fund the wallet's own wrapped-SOL
196
- * account, SyncNative, CloseAccount back to the wallet) with NO route. It moves nothing but fees, and
197
- * it exercises the same keypress → Phantom sign → verify → re-simulate → submit → confirm path as a
198
- * swap. Refused unless the RPC endpoint is devnet by classification (never a mainnet probe).
199
- */
200
- async buildProbe(args) {
201
- const panic = this.killSwitch.engaged();
202
- if (panic)
203
- throw new Error(`live trading is PANICKED (${panic.reason}); run \`dexterm wallet resume\` to re-enable`);
204
- const user = args.userPublicKey;
205
- if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(user))
206
- throw new Error('buildProbe needs a base58 wallet public key');
207
- if (!this.d.probe)
208
- throw new Error('no probe builder configured');
209
- if (rpcNetworkOf(this.d.rpc) !== 'devnet')
210
- throw new Error('the devnet probe runs only against a devnet RPC endpoint');
211
- const now = this.clock.now();
212
- const built = await this.d.probe.build(user);
213
- const decoded = decodeTransaction(Buffer.from(built.txBase64, 'base64'));
214
- const txDigest = digest(decoded.message);
215
- const pol = checkTxPolicyWith(decoded, user, this.allowed, { kind: 'probe' });
216
- let txPolicy = pol.ok ? { ok: true } : { ok: false, reason: pol.reason, message: pol.message };
217
- let sim = null;
218
- let solOut = null;
219
- let tokenWatch = [];
220
- if (pol.ok) {
221
- const tw = await this.tokenWatchFor(user, decoded);
222
- if (!tw.ok)
223
- txPolicy = { ok: false, reason: 'token-delta-unknown', message: tw.message };
224
- else {
225
- tokenWatch = tw.watch;
226
- const r = await this.simulateWithDelta(built.txBase64, user, tokenWatch, { side: 'buy', mint: WSOL_MINT, amountRaw: built.lamports });
227
- sim = r.sim;
228
- solOut = r.solOut;
229
- if (r.sim.ok && !r.tokens.ok)
230
- txPolicy = { ok: false, reason: r.tokens.reason, message: r.tokens.message };
231
- else if (r.sim.ok && r.tokens.ok && r.tokens.wsolLost > 0n) {
232
- // task-1337 F2: a probe only wraps/unwraps the wallet's own SOL; it must not drain pre-existing WSOL.
233
- const wb = checkWsolBound('buy', solOut, r.tokens.wsolLost, built.lamports, SOL_OVERHEAD_ALLOWANCE_LAMPORTS);
234
- if (!wb.ok)
235
- txPolicy = { ok: false, reason: wb.reason, message: wb.message };
236
- }
237
- }
238
- }
239
- const plan = Object.freeze({
240
- network: 'devnet', mint: WSOL_MINT, symbol: args.symbol ?? 'SOL', side: 'buy',
241
- amountInRaw: built.lamports, amountOutRaw: built.lamports, slippageBps: 0, route: Object.freeze([]),
242
- priceImpactPct: 0, notionalUsd: null, simulated: sim !== null, simOk: sim?.ok ?? false,
243
- simError: sim === null ? null : sim.ok ? null : safeErr(sim.err), solOutLamports: solOut, solLegLamports: built.lamports, txPolicy,
244
- });
245
- const hash = createHash('sha256').update(`probe|${bindHash(plan, txDigest)}`).digest('hex');
246
- const order = { mint: WSOL_MINT, symbol: plan.symbol, side: 'buy', decimals: 9, amountRaw: built.lamports, slippageBps: 0 };
247
- const quote = {
248
- inputMint: WSOL_MINT, outputMint: WSOL_MINT, inAmount: built.lamports, outAmount: built.lamports, otherAmountThreshold: built.lamports,
249
- mode: 'ExactIn', slippageBps: 0, priceImpactPct: 0, route: [], platformFeeBps: null, platformFeeAmount: null, usdValue: null, contextSlot: 0,
250
- source: 'jupiter-swap', fetchedAt: now,
251
- };
252
- const out = Object.freeze({
253
- kind: 'probe', lane: 'main', ref: null, plan, hash, txDigest, unsignedTxBase64: built.txBase64, order, quote, userPublicKey: user,
254
- solUsd: resolveSolUsd(this.d.solUsd?.(), now, 'devnet'), builtAt: now, decoded, expect: null, instructions: summarize(decoded),
255
- feeLamports: BigInt(5000 * decoded.numRequiredSignatures), prioritizationFeeLamports: null,
256
- tokenWatch: Object.freeze(tokenWatch.map((w) => Object.freeze({ ...w }))),
257
- });
258
- this.plans.set(hash, { built: out, at: now, used: false });
259
- this.sweep(now);
260
- return out;
261
- }
262
- /**
263
- * task-1363: a DEVNET FILL for the capped FAST WALLET — what an armed order plan executes when it fires on
264
- * devnet. Jupiter has no devnet, so the transaction is the probe's wrap-and-unwrap shape sized to the plan's
265
- * SOL leg (`lamports`), with a memo naming the plan leg; it runs the SAME pipeline as a swap or probe
266
- * (decode → probe-shape policy → simulate with token deltas → WSOL bound → hash; at confirm: fast-wallet
267
- * signature → compare → verify → policy → re-simulate → caps → submit → confirm → book), and the fast lane
268
- * enforces the USD per-trade/daily caps even on devnet plus a hard SOL-per-fill ceiling.
269
- * Refused (throws, fail closed) without a fast signer, off devnet, or without a probe builder; an over-ceiling
270
- * size is built with a `sol-out-cap` policy refusal so the caller can see why, and is never confirmable.
271
- */
272
- async buildFill(args) {
273
- const panic = this.killSwitch.engaged();
274
- if (panic)
275
- throw new Error(`live trading is PANICKED (${panic.reason}); run \`dexterm wallet resume\` to re-enable`);
276
- const fast = this.d.fastSigner;
277
- if (!fast)
278
- throw new Error('no fast wallet: create one with `dexterm wallet fast create` (devnet)');
279
- if (!this.d.probe)
280
- throw new Error('no probe builder configured');
281
- if (rpcNetworkOf(this.d.rpc) !== 'devnet')
282
- throw new Error('devnet fills run only against a devnet RPC endpoint');
283
- if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(args.mint))
284
- throw new Error('buildFill needs a base58 mint');
285
- if (!(args.lamports > 0n))
286
- throw new Error('buildFill needs a positive SOL leg');
287
- const user = fast.publicKey;
288
- const ref = args.ref.replace(/[^\x20-\x7e]/g, '?').slice(0, 64);
289
- const now = this.clock.now();
290
- const overCap = args.lamports > FAST_MAX_SOL_PER_FILL_LAMPORTS;
291
- // An over-ceiling leg is never built as a signable transaction: the plan carries the refusal and no bytes.
292
- const built = overCap ? null : await this.d.probe.build(user, { lamports: args.lamports, memo: `dexterm devnet fill ${ref}` });
293
- const decoded = built ? decodeTransaction(Buffer.from(built.txBase64, 'base64')) : null;
294
- const txDigest = decoded ? digest(decoded.message) : 'no-tx';
295
- let txPolicy = overCap
296
- ? { ok: false, reason: 'sol-out-cap', message: `a fast-wallet fill may move at most ${FAST_MAX_SOL_PER_FILL} SOL (asked ${lamportsToSol(args.lamports).toFixed(4)})` }
297
- : { ok: true };
298
- let sim = null;
299
- let solOut = null;
300
- let tokenWatch = [];
301
- if (decoded && built) {
302
- const pol = checkTxPolicyWith(decoded, user, this.allowed, { kind: 'probe' });
303
- if (!pol.ok)
304
- txPolicy = { ok: false, reason: pol.reason, message: pol.message };
305
- else {
306
- const tw = await this.tokenWatchFor(user, decoded);
307
- if (!tw.ok)
308
- txPolicy = { ok: false, reason: 'token-delta-unknown', message: tw.message };
309
- else {
310
- tokenWatch = tw.watch;
311
- const r = await this.simulateWithDelta(built.txBase64, user, tokenWatch, { side: 'buy', mint: WSOL_MINT, amountRaw: built.lamports });
312
- sim = r.sim;
313
- solOut = r.solOut;
314
- if (r.sim.ok && !r.tokens.ok)
315
- txPolicy = { ok: false, reason: r.tokens.reason, message: r.tokens.message };
316
- else if (r.sim.ok && r.tokens.ok && r.tokens.wsolLost > 0n) {
317
- const wb = checkWsolBound('buy', solOut, r.tokens.wsolLost, built.lamports, SOL_OVERHEAD_ALLOWANCE_LAMPORTS);
318
- if (!wb.ok)
319
- txPolicy = { ok: false, reason: wb.reason, message: wb.message };
320
- }
321
- }
322
- }
323
- }
324
- const solUsd = resolveSolUsd(this.d.solUsd?.(), now, 'devnet');
325
- const plan = Object.freeze({
326
- network: 'devnet', mint: args.mint, symbol: args.symbol.replace(/[^\x20-\x7e]/g, '?').slice(0, 16) || args.mint.slice(0, 4), side: args.side,
327
- amountInRaw: args.lamports, amountOutRaw: args.lamports, slippageBps: 0, route: Object.freeze(['devnet-fill']),
328
- priceImpactPct: 0, notionalUsd: notionalUsd('buy', args.lamports, solOut, solUsd), simulated: sim !== null, simOk: sim?.ok ?? false,
329
- simError: sim === null ? null : sim.ok ? null : safeErr(sim.err), solOutLamports: solOut, solLegLamports: args.lamports, txPolicy,
330
- });
331
- const hash = createHash('sha256').update(`fill|${ref}|${bindHash(plan, txDigest)}`).digest('hex');
332
- const order = { mint: args.mint, symbol: plan.symbol, side: args.side, decimals: args.decimals, amountRaw: args.lamports, slippageBps: 0 };
333
- const quote = {
334
- inputMint: WSOL_MINT, outputMint: WSOL_MINT, inAmount: args.lamports, outAmount: args.lamports, otherAmountThreshold: args.lamports,
335
- mode: 'ExactIn', slippageBps: 0, priceImpactPct: 0, route: [], platformFeeBps: null, platformFeeAmount: null, usdValue: null, contextSlot: 0,
336
- source: 'jupiter-swap', fetchedAt: now,
337
- };
338
- const out = Object.freeze({
339
- kind: 'devnet-fill', lane: 'fast', ref, plan, hash, txDigest, unsignedTxBase64: built?.txBase64 ?? '', order, quote, userPublicKey: user,
340
- solUsd, builtAt: now, decoded, expect: null, instructions: decoded ? summarize(decoded) : [],
341
- feeLamports: BigInt(5000 * (decoded?.numRequiredSignatures ?? 1)), prioritizationFeeLamports: null,
342
- tokenWatch: Object.freeze(tokenWatch.map((w) => Object.freeze({ ...w }))),
343
- });
344
- this.plans.set(hash, { built: out, at: now, used: false });
345
- this.sweep(now);
346
- return out;
347
- }
348
- /** task-1363: the fast lane's own ceiling on the SOL a fill may move, checked at precheck and on the signed bytes. */
349
- fastLaneCheck(plan) {
350
- if (plan.network !== 'devnet')
351
- return { ok: false, reason: 'network-mismatch', message: 'the fast wallet fills only on devnet' };
352
- if (plan.solLegLamports > FAST_MAX_SOL_PER_FILL_LAMPORTS)
353
- return { ok: false, reason: 'sol-out-cap', message: `a fast-wallet fill may move at most ${FAST_MAX_SOL_PER_FILL} SOL` };
354
- if (typeof plan.solOutLamports === 'bigint' && plan.solOutLamports > FAST_MAX_SOL_PER_FILL_LAMPORTS + SOL_OVERHEAD_ALLOWANCE_LAMPORTS)
355
- return { ok: false, reason: 'sol-out-cap', message: `the simulation moves ${lamportsToSol(plan.solOutLamports).toFixed(4)} SOL out of the fast wallet; ceiling ${FAST_MAX_SOL_PER_FILL} SOL` };
356
- return { ok: true };
357
- }
358
- /**
359
- * Pre-balance + simulate with the user's post-balance → SOL that left the wallet (negative = came in),
360
- * plus (task-1337) the post-balances of the watched token accounts, diffed against the order.
361
- */
362
- async simulateWithDelta(txBase64, user, watch, order) {
363
- const pre = await this.d.rpc.balance(user);
364
- let sim;
365
- try {
366
- sim = await this.d.rpc.simulate(txBase64, { accounts: [user, ...watch.map((w) => w.address)] });
367
- }
368
- catch (e) {
369
- // task-1337 (seen in the real app on devnet): the node may answer simulateTransaction with a JSON-RPC
370
- // error instead of `value.err` (e.g. "invalid transaction" when a mainnet Jupiter route is simulated
371
- // against devnet). That IS a failed simulation: record it so the PLAN screen shows it and the caps
372
- // refuse it ('simulation-failed'), rather than surfacing as an opaque build error. Transport errors
373
- // still throw (fail closed).
374
- if (!(e instanceof RpcRejectedError))
375
- throw e;
376
- sim = { ok: false, err: e.message, logs: null, unitsConsumed: null, accounts: null, accountsData: null };
377
- }
378
- const post = sim.accounts?.[0] ?? null;
379
- const solOut = sim.ok && typeof pre === 'bigint' && typeof post === 'bigint' ? pre - post : null;
380
- const tokens = checkTokenDeltas(order.side, order.mint, order.amountRaw, watch, postBalances(watch, sim.accountsData ?? null, 1));
381
- return { sim, solOut, tokens };
382
- }
383
- /**
384
- * task-1337: the wallet's token accounts (both token programs) to watch through the simulation, and the
385
- * transaction's full account set (static keys + resolved address-lookup tables) so "referenced by the
386
- * transaction" is exact. Anything that cannot be read is a refusal (fail closed), never a shrug.
387
- */
388
- async tokenWatchFor(user, decoded) {
389
- const rpc = this.d.rpc;
390
- if (typeof rpc.tokenAccountsByOwner !== 'function' || typeof rpc.accountsInfo !== 'function')
391
- return { ok: false, message: 'the RPC client cannot read token accounts; refusing' };
392
- try {
393
- const [spl, t22] = await Promise.all([
394
- rpc.tokenAccountsByOwner.call(this.d.rpc, user, TOKEN_PROGRAM),
395
- rpc.tokenAccountsByOwner.call(this.d.rpc, user, TOKEN_2022_PROGRAM),
396
- ]);
397
- // Nothing to protect (the wallet holds no token account besides wrapped SOL) → nothing to watch;
398
- // the lookup tables only matter for deciding what a watched account's missing post-state means.
399
- if (buildTokenWatch(user, [...spl, ...t22], new Set()).length === 0)
400
- return { ok: true, watch: [] };
401
- const tables = new Map();
402
- const tableKeys = [...new Set(decoded.lookups.map((l) => l.table))];
403
- if (tableKeys.length > 0) {
404
- const infos = await rpc.accountsInfo.call(this.d.rpc, tableKeys);
405
- tableKeys.forEach((t, i) => { const parsed = parseLookupTable(infos[i]?.data); if (parsed)
406
- tables.set(t, parsed); });
407
- }
408
- const referenced = referencedAccounts(decoded, tables);
409
- if (!referenced)
410
- return { ok: false, message: 'an address-lookup table the transaction loads could not be read; refusing' };
411
- const watch = buildTokenWatch(user, [...spl, ...t22], referenced);
412
- // task-1337 F3: if the transaction references more of the wallet's own token accounts than one
413
- // simulation can carry, the watch list cannot cover them all — refuse rather than drop any.
414
- if (referencedOverCap(watch))
415
- return { ok: false, message: `the transaction references more of the wallet's token accounts than one simulation can watch (${MAX_TOKEN_WATCH}); refusing` };
416
- return { ok: true, watch };
417
- }
418
- catch (e) {
419
- return { ok: false, message: `could not read the wallet's token accounts: ${safeErr(e)}` };
420
- }
421
- }
422
- /** The confirm step re-reads the watched accounts' balances (they may have moved since the plan). */
423
- async refreshTokenWatch(watch) {
424
- if (watch.length === 0)
425
- return [];
426
- const rpc = this.d.rpc;
427
- if (typeof rpc.accountsInfo !== 'function')
428
- return null;
429
- try {
430
- return refreshWatch(watch, await rpc.accountsInfo.call(this.d.rpc, watch.map((w) => w.address)));
431
- }
432
- catch {
433
- return null;
434
- }
435
- }
436
- sweep(now) {
437
- for (const [h, r] of this.plans)
438
- if (r.used || now - r.at > PLAN_TTL_MS)
439
- this.plans.delete(h);
440
- }
441
- /** The caps/plan verdict for a built plan, so the UI can show "will be refused: …" before the keypress. */
442
- precheck(built) {
443
- const reg = this.plans.get(built.hash);
444
- const b = reg ? reg.built : built;
445
- if (this.killSwitch.engaged() || this.d.ledger.locked())
446
- return { ok: false, reason: 'panicked', message: 'live trading is panicked' };
447
- const fast = b.lane === 'fast';
448
- if (fast) {
449
- if (!this.d.fastSigner || b.userPublicKey !== this.d.fastSigner.publicKey)
450
- return { ok: false, reason: 'signer-mismatch', message: 'the fast wallet is not available for this plan' };
451
- const f = this.fastLaneCheck(b.plan);
452
- if (!f.ok)
453
- return f;
454
- }
455
- return checkCaps(b.plan, this.d.caps, this.d.ledger.spentToday(), { enforceUsd: fast });
456
- }
457
- /**
458
- * Step 2 (after the physical keypress): look the plan up, re-check caps + reserve, then sign via the
459
- * wallet bridge, verify + re-simulate the signed bytes, submit and confirm, and record the fill.
460
- * `confirmedHash` is the hash the UI showed the user; it must name a plan THIS executor built.
461
- */
462
- async confirmAndSubmit(built, confirmedHash, userPublicKey) {
463
- const started = this.clock.now();
464
- const refusal = (reason, message) => ({ kind: 'refused', reason, message });
465
- // Plan-hash binding (incl. tx bytes, DEFECT 2) against the executor's OWN registry: an edited or
466
- // rebuilt plan, a rebound digest, an unknown or already-used hash → refuse.
467
- const reg = this.plans.get(confirmedHash);
468
- if (!reg || built.hash !== confirmedHash || reg.used) {
469
- return refusal('plan-mismatch', 'the plan changed after it was shown (or was already confirmed); confirm again');
470
- }
471
- const b = reg.built; // trusted frozen copy; `built` is not used past this point
472
- if (started - reg.at > PLAN_TTL_MS) {
473
- this.plans.delete(confirmedHash);
474
- return refusal('plan-expired', `the plan is older than ${PLAN_TTL_MS / 1000} s; rebuild it`);
475
- }
476
- const expectHash = b.kind === 'probe' ? createHash('sha256').update(`probe|${bindHash(b.plan, b.txDigest)}`).digest('hex')
477
- : b.kind === 'devnet-fill' ? createHash('sha256').update(`fill|${b.ref ?? ''}|${bindHash(b.plan, b.txDigest || 'no-tx')}`).digest('hex')
478
- : bindHash(b.plan, b.txDigest || 'no-tx');
479
- if (expectHash !== b.hash)
480
- return refusal('plan-mismatch', 'plan binding broken');
481
- if (userPublicKey !== b.userPublicKey)
482
- return refusal('signer-mismatch', 'the confirm names a different wallet than the plan');
483
- // task-1363: lane routing. A fast-lane plan is signed ONLY by the fast wallet it was built for, and the
484
- // fast wallet NEVER signs a main-lane plan (the user's wallet approves those in Phantom). Either mismatch
485
- // is a refusal before caps, reservation or signing.
486
- const fast = b.lane === 'fast';
487
- const fastSigner = this.d.fastSigner ?? null;
488
- if (fast && (!fastSigner || fastSigner.publicKey !== b.userPublicKey))
489
- return refusal('signer-mismatch', 'this fast-lane plan is not for the loaded fast wallet');
490
- if (!fast && fastSigner && fastSigner.publicKey === b.userPublicKey)
491
- return refusal('signer-mismatch', 'the fast wallet never signs a main-lane plan');
492
- if (fast) {
493
- const f = this.fastLaneCheck(b.plan);
494
- if (!f.ok)
495
- return refusal(f.reason, f.message);
496
- }
497
- const panic = this.killSwitch.engaged();
498
- if (panic)
499
- return refusal('panicked', `live trading is panicked (${panic.reason})`);
500
- const lock = this.d.ledger.locked();
501
- if (lock)
502
- return refusal('panicked', `the cap ledger is locked (${lock.reason})`);
503
- // Round 2: the network is re-derived at submit, not trusted from the plan.
504
- if (b.plan.network === 'devnet' && rpcNetworkOf(this.d.rpc) === 'mainnet') {
505
- return refusal('network-mismatch', 'the RPC endpoint is mainnet but the plan was built as devnet');
506
- }
507
- // DEFECT 5 / round 2: re-check caps AND reserve the USD atomically (ledger transaction) under the
508
- // in-process mutex. The reserve is charged BEFORE signing/submitting; it is refunded ONLY when the
509
- // send definitely never left.
510
- const caps = effectiveCaps(this.d.caps);
511
- // USD is reserved on mainnet, and (task-1363) on the fast lane even on devnet: a mandate fill counts against
512
- // the signer's per-trade and daily USD caps exactly as a real swap would.
513
- const usdLane = b.plan.network === 'mainnet' || fast;
514
- const reserveUsd = (usdLane && b.plan.notionalUsd !== null && b.plan.notionalUsd > 0) ? b.plan.notionalUsd : 0;
515
- const gate = await this.runExclusive(() => {
516
- if (reg.used)
517
- return { ok: false, reason: 'plan-mismatch', message: 'the plan was already confirmed' };
518
- const check = checkCaps(b.plan, this.d.caps, this.d.ledger.spentToday(), { enforceUsd: fast });
519
- if (!check.ok)
520
- return { ok: false, reason: check.reason, message: check.message ?? 'refused by caps' };
521
- const res = this.d.ledger.tryReserve(reserveUsd, caps.dailyUsd, this.clock.now());
522
- if (!res.ok) {
523
- return res.reason === 'panicked'
524
- ? { ok: false, reason: 'panicked', message: 'the cap ledger is locked' }
525
- : { ok: false, reason: 'daily-cap', message: `trade $${reserveUsd.toFixed(2)} would exceed today's remaining budget ($${Math.max(0, caps.dailyUsd - res.spent).toFixed(2)} of $${caps.dailyUsd.toFixed(2)})` };
526
- }
527
- reg.used = true; // single-use from here on (a double keypress gets 'plan-mismatch')
528
- this.plans.delete(confirmedHash);
529
- return { ok: true, day: res.day };
530
- });
531
- if (!gate.ok)
532
- return refusal(gate.reason, gate.message);
533
- let reserved = reserveUsd;
534
- const refund = () => { if (reserved > 0) {
535
- this.d.ledger.refund(reserved, gate.day);
536
- reserved = 0;
537
- } };
538
- // Hand the unsigned tx to the wallet for signing. Main lane: Phantom in the browser (the physical
539
- // confirmation; keys never enter dexterm). Fast lane (task-1363): the capped devnet fast wallet signs
540
- // locally — the keypress that armed the plan into its mandate was the physical confirmation.
541
- let signedTxBase64;
542
- try {
543
- const signer = fast ? fastSigner : this.d.bridge;
544
- const signed = await signer.signTransaction({ unsignedTxBase64: b.unsignedTxBase64, expectPublicKey: userPublicKey, label: `${b.plan.side} ${b.plan.symbol}` });
545
- signedTxBase64 = signed.signedTxBase64;
546
- }
547
- catch (e) {
548
- refund();
549
- return { kind: 'send-failed', signature: null, error: `wallet sign failed: ${safeErr(e)}` };
550
- }
551
- // DEFECT 2 + round 2: the signed tx must carry the SAME message that was simulated and approved, a
552
- // VALID Ed25519 signature by the linked wallet, and pass the same static policy. A wallet (or a
553
- // compromised bridge) that returns anything else is refused — the bytes never reach the chain.
554
- let signedTx;
555
- try {
556
- signedTx = decodeTransaction(Buffer.from(signedTxBase64, 'base64'));
557
- }
558
- catch (e) {
559
- refund();
560
- return refusal('tx-mismatch', `the signed transaction could not be decoded: ${safeErr(e)}`);
561
- }
562
- // task-1328: exact bytes, or the approved message plus an APPENDED Lighthouse assertion tail (what
563
- // Phantom may add). Anything else is a different transaction.
564
- if (!b.decoded) {
565
- refund();
566
- return refusal('tx-mismatch', 'the plan carries no approved transaction');
567
- }
568
- const cmp = compareApprovedMessage(b.decoded, signedTx);
569
- if (!cmp.ok) {
570
- refund();
571
- return refusal('tx-mismatch', `the signed transaction does not match the approved plan: ${cmp.message}`);
572
- }
573
- const sigCheck = verifyUserSignature(signedTx, userPublicKey);
574
- if (!sigCheck.ok) {
575
- refund();
576
- return refusal('bad-signature', sigCheck.message);
577
- }
578
- const pol = checkTxPolicyWith(signedTx, userPublicKey, this.allowed, { kind: policyKind(b.kind), lighthouseTail: cmp.lighthouseTail > 0 });
579
- if (!pol.ok) {
580
- refund();
581
- return refusal(pol.reason, pol.message);
582
- }
583
- if (b.kind === 'swap' && b.expect) {
584
- const bound = checkSwapBinding(signedTx, b.expect);
585
- if (!bound.ok) {
586
- refund();
587
- return refusal(bound.reason, bound.message);
588
- }
589
- }
590
- // Round 2 (SPEC §6.2 "re-simulates every transaction it is asked to sign"): simulate the SIGNED
591
- // bytes and compare the SOL delta with the plan. More SOL out than planned (+ fee allowance), or
592
- // more than the SOL cap, or a now-failing simulation → refuse, nothing submitted.
593
- let resim;
594
- try {
595
- // task-1337: fresh token pre-balances (the wallet may have moved since the plan), then the same diff.
596
- const watch = await this.refreshTokenWatch(b.tokenWatch);
597
- if (watch === null) {
598
- refund();
599
- return refusal('token-delta-unknown', 'could not re-read the wallet\'s token accounts before signing off');
600
- }
601
- resim = await this.simulateWithDelta(signedTxBase64, userPublicKey, watch, { side: b.order.side, mint: b.order.mint, amountRaw: b.order.amountRaw });
602
- }
603
- catch (e) {
604
- refund();
605
- return refusal('simulation-failed', `re-simulation failed: ${safeErr(e)}`);
606
- }
607
- if (!resim.sim.ok) {
608
- refund();
609
- return refusal('simulation-failed', `re-simulation failed: ${safeErr(resim.sim.err)}`);
610
- }
611
- if (resim.solOut === null) {
612
- refund();
613
- return refusal('sol-delta-unknown', 're-simulation reported no wallet balance change');
614
- }
615
- if (!resim.tokens.ok) {
616
- refund();
617
- return refusal(resim.tokens.reason, `re-simulation: ${resim.tokens.message}`);
618
- }
619
- // task-1337 F2: the signed bytes must not drain pre-existing wrapped SOL beyond the plan's SOL leg.
620
- const wsolBound = checkWsolBound(b.plan.side, resim.solOut, resim.tokens.wsolLost, b.plan.solLegLamports, SOL_OVERHEAD_ALLOWANCE_LAMPORTS);
621
- if (!wsolBound.ok) {
622
- refund();
623
- return refusal(wsolBound.reason, `re-simulation: ${wsolBound.message}`);
624
- }
625
- if (resim.solOut > b.plan.solLegLamports + SOL_OVERHEAD_ALLOWANCE_LAMPORTS) {
626
- refund();
627
- return refusal('sol-out-cap', `re-simulation moves ${lamportsToSol(resim.solOut).toFixed(4)} SOL out but the plan is ${lamportsToSol(b.plan.solLegLamports).toFixed(4)} SOL`);
628
- }
629
- if (resim.solOut > BigInt(Math.round(caps.maxSolOut * 1e9))) {
630
- refund();
631
- return refusal('sol-out-cap', `re-simulation moves ${lamportsToSol(resim.solOut).toFixed(4)} SOL out; cap ${caps.maxSolOut} SOL`);
632
- }
633
- if (fast && resim.solOut > FAST_MAX_SOL_PER_FILL_LAMPORTS + SOL_OVERHEAD_ALLOWANCE_LAMPORTS) {
634
- refund();
635
- return refusal('sol-out-cap', `re-simulation moves ${lamportsToSol(resim.solOut).toFixed(4)} SOL out of the fast wallet; ceiling ${FAST_MAX_SOL_PER_FILL} SOL`);
636
- }
637
- // Mainnet (and the fast lane): re-price with the CURRENT feed; if the trade now prices higher, the extra must also fit.
638
- if (usdLane) {
639
- const nowUsd = notionalUsd(b.plan.side, b.plan.solLegLamports, resim.solOut, resolveSolUsd(this.d.solUsd?.(), this.clock.now(), b.plan.network));
640
- if (nowUsd === null) {
641
- refund();
642
- return refusal('no-usd-value', 'the SOL/USD feed is no longer trustworthy');
643
- }
644
- if (nowUsd > caps.perTradeUsd) {
645
- refund();
646
- return refusal('per-trade-cap', `trade now $${nowUsd.toFixed(2)} exceeds the per-trade cap $${caps.perTradeUsd.toFixed(2)}`);
647
- }
648
- if (nowUsd > reserved) {
649
- const extra = nowUsd - reserved;
650
- const more = this.d.ledger.tryReserve(extra, caps.dailyUsd, this.clock.now());
651
- if (!more.ok) {
652
- refund();
653
- return refusal(more.reason === 'panicked' ? 'panicked' : 'daily-cap', 'the re-priced trade no longer fits today\'s budget');
654
- }
655
- reserved += extra;
656
- }
657
- }
658
- // Submit. Round 2 (re-verify MEDIUM #4): refund ONLY on a definite JSON-RPC rejection. On a
659
- // transport error the node may already have the tx; keep the reservation and ask about the
660
- // signature we know from the signed bytes before deciding.
661
- const knownSignature = base58(signedTx.signatures[0]);
662
- let signature;
663
- try {
664
- signature = await this.d.rpc.submit(signedTxBase64);
665
- }
666
- catch (e) {
667
- if (e instanceof RpcRejectedError) {
668
- refund();
669
- return { kind: 'send-failed', signature: null, error: `rejected before broadcast: ${safeErr(e)}` };
670
- }
671
- const seen = await this.lookup(knownSignature);
672
- if (seen === 'unknown' || seen === null) {
673
- return { kind: 'send-unknown', signature: knownSignature, error: `submit did not complete (${safeErr(e)}); the transaction may still land — do not resend` };
674
- }
675
- signature = knownSignature; // the node has it: continue to confirmation
676
- }
677
- // Past this point the tx IS (or may be) broadcast: the USD charge stands even if confirmation times
678
- // out (DEFECT 5 — a timed-out send must still count against the daily budget; funds may have moved).
679
- let conf;
680
- try {
681
- conf = await this.d.rpc.confirm(signature);
682
- }
683
- catch (e) {
684
- return { kind: 'send-unknown', signature, error: `could not read confirmation: ${safeErr(e)}` };
685
- }
686
- if (!conf.confirmed) {
687
- const timedOut = typeof conf.err === 'string' && /timed out/.test(conf.err);
688
- return timedOut
689
- ? { kind: 'send-unknown', signature, error: 'confirmation timed out; the transaction may still land — do not resend' }
690
- : { kind: 'send-failed', signature, error: `failed on chain: ${safeErr(conf.err)}` };
691
- }
692
- // Record the fill (positions/PnL). The daily USD was already reserved at send; do NOT charge twice.
693
- // The SOL leg is what the RE-SIMULATION of the signed bytes moved (not the quote); the token leg is
694
- // the quoted amount. True on-chain deltas (getTransaction) are a follow-up noted in the handoff.
695
- const fill = computeFill(b.order, b.quote, this.clock.now());
696
- // A probe or a devnet fill moves no tokens: what left the fast/main wallet is fees (the wrap is closed back).
697
- const booked = b.kind === 'probe' || b.kind === 'devnet-fill'
698
- ? { ...fill, tokenDelta: 0n, solDelta: -resim.solOut, grossLamports: 0n, feeLamports: resim.solOut, route: [...b.plan.route] }
699
- : { ...fill, solDelta: b.plan.side === 'buy' ? -resim.solOut : -resim.solOut, feeLamports: b.plan.side === 'buy' ? (resim.solOut > fill.grossLamports ? resim.solOut - fill.grossLamports : 0n) : fill.feeLamports };
700
- this.d.liveLedger?.record({ fill: booked, network: b.plan.network, signature, usd: b.plan.notionalUsd, kind: b.kind });
701
- return { kind: 'filled', signature, fill: booked, roundTripMs: this.clock.now() - started, lighthouseTail: cmp.lighthouseTail };
702
- }
703
- /** Did the node see this signature? 'unknown' when the lookup itself failed. */
704
- async lookup(signature) {
705
- const rpc = this.d.rpc;
706
- if (typeof rpc.signatureStatus !== 'function')
707
- return 'unknown';
708
- try {
709
- return await rpc.signatureStatus.call(this.d.rpc, signature);
710
- }
711
- catch {
712
- return 'unknown';
713
- }
714
- }
715
- }
716
- // --------------------------------------------------------------------------- helpers (pure)
717
- function rpcNetworkOf(rpc) {
718
- const explicit = rpc.network;
719
- return rpcNetwork(rpc.url, explicit === 'devnet' || explicit === 'mainnet' ? explicit : null);
720
- }
721
- /** Round 2 HIGH #1: the normalized quote must describe exactly the order's swap (mints, mode, amount). */
722
- function bindQuote(quote, req) {
723
- const bad = (message) => ({ ok: false, reason: 'quote-mismatch', message });
724
- if (quote.inputMint !== req.inputMint)
725
- return bad(`quote input mint ${quote.inputMint} is not ${req.inputMint}`);
726
- if (quote.outputMint !== req.outputMint)
727
- return bad(`quote output mint ${quote.outputMint} is not ${req.outputMint}`);
728
- if (quote.mode !== 'ExactIn')
729
- return bad(`quote mode ${quote.mode} is not ExactIn`);
730
- if (quote.inAmount !== req.amount)
731
- return bad(`quote inAmount ${quote.inAmount} is not the order amount ${req.amount}`);
732
- if (quote.outAmount <= 0n)
733
- return bad('quote outAmount is not positive');
734
- return { ok: true };
735
- }
736
- /** The SOL leg in lamports: the order amount for a buy; for a sell the larger of quoted and simulated SOL in. */
737
- function solLegLamports(order, quote, solOut) {
738
- if (order.side === 'buy')
739
- return order.amountRaw;
740
- const simIn = solOut !== null && solOut < 0n ? -solOut : 0n;
741
- return quote.outAmount > simIn ? quote.outAmount : simIn;
742
- }
743
- /**
744
- * USD notional priced LOCALLY: the larger of the planned SOL leg and what the simulation actually moved
745
- * out of the wallet (a buy), times the resolved SOL/USD. null when no trustworthy price → refused on mainnet.
746
- */
747
- function notionalUsd(side, solLeg, solOut, usd) {
748
- if (usd.price === null)
749
- return null;
750
- let lamports = solLeg;
751
- if (side === 'buy' && solOut !== null && solOut > lamports)
752
- lamports = solOut;
753
- const sol = lamportsToSol(lamports);
754
- if (!Number.isFinite(sol) || sol < 0)
755
- return null;
756
- const out = sol * usd.price;
757
- return Number.isFinite(out) ? out : null;
758
- }
759
- /** Ed25519-verify the user's signature slot over the message. The fee payer (key 0) is the user. */
760
- function verifyUserSignature(tx, userPublicKey) {
761
- const idx = tx.staticKeys.indexOf(userPublicKey);
762
- if (idx < 0 || idx >= tx.numRequiredSignatures)
763
- return { ok: false, message: 'the linked wallet is not a signer of this transaction' };
764
- const sig = tx.signatures[idx];
765
- if (!sig || sig.length !== 64 || sig.every((x) => x === 0))
766
- return { ok: false, message: 'the wallet returned no signature' };
767
- let pk;
768
- try {
769
- pk = Buffer.from(base58Decode(userPublicKey));
770
- }
771
- catch {
772
- return { ok: false, message: 'bad wallet public key' };
773
- }
774
- if (pk.length !== 32)
775
- return { ok: false, message: 'bad wallet public key' };
776
- let good = false;
777
- try {
778
- good = verifyEd25519(pk, tx.message, sig);
779
- }
780
- catch {
781
- good = false;
782
- }
783
- return good ? { ok: true } : { ok: false, message: 'the signature does not verify against the linked wallet' };
784
- }
785
- /**
786
- * Bind the confirm to both the trade's material fields and the exact transaction message (DEFECT 2).
787
- * planHash covers mint/size/route/side/network/SOL deltas; the tx digest covers the actual bytes.
788
- */
789
- function bindHash(plan, txDigest) {
790
- return createHash('sha256').update(`${planHash(plan)}|${txDigest}`).digest('hex');
791
- }
792
- const PROGRAM_NAMES = Object.freeze({
793
- [SYSTEM_PROGRAM]: 'System', [COMPUTE_BUDGET_PROGRAM]: 'ComputeBudget', [MEMO_PROGRAM]: 'Memo', [JUPITER_V6_PROGRAM]: 'Jupiter',
794
- [ASSOCIATED_TOKEN_PROGRAM]: 'AssociatedToken', [LIGHTHOUSE_PROGRAM]: 'Lighthouse',
795
- TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA: 'Token', TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb: 'Token-2022',
796
- });
797
- const TOKEN_OPS = Object.freeze({ 1: 'InitializeAccount', 9: 'CloseAccount→wallet', 16: 'InitializeAccount2', 17: 'SyncNative', 18: 'InitializeAccount3' });
798
- /** Human summary of the top-level instructions ("what will be signed"), for the PLAN screen. */
799
- export function summarize(tx) {
800
- return tx.instructions.map((ix) => {
801
- const program = ix.programId ? (PROGRAM_NAMES[ix.programId] ?? `${ix.programId.slice(0, 4)}…${ix.programId.slice(-4)}`) : 'lookup-table program';
802
- let op = '';
803
- if (ix.programId === JUPITER_V6_PROGRAM)
804
- op = JUPITER_ROUTE_DISCRIMINATORS.get(ix.data.subarray(0, 8).toString('hex')) ?? 'unknown';
805
- else if (ix.programId === SYSTEM_PROGRAM)
806
- op = ix.data.length >= 4 && ix.data.readUInt32LE(0) === 2 ? 'Transfer→own WSOL' : `op ${ix.data.length >= 4 ? ix.data.readUInt32LE(0) : '?'}`;
807
- else if (ix.programId === COMPUTE_BUDGET_PROGRAM)
808
- op = ix.data[0] === 2 ? 'SetComputeUnitLimit' : ix.data[0] === 3 ? 'SetComputeUnitPrice' : `op ${ix.data[0]}`;
809
- else if (ix.programId === ASSOCIATED_TOKEN_PROGRAM)
810
- op = ix.data.length === 0 || ix.data[0] === 0 ? 'Create' : 'CreateIdempotent';
811
- else if (program === 'Token' || program === 'Token-2022')
812
- op = TOKEN_OPS[ix.data[0]] ?? `op ${ix.data[0]}`;
813
- else if (ix.programId === LIGHTHOUSE_PROGRAM)
814
- op = 'assertion';
815
- else if (ix.programId === MEMO_PROGRAM)
816
- op = 'memo';
817
- return { program, op };
818
- });
819
- }
820
- function digest(message) {
821
- return createHash('sha256').update(message).digest('hex');
822
- }
823
- function safeErr(e) {
824
- if (e === null || e === undefined)
825
- return 'unknown';
826
- if (e instanceof Error)
827
- return e.message;
828
- if (typeof e === 'string')
829
- return e;
830
- try {
831
- return JSON.stringify(e).slice(0, 200);
832
- }
833
- catch {
834
- return String(e);
835
- }
836
- }
837
- /** Where the signer keeps confirmed REAL fills (the UI reads it read-only for positions). */
838
- export function defaultLiveLedgerPath() {
839
- return join(homedir(), '.dexterm', 'live-trades.sqlite');
840
- }
841
- export class LiveLedger {
842
- db;
843
- clock;
844
- constructor(opts = {}) {
845
- const path = opts.path ?? ':memory:';
846
- if (path !== ':memory:' && !opts.readOnly)
847
- mkdirSync(dirname(path), { recursive: true });
848
- this.db = opts.readOnly ? new DatabaseSync(path, { readOnly: true }) : new DatabaseSync(path);
849
- this.clock = opts.clock ?? systemClock;
850
- if (!opts.readOnly) {
851
- this.db.exec(`
852
- PRAGMA journal_mode = WAL;
853
- CREATE TABLE IF NOT EXISTS live_trades (
854
- id INTEGER PRIMARY KEY AUTOINCREMENT, t INTEGER NOT NULL, network TEXT NOT NULL, signature TEXT NOT NULL,
855
- mint TEXT NOT NULL, symbol TEXT NOT NULL, side TEXT NOT NULL,
856
- sol_delta TEXT NOT NULL, token_delta TEXT NOT NULL, price_sol REAL NOT NULL, usd REAL
857
- );
858
- CREATE INDEX IF NOT EXISTS live_trades_mint ON live_trades(mint, id);
859
- `);
860
- // task-1328 columns (positions need decimals + fee; realized is booked per sell). Additive migration.
861
- const cols = new Set(this.db.prepare('PRAGMA table_info(live_trades)').all().map((c) => c.name));
862
- if (!cols.has('decimals'))
863
- this.db.exec('ALTER TABLE live_trades ADD COLUMN decimals INTEGER NOT NULL DEFAULT 9');
864
- if (!cols.has('fee_lamports'))
865
- this.db.exec("ALTER TABLE live_trades ADD COLUMN fee_lamports TEXT NOT NULL DEFAULT '0'");
866
- if (!cols.has('realized_lamports'))
867
- this.db.exec("ALTER TABLE live_trades ADD COLUMN realized_lamports TEXT NOT NULL DEFAULT '0'");
868
- if (!cols.has('kind'))
869
- this.db.exec("ALTER TABLE live_trades ADD COLUMN kind TEXT NOT NULL DEFAULT 'swap'");
870
- }
871
- }
872
- /** Book a confirmed fill. A sell's realized PnL is computed against the position this ledger holds. */
873
- record(args) {
874
- const f = args.fill;
875
- let realized = 0n;
876
- if (f.side === 'sell' && (args.kind ?? 'swap') === 'swap') {
877
- const pos = this.position(f.mint) ?? emptyPosition(f.mint, f.symbol, f.decimals, f.t);
878
- realized = applySell(pos, f).realizedLamports;
879
- }
880
- this.db.prepare(`INSERT INTO live_trades (t, network, signature, mint, symbol, side, sol_delta, token_delta, price_sol, usd, decimals, fee_lamports, realized_lamports, kind)
881
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
882
- .run(f.t || this.clock.now(), args.network, args.signature, f.mint, f.symbol, f.side, f.solDelta.toString(), f.tokenDelta.toString(), f.priceSol, args.usd, f.decimals, f.feeLamports.toString(), realized.toString(), args.kind ?? 'swap');
883
- }
884
- trades(limit = 100) {
885
- const rows = this.db.prepare('SELECT * FROM live_trades ORDER BY id DESC LIMIT ?').all(limit);
886
- return rows.map((r) => ({
887
- id: Number(r.id), t: Number(r.t), network: String(r.network), signature: String(r.signature),
888
- mint: String(r.mint), symbol: String(r.symbol), side: String(r.side),
889
- solDelta: String(r.sol_delta), tokenDelta: String(r.token_delta), priceSol: Number(r.price_sol),
890
- usd: r.usd === null ? null : Number(r.usd),
891
- decimals: Number(r.decimals ?? 9), feeLamports: String(r.fee_lamports ?? '0'), realizedLamports: String(r.realized_lamports ?? '0'),
892
- kind: (r.kind === 'probe' ? 'probe' : r.kind === 'devnet-fill' ? 'devnet-fill' : 'swap'),
893
- }));
894
- }
895
- /** Fills as TradeRecords (the shape the positions screen / popup fills already consume). Probes are excluded. */
896
- records(limit = 100, mint) {
897
- return this.trades(limit * 2).filter((r) => r.kind === 'swap' && (mint === undefined || r.mint === mint)).slice(0, limit).map((r) => ({
898
- id: r.id, t: r.t, mint: r.mint, symbol: r.symbol, side: r.side, decimals: r.decimals,
899
- solDelta: BigInt(r.solDelta), tokenDelta: BigInt(r.tokenDelta),
900
- grossLamports: (BigInt(r.solDelta) < 0n ? -BigInt(r.solDelta) : BigInt(r.solDelta)) - (r.side === 'buy' ? BigInt(r.feeLamports) : -BigInt(r.feeLamports)),
901
- feeLamports: BigInt(r.feeLamports), priceSol: r.priceSol, priceUsd: null, priceImpactPct: null, slippageBps: 0, route: [],
902
- realizedLamports: BigInt(r.realizedLamports), paper: true,
903
- }));
904
- }
905
- /** Open + closed positions aggregated from the fills (oldest → newest), swaps only. */
906
- positions() {
907
- const rows = this.db.prepare("SELECT * FROM live_trades WHERE kind = 'swap' ORDER BY id ASC").all();
908
- const by = new Map();
909
- for (const r of rows) {
910
- const mint = String(r.mint);
911
- const fill = {
912
- t: Number(r.t), mint, symbol: String(r.symbol), side: String(r.side), decimals: Number(r.decimals ?? 9),
913
- solDelta: BigInt(String(r.sol_delta)), tokenDelta: BigInt(String(r.token_delta)), grossLamports: 0n, feeLamports: BigInt(String(r.fee_lamports ?? '0')),
914
- priceSol: Number(r.price_sol), priceUsd: null, priceImpactPct: null, slippageBps: 0, route: [], realizedLamports: 0n, paper: true,
915
- };
916
- const pos = by.get(mint) ?? emptyPosition(mint, fill.symbol, fill.decimals, fill.t);
917
- by.set(mint, fill.side === 'buy' ? applyBuy(pos, fill) : applySell(pos, fill).pos);
918
- }
919
- return [...by.values()];
920
- }
921
- position(mint) {
922
- return this.positions().find((p) => p.mint === mint) ?? null;
923
- }
924
- /** Net SOL moved across all recorded live fills (quick sanity surface for the status bar). */
925
- netSol() {
926
- const row = this.db.prepare('SELECT COALESCE(SUM(CAST(sol_delta AS INTEGER)),0) AS s FROM live_trades').get();
927
- return lamportsToSol(BigInt(Math.trunc(Number(row?.s ?? 0))));
928
- }
929
- close() { try {
930
- this.db.close();
931
- }
932
- catch { /* already closed */ } }
933
- }