Spaces:
Running
Running
| <html lang="ko"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>DEX AMM Visualizer | Crypto Math</title> | |
| <!-- External Libraries --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg-color: #0f172a; | |
| --card-bg: #1e293b; | |
| --accent-primary: #6366f1; | |
| --accent-secondary: #ec4899; | |
| --text-primary: #f8fafc; | |
| --text-secondary: #94a3b8; | |
| --success: #10b981; | |
| --danger: #ef4444; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: var(--bg-color); | |
| color: var(--text-primary); | |
| overflow-x: hidden; | |
| } | |
| /* Custom Scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: var(--bg-color); | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--card-bg); | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: var(--accent-primary); | |
| } | |
| /* Glassmorphism Cards */ | |
| .glass-card { | |
| background: rgba(30, 41, 59, 0.7); | |
| backdrop-filter: blur(12px); | |
| -webkit-backdrop-filter: blur(12px); | |
| border: 1px solid rgba(255, 255, 255, 0.05); | |
| border-radius: 16px; | |
| box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3); | |
| transition: transform 0.3s ease, box-shadow 0.3s ease; | |
| } | |
| .glass-card:hover { | |
| box-shadow: 0 10px 40px rgba(99, 102, 241, 0.1); | |
| } | |
| /* Inputs */ | |
| .custom-input { | |
| background: rgba(15, 23, 42, 0.6); | |
| border: 1px solid rgba(148, 163, 184, 0.2); | |
| color: var(--text-primary); | |
| transition: all 0.3s ease; | |
| } | |
| .custom-input:focus { | |
| outline: none; | |
| border-color: var(--accent-primary); | |
| box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2); | |
| } | |
| /* Range Slider */ | |
| input[type=range] { | |
| -webkit-appearance: none; | |
| background: transparent; | |
| } | |
| input[type=range]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| height: 20px; | |
| width: 20px; | |
| border-radius: 50%; | |
| background: var(--accent-primary); | |
| cursor: pointer; | |
| margin-top: -8px; | |
| box-shadow: 0 0 10px rgba(99, 102, 241, 0.5); | |
| } | |
| input[type=range]::-webkit-slider-runnable-track { | |
| width: 100%; | |
| height: 4px; | |
| cursor: pointer; | |
| background: var(--card-bg); | |
| border-radius: 2px; | |
| } | |
| /* Animations */ | |
| @keyframes pulse-glow { | |
| 0% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.4); } | |
| 70% { box-shadow: 0 0 0 10px rgba(99, 102, 241, 0); } | |
| 100% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); } | |
| } | |
| .animate-swap { | |
| animation: pulse-glow 1s infinite; | |
| } | |
| .formula-text { | |
| font-family: 'Courier New', monospace; | |
| } | |
| .gradient-text { | |
| background: linear-gradient(135deg, #6366f1 0%, #ec4899 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| } | |
| </style> | |
| </head> | |
| <body class="min-h-screen flex flex-col"> | |
| <!-- Header --> | |
| <header class="w-full py-4 px-6 border-b border-slate-800 flex justify-between items-center bg-slate-900/80 backdrop-blur-md sticky top-0 z-50"> | |
| <div class="flex items-center gap-3"> | |
| <div class="w-10 h-10 rounded-lg bg-gradient-to-br from-indigo-500 to-pink-500 flex items-center justify-center text-white font-bold text-xl"> | |
| <i class="fa-solid fa-shuffle"></i> | |
| </div> | |
| <h1 class="text-xl font-bold tracking-tight">DEX <span class="font-light text-slate-400">Simulator</span></h1> | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="text-sm font-medium text-slate-400 hover:text-indigo-400 transition-colors flex items-center gap-2"> | |
| Built with anycoder <i class="fa-solid fa-arrow-up-right-from-square text-xs"></i> | |
| </a> | |
| </header> | |
| <!-- Main Content --> | |
| <main class="flex-grow container mx-auto px-4 py-8 grid grid-cols-1 lg:grid-cols-12 gap-8"> | |
| <!-- Left Column: Controls & Swap --> | |
| <div class="lg:col-span-4 flex flex-col gap-6"> | |
| <!-- Pool Configuration --> | |
| <section class="glass-card p-6 relative overflow-hidden group"> | |
| <div class="absolute top-0 right-0 w-32 h-32 bg-indigo-500/10 rounded-full blur-3xl -mr-16 -mt-16 pointer-events-none"></div> | |
| <h2 class="text-lg font-semibold mb-4 flex items-center gap-2"> | |
| <i class="fa-solid fa-database text-indigo-400"></i> 유동성 풀 설정 (Liquidity Pool) | |
| </h2> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-xs uppercase font-bold text-slate-500 mb-1 tracking-wider">Token A (Reserve X)</label> | |
| <div class="flex items-center gap-2"> | |
| <span class="text-2xl">🍎</span> | |
| <input type="number" id="reserveA" value="1000" class="custom-input w-full p-3 rounded-lg font-mono text-lg" oninput="updateSimulation()"> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-xs uppercase font-bold text-slate-500 mb-1 tracking-wider">Token B (Reserve Y)</label> | |
| <div class="flex items-center gap-2"> | |
| <span class="text-2xl">🍌</span> | |
| <input type="number" id="reserveB" value="4000" class="custom-input w-full p-3 rounded-lg font-mono text-lg" oninput="updateSimulation()"> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mt-6 p-4 bg-slate-900/50 rounded-lg border border-slate-700/50"> | |
| <div class="flex justify-between text-sm mb-1"> | |
| <span class="text-slate-400">Constant Product (k):</span> | |
| <span id="constantK" class="font-mono text-indigo-300">4,000,000</span> | |
| </div> | |
| <div class="flex justify-between text-sm"> | |
| <span class="text-slate-400">Current Price (A/B):</span> | |
| <span id="spotPrice" class="font-mono text-emerald-400">1 A = 4.00 B</span> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Swap Interface --> | |
| <section class="glass-card p-6 border-t-4 border-t-pink-500"> | |
| <h2 class="text-lg font-semibold mb-4 flex items-center gap-2"> | |
| <i class="fa-solid fa-right-left text-pink-500"></i> 토큰 교환 (Swap) | |
| </h2> | |
| <div class="relative"> | |
| <div class="bg-slate-800/50 p-4 rounded-xl border border-slate-700 transition-colors focus-within:border-pink-500/50"> | |
| <div class="flex justify-between mb-2"> | |
| <label class="text-xs text-slate-400">지불할 금액 (You Pay)</label> | |
| <span class="text-xs text-slate-500">Token A</span> | |
| </div> | |
| <div class="flex items-center gap-3"> | |
| <input type="number" id="swapInput" value="0" class="bg-transparent text-2xl font-bold w-full outline-none text-white placeholder-slate-600" placeholder="0.0"> | |
| <div class="bg-slate-700 px-2 py-1 rounded text-sm font-bold flex items-center gap-1"> | |
| 🍎 A | |
| </div> | |
| </div> | |
| </div> | |
| <div class="absolute left-1/2 -translate-x-1/2 -translate-y-1/2 z-10"> | |
| <div class="bg-slate-800 border border-slate-700 p-2 rounded-full shadow-xl text-slate-400"> | |
| <i class="fa-solid fa-arrow-down"></i> | |
| </div> | |
| </div> | |
| <div class="bg-slate-800/50 p-4 rounded-xl border border-slate-700 mt-2"> | |
| <div class="flex justify-between mb-2"> | |
| <label class="text-xs text-slate-400">받을 금액 (You Receive)</label> | |
| <span class="text-xs text-slate-500">Token B</span> | |
| </div> | |
| <div class="flex items-center gap-3"> | |
| <input type="text" id="swapOutput" readonly class="bg-transparent text-2xl font-bold w-full outline-none text-emerald-400" value="0.00"> | |
| <div class="bg-slate-700 px-2 py-1 rounded text-sm font-bold flex items-center gap-1"> | |
| 🍌 B | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mt-4 space-y-2"> | |
| <label class="text-xs text-slate-400">빠른 입력 (Slider)</label> | |
| <input type="range" id="swapSlider" min="0" max="500" value="0" class="w-full" oninput="syncSlider()"> | |
| </div> | |
| </section> | |
| </div> | |
| <!-- Right Column: Visualization & Stats --> | |
| <div class="lg:col-span-8 flex flex-col gap-6"> | |
| <!-- Chart Section --> | |
| <section class="glass-card p-6 h-[400px] flex flex-col relative"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-lg font-semibold flex items-center gap-2"> | |
| <i class="fa-solid fa-chart-line text-emerald-400"></i> AMM Curve (x * y = k) | |
| </h2> | |
| <div class="text-xs bg-slate-800 px-3 py-1 rounded-full text-slate-400 border border-slate-700"> | |
| Interactive Visualization | |
| </div> | |
| </div> | |
| <div class="flex-grow relative w-full h-full"> | |
| <canvas id="ammChart"></canvas> | |
| </div> | |
| </section> | |
| <!-- Formula & Impact Details --> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <!-- Mathematical Breakdown --> | |
| <section class="glass-card p-6"> | |
| <h3 class="text-sm font-bold text-slate-400 uppercase mb-4 tracking-wider">수학적 공식 (The Math)</h3> | |
| <div class="space-y-4 text-sm"> | |
| <div class="flex items-center gap-3 p-3 bg-slate-900/50 rounded-lg"> | |
| <div class="w-8 h-8 rounded-full bg-indigo-500/20 flex items-center justify-center text-indigo-400 font-serif font-bold">1</div> | |
| <div> | |
| <div class="text-slate-400 text-xs">불변 상수 (Constant Product)</div> | |
| <div class="font-mono text-white"><span id="mathX">1000</span> × <span id="mathY">4000</span> = <span id="mathK">4M</span></div> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-3 p-3 bg-slate-900/50 rounded-lg"> | |
| <div class="w-8 h-8 rounded-full bg-pink-500/20 flex items-center justify-center text-pink-400 font-serif font-bold">2</div> | |
| <div> | |
| <div class="text-slate-400 text-xs">새로운 Reserve X (New X)</div> | |
| <div class="font-mono text-white"><span id="mathOldX">1000</span> + <span id="mathDeltaX" class="text-indigo-400">0</span> = <span id="mathNewX">1000</span></div> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-3 p-3 bg-slate-900/50 rounded-lg"> | |
| <div class="w-8 h-8 rounded-full bg-emerald-500/20 flex items-center justify-center text-emerald-400 font-serif font-bold">3</div> | |
| <div> | |
| <div class="text-slate-400 text-xs">받게 될 Token B (Output)</div> | |
| <div class="font-mono text-white">dy = Y - (k / NewX)</div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Trade Statistics --> | |
| <section class="glass-card p-6"> | |
| <h3 class="text-sm font-bold text-slate-400 uppercase mb-4 tracking-wider">거래 분석 (Analysis)</h3> | |
| <div class="space-y-4"> | |
| <div class="flex justify-between items-center border-b border-slate-700 pb-3"> | |
| <span class="text-slate-400 text-sm">실행 가격 (Execution Price)</span> | |
| <div class="text-right"> | |
| <div id="execPrice" class="font-bold text-white">0.00</div> | |
| <div class="text-xs text-slate-500">B per A</div> | |
| </div> | |
| </div> | |
| <div class="flex justify-between items-center border-b border-slate-700 pb-3"> | |
| <span class="text-slate-400 text-sm flex items-center gap-1"> | |
| 가격 영향 (Price Impact) | |
| <i class="fa-solid fa-circle-info text-[10px] text-slate-600"></i> | |
| </span> | |
| <div class="text-right"> | |
| <div id="priceImpact" class="font-bold text-emerald-400">0.00%</div> | |
| </div> | |
| </div> | |
| <div class="flex justify-between items-center"> | |
| <span class="text-slate-400 text-sm">풀 상태 변화 (Pool Share)</span> | |
| <div class="w-24 h-2 bg-slate-700 rounded-full overflow-hidden"> | |
| <div id="poolBar" class="h-full bg-indigo-500 transition-all duration-300" style="width: 50%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="warningMsg" class="hidden mt-4 p-3 bg-red-500/10 border border-red-500/20 rounded text-red-400 text-xs flex items-start gap-2"> | |
| <i class="fa-solid fa-triangle-exclamation mt-0.5"></i> | |
| <span>유동성에 비해 주문 금액이 너무 큽니다. 슬리피지가 매우 높습니다.</span> | |
| </div> | |
| </section> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- JavaScript Logic --> | |
| <script> | |
| // Global Variables | |
| let chartInstance = null; | |
| // DOM Elements | |
| const els = { | |
| resA: document.getElementById('reserveA'), | |
| resB: document.getElementById('reserveB'), | |
| inp: document.getElementById('swapInput'), | |
| out: document.getElementById('swapOutput'), | |
| slider: document.getElementById('swapSlider'), | |
| constK: document.getElementById('constantK'), | |
| spotPrice: document.getElementById('spotPrice'), | |
| mathX: document.getElementById('mathX'), | |
| mathY: document.getElementById('mathY'), | |
| mathK: document.getElementById('mathK'), | |
| mathOldX: document.getElementById('mathOldX'), | |
| mathDeltaX: document.getElementById('mathDeltaX'), | |
| mathNewX: document.getElementById('mathNewX'), | |
| execPrice: document.getElementById('execPrice'), | |
| priceImpact: document.getElementById('priceImpact'), | |
| warning: document.getElementById('warningMsg'), | |
| poolBar: document.getElementById('poolBar') | |
| }; | |
| // Initialization | |
| window.onload = () => { | |
| initChart(); | |
| updateSimulation(); | |
| // Event Listeners | |
| els.inp.addEventListener('input', () => { | |
| els.slider.value = els.inp.value; | |
| updateSimulation(); | |
| }); | |
| }; | |
| function syncSlider() { | |
| els.inp.value = els.slider.value; | |
| updateSimulation(); | |
| } | |
| function formatNum(num, decimals = 2) { | |
| return parseFloat(num).toLocaleString('en-US', { | |
| minimumFractionDigits: decimals, | |
| maximumFractionDigits: decimals | |
| }); | |
| } | |
| function initChart() { | |
| const ctx = document.getElementById('ammChart').getContext('2d'); | |
| // Chart.js Configuration | |
| chartInstance = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| datasets: [ | |
| { | |
| label: 'Bonding Curve (k=const)', | |
| data: [], | |
| borderColor: '#6366f1', | |
| borderWidth: 2, | |
| pointRadius: 0, | |
| fill: false, | |
| tension: 0.4 | |
| }, | |
| { | |
| label: 'Current State', | |
| data: [], | |
| backgroundColor: '#10b981', | |
| borderColor: '#fff', | |
| borderWidth: 2, | |
| pointRadius: 6, | |
| pointHoverRadius: 8 | |
| }, | |
| { | |
| label: 'New State', | |
| data: [], | |
| backgroundColor: '#ec4899', | |
| borderColor: '#fff', | |
| borderWidth: 2, | |
| pointRadius: 6, | |
| pointHoverRadius: 8 | |
| } | |
| ] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| interaction: { | |
| intersect: false, | |
| mode: 'index', | |
| }, | |
| plugins: { | |
| legend: { | |
| labels: { color: '#94a3b8' } | |
| }, | |
| tooltip: { | |
| backgroundColor: 'rgba(15, 23, 42, 0.9)', | |
| titleColor: '#fff', | |
| bodyColor: '#cbd5e1', | |
| borderColor: 'rgba(255,255,255,0.1)', | |
| borderWidth: 1 | |
| } | |
| }, | |
| scales: { | |
| x: { | |
| type: 'linear', | |
| title: { display: true, text: 'Reserve A (🍎)', color: '#64748b' }, | |
| grid: { color: 'rgba(255, 255, 255, 0.05)' }, | |
| ticks: { color: '#64748b' } | |
| }, | |
| y: { | |
| title: { display: true, text: 'Reserve B (🍌)', color: '#64748b' }, | |
| grid: { color: 'rgba(255, 255, 255, 0.05)' }, | |
| ticks: { color: '#64748b' } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| function updateSimulation() { | |
| // 1. Get Values | |
| const x = parseFloat(els.resA.value) || 1; | |
| const y = parseFloat(els.resB.value) || 1; | |
| const dx = parseFloat(els.inp.value) || 0; | |
| // 2. Calculate Constant Product | |
| const k = x * y; | |
| // 3. Calculate Swap | |
| // Formula: (x + dx)(y - dy) = k | |
| // y - dy = k / (x + dx) | |
| // dy = y - k / (x + dx) | |
| const newX = x + dx; | |
| const newY = k / newX; | |
| const dy = y - newY; | |
| // 4. Update UI Texts | |
| els.constK.innerText = formatNum(k, 0); | |
| els.out.value = dx > 0 ? formatNum(dy, 4) : "0.00"; | |
| // Spot Price (Current Market Price) = y / x | |
| const spotPrice = y / x; | |
| els.spotPrice.innerText = `1 A = ${formatNum(spotPrice, 4)} B`; | |
| // Execution Price = dy / dx | |
| const execPrice = dx > 0 ? dy / dx : 0; | |
| els.execPrice.innerText = dx > 0 ? formatNum(execPrice, 4) : "0.00"; | |
| // Price Impact = (Spot Price - Execution Price) / Spot Price * 100 | |
| let impact = 0; | |
| if (dx > 0) { | |
| impact = ((spotPrice - execPrice) / spotPrice) * 100; | |
| } | |
| // Color code impact | |
| const impactEl = els.priceImpact; | |
| impactEl.innerText = `${formatNum(impact, 2)}%`; | |
| if(impact < 1) impactEl.className = "font-bold text-emerald-400"; | |
| else if(impact < 5) impactEl.className = "font-bold text-yellow-400"; | |
| else impactEl.className = "font-bold text-red-500"; | |
| // Warning | |
| if(impact > 15) els.warning.classList.remove('hidden'); | |
| else els.warning.classList.add('hidden'); | |
| // Math Breakdown Update | |
| els.mathX.innerText = formatNum(x, 0); | |
| els.mathY.innerText = formatNum(y, 0); | |
| els.mathK.innerText = formatNum(k, 0); | |
| els.mathOldX.innerText = formatNum(x, 0); | |
| els.mathDeltaX.innerText = formatNum(dx, 2); | |
| els.mathNewX.innerText = formatNum(newX, 2); | |
| // Pool Bar Visual | |
| const totalVal = newX + newY; // Simplistic visualization | |
| const percent = (newX / (newX + newY)) * 100; // Just showing ratio shift | |
| // Actually, let's show price impact severity on bar | |
| els.poolBar.style.width = `${Math.min(impact * 5, 100)}%`; | |
| els.poolBar.className = `h-full transition-all duration-300 ${impact > 5 ? 'bg-red-500' : 'bg-indigo-500'}`; | |
| // 5. Update Chart | |
| updateChartData(x, y, newX, newY, k); | |
| } | |
| function updateChartData(x, y, newX, newY, k) { | |
| const dataPoints = []; | |
| // Determine range for chart (zoom out slightly based on trade) | |
| const startX = x * 0.5; | |
| const endX = Math.max(x * 1.5, newX * 1.2); | |
| const steps = 50; | |
| for (let i = 0; i <= steps; i++) { | |
| const curX = startX + (endX - startX) * (i / steps); | |
| const curY = k / curX; | |
| dataPoints.push({ x: curX, y: curY }); | |
| } | |
| chartInstance.data.datasets[0].data = dataPoints; | |
| // Current Point (Start) | |
| chartInstance.data.datasets[1].data = [{ x: x, y: y }]; | |
| // New Point (End) - Only if swapping | |
| if (Math.abs(newX - x) > 0.001) { | |
| chartInstance.data.datasets[2].data = [{ x: newX, y: newY }]; | |
| } else { | |
| chartInstance.data.datasets[2].data = []; | |
| } | |
| chartInstance.update('none'); // 'none' for performance | |
| } | |
| </script> | |
| </body> | |
| </html> |