mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-27 09:46:25 +02:00
- 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)
289 lines
7.2 KiB
TypeScript
289 lines
7.2 KiB
TypeScript
import type { PlasmoCSConfig } from "plasmo";
|
|
|
|
export const config: PlasmoCSConfig = {
|
|
matches: ["<all_urls>"],
|
|
all_frames: true,
|
|
world: "MAIN",
|
|
};
|
|
|
|
/**
|
|
* Content script for page context detection
|
|
* Extracts relevant data from crypto pages and sends to side panel
|
|
*/
|
|
|
|
type PageType = "dexscreener" | "coingecko" | "twitter" | "generic";
|
|
|
|
interface TokenData {
|
|
chain: string;
|
|
pairAddress: string;
|
|
tokenSymbol?: string;
|
|
price?: string;
|
|
volume24h?: string;
|
|
liquidity?: string;
|
|
}
|
|
|
|
interface PageContext {
|
|
url: string;
|
|
title: string;
|
|
pageType: PageType;
|
|
tokenData?: TokenData;
|
|
}
|
|
|
|
/**
|
|
* Detect page type from URL
|
|
*/
|
|
function detectPageType(url: string): PageType {
|
|
if (url.includes("dexscreener.com")) return "dexscreener";
|
|
if (url.includes("coingecko.com")) return "coingecko";
|
|
if (url.includes("twitter.com") || url.includes("x.com")) return "twitter";
|
|
return "generic";
|
|
}
|
|
|
|
/**
|
|
* Extract DexScreener token data from DOM
|
|
*/
|
|
function extractDexScreenerData(): TokenData | undefined {
|
|
const url = window.location.href;
|
|
const match = url.match(/dexscreener\.com\/([^\/]+)\/([^\/\?]+)/);
|
|
|
|
if (!match) return undefined;
|
|
|
|
const [, chain, pairAddress] = match;
|
|
|
|
// Try to extract data from DOM
|
|
// Note: DexScreener uses dynamic rendering, so selectors may need adjustment
|
|
const tokenSymbol =
|
|
document.querySelector('[data-test="token-symbol"]')?.textContent ||
|
|
document.querySelector(".token-symbol")?.textContent ||
|
|
undefined;
|
|
|
|
const price =
|
|
document.querySelector('[data-test="token-price"]')?.textContent ||
|
|
document.querySelector(".token-price")?.textContent ||
|
|
undefined;
|
|
|
|
const volume24h =
|
|
document.querySelector('[data-test="volume-24h"]')?.textContent ||
|
|
document.querySelector(".volume-24h")?.textContent ||
|
|
undefined;
|
|
|
|
const liquidity =
|
|
document.querySelector('[data-test="liquidity"]')?.textContent ||
|
|
document.querySelector(".liquidity")?.textContent ||
|
|
undefined;
|
|
|
|
return {
|
|
chain,
|
|
pairAddress,
|
|
tokenSymbol,
|
|
price,
|
|
volume24h,
|
|
liquidity,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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<string>();
|
|
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<string>();
|
|
|
|
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<string>();
|
|
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
|
|
*/
|
|
function extractPageContext(): PageContext {
|
|
const url = window.location.href;
|
|
const title = document.title;
|
|
const pageType = detectPageType(url);
|
|
|
|
const context: PageContext = {
|
|
url,
|
|
title,
|
|
pageType,
|
|
};
|
|
|
|
// 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;
|
|
}
|
|
|
|
/**
|
|
* Send context update to side panel
|
|
*/
|
|
function sendContextUpdate() {
|
|
const context = extractPageContext();
|
|
chrome.runtime.sendMessage({
|
|
type: "PAGE_CONTEXT_UPDATE",
|
|
data: context,
|
|
});
|
|
}
|
|
|
|
// Send initial context after page load
|
|
if (document.readyState === "complete") {
|
|
sendContextUpdate();
|
|
} else {
|
|
window.addEventListener("load", sendContextUpdate);
|
|
}
|
|
|
|
// Listen for context requests from side panel
|
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
if (message.type === "GET_PAGE_CONTEXT") {
|
|
sendContextUpdate();
|
|
}
|
|
});
|
|
|
|
// Watch for DOM changes (for dynamic content like DexScreener)
|
|
const observer = new MutationObserver(() => {
|
|
// Debounce updates
|
|
clearTimeout((window as any).__contextUpdateTimeout);
|
|
(window as any).__contextUpdateTimeout = setTimeout(sendContextUpdate, 1000);
|
|
});
|
|
|
|
observer.observe(document.body, {
|
|
childList: true,
|
|
subtree: true,
|
|
});
|