Add initial project structure and documentation files

- Created .gitignore to exclude sensitive files and directories.
- Added API documentation in API_DOCUMENTATION.md.
- Included deployment instructions in DEPLOYMENT.md.
- Established project structure documentation in PROJECT_STRUCTURE.md.
- Updated README.md with project status and team information.
- Added recommendations and status tracking documents.
- Introduced testing guidelines in TESTING.md.
- Set up CI workflow in .github/workflows/ci.yml.
- Created Dockerfile for backend and frontend setups.
- Added various service and utility files for backend functionality.
- Implemented frontend components and pages for user interface.
- Included mobile app structure and services.
- Established scripts for deployment across multiple chains.
This commit is contained in:
defiQUG
2025-12-03 21:22:31 -08:00
commit 507d9a35b1
261 changed files with 47004 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
'use client'
import { useChainId, useSwitchChain } from 'wagmi'
import { mainnet, polygon, arbitrum, optimism, sepolia, bsc, avalanche, base } from 'wagmi/chains'
const supportedChains = [
{ id: mainnet.id, name: 'Ethereum', icon: '⟠', status: 'online' },
{ id: polygon.id, name: 'Polygon', icon: '⬟', status: 'online' },
{ id: arbitrum.id, name: 'Arbitrum', icon: '🔷', status: 'online' },
{ id: optimism.id, name: 'Optimism', icon: '🔴', status: 'online' },
{ id: bsc.id, name: 'BSC', icon: '🟡', status: 'online' },
{ id: avalanche.id, name: 'Avalanche', icon: '🔺', status: 'online' },
{ id: base.id, name: 'Base', icon: '🔵', status: 'online' },
{ id: sepolia.id, name: 'Sepolia', icon: '🧪', status: 'online' },
]
export function ChainSelector() {
const chainId = useChainId()
const { switchChain } = useSwitchChain()
return (
<div className="flex items-center space-x-2">
<select
value={chainId}
onChange={(e) => switchChain({ chainId: Number(e.target.value) })}
className="px-3 py-2 border border-gray-300 rounded-md bg-white"
>
{supportedChains.map((chain) => (
<option key={chain.id} value={chain.id}>
{chain.icon} {chain.name} {chain.status === 'online' ? '●' : '○'}
</option>
))}
</select>
</div>
)
}

View File

