dexterm-cli 0.1.5 → 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.
- package/dist/packages/agents/src/index.js +2 -0
- package/dist/packages/agents/src/schema.js +22 -1
- package/dist/packages/agents/src/spectator.js +54 -0
- package/dist/packages/agents/src/story.js +532 -0
- package/dist/packages/agents/src/tiers.js +28 -2
- package/dist/packages/agents/src/tools.js +31 -2
- package/dist/packages/autotrade/assets/exit-profile.schema.json +492 -0
- package/dist/packages/autotrade/assets/profiles/moonbag.json +46 -0
- package/dist/packages/autotrade/assets/profiles/runner.json +88 -0
- package/dist/packages/autotrade/assets/profiles/scalp.json +88 -0
- package/dist/packages/autotrade/assets/profiles/swing.json +88 -0
- package/dist/packages/autotrade/assets/strategies/moonbag.json +101 -0
- package/dist/packages/autotrade/assets/strategies/runner.json +101 -0
- package/dist/packages/autotrade/assets/strategies/sample-dip-ladder.json +188 -0
- package/dist/packages/autotrade/assets/strategies/scalp.json +98 -0
- package/dist/packages/autotrade/assets/strategies/swing.json +97 -0
- package/dist/packages/autotrade/assets/strategies/yolo.json +157 -0
- package/dist/packages/autotrade/assets/tapes/yolo-rehearsal.json +1 -0
- package/dist/packages/autotrade/src/account.js +74 -0
- package/dist/packages/autotrade/src/backtest.js +357 -0
- package/dist/packages/autotrade/src/basis-text.js +32 -0
- package/dist/packages/autotrade/src/chat/approvals.js +484 -0
- package/dist/packages/autotrade/src/chat/audit.js +140 -0
- package/dist/packages/autotrade/src/chat/config.js +93 -0
- package/dist/packages/autotrade/src/chat/connector.js +24 -0
- package/dist/packages/autotrade/src/chat/control.js +103 -0
- package/dist/packages/autotrade/src/chat/fake.js +100 -0
- package/dist/packages/autotrade/src/chat/format.js +71 -0
- package/dist/packages/autotrade/src/chat/hub.js +187 -0
- package/dist/packages/autotrade/src/chat/index.js +18 -0
- package/dist/packages/autotrade/src/chat/notify.js +292 -0
- package/dist/packages/autotrade/src/chat/pairing.js +46 -0
- package/dist/packages/autotrade/src/chat/redact.js +48 -0
- package/dist/packages/autotrade/src/chat/secrets.js +99 -0
- package/dist/packages/autotrade/src/chat/service.js +248 -0
- package/dist/packages/autotrade/src/chat/telegram.js +290 -0
- package/dist/packages/autotrade/src/desk-executor.js +188 -0
- package/dist/packages/autotrade/src/engine.js +387 -0
- package/dist/packages/autotrade/src/execution-port.js +97 -0
- package/dist/packages/autotrade/src/fleet-config.js +169 -0
- package/dist/packages/autotrade/src/fleet-control.js +276 -0
- package/dist/packages/autotrade/src/fleet.js +639 -0
- package/dist/packages/autotrade/src/index.js +53 -0
- package/dist/packages/autotrade/src/ledger.js +108 -0
- package/dist/packages/autotrade/src/live-source.js +182 -0
- package/dist/packages/autotrade/src/monitor-daemon.js +715 -0
- package/dist/packages/autotrade/src/monitor.js +190 -0
- package/dist/packages/autotrade/src/ollama-sessions.js +137 -0
- package/dist/packages/autotrade/src/paper-accounts.js +72 -0
- package/dist/packages/autotrade/src/replay.js +198 -0
- package/dist/packages/autotrade/src/runner-config.js +99 -0
- package/dist/packages/autotrade/src/runner.js +872 -0
- package/dist/packages/autotrade/src/safety.js +243 -0
- package/dist/packages/autotrade/src/session.js +269 -0
- package/dist/packages/autotrade/src/sessions.js +503 -0
- package/dist/packages/autotrade/src/store.js +250 -0
- package/dist/packages/autotrade/src/strategy.js +256 -0
- package/dist/packages/autotrade/src/trade-ledger.js +241 -0
- package/dist/packages/autotrade/src/yolo.js +140 -0
- package/dist/packages/cli/assets/claude/analyst-system.md +4 -4
- package/dist/packages/cli/assets/claude/plugin/skills/REGISTRY.json +1 -1
- package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +15 -8
- package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +1 -1
- package/dist/packages/cli/src/agent-cmd.js +6 -4
- package/dist/packages/cli/src/agent-spectator.js +17 -1
- package/dist/packages/cli/src/ai-cmd.js +412 -0
- package/dist/packages/cli/src/analyst/backend.js +105 -15
- package/dist/packages/cli/src/analyst/codex.js +14 -9
- package/dist/packages/cli/src/analyst/larp-gauge.js +60 -0
- package/dist/packages/cli/src/analyst/ollama.js +263 -0
- package/dist/packages/cli/src/analyst/session.js +17 -3
- package/dist/packages/cli/src/app-help-gen.js +51 -15
- package/dist/packages/cli/src/autotrade-cmd/audit.js +57 -0
- package/dist/packages/cli/src/autotrade-cmd/backtest.js +199 -0
- package/dist/packages/cli/src/autotrade-cmd/flatten.js +64 -0
- package/dist/packages/cli/src/autotrade-cmd/fleet.js +397 -0
- package/dist/packages/cli/src/autotrade-cmd/requests.js +79 -0
- package/dist/packages/cli/src/autotrade-cmd/run.js +413 -0
- package/dist/packages/cli/src/autotrade-cmd/stop.js +76 -0
- package/dist/packages/cli/src/autotrade-cmd/strategies.js +198 -0
- package/dist/packages/cli/src/autotrade-cmd/strategy-control.js +174 -0
- package/dist/packages/cli/src/autotrade-cmd/telegram-host.js +101 -0
- package/dist/packages/cli/src/autotrade-cmd.js +168 -0
- package/dist/packages/cli/src/backtest-cmd.js +13 -2
- package/dist/packages/cli/src/display.js +208 -23
- package/dist/packages/cli/src/doctor.js +62 -3
- package/dist/packages/cli/src/export.js +10 -2
- package/dist/packages/cli/src/index.js +1 -1
- package/dist/packages/cli/src/ledger-cmd.js +288 -0
- package/dist/packages/cli/src/live/chart-fills.js +46 -0
- package/dist/packages/cli/src/live/dev-history.js +31 -15
- package/dist/packages/cli/src/live/index.js +28 -90
- package/dist/packages/cli/src/live/screens/newpairs-live.js +1 -1
- package/dist/packages/cli/src/live/screens/token-live.js +48 -15
- package/dist/packages/cli/src/live/screens/watchlist-live.js +1 -1
- package/dist/packages/cli/src/live/trade.js +30 -453
- package/dist/packages/cli/src/live/watchlist.js +1 -1
- package/dist/packages/cli/src/main.js +290 -199
- package/dist/packages/cli/src/mcp-cmd.js +7 -1
- package/dist/packages/cli/src/mcp-follow.js +103 -6
- package/dist/packages/cli/src/plan-cmd.js +15 -5
- package/dist/packages/cli/src/pnl-app.js +31 -21
- package/dist/packages/cli/src/pnl-card.js +158 -21
- package/dist/packages/cli/src/popup.js +38 -6
- package/dist/packages/cli/src/record-cmd.js +141 -20
- package/dist/packages/cli/src/scan/ui.js +7 -1
- package/dist/packages/cli/src/screens/agent-hud.js +773 -0
- package/dist/packages/cli/src/screens/analyst-session.js +11 -4
- package/dist/packages/cli/src/screens/analyst.js +3 -2
- package/dist/packages/cli/src/screens/autotrade-keys.js +169 -0
- package/dist/packages/cli/src/screens/autotrade-proposals.js +123 -0
- package/dist/packages/cli/src/screens/autotrade-screen.js +725 -0
- package/dist/packages/cli/src/screens/autotrade-ui.js +640 -0
- package/dist/packages/cli/src/screens/autotrade-worker.js +89 -0
- package/dist/packages/cli/src/screens/brain.js +634 -0
- package/dist/packages/cli/src/screens/newpairs.js +1 -1
- package/dist/packages/cli/src/screens/overlay.js +5 -2
- package/dist/packages/cli/src/screens/plans.js +5 -17
- package/dist/packages/cli/src/screens/pnl-chip.js +144 -0
- package/dist/packages/cli/src/screens/positions.js +6 -28
- package/dist/packages/cli/src/screens/screener.js +1 -0
- package/dist/packages/cli/src/screens/ticket.js +38 -205
- package/dist/packages/cli/src/screens/token.js +18 -9
- package/dist/packages/cli/src/screens/trending.js +1 -1
- package/dist/packages/cli/src/state.js +88 -18
- package/dist/packages/cli/src/telegram-cmd.js +451 -0
- package/dist/packages/cli/src/trade.js +3 -128
- package/dist/packages/cli/src/version.js +1 -1
- package/dist/packages/cli/src/wallet-cmd.js +39 -340
- package/dist/packages/cli/src/wallet-command.js +8 -11
- package/dist/packages/cli/src/widgets.js +0 -12
- package/dist/packages/custom/src/config.js +20 -2
- package/dist/packages/custom/src/schema.js +41 -10
- package/dist/packages/custom/src/workspace.js +3 -3
- package/dist/packages/data/src/basis.js +191 -0
- package/dist/packages/data/src/detail.js +53 -2
- package/dist/packages/data/src/index.js +2 -14
- package/dist/packages/data/src/larp/score.js +220 -0
- package/dist/packages/data/src/larp/x-account.js +316 -0
- package/dist/packages/data/src/store.js +14 -5
- package/dist/packages/data/src/trade/agent/paper-desk.js +300 -23
- package/dist/packages/data/src/trade/agent/rendezvous.js +3 -2
- package/dist/packages/data/src/trade/bridge.js +4 -102
- package/dist/packages/data/src/trade/ledger/export.js +101 -0
- package/dist/packages/data/src/trade/ledger/file.js +135 -0
- package/dist/packages/data/src/trade/ledger/fold.js +191 -0
- package/dist/packages/data/src/trade/ledger/index.js +9 -0
- package/dist/packages/data/src/trade/ledger/schema.js +83 -0
- package/dist/packages/data/src/trade/ledger/sources.js +307 -0
- package/dist/packages/data/src/trade/ledger/stats.js +123 -0
- package/dist/packages/data/src/trade/ledger/tags.js +116 -0
- package/dist/packages/data/src/trade/ledger/view.js +51 -0
- package/dist/packages/data/src/trade/plans/engine.js +7 -0
- package/dist/packages/data/src/trade/plans/index.js +0 -1
- package/dist/packages/data/src/trade/plans/mandate.js +28 -15
- package/dist/packages/data/src/trade/plans/service.js +4 -28
- package/dist/packages/mcp/assets/kb/app-help/autotrade-chat.md +48 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-parallel.md +42 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-runner.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-stop.md +37 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-strategies.md +35 -0
- package/dist/packages/mcp/assets/kb/app-help/autotrade-telegram.md +40 -0
- package/dist/packages/mcp/assets/kb/app-help/chart-trade-markers.md +42 -0
- package/dist/packages/mcp/assets/kb/app-help/dev-history.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/larp-check.md +5 -1
- package/dist/packages/mcp/assets/kb/app-help/link-a-wallet.md +4 -10
- package/dist/packages/mcp/assets/kb/app-help/live-trading.md +38 -0
- package/dist/packages/mcp/assets/kb/app-help/live-yolo.md +34 -0
- package/dist/packages/mcp/assets/kb/app-help/order-plans.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/price-or-marketcap.md +5 -2
- package/dist/packages/mcp/assets/kb/app-help/scan-mode.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/screen-analyst.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-autotrade.md +217 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-kol.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-newpairs.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-positions.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-screener.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-token.md +6 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-trending.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/screen-watchlist.md +3 -0
- package/dist/packages/mcp/assets/kb/app-help/spectate-an-agent.md +2 -1
- package/dist/packages/mcp/assets/kb/app-help/standing-mandates.md +1 -1
- package/dist/packages/mcp/assets/kb/app-help/trade-ledger.md +54 -0
- package/dist/packages/mcp/assets/kb/app-help/use-local-model.md +38 -0
- package/dist/packages/mcp/assets/kb/docs/docs-agents-agent-paper-trading.md +7 -3
- package/dist/packages/mcp/assets/kb/docs/docs-agents-reading-your-trade-ledger.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-agents-untrusted-by-default.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-if-claude-is-missing.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-starting-a-run.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-use-a-local-model-instead.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-analyst-what-it-sees.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-backtests-and-their-limits.md +56 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-change-a-strategy-by-chat.md +49 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-checking-the-record-of-a-run.md +28 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-comparing-them.md +41 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-exits-and-wakes.md +76 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-how-a-stop-reaches-the-runner.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-on-the-q-screen.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-one-strategy-or-all-of-them.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-paper-accounts.md +50 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-presets.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-running-it-all-day.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-sessions-and-their-limits.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-several-strategies-at-once.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-stopping-it.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-strategies-and-presets.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-autotrade-json-file.md +48 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-heartbeat.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-run-file-and-the-heartbeat.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-runner-and-candidate-sessions.md +45 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-each-session-shows-first.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-costs-claude-usage-per-session.md +58 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-is-paper-self-directed.md +43 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-will-not-do.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-which-account-day-caps-and-the-usage-guard.md +44 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade-your-strategies.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +22 -0
- package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
- package/dist/packages/mcp/assets/kb/docs/docs-beginners.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-markers.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-chart-y-axis.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-environment-variables.md +4 -1
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-files-on-disk.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-configuration-price-display-mode.md +9 -7
- package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-2-the-one-key-mirror-ticket-a-proposal.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-stage-3-capped-auto-mirror-default-off-less.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-copy-trading-what-is-proven-what-is-not.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-data-sources-sources.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-first-run.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started-running-the-tests.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +1 -0
- package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +60 -0
- package/dist/packages/mcp/assets/kb/docs/docs-larp-the-verdict.md +0 -2
- package/dist/packages/mcp/assets/kb/docs/docs-larp-what-it-looks-at.md +4 -1
- package/dist/packages/mcp/assets/kb/docs/docs-larp.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-changed-in-0-1-7.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-stopping.md +28 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-the-short-version.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-using-a-trading-bot-with-dexterm.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does-not-do.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-live-trading.md +23 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-another-machine.md +28 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-autotrade-on-a-local-model.md +29 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-check-your-model-dexterm-ai-probe.md +38 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-how-we-checked.md +27 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-models-without-tool-calling.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-pick-the-engine-globally-or-per-surface.md +43 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-restriction-profiles.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-set-it-up-in-two-minutes.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-troubleshooting.md +35 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-what-runs-on-a-local-model.md +39 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-what-stays-outside-the-model.md +36 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-which-model.md +38 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama-your-own-agent-on-the-local-model.md +28 -0
- package/dist/packages/mcp/assets/kb/docs/docs-ollama.md +28 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-letting-an-agent-paper-trade-experimental.md +3 -4
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-link-a-wallet-read-only.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-order-plans-and-standing-mandates.md +4 -13
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-panic.md +4 -7
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-paper-only-mandates.md +40 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-pnl-cards.md +7 -7
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-safety-model.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-ticket.md +3 -4
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-trade-ledger.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-wallet.md +4 -5
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-trade-moments.md +4 -5
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +4 -5
- package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-buy-and-sell-markers.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-market-cap-axis.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-popup-chart-what-it-shows.md +3 -1
- package/dist/packages/mcp/assets/kb/docs/docs-scan-the-read.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-autotrade-paper.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-screens-dev-history.md +10 -7
- package/dist/packages/mcp/assets/kb/docs/docs-screens-order-plans.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-screens-token-screen.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-follow-over-mcp-in-the-running-terminal.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-limits.md +2 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-agent-hud-spectator-v2.md +58 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-brain-view-and-the-pnl-chip.md +46 -0
- package/dist/packages/mcp/assets/kb/docs/docs-spectator-the-three-surfaces.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-spectator.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-approve-one-tap-entries.md +50 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-control-the-commands.md +43 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-if-something-goes-wrong.md +30 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-notify-what-reaches-your-phone.md +41 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-set-it-up.md +47 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-the-record.md +26 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +33 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-will-not-do.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-telegram.md +23 -0
- package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-common-questions.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-troubleshooting-doctor-lines.md +4 -0
- package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-voice.md +1 -1
- package/dist/packages/mcp/assets/kb/docs/docs-yolo-1-replay-it-on-your-own-history.md +42 -0
- package/dist/packages/mcp/assets/kb/docs/docs-yolo-2-run-it-on-paper.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-yolo-3-watch-it-and-stop-it.md +32 -0
- package/dist/packages/mcp/assets/kb/docs/docs-yolo-the-rules.md +37 -0
- package/dist/packages/mcp/assets/kb/docs/docs-yolo-what-it-is.md +34 -0
- package/dist/packages/mcp/assets/kb/docs/docs-yolo-what-it-will-not-do.md +31 -0
- package/dist/packages/mcp/assets/kb/docs/docs-yolo.md +26 -0
- package/dist/packages/mcp/assets/kb/entries/autotrade.md +38 -0
- package/dist/packages/mcp/src/app-help.js +1 -1
- package/dist/packages/mcp/src/autotrade-tools.js +944 -0
- package/dist/packages/mcp/src/backtest-requests.js +71 -0
- package/dist/packages/mcp/src/engine/config.js +159 -0
- package/dist/packages/mcp/src/engine/index.js +10 -0
- package/dist/packages/mcp/src/engine/loop.js +101 -0
- package/dist/packages/mcp/src/engine/mcp-client.js +123 -0
- package/dist/packages/mcp/src/engine/ollama.js +258 -0
- package/dist/packages/mcp/src/engine/probe.js +110 -0
- package/dist/packages/mcp/src/engine/reason.js +35 -0
- package/dist/packages/mcp/src/engine/restrictions.js +111 -0
- package/dist/packages/mcp/src/index.js +7 -1
- package/dist/packages/mcp/src/ledger-tools.js +346 -0
- package/dist/packages/mcp/src/paper-agent.js +5 -2
- package/dist/packages/mcp/src/server.js +285 -36
- package/dist/packages/mcp/src/spectator-sink.js +85 -0
- package/dist/packages/packs/src/backtest.js +3 -1
- package/dist/packages/packs/src/index.js +1 -0
- package/dist/packages/packs/src/larp-score-backtest.js +119 -0
- package/dist/packages/popup/src/pages/chart-lib.js +169 -35
- package/dist/packages/popup/src/pages/chart.css +18 -0
- package/dist/packages/popup/src/pages/chart.html +6 -1
- package/dist/packages/popup/src/pages/chart.js +136 -10
- package/dist/packages/popup/src/pages/spectator.css +45 -0
- package/dist/packages/popup/src/pages/spectator.html +1 -0
- package/dist/packages/popup/src/pages/spectator.js +69 -0
- package/dist/packages/popup/src/protocol.js +4 -0
- package/dist/packages/popup/src/server.js +9 -1
- package/dist/packages/recorder/src/dev-history.js +7 -3
- package/dist/packages/recorder/src/index.js +2 -0
- package/dist/packages/recorder/src/recorder.js +63 -6
- package/dist/packages/recorder/src/retention.js +100 -0
- package/dist/packages/recorder/src/schema.js +34 -1
- package/dist/packages/recorder/src/ticks.js +237 -0
- package/dist/packages/tui/src/canvas.js +7 -0
- package/dist/packages/tui/src/fmt.js +53 -0
- package/dist/packages/tui/src/index.js +1 -0
- package/dist/packages/tui/src/keymap.js +25 -1
- package/dist/packages/tui/src/markers.js +219 -0
- package/dist/packages/tui/src/widgets.js +158 -6
- package/package.json +2 -1
- package/dist/packages/data/src/trade/caps.js +0 -323
- package/dist/packages/data/src/trade/executor.js +0 -933
- package/dist/packages/data/src/trade/plans/executor-live.js +0 -220
- package/dist/packages/data/src/trade/rpc-exec.js +0 -229
- package/dist/packages/data/src/trade/signer/client.js +0 -197
- package/dist/packages/data/src/trade/signer/fast-wallet.js +0 -154
- package/dist/packages/data/src/trade/signer/host.js +0 -97
- package/dist/packages/data/src/trade/signer/main.js +0 -121
- package/dist/packages/data/src/trade/signer/probe-builder.js +0 -85
- package/dist/packages/data/src/trade/signer/protocol.js +0 -92
- package/dist/packages/data/src/trade/signer/service.js +0 -400
- package/dist/packages/data/src/trade/signer/sol-usd-feed.js +0 -98
- package/dist/packages/data/src/trade/sol-usd.js +0 -86
- package/dist/packages/data/src/trade/swap-build.js +0 -108
- package/dist/packages/data/src/trade/token-delta.js +0 -201
- package/dist/packages/data/vendor/solana-web3/LICENSE +0 -20
- package/dist/packages/data/vendor/solana-web3/NOTICE.md +0 -13
- package/dist/packages/data/vendor/solana-web3/index.iife.min.js +0 -20
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-a-live-ticket.md +0 -42
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-before-you-press-size-vs-caps.md +0 -34
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-devnet-signer-mainnet-token.md +0 -35
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-jupiter-rate-limits.md +0 -33
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-link-a-wallet.md +0 -32
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-live-trading-devnet-first.md +0 -30
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-mainnet.md +0 -31
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-live-fire-path-devnet-the-fast-wallet.md +0 -41
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-the-signer-process.md +0 -34
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-what-the-live-path-keeps.md +0 -37
- package/dist/packages/mcp/assets/kb/docs/docs-paper-trading-your-first-live-trade.md +0 -41
|
@@ -18,11 +18,22 @@
|
|
|
18
18
|
// (c) EVERY CALL IS VALIDATED, JOURNALED AND HALTABLE. Before any state change the desk re-reads the control
|
|
19
19
|
// surface (pause / take-over / stop) and the panic file and re-checks the mandate is alive; a refusal is
|
|
20
20
|
// journaled too, so the spectator view and the audit log see it. The user always wins.
|
|
21
|
+
//
|
|
22
|
+
// task-1506 (AUTOTRADE AT-4, additive): the autotrade position monitor sells through `PaperExitDesk`, a subclass
|
|
23
|
+
// at the end of this file, so the MCP desk's own surface is unchanged.
|
|
24
|
+
// P1 desk-side EXIT PLANS: an entry registers its exit plan (`agent.paper.exit.plan`), and every monitor sell
|
|
25
|
+
// is an `exit()` journaled as `agent.paper.exit.<rule>` (cost-recovery, bag-trail, tp1, stop, …) before
|
|
26
|
+
// its fill; a plan is done (`agent.paper.exit.done`) once the position is empty.
|
|
27
|
+
// P2 a `close-only` control state: sells (exits) still run, even on an expired or revoked mandate, while every
|
|
28
|
+
// buy is refused. A panic still halts everything.
|
|
29
|
+
// P4 a `note` on fills (who, which plan, the exit rule, the agent's own reason), journaled with the fill.
|
|
30
|
+
// None of these raises a cap or adds a buy path: a note and a plan are journal data, an exit is a sell.
|
|
21
31
|
import { systemClock } from "../../clock.js";
|
|
22
32
|
import { mandateAlive, TIERS } from "../plans/mandate.js";
|
|
23
33
|
import { KillSwitch } from "../kill-switch.js";
|
|
24
34
|
import { capsHash } from "../plans/mandate.js";
|
|
25
35
|
import { SOL_MINT, buildQuoteRequest, lamportsToSol, solToLamports, rawToTokens, } from "../../paper.js";
|
|
36
|
+
import { buildFillTag, buildLateTag } from "../ledger/tags.js";
|
|
26
37
|
/** The confirmation tier an agent is pinned to: the weakest, so an agent can never match a human keypress. */
|
|
27
38
|
export const AGENT_TIER = 'hands-free';
|
|
28
39
|
/** A refusal from the desk. `reason` is a stable code; `message` is human text (both are journaled). */
|
|
@@ -34,7 +45,51 @@ export class PaperDeskError extends Error {
|
|
|
34
45
|
this.reason = reason;
|
|
35
46
|
}
|
|
36
47
|
}
|
|
48
|
+
export const DEFAULT_QUOTE_RETRY = { maxAttempts: 4, maxWaitMs: 6_000, maxTotalMs: 25_000 };
|
|
49
|
+
/** A keyless rate limit (a 429, or the data layer's own 429 cool-down / bucket pause), which is worth a bounded re-quote. */
|
|
50
|
+
export function isRateLimitedQuoteError(e) {
|
|
51
|
+
if (!(e instanceof Error))
|
|
52
|
+
return false;
|
|
53
|
+
if (e.name === 'JupiterRateLimitedError')
|
|
54
|
+
return true;
|
|
55
|
+
if (e.name === 'SourceUnavailableError')
|
|
56
|
+
return /jupiter/.test(e.message);
|
|
57
|
+
return e.name === 'HttpError' && e.status === 429;
|
|
58
|
+
}
|
|
59
|
+
/** task-1530: most `tag` calls per minute (a journal-only call, but still bounded). */
|
|
60
|
+
export const TAG_RATE_PER_MIN = 30;
|
|
61
|
+
const SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9:_.-]{0,79}$/;
|
|
37
62
|
const isFiniteNum = (x) => typeof x === 'number' && Number.isFinite(x);
|
|
63
|
+
const NOTE_KEY = /^[A-Za-z][A-Za-z0-9_.-]{0,39}$/;
|
|
64
|
+
export function sanitizeFillNote(note) {
|
|
65
|
+
if (typeof note === 'string') {
|
|
66
|
+
const s = note.slice(0, 400);
|
|
67
|
+
return s ? s : undefined;
|
|
68
|
+
}
|
|
69
|
+
if (!note || typeof note !== 'object' || Array.isArray(note))
|
|
70
|
+
return undefined;
|
|
71
|
+
const out = {};
|
|
72
|
+
let n = 0;
|
|
73
|
+
for (const [k, v] of Object.entries(note)) {
|
|
74
|
+
if (n >= 16)
|
|
75
|
+
break;
|
|
76
|
+
if (!NOTE_KEY.test(k))
|
|
77
|
+
continue;
|
|
78
|
+
if (typeof v === 'string')
|
|
79
|
+
out[k] = v.slice(0, 200);
|
|
80
|
+
else if (isFiniteNum(v) || typeof v === 'boolean' || v === null)
|
|
81
|
+
out[k] = v;
|
|
82
|
+
else
|
|
83
|
+
continue;
|
|
84
|
+
n++;
|
|
85
|
+
}
|
|
86
|
+
return n ? out : undefined;
|
|
87
|
+
}
|
|
88
|
+
/** Exit rule → journal slug (`time_review:no-answer` → `time-review-no-answer`). `plan` and `done` are reserved. */
|
|
89
|
+
export function exitRuleSlug(rule) {
|
|
90
|
+
const s = String(rule).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40);
|
|
91
|
+
return s && s !== 'plan' && s !== 'done' ? s : null;
|
|
92
|
+
}
|
|
38
93
|
/**
|
|
39
94
|
* The paper agent desk. One instance per armed mandate + paper wallet. All four verbs (open / size / confirm /
|
|
40
95
|
* close) and both reads (positions / pnl) run here; nothing else is exposed, and there is no cap setter.
|
|
@@ -52,6 +107,10 @@ export class PaperAgentDesk {
|
|
|
52
107
|
kill;
|
|
53
108
|
clock;
|
|
54
109
|
agent;
|
|
110
|
+
quoteRetry;
|
|
111
|
+
onQuoteRetry;
|
|
112
|
+
sessionSrc;
|
|
113
|
+
tagTimes = [];
|
|
55
114
|
tickets = new Map();
|
|
56
115
|
ticketSeq = 0;
|
|
57
116
|
constructor(deps) {
|
|
@@ -71,6 +130,10 @@ export class PaperAgentDesk {
|
|
|
71
130
|
this.kill = deps.killSwitch ?? new KillSwitch();
|
|
72
131
|
this.clock = deps.clock ?? systemClock;
|
|
73
132
|
this.agent = deps.agent ?? 'mcp-agent';
|
|
133
|
+
const r = deps.quoteRetry ?? DEFAULT_QUOTE_RETRY;
|
|
134
|
+
this.quoteRetry = { maxAttempts: Math.max(1, Math.floor(r.maxAttempts)), maxWaitMs: Math.max(0, r.maxWaitMs), maxTotalMs: Math.max(0, r.maxTotalMs) };
|
|
135
|
+
this.onQuoteRetry = deps.onQuoteRetry ?? null;
|
|
136
|
+
this.sessionSrc = deps.sessionId ?? (() => null);
|
|
74
137
|
}
|
|
75
138
|
/** The agent's effective per-trade cap: the mandate cap times the weakest-tier multiplier. */
|
|
76
139
|
perTradeCapUsd() {
|
|
@@ -92,15 +155,18 @@ export class PaperAgentDesk {
|
|
|
92
155
|
* The gate every mutating call runs first: panic file → control surface → mandate alive. A failure here is
|
|
93
156
|
* journaled and throws, so a paused / taken-over / stopped session (or a dead mandate) halts the agent.
|
|
94
157
|
*/
|
|
95
|
-
gate(stage, extra = {}) {
|
|
158
|
+
gate(stage, extra = {}, side) {
|
|
96
159
|
const panic = this.kill.engaged();
|
|
97
160
|
if (panic)
|
|
98
161
|
this.refuse(stage, 'panic', `trading is halted (panic): ${panic.reason}`, extra);
|
|
99
162
|
const c = this.control();
|
|
100
|
-
|
|
163
|
+
// close-only (task-1506 P2): a sell passes the control surface and a dead mandate; a buy never does.
|
|
164
|
+
const exitOnly = c.state === 'close-only' && side === 'sell';
|
|
165
|
+
if (c.state !== 'running' && !exitOnly) {
|
|
101
166
|
const msg = c.state === 'takeover' ? 'the user has taken over — agent paper trading is paused'
|
|
102
167
|
: c.state === 'paused' ? `agent paper trading is paused${c.reason ? `: ${c.reason}` : ''}`
|
|
103
|
-
: `agent paper trading is
|
|
168
|
+
: c.state === 'close-only' ? `agent paper trading is close-only${c.reason ? `: ${c.reason}` : ''}; exits run, new buys are refused`
|
|
169
|
+
: `agent paper trading is stopped${c.reason ? `: ${c.reason}` : ''}`;
|
|
104
170
|
this.refuse(stage, c.state, msg, extra);
|
|
105
171
|
}
|
|
106
172
|
const m = this.mandateSrc();
|
|
@@ -108,7 +174,7 @@ export class PaperAgentDesk {
|
|
|
108
174
|
this.refuse(stage, 'no-mandate', 'no armed mandate is available for agent paper trading', extra);
|
|
109
175
|
this.mandate = m;
|
|
110
176
|
const alive = mandateAlive(this.mandate, this.clock.now());
|
|
111
|
-
if (!alive.ok)
|
|
177
|
+
if (!alive.ok && !exitOnly)
|
|
112
178
|
this.refuse(stage, alive.reason, `the mandate is not usable (${alive.reason})`, extra);
|
|
113
179
|
}
|
|
114
180
|
requireAction(stage, side, extra = {}) {
|
|
@@ -172,7 +238,7 @@ export class PaperAgentDesk {
|
|
|
172
238
|
/** Open an inert PAPER ticket for a mint + side. Validates the gate, the action and the allow-list only. */
|
|
173
239
|
open(input) {
|
|
174
240
|
const side = input.side;
|
|
175
|
-
this.gate('open', { mint: input.mint, side });
|
|
241
|
+
this.gate('open', { mint: input.mint, side }, side);
|
|
176
242
|
if (typeof input.mint !== 'string' || input.mint.length < 32)
|
|
177
243
|
this.refuse('open', 'bad-request', 'a base58 mint is required');
|
|
178
244
|
if (!isFiniteNum(input.decimals) || input.decimals < 0 || input.decimals > 18)
|
|
@@ -192,7 +258,7 @@ export class PaperAgentDesk {
|
|
|
192
258
|
const t = this.tickets.get(ticketId);
|
|
193
259
|
if (!t)
|
|
194
260
|
this.refuse('size', 'unknown-ticket', 'no such open ticket');
|
|
195
|
-
this.gate('size', { ticketId });
|
|
261
|
+
this.gate('size', { ticketId }, t.side);
|
|
196
262
|
if (t.side === 'buy') {
|
|
197
263
|
const usd = input.usd;
|
|
198
264
|
if (!isFiniteNum(usd) || usd <= 0)
|
|
@@ -213,21 +279,22 @@ export class PaperAgentDesk {
|
|
|
213
279
|
return { ...t };
|
|
214
280
|
}
|
|
215
281
|
/** Confirm a sized ticket: re-check the gate and the caps, fetch a FRESH quote, book the PAPER fill. */
|
|
216
|
-
async confirm(ticketId) {
|
|
282
|
+
async confirm(ticketId, opts = {}) {
|
|
217
283
|
const t = this.tickets.get(ticketId);
|
|
218
284
|
if (!t)
|
|
219
285
|
this.refuse('confirm', 'unknown-ticket', 'no such open ticket');
|
|
220
|
-
this.gate('confirm', { ticketId });
|
|
286
|
+
this.gate('confirm', { ticketId }, t.side);
|
|
221
287
|
this.requireAction('confirm', t.side, { ticketId });
|
|
222
288
|
this.requireMintAllowed('confirm', t.mint, { ticketId });
|
|
223
289
|
const order = await this.buildOrder('confirm', t);
|
|
224
|
-
const
|
|
290
|
+
const tag = buildFillTag(t.side, opts.tag);
|
|
291
|
+
const { rec, quoteAttempts } = await this.book('confirm', order, { ticketId }, sanitizeFillNote(opts.note), tag);
|
|
225
292
|
this.tickets.delete(ticketId);
|
|
226
|
-
return { fill: rec, position: this.positionView(this.wallet.position(t.mint)) };
|
|
293
|
+
return { fill: rec, position: this.positionView(this.wallet.position(t.mint)), quoteAttempts, tag };
|
|
227
294
|
}
|
|
228
295
|
/** Close a fraction of a held PAPER position in one call (open sell → size fraction → confirm). */
|
|
229
296
|
async close(input) {
|
|
230
|
-
this.gate('close', { mint: input.mint });
|
|
297
|
+
this.gate('close', { mint: input.mint }, 'sell');
|
|
231
298
|
this.requireAction('close', 'sell', { mint: input.mint });
|
|
232
299
|
this.requireMintAllowed('close', input.mint, {});
|
|
233
300
|
const pos = this.wallet.position(input.mint);
|
|
@@ -239,8 +306,9 @@ export class PaperAgentDesk {
|
|
|
239
306
|
const t = { id: `pt_close_${++this.ticketSeq}`, mint: input.mint, symbol: pos.symbol, decimals: pos.decimals, side: 'sell', usd: null, fraction, openedAt: this.clock.now() };
|
|
240
307
|
this.note('agent.paper.close', { mint: input.mint, fraction });
|
|
241
308
|
const order = await this.buildOrder('close', t);
|
|
242
|
-
const
|
|
243
|
-
|
|
309
|
+
const tag = buildFillTag('sell', input.tag);
|
|
310
|
+
const { rec, quoteAttempts } = await this.book('close', order, { mint: input.mint }, sanitizeFillNote(input.note), tag);
|
|
311
|
+
return { fill: rec, position: this.positionView(this.wallet.position(input.mint)), quoteAttempts, tag };
|
|
244
312
|
}
|
|
245
313
|
/** Build the paper order from a ticket, re-checking the buy caps against the live wallet at execute time. */
|
|
246
314
|
async buildOrder(stage, t) {
|
|
@@ -265,22 +333,73 @@ export class PaperAgentDesk {
|
|
|
265
333
|
this.refuse(stage, 'bad-request', 'the sell fraction rounds to zero tokens', { ticketId: t.id });
|
|
266
334
|
return { mint: t.mint, symbol: pos.symbol, side: 'sell', decimals: pos.decimals, amountRaw, slippageBps: this.mandate.limits.maxSlippageBps };
|
|
267
335
|
}
|
|
268
|
-
/**
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
336
|
+
/**
|
|
337
|
+
* Fetch a FRESH quote for this call. A short rate limit is re-quoted inside the bounded `quoteRetry` budget:
|
|
338
|
+
* each retry is journaled and reported first, waits min(stated wait, maxWaitMs), then re-runs the gate, so a
|
|
339
|
+
* pause during the wait still halts the agent. Any other error, the last allowed attempt, or the spent budget
|
|
340
|
+
* (an attempt still in flight is aborted) refuses with the verbatim message. No quote outlives the call.
|
|
341
|
+
*/
|
|
342
|
+
async freshQuote(stage, order, extra) {
|
|
343
|
+
const { maxAttempts, maxWaitMs, maxTotalMs } = this.quoteRetry;
|
|
344
|
+
const started = this.clock.now();
|
|
345
|
+
let lastRateLimit = null;
|
|
346
|
+
for (let attempt = 1;; attempt++) {
|
|
347
|
+
const budget = new AbortController();
|
|
348
|
+
const disarm = new AbortController();
|
|
349
|
+
void this.clock.sleep(Math.max(1, maxTotalMs - (this.clock.now() - started)), disarm.signal)
|
|
350
|
+
.then(() => { if (!disarm.signal.aborted)
|
|
351
|
+
budget.abort(); });
|
|
352
|
+
let quote = null;
|
|
353
|
+
let failure = null;
|
|
354
|
+
try {
|
|
355
|
+
quote = await this.quote(buildQuoteRequest(order), budget.signal);
|
|
356
|
+
}
|
|
357
|
+
catch (e) {
|
|
358
|
+
failure = e;
|
|
359
|
+
}
|
|
360
|
+
finally {
|
|
361
|
+
disarm.abort();
|
|
362
|
+
}
|
|
363
|
+
if (quote && !budget.signal.aborted)
|
|
364
|
+
return { quote, attempts: attempt };
|
|
365
|
+
const msg = failure instanceof Error ? failure.message : failure === null ? 'no quote' : String(failure);
|
|
366
|
+
const elapsed = this.clock.now() - started;
|
|
367
|
+
const tried = attempt > 1 ? ` (${attempt} quote attempts in ${Math.round(elapsed / 1000)}s)` : '';
|
|
368
|
+
if (budget.signal.aborted) {
|
|
369
|
+
this.refuse(stage, 'quote-failed', `could not price the trade within ${Math.round(maxTotalMs / 1000)}s${lastRateLimit ? `: ${lastRateLimit}` : ''}${tried}`, { ...extra, quoteAttempts: attempt });
|
|
370
|
+
}
|
|
371
|
+
if (!isRateLimitedQuoteError(failure))
|
|
372
|
+
this.refuse(stage, 'quote-failed', `could not price the trade: ${msg}${tried}`, { ...extra, quoteAttempts: attempt });
|
|
373
|
+
lastRateLimit = msg;
|
|
374
|
+
const stated = failure.retryAfterMs;
|
|
375
|
+
const wait = Math.min(maxWaitMs, Math.max(1_000, typeof stated === 'number' && Number.isFinite(stated) ? stated : 2_000 * attempt));
|
|
376
|
+
if (attempt >= maxAttempts || elapsed + wait >= maxTotalMs) {
|
|
377
|
+
this.refuse(stage, 'quote-failed', `could not price the trade: ${msg}${tried}`, { ...extra, quoteAttempts: attempt });
|
|
378
|
+
}
|
|
379
|
+
const info = { stage, mint: order.mint, side: order.side, attempt: attempt + 1, maxAttempts, waitMs: wait, message: msg };
|
|
380
|
+
this.note('agent.paper.quote-retry', { ...extra, ...info });
|
|
381
|
+
try {
|
|
382
|
+
this.onQuoteRetry?.(info);
|
|
383
|
+
}
|
|
384
|
+
catch { /* the spectator is best-effort */ }
|
|
385
|
+
await this.clock.sleep(wait);
|
|
386
|
+
this.gate(stage, extra, order.side);
|
|
276
387
|
}
|
|
388
|
+
}
|
|
389
|
+
/** Fetch a fresh quote and book the fill through the PAPER executor; journal the booked fill. */
|
|
390
|
+
async book(stage, order, extra, note, tag) {
|
|
391
|
+
const { quote, attempts } = await this.freshQuote(stage, order, extra);
|
|
392
|
+
if (attempts > 1)
|
|
393
|
+
extra = { ...extra, quoteAttempts: attempts };
|
|
277
394
|
// Buys honour the mandate's price-impact ceiling (SwapQuote.priceImpactPct is already a PERCENT — the source
|
|
278
395
|
// converts Jupiter's 0..1 fraction once). Exits are never trapped by it, like the plan engine's stop leg.
|
|
279
396
|
if (order.side === 'buy' && isFiniteNum(quote.priceImpactPct) && quote.priceImpactPct > this.mandate.limits.maxPriceImpactPct + 1e-9) {
|
|
280
397
|
this.refuse(stage, 'price-impact', `price impact ${quote.priceImpactPct.toFixed(2)}% exceeds the mandate max ${this.mandate.limits.maxPriceImpactPct}%`, extra);
|
|
281
398
|
}
|
|
282
399
|
// The control surface may have flipped while the quote was in flight: re-check right before booking.
|
|
283
|
-
this.gate(stage, extra);
|
|
400
|
+
this.gate(stage, extra, order.side);
|
|
401
|
+
// task-1530: the book (cash + open cost) just before this fill, for the trade ledger's % of book
|
|
402
|
+
const bookLamports = this.bookLamports();
|
|
284
403
|
let rec;
|
|
285
404
|
try {
|
|
286
405
|
rec = await this.executor.execute(order, quote);
|
|
@@ -292,8 +411,62 @@ export class PaperAgentDesk {
|
|
|
292
411
|
...extra, mint: rec.mint, symbol: rec.symbol, side: rec.side, paper: true,
|
|
293
412
|
solDelta: rec.solDelta.toString(), tokenDelta: rec.tokenDelta.toString(),
|
|
294
413
|
priceSol: rec.priceSol, priceUsd: rec.priceUsd, realizedLamports: rec.realizedLamports.toString(), fillId: rec.id,
|
|
414
|
+
...(note !== undefined ? { note } : {}),
|
|
415
|
+
// task-1530: the facts the unified trade ledger needs, journaled with the fill (older journals lack them; the
|
|
416
|
+
// ledger then joins them from the paper wallet's own table)
|
|
417
|
+
decimals: rec.decimals, feeLamports: rec.feeLamports.toString(), impactPct: rec.priceImpactPct, slippageBps: rec.slippageBps,
|
|
418
|
+
...(bookLamports !== null ? { bookLamports: bookLamports.toString() } : {}),
|
|
419
|
+
...(tag ? { tag } : {}),
|
|
420
|
+
...(this.sessionId() ? { sessionId: this.sessionId() } : {}),
|
|
295
421
|
});
|
|
296
|
-
return rec;
|
|
422
|
+
return { rec: rec, quoteAttempts: attempts };
|
|
423
|
+
}
|
|
424
|
+
/** The model session id the launcher named, if it is a plain id. */
|
|
425
|
+
sessionId() {
|
|
426
|
+
let s = null;
|
|
427
|
+
try {
|
|
428
|
+
s = this.sessionSrc();
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
s = null;
|
|
432
|
+
}
|
|
433
|
+
return typeof s === 'string' && SESSION_ID.test(s) ? s : null;
|
|
434
|
+
}
|
|
435
|
+
/** Paper cash + the cost basis of every open position, lamports (null when the wallet cannot be read). */
|
|
436
|
+
bookLamports() {
|
|
437
|
+
try {
|
|
438
|
+
return this.wallet.balance() + this.wallet.positions().reduce((a, p) => a + (p.tokensRaw > 0n ? p.costLamports : 0n), 0n);
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* task-1530: declare a PROFILE, REPROFILE or EXIT for a coin this desk traded, apart from its fill (a REPROFILE,
|
|
446
|
+
* or a line that was not passed with the fill: journaled `agent.paper.tag`, which the trade ledger reads as a
|
|
447
|
+
* late declaration). Journal-only: it books nothing, moves no money and works while trading is paused or stopped
|
|
448
|
+
* (a reason can always be recorded). The coin must be one the paper wallet holds or has held.
|
|
449
|
+
*/
|
|
450
|
+
tag(input) {
|
|
451
|
+
const now = this.clock.now();
|
|
452
|
+
this.tagTimes = this.tagTimes.filter((t) => now - t < 60_000);
|
|
453
|
+
if (this.tagTimes.length >= TAG_RATE_PER_MIN)
|
|
454
|
+
throw new PaperDeskError('rate-limited', `at most ${TAG_RATE_PER_MIN} tags a minute`);
|
|
455
|
+
if (typeof input.mint !== 'string' || !this.wallet.position(input.mint))
|
|
456
|
+
throw new PaperDeskError('no-position', 'tag a coin the paper wallet holds or has traded (pass its mint)');
|
|
457
|
+
const g = buildLateTag(input);
|
|
458
|
+
if (!g.ok)
|
|
459
|
+
throw new PaperDeskError('bad-request', g.why);
|
|
460
|
+
if (g.tagKind === 'reprofile') {
|
|
461
|
+
const pos = this.wallet.position(input.mint);
|
|
462
|
+
if (!pos || pos.tokensRaw <= 0n)
|
|
463
|
+
throw new PaperDeskError('no-position', 'a REPROFILE needs a position that is still held');
|
|
464
|
+
}
|
|
465
|
+
this.tagTimes.push(now);
|
|
466
|
+
const declared = g.tagKind === 'profile' ? { profile: g.profile, reason: g.reason }
|
|
467
|
+
: g.tagKind === 'reprofile' ? { from: g.from, to: g.to, reason: g.reason } : { exitReason: g.exitReason, reason: g.reason };
|
|
468
|
+
this.note('agent.paper.tag', { mint: input.mint, tagKind: g.tagKind, ...declared, ...(g.line ? { line: g.line } : {}), ...(this.sessionId() ? { sessionId: this.sessionId() } : {}) });
|
|
469
|
+
return { recorded: true, mint: input.mint, tagKind: g.tagKind, declared };
|
|
297
470
|
}
|
|
298
471
|
// ───────────────────────────── the two reads ─────────────────────────────
|
|
299
472
|
positionView(pos) {
|
|
@@ -333,4 +506,108 @@ export class PaperAgentDesk {
|
|
|
333
506
|
};
|
|
334
507
|
}
|
|
335
508
|
}
|
|
509
|
+
/**
|
|
510
|
+
* task-1506 P1: the paper desk the autotrade monitor sells through. It is the same desk (every gate, cap, quote,
|
|
511
|
+
* journal and the paper executor), plus exit plans and rule-named exits. It is a separate class on purpose: the
|
|
512
|
+
* MCP paper tools hold a plain PaperAgentDesk, whose surface stays exactly the six reviewed verbs.
|
|
513
|
+
*/
|
|
514
|
+
export class PaperExitDesk extends PaperAgentDesk {
|
|
515
|
+
/** Exit plans by mint (one held position per mint in the paper wallet). */
|
|
516
|
+
plans = new Map();
|
|
517
|
+
async confirm(ticketId, opts = {}) {
|
|
518
|
+
const r = await super.confirm(ticketId, opts);
|
|
519
|
+
if (r.fill.side === 'sell')
|
|
520
|
+
this.planDoneIfEmpty(r.fill.mint, 'confirm');
|
|
521
|
+
return r;
|
|
522
|
+
}
|
|
523
|
+
async close(input) {
|
|
524
|
+
const r = await super.close(input);
|
|
525
|
+
this.planDoneIfEmpty(input.mint, 'close');
|
|
526
|
+
return r;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Register the exit plan for a held position (P1). Journal data only: it needs a held paper position and moves
|
|
530
|
+
* no money, so it runs in every control state (a restart re-registers its plans, journaled `restored: true`).
|
|
531
|
+
* Registering the same position again returns the existing plan.
|
|
532
|
+
*/
|
|
533
|
+
registerExitPlan(input) {
|
|
534
|
+
const pos = this.wallet.position(input.mint);
|
|
535
|
+
if (!pos || pos.tokensRaw <= 0n)
|
|
536
|
+
this.refuse('exit-plan', 'no-position', 'no open paper position in this mint to plan exits for', { mint: input.mint });
|
|
537
|
+
const ok = (v, n) => typeof v === 'string' && v.length > 0 && v.length <= n;
|
|
538
|
+
if (!ok(input.owner, 40) || !ok(input.positionId, 80) || !ok(input.profileId, 64))
|
|
539
|
+
this.refuse('exit-plan', 'bad-request', 'an exit plan needs an owner, a position id and a profile id', { mint: input.mint });
|
|
540
|
+
const have = this.plans.get(input.mint);
|
|
541
|
+
if (have && have.positionId === input.positionId)
|
|
542
|
+
return { ...have };
|
|
543
|
+
const note = sanitizeFillNote(input.note);
|
|
544
|
+
const plan = {
|
|
545
|
+
planId: `xp_${++this.ticketSeq}_${this.clock.now().toString(36)}`, mint: input.mint, symbol: pos.symbol, owner: input.owner,
|
|
546
|
+
positionId: input.positionId, profileId: input.profileId, runId: input.runId ?? null, registeredAt: this.clock.now(), ...(note ? { note } : {}),
|
|
547
|
+
};
|
|
548
|
+
this.plans.set(input.mint, plan);
|
|
549
|
+
this.note('agent.paper.exit.plan', { ...plan, ...(input.restored === true ? { restored: true } : {}) });
|
|
550
|
+
return { ...plan };
|
|
551
|
+
}
|
|
552
|
+
/** The registered exit plans (read-only copies). */
|
|
553
|
+
exitPlans() { return [...this.plans.values()].map((p) => ({ ...p })); }
|
|
554
|
+
/**
|
|
555
|
+
* Sell a fraction of a planned position as one exit rule (P1): journaled `agent.paper.exit.<rule>` before the
|
|
556
|
+
* fill, the fill carries the note, and the plan is done once the position is empty. Runs in `close-only`.
|
|
557
|
+
*/
|
|
558
|
+
async exit(input) {
|
|
559
|
+
const rule = exitRuleSlug(input.rule);
|
|
560
|
+
this.gate('exit', { mint: input.mint }, 'sell');
|
|
561
|
+
if (!rule)
|
|
562
|
+
this.refuse('exit', 'bad-request', 'an exit needs a rule name', { mint: input.mint });
|
|
563
|
+
const plan = this.plans.get(input.mint);
|
|
564
|
+
if (!plan)
|
|
565
|
+
this.refuse('exit', 'no-exit-plan', 'no exit plan is registered for this mint', { mint: input.mint, rule });
|
|
566
|
+
this.requireAction('exit', 'sell', { mint: input.mint });
|
|
567
|
+
this.requireMintAllowed('exit', input.mint, {});
|
|
568
|
+
const pos = this.wallet.position(input.mint);
|
|
569
|
+
if (!pos || pos.tokensRaw <= 0n)
|
|
570
|
+
this.refuse('exit', 'no-position', 'no open paper position in this mint to exit', { mint: input.mint, rule });
|
|
571
|
+
const fraction = input.fraction ?? 1;
|
|
572
|
+
if (!isFiniteNum(fraction) || !(fraction > 0 && fraction <= 1))
|
|
573
|
+
this.refuse('exit', 'bad-request', 'fraction must be in (0, 1]', { mint: input.mint, rule });
|
|
574
|
+
const t = { id: `pt_exit_${++this.ticketSeq}`, mint: input.mint, symbol: pos.symbol, decimals: pos.decimals, side: 'sell', usd: null, fraction, openedAt: this.clock.now() };
|
|
575
|
+
this.note(`agent.paper.exit.${rule}`, { planId: plan.planId, positionId: plan.positionId, mint: input.mint, fraction, ...(input.detail ? { detail: String(input.detail).slice(0, 200) } : {}) });
|
|
576
|
+
const order = await this.buildOrder('exit', t);
|
|
577
|
+
const base = sanitizeFillNote(input.note);
|
|
578
|
+
const note = { ...(typeof base === 'object' ? base : base ? { text: base } : {}), exit: rule, planId: plan.planId, positionId: plan.positionId };
|
|
579
|
+
const { rec, quoteAttempts } = await this.book('exit', order, { mint: input.mint, rule }, sanitizeFillNote(note));
|
|
580
|
+
this.planDoneIfEmpty(input.mint, rule);
|
|
581
|
+
return { fill: rec, position: this.positionView(this.wallet.position(input.mint)), quoteAttempts, plan: { ...plan }, rule: rule };
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* A read-only sell QUOTE for a fraction of a held position (what an exit would net, after the simulated fee),
|
|
585
|
+
* for sizing a cost-recovery slice (task-1506). It books nothing and journals only a refusal.
|
|
586
|
+
*/
|
|
587
|
+
async previewSell(input) {
|
|
588
|
+
this.gate('preview', { mint: input.mint }, 'sell');
|
|
589
|
+
const pos = this.wallet.position(input.mint);
|
|
590
|
+
if (!pos || pos.tokensRaw <= 0n)
|
|
591
|
+
this.refuse('preview', 'no-position', 'no open paper position in this mint', { mint: input.mint });
|
|
592
|
+
const fraction = input.fraction;
|
|
593
|
+
if (!isFiniteNum(fraction) || !(fraction > 0 && fraction <= 1))
|
|
594
|
+
this.refuse('preview', 'bad-request', 'fraction must be in (0, 1]', { mint: input.mint });
|
|
595
|
+
const t = { id: `pt_preview_${++this.ticketSeq}`, mint: input.mint, symbol: pos.symbol, decimals: pos.decimals, side: 'sell', usd: null, fraction, openedAt: this.clock.now() };
|
|
596
|
+
const order = await this.buildOrder('preview', t);
|
|
597
|
+
const { quote } = await this.freshQuote('preview', order, { mint: input.mint });
|
|
598
|
+
const f = this.executor.previewFill(order, quote);
|
|
599
|
+
return { amountRaw: order.amountRaw, solOut: lamportsToSol(f.solDelta > 0n ? f.solDelta : 0n), priceImpactPct: f.priceImpactPct };
|
|
600
|
+
}
|
|
601
|
+
/** Once a planned position is empty its plan is done (journaled once). */
|
|
602
|
+
planDoneIfEmpty(mint, by) {
|
|
603
|
+
const plan = this.plans.get(mint);
|
|
604
|
+
if (!plan)
|
|
605
|
+
return;
|
|
606
|
+
const pos = this.wallet.position(mint);
|
|
607
|
+
if (pos && pos.tokensRaw > 0n)
|
|
608
|
+
return;
|
|
609
|
+
this.plans.delete(mint);
|
|
610
|
+
this.note('agent.paper.exit.done', { planId: plan.planId, positionId: plan.positionId, mint, by });
|
|
611
|
+
}
|
|
612
|
+
}
|
|
336
613
|
export { SOL_MINT };
|
|
@@ -54,8 +54,9 @@ export function readPaperAgentRendezvous(env = process.env) {
|
|
|
54
54
|
return null;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
-
const VALID_STATES = ['running', 'paused', 'stopped', 'takeover'];
|
|
58
|
-
/** Write the live control state (0600). The app calls this on pause / take-over / stop / resume
|
|
57
|
+
const VALID_STATES = ['running', 'paused', 'stopped', 'takeover', 'close-only'];
|
|
58
|
+
/** Write the live control state (0600). The app calls this on pause / take-over / stop / resume, and autotrade's
|
|
59
|
+
* soft stop writes `close-only` (task-1506 P2: exits keep running, buys are refused). */
|
|
59
60
|
export function writeDeskControl(control, env = process.env) {
|
|
60
61
|
const p = paperAgentControlPath(env);
|
|
61
62
|
mkdirSync(dirname(p), { recursive: true, mode: 0o700 });
|
|
@@ -7,9 +7,7 @@
|
|
|
7
7
|
// Security posture (mirrors research/popup-host/server/server.mjs §5):
|
|
8
8
|
// * listens on 127.0.0.1 only; never 0.0.0.0 / ::. task-1368 (g): on a FIXED preferred port
|
|
9
9
|
// (`WALLET_BRIDGE_PORT`, random only when it is busy) — Phantom remembers trust PER ORIGIN, and a
|
|
10
|
-
// random port made every page a brand-new origin
|
|
11
|
-
// origin Phantom had never connected and got 4100 ("not authorized"). The sign page now connects
|
|
12
|
-
// first and checks the account; the stable origin lets that connect resolve silently next time.
|
|
10
|
+
// random port made every page a brand-new origin, so Phantom asked to connect every time.
|
|
13
11
|
// * a single-use, random 32-byte TOKEN in the URL; a second use is refused (replay-proof).
|
|
14
12
|
// * strict Host check on every request; strict Origin + JSON content-type + 16 KB body cap on POST.
|
|
15
13
|
// * remote address must be loopback; anything else gets an empty 404.
|
|
@@ -23,32 +21,13 @@
|
|
|
23
21
|
import http from 'node:http';
|
|
24
22
|
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
25
23
|
import { spawn } from 'node:child_process';
|
|
26
|
-
import { mkdtempSync, writeFileSync, rmSync
|
|
24
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
27
25
|
import { tmpdir } from 'node:os';
|
|
28
26
|
import { join } from 'node:path';
|
|
29
27
|
import { systemClock } from "../clock.js";
|
|
30
28
|
import { base58 } from "../sources/solana-rpc.js";
|
|
31
29
|
import { verifyEd25519 } from "./tx-decode.js";
|
|
32
30
|
const TOKEN_TTL_MS = 120_000;
|
|
33
|
-
/**
|
|
34
|
-
* task-1328: the tx-sign page needs @solana/web3.js in the BROWSER (Phantom's signTransaction takes a
|
|
35
|
-
* VersionedTransaction object). The vendored IIFE bundle (packages/data/vendor/solana-web3, MIT,
|
|
36
|
-
* sha256-pinned by the tests) is inlined into the page under the CSP nonce — no CDN, no remote script.
|
|
37
|
-
* Read lazily and cached; a missing file leaves the page in its "needs the shell" state (closed).
|
|
38
|
-
*/
|
|
39
|
-
export const SOLANA_WEB3_VENDOR = new URL('../../vendor/solana-web3/index.iife.min.js', import.meta.url);
|
|
40
|
-
let web3Bundle;
|
|
41
|
-
export function solanaWeb3Bundle() {
|
|
42
|
-
if (web3Bundle !== undefined)
|
|
43
|
-
return web3Bundle;
|
|
44
|
-
try {
|
|
45
|
-
web3Bundle = readFileSync(SOLANA_WEB3_VENDOR, 'utf8');
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
web3Bundle = null;
|
|
49
|
-
}
|
|
50
|
-
return web3Bundle;
|
|
51
|
-
}
|
|
52
31
|
const MAX_BODY = 16 * 1024;
|
|
53
32
|
const LOOPBACK = new Set(['127.0.0.1', '::ffff:127.0.0.1', '::1']);
|
|
54
33
|
function spawnOpener(target) {
|
|
@@ -93,7 +72,7 @@ export const systemBrowserOpener = (url) => {
|
|
|
93
72
|
};
|
|
94
73
|
/**
|
|
95
74
|
* The wallet bridge's preferred loopback port: `http://127.0.0.1:41291` is the ORIGIN Phantom stores
|
|
96
|
-
* its trust for
|
|
75
|
+
* its trust for. Unassigned by IANA; the X login callback is 41287.
|
|
97
76
|
*/
|
|
98
77
|
export const WALLET_BRIDGE_PORT = 41291;
|
|
99
78
|
function b64url(buf) { return buf.toString('base64url'); }
|
|
@@ -223,7 +202,7 @@ function oneShot(args) {
|
|
|
223
202
|
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store', 'x-content-type-options': 'nosniff' });
|
|
224
203
|
res.end(JSON.stringify({ ok: out.error === undefined, error: out.error ?? null }));
|
|
225
204
|
// Resolve (and close every connection) only once the page has its answer: `finish` now tears
|
|
226
|
-
// the socket down, and the page's "✓
|
|
205
|
+
// the socket down, and the page's "✓ linked" line reads this response.
|
|
227
206
|
if (out.error === undefined) {
|
|
228
207
|
const done = () => finish(() => resolve(out.value));
|
|
229
208
|
res.once('finish', done);
|
|
@@ -350,38 +329,6 @@ export class WalletBridge {
|
|
|
350
329
|
},
|
|
351
330
|
});
|
|
352
331
|
}
|
|
353
|
-
/**
|
|
354
|
-
* Hand an unsigned transaction to the wallet for signing and resolve with the signed bytes. dexterm
|
|
355
|
-
* never holds a key; the browser approval IS the physical confirmation (SPEC §6.5). The unsigned tx
|
|
356
|
-
* is the plan the user already confirmed; a popup-host lane can swap the page for a chromeless window.
|
|
357
|
-
*/
|
|
358
|
-
signTransaction(args) {
|
|
359
|
-
const started = this.clock.now();
|
|
360
|
-
return oneShot({
|
|
361
|
-
route: 'sign',
|
|
362
|
-
open: this.open,
|
|
363
|
-
timeoutMs: this.timeoutMs,
|
|
364
|
-
clock: this.clock,
|
|
365
|
-
preferredPort: this.port,
|
|
366
|
-
page: ({ port, nonce: cspNonce, portNote }) => signPage(port, cspNonce, args.unsignedTxBase64, args.expectPublicKey ?? '', args.label ?? 'swap', portNote),
|
|
367
|
-
onResult: (body) => {
|
|
368
|
-
// task-1368 (g): the page reports which account signed (base58). When the caller named the
|
|
369
|
-
// linked wallet, a different account is refused HERE too — the page's own check is UX, this is
|
|
370
|
-
// the gate (the signer also verifies the signature over the bytes; this is belt and braces).
|
|
371
|
-
const account = str(body.publicKey);
|
|
372
|
-
if (args.expectPublicKey && account !== '' && account !== args.expectPublicKey)
|
|
373
|
-
return { error: `signed by a different wallet account (${sanitize(account, 8)}…), not the linked wallet` };
|
|
374
|
-
const signed = str(body.signedTx);
|
|
375
|
-
if (signed.length === 0)
|
|
376
|
-
return { error: 'wallet returned no signed transaction' };
|
|
377
|
-
if (signed.length > 4096)
|
|
378
|
-
return { error: 'signed transaction too large' };
|
|
379
|
-
if (!/^[A-Za-z0-9+/=]+$/.test(signed))
|
|
380
|
-
return { error: 'signed transaction is not base64' };
|
|
381
|
-
return { value: { signedTxBase64: signed, provider: sanitize(body.provider ?? 'unknown', 24), roundTripMs: this.clock.now() - started } };
|
|
382
|
-
},
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
332
|
}
|
|
386
333
|
// --------------------------------------------------------------------------- inlined browser pages
|
|
387
334
|
// These run in the user's browser (real Phantom). The fake-Phantom e2e POSTs the result directly, so
|
|
@@ -417,48 +364,3 @@ const j=await rp.json();if(j.ok){$('r').textContent='✓ linked. Return to dexte
|
|
|
417
364
|
catch(e){$('r').textContent='✗ '+(e&&e.message||e);$('r').className='bad';$('s').textContent='';$('go').disabled=false;}}
|
|
418
365
|
$('go').addEventListener('click',run);poll(0);})();</script></body></html>`.replace('"__T__"', `new URLSearchParams(location.search).get('t')`);
|
|
419
366
|
}
|
|
420
|
-
function signPage(port, nonce, unsignedTxB64, expectPk, label, portNote = '') {
|
|
421
|
-
const web3 = solanaWeb3Bundle();
|
|
422
|
-
return `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
423
|
-
<meta name="viewport" content="width=device-width, initial-scale=1"><title>dexterm · sign</title>
|
|
424
|
-
<style nonce="${nonce}">body{font:14px/1.5 -apple-system,system-ui,sans-serif;background:#07090d;color:#d7dbe0;margin:0;padding:2rem}
|
|
425
|
-
.card{max-width:640px;margin:auto;background:#0d1016;border:1px solid #1d2430;border-radius:12px;padding:1.5rem}
|
|
426
|
-
h1{font-size:1.1rem;color:#7fd1ff}button{font:inherit;background:#531;border:1px solid #a62;color:#fec;border-radius:8px;padding:.5rem 1rem;cursor:pointer}
|
|
427
|
-
.ok{color:#5de28a}.bad{color:#ff6b6b}.warn{color:#e2b85d}.muted{color:#7a828e}code{word-break:break-all}</style></head><body><div class="card">
|
|
428
|
-
<h1>✦ dexterm · approve ${esc(label)}</h1>
|
|
429
|
-
<p>Your terminal already showed you this trade and you pressed confirm. Approve it in your wallet to sign. dexterm submits the signed transaction; it never holds your key.</p>
|
|
430
|
-
${portNote ? `<p class="warn">${esc(portNote)}</p>\n` : ''}<p id="p" class="warn">looking for a wallet…</p>
|
|
431
|
-
<p><button id="go" disabled>Approve in wallet</button> <span id="s"></span></p>
|
|
432
|
-
<p id="r"></p>
|
|
433
|
-
<p class="muted">linked wallet: <code>${esc(expectPk || 'any')}</code> · connected: <code id="a">–</code></p></div>
|
|
434
|
-
${web3 ? `<script nonce="${nonce}">${web3}\n;window.dexSolana=solanaWeb3;</script>` : ''}
|
|
435
|
-
<script nonce="${nonce}">(()=>{'use strict';const $=i=>document.getElementById(i),PORT=${port},EXPECT=${JSON.stringify(expectPk)};
|
|
436
|
-
const TX=${JSON.stringify(unsignedTxB64)};const b64=u=>btoa(String.fromCharCode.apply(null,u));
|
|
437
|
-
const short=k=>k&&k.length>12?k.slice(0,4)+'…'+k.slice(-4):String(k||'?');
|
|
438
|
-
// task-1368 (g): Phantom's error codes in plain English. 4100 = this origin was never connected (or the
|
|
439
|
-
// user revoked it): the fix is the Connect prompt, not a retry of the signature.
|
|
440
|
-
function explain(e){const code=e&&(e.code!==undefined?e.code:(e.error&&e.error.code));const msg=(e&&e.message)||String(e);
|
|
441
|
-
if(code===4100)return 'Phantom has not authorized this page (4100). Press Approve again and accept the Connect prompt; if none appears, open Phantom → Settings → Trusted apps, allow 127.0.0.1:'+PORT+', then confirm the trade again in dexterm. Nothing was signed.';
|
|
442
|
-
if(code===4001)return 'you rejected the request in Phantom; nothing was signed. Confirm again in dexterm to retry.';
|
|
443
|
-
if(code===-32603)return 'Phantom could not process the transaction (internal error): '+msg;
|
|
444
|
-
return msg;}
|
|
445
|
-
function bytes(b){const s=atob(b),a=new Uint8Array(s.length);for(let i=0;i<s.length;i++)a[i]=s.charCodeAt(i);return a;}
|
|
446
|
-
let prov=null,name='none';function find(){if(window.phantom&&window.phantom.solana&&window.phantom.solana.isPhantom)return[window.phantom.solana,'phantom'];if(window.solana&&window.solana.isPhantom)return[window.solana,'phantom'];if(window.solana)return[window.solana,window.solana.isStubWallet?'stub':'solana'];return[null,'none'];}
|
|
447
|
-
function poll(n){[prov,name]=find();if(prov){$('p').textContent='wallet: '+name;$('p').className='ok';$('go').disabled=false;if(name==='stub')run();return;}if(n>40){$('p').textContent='No Solana wallet found.';$('p').className='bad';return;}setTimeout(()=>poll(n+1),100);}
|
|
448
|
-
async function run(){$('go').disabled=true;$('s').textContent='connecting to your wallet…';try{
|
|
449
|
-
// Real Phantom needs @solana/web3.js on the page to rebuild the VersionedTransaction before signTransaction().
|
|
450
|
-
// The dexterm wallet shell (popup-host lane) provides it as window.dexSolana; see scripts/ for the manual path.
|
|
451
|
-
if(!window.dexSolana||!window.dexSolana.VersionedTransaction)throw new Error('tx signing needs the dexterm wallet shell (window.dexSolana). See scripts/phantom-devnet.md.');
|
|
452
|
-
// task-1368 (g): CONNECT FIRST. signTransaction() on an origin Phantom has not connected is a 4100, so
|
|
453
|
-
// the page used to fail on every fresh (random-port) origin. connect() resolves silently on a trusted
|
|
454
|
-
// origin and prompts once on a new one; then the connected account must be the wallet dexterm linked.
|
|
455
|
-
const res=await prov.connect();const pk=(res&&res.publicKey)||prov.publicKey;const account=pk?(typeof pk.toBase58==='function'?pk.toBase58():String(pk)):'';
|
|
456
|
-
if(!account)throw new Error('the wallet connected without a public key; nothing was signed');$('a').textContent=account;
|
|
457
|
-
if(EXPECT&&account!==EXPECT)throw new Error('Phantom is on account '+short(account)+' but dexterm linked '+short(EXPECT)+'. Switch accounts in Phantom and press Approve again, or re-link with "dexterm wallet link". Nothing was signed.');
|
|
458
|
-
$('s').textContent='approve in your wallet…';
|
|
459
|
-
const vt=window.dexSolana.VersionedTransaction.deserialize(bytes(TX));const signed=await prov.signTransaction(vt);
|
|
460
|
-
const out=b64(signed.serialize());const rp=await fetch('http://127.0.0.1:'+PORT+'/sign/result',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({t:${JSON.stringify('__T__')},signedTx:out,provider:name,publicKey:account})});
|
|
461
|
-
const j=await rp.json();if(j.ok){$('r').textContent='✓ signed. Return to dexterm.';$('r').className='ok';$('s').textContent='done';}else{$('r').textContent='✗ '+(j.error||'rejected');$('r').className='bad';$('go').disabled=false;}}
|
|
462
|
-
catch(e){$('r').textContent='✗ '+explain(e);$('r').className='bad';$('s').textContent='';$('go').disabled=false;}}
|
|
463
|
-
$('go').addEventListener('click',run);poll(0);})();</script></body></html>`.replace('"__T__"', `new URLSearchParams(location.search).get('t')`);
|
|
464
|
-
}
|