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,632 @@
|
|
|
1
|
+
// dexterm T1 — transaction DECODER + signer-side policy (SPEC §6.2 CL-04; task-1310 round 2, HIGH #1).
|
|
2
|
+
//
|
|
3
|
+
// The executor used to bind the confirm to a hash of the tx bytes but never looked INSIDE them. A
|
|
4
|
+
// hostile or changed Jupiter response could therefore hand us a transaction that moves something other
|
|
5
|
+
// than the plan (a drain, an Approve, a SetAuthority) and, as long as the bytes were stable across the
|
|
6
|
+
// wallet round trip, it was submitted. This module parses the Solana wire format (legacy + v0) with
|
|
7
|
+
// no external library and applies the §6.2 policy as a POSITIVE description of what a Jupiter swap IS
|
|
8
|
+
// (round 3 — an independent verifier showed the round-2 blacklist never required the transaction to be a
|
|
9
|
+
// swap, so a top-level SPL `Transfer`/`Burn` of the user's tokens to an attacker was signed and submitted
|
|
10
|
+
// while the SOL-only simulated delta saw just the fee leave the wallet):
|
|
11
|
+
//
|
|
12
|
+
// * every top-level instruction's program must be on a small ALLOW-LIST (System, Token, Token-2022,
|
|
13
|
+
// Associated Token, Compute Budget, Memo, Jupiter v6); any unknown program → refuse. DEX programs are
|
|
14
|
+
// reached only by CPI from the route, so a DEX called directly at the top level → refuse. A program
|
|
15
|
+
// id loaded from an address-lookup table → refuse (Solana itself forbids it, and we could not name it
|
|
16
|
+
// without fetching the table).
|
|
17
|
+
// * exactly ONE Jupiter instruction, and it must be a route-family instruction (by Anchor
|
|
18
|
+
// discriminator). None → not a swap. Two → a second leg the plan never showed → refuse.
|
|
19
|
+
// * Token / Token-2022 at the top level: ONLY `SyncNative`, `InitializeAccount*` for the user, and
|
|
20
|
+
// `CloseAccount` with both the destination and the authority the user (Jupiter unwraps WSOL this
|
|
21
|
+
// way). `Transfer`, `TransferChecked`, `Burn`, `BurnChecked`, `Approve`, `ApproveChecked`,
|
|
22
|
+
// `SetAuthority`, `Revoke`, `MintTo`, `Freeze`/`Thaw` — anything else → refuse.
|
|
23
|
+
// * System: ONLY `Transfer` from the user into the user's OWN wrapped-SOL account, i.e. an account that
|
|
24
|
+
// is both `SyncNative`d and closed back to the user in the same transaction (the wrap step of
|
|
25
|
+
// wrapAndUnwrapSol). Any other destination, source, or System instruction → refuse.
|
|
26
|
+
// * Associated Token: ONLY `Create` / `CreateIdempotent` with the user as the owner.
|
|
27
|
+
// * the fee payer (static key 0) must be the user, and the user must be a required signer.
|
|
28
|
+
//
|
|
29
|
+
// Pure functions, no I/O, no keys. Everything fails CLOSED: a transaction we cannot fully parse, or whose
|
|
30
|
+
// shape we did not anticipate, is a transaction we refuse to sign.
|
|
31
|
+
import { createPublicKey, verify as edVerify } from 'node:crypto';
|
|
32
|
+
import { base58, TOKEN_PROGRAM, TOKEN_2022_PROGRAM } from "../sources/solana-rpc.js";
|
|
33
|
+
export { TOKEN_PROGRAM, TOKEN_2022_PROGRAM };
|
|
34
|
+
/**
|
|
35
|
+
* Verify an Ed25519 signature over `message` by the raw 32-byte public key (node:crypto only; no key is
|
|
36
|
+
* generated or stored). Shared by the wallet bridge (link proof) and the executor (signed-tx check).
|
|
37
|
+
*/
|
|
38
|
+
export function verifyEd25519(pubkey, message, signature) {
|
|
39
|
+
if (pubkey.length !== 32 || signature.length !== 64)
|
|
40
|
+
return false;
|
|
41
|
+
const spki = Buffer.concat([Buffer.from('302a300506032b6570032100', 'hex'), pubkey]);
|
|
42
|
+
const keyObj = createPublicKey({ key: spki, format: 'der', type: 'spki' });
|
|
43
|
+
return edVerify(null, message, keyObj, signature);
|
|
44
|
+
}
|
|
45
|
+
export const SYSTEM_PROGRAM = '11111111111111111111111111111111';
|
|
46
|
+
export const ASSOCIATED_TOKEN_PROGRAM = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL';
|
|
47
|
+
export const COMPUTE_BUDGET_PROGRAM = 'ComputeBudget111111111111111111111111111111';
|
|
48
|
+
export const MEMO_PROGRAM = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr';
|
|
49
|
+
export const JUPITER_V6_PROGRAM = 'JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4';
|
|
50
|
+
/**
|
|
51
|
+
* DEX programs Jupiter v6 routes through — documentation of the CPI targets a route may name as
|
|
52
|
+
* ACCOUNTS of the route instruction. They are NOT top-level allow-listed (round 3): Jupiter reaches a
|
|
53
|
+
* DEX only by CPI from the route, so a DEX called directly at the top level is not the swap shape and
|
|
54
|
+
* is refused. Extend deliberately, never automatically.
|
|
55
|
+
*/
|
|
56
|
+
export const KNOWN_DEX_PROGRAMS = Object.freeze({
|
|
57
|
+
'675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8': 'Raydium AMM v4',
|
|
58
|
+
'CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK': 'Raydium CLMM',
|
|
59
|
+
'CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C': 'Raydium CPMM',
|
|
60
|
+
'whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc': 'Orca Whirlpool',
|
|
61
|
+
'LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo': 'Meteora DLMM',
|
|
62
|
+
'Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB': 'Meteora Pools',
|
|
63
|
+
'cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG': 'Meteora DAMM v2',
|
|
64
|
+
'6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P': 'Pump.fun',
|
|
65
|
+
'pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA': 'PumpSwap',
|
|
66
|
+
'2wT8Yq49kHgDzXuPxZSaeLaH1qbmGXtEyPy64bL7aD3c': 'Lifinity v2',
|
|
67
|
+
'PhoeNiXZ8ByJGLkxNfZRnkUfjvmuYqLR89jjFHGqdXY': 'Phoenix',
|
|
68
|
+
'opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb': 'OpenBook v2',
|
|
69
|
+
'SoLFiHG9TfgtdUXUjWAxi3LtvYuFyDLVhBWxdMZxyCe': 'SolFi',
|
|
70
|
+
'obriQD1zbpyLz95G5n7nJe6a4DPjpFwa5XYPoNm113y': 'Obric v2',
|
|
71
|
+
'ZERor4xhbUycZ6gb9ntrhqscUcZmAbQVRpxkonpqJXk': 'ZeroFi',
|
|
72
|
+
'HyaB3W9q6XdA5xwpU4XnSZV94htfmbmqJXZcEbRaJutt': 'Invariant',
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* The complete top-level program allow-list (SPEC §6.2). Frozen; unknown → refuse. Every program here
|
|
76
|
+
* also has a whitelisted instruction SHAPE in checkTxPolicy; being allow-listed alone admits nothing.
|
|
77
|
+
*/
|
|
78
|
+
export const ALLOWED_PROGRAMS = new Set([
|
|
79
|
+
SYSTEM_PROGRAM, TOKEN_PROGRAM, TOKEN_2022_PROGRAM, ASSOCIATED_TOKEN_PROGRAM, COMPUTE_BUDGET_PROGRAM,
|
|
80
|
+
MEMO_PROGRAM, JUPITER_V6_PROGRAM,
|
|
81
|
+
]);
|
|
82
|
+
export class TxDecodeError extends Error {
|
|
83
|
+
constructor(message) { super(`malformed transaction: ${message}`); this.name = 'TxDecodeError'; }
|
|
84
|
+
}
|
|
85
|
+
class Reader {
|
|
86
|
+
off = 0;
|
|
87
|
+
buf;
|
|
88
|
+
constructor(buf) { this.buf = buf; }
|
|
89
|
+
get offset() { return this.off; }
|
|
90
|
+
get remaining() { return this.buf.length - this.off; }
|
|
91
|
+
u8() {
|
|
92
|
+
if (this.off >= this.buf.length)
|
|
93
|
+
throw new TxDecodeError('unexpected end (u8)');
|
|
94
|
+
return this.buf[this.off++];
|
|
95
|
+
}
|
|
96
|
+
bytes(n, what) {
|
|
97
|
+
if (n < 0 || this.off + n > this.buf.length)
|
|
98
|
+
throw new TxDecodeError(`unexpected end (${what})`);
|
|
99
|
+
const out = this.buf.subarray(this.off, this.off + n);
|
|
100
|
+
this.off += n;
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
/** Solana compact-u16 (shortvec). At most 3 bytes; values ≥ 2^16 are malformed. */
|
|
104
|
+
shortvec(what) {
|
|
105
|
+
let value = 0;
|
|
106
|
+
for (let i = 0; i < 3; i++) {
|
|
107
|
+
const b = this.u8();
|
|
108
|
+
value |= (b & 0x7f) << (7 * i);
|
|
109
|
+
if ((b & 0x80) === 0) {
|
|
110
|
+
if (i === 2 && (b & 0x7c) !== 0)
|
|
111
|
+
throw new TxDecodeError(`shortvec overflow (${what})`);
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
throw new TxDecodeError(`shortvec too long (${what})`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Decode a serialized transaction (legacy or v0). Throws TxDecodeError on ANY irregularity, including
|
|
120
|
+
* trailing bytes — a transaction we cannot account for byte-by-byte is refused.
|
|
121
|
+
*/
|
|
122
|
+
export function decodeTransaction(buf) {
|
|
123
|
+
const r = new Reader(buf);
|
|
124
|
+
const sigCount = r.shortvec('signature count');
|
|
125
|
+
if (sigCount > 16)
|
|
126
|
+
throw new TxDecodeError(`${sigCount} signatures`);
|
|
127
|
+
const signatures = [];
|
|
128
|
+
for (let i = 0; i < sigCount; i++)
|
|
129
|
+
signatures.push(Buffer.from(r.bytes(64, `signature ${i}`)));
|
|
130
|
+
const message = Buffer.from(buf.subarray(r.offset));
|
|
131
|
+
const m = new Reader(message);
|
|
132
|
+
let version = 'legacy';
|
|
133
|
+
const first = m.u8();
|
|
134
|
+
if (first & 0x80) {
|
|
135
|
+
const v = first & 0x7f;
|
|
136
|
+
if (v !== 0)
|
|
137
|
+
throw new TxDecodeError(`unsupported message version ${v}`);
|
|
138
|
+
version = 0;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
// Legacy: the first byte IS numRequiredSignatures; rewind by re-reading below.
|
|
142
|
+
version = 'legacy';
|
|
143
|
+
}
|
|
144
|
+
const numRequiredSignatures = version === 0 ? m.u8() : first;
|
|
145
|
+
const numReadonlySigned = m.u8();
|
|
146
|
+
const numReadonlyUnsigned = m.u8();
|
|
147
|
+
if (numRequiredSignatures === 0)
|
|
148
|
+
throw new TxDecodeError('no required signers');
|
|
149
|
+
const keyCount = m.shortvec('account key count');
|
|
150
|
+
if (keyCount === 0 || keyCount > 256)
|
|
151
|
+
throw new TxDecodeError(`${keyCount} account keys`);
|
|
152
|
+
if (numRequiredSignatures > keyCount)
|
|
153
|
+
throw new TxDecodeError('more signers than keys');
|
|
154
|
+
const staticKeys = [];
|
|
155
|
+
for (let i = 0; i < keyCount; i++)
|
|
156
|
+
staticKeys.push(base58(m.bytes(32, `account key ${i}`)));
|
|
157
|
+
const recentBlockhash = Buffer.from(m.bytes(32, 'recent blockhash'));
|
|
158
|
+
const ixCount = m.shortvec('instruction count');
|
|
159
|
+
if (ixCount > 64)
|
|
160
|
+
throw new TxDecodeError(`${ixCount} instructions`);
|
|
161
|
+
const instructions = [];
|
|
162
|
+
for (let i = 0; i < ixCount; i++) {
|
|
163
|
+
const programIdIndex = m.u8();
|
|
164
|
+
const nAcc = m.shortvec(`instruction ${i} account count`);
|
|
165
|
+
const accounts = [];
|
|
166
|
+
for (let j = 0; j < nAcc; j++)
|
|
167
|
+
accounts.push(m.u8());
|
|
168
|
+
const dataLen = m.shortvec(`instruction ${i} data length`);
|
|
169
|
+
const data = Buffer.from(m.bytes(dataLen, `instruction ${i} data`));
|
|
170
|
+
instructions.push({ programIdIndex, programId: programIdIndex < keyCount ? staticKeys[programIdIndex] : null, accounts, data });
|
|
171
|
+
}
|
|
172
|
+
const lookups = [];
|
|
173
|
+
if (version === 0) {
|
|
174
|
+
const nTables = m.shortvec('lookup table count');
|
|
175
|
+
if (nTables > 32)
|
|
176
|
+
throw new TxDecodeError(`${nTables} lookup tables`);
|
|
177
|
+
for (let t = 0; t < nTables; t++) {
|
|
178
|
+
const table = base58(m.bytes(32, `lookup table ${t}`));
|
|
179
|
+
const nw = m.shortvec(`lookup ${t} writable count`);
|
|
180
|
+
const writable = [];
|
|
181
|
+
for (let j = 0; j < nw; j++)
|
|
182
|
+
writable.push(m.u8());
|
|
183
|
+
const nr = m.shortvec(`lookup ${t} readonly count`);
|
|
184
|
+
const readonly = [];
|
|
185
|
+
for (let j = 0; j < nr; j++)
|
|
186
|
+
readonly.push(m.u8());
|
|
187
|
+
lookups.push({ table, writable, readonly });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (m.remaining !== 0)
|
|
191
|
+
throw new TxDecodeError(`${m.remaining} trailing bytes`);
|
|
192
|
+
if (signatures.length !== numRequiredSignatures && signatures.length !== 0) {
|
|
193
|
+
// A signed tx carries exactly numRequiredSignatures slots; an unsigned one from /swap also carries
|
|
194
|
+
// the slots (zero-filled). Anything else is not a transaction we built.
|
|
195
|
+
throw new TxDecodeError(`${signatures.length} signatures for ${numRequiredSignatures} required signers`);
|
|
196
|
+
}
|
|
197
|
+
return { version, signatures, message, numRequiredSignatures, numReadonlySigned, numReadonlyUnsigned, staticKeys, recentBlockhash, instructions, lookups };
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Jupiter v6 route-family instructions — the ONLY Jupiter instructions a dexterm swap may carry, keyed by
|
|
201
|
+
* Anchor discriminator (sha256("global:<name>")[0..8]; recomputed, and `route` matches the transaction
|
|
202
|
+
* shape used throughout the T1 suite). Any other Jupiter instruction at the top level (claim,
|
|
203
|
+
* set_token_ledger, create_token_account, …) is not a swap and is refused.
|
|
204
|
+
*/
|
|
205
|
+
export const JUPITER_ROUTE_DISCRIMINATORS = new Map([
|
|
206
|
+
['e517cb977ae3ad2a', 'route'],
|
|
207
|
+
['c1209b3341d69c81', 'shared_accounts_route'],
|
|
208
|
+
['96564774a75d0e68', 'route_with_token_ledger'],
|
|
209
|
+
['e6798f50779f6aaa', 'shared_accounts_route_with_token_ledger'],
|
|
210
|
+
['d033ef977b2bed5c', 'exact_out_route'],
|
|
211
|
+
['b0d169a89a7d453e', 'shared_accounts_exact_out_route'],
|
|
212
|
+
]);
|
|
213
|
+
// SPL Token / Token-2022 instruction discriminators (first data byte). Token-2022 shares the legacy
|
|
214
|
+
// numbering. Only the WHITELISTED set (SyncNative, InitializeAccount* for the user, CloseAccount to the
|
|
215
|
+
// user) may appear at the top level of a swap; every other op is refused, and is named so the refusal
|
|
216
|
+
// can say what the transaction tried to do.
|
|
217
|
+
const TOKEN_INITIALIZE_ACCOUNT = 1, TOKEN_TRANSFER = 3, TOKEN_APPROVE = 4, TOKEN_SET_AUTHORITY = 6, TOKEN_BURN = 8, TOKEN_CLOSE_ACCOUNT = 9, TOKEN_TRANSFER_CHECKED = 12, TOKEN_APPROVE_CHECKED = 13, TOKEN_BURN_CHECKED = 15, TOKEN_INITIALIZE_ACCOUNT2 = 16, TOKEN_SYNC_NATIVE = 17, TOKEN_INITIALIZE_ACCOUNT3 = 18;
|
|
218
|
+
const TOKEN_OP_NAMES = Object.freeze({
|
|
219
|
+
0: 'InitializeMint', 1: 'InitializeAccount', 2: 'InitializeMultisig', 3: 'Transfer', 4: 'Approve', 5: 'Revoke',
|
|
220
|
+
6: 'SetAuthority', 7: 'MintTo', 8: 'Burn', 9: 'CloseAccount', 10: 'FreezeAccount', 11: 'ThawAccount',
|
|
221
|
+
12: 'TransferChecked', 13: 'ApproveChecked', 14: 'MintToChecked', 15: 'BurnChecked', 16: 'InitializeAccount2',
|
|
222
|
+
17: 'SyncNative', 18: 'InitializeAccount3',
|
|
223
|
+
});
|
|
224
|
+
// System program instruction indexes (u32 LE). Only Transfer — into the user's own wrapped-SOL account —
|
|
225
|
+
// is part of a swap; everything else (CreateAccount*, Assign*, Allocate*, nonce ops, TransferWithSeed) is
|
|
226
|
+
// refused.
|
|
227
|
+
const SYSTEM_TRANSFER = 2;
|
|
228
|
+
const SYSTEM_OP_NAMES = Object.freeze({
|
|
229
|
+
0: 'CreateAccount', 1: 'Assign', 2: 'Transfer', 3: 'CreateAccountWithSeed', 4: 'AdvanceNonceAccount',
|
|
230
|
+
5: 'WithdrawNonceAccount', 6: 'InitializeNonceAccount', 7: 'AuthorizeNonceAccount', 8: 'Allocate',
|
|
231
|
+
9: 'AllocateWithSeed', 10: 'AssignWithSeed', 11: 'TransferWithSeed', 12: 'UpgradeNonceAccount',
|
|
232
|
+
});
|
|
233
|
+
// Associated Token program: Create (empty data or 0) and CreateIdempotent (1), for the user only.
|
|
234
|
+
const ATA_CREATE = 0, ATA_CREATE_IDEMPOTENT = 1;
|
|
235
|
+
/**
|
|
236
|
+
* Apply the §6.2 policy to a decoded transaction for `userPublicKey`. Pure. The policy is a POSITIVE
|
|
237
|
+
* description of what a Jupiter swap IS (round 3): the transaction is refused unless every top-level
|
|
238
|
+
* instruction fits one of the whitelisted shapes AND exactly one of them is a Jupiter route. Refuses on
|
|
239
|
+
* the first violation; the message names it so the ticket can show why.
|
|
240
|
+
*/
|
|
241
|
+
export function checkTxPolicy(tx, userPublicKey, allowed = ALLOWED_PROGRAMS) {
|
|
242
|
+
if (tx.staticKeys[0] !== userPublicKey) {
|
|
243
|
+
return { ok: false, reason: 'signer-mismatch', message: `fee payer ${short(tx.staticKeys[0])} is not the linked wallet` };
|
|
244
|
+
}
|
|
245
|
+
const userIndex = tx.staticKeys.indexOf(userPublicKey);
|
|
246
|
+
if (userIndex < 0 || userIndex >= tx.numRequiredSignatures) {
|
|
247
|
+
return { ok: false, reason: 'signer-mismatch', message: 'the linked wallet is not a required signer' };
|
|
248
|
+
}
|
|
249
|
+
const keyAt = (i) => (i < tx.staticKeys.length ? tx.staticKeys[i] : null);
|
|
250
|
+
const refuse = (reason, message) => ({ ok: false, reason, message });
|
|
251
|
+
// Pass 1: which accounts count as the user's OWN wrapped-SOL account in this transaction — one that is
|
|
252
|
+
// BOTH SyncNative'd (so it is a wrapped-SOL token account) AND closed back to the user by the user (so
|
|
253
|
+
// the user is its owner; CloseAccount's authority check is enforced on-chain, so a lie here fails the
|
|
254
|
+
// simulation). A System Transfer may fund nothing else.
|
|
255
|
+
const synced = new Set();
|
|
256
|
+
const closedToUser = new Set();
|
|
257
|
+
for (const ix of tx.instructions) {
|
|
258
|
+
if (ix.programId !== TOKEN_PROGRAM && ix.programId !== TOKEN_2022_PROGRAM)
|
|
259
|
+
continue;
|
|
260
|
+
const op = ix.data.length > 0 ? ix.data[0] : -1;
|
|
261
|
+
const acct = ix.accounts.length > 0 ? keyAt(ix.accounts[0]) : null;
|
|
262
|
+
if (acct === null)
|
|
263
|
+
continue;
|
|
264
|
+
if (op === TOKEN_SYNC_NATIVE)
|
|
265
|
+
synced.add(acct);
|
|
266
|
+
if (op === TOKEN_CLOSE_ACCOUNT && ix.accounts.length > 2 && keyAt(ix.accounts[1]) === userPublicKey && keyAt(ix.accounts[2]) === userPublicKey) {
|
|
267
|
+
closedToUser.add(acct);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const isUserWsol = (k) => k !== null && synced.has(k) && closedToUser.has(k);
|
|
271
|
+
// Pass 2: every top-level instruction must fit a whitelisted shape. (Shape violations are reported
|
|
272
|
+
// before the route count below so a transaction that is both not-a-swap AND carries a forbidden
|
|
273
|
+
// instruction names the instruction.)
|
|
274
|
+
let routes = 0;
|
|
275
|
+
for (let i = 0; i < tx.instructions.length; i++) {
|
|
276
|
+
const ix = tx.instructions[i];
|
|
277
|
+
if (ix.programId === null)
|
|
278
|
+
return refuse('program-not-allowed', `instruction ${i}: program loaded from a lookup table`);
|
|
279
|
+
if (!allowed.has(ix.programId))
|
|
280
|
+
return refuse('program-not-allowed', `instruction ${i}: program ${short(ix.programId)} is not allow-listed`);
|
|
281
|
+
const pid = ix.programId;
|
|
282
|
+
// Compute-budget and memo instructions cannot move funds.
|
|
283
|
+
if (pid === COMPUTE_BUDGET_PROGRAM || pid === MEMO_PROGRAM)
|
|
284
|
+
continue;
|
|
285
|
+
if (pid === JUPITER_V6_PROGRAM) {
|
|
286
|
+
const disc = ix.data.length >= 8 ? ix.data.subarray(0, 8).toString('hex') : '';
|
|
287
|
+
if (!JUPITER_ROUTE_DISCRIMINATORS.has(disc))
|
|
288
|
+
return refuse('not-a-swap', `instruction ${i}: Jupiter instruction ${disc || '(empty)'} is not a route`);
|
|
289
|
+
routes++;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (pid === TOKEN_PROGRAM || pid === TOKEN_2022_PROGRAM) {
|
|
293
|
+
const op = ix.data.length > 0 ? ix.data[0] : -1;
|
|
294
|
+
const name = TOKEN_OP_NAMES[op] ?? `op ${op}`;
|
|
295
|
+
if (op === TOKEN_SYNC_NATIVE)
|
|
296
|
+
continue;
|
|
297
|
+
if (op === TOKEN_CLOSE_ACCOUNT) {
|
|
298
|
+
// accounts: [account to close, destination (receives the lamports), owner/authority]
|
|
299
|
+
const dest = ix.accounts.length > 1 ? keyAt(ix.accounts[1]) : null;
|
|
300
|
+
const owner = ix.accounts.length > 2 ? keyAt(ix.accounts[2]) : null;
|
|
301
|
+
if (dest !== userPublicKey || owner !== userPublicKey)
|
|
302
|
+
return refuse('forbidden-instruction', `instruction ${i}: CloseAccount to a foreign owner/destination`);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (op === TOKEN_INITIALIZE_ACCOUNT) {
|
|
306
|
+
// accounts: [account, mint, owner, rent sysvar]
|
|
307
|
+
const owner = ix.accounts.length > 2 ? keyAt(ix.accounts[2]) : null;
|
|
308
|
+
if (owner !== userPublicKey)
|
|
309
|
+
return refuse('forbidden-instruction', `instruction ${i}: InitializeAccount for a foreign owner`);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (op === TOKEN_INITIALIZE_ACCOUNT2 || op === TOKEN_INITIALIZE_ACCOUNT3) {
|
|
313
|
+
// data: [op, owner (32 bytes)]
|
|
314
|
+
const owner = ix.data.length >= 33 ? base58(ix.data.subarray(1, 33)) : null;
|
|
315
|
+
if (owner !== userPublicKey)
|
|
316
|
+
return refuse('forbidden-instruction', `instruction ${i}: ${name} for a foreign owner`);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
// Everything else is never part of a swap dexterm signs. The user's tokens move ONLY inside the
|
|
320
|
+
// route; a top-level Transfer/Burn is a drain whatever the rest of the transaction looks like.
|
|
321
|
+
if (op === TOKEN_APPROVE || op === TOKEN_APPROVE_CHECKED)
|
|
322
|
+
return refuse('forbidden-instruction', `instruction ${i}: token Approve (delegation) is never signed`);
|
|
323
|
+
if (op === TOKEN_SET_AUTHORITY)
|
|
324
|
+
return refuse('forbidden-instruction', `instruction ${i}: token SetAuthority is never signed`);
|
|
325
|
+
if (op === TOKEN_TRANSFER || op === TOKEN_TRANSFER_CHECKED)
|
|
326
|
+
return refuse('forbidden-instruction', `instruction ${i}: a direct token ${name} is never signed (a swap moves tokens only inside the route)`);
|
|
327
|
+
if (op === TOKEN_BURN || op === TOKEN_BURN_CHECKED)
|
|
328
|
+
return refuse('forbidden-instruction', `instruction ${i}: token ${name} is never signed`);
|
|
329
|
+
return refuse('forbidden-instruction', `instruction ${i}: token ${name} is not part of a swap`);
|
|
330
|
+
}
|
|
331
|
+
if (pid === SYSTEM_PROGRAM) {
|
|
332
|
+
const op = ix.data.length >= 4 ? ix.data.readUInt32LE(0) : -1;
|
|
333
|
+
const name = SYSTEM_OP_NAMES[op] ?? `op ${op}`;
|
|
334
|
+
if (op !== SYSTEM_TRANSFER)
|
|
335
|
+
return refuse('forbidden-instruction', `instruction ${i}: System ${name} is not part of a swap`);
|
|
336
|
+
// accounts: [from, to]. Only the user wrapping SOL into their OWN wrapped-SOL account.
|
|
337
|
+
const from = ix.accounts.length > 0 ? keyAt(ix.accounts[0]) : null;
|
|
338
|
+
const to = ix.accounts.length > 1 ? keyAt(ix.accounts[1]) : null;
|
|
339
|
+
if (from !== userPublicKey)
|
|
340
|
+
return refuse('forbidden-instruction', `instruction ${i}: System Transfer from an account that is not the wallet`);
|
|
341
|
+
if (!isUserWsol(to)) {
|
|
342
|
+
return refuse('forbidden-instruction', `instruction ${i}: System Transfer to ${short(to ?? undefined)} — only the wallet's own wrapped-SOL account (synced and closed back to the wallet in this transaction) may be funded`);
|
|
343
|
+
}
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (pid === ASSOCIATED_TOKEN_PROGRAM) {
|
|
347
|
+
// accounts: [funder, ata, owner (wallet), mint, system, token]; data: empty / 0 = Create, 1 = CreateIdempotent
|
|
348
|
+
const op = ix.data.length === 0 ? ATA_CREATE : ix.data[0];
|
|
349
|
+
if (op !== ATA_CREATE && op !== ATA_CREATE_IDEMPOTENT)
|
|
350
|
+
return refuse('forbidden-instruction', `instruction ${i}: Associated Token op ${op} is not part of a swap`);
|
|
351
|
+
const owner = ix.accounts.length > 2 ? keyAt(ix.accounts[2]) : null;
|
|
352
|
+
if (owner !== userPublicKey)
|
|
353
|
+
return refuse('forbidden-instruction', `instruction ${i}: creating a token account for a foreign owner`);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
// Allow-listed by a caller-supplied set but with no whitelisted shape here: extending the allow-list
|
|
357
|
+
// without teaching the shape rule fails closed.
|
|
358
|
+
return refuse('not-a-swap', `instruction ${i}: program ${short(pid)} has no swap shape`);
|
|
359
|
+
}
|
|
360
|
+
// Pass 3: a swap is exactly ONE Jupiter route. None → whatever this is, it is not a swap. Two or more →
|
|
361
|
+
// a second route could move a second leg the plan never showed.
|
|
362
|
+
if (routes === 0)
|
|
363
|
+
return refuse('not-a-swap', 'the transaction carries no Jupiter route instruction');
|
|
364
|
+
if (routes > 1)
|
|
365
|
+
return refuse('not-a-swap', `the transaction carries ${routes} Jupiter route instructions; a plan is one swap`);
|
|
366
|
+
return { ok: true };
|
|
367
|
+
}
|
|
368
|
+
function short(s) {
|
|
369
|
+
return s === undefined ? '?' : s.length > 10 ? `${s.slice(0, 4)}…${s.slice(-4)}` : s;
|
|
370
|
+
}
|
|
371
|
+
// =====================================================================================================
|
|
372
|
+
// task-1328 (T2): BIND the one Jupiter route to the PLAN, tolerate Phantom's Lighthouse tail, and the
|
|
373
|
+
// devnet PROBE shape. Everything below is pure and fails closed.
|
|
374
|
+
//
|
|
375
|
+
// Recorded 2026-10-09 from the real keyless `/swap/v1/swap` (fixtures/trade/jupiter-swap-*.json): a
|
|
376
|
+
// SOL→USDC buy is `shared_accounts_route`, a USDC→SOL sell is `route`; both pass checkTxPolicy as
|
|
377
|
+
// written in round 3, and the route instruction's trailing args are exactly the quote's `inAmount`,
|
|
378
|
+
// `outAmount`, `slippageBps` and a zero platform fee. So the route instruction ITSELF can be bound to
|
|
379
|
+
// the plan: the signer refuses a route whose amounts/accounts are not the plan's.
|
|
380
|
+
import { base58Decode, findProgramAddress } from "../sources/pda.js";
|
|
381
|
+
/** Lighthouse assertion program (Phantom appends assertion instructions; same id on devnet + mainnet). */
|
|
382
|
+
export const LIGHTHOUSE_PROGRAM = 'L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95';
|
|
383
|
+
export const WSOL_MINT = 'So11111111111111111111111111111111111111112';
|
|
384
|
+
/**
|
|
385
|
+
* Lighthouse instruction discriminators (u8, lighthouse `instruction.rs`). All of them either ASSERT
|
|
386
|
+
* runtime state (and fail the tx when the assertion fails) or write/close a small memory PDA funded by
|
|
387
|
+
* the payer — none can move the user's tokens. Unknown discriminators are refused.
|
|
388
|
+
*/
|
|
389
|
+
export const LIGHTHOUSE_OPS = Object.freeze({
|
|
390
|
+
0: 'MemoryWrite', 1: 'MemoryClose', 2: 'AssertAccountData', 3: 'AssertAccountDelta', 4: 'AssertAccountInfo',
|
|
391
|
+
5: 'AssertAccountInfoMulti', 6: 'AssertMintAccount', 7: 'AssertMintAccountMulti', 8: 'AssertTokenAccount',
|
|
392
|
+
9: 'AssertTokenAccountMulti', 10: 'AssertStakeAccount', 11: 'AssertStakeAccountMulti',
|
|
393
|
+
12: 'AssertUpgradeableLoaderAccount', 13: 'AssertUpgradeableLoaderAccountMulti', 14: 'AssertSysvarClock',
|
|
394
|
+
15: 'AssertMerkleTreeAccount', 16: 'AssertBubblegumTreeConfigAccount',
|
|
395
|
+
});
|
|
396
|
+
/** The associated token address of `owner` for `mint` under a token program. Pure (sha256 + curve check). */
|
|
397
|
+
export function associatedTokenAddress(owner, mint, tokenProgram = TOKEN_PROGRAM) {
|
|
398
|
+
return findProgramAddress([base58Decode(owner), base58Decode(tokenProgram), base58Decode(mint)], base58Decode(ASSOCIATED_TOKEN_PROGRAM)).address;
|
|
399
|
+
}
|
|
400
|
+
const ROUTE_LAYOUT = Object.freeze({
|
|
401
|
+
route: { authority: 1, source: 2, destination: 3, platformFee: 6 },
|
|
402
|
+
route_with_token_ledger: { authority: 1, source: 2, destination: 3, platformFee: 6 },
|
|
403
|
+
shared_accounts_route: { authority: 2, source: 3, destination: 6, platformFee: 9 },
|
|
404
|
+
shared_accounts_route_with_token_ledger: { authority: 2, source: 3, destination: 6, platformFee: 9 },
|
|
405
|
+
});
|
|
406
|
+
/** The ExactIn route args sit at the END of the data: in_amount u64, quoted_out_amount u64, slippage_bps u16, platform_fee_bps u8. */
|
|
407
|
+
export function parseRouteArgs(data) {
|
|
408
|
+
if (data.length < 8 + 19)
|
|
409
|
+
return null;
|
|
410
|
+
const n = data.length;
|
|
411
|
+
return { inAmount: data.readBigUInt64LE(n - 19), quotedOutAmount: data.readBigUInt64LE(n - 11), slippageBps: data.readUInt16LE(n - 3), platformFeeBps: data[n - 1] };
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Bind the transaction's one Jupiter route to the plan (task-1328, closes the T1 residual "the route
|
|
415
|
+
* instruction itself is unchecked" / "sell leg and buy destination unverified"):
|
|
416
|
+
* * the route is an ExactIn family instruction with parseable args;
|
|
417
|
+
* * `in_amount`, `quoted_out_amount` and `slippage_bps` equal the plan's quote (a route sized to the
|
|
418
|
+
* user's whole balance, or re-priced, is refused);
|
|
419
|
+
* * `platform_fee_bps` is 0 and no platform-fee account is set (dexterm takes no fee, SPEC §6.9);
|
|
420
|
+
* * `user_transfer_authority` is the wallet; the SOURCE token account is the wallet's own associated
|
|
421
|
+
* token account for the INPUT mint (the sell leg can only spend the planned mint from the wallet's
|
|
422
|
+
* own account) and the DESTINATION is the wallet's own ATA for the OUTPUT mint (a buy can only land
|
|
423
|
+
* in the wallet); Token-2022 ATAs are accepted for the non-SOL mint;
|
|
424
|
+
* * every Associated-Token `Create` and every System `Transfer` destination is one of those same
|
|
425
|
+
* accounts (nothing is created or funded for any other mint or owner).
|
|
426
|
+
* A user account that lives in an address-lookup table cannot be named statically → refused (closed).
|
|
427
|
+
*/
|
|
428
|
+
export function checkSwapBinding(tx, expect) {
|
|
429
|
+
const refuse = (reason, message) => ({ ok: false, reason, message });
|
|
430
|
+
const keyAt = (i) => (i < tx.staticKeys.length ? tx.staticKeys[i] : null);
|
|
431
|
+
const user = expect.user;
|
|
432
|
+
const atas = (mint) => (mint === WSOL_MINT
|
|
433
|
+
? [associatedTokenAddress(user, mint, TOKEN_PROGRAM)]
|
|
434
|
+
: [associatedTokenAddress(user, mint, TOKEN_PROGRAM), associatedTokenAddress(user, mint, TOKEN_2022_PROGRAM)]);
|
|
435
|
+
let sourceAtas, destAtas;
|
|
436
|
+
try {
|
|
437
|
+
sourceAtas = atas(expect.inputMint);
|
|
438
|
+
destAtas = atas(expect.outputMint);
|
|
439
|
+
}
|
|
440
|
+
catch (e) {
|
|
441
|
+
return refuse('quote-mismatch', `cannot derive the wallet's token accounts: ${e instanceof Error ? e.message : String(e)}`);
|
|
442
|
+
}
|
|
443
|
+
const own = new Set([...sourceAtas, ...destAtas, ...atas(WSOL_MINT)]);
|
|
444
|
+
const routes = tx.instructions.map((ix, i) => ({ ix, i })).filter(({ ix }) => ix.programId === JUPITER_V6_PROGRAM);
|
|
445
|
+
if (routes.length !== 1)
|
|
446
|
+
return refuse('not-a-swap', `the transaction carries ${routes.length} Jupiter instructions; a plan is one swap`);
|
|
447
|
+
const { ix, i } = routes[0];
|
|
448
|
+
const disc = ix.data.length >= 8 ? ix.data.subarray(0, 8).toString('hex') : '';
|
|
449
|
+
const name = JUPITER_ROUTE_DISCRIMINATORS.get(disc) ?? '';
|
|
450
|
+
const layout = ROUTE_LAYOUT[name];
|
|
451
|
+
if (!layout)
|
|
452
|
+
return refuse('not-a-swap', `instruction ${i}: Jupiter ${name || disc || '(empty)'} is not an ExactIn route`);
|
|
453
|
+
const args = parseRouteArgs(ix.data);
|
|
454
|
+
if (!args)
|
|
455
|
+
return refuse('not-a-swap', `instruction ${i}: route args too short`);
|
|
456
|
+
if (args.inAmount !== expect.inAmount)
|
|
457
|
+
return refuse('quote-mismatch', `route in_amount ${args.inAmount} is not the plan's ${expect.inAmount}`);
|
|
458
|
+
if (args.quotedOutAmount !== expect.outAmount)
|
|
459
|
+
return refuse('quote-mismatch', `route quoted_out_amount ${args.quotedOutAmount} is not the plan's ${expect.outAmount}`);
|
|
460
|
+
if (args.slippageBps !== expect.slippageBps)
|
|
461
|
+
return refuse('quote-mismatch', `route slippage ${args.slippageBps} bps is not the plan's ${expect.slippageBps} bps`);
|
|
462
|
+
if (args.platformFeeBps !== 0)
|
|
463
|
+
return refuse('forbidden-instruction', `route carries a ${args.platformFeeBps} bps platform fee; dexterm takes no fee`);
|
|
464
|
+
const need = Math.max(layout.authority, layout.source, layout.destination, layout.platformFee) + 1;
|
|
465
|
+
if (ix.accounts.length < need)
|
|
466
|
+
return refuse('not-a-swap', `instruction ${i}: route has ${ix.accounts.length} accounts; ${name} needs ≥ ${need}`);
|
|
467
|
+
const authority = keyAt(ix.accounts[layout.authority]);
|
|
468
|
+
const source = keyAt(ix.accounts[layout.source]);
|
|
469
|
+
const destination = keyAt(ix.accounts[layout.destination]);
|
|
470
|
+
const platformFee = keyAt(ix.accounts[layout.platformFee]);
|
|
471
|
+
if (authority !== user)
|
|
472
|
+
return refuse('forbidden-instruction', `route transfer authority ${short(authority ?? undefined)} is not the wallet`);
|
|
473
|
+
if (source === null || !sourceAtas.includes(source))
|
|
474
|
+
return refuse('forbidden-instruction', `route source ${short(source ?? undefined)} is not the wallet's ${short(expect.inputMint)} token account`);
|
|
475
|
+
if (destination === null || !destAtas.includes(destination))
|
|
476
|
+
return refuse('forbidden-instruction', `route destination ${short(destination ?? undefined)} is not the wallet's ${short(expect.outputMint)} token account`);
|
|
477
|
+
// "No platform fee account" is encoded as the program id itself in the optional slot.
|
|
478
|
+
if (platformFee !== JUPITER_V6_PROGRAM)
|
|
479
|
+
return refuse('forbidden-instruction', `route names a platform-fee account ${short(platformFee ?? undefined)}; dexterm takes no fee`);
|
|
480
|
+
for (let j = 0; j < tx.instructions.length; j++) {
|
|
481
|
+
const x = tx.instructions[j];
|
|
482
|
+
if (x.programId === ASSOCIATED_TOKEN_PROGRAM) {
|
|
483
|
+
const ata = x.accounts.length > 1 ? keyAt(x.accounts[1]) : null;
|
|
484
|
+
if (ata === null || !own.has(ata))
|
|
485
|
+
return refuse('forbidden-instruction', `instruction ${j}: creates a token account ${short(ata ?? undefined)} that is not one of the wallet's accounts for this swap`);
|
|
486
|
+
}
|
|
487
|
+
if (x.programId === SYSTEM_PROGRAM) {
|
|
488
|
+
const to = x.accounts.length > 1 ? keyAt(x.accounts[1]) : null;
|
|
489
|
+
const wsol = atas(WSOL_MINT)[0];
|
|
490
|
+
if (to !== wsol)
|
|
491
|
+
return refuse('forbidden-instruction', `instruction ${j}: System Transfer to ${short(to ?? undefined)} is not the wallet's wrapped-SOL account`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return { ok: true, route: name, args };
|
|
495
|
+
}
|
|
496
|
+
function resolveRef(tx, idx) {
|
|
497
|
+
if (idx < tx.staticKeys.length)
|
|
498
|
+
return { key: tx.staticKeys[idx] };
|
|
499
|
+
let off = idx - tx.staticKeys.length;
|
|
500
|
+
for (const l of tx.lookups) {
|
|
501
|
+
if (off < l.writable.length)
|
|
502
|
+
return { table: l.table, kind: 'w', index: l.writable[off] };
|
|
503
|
+
off -= l.writable.length;
|
|
504
|
+
}
|
|
505
|
+
for (const l of tx.lookups) {
|
|
506
|
+
if (off < l.readonly.length)
|
|
507
|
+
return { table: l.table, kind: 'r', index: l.readonly[off] };
|
|
508
|
+
off -= l.readonly.length;
|
|
509
|
+
}
|
|
510
|
+
return { bad: idx };
|
|
511
|
+
}
|
|
512
|
+
function sameRef(a, b) {
|
|
513
|
+
if ('key' in a || 'key' in b)
|
|
514
|
+
return 'key' in a && 'key' in b && a.key === b.key;
|
|
515
|
+
if ('bad' in a || 'bad' in b)
|
|
516
|
+
return false;
|
|
517
|
+
return a.table === b.table && a.kind === b.kind && a.index === b.index;
|
|
518
|
+
}
|
|
519
|
+
function sameInstruction(a, ia, b, ib) {
|
|
520
|
+
if (ia.programId === null || ib.programId === null || ia.programId !== ib.programId)
|
|
521
|
+
return false;
|
|
522
|
+
if (!ia.data.equals(ib.data))
|
|
523
|
+
return false;
|
|
524
|
+
if (ia.accounts.length !== ib.accounts.length)
|
|
525
|
+
return false;
|
|
526
|
+
for (let k = 0; k < ia.accounts.length; k++)
|
|
527
|
+
if (!sameRef(resolveRef(a, ia.accounts[k]), resolveRef(b, ib.accounts[k])))
|
|
528
|
+
return false;
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Is `signed` the message the user approved? Exact bytes → yes. Otherwise the ONLY tolerated difference
|
|
533
|
+
* (task-1328, "handle Phantom Lighthouse instructions") is that Phantom APPENDED Lighthouse assertion
|
|
534
|
+
* instructions: same version, same blockhash, same signer count, same fee payer, same lookup tables, and
|
|
535
|
+
* every approved instruction present in order with the same program, data and (re-indexed) accounts;
|
|
536
|
+
* the tail must be Lighthouse instructions with known discriminators. Any other change — a dropped,
|
|
537
|
+
* reordered or edited instruction, a new non-Lighthouse instruction, a changed blockhash — is a
|
|
538
|
+
* mismatch. The caller still re-runs policy and re-simulates the signed bytes.
|
|
539
|
+
*/
|
|
540
|
+
export function compareApprovedMessage(approved, signed) {
|
|
541
|
+
if (approved.message.equals(signed.message))
|
|
542
|
+
return { ok: true, lighthouseTail: 0 };
|
|
543
|
+
const no = (message) => ({ ok: false, message });
|
|
544
|
+
if (approved.version !== signed.version)
|
|
545
|
+
return no('message version changed');
|
|
546
|
+
if (!approved.recentBlockhash.equals(signed.recentBlockhash))
|
|
547
|
+
return no('recent blockhash changed');
|
|
548
|
+
if (approved.numRequiredSignatures !== signed.numRequiredSignatures)
|
|
549
|
+
return no('signer count changed');
|
|
550
|
+
if (approved.staticKeys[0] !== signed.staticKeys[0])
|
|
551
|
+
return no('fee payer changed');
|
|
552
|
+
if (approved.lookups.length !== signed.lookups.length)
|
|
553
|
+
return no('lookup tables changed');
|
|
554
|
+
for (let t = 0; t < approved.lookups.length; t++) {
|
|
555
|
+
const a = approved.lookups[t], b = signed.lookups[t];
|
|
556
|
+
if (a.table !== b.table || a.writable.join(',') !== b.writable.join(',') || a.readonly.join(',') !== b.readonly.join(','))
|
|
557
|
+
return no('lookup tables changed');
|
|
558
|
+
}
|
|
559
|
+
if (signed.instructions.length < approved.instructions.length)
|
|
560
|
+
return no('an approved instruction was dropped');
|
|
561
|
+
for (let i = 0; i < approved.instructions.length; i++) {
|
|
562
|
+
if (!sameInstruction(approved, approved.instructions[i], signed, signed.instructions[i]))
|
|
563
|
+
return no(`instruction ${i} differs from the approved plan`);
|
|
564
|
+
}
|
|
565
|
+
const tail = signed.instructions.slice(approved.instructions.length);
|
|
566
|
+
for (let k = 0; k < tail.length; k++) {
|
|
567
|
+
const ix = tail[k];
|
|
568
|
+
if (ix.programId !== LIGHTHOUSE_PROGRAM)
|
|
569
|
+
return no(`appended instruction ${approved.instructions.length + k} is not a Lighthouse assertion (${short(ix.programId ?? undefined)})`);
|
|
570
|
+
const op = ix.data.length > 0 ? ix.data[0] : -1;
|
|
571
|
+
if (!(op in LIGHTHOUSE_OPS))
|
|
572
|
+
return no(`appended Lighthouse instruction has unknown discriminator ${op}`);
|
|
573
|
+
}
|
|
574
|
+
// Every static key the approved message named must still be present (Phantom may add keys, never remove).
|
|
575
|
+
for (const k of approved.staticKeys)
|
|
576
|
+
if (!signed.staticKeys.includes(k))
|
|
577
|
+
return no(`account ${short(k)} vanished from the message`);
|
|
578
|
+
return { ok: true, lighthouseTail: tail.length };
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* checkTxPolicy for the probe / Lighthouse cases: strips a trailing run of Lighthouse instructions (if
|
|
582
|
+
* allowed and well-formed), runs the round-3 policy on the rest, and applies the route-count rule for
|
|
583
|
+
* the kind. Kept as a separate entry point so the round-3 function and its tests stay byte-for-byte.
|
|
584
|
+
*/
|
|
585
|
+
export function checkTxPolicyWith(tx, userPublicKey, allowed = ALLOWED_PROGRAMS, opts = {}) {
|
|
586
|
+
let ixs = tx.instructions;
|
|
587
|
+
if (opts.lighthouseTail) {
|
|
588
|
+
let cut = ixs.length;
|
|
589
|
+
while (cut > 0 && ixs[cut - 1].programId === LIGHTHOUSE_PROGRAM)
|
|
590
|
+
cut--;
|
|
591
|
+
for (let k = cut; k < ixs.length; k++) {
|
|
592
|
+
const op = ixs[k].data.length > 0 ? ixs[k].data[0] : -1;
|
|
593
|
+
if (!(op in LIGHTHOUSE_OPS))
|
|
594
|
+
return { ok: false, reason: 'forbidden-instruction', message: `instruction ${k}: unknown Lighthouse op ${op}` };
|
|
595
|
+
}
|
|
596
|
+
ixs = ixs.slice(0, cut);
|
|
597
|
+
}
|
|
598
|
+
// A Lighthouse instruction anywhere but the tolerated tail is never part of a swap.
|
|
599
|
+
for (let k = 0; k < ixs.length; k++)
|
|
600
|
+
if (ixs[k].programId === LIGHTHOUSE_PROGRAM)
|
|
601
|
+
return { ok: false, reason: 'program-not-allowed', message: `instruction ${k}: Lighthouse outside the appended tail` };
|
|
602
|
+
const base = { ...tx, instructions: ixs };
|
|
603
|
+
if ((opts.kind ?? 'swap') === 'swap')
|
|
604
|
+
return checkTxPolicy(base, userPublicKey, allowed);
|
|
605
|
+
// probe: run the same per-instruction shape rules but require ZERO routes and at least the wrap shape.
|
|
606
|
+
const withRoute = checkTxPolicy(base, userPublicKey, allowed);
|
|
607
|
+
if (!withRoute.ok && withRoute.reason !== 'not-a-swap')
|
|
608
|
+
return withRoute;
|
|
609
|
+
const routes = ixs.filter((ix) => ix.programId === JUPITER_V6_PROGRAM).length;
|
|
610
|
+
if (routes !== 0)
|
|
611
|
+
return { ok: false, reason: 'not-a-swap', message: 'a probe carries no Jupiter route' };
|
|
612
|
+
const wsol = associatedTokenAddress(userPublicKey, WSOL_MINT, TOKEN_PROGRAM);
|
|
613
|
+
const keyAt = (i) => (i < tx.staticKeys.length ? tx.staticKeys[i] : null);
|
|
614
|
+
let synced = false, closed = false, funded = false;
|
|
615
|
+
for (let k = 0; k < ixs.length; k++) {
|
|
616
|
+
const ix = ixs[k];
|
|
617
|
+
if (ix.programId === TOKEN_PROGRAM && ix.data[0] === 17 && keyAt(ix.accounts[0]) === wsol)
|
|
618
|
+
synced = true;
|
|
619
|
+
if (ix.programId === TOKEN_PROGRAM && ix.data[0] === 9 && keyAt(ix.accounts[0]) === wsol)
|
|
620
|
+
closed = true;
|
|
621
|
+
if (ix.programId === SYSTEM_PROGRAM) {
|
|
622
|
+
if (keyAt(ix.accounts[1] ?? -1) !== wsol)
|
|
623
|
+
return { ok: false, reason: 'forbidden-instruction', message: `instruction ${k}: probe transfer must fund the wallet's own wrapped-SOL account` };
|
|
624
|
+
funded = true;
|
|
625
|
+
}
|
|
626
|
+
if (ix.programId === ASSOCIATED_TOKEN_PROGRAM && keyAt(ix.accounts[1] ?? -1) !== wsol)
|
|
627
|
+
return { ok: false, reason: 'forbidden-instruction', message: `instruction ${k}: probe may only create the wallet's wrapped-SOL account` };
|
|
628
|
+
}
|
|
629
|
+
if (!synced || !closed || !funded)
|
|
630
|
+
return { ok: false, reason: 'not-a-swap', message: 'a probe is the wrap-and-unwrap shape (fund + SyncNative + CloseAccount to the wallet)' };
|
|
631
|
+
return { ok: true };
|
|
632
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function chunk(xs, size) {
|
|
2
|
+
const out = [];
|
|
3
|
+
for (let i = 0; i < xs.length; i += size)
|
|
4
|
+
out.push(xs.slice(i, i + size));
|
|
5
|
+
return out;
|
|
6
|
+
}
|
|
7
|
+
/** Finite number or 0 (for counts and required numeric fields). */
|
|
8
|
+
export function num(v) {
|
|
9
|
+
const n = typeof v === 'string' ? Number(v) : v;
|
|
10
|
+
return typeof n === 'number' && Number.isFinite(n) ? n : 0;
|
|
11
|
+
}
|
|
12
|
+
/** Finite number or null ("source did not say"). */
|
|
13
|
+
export function numOrNull(v) {
|
|
14
|
+
if (v === null || v === undefined || v === '')
|
|
15
|
+
return null;
|
|
16
|
+
const n = typeof v === 'string' ? Number(v) : v;
|
|
17
|
+
return typeof n === 'number' && Number.isFinite(n) ? n : null;
|
|
18
|
+
}
|
|
19
|
+
export function isoToMs(v) {
|
|
20
|
+
if (typeof v !== 'string')
|
|
21
|
+
return null;
|
|
22
|
+
const t = Date.parse(v);
|
|
23
|
+
return Number.isFinite(t) ? t : null;
|
|
24
|
+
}
|