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
|
@@ -1,400 +0,0 @@
|
|
|
1
|
-
// dexterm T2 — the SIGNER SERVICE (task-1328, SPEC §6.5). The policy-owning side of the trade: it holds
|
|
2
|
-
// the caps, the daily-cap ledger, the kill switch, the plan registry, the wallet bridge and the live
|
|
3
|
-
// fill ledger, and exposes exactly the small API the protocol names. It is transport-agnostic (the host
|
|
4
|
-
// serves it over the authenticated stdio channel; tests call it directly).
|
|
5
|
-
//
|
|
6
|
-
// What the UI can and cannot do through this surface:
|
|
7
|
-
// * plan(order) → the signer quotes, builds, decodes, policy-checks, BINDS and simulates ITS OWN
|
|
8
|
-
// transaction for the linked wallet it reads from ITS OWN link store; the UI gets a
|
|
9
|
-
// JSON view (no bytes) and a hash.
|
|
10
|
-
// * confirm(hash) → the signer re-checks caps, reserves USD, opens the wallet page (the physical
|
|
11
|
-
// approval), verifies and re-simulates the signed bytes, submits, confirms, books.
|
|
12
|
-
// * probe() → the devnet plumbing transaction (never on mainnet).
|
|
13
|
-
// * status / precheck / drop / panic.
|
|
14
|
-
// There is no sign(bytes), no submit(bytes), no setCaps(), no way to name a different wallet.
|
|
15
|
-
import { systemClock } from "../../clock.js";
|
|
16
|
-
import { LiveExecutor, PLAN_TTL_MS, LiveLedger } from "../executor.js";
|
|
17
|
-
import { effectiveCaps, HARD_MAX_SLIPPAGE_BPS } from "../caps.js";
|
|
18
|
-
import { WalletLinkStore } from "../link-store.js";
|
|
19
|
-
import { rpcNetwork } from "../rpc-exec.js";
|
|
20
|
-
import { resolveSolUsd } from "../sol-usd.js";
|
|
21
|
-
import { FAST_MAX_SOL_PER_FILL } from "./fast-wallet.js";
|
|
22
|
-
import { SOL_MINT } from "../../paper.js";
|
|
23
|
-
import { JupiterRateLimitedError, normalizeSwapQuote } from "../../sources/jupiter-swap.js";
|
|
24
|
-
import { SIGNER_PROTOCOL_VERSION } from "./protocol.js";
|
|
25
|
-
const B58 = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
26
|
-
const HASH = /^[0-9a-f]{64}$/;
|
|
27
|
-
export class SignerError extends Error {
|
|
28
|
-
reason;
|
|
29
|
-
/** task-1368: for `rate-limited`, how long the caller should wait before asking again (ms). */
|
|
30
|
-
retryAfterMs;
|
|
31
|
-
constructor(reason, message, retryAfterMs) {
|
|
32
|
-
super(message);
|
|
33
|
-
this.name = 'SignerError';
|
|
34
|
-
this.reason = reason;
|
|
35
|
-
if (retryAfterMs !== undefined)
|
|
36
|
-
this.retryAfterMs = retryAfterMs;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* task-1368: a build error that is really Jupiter rate limiting (the data layer's typed error after its bounded
|
|
41
|
-
* retry) becomes `rate-limited` with the honest wait, so the ticket keeps the plan and retries; anything else is
|
|
42
|
-
* `build-failed` as before. Never swallows: the message is the data layer's own.
|
|
43
|
-
*/
|
|
44
|
-
function buildError(e) {
|
|
45
|
-
if (e instanceof JupiterRateLimitedError)
|
|
46
|
-
return new SignerError('rate-limited', e.message, e.retryAfterMs);
|
|
47
|
-
return new SignerError('build-failed', e instanceof Error ? e.message : String(e));
|
|
48
|
-
}
|
|
49
|
-
/** task-1363: validate an untrusted fill request. Anything off-shape is a bad request (never coerced). */
|
|
50
|
-
export function parseFillParams(p) {
|
|
51
|
-
if (p === null || typeof p !== 'object')
|
|
52
|
-
throw new SignerError('bad-request', 'fill params must be an object');
|
|
53
|
-
const x = p;
|
|
54
|
-
if (typeof x.ref !== 'string' || !/^[\x21-\x7e]{1,64}$/.test(x.ref))
|
|
55
|
-
throw new SignerError('bad-request', 'ref must be 1..64 printable characters (planId:legKey)');
|
|
56
|
-
if (typeof x.mint !== 'string' || !B58.test(x.mint))
|
|
57
|
-
throw new SignerError('bad-request', 'mint must be a base58 address');
|
|
58
|
-
if (x.side !== 'buy' && x.side !== 'sell')
|
|
59
|
-
throw new SignerError('bad-request', 'side must be buy or sell');
|
|
60
|
-
if (typeof x.decimals !== 'number' || !Number.isInteger(x.decimals) || x.decimals < 0 || x.decimals > 18)
|
|
61
|
-
throw new SignerError('bad-request', 'decimals must be an integer 0..18');
|
|
62
|
-
if (typeof x.lamports !== 'string' || !/^[0-9]{1,19}$/.test(x.lamports) || BigInt(x.lamports) <= 0n)
|
|
63
|
-
throw new SignerError('bad-request', 'lamports must be a positive integer string');
|
|
64
|
-
const symbol = typeof x.symbol === 'string' ? x.symbol.replace(/[^\x20-\x7e]/g, '?').slice(0, 16) : '';
|
|
65
|
-
return { ref: x.ref, mint: x.mint, symbol: symbol || x.mint.slice(0, 4), side: x.side, decimals: x.decimals, lamports: x.lamports };
|
|
66
|
-
}
|
|
67
|
-
/** Validate an untrusted plan request. Anything off-shape is a bad request (never coerced). */
|
|
68
|
-
export function parsePlanParams(p) {
|
|
69
|
-
if (p === null || typeof p !== 'object')
|
|
70
|
-
throw new SignerError('bad-request', 'plan params must be an object');
|
|
71
|
-
const x = p;
|
|
72
|
-
if (typeof x.mint !== 'string' || !B58.test(x.mint))
|
|
73
|
-
throw new SignerError('bad-request', 'mint must be a base58 address');
|
|
74
|
-
if (x.side !== 'buy' && x.side !== 'sell')
|
|
75
|
-
throw new SignerError('bad-request', 'side must be buy or sell');
|
|
76
|
-
if (typeof x.decimals !== 'number' || !Number.isInteger(x.decimals) || x.decimals < 0 || x.decimals > 18)
|
|
77
|
-
throw new SignerError('bad-request', 'decimals must be an integer 0..18');
|
|
78
|
-
if (typeof x.amountRaw !== 'string' || !/^[0-9]{1,30}$/.test(x.amountRaw) || BigInt(x.amountRaw) <= 0n)
|
|
79
|
-
throw new SignerError('bad-request', 'amountRaw must be a positive integer string');
|
|
80
|
-
if (typeof x.slippageBps !== 'number' || !Number.isInteger(x.slippageBps) || x.slippageBps < 0 || x.slippageBps > HARD_MAX_SLIPPAGE_BPS)
|
|
81
|
-
throw new SignerError('bad-request', `slippageBps must be an integer 0..${HARD_MAX_SLIPPAGE_BPS}`);
|
|
82
|
-
const symbol = typeof x.symbol === 'string' ? x.symbol.replace(/[^\x20-\x7e]/g, '?').slice(0, 16) : '';
|
|
83
|
-
return { mint: x.mint, symbol: symbol || x.mint.slice(0, 4), side: x.side, decimals: x.decimals, amountRaw: x.amountRaw, slippageBps: x.slippageBps };
|
|
84
|
-
}
|
|
85
|
-
export function parseHashParam(p) {
|
|
86
|
-
const h = p !== null && typeof p === 'object' ? p.hash : undefined;
|
|
87
|
-
if (typeof h !== 'string' || !HASH.test(h))
|
|
88
|
-
throw new SignerError('bad-request', 'hash must be a 64-hex plan hash');
|
|
89
|
-
return h;
|
|
90
|
-
}
|
|
91
|
-
export class SignerService {
|
|
92
|
-
ex;
|
|
93
|
-
d;
|
|
94
|
-
links;
|
|
95
|
-
live;
|
|
96
|
-
feed;
|
|
97
|
-
clock;
|
|
98
|
-
fastStore;
|
|
99
|
-
log;
|
|
100
|
-
/** Plans this service built, by hash (the executor keeps its own frozen registry too). */
|
|
101
|
-
plans = new Map();
|
|
102
|
-
/** task-1368: per plan hash, whether the order's mint exists on this signer's network (swap plans). */
|
|
103
|
-
mintOnNet = new Map();
|
|
104
|
-
/** One confirm at a time: a second keypress while the wallet page is open is refused, not queued. */
|
|
105
|
-
confirming = null;
|
|
106
|
-
constructor(s) {
|
|
107
|
-
this.ex = s.executor;
|
|
108
|
-
this.d = s.deps;
|
|
109
|
-
this.links = s.linkStore;
|
|
110
|
-
this.live = s.liveLedger;
|
|
111
|
-
this.feed = s.solUsdFeed ?? null;
|
|
112
|
-
this.clock = s.clock ?? systemClock;
|
|
113
|
-
this.fastStore = s.fastStore ?? null;
|
|
114
|
-
this.log = s.log ?? (() => { });
|
|
115
|
-
}
|
|
116
|
-
/** The network of the RPC endpoint the signer actually talks to. Classified, never a label. */
|
|
117
|
-
network() {
|
|
118
|
-
const rpc = this.d.rpc;
|
|
119
|
-
return rpcNetwork(rpc.url, rpc.network ?? null);
|
|
120
|
-
}
|
|
121
|
-
rpcHost() {
|
|
122
|
-
try {
|
|
123
|
-
return new URL(this.d.rpc.url).hostname;
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
return '?';
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
linked() {
|
|
130
|
-
const w = this.links.get();
|
|
131
|
-
if (!w)
|
|
132
|
-
throw new SignerError('no-wallet', 'no wallet is linked; run `dexterm wallet link` (or /wallet link)');
|
|
133
|
-
return { publicKey: w.publicKey, linkedAt: w.linkedAt };
|
|
134
|
-
}
|
|
135
|
-
solUsdView() {
|
|
136
|
-
const r = resolveSolUsd(this.d.solUsd?.(), this.clock.now(), this.network());
|
|
137
|
-
return r.price === null ? { price: null, reason: r.reason } : { price: r.price, sources: r.sources };
|
|
138
|
-
}
|
|
139
|
-
status() {
|
|
140
|
-
const caps = effectiveCaps(this.d.caps);
|
|
141
|
-
const ks = this.ex.killSwitch;
|
|
142
|
-
const panic = ks.engaged();
|
|
143
|
-
const lock = this.d.ledger.locked();
|
|
144
|
-
this.sweep();
|
|
145
|
-
return {
|
|
146
|
-
version: SIGNER_PROTOCOL_VERSION,
|
|
147
|
-
pid: process.pid,
|
|
148
|
-
linked: this.links.get() ? this.linked() : null,
|
|
149
|
-
fastWallet: this.d.fastSigner ? { publicKey: this.d.fastSigner.publicKey } : null,
|
|
150
|
-
network: this.network(),
|
|
151
|
-
rpcHost: this.rpcHost(),
|
|
152
|
-
caps: { network: caps.network, mainnetEnabled: caps.mainnetEnabled, perTradeUsd: caps.perTradeUsd, dailyUsd: caps.dailyUsd, maxPriceImpactPct: caps.maxPriceImpactPct, maxSolOut: caps.maxSolOut, maxSlippageBps: caps.maxSlippageBps },
|
|
153
|
-
panic,
|
|
154
|
-
ledgerLocked: lock,
|
|
155
|
-
spentTodayUsd: this.d.ledger.spentToday(),
|
|
156
|
-
plans: this.plans.size,
|
|
157
|
-
solUsd: this.solUsdView(),
|
|
158
|
-
probeAvailable: !!this.d.probe && this.network() === 'devnet',
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
sweep() {
|
|
162
|
-
const now = this.clock.now();
|
|
163
|
-
for (const [h, b] of this.plans)
|
|
164
|
-
if (now - b.builtAt > PLAN_TTL_MS) {
|
|
165
|
-
this.plans.delete(h);
|
|
166
|
-
this.mintOnNet.delete(h);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
/**
|
|
170
|
-
* task-1368: does the mint account exist on the network the signer talks to? Read-only `getMultipleAccounts`;
|
|
171
|
-
* null when the RPC cannot answer. On a devnet signer a mainnet-only token answers false — the reason the swap's
|
|
172
|
-
* simulation fails there, shown in plain English on the ticket instead of a truncated RPC error.
|
|
173
|
-
*/
|
|
174
|
-
async mintExists(mint) {
|
|
175
|
-
const rpc = this.d.rpc;
|
|
176
|
-
if (typeof rpc.accountsInfo !== 'function')
|
|
177
|
-
return null;
|
|
178
|
-
try {
|
|
179
|
-
const infos = await rpc.accountsInfo.call(this.d.rpc, [mint]);
|
|
180
|
-
return infos[0] !== null && infos[0] !== undefined;
|
|
181
|
-
}
|
|
182
|
-
catch {
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
quoteRequest(p) {
|
|
187
|
-
return {
|
|
188
|
-
inputMint: p.side === 'buy' ? SOL_MINT : p.mint,
|
|
189
|
-
outputMint: p.side === 'buy' ? p.mint : SOL_MINT,
|
|
190
|
-
amount: BigInt(p.amountRaw),
|
|
191
|
-
slippageBps: p.slippageBps,
|
|
192
|
-
mode: 'ExactIn',
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* task-1368: the keyless Jupiter quote for an order, fetched by the signer's own HTTP client (ONE limiter for
|
|
197
|
-
* preview, re-quote and build) and reusable by `plan` for QUOTE_REUSE_MS. Read-only: numbers and strings go
|
|
198
|
-
* back, never a transaction, and nothing is registered. Rate limiting past the retry budget is `rate-limited`.
|
|
199
|
-
*/
|
|
200
|
-
async quote(params) {
|
|
201
|
-
const p = parsePlanParams(params);
|
|
202
|
-
const req = this.quoteRequest(p);
|
|
203
|
-
const build = this.d.build;
|
|
204
|
-
try {
|
|
205
|
-
const raw = await build.quoteRaw(req);
|
|
206
|
-
const q = normalizeSwapQuote(raw, this.clock.now());
|
|
207
|
-
const age = typeof build.quoteAge === 'function' ? build.quoteAge(req) : null;
|
|
208
|
-
return {
|
|
209
|
-
inputMint: q.inputMint, outputMint: q.outputMint, inAmountRaw: q.inAmount.toString(), outAmountRaw: q.outAmount.toString(),
|
|
210
|
-
minOutRaw: q.otherAmountThreshold.toString(), slippageBps: q.slippageBps, priceImpactPct: q.priceImpactPct, route: [...q.route],
|
|
211
|
-
usdValue: q.usdValue, solUsd: this.solUsdView(), host: typeof build.host === 'string' ? build.host : '?', ageMs: Math.max(0, age ?? 0), fetchedAt: this.clock.now(),
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
catch (e) {
|
|
215
|
-
throw buildError(e);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
view(b) {
|
|
219
|
-
const p = b.plan;
|
|
220
|
-
const pre = this.ex.precheck(b);
|
|
221
|
-
const policy = p.txPolicy.ok ? { ok: true } : { ok: false, reason: p.txPolicy.reason, message: p.txPolicy.message };
|
|
222
|
-
return {
|
|
223
|
-
hash: b.hash, kind: b.kind, lane: b.lane, ref: b.ref, network: this.network(), mint: p.mint, symbol: p.symbol, side: p.side, decimals: b.order.decimals,
|
|
224
|
-
amountInRaw: p.amountInRaw.toString(), amountOutRaw: p.amountOutRaw.toString(), minOutRaw: b.quote.otherAmountThreshold.toString(),
|
|
225
|
-
slippageBps: p.slippageBps, route: [...p.route], priceImpactPct: p.priceImpactPct, notionalUsd: p.notionalUsd,
|
|
226
|
-
solUsd: b.solUsd.price === null ? { price: null, reason: b.solUsd.reason } : { price: b.solUsd.price, sources: b.solUsd.sources },
|
|
227
|
-
sim: { ran: p.simulated, ok: p.simOk, error: p.simError, unitsConsumed: null, solOutLamports: p.solOutLamports === null ? null : p.solOutLamports.toString() },
|
|
228
|
-
mintOnNetwork: this.mintOnNet.get(b.hash) ?? null,
|
|
229
|
-
feeLamports: b.feeLamports.toString(), prioritizationFeeLamports: b.prioritizationFeeLamports, instructions: [...b.instructions],
|
|
230
|
-
policy, precheck: pre.ok ? { ok: true } : { ok: false, reason: pre.reason, message: pre.message },
|
|
231
|
-
userPublicKey: b.userPublicKey, builtAt: b.builtAt, expiresAt: b.builtAt + PLAN_TTL_MS,
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
/** Build a SWAP plan for the LINKED wallet. The order comes from the UI; the wallet, caps, tx do not. */
|
|
235
|
-
async plan(params) {
|
|
236
|
-
const p = parsePlanParams(params);
|
|
237
|
-
const linked = this.linked();
|
|
238
|
-
await this.feed?.ensureFresh();
|
|
239
|
-
const order = { mint: p.mint, symbol: p.symbol, side: p.side, decimals: p.decimals, amountRaw: BigInt(p.amountRaw), slippageBps: p.slippageBps };
|
|
240
|
-
let built;
|
|
241
|
-
try {
|
|
242
|
-
built = await this.ex.buildPlan({ order, userPublicKey: linked.publicKey, network: this.network() });
|
|
243
|
-
}
|
|
244
|
-
catch (e) {
|
|
245
|
-
throw buildError(e);
|
|
246
|
-
}
|
|
247
|
-
this.plans.set(built.hash, built);
|
|
248
|
-
// task-1368: the mint's existence on THIS network (devnet signer + mainnet token = the sim-failure reason), and
|
|
249
|
-
// the FULL simulation error in the log (the ticket row truncates; `invalid transaction: Tr…` told the user nothing).
|
|
250
|
-
this.mintOnNet.set(built.hash, await this.mintExists(p.mint));
|
|
251
|
-
if (built.plan.simulated && !built.plan.simOk)
|
|
252
|
-
this.log(`signer: plan ${built.hash.slice(0, 8)} ${p.side} ${p.symbol} simulation failed on ${this.network()} (${this.rpcHost()}): ${built.plan.simError ?? 'unknown error'}`);
|
|
253
|
-
this.sweep();
|
|
254
|
-
return this.view(built);
|
|
255
|
-
}
|
|
256
|
-
/** Build the devnet PROBE plan. Refused unless the RPC is devnet by classification. */
|
|
257
|
-
async probe() {
|
|
258
|
-
const linked = this.linked();
|
|
259
|
-
if (this.network() !== 'devnet')
|
|
260
|
-
throw new SignerError('network-mismatch', 'the probe runs only on devnet');
|
|
261
|
-
let built;
|
|
262
|
-
try {
|
|
263
|
-
built = await this.ex.buildProbe({ userPublicKey: linked.publicKey });
|
|
264
|
-
}
|
|
265
|
-
catch (e) {
|
|
266
|
-
throw new SignerError('build-failed', e instanceof Error ? e.message : String(e));
|
|
267
|
-
}
|
|
268
|
-
this.plans.set(built.hash, built);
|
|
269
|
-
return this.view(built);
|
|
270
|
-
}
|
|
271
|
-
/**
|
|
272
|
-
* task-1363: the fast wallet's public status. Loaded only when the signer runs against devnet AND a record
|
|
273
|
-
* exists; the balance is read from the devnet RPC (null when unreadable). Never carries the seed.
|
|
274
|
-
*/
|
|
275
|
-
async fast() {
|
|
276
|
-
const signer = this.d.fastSigner ?? null;
|
|
277
|
-
const rec = this.fastStore?.get() ?? null;
|
|
278
|
-
const base = { network: 'devnet', maxSolPerFill: FAST_MAX_SOL_PER_FILL, createdAt: rec?.createdAt ?? null };
|
|
279
|
-
if (!signer) {
|
|
280
|
-
const reason = this.network() !== 'devnet' ? 'the fast wallet exists only on devnet; this signer is not on a devnet RPC'
|
|
281
|
-
: rec ? 'the fast wallet record could not be loaded' : 'no fast wallet: `dexterm wallet fast create`';
|
|
282
|
-
return { ...base, loaded: false, publicKey: rec?.publicKey ?? null, balanceLamports: null, reason };
|
|
283
|
-
}
|
|
284
|
-
let balance = null;
|
|
285
|
-
try {
|
|
286
|
-
balance = (await this.d.rpc.balance(signer.publicKey)).toString();
|
|
287
|
-
}
|
|
288
|
-
catch {
|
|
289
|
-
balance = null;
|
|
290
|
-
}
|
|
291
|
-
return { ...base, loaded: true, publicKey: signer.publicKey, balanceLamports: balance, reason: null };
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* task-1363: build a DEVNET FILL for the fast wallet from an order-plan leg. The engine (in the terminal
|
|
295
|
-
* process) asks; the signer builds its own transaction for ITS fast wallet, applies the probe-shape policy,
|
|
296
|
-
* simulates, prices the SOL leg in USD from its own feed and registers the plan under a hash the engine then
|
|
297
|
-
* confirms. Refused off devnet or without a loaded fast wallet. Caps are decided at precheck/confirm.
|
|
298
|
-
*/
|
|
299
|
-
async fill(params) {
|
|
300
|
-
const p = parseFillParams(params);
|
|
301
|
-
if (this.network() !== 'devnet')
|
|
302
|
-
throw new SignerError('network-mismatch', 'devnet fills run only on devnet');
|
|
303
|
-
if (!this.d.fastSigner)
|
|
304
|
-
throw new SignerError('no-fast-wallet', 'no fast wallet is loaded; create one with `dexterm wallet fast create` (devnet) and restart dexterm');
|
|
305
|
-
await this.feed?.ensureFresh();
|
|
306
|
-
let built;
|
|
307
|
-
try {
|
|
308
|
-
built = await this.ex.buildFill({ lamports: BigInt(p.lamports), mint: p.mint, symbol: p.symbol, decimals: p.decimals, side: p.side, ref: p.ref });
|
|
309
|
-
}
|
|
310
|
-
catch (e) {
|
|
311
|
-
throw new SignerError('build-failed', e instanceof Error ? e.message : String(e));
|
|
312
|
-
}
|
|
313
|
-
this.plans.set(built.hash, built);
|
|
314
|
-
this.sweep();
|
|
315
|
-
return this.view(built);
|
|
316
|
-
}
|
|
317
|
-
precheck(params) {
|
|
318
|
-
const hash = parseHashParam(params);
|
|
319
|
-
const b = this.plans.get(hash);
|
|
320
|
-
if (!b)
|
|
321
|
-
return { ok: false, reason: 'plan-mismatch', message: 'unknown plan' };
|
|
322
|
-
const r = this.ex.precheck(b);
|
|
323
|
-
return r.ok ? { ok: true } : { ok: false, reason: r.reason, message: r.message };
|
|
324
|
-
}
|
|
325
|
-
drop(params) {
|
|
326
|
-
const hash = parseHashParam(params);
|
|
327
|
-
return { dropped: this.plans.delete(hash) };
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* The keypress arrived: confirm the plan named by hash. The wallet is re-read from the link store (a
|
|
331
|
-
* link changed since the plan → signer-mismatch). Single-flight: a second confirm while one is in
|
|
332
|
-
* progress is refused rather than queued (the wallet page is already open).
|
|
333
|
-
*/
|
|
334
|
-
async confirm(params) {
|
|
335
|
-
const hash = parseHashParam(params);
|
|
336
|
-
const b = this.plans.get(hash);
|
|
337
|
-
if (!b)
|
|
338
|
-
return { kind: 'refused', reason: 'plan-mismatch', message: 'unknown or expired plan; build it again' };
|
|
339
|
-
if (this.confirming)
|
|
340
|
-
return { kind: 'refused', reason: 'plan-mismatch', message: 'another confirm is in progress' };
|
|
341
|
-
// task-1363: a fast-lane plan is confirmed for the FAST wallet (no Phantom link needed); a main-lane plan for
|
|
342
|
-
// the linked wallet as before. The executor re-checks the lane against the plan's own public key.
|
|
343
|
-
const signerKey = b.lane === 'fast' ? (this.d.fastSigner?.publicKey ?? null) : this.linked().publicKey;
|
|
344
|
-
if (!signerKey)
|
|
345
|
-
return { kind: 'refused', reason: 'signer-mismatch', message: 'no fast wallet is loaded for this fill' };
|
|
346
|
-
this.confirming = hash;
|
|
347
|
-
try {
|
|
348
|
-
await this.feed?.ensureFresh();
|
|
349
|
-
const out = await this.ex.confirmAndSubmit(b, hash, signerKey);
|
|
350
|
-
this.plans.delete(hash);
|
|
351
|
-
return toConfirmView(out, this.network());
|
|
352
|
-
}
|
|
353
|
-
finally {
|
|
354
|
-
this.confirming = null;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
/** PANIC: engage the kill switch, lock the ledger, forget every plan. One-way; resume needs the TTY path. */
|
|
358
|
-
panic(params) {
|
|
359
|
-
const reason = (params !== null && typeof params === 'object' && typeof params.reason === 'string')
|
|
360
|
-
? String(params.reason).replace(/[^\x20-\x7e]/g, '?').slice(0, 120) : 'panic';
|
|
361
|
-
const ks = this.ex.killSwitch;
|
|
362
|
-
const rec = ks.engage(reason, this.clock.now());
|
|
363
|
-
try {
|
|
364
|
-
this.d.ledger.lock(reason, this.clock.now());
|
|
365
|
-
}
|
|
366
|
-
catch { /* the kill-switch file alone disables trading */ }
|
|
367
|
-
this.plans.clear();
|
|
368
|
-
return { engaged: true, reason: rec.reason };
|
|
369
|
-
}
|
|
370
|
-
/** Dispatch one protocol method. Unknown methods are refused; errors become `ok:false`. */
|
|
371
|
-
async call(method, params) {
|
|
372
|
-
switch (method) {
|
|
373
|
-
case 'status': return this.status();
|
|
374
|
-
case 'plan': return this.plan(params);
|
|
375
|
-
case 'probe': return this.probe();
|
|
376
|
-
case 'precheck': return this.precheck(params);
|
|
377
|
-
case 'confirm': return this.confirm(params);
|
|
378
|
-
case 'drop': return this.drop(params);
|
|
379
|
-
case 'panic': return this.panic(params);
|
|
380
|
-
case 'fast': return this.fast();
|
|
381
|
-
case 'fill': return this.fill(params);
|
|
382
|
-
case 'quote': return this.quote(params);
|
|
383
|
-
default: throw new SignerError('bad-request', `unknown method ${JSON.stringify(String(method).slice(0, 32))}`);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
/** The live fill ledger (for tests / the CLI probe). */
|
|
387
|
-
get liveLedger() { return this.live; }
|
|
388
|
-
get caps() { return effectiveCaps(this.d.caps); }
|
|
389
|
-
}
|
|
390
|
-
function fillView(f) {
|
|
391
|
-
return { t: f.t, mint: f.mint, symbol: f.symbol, side: f.side, decimals: f.decimals, solDelta: f.solDelta.toString(), tokenDelta: f.tokenDelta.toString(), feeLamports: f.feeLamports.toString(), priceSol: f.priceSol, realizedLamports: f.realizedLamports.toString() };
|
|
392
|
-
}
|
|
393
|
-
export function toConfirmView(out, network) {
|
|
394
|
-
switch (out.kind) {
|
|
395
|
-
case 'filled': return { kind: 'filled', signature: out.signature, fill: fillView(out.fill), roundTripMs: out.roundTripMs, lighthouseTail: out.lighthouseTail, network };
|
|
396
|
-
case 'refused': return { kind: 'refused', reason: out.reason, message: out.message };
|
|
397
|
-
case 'send-failed': return { kind: 'send-failed', signature: out.signature, error: out.error };
|
|
398
|
-
case 'send-unknown': return { kind: 'send-unknown', signature: out.signature, error: out.error };
|
|
399
|
-
}
|
|
400
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
// dexterm T2 — the signer's OWN SOL/USD feed (task-1328, closes "SOL/USD freshness check"). The UI's
|
|
2
|
-
// number is never trusted: the signer fetches its own timestamped samples from two INDEPENDENT keyless
|
|
3
|
-
// sources — DexScreener (DEX-derived) and CoinGecko (exchange-derived) — and `resolveSolUsd` applies the
|
|
4
|
-
// freshness (60 s), plausibility and two-source-agreement rules at every plan and confirm. A sample is
|
|
5
|
-
// stamped with the clock at fetch; a fetch that fails leaves the old sample to AGE OUT rather than being
|
|
6
|
-
// refreshed, so a dead feed turns into a refusal on mainnet, never into a stale price.
|
|
7
|
-
//
|
|
8
|
-
// Plain fetch with an unref'd timeout (no AbortSignal.timeout — its ref'd timer kept processes alive,
|
|
9
|
-
// task-1319). No key, no cookies, GET only.
|
|
10
|
-
import { systemClock } from "../../clock.js";
|
|
11
|
-
import { WSOL_MINT } from "../tx-decode.js";
|
|
12
|
-
export const DEXSCREENER_SOL_URL = `https://api.dexscreener.com/tokens/v1/solana/${WSOL_MINT}`;
|
|
13
|
-
export const COINGECKO_SOL_URL = 'https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd';
|
|
14
|
-
async function fetchJson(url, signal) {
|
|
15
|
-
const res = await globalThis.fetch(url, { signal, headers: { accept: 'application/json', 'user-agent': 'dexterm/0.0.1 (+signer; keyless read-only)' } });
|
|
16
|
-
if (!res.ok)
|
|
17
|
-
throw new Error(`HTTP ${res.status}`);
|
|
18
|
-
return res.json();
|
|
19
|
-
}
|
|
20
|
-
const num = (v) => { const n = typeof v === 'number' ? v : typeof v === 'string' ? Number(v) : NaN; return Number.isFinite(n) ? n : null; };
|
|
21
|
-
/** DexScreener: pick the most liquid SOL pair's priceUsd. */
|
|
22
|
-
export function parseDexScreenerSol(body) {
|
|
23
|
-
if (!Array.isArray(body))
|
|
24
|
-
return null;
|
|
25
|
-
let best = null;
|
|
26
|
-
for (const p of body) {
|
|
27
|
-
const price = num(p.priceUsd);
|
|
28
|
-
const liq = num(p.liquidity?.usd) ?? 0;
|
|
29
|
-
if (price === null)
|
|
30
|
-
continue;
|
|
31
|
-
if (!best || liq > best.liq)
|
|
32
|
-
best = { liq, price };
|
|
33
|
-
}
|
|
34
|
-
return best?.price ?? null;
|
|
35
|
-
}
|
|
36
|
-
export function parseCoinGeckoSol(body) {
|
|
37
|
-
if (body === null || typeof body !== 'object')
|
|
38
|
-
return null;
|
|
39
|
-
return num((body.solana)?.usd);
|
|
40
|
-
}
|
|
41
|
-
export class SolUsdFeed {
|
|
42
|
-
clock;
|
|
43
|
-
fetcher;
|
|
44
|
-
timeoutMs;
|
|
45
|
-
bySource = new Map();
|
|
46
|
-
inflight = null;
|
|
47
|
-
lastError = null;
|
|
48
|
-
constructor(opts = {}) {
|
|
49
|
-
this.clock = opts.clock ?? systemClock;
|
|
50
|
-
this.fetcher = opts.fetcher ?? fetchJson;
|
|
51
|
-
this.timeoutMs = opts.timeoutMs ?? 5_000;
|
|
52
|
-
}
|
|
53
|
-
/** Current samples (possibly stale — the resolver decides). */
|
|
54
|
-
samples() { return [...this.bySource.values()]; }
|
|
55
|
-
/** Age of the freshest sample in ms, or Infinity. */
|
|
56
|
-
age(now = this.clock.now()) {
|
|
57
|
-
let best = Infinity;
|
|
58
|
-
for (const s of this.bySource.values())
|
|
59
|
-
best = Math.min(best, now - s.fetchedAt);
|
|
60
|
-
return best;
|
|
61
|
-
}
|
|
62
|
-
/** Refresh both sources (coalesced). Never throws; failures leave old samples to age out. */
|
|
63
|
-
refresh() {
|
|
64
|
-
if (this.inflight)
|
|
65
|
-
return this.inflight;
|
|
66
|
-
this.inflight = this.doRefresh().finally(() => { this.inflight = null; });
|
|
67
|
-
return this.inflight;
|
|
68
|
-
}
|
|
69
|
-
/** Refresh only when the freshest sample is older than `maxAgeMs`. */
|
|
70
|
-
async ensureFresh(maxAgeMs = 20_000) {
|
|
71
|
-
if (this.age() > maxAgeMs)
|
|
72
|
-
await this.refresh();
|
|
73
|
-
}
|
|
74
|
-
async one(source, url, parse) {
|
|
75
|
-
const controller = new AbortController();
|
|
76
|
-
const timer = setTimeout(() => controller.abort(new Error('timeout')), this.timeoutMs);
|
|
77
|
-
timer.unref?.();
|
|
78
|
-
try {
|
|
79
|
-
const body = await this.fetcher(url, controller.signal);
|
|
80
|
-
const price = parse(body);
|
|
81
|
-
if (price === null)
|
|
82
|
-
throw new Error('no price in response');
|
|
83
|
-
this.bySource.set(source, { price, fetchedAt: this.clock.now(), source });
|
|
84
|
-
}
|
|
85
|
-
catch (e) {
|
|
86
|
-
this.lastError = `${source}: ${e instanceof Error ? e.message : String(e)}`;
|
|
87
|
-
}
|
|
88
|
-
finally {
|
|
89
|
-
clearTimeout(timer);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
async doRefresh() {
|
|
93
|
-
await Promise.all([
|
|
94
|
-
this.one('dexscreener', DEXSCREENER_SOL_URL, parseDexScreenerSol),
|
|
95
|
-
this.one('coingecko', COINGECKO_SOL_URL, parseCoinGeckoSol),
|
|
96
|
-
]);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
// dexterm T1 — the SOL/USD price the signer prices a trade with (task-1310 round 2, HIGH #2).
|
|
2
|
-
//
|
|
3
|
-
// The USD caps are only as good as the SOL/USD number behind them: a stale or absurd price (say
|
|
4
|
-
// $0.0001) makes a 100 SOL trade look like a cent. The executor therefore never takes a bare number.
|
|
5
|
-
// It takes timestamped SAMPLES from named sources and resolves them here, refusing when:
|
|
6
|
-
// * a sample is older than MAX_AGE_MS (60 s) or dated in the future;
|
|
7
|
-
// * a price is outside the plausibility band [MIN_PLAUSIBLE_USD, MAX_PLAUSIBLE_USD] ($1–$10,000);
|
|
8
|
-
// * on MAINNET, fewer than two INDEPENDENT non-Jupiter sources agree within AGREE_PCT (3%). Jupiter
|
|
9
|
-
// is the swap vendor, so its price API is not an independent check of a Jupiter route and can
|
|
10
|
-
// never be the only source.
|
|
11
|
-
// The resolved price is the HIGHEST of the agreeing sources: that over-states the notional slightly,
|
|
12
|
-
// which is the conservative direction for a cap. Pure; no I/O.
|
|
13
|
-
export const SOL_USD_MAX_AGE_MS = 60_000;
|
|
14
|
-
export const SOL_USD_MIN_PLAUSIBLE = 1;
|
|
15
|
-
export const SOL_USD_MAX_PLAUSIBLE = 10_000;
|
|
16
|
-
export const SOL_USD_AGREE_PCT = 3;
|
|
17
|
-
/** Sources that are the swap vendor itself (or a re-export of it) and so count as dependent. */
|
|
18
|
-
export const SOL_USD_DEPENDENT_SOURCES = new Set(['jupiter', 'jupiter-swap', 'jupiter-price', 'jup']);
|
|
19
|
-
function isSample(x) {
|
|
20
|
-
if (x === null || typeof x !== 'object')
|
|
21
|
-
return false;
|
|
22
|
-
const s = x;
|
|
23
|
-
return typeof s.price === 'number' && typeof s.fetchedAt === 'number' && typeof s.source === 'string';
|
|
24
|
-
}
|
|
25
|
-
function dependent(source) {
|
|
26
|
-
const s = source.toLowerCase();
|
|
27
|
-
return SOL_USD_DEPENDENT_SOURCES.has(s) || s.startsWith('jupiter') || s.startsWith('jup-');
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Resolve a SOL/USD price from samples. `input` may be a single sample, an array, or anything else
|
|
31
|
-
* (a bare number, null, undefined) — anything that is not a well-formed sample is ignored, so a legacy
|
|
32
|
-
* `() => 150` feed yields NO price and a mainnet trade is refused ('no-usd-value').
|
|
33
|
-
*/
|
|
34
|
-
export function resolveSolUsd(input, now, network) {
|
|
35
|
-
const raw = Array.isArray(input) ? input : input === null || input === undefined ? [] : [input];
|
|
36
|
-
const rejected = [];
|
|
37
|
-
const fresh = new Map(); // one sample per source (the freshest)
|
|
38
|
-
for (const x of raw) {
|
|
39
|
-
if (!isSample(x)) {
|
|
40
|
-
rejected.push('malformed sample');
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
const src = x.source.trim().toLowerCase();
|
|
44
|
-
if (src.length === 0) {
|
|
45
|
-
rejected.push('unnamed source');
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
if (!Number.isFinite(x.price) || x.price < SOL_USD_MIN_PLAUSIBLE || x.price > SOL_USD_MAX_PLAUSIBLE) {
|
|
49
|
-
rejected.push(`${src}: implausible $${x.price}`);
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
if (!Number.isFinite(x.fetchedAt) || x.fetchedAt > now + 5_000) {
|
|
53
|
-
rejected.push(`${src}: bad timestamp`);
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
const age = now - x.fetchedAt;
|
|
57
|
-
if (age > SOL_USD_MAX_AGE_MS) {
|
|
58
|
-
rejected.push(`${src}: stale (${Math.round(age / 1000)} s)`);
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
const prev = fresh.get(src);
|
|
62
|
-
if (!prev || x.fetchedAt > prev.fetchedAt)
|
|
63
|
-
fresh.set(src, { price: x.price, fetchedAt: x.fetchedAt, source: src });
|
|
64
|
-
}
|
|
65
|
-
const valid = [...fresh.values()];
|
|
66
|
-
const why = rejected.length > 0 ? ` (${rejected.join('; ')})` : '';
|
|
67
|
-
if (network !== 'mainnet') {
|
|
68
|
-
// Devnet funds have no USD value; one fresh plausible sample from anyone is enough for display.
|
|
69
|
-
if (valid.length === 0)
|
|
70
|
-
return { price: null, reason: `no fresh SOL/USD sample${why}` };
|
|
71
|
-
const best = valid.reduce((a, b) => (b.price > a.price ? b : a));
|
|
72
|
-
return { price: best.price, sources: [best.source] };
|
|
73
|
-
}
|
|
74
|
-
const independent = valid.filter((s) => !dependent(s.source));
|
|
75
|
-
if (independent.length < 2) {
|
|
76
|
-
const have = independent.length === 1 ? `only ${independent[0].source}` : valid.length > 0 ? 'only the swap vendor' : 'none';
|
|
77
|
-
return { price: null, reason: `mainnet needs two independent non-Jupiter SOL/USD sources; have ${have}${why}` };
|
|
78
|
-
}
|
|
79
|
-
const lo = Math.min(...independent.map((s) => s.price));
|
|
80
|
-
const hi = Math.max(...independent.map((s) => s.price));
|
|
81
|
-
const spreadPct = ((hi - lo) / lo) * 100;
|
|
82
|
-
if (spreadPct > SOL_USD_AGREE_PCT) {
|
|
83
|
-
return { price: null, reason: `SOL/USD sources disagree by ${spreadPct.toFixed(1)}% (${independent.map((s) => `${s.source} $${s.price}`).join(', ')})` };
|
|
84
|
-
}
|
|
85
|
-
return { price: hi, sources: independent.map((s) => s.source) };
|
|
86
|
-
}
|