diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/dexscreener-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/dexscreener-connect-form.tsx new file mode 100644 index 000000000..cda7cc766 --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/dexscreener-connect-form.tsx @@ -0,0 +1,387 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { Info, Plus, X } from "lucide-react"; +import type { FC } from "react"; +import { useRef, useState } from "react"; +import { useForm } from "react-hook-form"; +import * as z from "zod"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { EnumConnectorName } from "@/contracts/enums/connector"; +import { DateRangeSelector } from "../../components/date-range-selector"; +import { getConnectorBenefits } from "../connector-benefits"; +import type { ConnectFormProps } from "../index"; + +// Token configuration schema +const tokenSchema = z.object({ + chain: z.string().min(1, "Chain is required"), + address: z.string().regex(/^0x[a-fA-F0-9]{40}$/, "Invalid token address (must be 0x followed by 40 hex characters)"), + name: z.string().optional(), +}); + +type TokenConfig = z.infer; + +// Form schema +const dexScreenerFormSchema = z.object({ + name: z.string().min(3, { + message: "Connector name must be at least 3 characters.", + }), + tokens: z.array(tokenSchema).min(1, "At least one token is required").max(50, "Maximum 50 tokens allowed"), +}); + +type DexScreenerFormValues = z.infer; + +// Supported chains +const SUPPORTED_CHAINS = [ + { value: "ethereum", label: "Ethereum" }, + { value: "bsc", label: "BSC (Binance Smart Chain)" }, + { value: "polygon", label: "Polygon" }, + { value: "arbitrum", label: "Arbitrum" }, + { value: "optimism", label: "Optimism" }, + { value: "base", label: "Base" }, + { value: "avalanche", label: "Avalanche" }, + { value: "solana", label: "Solana" }, +] as const; + +export const DexScreenerConnectForm: FC = ({ onSubmit, isSubmitting }) => { + const isSubmittingRef = useRef(false); + const [startDate, setStartDate] = useState(undefined); + const [endDate, setEndDate] = useState(undefined); + const [periodicEnabled, setPeriodicEnabled] = useState(false); + const [frequencyMinutes, setFrequencyMinutes] = useState("1440"); + const [tokens, setTokens] = useState([ + { chain: "ethereum", address: "", name: "" }, + ]); + + const form = useForm({ + resolver: zodResolver(dexScreenerFormSchema), + defaultValues: { + name: "DexScreener Connector", + tokens: tokens, + }, + }); + + // Sync tokens state with form + const updateFormTokens = (newTokens: TokenConfig[]) => { + setTokens(newTokens); + form.setValue("tokens", newTokens); + }; + + const addToken = () => { + if (tokens.length < 50) { + updateFormTokens([...tokens, { chain: "ethereum", address: "", name: "" }]); + } + }; + + const removeToken = (index: number) => { + if (tokens.length > 1) { + updateFormTokens(tokens.filter((_, i) => i !== index)); + } + }; + + const updateToken = (index: number, field: keyof TokenConfig, value: string) => { + const newTokens = [...tokens]; + newTokens[index] = { ...newTokens[index], [field]: value }; + updateFormTokens(newTokens); + }; + + const handleSubmit = async (values: DexScreenerFormValues) => { + // Prevent multiple submissions + if (isSubmittingRef.current || isSubmitting) { + return; + } + + isSubmittingRef.current = true; + try { + await onSubmit({ + name: values.name, + connector_type: EnumConnectorName.DEXSCREENER_CONNECTOR, + config: { + tokens: values.tokens, + }, + is_indexable: true, + is_active: true, + last_indexed_at: null, + periodic_indexing_enabled: periodicEnabled, + indexing_frequency_minutes: periodicEnabled ? parseInt(frequencyMinutes, 10) : null, + next_scheduled_at: null, + startDate, + endDate, + periodicEnabled, + frequencyMinutes, + }); + } finally { + isSubmittingRef.current = false; + } + }; + + return ( +
+ + +
+ No API Key Required + + DexScreener API is public and free to use. Simply add the tokens you want to track.{" "} + + View API Documentation + + +
+
+ +
+
+ + ( + + Connector Name + + + + + A friendly name to identify this connector. + + + + )} + /> + + {/* Token List */} +
+
+

Tracked Tokens

