diff --git a/.example.env b/.example.env index d4724cde..4944b3d4 100644 --- a/.example.env +++ b/.example.env @@ -42,3 +42,7 @@ NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=XXX # Add a Reservoir API key for NFT price information RESERVOIR_API_KEY=XXX + + +#Add a GoldRush Api key to use Scam Tracker +GOLD_RUSH_API_KEY=xxxx \ No newline at end of file diff --git a/app/[locale]/scam-tracker/[slug]/CustomTransactionNode.tsx b/app/[locale]/scam-tracker/[slug]/CustomTransactionNode.tsx new file mode 100644 index 00000000..2a6a8dc6 --- /dev/null +++ b/app/[locale]/scam-tracker/[slug]/CustomTransactionNode.tsx @@ -0,0 +1,39 @@ +// import React from 'react'; +// import { Handle, Position, type NodeProps } from '@xyflow/react'; +// import { formatEther } from 'viem'; +// import Card from 'components/common/Card'; + +// interface TransactionData { +// address: string; +// amount?: string; +// token?: string; +// type: 'sender' | 'receiver'; +// timestamp?: number; +// } + +// const CustomTransactionNode = ({ data }: NodeProps) => { +// return ( +// +// +//
+//
+// +// {data.type === 'sender' ? 'From' : 'To'} +// +// +// {data.timestamp ? new Date(data.timestamp * 1000).toLocaleString() : ''} +// +//
+//
{data.address}
+// {data.amount && ( +//
+// {formatEther(BigInt(data.amount))} {data.token} +//
+// )} +//
+// +//
+// ); +// }; + +// export default CustomTransactionNode; diff --git a/app/[locale]/scam-tracker/[slug]/ScamTrackerChainSelect.tsx b/app/[locale]/scam-tracker/[slug]/ScamTrackerChainSelect.tsx new file mode 100644 index 00000000..cfee8004 --- /dev/null +++ b/app/[locale]/scam-tracker/[slug]/ScamTrackerChainSelect.tsx @@ -0,0 +1,17 @@ +'use client'; + +import ChainSelectHref from 'components/common/select/ChainSelectHref'; +import { getChainSlug } from 'lib/utils/chains'; +import { useCallback } from 'react'; + +interface Props { + chainId: number; +} + +// This is a wrapper around ChainSelectHref because we cannot pass the getUrl function as a prop from a server component +const ScamTrackerChainSelect = ({ chainId }: Props) => { + const getUrl = useCallback((chainId: number) => `/scam-tracker/${getChainSlug(chainId)}`, []); + return ; +}; + +export default ScamTrackerChainSelect; diff --git a/app/[locale]/scam-tracker/[slug]/ScamTrackerContent.tsx b/app/[locale]/scam-tracker/[slug]/ScamTrackerContent.tsx new file mode 100644 index 00000000..e95fec64 --- /dev/null +++ b/app/[locale]/scam-tracker/[slug]/ScamTrackerContent.tsx @@ -0,0 +1,48 @@ +'use client'; +import { buildGraphData, getTokenTransfers } from 'lib/utils/token-tracking'; +import { useState } from 'react'; +import { usePublicClient } from 'wagmi'; +import TransactionGraph from './TransactionGraph'; + +interface Props { + chainId: number; +} + +const ScamTrackerContent = ({ chainId }: Props) => { + const publicClient = usePublicClient(); + + const [graphData, setGraphData] = useState<{ nodes: any[]; edges: any[] } | null>(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const handleTransactionSubmit = async (hash: string) => { + try { + setIsLoading(true); + setError(null); + + const transfers = await getTokenTransfers(publicClient, hash as `0x${string}`); + const data = await buildGraphData(transfers); + + setGraphData(data); + } catch (err) { + console.error('Error fetching transaction data:', err); + setError('Failed to fetch transaction data. Please try again.'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ {error &&
{error}
} + {isLoading && ( +
+
+
+ )} + {graphData && } +
+ ); +}; + +export default ScamTrackerContent; diff --git a/app/[locale]/scam-tracker/[slug]/ScamTrackerSearchBox.tsx b/app/[locale]/scam-tracker/[slug]/ScamTrackerSearchBox.tsx new file mode 100644 index 00000000..28bf4546 --- /dev/null +++ b/app/[locale]/scam-tracker/[slug]/ScamTrackerSearchBox.tsx @@ -0,0 +1,55 @@ +'use client'; + +import { XMarkIcon } from '@heroicons/react/24/solid'; +import Button from 'components/common/Button'; +import Loader from 'components/common/Loader'; +import SearchBox from 'components/common/SearchBox'; +import { useTranslations } from 'next-intl'; +import { useState } from 'react'; +import type { FormEventHandler, HTMLAttributes } from 'react'; + +interface Props extends Omit, 'onSubmit'> { + onSubmit: (hash: string) => void; + chainName: string; +} + +const isValidHash = (hash: string) => /^0x([A-Fa-f0-9]{64})$/.test(hash); + +const ScamTrackerSearchBox = ({ onSubmit, chainName, ...props }: Props) => { + const t = useTranslations(); + const [value, setValue] = useState(''); + const [isValidating, setIsValidating] = useState(false); + + const handleSubmit: FormEventHandler = async (e) => { + e.preventDefault(); + if (value.trim() && isValidHash(value.trim())) { + setIsValidating(true); + // Simulate validation delay + await new Promise((resolve) => setTimeout(resolve, 500)); + setIsValidating(false); + onSubmit(value.trim()); + } + }; + + const isValid = isValidHash(value); + + return ( + setValue(e.target.value)} + value={value} + placeholder={t('scam_tracker.placeholder', { chainName })} + {...props} + > + {value && isValidating && }{' '} + {value && !isValidating && !isValid && } + {value && !isValidating && isValid && ( + + )} + + ); +}; + +export default ScamTrackerSearchBox; diff --git a/app/[locale]/scam-tracker/[slug]/TransactionGraph.tsx b/app/[locale]/scam-tracker/[slug]/TransactionGraph.tsx new file mode 100644 index 00000000..7e4ac650 --- /dev/null +++ b/app/[locale]/scam-tracker/[slug]/TransactionGraph.tsx @@ -0,0 +1,97 @@ +// 'use client'; +// import { useCallback, useState, useEffect } from 'react'; +// import ReactFlow, { +// type Node, +// type Edge, +// Controls, +// Background, +// useNodesState, +// useEdgesState, +// addEdge, +// MiniMap, +// } from '@xyflow/react'; +// import '@xyflow/react/dist/style.css'; +// // import TransactionModal from 'components/scam_tracker/TransactionModal'; +// import CustomTransactionNode from './CustomTransactionNode'; + +// interface TokenTransfer { +// token_address: string; +// from_address: string; +// to_address: string; +// amount: string; +// token_symbol: string; +// token_decimals: number; +// } + +// interface TransactionData { +// from_address: string; +// to_address: string; +// value: string; +// tx_hash: string; +// gas_metadata: { +// contract_ticker_symbol: string; +// contract_name: string; +// logo_url: string; +// }; +// block_signed_at: string; +// gas_offered: number; +// gas_spent: number; +// gas_price: number; +// fees_paid: string; +// tokenTransfers: TokenTransfer[]; +// } + +// interface TransactionGraphProps { +// data: { +// nodes: Node[]; +// edges: Edge[]; +// }; +// } + +// export default function TransactionGraph({ data }: TransactionGraphProps) { +// const [nodes, setNodes, onNodesChange] = useNodesState(data.nodes); +// const [edges, setEdges, onEdgesChange] = useEdgesState(data.edges); +// const [selectedTransaction, setSelectedTransaction] = useState(null); +// const [isModalOpen, setIsModalOpen] = useState(false); + +// useEffect(() => { +// setNodes(data.nodes); +// setEdges(data.edges); +// }, [data, setNodes, setEdges]); + +// const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), [setEdges]); + +// const onEdgeClick = useCallback((event: React.MouseEvent, edge: Edge) => { +// setSelectedTransaction(edge.data as TransactionData); +// setIsModalOpen(true); +// }, []); + +// const closeModal = () => { +// setIsModalOpen(false); +// setSelectedTransaction(null); +// }; + +// const nodeTypes = { customTransaction: CustomTransactionNode }; + +// return ( +// <> +//
+// +// +// +// +// +//
+// +// +// ); +// } diff --git a/app/[locale]/scam-tracker/[slug]/page.tsx b/app/[locale]/scam-tracker/[slug]/page.tsx new file mode 100644 index 00000000..9496d871 --- /dev/null +++ b/app/[locale]/scam-tracker/[slug]/page.tsx @@ -0,0 +1,77 @@ +import ChainDescription from 'components/common/ChainDescription'; +import ChainLogo from 'components/common/ChainLogo'; +import Prose from 'components/common/Prose'; +import { locales } from 'lib/i18n/config'; +import { SUPPORTED_CHAINS, getChainIdFromSlug, getChainName, getChainSlug } from 'lib/utils/chains'; +import { getOpenGraphImageUrl } from 'lib/utils/og'; +import type { Metadata, NextPage } from 'next'; +import { useTranslations } from 'next-intl'; +import { getTranslations, unstable_setRequestLocale } from 'next-intl/server'; +import ScamTrackerChainSelect from './ScamTrackerChainSelect'; +import ScamTrackerContent from './ScamTrackerContent'; +import ScamTrackerSearchBox from './ScamTrackerSearchBox'; + +interface Props { + params: { + locale: string; + slug: string; + }; +} + +export const dynamic = 'error'; +export const dynamicParams = false; + +export const generateStaticParams = () => { + const slugs = SUPPORTED_CHAINS.map(getChainSlug); + return locales.flatMap((locale) => slugs.map((slug) => ({ locale, slug }))); +}; + +export const generateMetadata = async ({ params: { locale, slug } }: Props): Promise => { + const t = await getTranslations({ locale }); + const chainId = getChainIdFromSlug(slug); + const chainName = getChainName(chainId); + + return { + title: t('scam_tracker.meta.title', { chainName }), + description: t('common.meta.description', { chainName }), + openGraph: { + images: getOpenGraphImageUrl(`/scam-tracker/${slug}`, locale), + }, + }; +}; + +const ScamTrackerPage: NextPage = ({ params }) => { + unstable_setRequestLocale(params.locale); + const t = useTranslations(); + + const chainId = getChainIdFromSlug(params.slug); + const chainName = getChainName(chainId); + + return ( +
+
+

+ {' '} +
{t('scam_tracker.title', { chainName })}
+

+
+ +
+

{t('scam_tracker.different_chain')}:

+
+ +
+
+ + + +

{t('scam_tracker.what_is_fund_flow.title', { chainName })}

+

{t('scam_tracker.what_is_fund_flow.content', { chainName })}

+

{t('scam_tracker.how_to_track.title', { chainName })}

+

{t('scam_tracker.how_to_track.content', { chainName })}

+
+
+ ); +}; + +export default ScamTrackerPage; diff --git a/components/scam_tracker/TransactionModal.tsx b/components/scam_tracker/TransactionModal.tsx new file mode 100644 index 00000000..e69de29b diff --git a/i18n.ts b/i18n.ts index b1074e43..67416d48 100644 --- a/i18n.ts +++ b/i18n.ts @@ -20,6 +20,7 @@ export default getRequestConfig(async ({ locale }) => { networks: (await import(`./locales/${locale}/networks.json`)).default, token_approval_checker: (await import(`./locales/${locale}/token_approval_checker.json`)).default, merchandise: (await import(`./locales/${locale}/merchandise.json`)).default, + scam_tracker: (await import(`./locales/${locale}/scam_tracker.json`)).default, }, defaultTranslationValues, }; diff --git a/lib/constants.ts b/lib/constants.ts index d8705f23..434c747b 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -31,3 +31,4 @@ export const HARPIE_API_KEY = process.env.HARPIE_API_KEY ?? process.env.NEXT_PUB export const WEBACY_API_KEY = process.env.WEBACY_API_KEY ?? process.env.NEXT_PUBLIC_WEBACY_API_KEY; export const NEFTURE_API_KEY = process.env.NEFTURE_API_KEY ?? process.env.NEXT_PUBLIC_NEFTURE_API_KEY; export const RESERVOIR_API_KEY = process.env.RESERVOIR_API_KEY ?? process.env.NEXT_PUBLIC_RESERVOIR_API_KEY; +export const GOLD_RUSH_API_KEY = process.env.GOLD_RUSH_API_KEY; diff --git a/lib/utils/track.ts b/lib/utils/track.ts new file mode 100644 index 00000000..6b035751 --- /dev/null +++ b/lib/utils/track.ts @@ -0,0 +1,97 @@ +import axios, { AxiosInstance } from 'axios'; +import { GOLD_RUSH_API_KEY } from 'lib/constants'; + +interface CovalentResponse { + data: { + data: { + items: T[]; + }; + error: boolean; + error_message?: string; + }; +} + +interface TransactionData { + // Add specific transaction fields as needed + [key: string]: any; +} + +class CovalentAPI { + private api: AxiosInstance; + + constructor(apiKey = GOLD_RUSH_API_KEY) { + if (!apiKey) { + throw new Error('Covalent API key is not configured'); + } + this.api = axios.create({ + baseURL: 'https://api.covalenthq.com/v1', + params: { key: apiKey }, + timeout: 30000, // 30 seconds timeout + }); + } + + private async handleRequest(request: Promise>): Promise { + try { + const response = await request; + console.log('Covalent API Response:', JSON.stringify(response, null, 2)); + + if (!response?.data) { + throw new Error('Invalid response format from Covalent API'); + } + + if (response.data.error) { + throw new Error(response.data.error_message || 'An error occurred while fetching data from Covalent'); + } + + if (!response.data.data?.items?.length) { + throw new Error('No transaction data found. Please verify the transaction hash and try again.'); + } + + return response.data.data.items; + } catch (error) { + console.error('Covalent API Error:', error); + if (axios.isAxiosError(error)) { + if (error.code === 'ECONNABORTED') { + throw new Error('Request timed out. Please try again later.'); + } else if (!error.response) { + throw new Error('Network connection failed. Please check your internet connection and try again.'); + } else if (error.response.status === 429) { + throw new Error('Rate limit exceeded. Please wait a moment and try again.'); + } else if (error.response.status === 401 || error.response.status === 403) { + throw new Error('API authentication failed. Please check your API key configuration.'); + } else { + throw new Error(`Failed to fetch transaction data: ${error.message}`); + } + } + throw new Error('Failed to fetch transaction data. Please try again.'); + } + } + + async getTransactionData(txHash: string): Promise { + if (!txHash || typeof txHash !== 'string') { + throw new Error('Invalid transaction hash provided'); + } + + const items = await this.handleRequest( + this.api.get(`/1/transaction_v2/${txHash}/`) + ); + return items[0]; + } + + async getAddressTransactions(address: string, pageSize: number = 5): Promise { + if (!address || typeof address !== 'string') { + throw new Error('Invalid address provided'); + } + + return this.handleRequest( + this.api.get(`/1/address/${address}/transactions_v2/`, { + params: { 'page-size': pageSize }, + }) + ); + } +} + +// Export a singleton instance +const covalentAPI = new CovalentAPI(); + +export const { getTransactionData, getAddressTransactions } = covalentAPI; diff --git a/locales/en/common.json b/locales/en/common.json index 4831f3eb..7ba3806d 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -32,6 +32,7 @@ "sending": "Sending", "switch_chain": "Switch Network", "switching": "Switching", + "track_funds": "Track Funds", "translate": "Help Us Translate This Page", "understand": "I Understand", "update": "Update", @@ -121,5 +122,6 @@ "transaction_failed": "❌ Transaction failed: {message}", "transaction_submitted": "✅ Transaction submitted!", "update_failed": "❌ This token does not support updating approvals, please revoke instead." - } + }, + "transaction_visualizer": "Transaction Visualizer" } diff --git a/locales/en/scam_tracker.json b/locales/en/scam_tracker.json new file mode 100644 index 00000000..e318b135 --- /dev/null +++ b/locales/en/scam_tracker.json @@ -0,0 +1,16 @@ +{ + "different_chain": "Or check fund flows on a different chain", + "meta": { + "title": "{chainName} Fund Flow Visualizer" + }, + "title": "{chainName} Fund Flow Visualizer", + "placeholder": "Enter transaction hash", + "what_is_fund_flow": { + "title": "What is {chainName} Fund Flow Visualizer?", + "content": "The Fund Flow Visualizer helps you track the movement of ERC20 tokens across {chainName}. Starting from a single transaction, it traces how tokens move between different wallets, helping you understand the flow of funds and identify potential patterns." + }, + "how_to_track": { + "title": "How to Track Fund Flows on {chainName}", + "content": "Enter a transaction hash above to see how tokens move across {chainName}. The visualizer will show you the initial transfer and subsequent movements of those tokens. You can expand the visualization to see more transactions and get a better understanding of where the funds eventually end up." + } +} diff --git a/locales/es/scam_tracker.json b/locales/es/scam_tracker.json new file mode 100644 index 00000000..7091cb17 --- /dev/null +++ b/locales/es/scam_tracker.json @@ -0,0 +1,16 @@ +{ + "different_chain": "O verificar los flujos de fondos en una cadena diferente", + "meta": { + "title": "Visualizador de Flujo de Fondos de {chainName}" + }, + "title": "Visualizador de Flujo de Fondos de {chainName}", + "placeholder": "Ingrese el hash de la transacción de {chainName}", + "what_is_fund_flow": { + "title": "¿Qué es el Visualizador de Flujo de Fondos de {chainName}?", + "content": "El Visualizador de Flujo de Fondos te ayuda a rastrear el movimiento de tokens ERC20 a través de {chainName}. Comenzando desde una única transacción, rastrea cómo los tokens se mueven entre diferentes billeteras, ayudándote a entender el flujo de fondos e identificar patrones potenciales." + }, + "how_to_track": { + "title": "Cómo Rastrear Flujos de Fondos en {chainName}", + "content": "Ingresa un hash de transacción arriba para ver cómo se mueven los tokens a través de {chainName}. El visualizador te mostrará la transferencia inicial y los movimientos subsiguientes de esos tokens. Puedes expandir la visualización para ver más transacciones y obtener una mejor comprensión de dónde terminan los fondos finalmente." + } +} diff --git a/locales/ja/scam_tracker.json b/locales/ja/scam_tracker.json new file mode 100644 index 00000000..742a0fdf --- /dev/null +++ b/locales/ja/scam_tracker.json @@ -0,0 +1,16 @@ +{ + "different_chain": "または別のチェーンで資金の流れを確認する", + "meta": { + "title": "{chainName}資金フロー可視化ツール" + }, + "title": "{chainName}資金フロー可視化ツール", + "placeholder": "{chainName}のトランザクションハッシュを入力", + "what_is_fund_flow": { + "title": "{chainName}資金フロー可視化ツールとは?", + "content": "資金フロー可視化ツールは、{chainName}上のERC20トークンの移動を追跡するのに役立ちます。単一のトランザクションから始まり、トークンが異なるウォレット間でどのように移動するかを追跡し、資金の流れを理解し、潜在的なパターンを特定するのに役立ちます。" + }, + "how_to_track": { + "title": "{chainName}での資金フローの追跡方法", + "content": "上記のトランザクションハッシュを入力して、{chainName}でトークンがどのように移動するかを確認します。可視化ツールは、初期の送金とそれらのトークンのその後の移動を表示します。可視化を拡張してより多くのトランザクションを表示し、最終的に資金がどこに行き着くかをより良く理解することができます。" + } +} diff --git a/locales/ru/scam_tracker.json b/locales/ru/scam_tracker.json new file mode 100644 index 00000000..7cf82cf8 --- /dev/null +++ b/locales/ru/scam_tracker.json @@ -0,0 +1,16 @@ +{ + "different_chain": "Или проверьте движение средств в другой сети", + "meta": { + "title": "Визуализатор движения средств {chainName}" + }, + "title": "Визуализатор движения средств {chainName}", + "placeholder": "Введите хэш транзакции {chainName}", + "what_is_fund_flow": { + "title": "Что такое визуализатор движения средств {chainName}?", + "content": "Визуализатор движения средств помогает отслеживать перемещение токенов ERC20 в сети {chainName}. Начиная с одной транзакции, он отслеживает, как токены перемещаются между различными кошельками, помогая понять поток средств и выявить потенциальные закономерности." + }, + "how_to_track": { + "title": "Как отследить движение средств в {chainName}", + "content": "Введите хэш транзакции выше, чтобы увидеть, как токены перемещаются по сети {chainName}. Визуализатор покажет вам начальный перевод и последующие перемещения этих токенов. Вы можете расширить визуализацию, чтобы увидеть больше транзакций и лучше понять, где в итоге оказываются средства." + } +} diff --git a/locales/zh/scam_tracker.json b/locales/zh/scam_tracker.json new file mode 100644 index 00000000..975236cc --- /dev/null +++ b/locales/zh/scam_tracker.json @@ -0,0 +1,16 @@ +{ + "different_chain": "或在其他链上查看资金流向", + "meta": { + "title": "{chainName}资金流向可视化工具" + }, + "title": "{chainName}资金流向可视化工具", + "placeholder": "输入{chainName}交易哈希", + "what_is_fund_flow": { + "title": "什么是{chainName}资金流向可视化工具?", + "content": "资金流向可视化工具帮助您追踪{chainName}上ERC20代币的转移。从单个交易开始,它会追踪代币在不同钱包之间的转移,帮助您理解资金流向并识别潜在模式。" + }, + "how_to_track": { + "title": "如何在{chainName}上追踪资金流向", + "content": "在上方输入交易哈希以查看代币在{chainName}上的转移情况。可视化工具将显示初始转账和这些代币后续的转移。您可以展开可视化视图以查看更多交易,更好地了解资金最终流向何处。" + } +} diff --git a/package.json b/package.json index 682471f1..815b9fff 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "dependencies": { "@abstract-foundation/agw-client": "^1.0.0", "@abstract-foundation/agw-react": "^1.0.0", + "@covalenthq/client-sdk": "^2.2.2", "@dotenvx/dotenvx": "^1.14.2", "@headlessui/react": "^2.1.2", "@heroicons/react": "^2.1.5", @@ -39,6 +40,8 @@ "@upstash/ratelimit": "^2.0.1", "@upstash/redis": "^1.34.0", "@vercel/speed-insights": "^1.0.12", + "@xyflow/react": "^12.4.2", + "axios": "^1.7.9", "dexie": "^4.0.8", "iron-session": "^8.0.3", "ky": "^1.7.0", diff --git a/yarn.lock b/yarn.lock index 7b08b42e..690ca889 100644 --- a/yarn.lock +++ b/yarn.lock @@ -516,6 +516,15 @@ __metadata: languageName: node linkType: hard +"@covalenthq/client-sdk@npm:^2.2.2": + version: 2.2.2 + resolution: "@covalenthq/client-sdk@npm:2.2.2" + dependencies: + big.js: "npm:^6.2.1" + checksum: 10/aca40f1bb6dad537af0ef22d783d563638b2a2027c3d19df4f8252f9c70afcccb6611d09a02cabd32a59183204eb5ce519a0118d8a92cda7ae25dce24032440e + languageName: node + linkType: hard + "@cypress/grep@npm:^4.1.0": version: 4.1.0 resolution: "@cypress/grep@npm:4.1.0" @@ -2707,6 +2716,57 @@ __metadata: languageName: node linkType: hard +"@types/d3-color@npm:*": + version: 3.1.3 + resolution: "@types/d3-color@npm:3.1.3" + checksum: 10/1cf0f512c09357b25d644ab01b54200be7c9b15c808333b0ccacf767fff36f17520b2fcde9dad45e1bd7ce84befad39b43da42b4fded57680fa2127006ca3ece + languageName: node + linkType: hard + +"@types/d3-drag@npm:^3.0.7": + version: 3.0.7 + resolution: "@types/d3-drag@npm:3.0.7" + dependencies: + "@types/d3-selection": "npm:*" + checksum: 10/93aba299c3a8d41ee326c5304ab694ceea135ed115c3b2ccab727a5d9bfc935f7f36d3fc416c013010eb755ac536c52adfcb15c195f241dc61f62650cc95088e + languageName: node + linkType: hard + +"@types/d3-interpolate@npm:*": + version: 3.0.4 + resolution: "@types/d3-interpolate@npm:3.0.4" + dependencies: + "@types/d3-color": "npm:*" + checksum: 10/72a883afd52c91132598b02a8cdfced9e783c54ca7e4459f9e29d5f45d11fb33f2cabc844e42fd65ba6e28f2a931dcce1add8607d2f02ef6fb8ea5b83ae84127 + languageName: node + linkType: hard + +"@types/d3-selection@npm:*, @types/d3-selection@npm:^3.0.10": + version: 3.0.11 + resolution: "@types/d3-selection@npm:3.0.11" + checksum: 10/2d2d993b9e9553d066566cb22916c632e5911090db99e247bd8c32855a344e6b7c25b674f3c27956c367a6b3b1214b09931ce854788c3be2072003e01f2c75d7 + languageName: node + linkType: hard + +"@types/d3-transition@npm:^3.0.8": + version: 3.0.9 + resolution: "@types/d3-transition@npm:3.0.9" + dependencies: + "@types/d3-selection": "npm:*" + checksum: 10/dad647c485440f176117e8a45f31aee9427d8d4dfa174eaa2f01e702641db53ad0f752a144b20987c7189723c4f0afe0bf0f16d95b2a91aa28937eee4339c161 + languageName: node + linkType: hard + +"@types/d3-zoom@npm:^3.0.8": + version: 3.0.8 + resolution: "@types/d3-zoom@npm:3.0.8" + dependencies: + "@types/d3-interpolate": "npm:*" + "@types/d3-selection": "npm:*" + checksum: 10/cc6ba975cf4f55f94933413954d81b87feb1ee8b8cee8f2202cf526f218dcb3ba240cbeb04ed80522416201c4a7394b37de3eb695d840a36d190dfb2d3e62cb5 + languageName: node + linkType: hard + "@types/debug@npm:^4.0.0, @types/debug@npm:^4.1.7": version: 4.1.12 resolution: "@types/debug@npm:4.1.12" @@ -3382,6 +3442,35 @@ __metadata: languageName: node linkType: hard +"@xyflow/react@npm:^12.4.2": + version: 12.4.2 + resolution: "@xyflow/react@npm:12.4.2" + dependencies: + "@xyflow/system": "npm:0.0.50" + classcat: "npm:^5.0.3" + zustand: "npm:^4.4.0" + peerDependencies: + react: ">=17" + react-dom: ">=17" + checksum: 10/7137228d1fa29dc77dd9798c646638ff16c1c4f983c6d6b0f5898118f3c519eecbf1e6c6b2f6b4e4516409d921e6aed676b0ba7e7b000d029b0fca2a0193c712 + languageName: node + linkType: hard + +"@xyflow/system@npm:0.0.50": + version: 0.0.50 + resolution: "@xyflow/system@npm:0.0.50" + dependencies: + "@types/d3-drag": "npm:^3.0.7" + "@types/d3-selection": "npm:^3.0.10" + "@types/d3-transition": "npm:^3.0.8" + "@types/d3-zoom": "npm:^3.0.8" + d3-drag: "npm:^3.0.0" + d3-selection: "npm:^3.0.0" + d3-zoom: "npm:^3.0.0" + checksum: 10/ac81cdb5e40b0ba2871d084395e885438fa038ed99421a60a166e6323588adbd09f57a1fe86f3e06c63c4c2d244fc2cfe609e4cc0177f32ac1d8bb2c45970d29 + languageName: node + linkType: hard + "JSONStream@npm:^1.3.5": version: 1.3.5 resolution: "JSONStream@npm:1.3.5" @@ -3744,6 +3833,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.7.9": + version: 1.7.9 + resolution: "axios@npm:1.7.9" + dependencies: + follow-redirects: "npm:^1.15.6" + form-data: "npm:^4.0.0" + proxy-from-env: "npm:^1.1.0" + checksum: 10/b7a5f660ea53ba9c2a745bf5ad77ad8bf4f1338e13ccc3f9f09f810267d6c638c03dac88b55dae8dc98b79c57d2d6835be651d58d2af97c174f43d289a9fd007 + languageName: node + linkType: hard + "babel-plugin-macros@npm:^3.1.0": version: 3.1.0 resolution: "babel-plugin-macros@npm:3.1.0" @@ -3792,6 +3892,13 @@ __metadata: languageName: node linkType: hard +"big.js@npm:^6.2.1": + version: 6.2.2 + resolution: "big.js@npm:6.2.2" + checksum: 10/018af3e572780b41536a987c3fc3636efe7d05671e8bf4a6bd22b62316e32f57abfc0fc849732adfd81b00b249f873a5a107e01ab5aa4fc3d42c181cc821bf47 + languageName: node + linkType: hard + "bin-links@npm:4.0.4": version: 4.0.4 resolution: "bin-links@npm:4.0.4" @@ -4206,6 +4313,13 @@ __metadata: languageName: node linkType: hard +"classcat@npm:^5.0.3": + version: 5.0.5 + resolution: "classcat@npm:5.0.5" + checksum: 10/19bdeb99b8923b47f9df978b6ef2c5a4cc3bcaa8fb6be16244e31fad619b291b366429747331903ac2ea27560ffd6066d14089a99c95535ce0f1e897525fa63d + languageName: node + linkType: hard + "clean-stack@npm:^2.0.0": version: 2.2.0 resolution: "clean-stack@npm:2.2.0" @@ -4395,7 +4509,7 @@ __metadata: languageName: node linkType: hard -"combined-stream@npm:^1.0.6, combined-stream@npm:~1.0.6": +"combined-stream@npm:^1.0.6, combined-stream@npm:^1.0.8, combined-stream@npm:~1.0.6": version: 1.0.8 resolution: "combined-stream@npm:1.0.8" dependencies: @@ -4726,6 +4840,88 @@ __metadata: languageName: node linkType: hard +"d3-color@npm:1 - 3": + version: 3.1.0 + resolution: "d3-color@npm:3.1.0" + checksum: 10/536ba05bfd9f4fcd6fa289b5974f5c846b21d186875684637e22bf6855e6aba93e24a2eb3712985c6af3f502fbbfa03708edb72f58142f626241a8a17258e545 + languageName: node + linkType: hard + +"d3-dispatch@npm:1 - 3": + version: 3.0.1 + resolution: "d3-dispatch@npm:3.0.1" + checksum: 10/2b82f41bf4ef88c2f9033dfe32815b67e2ef1c5754a74137a74c7d44d6f0d6ecfa934ac56ed8afe358f6c1f06462e8aa42ca0a388397b5b77a42721570e80487 + languageName: node + linkType: hard + +"d3-drag@npm:2 - 3, d3-drag@npm:^3.0.0": + version: 3.0.0 + resolution: "d3-drag@npm:3.0.0" + dependencies: + d3-dispatch: "npm:1 - 3" + d3-selection: "npm:3" + checksum: 10/80bc689935e5a46ee92b2d7f71e1c792279382affed9fbcf46034bff3ff7d3f50cf61a874da4bdf331037292b9e7dca5c6401a605d4bb699fdcb4e0c87e176ec + languageName: node + linkType: hard + +"d3-ease@npm:1 - 3": + version: 3.0.1 + resolution: "d3-ease@npm:3.0.1" + checksum: 10/985d46e868494e9e6806fedd20bad712a50dcf98f357bf604a843a9f6bc17714a657c83dd762f183173dcde983a3570fa679b2bc40017d40b24163cdc4167796 + languageName: node + linkType: hard + +"d3-interpolate@npm:1 - 3": + version: 3.0.1 + resolution: "d3-interpolate@npm:3.0.1" + dependencies: + d3-color: "npm:1 - 3" + checksum: 10/988d66497ef5c190cf64f8c80cd66e1e9a58c4d1f8932d776a8e3ae59330291795d5a342f5a97602782ccbef21a5df73bc7faf1f0dc46a5145ba6243a82a0f0e + languageName: node + linkType: hard + +"d3-selection@npm:2 - 3, d3-selection@npm:3, d3-selection@npm:^3.0.0": + version: 3.0.0 + resolution: "d3-selection@npm:3.0.0" + checksum: 10/0e5acfd305b31628b7be5009ba7303d84bb34817a88ed4dde9c8bd9c23528573fc5272f89fc04e5be03d2cbf5441a248d7274aaf55a8ef3dad46e16333d72298 + languageName: node + linkType: hard + +"d3-timer@npm:1 - 3": + version: 3.0.1 + resolution: "d3-timer@npm:3.0.1" + checksum: 10/004128602bb187948d72c7dc153f0f063f38ac7a584171de0b45e3a841ad2e17f1e40ad396a4af9cce5551b6ab4a838d5246d23492553843d9da4a4050a911e2 + languageName: node + linkType: hard + +"d3-transition@npm:2 - 3": + version: 3.0.1 + resolution: "d3-transition@npm:3.0.1" + dependencies: + d3-color: "npm:1 - 3" + d3-dispatch: "npm:1 - 3" + d3-ease: "npm:1 - 3" + d3-interpolate: "npm:1 - 3" + d3-timer: "npm:1 - 3" + peerDependencies: + d3-selection: 2 - 3 + checksum: 10/02571636acb82f5532117928a87fe25de68f088c38ab4a8b16e495f0f2d08a3fd2937eaebdefdfcf7f1461545524927d2632d795839b88d2e4c71e387aaaffac + languageName: node + linkType: hard + +"d3-zoom@npm:^3.0.0": + version: 3.0.0 + resolution: "d3-zoom@npm:3.0.0" + dependencies: + d3-dispatch: "npm:1 - 3" + d3-drag: "npm:2 - 3" + d3-interpolate: "npm:1 - 3" + d3-selection: "npm:2 - 3" + d3-transition: "npm:2 - 3" + checksum: 10/0e6e5c14e33c4ecdff311a900dd037dea407734f2dd2818988ed6eae342c1799e8605824523678bd404f81e37824cc588f62dbde46912444c89acc7888036c6b + languageName: node + linkType: hard + "dargs@npm:^8.0.0": version: 8.1.0 resolution: "dargs@npm:8.1.0" @@ -5722,6 +5918,16 @@ __metadata: languageName: node linkType: hard +"follow-redirects@npm:^1.15.6": + version: 1.15.9 + resolution: "follow-redirects@npm:1.15.9" + peerDependenciesMeta: + debug: + optional: true + checksum: 10/e3ab42d1097e90d28b913903841e6779eb969b62a64706a3eb983e894a5db000fbd89296f45f08885a0e54cd558ef62e81be1165da9be25a6c44920da10f424c + languageName: node + linkType: hard + "for-each@npm:^0.3.3": version: 0.3.3 resolution: "for-each@npm:0.3.3" @@ -5748,6 +5954,17 @@ __metadata: languageName: node linkType: hard +"form-data@npm:^4.0.0": + version: 4.0.1 + resolution: "form-data@npm:4.0.1" + dependencies: + asynckit: "npm:^0.4.0" + combined-stream: "npm:^1.0.8" + mime-types: "npm:^2.1.12" + checksum: 10/6adb1cff557328bc6eb8a68da205f9ae44ab0e88d4d9237aaf91eed591ffc64f77411efb9016af7d87f23d0a038c45a788aa1c6634e51175c4efa36c2bc53774 + languageName: node + linkType: hard + "form-data@npm:~2.3.2": version: 2.3.3 resolution: "form-data@npm:2.3.3" @@ -9605,6 +9822,13 @@ __metadata: languageName: node linkType: hard +"proxy-from-env@npm:^1.1.0": + version: 1.1.0 + resolution: "proxy-from-env@npm:1.1.0" + checksum: 10/f0bb4a87cfd18f77bc2fba23ae49c3b378fb35143af16cc478171c623eebe181678f09439707ad80081d340d1593cd54a33a0113f3ccb3f4bc9451488780ee23 + languageName: node + linkType: hard + "psl@npm:^1.1.33": version: 1.9.0 resolution: "psl@npm:1.9.0" @@ -10211,6 +10435,7 @@ __metadata: "@biomejs/biome": "npm:^1.9.4" "@commitlint/cli": "npm:^19.6.0" "@commitlint/config-conventional": "npm:^19.6.0" + "@covalenthq/client-sdk": "npm:^2.2.2" "@cypress/grep": "npm:^4.1.0" "@dotenvx/dotenvx": "npm:^1.14.2" "@headlessui/react": "npm:^2.1.2" @@ -10238,7 +10463,9 @@ __metadata: "@upstash/ratelimit": "npm:^2.0.1" "@upstash/redis": "npm:^1.34.0" "@vercel/speed-insights": "npm:^1.0.12" + "@xyflow/react": "npm:^12.4.2" autoprefixer: "npm:^10.4.20" + axios: "npm:^1.7.9" chai: "npm:^5.1.1" cypress: "npm:^13.13.3" dexie: "npm:^4.0.8" @@ -11701,6 +11928,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1.2.2": + version: 1.4.0 + resolution: "use-sync-external-store@npm:1.4.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10/08bf581a8a2effaefc355e9d18ed025d436230f4cc973db2f593166df357cf63e47b9097b6e5089b594758bde322e1737754ad64905e030d70f8ff7ee671fd01 + languageName: node + linkType: hard + "utf-8-validate@npm:^5.0.2": version: 5.0.10 resolution: "utf-8-validate@npm:5.0.10" @@ -12297,6 +12533,26 @@ __metadata: languageName: node linkType: hard +"zustand@npm:^4.4.0": + version: 4.5.6 + resolution: "zustand@npm:4.5.6" + dependencies: + use-sync-external-store: "npm:^1.2.2" + peerDependencies: + "@types/react": ">=16.8" + immer: ">=9.0.6" + react: ">=16.8" + peerDependenciesMeta: + "@types/react": + optional: true + immer: + optional: true + react: + optional: true + checksum: 10/9efd6faca813d9e6e35289b606b5f3c744e806c5a0e5e22ecf73b08af4a23037bdc13ac97902505c664a51aed806384c0652e259a0f199fa31a9bc672e1d7d91 + languageName: node + linkType: hard + "zustand@npm:^5.0.0-rc.2": version: 5.0.1 resolution: "zustand@npm:5.0.1"