mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-02 04:12:47 +02:00
feat(frontend): Add DexScreener connector UI components
- Added DexScreener connect form and config components - Added connector icon, benefits, and documentation - Updated connector enums and types - Added dexscreener.mdx documentation This completes the DexScreener integration UI for the frontend.
This commit is contained in:
parent
b5d0413459
commit
4f8faad5da
11 changed files with 875 additions and 0 deletions
|
|
@ -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<typeof tokenSchema>;
|
||||
|
||||
// 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<typeof dexScreenerFormSchema>;
|
||||
|
||||
// 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<ConnectFormProps> = ({ onSubmit, isSubmitting }) => {
|
||||
const isSubmittingRef = useRef(false);
|
||||
const [startDate, setStartDate] = useState<Date | undefined>(undefined);
|
||||
const [endDate, setEndDate] = useState<Date | undefined>(undefined);
|
||||
const [periodicEnabled, setPeriodicEnabled] = useState(false);
|
||||
const [frequencyMinutes, setFrequencyMinutes] = useState("1440");
|
||||
const [tokens, setTokens] = useState<TokenConfig[]>([
|
||||
{ chain: "ethereum", address: "", name: "" },
|
||||
]);
|
||||
|
||||
const form = useForm<DexScreenerFormValues>({
|
||||
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 (
|
||||
<div className="space-y-6 pb-6">
|
||||
<Alert className="bg-slate-400/5 dark:bg-white/5 border-slate-400/20 p-2 sm:p-3 flex items-center [&>svg]:relative [&>svg]:left-0 [&>svg]:top-0 [&>svg+div]:translate-y-0">
|
||||
<Info className="h-3 w-3 sm:h-4 sm:w-4 shrink-0 ml-1" />
|
||||
<div className="-ml-1">
|
||||
<AlertTitle className="text-xs sm:text-sm">No API Key Required</AlertTitle>
|
||||
<AlertDescription className="text-[10px] sm:text-xs !pl-0">
|
||||
DexScreener API is public and free to use. Simply add the tokens you want to track.{" "}
|
||||
<a
|
||||
href="https://docs.dexscreener.com/api/reference"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium underline underline-offset-4"
|
||||
>
|
||||
View API Documentation
|
||||
</a>
|
||||
</AlertDescription>
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6 space-y-3 sm:space-y-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="dexscreener-connect-form"
|
||||
onSubmit={form.handleSubmit(handleSubmit)}
|
||||
className="space-y-4 sm:space-y-6"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel className="text-xs sm:text-sm">Connector Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="My Crypto Tracker"
|
||||
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}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription className="text-[10px] sm:text-xs">
|
||||
A friendly name to identify this connector.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Token List */}
|
||||
<div className="space-y-4 pt-4 border-t border-slate-400/20">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm sm:text-base font-medium">Tracked Tokens</h3>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{tokens.length} / 50 tokens
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{tokens.map((token, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-lg border border-slate-400/20 bg-slate-400/5 dark:bg-white/5 p-3 space-y-3"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
Token #{index + 1}
|
||||
</span>
|
||||
{tokens.length > 1 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeToken(index)}
|
||||
disabled={isSubmitting}
|
||||
className="h-6 w-6 p-0 text-destructive hover:text-destructive"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`chain-${index}`} className="text-xs sm:text-sm">
|
||||
Chain
|
||||
</Label>
|
||||
<Select
|
||||
value={token.chain}
|
||||
onValueChange={(value) => updateToken(index, "chain", value)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<SelectTrigger
|
||||
id={`chain-${index}`}
|
||||
className="h-8 sm:h-10 bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm"
|
||||
>
|
||||
<SelectValue placeholder="Select chain" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[100]">
|
||||
{SUPPORTED_CHAINS.map((chain) => (
|
||||
<SelectItem
|
||||
key={chain.value}
|
||||
value={chain.value}
|
||||
className="text-xs sm:text-sm"
|
||||
>
|
||||
{chain.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`address-${index}`} className="text-xs sm:text-sm">
|
||||
Token Address
|
||||
</Label>
|
||||
<Input
|
||||
id={`address-${index}`}
|
||||
placeholder="0x..."
|
||||
value={token.address}
|
||||
onChange={(e) => 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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`name-${index}`} className="text-xs sm:text-sm">
|
||||
Token Name (Optional)
|
||||
</Label>
|
||||
<Input
|
||||
id={`name-${index}`}
|
||||
placeholder="e.g., Wrapped Ether"
|
||||
value={token.name || ""}
|
||||
onChange={(e) => 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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={addToken}
|
||||
disabled={tokens.length >= 50 || isSubmitting}
|
||||
className="w-full h-8 sm:h-10 text-xs sm:text-sm"
|
||||
>
|
||||
<Plus className="h-3 w-3 sm:h-4 sm:w-4 mr-2" />
|
||||
Add Token {tokens.length >= 50 && "(Maximum reached)"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Indexing Configuration */}
|
||||
<div className="space-y-4 pt-4 border-t border-slate-400/20">
|
||||
<h3 className="text-sm sm:text-base font-medium">Indexing Configuration</h3>
|
||||
|
||||
{/* Date Range Selector */}
|
||||
<DateRangeSelector
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onStartDateChange={setStartDate}
|
||||
onEndDateChange={setEndDate}
|
||||
allowFutureDates={true}
|
||||
/>
|
||||
|
||||
{/* Periodic Sync Config */}
|
||||
<div className="rounded-xl bg-slate-400/5 dark:bg-white/5 p-3 sm:p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-medium text-sm sm:text-base">Enable Periodic Sync</h3>
|
||||
<p className="text-xs sm:text-sm text-muted-foreground">
|
||||
Automatically re-index at regular intervals
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={periodicEnabled}
|
||||
onCheckedChange={setPeriodicEnabled}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{periodicEnabled && (
|
||||
<div className="mt-4 pt-4 border-t border-slate-400/20 space-y-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="frequency" className="text-xs sm:text-sm">
|
||||
Sync Frequency
|
||||
</Label>
|
||||
<Select
|
||||
value={frequencyMinutes}
|
||||
onValueChange={setFrequencyMinutes}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<SelectTrigger
|
||||
id="frequency"
|
||||
className="w-full bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-xs sm:text-sm"
|
||||
>
|
||||
<SelectValue placeholder="Select frequency" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[100]">
|
||||
<SelectItem value="5" className="text-xs sm:text-sm">
|
||||
Every 5 minutes
|
||||
</SelectItem>
|
||||
<SelectItem value="15" className="text-xs sm:text-sm">
|
||||
Every 15 minutes
|
||||
</SelectItem>
|
||||
<SelectItem value="60" className="text-xs sm:text-sm">
|
||||
Every hour
|
||||
</SelectItem>
|
||||
<SelectItem value="360" className="text-xs sm:text-sm">
|
||||
Every 6 hours
|
||||
</SelectItem>
|
||||
<SelectItem value="720" className="text-xs sm:text-sm">
|
||||
Every 12 hours
|
||||
</SelectItem>
|
||||
<SelectItem value="1440" className="text-xs sm:text-sm">
|
||||
Daily
|
||||
</SelectItem>
|
||||
<SelectItem value="10080" className="text-xs sm:text-sm">
|
||||
Weekly
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
{/* What you get section */}
|
||||
{getConnectorBenefits(EnumConnectorName.DEXSCREENER_CONNECTOR) && (
|
||||
<div className="rounded-xl border border-border bg-slate-400/5 dark:bg-white/5 px-3 sm:px-6 py-4 space-y-2">
|
||||
<h4 className="text-xs sm:text-sm font-medium">What you get with DexScreener integration:</h4>
|
||||
<ul className="list-disc pl-5 text-[10px] sm:text-xs text-muted-foreground space-y-1">
|
||||
{getConnectorBenefits(EnumConnectorName.DEXSCREENER_CONNECTOR)?.map((benefit) => (
|
||||
<li key={benefit}>{benefit}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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<ConnectorConfigProps> = ({
|
||||
connector,
|
||||
onConfigChange,
|
||||
onNameChange,
|
||||
}) => {
|
||||
const [tokens, setTokens] = useState<TokenConfig[]>(
|
||||
(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 (
|
||||
<div className="space-y-6">
|
||||
{/* Connector Name */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="connector-name" className="text-sm font-medium">
|
||||
Connector Name
|
||||
</Label>
|
||||
<Input
|
||||
id="connector-name"
|
||||
value={name}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
A friendly name to identify this connector.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Token Configuration */}
|
||||
<div className="space-y-4 pt-4 border-t border-slate-400/20">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-base font-medium">Tracked Tokens</h3>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{tokens.length} / 50 tokens
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{tokens.map((token, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-lg border border-slate-400/20 bg-slate-400/5 dark:bg-white/5 p-3 space-y-3"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-medium text-muted-foreground">
|
||||
Token #{index + 1}
|
||||
</span>
|
||||
{tokens.length > 1 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeToken(index)}
|
||||
className="h-6 w-6 p-0 text-destructive hover:text-destructive"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`chain-${index}`} className="text-sm">
|
||||
Chain
|
||||
</Label>
|
||||
<Select
|
||||
value={token.chain}
|
||||
onValueChange={(value) => updateToken(index, "chain", value)}
|
||||
>
|
||||
<SelectTrigger
|
||||
id={`chain-${index}`}
|
||||
className="h-10 bg-slate-400/5 dark:bg-slate-400/5 border-slate-400/20 text-sm"
|
||||
>
|
||||
<SelectValue placeholder="Select chain" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[100]">
|
||||
{SUPPORTED_CHAINS.map((chain) => (
|
||||
<SelectItem
|
||||
key={chain.value}
|
||||
value={chain.value}
|
||||
className="text-sm"
|
||||
>
|
||||
{chain.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`address-${index}`} className="text-sm">
|
||||
Token Address
|
||||
</Label>
|
||||
<Input
|
||||
id={`address-${index}`}
|
||||
placeholder="0x..."
|
||||
value={token.address}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor={`name-${index}`} className="text-sm">
|
||||
Token Name (Optional)
|
||||
</Label>
|
||||
<Input
|
||||
id={`name-${index}`}
|
||||
placeholder="e.g., Wrapped Ether"
|
||||
value={token.name || ""}
|
||||
onChange={(e) => updateToken(index, "name", e.target.value)}
|
||||
className="h-10 px-3 text-sm border-slate-400/20 focus-visible:border-slate-400/40"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={addToken}
|
||||
disabled={tokens.length >= 50}
|
||||
className="w-full h-10 text-sm"
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Token {tokens.length >= 50 && "(Maximum reached)"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="rounded-lg bg-slate-400/5 dark:bg-white/5 p-4 space-y-2">
|
||||
<h4 className="text-sm font-medium">Configuration Tips</h4>
|
||||
<ul className="list-disc pl-5 text-xs text-muted-foreground space-y-1">
|
||||
<li>Token addresses must be valid 40-character hex strings (0x...)</li>
|
||||
<li>You can track up to 50 tokens per connector</li>
|
||||
<li>Changes are saved automatically when you update the configuration</li>
|
||||
<li>Token names are optional but help identify tokens in search results</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ export const ConnectorConnectView: FC<ConnectorConnectViewProps> = ({
|
|||
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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
237
surfsense_web/content/docs/connectors/dexscreener.mdx
Normal file
237
surfsense_web/content/docs/connectors/dexscreener.mdx
Normal file
|
|
@ -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)
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
19
surfsense_web/public/assets/connector-icons/dexscreener.svg
Normal file
19
surfsense_web/public/assets/connector-icons/dexscreener.svg
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Background circle -->
|
||||
<circle cx="12" cy="12" r="11" fill="url(#dexGradient)" stroke="currentColor" stroke-width="1"/>
|
||||
|
||||
<!-- Chart/Trading icon -->
|
||||
<path d="M7 14L10 11L13 14L17 8" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="7" cy="14" r="1.5" fill="white"/>
|
||||
<circle cx="10" cy="11" r="1.5" fill="white"/>
|
||||
<circle cx="13" cy="14" r="1.5" fill="white"/>
|
||||
<circle cx="17" cy="8" r="1.5" fill="white"/>
|
||||
|
||||
<!-- Gradient definition -->
|
||||
<defs>
|
||||
<linearGradient id="dexGradient" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#3b82f6"/>
|
||||
<stop offset="100%" stop-color="#8b5cf6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 844 B |
Loading…
Add table
Add a link
Reference in a new issue