+ + {tokens.length} / 50 tokens + +
+ +
+ {tokens.map((token, index) => ( +
+
+ + Token #{index + 1} + + {tokens.length > 1 && ( + + )} +
+ +
+
+ + +
+ +
+ + updateToken(index, "address", e.target.value)} + className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40 font-mono" + disabled={isSubmitting} + /> +
+
+ +
+ + updateToken(index, "name", e.target.value)} + className="h-8 sm:h-10 px-2 sm:px-3 text-xs sm:text-sm border-slate-400/20 focus-visible:border-slate-400/40" + disabled={isSubmitting} + /> +
+
+ ))} +
+ + +
+ + {/* Indexing Configuration */} +
+

Indexing Configuration

+ + {/* Date Range Selector */} + + + {/* Periodic Sync Config */} +
+
+
+

Enable Periodic Sync

+

+ Automatically re-index at regular intervals +

+
+ +
+ + {periodicEnabled && ( +
+
+ + +
+
+ )} +
+
+ + +
+ + {/* What you get section */} + {getConnectorBenefits(EnumConnectorName.DEXSCREENER_CONNECTOR) && ( +
+

What you get with DexScreener integration:

+
    + {getConnectorBenefits(EnumConnectorName.DEXSCREENER_CONNECTOR)?.map((benefit) => ( +
  • {benefit}
  • + ))} +
+
+ )} +
+ ); +}; diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/connector-benefits.ts b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/connector-benefits.ts index 392de4bc8..b12f9331d 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/connector-benefits.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/connector-benefits.ts @@ -116,6 +116,13 @@ export function getConnectorBenefits(connectorType: string): string[] | null { "Incremental sync - only changed files are re-indexed", "Full support for your vault's folder structure", ], + DEXSCREENER_CONNECTOR: [ + "Real-time cryptocurrency trading pair data from multiple DEXs", + "Track token prices, volume, and liquidity across chains", + "Search and analyze market data with AI-powered insights", + "Monitor your crypto portfolio with automated updates", + "Access historical price trends and trading volumes", + ], }; return benefits[connectorType] || null; diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx index ffaeb1478..3a7f8a344 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/index.tsx @@ -2,6 +2,7 @@ import type { FC } from "react"; import { BaiduSearchApiConnectForm } from "./components/baidu-search-api-connect-form"; import { BookStackConnectForm } from "./components/bookstack-connect-form"; import { CirclebackConnectForm } from "./components/circleback-connect-form"; +import { DexScreenerConnectForm } from "./components/dexscreener-connect-form"; import { ElasticsearchConnectForm } from "./components/elasticsearch-connect-form"; import { GithubConnectForm } from "./components/github-connect-form"; import { LinkupApiConnectForm } from "./components/linkup-api-connect-form"; @@ -57,6 +58,8 @@ export function getConnectFormComponent(connectorType: string): ConnectFormCompo return LumaConnectForm; case "CIRCLEBACK_CONNECTOR": return CirclebackConnectForm; + case "DEXSCREENER_CONNECTOR": + return DexScreenerConnectForm; case "MCP_CONNECTOR": return MCPConnectForm; case "OBSIDIAN_CONNECTOR": diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/dexscreener-config.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/dexscreener-config.tsx new file mode 100644 index 000000000..2a224a969 --- /dev/null +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/components/dexscreener-config.tsx @@ -0,0 +1,210 @@ +"use client"; + +import { Plus, X } from "lucide-react"; +import type { FC } from "react"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import type { ConnectorConfigProps } from "../index"; + +// Token configuration interface +interface TokenConfig { + chain: string; + address: string; + name?: string; +} + +// Supported chains +const SUPPORTED_CHAINS = [ + { value: "ethereum", label: "Ethereum" }, + { value: "bsc", label: "BSC (Binance Smart Chain)" }, + { value: "polygon", label: "Polygon" }, + { value: "arbitrum", label: "Arbitrum" }, + { value: "optimism", label: "Optimism" }, + { value: "base", label: "Base" }, + { value: "avalanche", label: "Avalanche" }, + { value: "solana", label: "Solana" }, +] as const; + +export const DexScreenerConfig: FC = ({ + connector, + onConfigChange, + onNameChange, +}) => { + const [tokens, setTokens] = useState( + (connector.config?.tokens as TokenConfig[]) || [] + ); + const [name, setName] = useState(connector.name || ""); + + const handleTokensChange = (newTokens: TokenConfig[]) => { + setTokens(newTokens); + onConfigChange?.({ ...connector.config, tokens: newTokens }); + }; + + const handleNameChange = (newName: string) => { + setName(newName); + onNameChange?.(newName); + }; + + const addToken = () => { + if (tokens.length < 50) { + handleTokensChange([...tokens, { chain: "ethereum", address: "", name: "" }]); + } + }; + + const removeToken = (index: number) => { + if (tokens.length > 1) { + handleTokensChange(tokens.filter((_, i) => i !== index)); + } + }; + + const updateToken = (index: number, field: keyof TokenConfig, value: string) => { + const newTokens = [...tokens]; + newTokens[index] = { ...newTokens[index], [field]: value }; + handleTokensChange(newTokens); + }; + + return ( +
+ {/* Connector Name */} +
+ + handleNameChange(e.target.value)} + placeholder="My Crypto Tracker" + className="h-10 px-3 text-sm border-slate-400/20 focus-visible:border-slate-400/40" + /> +

