From e89824db0f743fca0217de85cfbbdcb67f727bfa Mon Sep 17 00:00:00 2001 From: API Test Bot Date: Wed, 4 Feb 2026 10:55:49 +0700 Subject: [PATCH] feat(detection): implement multi-page token detection system - Add extractTwitterTokens() to detect $TOKEN mentions (e.g., $BONK, $SOL) - Add extractContractAddresses() for Solana (base58) and Ethereum (0x) addresses - Add extractTradingPairs() to detect TOKEN/SOL, TOKEN/USDT patterns - Update extractPageContext() to use new detection functions - Add detectedTokens field to PageContext interface - Create DetectedTokensList component to display detected tokens - Integrate DetectedTokensList into ChatInterface - Add handleDetectedTokenClick to analyze selected tokens - Support auto-detection on Twitter, generic pages, and DexScreener Implements Task 2: Multi-Page Token Detection Part of hybrid token detection system (manual search + auto-detect) --- surfsense_browser_extension/content.ts | 147 ++++++++++++++++++ .../sidepanel/chat/ChatInterface.tsx | 18 +++ .../components/DetectedTokensList.tsx | 84 ++++++++++ .../sidepanel/context/PageContextProvider.tsx | 2 + 4 files changed, 251 insertions(+) create mode 100644 surfsense_browser_extension/sidepanel/components/DetectedTokensList.tsx diff --git a/surfsense_browser_extension/content.ts b/surfsense_browser_extension/content.ts index 875404d32..83320dbb9 100644 --- a/surfsense_browser_extension/content.ts +++ b/surfsense_browser_extension/content.ts @@ -82,6 +82,122 @@ function extractDexScreenerData(): TokenData | undefined { }; } +/** + * Extract token mentions from Twitter/X + * Detects $TOKEN format (e.g., $BONK, $SOL) + */ +function extractTwitterTokens(): TokenData[] { + const tokens: TokenData[] = []; + const pageText = document.body.innerText; + + // Match $TOKEN pattern (e.g., $BONK, $SOL, $PEPE) + const tokenPattern = /\$([A-Z]{2,10})\b/g; + const matches = pageText.matchAll(tokenPattern); + + const uniqueTokens = new Set(); + for (const match of matches) { + const symbol = match[1]; + if (!uniqueTokens.has(symbol)) { + uniqueTokens.add(symbol); + tokens.push({ + chain: "solana", // Default to Solana, can be enhanced + pairAddress: "", // Will be resolved via API + tokenSymbol: symbol, + }); + } + } + + return tokens; +} + +/** + * Extract contract addresses from page content + * Supports Solana and Ethereum address formats + */ +function extractContractAddresses(): TokenData[] { + const tokens: TokenData[] = []; + const pageText = document.body.innerText; + + // Solana address pattern (base58, 32-44 characters) + const solanaPattern = /\b([1-9A-HJ-NP-Za-km-z]{32,44})\b/g; + + // Ethereum address pattern (0x followed by 40 hex characters) + const ethPattern = /\b(0x[a-fA-F0-9]{40})\b/g; + + // Extract Ethereum addresses + const ethMatches = pageText.matchAll(ethPattern); + for (const match of ethMatches) { + const address = match[1]; + tokens.push({ + chain: "ethereum", + pairAddress: address, + tokenSymbol: undefined, + }); + } + + // Extract Solana addresses (more selective to avoid false positives) + const solanaMatches = pageText.matchAll(solanaPattern); + const uniqueSolanaAddresses = new Set(); + + for (const match of solanaMatches) { + const address = match[1]; + // Basic validation: should not be all same character, should have variety + if (address.length >= 32 && + address.length <= 44 && + new Set(address).size > 10 && + !uniqueSolanaAddresses.has(address)) { + uniqueSolanaAddresses.add(address); + tokens.push({ + chain: "solana", + pairAddress: address, + tokenSymbol: undefined, + }); + } + } + + return tokens.slice(0, 5); // Limit to first 5 to avoid spam +} + +/** + * Extract trading pairs from page content + * Detects patterns like TOKEN/SOL, TOKEN/USDT, etc. + */ +function extractTradingPairs(): TokenData[] { + const tokens: TokenData[] = []; + const pageText = document.body.innerText; + + // Match trading pair patterns (e.g., BONK/SOL, PEPE/USDT) + const pairPattern = /\b([A-Z]{2,10})\/([A-Z]{2,10})\b/g; + const matches = pageText.matchAll(pairPattern); + + const uniquePairs = new Set(); + for (const match of matches) { + const baseToken = match[1]; + const quoteToken = match[2]; + const pairKey = `${baseToken}/${quoteToken}`; + + if (!uniquePairs.has(pairKey)) { + uniquePairs.add(pairKey); + tokens.push({ + chain: "solana", // Default to Solana + pairAddress: "", // Will be resolved via API + tokenSymbol: baseToken, + }); + } + } + + return tokens.slice(0, 3); // Limit to first 3 pairs +} + +interface PageContext { + url: string; + title: string; + pageType: PageType; + tokenData?: TokenData; + /** Detected tokens from page content (Twitter mentions, addresses, pairs) */ + detectedTokens?: TokenData[]; +} + /** * Extract page context based on page type */ @@ -99,6 +215,37 @@ function extractPageContext(): PageContext { // Add page-specific data if (pageType === "dexscreener") { context.tokenData = extractDexScreenerData(); + } else if (pageType === "twitter") { + // Extract Twitter token mentions + const twitterTokens = extractTwitterTokens(); + const contractAddresses = extractContractAddresses(); + const tradingPairs = extractTradingPairs(); + + // Combine all detected tokens + context.detectedTokens = [ + ...twitterTokens, + ...contractAddresses, + ...tradingPairs, + ]; + + // Set primary token if available + if (context.detectedTokens.length > 0) { + context.tokenData = context.detectedTokens[0]; + } + } else if (pageType === "generic") { + // For generic pages, try to detect contract addresses and trading pairs + const contractAddresses = extractContractAddresses(); + const tradingPairs = extractTradingPairs(); + + context.detectedTokens = [ + ...contractAddresses, + ...tradingPairs, + ]; + + // Set primary token if available + if (context.detectedTokens.length > 0) { + context.tokenData = context.detectedTokens[0]; + } } return context; diff --git a/surfsense_browser_extension/sidepanel/chat/ChatInterface.tsx b/surfsense_browser_extension/sidepanel/chat/ChatInterface.tsx index 79acdb8c7..a58e80254 100644 --- a/surfsense_browser_extension/sidepanel/chat/ChatInterface.tsx +++ b/surfsense_browser_extension/sidepanel/chat/ChatInterface.tsx @@ -21,7 +21,9 @@ import { import { SafetyScoreDisplay } from "../crypto/SafetyScoreDisplay"; import { WatchlistPanel } from "../crypto/WatchlistPanel"; import { AlertConfigModal } from "../crypto/AlertConfigModal"; +import { DetectedTokensList } from "../components/DetectedTokensList"; import type { WatchlistItem } from "../widgets"; +import type { TokenData } from "../context/PageContextProvider"; type ViewMode = "chat" | "watchlist" | "safety"; @@ -471,6 +473,14 @@ What would you like to know?`; }, 500); }; + /** + * Handle detected token click + */ + const handleDetectedTokenClick = (token: TokenData) => { + const query = token.tokenSymbol || token.pairAddress; + handleTokenSearch(query); + }; + return (
{/* Header with space selector and settings */} @@ -495,6 +505,14 @@ What would you like to know?`; /> )} + {/* Detected tokens list (on Twitter and other pages) */} + {context?.detectedTokens && context.detectedTokens.length > 0 && viewMode === "chat" && ( + + )} + {/* Main content area */}
{viewMode === "chat" && ( diff --git a/surfsense_browser_extension/sidepanel/components/DetectedTokensList.tsx b/surfsense_browser_extension/sidepanel/components/DetectedTokensList.tsx new file mode 100644 index 000000000..9a1c31b35 --- /dev/null +++ b/surfsense_browser_extension/sidepanel/components/DetectedTokensList.tsx @@ -0,0 +1,84 @@ +import { Coins, ExternalLink } from "lucide-react"; +import { Button } from "@/routes/ui/button"; +import { cn } from "~/lib/utils"; +import type { TokenData } from "../context/PageContextProvider"; +import { ChainIcon } from "./shared/ChainIcon"; + +export interface DetectedTokensListProps { + /** List of detected tokens */ + tokens: TokenData[]; + /** Callback when a token is clicked */ + onTokenClick?: (token: TokenData) => void; + /** Additional class names */ + className?: string; +} + +/** + * DetectedTokensList - Display list of tokens detected from page content + * + * Features: + * - Shows tokens detected from Twitter mentions, contract addresses, trading pairs + * - Click to analyze token + * - Shows chain icon for each token + */ +export function DetectedTokensList({ + tokens, + onTokenClick, + className, +}: DetectedTokensListProps) { + if (tokens.length === 0) { + return null; + } + + return ( +
+
+
+ +

Detected Tokens

+ + ({tokens.length} found) + +
+ +
+ {tokens.slice(0, 5).map((token, index) => ( + + ))} +
+ + {tokens.length > 5 && ( +
+ +{tokens.length - 5} more tokens detected +
+ )} +
+
+ ); +} + diff --git a/surfsense_browser_extension/sidepanel/context/PageContextProvider.tsx b/surfsense_browser_extension/sidepanel/context/PageContextProvider.tsx index f0b548d00..448870cc0 100644 --- a/surfsense_browser_extension/sidepanel/context/PageContextProvider.tsx +++ b/surfsense_browser_extension/sidepanel/context/PageContextProvider.tsx @@ -23,6 +23,8 @@ export interface PageContext { title: string; pageType: PageType; tokenData?: TokenData; + /** Detected tokens from page content (Twitter mentions, addresses, pairs) */ + detectedTokens?: TokenData[]; } interface PageContextValue {