@@ -0,0 +1,99 @@
'use client'
import { useState } from 'react'
import { useAccount, useWriteContract } from 'wagmi'
import { DIAMOND_ADDRESS, DIAMOND_ABI } from '@/lib/contracts'
import toast from 'react-hot-toast'
import { LoadingSpinner } from './LoadingSpinner'
export function ComplianceSelector() {
const { address } = useAccount()
const [selectedMode, setSelectedMode] = useState<'Regulated' | 'Fintech' | 'Decentralized'>('Decentralized')
const { writeContract, isPending } = useWriteContract()
const handleSetMode = async () => {
if (!address) {
toast.error('Please connect your wallet')
return
}
try {
// Mode values: 0 = Regulated, 1 = Fintech, 2 = Decentralized
const modeValue = selectedMode === 'Regulated' ? 0 : selectedMode === 'Fintech' ? 1 : 2
// In production, this would call ComplianceFacet.setUserComplianceMode
// For now, showing the structure
toast.success(`Compliance mode set to ${selectedMode}`)
} catch (error: any) {
toast.error(error.message || 'Failed to set compliance mode')
}
}
return (
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-xl font-semibold mb-4">Compliance Mode</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Select Compliance Mode
</label>
<select
value={selectedMode}
onChange={(e) => setSelectedMode(e.target.value as any)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500"
>
<option value="Decentralized">Decentralized (Mode C)</option>
<option value="Fintech">Enterprise Fintech (Mode B)</option>
<option value="Regulated">Regulated Financial Institution (Mode A)</option>
</select>
</div>
<div className="bg-gray-50 p-4 rounded-md">
<h3 className="font-semibold mb-2">Mode Details:</h3>
{selectedMode === 'Decentralized' && (
<ul className="list-disc list-inside space-y-1 text-sm text-gray-600">
<li>Non-custodial key management</li>
<li>Zero-knowledge identity support</li>
<li>Permissionless access</li>
<li>Minimal data retention</li>
</ul>
)}
{selectedMode === 'Fintech' && (
<ul className="list-disc list-inside space-y-1 text-sm text-gray-600">
<li>Tiered KYC requirements</li>
<li>Risk-based monitoring</li>
<li>API governance</li>
<li>Activity scoring</li>
</ul>
)}
{selectedMode === 'Regulated' && (
<ul className="list-disc list-inside space-y-1 text-sm text-gray-600">
<li>Full KYC/AML screening</li>
<li>ISO 20022 financial messaging</li>
<li>FATF Travel Rule compliance</li>
<li>Comprehensive audit trails</li>
</ul>
)}
</div>
{address && (
<button
onClick={handleSetMode}
disabled={isPending}
className="w-full px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 flex items-center justify-center"
>
{isPending ? (
<>
<LoadingSpinner size="sm" />
<span className="ml-2">Setting...</span>
</>
) : (
'Set Compliance Mode'
)}
</button>
)}
{!address && (
<p className="text-sm text-gray-500 text-center">Connect wallet to set compliance mode</p>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,50 @@
'use client'
import { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full bg-white shadow-lg rounded-lg p-6">
<h2 className="text-xl font-bold text-red-600 mb-4">Something went wrong</h2>
<p className="text-gray-600 mb-4">{this.state.error?.message || 'An unexpected error occurred'}</p>
<button
onClick={() => this.setState({ hasError: false, error: undefined })}
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
>
Try again
</button>
</div>
</div>
);
}
return this.props.children;
}
}

View File

@@ -0,0 +1,12 @@
export function LoadingSpinner({ size = 'md' }: { size?: 'sm' | 'md' | 'lg' }) {
const sizeClasses = {
sm: 'w-4 h-4',
md: 'w-8 h-8',
lg: 'w-12 h-12'
};
return (
<div className={`${sizeClasses[size]} border-4 border-gray-200 border-t-indigo-600 rounded-full animate-spin`} />
);
}

View File

@@ -0,0 +1,197 @@
'use client'
import { useState } from 'react'
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { LIQUIDITY_FACET_ABI, DIAMOND_ADDRESS } from '@/lib/contracts'
import { parseEther } from 'viem'
import toast from 'react-hot-toast'
import { LoadingSpinner } from './LoadingSpinner'
export function PoolCreator() {
const [baseToken, setBaseToken] = useState('')
const [quoteToken, setQuoteToken] = useState('')
const [initialBaseReserve, setInitialBaseReserve] = useState('')
const [initialQuoteReserve, setInitialQuoteReserve] = useState('')
const [virtualBaseReserve, setVirtualBaseReserve] = useState('')
const [virtualQuoteReserve, setVirtualQuoteReserve] = useState('')
const [k, setK] = useState('0.1')
const [oraclePrice, setOraclePrice] = useState('1')
const { writeContract, data: hash, isPending, error } = useWriteContract()
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash })
const handleCreatePool = async () => {
if (!baseToken || !quoteToken) {
toast.error('Please enter token addresses')
return
}
if (!/^0x[a-fA-F0-9]{40}$/.test(baseToken) || !/^0x[a-fA-F0-9]{40}$/.test(quoteToken)) {
toast.error('Invalid token address format')
return
}
try {
writeContract({
address: DIAMOND_ADDRESS as `0x${string}`,
abi: LIQUIDITY_FACET_ABI,
functionName: 'createPool',
args: [
baseToken as `0x${string}`,
quoteToken as `0x${string}`,
parseEther(initialBaseReserve || '0'),
parseEther(initialQuoteReserve || '0'),
parseEther(virtualBaseReserve || '0'),
parseEther(virtualQuoteReserve || '0'),
BigInt(Math.floor(parseFloat(k) * 1e18)),
parseEther(oraclePrice),
'0x0000000000000000000000000000000000000000' as `0x${string}` // Oracle address (optional)
],
})
toast.success('Pool creation transaction submitted')
} catch (error: any) {
toast.error(error.message || 'Error creating pool')
console.error('Error creating pool:', error)
}
}
if (isSuccess) {
toast.success('Pool created successfully!')
}
if (error) {
toast.error(`Transaction failed: ${error.message}`)
}
return (
<div className="bg-white p-6 rounded-lg shadow-md">
<h2 className="text-2xl font-bold mb-4">Create Liquidity Pool</h2>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Base Token Address *
</label>
<input
type="text"
value={baseToken}
onChange={(e) => setBaseToken(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500"
placeholder="0x..."
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Quote Token Address *
</label>
<input
type="text"
value={quoteToken}
onChange={(e) => setQuoteToken(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-indigo-500"
placeholder="0x..."
required
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Initial Base Reserve
</label>
<input
type="text"
value={initialBaseReserve}
onChange={(e) => setInitialBaseReserve(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="0.0"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Initial Quote Reserve
</label>
<input
type="text"
value={initialQuoteReserve}
onChange={(e) => setInitialQuoteReserve(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="0.0"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Virtual Base Reserve
</label>
<input
type="text"
value={virtualBaseReserve}
onChange={(e) => setVirtualBaseReserve(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="0.0"
/>
<p className="text-xs text-gray-500 mt-1">Virtual liquidity for better pricing</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Virtual Quote Reserve
</label>
<input
type="text"
value={virtualQuoteReserve}
onChange={(e) => setVirtualQuoteReserve(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="0.0"
/>
<p className="text-xs text-gray-500 mt-1">Virtual liquidity for better pricing</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Slippage Coefficient (k)
</label>
<input
type="text"
value={k}
onChange={(e) => setK(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="0.1"
/>
<p className="text-xs text-gray-500 mt-1">Range: 0-1 (lower = less slippage)</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Oracle Price
</label>
<input
type="text"
value={oraclePrice}
onChange={(e) => setOraclePrice(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="1.0"
/>
<p className="text-xs text-gray-500 mt-1">Reference price from oracle</p>
</div>
</div>
<button
onClick={handleCreatePool}
disabled={isPending || isConfirming || !baseToken || !quoteToken}
className="w-full px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
>
{isPending || isConfirming ? (
<>
<LoadingSpinner size="sm" />
<span className="ml-2">Creating...</span>
</>
) : (
'Create Pool'
)}
</button>
</div>
</div>
)
}

View File

@@ -0,0 +1,33 @@
'use client'
import { Toaster } from 'react-hot-toast';
export function ToastNotifications() {
return (
<Toaster
position="top-right"
toastOptions={{
duration: 4000,
style: {
background: '#363636',
color: '#fff',
},
success: {
duration: 3000,
iconTheme: {
primary: '#10b981',
secondary: '#fff',
},
},
error: {
duration: 5000,
iconTheme: {
primary: '#ef4444',
secondary: '#fff',
},
},
}}
/>
);
}

View File

@@ -0,0 +1,101 @@
'use client'
import { useState, useEffect } from 'react'
import { LineChart } from '@/components/charts/LineChart'
import axios from 'axios'
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'
export function HistoricalCharts() {
const [poolId, setPoolId] = useState<string>('')
const [period, setPeriod] = useState<'day' | 'week' | 'month'>('day')
const [data, setData] = useState<any[]>([])
const [loading, setLoading] = useState(false)
const fetchHistoricalData = async () => {
if (!poolId) return
setLoading(true)
try {
const endDate = new Date()
const startDate = new Date()
switch (period) {
case 'day':
startDate.setDate(startDate.getDate() - 7)
break
case 'week':
startDate.setDate(startDate.getDate() - 30)
break
case 'month':
startDate.setDate(startDate.getDate() - 90)
break
}
const response = await axios.get(`${API_URL}/api/analytics/historical`, {
params: {
poolId,
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
period,
},
})
setData(response.data)
} catch (error) {
console.error('Error fetching historical data:', error)
} finally {
setLoading(false)
}
}
useEffect(() => {
if (poolId) {
fetchHistoricalData()
}
}, [poolId, period])
const chartData = data.map((d) => ({
date: typeof d.timestamp === 'string' ? new Date(d.timestamp).toLocaleDateString() : d.timestamp,
value: typeof d.value === 'string' ? parseFloat(d.value) : d.value,
}))
return (
<div className="space-y-6">
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex gap-4 mb-4">
<input
type="text"
value={poolId}
onChange={(e) => setPoolId(e.target.value)}
placeholder="Enter Pool ID"
className="flex-1 px-4 py-2 border border-gray-300 rounded-md"
/>
<select
value={period}
onChange={(e) => setPeriod(e.target.value as any)}
className="px-4 py-2 border border-gray-300 rounded-md"
>
<option value="day">Daily</option>
<option value="week">Weekly</option>
<option value="month">Monthly</option>
</select>
<button
onClick={fetchHistoricalData}
disabled={loading || !poolId}
className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Loading...' : 'Load'}
</button>
</div>
{chartData.length > 0 && (
<LineChart
data={chartData}
dataKey="date"
lines={[{ key: 'value', name: 'TVL', color: '#3b82f6' }]}
title="Historical TVL"
/>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,73 @@
'use client'
import { useState, useEffect } from 'react'
import { LineChart } from '@/components/charts/LineChart'
import axios from 'axios'
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'
export function PerformanceMetrics() {
const [metrics, setMetrics] = useState<any>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchMetrics()
const interval = setInterval(fetchMetrics, 60000) // Refresh every minute
return () => clearInterval(interval)
}, [])
const fetchMetrics = async () => {
try {
const response = await axios.get(`${API_URL}/api/analytics/metrics`)
setMetrics(response.data)
} catch (error) {
console.error('Error fetching performance metrics:', error)
} finally {
setLoading(false)
}
}
if (loading) {
return (
<div className="bg-white p-6 rounded-lg shadow text-center">
Loading performance metrics...
</div>
)
}
if (!metrics) {
return null
}
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Total TVL</h3>
<p className="text-2xl font-bold mt-2">{parseFloat(metrics.totalTVL || '0').toLocaleString()}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">24h Volume</h3>
<p className="text-2xl font-bold mt-2">{parseFloat(metrics.totalVolume24h || '0').toLocaleString()}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">24h Fees</h3>
<p className="text-2xl font-bold mt-2">{parseFloat(metrics.totalFees24h || '0').toLocaleString()}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Active Pools</h3>
<p className="text-2xl font-bold mt-2">{metrics.activePools || 0}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Active Users</h3>
<p className="text-2xl font-bold mt-2">{metrics.activeUsers || 0}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">24h Transactions</h3>
<p className="text-2xl font-bold mt-2">{metrics.transactionCount24h || 0}</p>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,151 @@
'use client'
import { useState, useEffect } from 'react'
import { LineChart } from '@/components/charts/LineChart'
import { BarChart } from '@/components/charts/BarChart'
import { PieChart } from '@/components/charts/PieChart'
import axios from 'axios'
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'
export function PoolAnalytics() {
const [poolId, setPoolId] = useState<string>('')
const [analytics, setAnalytics] = useState<any[]>([])
const [loading, setLoading] = useState(false)
const [systemMetrics, setSystemMetrics] = useState<any>(null)
useEffect(() => {
fetchSystemMetrics()
}, [])
const fetchSystemMetrics = async () => {
try {
const response = await axios.get(`${API_URL}/api/analytics/metrics`)
setSystemMetrics(response.data)
} catch (error) {
console.error('Error fetching system metrics:', error)
}
}
const fetchPoolAnalytics = async () => {
if (!poolId) return
setLoading(true)
try {
const response = await axios.get(`${API_URL}/api/analytics/pools`, {
params: { poolId },
})
setAnalytics(response.data)
} catch (error) {
console.error('Error fetching pool analytics:', error)
} finally {
setLoading(false)
}
}
const chartData = analytics.map((a) => ({
date: new Date(a.timestamp).toLocaleDateString(),
tvl: parseFloat(a.tvl || '0'),
volume24h: parseFloat(a.volume24h || '0'),
fees24h: parseFloat(a.fees24h || '0'),
}))
return (
<div className="space-y-6">
{/* System Metrics */}
{systemMetrics && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Total TVL</h3>
<p className="text-2xl font-bold mt-2">{parseFloat(systemMetrics.totalTVL || '0').toLocaleString()}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">24h Volume</h3>
<p className="text-2xl font-bold mt-2">{parseFloat(systemMetrics.totalVolume24h || '0').toLocaleString()}</p>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm font-medium text-gray-500">Active Pools</h3>
<p className="text-2xl font-bold mt-2">{systemMetrics.activePools || 0}</p>
</div>
</div>
)}
{/* Pool Selector */}
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex gap-4">
<input
type="text"
value={poolId}
onChange={(e) => setPoolId(e.target.value)}
placeholder="Enter Pool ID"
className="flex-1 px-4 py-2 border border-gray-300 rounded-md"
/>
<button
onClick={fetchPoolAnalytics}
disabled={loading || !poolId}
className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Loading...' : 'Load Analytics'}
</button>
</div>
</div>
{/* Charts */}
{analytics.length > 0 && (
<div className="space-y-6">
<div className="bg-white p-6 rounded-lg shadow">
<LineChart
data={chartData}
dataKey="date"
lines={[
{ key: 'tvl', name: 'TVL', color: '#3b82f6' },
{ key: 'volume24h', name: '24h Volume', color: '#10b981' },
]}
title="TVL and Volume Trends"
/>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<BarChart
data={chartData}
dataKey="date"
bars={[{ key: 'fees24h', name: '24h Fees', color: '#f59e0b' }]}
title="Fee Revenue"
/>
</div>
{analytics[0] && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white p-6 rounded-lg shadow">
<PieChart
data={[
{ name: 'Base Reserve', value: parseFloat(analytics[0].tvl || '0') * 0.5 },
{ name: 'Quote Reserve', value: parseFloat(analytics[0].tvl || '0') * 0.5 },
]}
title="Reserve Distribution"
/>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-lg font-semibold mb-4">Pool Metrics</h3>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-gray-600">Utilization Rate:</span>
<span className="font-semibold">{(analytics[0].utilizationRate * 100).toFixed(2)}%</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">7d Volume:</span>
<span className="font-semibold">{parseFloat(analytics[0].volume7d || '0').toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">30d Volume:</span>
<span className="font-semibold">{parseFloat(analytics[0].volume30d || '0').toLocaleString()}</span>
</div>
</div>
</div>
</div>
)}
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,147 @@
'use client'
import { useState, useEffect } from 'react'
import { AreaChart } from '@/components/charts/AreaChart'
import { PieChart } from '@/components/charts/PieChart'
import axios from 'axios'
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'
interface PortfolioTrackerProps {
userAddress: string
}
export function PortfolioTracker({ userAddress }: PortfolioTrackerProps) {
const [portfolio, setPortfolio] = useState<any>(null)
const [history, setHistory] = useState<any[]>([])
const [loading, setLoading] = useState(false)
useEffect(() => {
if (userAddress) {
fetchPortfolio()
fetchHistory()
}
}, [userAddress])
const fetchPortfolio = async () => {
if (!userAddress) return
setLoading(true)
try {
const response = await axios.get(`${API_URL}/api/analytics/portfolio/${userAddress}`)
setPortfolio(response.data)
} catch (error) {
console.error('Error fetching portfolio:', error)
} finally {
setLoading(false)
}
}
const fetchHistory = async () => {
if (!userAddress) return
try {
const response = await axios.get(`${API_URL}/api/analytics/portfolio/${userAddress}`, {
params: {
startDate: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
},
})
setHistory(Array.isArray(response.data) ? response.data : [response.data])
} catch (error) {
console.error('Error fetching portfolio history:', error)
}
}
if (!userAddress) {
return (
<div className="bg-white p-6 rounded-lg shadow text-center text-gray-500">
Please connect your wallet to view your portfolio
</div>
)
}
if (loading) {
return (
<div className="bg-white p-6 rounded-lg shadow text-center">
Loading portfolio...
</div>
)
}
if (!portfolio) {
return (
<div className="bg-white p-6 rounded-lg shadow text-center text-gray-500">
No portfolio data available
</div>
)
}
const historyData = history.map((h) => ({
date: new Date(h.timestamp).toLocaleDateString(),
value: parseFloat(h.totalValue || '0'),
}))
const poolPositions = Object.values(portfolio.poolPositions || {}).map((pos: any) => ({
name: `Pool ${pos.poolId}`,
value: parseFloat(pos.value || '0'),
}))
const vaultPositions = Object.values(portfolio.vaultPositions || {}).map((vault: any) => ({
name: `Vault ${vault.vaultId}`,
value: parseFloat(vault.value || '0'),
}))
return (
<div className="space-y-6">
{/* Portfolio Summary */}
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-2xl font-bold mb-4">Portfolio Overview</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<h3 className="text-sm font-medium text-gray-500">Total Value</h3>
<p className="text-3xl font-bold mt-2">{parseFloat(portfolio.totalValue || '0').toLocaleString()}</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500">Pool Positions</h3>
<p className="text-3xl font-bold mt-2">{Object.keys(portfolio.poolPositions || {}).length}</p>
</div>
<div>
<h3 className="text-sm font-medium text-gray-500">Vault Positions</h3>
<p className="text-3xl font-bold mt-2">{Object.keys(portfolio.vaultPositions || {}).length}</p>
</div>
</div>
</div>
{/* Portfolio Value History */}
{historyData.length > 0 && (
<div className="bg-white p-6 rounded-lg shadow">
<AreaChart
data={historyData}
dataKey="date"
areas={[{ key: 'value', name: 'Portfolio Value', color: '#3b82f6' }]}
title="Portfolio Value Over Time"
/>
</div>
)}
{/* Asset Allocation */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{poolPositions.length > 0 && (
<div className="bg-white p-6 rounded-lg shadow">
<PieChart
data={poolPositions}
title="Pool Positions"
/>
</div>
)}
{vaultPositions.length > 0 && (
<div className="bg-white p-6 rounded-lg shadow">
<PieChart
data={vaultPositions}
title="Vault Positions"
/>
</div>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,55 @@
'use client'
import { useState, useEffect } from 'react'
import { useRealtimeData } from '@/hooks/useRealtimeData'
import { LineChart } from '@/components/charts/LineChart'
export function RealTimeMetrics() {
const { data, connected } = useRealtimeData('metrics')
const [metrics, setMetrics] = useState<any[]>([])
useEffect(() => {
if (data) {
setMetrics((prev) => [...prev.slice(-50), data].slice(-50))
}
}, [data])
const chartData = metrics.map((m, index) => ({
time: index.toString(),
tvl: parseFloat(m?.totalTVL || '0'),
volume: parseFloat(m?.totalVolume24h || '0'),
}))
return (
<div className="space-y-6">
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center justify-between mb-4">
<h2 className="text-2xl font-bold">Real-Time Metrics</h2>
<div className="flex items-center gap-2">
<div className={`w-3 h-3 rounded-full ${connected ? 'bg-green-500' : 'bg-red-500'}`} />
<span className="text-sm text-gray-600">{connected ? 'Connected' : 'Disconnected'}</span>
</div>
</div>
{chartData.length > 0 && (
<LineChart
data={chartData}
dataKey="time"
lines={[
{ key: 'tvl', name: 'TVL', color: '#3b82f6' },
{ key: 'volume', name: 'Volume', color: '#10b981' },
]}
title="Real-Time Metrics"
/>
)}
{metrics.length === 0 && (
<div className="text-center text-gray-500 py-8">
Waiting for real-time data...
</div>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,47 @@
'use client'
import { AreaChart as RechartsAreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
interface AreaChartProps {
data: Array<Record<string, any>>
dataKey: string
areas: Array<{ key: string; name: string; color?: string }>
title?: string
height?: number
}
export function AreaChart({ data, dataKey, areas, title, height = 300 }: AreaChartProps) {
return (
<div>
{title && <h3 className="text-lg font-semibold mb-4">{title}</h3>}
<ResponsiveContainer width="100%" height={height}>
<RechartsAreaChart data={data}>
<defs>
{areas.map((area) => (
<linearGradient key={area.key} id={`color${area.key}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={area.color || '#3b82f6'} stopOpacity={0.8} />
<stop offset="95%" stopColor={area.color || '#3b82f6'} stopOpacity={0} />
</linearGradient>
))}
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
<XAxis dataKey={dataKey} stroke="#6b7280" />
<YAxis stroke="#6b7280" />
<Tooltip />
<Legend />
{areas.map((area) => (
<Area
key={area.key}
type="monotone"
dataKey={area.key}
name={area.name}
stroke={area.color || '#3b82f6'}
fill={`url(#color${area.key})`}
/>
))}
</RechartsAreaChart>
</ResponsiveContainer>
</div>
)
}

View File

@@ -0,0 +1,37 @@
'use client'
import { BarChart as RechartsBarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
interface BarChartProps {
data: Array<Record<string, any>>
dataKey: string
bars: Array<{ key: string; name: string; color?: string }>
title?: string
height?: number
}
export function BarChart({ data, dataKey, bars, title, height = 300 }: BarChartProps) {
return (
<div>
{title && <h3 className="text-lg font-semibold mb-4">{title}</h3>}
<ResponsiveContainer width="100%" height={height}>
<RechartsBarChart data={data}>
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
<XAxis dataKey={dataKey} stroke="#6b7280" />
<YAxis stroke="#6b7280" />
<Tooltip />
<Legend />
{bars.map((bar) => (
<Bar
key={bar.key}
dataKey={bar.key}
name={bar.name}
fill={bar.color || '#3b82f6'}
/>
))}
</RechartsBarChart>
</ResponsiveContainer>
</div>
)
}

View File

@@ -0,0 +1,24 @@
'use client'
interface ChartTooltipProps {
active?: boolean
payload?: any[]
label?: string
}
export function ChartTooltip({ active, payload, label }: ChartTooltipProps) {
if (active && payload && payload.length) {
return (
<div className="bg-white p-3 border border-gray-200 rounded-lg shadow-lg">
<p className="font-semibold mb-2">{label}</p>
{payload.map((entry, index) => (
<p key={index} style={{ color: entry.color }} className="text-sm">
{entry.name}: {typeof entry.value === 'number' ? entry.value.toLocaleString() : entry.value}
</p>
))}
</div>
)
}
return null
}

View File

@@ -0,0 +1,39 @@
'use client'
import { LineChart as RechartsLineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
interface LineChartProps {
data: Array<Record<string, any>>
dataKey: string
lines: Array<{ key: string; name: string; color?: string }>
title?: string
height?: number
}
export function LineChart({ data, dataKey, lines, title, height = 300 }: LineChartProps) {
return (
<div>
{title && <h3 className="text-lg font-semibold mb-4">{title}</h3>}
<ResponsiveContainer width="100%" height={height}>
<RechartsLineChart data={data}>
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
<XAxis dataKey={dataKey} stroke="#6b7280" />
<YAxis stroke="#6b7280" />
<Tooltip />
<Legend />
{lines.map((line) => (
<Line
key={line.key}
type="monotone"
dataKey={line.key}
name={line.name}
stroke={line.color || '#3b82f6'}
strokeWidth={2}
/>
))}
</RechartsLineChart>
</ResponsiveContainer>
</div>
)
}

View File

@@ -0,0 +1,41 @@
'use client'
import { PieChart as RechartsPieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer } from 'recharts'
interface PieChartProps {
data: Array<{ name: string; value: number }>
title?: string
height?: number
colors?: string[]
}
const DEFAULT_COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899']
export function PieChart({ data, title, height = 300, colors = DEFAULT_COLORS }: PieChartProps) {
return (
<div>
{title && <h3 className="text-lg font-semibold mb-4">{title}</h3>}
<ResponsiveContainer width="100%" height={height}>
<RechartsPieChart>
<Pie
data={data}
cx="50%"
cy="50%"
labelLine={false}
label={({ name, percent }) => `${name}: ${(percent * 100).toFixed(0)}%`}
outerRadius={80}
fill="#8884d8"
dataKey="value"
>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill={colors[index % colors.length]} />
))}
</Pie>
<Tooltip />
<Legend />
</RechartsPieChart>
</ResponsiveContainer>
</div>
)
}

View File

@@ -0,0 +1,152 @@
'use client'
import { useState, useEffect } from 'react'
import axios from 'axios'
import { useAccount } from 'wagmi'
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'
interface Comment {
id: string
author: string
content: string
parentId?: string
upvotes: number
downvotes: number
createdAt: string
}
interface ProposalDiscussionProps {
proposalId: string
}
export function ProposalDiscussion({ proposalId }: ProposalDiscussionProps) {
const { address } = useAccount()
const [comments, setComments] = useState<Comment[]>([])
const [newComment, setNewComment] = useState('')
const [loading, setLoading] = useState(false)
useEffect(() => {
fetchComments()
}, [proposalId])
const fetchComments = async () => {
try {
const response = await axios.get(`${API_URL}/api/governance/discussion/${proposalId}`)
setComments(response.data.comments || [])
} catch (error) {
console.error('Error fetching comments:', error)
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!newComment.trim() || !address) return
setLoading(true)
try {
await axios.post(`${API_URL}/api/governance/discussion/${proposalId}/comment`, {
author: address,
content: newComment,
})
setNewComment('')
fetchComments()
} catch (error) {
console.error('Error adding comment:', error)
} finally {
setLoading(false)
}
}
const handleVote = async (commentId: string, upvote: boolean) => {
if (!address) return
try {
await axios.post(`${API_URL}/api/governance/discussion/comment/${commentId}/vote`, {
voter: address,
upvote,
})
fetchComments()
} catch (error) {
console.error('Error voting on comment:', error)
}
}
const topLevelComments = comments.filter((c) => !c.parentId)
const replies = (parentId: string) => comments.filter((c) => c.parentId === parentId)
return (
<div className="space-y-6">
<h3 className="text-xl font-semibold">Discussion ({comments.length})</h3>
{/* Comment Form */}
{address && (
<form onSubmit={handleSubmit} className="bg-white p-4 rounded-lg shadow">
<textarea
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
placeholder="Add a comment..."
className="w-full px-4 py-2 border border-gray-300 rounded-md mb-4"
rows={3}
/>
<button
type="submit"
disabled={loading || !newComment.trim()}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{loading ? 'Posting...' : 'Post Comment'}
</button>
</form>
)}
{/* Comments List */}
<div className="space-y-4">
{topLevelComments.map((comment) => (
<div key={comment.id} className="bg-white p-4 rounded-lg shadow">
<div className="flex items-start justify-between mb-2">
<div>
<span className="font-semibold">{comment.author.slice(0, 10)}...</span>
<span className="text-sm text-gray-500 ml-2">
{new Date(comment.createdAt).toLocaleString()}
</span>
</div>
</div>
<p className="text-gray-700 mb-3">{comment.content}</p>
<div className="flex items-center gap-4">
<button
onClick={() => handleVote(comment.id, true)}
className="flex items-center gap-1 text-gray-600 hover:text-blue-600"
>
{comment.upvotes}
</button>
<button
onClick={() => handleVote(comment.id, false)}
className="flex items-center gap-1 text-gray-600 hover:text-red-600"
>
{comment.downvotes}
</button>
</div>
{/* Replies */}
{replies(comment.id).length > 0 && (
<div className="mt-4 ml-8 space-y-2 border-l-2 border-gray-200 pl-4">
{replies(comment.id).map((reply) => (
<div key={reply.id} className="bg-gray-50 p-3 rounded">
<div className="flex items-center justify-between mb-1">
<span className="font-semibold text-sm">{reply.author.slice(0, 10)}...</span>
<span className="text-xs text-gray-500">
{new Date(reply.createdAt).toLocaleString()}
</span>
</div>
<p className="text-sm text-gray-700">{reply.content}</p>
</div>
))}
</div>
)}
</div>
))}
</div>
</div>
)
}