+ A friendly name to identify this connector. +

+
+ + {/* Token Configuration */} +
+
+

Tracked Tokens

+ + {tokens.length} / 50 tokens + +
+ +
+ {tokens.map((token, index) => ( +
+
+ + Token #{index + 1} + + {tokens.length > 1 && ( + + )} +
+ +
+
+ + +
+ +
+ + updateToken(index, "address", e.target.value)} + className="h-10 px-3 text-sm border-slate-400/20 focus-visible:border-slate-400/40 font-mono" + /> +
+
+ +
+ + updateToken(index, "name", e.target.value)} + className="h-10 px-3 text-sm border-slate-400/20 focus-visible:border-slate-400/40" + /> +
+
+ ))} +
+ + +
+ + {/* Info */} +
+

Configuration Tips

+
    +
  • Token addresses must be valid 40-character hex strings (0x...)
  • +
  • You can track up to 50 tokens per connector
  • +
  • Changes are saved automatically when you update the configuration
  • +
  • Token names are optional but help identify tokens in search results
  • +
+
+
+ ); +}; diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/index.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/index.tsx index 6b4d86b5a..c6211f2b6 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/index.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/index.tsx @@ -10,6 +10,7 @@ import { ComposioCalendarConfig } from "./components/composio-calendar-config"; import { ComposioDriveConfig } from "./components/composio-drive-config"; import { ComposioGmailConfig } from "./components/composio-gmail-config"; import { ConfluenceConfig } from "./components/confluence-config"; +import { DexScreenerConfig } from "./components/dexscreener-config"; import { DiscordConfig } from "./components/discord-config"; import { ElasticsearchConfig } from "./components/elasticsearch-config"; import { GithubConfig } from "./components/github-config"; @@ -75,6 +76,8 @@ export function getConnectorConfigComponent( return LumaConfig; case "CIRCLEBACK_CONNECTOR": return CirclebackConfig; + case "DEXSCREENER_CONNECTOR": + return DexScreenerConfig; case "MCP_CONNECTOR": return MCPConfig; case "OBSIDIAN_CONNECTOR": diff --git a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx index ec754da2e..494467c21 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx +++ b/surfsense_web/components/assistant-ui/connector-popup/connector-configs/views/connector-connect-view.tsx @@ -56,6 +56,7 @@ export const ConnectorConnectView: FC = ({ BOOKSTACK_CONNECTOR: "bookstack-connect-form", GITHUB_CONNECTOR: "github-connect-form", LUMA_CONNECTOR: "luma-connect-form", + DEXSCREENER_CONNECTOR: "dexscreener-connect-form", CIRCLEBACK_CONNECTOR: "circleback-connect-form", MCP_CONNECTOR: "mcp-connect-form", OBSIDIAN_CONNECTOR: "obsidian-connect-form", diff --git a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts index a3e8ae272..9402eb673 100644 --- a/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts +++ b/surfsense_web/components/assistant-ui/connector-popup/constants/connector-constants.ts @@ -124,6 +124,12 @@ export const OTHER_CONNECTORS = [ description: "Search Luma events", connectorType: EnumConnectorName.LUMA_CONNECTOR, }, + { + id: "dexscreener-connector", + title: "DexScreener", + description: "Track cryptocurrency trading pairs across DEXs", + connectorType: EnumConnectorName.DEXSCREENER_CONNECTOR, + }, { id: "elasticsearch-connector", title: "Elasticsearch", diff --git a/surfsense_web/content/docs/connectors/dexscreener.mdx b/surfsense_web/content/docs/connectors/dexscreener.mdx new file mode 100644 index 000000000..c13337fd3 --- /dev/null +++ b/surfsense_web/content/docs/connectors/dexscreener.mdx @@ -0,0 +1,237 @@ +--- +title: DexScreener +description: Connect DexScreener trading pair data to SurfSense +--- + +# DexScreener Integration Setup Guide + +DexScreener is a powerful cryptocurrency trading analytics platform that provides real-time data for trading pairs across multiple decentralized exchanges (DEXs). Integrate DexScreener with SurfSense to search and analyze crypto market data using AI. + +## How it works + +The DexScreener connector fetches trading pair data for tokens you specify and indexes it into your SurfSense search space. This allows you to: + +- Track real-time price movements across multiple DEXs +- Monitor trading volume and liquidity metrics +- Analyze historical price trends +- Search for specific tokens and trading pairs using natural language +- Get AI-powered insights on market data + +## Authorization + +**No authentication required!** DexScreener's API is public and free to use. Simply configure the tokens you want to track and start indexing. + +## Supported Chains + +The DexScreener connector supports the following blockchain networks: + +- **Ethereum** - The largest DeFi ecosystem +- **BSC (Binance Smart Chain)** - High-speed, low-cost transactions +- **Polygon** - Ethereum scaling solution +- **Arbitrum** - Ethereum Layer 2 rollup +- **Optimism** - Ethereum Layer 2 optimistic rollup +- **Base** - Coinbase's Layer 2 network +- **Avalanche** - High-throughput blockchain platform +- **Solana** - High-performance blockchain + +## Setup Instructions + +### 1. Add the Connector + +1. Navigate to your SurfSense dashboard +2. Click **"Add Connector"** in the connector popup +3. Select **"DexScreener"** from the connector list + +### 2. Configure Connector Name + +Enter a friendly name for your connector (e.g., "My Crypto Tracker", "DeFi Portfolio Monitor") + +### 3. Add Tokens to Track + +For each token you want to monitor: + +1. Click **"Add Token"** +2. Select the **blockchain network** from the dropdown +3. Enter the **token contract address** (must be a valid 40-character hex address starting with `0x`) +4. (Optional) Add a **friendly name** to help identify the token + +**Example:** +- Chain: Ethereum +- Address: `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` +- Name: Wrapped Ether (WETH) + +You can track up to **50 tokens** per connector. + +### 4. Configure Indexing Settings + +#### Date Range (Optional) +Set a date range to limit historical data indexing. Leave blank to index all available data. + +#### Periodic Sync +Enable automatic re-indexing to keep your data up-to-date: + +- **Every 5 minutes** - For active trading monitoring +- **Every 15 minutes** - For frequent updates +- **Every hour** - For regular monitoring +- **Every 6 hours** - For daily tracking +- **Every 12 hours** - For bi-daily updates +- **Daily** - For long-term portfolio tracking +- **Weekly** - For occasional updates + +### 5. Connect + +Click **"Connect"** to create the connector and start indexing. + +## Token Configuration + +### Finding Token Addresses + +Token contract addresses can be found on: + +- **DexScreener**: Search for the token and copy the address from the URL or token info +- **Etherscan** (Ethereum): etherscan.io +- **BscScan** (BSC): bscscan.com +- **PolygonScan** (Polygon): polygonscan.com +- **Block explorers** for other chains + +### Address Format + +Token addresses must be: +- Exactly 42 characters long +- Start with `0x` +- Followed by 40 hexadecimal characters (0-9, a-f, A-F) + +**Valid example:** `0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` +**Invalid examples:** +- `C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` (missing 0x) +- `0x123` (too short) +- `0xGGGGaaA39b223FE8D0A0e5C4F27eAD9083C756Cc2` (invalid hex characters) + +## What Gets Indexed + +For each tracked token, the connector indexes: + +- **Token Information** + - Token symbol and name + - Contract address + - Blockchain network + +- **Price Data** + - Current price in USD + - Price in native currency + - Price change percentages (5m, 1h, 6h, 24h) + +- **Volume Metrics** + - 24-hour trading volume + - 6-hour trading volume + - 1-hour trading volume + +- **Liquidity Information** + - Total liquidity in USD + - Liquidity distribution across DEXs + +- **DEX Information** + - Exchange names + - Trading pair details + - Pool addresses + +## Managing Your Connector + +### Editing Configuration + +1. Click **"Configure"** on your DexScreener connector +2. Update the connector name if needed +3. Add or remove tokens from the tracked list +4. Modify token details (chain, address, name) +5. Click **"Save"** to apply changes + +### Triggering Manual Indexing + +To manually re-index your connector: + +1. Open the connector configuration +2. Click **"Index Now"** or wait for the next scheduled sync + +### Deleting the Connector + +To remove the connector and all indexed data: + +1. Click the **delete icon** on the connector card +2. Confirm the deletion + +## Troubleshooting + +### "Invalid token address" error + +**Cause:** The token address doesn't match the required format. + +**Solution:** +- Ensure the address is exactly 42 characters (0x + 40 hex chars) +- Verify you copied the full address without spaces +- Check that the address contains only valid hex characters (0-9, a-f, A-F) + +### "Token not found" error + +**Cause:** The token doesn't exist on DexScreener or hasn't been indexed yet. + +**Solution:** +- Verify the token address is correct +- Check that the token has trading activity on DEXs +- Try searching for the token on [dexscreener.com](https://dexscreener.com) first +- Ensure you selected the correct blockchain network + +### No data appearing in search + +**Cause:** Indexing may not have completed yet. + +**Solution:** +- Wait a few minutes for the initial indexing to complete +- Check the connector status in your dashboard +- Verify the connector is active and not paused +- Try manually triggering a re-index + +### "Maximum tokens exceeded" error + +**Cause:** You're trying to add more than 50 tokens to a single connector. + +**Solution:** +- Remove some tokens you no longer need to track +- Create a second DexScreener connector for additional tokens +- Prioritize the most important tokens for your use case + +## Best Practices + +1. **Start Small**: Begin with 5-10 important tokens and expand as needed +2. **Use Descriptive Names**: Add friendly names to tokens for easier identification +3. **Set Appropriate Sync Frequency**: Balance data freshness with API usage +4. **Organize by Strategy**: Create separate connectors for different trading strategies or portfolios +5. **Regular Cleanup**: Remove tokens you're no longer tracking to keep data relevant + +## API Rate Limits + +DexScreener's public API has rate limits. To avoid issues: + +- Don't track more tokens than you actively need +- Use reasonable sync frequencies (avoid 5-minute intervals unless necessary) +- If you encounter rate limit errors, increase the sync interval + +## Privacy & Security + +- **No Authentication**: DexScreener connector doesn't require API keys or credentials +- **Public Data Only**: All indexed data is publicly available market data +- **No Personal Info**: Token addresses and market data don't contain personal information +- **Secure Storage**: All connector configurations are encrypted and stored securely + +## Support + +For issues or questions: + +- Check the [DexScreener API Documentation](https://docs.dexscreener.com/api/reference) +- Visit the SurfSense community forums +- Contact SurfSense support + +## Related Resources + +- [DexScreener Website](https://dexscreener.com) +- [DexScreener API Docs](https://docs.dexscreener.com/api/reference) +- [SurfSense Connector Overview](/docs/connectors) diff --git a/surfsense_web/contracts/enums/connector.ts b/surfsense_web/contracts/enums/connector.ts index 45b13a20b..8a65b7f47 100644 --- a/surfsense_web/contracts/enums/connector.ts +++ b/surfsense_web/contracts/enums/connector.ts @@ -19,6 +19,7 @@ export enum EnumConnectorName { GOOGLE_DRIVE_CONNECTOR = "GOOGLE_DRIVE_CONNECTOR", AIRTABLE_CONNECTOR = "AIRTABLE_CONNECTOR", LUMA_CONNECTOR = "LUMA_CONNECTOR", + DEXSCREENER_CONNECTOR = "DEXSCREENER_CONNECTOR", ELASTICSEARCH_CONNECTOR = "ELASTICSEARCH_CONNECTOR", WEBCRAWLER_CONNECTOR = "WEBCRAWLER_CONNECTOR", YOUTUBE_CONNECTOR = "YOUTUBE_CONNECTOR", diff --git a/surfsense_web/contracts/types/connector.types.ts b/surfsense_web/contracts/types/connector.types.ts index 5082fe49c..6ca35e09d 100644 --- a/surfsense_web/contracts/types/connector.types.ts +++ b/surfsense_web/contracts/types/connector.types.ts @@ -31,6 +31,7 @@ export const searchSourceConnectorTypeEnum = z.enum([ "COMPOSIO_GOOGLE_DRIVE_CONNECTOR", "COMPOSIO_GMAIL_CONNECTOR", "COMPOSIO_GOOGLE_CALENDAR_CONNECTOR", + "DEXSCREENER_CONNECTOR", ]); export const searchSourceConnector = z.object({ diff --git a/surfsense_web/public/assets/connector-icons/dexscreener.svg b/surfsense_web/public/assets/connector-icons/dexscreener.svg new file mode 100644 index 000000000..480f6065e --- /dev/null +++ b/surfsense_web/public/assets/connector-icons/dexscreener.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + +