dexterm-cli 0.0.0-stage → 0.1.2
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/LICENSE +661 -0
- package/dist/packages/agents/src/audit.js +122 -0
- package/dist/packages/agents/src/bus.js +394 -0
- package/dist/packages/agents/src/executor.js +202 -0
- package/dist/packages/agents/src/gate.js +179 -0
- package/dist/packages/agents/src/identity.js +218 -0
- package/dist/packages/agents/src/index.js +15 -0
- package/dist/packages/agents/src/proposals.js +153 -0
- package/dist/packages/agents/src/schema.js +87 -0
- package/dist/packages/agents/src/sdk.js +140 -0
- package/dist/packages/agents/src/tiers.js +119 -0
- package/dist/packages/agents/src/tools.js +72 -0
- package/dist/packages/agents/src/ws.js +158 -0
- package/dist/packages/cli/assets/claude/analyst-system.md +76 -0
- package/dist/packages/cli/assets/claude/plugin/.claude-plugin/plugin.json +5 -0
- package/dist/packages/cli/assets/claude/plugin/skills/REGISTRY.json +16 -0
- package/dist/packages/cli/assets/claude/plugin/skills/larp-check/SKILL.md +75 -0
- package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +74 -0
- package/dist/packages/cli/assets/claude/plugin/skills/token-dossier/SKILL.md +63 -0
- package/dist/packages/cli/assets/claude/plugin/skills/trench-scan/SKILL.md +44 -0
- package/dist/packages/cli/assets/claude/schemas/larp.schema.json +117 -0
- package/dist/packages/cli/assets/claude/schemas/trench-scan.schema.json +71 -0
- package/dist/packages/cli/assets/claude/schemas/verdict.schema.json +115 -0
- package/dist/packages/cli/bin/dexterm.mjs +11 -0
- package/dist/packages/cli/src/agent-cmd.js +336 -0
- package/dist/packages/cli/src/agent-roster.js +26 -0
- package/dist/packages/cli/src/alerts/adapters/desktop.js +40 -0
- package/dist/packages/cli/src/alerts/adapters/discord.js +36 -0
- package/dist/packages/cli/src/alerts/adapters/telegram.js +43 -0
- package/dist/packages/cli/src/alerts/adapters/toast.js +16 -0
- package/dist/packages/cli/src/alerts/creds.js +83 -0
- package/dist/packages/cli/src/alerts/format.js +41 -0
- package/dist/packages/cli/src/alerts/index.js +10 -0
- package/dist/packages/cli/src/alerts/notifier.js +53 -0
- package/dist/packages/cli/src/alerts/route.js +19 -0
- package/dist/packages/cli/src/alerts/types.js +1 -0
- package/dist/packages/cli/src/alerts-cmd.js +212 -0
- package/dist/packages/cli/src/analyst/backend.js +526 -0
- package/dist/packages/cli/src/analyst/results.js +38 -0
- package/dist/packages/cli/src/analyst/session.js +319 -0
- package/dist/packages/cli/src/audit.js +51 -0
- package/dist/packages/cli/src/backtest-cmd.js +208 -0
- package/dist/packages/cli/src/boot-state.js +62 -0
- package/dist/packages/cli/src/coin-target.js +2 -0
- package/dist/packages/cli/src/custom.js +261 -0
- package/dist/packages/cli/src/display.js +191 -0
- package/dist/packages/cli/src/doctor.js +375 -0
- package/dist/packages/cli/src/export.js +235 -0
- package/dist/packages/cli/src/feed.js +634 -0
- package/dist/packages/cli/src/fixtures.js +275 -0
- package/dist/packages/cli/src/follow-cmd.js +221 -0
- package/dist/packages/cli/src/images-config.js +59 -0
- package/dist/packages/cli/src/index.js +31 -0
- package/dist/packages/cli/src/kb-skill-cmd.js +240 -0
- package/dist/packages/cli/src/larp-record.js +21 -0
- package/dist/packages/cli/src/live/copy.js +80 -0
- package/dist/packages/cli/src/live/flow.js +233 -0
- package/dist/packages/cli/src/live/index.js +511 -0
- package/dist/packages/cli/src/live/intel.js +227 -0
- package/dist/packages/cli/src/live/newpairs.js +334 -0
- package/dist/packages/cli/src/live/screens/newpairs-live.js +219 -0
- package/dist/packages/cli/src/live/screens/token-live.js +459 -0
- package/dist/packages/cli/src/live/screens/watchlist-live.js +149 -0
- package/dist/packages/cli/src/live/token.js +585 -0
- package/dist/packages/cli/src/live/trade.js +929 -0
- package/dist/packages/cli/src/live/watchlist.js +178 -0
- package/dist/packages/cli/src/main.js +5785 -0
- package/dist/packages/cli/src/mcp-cmd.js +326 -0
- package/dist/packages/cli/src/narrative-cmd.js +76 -0
- package/dist/packages/cli/src/pack-cmd.js +323 -0
- package/dist/packages/cli/src/plan-cmd.js +278 -0
- package/dist/packages/cli/src/popup.js +810 -0
- package/dist/packages/cli/src/rules-cmd.js +131 -0
- package/dist/packages/cli/src/rules.js +358 -0
- package/dist/packages/cli/src/scan/command.js +57 -0
- package/dist/packages/cli/src/scan/engine.js +419 -0
- package/dist/packages/cli/src/scan/observe.js +64 -0
- package/dist/packages/cli/src/scan/ui.js +98 -0
- package/dist/packages/cli/src/screener/filter.js +241 -0
- package/dist/packages/cli/src/screener/presets.js +80 -0
- package/dist/packages/cli/src/screener/rows.js +146 -0
- package/dist/packages/cli/src/screener/rug.js +50 -0
- package/dist/packages/cli/src/screener/sort.js +52 -0
- package/dist/packages/cli/src/screener/view.js +183 -0
- package/dist/packages/cli/src/screens/analyst-session.js +343 -0
- package/dist/packages/cli/src/screens/analyst.js +326 -0
- package/dist/packages/cli/src/screens/boot.js +74 -0
- package/dist/packages/cli/src/screens/bubbles.js +255 -0
- package/dist/packages/cli/src/screens/coin.js +33 -0
- package/dist/packages/cli/src/screens/follows.js +201 -0
- package/dist/packages/cli/src/screens/kol.js +90 -0
- package/dist/packages/cli/src/screens/narrative.js +90 -0
- package/dist/packages/cli/src/screens/newpairs.js +150 -0
- package/dist/packages/cli/src/screens/overlay.js +257 -0
- package/dist/packages/cli/src/screens/plans.js +126 -0
- package/dist/packages/cli/src/screens/positions.js +176 -0
- package/dist/packages/cli/src/screens/rules.js +170 -0
- package/dist/packages/cli/src/screens/screener.js +459 -0
- package/dist/packages/cli/src/screens/social-panel.js +107 -0
- package/dist/packages/cli/src/screens/ticket.js +342 -0
- package/dist/packages/cli/src/screens/token.js +312 -0
- package/dist/packages/cli/src/screens/trending.js +181 -0
- package/dist/packages/cli/src/screens/watchlist.js +104 -0
- package/dist/packages/cli/src/skill-eval-build.js +109 -0
- package/dist/packages/cli/src/social/demo.js +96 -0
- package/dist/packages/cli/src/social/view.js +250 -0
- package/dist/packages/cli/src/state.js +463 -0
- package/dist/packages/cli/src/theme-cmd.js +126 -0
- package/dist/packages/cli/src/trade.js +319 -0
- package/dist/packages/cli/src/update.js +294 -0
- package/dist/packages/cli/src/version.js +3 -0
- package/dist/packages/cli/src/voice-cmd.js +119 -0
- package/dist/packages/cli/src/voice-hud.js +42 -0
- package/dist/packages/cli/src/wallet-cmd.js +440 -0
- package/dist/packages/cli/src/wallet-command.js +31 -0
- package/dist/packages/cli/src/watch-cmd.js +190 -0
- package/dist/packages/cli/src/widgets.js +208 -0
- package/dist/packages/cli/src/workspace-cmd.js +68 -0
- package/dist/packages/cli/src/x-cmd.js +154 -0
- package/dist/packages/custom/src/config.js +213 -0
- package/dist/packages/custom/src/index.js +10 -0
- package/dist/packages/custom/src/keymap.js +107 -0
- package/dist/packages/custom/src/layout.js +235 -0
- package/dist/packages/custom/src/packs.js +633 -0
- package/dist/packages/custom/src/rules.js +704 -0
- package/dist/packages/custom/src/schema.js +538 -0
- package/dist/packages/custom/src/themes.js +176 -0
- package/dist/packages/custom/src/widget-html.js +222 -0
- package/dist/packages/custom/src/workspace.js +141 -0
- package/dist/packages/data/src/analyst-actions.js +133 -0
- package/dist/packages/data/src/budget.js +88 -0
- package/dist/packages/data/src/cache.js +142 -0
- package/dist/packages/data/src/candles.js +168 -0
- package/dist/packages/data/src/clock.js +64 -0
- package/dist/packages/data/src/coin-target.js +82 -0
- package/dist/packages/data/src/credits.js +137 -0
- package/dist/packages/data/src/detail.js +97 -0
- package/dist/packages/data/src/fast-lane.js +321 -0
- package/dist/packages/data/src/fixtures.js +135 -0
- package/dist/packages/data/src/health.js +245 -0
- package/dist/packages/data/src/http.js +277 -0
- package/dist/packages/data/src/images/cache.js +141 -0
- package/dist/packages/data/src/images/decode-worker.js +28 -0
- package/dist/packages/data/src/images/decode.js +81 -0
- package/dist/packages/data/src/images/fetch.js +162 -0
- package/dist/packages/data/src/images/gif.js +214 -0
- package/dist/packages/data/src/images/index.js +10 -0
- package/dist/packages/data/src/images/jpeg.js +544 -0
- package/dist/packages/data/src/images/png.js +310 -0
- package/dist/packages/data/src/images/service.js +334 -0
- package/dist/packages/data/src/images/types.js +27 -0
- package/dist/packages/data/src/index.js +211 -0
- package/dist/packages/data/src/intel/helius-history.js +70 -0
- package/dist/packages/data/src/intel/holder-class.js +125 -0
- package/dist/packages/data/src/intel/rules.js +421 -0
- package/dist/packages/data/src/intel/scan.js +170 -0
- package/dist/packages/data/src/larp/evidence.js +288 -0
- package/dist/packages/data/src/larp/link-baseline.js +196 -0
- package/dist/packages/data/src/larp/links.js +137 -0
- package/dist/packages/data/src/larp/robots.js +90 -0
- package/dist/packages/data/src/larp/safe-fetch.js +389 -0
- package/dist/packages/data/src/limiter.js +341 -0
- package/dist/packages/data/src/model.js +12 -0
- package/dist/packages/data/src/paper.js +331 -0
- package/dist/packages/data/src/rpc-config.js +126 -0
- package/dist/packages/data/src/rpc-pool.js +166 -0
- package/dist/packages/data/src/social/chain.js +83 -0
- package/dist/packages/data/src/social/dedupe.js +205 -0
- package/dist/packages/data/src/social/feed.js +462 -0
- package/dist/packages/data/src/social/firewall.js +110 -0
- package/dist/packages/data/src/social/index.js +11 -0
- package/dist/packages/data/src/social/kol.js +269 -0
- package/dist/packages/data/src/social/model.js +11 -0
- package/dist/packages/data/src/social/rank.js +184 -0
- package/dist/packages/data/src/social/rss.js +113 -0
- package/dist/packages/data/src/social/summary.js +104 -0
- package/dist/packages/data/src/social/venues.js +186 -0
- package/dist/packages/data/src/sources/dexscreener.js +184 -0
- package/dist/packages/data/src/sources/geckoterminal.js +133 -0
- package/dist/packages/data/src/sources/goplus.js +161 -0
- package/dist/packages/data/src/sources/jupiter-swap.js +236 -0
- package/dist/packages/data/src/sources/jupiter.js +198 -0
- package/dist/packages/data/src/sources/pda.js +113 -0
- package/dist/packages/data/src/sources/pump-curve.js +58 -0
- package/dist/packages/data/src/sources/pumpportal.js +252 -0
- package/dist/packages/data/src/sources/rugcheck.js +71 -0
- package/dist/packages/data/src/sources/solana-rpc.js +299 -0
- package/dist/packages/data/src/sources/solana-ws.js +287 -0
- package/dist/packages/data/src/store.js +289 -0
- package/dist/packages/data/src/trade/bridge.js +464 -0
- package/dist/packages/data/src/trade/caps.js +323 -0
- package/dist/packages/data/src/trade/copy/auto.js +385 -0
- package/dist/packages/data/src/trade/copy/follows.js +103 -0
- package/dist/packages/data/src/trade/copy/index.js +9 -0
- package/dist/packages/data/src/trade/copy/mirror.js +91 -0
- package/dist/packages/data/src/trade/copy/stats.js +120 -0
- package/dist/packages/data/src/trade/copy/tracker.js +158 -0
- package/dist/packages/data/src/trade/copy/trades.js +87 -0
- package/dist/packages/data/src/trade/copy/types.js +6 -0
- package/dist/packages/data/src/trade/executor.js +933 -0
- package/dist/packages/data/src/trade/kill-switch.js +47 -0
- package/dist/packages/data/src/trade/link-store.js +52 -0
- package/dist/packages/data/src/trade/plans/confirm.js +157 -0
- package/dist/packages/data/src/trade/plans/engine.js +784 -0
- package/dist/packages/data/src/trade/plans/executor-live.js +220 -0
- package/dist/packages/data/src/trade/plans/executor-paper.js +44 -0
- package/dist/packages/data/src/trade/plans/executor.js +11 -0
- package/dist/packages/data/src/trade/plans/index.js +12 -0
- package/dist/packages/data/src/trade/plans/journal.js +77 -0
- package/dist/packages/data/src/trade/plans/mandate.js +129 -0
- package/dist/packages/data/src/trade/plans/plan.js +179 -0
- package/dist/packages/data/src/trade/plans/service.js +168 -0
- package/dist/packages/data/src/trade/plans/types.js +6 -0
- package/dist/packages/data/src/trade/rpc-exec.js +229 -0
- package/dist/packages/data/src/trade/signer/client.js +197 -0
- package/dist/packages/data/src/trade/signer/fast-wallet.js +154 -0
- package/dist/packages/data/src/trade/signer/host.js +97 -0
- package/dist/packages/data/src/trade/signer/main.js +121 -0
- package/dist/packages/data/src/trade/signer/probe-builder.js +85 -0
- package/dist/packages/data/src/trade/signer/protocol.js +92 -0
- package/dist/packages/data/src/trade/signer/service.js +400 -0
- package/dist/packages/data/src/trade/signer/sol-usd-feed.js +98 -0
- package/dist/packages/data/src/trade/sol-usd.js +86 -0
- package/dist/packages/data/src/trade/swap-build.js +108 -0
- package/dist/packages/data/src/trade/token-delta.js +201 -0
- package/dist/packages/data/src/trade/tx-decode.js +632 -0
- package/dist/packages/data/src/util.js +24 -0
- package/dist/packages/data/src/watchlist.js +176 -0
- package/dist/packages/data/src/x/api.js +256 -0
- package/dist/packages/data/src/x/budget.js +66 -0
- package/dist/packages/data/src/x/config.js +49 -0
- package/dist/packages/data/src/x/index.js +10 -0
- package/dist/packages/data/src/x/mock-server.js +217 -0
- package/dist/packages/data/src/x/oauth.js +242 -0
- package/dist/packages/data/src/x/pkce.js +21 -0
- package/dist/packages/data/src/x/source.js +285 -0
- package/dist/packages/data/src/x/token-store.js +134 -0
- package/dist/packages/data/src/x/transport.js +71 -0
- package/dist/packages/data/vendor/solana-web3/LICENSE +20 -0
- package/dist/packages/data/vendor/solana-web3/NOTICE.md +13 -0
- package/dist/packages/data/vendor/solana-web3/index.iife.min.js +20 -0
- package/dist/packages/mcp/assets/cases.json +1194 -0
- package/dist/packages/mcp/assets/kb/entries/cabal-and-coordinated-launches.md +28 -0
- package/dist/packages/mcp/assets/kb/entries/common-rug-patterns.md +40 -0
- package/dist/packages/mcp/assets/kb/entries/dbc-mechanics.md +30 -0
- package/dist/packages/mcp/assets/kb/entries/dev-wallet-behaviour.md +33 -0
- package/dist/packages/mcp/assets/kb/entries/glossary.md +35 -0
- package/dist/packages/mcp/assets/kb/entries/holder-concentration.md +30 -0
- package/dist/packages/mcp/assets/kb/entries/jito-bundles.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/launchlab-curve.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/lp-burn-vs-lock.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/metadata-and-copycats.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/mint-freeze-authority.md +31 -0
- package/dist/packages/mcp/assets/kb/entries/narrative-lifecycle.md +32 -0
- package/dist/packages/mcp/assets/kb/entries/pump-bonding-curve.md +33 -0
- package/dist/packages/mcp/assets/kb/entries/pumpswap-migration.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/sniping-and-snipers.md +29 -0
- package/dist/packages/mcp/assets/kb/entries/token2022-extensions-risk.md +32 -0
- package/dist/packages/mcp/assets/kb/entries/wash-trading-fake-volume.md +33 -0
- package/dist/packages/mcp/assets/skills/rug-check.md +52 -0
- package/dist/packages/mcp/assets/skills/token-dossier.md +57 -0
- package/dist/packages/mcp/assets/skills/trench-scan.md +40 -0
- package/dist/packages/mcp/bin/dexterm-mcp.mjs +16 -0
- package/dist/packages/mcp/src/dossier.js +78 -0
- package/dist/packages/mcp/src/firewall.js +21 -0
- package/dist/packages/mcp/src/index.js +22 -0
- package/dist/packages/mcp/src/server.js +936 -0
- package/dist/packages/mcp/src/user-content.js +434 -0
- package/dist/packages/packs/src/backtest.js +183 -0
- package/dist/packages/packs/src/digest.js +95 -0
- package/dist/packages/packs/src/firewall.js +57 -0
- package/dist/packages/packs/src/index.js +20 -0
- package/dist/packages/packs/src/larp-eval.js +100 -0
- package/dist/packages/packs/src/narrative.js +309 -0
- package/dist/packages/packs/src/reason.js +135 -0
- package/dist/packages/packs/src/rug-model.js +137 -0
- package/dist/packages/packs/src/schema.js +54 -0
- package/dist/packages/packs/src/skill-backtest.js +300 -0
- package/dist/packages/packs/src/token-dossier.js +174 -0
- package/dist/packages/packs/src/verdicts.js +17 -0
- package/dist/packages/packs/src/wallet-dossier.js +105 -0
- package/dist/packages/popup/src/config.js +71 -0
- package/dist/packages/popup/src/dock.js +271 -0
- package/dist/packages/popup/src/host.js +210 -0
- package/dist/packages/popup/src/index.js +12 -0
- package/dist/packages/popup/src/loopback.js +53 -0
- package/dist/packages/popup/src/open.js +183 -0
- package/dist/packages/popup/src/pages/bubbles-lib.js +160 -0
- package/dist/packages/popup/src/pages/bubbles.css +23 -0
- package/dist/packages/popup/src/pages/bubbles.html +36 -0
- package/dist/packages/popup/src/pages/bubbles.js +214 -0
- package/dist/packages/popup/src/pages/chart-lib.js +262 -0
- package/dist/packages/popup/src/pages/chart.css +50 -0
- package/dist/packages/popup/src/pages/chart.html +56 -0
- package/dist/packages/popup/src/pages/chart.js +554 -0
- package/dist/packages/popup/src/pages/common.css +34 -0
- package/dist/packages/popup/src/pages/common.js +187 -0
- package/dist/packages/popup/src/pages/feeds.html +20 -0
- package/dist/packages/popup/src/pages/feeds.js +77 -0
- package/dist/packages/popup/src/pages/narrative.html +20 -0
- package/dist/packages/popup/src/pages/narrative.js +61 -0
- package/dist/packages/popup/src/pages/plans.css +30 -0
- package/dist/packages/popup/src/pages/plans.html +26 -0
- package/dist/packages/popup/src/pages/plans.js +82 -0
- package/dist/packages/popup/src/pages/roster.css +41 -0
- package/dist/packages/popup/src/pages/roster.html +25 -0
- package/dist/packages/popup/src/pages/roster.js +75 -0
- package/dist/packages/popup/src/pages/token.html +20 -0
- package/dist/packages/popup/src/pages/token.js +112 -0
- package/dist/packages/popup/src/pages/widget-sdk.js +224 -0
- package/dist/packages/popup/src/pages/widget.css +5 -0
- package/dist/packages/popup/src/pages/widget.html +21 -0
- package/dist/packages/popup/src/pages/widget.js +85 -0
- package/dist/packages/popup/src/protocol.js +31 -0
- package/dist/packages/popup/src/selftest.js +156 -0
- package/dist/packages/popup/src/server.js +582 -0
- package/dist/packages/popup/src/sinkproxy.js +167 -0
- package/dist/packages/popup/src/widget-html.js +231 -0
- package/dist/packages/popup/vendor/lightweight-charts/LICENSE +201 -0
- package/dist/packages/popup/vendor/lightweight-charts/NOTICE +2 -0
- package/dist/packages/popup/vendor/lightweight-charts/README.md +30 -0
- package/dist/packages/popup/vendor/lightweight-charts/VERSION +1 -0
- package/dist/packages/popup/vendor/lightweight-charts/lightweight-charts.standalone.production.js +7 -0
- package/dist/packages/recorder/bin/dexterm-recorder.mjs +11 -0
- package/dist/packages/recorder/src/cli.js +113 -0
- package/dist/packages/recorder/src/export.js +71 -0
- package/dist/packages/recorder/src/index.js +20 -0
- package/dist/packages/recorder/src/labels.js +151 -0
- package/dist/packages/recorder/src/open.js +29 -0
- package/dist/packages/recorder/src/recorder.js +553 -0
- package/dist/packages/recorder/src/schedule.js +43 -0
- package/dist/packages/recorder/src/schema.js +135 -0
- package/dist/packages/recorder/src/snapshot.js +191 -0
- package/dist/packages/recorder/src/sweep.js +52 -0
- package/dist/packages/recorder/src/verdicts.js +52 -0
- package/dist/packages/tui/src/bubbles.js +415 -0
- package/dist/packages/tui/src/canvas.js +370 -0
- package/dist/packages/tui/src/chrome.js +85 -0
- package/dist/packages/tui/src/color.js +133 -0
- package/dist/packages/tui/src/flush.js +62 -0
- package/dist/packages/tui/src/fmt.js +188 -0
- package/dist/packages/tui/src/fx-trade.js +278 -0
- package/dist/packages/tui/src/fx.js +527 -0
- package/dist/packages/tui/src/images/blocks.js +141 -0
- package/dist/packages/tui/src/images/coin.js +22 -0
- package/dist/packages/tui/src/images/detect.js +140 -0
- package/dist/packages/tui/src/images/index.js +7 -0
- package/dist/packages/tui/src/images/layer.js +146 -0
- package/dist/packages/tui/src/images/pixels.js +157 -0
- package/dist/packages/tui/src/images/protocols.js +87 -0
- package/dist/packages/tui/src/index.js +18 -0
- package/dist/packages/tui/src/keymap.js +357 -0
- package/dist/packages/tui/src/layout.js +11 -0
- package/dist/packages/tui/src/raster-font.js +195 -0
- package/dist/packages/tui/src/raster.js +213 -0
- package/dist/packages/tui/src/sound.js +177 -0
- package/dist/packages/tui/src/theme.js +182 -0
- package/dist/packages/tui/src/widgets.js +598 -0
- package/dist/packages/voice/src/controller.js +407 -0
- package/dist/packages/voice/src/doctor.js +131 -0
- package/dist/packages/voice/src/download.js +184 -0
- package/dist/packages/voice/src/engines.js +84 -0
- package/dist/packages/voice/src/index.js +20 -0
- package/dist/packages/voice/src/router.js +502 -0
- package/dist/packages/voice/src/stt/apple-helper.swift.js +103 -0
- package/dist/packages/voice/src/stt/apple.js +179 -0
- package/dist/packages/voice/src/stt/fake.js +34 -0
- package/dist/packages/voice/src/stt/whisper.js +112 -0
- package/dist/packages/voice/src/summary.js +44 -0
- package/dist/packages/voice/src/tts/espeak.js +131 -0
- package/dist/packages/voice/src/tts/fake.js +30 -0
- package/dist/packages/voice/src/tts/piper.js +206 -0
- package/dist/packages/voice/src/tts/say.js +132 -0
- package/dist/packages/voice/src/tts/voices.js +206 -0
- package/dist/packages/voice/src/types.js +3 -0
- package/package.json +38 -4
- package/README.md +0 -3
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
// dexterm — live bindings for the PAPER trade ticket and the positions screen (task-1310 T0).
|
|
2
|
+
// The ticket binding fetches a warm keyless swap quote for the current order, previews the fill, and
|
|
3
|
+
// on confirm fetches a FRESH quote and asks the executor to book it (SPEC §6.1 step 6: book from the
|
|
4
|
+
// fill, not a stale on-screen quote). The positions binding marks held tokens from the keyless price
|
|
5
|
+
// feed. Both go through the data layer's rate limiter + budget meter; neither signs, holds a key, or
|
|
6
|
+
// sends a transaction — the only trade op is `executor.execute`, a TradeExecutor the UI holds behind
|
|
7
|
+
// an interface (acceptance (d)).
|
|
8
|
+
import { SOL_MINT, lamportsToSol, rawToTokens, positionPnl, shortPubkey, LiveLedger, withPriority, } from '../../../data/src/index.js';
|
|
9
|
+
import { openTicket, ticketKey, toOrder, orderValid, buyNotionalSol, capPreflight, devnetMintWarning, DEFAULT_TYPEBACK_THRESHOLD_SOL, } from "../trade.js";
|
|
10
|
+
/** task-1368: bounds on the signer's "retry in Ns" before the ticket retries a rate-limited plan by itself. */
|
|
11
|
+
export const RETRY_MIN_MS = 1_000;
|
|
12
|
+
export const RETRY_MAX_MS = 120_000;
|
|
13
|
+
/** When the signer said rate-limited but gave no wait (an older signer), retry after this. */
|
|
14
|
+
export const RETRY_DEFAULT_MS = 10_000;
|
|
15
|
+
/** The signer's rate-limit refusal, if that is what `e` is: the honest wait and its own line. */
|
|
16
|
+
export function rateLimitOf(e) {
|
|
17
|
+
if (!(e instanceof Error))
|
|
18
|
+
return null;
|
|
19
|
+
const reason = e.reason;
|
|
20
|
+
const plain = e.message.replace(/^rate-limited:\s*/, '');
|
|
21
|
+
if (reason !== 'rate-limited' && !/^rate-limited:/.test(e.message))
|
|
22
|
+
return null;
|
|
23
|
+
const given = e.retryAfterMs;
|
|
24
|
+
const parsed = /retry in (\d+)s/.exec(plain);
|
|
25
|
+
const ms = typeof given === 'number' && Number.isFinite(given) && given > 0 ? given : parsed ? Number(parsed[1]) * 1000 : RETRY_DEFAULT_MS;
|
|
26
|
+
return { ms: Math.min(RETRY_MAX_MS, Math.max(RETRY_MIN_MS, ms)), message: plain };
|
|
27
|
+
}
|
|
28
|
+
/** A signer QuoteView → the SwapQuote shape the preview fill and the ticket already understand. */
|
|
29
|
+
export function quoteFromView(v) {
|
|
30
|
+
return {
|
|
31
|
+
inputMint: v.inputMint, outputMint: v.outputMint, inAmount: BigInt(v.inAmountRaw), outAmount: BigInt(v.outAmountRaw), otherAmountThreshold: BigInt(v.minOutRaw),
|
|
32
|
+
mode: 'ExactIn', slippageBps: v.slippageBps, priceImpactPct: v.priceImpactPct, route: [...v.route], platformFeeBps: null, platformFeeAmount: null,
|
|
33
|
+
usdValue: v.usdValue, contextSlot: null, source: 'jupiter-swap', fetchedAt: v.fetchedAt,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** Default bound on the confirm-time quote: after this the ticket errors instead of hanging. */
|
|
37
|
+
export const CONFIRM_QUOTE_TIMEOUT_MS = 8_000;
|
|
38
|
+
/** A live PAPER trade ticket over one mint. */
|
|
39
|
+
export class TradeTicketLive {
|
|
40
|
+
ticket;
|
|
41
|
+
dl;
|
|
42
|
+
wallet;
|
|
43
|
+
exec;
|
|
44
|
+
now;
|
|
45
|
+
onChange;
|
|
46
|
+
threshold;
|
|
47
|
+
confirmTimeoutMs;
|
|
48
|
+
quote = null;
|
|
49
|
+
quoteAt = 0;
|
|
50
|
+
loading = false;
|
|
51
|
+
error = null;
|
|
52
|
+
receipt = null;
|
|
53
|
+
decimalsKnown;
|
|
54
|
+
seq = 0;
|
|
55
|
+
quoteTimer = null;
|
|
56
|
+
/** In-flight confirm quote; aborting it (timeout or esc) unblocks the `filling` phase. */
|
|
57
|
+
confirmAbort = null;
|
|
58
|
+
/** Set when the user cancels (esc) during `filling`, so confirm() leaves the ticket in editing. */
|
|
59
|
+
cancelled = false;
|
|
60
|
+
// ---- LIVE (task-1328)
|
|
61
|
+
liveDeps;
|
|
62
|
+
livePositionRaw;
|
|
63
|
+
onSettled;
|
|
64
|
+
signerStatus = null;
|
|
65
|
+
livePlan = null;
|
|
66
|
+
liveOutcome = null;
|
|
67
|
+
liveError = null;
|
|
68
|
+
liveProbe = false;
|
|
69
|
+
planSeq = 0;
|
|
70
|
+
// ---- task-1368: rate-limited plan → wait + auto-retry (never an error phase); the signer's SOL/USD for the preflight
|
|
71
|
+
liveRetryAt = null;
|
|
72
|
+
liveRetryMsg = null;
|
|
73
|
+
retryTimer = null;
|
|
74
|
+
liveSolUsd = null;
|
|
75
|
+
constructor(dl, wallet, exec, seed, opts = {}) {
|
|
76
|
+
this.dl = dl;
|
|
77
|
+
this.wallet = wallet;
|
|
78
|
+
this.exec = exec;
|
|
79
|
+
this.now = opts.now ?? (() => Date.now());
|
|
80
|
+
this.onChange = opts.onChange ?? (() => { });
|
|
81
|
+
this.threshold = opts.typebackThresholdSol ?? DEFAULT_TYPEBACK_THRESHOLD_SOL;
|
|
82
|
+
this.confirmTimeoutMs = opts.confirmQuoteTimeoutMs ?? CONFIRM_QUOTE_TIMEOUT_MS;
|
|
83
|
+
this.liveDeps = opts.live ?? null;
|
|
84
|
+
this.livePositionRaw = opts.livePositionRaw ?? (() => 0n);
|
|
85
|
+
this.onSettled = opts.onSettled ?? null;
|
|
86
|
+
this.decimalsKnown = typeof seed.decimals === 'number' && Number.isFinite(seed.decimals) && seed.decimals > 0;
|
|
87
|
+
this.ticket = openTicket(seed);
|
|
88
|
+
void this.ensureDecimals();
|
|
89
|
+
this.refreshQuote();
|
|
90
|
+
}
|
|
91
|
+
positionTokensRaw() {
|
|
92
|
+
if (this.ticket.mode === 'live')
|
|
93
|
+
return this.livePositionRaw(this.ticket.mint);
|
|
94
|
+
return this.wallet.position(this.ticket.mint)?.tokensRaw ?? 0n;
|
|
95
|
+
}
|
|
96
|
+
// ------------------------------------------------------------------------------------ LIVE (task-1328)
|
|
97
|
+
/** Is LIVE mode available right now? A reason string when not. */
|
|
98
|
+
liveUnavailable() {
|
|
99
|
+
if (!this.liveDeps)
|
|
100
|
+
return 'LIVE trading is not available in this run';
|
|
101
|
+
if (!this.liveDeps.linked())
|
|
102
|
+
return 'no wallet linked — /wallet link first';
|
|
103
|
+
const s = this.liveDeps.signer();
|
|
104
|
+
if (!s)
|
|
105
|
+
return 'the signer process is not available';
|
|
106
|
+
if (!s.alive)
|
|
107
|
+
return 'the signer process died — restart dexterm to trade live';
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Flip PAPER ⇄ LIVE (the user's `L`). Entering LIVE reads the signer's status (its network + caps,
|
|
112
|
+
* which the banner shows — never the UI's own flags) and refuses when no wallet is linked or the
|
|
113
|
+
* signer is down. Leaving LIVE drops any plan.
|
|
114
|
+
*/
|
|
115
|
+
setMode(mode) {
|
|
116
|
+
if (this.ticket.phase !== 'editing')
|
|
117
|
+
return;
|
|
118
|
+
if (mode === this.ticket.mode)
|
|
119
|
+
return;
|
|
120
|
+
if (mode === 'live') {
|
|
121
|
+
const why = this.liveUnavailable();
|
|
122
|
+
if (why) {
|
|
123
|
+
this.ticket.message = why;
|
|
124
|
+
this.onChange();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.ticket.mode = 'live';
|
|
128
|
+
this.liveOutcome = null;
|
|
129
|
+
this.liveError = null;
|
|
130
|
+
this.livePlan = null;
|
|
131
|
+
this.liveProbe = false;
|
|
132
|
+
this.ticket.message = 'LIVE: ⏎ builds the plan in the signer · Y on the plan signs in Phantom';
|
|
133
|
+
void this.refreshSignerStatus();
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
this.dropPlan();
|
|
137
|
+
this.ticket.mode = 'paper';
|
|
138
|
+
this.liveProbe = false;
|
|
139
|
+
this.ticket.message = 'PAPER';
|
|
140
|
+
}
|
|
141
|
+
this.refreshQuote();
|
|
142
|
+
this.onChange();
|
|
143
|
+
}
|
|
144
|
+
/** Re-read the signer's status (network, caps, panic). Null when it cannot be reached. */
|
|
145
|
+
async refreshSignerStatus() {
|
|
146
|
+
const s = this.liveDeps?.signer() ?? null;
|
|
147
|
+
if (!s) {
|
|
148
|
+
this.signerStatus = null;
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
this.signerStatus = await s.status();
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
this.signerStatus = null;
|
|
156
|
+
this.liveError = msg(e);
|
|
157
|
+
}
|
|
158
|
+
this.onChange();
|
|
159
|
+
return this.signerStatus;
|
|
160
|
+
}
|
|
161
|
+
/** LIVE ⏎: ask the signer to quote → build → decode → simulate the plan for this order. */
|
|
162
|
+
async planLive() {
|
|
163
|
+
const s = this.liveDeps?.signer() ?? null;
|
|
164
|
+
const my = ++this.planSeq;
|
|
165
|
+
this.livePlan = null;
|
|
166
|
+
this.liveOutcome = null;
|
|
167
|
+
this.liveError = null;
|
|
168
|
+
this.clearRetryTimer();
|
|
169
|
+
this.liveRetryAt = null;
|
|
170
|
+
this.liveRetryMsg = null;
|
|
171
|
+
this.onChange();
|
|
172
|
+
try {
|
|
173
|
+
if (!s || !s.alive)
|
|
174
|
+
throw new Error(this.liveUnavailable() ?? 'signer unavailable');
|
|
175
|
+
const ctx = this.ctx();
|
|
176
|
+
const order = toOrder(this.ticket, ctx);
|
|
177
|
+
if (!this.liveProbe && !orderValid(this.ticket, ctx))
|
|
178
|
+
throw new Error('order is no longer valid');
|
|
179
|
+
const [plan, status] = await Promise.all([
|
|
180
|
+
this.liveProbe ? s.probe() : s.plan({ mint: order.mint, symbol: order.symbol, side: order.side, decimals: order.decimals, amountRaw: order.amountRaw.toString(), slippageBps: order.slippageBps }),
|
|
181
|
+
s.status().catch(() => null),
|
|
182
|
+
]);
|
|
183
|
+
if (my !== this.planSeq) {
|
|
184
|
+
void s.drop(plan.hash).catch(() => { });
|
|
185
|
+
return;
|
|
186
|
+
} // cancelled meanwhile
|
|
187
|
+
this.signerStatus = status;
|
|
188
|
+
this.livePlan = plan;
|
|
189
|
+
if (this.ticket.phase === 'planning')
|
|
190
|
+
this.ticket.phase = 'plan';
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
if (my !== this.planSeq)
|
|
194
|
+
return;
|
|
195
|
+
// task-1368: Jupiter rate limiting is NOT a dead end. The ticket stays in `planning`, shows the signer's own
|
|
196
|
+
// line with the countdown, retries by itself when the wait is over, and `r` retries now / esc cancels.
|
|
197
|
+
const rl = this.ticket.phase === 'planning' ? rateLimitOf(e) : null;
|
|
198
|
+
if (rl) {
|
|
199
|
+
this.liveRetryAt = this.now() + rl.ms;
|
|
200
|
+
this.liveRetryMsg = rl.message;
|
|
201
|
+
this.ticket.message = `${rl.message} · r retries now · esc cancels`;
|
|
202
|
+
this.scheduleRetry(rl.ms, my);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
this.liveError = msg(e);
|
|
206
|
+
this.ticket.phase = 'error';
|
|
207
|
+
this.ticket.message = `plan failed: ${this.liveError}`;
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
this.onChange();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/** task-1368: the auto-retry after a rate-limited plan. Fires only if the ticket is still waiting on that very attempt. */
|
|
214
|
+
scheduleRetry(ms, seq) {
|
|
215
|
+
this.clearRetryTimer();
|
|
216
|
+
const h = setTimeout(() => {
|
|
217
|
+
this.retryTimer = null;
|
|
218
|
+
if (this.ticket.phase === 'planning' && this.planSeq === seq && this.liveRetryAt !== null)
|
|
219
|
+
void this.planLive();
|
|
220
|
+
}, Math.max(0, ms));
|
|
221
|
+
if (typeof h.unref === 'function')
|
|
222
|
+
h.unref();
|
|
223
|
+
this.retryTimer = h;
|
|
224
|
+
}
|
|
225
|
+
clearRetryTimer() {
|
|
226
|
+
if (this.retryTimer) {
|
|
227
|
+
clearTimeout(this.retryTimer);
|
|
228
|
+
this.retryTimer = null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/** task-1368: `r` while rate limited — retry the plan now instead of waiting out the countdown. No-op otherwise. */
|
|
232
|
+
retryPlan() {
|
|
233
|
+
if (this.ticket.phase !== 'planning' || this.liveRetryAt === null)
|
|
234
|
+
return;
|
|
235
|
+
void this.planLive();
|
|
236
|
+
}
|
|
237
|
+
/** Ms until the automatic retry (0 when due, null when not waiting). */
|
|
238
|
+
retryInMs() {
|
|
239
|
+
return this.liveRetryAt === null ? null : Math.max(0, this.liveRetryAt - this.now());
|
|
240
|
+
}
|
|
241
|
+
/** LIVE Y on the plan: the signer re-checks caps, opens Phantom, verifies, re-simulates, submits. */
|
|
242
|
+
async confirmLive() {
|
|
243
|
+
const s = this.liveDeps?.signer() ?? null;
|
|
244
|
+
const plan = this.livePlan;
|
|
245
|
+
try {
|
|
246
|
+
if (!s || !s.alive)
|
|
247
|
+
throw new Error(this.liveUnavailable() ?? 'signer unavailable');
|
|
248
|
+
if (!plan)
|
|
249
|
+
throw new Error('no plan to confirm');
|
|
250
|
+
const out = await s.confirm(plan.hash);
|
|
251
|
+
this.liveOutcome = out;
|
|
252
|
+
if (out.kind === 'filled') {
|
|
253
|
+
this.ticket.phase = 'filled';
|
|
254
|
+
this.ticket.message = null;
|
|
255
|
+
this.liveDeps?.onFill?.();
|
|
256
|
+
}
|
|
257
|
+
else if (out.kind === 'refused') {
|
|
258
|
+
this.ticket.phase = 'refused';
|
|
259
|
+
this.ticket.message = `${out.reason}: ${out.message}`;
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
this.ticket.phase = 'error';
|
|
263
|
+
this.ticket.message = out.error;
|
|
264
|
+
}
|
|
265
|
+
// task-1340: the settled outcome (and only that) reaches the effects. `send-unknown` is deliberately
|
|
266
|
+
// excluded: the chain may still confirm it, so there is no verdict to show.
|
|
267
|
+
const settled = settledFromConfirm(out, this.ticket.symbol, this.ticket.mint, this.liveProbe);
|
|
268
|
+
if (settled)
|
|
269
|
+
this.settle(settled);
|
|
270
|
+
}
|
|
271
|
+
catch (e) {
|
|
272
|
+
this.liveError = msg(e);
|
|
273
|
+
this.ticket.phase = 'error';
|
|
274
|
+
this.ticket.message = `confirm failed: ${this.liveError}`;
|
|
275
|
+
this.settle({ status: 'failed', side: this.ticket.side, symbol: this.ticket.symbol, mint: this.ticket.mint, sig: null, solDelta: null, pnlSol: null, pnlPct: null, ledger: 'live', reason: this.liveError });
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
void this.refreshSignerStatus();
|
|
279
|
+
this.onChange();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/** Esc while planning: forget the in-flight plan (it is dropped when it lands), or stop a rate-limit wait. */
|
|
283
|
+
cancelPlan() {
|
|
284
|
+
if (this.ticket.phase !== 'planning')
|
|
285
|
+
return;
|
|
286
|
+
this.planSeq++;
|
|
287
|
+
this.clearRetryTimer();
|
|
288
|
+
this.liveRetryAt = null;
|
|
289
|
+
this.liveRetryMsg = null;
|
|
290
|
+
this.ticket.phase = 'editing';
|
|
291
|
+
this.ticket.message = 'plan cancelled';
|
|
292
|
+
this.onChange();
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* task-1368: the order's size against the signer's caps, priced at the SIGNER's SOL/USD (the preview quote carries
|
|
296
|
+
* it; the status does too). Null on PAPER, without signer status, or for the probe.
|
|
297
|
+
*/
|
|
298
|
+
preflight() {
|
|
299
|
+
if (this.ticket.mode !== 'live' || this.liveProbe)
|
|
300
|
+
return null;
|
|
301
|
+
const st = this.signerStatus;
|
|
302
|
+
if (!st)
|
|
303
|
+
return null;
|
|
304
|
+
const sizeSol = this.ticket.side === 'buy'
|
|
305
|
+
? buyNotionalSol(this.ticket, this.wallet.balance())
|
|
306
|
+
: (this.quote && this.quote.inputMint === this.ticket.mint ? lamportsToSol(this.quote.outAmount) : 0);
|
|
307
|
+
return capPreflight({
|
|
308
|
+
side: this.ticket.side, sizeSol, solUsd: this.liveSolUsd ?? st.solUsd.price,
|
|
309
|
+
perTradeUsd: st.caps.perTradeUsd, dailyUsd: st.caps.dailyUsd, spentTodayUsd: st.spentTodayUsd,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
/** task-1368 `f`: size a LIVE buy to the cap (SOL unit). Says why when it cannot. */
|
|
313
|
+
fitToCap() {
|
|
314
|
+
const pf = this.preflight();
|
|
315
|
+
if (!pf) {
|
|
316
|
+
this.ticket.message = 'fit to cap needs the signer status (press L again to re-read it)';
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (pf.fitSizeSol === null) {
|
|
320
|
+
this.ticket.message = pf.sizeUsd === null && pf.sizeSol > 0 ? 'fit to cap needs the signer\'s SOL/USD price (not available yet)' : 'nothing fits: the cap is spent for today';
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
this.ticket.sizeUnit = 'sol';
|
|
324
|
+
this.ticket.sizeInput = String(pf.fitSizeSol);
|
|
325
|
+
this.ticket.typeback = '';
|
|
326
|
+
this.receipt = null;
|
|
327
|
+
this.liveOutcome = null;
|
|
328
|
+
this.ticket.message = `fit to cap: ${pf.fitSizeSol} SOL (≤ $${pf.capUsd.toFixed(2)} with room for fees + rent)`;
|
|
329
|
+
this.scheduleQuote(0);
|
|
330
|
+
}
|
|
331
|
+
dropPlan() {
|
|
332
|
+
const plan = this.livePlan;
|
|
333
|
+
this.livePlan = null;
|
|
334
|
+
if (plan) {
|
|
335
|
+
const s = this.liveDeps?.signer();
|
|
336
|
+
if (s?.alive)
|
|
337
|
+
void s.drop(plan.hash).catch(() => { });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/** Make this LIVE ticket the devnet PROBE (wrap-and-unwrap, fees only). The plan comes from the signer. */
|
|
341
|
+
startProbe() {
|
|
342
|
+
const why = this.liveUnavailable();
|
|
343
|
+
if (why)
|
|
344
|
+
return why;
|
|
345
|
+
this.ticket.mode = 'live';
|
|
346
|
+
this.liveProbe = true;
|
|
347
|
+
this.ticket.phase = 'planning';
|
|
348
|
+
this.ticket.message = 'devnet probe: building the wrap-and-unwrap plan…';
|
|
349
|
+
void this.planLive();
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
liveView() {
|
|
353
|
+
if (this.ticket.mode !== 'live')
|
|
354
|
+
return undefined;
|
|
355
|
+
const st = this.signerStatus;
|
|
356
|
+
return {
|
|
357
|
+
signer: st, plan: this.livePlan, outcome: this.liveOutcome, error: this.liveError,
|
|
358
|
+
linked: this.liveDeps?.linked()?.publicKey ? shortPubkey(this.liveDeps.linked().publicKey) : null, probe: this.liveProbe,
|
|
359
|
+
preflight: this.preflight(),
|
|
360
|
+
retryAt: this.liveRetryAt, retryMessage: this.liveRetryMsg,
|
|
361
|
+
devnetWarning: devnetMintWarning({ network: st?.network ?? null, probe: this.liveProbe, mint: this.ticket.mint, perTradeUsd: st?.caps.perTradeUsd, dailyUsd: st?.caps.dailyUsd }),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
notionalSol() {
|
|
365
|
+
if (this.ticket.side === 'buy')
|
|
366
|
+
return buyNotionalSol(this.ticket, this.wallet.balance());
|
|
367
|
+
// sell: SOL the warm quote would return, else mark the held slice of the position
|
|
368
|
+
if (this.quote && this.quote.inputMint === this.ticket.mint)
|
|
369
|
+
return lamportsToSol(this.quote.outAmount);
|
|
370
|
+
return 0;
|
|
371
|
+
}
|
|
372
|
+
ctx() {
|
|
373
|
+
return {
|
|
374
|
+
balanceLamports: this.wallet.balance(),
|
|
375
|
+
positionTokensRaw: this.positionTokensRaw(),
|
|
376
|
+
notionalSol: this.notionalSol(),
|
|
377
|
+
typebackThresholdSol: this.threshold,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
/** Apply a key; refetch the warm quote on a change. Returns 'confirm'/'close' for the loop. */
|
|
381
|
+
key(k) {
|
|
382
|
+
const eff = ticketKey(this.ticket, k, this.ctx());
|
|
383
|
+
if (eff === 'confirm' || eff === 'close' || eff === 'plan')
|
|
384
|
+
this.clearQuoteTimer();
|
|
385
|
+
if (eff === 'changed') {
|
|
386
|
+
this.receipt = null;
|
|
387
|
+
this.liveOutcome = null;
|
|
388
|
+
this.scheduleQuote();
|
|
389
|
+
}
|
|
390
|
+
if (eff === 'toggle-live')
|
|
391
|
+
this.setMode(this.ticket.mode === 'live' ? 'paper' : 'live');
|
|
392
|
+
if (eff === 'fit-cap')
|
|
393
|
+
this.fitToCap();
|
|
394
|
+
if (eff === 'drop-plan')
|
|
395
|
+
this.dropPlan();
|
|
396
|
+
if (eff === 'close')
|
|
397
|
+
this.dropPlan();
|
|
398
|
+
if (eff === 'plan')
|
|
399
|
+
void this.planLive();
|
|
400
|
+
if (eff === 'confirm-live')
|
|
401
|
+
void this.confirmLive();
|
|
402
|
+
return eff;
|
|
403
|
+
}
|
|
404
|
+
clearQuoteTimer() {
|
|
405
|
+
if (this.quoteTimer) {
|
|
406
|
+
clearTimeout(this.quoteTimer);
|
|
407
|
+
this.quoteTimer = null;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Debounce the warm quote so a size typed digit-by-digit fires ONE request, not one per key — the
|
|
412
|
+
* keyless `jupiter` bucket is shared with token enrichment and must not be flooded (SPEC §6.3 warm
|
|
413
|
+
* quote stream, free-forever limiter). The timer is unref'd so a pending fetch never blocks quit.
|
|
414
|
+
*/
|
|
415
|
+
scheduleQuote(delayMs = 300) {
|
|
416
|
+
this.clearQuoteTimer();
|
|
417
|
+
const h = setTimeout(() => { this.quoteTimer = null; this.refreshQuote(); }, delayMs);
|
|
418
|
+
if (typeof h.unref === 'function')
|
|
419
|
+
h.unref();
|
|
420
|
+
this.quoteTimer = h;
|
|
421
|
+
}
|
|
422
|
+
/** Token decimals drive display + SOL/token price. Pull from the enrichment cache (usually a hit). */
|
|
423
|
+
async ensureDecimals() {
|
|
424
|
+
if (this.decimalsKnown)
|
|
425
|
+
return;
|
|
426
|
+
try {
|
|
427
|
+
const r = await this.dl.store.enrich([this.ticket.mint]);
|
|
428
|
+
const d = r.rows[0]?.token.decimals;
|
|
429
|
+
if (typeof d === 'number' && d > 0) {
|
|
430
|
+
this.ticket.decimals = d;
|
|
431
|
+
this.decimalsKnown = true;
|
|
432
|
+
this.refreshQuote();
|
|
433
|
+
this.onChange();
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
catch { /* keep the default; display may be approximate until known */ }
|
|
437
|
+
}
|
|
438
|
+
/** Fetch a warm quote for the current order (debounced by a sequence guard). */
|
|
439
|
+
refreshQuote() {
|
|
440
|
+
const ctx = this.ctx();
|
|
441
|
+
if (!orderValid(this.ticket, ctx) || this.ticket.phase === 'filling' || this.liveProbe) {
|
|
442
|
+
this.quote = null;
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const order = toOrder(this.ticket, ctx);
|
|
446
|
+
const my = ++this.seq;
|
|
447
|
+
this.loading = true;
|
|
448
|
+
this.error = null;
|
|
449
|
+
const req = { inputMint: order.side === 'buy' ? SOL_MINT : order.mint, outputMint: order.side === 'buy' ? order.mint : SOL_MINT, amount: order.amountRaw, slippageBps: order.slippageBps, mode: 'ExactIn' };
|
|
450
|
+
// task-1368: a LIVE preview is quoted BY THE SIGNER (its own limiter paces preview, re-quote and build as one,
|
|
451
|
+
// and the build reuses this very quote); PAPER keeps the terminal's keyless adapter. A fake signer without
|
|
452
|
+
// `quote` falls back to the terminal's adapter. task-1387: the terminal quote runs at `high`
|
|
453
|
+
// priority so a user-initiated trade quote preempts the background screener/token enrichment on
|
|
454
|
+
// the shared 0.5 rps keyless `jupiter` bucket — otherwise the warm quote queues for seconds.
|
|
455
|
+
const s = this.ticket.mode === 'live' ? this.liveDeps?.signer() ?? null : null;
|
|
456
|
+
const viaSigner = s && s.alive && typeof s.quote === 'function'
|
|
457
|
+
? s.quote({ mint: order.mint, symbol: order.symbol, side: order.side, decimals: order.decimals, amountRaw: order.amountRaw.toString(), slippageBps: order.slippageBps })
|
|
458
|
+
.then((v) => { this.liveSolUsd = v.solUsd.price; return quoteFromView(v); })
|
|
459
|
+
: null;
|
|
460
|
+
(viaSigner ?? withPriority('high', () => this.dl.jupiterSwap.quote(req)))
|
|
461
|
+
.then((q) => { if (my !== this.seq)
|
|
462
|
+
return; this.quote = q; this.quoteAt = this.now(); this.loading = false; this.onChange(); })
|
|
463
|
+
.catch((e) => { if (my !== this.seq)
|
|
464
|
+
return; this.quote = null; this.loading = false; this.error = rateLimitOf(e)?.message ?? msg(e); this.onChange(); });
|
|
465
|
+
}
|
|
466
|
+
/** True while Jupiter's shared keyless bucket is paused (429 backoff) — drives the 'rate limited' UI. */
|
|
467
|
+
jupiterPaused() {
|
|
468
|
+
try {
|
|
469
|
+
return this.dl.limiter.bucket('jupiter').stats().pausedUntil > this.now();
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Cancel an in-flight confirm (esc during `filling`). Aborts the bounded quote wait so the booking
|
|
477
|
+
* can never trap the user, and returns the ticket to editing. No-op outside `filling`.
|
|
478
|
+
*/
|
|
479
|
+
cancel() {
|
|
480
|
+
if (this.ticket.phase !== 'filling')
|
|
481
|
+
return;
|
|
482
|
+
this.cancelled = true;
|
|
483
|
+
this.confirmAbort?.abort(abortError('confirm cancelled'));
|
|
484
|
+
this.ticket.phase = 'editing';
|
|
485
|
+
this.ticket.message = 'confirm cancelled';
|
|
486
|
+
this.loading = false;
|
|
487
|
+
this.onChange();
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Execute the armed order against a FRESH quote fetched at confirm time, then book the fill.
|
|
491
|
+
* The ticket is already in `filling`; moves to `filled` (receipt), `error`, or (on esc) back to
|
|
492
|
+
* `editing`. The quote is bounded by `confirmTimeoutMs` (task-1310 MAJOR fix): if the shared
|
|
493
|
+
* keyless `jupiter` bucket is paused after a 429, the wait is aborted and the ticket errors
|
|
494
|
+
* instead of hanging and swallowing esc/P/q.
|
|
495
|
+
*/
|
|
496
|
+
async confirm() {
|
|
497
|
+
const ctx = this.ctx();
|
|
498
|
+
const order = toOrder(this.ticket, ctx);
|
|
499
|
+
this.clearQuoteTimer();
|
|
500
|
+
this.seq++; // cancel any in-flight warm quote
|
|
501
|
+
this.cancelled = false;
|
|
502
|
+
this.loading = true;
|
|
503
|
+
const ac = new AbortController();
|
|
504
|
+
this.confirmAbort = ac;
|
|
505
|
+
const timer = setTimeout(() => ac.abort(abortError('confirm quote timed out')), this.confirmTimeoutMs);
|
|
506
|
+
if (typeof timer.unref === 'function')
|
|
507
|
+
timer.unref();
|
|
508
|
+
// Visible 'rate limited, retrying' state when the fresh quote is queued behind a 429 pause.
|
|
509
|
+
if (this.jupiterPaused())
|
|
510
|
+
this.ticket.message = 'rate limited by Jupiter — retrying…';
|
|
511
|
+
this.onChange();
|
|
512
|
+
try {
|
|
513
|
+
if (!orderValid(this.ticket, ctx))
|
|
514
|
+
throw new Error('order is no longer valid');
|
|
515
|
+
const req = { inputMint: order.side === 'buy' ? SOL_MINT : order.mint, outputMint: order.side === 'buy' ? order.mint : SOL_MINT, amount: order.amountRaw, slippageBps: order.slippageBps, mode: 'ExactIn' };
|
|
516
|
+
// task-1387: the confirm-time re-quote is the user's committed action — at `high` priority it
|
|
517
|
+
// jumps the shared keyless `jupiter` bucket ahead of background enrichment and drains within
|
|
518
|
+
// the `confirmTimeoutMs` abort, so a PAPER fill no longer errors with "quote timed out".
|
|
519
|
+
const fresh = await withPriority('high', () => this.dl.jupiterSwap.quote(req, ac.signal));
|
|
520
|
+
const costBefore = this.wallet.position(this.ticket.mint)?.costLamports ?? null; // PnL % basis for a sell
|
|
521
|
+
const rec = await this.exec.execute(order, fresh);
|
|
522
|
+
this.receipt = rec;
|
|
523
|
+
this.quote = fresh;
|
|
524
|
+
this.quoteAt = this.now();
|
|
525
|
+
this.ticket.phase = 'filled';
|
|
526
|
+
this.ticket.message = null;
|
|
527
|
+
this.settle(settledFromFill(rec, costBefore));
|
|
528
|
+
}
|
|
529
|
+
catch (e) {
|
|
530
|
+
if (this.cancelled) {
|
|
531
|
+
// esc already returned the ticket to editing; leave it there.
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
const aborted = isAbortError(e);
|
|
535
|
+
this.ticket.phase = 'error';
|
|
536
|
+
this.ticket.message = aborted
|
|
537
|
+
? (this.jupiterPaused()
|
|
538
|
+
? 'rate limited by Jupiter — quote timed out. Press any key to retry, esc to cancel.'
|
|
539
|
+
: 'quote timed out. Press any key to retry, esc to cancel.')
|
|
540
|
+
: msg(e);
|
|
541
|
+
this.error = this.ticket.message;
|
|
542
|
+
this.settle({ status: 'failed', side: this.ticket.side, symbol: this.ticket.symbol, mint: this.ticket.mint, sig: null, solDelta: null, pnlSol: null, pnlPct: null, ledger: 'paper', reason: this.ticket.message });
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
finally {
|
|
546
|
+
clearTimeout(timer);
|
|
547
|
+
this.confirmAbort = null;
|
|
548
|
+
this.loading = false;
|
|
549
|
+
this.onChange();
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
/** The single exit for a settled outcome (paper fill, live fill, refusal, failure). Never called with a quote. */
|
|
553
|
+
settle(tx) {
|
|
554
|
+
if (!this.onSettled)
|
|
555
|
+
return;
|
|
556
|
+
try {
|
|
557
|
+
this.onSettled(tx);
|
|
558
|
+
}
|
|
559
|
+
catch { /* effects never break the ticket */ }
|
|
560
|
+
}
|
|
561
|
+
view() {
|
|
562
|
+
const ctx = this.ctx();
|
|
563
|
+
const pos = this.wallet.position(this.ticket.mint);
|
|
564
|
+
const preview = this.quote && orderValid(this.ticket, ctx) ? safePreview(this.exec, toOrder(this.ticket, ctx), this.quote) : null;
|
|
565
|
+
const positionTokens = pos ? rawToTokens(pos.tokensRaw, pos.decimals) : 0;
|
|
566
|
+
return {
|
|
567
|
+
ticket: this.ticket,
|
|
568
|
+
balanceSol: lamportsToSol(this.wallet.balance()),
|
|
569
|
+
positionTokens,
|
|
570
|
+
positionValueSol: null,
|
|
571
|
+
quote: this.quote,
|
|
572
|
+
preview,
|
|
573
|
+
quoteAgeMs: this.quote ? this.now() - this.quoteAt : null,
|
|
574
|
+
loading: this.loading,
|
|
575
|
+
error: this.error,
|
|
576
|
+
receipt: this.ticket.phase === 'filled' ? this.receipt : null,
|
|
577
|
+
rateLimited: this.ticket.phase === 'filling' && this.jupiterPaused(),
|
|
578
|
+
decimalsKnown: this.decimalsKnown,
|
|
579
|
+
live: this.liveView(),
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
/** A booked PAPER fill → the settled outcome the effects read. PnL fraction uses the position's cost before the sell. */
|
|
584
|
+
export function settledFromFill(f, costBeforeLamports) {
|
|
585
|
+
const pnlSol = f.side === 'sell' ? lamportsToSol(f.realizedLamports) : 0;
|
|
586
|
+
const basis = f.side === 'sell' && costBeforeLamports !== null && costBeforeLamports > 0n ? lamportsToSol(costBeforeLamports) : null;
|
|
587
|
+
return { status: 'confirmed', side: f.side, symbol: f.symbol, mint: f.mint, sig: null, solDelta: lamportsToSol(f.solDelta), pnlSol, pnlPct: basis ? pnlSol / basis : null, ledger: 'paper' };
|
|
588
|
+
}
|
|
589
|
+
/** A signer ConfirmView → the settled outcome, or null when there is none to show (`send-unknown`, probes). */
|
|
590
|
+
export function settledFromConfirm(out, symbol, mint, probe) {
|
|
591
|
+
if (out.kind === 'send-unknown')
|
|
592
|
+
return null;
|
|
593
|
+
if (out.kind === 'filled') {
|
|
594
|
+
if (probe)
|
|
595
|
+
return null; // the devnet wrap-and-unwrap probe is not a trade
|
|
596
|
+
const f = out.fill;
|
|
597
|
+
const pnlSol = f.side === 'sell' ? lamportsToSol(BigInt(f.realizedLamports)) : 0;
|
|
598
|
+
return { status: 'confirmed', side: f.side, symbol: f.symbol || symbol, mint: f.mint || mint, sig: out.signature, solDelta: lamportsToSol(BigInt(f.solDelta)), pnlSol, pnlPct: null, ledger: 'live' };
|
|
599
|
+
}
|
|
600
|
+
const reason = out.kind === 'refused' ? `${out.reason}: ${out.message}` : out.error;
|
|
601
|
+
return { status: 'failed', side: 'buy', symbol, mint, sig: out.kind === 'send-failed' ? out.signature : null, solDelta: null, pnlSol: null, pnlPct: null, ledger: 'live', reason };
|
|
602
|
+
}
|
|
603
|
+
function safePreview(exec, order, q) {
|
|
604
|
+
try {
|
|
605
|
+
return exec.previewFill(order, q);
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
export const POSITIONS_REFRESH_MS = 10_000;
|
|
612
|
+
/** A paper ledger row → HeldPosition (bigint → number happens here). Null when nothing is held. */
|
|
613
|
+
export function heldFromPaper(p, markSol = null) {
|
|
614
|
+
if (p.tokensRaw <= 0n)
|
|
615
|
+
return null;
|
|
616
|
+
const pnl = positionPnl(p, null);
|
|
617
|
+
return {
|
|
618
|
+
mint: p.mint,
|
|
619
|
+
symbol: p.symbol,
|
|
620
|
+
side: 'long',
|
|
621
|
+
tokens: rawToTokens(p.tokensRaw, p.decimals),
|
|
622
|
+
costSol: pnl.costSol,
|
|
623
|
+
avgEntrySol: pnl.avgEntrySol,
|
|
624
|
+
realizedSol: pnl.realizedSol,
|
|
625
|
+
markSol,
|
|
626
|
+
stopUsd: null,
|
|
627
|
+
targetUsd: null,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
const finitePos = (n) => typeof n === 'number' && Number.isFinite(n) && n > 0;
|
|
631
|
+
/**
|
|
632
|
+
* Price a held position at the screen's live tick. `tickUsd` is the same USD price the header shows;
|
|
633
|
+
* `solUsd` converts the SOL ledger. The SOL ledger is the truth: USD figures are SOL figures × the
|
|
634
|
+
* current SOL/USD, so value − cost = unrealized holds in both units.
|
|
635
|
+
*/
|
|
636
|
+
export function positionRow(h, tickUsd, solUsd) {
|
|
637
|
+
const sol = finitePos(solUsd) ? solUsd : null;
|
|
638
|
+
const tick = finitePos(tickUsd) ? tickUsd : null;
|
|
639
|
+
const mark = finitePos(h.markSol) ? h.markSol : null;
|
|
640
|
+
const priceSol = tick !== null && sol !== null ? tick / sol : mark;
|
|
641
|
+
const priceUsd = tick ?? (mark !== null && sol !== null ? mark * sol : null);
|
|
642
|
+
const valueSol = priceSol !== null ? h.tokens * priceSol : null;
|
|
643
|
+
const dir = h.side === 'long' ? 1 : -1;
|
|
644
|
+
const unrealizedSol = valueSol !== null ? dir * (valueSol - h.costSol) : null;
|
|
645
|
+
const toUsd = (n) => (n !== null && sol !== null ? n * sol : null);
|
|
646
|
+
const dist = (lvl) => (finitePos(lvl) && priceUsd !== null ? (lvl - priceUsd) / priceUsd : null);
|
|
647
|
+
return {
|
|
648
|
+
side: h.side,
|
|
649
|
+
symbol: h.symbol,
|
|
650
|
+
tokens: h.tokens,
|
|
651
|
+
priceSol,
|
|
652
|
+
priceUsd,
|
|
653
|
+
valueSol,
|
|
654
|
+
valueUsd: priceUsd !== null ? h.tokens * priceUsd : null,
|
|
655
|
+
avgEntrySol: h.avgEntrySol,
|
|
656
|
+
avgEntryUsd: toUsd(h.avgEntrySol),
|
|
657
|
+
unrealizedSol,
|
|
658
|
+
unrealizedUsd: toUsd(unrealizedSol),
|
|
659
|
+
unrealizedPct: unrealizedSol !== null && h.costSol > 0 ? unrealizedSol / h.costSol : null,
|
|
660
|
+
realizedSol: h.realizedSol,
|
|
661
|
+
realizedUsd: toUsd(h.realizedSol),
|
|
662
|
+
stopUsd: finitePos(h.stopUsd) ? h.stopUsd : null,
|
|
663
|
+
stopDistPct: dist(h.stopUsd),
|
|
664
|
+
targetUsd: finitePos(h.targetUsd) ? h.targetUsd : null,
|
|
665
|
+
targetDistPct: dist(h.targetUsd),
|
|
666
|
+
basis: tick !== null ? 'tick' : mark !== null ? 'mark' : 'none',
|
|
667
|
+
solUsd: sol,
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
/** How long a `held()` lookup is reused before re-reading the ledger (the render loop calls it per frame). */
|
|
671
|
+
export const HELD_MEMO_MS = 250;
|
|
672
|
+
/** Minimum spacing of the token screen's automatic mark pass for a held coin missing SOL/USD or a mark. */
|
|
673
|
+
export const HELD_AUTOMARK_MS = 30_000;
|
|
674
|
+
/** Live PAPER positions + PnL: marks held tokens from the keyless price feed. */
|
|
675
|
+
export class PositionsLive {
|
|
676
|
+
kind = 'paper';
|
|
677
|
+
dl;
|
|
678
|
+
wallet;
|
|
679
|
+
onChange;
|
|
680
|
+
now;
|
|
681
|
+
/** mint → current price in SOL per whole token. */
|
|
682
|
+
marks = new Map();
|
|
683
|
+
solPriceUsd = null;
|
|
684
|
+
loading = false;
|
|
685
|
+
heldMemo = new Map();
|
|
686
|
+
lastAutoMark = -Infinity;
|
|
687
|
+
errors = {};
|
|
688
|
+
constructor(dl, wallet, opts = {}) {
|
|
689
|
+
this.dl = dl;
|
|
690
|
+
this.wallet = wallet;
|
|
691
|
+
this.onChange = opts.onChange ?? (() => { });
|
|
692
|
+
this.now = opts.now ?? (() => Date.now());
|
|
693
|
+
}
|
|
694
|
+
setSolPrice(p) {
|
|
695
|
+
if (typeof p === 'number' && Number.isFinite(p) && p > 0)
|
|
696
|
+
this.solPriceUsd = p;
|
|
697
|
+
}
|
|
698
|
+
/** PositionSource: SOL/USD from the last mark pass (or setSolPrice). */
|
|
699
|
+
solUsd() {
|
|
700
|
+
return this.solPriceUsd;
|
|
701
|
+
}
|
|
702
|
+
/** The last DexScreener mark for a mint (SOL per token), shared with the live positions source. */
|
|
703
|
+
markOf(mint) {
|
|
704
|
+
return this.marks.get(mint) ?? null;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* PositionSource: the open paper position for a mint, or null. One indexed SQLite read, reused for
|
|
708
|
+
* HELD_MEMO_MS so a 30 fps render loop does not hit the ledger every frame. A closed wallet reads as flat.
|
|
709
|
+
*/
|
|
710
|
+
held(mint) {
|
|
711
|
+
const now = this.now();
|
|
712
|
+
const memo = this.heldMemo.get(mint);
|
|
713
|
+
if (memo && now - memo.at >= 0 && now - memo.at < HELD_MEMO_MS)
|
|
714
|
+
return memo.v;
|
|
715
|
+
let v = null;
|
|
716
|
+
try {
|
|
717
|
+
const p = this.wallet.position(mint);
|
|
718
|
+
v = p ? heldFromPaper(p, this.marks.get(mint) ?? null) : null;
|
|
719
|
+
}
|
|
720
|
+
catch {
|
|
721
|
+
v = null;
|
|
722
|
+
}
|
|
723
|
+
if (this.heldMemo.size > 64)
|
|
724
|
+
this.heldMemo.clear();
|
|
725
|
+
this.heldMemo.set(mint, { at: now, v });
|
|
726
|
+
// A held coin with no SOL/USD (presets whose rows carry no SOL) or no mark yet: run the same
|
|
727
|
+
// DexScreener mark pass the positions screen uses, at most once per HELD_AUTOMARK_MS — so the
|
|
728
|
+
// token screen gets USD PnL without visiting positions. Held coins only; never on a flat mint.
|
|
729
|
+
if (v && (this.solPriceUsd === null || v.markSol === null) && now - this.lastAutoMark >= HELD_AUTOMARK_MS) {
|
|
730
|
+
this.lastAutoMark = now;
|
|
731
|
+
void this.refresh().catch(() => { });
|
|
732
|
+
}
|
|
733
|
+
return v;
|
|
734
|
+
}
|
|
735
|
+
/**
|
|
736
|
+
* Mark held tokens in SOL. Prices come from DexScreener (its own fast 300 rpm bucket), not the
|
|
737
|
+
* keyless `jupiter` bucket the screener feed and quotes share — so a mark never queues behind them.
|
|
738
|
+
* SOL is priced in the same call so a SOL-per-token mark is derivable without the feed's quote.
|
|
739
|
+
*/
|
|
740
|
+
async refresh() {
|
|
741
|
+
const held = this.wallet.positions().filter((p) => p.tokensRaw > 0n).map((p) => p.mint);
|
|
742
|
+
if (held.length === 0) {
|
|
743
|
+
this.marks.clear();
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const mints = held.includes(SOL_MINT) ? held : [...held, SOL_MINT];
|
|
747
|
+
this.loading = true;
|
|
748
|
+
this.errors = {};
|
|
749
|
+
try {
|
|
750
|
+
const byMint = await this.dl.dexscreener.tokens(mints);
|
|
751
|
+
const solUsd = byMint.get(SOL_MINT)?.pair.priceUsd ?? null;
|
|
752
|
+
if (solUsd && solUsd > 0)
|
|
753
|
+
this.solPriceUsd = solUsd;
|
|
754
|
+
if (this.solPriceUsd && this.solPriceUsd > 0) {
|
|
755
|
+
for (const m of held) {
|
|
756
|
+
if (m === SOL_MINT) {
|
|
757
|
+
this.marks.set(m, 1);
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
const usd = byMint.get(m)?.pair.priceUsd ?? null;
|
|
761
|
+
if (usd && usd > 0)
|
|
762
|
+
this.marks.set(m, usd / this.solPriceUsd);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
catch (e) {
|
|
767
|
+
this.errors.positions = msg(e);
|
|
768
|
+
}
|
|
769
|
+
finally {
|
|
770
|
+
this.loading = false;
|
|
771
|
+
this.heldMemo.clear(); // a fill or a new mark: the token screen re-reads on its next frame
|
|
772
|
+
this.onChange();
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
view() {
|
|
776
|
+
const summary = this.wallet.summary();
|
|
777
|
+
const positions = this.wallet.positions();
|
|
778
|
+
const rows = [];
|
|
779
|
+
let equity = lamportsToSol(summary.balanceLamports);
|
|
780
|
+
let totalUnrealized = 0;
|
|
781
|
+
let totalRealized = 0;
|
|
782
|
+
for (const p of positions) {
|
|
783
|
+
const mark = this.marks.get(p.mint) ?? null;
|
|
784
|
+
const pnl = positionPnl(p, mark);
|
|
785
|
+
totalRealized += pnl.realizedSol;
|
|
786
|
+
if (p.tokensRaw <= 0n && p.realizedLamports === 0n)
|
|
787
|
+
continue; // fully flat, no history interest
|
|
788
|
+
if (pnl.valueSol)
|
|
789
|
+
equity += pnl.valueSol;
|
|
790
|
+
if (pnl.unrealizedSol !== null)
|
|
791
|
+
totalUnrealized += pnl.unrealizedSol;
|
|
792
|
+
rows.push({
|
|
793
|
+
mint: p.mint,
|
|
794
|
+
symbol: p.symbol,
|
|
795
|
+
tokens: rawToTokens(p.tokensRaw, p.decimals),
|
|
796
|
+
costSol: pnl.costSol,
|
|
797
|
+
avgEntrySol: pnl.avgEntrySol,
|
|
798
|
+
priceSol: mark,
|
|
799
|
+
valueSol: mark !== null ? pnl.valueSol : null,
|
|
800
|
+
unrealizedSol: pnl.unrealizedSol,
|
|
801
|
+
unrealizedPct: pnl.unrealizedPct,
|
|
802
|
+
realizedSol: pnl.realizedSol,
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
rows.sort((a, b) => (b.valueSol ?? 0) - (a.valueSol ?? 0) || b.tokens - a.tokens);
|
|
806
|
+
const startSol = lamportsToSol(summary.startLamports);
|
|
807
|
+
return {
|
|
808
|
+
balanceSol: lamportsToSol(summary.balanceLamports),
|
|
809
|
+
startSol,
|
|
810
|
+
equitySol: equity,
|
|
811
|
+
totalUnrealizedSol: totalUnrealized,
|
|
812
|
+
totalRealizedSol: totalRealized,
|
|
813
|
+
totalPct: startSol > 0 ? (equity - startSol) / startSol : null,
|
|
814
|
+
solPriceUsd: this.solPriceUsd,
|
|
815
|
+
rows,
|
|
816
|
+
history: this.wallet.trades(100),
|
|
817
|
+
loading: this.loading,
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
// ---------------------------------------------------------------------------------------------
|
|
822
|
+
// task-1328 item 5: LIVE positions. The signer process books confirmed real fills into its live ledger
|
|
823
|
+
// file; the UI reads that file READ-ONLY (never writes it) and serves the same PositionSource interface
|
|
824
|
+
// the paper ledger does, so the token screen's position row and the popup chart show real positions
|
|
825
|
+
// through the task-1323 selector unchanged. A CombinedPositions wraps both: live wins for a mint when
|
|
826
|
+
// held live, else paper; every HeldPosition says which ledger it came from.
|
|
827
|
+
/** Live positions from the signer's ledger file (read-only). A missing file reads as flat. */
|
|
828
|
+
export class LivePositions {
|
|
829
|
+
kind = 'live';
|
|
830
|
+
path;
|
|
831
|
+
now;
|
|
832
|
+
mark;
|
|
833
|
+
memo = new Map();
|
|
834
|
+
rowsMemo = null;
|
|
835
|
+
solUsdValue = null;
|
|
836
|
+
constructor(path, opts = {}) {
|
|
837
|
+
this.path = path;
|
|
838
|
+
this.now = opts.now ?? (() => Date.now());
|
|
839
|
+
this.mark = opts.mark ?? (() => null);
|
|
840
|
+
if (opts.solUsd)
|
|
841
|
+
this.solUsdFn = opts.solUsd;
|
|
842
|
+
}
|
|
843
|
+
solUsdFn = () => this.solUsdValue;
|
|
844
|
+
setSolPrice(p) { if (typeof p === 'number' && Number.isFinite(p) && p > 0)
|
|
845
|
+
this.solUsdValue = p; }
|
|
846
|
+
solUsd() { return this.solUsdFn(); }
|
|
847
|
+
/** All positions (open + closed) from the ledger file, memoised HELD_MEMO_MS. Unreadable → []. */
|
|
848
|
+
positions() {
|
|
849
|
+
const now = this.now();
|
|
850
|
+
if (this.rowsMemo && now - this.rowsMemo.at >= 0 && now - this.rowsMemo.at < HELD_MEMO_MS)
|
|
851
|
+
return this.rowsMemo.rows;
|
|
852
|
+
let rows = [];
|
|
853
|
+
try {
|
|
854
|
+
const l = new LiveLedger({ path: this.path, readOnly: true });
|
|
855
|
+
try {
|
|
856
|
+
rows = l.positions();
|
|
857
|
+
}
|
|
858
|
+
finally {
|
|
859
|
+
l.close();
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
catch {
|
|
863
|
+
rows = [];
|
|
864
|
+
}
|
|
865
|
+
this.rowsMemo = { at: now, rows };
|
|
866
|
+
return rows;
|
|
867
|
+
}
|
|
868
|
+
/** Confirmed live fills (newest first) for the popup markers / history; probes excluded. */
|
|
869
|
+
records(limit = 100, mint) {
|
|
870
|
+
try {
|
|
871
|
+
const l = new LiveLedger({ path: this.path, readOnly: true });
|
|
872
|
+
try {
|
|
873
|
+
return l.records(limit, mint);
|
|
874
|
+
}
|
|
875
|
+
finally {
|
|
876
|
+
l.close();
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
catch {
|
|
880
|
+
return [];
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
held(mint) {
|
|
884
|
+
const now = this.now();
|
|
885
|
+
const m = this.memo.get(mint);
|
|
886
|
+
if (m && now - m.at >= 0 && now - m.at < HELD_MEMO_MS)
|
|
887
|
+
return m.v;
|
|
888
|
+
const p = this.positions().find((x) => x.mint === mint) ?? null;
|
|
889
|
+
const v = p ? heldFromPaper(p, this.mark(mint)) : null;
|
|
890
|
+
if (v)
|
|
891
|
+
v.kind = 'live';
|
|
892
|
+
if (this.memo.size > 64)
|
|
893
|
+
this.memo.clear();
|
|
894
|
+
this.memo.set(mint, { at: now, v });
|
|
895
|
+
return v;
|
|
896
|
+
}
|
|
897
|
+
/** Forget memoised reads (after a fill). */
|
|
898
|
+
invalidate() { this.memo.clear(); this.rowsMemo = null; }
|
|
899
|
+
}
|
|
900
|
+
/** Paper + live behind one PositionSource: live wins for a mint when held live, else paper. */
|
|
901
|
+
export class CombinedPositions {
|
|
902
|
+
kind = 'paper';
|
|
903
|
+
paper;
|
|
904
|
+
live;
|
|
905
|
+
constructor(paper, live) { this.paper = paper; this.live = live; }
|
|
906
|
+
held(mint) {
|
|
907
|
+
const l = this.live?.held(mint) ?? null;
|
|
908
|
+
if (l)
|
|
909
|
+
return l;
|
|
910
|
+
const p = this.paper.held(mint);
|
|
911
|
+
if (p)
|
|
912
|
+
p.kind = 'paper';
|
|
913
|
+
return p;
|
|
914
|
+
}
|
|
915
|
+
solUsd() { return this.paper.solUsd() ?? this.live?.solUsd() ?? null; }
|
|
916
|
+
}
|
|
917
|
+
function msg(e) {
|
|
918
|
+
return e instanceof Error ? e.message : String(e);
|
|
919
|
+
}
|
|
920
|
+
function abortError(reason) {
|
|
921
|
+
const e = new Error(reason);
|
|
922
|
+
e.name = 'AbortError';
|
|
923
|
+
return e;
|
|
924
|
+
}
|
|
925
|
+
/** An AbortError from either our bounded confirm timeout/cancel or fetch's own abort. */
|
|
926
|
+
function isAbortError(e) {
|
|
927
|
+
return (e instanceof Error && e.name === 'AbortError')
|
|
928
|
+
|| (typeof DOMException !== 'undefined' && e instanceof DOMException && (e.name === 'AbortError' || e.name === 'TimeoutError'));
|
|
929
|
+
}
|