dexterm-cli 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/dist/packages/agents/src/audit.js +122 -0
- package/dist/packages/agents/src/bus.js +379 -0
- package/dist/packages/agents/src/executor.js +177 -0
- package/dist/packages/agents/src/gate.js +176 -0
- package/dist/packages/agents/src/identity.js +179 -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 +114 -0
- package/dist/packages/agents/src/tools.js +69 -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 +67 -0
- package/dist/packages/cli/assets/claude/plugin/skills/rug-check/SKILL.md +64 -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 +303 -0
- package/dist/packages/cli/src/agent-roster.js +26 -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 +149 -0
- package/dist/packages/cli/src/boot-state.js +62 -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 +361 -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 +509 -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 +5583 -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 +339 -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/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/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/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 +515 -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/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 +208 -0
- package/dist/packages/data/src/intel/helius-history.js +59 -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/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 +170 -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 +247 -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 +849 -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 +19 -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/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 +15 -0
- package/dist/packages/tui/src/keymap.js +355 -0
- package/dist/packages/tui/src/layout.js +11 -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 +395 -0
- package/dist/packages/voice/src/doctor.js +63 -0
- package/dist/packages/voice/src/engines.js +44 -0
- package/dist/packages/voice/src/index.js +16 -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 +102 -0
- package/dist/packages/voice/src/summary.js +44 -0
- package/dist/packages/voice/src/tts/fake.js +30 -0
- package/dist/packages/voice/src/tts/say.js +96 -0
- package/dist/packages/voice/src/types.js +3 -0
- package/package.json +42 -4
- package/README.md +0 -3
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
import { SourceUnavailableError, RateDeferredError, extractLinks, bestPairByMint, aggregateCandles, withPriority, CANDLE_BASE, CANDLE_BASE_INTERVALS, CANDLE_TIMEFRAMES } from '../../../data/src/index.js';
|
|
2
|
+
import { cleanText, deriveFlags, impliedSpark } from "../screener/rows.js";
|
|
3
|
+
export const PANELS = ['market', 'candles', 'authorities', 'holders', 'rugcheck', 'goplus', 'trades'];
|
|
4
|
+
export const INTERVALS = ['1m', '5m', '15m', '1h', '4h', '1d'];
|
|
5
|
+
/** task-1314: one big fetch per base interval (1m, 1h) covers every tab via local aggregation. */
|
|
6
|
+
export const DEFAULT_CANDLE_BASE_LIMIT = 1000;
|
|
7
|
+
/** How long the candle/trades loader waits for DexScreener's pair before asking GeckoTerminal for the pool (task-1323). */
|
|
8
|
+
export const POOL_WAIT_MS = 1_500;
|
|
9
|
+
/** Refresh cadence for the open token (SPEC §3.4 says 3 s for the selected token; the cache TTLs do the real pacing). */
|
|
10
|
+
export const TOKEN_REFRESH_MS = 10_000;
|
|
11
|
+
const idle = () => ({ state: 'idle', via: null, reason: null, fetchedAt: null, stale: false });
|
|
12
|
+
export function emptyView(row, interval = '1h') {
|
|
13
|
+
const panels = Object.fromEntries(PANELS.map((p) => [p, idle()]));
|
|
14
|
+
return {
|
|
15
|
+
mint: row.mint, symbol: row.symbol, name: row.name, launchpad: row.launchpad, pair: row.pair ?? null,
|
|
16
|
+
price: row.price ?? null, chg: { m5: row.chg?.m5 ?? null, h1: row.chg?.h1 ?? null, h6: row.chg?.h6 ?? null, h24: row.chg?.h24 ?? null },
|
|
17
|
+
mcap: row.mcap ?? null, mcapFlagged: row.mcapFlagged ?? false, fdv: row.fdv ?? null, liq: row.liq ?? null, vol24: row.vol24 ?? null,
|
|
18
|
+
holders: row.holders ?? null, holdersChgPct: row.holdersChgPct ?? null, org: row.org ?? null, ageMin: row.ageMin ?? null,
|
|
19
|
+
socials: { x: false, tg: false, web: false }, socialLinks: { web: null, x: null, tg: null }, verified: row.verified ?? false, sus: row.sus ?? false, image: row.image ?? null, spark: row.spark,
|
|
20
|
+
interval, candles: [], holdersTop: [], holdersSample: null, top10: row.top10 ?? null, dev: row.dev ?? null, devMints: null,
|
|
21
|
+
lpLocked: row.lpLocked ?? null, mintAuth: row.mintAuth ?? null, freezeAuth: row.freezeAuth ?? null, tokenProgram: null, riskyExtensions: [],
|
|
22
|
+
rug: null, goplus: null, trades: [], snipers: null, bundled: null, flags: row.flags ?? [], panels, updatedAt: null, sources: [],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function reasonOf(err) {
|
|
26
|
+
if (err instanceof SourceUnavailableError) {
|
|
27
|
+
const m = /is (needs-key|disabled|degraded|probing)/.exec(err.message);
|
|
28
|
+
const st = m?.[1];
|
|
29
|
+
return { state: st === 'needs-key' ? 'needs-key' : 'error', reason: st === 'needs-key' ? 'needs key' : st === 'disabled' ? `${err.source} off` : `${err.source} degraded` };
|
|
30
|
+
}
|
|
31
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32
|
+
const http = /HTTP (\d{3})/.exec(msg);
|
|
33
|
+
if (http)
|
|
34
|
+
return { state: 'error', reason: http[1] === '429' ? 'rate limited (429)' : `HTTP ${http[1]}` };
|
|
35
|
+
if (/no recorded fixture/.test(msg))
|
|
36
|
+
return { state: 'error', reason: 'not in recording' };
|
|
37
|
+
if (/budget exhausted/.test(msg))
|
|
38
|
+
return { state: 'error', reason: 'request budget exhausted' };
|
|
39
|
+
// never echo URLs (they can carry keys and are too long for a caption): keep the host at most
|
|
40
|
+
const clean = msg.replace(/https?:\/\/([^/\s?]+)[^\s]*/g, '$1').replace(/\s+/g, ' ').trim();
|
|
41
|
+
return { state: 'error', reason: clean.slice(0, 60) };
|
|
42
|
+
}
|
|
43
|
+
function holdersFrom(h) {
|
|
44
|
+
// task-1309: keyless aggregate — no ranked list, just count + top-10 share from Jupiter.
|
|
45
|
+
if (h.aggregateOnly) {
|
|
46
|
+
const count = typeof h.holderCount === 'number' ? h.holderCount.toLocaleString('en-US') : '?';
|
|
47
|
+
const top10 = h.top10Pct !== null ? `top-10 ${h.top10Pct.toFixed(1)}%` : 'top-10 n/a';
|
|
48
|
+
return { top: [], sample: `${count} holders · ${top10} · list needs a free RPC key` };
|
|
49
|
+
}
|
|
50
|
+
const top = h.holders.map((x) => ({ address: x.address, pct: x.pct, amount: x.amount, label: x.label ?? null, tokenAccountOnly: x.owner === null }));
|
|
51
|
+
const share = top.reduce((s, x) => s + x.pct, 0);
|
|
52
|
+
return { top, sample: `top ${top.length} · ${share.toFixed(1)}% of supply` };
|
|
53
|
+
}
|
|
54
|
+
/** One open token. `load()` refreshes every panel through the cache (cheap when within TTL). */
|
|
55
|
+
export class TokenLive {
|
|
56
|
+
dl;
|
|
57
|
+
view;
|
|
58
|
+
now;
|
|
59
|
+
onChange;
|
|
60
|
+
candleLimit;
|
|
61
|
+
candleBaseLimit;
|
|
62
|
+
prefetchCandles;
|
|
63
|
+
loading = null;
|
|
64
|
+
generation = 0;
|
|
65
|
+
/** Per-interval candles kept in memory: native base fetches (1m, 1h) plus their local derivations,
|
|
66
|
+
* so a timeframe switch paints instantly from memory while a background refresh reconciles (task-1314). */
|
|
67
|
+
bars = new Map();
|
|
68
|
+
prefetching = null;
|
|
69
|
+
/** The current load's market (DexScreener) leg, so the pool lookup can wait for its pair (task-1323). */
|
|
70
|
+
marketLoad = null;
|
|
71
|
+
constructor(dl, row, opts = {}) {
|
|
72
|
+
this.dl = dl;
|
|
73
|
+
this.view = emptyView(row, opts.interval ?? '1h');
|
|
74
|
+
this.now = opts.now ?? (() => Date.now());
|
|
75
|
+
this.onChange = opts.onChange;
|
|
76
|
+
this.candleLimit = opts.candleLimit ?? 120;
|
|
77
|
+
this.candleBaseLimit = opts.candleBaseLimit ?? DEFAULT_CANDLE_BASE_LIMIT;
|
|
78
|
+
this.prefetchCandles = opts.prefetchCandles ?? true;
|
|
79
|
+
}
|
|
80
|
+
get mint() {
|
|
81
|
+
return this.view.mint;
|
|
82
|
+
}
|
|
83
|
+
setInterval(iv) {
|
|
84
|
+
if (this.view.interval === iv)
|
|
85
|
+
return Promise.resolve();
|
|
86
|
+
this.view.interval = iv;
|
|
87
|
+
// Progressive paint: show the cached or locally-derived series instantly, never a blank chart,
|
|
88
|
+
// while the background refresh reconciles. Only a truly cold tab (no base cached yet) shows the
|
|
89
|
+
// loading marker (task-1314).
|
|
90
|
+
if (!this.applyInterval(iv)) {
|
|
91
|
+
this.view.candles = [];
|
|
92
|
+
this.view.panels.candles = { ...idle(), state: 'loading' };
|
|
93
|
+
}
|
|
94
|
+
this.emit();
|
|
95
|
+
return this.loadCandles(++this.generation);
|
|
96
|
+
}
|
|
97
|
+
/** Paint `view.candles` for `iv` from the in-memory base/derived cache. Returns false when nothing is
|
|
98
|
+
* cached yet (cold: the caller shows the loading marker and a fetch fills it in). */
|
|
99
|
+
applyInterval(iv) {
|
|
100
|
+
const got = this.candlesFor(iv);
|
|
101
|
+
if (!got || got.candles.length === 0)
|
|
102
|
+
return false;
|
|
103
|
+
this.view.candles = got.candles.slice(-this.candleLimit);
|
|
104
|
+
this.view.panels.candles = { state: 'ok', via: 'geckoterminal', reason: null, fetchedAt: got.fetchedAt, stale: got.stale };
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
/** Cached series for `iv`: a direct base hit, else derived on demand from its base interval. */
|
|
108
|
+
candlesFor(iv) {
|
|
109
|
+
const direct = this.bars.get(iv);
|
|
110
|
+
if (direct)
|
|
111
|
+
return direct;
|
|
112
|
+
const base = CANDLE_BASE[iv];
|
|
113
|
+
if (!base)
|
|
114
|
+
return null;
|
|
115
|
+
const src = this.bars.get(base);
|
|
116
|
+
if (!src || src.candles.length === 0)
|
|
117
|
+
return null;
|
|
118
|
+
const candles = aggregateCandles(src.candles, CANDLE_TIMEFRAMES[base].periodMs, CANDLE_TIMEFRAMES[iv].periodMs);
|
|
119
|
+
if (candles.length === 0)
|
|
120
|
+
return null;
|
|
121
|
+
const entry = { candles, fetchedAt: src.fetchedAt, stale: src.stale, derived: true };
|
|
122
|
+
this.bars.set(iv, entry);
|
|
123
|
+
return entry;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* task-1325: the full cached series for `iv` for the chart popup — a base hit or a local derivation
|
|
127
|
+
* (5m/15m from 1m, 4h/1d from 1h), NEVER a fetch. null while that base is not in memory yet (the popup
|
|
128
|
+
* shows "warming" and picks it up on the next tick, once the terminal's own load/prefetch fills it).
|
|
129
|
+
*/
|
|
130
|
+
cachedSeries(iv) {
|
|
131
|
+
const got = this.candlesFor(iv);
|
|
132
|
+
return got && got.candles.length > 0 ? got : null;
|
|
133
|
+
}
|
|
134
|
+
/** Store a freshly fetched base series and recompute every tab that derives from it. */
|
|
135
|
+
setBase(base, candles, fetchedAt, stale) {
|
|
136
|
+
this.bars.set(base, { candles, fetchedAt, stale, derived: false });
|
|
137
|
+
for (const iv of INTERVALS) {
|
|
138
|
+
if (CANDLE_BASE[iv] === base)
|
|
139
|
+
this.bars.delete(iv); // drop the stale derivation; recomputed lazily
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Warm both base intervals (1m, 1h) in the background so every timeframe tab paints instantly from
|
|
144
|
+
* local aggregation. Cheap within the GeckoTerminal view TTL (the store serves the cached series);
|
|
145
|
+
* coalesced so concurrent callers share one pass. task-1323: LOW priority (never takes the bucket's
|
|
146
|
+
* foreground reserve, so a cold tab the user presses meanwhile goes out first), and a base already in
|
|
147
|
+
* memory is not re-fetched — the viewed base is kept fresh by load(), the other one by its own switch.
|
|
148
|
+
*/
|
|
149
|
+
prefetchAdjacent() {
|
|
150
|
+
if (this.prefetching)
|
|
151
|
+
return this.prefetching;
|
|
152
|
+
this.prefetching = (async () => {
|
|
153
|
+
const pool = this.view.pair ?? (await this.pool().catch(() => null));
|
|
154
|
+
if (!pool)
|
|
155
|
+
return;
|
|
156
|
+
for (const base of CANDLE_BASE_INTERVALS) {
|
|
157
|
+
const have = this.bars.get(base);
|
|
158
|
+
if (have && !have.derived && have.candles.length > 0)
|
|
159
|
+
continue;
|
|
160
|
+
try {
|
|
161
|
+
const s = await withPriority('low', () => this.dl.detail.candles(pool, base, { limit: this.candleBaseLimit, token: this.view.mint }));
|
|
162
|
+
this.setBase(base, s.candles, s.fetchedAt, s.stale);
|
|
163
|
+
if (this.view.interval === base || CANDLE_BASE[this.view.interval] === base) {
|
|
164
|
+
this.applyInterval(this.view.interval);
|
|
165
|
+
this.emit();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch { /* leave it; the viewed tab still has its own fetch */ }
|
|
169
|
+
}
|
|
170
|
+
})().finally(() => { this.prefetching = null; });
|
|
171
|
+
return this.prefetching;
|
|
172
|
+
}
|
|
173
|
+
emit() {
|
|
174
|
+
this.onChange?.(this.view);
|
|
175
|
+
}
|
|
176
|
+
start(p) {
|
|
177
|
+
const cur = this.view.panels[p];
|
|
178
|
+
this.view.panels[p] = { ...cur, state: cur.state === 'ok' ? 'ok' : 'loading' };
|
|
179
|
+
}
|
|
180
|
+
ok(p, via, fetchedAt, stale = false) {
|
|
181
|
+
this.view.panels[p] = { state: 'ok', via, reason: null, fetchedAt, stale };
|
|
182
|
+
}
|
|
183
|
+
fail(p, err) {
|
|
184
|
+
const r = reasonOf(err);
|
|
185
|
+
const cur = this.view.panels[p];
|
|
186
|
+
// keep stale data on screen (SWR) but say the refresh failed
|
|
187
|
+
this.view.panels[p] = { state: r.state, via: cur.via, reason: r.reason, fetchedAt: cur.fetchedAt, stale: cur.fetchedAt !== null };
|
|
188
|
+
}
|
|
189
|
+
/** Load / refresh every panel in parallel. Never throws. Coalesces concurrent calls. */
|
|
190
|
+
load() {
|
|
191
|
+
if (this.loading)
|
|
192
|
+
return this.loading;
|
|
193
|
+
const gen = ++this.generation;
|
|
194
|
+
for (const p of PANELS)
|
|
195
|
+
this.start(p);
|
|
196
|
+
this.emit();
|
|
197
|
+
const market = this.loadMarket(gen);
|
|
198
|
+
this.marketLoad = market;
|
|
199
|
+
this.loading = Promise.all([
|
|
200
|
+
market,
|
|
201
|
+
this.loadChainAndSecurity(gen),
|
|
202
|
+
this.loadPoolCandlesTrades(gen),
|
|
203
|
+
]).then(() => {
|
|
204
|
+
if (gen === this.generation) {
|
|
205
|
+
this.view.updatedAt = this.now();
|
|
206
|
+
this.emit();
|
|
207
|
+
}
|
|
208
|
+
// Warm the other base interval so every timeframe tab is instant (task-1314). Background, budgeted.
|
|
209
|
+
if (this.prefetchCandles)
|
|
210
|
+
void this.prefetchAdjacent();
|
|
211
|
+
}).finally(() => { this.loading = null; });
|
|
212
|
+
return this.loading;
|
|
213
|
+
}
|
|
214
|
+
async loadMarket(gen) {
|
|
215
|
+
const v = this.view;
|
|
216
|
+
try {
|
|
217
|
+
const res = await this.dl.store.enrich([v.mint], { price: true });
|
|
218
|
+
if (gen !== this.generation)
|
|
219
|
+
return;
|
|
220
|
+
const r = res.rows[0];
|
|
221
|
+
if (!r)
|
|
222
|
+
throw new Error(Object.values(res.errors)[0] ?? 'no market data for this mint');
|
|
223
|
+
const { token, pair, stats, audit } = r;
|
|
224
|
+
v.symbol = cleanText(token.symbol, 12) || v.symbol;
|
|
225
|
+
v.name = cleanText(token.name, 40) || v.name;
|
|
226
|
+
if (token.launchpad !== 'unknown')
|
|
227
|
+
v.launchpad = token.launchpad;
|
|
228
|
+
v.pair = v.pair ?? (pair.pairAddress || null);
|
|
229
|
+
v.price = pair.priceUsd > 0 ? pair.priceUsd : r.price?.usdPrice ?? v.price;
|
|
230
|
+
v.chg = { m5: pair.priceChange.m5, h1: pair.priceChange.h1, h6: pair.priceChange.h6, h24: pair.priceChange.h24 };
|
|
231
|
+
// task-1311 (QA M9): a fresh pump.fun pair has no indexed pool yet, so these come back null.
|
|
232
|
+
// Keep what the new-pairs stream already seeded (mcap/price/age) instead of blanking the screen.
|
|
233
|
+
v.mcap = r.mcap && r.mcap.value != null ? r.mcap.value : (pair.mcap ?? pair.fdv ?? v.mcap);
|
|
234
|
+
v.mcapFlagged = r.mcap?.flagged ?? false;
|
|
235
|
+
v.fdv = pair.fdv ?? v.fdv;
|
|
236
|
+
v.liq = pair.liquidityUsd ?? v.liq;
|
|
237
|
+
v.vol24 = pair.volume.h24 ?? v.vol24;
|
|
238
|
+
v.holders = stats?.holderCount ?? v.holders;
|
|
239
|
+
v.holdersChgPct = stats?.holders24hDelta ?? null;
|
|
240
|
+
v.org = stats?.organicScore ?? null;
|
|
241
|
+
const created = token.createdAt ?? pair.createdAt;
|
|
242
|
+
v.ageMin = created ? Math.max(0, (this.now() - created) / 60000) : v.ageMin;
|
|
243
|
+
v.socials = { x: !!token.socials.x, tg: !!token.socials.tg, web: !!token.socials.web };
|
|
244
|
+
// task-1315: keep the real hrefs so the token screen's W/X/T keys can open them (after scheme check).
|
|
245
|
+
v.socialLinks = { web: token.socials.web ?? null, x: token.socials.x ?? null, tg: token.socials.tg ?? null };
|
|
246
|
+
try {
|
|
247
|
+
const raw = await this.dl.dexscreener.tokensRaw([v.mint]);
|
|
248
|
+
if (gen !== this.generation)
|
|
249
|
+
return;
|
|
250
|
+
const pair = bestPairByMint(raw.pairs, [v.mint]).get(v.mint);
|
|
251
|
+
const links = extractLinks({ dexscreener: pair, jupiter: { website: token.socials.web, twitter: token.socials.x, telegram: token.socials.tg } });
|
|
252
|
+
v.socialLinks = { web: links.website, x: links.x, tg: links.telegram, all: links.all };
|
|
253
|
+
}
|
|
254
|
+
catch { /* retain the enriched primary links when the full list is unavailable */ }
|
|
255
|
+
v.verified = token.verified;
|
|
256
|
+
v.sus = token.sus;
|
|
257
|
+
if (typeof token.image === 'string' && /^https?:\/\//i.test(token.image))
|
|
258
|
+
v.image = token.image;
|
|
259
|
+
v.spark = r.sparkline ?? impliedSpark(v.price, v.chg);
|
|
260
|
+
// Jupiter's audit fills what chain truth has not (yet): the chain panel overwrites these when it lands.
|
|
261
|
+
if (v.panels.authorities.state !== 'ok') {
|
|
262
|
+
v.mintAuth = audit?.mintAuthority ?? v.mintAuth;
|
|
263
|
+
v.freezeAuth = audit?.freezeAuthority ?? v.freezeAuth;
|
|
264
|
+
}
|
|
265
|
+
if (v.panels.holders.state !== 'ok')
|
|
266
|
+
v.top10 = audit?.top10Pct ?? v.top10;
|
|
267
|
+
v.dev = audit?.devPct ?? null;
|
|
268
|
+
v.devMints = audit?.devMints ?? null;
|
|
269
|
+
v.sources = [...new Set([...v.sources, ...r.sources])];
|
|
270
|
+
const SHORT = { dexscreener: 'ds', 'jupiter-tokens': 'jup', 'jupiter-price': 'jup-price' };
|
|
271
|
+
this.ok('market', r.sources.map((x) => SHORT[x] ?? x).join('+'), pair.fetchedAt, r.stale);
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
if (gen !== this.generation)
|
|
275
|
+
return;
|
|
276
|
+
this.fail('market', err);
|
|
277
|
+
}
|
|
278
|
+
this.recomputeFlags();
|
|
279
|
+
this.emit();
|
|
280
|
+
}
|
|
281
|
+
async loadChainAndSecurity(gen) {
|
|
282
|
+
const v = this.view;
|
|
283
|
+
await Promise.all([
|
|
284
|
+
(async () => {
|
|
285
|
+
try {
|
|
286
|
+
const info = await this.dl.detail.mintInfo(v.mint);
|
|
287
|
+
if (gen !== this.generation)
|
|
288
|
+
return;
|
|
289
|
+
v.mintAuth = info.value.mintAuthority !== null;
|
|
290
|
+
v.freezeAuth = info.value.freezeAuthority !== null;
|
|
291
|
+
v.tokenProgram = info.value.program;
|
|
292
|
+
v.riskyExtensions = info.value.riskyExtensions;
|
|
293
|
+
this.ok('authorities', info.value.source, info.fetchedAt, info.stale);
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
if (gen === this.generation)
|
|
297
|
+
this.fail('authorities', err);
|
|
298
|
+
}
|
|
299
|
+
this.recomputeFlags();
|
|
300
|
+
this.emit();
|
|
301
|
+
})(),
|
|
302
|
+
(async () => {
|
|
303
|
+
try {
|
|
304
|
+
const h = await this.dl.detail.holders(v.mint);
|
|
305
|
+
if (gen !== this.generation)
|
|
306
|
+
return;
|
|
307
|
+
const { top, sample } = holdersFrom(h.value);
|
|
308
|
+
v.holdersTop = top;
|
|
309
|
+
v.holdersSample = sample;
|
|
310
|
+
v.top10 = h.value.top10Pct;
|
|
311
|
+
this.ok('holders', h.value.source, h.fetchedAt, h.stale);
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
if (gen === this.generation)
|
|
315
|
+
this.fail('holders', err);
|
|
316
|
+
}
|
|
317
|
+
this.recomputeFlags();
|
|
318
|
+
this.emit();
|
|
319
|
+
})(),
|
|
320
|
+
(async () => {
|
|
321
|
+
try {
|
|
322
|
+
const r = await this.dl.detail.rugcheck(v.mint);
|
|
323
|
+
if (gen !== this.generation)
|
|
324
|
+
return;
|
|
325
|
+
v.rug = { score: r.value.scoreNormalised, raw: r.value.score, risks: r.value.risks.map((x) => ({ name: x.name, level: x.level })), lpLockedPct: r.value.lpLockedPct };
|
|
326
|
+
v.lpLocked = r.value.lpLockedPct ?? (v.launchpad === 'pump.fun' ? 100 : v.lpLocked);
|
|
327
|
+
this.ok('rugcheck', 'rugcheck', r.fetchedAt, r.stale);
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
if (gen === this.generation)
|
|
331
|
+
this.fail('rugcheck', err);
|
|
332
|
+
}
|
|
333
|
+
this.recomputeFlags();
|
|
334
|
+
this.emit();
|
|
335
|
+
})(),
|
|
336
|
+
(async () => {
|
|
337
|
+
try {
|
|
338
|
+
const g = await this.dl.detail.security(v.mint);
|
|
339
|
+
if (gen !== this.generation)
|
|
340
|
+
return;
|
|
341
|
+
v.goplus = g.value;
|
|
342
|
+
if (v.panels.authorities.state !== 'ok') {
|
|
343
|
+
v.mintAuth = g.value.mintable ?? v.mintAuth;
|
|
344
|
+
v.freezeAuth = g.value.freezable ?? v.freezeAuth;
|
|
345
|
+
}
|
|
346
|
+
if (v.holders === null && g.value.holderCount !== null)
|
|
347
|
+
v.holders = g.value.holderCount;
|
|
348
|
+
this.ok('goplus', 'goplus', g.fetchedAt, g.stale);
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
if (gen === this.generation)
|
|
352
|
+
this.fail('goplus', err);
|
|
353
|
+
}
|
|
354
|
+
this.recomputeFlags();
|
|
355
|
+
this.emit();
|
|
356
|
+
})(),
|
|
357
|
+
]);
|
|
358
|
+
}
|
|
359
|
+
async pool() {
|
|
360
|
+
if (this.view.pair)
|
|
361
|
+
return this.view.pair;
|
|
362
|
+
// task-1323: a token opened by mint alone (no screener row) learns its pair from the market leg
|
|
363
|
+
// (DexScreener, fast bucket) moments later; wait briefly for it instead of spending a GeckoTerminal
|
|
364
|
+
// call — the chart's own bucket — on a pool lookup.
|
|
365
|
+
if (this.marketLoad) {
|
|
366
|
+
let t = null;
|
|
367
|
+
await Promise.race([this.marketLoad, new Promise((r) => { t = setTimeout(r, POOL_WAIT_MS); t.unref?.(); })]);
|
|
368
|
+
if (t)
|
|
369
|
+
clearTimeout(t);
|
|
370
|
+
if (this.view.pair)
|
|
371
|
+
return this.view.pair;
|
|
372
|
+
}
|
|
373
|
+
const p = await withPriority('high', () => this.dl.detail.topPool(this.view.mint));
|
|
374
|
+
if (p)
|
|
375
|
+
this.view.pair = p.address;
|
|
376
|
+
return p?.address ?? null;
|
|
377
|
+
}
|
|
378
|
+
async loadPoolCandlesTrades(gen) {
|
|
379
|
+
let pool = null;
|
|
380
|
+
try {
|
|
381
|
+
pool = await this.pool();
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
if (gen !== this.generation)
|
|
385
|
+
return;
|
|
386
|
+
this.fail('candles', err);
|
|
387
|
+
this.fail('trades', err);
|
|
388
|
+
this.emit();
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (!pool) {
|
|
392
|
+
const e = new Error('no pool found for this mint');
|
|
393
|
+
this.fail('candles', e);
|
|
394
|
+
this.fail('trades', e);
|
|
395
|
+
this.emit();
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
await Promise.all([this.loadCandles(gen), this.loadTrades(gen, pool)]);
|
|
399
|
+
}
|
|
400
|
+
async loadCandles(gen) {
|
|
401
|
+
const v = this.view;
|
|
402
|
+
const iv = v.interval;
|
|
403
|
+
// Fetch the base interval the viewed tab derives from (1m or 1h); coarser tabs never hit the network.
|
|
404
|
+
const base = CANDLE_BASE[iv] ?? iv;
|
|
405
|
+
// task-1323: an EMPTY chart is what the user is waiting on — HIGH priority, which may use the
|
|
406
|
+
// GeckoTerminal bucket's reserved slot and jumps queued background work. A chart that already shows
|
|
407
|
+
// candles (poll refresh, SWR) is LOW: it must never take that slot from the next cold tab.
|
|
408
|
+
const priority = v.candles.length === 0 ? 'high' : 'low';
|
|
409
|
+
try {
|
|
410
|
+
const pool = v.pair ?? (await this.pool());
|
|
411
|
+
if (!pool)
|
|
412
|
+
throw new Error('no pool found for this mint');
|
|
413
|
+
const s = await withPriority(priority, () => this.dl.detail.candles(pool, base, { limit: this.candleBaseLimit, token: v.mint }));
|
|
414
|
+
this.setBase(base, s.candles, s.fetchedAt, s.stale);
|
|
415
|
+
if (gen !== this.generation)
|
|
416
|
+
return; // user switched tabs mid-fetch; the cache is still warmed
|
|
417
|
+
this.applyInterval(iv);
|
|
418
|
+
// A base refresh that returned nothing (dead pool) still leaves an empty chart: surface it.
|
|
419
|
+
if (v.candles.length === 0)
|
|
420
|
+
this.fail('candles', new Error('no candles returned'));
|
|
421
|
+
}
|
|
422
|
+
catch (err) {
|
|
423
|
+
// Keep any derived/cached candles on screen (SWR); only report when the chart is actually empty.
|
|
424
|
+
if (gen === this.generation && v.candles.length === 0)
|
|
425
|
+
this.fail('candles', err);
|
|
426
|
+
}
|
|
427
|
+
this.emit();
|
|
428
|
+
}
|
|
429
|
+
async loadTrades(gen, pool) {
|
|
430
|
+
const v = this.view;
|
|
431
|
+
try {
|
|
432
|
+
// task-1323: the tape is background to the chart on the shared GeckoTerminal bucket (LOW).
|
|
433
|
+
const r = await withPriority('low', () => this.dl.detail.trades(pool, v.mint));
|
|
434
|
+
if (gen !== this.generation)
|
|
435
|
+
return;
|
|
436
|
+
v.trades = r.value.slice(0, 60).map((t) => ({ t: t.t, side: t.side, usd: t.usd, price: t.price, wallet: t.wallet }));
|
|
437
|
+
this.ok('trades', 'geckoterminal', r.fetchedAt, r.stale);
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
if (gen === this.generation)
|
|
441
|
+
this.fail('trades', err);
|
|
442
|
+
}
|
|
443
|
+
this.emit();
|
|
444
|
+
}
|
|
445
|
+
recomputeFlags() {
|
|
446
|
+
const v = this.view;
|
|
447
|
+
v.flags = deriveFlags({ mintAuth: v.mintAuth, freezeAuth: v.freezeAuth, top10: v.top10, dev: v.dev, bundled: null, lpLocked: v.lpLocked });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/** Debounce before the screener cursor's row counts as "hovered" (task-1323). */
|
|
451
|
+
export const HOVER_DEBOUNCE_MS = 350;
|
|
452
|
+
/**
|
|
453
|
+
* Soft cap on hover-prefetch requests per rolling minute (task-1323). The GeckoTerminal bucket allows 7
|
|
454
|
+
* per 60 s, but the keyless server was seen answering 429 at 6 in ~47 s; hover keeps to 3 so at least
|
|
455
|
+
* 4 slots per minute stay for the token screen (open, cold tab, tape).
|
|
456
|
+
*/
|
|
457
|
+
export const HOVER_MAX_PER_MIN = 3;
|
|
458
|
+
/**
|
|
459
|
+
* Hover prefetch for the screener (task-1323): when the cursor rests on a row for `debounceMs`, warm
|
|
460
|
+
* that token's candle bases (the token screen's default base first, then the other one) so ⏎ paints a
|
|
461
|
+
* chart instantly. Budget-safe by construction:
|
|
462
|
+
* - IDLE priority: a request is only issued when the GeckoTerminal bucket is completely idle with its
|
|
463
|
+
* foreground reserve intact — otherwise acquire() refuses at once (RateDeferredError) and nothing is
|
|
464
|
+
* sent or queued, so scrolling can never starve the token screen or build a backlog;
|
|
465
|
+
* - debounced: a fast scroll through the list only ever considers the row it stops on;
|
|
466
|
+
* - soft cap of `maxPerMin` issued requests per rolling minute;
|
|
467
|
+
* - bases already cached (fresh or stale) are skipped.
|
|
468
|
+
*/
|
|
469
|
+
export class CandlePrefetcher {
|
|
470
|
+
dl;
|
|
471
|
+
opts;
|
|
472
|
+
target = null;
|
|
473
|
+
timer = null;
|
|
474
|
+
busy = false;
|
|
475
|
+
closed = false;
|
|
476
|
+
stamps = [];
|
|
477
|
+
/** Requests actually sent (granted by the limiter). */
|
|
478
|
+
requests = 0;
|
|
479
|
+
/** Attempts the limiter refused because the bucket was not idle (nothing sent). */
|
|
480
|
+
deferred = 0;
|
|
481
|
+
constructor(dl, opts) {
|
|
482
|
+
this.dl = dl;
|
|
483
|
+
this.opts = {
|
|
484
|
+
interval: opts.interval,
|
|
485
|
+
debounceMs: opts.debounceMs ?? HOVER_DEBOUNCE_MS,
|
|
486
|
+
maxPerMin: opts.maxPerMin ?? HOVER_MAX_PER_MIN,
|
|
487
|
+
limit: opts.limit ?? DEFAULT_CANDLE_BASE_LIMIT,
|
|
488
|
+
now: opts.now ?? (() => Date.now()),
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
/** The row under the cursor changed (or null: left the screener). Restarts the debounce on a new mint. */
|
|
492
|
+
hover(t) {
|
|
493
|
+
if (this.closed)
|
|
494
|
+
return;
|
|
495
|
+
if (t && this.target && t.mint === this.target.mint && t.pair === this.target.pair)
|
|
496
|
+
return; // same row: keep waiting
|
|
497
|
+
this.target = t;
|
|
498
|
+
this.schedule(this.opts.debounceMs);
|
|
499
|
+
}
|
|
500
|
+
/** Requests sent in the trailing 60 s. */
|
|
501
|
+
rate() {
|
|
502
|
+
const now = this.opts.now();
|
|
503
|
+
while (this.stamps.length && this.stamps[0] + 60_000 <= now)
|
|
504
|
+
this.stamps.shift();
|
|
505
|
+
return this.stamps.length;
|
|
506
|
+
}
|
|
507
|
+
close() {
|
|
508
|
+
this.closed = true;
|
|
509
|
+
this.target = null;
|
|
510
|
+
if (this.timer) {
|
|
511
|
+
clearTimeout(this.timer);
|
|
512
|
+
this.timer = null;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
schedule(ms) {
|
|
516
|
+
if (this.timer) {
|
|
517
|
+
clearTimeout(this.timer);
|
|
518
|
+
this.timer = null;
|
|
519
|
+
}
|
|
520
|
+
const t = this.target;
|
|
521
|
+
if (!t || !t.pair || this.closed)
|
|
522
|
+
return;
|
|
523
|
+
this.timer = setTimeout(() => { this.timer = null; void this.step(t); }, Math.max(0, ms));
|
|
524
|
+
this.timer.unref?.();
|
|
525
|
+
}
|
|
526
|
+
bases() {
|
|
527
|
+
const iv = this.opts.interval();
|
|
528
|
+
const first = CANDLE_BASE[iv] ?? iv;
|
|
529
|
+
return [first, ...CANDLE_BASE_INTERVALS.filter((b) => b !== first)];
|
|
530
|
+
}
|
|
531
|
+
async step(t) {
|
|
532
|
+
if (this.closed || this.target !== t || !t.pair)
|
|
533
|
+
return;
|
|
534
|
+
if (this.busy) {
|
|
535
|
+
this.schedule(250);
|
|
536
|
+
return;
|
|
537
|
+
} // the previous row's request is still in flight
|
|
538
|
+
for (const base of this.bases()) {
|
|
539
|
+
const req = { limit: this.opts.limit, token: t.mint };
|
|
540
|
+
if (this.dl.detail.candleStore.has(t.pair, base, req))
|
|
541
|
+
continue;
|
|
542
|
+
if (this.rate() >= this.opts.maxPerMin) {
|
|
543
|
+
this.schedule(this.stamps[0] + 60_000 - this.opts.now() + 50);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
this.busy = true;
|
|
547
|
+
try {
|
|
548
|
+
await withPriority('idle', () => this.dl.detail.candles(t.pair, base, req));
|
|
549
|
+
this.sent();
|
|
550
|
+
}
|
|
551
|
+
catch (err) {
|
|
552
|
+
if (err instanceof RateDeferredError) {
|
|
553
|
+
this.deferred++;
|
|
554
|
+
// Try again when the bucket could be idle again (still only if the cursor stays here).
|
|
555
|
+
const wait = this.dl.limiter.bucket('geckoterminal').delayFor('idle', this.opts.now());
|
|
556
|
+
this.schedule(Number.isFinite(wait) ? Math.max(250, wait) : 60_000);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
this.sent(); // a real request that failed (HTTP error) still counts against the budget
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
finally {
|
|
563
|
+
this.busy = false;
|
|
564
|
+
}
|
|
565
|
+
if (this.closed || this.target !== t)
|
|
566
|
+
return; // the cursor moved on: its own debounce takes over
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
sent() {
|
|
570
|
+
this.requests++;
|
|
571
|
+
this.stamps.push(this.opts.now());
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
/** Short printable caption for a panel: `via helius · 12s`, `unavailable: rate limited (429)`, `needs key`. */
|
|
575
|
+
export function panelCaption(p, now, label = 'unavailable') {
|
|
576
|
+
if (p.state === 'needs-key')
|
|
577
|
+
return 'needs key';
|
|
578
|
+
if (p.state === 'loading' && p.fetchedAt === null)
|
|
579
|
+
return 'loading…';
|
|
580
|
+
if ((p.state === 'error') && p.fetchedAt === null)
|
|
581
|
+
return `${label}: ${p.reason ?? 'error'}`;
|
|
582
|
+
const age = p.fetchedAt === null ? '' : ` · ${Math.max(0, Math.round((now - p.fetchedAt) / 1000))}s`;
|
|
583
|
+
const stale = p.state === 'error' ? ` · stale (${p.reason ?? 'refresh failed'})` : p.stale ? ' · stale' : '';
|
|
584
|
+
return `${p.via ? `via ${p.via}` : ''}${age}${stale}`.replace(/^ · /, '');
|
|
585
|
+
}
|