Created
February 1, 2025 21:05
-
-
Save Y0lan/bb714a71b9db090febf73d686fbbd385 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| === Directory Structure === | |
| . | |
| ├── ./lib | |
| │ ├── ./lib/components | |
| │ │ ├── ./lib/components/LotteryModal.svelte | |
| │ │ ├── ./lib/components/LotteryTicket.svelte | |
| │ │ ├── ./lib/components/LotteryTicketView.svelte | |
| │ │ └── ./lib/components/SlotMachine.svelte | |
| │ ├── ./lib/stores | |
| │ │ └── ./lib/stores/auth.js | |
| │ ├── ./lib/utils | |
| │ │ ├── ./lib/utils/auth.js | |
| │ │ ├── ./lib/utils/fetchWithAuth.js | |
| │ │ └── ./lib/utils/jwt.js | |
| │ ├── ./lib/cache.js | |
| │ ├── ./lib/db.js | |
| │ ├── ./lib/index.js | |
| │ ├── ./lib/lottery-data.js | |
| │ ├── ./lib/prices.js | |
| │ └── ./lib/utils.js | |
| ├── ./routes | |
| │ ├── ./routes/admin | |
| │ │ ├── ./routes/admin/login | |
| │ │ │ └── ./routes/admin/login/+server.js | |
| │ │ ├── ./routes/admin/lottery | |
| │ │ │ ├── ./routes/admin/lottery/draws | |
| │ │ │ │ ├── ./routes/admin/lottery/draws/[drawId] | |
| │ │ │ │ │ └── ./routes/admin/lottery/draws/[drawId]/conduct | |
| │ │ │ │ │ └── ./routes/admin/lottery/draws/[drawId]/conduct/+server.js | |
| │ │ │ │ └── ./routes/admin/lottery/draws/+server.js | |
| │ │ │ ├── ./routes/admin/lottery/generate-tickets | |
| │ │ │ │ └── ./routes/admin/lottery/generate-tickets/+server.js | |
| │ │ │ └── ./routes/admin/lottery/+page.svelte | |
| │ │ └── ./routes/admin/verify | |
| │ │ └── ./routes/admin/verify/+server.js | |
| │ ├── ./routes/api | |
| │ │ ├── ./routes/api/lottery | |
| │ │ │ ├── ./routes/api/lottery/stats | |
| │ │ │ │ └── ./routes/api/lottery/stats/+server.js | |
| │ │ │ ├── ./routes/api/lottery/tickets | |
| │ │ │ │ └── ./routes/api/lottery/tickets/+server.js | |
| │ │ │ └── ./routes/api/lottery/[userId] | |
| │ │ │ └── ./routes/api/lottery/[userId]/+server.js | |
| │ │ ├── ./routes/api/rankings | |
| │ │ │ └── ./routes/api/rankings/+server.js | |
| │ │ ├── ./routes/api/search | |
| │ │ │ └── ./routes/api/search/[term] | |
| │ │ │ └── ./routes/api/search/[term]/+server.js | |
| │ │ └── ./routes/api/target | |
| │ │ └── ./routes/api/target/[userId] | |
| │ │ └── ./routes/api/target/[userId]/+server.js | |
| │ ├── ./routes/+layout.svelte | |
| │ ├── ./routes/+page.svelte | |
| │ └── ./routes/+page.svelte.bak | |
| ├── ./all_databases_dump.sql | |
| ├── ./app.css | |
| ├── ./app.html | |
| ├── ./codebase | |
| ├── ./holdings.sql | |
| ├── ./<q | |
| ├── ./schema_holdings.sql | |
| ├── ./schema_xshot_core_prod.sql | |
| ├── ./src_contents_and_structure.txt | |
| └── ./s.sh | |
| 24 directories, 39 files | |
| === File Contents === | |
| === ./all_databases_dump.sql === | |
| === ./app.css === | |
| @tailwind base; | |
| @tailwind components; | |
| @tailwind utilities; | |
| :root { | |
| font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; | |
| line-height: 1.5; | |
| font-weight: 400; | |
| } | |
| === ./app.html === | |
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <link rel="icon" href="%sveltekit.assets%/favicon.png" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| %sveltekit.head% | |
| </head> | |
| <body data-sveltekit-preload-data="hover"> | |
| <div style="display: contents">%sveltekit.body%</div> | |
| </body> | |
| </html> | |
| === ./codebase === | |
| . | |
| ├── all_databases_dump.sql | |
| ├── app.css | |
| ├── app.html | |
| ├── cb.txt | |
| ├── codebase | |
| ├── holdings.sql | |
| ├── lib | |
| │ ├── cache.js | |
| │ ├── components | |
| │ │ ├── LotteryModal.svelte | |
| │ │ ├── LotteryTicket.svelte | |
| │ │ └── SlotMachine.svelte | |
| │ ├── db.js | |
| │ ├── index.js | |
| │ ├── lottery-data.js | |
| │ └── prices.js | |
| ├── <q | |
| ├── routes | |
| │ ├── api | |
| │ │ ├── lottery | |
| │ │ │ ├── stats | |
| │ │ │ │ └── +server.js | |
| │ │ │ ├── tickets | |
| │ │ │ │ └── +server.js | |
| │ │ │ └── [userId] | |
| │ │ │ └── +server.js | |
| │ │ ├── rankings | |
| │ │ │ └── +server.js | |
| │ │ ├── search | |
| │ │ │ └── [term] | |
| │ │ │ └── +server.js | |
| │ │ └── target | |
| │ │ └── [userId] | |
| │ │ └── +server.js | |
| │ ├── +layout.svelte | |
| │ ├── +page.svelte | |
| │ └── +page.svelte.bak | |
| └── s.sh | |
| 14 directories, 25 files | |
| ========== ./all_databases_dump.sql ========== | |
| ========== ./app.css ========== | |
| @tailwind base; | |
| @tailwind components; | |
| @tailwind utilities; | |
| :root { | |
| font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; | |
| line-height: 1.5; | |
| font-weight: 400; | |
| } | |
| ========== ./app.html ========== | |
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <link rel="icon" href="%sveltekit.assets%/favicon.png" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| %sveltekit.head% | |
| </head> | |
| <body data-sveltekit-preload-data="hover"> | |
| <div style="display: contents">%sveltekit.body%</div> | |
| </body> | |
| </html> | |
| ========== ./cb.txt ========== | |
| . | |
| ├── app.css | |
| ├── app.html | |
| ├── backup_20250108_170844 | |
| │ └── +page.svelte | |
| ├── codebase | |
| ├── lib | |
| │ ├── cache.js | |
| │ ├── components | |
| │ │ ├── LotteryModal.svelte | |
| │ │ ├── LotteryTicket.svelte | |
| │ │ └── SlotMachine.svelte | |
| │ ├── db.js | |
| │ ├── index.js | |
| │ ├── lottery-data.js | |
| │ └── prices.js | |
| ├── routes | |
| │ ├── api | |
| │ │ ├── lottery | |
| │ │ │ ├── stats | |
| │ │ │ │ └── +server.js | |
| │ │ │ ├── tickets | |
| │ │ │ │ └── +server.js | |
| │ │ │ └── [userId] | |
| │ │ │ └── +server.js | |
| │ │ ├── rankings | |
| │ │ │ └── +server.js | |
| │ │ ├── search | |
| │ │ │ └── [term] | |
| │ │ │ └── +server.js | |
| │ │ └── target | |
| │ │ └── [userId] | |
| │ │ └── +server.js | |
| │ ├── +layout.svelte | |
| │ ├── +page.svelte | |
| │ └── +page.svelte.bak | |
| └── s.sh | |
| 15 directories, 22 files | |
| ========== ./app.css ========== | |
| @tailwind base; | |
| @tailwind components; | |
| @tailwind utilities; | |
| :root { | |
| font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; | |
| line-height: 1.5; | |
| font-weight: 400; | |
| } | |
| ========== ./app.html ========== | |
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <link rel="icon" href="%sveltekit.assets%/favicon.png" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| %sveltekit.head% | |
| </head> | |
| <body data-sveltekit-preload-data="hover"> | |
| <div style="display: contents">%sveltekit.body%</div> | |
| </body> | |
| </html> | |
| ========== ./backup_20250108_170844/+page.svelte ========== | |
| <script> | |
| import { onMount } from 'svelte'; | |
| import { getAllWalletBalances } from '$lib/prices'; | |
| let rankings = []; | |
| let searchTerm = ''; | |
| let targetVolume = null; | |
| let loading = false; | |
| let error = ''; | |
| let darkMode = true; | |
| let walletBalances = []; | |
| let totalSolUsd = 0; | |
| let totalEthUsd = 0; | |
| let searchInputRef; | |
| let showTable = false; // Controls the visibility of the Rankings Table | |
| const DISTRIBUTION = { | |
| first: 0.5, | |
| second: 0.3, | |
| third: 0.2 | |
| }; | |
| onMount(async () => { | |
| loadRankings(); | |
| loadPrizes(); | |
| if (window.matchMedia('(prefers-color-scheme: light)').matches) { | |
| darkMode = false; | |
| } | |
| }); | |
| async function loadRankings() { | |
| try { | |
| const response = await fetch('/api/rankings'); | |
| if (!response.ok) throw new Error('Failed to fetch rankings'); | |
| rankings = await response.json(); | |
| } catch (err) { | |
| error = 'Failed to load rankings'; | |
| } | |
| } | |
| async function loadPrizes() { | |
| try { | |
| walletBalances = await getAllWalletBalances(); | |
| // 50% of total for each chain | |
| totalSolUsd = walletBalances.reduce((sum, w) => sum + w.solana.usdValue, 0) * 0.5; | |
| totalEthUsd = walletBalances.reduce((sum, w) => sum + w.ethereum.usdValue, 0) * 0.5; | |
| } catch (err) { | |
| console.error('Error loading prizes:', err); | |
| } | |
| } | |
| async function searchUser() { | |
| if (!searchTerm) return; | |
| loading = true; | |
| error = ''; | |
| try { | |
| const response = await fetch(`/api/search/${encodeURIComponent(searchTerm)}`); | |
| if (!response.ok) throw new Error('Failed to fetch user data'); | |
| targetVolume = await response.json(); | |
| } catch (err) { | |
| error = 'User not found'; | |
| targetVolume = null; | |
| } finally { | |
| loading = false; | |
| } | |
| } | |
| function formatUsd(num) { | |
| return new Intl.NumberFormat('en-US', { | |
| style: 'currency', | |
| currency: 'USD', | |
| notation: 'compact', | |
| maximumFractionDigits: 2 | |
| }).format(num); | |
| } | |
| function toggleDarkMode() { | |
| darkMode = !darkMode; | |
| } | |
| </script> | |
| <!-- Outer Container with smooth transitions --> | |
| <div class={`min-h-screen transition-all duration-300 ${darkMode ? 'bg-gray-900 text-gray-100' : 'bg-gray-50 text-gray-900'}`}> | |
| <div class="container mx-auto px-3 sm:px-4 py-4 sm:py-6 lg:py-8 max-w-5xl relative"> | |
| <!-- Dark Mode Toggle with improved positioning --> | |
| <div class="absolute top-2 right-2 sm:top-4 sm:right-4 z-10"> | |
| <button | |
| on:click={toggleDarkMode} | |
| class="p-2 rounded-full hover:bg-opacity-20 hover:bg-gray-500 transition-all duration-300 transform hover:scale-110" | |
| > | |
| {#if darkMode} | |
| <span class="text-xl sm:text-2xl">🌞</span> | |
| {:else} | |
| <span class="text-xl sm:text-2xl">🌙</span> | |
| {/if} | |
| </button> | |
| </div> | |
| <div class={`mb-4 sm:mb-6 p-4 sm:p-6 rounded-xl sm:rounded-2xl shadow-xl | |
| backdrop-filter backdrop-blur-sm transition-all duration-300 | |
| ${darkMode ? 'bg-gray-800/90 shadow-gray-900/50' : 'bg-white/90 shadow-gray-200/50'}`}> | |
| <div class="text-center space-y-4 md:space-y-6"> | |
| <!-- Improved Title Section --> | |
| <div class="relative inline-block"> | |
| <h2 class="text-xl sm:text-2xl lg:text-4xl font-bold flex items-center justify-center gap-3 p-2"> | |
| <span class="trophy-icon text-2xl sm:text-3xl lg:text-4xl animate-gentle-pulse">🏆</span> | |
| <span class="gradient-text tracking-tight">January Trading Competition</span> | |
| <span class="trophy-icon text-2xl sm:text-3xl lg:text-4xl animate-gentle-pulse">🏆</span> | |
| </h2> | |
| </div> | |
| <!-- Enhanced Prize Pool Amount --> | |
| <div class="prize-pool transform hover:scale-105 transition-all duration-300"> | |
| <div class="text-3xl sm:text-4xl lg:text-6xl xl:text-7xl font-extrabold bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 bg-clip-text text-transparent tracking-tight"> | |
| {formatUsd(totalSolUsd + totalEthUsd)} | |
| </div> | |
| <div class="text-base sm:text-lg lg:text-xl font-medium text-indigo-400 mt-2"> | |
| Total Prize Pool to Win | |
| </div> | |
| </div> | |
| <!-- Redesigned Distribution Cards --> | |
| <div class="max-w-2xl mx-auto p-3 rounded-xl bg-gray-700/20 backdrop-filter backdrop-blur-sm transition-all duration-300"> | |
| <div class="grid grid-cols-1 sm:grid-cols-3 gap-3"> | |
| {#each [ | |
| { place: "1st Place", emoji: "🥇", amount: DISTRIBUTION.first, gradient: "from-blue-500 to-indigo-500" }, | |
| { place: "2nd Place", emoji: "🥈", amount: DISTRIBUTION.second, gradient: "from-indigo-500 to-purple-500" }, | |
| { place: "3rd Place", emoji: "🥉", amount: DISTRIBUTION.third, gradient: "from-purple-500 to-pink-500" } | |
| ] as { place, emoji, amount, gradient }} | |
| <div class="prize-card group"> | |
| <div class="flex flex-col items-center space-y-2"> | |
| <div class="text-base sm:text-lg font-medium flex items-center gap-2"> | |
| <span class="text-xl sm:text-2xl">{emoji}</span> | |
| {place} | |
| </div> | |
| <div class="font-bold text-lg sm:text-xl lg:text-2xl bg-gradient-to-r {gradient} bg-clip-text text-transparent"> | |
| {formatUsd((totalSolUsd + totalEthUsd) * amount)} | |
| </div> | |
| <div class="text-xs sm:text-sm text-gray-400 group-hover:text-gray-300 transition-colors duration-300"> | |
| {(amount * 100)}% of prize pool | |
| </div> | |
| </div> | |
| </div> | |
| {/each} | |
| </div> | |
| </div> | |
| <!-- Enhanced Competition Explainer --> | |
| <div class="p-4 rounded-xl bg-gray-700/20 backdrop-filter backdrop-blur-sm transition-all duration-300 | |
| max-w-2xl mx-auto hover:bg-gray-700/30"> | |
| <h3 class="text-base sm:text-lg lg:text-xl font-semibold mb-2 gradient-text"> | |
| Why Join the Competition? | |
| </h3> | |
| <p class="text-sm sm:text-base leading-relaxed text-gray-300"> | |
| The 3 biggest volume makers on XSHOT will split | |
| <span class="font-semibold text-indigo-400">50% of the total generated fees</span> | |
| on XSHOT throughout the month, which is | |
| <span class="font-semibold text-indigo-400">0.6% of every trade</span> from most users. | |
| This is how top competitors can earn a substantial reward, and why we see this as | |
| an exciting opportunity to <em>share the bread</em> and make trading fun for everyone. | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Wallet Info Cards --> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6 mb-6"> | |
| <!-- Solana Card --> | |
| <div class={`p-4 sm:p-6 rounded-xl shadow-lg transition-all duration-300 transform hover:scale-102 | |
| ${darkMode ? 'bg-gray-800/90' : 'bg-white/90'}`}> | |
| <div class="text-lg sm:text-xl font-medium mb-2"> | |
| Solana Wallet | |
| </div> | |
| <div class="text-2xl sm:text-3xl font-extrabold mb-4 text-indigo-500"> | |
| {formatUsd(totalSolUsd)} | |
| </div> | |
| <div class="text-sm"> | |
| <a | |
| href={`https://solscan.io/account/${walletBalances[0]?.solana.wallet}`} | |
| target="_blank" | |
| class="text-indigo-600 hover:text-indigo-500 hover:underline dark:text-indigo-400 transition-colors" | |
| > | |
| Verify Prize Pool on Solscan ↗ | |
| </a> | |
| </div> | |
| </div> | |
| <!-- Ethereum Card --> | |
| <div class={`p-4 sm:p-6 rounded-xl shadow-lg transition-all duration-300 transform hover:scale-102 | |
| ${darkMode ? 'bg-gray-800/90' : 'bg-white/90'}`}> | |
| <div class="text-lg sm:text-xl font-medium mb-2"> | |
| Ethereum Wallet | |
| </div> | |
| <div class="text-2xl sm:text-3xl font-extrabold mb-4 text-indigo-500"> | |
| {formatUsd(totalEthUsd)} | |
| </div> | |
| <div class="text-sm"> | |
| <a | |
| href={`https://etherscan.io/address/${walletBalances[0]?.ethereum.wallet}`} | |
| target="_blank" | |
| class="text-indigo-600 hover:text-indigo-500 hover:underline dark:text-indigo-400 transition-colors" | |
| > | |
| Verify Prize Pool on Etherscan ↗ | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Search Section --> | |
| <div class={`mb-6 p-4 sm:p-6 rounded-xl shadow-lg backdrop-filter backdrop-blur-sm | |
| ${darkMode ? 'bg-gray-800/90' : 'bg-white/90'}`}> | |
| <div class="max-w-xl mx-auto space-y-4"> | |
| <div class="relative"> | |
| <input | |
| bind:this={searchInputRef} | |
| type="text" | |
| bind:value={searchTerm} | |
| on:keydown={(e) => e.key === 'Enter' && searchUser()} | |
| class={`w-full px-4 py-3 rounded-lg border text-base | |
| focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-all duration-300 | |
| ${darkMode | |
| ? 'bg-gray-700/70 border-gray-600 text-white placeholder-gray-300' | |
| : 'bg-white border-gray-300 text-gray-900 placeholder-gray-500' | |
| }`} | |
| placeholder="Enter your User ID" | |
| /> | |
| <button | |
| on:click={searchUser} | |
| disabled={loading} | |
| class="absolute right-2 top-2 bg-indigo-600 text-white px-4 py-1 rounded-md | |
| hover:bg-indigo-700 disabled:opacity-50 transition-all duration-300" | |
| > | |
| {loading ? 'Searching...' : 'Search'} | |
| </button> | |
| </div> | |
| <div class={`flex items-center justify-center gap-4 p-3 rounded-lg bg-opacity-50 | |
| ${darkMode ? 'bg-gray-700/50' : 'bg-gray-100/50'}`}> | |
| <div class="text-sm font-medium">Find your User ID:</div> | |
| <div class="font-mono text-sm bg-indigo-500/20 px-3 py-1 rounded"> | |
| Type /id in XSHOT | |
| </div> | |
| </div> | |
| {#if error} | |
| <div class="text-red-500 text-center text-sm animate-fade-in">{error}</div> | |
| {/if} | |
| {#if targetVolume} | |
| <div class={`mt-4 p-4 rounded-lg animate-fade-in | |
| ${darkMode ? 'bg-gray-700/50' : 'bg-gray-100/50'}`}> | |
| <h3 class="text-lg font-semibold mb-3 text-center gradient-text">Your Trading Stats</h3> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> | |
| <div class="p-4 rounded-xl bg-green-500/10 backdrop-filter backdrop-blur-sm"> | |
| <div class="text-sm opacity-80">Your Current Volume</div> | |
| <div class="text-xl font-bold text-green-400"> | |
| {formatUsd(targetVolume.currentVolumeUsd)} | |
| </div> | |
| </div> | |
| <div class="p-4 rounded-xl bg-purple-500/10 backdrop-filter backdrop-blur-sm"> | |
| <div class="text-sm opacity-80">Volume Needed for Top 3</div> | |
| <div class="text-xl font-bold text-purple-400"> | |
| {formatUsd(targetVolume.volumeNeededForTop3Usd)} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/if} | |
| </div> | |
| </div> | |
| <!-- CTA Button --> | |
| <div class="text-center mb-6"> | |
| <a | |
| href="https://t.me/xshot_trading_bot" | |
| target="_blank" | |
| class="inline-block px-6 sm:px-8 py-3 sm:py-4 bg-indigo-600 text-white font-bold rounded-xl | |
| shadow-lg transform hover:scale-105 hover:bg-indigo-500 transition-all duration-300" | |
| > | |
| 🚀 Start Trading with XSHOT Now | |
| </a> | |
| </div> | |
| <!-- Rankings Toggle --> | |
| <div class="text-center mb-4"> | |
| <button | |
| on:click={() => (showTable = !showTable)} | |
| class="flex mx-auto items-center justify-center gap-2 text-indigo-600 dark:text-indigo-400 | |
| font-bold text-lg sm:text-xl transform hover:scale-110 transition-all duration-300" | |
| > | |
| {#if showTable} | |
| <span>Hide Rankings</span> | |
| <span class="text-2xl sm:text-3xl rotate-180 inline-block transition-transform duration-300">⬇</span> | |
| {:else} | |
| <span>View Rankings</span> | |
| <span class="text-2xl sm:text-3xl inline-block transition-transform duration-300">⬇</span> | |
| {/if} | |
| </button> | |
| </div> | |
| <!-- Rankings Table --> | |
| {#if showTable} | |
| <div class={`rounded-xl shadow-lg overflow-hidden mt-4 mx-auto backdrop-filter backdrop-blur-sm | |
| ${darkMode ? 'bg-gray-800/90' : 'bg-white/90'}`}> | |
| <div class="overflow-x-auto"> | |
| <table class="min-w-full"> | |
| <thead class={darkMode ? 'bg-gray-700/50 text-gray-200' : 'bg-gray-100/50 text-gray-800'}> | |
| <tr> | |
| <th class="px-4 sm:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Rank</th> | |
| <th class="px-4 sm:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">User ID</th> | |
| <th class="px-4 sm:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Volume (USD)</th> | |
| </tr> | |
| </thead> | |
| <tbody class={`divide-y ${darkMode ? 'divide-gray-700/50' : 'divide-gray-200/50'}`}> | |
| {#each rankings as { rank, userId, volumeUsd, isTop3 }} | |
| <tr class={` | |
| transition-colors duration-200 | |
| ${darkMode ? 'hover:bg-gray-700/50' : 'hover:bg-gray-50/50'} | |
| ${isTop3 ? (darkMode ? 'bg-gray-700/30' : 'bg-gray-50/30') : ''} | |
| `}> | |
| <td class="px-4 sm:px-6 py-3 whitespace-nowrap"> | |
| {#if rank <= 3} | |
| <span class="text-xl">{rank === 1 ? '🥇' : rank === 2 ? '🥈' : '🥉'}</span> | |
| {:else} | |
| {rank} | |
| {/if} | |
| </td> | |
| <td class="px-4 sm:px-6 py-3 whitespace-nowrap font-medium"> | |
| {userId} | |
| </td> | |
| <td class="px-4 sm:px-6 py-3 whitespace-nowrap"> | |
| {formatUsd(volumeUsd)} | |
| </td> | |
| </tr> | |
| {/each} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| {/if} | |
| </div> | |
| <!-- Footer --> | |
| <footer class="mt-8 py-4 text-center text-sm text-gray-500 dark:text-gray-400 border-t | |
| border-gray-200/20 dark:border-gray-700/20 backdrop-filter backdrop-blur-sm"> | |
| <div class="container mx-auto px-4"> | |
| <p class="mb-2 text-xs sm:text-sm">Explore more on:</p> | |
| <div class="flex items-center justify-center gap-4 text-sm sm:text-base"> | |
| <a | |
| href="https://www.xprojecterc.com/" | |
| target="_blank" | |
| class="text-indigo-500 hover:text-indigo-400 hover:underline dark:text-indigo-400 | |
| transition-colors duration-300" | |
| > | |
| XProject Main Website | |
| </a> | |
| <span class="text-gray-400">|</span> | |
| <a | |
| href="https://xshot.xprojecterc.com/" | |
| target="_blank" | |
| class="text-indigo-500 hover:text-indigo-400 hover:underline dark:text-indigo-400 | |
| transition-colors duration-300" | |
| > | |
| XShot Website | |
| </a> | |
| </div> | |
| </div> | |
| </footer> | |
| </div> | |
| <style> | |
| /* Modern font setup */ | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap'); | |
| :global(html) { | |
| scroll-behavior: smooth; | |
| } | |
| :global(html), | |
| :global(body), | |
| :global(button), | |
| :global(input), | |
| :global(select), | |
| :global(textarea) { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | |
| } | |
| /* Elegant scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| height: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #6366f1; | |
| border-radius: 3px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #4f46e5; | |
| } | |
| /* Enhanced custom classes */ | |
| .gradient-text { | |
| @apply bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400 bg-clip-text text-transparent; | |
| } | |
| .prize-card { | |
| @apply p-4 rounded-xl bg-gray-800/50 backdrop-filter backdrop-blur-sm shadow-lg transition-all duration-300; | |
| } | |
| .trophy-icon { | |
| @apply text-yellow-500; | |
| } | |
| /* Custom animations */ | |
| @keyframes gentle-pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.8; } | |
| } | |
| .animate-gentle-pulse { | |
| animation: gentle-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; | |
| } | |
| .animate-fade-in { | |
| animation: fadeIn 0.5s ease-in-out; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(-10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| </style> | |
| ========== ./codebase ========== | |
| ========== ./lib/cache.js ========== | |
| // src/lib/cache.js | |
| import { tradeDb, userDb } from '$lib/db'; | |
| // We'll store the "rankings" data in a global variable here. | |
| let cachedData = null; | |
| // Store timestamp of last fetch | |
| let lastFetchTimestamp = 0; | |
| // Set the cache TTL (time to live) in ms; e.g., 10 minutes | |
| const CACHE_TTL = 10 * 60 * 1000; | |
| /** | |
| * Actually queries the DB for fresh rankings data. | |
| */ | |
| async function fetchFreshData() { | |
| // This is the same logic from your /api/rankings/+server.js | |
| // except we return the result here directly. | |
| // Example: January 1st, 2025 at 00:00 UTC | |
| const firstDayOfMonth = new Date(2025, 0, 1); | |
| firstDayOfMonth.setUTCHours(0, 0, 0, 0); | |
| // Get all trades from tradeDb | |
| const tradeVolumes = await tradeDb` | |
| WITH monthly_trades AS ( | |
| SELECT | |
| t."walletId", | |
| hw.address, | |
| SUM(COALESCE(t.value_total_usd, 0)) as total_volume_usd | |
| FROM "Trade" t | |
| JOIN "Wallet" hw ON t."walletId" = hw.id | |
| WHERE t.executed_at >= ${firstDayOfMonth} | |
| GROUP BY t."walletId", hw.address | |
| ) | |
| SELECT * FROM monthly_trades | |
| ORDER BY total_volume_usd DESC | |
| `; | |
| // Build a map of addresses -> user IDs | |
| const addresses = tradeVolumes.map(tv => tv.address); | |
| const userWallets = await userDb` | |
| SELECT | |
| w.address, | |
| w.user_id, | |
| u.id as user_id | |
| FROM "Wallet" w | |
| JOIN "User" u ON u.id = w.user_id | |
| WHERE w.address = ANY(${addresses}) | |
| `; | |
| const addressMap = new Map( | |
| userWallets.map((w) => [ | |
| w.address.toLowerCase(), | |
| { userId: w.user_id } | |
| ]) | |
| ); | |
| // Accumulate volumes by userId | |
| const userVolumes = new Map(); | |
| tradeVolumes.forEach((trade) => { | |
| const userInfo = addressMap.get(trade.address.toLowerCase()); | |
| if (userInfo) { | |
| const userId = userInfo.userId; | |
| if (!userVolumes.has(userId)) { | |
| userVolumes.set(userId, { | |
| userId, | |
| volumeUsd: 0 | |
| }); | |
| } | |
| const userVol = userVolumes.get(userId); | |
| userVol.volumeUsd += parseFloat(trade.total_volume_usd); | |
| } | |
| }); | |
| // Convert map -> array, sort, label rank, mark isTop3 | |
| const rankings = Array.from(userVolumes.values()) | |
| .sort((a, b) => b.volumeUsd - a.volumeUsd) | |
| .map((user, index) => ({ | |
| rank: index + 1, | |
| userId: user.userId, | |
| volumeUsd: user.volumeUsd, | |
| isTop3: index < 3 | |
| })); | |
| return rankings; | |
| } | |
| /** | |
| * Checks if we have valid cached data; if not, fetch fresh from DB. | |
| */ | |
| export async function getCachedData() { | |
| const now = Date.now(); | |
| // If we've never fetched or it's older than CACHE_TTL... | |
| if (!cachedData || now - lastFetchTimestamp > CACHE_TTL) { | |
| console.log('[Cache] Rankings cache is stale, fetching fresh data...'); | |
| cachedData = await fetchFreshData(); | |
| lastFetchTimestamp = now; | |
| } | |
| // Return the cached data (fresh or stale-if not TTL expired) | |
| return cachedData; | |
| } | |
| ========== ./lib/components/LotteryModal.svelte ========== | |
| # lib/components/LotteryModal.svelte | |
| <script> | |
| import { onMount } from 'svelte'; | |
| import { fade, scale } from 'svelte/transition'; | |
| import { Ticket, Star, Trophy } from 'lucide-svelte'; | |
| import confetti from 'canvas-confetti'; | |
| import LotteryTicket from './LotteryTicket.svelte'; | |
| export let userId; | |
| export let isOpen = false; | |
| export let onClose; | |
| let loading = true; | |
| let error = null; | |
| let data = null; | |
| let revealingAll = false; | |
| let revealQueue = []; | |
| // Calculate time until drawing | |
| const endDate = new Date(2025, 1, 28, 23, 59, 59); | |
| $: timeLeft = endDate - new Date(); | |
| $: daysLeft = Math.ceil(timeLeft / (1000 * 60 * 60 * 24)); | |
| async function loadTickets() { | |
| try { | |
| loading = true; | |
| error = null; | |
| const response = await fetch(`/api/lottery/${userId}`); | |
| if (!response.ok) throw new Error('Failed to fetch tickets'); | |
| data = await response.json(); | |
| } catch (err) { | |
| error = err.message; | |
| } finally { | |
| loading = false; | |
| } | |
| } | |
| async function revealTicket() { | |
| try { | |
| const response = await fetch(`/api/lottery/${userId}`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' } | |
| }); | |
| if (!response.ok) throw new Error('Failed to reveal ticket'); | |
| const result = await response.json(); | |
| confetti({ | |
| particleCount: 50, | |
| spread: 45, | |
| origin: { y: 0.6 } | |
| }); | |
| return result.ticketHash; | |
| } catch (err) { | |
| error = err.message; | |
| return null; | |
| } | |
| } | |
| function sleep(ms) { | |
| return new Promise(resolve => setTimeout(resolve, ms)); | |
| } | |
| async function scratchAll() { | |
| if (revealingAll || !data?.remainingTickets) return; | |
| revealingAll = true; | |
| revealQueue = []; | |
| for (let i = 0; i < data.remainingTickets; i++) { | |
| setTimeout(async () => { | |
| const hash = await revealTicket(); | |
| if (hash) { | |
| revealQueue = [...revealQueue, hash]; | |
| } | |
| }, i * 500); | |
| } | |
| await sleep(data.remainingTickets * 500 + 1000); | |
| await loadTickets(); | |
| revealingAll = false; | |
| } | |
| $: if (isOpen && userId) { | |
| loadTickets(); | |
| } | |
| </script> | |
| <div class="fixed inset-0 z-50 flex items-center justify-center p-4" | |
| transition:fade> | |
| <button | |
| class="absolute inset-0 bg-black bg-opacity-60 backdrop-filter backdrop-blur-sm" | |
| on:click={onClose} | |
| on:keydown={(e) => e.key === 'Escape' && onClose()} | |
| aria-label="Close modal" | |
| ></button> | |
| <div class="relative w-full max-w-4xl bg-gray-800 rounded-2xl shadow-2xl p-6" | |
| transition:scale> | |
| <!-- Close button --> | |
| <button | |
| class="absolute top-4 right-4 text-gray-400 hover:text-gray-200 transition-colors" | |
| on:click={onClose} | |
| aria-label="Close modal" | |
| > | |
| ✕ | |
| </button> | |
| <!-- Header --> | |
| <div class="text-center mb-8"> | |
| <h2 class="text-3xl font-bold flex items-center justify-center gap-3 mb-2"> | |
| <Star class="w-8 h-8 text-yellow-500" /> | |
| <span class="bg-gradient-to-r from-yellow-400 to-purple-400 bg-clip-text text-transparent"> | |
| MEGA LOTTERY EVENT | |
| </span> | |
| <Star class="w-8 h-8 text-yellow-500" /> | |
| </h2> | |
| <!-- Countdown --> | |
| <div class="text-xl text-pink-400 font-bold animate-pulse mb-4"> | |
| {daysLeft} Days Until The Big Draw! 🎉 | |
| </div> | |
| <p class="text-gray-300"> | |
| Trade More, Win More! One Lucky Winner Takes The Jackpot! 💰 | |
| </p> | |
| </div> | |
| {#if loading} | |
| <div class="flex justify-center items-center py-12"> | |
| <div class="animate-spin text-indigo-500 text-4xl">🎰</div> | |
| </div> | |
| {:else if error} | |
| <div class="text-red-400 text-center py-8"> | |
| {error} | |
| <button | |
| class="mt-4 px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors" | |
| on:click={loadTickets} | |
| > | |
| Try Again | |
| </button> | |
| </div> | |
| {:else} | |
| <!-- Stats --> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8"> | |
| <!-- Total Tickets --> | |
| <div class="bg-gradient-to-br from-indigo-900/50 to-purple-900/50 rounded-xl p-4 text-center transform hover:scale-105 transition-all duration-300"> | |
| <div class="text-sm text-gray-400">Total Tickets</div> | |
| <div class="text-2xl font-bold text-indigo-400"> | |
| {data.totalTickets} | |
| </div> | |
| <div class="text-xs text-gray-500 mt-1">Keep trading to earn more!</div> | |
| </div> | |
| <!-- Tickets to Reveal --> | |
| <div class="bg-gradient-to-br from-green-900/50 to-emerald-900/50 rounded-xl p-4 text-center transform hover:scale-105 transition-all duration-300"> | |
| <div class="text-sm text-gray-400">Left to Reveal</div> | |
| <div class="text-2xl font-bold text-green-400"> | |
| {data.remainingTickets} | |
| </div> | |
| <div class="text-xs text-gray-500 mt-1">Click to scratch!</div> | |
| </div> | |
| <!-- Trading Volume --> | |
| <div class="bg-gradient-to-br from-yellow-900/50 to-orange-900/50 rounded-xl p-4 text-center transform hover:scale-105 transition-all duration-300"> | |
| <div class="text-sm text-gray-400">Volume Traded</div> | |
| <div class="text-2xl font-bold text-yellow-400"> | |
| ${Math.floor(data.volumeUsd).toLocaleString()} | |
| </div> | |
| <div class="text-xs text-gray-500 mt-1">$100 = 1 ticket</div> | |
| </div> | |
| </div> | |
| <!-- Scratch All Button --> | |
| {#if data.remainingTickets > 0} | |
| <div class="text-center mb-8"> | |
| <button | |
| class="px-6 py-3 bg-gradient-to-r from-indigo-600 to-purple-600 | |
| hover:from-indigo-500 hover:to-purple-500 text-white rounded-xl | |
| font-bold transform hover:scale-105 transition-all duration-300 | |
| shadow-lg shadow-indigo-900/50 | |
| disabled:opacity-50 disabled:cursor-not-allowed" | |
| on:click={scratchAll} | |
| disabled={revealingAll} | |
| > | |
| {#if revealingAll} | |
| <div class="flex items-center gap-2 justify-center"> | |
| <div class="animate-spin text-xl">🎰</div> | |
| Revealing {revealQueue.length}/{data.remainingTickets}... | |
| </div> | |
| {:else} | |
| <div class="flex items-center gap-2 justify-center"> | |
| 🎰 Scratch All {data.remainingTickets} Tickets! 🎰 | |
| </div> | |
| {/if} | |
| </button> | |
| </div> | |
| {/if} | |
| <!-- Tickets Grid --> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 max-h-[60vh] overflow-y-auto p-4"> | |
| {#each [...data.revealedTickets, ...revealQueue] as hash} | |
| <LotteryTicket revealed={true} {hash} /> | |
| {/each} | |
| {#each Array(data.remainingTickets - revealQueue.length) as _} | |
| <LotteryTicket revealed={false} onReveal={revealTicket} /> | |
| {/each} | |
| </div> | |
| <!-- Footer Stats --> | |
| <div class="mt-8 pt-4 border-t border-gray-700/50 text-center text-sm text-gray-400"> | |
| <p>Trade more to increase your chances! Every $100 = 1 new ticket</p> | |
| {#if data.totalTickets > 0} | |
| <p class="mt-2"> | |
| Your current win chance: | |
| <span class="text-yellow-400 font-bold"> | |
| {((data.totalTickets / (data.totalTickets + 1000)) * 100).toFixed(4)}% | |
| </span> | |
| </p> | |
| {/if} | |
| </div> | |
| {/if} | |
| </div> | |
| </div> | |
| <style> | |
| /* Custom scrollbar */ | |
| div :global(::-webkit-scrollbar) { | |
| width: 8px; | |
| height: 8px; | |
| } | |
| div :global(::-webkit-scrollbar-track) { | |
| background: rgba(31, 41, 55, 0.5); | |
| border-radius: 4px; | |
| } | |
| div :global(::-webkit-scrollbar-thumb) { | |
| background: rgba(99, 102, 241, 0.5); | |
| border-radius: 4px; | |
| } | |
| div :global(::-webkit-scrollbar-thumb:hover) { | |
| background: rgba(99, 102, 241, 0.7); | |
| } | |
| /* Animations */ | |
| @keyframes float { | |
| 0%, 100% { transform: translateY(0px); } | |
| 50% { transform: translateY(-5px); } | |
| } | |
| .animate-float { | |
| animation: float 3s ease-in-out infinite; | |
| } | |
| /* Gradient text animation */ | |
| @keyframes gradient { | |
| 0% { background-position: 0% 50%; } | |
| 50% { background-position: 100% 50%; } | |
| 100% { background-position: 0% 50%; } | |
| } | |
| .bg-gradient-to-r { | |
| background-size: 200% auto; | |
| animation: gradient 5s linear infinite; | |
| } | |
| </style> | |
| ========== ./lib/components/LotteryTicket.svelte ========== | |
| <script> | |
| import { slide } from 'svelte/transition'; | |
| import SlotMachine from './SlotMachine.svelte'; | |
| export let revealed = false; | |
| export let hash = ''; | |
| export let onReveal = () => {}; | |
| let isSpinning = false; | |
| let showSlot = false; | |
| async function handleReveal() { | |
| if (revealed || isSpinning) return; | |
| showSlot = true; | |
| isSpinning = true; | |
| const result = await onReveal(); | |
| if (result) { | |
| hash = result; | |
| setTimeout(() => { | |
| isSpinning = false; | |
| }, 2000); | |
| } else { | |
| isSpinning = false; | |
| showSlot = false; | |
| } | |
| } | |
| </script> | |
| <div | |
| <button | |
| class="relative bg-gradient-to-br from-purple-600/30 to-pink-600/30 | |
| p-4 rounded-xl shadow-lg transform transition-all duration-300 | |
| hover:scale-105 cursor-pointer overflow-hidden w-full text-left" | |
| class:revealed | |
| on:click={handleReveal} | |
| on:keydown={(e) => e.key === 'Enter' && handleReveal()} | |
| aria-label={revealed ? 'Revealed ticket' : 'Click to reveal ticket'} | |
| disabled={revealed} | |
| > | |
| {#if showSlot} | |
| <div transition:slide> | |
| <SlotMachine | |
| spinning={isSpinning} | |
| finalValue={hash} | |
| on:spinComplete={() => revealed = true} | |
| /> | |
| </div> | |
| {:else} | |
| <div class="text-center" in:slide> | |
| <div class="text-3xl mb-2">🎟️</div> | |
| <div class="text-sm text-gray-300"> | |
| Click to Reveal! | |
| </div> | |
| </div> | |
| {/if} | |
| </div> | |
| <style> | |
| .revealed { | |
| cursor: default; | |
| } | |
| </style> | |
| ========== ./lib/components/SlotMachine.svelte ========== | |
| <!-- lib/components/SlotMachine.svelte --> | |
| <script> | |
| import { onMount, createEventDispatcher } from 'svelte'; | |
| const dispatch = createEventDispatcher(); | |
| export let spinning = false; | |
| export let finalValue = ''; | |
| export let duration = 2000; | |
| const chars = '0123456789ABCDEF'; | |
| let reels = Array(32).fill('0'); | |
| let spinInterval; | |
| let spinStart; | |
| onMount(() => { | |
| if (spinning) startSpin(); | |
| return () => clearInterval(spinInterval); | |
| }); | |
| $: if (spinning) { | |
| startSpin(); | |
| } else { | |
| stopSpin(); | |
| } | |
| function startSpin() { | |
| spinStart = Date.now(); | |
| spinInterval = setInterval(updateReels, 50); | |
| } | |
| function updateReels() { | |
| const elapsed = Date.now() - spinStart; | |
| const progress = Math.min(1, elapsed / duration); | |
| reels = reels.map((_, i) => { | |
| // Slow down based on position and progress | |
| const shouldStop = progress > (i / reels.length * 0.5); | |
| if (shouldStop && finalValue[i]) { | |
| return finalValue[i]; | |
| } | |
| return chars[Math.floor(Math.random() * chars.length)]; | |
| }); | |
| if (progress >= 1) { | |
| stopSpin(); | |
| dispatch('spinComplete'); | |
| } | |
| } | |
| function stopSpin() { | |
| clearInterval(spinInterval); | |
| reels = finalValue.split(''); | |
| } | |
| </script> | |
| <div class="font-mono text-2xl grid grid-flow-col gap-1 tracking-wider"> | |
| {#each reels as char, i} | |
| <div | |
| class="bg-gray-800 px-2 py-1 rounded-md shadow-inner transform transition-all duration-300" | |
| style="animation-delay: {i * 50}ms;" | |
| > | |
| {char} | |
| </div> | |
| {/each} | |
| </div> | |
| <style> | |
| .grid { | |
| grid-template-columns: repeat(32, 1fr); | |
| } | |
| @keyframes bounceIn { | |
| 0% { transform: scale(0.3); opacity: 0; } | |
| 50% { transform: scale(1.05); opacity: 0.8; } | |
| 70% { transform: scale(0.9); opacity: 0.9; } | |
| 100% { transform: scale(1); opacity: 1; } | |
| } | |
| div > div { | |
| animation: bounceIn 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55) backwards; | |
| } | |
| </style> | |
| ========== ./lib/db.js ========== | |
| import postgres from 'postgres'; | |
| //export const userDb = postgres('postgres://postgres:o3j0CfWk4x2R0T@x-psql-prod.flycast:5432/xshot_core_prod'); | |
| //export const tradeDb = postgres('postgres://postgres:o3j0CfWk4x2R0T@x-psql-prod.flycast:5432/holdings_db'); | |
| export const userDb = postgres('postgres://postgres:o3j0CfWk4x2R0T@localhost:5432/xshot_core_prod'); | |
| export const tradeDb = postgres('postgres://postgres:o3j0CfWk4x2R0T@localhost:5432/holdings_db'); | |
| ========== ./lib/index.js ========== | |
| // place files you want to import through the `$lib` alias in this folder. | |
| ========== ./lib/lottery-data.js ========== | |
| export let lotteryStats = { | |
| volumeUsd: 0, | |
| tradeCount: 0, | |
| ticketCount: 0, | |
| tickets: 0, | |
| totalPoolTickets: 0, | |
| winProbability: 0 | |
| }; | |
| export async function loadLotteryStats() { | |
| try { | |
| const response = await fetch('/api/lottery/stats'); | |
| if (!response.ok) throw new Error('Failed to fetch lottery stats'); | |
| const data = await response.json(); | |
| lotteryStats = { | |
| ...data, | |
| tickets: Math.floor(data.volumeUsd / 100), | |
| totalPoolTickets: Math.floor(data.volumeUsd / 100), | |
| winProbability: data.ticketCount > 0 ? data.ticketCount / Math.floor(data.volumeUsd / 100) : 0 | |
| }; | |
| return lotteryStats; | |
| } catch (err) { | |
| console.error('Error loading lottery stats:', err); | |
| return lotteryStats; | |
| } | |
| } | |
| ========== ./lib/prices.js ========== | |
| import { Connection, PublicKey } from '@solana/web3.js'; | |
| import { ethers } from 'ethers'; | |
| const SOL_RPC = 'https://hidden-small-wish.solana-mainnet.quiknode.pro/8aa3a97956500152c37afb6d81a1bcb34387d642/'; | |
| const ETH_RPC = 'https://bold-virulent-hill.quiknode.pro/6c308edd006d129e8c940757cfaac7ca5852eabe/'; | |
| // Multiple wallet support | |
| const WALLETS = [ | |
| { | |
| sol: '4fRQWKJzJYjKi9PHmpd2SXMkiQDgr6yuNADF1GL2wWZW', | |
| eth: '0xFDBa7f32A6f18cE1c97753A616DA32A67A3C93fA' | |
| } | |
| ]; | |
| async function getTokenPrices() { | |
| try { | |
| const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=solana,ethereum&vs_currencies=usd'); | |
| const data = await response.json(); | |
| return { | |
| solana: data.solana.usd, | |
| ethereum: data.ethereum.usd | |
| }; | |
| } catch (error) { | |
| console.error('Error fetching token prices:', error); | |
| return { solana: 0, ethereum: 0 }; | |
| } | |
| } | |
| async function getSolanaBalance(walletAddress) { | |
| try { | |
| const connection = new Connection(SOL_RPC); | |
| const pubKey = new PublicKey(walletAddress); | |
| // Remove the TS type assertion: | |
| const config = { | |
| commitment: 'confirmed', | |
| minContextSlot: 0 | |
| }; | |
| const balance = await connection.getBalance(pubKey, config); | |
| console.log('Solana Response:', { | |
| address: walletAddress, | |
| rawBalance: balance, | |
| convertedBalance: balance / 1e9 | |
| }); | |
| return balance / 1e9; // Convert lamports to SOL | |
| } catch (error) { | |
| console.error('Error fetching SOL balance:', error); | |
| return 0; | |
| } | |
| } | |
| async function getEthBalance(walletAddress) { | |
| try { | |
| const provider = new ethers.JsonRpcProvider(ETH_RPC); | |
| const balance = await provider.getBalance(walletAddress); | |
| return parseFloat(ethers.formatEther(balance)); | |
| } catch (error) { | |
| console.error('Error fetching ETH balance:', error); | |
| return 0; | |
| } | |
| } | |
| export async function getAllWalletBalances() { | |
| const prices = await getTokenPrices(); | |
| const walletBalances = []; | |
| for (const wallet of WALLETS) { | |
| const solBalance = await getSolanaBalance(wallet.sol); | |
| const ethBalance = await getEthBalance(wallet.eth); | |
| walletBalances.push({ | |
| solana: { | |
| wallet: wallet.sol, | |
| balance: solBalance, | |
| usdValue: solBalance * prices.solana, | |
| price: prices.solana | |
| }, | |
| ethereum: { | |
| wallet: wallet.eth, | |
| balance: ethBalance, | |
| usdValue: ethBalance * prices.ethereum, | |
| price: prices.ethereum | |
| } | |
| }); | |
| } | |
| return walletBalances; | |
| } | |
| ========== ./routes/+layout.svelte ========== | |
| <script> | |
| import "../app.css"; | |
| </script> | |
| <slot /> | |
| ========== ./routes/+page.svelte ========== | |
| <script> | |
| import { onMount } from 'svelte'; | |
| import LotteryModal from '$lib/components/LotteryModal.svelte'; | |
| import { Ticket } from 'lucide-svelte'; | |
| import { getAllWalletBalances } from '$lib/prices'; | |
| import { lotteryStats, loadLotteryStats } from "$lib/lottery-data"; | |
| let rankings = []; | |
| let searchTerm = ''; | |
| let targetVolume = null; | |
| let loading = false; | |
| let error = ''; | |
| let darkMode = true; | |
| let showLotteryModal = false; | |
| let walletBalances = []; | |
| let totalSolUsd = 0; | |
| let totalEthUsd = 0; | |
| let searchInputRef; | |
| let showTable = false; // Controls the visibility of the Rankings Table | |
| const DISTRIBUTION = { | |
| first: 0.5, | |
| second: 0.3, | |
| third: 0.2 | |
| }; | |
| let daysUntilDraw; | |
| onMount(async () => { | |
| await loadLotteryStats(); | |
| loadRankings(); | |
| loadPrizes(); | |
| const endDate = new Date(2025, 1, 28, 23, 59, 59); // Feb 28, 2025 | |
| const now = new Date(); | |
| daysUntilDraw = Math.ceil((endDate - now) / (1000 * 60 * 60 * 24)); | |
| if (window.matchMedia('(prefers-color-scheme: light)').matches) { | |
| darkMode = false; | |
| } | |
| }); | |
| async function loadRankings() { | |
| try { | |
| const response = await fetch('/api/rankings'); | |
| if (!response.ok) throw new Error('Failed to fetch rankings'); | |
| rankings = await response.json(); | |
| } catch (err) { | |
| error = 'Failed to load rankings'; | |
| } | |
| } | |
| async function loadPrizes() { | |
| try { | |
| walletBalances = await getAllWalletBalances(); | |
| // 50% of total for each chain | |
| totalSolUsd = walletBalances.reduce((sum, w) => sum + w.solana.usdValue, 0) * 0.5; | |
| totalEthUsd = walletBalances.reduce((sum, w) => sum + w.ethereum.usdValue, 0) * 0.5; | |
| } catch (err) { | |
| console.error('Error loading prizes:', err); | |
| } | |
| } | |
| async function searchUser() { | |
| if (!searchTerm) return; | |
| loading = true; | |
| error = ''; | |
| try { | |
| const response = await fetch(`/api/search/${encodeURIComponent(searchTerm)}`); | |
| if (!response.ok) throw new Error('Failed to fetch user data'); | |
| targetVolume = await response.json(); | |
| } catch (err) { | |
| error = 'User not found'; | |
| targetVolume = null; | |
| } finally { | |
| loading = false; | |
| } | |
| } | |
| function formatUsd(num) { | |
| return new Intl.NumberFormat('en-US', { | |
| style: 'currency', | |
| currency: 'USD', | |
| notation: 'compact', | |
| maximumFractionDigits: 2 | |
| }).format(num); | |
| } | |
| function toggleDarkMode() { | |
| darkMode = !darkMode; | |
| } | |
| </script> | |
| <!-- Outer Container with smooth transitions --> | |
| <div class={`min-h-screen transition-all duration-300 ${darkMode ? 'bg-gray-900 text-gray-100' : 'bg-gray-50 text-gray-900'}`}> | |
| <div class="container mx-auto px-3 sm:px-4 py-4 sm:py-6 lg:py-8 max-w-5xl relative"> | |
| <!-- Dark Mode Toggle with improved positioning --> | |
| <div class="absolute top-2 right-2 sm:top-4 sm:right-4 z-10"> | |
| <button | |
| on:click={toggleDarkMode} | |
| class="p-2 rounded-full hover:bg-opacity-20 hover:bg-gray-500 transition-all duration-300 transform hover:scale-110" | |
| > | |
| {#if darkMode} | |
| <span class="text-xl sm:text-2xl">🌞</span> | |
| {:else} | |
| <span class="text-xl sm:text-2xl">🌙</span> | |
| {/if} | |
| </button> | |
| </div> | |
| <div class={`mb-4 sm:mb-6 p-4 sm:p-6 rounded-xl sm:rounded-2xl shadow-xl | |
| backdrop-filter backdrop-blur-sm transition-all duration-300 | |
| ${darkMode ? 'bg-gray-800/90 shadow-gray-900/50' : 'bg-white/90 shadow-gray-200/50'}`}> | |
| <div class="text-center space-y-4 md:space-y-6"> | |
| <!-- Improved Title Section --> | |
| <div class="relative inline-block"> | |
| <h2 class="text-xl sm:text-2xl lg:text-4xl font-bold flex items-center justify-center gap-3 p-2"> | |
| <span class="trophy-icon text-2xl sm:text-3xl lg:text-4xl animate-gentle-pulse">🏆</span> | |
| <span class="gradient-text tracking-tight">January Trading Competition</span> | |
| <span class="trophy-icon text-2xl sm:text-3xl lg:text-4xl animate-gentle-pulse">🏆</span> | |
| </h2> | |
| </div> | |
| <!-- Enhanced Prize Pool Amount --> | |
| <div class="prize-pool transform hover:scale-105 transition-all duration-300"> | |
| <div class="text-3xl sm:text-4xl lg:text-6xl xl:text-7xl font-extrabold bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 bg-clip-text text-transparent tracking-tight"> | |
| {formatUsd(totalSolUsd + totalEthUsd)} | |
| </div> | |
| <div class="text-base sm:text-lg lg:text-xl font-medium text-indigo-400 mt-2"> | |
| Total Prize Pool to Win | |
| </div> | |
| </div> | |
| <!-- Redesigned Distribution Cards --> | |
| <div class="max-w-2xl mx-auto p-3 rounded-xl bg-gray-700/20 backdrop-filter backdrop-blur-sm transition-all duration-300"> | |
| <div class="grid grid-cols-1 sm:grid-cols-3 gap-3"> | |
| {#each [ | |
| { place: "1st Place", emoji: "🥇", amount: DISTRIBUTION.first, gradient: "from-blue-500 to-indigo-500" }, | |
| { place: "2nd Place", emoji: "🥈", amount: DISTRIBUTION.second, gradient: "from-indigo-500 to-purple-500" }, | |
| { place: "3rd Place", emoji: "🥉", amount: DISTRIBUTION.third, gradient: "from-purple-500 to-pink-500" } | |
| ] as { place, emoji, amount, gradient }} | |
| <div class="prize-card group"> | |
| <div class="flex flex-col items-center space-y-2"> | |
| <div class="text-base sm:text-lg font-medium flex items-center gap-2"> | |
| <span class="text-xl sm:text-2xl">{emoji}</span> | |
| {place} | |
| </div> | |
| <div class="font-bold text-lg sm:text-xl lg:text-2xl bg-gradient-to-r {gradient} bg-clip-text text-transparent"> | |
| {formatUsd((totalSolUsd + totalEthUsd) * amount)} | |
| </div> | |
| <div class="text-xs sm:text-sm text-gray-400 group-hover:text-gray-300 transition-colors duration-300"> | |
| {(amount * 100)}% of prize pool | |
| </div> | |
| </div> | |
| </div> | |
| {/each} | |
| </div> | |
| </div> | |
| <!-- Enhanced Competition Explainer --> | |
| <div class="p-4 rounded-xl bg-gray-700/20 backdrop-filter backdrop-blur-sm transition-all duration-300 | |
| max-w-2xl mx-auto hover:bg-gray-700/30"> | |
| <h3 class="text-base sm:text-lg lg:text-xl font-semibold mb-2 gradient-text"> | |
| Why Join the Competition? | |
| </h3> | |
| <p class="text-sm sm:text-base leading-relaxed text-gray-300"> | |
| The 3 biggest volume makers on XSHOT will split | |
| <span class="font-semibold text-indigo-400">50% of the total generated fees</span> | |
| on XSHOT throughout the month, which is | |
| <span class="font-semibold text-indigo-400">0.6% of every trade</span> from most users. | |
| This is how top competitors can earn a substantial reward, and why we see this as | |
| an exciting opportunity to <em>share the bread</em> and make trading fun for everyone. | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Wallet Info Cards --> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6 mb-6"> | |
| <!-- Solana Card --> | |
| <div class={`p-4 sm:p-6 rounded-xl shadow-lg transition-all duration-300 transform hover:scale-102 | |
| ${darkMode ? 'bg-gray-800/90' : 'bg-white/90'}`}> | |
| <div class="text-lg sm:text-xl font-medium mb-2"> | |
| Solana Wallet | |
| </div> | |
| <div class="text-2xl sm:text-3xl font-extrabold mb-4 text-indigo-500"> | |
| {formatUsd(totalSolUsd)} | |
| </div> | |
| <div class="text-sm"> | |
| <a | |
| href={`https://solscan.io/account/${walletBalances[0]?.solana.wallet}`} | |
| target="_blank" | |
| class="text-indigo-600 hover:text-indigo-500 hover:underline dark:text-indigo-400 transition-colors" | |
| > | |
| Verify Prize Pool on Solscan ↗ | |
| </a> | |
| </div> | |
| </div> | |
| <!-- Ethereum Card --> | |
| <div class={`p-4 sm:p-6 rounded-xl shadow-lg transition-all duration-300 transform hover:scale-102 | |
| ${darkMode ? 'bg-gray-800/90' : 'bg-white/90'}`}> | |
| <div class="text-lg sm:text-xl font-medium mb-2"> | |
| Ethereum Wallet | |
| </div> | |
| <div class="text-2xl sm:text-3xl font-extrabold mb-4 text-indigo-500"> | |
| {formatUsd(totalEthUsd)} | |
| </div> | |
| <div class="text-sm"> | |
| <a | |
| href={`https://etherscan.io/address/${walletBalances[0]?.ethereum.wallet}`} | |
| target="_blank" | |
| class="text-indigo-600 hover:text-indigo-500 hover:underline dark:text-indigo-400 transition-colors" | |
| > | |
| Verify Prize Pool on Etherscan ↗ | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Search Section --> | |
| <div class={`mb-6 p-4 sm:p-6 rounded-xl shadow-lg backdrop-filter backdrop-blur-sm | |
| ${darkMode ? 'bg-gray-800/90' : 'bg-white/90'}`}> | |
| <div class="max-w-xl mx-auto space-y-4"> | |
| <div class="relative"> | |
| <input | |
| bind:this={searchInputRef} | |
| type="text" | |
| bind:value={searchTerm} | |
| on:keydown={(e) => e.key === 'Enter' && searchUser()} | |
| class={`w-full px-4 py-3 rounded-lg border text-base | |
| focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-all duration-300 | |
| ${darkMode | |
| ? 'bg-gray-700/70 border-gray-600 text-white placeholder-gray-300' | |
| : 'bg-white border-gray-300 text-gray-900 placeholder-gray-500' | |
| }`} | |
| placeholder="Enter your User ID" | |
| /> | |
| <button | |
| on:click={searchUser} | |
| disabled={loading} | |
| class="absolute right-2 top-2 bg-indigo-600 text-white px-4 py-1 rounded-md | |
| hover:bg-indigo-700 disabled:opacity-50 transition-all duration-300" | |
| > | |
| {loading ? 'Searching...' : 'Search'} | |
| </button> | |
| </div> | |
| <div class={`flex items-center justify-center gap-4 p-3 rounded-lg bg-opacity-50 | |
| ${darkMode ? 'bg-gray-700/50' : 'bg-gray-100/50'}`}> | |
| <div class="text-sm font-medium">Find your User ID:</div> | |
| <div class="font-mono text-sm bg-indigo-500/20 px-3 py-1 rounded"> | |
| Type /id in XSHOT | |
| </div> | |
| </div> | |
| {#if error} | |
| <div class="text-red-500 text-center text-sm animate-fade-in">{error}</div> | |
| {/if} | |
| {#if targetVolume} | |
| <div class={`mt-4 p-4 rounded-lg animate-fade-in | |
| ${darkMode ? 'bg-gray-700/50' : 'bg-gray-100/50'}`}> | |
| <h3 class="text-lg font-semibold mb-3 text-center gradient-text">Your Trading Stats</h3> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> | |
| <div class="p-4 rounded-xl bg-green-500/10 backdrop-filter backdrop-blur-sm"> | |
| <div class="text-sm opacity-80">Your Current Volume</div> | |
| <div class="text-xl font-bold text-green-400"> | |
| {formatUsd(targetVolume.currentVolumeUsd)} | |
| </div> | |
| </div> | |
| <div class="p-4 rounded-xl bg-purple-500/10 backdrop-filter backdrop-blur-sm"> | |
| <div class="text-sm opacity-80">Volume Needed for Top 3</div> | |
| <div class="text-xl font-bold text-purple-400"> | |
| {formatUsd(targetVolume.volumeNeededForTop3Usd)} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/if} | |
| </div> | |
| </div> | |
| <!-- Add to your +page.svelte where you want the lottery section --> | |
| <div class="max-w-2xl mx-auto mb-8"> | |
| <div class={`p-4 sm:p-6 rounded-xl shadow-xl transition-all duration-300 | |
| ${darkMode ? 'bg-gray-800/90 shadow-gray-900/50' : 'bg-white/90 shadow-gray-200/50'} | |
| backdrop-filter backdrop-blur-sm`}> | |
| <div class="text-center space-y-4"> | |
| <!-- Title with matching gradient --> | |
| <div class="relative inline-block"> | |
| <h2 class="text-xl sm:text-2xl lg:text-3xl font-bold flex items-center justify-center gap-3 p-2"> | |
| <span class="text-yellow-500 animate-pulse">⭐</span> | |
| <span class="gradient-text tracking-tight">MEGA JACKPOT LOTTERY</span> | |
| <span class="text-yellow-500 animate-pulse">⭐</span> | |
| </h2> | |
| </div> | |
| <!-- Countdown Banner --> | |
| <div class="bg-gradient-to-r from-yellow-500/20 to-pink-500/20 rounded-full px-4 py-2 inline-block"> | |
| <span class="text-yellow-400 font-bold animate-pulse"> | |
| 52 Days Until Draw! 🎰 | |
| </span> | |
| </div> | |
| <!-- Ticket Info --> | |
| <div class="bg-gradient-to-br from-purple-500/10 to-pink-500/10 rounded-xl p-4"> | |
| <h3 class="text-lg font-bold text-purple-400 mb-2"> | |
| Your Lucky Tickets: <span class="text-2xl">🎟️ {Math.floor((targetVolume?.currentVolumeUsd || 0) / 100)}</span> | |
| </h3> | |
| <p class="text-sm text-gray-400"> | |
| Each $100 traded = 1 ticket to victory! | |
| </p> | |
| </div> | |
| <!-- Stats Grid --> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <div class="bg-gradient-to-br from-blue-500/10 to-purple-500/10 rounded-xl p-4"> | |
| <div class="text-sm text-gray-400">Your Trades</div> | |
| <div class="text-xl font-bold text-blue-400">{lotteryStats.tradeCount}</div> | |
| </div> | |
| <div class="bg-gradient-to-br from-green-500/10 to-teal-500/10 rounded-xl p-4"> | |
| <div class="text-sm text-gray-400">Volume</div> | |
| <div class="text-xl font-bold text-green-400">{formatUsd(lotteryStats.volumeUsd)}</div> | |
| </div> | |
| </div> | |
| <!-- Win Chance --> | |
| <div class="bg-gradient-to-r from-yellow-500/10 to-orange-500/10 rounded-xl p-4"> | |
| <h3 class="text-lg font-bold text-yellow-400 mb-2"> | |
| Current Win Chance | |
| </h3> | |
| <div class="text-2xl font-bold">{((targetVolume?.currentVolumeUsd || 0) / (lotteryStats.volumeUsd || 1) * 100).toFixed(4)}%</div> | |
| <p class="text-sm text-gray-400 mt-1"> | |
| Based on your tickets vs total pool | |
| </p> | |
| </div> | |
| <!-- Call to Action --> | |
| {#if searchTerm} | |
| <button | |
| on:click={() => showLotteryModal = true} | |
| class="px-6 py-3 bg-gradient-to-r from-purple-600 to-pink-600 text-white | |
| font-semibold rounded-xl shadow-lg transform hover:scale-105 | |
| transition-all duration-300" | |
| > | |
| View My Tickets 🎟️ | |
| </button> | |
| {:else} | |
| <div class="text-sm text-purple-400"> | |
| Search your User ID above to view your tickets | |
| </div> | |
| {/if} | |
| </div> | |
| </div> | |
| </div> | |
| <!-- CTA Button --> | |
| <div class="text-center mb-6"> | |
| <a | |
| href="https://t.me/xshot_trading_bot" | |
| target="_blank" | |
| class="inline-block px-6 sm:px-8 py-3 sm:py-4 bg-indigo-600 text-white font-bold rounded-xl | |
| shadow-lg transform hover:scale-105 hover:bg-indigo-500 transition-all duration-300" | |
| > | |
| 🚀 Start Trading with XSHOT Now | |
| </a> | |
| </div> | |
| <!-- Rankings Toggle --> | |
| <div class="text-center mb-4"> | |
| <button | |
| on:click={() => (showTable = !showTable)} | |
| class="flex mx-auto items-center justify-center gap-2 text-indigo-600 dark:text-indigo-400 | |
| font-bold text-lg sm:text-xl transform hover:scale-110 transition-all duration-300" | |
| > | |
| {#if showTable} | |
| <span>Hide Rankings</span> | |
| <span class="text-2xl sm:text-3xl rotate-180 inline-block transition-transform duration-300">⬇</span> | |
| {:else} | |
| <span>View Rankings</span> | |
| <span class="text-2xl sm:text-3xl inline-block transition-transform duration-300">⬇</span> | |
| {/if} | |
| </button> | |
| </div> | |
| <!-- Rankings Table --> | |
| {#if showTable} | |
| <div class={`rounded-xl shadow-lg overflow-hidden mt-4 mx-auto backdrop-filter backdrop-blur-sm | |
| ${darkMode ? 'bg-gray-800/90' : 'bg-white/90'}`}> | |
| <div class="overflow-x-auto"> | |
| <table class="min-w-full"> | |
| <!-- In your rankings table section in +page.svelte --> | |
| <thead class={darkMode ? 'bg-gray-700/50 text-gray-200' : 'bg-gray-100/50 text-gray-800'}> | |
| <tr> | |
| <th class="px-4 sm:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Rank</th> | |
| <th class="px-4 sm:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">User ID</th> | |
| <th class="px-4 sm:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Volume (USD)</th> | |
| <th class="px-4 sm:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Lottery Tickets</th> | |
| </tr> | |
| </thead> | |
| <tbody class={`divide-y ${darkMode ? 'divide-gray-700/50' : 'divide-gray-200/50'}`}> | |
| {#each rankings as { rank, userId, volumeUsd, isTop3 }} | |
| <tr class={` | |
| transition-colors duration-200 | |
| ${darkMode ? 'hover:bg-gray-700/50' : 'hover:bg-gray-50/50'} | |
| ${isTop3 ? (darkMode ? 'bg-gray-700/30' : 'bg-gray-50/30') : ''} | |
| `}> | |
| <td class="px-4 sm:px-6 py-3 whitespace-nowrap"> | |
| {#if rank <= 3} | |
| <span class="text-xl">{rank === 1 ? '🥇' : rank === 2 ? '🥈' : '🥉'}</span> | |
| {:else} | |
| {rank} | |
| {/if} | |
| </td> | |
| <td class="px-4 sm:px-6 py-3 whitespace-nowrap font-medium"> | |
| {userId} | |
| </td> | |
| <td class="px-4 sm:px-6 py-3 whitespace-nowrap"> | |
| {formatUsd(volumeUsd)} | |
| </td> | |
| <td class="px-4 sm:px-6 py-3 whitespace-nowrap font-mono text-indigo-400"> | |
| {Math.floor(volumeUsd / 100)} 🎟️ | |
| </td> | |
| </tr> | |
| {/each} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| {/if} | |
| </div> | |
| <!-- Footer --> | |
| <footer class="mt-8 py-4 text-center text-sm text-gray-500 dark:text-gray-400 border-t | |
| border-gray-200/20 dark:border-gray-700/20 backdrop-filter backdrop-blur-sm"> | |
| <div class="container mx-auto px-4"> | |
| <p class="mb-2 text-xs sm:text-sm">Explore more on:</p> | |
| <div class="flex items-center justify-center gap-4 text-sm sm:text-base"> | |
| <a | |
| href="https://www.xprojecterc.com/" | |
| target="_blank" | |
| class="text-indigo-500 hover:text-indigo-400 hover:underline dark:text-indigo-400 | |
| transition-colors duration-300" | |
| > | |
| XProject Main Website | |
| </a> | |
| <span class="text-gray-400">|</span> | |
| <a | |
| href="https://xshot.xprojecterc.com/" | |
| target="_blank" | |
| class="text-indigo-500 hover:text-indigo-400 hover:underline dark:text-indigo-400 | |
| transition-colors duration-300" | |
| > | |
| XShot Website | |
| </a> | |
| </div> | |
| </div> | |
| </footer> | |
| {#if showLotteryModal} | |
| <LotteryModal | |
| userId={parseInt(searchTerm) || 0} | |
| isOpen={showLotteryModal} | |
| onClose={() => showLotteryModal = false} | |
| {darkMode} | |
| /> | |
| {/if} | |
| </div> | |
| <style> | |
| /* Modern font setup */ | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap'); | |
| :global(html) { | |
| scroll-behavior: smooth; | |
| } | |
| :global(html), | |
| :global(body), | |
| :global(button), | |
| :global(input), | |
| :global(select), | |
| :global(textarea) { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | |
| } | |
| /* Elegant scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| height: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #6366f1; | |
| border-radius: 3px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #4f46e5; | |
| } | |
| /* Enhanced custom classes */ | |
| .gradient-text { | |
| @apply bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400 bg-clip-text text-transparent; | |
| } | |
| .prize-card { | |
| @apply p-4 rounded-xl bg-gray-800/50 backdrop-filter backdrop-blur-sm shadow-lg transition-all duration-300; | |
| } | |
| .trophy-icon { | |
| @apply text-yellow-500; | |
| } | |
| /* Custom animations */ | |
| @keyframes gentle-pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.8; } | |
| } | |
| .animate-gentle-pulse { | |
| animation: gentle-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; | |
| } | |
| .animate-fade-in { | |
| animation: fadeIn 0.5s ease-in-out; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(-10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| </style> | |
| ========== ./routes/+page.svelte.bak ========== | |
| <script> | |
| import { onMount } from 'svelte'; | |
| import { getAllWalletBalances } from '$lib/prices'; | |
| let rankings = []; | |
| let searchTerm = ''; | |
| let targetVolume = null; | |
| let loading = false; | |
| let error = ''; | |
| let darkMode = true; | |
| let walletBalances = []; | |
| let totalSolUsd = 0; | |
| let totalEthUsd = 0; | |
| let searchInputRef; | |
| const DISTRIBUTION = { | |
| first: 0.5, | |
| second: 0.3, | |
| third: 0.2 | |
| }; | |
| onMount(async () => { | |
| loadRankings(); | |
| loadPrizes(); | |
| if (window.matchMedia('(prefers-color-scheme: light)').matches) { | |
| darkMode = false; | |
| } | |
| }); | |
| async function loadRankings() { | |
| try { | |
| const response = await fetch('/api/rankings'); | |
| if (!response.ok) throw new Error('Failed to fetch rankings'); | |
| rankings = await response.json(); | |
| } catch (err) { | |
| error = 'Failed to load rankings'; | |
| } | |
| } | |
| async function loadPrizes() { | |
| try { | |
| walletBalances = await getAllWalletBalances(); | |
| totalSolUsd = walletBalances.reduce((sum, wallet) => sum + wallet.solana.usdValue, 0) * 0.5; // 50% of total | |
| totalEthUsd = walletBalances.reduce((sum, wallet) => sum + wallet.ethereum.usdValue, 0) * 0.5; // 50% of total | |
| } catch (err) { | |
| console.error('Error loading prizes:', err); | |
| } | |
| } | |
| async function searchUser() { | |
| if (!searchTerm) return; | |
| loading = true; | |
| error = ''; | |
| try { | |
| const response = await fetch(`/api/search/${encodeURIComponent(searchTerm)}`); | |
| if (!response.ok) throw new Error('Failed to fetch user data'); | |
| targetVolume = await response.json(); | |
| } catch (err) { | |
| error = 'User not found'; | |
| targetVolume = null; | |
| } finally { | |
| loading = false; | |
| } | |
| } | |
| function formatUsd(num) { | |
| return new Intl.NumberFormat('en-US', { | |
| style: 'currency', | |
| currency: 'USD', | |
| notation: 'compact', | |
| maximumFractionDigits: 2 | |
| }).format(num); | |
| } | |
| function toggleDarkMode() { | |
| darkMode = !darkMode; | |
| } | |
| </script> | |
| <div class={`min-h-screen transition-colors duration-200 ${darkMode ? 'bg-gray-900 text-gray-100' : 'bg-gray-50 text-gray-900'}`}> | |
| <div class="container mx-auto px-4 py-8 max-w-6xl"> | |
| <!-- Dark Mode Toggle --> | |
| <div class="absolute top-4 right-4"> | |
| <button | |
| on:click={toggleDarkMode} | |
| class="p-2 rounded-full hover:bg-opacity-20 hover:bg-gray-500 transition-colors" | |
| > | |
| {#if darkMode} | |
| 🌞 | |
| {:else} | |
| 🌙 | |
| {/if} | |
| </button> | |
| </div> | |
| <!-- Prize Pool Banner --> | |
| <div class={`mb-8 p-6 rounded-lg shadow-lg ${darkMode ? 'bg-gradient-to-r from-blue-900/50 to-purple-900/50' : 'bg-gradient-to-r from-blue-100 to-purple-100'}`}> | |
| <div class="text-center"> | |
| <!-- Main Title --> | |
| <h2 class="text-4xl font-bold mb-6 bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-purple-400"> | |
| 🏆 Monthly Trading Competition 🏆 | |
| </h2> | |
| <!-- Grand Total Prize Display --> | |
| <div class="mb-8 relative"> | |
| <div class="absolute inset-0 bg-gradient-to-r from-yellow-400 via-red-500 to-pink-500 opacity-10 blur-xl"></div> | |
| <div class="relative"> | |
| <div class="text-7xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-yellow-400 via-red-500 to-pink-500 animate-pulse mb-2"> | |
| {formatUsd(totalSolUsd + totalEthUsd)} | |
| </div> | |
| <div class="text-2xl font-semibold bg-clip-text text-transparent bg-gradient-to-r from-yellow-400 to-pink-500"> | |
| Total Prize Pool to Win | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Prize Distribution --> | |
| <div class="max-w-2xl mx-auto mb-8 p-6 rounded-xl {darkMode ? 'bg-gray-800/50' : 'bg-white/70'}"> | |
| <div class="grid grid-cols-3 gap-6 mb-6"> | |
| <div class="p-4 rounded-lg bg-opacity-20 {darkMode ? 'bg-blue-500/20' : 'bg-blue-100'}"> | |
| <div class="font-medium text-lg mb-1">🥇 1st Place</div> | |
| <div class="font-bold text-xl">{formatUsd((totalSolUsd + totalEthUsd) * DISTRIBUTION.first)}</div> | |
| <div class="text-sm opacity-70">50% of prize pool</div> | |
| </div> | |
| <div class="p-4 rounded-lg bg-opacity-20 {darkMode ? 'bg-blue-500/20' : 'bg-blue-100'}"> | |
| <div class="font-medium text-lg mb-1">🥈 2nd Place</div> | |
| <div class="font-bold text-xl">{formatUsd((totalSolUsd + totalEthUsd) * DISTRIBUTION.second)}</div> | |
| <div class="text-sm opacity-70">30% of prize pool</div> | |
| </div> | |
| <div class="p-4 rounded-lg bg-opacity-20 {darkMode ? 'bg-blue-500/20' : 'bg-blue-100'}"> | |
| <div class="font-medium text-lg mb-1">🥉 3rd Place</div> | |
| <div class="font-bold text-xl">{formatUsd((totalSolUsd + totalEthUsd) * DISTRIBUTION.third)}</div> | |
| <div class="text-sm opacity-70">20% of prize pool</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Prize Categories --> | |
| <div class="grid md:grid-cols-2 gap-8 mb-8"> | |
| <!-- Solana Prize Pool --> | |
| <div class={`p-6 rounded-lg transform transition-all hover:scale-105 ${darkMode ? 'bg-blue-900/30' : 'bg-blue-50'}`}> | |
| <div class="text-xl font-medium mb-2">Solana Wallet</div> | |
| <div class="text-3xl font-bold mb-4 bg-clip-text text-transparent bg-gradient-to-r from-[#00FFA3] to-[#DC1FFF]"> | |
| {formatUsd(totalSolUsd)} | |
| </div> | |
| <div class="text-sm"> | |
| <a href={`https://solscan.io/account/${walletBalances[0]?.solana.wallet}`} | |
| target="_blank" | |
| class="text-blue-400 hover:underline"> | |
| Verify Prize Pool on Solscan ↗ | |
| </a> | |
| </div> | |
| </div> | |
| <!-- ETH Prize Pool --> | |
| <div class={`p-6 rounded-lg transform transition-all hover:scale-105 ${darkMode ? 'bg-purple-900/30' : 'bg-purple-50'}`}> | |
| <div class="text-xl font-medium mb-2">Ethereum Wallet</div> | |
| <div class="text-3xl font-bold mb-4 bg-clip-text text-transparent bg-gradient-to-r from-[#454A75] to-[#8A92B2]"> | |
| {formatUsd(totalEthUsd)} | |
| </div> | |
| <div class="text-sm"> | |
| <a href={`https://etherscan.io/address/${walletBalances[0]?.ethereum.wallet}`} | |
| target="_blank" | |
| class="text-purple-400 hover:underline"> | |
| Verify Prize Pool on Etherscan ↗ | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Search Section with ID Instructions --> | |
| <div class={`mb-8 p-6 rounded-lg shadow-lg ${darkMode ? 'bg-gray-800' : 'bg-white'}`}> | |
| <div class="max-w-xl mx-auto"> | |
| <div class="relative"> | |
| <input | |
| bind:this={searchInputRef} | |
| type="text" | |
| bind:value={searchTerm} | |
| on:keydown={(e) => e.key === 'Enter' && searchUser()} | |
| class={`w-full px-4 py-3 rounded-lg border ${ | |
| darkMode | |
| ? 'bg-gray-700 border-gray-600 text-white placeholder-gray-400' | |
| : 'bg-white border-gray-300 text-gray-900 placeholder-gray-500' | |
| } focus:outline-none focus:ring-2 focus:ring-indigo-500`} | |
| placeholder="Enter your User ID" | |
| /> | |
| <button | |
| on:click={searchUser} | |
| disabled={loading} | |
| class="absolute right-2 top-2 bg-indigo-600 text-white px-4 py-1 rounded-md hover:bg-indigo-700 disabled:opacity-50 transition-colors" | |
| > | |
| {loading ? 'Searching...' : 'Search'} | |
| </button> | |
| </div> | |
| <div class="mt-4 flex items-center justify-center gap-4 p-3 rounded-lg bg-opacity-50 {darkMode ? 'bg-gray-700' : 'bg-gray-100'}"> | |
| <div class="text-sm font-medium">Find your User ID:</div> | |
| <div class="font-mono text-sm bg-opacity-20 bg-blue-500 px-3 py-1 rounded">Type /id in XSHOT</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Key Info & CTA --> | |
| <div class="space-y-4"> | |
| <a href="https://t.me/xshot_trading_bot" | |
| target="_blank" | |
| class="inline-block px-8 py-4 bg-gradient-to-r from-indigo-500 to-purple-600 text-white font-bold rounded-xl shadow-lg transform hover:scale-105 transition-transform"> | |
| 🚀 Start Trading with XSHOT Now | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Search Section --> | |
| <div class={`mb-8 p-6 rounded-lg shadow-lg ${darkMode ? 'bg-gray-800' : 'bg-white'}`}> | |
| <div class="max-w-xl mx-auto"> | |
| <h2 class="text-2xl font-semibold mb-4">Check Your Position</h2> | |
| <div class="space-y-4"> | |
| <div class="relative"> | |
| <input | |
| bind:this={searchInputRef} | |
| type="text" | |
| bind:value={searchTerm} | |
| on:keydown={(e) => e.key === 'Enter' && searchUser()} | |
| class={`w-full px-4 py-3 rounded-lg border ${ | |
| darkMode | |
| ? 'bg-gray-700 border-gray-600 text-white placeholder-gray-400' | |
| : 'bg-white border-gray-300 text-gray-900 placeholder-gray-500' | |
| } focus:outline-none focus:ring-2 focus:ring-indigo-500`} | |
| placeholder="Enter your User ID" | |
| /> | |
| <button | |
| on:click={searchUser} | |
| disabled={loading} | |
| class="absolute right-2 top-2 bg-indigo-600 text-white px-4 py-1 rounded-md hover:bg-indigo-700 disabled:opacity-50 transition-colors" | |
| > | |
| {loading ? 'Searching...' : 'Search'} | |
| </button> | |
| </div> | |
| {#if error} | |
| <div class="text-red-500 text-sm">{error}</div> | |
| {/if} | |
| {#if targetVolume} | |
| <div class={`mt-4 p-4 rounded-lg ${darkMode ? 'bg-gray-700' : 'bg-gray-100'}`}> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div class="p-4 rounded-lg bg-opacity-20 bg-green-500"> | |
| <div class="text-sm opacity-80">Your Current Volume</div> | |
| <div class="text-xl font-bold">{formatUsd(targetVolume.currentVolumeUsd)}</div> | |
| </div> | |
| <div class="p-4 rounded-lg bg-opacity-20 bg-purple-500"> | |
| <div class="text-sm opacity-80">Volume Needed for Top 3</div> | |
| <div class="text-xl font-bold">{formatUsd(targetVolume.volumeNeededForTop3Usd)}</div> | |
| </div> | |
| </div> | |
| </div> | |
| {/if} | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Rankings Table --> | |
| <div class={`rounded-lg shadow-lg overflow-hidden ${darkMode ? 'bg-gray-800' : 'bg-white'}`}> | |
| <div class="overflow-x-auto"> | |
| <table class="min-w-full"> | |
| <thead class={darkMode ? 'bg-gray-700' : 'bg-gray-50'}> | |
| <tr> | |
| <th class="px-6 py-4 text-left text-xs font-medium uppercase tracking-wider">Rank</th> | |
| <th class="px-6 py-4 text-left text-xs font-medium uppercase tracking-wider">User ID</th> | |
| <th class="px-6 py-4 text-left text-xs font-medium uppercase tracking-wider">Volume (USD)</th> | |
| </tr> | |
| </thead> | |
| <tbody class="divide-y divide-gray-700"> | |
| {#each rankings as { rank, userId, volumeUsd, isTop3 }} | |
| <tr class={` | |
| ${darkMode ? 'hover:bg-gray-700' : 'hover:bg-gray-50'} | |
| transition-colors | |
| ${isTop3 ? (darkMode ? 'bg-gray-700/50' : 'bg-gray-50/50') : ''} | |
| `}> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| {#if rank <= 3} | |
| <span class="text-xl">{rank === 1 ? '🥇' : rank === 2 ? '🥈' : '🥉'}</span> | |
| {:else} | |
| {rank} | |
| {/if} | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap font-medium"> | |
| {userId} | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap">{formatUsd(volumeUsd)}</td> | |
| </tr> | |
| {/each} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <style> | |
| ::-webkit-scrollbar { | |
| width: 10px; | |
| height: 10px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #4f46e5; | |
| border-radius: 5px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #4338ca; | |
| } | |
| </style> | |
| ========== ./routes/api/lottery/stats/+server.js ========== | |
| import { json } from '@sveltejs/kit'; | |
| import { tradeDb } from '$lib/db'; | |
| export async function GET() { | |
| try { | |
| const stats = await tradeDb` | |
| SELECT | |
| SUM(volume_usd) as volume_usd, | |
| SUM(total_trades) as trade_count, | |
| SUM(total_tickets) as ticket_count | |
| FROM lottery_stats | |
| `; | |
| return json({ | |
| volumeUsd: parseFloat(stats[0]?.volume_usd || 0), | |
| tradeCount: parseInt(stats[0]?.trade_count || 0), | |
| ticketCount: parseInt(stats[0]?.ticket_count || 0) | |
| }); | |
| } catch (err) { | |
| console.error('Error fetching lottery stats:', err); | |
| return json({ error: 'Failed to fetch lottery stats' }, { status: 500 }); | |
| } | |
| } | |
| ========== ./routes/api/lottery/tickets/+server.js ========== | |
| import { json } from '@sveltejs/kit'; | |
| import { tradeDb } from '$lib/db'; | |
| export async function GET({ params }) { | |
| try { | |
| const stats = await tradeDb` | |
| SELECT | |
| * | |
| FROM lottery_stats | |
| WHERE user_id = ${params.userId} | |
| `; | |
| return json({ | |
| tickets: Math.floor((stats[0]?.volume_usd || 0) / 100), | |
| revealed: stats[0]?.revealed_tickets || 0, | |
| total: stats[0]?.total_tickets || 0 | |
| }); | |
| } catch (err) { | |
| console.error('Error fetching tickets:', err); | |
| return json({ error: 'Failed to fetch tickets' }, { status: 500 }); | |
| } | |
| } | |
| ========== ./routes/api/lottery/[userId]/+server.js ========== | |
| // routes/api/lottery/[userId]/+server.js | |
| import { json } from '@sveltejs/kit'; | |
| import { userDb, tradeDb } from '$lib/db'; | |
| export async function GET({ params }) { | |
| try { | |
| const userId = parseInt(params.userId); | |
| // Get user's wallets from core_db | |
| const userWallets = await userDb` | |
| SELECT address | |
| FROM "Wallet" | |
| WHERE user_id = ${userId} | |
| `; | |
| const walletAddresses = userWallets.map(w => w.address); | |
| const holdingsWallets = await tradeDb` | |
| SELECT id | |
| FROM "Wallet" | |
| WHERE address = ANY(${walletAddresses}) | |
| `; | |
| const walletIds = holdingsWallets.map(w => w.id); | |
| // Get trading volume and revealed tickets | |
| const [volumeData, revealedTickets] = await Promise.all([ | |
| tradeDb` | |
| SELECT SUM(value_total_usd) as volume | |
| FROM "Trade" | |
| WHERE "walletId" = ANY(${walletIds}) | |
| AND executed_at >= '2025-02-01' | |
| AND executed_at < '2025-03-01' | |
| `, | |
| tradeDb` | |
| SELECT ticket_hash, created_at | |
| FROM "LotteryTickets" | |
| WHERE user_id = ${userId} | |
| ORDER BY created_at DESC | |
| ` | |
| ]); | |
| const volume = volumeData[0]?.volume || 0; | |
| const totalTickets = Math.floor(volume / 100); | |
| const remainingTickets = totalTickets - revealedTickets.length; | |
| return json({ | |
| totalTickets, | |
| remainingTickets, | |
| revealedTickets: revealedTickets.map(t => t.ticket_hash), | |
| volumeUsd: volume | |
| }); | |
| } catch (err) { | |
| console.error('Error fetching lottery data:', err); | |
| return json({ error: 'Failed to fetch lottery data' }, { status: 500 }); | |
| } | |
| } | |
| export async function POST({ params }) { | |
| try { | |
| const userId = parseInt(params.userId); | |
| // Verify user has remaining tickets | |
| const { remainingTickets } = await GET({ params }).then(r => r.json()); | |
| if (remainingTickets <= 0) { | |
| return json({ error: 'No tickets remaining' }, { status: 400 }); | |
| } | |
| // Generate ticket hash | |
| const ticketHash = Array(8).fill(0) | |
| .map(() => Math.random().toString(16).substr(2, 4)) | |
| .join(''); | |
| await tradeDb` | |
| INSERT INTO "LotteryTickets" ( | |
| user_id, | |
| ticket_hash, | |
| status, | |
| revealed_at | |
| ) VALUES ( | |
| ${userId}, | |
| ${ticketHash}, | |
| 'revealed', | |
| CURRENT_TIMESTAMP | |
| ) | |
| `; | |
| return json({ | |
| success: true, | |
| ticketHash, | |
| remainingTickets: remainingTickets - 1 | |
| }); | |
| } catch (err) { | |
| console.error('Error revealing ticket:', err); | |
| return json({ error: 'Failed to reveal ticket' }, { status: 500 }); | |
| } | |
| } | |
| ========== ./routes/api/rankings/+server.js ========== | |
| // /src/routes/api/rankings/+server.js | |
| import { json } from '@sveltejs/kit'; | |
| import { getCachedData } from '$lib/cache.js'; | |
| export async function GET() { | |
| try { | |
| // Instead of directly hitting DB, ask the cache for the data | |
| const rankings = await getCachedData(); | |
| return json(rankings); | |
| } catch (err) { | |
| console.error('Error loading rankings:', err); | |
| return json({ error: 'Failed to load rankings' }, { status: 500 }); | |
| } | |
| } | |
| ========== ./routes/api/search/[term]/+server.js ========== | |
| import { json } from '@sveltejs/kit'; | |
| import { userDb, tradeDb } from '$lib/db'; | |
| export async function GET({ params }) { | |
| try { | |
| const userId = parseInt(params.term); | |
| if (isNaN(userId)) { | |
| return json({ error: 'Invalid user ID' }, { status: 400 }); | |
| } | |
| // Example: January 1, 2025 at 00:00:00 UTC | |
| const firstDayOfMonth = new Date(2025, 0, 1); | |
| firstDayOfMonth.setUTCHours(0, 0, 0, 0); | |
| // 1) Get all of this user's wallet addresses from userDb | |
| const userWallets = await userDb` | |
| SELECT address | |
| FROM "Wallet" | |
| WHERE user_id = ${userId} | |
| `; | |
| const addresses = userWallets.map((w) => w.address); | |
| // 2) Convert those addresses to Wallet IDs in tradeDb | |
| const holdingsWallets = await tradeDb` | |
| SELECT id | |
| FROM "Wallet" | |
| WHERE address = ANY(${addresses}) | |
| `; | |
| const walletIds = holdingsWallets.map((w) => w.id); | |
| // 3) Query: | |
| // - "user_volume": sum of this user's trades | |
| // - "ranked_user_volumes": everyone’s total volume with a rank | |
| // - "third_place_volume": volume of rank=3 (if it exists) | |
| const volumeResults = await tradeDb` | |
| WITH user_volume AS ( | |
| SELECT COALESCE(SUM(value_total_usd), 0) AS volume | |
| FROM "Trade" | |
| WHERE "walletId" = ANY(${walletIds}) | |
| AND executed_at >= ${firstDayOfMonth} | |
| ), | |
| ranked_user_volumes AS ( | |
| SELECT | |
| w.address, | |
| SUM(t.value_total_usd) AS volume, | |
| RANK() OVER (ORDER BY SUM(t.value_total_usd) DESC) AS rank | |
| FROM "Trade" t | |
| JOIN "Wallet" w ON t."walletId" = w.id | |
| WHERE t.executed_at >= ${firstDayOfMonth} | |
| GROUP BY w.address | |
| ), | |
| third_place_volume AS ( | |
| SELECT volume | |
| FROM ranked_user_volumes | |
| WHERE rank = 3 | |
| LIMIT 1 | |
| ) | |
| SELECT | |
| (SELECT volume FROM user_volume) AS user_volume, | |
| COALESCE((SELECT volume FROM third_place_volume), 0) AS third_place_volume; | |
| `; | |
| // 4) Calculate how much more volume is needed for the user to get from their current volume to 3rd place | |
| const userVolume = parseFloat(volumeResults[0]?.user_volume ?? 0); | |
| const thirdPlaceVolume = parseFloat(volumeResults[0]?.third_place_volume ?? 0); | |
| const volumeNeeded = Math.max(0, thirdPlaceVolume - userVolume); | |
| return json({ | |
| currentVolumeUsd: userVolume, | |
| currentTop3ThresholdUsd: thirdPlaceVolume, | |
| volumeNeededForTop3Usd: volumeNeeded | |
| }); | |
| } catch (err) { | |
| console.error('Error searching user:', err); | |
| return json({ error: 'Failed to search user' }, { status: 500 }); | |
| } | |
| } | |
| ========== ./routes/api/target/[userId]/+server.js ========== | |
| import { json } from '@sveltejs/kit'; | |
| import { userDb, tradeDb } from '$lib/db'; | |
| export async function GET({ params }) { | |
| try { | |
| const userId = parseInt(params.userId); | |
| const firstDayOfMonth = new Date(); | |
| firstDayOfMonth.setDate(1); | |
| firstDayOfMonth.setHours(0, 0, 0, 0); | |
| // First get user's wallet addresses from core db | |
| const userWallets = await userDb` | |
| SELECT address | |
| FROM "Wallet" | |
| WHERE user_id = ${userId} | |
| `; | |
| const addresses = userWallets.map(w => w.address); | |
| if (addresses.length === 0) { | |
| return json({ | |
| currentVolume: 0, | |
| currentVolumeUsd: 0, | |
| volumeNeededForTop3: 0, | |
| volumeNeededForTop3Usd: 0, | |
| currentTop3Threshold: 0, | |
| currentTop3ThresholdUsd: 0 | |
| }); | |
| } | |
| // Get holdings wallet IDs for these addresses | |
| const holdingsWallets = await tradeDb` | |
| SELECT id | |
| FROM "Wallet" | |
| WHERE address = ANY(${addresses}) | |
| `; | |
| const walletIds = holdingsWallets.map(w => w.id); | |
| // Get top 3 volumes (need to do the same address matching) | |
| const top3Result = await tradeDb` | |
| WITH all_volumes AS ( | |
| SELECT | |
| hw.address, | |
| SUM(COALESCE(t.amount_in, 0) + COALESCE(t.amount_out, 0)) as total_volume, | |
| SUM(COALESCE(t.value_total_usd, 0)) as total_volume_usd | |
| FROM "Trade" t | |
| JOIN "Wallet" hw ON t."walletId" = hw.id | |
| WHERE t.executed_at >= ${firstDayOfMonth} | |
| GROUP BY hw.address | |
| ), | |
| user_volumes AS ( | |
| SELECT | |
| cw.user_id, | |
| SUM(av.total_volume) as total_volume, | |
| SUM(av.total_volume_usd) as total_volume_usd | |
| FROM all_volumes av | |
| JOIN ${userDb.raw(`xshot_core_prod.public."Wallet"`)} cw ON cw.address = av.address | |
| GROUP BY cw.user_id | |
| ORDER BY total_volume_usd DESC | |
| LIMIT 3 | |
| ) | |
| SELECT total_volume, total_volume_usd | |
| FROM user_volumes | |
| ORDER BY total_volume_usd DESC | |
| `; | |
| // Get user's current volume | |
| const userVolumeResult = await tradeDb` | |
| SELECT | |
| SUM(COALESCE(t.amount_in, 0) + COALESCE(t.amount_out, 0)) as total_volume, | |
| SUM(COALESCE(t.value_total_usd, 0)) as total_volume_usd | |
| FROM "Trade" t | |
| WHERE t.executed_at >= ${firstDayOfMonth} | |
| AND t."walletId" = ANY(${walletIds}) | |
| `; | |
| const currentVolume = userVolumeResult[0]?.total_volume || 0; | |
| const currentVolumeUsd = userVolumeResult[0]?.total_volume_usd || 0; | |
| const lowestTop3Volume = top3Result[2]?.total_volume || 0; | |
| const lowestTop3VolumeUsd = top3Result[2]?.total_volume_usd || 0; | |
| const volumeNeeded = Math.max(0, lowestTop3Volume - currentVolume); | |
| const volumeNeededUsd = Math.max(0, lowestTop3VolumeUsd - currentVolumeUsd); | |
| return json({ | |
| currentVolume: parseFloat(currentVolume), | |
| currentVolumeUsd: parseFloat(currentVolumeUsd), | |
| volumeNeededForTop3: parseFloat(volumeNeeded), | |
| volumeNeededForTop3Usd: parseFloat(volumeNeededUsd), | |
| currentTop3Threshold: parseFloat(lowestTop3Volume), | |
| currentTop3ThresholdUsd: parseFloat(lowestTop3VolumeUsd) | |
| }); | |
| } catch (err) { | |
| console.error('Error calculating target volume:', err); | |
| return json({ error: 'Failed to calculate target volume' }, { status: 500 }); | |
| } | |
| } | |
| ========== ./s.sh ========== | |
| #!/bin/bash | |
| # Exit on any error | |
| set -e | |
| # Color codes for output | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| BLUE='\033[0;34m' | |
| NC='\033[0m' # No Color | |
| # Helper functions | |
| print_step() { | |
| echo -e "${BLUE}==== $1 ====${NC}" | |
| } | |
| print_success() { | |
| echo -e "${GREEN}✓ $1${NC}" | |
| } | |
| print_error() { | |
| echo -e "${RED}✗ $1${NC}" | |
| } | |
| # Check if we're in the right directory (should contain routes and lib folders) | |
| if [ ! -d "routes" ] || [ ! -d "lib" ]; then | |
| print_error "Please run this script from the root of your SvelteKit project" | |
| exit 1 | |
| fi | |
| # Create backup of existing files | |
| print_step "Creating backups" | |
| timestamp=$(date +%Y%m%d_%H%M%S) | |
| backup_dir="backup_$timestamp" | |
| mkdir -p "$backup_dir" | |
| if [ -f "routes/+page.svelte" ]; then | |
| cp "routes/+page.svelte" "$backup_dir/" | |
| print_success "Backed up +page.svelte" | |
| fi | |
| # Create new API endpoint directory | |
| print_step "Creating lottery API endpoint" | |
| mkdir -p "routes/api/lottery/[userId]" | |
| # Create the API endpoint file | |
| cat > "routes/api/lottery/[userId]/+server.js" << 'EOL' | |
| import { json } from '@sveltejs/kit'; | |
| import { userDb, tradeDb } from '$lib/db'; | |
| export async function GET({ params }) { | |
| try { | |
| const userId = parseInt(params.userId); | |
| // Start date: February 1st, 2025 | |
| const startDate = new Date(2025, 1, 1); | |
| startDate.setUTCHours(0, 0, 0, 0); | |
| // Get user's wallet addresses | |
| const userWallets = await userDb` | |
| SELECT address | |
| FROM "Wallet" | |
| WHERE user_id = ${userId} | |
| `; | |
| const addresses = userWallets.map(w => w.address); | |
| // Get holdings wallet IDs | |
| const holdingsWallets = await tradeDb` | |
| SELECT id | |
| FROM "Wallet" | |
| WHERE address = ANY(${addresses}) | |
| `; | |
| const walletIds = holdingsWallets.map(w => w.id); | |
| // Get trading statistics | |
| const tradeStats = await tradeDb` | |
| WITH user_trades AS ( | |
| SELECT | |
| COUNT(*) as total_trades, | |
| SUM(COALESCE(value_total_usd, 0)) as total_volume_usd | |
| FROM "Trade" | |
| WHERE "walletId" = ANY(${walletIds}) | |
| AND executed_at >= ${startDate} | |
| ), | |
| all_trades AS ( | |
| SELECT | |
| SUM(COALESCE(value_total_usd, 0)) as total_pool_volume_usd, | |
| COUNT(*) as total_pool_trades | |
| FROM "Trade" | |
| WHERE executed_at >= ${startDate} | |
| ) | |
| SELECT | |
| ut.*, | |
| at.total_pool_volume_usd, | |
| at.total_pool_trades | |
| FROM user_trades ut, all_trades at | |
| `; | |
| const stats = tradeStats[0]; | |
| // Calculate tickets (1 ticket per $100 traded) | |
| const tickets = Math.floor((stats?.total_volume_usd || 0) / 100); | |
| const totalPoolTickets = Math.floor((stats?.total_pool_volume_usd || 0) / 100); | |
| // Calculate win probability | |
| const winProbability = tickets / (totalPoolTickets || 1); | |
| return json({ | |
| tickets, | |
| totalPoolTickets, | |
| tradeCount: stats?.total_trades || 0, | |
| volumeUsd: stats?.total_volume_usd || 0, | |
| winProbability, | |
| poolVolumeUsd: stats?.total_pool_volume_usd || 0, | |
| poolTradeCount: stats?.total_pool_trades || 0 | |
| }); | |
| } catch (err) { | |
| console.error('Error fetching lottery data:', err); | |
| return json({ error: 'Failed to fetch lottery data' }, { status: 500 }); | |
| } | |
| } | |
| EOL | |
| print_success "Created lottery API endpoint" | |
| # Create LotteryModal component | |
| print_step "Creating LotteryModal component" | |
| mkdir -p "src/lib/components" | |
| cat > "src/lib/components/LotteryModal.svelte" << 'EOL' | |
| <script> | |
| import { onMount } from 'svelte'; | |
| import { Ticket, Star, ArrowRight, Trophy, Coins } from 'lucide-svelte'; | |
| export let userId; | |
| export let isOpen = false; | |
| export let onClose; | |
| export let darkMode = true; | |
| let data = null; | |
| let loading = true; | |
| $: if (isOpen && userId) { | |
| fetchLotteryData(); | |
| } | |
| async function fetchLotteryData() { | |
| try { | |
| const response = await fetch(`/api/lottery/${userId}`); | |
| data = await response.json(); | |
| } catch (error) { | |
| console.error('Error fetching lottery data:', error); | |
| } finally { | |
| loading = false; | |
| } | |
| } | |
| function formatNumber(num) { | |
| return new Intl.NumberFormat('en-US', { | |
| maximumFractionDigits: 2, | |
| }).format(num); | |
| } | |
| function formatUsd(num) { | |
| return new Intl.NumberFormat('en-US', { | |
| style: 'currency', | |
| currency: 'USD', | |
| notation: 'compact', | |
| maximumFractionDigits: 2 | |
| }).format(num); | |
| } | |
| function formatPercent(num) { | |
| return (num * 100).toFixed(4) + '%'; | |
| } | |
| </script> | |
| {#if isOpen} | |
| <div class="fixed inset-0 z-50 flex items-center justify-center p-4"> | |
| <!-- Backdrop --> | |
| <div | |
| class="absolute inset-0 bg-black/60 backdrop-blur-sm" | |
| on:click={onClose} | |
| /> | |
| <!-- Modal --> | |
| <div class="relative w-full max-w-2xl rounded-2xl shadow-xl p-6 { | |
| darkMode ? 'bg-gray-800' : 'bg-white' | |
| }"> | |
| <!-- Close button --> | |
| <button | |
| on:click={onClose} | |
| class="absolute top-4 right-4 text-gray-400 hover:text-gray-200 transition-colors" | |
| > | |
| ✕ | |
| </button> | |
| <!-- Title --> | |
| <div class="text-center mb-8"> | |
| <h2 class="text-2xl font-bold mb-2 flex items-center justify-center gap-3"> | |
| <Star class="w-6 h-6 text-yellow-500" /> | |
| February Lottery | |
| <Star class="w-6 h-6 text-yellow-500" /> | |
| </h2> | |
| <p class="text-gray-400">One lucky trader will win 50% of all February fees!</p> | |
| </div> | |
| {#if loading} | |
| <div class="text-center py-8">Loading...</div> | |
| {:else if data} | |
| <!-- Tickets Section --> | |
| <div class="mb-8"> | |
| <div class="p-6 rounded-xl {darkMode ? 'bg-gray-700/50' : 'bg-gray-100'}"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <div class="flex items-center gap-3"> | |
| <Ticket class="w-6 h-6 text-indigo-400" /> | |
| <h3 class="text-xl font-semibold">Your Tickets</h3> | |
| </div> | |
| <div class="text-2xl font-bold text-indigo-400"> | |
| {formatNumber(data.tickets)} | |
| </div> | |
| </div> | |
| <div class="text-sm text-gray-400"> | |
| 1 ticket per $100 traded in February | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Stats Grid --> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8"> | |
| <div class="p-4 rounded-xl {darkMode ? 'bg-gray-700/30' : 'bg-gray-50'}"> | |
| <div class="flex items-center gap-2 mb-2"> | |
| <ArrowRight class="w-5 h-5 text-green-400" /> | |
| <div class="text-sm text-gray-400">Your Trades</div> | |
| </div> | |
| <div class="text-xl font-bold">{formatNumber(data.tradeCount)}</div> | |
| </div> | |
| <div class="p-4 rounded-xl {darkMode ? 'bg-gray-700/30' : 'bg-gray-50'}"> | |
| <div class="flex items-center gap-2 mb-2"> | |
| <Coins class="w-5 h-5 text-blue-400" /> | |
| <div class="text-sm text-gray-400">Your Volume</div> | |
| </div> | |
| <div class="text-xl font-bold">{formatUsd(data.volumeUsd)}</div> | |
| </div> | |
| </div> | |
| <!-- Win Chance Section --> | |
| <div class="p-6 rounded-xl mb-6 {darkMode ? 'bg-gray-700/50' : 'bg-gray-100'}"> | |
| <div class="flex items-center gap-3 mb-4"> | |
| <Trophy class="w-6 h-6 text-yellow-500" /> | |
| <h3 class="text-xl font-semibold">Win Probability</h3> | |
| </div> | |
| <div class="text-3xl font-bold text-indigo-400 mb-2"> | |
| {formatPercent(data.winProbability)} | |
| </div> | |
| <div class="text-sm text-gray-400"> | |
| Based on your {formatNumber(data.tickets)} tickets out of {formatNumber(data.totalPoolTickets)} total tickets | |
| </div> | |
| </div> | |
| <!-- Pool Stats --> | |
| <div class="text-center text-sm text-gray-400"> | |
| Total Pool Volume: {formatUsd(data.poolVolumeUsd)} • Total Pool Trades: {formatNumber(data.poolTradeCount)} | |
| </div> | |
| {:else} | |
| <div class="text-center py-8 text-red-400">Failed to load lottery data</div> | |
| {/if} | |
| </div> | |
| </div> | |
| {/if} | |
| <style> | |
| .backdrop-blur-sm { | |
| backdrop-filter: blur(8px); | |
| } | |
| </style> | |
| EOL | |
| print_success "Created LotteryModal component" | |
| # Update +page.svelte | |
| print_step "Updating main page" | |
| # First, let's add the imports and lottery section to the existing file | |
| awk ' | |
| /import \{ onMount \} from '"'"'svelte'"'"';/ { | |
| print; | |
| print " import LotteryModal from '"'"'$lib/components/LotteryModal.svelte'"'"';"; | |
| print " import { Ticket } from '"'"'lucide-svelte'"'"';"; | |
| next; | |
| } | |
| /let darkMode = true;/ { | |
| print; | |
| print " let showLotteryModal = false;"; | |
| next; | |
| } | |
| /<!-- Prize Distribution -->/ { | |
| print; | |
| print " <!-- Lottery Section -->"; | |
| print " <div class=\"max-w-2xl mx-auto mb-8\">"; | |
| print " <div class={`p-6 rounded-xl ${darkMode ? '"'"'bg-purple-900/30 backdrop-blur-sm'"'"' : '"'"'bg-purple-50'"'"'}`}>"; | |
| print " <div class=\"text-center space-y-4\">"; | |
| print " <div class=\"flex items-center justify-center gap-2\">"; | |
| print " <Ticket class=\"w-6 h-6 text-purple-400\" />"; | |
| print " <h3 class=\"text-xl font-bold bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent\">"; | |
| print " February Lottery Event"; | |
| print " </h3>"; | |
| print " </div>"; | |
| print " "; | |
| print " <p class=\"text-lg\">"; | |
| print " Get 1 lottery ticket for every $100 traded!"; | |
| print " </p>"; | |
| print " "; | |
| print " <div class=\"text-sm text-gray-400\">"; | |
| print " One lucky winner will receive 50% of all February trading fees"; | |
| print " </div>"; | |
| print ""; | |
| print " {#if searchTerm}"; | |
| print " <button"; | |
| print " on:click={() => showLotteryModal = true}"; | |
| print " class=\"px-6 py-3 bg-purple-600 text-white font-semibold rounded-xl "; | |
| print " shadow-lg transform hover:scale-105 transition-all duration-300\""; | |
| print " >"; | |
| print " View My Tickets"; | |
| print " </button>"; | |
| print " {:else}"; | |
| print " <div class=\"text-sm text-purple-400\">"; | |
| print " Search your User ID above to view your tickets"; | |
| print " </div>"; | |
| print " {/if}"; | |
| print " </div>"; | |
| print " </div>"; | |
| print " </div>"; | |
| } | |
| /<\/div>$/ { | |
| if (!added_modal) { | |
| print ""; | |
| print " {#if showLotteryModal}"; | |
| print " <LotteryModal"; | |
| print " userId={searchTerm}"; | |
| print " isOpen={showLotteryModal}"; | |
| print " onClose={() => showLotteryModal = false}"; | |
| print " {darkMode}"; | |
| print " />"; | |
| print " {/if}"; | |
| added_modal = 1; | |
| } | |
| } | |
| /<style>/ { | |
| print; | |
| print " .backdrop-blur-sm {"; | |
| print " backdrop-filter: blur(8px);"; | |
| print " }"; | |
| next; | |
| } | |
| { print } | |
| ' routes/+page.svelte > routes/+page.svelte.new | |
| mv routes/+page.svelte.new routes/+page.svelte | |
| print_success "Updated main page" | |
| print_step "Installing required packages" | |
| npm install lucide-svelte --save | |
| print_success "All updates completed successfully!" | |
| echo -e "${GREEN}Backup of original files can be found in: $backup_dir${NC}" | |
| echo -e "${BLUE}Please review the changes and run your application to test the new features${NC}" | |
| ========== ./codebase ========== | |
| ========== ./holdings.sql ========== | |
| ========== ./lib/cache.js ========== | |
| // src/lib/cache.js | |
| import { tradeDb, userDb } from '$lib/db'; | |
| // We'll store the "rankings" data in a global variable here. | |
| let cachedData = null; | |
| // Store timestamp of last fetch | |
| let lastFetchTimestamp = 0; | |
| // Set the cache TTL (time to live) in ms; e.g., 10 minutes | |
| const CACHE_TTL = 10 * 60 * 1000; | |
| /** | |
| * Actually queries the DB for fresh rankings data. | |
| */ | |
| async function fetchFreshData() { | |
| // This is the same logic from your /api/rankings/+server.js | |
| // except we return the result here directly. | |
| // Example: January 1st, 2025 at 00:00 UTC | |
| const firstDayOfMonth = new Date(2025, 0, 1); | |
| firstDayOfMonth.setUTCHours(0, 0, 0, 0); | |
| // Get all trades from tradeDb | |
| const tradeVolumes = await tradeDb` | |
| WITH monthly_trades AS ( | |
| SELECT | |
| t."walletId", | |
| hw.address, | |
| SUM(COALESCE(t.value_total_usd, 0)) as total_volume_usd | |
| FROM "Trade" t | |
| JOIN "Wallet" hw ON t."walletId" = hw.id | |
| WHERE t.executed_at >= ${firstDayOfMonth} | |
| GROUP BY t."walletId", hw.address | |
| ) | |
| SELECT * FROM monthly_trades | |
| ORDER BY total_volume_usd DESC | |
| `; | |
| // Build a map of addresses -> user IDs | |
| const addresses = tradeVolumes.map(tv => tv.address); | |
| const userWallets = await userDb` | |
| SELECT | |
| w.address, | |
| w.user_id, | |
| u.id as user_id | |
| FROM "Wallet" w | |
| JOIN "User" u ON u.id = w.user_id | |
| WHERE w.address = ANY(${addresses}) | |
| `; | |
| const addressMap = new Map( | |
| userWallets.map((w) => [ | |
| w.address.toLowerCase(), | |
| { userId: w.user_id } | |
| ]) | |
| ); | |
| // Accumulate volumes by userId | |
| const userVolumes = new Map(); | |
| tradeVolumes.forEach((trade) => { | |
| const userInfo = addressMap.get(trade.address.toLowerCase()); | |
| if (userInfo) { | |
| const userId = userInfo.userId; | |
| if (!userVolumes.has(userId)) { | |
| userVolumes.set(userId, { | |
| userId, | |
| volumeUsd: 0 | |
| }); | |
| } | |
| const userVol = userVolumes.get(userId); | |
| userVol.volumeUsd += parseFloat(trade.total_volume_usd); | |
| } | |
| }); | |
| // Convert map -> array, sort, label rank, mark isTop3 | |
| const rankings = Array.from(userVolumes.values()) | |
| .sort((a, b) => b.volumeUsd - a.volumeUsd) | |
| .map((user, index) => ({ | |
| rank: index + 1, | |
| userId: user.userId, | |
| volumeUsd: user.volumeUsd, | |
| isTop3: index < 3 | |
| })); | |
| return rankings; | |
| } | |
| /** | |
| * Checks if we have valid cached data; if not, fetch fresh from DB. | |
| */ | |
| export async function getCachedData() { | |
| const now = Date.now(); | |
| // If we've never fetched or it's older than CACHE_TTL... | |
| if (!cachedData || now - lastFetchTimestamp > CACHE_TTL) { | |
| console.log('[Cache] Rankings cache is stale, fetching fresh data...'); | |
| cachedData = await fetchFreshData(); | |
| lastFetchTimestamp = now; | |
| } | |
| // Return the cached data (fresh or stale-if not TTL expired) | |
| return cachedData; | |
| } | |
| ========== ./lib/components/LotteryModal.svelte ========== | |
| # lib/components/LotteryModal.svelte | |
| <script> | |
| import { onMount } from 'svelte'; | |
| import { fade, scale } from 'svelte/transition'; | |
| import { Ticket, Star, Trophy } from 'lucide-svelte'; | |
| import confetti from 'canvas-confetti'; | |
| import LotteryTicket from './LotteryTicket.svelte'; | |
| export let userId; | |
| export let isOpen = false; | |
| export let onClose; | |
| let loading = true; | |
| let error = null; | |
| let data = null; | |
| let revealingAll = false; | |
| let revealQueue = []; | |
| // Calculate time until drawing | |
| const endDate = new Date(2025, 1, 28, 23, 59, 59); | |
| $: timeLeft = endDate - new Date(); | |
| $: daysLeft = Math.ceil(timeLeft / (1000 * 60 * 60 * 24)); | |
| async function loadTickets() { | |
| try { | |
| loading = true; | |
| error = null; | |
| const response = await fetch(`/api/lottery/${userId}`); | |
| if (!response.ok) throw new Error('Failed to fetch tickets'); | |
| data = await response.json(); | |
| } catch (err) { | |
| error = err.message; | |
| } finally { | |
| loading = false; | |
| } | |
| } | |
| async function revealTicket() { | |
| try { | |
| const response = await fetch(`/api/lottery/${userId}`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' } | |
| }); | |
| if (!response.ok) throw new Error('Failed to reveal ticket'); | |
| const result = await response.json(); | |
| confetti({ | |
| particleCount: 50, | |
| spread: 45, | |
| origin: { y: 0.6 } | |
| }); | |
| return result.ticketHash; | |
| } catch (err) { | |
| error = err.message; | |
| return null; | |
| } | |
| } | |
| function sleep(ms) { | |
| return new Promise(resolve => setTimeout(resolve, ms)); | |
| } | |
| async function scratchAll() { | |
| if (revealingAll || !data?.remainingTickets) return; | |
| revealingAll = true; | |
| revealQueue = []; | |
| for (let i = 0; i < data.remainingTickets; i++) { | |
| // revealTicket returns the ticketHash | |
| const hash = await revealTicket(); | |
| if (hash) { | |
| // push it immediately so user sees each reveal in real time | |
| revealQueue = [...revealQueue, hash]; | |
| // wait a bit (e.g. 1 second) before next reveal | |
| await sleep(1000); | |
| } | |
| } | |
| // Finally re-fetch | |
| await loadTickets(); | |
| revealingAll = false; | |
| } | |
| </script> | |
| <div class="fixed inset-0 z-50 flex items-center justify-center p-4" | |
| transition:fade> | |
| <button | |
| class="absolute inset-0 bg-black bg-opacity-60 backdrop-filter backdrop-blur-sm" | |
| on:click={onClose} | |
| on:keydown={(e) => e.key === 'Escape' && onClose()} | |
| aria-label="Close modal" | |
| ></button> | |
| <div class="relative w-full max-w-4xl bg-gray-800 rounded-2xl shadow-2xl p-6" | |
| transition:scale> | |
| <!-- Close button --> | |
| <button | |
| class="absolute top-4 right-4 text-gray-400 hover:text-gray-200 transition-colors" | |
| on:click={onClose} | |
| aria-label="Close modal" | |
| > | |
| ✕ | |
| </button> | |
| <!-- Header --> | |
| <div class="text-center mb-8"> | |
| <h2 class="text-3xl font-bold flex items-center justify-center gap-3 mb-2"> | |
| <Star class="w-8 h-8 text-yellow-500" /> | |
| <span class="bg-gradient-to-r from-yellow-400 to-purple-400 bg-clip-text text-transparent"> | |
| MEGA LOTTERY EVENT | |
| </span> | |
| <Star class="w-8 h-8 text-yellow-500" /> | |
| </h2> | |
| <!-- Countdown --> | |
| <div class="text-xl text-pink-400 font-bold animate-pulse mb-4"> | |
| {daysLeft} Days Until The Big Draw! 🎉 | |
| </div> | |
| <p class="text-gray-300"> | |
| Trade More, Win More! One Lucky Winner Takes The Jackpot! 💰 | |
| </p> | |
| </div> | |
| {#if loading} | |
| <div class="flex justify-center items-center py-12"> | |
| <div class="animate-spin text-indigo-500 text-4xl">🎰</div> | |
| </div> | |
| {:else if error} | |
| <div class="text-red-400 text-center py-8"> | |
| {error} | |
| <button | |
| class="mt-4 px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors" | |
| on:click={loadTickets} | |
| > | |
| Try Again | |
| </button> | |
| </div> | |
| {:else} | |
| <!-- Stats --> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8"> | |
| <!-- Total Tickets --> | |
| <div class="bg-gradient-to-br from-indigo-900/50 to-purple-900/50 rounded-xl p-4 text-center transform hover:scale-105 transition-all duration-300"> | |
| <div class="text-sm text-gray-400">Total Tickets</div> | |
| <div class="text-2xl font-bold text-indigo-400"> | |
| {data.totalTickets} | |
| </div> | |
| <div class="text-xs text-gray-500 mt-1">Keep trading to earn more!</div> | |
| </div> | |
| <!-- Tickets to Reveal --> | |
| <div class="bg-gradient-to-br from-green-900/50 to-emerald-900/50 rounded-xl p-4 text-center transform hover:scale-105 transition-all duration-300"> | |
| <div class="text-sm text-gray-400">Left to Reveal</div> | |
| <div class="text-2xl font-bold text-green-400"> | |
| {data.remainingTickets} | |
| </div> | |
| <div class="text-xs text-gray-500 mt-1">Click to scratch!</div> | |
| </div> | |
| <!-- Trading Volume --> | |
| <div class="bg-gradient-to-br from-yellow-900/50 to-orange-900/50 rounded-xl p-4 text-center transform hover:scale-105 transition-all duration-300"> | |
| <div class="text-sm text-gray-400">Volume Traded</div> | |
| <div class="text-2xl font-bold text-yellow-400"> | |
| ${Math.floor(data.volumeUsd).toLocaleString()} | |
| </div> | |
| <div class="text-xs text-gray-500 mt-1">$100 = 1 ticket</div> | |
| </div> | |
| </div> | |
| <!-- Scratch All Button --> | |
| {#if data.remainingTickets > 0} | |
| <div class="text-center mb-8"> | |
| <button | |
| class="px-6 py-3 bg-gradient-to-r from-indigo-600 to-purple-600 | |
| hover:from-indigo-500 hover:to-purple-500 text-white rounded-xl | |
| font-bold transform hover:scale-105 transition-all duration-300 | |
| shadow-lg shadow-indigo-900/50 | |
| disabled:opacity-50 disabled:cursor-not-allowed" | |
| on:click={scratchAll} | |
| disabled={revealingAll} | |
| > | |
| {#if revealingAll} | |
| <div class="flex items-center gap-2 justify-center"> | |
| <div class="animate-spin text-xl">🎰</div> | |
| Revealing {revealQueue.length}/{data.remainingTickets}... | |
| </div> | |
| {:else} | |
| <div class="flex items-center gap-2 justify-center"> | |
| 🎰 Scratch All {data.remainingTickets} Tickets! 🎰 | |
| </div> | |
| {/if} | |
| </button> | |
| </div> | |
| {/if} | |
| <!-- Tickets Grid --> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 max-h-[60vh] overflow-y-auto p-4"> | |
| {#each [...data.revealedTickets, ...revealQueue] as hash} | |
| <LotteryTicket revealed={true} {hash} /> | |
| {/each} | |
| {#each Array(data.remainingTickets - revealQueue.length) as _} | |
| <LotteryTicket revealed={false} onReveal={revealTicket} /> | |
| {/each} | |
| </div> | |
| <!-- Footer Stats --> | |
| <div class="mt-8 pt-4 border-t border-gray-700/50 text-center text-sm text-gray-400"> | |
| <p>Trade more to increase your chances! Every $100 = 1 new ticket</p> | |
| {#if data.totalTickets > 0} | |
| <p class="mt-2"> | |
| Your current win chance: | |
| <span class="text-yellow-400 font-bold"> | |
| {((data.totalTickets / (data.totalTickets + 1000)) * 100).toFixed(4)}% | |
| </span> | |
| </p> | |
| {/if} | |
| </div> | |
| {/if} | |
| </div> | |
| </div> | |
| <style> | |
| /* Custom scrollbar */ | |
| div :global(::-webkit-scrollbar) { | |
| width: 8px; | |
| height: 8px; | |
| } | |
| div :global(::-webkit-scrollbar-track) { | |
| background: rgba(31, 41, 55, 0.5); | |
| border-radius: 4px; | |
| } | |
| div :global(::-webkit-scrollbar-thumb) { | |
| background: rgba(99, 102, 241, 0.5); | |
| border-radius: 4px; | |
| } | |
| div :global(::-webkit-scrollbar-thumb:hover) { | |
| background: rgba(99, 102, 241, 0.7); | |
| } | |
| /* Animations */ | |
| @keyframes float { | |
| 0%, 100% { transform: translateY(0px); } | |
| 50% { transform: translateY(-5px); } | |
| } | |
| .animate-float { | |
| animation: float 3s ease-in-out infinite; | |
| } | |
| /* Gradient text animation */ | |
| @keyframes gradient { | |
| 0% { background-position: 0% 50%; } | |
| 50% { background-position: 100% 50%; } | |
| 100% { background-position: 0% 50%; } | |
| } | |
| .bg-gradient-to-r { | |
| background-size: 200% auto; | |
| animation: gradient 5s linear infinite; | |
| } | |
| </style> | |
| ========== ./lib/components/LotteryTicket.svelte ========== | |
| <script> | |
| import { slide } from 'svelte/transition'; | |
| import SlotMachine from './SlotMachine.svelte'; | |
| export let revealed = false; | |
| export let hash = ''; | |
| export let onReveal = () => {}; | |
| let isSpinning = false; | |
| let showSlot = false; | |
| // If we come in with revealed=true from the parent, | |
| // immediately show the slot with final value: | |
| $: if (revealed && !showSlot) { | |
| showSlot = true; | |
| isSpinning = false; | |
| } | |
| async function handleReveal() { | |
| if (revealed || isSpinning) return; | |
| showSlot = true; | |
| isSpinning = true; | |
| const result = await onReveal(); | |
| if (result) { | |
| hash = result; | |
| // after a short timeout, mark spinning as false | |
| setTimeout(() => { | |
| isSpinning = false; | |
| }, 2000); | |
| } else { | |
| // revert if reveal failed | |
| isSpinning = false; | |
| showSlot = false; | |
| } | |
| } | |
| </script> | |
| <div | |
| class="relative bg-gradient-to-br from-purple-600/30 to-pink-600/30 | |
| p-4 rounded-xl shadow-lg transform transition-all duration-300 | |
| hover:scale-105 cursor-pointer overflow-hidden w-full text-left" | |
| on:click={handleReveal} | |
| aria-label={revealed ? 'Revealed ticket' : 'Click to reveal ticket'} | |
| disabled={revealed} | |
| > | |
| {#if showSlot} | |
| <!-- Already revealing or revealed --> | |
| <div transition:slide> | |
| <SlotMachine | |
| spinning={isSpinning} | |
| finalValue={hash} | |
| on:spinComplete={() => revealed = true} | |
| /> | |
| </div> | |
| {:else} | |
| <!-- Not revealed yet --> | |
| <div class="text-center" in:slide> | |
| <div class="text-3xl mb-2">🎟️</div> | |
| <div class="text-sm text-gray-300"> | |
| Click to Reveal! | |
| </div> | |
| </div> | |
| {/if} | |
| </div> | |
| <style> | |
| .revealed { | |
| cursor: default; | |
| } | |
| </style> | |
| ========== ./lib/components/SlotMachine.svelte ========== | |
| <!-- lib/components/SlotMachine.svelte --> | |
| <script> | |
| import { onMount, createEventDispatcher } from 'svelte'; | |
| const dispatch = createEventDispatcher(); | |
| export let spinning = false; | |
| export let finalValue = ''; | |
| export let duration = 2000; | |
| const chars = '0123456789ABCDEF'; | |
| let reels = Array(32).fill('0'); | |
| let spinInterval; | |
| let spinStart; | |
| onMount(() => { | |
| if (spinning) startSpin(); | |
| return () => clearInterval(spinInterval); | |
| }); | |
| $: if (spinning) { | |
| startSpin(); | |
| } else { | |
| stopSpin(); | |
| } | |
| function startSpin() { | |
| spinStart = Date.now(); | |
| spinInterval = setInterval(updateReels, 50); | |
| } | |
| function updateReels() { | |
| const elapsed = Date.now() - spinStart; | |
| const progress = Math.min(1, elapsed / duration); | |
| reels = reels.map((_, i) => { | |
| // Slow down based on position and progress | |
| const shouldStop = progress > (i / reels.length * 0.5); | |
| if (shouldStop && finalValue[i]) { | |
| return finalValue[i]; | |
| } | |
| return chars[Math.floor(Math.random() * chars.length)]; | |
| }); | |
| if (progress >= 1) { | |
| stopSpin(); | |
| dispatch('spinComplete'); | |
| } | |
| } | |
| function stopSpin() { | |
| clearInterval(spinInterval); | |
| reels = finalValue.split(''); | |
| } | |
| </script> | |
| <div class="font-mono text-2xl grid grid-flow-col gap-1 tracking-wider"> | |
| {#each reels as char, i} | |
| <div | |
| class="bg-gray-800 px-2 py-1 rounded-md shadow-inner transform transition-all duration-300" | |
| style="animation-delay: {i * 50}ms;" | |
| > | |
| {char} | |
| </div> | |
| {/each} | |
| </div> | |
| <style> | |
| .grid { | |
| grid-template-columns: repeat(32, 1fr); | |
| } | |
| @keyframes bounceIn { | |
| 0% { transform: scale(0.3); opacity: 0; } | |
| 50% { transform: scale(1.05); opacity: 0.8; } | |
| 70% { transform: scale(0.9); opacity: 0.9; } | |
| 100% { transform: scale(1); opacity: 1; } | |
| } | |
| div > div { | |
| animation: bounceIn 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55) backwards; | |
| } | |
| </style> | |
| ========== ./lib/db.js ========== | |
| import postgres from 'postgres'; | |
| //export const userDb = postgres('postgres://postgres:o3j0CfWk4x2R0T@x-psql-prod.flycast:5432/xshot_core_prod'); | |
| //export const tradeDb = postgres('postgres://postgres:o3j0CfWk4x2R0T@x-psql-prod.flycast:5432/holdings_db'); | |
| export const userDb = postgres('postgres://postgres:o3j0CfWk4x2R0T@localhost:5432/xshot_core_prod'); | |
| export const tradeDb = postgres('postgres://postgres:o3j0CfWk4x2R0T@localhost:5432/holdings_db'); | |
| ========== ./lib/index.js ========== | |
| // place files you want to import through the `$lib` alias in this folder. | |
| ========== ./lib/lottery-data.js ========== | |
| export let lotteryStats = { | |
| volumeUsd: 0, | |
| tradeCount: 0, | |
| ticketCount: 0, | |
| tickets: 0, | |
| totalPoolTickets: 0, | |
| winProbability: 0 | |
| }; | |
| export async function loadLotteryStats() { | |
| try { | |
| const response = await fetch('/api/lottery/stats'); | |
| if (!response.ok) throw new Error('Failed to fetch lottery stats'); | |
| const data = await response.json(); | |
| lotteryStats = { | |
| ...data, | |
| tickets: Math.floor(data.volumeUsd / 100), | |
| totalPoolTickets: Math.floor(data.volumeUsd / 100), | |
| winProbability: data.ticketCount > 0 ? data.ticketCount / Math.floor(data.volumeUsd / 100) : 0 | |
| }; | |
| return lotteryStats; | |
| } catch (err) { | |
| console.error('Error loading lottery stats:', err); | |
| return lotteryStats; | |
| } | |
| } | |
| ========== ./lib/prices.js ========== | |
| import { Connection, PublicKey } from '@solana/web3.js'; | |
| import { ethers } from 'ethers'; | |
| const SOL_RPC = 'https://hidden-small-wish.solana-mainnet.quiknode.pro/8aa3a97956500152c37afb6d81a1bcb34387d642/'; | |
| const ETH_RPC = 'https://bold-virulent-hill.quiknode.pro/6c308edd006d129e8c940757cfaac7ca5852eabe/'; | |
| // Multiple wallet support | |
| const WALLETS = [ | |
| { | |
| sol: '4fRQWKJzJYjKi9PHmpd2SXMkiQDgr6yuNADF1GL2wWZW', | |
| eth: '0xFDBa7f32A6f18cE1c97753A616DA32A67A3C93fA' | |
| } | |
| ]; | |
| async function getTokenPrices() { | |
| try { | |
| const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=solana,ethereum&vs_currencies=usd'); | |
| const data = await response.json(); | |
| return { | |
| solana: data.solana.usd, | |
| ethereum: data.ethereum.usd | |
| }; | |
| } catch (error) { | |
| console.error('Error fetching token prices:', error); | |
| return { solana: 0, ethereum: 0 }; | |
| } | |
| } | |
| async function getSolanaBalance(walletAddress) { | |
| try { | |
| const connection = new Connection(SOL_RPC); | |
| const pubKey = new PublicKey(walletAddress); | |
| // Remove the TS type assertion: | |
| const config = { | |
| commitment: 'confirmed', | |
| minContextSlot: 0 | |
| }; | |
| const balance = await connection.getBalance(pubKey, config); | |
| console.log('Solana Response:', { | |
| address: walletAddress, | |
| rawBalance: balance, | |
| convertedBalance: balance / 1e9 | |
| }); | |
| return balance / 1e9; // Convert lamports to SOL | |
| } catch (error) { | |
| console.error('Error fetching SOL balance:', error); | |
| return 0; | |
| } | |
| } | |
| async function getEthBalance(walletAddress) { | |
| try { | |
| const provider = new ethers.JsonRpcProvider(ETH_RPC); | |
| const balance = await provider.getBalance(walletAddress); | |
| return parseFloat(ethers.formatEther(balance)); | |
| } catch (error) { | |
| console.error('Error fetching ETH balance:', error); | |
| return 0; | |
| } | |
| } | |
| export async function getAllWalletBalances() { | |
| const prices = await getTokenPrices(); | |
| const walletBalances = []; | |
| for (const wallet of WALLETS) { | |
| const solBalance = await getSolanaBalance(wallet.sol); | |
| const ethBalance = await getEthBalance(wallet.eth); | |
| walletBalances.push({ | |
| solana: { | |
| wallet: wallet.sol, | |
| balance: solBalance, | |
| usdValue: solBalance * prices.solana, | |
| price: prices.solana | |
| }, | |
| ethereum: { | |
| wallet: wallet.eth, | |
| balance: ethBalance, | |
| usdValue: ethBalance * prices.ethereum, | |
| price: prices.ethereum | |
| } | |
| }); | |
| } | |
| return walletBalances; | |
| } | |
| ========== ./routes/+layout.svelte ========== | |
| <script> | |
| import "../app.css"; | |
| </script> | |
| <slot /> | |
| ========== ./routes/+page.svelte ========== | |
| <script> | |
| import { onMount } from 'svelte'; | |
| import LotteryModal from '$lib/components/LotteryModal.svelte'; | |
| import { Ticket } from 'lucide-svelte'; | |
| import { getAllWalletBalances } from '$lib/prices'; | |
| import { lotteryStats, loadLotteryStats } from "$lib/lottery-data"; | |
| let rankings = []; | |
| let searchTerm = ''; | |
| let targetVolume = null; | |
| let loading = false; | |
| let error = ''; | |
| let darkMode = true; | |
| let showLotteryModal = false; | |
| let walletBalances = []; | |
| let totalSolUsd = 0; | |
| let totalEthUsd = 0; | |
| let searchInputRef; | |
| let showTable = false; // Controls the visibility of the Rankings Table | |
| const DISTRIBUTION = { | |
| first: 0.5, | |
| second: 0.3, | |
| third: 0.2 | |
| }; | |
| let daysUntilDraw; | |
| onMount(async () => { | |
| await loadLotteryStats(); | |
| loadRankings(); | |
| loadPrizes(); | |
| const endDate = new Date(2025, 1, 28, 23, 59, 59); // Feb 28, 2025 | |
| const now = new Date(); | |
| daysUntilDraw = Math.ceil((endDate - now) / (1000 * 60 * 60 * 24)); | |
| if (window.matchMedia('(prefers-color-scheme: light)').matches) { | |
| darkMode = false; | |
| } | |
| }); | |
| async function loadRankings() { | |
| try { | |
| const response = await fetch('/api/rankings'); | |
| if (!response.ok) throw new Error('Failed to fetch rankings'); | |
| rankings = await response.json(); | |
| } catch (err) { | |
| error = 'Failed to load rankings'; | |
| } | |
| } | |
| async function loadPrizes() { | |
| try { | |
| walletBalances = await getAllWalletBalances(); | |
| // 50% of total for each chain | |
| totalSolUsd = walletBalances.reduce((sum, w) => sum + w.solana.usdValue, 0) * 0.5; | |
| totalEthUsd = walletBalances.reduce((sum, w) => sum + w.ethereum.usdValue, 0) * 0.5; | |
| } catch (err) { | |
| console.error('Error loading prizes:', err); | |
| } | |
| } | |
| async function searchUser() { | |
| if (!searchTerm) return; | |
| loading = true; | |
| error = ''; | |
| try { | |
| const response = await fetch(`/api/search/${encodeURIComponent(searchTerm)}`); | |
| if (!response.ok) throw new Error('Failed to fetch user data'); | |
| targetVolume = await response.json(); | |
| } catch (err) { | |
| error = 'User not found'; | |
| targetVolume = null; | |
| } finally { | |
| loading = false; | |
| } | |
| } | |
| function formatUsd(num) { | |
| return new Intl.NumberFormat('en-US', { | |
| style: 'currency', | |
| currency: 'USD', | |
| notation: 'compact', | |
| maximumFractionDigits: 2 | |
| }).format(num); | |
| } | |
| function toggleDarkMode() { | |
| darkMode = !darkMode; | |
| } | |
| </script> | |
| <!-- Outer Container with smooth transitions --> | |
| <div class={`min-h-screen transition-all duration-300 ${darkMode ? 'bg-gray-900 text-gray-100' : 'bg-gray-50 text-gray-900'}`}> | |
| <div class="container mx-auto px-3 sm:px-4 py-4 sm:py-6 lg:py-8 max-w-5xl relative"> | |
| <!-- Dark Mode Toggle with improved positioning --> | |
| <div class="absolute top-2 right-2 sm:top-4 sm:right-4 z-10"> | |
| <button | |
| on:click={toggleDarkMode} | |
| class="p-2 rounded-full hover:bg-opacity-20 hover:bg-gray-500 transition-all duration-300 transform hover:scale-110" | |
| > | |
| {#if darkMode} | |
| <span class="text-xl sm:text-2xl">🌞</span> | |
| {:else} | |
| <span class="text-xl sm:text-2xl">🌙</span> | |
| {/if} | |
| </button> | |
| </div> | |
| <div class={`mb-4 sm:mb-6 p-4 sm:p-6 rounded-xl sm:rounded-2xl shadow-xl | |
| backdrop-filter backdrop-blur-sm transition-all duration-300 | |
| ${darkMode ? 'bg-gray-800/90 shadow-gray-900/50' : 'bg-white/90 shadow-gray-200/50'}`}> | |
| <div class="text-center space-y-4 md:space-y-6"> | |
| <!-- Improved Title Section --> | |
| <div class="relative inline-block"> | |
| <h2 class="text-xl sm:text-2xl lg:text-4xl font-bold flex items-center justify-center gap-3 p-2"> | |
| <span class="trophy-icon text-2xl sm:text-3xl lg:text-4xl animate-gentle-pulse">🏆</span> | |
| <span class="gradient-text tracking-tight">January Trading Competition</span> | |
| <span class="trophy-icon text-2xl sm:text-3xl lg:text-4xl animate-gentle-pulse">🏆</span> | |
| </h2> | |
| </div> | |
| <!-- Enhanced Prize Pool Amount --> | |
| <div class="prize-pool transform hover:scale-105 transition-all duration-300"> | |
| <div class="text-3xl sm:text-4xl lg:text-6xl xl:text-7xl font-extrabold bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 bg-clip-text text-transparent tracking-tight"> | |
| {formatUsd(totalSolUsd + totalEthUsd)} | |
| </div> | |
| <div class="text-base sm:text-lg lg:text-xl font-medium text-indigo-400 mt-2"> | |
| Total Prize Pool to Win | |
| </div> | |
| </div> | |
| <!-- Redesigned Distribution Cards --> | |
| <div class="max-w-2xl mx-auto p-3 rounded-xl bg-gray-700/20 backdrop-filter backdrop-blur-sm transition-all duration-300"> | |
| <div class="grid grid-cols-1 sm:grid-cols-3 gap-3"> | |
| {#each [ | |
| { place: "1st Place", emoji: "🥇", amount: DISTRIBUTION.first, gradient: "from-blue-500 to-indigo-500" }, | |
| { place: "2nd Place", emoji: "🥈", amount: DISTRIBUTION.second, gradient: "from-indigo-500 to-purple-500" }, | |
| { place: "3rd Place", emoji: "🥉", amount: DISTRIBUTION.third, gradient: "from-purple-500 to-pink-500" } | |
| ] as { place, emoji, amount, gradient }} | |
| <div class="prize-card group"> | |
| <div class="flex flex-col items-center space-y-2"> | |
| <div class="text-base sm:text-lg font-medium flex items-center gap-2"> | |
| <span class="text-xl sm:text-2xl">{emoji}</span> | |
| {place} | |
| </div> | |
| <div class="font-bold text-lg sm:text-xl lg:text-2xl bg-gradient-to-r {gradient} bg-clip-text text-transparent"> | |
| {formatUsd((totalSolUsd + totalEthUsd) * amount)} | |
| </div> | |
| <div class="text-xs sm:text-sm text-gray-400 group-hover:text-gray-300 transition-colors duration-300"> | |
| {(amount * 100)}% of prize pool | |
| </div> | |
| </div> | |
| </div> | |
| {/each} | |
| </div> | |
| </div> | |
| <!-- Enhanced Competition Explainer --> | |
| <div class="p-4 rounded-xl bg-gray-700/20 backdrop-filter backdrop-blur-sm transition-all duration-300 | |
| max-w-2xl mx-auto hover:bg-gray-700/30"> | |
| <h3 class="text-base sm:text-lg lg:text-xl font-semibold mb-2 gradient-text"> | |
| Why Join the Competition? | |
| </h3> | |
| <p class="text-sm sm:text-base leading-relaxed text-gray-300"> | |
| The 3 biggest volume makers on XSHOT will split | |
| <span class="font-semibold text-indigo-400">50% of the total generated fees</span> | |
| on XSHOT throughout the month, which is | |
| <span class="font-semibold text-indigo-400">0.6% of every trade</span> from most users. | |
| This is how top competitors can earn a substantial reward, and why we see this as | |
| an exciting opportunity to <em>share the bread</em> and make trading fun for everyone. | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Wallet Info Cards --> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6 mb-6"> | |
| <!-- Solana Card --> | |
| <div class={`p-4 sm:p-6 rounded-xl shadow-lg transition-all duration-300 transform hover:scale-102 | |
| ${darkMode ? 'bg-gray-800/90' : 'bg-white/90'}`}> | |
| <div class="text-lg sm:text-xl font-medium mb-2"> | |
| Solana Wallet | |
| </div> | |
| <div class="text-2xl sm:text-3xl font-extrabold mb-4 text-indigo-500"> | |
| {formatUsd(totalSolUsd)} | |
| </div> | |
| <div class="text-sm"> | |
| <a | |
| href={`https://solscan.io/account/${walletBalances[0]?.solana.wallet}`} | |
| target="_blank" | |
| class="text-indigo-600 hover:text-indigo-500 hover:underline dark:text-indigo-400 transition-colors" | |
| > | |
| Verify Prize Pool on Solscan ↗ | |
| </a> | |
| </div> | |
| </div> | |
| <!-- Ethereum Card --> | |
| <div class={`p-4 sm:p-6 rounded-xl shadow-lg transition-all duration-300 transform hover:scale-102 | |
| ${darkMode ? 'bg-gray-800/90' : 'bg-white/90'}`}> | |
| <div class="text-lg sm:text-xl font-medium mb-2"> | |
| Ethereum Wallet | |
| </div> | |
| <div class="text-2xl sm:text-3xl font-extrabold mb-4 text-indigo-500"> | |
| {formatUsd(totalEthUsd)} | |
| </div> | |
| <div class="text-sm"> | |
| <a | |
| href={`https://etherscan.io/address/${walletBalances[0]?.ethereum.wallet}`} | |
| target="_blank" | |
| class="text-indigo-600 hover:text-indigo-500 hover:underline dark:text-indigo-400 transition-colors" | |
| > | |
| Verify Prize Pool on Etherscan ↗ | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Search Section --> | |
| <div class={`mb-6 p-4 sm:p-6 rounded-xl shadow-lg backdrop-filter backdrop-blur-sm | |
| ${darkMode ? 'bg-gray-800/90' : 'bg-white/90'}`}> | |
| <div class="max-w-xl mx-auto space-y-4"> | |
| <div class="relative"> | |
| <input | |
| bind:this={searchInputRef} | |
| type="text" | |
| bind:value={searchTerm} | |
| on:keydown={(e) => e.key === 'Enter' && searchUser()} | |
| class={`w-full px-4 py-3 rounded-lg border text-base | |
| focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-all duration-300 | |
| ${darkMode | |
| ? 'bg-gray-700/70 border-gray-600 text-white placeholder-gray-300' | |
| : 'bg-white border-gray-300 text-gray-900 placeholder-gray-500' | |
| }`} | |
| placeholder="Enter your User ID" | |
| /> | |
| <button | |
| on:click={searchUser} | |
| disabled={loading} | |
| class="absolute right-2 top-2 bg-indigo-600 text-white px-4 py-1 rounded-md | |
| hover:bg-indigo-700 disabled:opacity-50 transition-all duration-300" | |
| > | |
| {loading ? 'Searching...' : 'Search'} | |
| </button> | |
| </div> | |
| <div class={`flex items-center justify-center gap-4 p-3 rounded-lg bg-opacity-50 | |
| ${darkMode ? 'bg-gray-700/50' : 'bg-gray-100/50'}`}> | |
| <div class="text-sm font-medium">Find your User ID:</div> | |
| <div class="font-mono text-sm bg-indigo-500/20 px-3 py-1 rounded"> | |
| Type /id in XSHOT | |
| </div> | |
| </div> | |
| {#if error} | |
| <div class="text-red-500 text-center text-sm animate-fade-in">{error}</div> | |
| {/if} | |
| {#if targetVolume} | |
| <div class={`mt-4 p-4 rounded-lg animate-fade-in | |
| ${darkMode ? 'bg-gray-700/50' : 'bg-gray-100/50'}`}> | |
| <h3 class="text-lg font-semibold mb-3 text-center gradient-text">Your Trading Stats</h3> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> | |
| <div class="p-4 rounded-xl bg-green-500/10 backdrop-filter backdrop-blur-sm"> | |
| <div class="text-sm opacity-80">Your Current Volume</div> | |
| <div class="text-xl font-bold text-green-400"> | |
| {formatUsd(targetVolume.currentVolumeUsd)} | |
| </div> | |
| </div> | |
| <div class="p-4 rounded-xl bg-purple-500/10 backdrop-filter backdrop-blur-sm"> | |
| <div class="text-sm opacity-80">Volume Needed for Top 3</div> | |
| <div class="text-xl font-bold text-purple-400"> | |
| {formatUsd(targetVolume.volumeNeededForTop3Usd)} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/if} | |
| </div> | |
| </div> | |
| <!-- Add to your +page.svelte where you want the lottery section --> | |
| <div class="max-w-2xl mx-auto mb-8"> | |
| <div class={`p-4 sm:p-6 rounded-xl shadow-xl transition-all duration-300 | |
| ${darkMode ? 'bg-gray-800/90 shadow-gray-900/50' : 'bg-white/90 shadow-gray-200/50'} | |
| backdrop-filter backdrop-blur-sm`}> | |
| <div class="text-center space-y-4"> | |
| <!-- Title with matching gradient --> | |
| <div class="relative inline-block"> | |
| <h2 class="text-xl sm:text-2xl lg:text-3xl font-bold flex items-center justify-center gap-3 p-2"> | |
| <span class="text-yellow-500 animate-pulse">⭐</span> | |
| <span class="gradient-text tracking-tight">MEGA JACKPOT LOTTERY</span> | |
| <span class="text-yellow-500 animate-pulse">⭐</span> | |
| </h2> | |
| </div> | |
| <!-- Countdown Banner --> | |
| <div class="bg-gradient-to-r from-yellow-500/20 to-pink-500/20 rounded-full px-4 py-2 inline-block"> | |
| <span class="text-yellow-400 font-bold animate-pulse"> | |
| 52 Days Until Draw! 🎰 | |
| </span> | |
| </div> | |
| <!-- Ticket Info --> | |
| <div class="bg-gradient-to-br from-purple-500/10 to-pink-500/10 rounded-xl p-4"> | |
| <h3 class="text-lg font-bold text-purple-400 mb-2"> | |
| Your Lucky Tickets: <span class="text-2xl">🎟️ {Math.floor((targetVolume?.currentVolumeUsd || 0) / 100)}</span> | |
| </h3> | |
| <p class="text-sm text-gray-400"> | |
| Each $100 traded = 1 ticket to victory! | |
| </p> | |
| </div> | |
| <!-- Call to Action --> | |
| {#if searchTerm} | |
| <button | |
| on:click={() => showLotteryModal = true} | |
| class="px-6 py-3 bg-gradient-to-r from-purple-600 to-pink-600 text-white | |
| font-semibold rounded-xl shadow-lg transform hover:scale-105 | |
| transition-all duration-300" | |
| > | |
| View My Tickets 🎟️ | |
| </button> | |
| {:else} | |
| <div class="text-sm text-purple-400"> | |
| Search your User ID above to view your tickets | |
| </div> | |
| {/if} | |
| </div> | |
| </div> | |
| </div> | |
| <!-- CTA Button --> | |
| <div class="text-center mb-6"> | |
| <a | |
| href="https://t.me/xshot_trading_bot" | |
| target="_blank" | |
| class="inline-block px-6 sm:px-8 py-3 sm:py-4 bg-indigo-600 text-white font-bold rounded-xl | |
| shadow-lg transform hover:scale-105 hover:bg-indigo-500 transition-all duration-300" | |
| > | |
| 🚀 Start Trading with XSHOT Now | |
| </a> | |
| </div> | |
| <!-- Rankings Toggle --> | |
| <div class="text-center mb-4"> | |
| <button | |
| on:click={() => (showTable = !showTable)} | |
| class="flex mx-auto items-center justify-center gap-2 text-indigo-600 dark:text-indigo-400 | |
| font-bold text-lg sm:text-xl transform hover:scale-110 transition-all duration-300" | |
| > | |
| {#if showTable} | |
| <span>Hide Rankings</span> | |
| <span class="text-2xl sm:text-3xl rotate-180 inline-block transition-transform duration-300">⬇</span> | |
| {:else} | |
| <span>View Rankings</span> | |
| <span class="text-2xl sm:text-3xl inline-block transition-transform duration-300">⬇</span> | |
| {/if} | |
| </button> | |
| </div> | |
| <!-- Rankings Table --> | |
| {#if showTable} | |
| <div class={`rounded-xl shadow-lg overflow-hidden mt-4 mx-auto backdrop-filter backdrop-blur-sm | |
| ${darkMode ? 'bg-gray-800/90' : 'bg-white/90'}`}> | |
| <div class="overflow-x-auto"> | |
| <table class="min-w-full"> | |
| <!-- In your rankings table section in +page.svelte --> | |
| <thead class={darkMode ? 'bg-gray-700/50 text-gray-200' : 'bg-gray-100/50 text-gray-800'}> | |
| <tr> | |
| <th class="px-4 sm:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Rank</th> | |
| <th class="px-4 sm:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">User ID</th> | |
| <th class="px-4 sm:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Volume (USD)</th> | |
| <th class="px-4 sm:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Lottery Tickets</th> | |
| </tr> | |
| </thead> | |
| <tbody class={`divide-y ${darkMode ? 'divide-gray-700/50' : 'divide-gray-200/50'}`}> | |
| {#each rankings as { rank, userId, volumeUsd, isTop3 }} | |
| {#if volumeUsd > 0} | |
| <tr class={` | |
| transition-colors duration-200 | |
| ${darkMode ? 'hover:bg-gray-700/50' : 'hover:bg-gray-50/50'} | |
| ${isTop3 ? (darkMode ? 'bg-gray-700/30' : 'bg-gray-50/30') : ''} | |
| `}> | |
| <td class="px-4 sm:px-6 py-3 whitespace-nowrap"> | |
| {#if rank <= 3} | |
| <span class="text-xl">{rank === 1 ? '🥇' : rank === 2 ? '🥈' : '🥉'}</span> | |
| {:else} | |
| {rank} | |
| {/if} | |
| </td> | |
| <td class="px-4 sm:px-6 py-3 whitespace-nowrap font-medium"> | |
| {userId} | |
| </td> | |
| <td class="px-4 sm:px-6 py-3 whitespace-nowrap"> | |
| {formatUsd(volumeUsd)} | |
| </td> | |
| <td class="px-4 sm:px-6 py-3 whitespace-nowrap font-mono text-indigo-400"> | |
| {Math.floor(volumeUsd / 100)} 🎟️ | |
| </td> | |
| </tr> | |
| {/if} | |
| {/each} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| {/if} | |
| </div> | |
| <!-- Footer --> | |
| <footer class="mt-8 py-4 text-center text-sm text-gray-500 dark:text-gray-400 border-t | |
| border-gray-200/20 dark:border-gray-700/20 backdrop-filter backdrop-blur-sm"> | |
| <div class="container mx-auto px-4"> | |
| <p class="mb-2 text-xs sm:text-sm">Explore more on:</p> | |
| <div class="flex items-center justify-center gap-4 text-sm sm:text-base"> | |
| <a | |
| href="https://www.xprojecterc.com/" | |
| target="_blank" | |
| class="text-indigo-500 hover:text-indigo-400 hover:underline dark:text-indigo-400 | |
| transition-colors duration-300" | |
| > | |
| XProject Main Website | |
| </a> | |
| <span class="text-gray-400">|</span> | |
| <a | |
| href="https://xshot.xprojecterc.com/" | |
| target="_blank" | |
| class="text-indigo-500 hover:text-indigo-400 hover:underline dark:text-indigo-400 | |
| transition-colors duration-300" | |
| > | |
| XShot Website | |
| </a> | |
| </div> | |
| </div> | |
| </footer> | |
| {#if showLotteryModal} | |
| <LotteryModal | |
| userId={parseInt(searchTerm) || 0} | |
| isOpen={showLotteryModal} | |
| onClose={() => showLotteryModal = false} | |
| {darkMode} | |
| /> | |
| {/if} | |
| </div> | |
| <style> | |
| /* Modern font setup */ | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap'); | |
| :global(html) { | |
| scroll-behavior: smooth; | |
| } | |
| :global(html), | |
| :global(body), | |
| :global(button), | |
| :global(input), | |
| :global(select), | |
| :global(textarea) { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | |
| } | |
| /* Elegant scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| height: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #6366f1; | |
| border-radius: 3px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #4f46e5; | |
| } | |
| /* Enhanced custom classes */ | |
| .gradient-text { | |
| @apply bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400 bg-clip-text text-transparent; | |
| } | |
| .prize-card { | |
| @apply p-4 rounded-xl bg-gray-800/50 backdrop-filter backdrop-blur-sm shadow-lg transition-all duration-300; | |
| } | |
| .trophy-icon { | |
| @apply text-yellow-500; | |
| } | |
| /* Custom animations */ | |
| @keyframes gentle-pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.8; } | |
| } | |
| .animate-gentle-pulse { | |
| animation: gentle-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; | |
| } | |
| .animate-fade-in { | |
| animation: fadeIn 0.5s ease-in-out; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(-10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| </style> | |
| ========== ./routes/+page.svelte.bak ========== | |
| <script> | |
| import { onMount } from 'svelte'; | |
| import { getAllWalletBalances } from '$lib/prices'; | |
| let rankings = []; | |
| let searchTerm = ''; | |
| let targetVolume = null; | |
| let loading = false; | |
| let error = ''; | |
| let darkMode = true; | |
| let walletBalances = []; | |
| let totalSolUsd = 0; | |
| let totalEthUsd = 0; | |
| let searchInputRef; | |
| const DISTRIBUTION = { | |
| first: 0.5, | |
| second: 0.3, | |
| third: 0.2 | |
| }; | |
| onMount(async () => { | |
| loadRankings(); | |
| loadPrizes(); | |
| if (window.matchMedia('(prefers-color-scheme: light)').matches) { | |
| darkMode = false; | |
| } | |
| }); | |
| async function loadRankings() { | |
| try { | |
| const response = await fetch('/api/rankings'); | |
| if (!response.ok) throw new Error('Failed to fetch rankings'); | |
| rankings = await response.json(); | |
| } catch (err) { | |
| error = 'Failed to load rankings'; | |
| } | |
| } | |
| async function loadPrizes() { | |
| try { | |
| walletBalances = await getAllWalletBalances(); | |
| totalSolUsd = walletBalances.reduce((sum, wallet) => sum + wallet.solana.usdValue, 0) * 0.5; // 50% of total | |
| totalEthUsd = walletBalances.reduce((sum, wallet) => sum + wallet.ethereum.usdValue, 0) * 0.5; // 50% of total | |
| } catch (err) { | |
| console.error('Error loading prizes:', err); | |
| } | |
| } | |
| async function searchUser() { | |
| if (!searchTerm) return; | |
| loading = true; | |
| error = ''; | |
| try { | |
| const response = await fetch(`/api/search/${encodeURIComponent(searchTerm)}`); | |
| if (!response.ok) throw new Error('Failed to fetch user data'); | |
| targetVolume = await response.json(); | |
| } catch (err) { | |
| error = 'User not found'; | |
| targetVolume = null; | |
| } finally { | |
| loading = false; | |
| } | |
| } | |
| function formatUsd(num) { | |
| return new Intl.NumberFormat('en-US', { | |
| style: 'currency', | |
| currency: 'USD', | |
| notation: 'compact', | |
| maximumFractionDigits: 2 | |
| }).format(num); | |
| } | |
| function toggleDarkMode() { | |
| darkMode = !darkMode; | |
| } | |
| </script> | |
| <div class={`min-h-screen transition-colors duration-200 ${darkMode ? 'bg-gray-900 text-gray-100' : 'bg-gray-50 text-gray-900'}`}> | |
| <div class="container mx-auto px-4 py-8 max-w-6xl"> | |
| <!-- Dark Mode Toggle --> | |
| <div class="absolute top-4 right-4"> | |
| <button | |
| on:click={toggleDarkMode} | |
| class="p-2 rounded-full hover:bg-opacity-20 hover:bg-gray-500 transition-colors" | |
| > | |
| {#if darkMode} | |
| 🌞 | |
| {:else} | |
| 🌙 | |
| {/if} | |
| </button> | |
| </div> | |
| <!-- Prize Pool Banner --> | |
| <div class={`mb-8 p-6 rounded-lg shadow-lg ${darkMode ? 'bg-gradient-to-r from-blue-900/50 to-purple-900/50' : 'bg-gradient-to-r from-blue-100 to-purple-100'}`}> | |
| <div class="text-center"> | |
| <!-- Main Title --> | |
| <h2 class="text-4xl font-bold mb-6 bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-purple-400"> | |
| 🏆 Monthly Trading Competition 🏆 | |
| </h2> | |
| <!-- Grand Total Prize Display --> | |
| <div class="mb-8 relative"> | |
| <div class="absolute inset-0 bg-gradient-to-r from-yellow-400 via-red-500 to-pink-500 opacity-10 blur-xl"></div> | |
| <div class="relative"> | |
| <div class="text-7xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-yellow-400 via-red-500 to-pink-500 animate-pulse mb-2"> | |
| {formatUsd(totalSolUsd + totalEthUsd)} | |
| </div> | |
| <div class="text-2xl font-semibold bg-clip-text text-transparent bg-gradient-to-r from-yellow-400 to-pink-500"> | |
| Total Prize Pool to Win | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Prize Distribution --> | |
| <div class="max-w-2xl mx-auto mb-8 p-6 rounded-xl {darkMode ? 'bg-gray-800/50' : 'bg-white/70'}"> | |
| <div class="grid grid-cols-3 gap-6 mb-6"> | |
| <div class="p-4 rounded-lg bg-opacity-20 {darkMode ? 'bg-blue-500/20' : 'bg-blue-100'}"> | |
| <div class="font-medium text-lg mb-1">🥇 1st Place</div> | |
| <div class="font-bold text-xl">{formatUsd((totalSolUsd + totalEthUsd) * DISTRIBUTION.first)}</div> | |
| <div class="text-sm opacity-70">50% of prize pool</div> | |
| </div> | |
| <div class="p-4 rounded-lg bg-opacity-20 {darkMode ? 'bg-blue-500/20' : 'bg-blue-100'}"> | |
| <div class="font-medium text-lg mb-1">🥈 2nd Place</div> | |
| <div class="font-bold text-xl">{formatUsd((totalSolUsd + totalEthUsd) * DISTRIBUTION.second)}</div> | |
| <div class="text-sm opacity-70">30% of prize pool</div> | |
| </div> | |
| <div class="p-4 rounded-lg bg-opacity-20 {darkMode ? 'bg-blue-500/20' : 'bg-blue-100'}"> | |
| <div class="font-medium text-lg mb-1">🥉 3rd Place</div> | |
| <div class="font-bold text-xl">{formatUsd((totalSolUsd + totalEthUsd) * DISTRIBUTION.third)}</div> | |
| <div class="text-sm opacity-70">20% of prize pool</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Prize Categories --> | |
| <div class="grid md:grid-cols-2 gap-8 mb-8"> | |
| <!-- Solana Prize Pool --> | |
| <div class={`p-6 rounded-lg transform transition-all hover:scale-105 ${darkMode ? 'bg-blue-900/30' : 'bg-blue-50'}`}> | |
| <div class="text-xl font-medium mb-2">Solana Wallet</div> | |
| <div class="text-3xl font-bold mb-4 bg-clip-text text-transparent bg-gradient-to-r from-[#00FFA3] to-[#DC1FFF]"> | |
| {formatUsd(totalSolUsd)} | |
| </div> | |
| <div class="text-sm"> | |
| <a href={`https://solscan.io/account/${walletBalances[0]?.solana.wallet}`} | |
| target="_blank" | |
| class="text-blue-400 hover:underline"> | |
| Verify Prize Pool on Solscan ↗ | |
| </a> | |
| </div> | |
| </div> | |
| <!-- ETH Prize Pool --> | |
| <div class={`p-6 rounded-lg transform transition-all hover:scale-105 ${darkMode ? 'bg-purple-900/30' : 'bg-purple-50'}`}> | |
| <div class="text-xl font-medium mb-2">Ethereum Wallet</div> | |
| <div class="text-3xl font-bold mb-4 bg-clip-text text-transparent bg-gradient-to-r from-[#454A75] to-[#8A92B2]"> | |
| {formatUsd(totalEthUsd)} | |
| </div> | |
| <div class="text-sm"> | |
| <a href={`https://etherscan.io/address/${walletBalances[0]?.ethereum.wallet}`} | |
| target="_blank" | |
| class="text-purple-400 hover:underline"> | |
| Verify Prize Pool on Etherscan ↗ | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Search Section with ID Instructions --> | |
| <div class={`mb-8 p-6 rounded-lg shadow-lg ${darkMode ? 'bg-gray-800' : 'bg-white'}`}> | |
| <div class="max-w-xl mx-auto"> | |
| <div class="relative"> | |
| <input | |
| bind:this={searchInputRef} | |
| type="text" | |
| bind:value={searchTerm} | |
| on:keydown={(e) => e.key === 'Enter' && searchUser()} | |
| class={`w-full px-4 py-3 rounded-lg border ${ | |
| darkMode | |
| ? 'bg-gray-700 border-gray-600 text-white placeholder-gray-400' | |
| : 'bg-white border-gray-300 text-gray-900 placeholder-gray-500' | |
| } focus:outline-none focus:ring-2 focus:ring-indigo-500`} | |
| placeholder="Enter your User ID" | |
| /> | |
| <button | |
| on:click={searchUser} | |
| disabled={loading} | |
| class="absolute right-2 top-2 bg-indigo-600 text-white px-4 py-1 rounded-md hover:bg-indigo-700 disabled:opacity-50 transition-colors" | |
| > | |
| {loading ? 'Searching...' : 'Search'} | |
| </button> | |
| </div> | |
| <div class="mt-4 flex items-center justify-center gap-4 p-3 rounded-lg bg-opacity-50 {darkMode ? 'bg-gray-700' : 'bg-gray-100'}"> | |
| <div class="text-sm font-medium">Find your User ID:</div> | |
| <div class="font-mono text-sm bg-opacity-20 bg-blue-500 px-3 py-1 rounded">Type /id in XSHOT</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Key Info & CTA --> | |
| <div class="space-y-4"> | |
| <a href="https://t.me/xshot_trading_bot" | |
| target="_blank" | |
| class="inline-block px-8 py-4 bg-gradient-to-r from-indigo-500 to-purple-600 text-white font-bold rounded-xl shadow-lg transform hover:scale-105 transition-transform"> | |
| 🚀 Start Trading with XSHOT Now | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Search Section --> | |
| <div class={`mb-8 p-6 rounded-lg shadow-lg ${darkMode ? 'bg-gray-800' : 'bg-white'}`}> | |
| <div class="max-w-xl mx-auto"> | |
| <h2 class="text-2xl font-semibold mb-4">Check Your Position</h2> | |
| <div class="space-y-4"> | |
| <div class="relative"> | |
| <input | |
| bind:this={searchInputRef} | |
| type="text" | |
| bind:value={searchTerm} | |
| on:keydown={(e) => e.key === 'Enter' && searchUser()} | |
| class={`w-full px-4 py-3 rounded-lg border ${ | |
| darkMode | |
| ? 'bg-gray-700 border-gray-600 text-white placeholder-gray-400' | |
| : 'bg-white border-gray-300 text-gray-900 placeholder-gray-500' | |
| } focus:outline-none focus:ring-2 focus:ring-indigo-500`} | |
| placeholder="Enter your User ID" | |
| /> | |
| <button | |
| on:click={searchUser} | |
| disabled={loading} | |
| class="absolute right-2 top-2 bg-indigo-600 text-white px-4 py-1 rounded-md hover:bg-indigo-700 disabled:opacity-50 transition-colors" | |
| > | |
| {loading ? 'Searching...' : 'Search'} | |
| </button> | |
| </div> | |
| {#if error} | |
| <div class="text-red-500 text-sm">{error}</div> | |
| {/if} | |
| {#if targetVolume} | |
| <div class={`mt-4 p-4 rounded-lg ${darkMode ? 'bg-gray-700' : 'bg-gray-100'}`}> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div class="p-4 rounded-lg bg-opacity-20 bg-green-500"> | |
| <div class="text-sm opacity-80">Your Current Volume</div> | |
| <div class="text-xl font-bold">{formatUsd(targetVolume.currentVolumeUsd)}</div> | |
| </div> | |
| <div class="p-4 rounded-lg bg-opacity-20 bg-purple-500"> | |
| <div class="text-sm opacity-80">Volume Needed for Top 3</div> | |
| <div class="text-xl font-bold">{formatUsd(targetVolume.volumeNeededForTop3Usd)}</div> | |
| </div> | |
| </div> | |
| </div> | |
| {/if} | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Rankings Table --> | |
| <div class={`rounded-lg shadow-lg overflow-hidden ${darkMode ? 'bg-gray-800' : 'bg-white'}`}> | |
| <div class="overflow-x-auto"> | |
| <table class="min-w-full"> | |
| <thead class={darkMode ? 'bg-gray-700' : 'bg-gray-50'}> | |
| <tr> | |
| <th class="px-6 py-4 text-left text-xs font-medium uppercase tracking-wider">Rank</th> | |
| <th class="px-6 py-4 text-left text-xs font-medium uppercase tracking-wider">User ID</th> | |
| <th class="px-6 py-4 text-left text-xs font-medium uppercase tracking-wider">Volume (USD)</th> | |
| </tr> | |
| </thead> | |
| <tbody class="divide-y divide-gray-700"> | |
| {#each rankings as { rank, userId, volumeUsd, isTop3 }} | |
| <tr class={` | |
| ${darkMode ? 'hover:bg-gray-700' : 'hover:bg-gray-50'} | |
| transition-colors | |
| ${isTop3 ? (darkMode ? 'bg-gray-700/50' : 'bg-gray-50/50') : ''} | |
| `}> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| {#if rank <= 3} | |
| <span class="text-xl">{rank === 1 ? '🥇' : rank === 2 ? '🥈' : '🥉'}</span> | |
| {:else} | |
| {rank} | |
| {/if} | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap font-medium"> | |
| {userId} | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap">{formatUsd(volumeUsd)}</td> | |
| </tr> | |
| {/each} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <style> | |
| ::-webkit-scrollbar { | |
| width: 10px; | |
| height: 10px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #4f46e5; | |
| border-radius: 5px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #4338ca; | |
| } | |
| </style> | |
| ========== ./routes/api/lottery/stats/+server.js ========== | |
| import { json } from '@sveltejs/kit'; | |
| import { tradeDb } from '$lib/db'; | |
| export async function GET({ params }) { | |
| try { | |
| const userId = parseInt(params.userId); | |
| // 1) Call the function get_user_lottery_stats | |
| // The function returns a single row with these columns: | |
| // total_tickets, revealed_tickets, total_volume_usd, total_trades, win_probability | |
| // Make sure this matches the OUT parameters in your PL/pgSQL function definition | |
| const [stats] = await tradeDb` | |
| SELECT * | |
| FROM get_user_lottery_stats(${userId}) | |
| AS ( | |
| total_tickets bigint, | |
| revealed_tickets bigint, | |
| total_volume_usd numeric, | |
| total_trades bigint, | |
| win_probability numeric | |
| ) | |
| `; | |
| // 2) If the query returns nothing, it might be because the user has no data | |
| if (!stats) { | |
| return json({ error: 'User not found or no stats' }, { status: 404 }); | |
| } | |
| // 3) Otherwise, parse the returned row | |
| const { | |
| total_tickets, | |
| revealed_tickets, | |
| total_volume_usd, | |
| total_trades, | |
| win_probability | |
| } = stats; | |
| // 4) Return the data to your frontend in JSON | |
| return json({ | |
| totalTickets: parseInt(total_tickets), | |
| revealedTickets: parseInt(revealed_tickets), | |
| totalVolumeUsd: parseFloat(total_volume_usd), | |
| totalTrades: parseInt(total_trades), | |
| winProbability: parseFloat(win_probability) | |
| }); | |
| } catch (err) { | |
| console.error('Error fetching lottery data:', err); | |
| return json({ error: 'Failed to fetch lottery data' }, { status: 500 }); | |
| } | |
| } | |
| ========== ./routes/api/lottery/tickets/+server.js ========== | |
| import { json } from '@sveltejs/kit'; | |
| import { tradeDb } from '$lib/db'; | |
| export async function GET({ params }) { | |
| try { | |
| const stats = await tradeDb` | |
| SELECT | |
| * | |
| FROM lottery_stats | |
| WHERE user_id = ${params.userId} | |
| `; | |
| return json({ | |
| tickets: Math.floor((stats[0]?.volume_usd || 0) / 100), | |
| revealed: stats[0]?.revealed_tickets || 0, | |
| total: stats[0]?.total_tickets || 0 | |
| }); | |
| } catch (err) { | |
| console.error('Error fetching tickets:', err); | |
| return json({ error: 'Failed to fetch tickets' }, { status: 500 }); | |
| } | |
| } | |
| ========== ./routes/api/lottery/[userId]/+server.js ========== | |
| // routes/api/lottery/[userId]/+server.js | |
| import { json } from '@sveltejs/kit'; | |
| import { userDb, tradeDb } from '$lib/db'; | |
| export async function GET({ params }) { | |
| try { | |
| const userId = parseInt(params.userId); | |
| // Get user's wallets from core_db | |
| const userWallets = await userDb` | |
| SELECT address | |
| FROM "Wallet" | |
| WHERE user_id = ${userId} | |
| `; | |
| const walletAddresses = userWallets.map(w => w.address); | |
| const holdingsWallets = await tradeDb` | |
| SELECT id | |
| FROM "Wallet" | |
| WHERE address = ANY(${walletAddresses}) | |
| `; | |
| const walletIds = holdingsWallets.map(w => w.id); | |
| // Get trading volume and revealed tickets | |
| const [volumeData, revealedTickets] = await Promise.all([ | |
| tradeDb` | |
| SELECT SUM(value_total_usd) as volume | |
| FROM "Trade" | |
| WHERE "walletId" = ANY(${walletIds}) | |
| AND executed_at >= '2025-01-01' | |
| AND executed_at < '2025-03-01' | |
| `, | |
| tradeDb` | |
| SELECT ticket_hash, created_at | |
| FROM "LotteryTickets" | |
| WHERE user_id = ${userId} | |
| ORDER BY created_at DESC | |
| ` | |
| ]); | |
| const volume = volumeData[0]?.volume || 0; | |
| const totalTickets = Math.floor(volume / 100); | |
| const remainingTickets = totalTickets - revealedTickets.length; | |
| return json({ | |
| totalTickets, | |
| remainingTickets, | |
| revealedTickets: revealedTickets.map(t => t.ticket_hash), | |
| volumeUsd: volume | |
| }); | |
| } catch (err) { | |
| console.error('Error fetching lottery data:', err); | |
| return json({ error: 'Failed to fetch lottery data' }, { status: 500 }); | |
| } | |
| } | |
| export async function POST({ params }) { | |
| try { | |
| const userId = parseInt(params.userId); | |
| // Verify user has remaining tickets | |
| const { remainingTickets } = await GET({ params }).then(r => r.json()); | |
| if (remainingTickets <= 0) { | |
| return json({ error: 'No tickets remaining' }, { status: 400 }); | |
| } | |
| // Generate ticket hash | |
| const ticketHash = Array(8).fill(0) | |
| .map(() => Math.random().toString(16).substr(2, 4)) | |
| .join(''); | |
| await tradeDb` | |
| INSERT INTO "LotteryTickets" ( | |
| user_id, | |
| ticket_hash, | |
| status, | |
| revealed_at | |
| ) VALUES ( | |
| ${userId}, | |
| ${ticketHash}, | |
| 'revealed', | |
| CURRENT_TIMESTAMP | |
| ) | |
| `; | |
| return json({ | |
| success: true, | |
| ticketHash, | |
| remainingTickets: remainingTickets - 1 | |
| }); | |
| } catch (err) { | |
| console.error('Error revealing ticket:', err); | |
| return json({ error: 'Failed to reveal ticket' }, { status: 500 }); | |
| } | |
| } | |
| ========== ./routes/api/rankings/+server.js ========== | |
| // /src/routes/api/rankings/+server.js | |
| import { json } from '@sveltejs/kit'; | |
| import { getCachedData } from '$lib/cache.js'; | |
| export async function GET() { | |
| try { | |
| // Instead of directly hitting DB, ask the cache for the data | |
| const rankings = await getCachedData(); | |
| return json(rankings); | |
| } catch (err) { | |
| console.error('Error loading rankings:', err); | |
| return json({ error: 'Failed to load rankings' }, { status: 500 }); | |
| } | |
| } | |
| ========== ./routes/api/search/[term]/+server.js ========== | |
| import { json } from '@sveltejs/kit'; | |
| import { userDb, tradeDb } from '$lib/db'; | |
| export async function GET({ params }) { | |
| try { | |
| const userId = parseInt(params.term); | |
| if (isNaN(userId)) { | |
| return json({ error: 'Invalid user ID' }, { status: 400 }); | |
| } | |
| // Example: January 1, 2025 at 00:00:00 UTC | |
| const firstDayOfMonth = new Date(2025, 0, 1); | |
| firstDayOfMonth.setUTCHours(0, 0, 0, 0); | |
| // 1) Get all of this user's wallet addresses from userDb | |
| const userWallets = await userDb` | |
| SELECT address | |
| FROM "Wallet" | |
| WHERE user_id = ${userId} | |
| `; | |
| const addresses = userWallets.map((w) => w.address); | |
| // 2) Convert those addresses to Wallet IDs in tradeDb | |
| const holdingsWallets = await tradeDb` | |
| SELECT id | |
| FROM "Wallet" | |
| WHERE address = ANY(${addresses}) | |
| `; | |
| const walletIds = holdingsWallets.map((w) => w.id); | |
| // 3) Query: | |
| // - "user_volume": sum of this user's trades | |
| // - "ranked_user_volumes": everyone’s total volume with a rank | |
| // - "third_place_volume": volume of rank=3 (if it exists) | |
| const volumeResults = await tradeDb` | |
| WITH user_volume AS ( | |
| SELECT COALESCE(SUM(value_total_usd), 0) AS volume | |
| FROM "Trade" | |
| WHERE "walletId" = ANY(${walletIds}) | |
| AND executed_at >= ${firstDayOfMonth} | |
| ), | |
| ranked_user_volumes AS ( | |
| SELECT | |
| w.address, | |
| SUM(t.value_total_usd) AS volume, | |
| RANK() OVER (ORDER BY SUM(t.value_total_usd) DESC) AS rank | |
| FROM "Trade" t | |
| JOIN "Wallet" w ON t."walletId" = w.id | |
| WHERE t.executed_at >= ${firstDayOfMonth} | |
| GROUP BY w.address | |
| ), | |
| third_place_volume AS ( | |
| SELECT volume | |
| FROM ranked_user_volumes | |
| WHERE rank = 3 | |
| LIMIT 1 | |
| ) | |
| SELECT | |
| (SELECT volume FROM user_volume) AS user_volume, | |
| COALESCE((SELECT volume FROM third_place_volume), 0) AS third_place_volume; | |
| `; | |
| // 4) Calculate how much more volume is needed for the user to get from their current volume to 3rd place | |
| const userVolume = parseFloat(volumeResults[0]?.user_volume ?? 0); | |
| const thirdPlaceVolume = parseFloat(volumeResults[0]?.third_place_volume ?? 0); | |
| const volumeNeeded = Math.max(0, thirdPlaceVolume - userVolume); | |
| return json({ | |
| currentVolumeUsd: userVolume, | |
| currentTop3ThresholdUsd: thirdPlaceVolume, | |
| volumeNeededForTop3Usd: volumeNeeded | |
| }); | |
| } catch (err) { | |
| console.error('Error searching user:', err); | |
| return json({ error: 'Failed to search user' }, { status: 500 }); | |
| } | |
| } | |
| ========== ./routes/api/target/[userId]/+server.js ========== | |
| import { json } from '@sveltejs/kit'; | |
| import { userDb, tradeDb } from '$lib/db'; | |
| export async function GET({ params }) { | |
| try { | |
| const userId = parseInt(params.userId); | |
| const firstDayOfMonth = new Date(); | |
| firstDayOfMonth.setDate(1); | |
| firstDayOfMonth.setHours(0, 0, 0, 0); | |
| // First get user's wallet addresses from core db | |
| const userWallets = await userDb` | |
| SELECT address | |
| FROM "Wallet" | |
| WHERE user_id = ${userId} | |
| `; | |
| const addresses = userWallets.map(w => w.address); | |
| if (addresses.length === 0) { | |
| return json({ | |
| currentVolume: 0, | |
| currentVolumeUsd: 0, | |
| volumeNeededForTop3: 0, | |
| volumeNeededForTop3Usd: 0, | |
| currentTop3Threshold: 0, | |
| currentTop3ThresholdUsd: 0 | |
| }); | |
| } | |
| // Get holdings wallet IDs for these addresses | |
| const holdingsWallets = await tradeDb` | |
| SELECT id | |
| FROM "Wallet" | |
| WHERE address = ANY(${addresses}) | |
| `; | |
| const walletIds = holdingsWallets.map(w => w.id); | |
| // Get top 3 volumes (need to do the same address matching) | |
| const top3Result = await tradeDb` | |
| WITH all_volumes AS ( | |
| SELECT | |
| hw.address, | |
| SUM(COALESCE(t.amount_in, 0) + COALESCE(t.amount_out, 0)) as total_volume, | |
| SUM(COALESCE(t.value_total_usd, 0)) as total_volume_usd | |
| FROM "Trade" t | |
| JOIN "Wallet" hw ON t."walletId" = hw.id | |
| WHERE t.executed_at >= ${firstDayOfMonth} | |
| GROUP BY hw.address | |
| ), | |
| user_volumes AS ( | |
| SELECT | |
| cw.user_id, | |
| SUM(av.total_volume) as total_volume, | |
| SUM(av.total_volume_usd) as total_volume_usd | |
| FROM all_volumes av | |
| JOIN ${userDb.raw(`xshot_core_prod.public."Wallet"`)} cw ON cw.address = av.address | |
| GROUP BY cw.user_id | |
| ORDER BY total_volume_usd DESC | |
| LIMIT 3 | |
| ) | |
| SELECT total_volume, total_volume_usd | |
| FROM user_volumes | |
| ORDER BY total_volume_usd DESC | |
| `; | |
| // Get user's current volume | |
| const userVolumeResult = await tradeDb` | |
| SELECT | |
| SUM(COALESCE(t.amount_in, 0) + COALESCE(t.amount_out, 0)) as total_volume, | |
| SUM(COALESCE(t.value_total_usd, 0)) as total_volume_usd | |
| FROM "Trade" t | |
| WHERE t.executed_at >= ${firstDayOfMonth} | |
| AND t."walletId" = ANY(${walletIds}) | |
| `; | |
| const currentVolume = userVolumeResult[0]?.total_volume || 0; | |
| const currentVolumeUsd = userVolumeResult[0]?.total_volume_usd || 0; | |
| const lowestTop3Volume = top3Result[2]?.total_volume || 0; | |
| const lowestTop3VolumeUsd = top3Result[2]?.total_volume_usd || 0; | |
| const volumeNeeded = Math.max(0, lowestTop3Volume - currentVolume); | |
| const volumeNeededUsd = Math.max(0, lowestTop3VolumeUsd - currentVolumeUsd); | |
| return json({ | |
| currentVolume: parseFloat(currentVolume), | |
| currentVolumeUsd: parseFloat(currentVolumeUsd), | |
| volumeNeededForTop3: parseFloat(volumeNeeded), | |
| volumeNeededForTop3Usd: parseFloat(volumeNeededUsd), | |
| currentTop3Threshold: parseFloat(lowestTop3Volume), | |
| currentTop3ThresholdUsd: parseFloat(lowestTop3VolumeUsd) | |
| }); | |
| } catch (err) { | |
| console.error('Error calculating target volume:', err); | |
| return json({ error: 'Failed to calculate target volume' }, { status: 500 }); | |
| } | |
| } | |
| ========== ./s.sh ========== | |
| #!/bin/bash | |
| # Exit on any error | |
| set -e | |
| # Color codes for output | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| BLUE='\033[0;34m' | |
| NC='\033[0m' # No Color | |
| # Helper functions | |
| print_step() { | |
| echo -e "${BLUE}==== $1 ====${NC}" | |
| } | |
| print_success() { | |
| echo -e "${GREEN}✓ $1${NC}" | |
| } | |
| print_error() { | |
| echo -e "${RED}✗ $1${NC}" | |
| } | |
| # Check if we're in the right directory (should contain routes and lib folders) | |
| if [ ! -d "routes" ] || [ ! -d "lib" ]; then | |
| print_error "Please run this script from the root of your SvelteKit project" | |
| exit 1 | |
| fi | |
| # Create backup of existing files | |
| print_step "Creating backups" | |
| timestamp=$(date +%Y%m%d_%H%M%S) | |
| backup_dir="backup_$timestamp" | |
| mkdir -p "$backup_dir" | |
| if [ -f "routes/+page.svelte" ]; then | |
| cp "routes/+page.svelte" "$backup_dir/" | |
| print_success "Backed up +page.svelte" | |
| fi | |
| # Create new API endpoint directory | |
| print_step "Creating lottery API endpoint" | |
| mkdir -p "routes/api/lottery/[userId]" | |
| # Create the API endpoint file | |
| cat > "routes/api/lottery/[userId]/+server.js" << 'EOL' | |
| import { json } from '@sveltejs/kit'; | |
| import { userDb, tradeDb } from '$lib/db'; | |
| export async function GET({ params }) { | |
| try { | |
| const userId = parseInt(params.userId); | |
| // Start date: February 1st, 2025 | |
| const startDate = new Date(2025, 1, 1); | |
| startDate.setUTCHours(0, 0, 0, 0); | |
| // Get user's wallet addresses | |
| const userWallets = await userDb` | |
| SELECT address | |
| FROM "Wallet" | |
| WHERE user_id = ${userId} | |
| `; | |
| const addresses = userWallets.map(w => w.address); | |
| // Get holdings wallet IDs | |
| const holdingsWallets = await tradeDb` | |
| SELECT id | |
| FROM "Wallet" | |
| WHERE address = ANY(${addresses}) | |
| `; | |
| const walletIds = holdingsWallets.map(w => w.id); | |
| // Get trading statistics | |
| const tradeStats = await tradeDb` | |
| WITH user_trades AS ( | |
| SELECT | |
| COUNT(*) as total_trades, | |
| SUM(COALESCE(value_total_usd, 0)) as total_volume_usd | |
| FROM "Trade" | |
| WHERE "walletId" = ANY(${walletIds}) | |
| AND executed_at >= ${startDate} | |
| ), | |
| all_trades AS ( | |
| SELECT | |
| SUM(COALESCE(value_total_usd, 0)) as total_pool_volume_usd, | |
| COUNT(*) as total_pool_trades | |
| FROM "Trade" | |
| WHERE executed_at >= ${startDate} | |
| ) | |
| SELECT | |
| ut.*, | |
| at.total_pool_volume_usd, | |
| at.total_pool_trades | |
| FROM user_trades ut, all_trades at | |
| `; | |
| const stats = tradeStats[0]; | |
| // Calculate tickets (1 ticket per $100 traded) | |
| const tickets = Math.floor((stats?.total_volume_usd || 0) / 100); | |
| const totalPoolTickets = Math.floor((stats?.total_pool_volume_usd || 0) / 100); | |
| // Calculate win probability | |
| const winProbability = tickets / (totalPoolTickets || 1); | |
| return json({ | |
| tickets, | |
| totalPoolTickets, | |
| tradeCount: stats?.total_trades || 0, | |
| volumeUsd: stats?.total_volume_usd || 0, | |
| winProbability, | |
| poolVolumeUsd: stats?.total_pool_volume_usd || 0, | |
| poolTradeCount: stats?.total_pool_trades || 0 | |
| }); | |
| } catch (err) { | |
| console.error('Error fetching lottery data:', err); | |
| return json({ error: 'Failed to fetch lottery data' }, { status: 500 }); | |
| } | |
| } | |
| EOL | |
| print_success "Created lottery API endpoint" | |
| # Create LotteryModal component | |
| print_step "Creating LotteryModal component" | |
| mkdir -p "src/lib/components" | |
| cat > "src/lib/components/LotteryModal.svelte" << 'EOL' | |
| <script> | |
| import { onMount } from 'svelte'; | |
| import { Ticket, Star, ArrowRight, Trophy, Coins } from 'lucide-svelte'; | |
| export let userId; | |
| export let isOpen = false; | |
| export let onClose; | |
| export let darkMode = true; | |
| let data = null; | |
| let loading = true; | |
| $: if (isOpen && userId) { | |
| fetchLotteryData(); | |
| } | |
| async function fetchLotteryData() { | |
| try { | |
| const response = await fetch(`/api/lottery/${userId}`); | |
| data = await response.json(); | |
| } catch (error) { | |
| console.error('Error fetching lottery data:', error); | |
| } finally { | |
| loading = false; | |
| } | |
| } | |
| function formatNumber(num) { | |
| return new Intl.NumberFormat('en-US', { | |
| maximumFractionDigits: 2, | |
| }).format(num); | |
| } | |
| function formatUsd(num) { | |
| return new Intl.NumberFormat('en-US', { | |
| style: 'currency', | |
| currency: 'USD', | |
| notation: 'compact', | |
| maximumFractionDigits: 2 | |
| }).format(num); | |
| } | |
| function formatPercent(num) { | |
| return (num * 100).toFixed(4) + '%'; | |
| } | |
| </script> | |
| {#if isOpen} | |
| <div class="fixed inset-0 z-50 flex items-center justify-center p-4"> | |
| <!-- Backdrop --> | |
| <div | |
| class="absolute inset-0 bg-black/60 backdrop-blur-sm" | |
| on:click={onClose} | |
| /> | |
| <!-- Modal --> | |
| <div class="relative w-full max-w-2xl rounded-2xl shadow-xl p-6 { | |
| darkMode ? 'bg-gray-800' : 'bg-white' | |
| }"> | |
| <!-- Close button --> | |
| <button | |
| on:click={onClose} | |
| class="absolute top-4 right-4 text-gray-400 hover:text-gray-200 transition-colors" | |
| > | |
| ✕ | |
| </button> | |
| <!-- Title --> | |
| <div class="text-center mb-8"> | |
| <h2 class="text-2xl font-bold mb-2 flex items-center justify-center gap-3"> | |
| <Star class="w-6 h-6 text-yellow-500" /> | |
| February Lottery | |
| <Star class="w-6 h-6 text-yellow-500" /> | |
| </h2> | |
| <p class="text-gray-400">One lucky trader will win 50% of all February fees!</p> | |
| </div> | |
| {#if loading} | |
| <div class="text-center py-8">Loading...</div> | |
| {:else if data} | |
| <!-- Tickets Section --> | |
| <div class="mb-8"> | |
| <div class="p-6 rounded-xl {darkMode ? 'bg-gray-700/50' : 'bg-gray-100'}"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <div class="flex items-center gap-3"> | |
| <Ticket class="w-6 h-6 text-indigo-400" /> | |
| <h3 class="text-xl font-semibold">Your Tickets</h3> | |
| </div> | |
| <div class="text-2xl font-bold text-indigo-400"> | |
| {formatNumber(data.tickets)} | |
| </div> | |
| </div> | |
| <div class="text-sm text-gray-400"> | |
| 1 ticket per $100 traded in February | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Stats Grid --> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8"> | |
| <div class="p-4 rounded-xl {darkMode ? 'bg-gray-700/30' : 'bg-gray-50'}"> | |
| <div class="flex items-center gap-2 mb-2"> | |
| <ArrowRight class="w-5 h-5 text-green-400" /> | |
| <div class="text-sm text-gray-400">Your Trades</div> | |
| </div> | |
| <div class="text-xl font-bold">{formatNumber(data.tradeCount)}</div> | |
| </div> | |
| <div class="p-4 rounded-xl {darkMode ? 'bg-gray-700/30' : 'bg-gray-50'}"> | |
| <div class="flex items-center gap-2 mb-2"> | |
| <Coins class="w-5 h-5 text-blue-400" /> | |
| <div class="text-sm text-gray-400">Your Volume</div> | |
| </div> | |
| <div class="text-xl font-bold">{formatUsd(data.volumeUsd)}</div> | |
| </div> | |
| </div> | |
| <!-- Win Chance Section --> | |
| <div class="p-6 rounded-xl mb-6 {darkMode ? 'bg-gray-700/50' : 'bg-gray-100'}"> | |
| <div class="flex items-center gap-3 mb-4"> | |
| <Trophy class="w-6 h-6 text-yellow-500" /> | |
| <h3 class="text-xl font-semibold">Win Probability</h3> | |
| </div> | |
| <div class="text-3xl font-bold text-indigo-400 mb-2"> | |
| {formatPercent(data.winProbability)} | |
| </div> | |
| <div class="text-sm text-gray-400"> | |
| Based on your {formatNumber(data.tickets)} tickets out of {formatNumber(data.totalPoolTickets)} total tickets | |
| </div> | |
| </div> | |
| <!-- Pool Stats --> | |
| <div class="text-center text-sm text-gray-400"> | |
| Total Pool Volume: {formatUsd(data.poolVolumeUsd)} • Total Pool Trades: {formatNumber(data.poolTradeCount)} | |
| </div> | |
| {:else} | |
| <div class="text-center py-8 text-red-400">Failed to load lottery data</div> | |
| {/if} | |
| </div> | |
| </div> | |
| {/if} | |
| <style> | |
| .backdrop-blur-sm { | |
| backdrop-filter: blur(8px); | |
| } | |
| </style> | |
| EOL | |
| print_success "Created LotteryModal component" | |
| # Update +page.svelte | |
| print_step "Updating main page" | |
| # First, let's add the imports and lottery section to the existing file | |
| awk ' | |
| /import \{ onMount \} from '"'"'svelte'"'"';/ { | |
| print; | |
| print " import LotteryModal from '"'"'$lib/components/LotteryModal.svelte'"'"';"; | |
| print " import { Ticket } from '"'"'lucide-svelte'"'"';"; | |
| next; | |
| } | |
| /let darkMode = true;/ { | |
| print; | |
| print " let showLotteryModal = false;"; | |
| next; | |
| } | |
| /<!-- Prize Distribution -->/ { | |
| print; | |
| print " <!-- Lottery Section -->"; | |
| print " <div class=\"max-w-2xl mx-auto mb-8\">"; | |
| print " <div class={`p-6 rounded-xl ${darkMode ? '"'"'bg-purple-900/30 backdrop-blur-sm'"'"' : '"'"'bg-purple-50'"'"'}`}>"; | |
| print " <div class=\"text-center space-y-4\">"; | |
| print " <div class=\"flex items-center justify-center gap-2\">"; | |
| print " <Ticket class=\"w-6 h-6 text-purple-400\" />"; | |
| print " <h3 class=\"text-xl font-bold bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent\">"; | |
| print " February Lottery Event"; | |
| print " </h3>"; | |
| print " </div>"; | |
| print " "; | |
| print " <p class=\"text-lg\">"; | |
| print " Get 1 lottery ticket for every $100 traded!"; | |
| print " </p>"; | |
| print " "; | |
| print " <div class=\"text-sm text-gray-400\">"; | |
| print " One lucky winner will receive 50% of all February trading fees"; | |
| print " </div>"; | |
| print ""; | |
| print " {#if searchTerm}"; | |
| print " <button"; | |
| print " on:click={() => showLotteryModal = true}"; | |
| print " class=\"px-6 py-3 bg-purple-600 text-white font-semibold rounded-xl "; | |
| print " shadow-lg transform hover:scale-105 transition-all duration-300\""; | |
| print " >"; | |
| print " View My Tickets"; | |
| print " </button>"; | |
| print " {:else}"; | |
| print " <div class=\"text-sm text-purple-400\">"; | |
| print " Search your User ID above to view your tickets"; | |
| print " </div>"; | |
| print " {/if}"; | |
| print " </div>"; | |
| print " </div>"; | |
| print " </div>"; | |
| } | |
| /<\/div>$/ { | |
| if (!added_modal) { | |
| print ""; | |
| print " {#if showLotteryModal}"; | |
| print " <LotteryModal"; | |
| print " userId={searchTerm}"; | |
| print " isOpen={showLotteryModal}"; | |
| print " onClose={() => showLotteryModal = false}"; | |
| print " {darkMode}"; | |
| print " />"; | |
| print " {/if}"; | |
| added_modal = 1; | |
| } | |
| } | |
| /<style>/ { | |
| print; | |
| print " .backdrop-blur-sm {"; | |
| print " backdrop-filter: blur(8px);"; | |
| print " }"; | |
| next; | |
| } | |
| { print } | |
| ' routes/+page.svelte > routes/+page.svelte.new | |
| mv routes/+page.svelte.new routes/+page.svelte | |
| print_success "Updated main page" | |
| print_step "Installing required packages" | |
| npm install lucide-svelte --save | |
| print_success "All updates completed successfully!" | |
| echo -e "${GREEN}Backup of original files can be found in: $backup_dir${NC}" | |
| echo -e "${BLUE}Please review the changes and run your application to test the new features${NC}" | |
| ========== ./<q ========== | |
| <script> | |
| import { slide } from 'svelte/transition'; | |
| import SlotMachine from './SlotMachine.svelte'; | |
| export let revealed = false; | |
| export let hash = ''; | |
| export let onReveal = () => {}; | |
| let isSpinning = false; | |
| let showSlot = false; | |
| // If we come in with revealed=true from the parent, | |
| // immediately show the slot with final value: | |
| $: if (revealed && !showSlot) { | |
| showSlot = true; | |
| isSpinning = false; | |
| } | |
| async function handleReveal() { | |
| if (revealed || isSpinning) return; | |
| showSlot = true; | |
| isSpinning = true; | |
| const result = await onReveal(); | |
| if (result) { | |
| hash = result; | |
| // after a short timeout, mark spinning as false | |
| setTimeout(() => { | |
| isSpinning = false; | |
| }, 2000); | |
| } else { | |
| // revert if reveal failed | |
| isSpinning = false; | |
| showSlot = false; | |
| } | |
| } | |
| </script> | |
| <div | |
| class="relative bg-gradient-to-br from-purple-600/30 to-pink-600/30 | |
| p-4 rounded-xl shadow-lg transform transition-all duration-300 | |
| hover:scale-105 cursor-pointer overflow-hidden w-full text-left" | |
| on:click={handleReveal} | |
| aria-label={revealed ? 'Revealed ticket' : 'Click to reveal ticket'} | |
| disabled={revealed} | |
| > | |
| {#if showSlot} | |
| <!-- Already revealing or revealed --> | |
| <div transition:slide> | |
| <SlotMachine | |
| spinning={isSpinning} | |
| finalValue={hash} | |
| on:spinComplete={() => revealed = true} | |
| /> | |
| </div> | |
| {:else} | |
| <!-- Not revealed yet --> | |
| <div class="text-center" in:slide> | |
| <div class="text-3xl mb-2">🎟️</div> | |
| <div class="text-sm text-gray-300"> | |
| Click to Reveal! | |
| </div> | |
| </div> | |
| {/if} | |
| </div> | |
| <style> | |
| .revealed { | |
| cursor: default; | |
| } | |
| </style> | |
| === ./holdings.sql === | |
| === ./lib/cache.js === | |
| // src/lib/cache.js | |
| import { tradeDb, userDb } from '$lib/db'; | |
| // We'll store the "rankings" data in a global variable here. | |
| let cachedData = null; | |
| // Store timestamp of last fetch | |
| let lastFetchTimestamp = 0; | |
| // Set the cache TTL (time to live) in ms; e.g., 10 minutes | |
| const CACHE_TTL = 10 * 60 * 1000; | |
| /** | |
| * Actually queries the DB for fresh rankings data. | |
| */ | |
| async function fetchFreshData() { | |
| // This is the same logic from your /api/rankings/+server.js | |
| // except we return the result here directly. | |
| // Example: January 1st, 2025 at 00:00 UTC | |
| const firstDayOfMonth = new Date(2025, 0, 1); | |
| firstDayOfMonth.setUTCHours(0, 0, 0, 0); | |
| // Get all trades from tradeDb | |
| const tradeVolumes = await tradeDb` | |
| WITH monthly_trades AS ( | |
| SELECT | |
| t."walletId", | |
| hw.address, | |
| SUM(COALESCE(t.value_total_usd, 0)) as total_volume_usd | |
| FROM "Trade" t | |
| JOIN "Wallet" hw ON t."walletId" = hw.id | |
| WHERE t.executed_at >= ${firstDayOfMonth} | |
| GROUP BY t."walletId", hw.address | |
| ) | |
| SELECT * FROM monthly_trades | |
| ORDER BY total_volume_usd DESC | |
| `; | |
| // Build a map of addresses -> user IDs | |
| const addresses = tradeVolumes.map(tv => tv.address); | |
| const userWallets = await userDb` | |
| SELECT | |
| w.address, | |
| w.user_id, | |
| u.id as user_id | |
| FROM "Wallet" w | |
| JOIN "User" u ON u.id = w.user_id | |
| WHERE w.address = ANY(${addresses}) | |
| `; | |
| const addressMap = new Map( | |
| userWallets.map((w) => [ | |
| w.address.toLowerCase(), | |
| { userId: w.user_id } | |
| ]) | |
| ); | |
| // Accumulate volumes by userId | |
| const userVolumes = new Map(); | |
| tradeVolumes.forEach((trade) => { | |
| const userInfo = addressMap.get(trade.address.toLowerCase()); | |
| if (userInfo) { | |
| const userId = userInfo.userId; | |
| if (!userVolumes.has(userId)) { | |
| userVolumes.set(userId, { | |
| userId, | |
| volumeUsd: 0 | |
| }); | |
| } | |
| const userVol = userVolumes.get(userId); | |
| userVol.volumeUsd += parseFloat(trade.total_volume_usd); | |
| } | |
| }); | |
| // Convert map -> array, sort, label rank, mark isTop3 | |
| const rankings = Array.from(userVolumes.values()) | |
| .sort((a, b) => b.volumeUsd - a.volumeUsd) | |
| .map((user, index) => ({ | |
| rank: index + 1, | |
| userId: user.userId, | |
| volumeUsd: user.volumeUsd, | |
| isTop3: index < 3 | |
| })); | |
| return rankings; | |
| } | |
| /** | |
| * Checks if we have valid cached data; if not, fetch fresh from DB. | |
| */ | |
| export async function getCachedData() { | |
| const now = Date.now(); | |
| // If we've never fetched or it's older than CACHE_TTL... | |
| if (!cachedData || now - lastFetchTimestamp > CACHE_TTL) { | |
| console.log('[Cache] Rankings cache is stale, fetching fresh data...'); | |
| cachedData = await fetchFreshData(); | |
| lastFetchTimestamp = now; | |
| } | |
| // Return the cached data (fresh or stale-if not TTL expired) | |
| return cachedData; | |
| } | |
| === ./lib/components/LotteryModal.svelte === | |
| # lib/components/LotteryModal.svelte | |
| <script> | |
| import { onMount } from 'svelte'; | |
| import { fade, scale } from 'svelte/transition'; | |
| import { Ticket, Star, Trophy } from 'lucide-svelte'; | |
| import confetti from 'canvas-confetti'; | |
| import LotteryTicket from './LotteryTicket.svelte'; | |
| export let userId; | |
| export let isOpen = false; | |
| export let onClose; | |
| let loading = true; | |
| let error = null; | |
| let data = null; | |
| let revealingAll = false; | |
| let revealQueue = []; | |
| // Calculate time until drawing | |
| const endDate = new Date(2025, 1, 28, 23, 59, 59); | |
| $: timeLeft = endDate - new Date(); | |
| $: daysLeft = Math.ceil(timeLeft / (1000 * 60 * 60 * 24)); | |
| async function loadTickets() { | |
| try { | |
| loading = true; | |
| error = null; | |
| const response = await fetch(`/api/lottery/${userId}`); | |
| if (!response.ok) throw new Error('Failed to fetch tickets'); | |
| data = await response.json(); | |
| } catch (err) { | |
| error = err.message; | |
| } finally { | |
| loading = false; | |
| } | |
| } | |
| async function revealTicket() { | |
| try { | |
| const response = await fetch(`/api/lottery/${userId}`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' } | |
| }); | |
| if (!response.ok) throw new Error('Failed to reveal ticket'); | |
| const result = await response.json(); | |
| confetti({ | |
| particleCount: 50, | |
| spread: 45, | |
| origin: { y: 0.6 } | |
| }); | |
| return result.ticketHash; | |
| } catch (err) { | |
| error = err.message; | |
| return null; | |
| } | |
| } | |
| function sleep(ms) { | |
| return new Promise(resolve => setTimeout(resolve, ms)); | |
| } | |
| async function scratchAll() { | |
| if (revealingAll || !data?.remainingTickets) return; | |
| revealingAll = true; | |
| revealQueue = []; | |
| for (let i = 0; i < data.remainingTickets; i++) { | |
| // revealTicket returns the ticketHash | |
| const hash = await revealTicket(); | |
| if (hash) { | |
| // push it immediately so user sees each reveal in real time | |
| revealQueue = [...revealQueue, hash]; | |
| // wait a bit (e.g. 1 second) before next reveal | |
| await sleep(1000); | |
| } | |
| } | |
| // Finally re-fetch | |
| await loadTickets(); | |
| revealingAll = false; | |
| } | |
| </script> | |
| <div class="fixed inset-0 z-50 flex items-center justify-center p-4" | |
| transition:fade> | |
| <button | |
| class="absolute inset-0 bg-black bg-opacity-60 backdrop-filter backdrop-blur-sm" | |
| on:click={onClose} | |
| on:keydown={(e) => e.key === 'Escape' && onClose()} | |
| aria-label="Close modal" | |
| ></button> | |
| <div class="relative w-full max-w-4xl bg-gray-800 rounded-2xl shadow-2xl p-6" | |
| transition:scale> | |
| <!-- Close button --> | |
| <button | |
| class="absolute top-4 right-4 text-gray-400 hover:text-gray-200 transition-colors" | |
| on:click={onClose} | |
| aria-label="Close modal" | |
| > | |
| ✕ | |
| </button> | |
| <!-- Header --> | |
| <div class="text-center mb-8"> | |
| <h2 class="text-3xl font-bold flex items-center justify-center gap-3 mb-2"> | |
| <Star class="w-8 h-8 text-yellow-500" /> | |
| <span class="bg-gradient-to-r from-yellow-400 to-purple-400 bg-clip-text text-transparent"> | |
| MEGA LOTTERY EVENT | |
| </span> | |
| <Star class="w-8 h-8 text-yellow-500" /> | |
| </h2> | |
| <!-- Countdown --> | |
| <div class="text-xl text-pink-400 font-bold animate-pulse mb-4"> | |
| {daysLeft} Days Until The Big Draw! 🎉 | |
| </div> | |
| <p class="text-gray-300"> | |
| Trade More, Win More! One Lucky Winner Takes The Jackpot! 💰 | |
| </p> | |
| </div> | |
| {#if loading} | |
| <div class="flex justify-center items-center py-12"> | |
| <div class="animate-spin text-indigo-500 text-4xl">🎰</div> | |
| </div> | |
| {:else if error} | |
| <div class="text-red-400 text-center py-8"> | |
| {error} | |
| <button | |
| class="mt-4 px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors" | |
| on:click={loadTickets} | |
| > | |
| Try Again | |
| </button> | |
| </div> | |
| {:else} | |
| <!-- Stats --> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8"> | |
| <!-- Total Tickets --> | |
| <div class="bg-gradient-to-br from-indigo-900/50 to-purple-900/50 rounded-xl p-4 text-center transform hover:scale-105 transition-all duration-300"> | |
| <div class="text-sm text-gray-400">Total Tickets</div> | |
| <div class="text-2xl font-bold text-indigo-400"> | |
| {data.totalTickets} | |
| </div> | |
| <div class="text-xs text-gray-500 mt-1">Keep trading to earn more!</div> | |
| </div> | |
| <!-- Tickets to Reveal --> | |
| <div class="bg-gradient-to-br from-green-900/50 to-emerald-900/50 rounded-xl p-4 text-center transform hover:scale-105 transition-all duration-300"> | |
| <div class="text-sm text-gray-400">Left to Reveal</div> | |
| <div class="text-2xl font-bold text-green-400"> | |
| {data.remainingTickets} | |
| </div> | |
| <div class="text-xs text-gray-500 mt-1">Click to scratch!</div> | |
| </div> | |
| <!-- Trading Volume --> | |
| <div class="bg-gradient-to-br from-yellow-900/50 to-orange-900/50 rounded-xl p-4 text-center transform hover:scale-105 transition-all duration-300"> | |
| <div class="text-sm text-gray-400">Volume Traded</div> | |
| <div class="text-2xl font-bold text-yellow-400"> | |
| ${Math.floor(data.volumeUsd).toLocaleString()} | |
| </div> | |
| <div class="text-xs text-gray-500 mt-1">$100 = 1 ticket</div> | |
| </div> | |
| </div> | |
| <!-- Scratch All Button --> | |
| {#if data.remainingTickets > 0} | |
| <div class="text-center mb-8"> | |
| <button | |
| class="px-6 py-3 bg-gradient-to-r from-indigo-600 to-purple-600 | |
| hover:from-indigo-500 hover:to-purple-500 text-white rounded-xl | |
| font-bold transform hover:scale-105 transition-all duration-300 | |
| shadow-lg shadow-indigo-900/50 | |
| disabled:opacity-50 disabled:cursor-not-allowed" | |
| on:click={scratchAll} | |
| disabled={revealingAll} | |
| > | |
| {#if revealingAll} | |
| <div class="flex items-center gap-2 justify-center"> | |
| <div class="animate-spin text-xl">🎰</div> | |
| Revealing {revealQueue.length}/{data.remainingTickets}... | |
| </div> | |
| {:else} | |
| <div class="flex items-center gap-2 justify-center"> | |
| 🎰 Scratch All {data.remainingTickets} Tickets! 🎰 | |
| </div> | |
| {/if} | |
| </button> | |
| </div> | |
| {/if} | |
| <!-- Tickets Grid --> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 max-h-[60vh] overflow-y-auto p-4"> | |
| {#each [...data.revealedTickets, ...revealQueue] as hash} | |
| <LotteryTicket revealed={true} {hash} /> | |
| {/each} | |
| {#each Array(data.remainingTickets - revealQueue.length) as _} | |
| <LotteryTicket revealed={false} onReveal={revealTicket} /> | |
| {/each} | |
| </div> | |
| <!-- Footer Stats --> | |
| <div class="mt-8 pt-4 border-t border-gray-700/50 text-center text-sm text-gray-400"> | |
| <p>Trade more to increase your chances! Every $100 = 1 new ticket</p> | |
| {#if data.totalTickets > 0} | |
| <p class="mt-2"> | |
| Your current win chance: | |
| <span class="text-yellow-400 font-bold"> | |
| {((data.totalTickets / (data.totalTickets + 1000)) * 100).toFixed(4)}% | |
| </span> | |
| </p> | |
| {/if} | |
| </div> | |
| {/if} | |
| </div> | |
| </div> | |
| <style> | |
| /* Custom scrollbar */ | |
| div :global(::-webkit-scrollbar) { | |
| width: 8px; | |
| height: 8px; | |
| } | |
| div :global(::-webkit-scrollbar-track) { | |
| background: rgba(31, 41, 55, 0.5); | |
| border-radius: 4px; | |
| } | |
| div :global(::-webkit-scrollbar-thumb) { | |
| background: rgba(99, 102, 241, 0.5); | |
| border-radius: 4px; | |
| } | |
| div :global(::-webkit-scrollbar-thumb:hover) { | |
| background: rgba(99, 102, 241, 0.7); | |
| } | |
| /* Animations */ | |
| @keyframes float { | |
| 0%, 100% { transform: translateY(0px); } | |
| 50% { transform: translateY(-5px); } | |
| } | |
| .animate-float { | |
| animation: float 3s ease-in-out infinite; | |
| } | |
| /* Gradient text animation */ | |
| @keyframes gradient { | |
| 0% { background-position: 0% 50%; } | |
| 50% { background-position: 100% 50%; } | |
| 100% { background-position: 0% 50%; } | |
| } | |
| .bg-gradient-to-r { | |
| background-size: 200% auto; | |
| animation: gradient 5s linear infinite; | |
| } | |
| </style> | |
| === ./lib/components/LotteryTicket.svelte === | |
| <script> | |
| import { slide } from 'svelte/transition'; | |
| import SlotMachine from './SlotMachine.svelte'; | |
| export let revealed = false; | |
| export let hash = ''; | |
| export let onReveal = () => {}; | |
| let isSpinning = false; | |
| let showSlot = false; | |
| // If we come in with revealed=true from the parent, | |
| // immediately show the slot with final value: | |
| $: if (revealed && !showSlot) { | |
| showSlot = true; | |
| isSpinning = false; | |
| } | |
| async function handleReveal() { | |
| if (revealed || isSpinning) return; | |
| showSlot = true; | |
| isSpinning = true; | |
| const result = await onReveal(); | |
| if (result) { | |
| hash = result; | |
| // after a short timeout, mark spinning as false | |
| setTimeout(() => { | |
| isSpinning = false; | |
| }, 2000); | |
| } else { | |
| // revert if reveal failed | |
| isSpinning = false; | |
| showSlot = false; | |
| } | |
| } | |
| </script> | |
| <div | |
| class="relative bg-gradient-to-br from-purple-600/30 to-pink-600/30 | |
| p-4 rounded-xl shadow-lg transform transition-all duration-300 | |
| hover:scale-105 cursor-pointer overflow-hidden w-full text-left" | |
| on:click={handleReveal} | |
| aria-label={revealed ? 'Revealed ticket' : 'Click to reveal ticket'} | |
| disabled={revealed} | |
| > | |
| {#if showSlot} | |
| <!-- Already revealing or revealed --> | |
| <div transition:slide> | |
| <SlotMachine | |
| spinning={isSpinning} | |
| finalValue={hash} | |
| on:spinComplete={() => revealed = true} | |
| /> | |
| </div> | |
| {:else} | |
| <!-- Not revealed yet --> | |
| <div class="text-center" in:slide> | |
| <div class="text-3xl mb-2">🎟️</div> | |
| <div class="text-sm text-gray-300"> | |
| Click to Reveal! | |
| </div> | |
| </div> | |
| {/if} | |
| </div> | |
| <style> | |
| .revealed { | |
| cursor: default; | |
| } | |
| </style> | |
| === ./lib/components/LotteryTicketView.svelte === | |
| <script> | |
| import { onMount } from 'svelte'; | |
| import { fade, fly, scale } from 'svelte/transition'; | |
| import { elasticOut } from 'svelte/easing'; | |
| import confetti from 'canvas-confetti'; | |
| import { maskUserId, formatUsd, formatPercent } from '$lib/utils'; | |
| import { Card } from '@/components/ui/card'; | |
| export let userId; | |
| export let darkMode = true; | |
| let loading = true; | |
| let error = null; | |
| let ticketData = null; | |
| let newTickets = []; | |
| let showNewTicketsAnimation = false; | |
| let lastKnownTicketCount = 0; | |
| $: winChance = ticketData ? | |
| ticketData.totalTickets / (ticketData.totalPoolTickets || 1) : 0; | |
| async function loadTickets() { | |
| try { | |
| loading = true; | |
| error = null; | |
| const response = await fetch(`/api/lottery/${userId}`); | |
| if (!response.ok) throw new Error('Failed to fetch tickets'); | |
| const data = await response.json(); | |
| // Check for new tickets | |
| if (lastKnownTicketCount > 0 && data.totalTickets > lastKnownTicketCount) { | |
| const newCount = data.totalTickets - lastKnownTicketCount; | |
| newTickets = data.tickets.slice(0, newCount); | |
| showNewTicketsAnimation = true; | |
| celebrateNewTickets(newCount); | |
| } | |
| lastKnownTicketCount = data.totalTickets; | |
| ticketData = data; | |
| } catch (err) { | |
| error = err.message; | |
| } finally { | |
| loading = false; | |
| } | |
| } | |
| function celebrateNewTickets(count) { | |
| const duration = 2000; | |
| const animationEnd = Date.now() + duration; | |
| const colors = ['#FF69B4', '#4169E1', '#7B68EE', '#00CED1']; | |
| const interval = setInterval(() => { | |
| if (Date.now() > animationEnd) { | |
| clearInterval(interval); | |
| return; | |
| } | |
| confetti({ | |
| particleCount: 3 * count, | |
| startVelocity: 30, | |
| spread: 360, | |
| origin: { x: Math.random(), y: Math.random() * 0.5 }, | |
| colors: colors, | |
| shapes: ['circle', 'square'], | |
| scalar: 0.7, | |
| }); | |
| }, 50); | |
| setTimeout(() => { | |
| showNewTicketsAnimation = false; | |
| newTickets = []; | |
| }, duration + 1000); | |
| } | |
| onMount(() => { | |
| loadTickets(); | |
| // Poll for updates every 30 seconds | |
| const interval = setInterval(loadTickets, 30000); | |
| return () => clearInterval(interval); | |
| }); | |
| </script> | |
| <div class="space-y-6"> | |
| <!-- Stats Overview --> | |
| {#if ticketData} | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
| <Card class={`p-4 ${darkMode ? 'bg-gray-800' : 'bg-white'} transform hover:scale-105 transition-duration-300`}> | |
| <div class="text-center"> | |
| <div class="text-lg text-gray-400">Your Tickets</div> | |
| <div class="text-3xl font-bold bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent"> | |
| {ticketData.totalTickets} | |
| </div> | |
| </div> | |
| </Card> | |
| <Card class={`p-4 ${darkMode ? 'bg-gray-800' : 'bg-white'} transform hover:scale-105 transition-duration-300`}> | |
| <div class="text-center"> | |
| <div class="text-lg text-gray-400">Win Chance</div> | |
| <div class="text-3xl font-bold text-green-400"> | |
| {formatPercent(winChance)} | |
| </div> | |
| </div> | |
| </Card> | |
| <Card class={`p-4 ${darkMode ? 'bg-gray-800' : 'bg-white'} transform hover:scale-105 transition-duration-300`}> | |
| <div class="text-center"> | |
| <div class="text-lg text-gray-400">Total Volume</div> | |
| <div class="text-3xl font-bold text-blue-400"> | |
| {formatUsd(ticketData.volumeUsd)} | |
| </div> | |
| </div> | |
| </Card> | |
| </div> | |
| {/if} | |
| <!-- New Tickets Animation --> | |
| {#if showNewTicketsAnimation} | |
| <div class="fixed inset-0 flex items-center justify-center pointer-events-none z-50"> | |
| <div class="text-center" in:scale={{duration: 600, easing: elasticOut}}> | |
| <div class="text-4xl md:text-6xl font-bold text-yellow-400 mb-4"> | |
| 🎉 {newTickets.length} New Tickets! 🎉 | |
| </div> | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 my-8"> | |
| {#each newTickets as ticket} | |
| <div | |
| in:fly={{y: 100, duration: 600}} | |
| class={`p-4 rounded-xl ${darkMode ? 'bg-purple-900/30' : 'bg-purple-50'} | |
| transform hover:scale-105 transition-all duration-300`} | |
| > | |
| <div class="text-center"> | |
| <div class="text-2xl mb-2">🎟️</div> | |
| <div class="font-mono text-sm opacity-80">#{ticket.number}</div> | |
| </div> | |
| </div> | |
| {/each} | |
| </div> | |
| {/if} | |
| <!-- All Tickets Display --> | |
| {#if ticketData?.tickets?.length} | |
| <div class="mt-8"> | |
| <h3 class="text-xl font-bold mb-4">Your Lucky Tickets</h3> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"> | |
| {#each ticketData.tickets as ticket} | |
| <div | |
| class={`p-4 rounded-xl ${darkMode ? 'bg-gray-800/80' : 'bg-gray-50'} | |
| transform hover:scale-105 transition-all duration-300 | |
| border-2 ${ticket.isSpecial ? 'border-yellow-500/50' : 'border-transparent'}`} | |
| > | |
| <div class="text-center"> | |
| <div class="text-2xl mb-2">{ticket.isSpecial ? '🌟' : '🎟️'}</div> | |
| <div class="font-mono text-sm opacity-80">#{ticket.number}</div> | |
| {#if ticket.isSpecial} | |
| <div class="text-xs text-yellow-500 mt-1">Special Ticket!</div> | |
| {/if} | |
| </div> | |
| </div> | |
| {/each} | |
| </div> | |
| </div> | |
| {/if} | |
| <!-- Loading State --> | |
| {#if loading && !ticketData} | |
| <div class="flex justify-center items-center py-12"> | |
| <div class="animate-spin text-4xl">🎰</div> | |
| </div> | |
| {/if} | |
| <!-- Error State --> | |
| {#if error} | |
| <div class="text-red-500 text-center py-8"> | |
| {error} | |
| <button | |
| class="mt-4 px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors" | |
| on:click={loadTickets} | |
| > | |
| Try Again | |
| </button> | |
| </div> | |
| {/if} | |
| </div> | |
| <style> | |
| .animate-gentle-pulse { | |
| animation: gentlePulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; | |
| } | |
| @keyframes gentlePulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.7; } | |
| } | |
| :global(.prize-card) { | |
| transform-style: preserve-3d; | |
| perspective: 1000px; | |
| } | |
| :global(.prize-card:hover) { | |
| transform: rotateY(10deg); | |
| } | |
| </style> | |
| === ./lib/components/SlotMachine.svelte === | |
| <!-- lib/components/SlotMachine.svelte --> | |
| <script> | |
| import { onMount, createEventDispatcher } from 'svelte'; | |
| const dispatch = createEventDispatcher(); | |
| export let spinning = false; | |
| export let finalValue = ''; | |
| export let duration = 2000; | |
| const chars = '0123456789ABCDEF'; | |
| let reels = Array(32).fill('0'); | |
| let spinInterval; | |
| let spinStart; | |
| onMount(() => { | |
| if (spinning) startSpin(); | |
| return () => clearInterval(spinInterval); | |
| }); | |
| $: if (spinning) { | |
| startSpin(); | |
| } else { | |
| stopSpin(); | |
| } | |
| function startSpin() { | |
| spinStart = Date.now(); | |
| spinInterval = setInterval(updateReels, 50); | |
| } | |
| function updateReels() { | |
| const elapsed = Date.now() - spinStart; | |
| const progress = Math.min(1, elapsed / duration); | |
| reels = reels.map((_, i) => { | |
| // Slow down based on position and progress | |
| const shouldStop = progress > (i / reels.length * 0.5); | |
| if (shouldStop && finalValue[i]) { | |
| return finalValue[i]; | |
| } | |
| return chars[Math.floor(Math.random() * chars.length)]; | |
| }); | |
| if (progress >= 1) { | |
| stopSpin(); | |
| dispatch('spinComplete'); | |
| } | |
| } | |
| function stopSpin() { | |
| clearInterval(spinInterval); | |
| reels = finalValue.split(''); | |
| } | |
| </script> | |
| <div class="font-mono text-2xl grid grid-flow-col gap-1 tracking-wider"> | |
| {#each reels as char, i} | |
| <div | |
| class="bg-gray-800 px-2 py-1 rounded-md shadow-inner transform transition-all duration-300" | |
| style="animation-delay: {i * 50}ms;" | |
| > | |
| {char} | |
| </div> | |
| {/each} | |
| </div> | |
| <style> | |
| .grid { | |
| grid-template-columns: repeat(32, 1fr); | |
| } | |
| @keyframes bounceIn { | |
| 0% { transform: scale(0.3); opacity: 0; } | |
| 50% { transform: scale(1.05); opacity: 0.8; } | |
| 70% { transform: scale(0.9); opacity: 0.9; } | |
| 100% { transform: scale(1); opacity: 1; } | |
| } | |
| div > div { | |
| animation: bounceIn 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55) backwards; | |
| } | |
| </style> | |
| === ./lib/db.js === | |
| import postgres from 'postgres'; | |
| //export const userDb = postgres('postgres://postgres:o3j0CfWk4x2R0T@x-psql-prod.flycast:5432/xshot_core_prod'); | |
| //export const tradeDb = postgres('postgres://postgres:o3j0CfWk4x2R0T@x-psql-prod.flycast:5432/holdings_db'); | |
| export const userDb = postgres('postgres://postgres:o3j0CfWk4x2R0T@localhost:5432/xshot_core_prod'); | |
| export const tradeDb = postgres('postgres://postgres:o3j0CfWk4x2R0T@localhost:5432/holdings_db'); | |
| === ./lib/index.js === | |
| // place files you want to import through the `$lib` alias in this folder. | |
| === ./lib/lottery-data.js === | |
| export let lotteryStats = { | |
| volumeUsd: 0, | |
| tradeCount: 0, | |
| ticketCount: 0, | |
| tickets: 0, | |
| totalPoolTickets: 0, | |
| winProbability: 0 | |
| }; | |
| export async function loadLotteryStats() { | |
| try { | |
| const response = await fetch('/api/lottery/stats'); | |
| if (!response.ok) throw new Error('Failed to fetch lottery stats'); | |
| const data = await response.json(); | |
| lotteryStats = { | |
| ...data, | |
| tickets: Math.floor(data.volumeUsd / 100), | |
| totalPoolTickets: Math.floor(data.volumeUsd / 100), | |
| winProbability: data.ticketCount > 0 ? data.ticketCount / Math.floor(data.volumeUsd / 100) : 0 | |
| }; | |
| return lotteryStats; | |
| } catch (err) { | |
| console.error('Error loading lottery stats:', err); | |
| return lotteryStats; | |
| } | |
| } | |
| === ./lib/prices.js === | |
| import { Connection, PublicKey } from '@solana/web3.js'; | |
| import { ethers } from 'ethers'; | |
| const SOL_RPC = 'https://hidden-small-wish.solana-mainnet.quiknode.pro/8aa3a97956500152c37afb6d81a1bcb34387d642/'; | |
| const ETH_RPC = 'https://bold-virulent-hill.quiknode.pro/6c308edd006d129e8c940757cfaac7ca5852eabe/'; | |
| // Multiple wallet support | |
| const WALLETS = [ | |
| { | |
| sol: '4fRQWKJzJYjKi9PHmpd2SXMkiQDgr6yuNADF1GL2wWZW', | |
| eth: '0xFDBa7f32A6f18cE1c97753A616DA32A67A3C93fA' | |
| } | |
| ]; | |
| async function getTokenPrices() { | |
| try { | |
| const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=solana,ethereum&vs_currencies=usd'); | |
| const data = await response.json(); | |
| return { | |
| solana: data.solana.usd, | |
| ethereum: data.ethereum.usd | |
| }; | |
| } catch (error) { | |
| console.error('Error fetching token prices:', error); | |
| return { solana: 0, ethereum: 0 }; | |
| } | |
| } | |
| async function getSolanaBalance(walletAddress) { | |
| try { | |
| const connection = new Connection(SOL_RPC); | |
| const pubKey = new PublicKey(walletAddress); | |
| // Remove the TS type assertion: | |
| const config = { | |
| commitment: 'confirmed', | |
| minContextSlot: 0 | |
| }; | |
| const balance = await connection.getBalance(pubKey, config); | |
| console.log('Solana Response:', { | |
| address: walletAddress, | |
| rawBalance: balance, | |
| convertedBalance: balance / 1e9 | |
| }); | |
| return balance / 1e9; // Convert lamports to SOL | |
| } catch (error) { | |
| console.error('Error fetching SOL balance:', error); | |
| return 0; | |
| } | |
| } | |
| async function getEthBalance(walletAddress) { | |
| try { | |
| const provider = new ethers.JsonRpcProvider(ETH_RPC); | |
| const balance = await provider.getBalance(walletAddress); | |
| return parseFloat(ethers.formatEther(balance)); | |
| } catch (error) { | |
| console.error('Error fetching ETH balance:', error); | |
| return 0; | |
| } | |
| } | |
| export async function getAllWalletBalances() { | |
| const prices = await getTokenPrices(); | |
| const walletBalances = []; | |
| for (const wallet of WALLETS) { | |
| const solBalance = await getSolanaBalance(wallet.sol); | |
| const ethBalance = await getEthBalance(wallet.eth); | |
| walletBalances.push({ | |
| solana: { | |
| wallet: wallet.sol, | |
| balance: solBalance, | |
| usdValue: solBalance * prices.solana, | |
| price: prices.solana | |
| }, | |
| ethereum: { | |
| wallet: wallet.eth, | |
| balance: ethBalance, | |
| usdValue: ethBalance * prices.ethereum, | |
| price: prices.ethereum | |
| } | |
| }); | |
| } | |
| return walletBalances; | |
| } | |
| === ./lib/stores/auth.js === | |
| import { writable } from 'svelte/store'; | |
| export const authToken = writable(localStorage.getItem('adminToken') || null); | |
| authToken.subscribe(value => { | |
| if (value) { | |
| localStorage.setItem('adminToken', value); | |
| } else { | |
| localStorage.removeItem('adminToken'); | |
| } | |
| }); | |
| === ./lib/utils/auth.js === | |
| // lib/utils/auth.js | |
| import { tradeDb } from '$lib/db'; | |
| import { verifyToken } from './jwt'; | |
| import { json } from '@sveltejs/kit'; | |
| export async function validateAdmin(userId) { | |
| try { | |
| const [admin] = await tradeDb` | |
| SELECT EXISTS( | |
| SELECT 1 FROM validate_admin_user(${userId}::bigint) as is_admin | |
| WHERE is_admin = true | |
| ) as is_admin | |
| `; | |
| return admin?.is_admin || false; | |
| } catch (err) { | |
| console.error('Admin validation error:', err); | |
| return false; | |
| } | |
| } | |
| export async function requireAdmin(request) { | |
| const authHeader = request.headers.get('Authorization'); | |
| if (!authHeader?.startsWith('Bearer ')) { | |
| return { | |
| error: json({ error: 'No token provided' }, { status: 401 }) | |
| }; | |
| } | |
| const token = authHeader.split(' ')[1]; | |
| const payload = verifyToken(token); | |
| if (!payload) { | |
| return { | |
| error: json({ error: 'Invalid token' }, { status: 401 }) | |
| }; | |
| } | |
| if (!payload.isAdmin) { | |
| return { | |
| error: json({ error: 'Unauthorized' }, { status: 403 }) | |
| }; | |
| } | |
| return { userId: payload.userId }; | |
| } | |
| export function getAdminIdFromRequest(request) { | |
| const authHeader = request.headers.get('Authorization'); | |
| if (!authHeader?.startsWith('Bearer ')) { | |
| return null; | |
| } | |
| const token = authHeader.split(' ')[1]; | |
| const payload = verifyToken(token); | |
| return payload?.userId || null; | |
| } | |
| === ./lib/utils/fetchWithAuth.js === | |
| // lib/utils/fetchWithAuth.js | |
| import { authToken } from '../stores/auth'; | |
| import { get } from 'svelte/store'; | |
| export async function fetchWithAuth(url, options = {}) { | |
| const token = get(authToken); | |
| if (!token) { | |
| throw new Error('No auth token'); | |
| } | |
| const headers = { | |
| ...options.headers, | |
| 'Authorization': `Bearer ${token}`, | |
| 'Content-Type': 'application/json' | |
| }; | |
| const response = await fetch(url, { | |
| ...options, | |
| headers | |
| }); | |
| if (response.status === 401) { | |
| authToken.set(null); | |
| throw new Error('Session expired'); | |
| } | |
| return response; | |
| } | |
| === ./lib/utils/jwt.js === | |
| import jwt from 'jsonwebtoken'; | |
| const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; // Use environment variable in production | |
| const JWT_EXPIRES_IN = '24h'; | |
| export function generateToken(userId, isAdmin) { | |
| return jwt.sign( | |
| { | |
| userId, | |
| isAdmin, | |
| iat: Math.floor(Date.now() / 1000) | |
| }, | |
| JWT_SECRET, | |
| { expiresIn: JWT_EXPIRES_IN } | |
| ); | |
| } | |
| export function verifyToken(token) { | |
| try { | |
| return jwt.verify(token, JWT_SECRET); | |
| } catch (err) { | |
| return null; | |
| } | |
| } | |
| === ./lib/utils.js === | |
| export function maskUserId(userId) { | |
| const str = userId.toString(); | |
| return `${str.slice(0, 2)}...${str.slice(-2)}`; | |
| } | |
| export function formatUsd(num) { | |
| return new Intl.NumberFormat('en-US', { | |
| style: 'currency', | |
| currency: 'USD', | |
| notation: 'compact', | |
| maximumFractionDigits: 2 | |
| }).format(num); | |
| } | |
| export function formatPercent(num, decimals = 4) { | |
| return (num * 100).toFixed(decimals) + '%'; | |
| } | |
| export function generateTicketCode() { | |
| const chars = '0123456789ABCDEF'; | |
| let code = ''; | |
| for (let i = 0; i < 32; i++) { | |
| code += chars[Math.floor(Math.random() * chars.length)]; | |
| } | |
| return code; | |
| } | |
| === ./routes/+layout.svelte === | |
| <script> | |
| import "../app.css"; | |
| </script> | |
| <slot /> | |
| === ./routes/+page.svelte === | |
| <script> | |
| import { onMount } from 'svelte'; | |
| import LotteryModal from '$lib/components/LotteryModal.svelte'; | |
| import { Ticket } from 'lucide-svelte'; | |
| import { getAllWalletBalances } from '$lib/prices'; | |
| import { lotteryStats, loadLotteryStats } from "$lib/lottery-data"; | |
| let rankings = []; | |
| let searchTerm = ''; | |
| let targetVolume = null; | |
| let loading = false; | |
| let error = ''; | |
| let darkMode = true; | |
| let showLotteryModal = false; | |
| let walletBalances = []; | |
| let totalSolUsd = 0; | |
| let totalEthUsd = 0; | |
| let searchInputRef; | |
| let showTable = false; // Controls the visibility of the Rankings Table | |
| const DISTRIBUTION = { | |
| first: 0.5, | |
| second: 0.3, | |
| third: 0.2 | |
| }; | |
| let daysUntilDraw; | |
| onMount(async () => { | |
| await loadLotteryStats(); | |
| loadRankings(); | |
| loadPrizes(); | |
| const endDate = new Date(2025, 1, 28, 23, 59, 59); // Feb 28, 2025 | |
| const now = new Date(); | |
| daysUntilDraw = Math.ceil((endDate - now) / (1000 * 60 * 60 * 24)); | |
| if (window.matchMedia('(prefers-color-scheme: light)').matches) { | |
| darkMode = false; | |
| } | |
| }); | |
| async function loadRankings() { | |
| try { | |
| const response = await fetch('/api/rankings'); | |
| if (!response.ok) throw new Error('Failed to fetch rankings'); | |
| rankings = await response.json(); | |
| } catch (err) { | |
| error = 'Failed to load rankings'; | |
| } | |
| } | |
| async function loadPrizes() { | |
| try { | |
| walletBalances = await getAllWalletBalances(); | |
| // 50% of total for each chain | |
| totalSolUsd = walletBalances.reduce((sum, w) => sum + w.solana.usdValue, 0) * 0.5; | |
| totalEthUsd = walletBalances.reduce((sum, w) => sum + w.ethereum.usdValue, 0) * 0.5; | |
| } catch (err) { | |
| console.error('Error loading prizes:', err); | |
| } | |
| } | |
| async function searchUser() { | |
| if (!searchTerm) return; | |
| loading = true; | |
| error = ''; | |
| try { | |
| const response = await fetch(`/api/search/${encodeURIComponent(searchTerm)}`); | |
| if (!response.ok) throw new Error('Failed to fetch user data'); | |
| targetVolume = await response.json(); | |
| } catch (err) { | |
| error = 'User not found'; | |
| targetVolume = null; | |
| } finally { | |
| loading = false; | |
| } | |
| } | |
| function formatUsd(num) { | |
| return new Intl.NumberFormat('en-US', { | |
| style: 'currency', | |
| currency: 'USD', | |
| notation: 'compact', | |
| maximumFractionDigits: 2 | |
| }).format(num); | |
| } | |
| function toggleDarkMode() { | |
| darkMode = !darkMode; | |
| } | |
| </script> | |
| <!-- Outer Container with smooth transitions --> | |
| <div class={`min-h-screen transition-all duration-300 ${darkMode ? 'bg-gray-900 text-gray-100' : 'bg-gray-50 text-gray-900'}`}> | |
| <div class="container mx-auto px-3 sm:px-4 py-4 sm:py-6 lg:py-8 max-w-5xl relative"> | |
| <!-- Dark Mode Toggle with improved positioning --> | |
| <div class="absolute top-2 right-2 sm:top-4 sm:right-4 z-10"> | |
| <button | |
| on:click={toggleDarkMode} | |
| class="p-2 rounded-full hover:bg-opacity-20 hover:bg-gray-500 transition-all duration-300 transform hover:scale-110" | |
| > | |
| {#if darkMode} | |
| <span class="text-xl sm:text-2xl">🌞</span> | |
| {:else} | |
| <span class="text-xl sm:text-2xl">🌙</span> | |
| {/if} | |
| </button> | |
| </div> | |
| <div class={`mb-4 sm:mb-6 p-4 sm:p-6 rounded-xl sm:rounded-2xl shadow-xl | |
| backdrop-filter backdrop-blur-sm transition-all duration-300 | |
| ${darkMode ? 'bg-gray-800/90 shadow-gray-900/50' : 'bg-white/90 shadow-gray-200/50'}`}> | |
| <div class="text-center space-y-4 md:space-y-6"> | |
| <!-- Improved Title Section --> | |
| <div class="relative inline-block"> | |
| <h2 class="text-xl sm:text-2xl lg:text-4xl font-bold flex items-center justify-center gap-3 p-2"> | |
| <span class="trophy-icon text-2xl sm:text-3xl lg:text-4xl animate-gentle-pulse">🏆</span> | |
| <span class="gradient-text tracking-tight">January Trading Competition</span> | |
| <span class="trophy-icon text-2xl sm:text-3xl lg:text-4xl animate-gentle-pulse">🏆</span> | |
| </h2> | |
| </div> | |
| <!-- Enhanced Prize Pool Amount --> | |
| <div class="prize-pool transform hover:scale-105 transition-all duration-300"> | |
| <div class="text-3xl sm:text-4xl lg:text-6xl xl:text-7xl font-extrabold bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 bg-clip-text text-transparent tracking-tight"> | |
| {formatUsd(totalSolUsd + totalEthUsd)} | |
| </div> | |
| <div class="text-base sm:text-lg lg:text-xl font-medium text-indigo-400 mt-2"> | |
| Total Prize Pool to Win | |
| </div> | |
| </div> | |
| <!-- Redesigned Distribution Cards --> | |
| <div class="max-w-2xl mx-auto p-3 rounded-xl bg-gray-700/20 backdrop-filter backdrop-blur-sm transition-all duration-300"> | |
| <div class="grid grid-cols-1 sm:grid-cols-3 gap-3"> | |
| {#each [ | |
| { place: "1st Place", emoji: "🥇", amount: DISTRIBUTION.first, gradient: "from-blue-500 to-indigo-500" }, | |
| { place: "2nd Place", emoji: "🥈", amount: DISTRIBUTION.second, gradient: "from-indigo-500 to-purple-500" }, | |
| { place: "3rd Place", emoji: "🥉", amount: DISTRIBUTION.third, gradient: "from-purple-500 to-pink-500" } | |
| ] as { place, emoji, amount, gradient }} | |
| <div class="prize-card group"> | |
| <div class="flex flex-col items-center space-y-2"> | |
| <div class="text-base sm:text-lg font-medium flex items-center gap-2"> | |
| <span class="text-xl sm:text-2xl">{emoji}</span> | |
| {place} | |
| </div> | |
| <div class="font-bold text-lg sm:text-xl lg:text-2xl bg-gradient-to-r {gradient} bg-clip-text text-transparent"> | |
| {formatUsd((totalSolUsd + totalEthUsd) * amount)} | |
| </div> | |
| <div class="text-xs sm:text-sm text-gray-400 group-hover:text-gray-300 transition-colors duration-300"> | |
| {(amount * 100)}% of prize pool | |
| </div> | |
| </div> | |
| </div> | |
| {/each} | |
| </div> | |
| </div> | |
| <!-- Enhanced Competition Explainer --> | |
| <div class="p-4 rounded-xl bg-gray-700/20 backdrop-filter backdrop-blur-sm transition-all duration-300 | |
| max-w-2xl mx-auto hover:bg-gray-700/30"> | |
| <h3 class="text-base sm:text-lg lg:text-xl font-semibold mb-2 gradient-text"> | |
| Why Join the Competition? | |
| </h3> | |
| <p class="text-sm sm:text-base leading-relaxed text-gray-300"> | |
| The 3 biggest volume makers on XSHOT will split | |
| <span class="font-semibold text-indigo-400">50% of the total generated fees</span> | |
| on XSHOT throughout the month, which is | |
| <span class="font-semibold text-indigo-400">0.6% of every trade</span> from most users. | |
| This is how top competitors can earn a substantial reward, and why we see this as | |
| an exciting opportunity to <em>share the bread</em> and make trading fun for everyone. | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Wallet Info Cards --> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6 mb-6"> | |
| <!-- Solana Card --> | |
| <div class={`p-4 sm:p-6 rounded-xl shadow-lg transition-all duration-300 transform hover:scale-102 | |
| ${darkMode ? 'bg-gray-800/90' : 'bg-white/90'}`}> | |
| <div class="text-lg sm:text-xl font-medium mb-2"> | |
| Solana Wallet | |
| </div> | |
| <div class="text-2xl sm:text-3xl font-extrabold mb-4 text-indigo-500"> | |
| {formatUsd(totalSolUsd)} | |
| </div> | |
| <div class="text-sm"> | |
| <a | |
| href={`https://solscan.io/account/${walletBalances[0]?.solana.wallet}`} | |
| target="_blank" | |
| class="text-indigo-600 hover:text-indigo-500 hover:underline dark:text-indigo-400 transition-colors" | |
| > | |
| Verify Prize Pool on Solscan ↗ | |
| </a> | |
| </div> | |
| </div> | |
| <!-- Ethereum Card --> | |
| <div class={`p-4 sm:p-6 rounded-xl shadow-lg transition-all duration-300 transform hover:scale-102 | |
| ${darkMode ? 'bg-gray-800/90' : 'bg-white/90'}`}> | |
| <div class="text-lg sm:text-xl font-medium mb-2"> | |
| Ethereum Wallet | |
| </div> | |
| <div class="text-2xl sm:text-3xl font-extrabold mb-4 text-indigo-500"> | |
| {formatUsd(totalEthUsd)} | |
| </div> | |
| <div class="text-sm"> | |
| <a | |
| href={`https://etherscan.io/address/${walletBalances[0]?.ethereum.wallet}`} | |
| target="_blank" | |
| class="text-indigo-600 hover:text-indigo-500 hover:underline dark:text-indigo-400 transition-colors" | |
| > | |
| Verify Prize Pool on Etherscan ↗ | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Search Section --> | |
| <div class={`mb-6 p-4 sm:p-6 rounded-xl shadow-lg backdrop-filter backdrop-blur-sm | |
| ${darkMode ? 'bg-gray-800/90' : 'bg-white/90'}`}> | |
| <div class="max-w-xl mx-auto space-y-4"> | |
| <div class="relative"> | |
| <input | |
| bind:this={searchInputRef} | |
| type="text" | |
| bind:value={searchTerm} | |
| on:keydown={(e) => e.key === 'Enter' && searchUser()} | |
| class={`w-full px-4 py-3 rounded-lg border text-base | |
| focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-all duration-300 | |
| ${darkMode | |
| ? 'bg-gray-700/70 border-gray-600 text-white placeholder-gray-300' | |
| : 'bg-white border-gray-300 text-gray-900 placeholder-gray-500' | |
| }`} | |
| placeholder="Enter your User ID" | |
| /> | |
| <button | |
| on:click={searchUser} | |
| disabled={loading} | |
| class="absolute right-2 top-2 bg-indigo-600 text-white px-4 py-1 rounded-md | |
| hover:bg-indigo-700 disabled:opacity-50 transition-all duration-300" | |
| > | |
| {loading ? 'Searching...' : 'Search'} | |
| </button> | |
| </div> | |
| <div class={`flex items-center justify-center gap-4 p-3 rounded-lg bg-opacity-50 | |
| ${darkMode ? 'bg-gray-700/50' : 'bg-gray-100/50'}`}> | |
| <div class="text-sm font-medium">Find your User ID:</div> | |
| <div class="font-mono text-sm bg-indigo-500/20 px-3 py-1 rounded"> | |
| Type /id in XSHOT | |
| </div> | |
| </div> | |
| {#if error} | |
| <div class="text-red-500 text-center text-sm animate-fade-in">{error}</div> | |
| {/if} | |
| {#if targetVolume} | |
| <div class={`mt-4 p-4 rounded-lg animate-fade-in | |
| ${darkMode ? 'bg-gray-700/50' : 'bg-gray-100/50'}`}> | |
| <h3 class="text-lg font-semibold mb-3 text-center gradient-text">Your Trading Stats</h3> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> | |
| <div class="p-4 rounded-xl bg-green-500/10 backdrop-filter backdrop-blur-sm"> | |
| <div class="text-sm opacity-80">Your Current Volume</div> | |
| <div class="text-xl font-bold text-green-400"> | |
| {formatUsd(targetVolume.currentVolumeUsd)} | |
| </div> | |
| </div> | |
| <div class="p-4 rounded-xl bg-purple-500/10 backdrop-filter backdrop-blur-sm"> | |
| <div class="text-sm opacity-80">Volume Needed for Top 3</div> | |
| <div class="text-xl font-bold text-purple-400"> | |
| {formatUsd(targetVolume.volumeNeededForTop3Usd)} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/if} | |
| </div> | |
| </div> | |
| <!-- Add to your +page.svelte where you want the lottery section --> | |
| <div class="max-w-2xl mx-auto mb-8"> | |
| <div class={`p-4 sm:p-6 rounded-xl shadow-xl transition-all duration-300 | |
| ${darkMode ? 'bg-gray-800/90 shadow-gray-900/50' : 'bg-white/90 shadow-gray-200/50'} | |
| backdrop-filter backdrop-blur-sm`}> | |
| <div class="text-center space-y-4"> | |
| <!-- Title with matching gradient --> | |
| <div class="relative inline-block"> | |
| <h2 class="text-xl sm:text-2xl lg:text-3xl font-bold flex items-center justify-center gap-3 p-2"> | |
| <span class="text-yellow-500 animate-pulse">⭐</span> | |
| <span class="gradient-text tracking-tight">MEGA JACKPOT LOTTERY</span> | |
| <span class="text-yellow-500 animate-pulse">⭐</span> | |
| </h2> | |
| </div> | |
| <!-- Countdown Banner --> | |
| <div class="bg-gradient-to-r from-yellow-500/20 to-pink-500/20 rounded-full px-4 py-2 inline-block"> | |
| <span class="text-yellow-400 font-bold animate-pulse"> | |
| 52 Days Until Draw! 🎰 | |
| </span> | |
| </div> | |
| <!-- Ticket Info --> | |
| <div class="bg-gradient-to-br from-purple-500/10 to-pink-500/10 rounded-xl p-4"> | |
| <h3 class="text-lg font-bold text-purple-400 mb-2"> | |
| Your Lucky Tickets: <span class="text-2xl">🎟️ {Math.floor((targetVolume?.currentVolumeUsd || 0) / 100)}</span> | |
| </h3> | |
| <p class="text-sm text-gray-400"> | |
| Each $100 traded = 1 ticket to victory! | |
| </p> | |
| </div> | |
| <!-- Call to Action --> | |
| {#if searchTerm} | |
| <button | |
| on:click={() => showLotteryModal = true} | |
| class="px-6 py-3 bg-gradient-to-r from-purple-600 to-pink-600 text-white | |
| font-semibold rounded-xl shadow-lg transform hover:scale-105 | |
| transition-all duration-300" | |
| > | |
| View My Tickets 🎟️ | |
| </button> | |
| {:else} | |
| <div class="text-sm text-purple-400"> | |
| Search your User ID above to view your tickets | |
| </div> | |
| {/if} | |
| </div> | |
| </div> | |
| </div> | |
| <!-- CTA Button --> | |
| <div class="text-center mb-6"> | |
| <a | |
| href="https://t.me/xshot_trading_bot" | |
| target="_blank" | |
| class="inline-block px-6 sm:px-8 py-3 sm:py-4 bg-indigo-600 text-white font-bold rounded-xl | |
| shadow-lg transform hover:scale-105 hover:bg-indigo-500 transition-all duration-300" | |
| > | |
| 🚀 Start Trading with XSHOT Now | |
| </a> | |
| </div> | |
| <!-- Rankings Toggle --> | |
| <div class="text-center mb-4"> | |
| <button | |
| on:click={() => (showTable = !showTable)} | |
| class="flex mx-auto items-center justify-center gap-2 text-indigo-600 dark:text-indigo-400 | |
| font-bold text-lg sm:text-xl transform hover:scale-110 transition-all duration-300" | |
| > | |
| {#if showTable} | |
| <span>Hide Rankings</span> | |
| <span class="text-2xl sm:text-3xl rotate-180 inline-block transition-transform duration-300">⬇</span> | |
| {:else} | |
| <span>View Rankings</span> | |
| <span class="text-2xl sm:text-3xl inline-block transition-transform duration-300">⬇</span> | |
| {/if} | |
| </button> | |
| </div> | |
| <!-- Rankings Table --> | |
| {#if showTable} | |
| <div class={`rounded-xl shadow-lg overflow-hidden mt-4 mx-auto backdrop-filter backdrop-blur-sm | |
| ${darkMode ? 'bg-gray-800/90' : 'bg-white/90'}`}> | |
| <div class="overflow-x-auto"> | |
| <table class="min-w-full"> | |
| <!-- In your rankings table section in +page.svelte --> | |
| <thead class={darkMode ? 'bg-gray-700/50 text-gray-200' : 'bg-gray-100/50 text-gray-800'}> | |
| <tr> | |
| <th class="px-4 sm:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Rank</th> | |
| <th class="px-4 sm:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">User ID</th> | |
| <th class="px-4 sm:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Volume (USD)</th> | |
| <th class="px-4 sm:px-6 py-3 text-left text-xs font-medium uppercase tracking-wider">Lottery Tickets</th> | |
| </tr> | |
| </thead> | |
| <tbody class={`divide-y ${darkMode ? 'divide-gray-700/50' : 'divide-gray-200/50'}`}> | |
| {#each rankings as { rank, userId, volumeUsd, isTop3 }} | |
| {#if volumeUsd > 0} | |
| <tr class={` | |
| transition-colors duration-200 | |
| ${darkMode ? 'hover:bg-gray-700/50' : 'hover:bg-gray-50/50'} | |
| ${isTop3 ? (darkMode ? 'bg-gray-700/30' : 'bg-gray-50/30') : ''} | |
| `}> | |
| <td class="px-4 sm:px-6 py-3 whitespace-nowrap"> | |
| {#if rank <= 3} | |
| <span class="text-xl">{rank === 1 ? '🥇' : rank === 2 ? '🥈' : '🥉'}</span> | |
| {:else} | |
| {rank} | |
| {/if} | |
| </td> | |
| <td class="px-4 sm:px-6 py-3 whitespace-nowrap font-medium"> | |
| {userId} | |
| </td> | |
| <td class="px-4 sm:px-6 py-3 whitespace-nowrap"> | |
| {formatUsd(volumeUsd)} | |
| </td> | |
| <td class="px-4 sm:px-6 py-3 whitespace-nowrap font-mono text-indigo-400"> | |
| {Math.floor(volumeUsd / 100)} 🎟️ | |
| </td> | |
| </tr> | |
| {/if} | |
| {/each} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| {/if} | |
| </div> | |
| <!-- Footer --> | |
| <footer class="mt-8 py-4 text-center text-sm text-gray-500 dark:text-gray-400 border-t | |
| border-gray-200/20 dark:border-gray-700/20 backdrop-filter backdrop-blur-sm"> | |
| <div class="container mx-auto px-4"> | |
| <p class="mb-2 text-xs sm:text-sm">Explore more on:</p> | |
| <div class="flex items-center justify-center gap-4 text-sm sm:text-base"> | |
| <a | |
| href="https://www.xprojecterc.com/" | |
| target="_blank" | |
| class="text-indigo-500 hover:text-indigo-400 hover:underline dark:text-indigo-400 | |
| transition-colors duration-300" | |
| > | |
| XProject Main Website | |
| </a> | |
| <span class="text-gray-400">|</span> | |
| <a | |
| href="https://xshot.xprojecterc.com/" | |
| target="_blank" | |
| class="text-indigo-500 hover:text-indigo-400 hover:underline dark:text-indigo-400 | |
| transition-colors duration-300" | |
| > | |
| XShot Website | |
| </a> | |
| </div> | |
| </div> | |
| </footer> | |
| {#if showLotteryModal} | |
| <LotteryModal | |
| userId={parseInt(searchTerm) || 0} | |
| isOpen={showLotteryModal} | |
| onClose={() => showLotteryModal = false} | |
| {darkMode} | |
| /> | |
| {/if} | |
| </div> | |
| <style> | |
| /* Modern font setup */ | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap'); | |
| :global(html) { | |
| scroll-behavior: smooth; | |
| } | |
| :global(html), | |
| :global(body), | |
| :global(button), | |
| :global(input), | |
| :global(select), | |
| :global(textarea) { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | |
| } | |
| /* Elegant scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| height: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #6366f1; | |
| border-radius: 3px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #4f46e5; | |
| } | |
| /* Enhanced custom classes */ | |
| .gradient-text { | |
| @apply bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400 bg-clip-text text-transparent; | |
| } | |
| .prize-card { | |
| @apply p-4 rounded-xl bg-gray-800/50 backdrop-filter backdrop-blur-sm shadow-lg transition-all duration-300; | |
| } | |
| .trophy-icon { | |
| @apply text-yellow-500; | |
| } | |
| /* Custom animations */ | |
| @keyframes gentle-pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.8; } | |
| } | |
| .animate-gentle-pulse { | |
| animation: gentle-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; | |
| } | |
| .animate-fade-in { | |
| animation: fadeIn 0.5s ease-in-out; | |
| } | |
| @keyframes fadeIn { | |
| from { opacity: 0; transform: translateY(-10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| </style> | |
| === ./routes/+page.svelte.bak === | |
| <script> | |
| import { onMount } from 'svelte'; | |
| import { getAllWalletBalances } from '$lib/prices'; | |
| let rankings = []; | |
| let searchTerm = ''; | |
| let targetVolume = null; | |
| let loading = false; | |
| let error = ''; | |
| let darkMode = true; | |
| let walletBalances = []; | |
| let totalSolUsd = 0; | |
| let totalEthUsd = 0; | |
| let searchInputRef; | |
| const DISTRIBUTION = { | |
| first: 0.5, | |
| second: 0.3, | |
| third: 0.2 | |
| }; | |
| onMount(async () => { | |
| loadRankings(); | |
| loadPrizes(); | |
| if (window.matchMedia('(prefers-color-scheme: light)').matches) { | |
| darkMode = false; | |
| } | |
| }); | |
| async function loadRankings() { | |
| try { | |
| const response = await fetch('/api/rankings'); | |
| if (!response.ok) throw new Error('Failed to fetch rankings'); | |
| rankings = await response.json(); | |
| } catch (err) { | |
| error = 'Failed to load rankings'; | |
| } | |
| } | |
| async function loadPrizes() { | |
| try { | |
| walletBalances = await getAllWalletBalances(); | |
| totalSolUsd = walletBalances.reduce((sum, wallet) => sum + wallet.solana.usdValue, 0) * 0.5; // 50% of total | |
| totalEthUsd = walletBalances.reduce((sum, wallet) => sum + wallet.ethereum.usdValue, 0) * 0.5; // 50% of total | |
| } catch (err) { | |
| console.error('Error loading prizes:', err); | |
| } | |
| } | |
| async function searchUser() { | |
| if (!searchTerm) return; | |
| loading = true; | |
| error = ''; | |
| try { | |
| const response = await fetch(`/api/search/${encodeURIComponent(searchTerm)}`); | |
| if (!response.ok) throw new Error('Failed to fetch user data'); | |
| targetVolume = await response.json(); | |
| } catch (err) { | |
| error = 'User not found'; | |
| targetVolume = null; | |
| } finally { | |
| loading = false; | |
| } | |
| } | |
| function formatUsd(num) { | |
| return new Intl.NumberFormat('en-US', { | |
| style: 'currency', | |
| currency: 'USD', | |
| notation: 'compact', | |
| maximumFractionDigits: 2 | |
| }).format(num); | |
| } | |
| function toggleDarkMode() { | |
| darkMode = !darkMode; | |
| } | |
| </script> | |
| <div class={`min-h-screen transition-colors duration-200 ${darkMode ? 'bg-gray-900 text-gray-100' : 'bg-gray-50 text-gray-900'}`}> | |
| <div class="container mx-auto px-4 py-8 max-w-6xl"> | |
| <!-- Dark Mode Toggle --> | |
| <div class="absolute top-4 right-4"> | |
| <button | |
| on:click={toggleDarkMode} | |
| class="p-2 rounded-full hover:bg-opacity-20 hover:bg-gray-500 transition-colors" | |
| > | |
| {#if darkMode} | |
| 🌞 | |
| {:else} | |
| 🌙 | |
| {/if} | |
| </button> | |
| </div> | |
| <!-- Prize Pool Banner --> | |
| <div class={`mb-8 p-6 rounded-lg shadow-lg ${darkMode ? 'bg-gradient-to-r from-blue-900/50 to-purple-900/50' : 'bg-gradient-to-r from-blue-100 to-purple-100'}`}> | |
| <div class="text-center"> | |
| <!-- Main Title --> | |
| <h2 class="text-4xl font-bold mb-6 bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-purple-400"> | |
| 🏆 Monthly Trading Competition 🏆 | |
| </h2> | |
| <!-- Grand Total Prize Display --> | |
| <div class="mb-8 relative"> | |
| <div class="absolute inset-0 bg-gradient-to-r from-yellow-400 via-red-500 to-pink-500 opacity-10 blur-xl"></div> | |
| <div class="relative"> | |
| <div class="text-7xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-yellow-400 via-red-500 to-pink-500 animate-pulse mb-2"> | |
| {formatUsd(totalSolUsd + totalEthUsd)} | |
| </div> | |
| <div class="text-2xl font-semibold bg-clip-text text-transparent bg-gradient-to-r from-yellow-400 to-pink-500"> | |
| Total Prize Pool to Win | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Prize Distribution --> | |
| <div class="max-w-2xl mx-auto mb-8 p-6 rounded-xl {darkMode ? 'bg-gray-800/50' : 'bg-white/70'}"> | |
| <div class="grid grid-cols-3 gap-6 mb-6"> | |
| <div class="p-4 rounded-lg bg-opacity-20 {darkMode ? 'bg-blue-500/20' : 'bg-blue-100'}"> | |
| <div class="font-medium text-lg mb-1">🥇 1st Place</div> | |
| <div class="font-bold text-xl">{formatUsd((totalSolUsd + totalEthUsd) * DISTRIBUTION.first)}</div> | |
| <div class="text-sm opacity-70">50% of prize pool</div> | |
| </div> | |
| <div class="p-4 rounded-lg bg-opacity-20 {darkMode ? 'bg-blue-500/20' : 'bg-blue-100'}"> | |
| <div class="font-medium text-lg mb-1">🥈 2nd Place</div> | |
| <div class="font-bold text-xl">{formatUsd((totalSolUsd + totalEthUsd) * DISTRIBUTION.second)}</div> | |
| <div class="text-sm opacity-70">30% of prize pool</div> | |
| </div> | |
| <div class="p-4 rounded-lg bg-opacity-20 {darkMode ? 'bg-blue-500/20' : 'bg-blue-100'}"> | |
| <div class="font-medium text-lg mb-1">🥉 3rd Place</div> | |
| <div class="font-bold text-xl">{formatUsd((totalSolUsd + totalEthUsd) * DISTRIBUTION.third)}</div> | |
| <div class="text-sm opacity-70">20% of prize pool</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Prize Categories --> | |
| <div class="grid md:grid-cols-2 gap-8 mb-8"> | |
| <!-- Solana Prize Pool --> | |
| <div class={`p-6 rounded-lg transform transition-all hover:scale-105 ${darkMode ? 'bg-blue-900/30' : 'bg-blue-50'}`}> | |
| <div class="text-xl font-medium mb-2">Solana Wallet</div> | |
| <div class="text-3xl font-bold mb-4 bg-clip-text text-transparent bg-gradient-to-r from-[#00FFA3] to-[#DC1FFF]"> | |
| {formatUsd(totalSolUsd)} | |
| </div> | |
| <div class="text-sm"> | |
| <a href={`https://solscan.io/account/${walletBalances[0]?.solana.wallet}`} | |
| target="_blank" | |
| class="text-blue-400 hover:underline"> | |
| Verify Prize Pool on Solscan ↗ | |
| </a> | |
| </div> | |
| </div> | |
| <!-- ETH Prize Pool --> | |
| <div class={`p-6 rounded-lg transform transition-all hover:scale-105 ${darkMode ? 'bg-purple-900/30' : 'bg-purple-50'}`}> | |
| <div class="text-xl font-medium mb-2">Ethereum Wallet</div> | |
| <div class="text-3xl font-bold mb-4 bg-clip-text text-transparent bg-gradient-to-r from-[#454A75] to-[#8A92B2]"> | |
| {formatUsd(totalEthUsd)} | |
| </div> | |
| <div class="text-sm"> | |
| <a href={`https://etherscan.io/address/${walletBalances[0]?.ethereum.wallet}`} | |
| target="_blank" | |
| class="text-purple-400 hover:underline"> | |
| Verify Prize Pool on Etherscan ↗ | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Search Section with ID Instructions --> | |
| <div class={`mb-8 p-6 rounded-lg shadow-lg ${darkMode ? 'bg-gray-800' : 'bg-white'}`}> | |
| <div class="max-w-xl mx-auto"> | |
| <div class="relative"> | |
| <input | |
| bind:this={searchInputRef} | |
| type="text" | |
| bind:value={searchTerm} | |
| on:keydown={(e) => e.key === 'Enter' && searchUser()} | |
| class={`w-full px-4 py-3 rounded-lg border ${ | |
| darkMode | |
| ? 'bg-gray-700 border-gray-600 text-white placeholder-gray-400' | |
| : 'bg-white border-gray-300 text-gray-900 placeholder-gray-500' | |
| } focus:outline-none focus:ring-2 focus:ring-indigo-500`} | |
| placeholder="Enter your User ID" | |
| /> | |
| <button | |
| on:click={searchUser} | |
| disabled={loading} | |
| class="absolute right-2 top-2 bg-indigo-600 text-white px-4 py-1 rounded-md hover:bg-indigo-700 disabled:opacity-50 transition-colors" | |
| > | |
| {loading ? 'Searching...' : 'Search'} | |
| </button> | |
| </div> | |
| <div class="mt-4 flex items-center justify-center gap-4 p-3 rounded-lg bg-opacity-50 {darkMode ? 'bg-gray-700' : 'bg-gray-100'}"> | |
| <div class="text-sm font-medium">Find your User ID:</div> | |
| <div class="font-mono text-sm bg-opacity-20 bg-blue-500 px-3 py-1 rounded">Type /id in XSHOT</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Key Info & CTA --> | |
| <div class="space-y-4"> | |
| <a href="https://t.me/xshot_trading_bot" | |
| target="_blank" | |
| class="inline-block px-8 py-4 bg-gradient-to-r from-indigo-500 to-purple-600 text-white font-bold rounded-xl shadow-lg transform hover:scale-105 transition-transform"> | |
| 🚀 Start Trading with XSHOT Now | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Search Section --> | |
| <div class={`mb-8 p-6 rounded-lg shadow-lg ${darkMode ? 'bg-gray-800' : 'bg-white'}`}> | |
| <div class="max-w-xl mx-auto"> | |
| <h2 class="text-2xl font-semibold mb-4">Check Your Position</h2> | |
| <div class="space-y-4"> | |
| <div class="relative"> | |
| <input | |
| bind:this={searchInputRef} | |
| type="text" | |
| bind:value={searchTerm} | |
| on:keydown={(e) => e.key === 'Enter' && searchUser()} | |
| class={`w-full px-4 py-3 rounded-lg border ${ | |
| darkMode | |
| ? 'bg-gray-700 border-gray-600 text-white placeholder-gray-400' | |
| : 'bg-white border-gray-300 text-gray-900 placeholder-gray-500' | |
| } focus:outline-none focus:ring-2 focus:ring-indigo-500`} | |
| placeholder="Enter your User ID" | |
| /> | |
| <button | |
| on:click={searchUser} | |
| disabled={loading} | |
| class="absolute right-2 top-2 bg-indigo-600 text-white px-4 py-1 rounded-md hover:bg-indigo-700 disabled:opacity-50 transition-colors" | |
| > | |
| {loading ? 'Searching...' : 'Search'} | |
| </button> | |
| </div> | |
| {#if error} | |
| <div class="text-red-500 text-sm">{error}</div> | |
| {/if} | |
| {#if targetVolume} | |
| <div class={`mt-4 p-4 rounded-lg ${darkMode ? 'bg-gray-700' : 'bg-gray-100'}`}> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div class="p-4 rounded-lg bg-opacity-20 bg-green-500"> | |
| <div class="text-sm opacity-80">Your Current Volume</div> | |
| <div class="text-xl font-bold">{formatUsd(targetVolume.currentVolumeUsd)}</div> | |
| </div> | |
| <div class="p-4 rounded-lg bg-opacity-20 bg-purple-500"> | |
| <div class="text-sm opacity-80">Volume Needed for Top 3</div> | |
| <div class="text-xl font-bold">{formatUsd(targetVolume.volumeNeededForTop3Usd)}</div> | |
| </div> | |
| </div> | |
| </div> | |
| {/if} | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Rankings Table --> | |
| <div class={`rounded-lg shadow-lg overflow-hidden ${darkMode ? 'bg-gray-800' : 'bg-white'}`}> | |
| <div class="overflow-x-auto"> | |
| <table class="min-w-full"> | |
| <thead class={darkMode ? 'bg-gray-700' : 'bg-gray-50'}> | |
| <tr> | |
| <th class="px-6 py-4 text-left text-xs font-medium uppercase tracking-wider">Rank</th> | |
| <th class="px-6 py-4 text-left text-xs font-medium uppercase tracking-wider">User ID</th> | |
| <th class="px-6 py-4 text-left text-xs font-medium uppercase tracking-wider">Volume (USD)</th> | |
| </tr> | |
| </thead> | |
| <tbody class="divide-y divide-gray-700"> | |
| {#each rankings as { rank, userId, volumeUsd, isTop3 }} | |
| <tr class={` | |
| ${darkMode ? 'hover:bg-gray-700' : 'hover:bg-gray-50'} | |
| transition-colors | |
| ${isTop3 ? (darkMode ? 'bg-gray-700/50' : 'bg-gray-50/50') : ''} | |
| `}> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| {#if rank <= 3} | |
| <span class="text-xl">{rank === 1 ? '🥇' : rank === 2 ? '🥈' : '🥉'}</span> | |
| {:else} | |
| {rank} | |
| {/if} | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap font-medium"> | |
| {userId} | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap">{formatUsd(volumeUsd)}</td> | |
| </tr> | |
| {/each} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <style> | |
| ::-webkit-scrollbar { | |
| width: 10px; | |
| height: 10px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #4f46e5; | |
| border-radius: 5px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: #4338ca; | |
| } | |
| </style> | |
| === ./routes/admin/login/+server.js === | |
| // routes/api/admin/login/+server.js | |
| import { json } from '@sveltejs/kit'; | |
| import { validateAdmin } from '$lib/utils/auth.js'; | |
| import { generateToken } from '$lib/utils/jwt.js'; | |
| export async function POST({ request }) { | |
| try { | |
| const { userId } = await request.json(); | |
| if (!userId) { | |
| return json({ error: 'User ID required' }, { status: 400 }); | |
| } | |
| const isAdmin = await validateAdmin(userId); | |
| if (!isAdmin) { | |
| return json({ error: 'Unauthorized' }, { status: 403 }); | |
| } | |
| const token = generateToken(userId, true); | |
| return json({ token }); | |
| } catch (err) { | |
| console.error('Login error:', err); | |
| return json({ error: 'Login failed' }, { status: 500 }); | |
| } | |
| } | |
| === ./routes/admin/lottery/+page.svelte === | |
| <script> | |
| import {onMount} from 'svelte'; | |
| import {authToken} from '$lib/stores/auth'; | |
| import {fetchWithAuth} from '$lib/utils/fetchWithAuth'; | |
| let userId = ''; | |
| let startDate = ''; | |
| let endDate = ''; | |
| let loading = false; | |
| let error = ''; | |
| let success = ''; | |
| let draws = []; | |
| let isAdmin = false; | |
| let verifyLoading = false; | |
| let adminVerified = false; | |
| async function login() { | |
| try { | |
| loading = true; | |
| error = ''; | |
| const response = await fetch('/api/admin/login', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({userId}) | |
| }); | |
| const data = await response.json(); | |
| if (!response.ok) { | |
| throw new Error(data.error); | |
| } | |
| authToken.set(data.token); | |
| } catch (err) { | |
| error = err.message; | |
| } finally { | |
| loading = false; | |
| } | |
| } | |
| onMount(async () => { | |
| // Check if we have a stored admin ID | |
| const storedId = localStorage.getItem('adminUserId'); | |
| if (storedId) { | |
| userId = storedId; | |
| await verifyAdmin(); | |
| } | |
| }); | |
| async function verifyAdmin() { | |
| if (!userId) { | |
| error = 'Please enter your User ID'; | |
| return; | |
| } | |
| verifyLoading = true; | |
| error = ''; | |
| try { | |
| const response = await fetch(`/admin/verify?id=${userId}`); | |
| if (!response.ok) throw new Error('Unauthorized access'); | |
| const data = await response.json(); | |
| if (data.isAdmin) { | |
| adminVerified = true; | |
| localStorage.setItem('adminUserId', userId); | |
| await loadDraws(); | |
| } else { | |
| error = 'Unauthorized: Not an admin user'; | |
| } | |
| } catch (err) { | |
| error = err.message; | |
| adminVerified = false; | |
| } finally { | |
| verifyLoading = false; | |
| } | |
| } | |
| async function loadDraws() { | |
| try { | |
| const response = await fetch('/admin/lottery/draws'); | |
| if (!response.ok) throw new Error('Failed to fetch draws'); | |
| draws = await response.json(); | |
| } catch (err) { | |
| error = 'Failed to load draws'; | |
| } | |
| } | |
| async function generateTickets() { | |
| if (!startDate || !endDate) { | |
| error = 'Please select both start and end dates'; | |
| return; | |
| } | |
| loading = true; | |
| error = ''; | |
| success = ''; | |
| try { | |
| const response = await fetchWithAuth('/admin/lottery/generate-tickets', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({startDate, endDate}) | |
| }); | |
| if (!response.ok) throw new Error('Failed to generate tickets'); | |
| const result = await response.json(); | |
| success = `Successfully generated ${result.totalTickets} tickets for ${result.userCount} users`; | |
| await loadDraws(); | |
| } catch (err) { | |
| error = err.message; | |
| } finally { | |
| loading = false; | |
| } | |
| } | |
| async function createDraw() { | |
| try { | |
| const response = await fetchWithAuth('/api/admin/lottery/draws', { | |
| method: 'POST', | |
| body: JSON.stringify({ | |
| name: 'February Draw', | |
| description: 'Monthly lottery draw', | |
| prizeAmount: 1000, | |
| drawDate: new Date('2025-02-28').toISOString() | |
| }) | |
| }); | |
| if (!response.ok) { | |
| throw new Error('Failed to create draw'); | |
| } | |
| // Handle success | |
| } catch (err) { | |
| console.error('Error:', err); | |
| } | |
| } | |
| async function conductDraw(drawId) { | |
| if (!confirm('Are you sure you want to conduct this draw?')) return; | |
| loading = true; | |
| error = ''; | |
| success = ''; | |
| try { | |
| const response = await fetchWithAuth(`/admin/lottery/draws/${drawId}/conduct`, { | |
| method: 'POST' | |
| }); | |
| if (!response.ok) throw new Error('Failed to conduct draw'); | |
| const result = await response.json(); | |
| success = `Winner selected! Ticket ${result.winningTicket} belonging to user ${maskUserId(result.winnerId)}`; | |
| await loadDraws(); | |
| } catch (err) { | |
| error = err.message; | |
| } finally { | |
| loading = false; | |
| } | |
| } | |
| function logout() { | |
| localStorage.removeItem('adminUserId'); | |
| adminVerified = false; | |
| userId = ''; | |
| draws = []; | |
| } | |
| </script> | |
| <div class="container mx-auto p-6 max-w-6xl"> | |
| {#if !adminVerified} | |
| <!-- Admin Verification Form --> | |
| <div class="max-w-md mx-auto mt-12"> | |
| <div class="bg-white shadow-lg rounded-lg p-6"> | |
| <h2 class="text-2xl font-bold mb-6 text-center text-gray-800">Admin Verification</h2> | |
| {#if error} | |
| <div class="mb-4 p-3 bg-red-100 text-red-700 rounded-lg"> | |
| {error} | |
| </div> | |
| {/if} | |
| <div class="space-y-4"> | |
| <div> | |
| <label for="userId" class="block text-sm font-medium text-gray-700 mb-1"> | |
| Enter your User ID | |
| </label> | |
| <input | |
| id="userId" | |
| type="text" | |
| bind:value={userId} | |
| placeholder="From /id command in XSHOT" | |
| class="w-full p-2 border rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" | |
| /> | |
| </div> | |
| <button | |
| on:click={verifyAdmin} | |
| disabled={verifyLoading} | |
| class="w-full bg-indigo-600 text-white py-2 rounded-lg hover:bg-indigo-700 | |
| transition-colors disabled:opacity-50" | |
| > | |
| {verifyLoading ? 'Verifying...' : 'Verify Admin Access'} | |
| </button> | |
| <p class="text-sm text-gray-600 text-center mt-4"> | |
| Use the /id command in XSHOT to get your User ID | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| {:else} | |
| <!-- Admin Dashboard --> | |
| <div class="space-y-6"> | |
| <!-- Header --> | |
| <div class="flex justify-between items-center mb-8"> | |
| <h1 class="text-3xl font-bold text-gray-800">Lottery Administration</h1> | |
| <button | |
| on:click={logout} | |
| class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors" | |
| > | |
| Logout | |
| </button> | |
| </div> | |
| {#if error} | |
| <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4"> | |
| {error} | |
| </div> | |
| {/if} | |
| {#if success} | |
| <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4"> | |
| {success} | |
| </div> | |
| {/if} | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <!-- Ticket Generation --> | |
| <div class="bg-white shadow rounded-lg p-6"> | |
| <h2 class="text-xl font-semibold mb-4">Generate Tickets</h2> | |
| <div class="space-y-4"> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">Start Date</label> | |
| <input | |
| type="date" | |
| bind:value={startDate} | |
| class="w-full p-2 border rounded-lg" | |
| /> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">End Date</label> | |
| <input | |
| type="date" | |
| bind:value={endDate} | |
| class="w-full p-2 border rounded-lg" | |
| /> | |
| </div> | |
| </div> | |
| <button | |
| on:click={generateTickets} | |
| disabled={loading} | |
| class="w-full bg-indigo-600 text-white py-2 rounded-lg hover:bg-indigo-700 | |
| transition-colors disabled:opacity-50" | |
| > | |
| {loading ? 'Generating...' : 'Generate Tickets'} | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Create Draw --> | |
| <div class="bg-white shadow rounded-lg p-6"> | |
| <h2 class="text-xl font-semibold mb-4">Create New Draw</h2> | |
| <button | |
| on:click={createDraw} | |
| disabled={loading} | |
| class="w-full bg-green-600 text-white py-2 rounded-lg hover:bg-green-700 | |
| transition-colors disabled:opacity-50" | |
| > | |
| {loading ? 'Creating...' : 'Create Draw'} | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Active Draws --> | |
| <div class="bg-white shadow rounded-lg overflow-hidden"> | |
| <div class="p-6"> | |
| <h2 class="text-xl font-semibold mb-4">Active Draws</h2> | |
| <div class="overflow-x-auto"> | |
| <table class="w-full"> | |
| <thead class="bg-gray-50"> | |
| <tr> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"> | |
| Draw ID | |
| </th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"> | |
| Name | |
| </th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"> | |
| Prize | |
| </th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"> | |
| Status | |
| </th> | |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"> | |
| Actions | |
| </th> | |
| </tr> | |
| </thead> | |
| <tbody class="divide-y divide-gray-200"> | |
| {#each draws as draw} | |
| <tr class="hover:bg-gray-50"> | |
| <td class="px-6 py-4 whitespace-nowrap">{draw.id}</td> | |
| <td class="px-6 py-4 whitespace-nowrap">{draw.name}</td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| {formatUsd(draw.prizeAmount)} | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <span class={`px-2 py-1 text-xs font-semibold rounded-full | |
| ${draw.status === 'PENDING' ? 'bg-yellow-100 text-yellow-800' : | |
| draw.status === 'COMPLETED' ? 'bg-green-100 text-green-800' : | |
| 'bg-gray-100 text-gray-800'}`}> | |
| {draw.status} | |
| </span> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| {#if draw.status === 'PENDING'} | |
| <button | |
| on:click={() => conductDraw(draw.id)} | |
| class="bg-blue-500 text-white px-4 py-2 rounded | |
| hover:bg-blue-600 transition-colors" | |
| > | |
| Conduct Draw | |
| </button> | |
| {/if} | |
| </td> | |
| </tr> | |
| {/each} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/if} | |
| </div> | |
| <style> | |
| /* Add any additional styles here */ | |
| :global(body) { | |
| background-color: #f3f4f6; | |
| } | |
| </style> | |
| === ./routes/admin/lottery/draws/+server.js === | |
| import { json } from '@sveltejs/kit'; | |
| import { tradeDb } from '$lib/db.js'; | |
| import { requireAdmin } from '$lib/utils/auth'; | |
| // GET - List draws | |
| export async function GET() { | |
| try { | |
| const draws = await tradeDb` | |
| SELECT * | |
| FROM "LotteryDraws" | |
| ORDER BY created_at DESC | |
| `; | |
| return json(draws); | |
| } catch (err) { | |
| return json({ error: 'Failed to fetch draws' }, { status: 500 }); | |
| } | |
| } | |
| export async function POST({ request }) { | |
| // Check admin authentication | |
| const auth = await requireAdmin(request); | |
| if (auth.error) return auth.error; | |
| try { | |
| const { name, description, prizeAmount, drawDate } = await request.json(); | |
| if (!name || !prizeAmount || !drawDate) { | |
| return json({ error: 'Missing required fields' }, { status: 400 }); | |
| } | |
| const parsedDrawDate = new Date(drawDate); | |
| if (isNaN(parsedDrawDate.getTime())) { | |
| return json({ error: 'Invalid draw date' }, { status: 400 }); | |
| } | |
| const [draw] = await tradeDb` | |
| SELECT * FROM create_lottery_draw( | |
| ${name}, | |
| ${description || ''}, | |
| ${prizeAmount}::numeric, | |
| CURRENT_TIMESTAMP, | |
| ${parsedDrawDate}::timestamp, | |
| ${parsedDrawDate}::timestamp, | |
| ${auth.userId} | |
| ) | |
| `; | |
| return json({ success: true, drawId: draw }); | |
| } catch (err) { | |
| console.error('Error creating draw:', err); | |
| return json({ | |
| error: 'Failed to create draw', | |
| details: err.message | |
| }, { status: 500 }); | |
| } | |
| } | |
| === ./routes/admin/lottery/draws/[drawId]/conduct/+server.js === | |
| import { json } from '@sveltejs/kit'; | |
| import { tradeDb } from '$lib/db'; | |
| export async function POST({ request }) { | |
| try { | |
| const { name, prizeAmount, drawDate } = await request.json(); | |
| const adminId = 278227026; // You should get this from your auth system | |
| const [draw] = await tradeDb` | |
| INSERT INTO "LotteryDraws" ( | |
| name, | |
| prize_amount, | |
| start_date, | |
| end_date, | |
| draw_date, | |
| admin_user_id | |
| ) VALUES ( | |
| ${name}, | |
| ${prizeAmount}, | |
| CURRENT_TIMESTAMP, | |
| ${drawDate}, | |
| ${drawDate}, | |
| ${adminId} | |
| ) | |
| RETURNING * | |
| `; | |
| return json(draw); | |
| } catch (err) { | |
| console.error('Error creating draw:', err); | |
| return json({ error: 'Failed to create draw' }, { status: 500 }); | |
| } | |
| } | |
| === ./routes/admin/lottery/generate-tickets/+server.js === | |
| import {json} from '@sveltejs/kit'; | |
| import {tradeDb} from '$lib/db.js'; | |
| import { validateAdmin, getAdminIdFromRequest } from '$lib/utils/auth'; | |
| export async function POST({ request }) { | |
| try { | |
| const adminId = getAdminIdFromRequest(request); | |
| if (!adminId) { | |
| return json({ error: 'Admin ID not provided' }, { status: 401 }); | |
| } | |
| const isAdmin = await validateAdmin(adminId); | |
| if (!isAdmin) { | |
| return json({ error: 'Unauthorized' }, { status: 403 }); | |
| } | |
| const { startDate, endDate } = await request.json(); | |
| // Validate dates | |
| const parsedStartDate = new Date(startDate); | |
| const parsedEndDate = new Date(endDate); | |
| if (isNaN(parsedStartDate.getTime()) || isNaN(parsedEndDate.getTime())) { | |
| return json({ error: 'Invalid date format' }, { status: 400 }); | |
| } | |
| if (parsedEndDate < parsedStartDate) { | |
| return json({ error: 'End date must be after start date' }, { status: 400 }); | |
| } | |
| // Generate tickets | |
| await tradeDb`SELECT generate_all_initial_tickets()`; | |
| // Get stats | |
| const [stats] = await tradeDb` | |
| SELECT | |
| COUNT(DISTINCT user_id) as user_count, | |
| COUNT(*) as total_tickets | |
| FROM "LotteryTickets" | |
| WHERE created_at BETWEEN ${parsedStartDate}::timestamp AND ${parsedEndDate}::timestamp | |
| `; | |
| return json({ | |
| success: true, | |
| userCount: stats.user_count, | |
| totalTickets: stats.total_tickets | |
| }); | |
| } catch (err) { | |
| console.error('Error generating tickets:', err); | |
| return json({ | |
| error: 'Failed to generate tickets', | |
| details: err.message | |
| }, { status: 500 }); | |
| } | |
| } | |
| === ./routes/admin/verify/+server.js === | |
| import { json } from '@sveltejs/kit'; | |
| import { userDb } from '$lib/db.js'; | |
| export async function GET({ url }) { | |
| const adminId = url.searchParams.get('id'); | |
| if (!adminId) { | |
| return json({ error: 'Please provide your admin ID from /id command in XSHOT' }, { status: 401 }); | |
| } | |
| try { | |
| const [admin] = await userDb` | |
| SELECT is_admin, is_lottery_admin | |
| FROM "User" | |
| WHERE id = ${parseInt(adminId)} | |
| `; | |
| if (!admin || (!admin.is_admin && !admin.is_lottery_admin)) { | |
| return json({ error: 'Unauthorized - Only XSHOT admins can access this page' }, { status: 401 }); | |
| } | |
| return json({ isAdmin: true }); | |
| } catch (err) { | |
| return json({ error: 'Failed to verify admin status' }, { status: 500 }); | |
| } | |
| } | |
| === ./routes/api/lottery/stats/+server.js === | |
| import { json } from '@sveltejs/kit'; | |
| import { tradeDb } from '$lib/db'; | |
| export async function GET({ params }) { | |
| try { | |
| const userId = parseInt(params.userId); | |
| // Remove the redundant column definition list | |
| const [stats] = await tradeDb` | |
| SELECT * FROM get_user_lottery_stats(${userId}::bigint, NULL::bigint) | |
| `; | |
| if (!stats) { | |
| return json({ error: 'User not found or no stats' }, { status: 404 }); | |
| } | |
| return json({ | |
| totalTickets: parseInt(stats.total_tickets), | |
| revealedTickets: parseInt(stats.revealed_tickets), | |
| pendingTickets: parseInt(stats.pending_tickets), | |
| totalVolumeUsd: parseFloat(stats.total_volume), | |
| winProbability: parseFloat(stats.win_probability), | |
| specialTickets: parseInt(stats.special_tickets), | |
| drawName: stats.current_draw_name, | |
| drawEndDate: stats.draw_end_date, | |
| prizeAmount: parseFloat(stats.prize_amount) | |
| }); | |
| } catch (err) { | |
| console.error('Error fetching lottery data:', err); | |
| return json({ error: 'Failed to fetch lottery data' }, { status: 500 }); | |
| } | |
| } | |
| === ./routes/api/lottery/tickets/+server.js === | |
| import { json } from '@sveltejs/kit'; | |
| import { tradeDb } from '$lib/db'; | |
| export async function GET({ params }) { | |
| try { | |
| const stats = await tradeDb` | |
| SELECT | |
| * | |
| FROM lottery_stats | |
| WHERE user_id = ${params.userId} | |
| `; | |
| return json({ | |
| tickets: Math.floor((stats[0]?.volume_usd || 0) / 100), | |
| revealed: stats[0]?.revealed_tickets || 0, | |
| total: stats[0]?.total_tickets || 0 | |
| }); | |
| } catch (err) { | |
| console.error('Error fetching tickets:', err); | |
| return json({ error: 'Failed to fetch tickets' }, { status: 500 }); | |
| } | |
| } | |
| === ./routes/api/lottery/[userId]/+server.js === | |
| import { json } from '@sveltejs/kit'; | |
| import { tradeDb } from '$lib/db'; | |
| export async function GET({ params }) { | |
| try { | |
| const userId = parseInt(params.userId); | |
| // PostgreSQL function call with proper syntax | |
| const [stats] = await tradeDb` | |
| SELECT * FROM get_user_lottery_stats(${userId}::bigint); | |
| `; | |
| // Get revealed tickets - PostgreSQL specific syntax | |
| const [revealedTickets] = await tradeDb` | |
| SELECT ARRAY_AGG(ticket_hash ORDER BY created_at DESC) as hashes | |
| FROM "LotteryTickets" | |
| WHERE user_id = ${userId} | |
| AND status = 'revealed' | |
| AND created_at >= '2025-01-01'::timestamp | |
| AND created_at < '2025-03-01'::timestamp | |
| `; | |
| const ticketHashes = revealedTickets?.hashes || []; | |
| return json({ | |
| totalTickets: parseInt(stats.total_tickets), | |
| remainingTickets: parseInt(stats.total_tickets) - ticketHashes.length, | |
| revealedTickets: ticketHashes, | |
| volumeUsd: parseFloat(stats.total_volume_usd) | |
| }); | |
| } catch (err) { | |
| console.error('Error fetching lottery data:', err); | |
| return json({ error: 'Failed to fetch lottery data' }, { status: 500 }); | |
| } | |
| } | |
| export async function POST({ params }) { | |
| // POST handler remains unchanged since it works correctly | |
| try { | |
| const userId = parseInt(params.userId); | |
| // Verify user has remaining tickets | |
| const { remainingTickets } = await GET({ params }).then(r => r.json()); | |
| if (remainingTickets <= 0) { | |
| return json({ error: 'No tickets remaining' }, { status: 400 }); | |
| } | |
| // Generate ticket hash | |
| const ticketHash = Array(8).fill(0) | |
| .map(() => Math.random().toString(16).substr(2, 4)) | |
| .join(''); | |
| await tradeDb` | |
| INSERT INTO "LotteryTickets" ( | |
| user_id, | |
| ticket_hash, | |
| status, | |
| revealed_at | |
| ) VALUES ( | |
| ${userId}, | |
| ${ticketHash}, | |
| 'revealed', | |
| CURRENT_TIMESTAMP | |
| ) | |
| `; | |
| return json({ | |
| success: true, | |
| ticketHash, | |
| remainingTickets: remainingTickets - 1 | |
| }); | |
| } catch (err) { | |
| console.error('Error revealing ticket:', err); | |
| return json({ error: 'Failed to reveal ticket' }, { status: 500 }); | |
| } | |
| } | |
| === ./routes/api/rankings/+server.js === | |
| import { json } from '@sveltejs/kit'; | |
| import { getCachedData } from '$lib/cache.js'; | |
| import { maskUserId } from '$lib/utils.js'; | |
| export async function GET() { | |
| try { | |
| const rankings = await getCachedData(); | |
| // Mask user IDs before sending to frontend | |
| const maskedRankings = rankings.map(ranking => ({ | |
| ...ranking, | |
| userId: maskUserId(ranking.userId) | |
| })); | |
| return json(maskedRankings); | |
| } catch (err) { | |
| console.error('Error loading rankings:', err); | |
| return json({ error: 'Failed to load rankings' }, { status: 500 }); | |
| } | |
| } | |
| === ./routes/api/search/[term]/+server.js === | |
| import { json } from '@sveltejs/kit'; | |
| import { userDb, tradeDb } from '$lib/db'; | |
| export async function GET({ params }) { | |
| try { | |
| const userId = parseInt(params.term); | |
| if (isNaN(userId)) { | |
| return json({ error: 'Invalid user ID' }, { status: 400 }); | |
| } | |
| // Example: January 1, 2025 at 00:00:00 UTC | |
| const firstDayOfMonth = new Date(2025, 0, 1); | |
| firstDayOfMonth.setUTCHours(0, 0, 0, 0); | |
| // 1) Get all of this user's wallet addresses from userDb | |
| const userWallets = await userDb` | |
| SELECT address | |
| FROM "Wallet" | |
| WHERE user_id = ${userId} | |
| `; | |
| const addresses = userWallets.map((w) => w.address); | |
| // 2) Convert those addresses to Wallet IDs in tradeDb | |
| const holdingsWallets = await tradeDb` | |
| SELECT id | |
| FROM "Wallet" | |
| WHERE address = ANY(${addresses}) | |
| `; | |
| const walletIds = holdingsWallets.map((w) => w.id); | |
| // 3) Query: | |
| // - "user_volume": sum of this user's trades | |
| // - "ranked_user_volumes": everyone’s total volume with a rank | |
| // - "third_place_volume": volume of rank=3 (if it exists) | |
| const volumeResults = await tradeDb` | |
| WITH user_volume AS ( | |
| SELECT COALESCE(SUM(value_total_usd), 0) AS volume | |
| FROM "Trade" | |
| WHERE "walletId" = ANY(${walletIds}) | |
| AND executed_at >= ${firstDayOfMonth} | |
| ), | |
| ranked_user_volumes AS ( | |
| SELECT | |
| w.address, | |
| SUM(t.value_total_usd) AS volume, | |
| RANK() OVER (ORDER BY SUM(t.value_total_usd) DESC) AS rank | |
| FROM "Trade" t | |
| JOIN "Wallet" w ON t."walletId" = w.id | |
| WHERE t.executed_at >= ${firstDayOfMonth} | |
| GROUP BY w.address | |
| ), | |
| third_place_volume AS ( | |
| SELECT volume | |
| FROM ranked_user_volumes | |
| WHERE rank = 3 | |
| LIMIT 1 | |
| ) | |
| SELECT | |
| (SELECT volume FROM user_volume) AS user_volume, | |
| COALESCE((SELECT volume FROM third_place_volume), 0) AS third_place_volume; | |
| `; | |
| // 4) Calculate how much more volume is needed for the user to get from their current volume to 3rd place | |
| const userVolume = parseFloat(volumeResults[0]?.user_volume ?? 0); | |
| const thirdPlaceVolume = parseFloat(volumeResults[0]?.third_place_volume ?? 0); | |
| const volumeNeeded = Math.max(0, thirdPlaceVolume - userVolume); | |
| return json({ | |
| currentVolumeUsd: userVolume, | |
| currentTop3ThresholdUsd: thirdPlaceVolume, | |
| volumeNeededForTop3Usd: volumeNeeded | |
| }); | |
| } catch (err) { | |
| console.error('Error searching user:', err); | |
| return json({ error: 'Failed to search user' }, { status: 500 }); | |
| } | |
| } | |
| === ./routes/api/target/[userId]/+server.js === | |
| import { json } from '@sveltejs/kit'; | |
| import { userDb, tradeDb } from '$lib/db'; | |
| export async function GET({ params }) { | |
| try { | |
| const userId = parseInt(params.userId); | |
| const firstDayOfMonth = new Date(); | |
| firstDayOfMonth.setDate(1); | |
| firstDayOfMonth.setHours(0, 0, 0, 0); | |
| // First get user's wallet addresses from core db | |
| const userWallets = await userDb` | |
| SELECT address | |
| FROM "Wallet" | |
| WHERE user_id = ${userId} | |
| `; | |
| const addresses = userWallets.map(w => w.address); | |
| if (addresses.length === 0) { | |
| return json({ | |
| currentVolume: 0, | |
| currentVolumeUsd: 0, | |
| volumeNeededForTop3: 0, | |
| volumeNeededForTop3Usd: 0, | |
| currentTop3Threshold: 0, | |
| currentTop3ThresholdUsd: 0 | |
| }); | |
| } | |
| // Get holdings wallet IDs for these addresses | |
| const holdingsWallets = await tradeDb` | |
| SELECT id | |
| FROM "Wallet" | |
| WHERE address = ANY(${addresses}) | |
| `; | |
| const walletIds = holdingsWallets.map(w => w.id); | |
| // Get top 3 volumes (need to do the same address matching) | |
| const top3Result = await tradeDb` | |
| WITH all_volumes AS ( | |
| SELECT | |
| hw.address, | |
| SUM(COALESCE(t.amount_in, 0) + COALESCE(t.amount_out, 0)) as total_volume, | |
| SUM(COALESCE(t.value_total_usd, 0)) as total_volume_usd | |
| FROM "Trade" t | |
| JOIN "Wallet" hw ON t."walletId" = hw.id | |
| WHERE t.executed_at >= ${firstDayOfMonth} | |
| GROUP BY hw.address | |
| ), | |
| user_volumes AS ( | |
| SELECT | |
| cw.user_id, | |
| SUM(av.total_volume) as total_volume, | |
| SUM(av.total_volume_usd) as total_volume_usd | |
| FROM all_volumes av | |
| JOIN ${userDb.raw(`xshot_core_prod.public."Wallet"`)} cw ON cw.address = av.address | |
| GROUP BY cw.user_id | |
| ORDER BY total_volume_usd DESC | |
| LIMIT 3 | |
| ) | |
| SELECT total_volume, total_volume_usd | |
| FROM user_volumes | |
| ORDER BY total_volume_usd DESC | |
| `; | |
| // Get user's current volume | |
| const userVolumeResult = await tradeDb` | |
| SELECT | |
| SUM(COALESCE(t.amount_in, 0) + COALESCE(t.amount_out, 0)) as total_volume, | |
| SUM(COALESCE(t.value_total_usd, 0)) as total_volume_usd | |
| FROM "Trade" t | |
| WHERE t.executed_at >= ${firstDayOfMonth} | |
| AND t."walletId" = ANY(${walletIds}) | |
| `; | |
| const currentVolume = userVolumeResult[0]?.total_volume || 0; | |
| const currentVolumeUsd = userVolumeResult[0]?.total_volume_usd || 0; | |
| const lowestTop3Volume = top3Result[2]?.total_volume || 0; | |
| const lowestTop3VolumeUsd = top3Result[2]?.total_volume_usd || 0; | |
| const volumeNeeded = Math.max(0, lowestTop3Volume - currentVolume); | |
| const volumeNeededUsd = Math.max(0, lowestTop3VolumeUsd - currentVolumeUsd); | |
| return json({ | |
| currentVolume: parseFloat(currentVolume), | |
| currentVolumeUsd: parseFloat(currentVolumeUsd), | |
| volumeNeededForTop3: parseFloat(volumeNeeded), | |
| volumeNeededForTop3Usd: parseFloat(volumeNeededUsd), | |
| currentTop3Threshold: parseFloat(lowestTop3Volume), | |
| currentTop3ThresholdUsd: parseFloat(lowestTop3VolumeUsd) | |
| }); | |
| } catch (err) { | |
| console.error('Error calculating target volume:', err); | |
| return json({ error: 'Failed to calculate target volume' }, { status: 500 }); | |
| } | |
| } | |
| === ./s.sh === | |
| #!/bin/bash | |
| # Exit on any error | |
| set -e | |
| # Color codes for output | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| BLUE='\033[0;34m' | |
| NC='\033[0m' # No Color | |
| # Helper functions | |
| print_step() { | |
| echo -e "${BLUE}==== $1 ====${NC}" | |
| } | |
| print_success() { | |
| echo -e "${GREEN}✓ $1${NC}" | |
| } | |
| print_error() { | |
| echo -e "${RED}✗ $1${NC}" | |
| } | |
| # Check if we're in the right directory (should contain routes and lib folders) | |
| if [ ! -d "routes" ] || [ ! -d "lib" ]; then | |
| print_error "Please run this script from the root of your SvelteKit project" | |
| exit 1 | |
| fi | |
| # Create backup of existing files | |
| print_step "Creating backups" | |
| timestamp=$(date +%Y%m%d_%H%M%S) | |
| backup_dir="backup_$timestamp" | |
| mkdir -p "$backup_dir" | |
| if [ -f "routes/+page.svelte" ]; then | |
| cp "routes/+page.svelte" "$backup_dir/" | |
| print_success "Backed up +page.svelte" | |
| fi | |
| # Create new API endpoint directory | |
| print_step "Creating lottery API endpoint" | |
| mkdir -p "routes/api/lottery/[userId]" | |
| # Create the API endpoint file | |
| cat > "routes/api/lottery/[userId]/+server.js" << 'EOL' | |
| import { json } from '@sveltejs/kit'; | |
| import { userDb, tradeDb } from '$lib/db'; | |
| export async function GET({ params }) { | |
| try { | |
| const userId = parseInt(params.userId); | |
| // Start date: February 1st, 2025 | |
| const startDate = new Date(2025, 1, 1); | |
| startDate.setUTCHours(0, 0, 0, 0); | |
| // Get user's wallet addresses | |
| const userWallets = await userDb` | |
| SELECT address | |
| FROM "Wallet" | |
| WHERE user_id = ${userId} | |
| `; | |
| const addresses = userWallets.map(w => w.address); | |
| // Get holdings wallet IDs | |
| const holdingsWallets = await tradeDb` | |
| SELECT id | |
| FROM "Wallet" | |
| WHERE address = ANY(${addresses}) | |
| `; | |
| const walletIds = holdingsWallets.map(w => w.id); | |
| // Get trading statistics | |
| const tradeStats = await tradeDb` | |
| WITH user_trades AS ( | |
| SELECT | |
| COUNT(*) as total_trades, | |
| SUM(COALESCE(value_total_usd, 0)) as total_volume_usd | |
| FROM "Trade" | |
| WHERE "walletId" = ANY(${walletIds}) | |
| AND executed_at >= ${startDate} | |
| ), | |
| all_trades AS ( | |
| SELECT | |
| SUM(COALESCE(value_total_usd, 0)) as total_pool_volume_usd, | |
| COUNT(*) as total_pool_trades | |
| FROM "Trade" | |
| WHERE executed_at >= ${startDate} | |
| ) | |
| SELECT | |
| ut.*, | |
| at.total_pool_volume_usd, | |
| at.total_pool_trades | |
| FROM user_trades ut, all_trades at | |
| `; | |
| const stats = tradeStats[0]; | |
| // Calculate tickets (1 ticket per $100 traded) | |
| const tickets = Math.floor((stats?.total_volume_usd || 0) / 100); | |
| const totalPoolTickets = Math.floor((stats?.total_pool_volume_usd || 0) / 100); | |
| // Calculate win probability | |
| const winProbability = tickets / (totalPoolTickets || 1); | |
| return json({ | |
| tickets, | |
| totalPoolTickets, | |
| tradeCount: stats?.total_trades || 0, | |
| volumeUsd: stats?.total_volume_usd || 0, | |
| winProbability, | |
| poolVolumeUsd: stats?.total_pool_volume_usd || 0, | |
| poolTradeCount: stats?.total_pool_trades || 0 | |
| }); | |
| } catch (err) { | |
| console.error('Error fetching lottery data:', err); | |
| return json({ error: 'Failed to fetch lottery data' }, { status: 500 }); | |
| } | |
| } | |
| EOL | |
| print_success "Created lottery API endpoint" | |
| # Create LotteryModal component | |
| print_step "Creating LotteryModal component" | |
| mkdir -p "src/lib/components" | |
| cat > "src/lib/components/LotteryModal.svelte" << 'EOL' | |
| <script> | |
| import { onMount } from 'svelte'; | |
| import { Ticket, Star, ArrowRight, Trophy, Coins } from 'lucide-svelte'; | |
| export let userId; | |
| export let isOpen = false; | |
| export let onClose; | |
| export let darkMode = true; | |
| let data = null; | |
| let loading = true; | |
| $: if (isOpen && userId) { | |
| fetchLotteryData(); | |
| } | |
| async function fetchLotteryData() { | |
| try { | |
| const response = await fetch(`/api/lottery/${userId}`); | |
| data = await response.json(); | |
| } catch (error) { | |
| console.error('Error fetching lottery data:', error); | |
| } finally { | |
| loading = false; | |
| } | |
| } | |
| function formatNumber(num) { | |
| return new Intl.NumberFormat('en-US', { | |
| maximumFractionDigits: 2, | |
| }).format(num); | |
| } | |
| function formatUsd(num) { | |
| return new Intl.NumberFormat('en-US', { | |
| style: 'currency', | |
| currency: 'USD', | |
| notation: 'compact', | |
| maximumFractionDigits: 2 | |
| }).format(num); | |
| } | |
| function formatPercent(num) { | |
| return (num * 100).toFixed(4) + '%'; | |
| } | |
| </script> | |
| {#if isOpen} | |
| <div class="fixed inset-0 z-50 flex items-center justify-center p-4"> | |
| <!-- Backdrop --> | |
| <div | |
| class="absolute inset-0 bg-black/60 backdrop-blur-sm" | |
| on:click={onClose} | |
| /> | |
| <!-- Modal --> | |
| <div class="relative w-full max-w-2xl rounded-2xl shadow-xl p-6 { | |
| darkMode ? 'bg-gray-800' : 'bg-white' | |
| }"> | |
| <!-- Close button --> | |
| <button | |
| on:click={onClose} | |
| class="absolute top-4 right-4 text-gray-400 hover:text-gray-200 transition-colors" | |
| > | |
| ✕ | |
| </button> | |
| <!-- Title --> | |
| <div class="text-center mb-8"> | |
| <h2 class="text-2xl font-bold mb-2 flex items-center justify-center gap-3"> | |
| <Star class="w-6 h-6 text-yellow-500" /> | |
| February Lottery | |
| <Star class="w-6 h-6 text-yellow-500" /> | |
| </h2> | |
| <p class="text-gray-400">One lucky trader will win 50% of all February fees!</p> | |
| </div> | |
| {#if loading} | |
| <div class="text-center py-8">Loading...</div> | |
| {:else if data} | |
| <!-- Tickets Section --> | |
| <div class="mb-8"> | |
| <div class="p-6 rounded-xl {darkMode ? 'bg-gray-700/50' : 'bg-gray-100'}"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <div class="flex items-center gap-3"> | |
| <Ticket class="w-6 h-6 text-indigo-400" /> | |
| <h3 class="text-xl font-semibold">Your Tickets</h3> | |
| </div> | |
| <div class="text-2xl font-bold text-indigo-400"> | |
| {formatNumber(data.tickets)} | |
| </div> | |
| </div> | |
| <div class="text-sm text-gray-400"> | |
| 1 ticket per $100 traded in February | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Stats Grid --> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-8"> | |
| <div class="p-4 rounded-xl {darkMode ? 'bg-gray-700/30' : 'bg-gray-50'}"> | |
| <div class="flex items-center gap-2 mb-2"> | |
| <ArrowRight class="w-5 h-5 text-green-400" /> | |
| <div class="text-sm text-gray-400">Your Trades</div> | |
| </div> | |
| <div class="text-xl font-bold">{formatNumber(data.tradeCount)}</div> | |
| </div> | |
| <div class="p-4 rounded-xl {darkMode ? 'bg-gray-700/30' : 'bg-gray-50'}"> | |
| <div class="flex items-center gap-2 mb-2"> | |
| <Coins class="w-5 h-5 text-blue-400" /> | |
| <div class="text-sm text-gray-400">Your Volume</div> | |
| </div> | |
| <div class="text-xl font-bold">{formatUsd(data.volumeUsd)}</div> | |
| </div> | |
| </div> | |
| <!-- Win Chance Section --> | |
| <div class="p-6 rounded-xl mb-6 {darkMode ? 'bg-gray-700/50' : 'bg-gray-100'}"> | |
| <div class="flex items-center gap-3 mb-4"> | |
| <Trophy class="w-6 h-6 text-yellow-500" /> | |
| <h3 class="text-xl font-semibold">Win Probability</h3> | |
| </div> | |
| <div class="text-3xl font-bold text-indigo-400 mb-2"> | |
| {formatPercent(data.winProbability)} | |
| </div> | |
| <div class="text-sm text-gray-400"> | |
| Based on your {formatNumber(data.tickets)} tickets out of {formatNumber(data.totalPoolTickets)} total tickets | |
| </div> | |
| </div> | |
| <!-- Pool Stats --> | |
| <div class="text-center text-sm text-gray-400"> | |
| Total Pool Volume: {formatUsd(data.poolVolumeUsd)} • Total Pool Trades: {formatNumber(data.poolTradeCount)} | |
| </div> | |
| {:else} | |
| <div class="text-center py-8 text-red-400">Failed to load lottery data</div> | |
| {/if} | |
| </div> | |
| </div> | |
| {/if} | |
| <style> | |
| .backdrop-blur-sm { | |
| backdrop-filter: blur(8px); | |
| } | |
| </style> | |
| EOL | |
| print_success "Created LotteryModal component" | |
| # Update +page.svelte | |
| print_step "Updating main page" | |
| # First, let's add the imports and lottery section to the existing file | |
| awk ' | |
| /import \{ onMount \} from '"'"'svelte'"'"';/ { | |
| print; | |
| print " import LotteryModal from '"'"'$lib/components/LotteryModal.svelte'"'"';"; | |
| print " import { Ticket } from '"'"'lucide-svelte'"'"';"; | |
| next; | |
| } | |
| /let darkMode = true;/ { | |
| print; | |
| print " let showLotteryModal = false;"; | |
| next; | |
| } | |
| /<!-- Prize Distribution -->/ { | |
| print; | |
| print " <!-- Lottery Section -->"; | |
| print " <div class=\"max-w-2xl mx-auto mb-8\">"; | |
| print " <div class={`p-6 rounded-xl ${darkMode ? '"'"'bg-purple-900/30 backdrop-blur-sm'"'"' : '"'"'bg-purple-50'"'"'}`}>"; | |
| print " <div class=\"text-center space-y-4\">"; | |
| print " <div class=\"flex items-center justify-center gap-2\">"; | |
| print " <Ticket class=\"w-6 h-6 text-purple-400\" />"; | |
| print " <h3 class=\"text-xl font-bold bg-gradient-to-r from-purple-400 to-pink-400 bg-clip-text text-transparent\">"; | |
| print " February Lottery Event"; | |
| print " </h3>"; | |
| print " </div>"; | |
| print " "; | |
| print " <p class=\"text-lg\">"; | |
| print " Get 1 lottery ticket for every $100 traded!"; | |
| print " </p>"; | |
| print " "; | |
| print " <div class=\"text-sm text-gray-400\">"; | |
| print " One lucky winner will receive 50% of all February trading fees"; | |
| print " </div>"; | |
| print ""; | |
| print " {#if searchTerm}"; | |
| print " <button"; | |
| print " on:click={() => showLotteryModal = true}"; | |
| print " class=\"px-6 py-3 bg-purple-600 text-white font-semibold rounded-xl "; | |
| print " shadow-lg transform hover:scale-105 transition-all duration-300\""; | |
| print " >"; | |
| print " View My Tickets"; | |
| print " </button>"; | |
| print " {:else}"; | |
| print " <div class=\"text-sm text-purple-400\">"; | |
| print " Search your User ID above to view your tickets"; | |
| print " </div>"; | |
| print " {/if}"; | |
| print " </div>"; | |
| print " </div>"; | |
| print " </div>"; | |
| } | |
| /<\/div>$/ { | |
| if (!added_modal) { | |
| print ""; | |
| print " {#if showLotteryModal}"; | |
| print " <LotteryModal"; | |
| print " userId={searchTerm}"; | |
| print " isOpen={showLotteryModal}"; | |
| print " onClose={() => showLotteryModal = false}"; | |
| print " {darkMode}"; | |
| print " />"; | |
| print " {/if}"; | |
| added_modal = 1; | |
| } | |
| } | |
| /<style>/ { | |
| print; | |
| print " .backdrop-blur-sm {"; | |
| print " backdrop-filter: blur(8px);"; | |
| print " }"; | |
| next; | |
| } | |
| { print } | |
| ' routes/+page.svelte > routes/+page.svelte.new | |
| mv routes/+page.svelte.new routes/+page.svelte | |
| print_success "Updated main page" | |
| print_step "Installing required packages" | |
| npm install lucide-svelte --save | |
| print_success "All updates completed successfully!" | |
| echo -e "${GREEN}Backup of original files can be found in: $backup_dir${NC}" | |
| echo -e "${BLUE}Please review the changes and run your application to test the new features${NC}" | |
| === ./schema_holdings.sql === | |
| -- | |
| -- PostgreSQL database dump | |
| -- | |
| -- Dumped from database version 16.4 (Ubuntu 16.4-1.pgdg24.04+1) | |
| -- Dumped by pg_dump version 16.6 (Debian 16.6-1.pgdg120+1) | |
| SET statement_timeout = 0; | |
| SET lock_timeout = 0; | |
| SET idle_in_transaction_session_timeout = 0; | |
| SET client_encoding = 'UTF8'; | |
| SET standard_conforming_strings = on; | |
| SELECT pg_catalog.set_config('search_path', '', false); | |
| SET check_function_bodies = false; | |
| SET xmloption = content; | |
| SET client_min_messages = warning; | |
| SET row_security = off; | |
| -- | |
| -- Name: dblink; Type: EXTENSION; Schema: -; Owner: - | |
| -- | |
| CREATE EXTENSION IF NOT EXISTS dblink WITH SCHEMA public; | |
| -- | |
| -- Name: EXTENSION dblink; Type: COMMENT; Schema: -; Owner: | |
| -- | |
| COMMENT ON EXTENSION dblink IS 'connect to other PostgreSQL databases from within a database'; | |
| -- | |
| -- Name: DrawStatus; Type: TYPE; Schema: public; Owner: postgres | |
| -- | |
| CREATE TYPE public."DrawStatus" AS ENUM ( | |
| 'PENDING', | |
| 'ACTIVE', | |
| 'COMPLETED', | |
| 'CANCELLED' | |
| ); | |
| ALTER TYPE public."DrawStatus" OWNER TO postgres; | |
| -- | |
| -- Name: TradeOperation; Type: TYPE; Schema: public; Owner: postgres | |
| -- | |
| CREATE TYPE public."TradeOperation" AS ENUM ( | |
| 'BUY', | |
| 'SELL' | |
| ); | |
| ALTER TYPE public."TradeOperation" OWNER TO postgres; | |
| -- | |
| -- Name: ticket_status; Type: TYPE; Schema: public; Owner: postgres | |
| -- | |
| CREATE TYPE public.ticket_status AS ENUM ( | |
| 'PENDING', | |
| 'REVEALED', | |
| 'DRAWN', | |
| 'WINNER', | |
| 'EXPIRED' | |
| ); | |
| ALTER TYPE public.ticket_status OWNER TO postgres; | |
| -- | |
| -- Name: conduct_lottery_draw(bigint, bigint); Type: FUNCTION; Schema: public; Owner: postgres | |
| -- | |
| CREATE FUNCTION public.conduct_lottery_draw(p_draw_id bigint, p_admin_user_id bigint) RETURNS TABLE(winning_ticket_hash text, winner_user_id bigint, masked_winner_id text, ticket_number bigint, prize_amount numeric, winner_total_tickets bigint, winner_total_volume numeric, total_participants bigint, total_tickets bigint) | |
| LANGUAGE plpgsql | |
| AS $$ | |
| DECLARE | |
| v_draw RECORD; | |
| v_winning_ticket RECORD; | |
| v_total_tickets BIGINT; | |
| v_total_participants BIGINT; | |
| BEGIN | |
| -- Validate admin and draw status | |
| IF NOT validate_admin_user(p_admin_user_id) THEN | |
| RAISE EXCEPTION 'Unauthorized: User is not an admin'; | |
| END IF; | |
| SELECT * INTO v_draw | |
| FROM "LotteryDraws" | |
| WHERE id = p_draw_id AND status = 'PENDING' | |
| FOR UPDATE; | |
| IF NOT FOUND THEN | |
| RAISE EXCEPTION 'Invalid draw or wrong status'; | |
| END IF; | |
| -- Update draw status to active | |
| UPDATE "LotteryDraws" | |
| SET status = 'ACTIVE' | |
| WHERE id = p_draw_id; | |
| -- Get participation stats | |
| SELECT | |
| COUNT(DISTINCT user_id), | |
| COUNT(*) | |
| INTO | |
| v_total_participants, | |
| v_total_tickets | |
| FROM "LotteryTickets" | |
| WHERE draw_id = p_draw_id; | |
| -- Select winning ticket randomly | |
| SELECT | |
| t.*, | |
| s.total_tickets_all_time, | |
| s.total_volume_all_time | |
| INTO v_winning_ticket | |
| FROM "LotteryTickets" t | |
| JOIN "UserLotteryStats" s ON s.user_id = t.user_id | |
| WHERE t.draw_id = p_draw_id | |
| OFFSET floor(random() * v_total_tickets) | |
| LIMIT 1; | |
| -- Update draw and ticket status | |
| UPDATE "LotteryDraws" | |
| SET | |
| status = 'COMPLETED', | |
| winner_ticket_id = v_winning_ticket.id, | |
| updated_at = CURRENT_TIMESTAMP | |
| WHERE id = p_draw_id; | |
| UPDATE "LotteryTickets" | |
| SET status = | |
| CASE | |
| WHEN id = v_winning_ticket.id THEN 'WINNER' | |
| ELSE 'DRAWN' | |
| END | |
| WHERE draw_id = p_draw_id; | |
| -- Update winner stats | |
| UPDATE "UserLotteryStats" | |
| SET | |
| total_wins = total_wins + 1, | |
| last_win_date = CURRENT_TIMESTAMP | |
| WHERE user_id = v_winning_ticket.user_id; | |
| -- Return winner information | |
| RETURN QUERY | |
| SELECT | |
| v_winning_ticket.ticket_hash::TEXT, | |
| v_winning_ticket.user_id, | |
| mask_user_id(v_winning_ticket.user_id), | |
| v_winning_ticket.ticket_number, | |
| v_draw.prize_amount, | |
| v_winning_ticket.total_tickets_all_time, | |
| v_winning_ticket.total_volume_all_time, | |
| v_total_participants, | |
| v_total_tickets; | |
| END; | |
| $$; | |
| ALTER FUNCTION public.conduct_lottery_draw(p_draw_id bigint, p_admin_user_id bigint) OWNER TO postgres; | |
| -- | |
| -- Name: create_lottery_draw(text, text, numeric, timestamp without time zone, timestamp without time zone, timestamp without time zone, bigint); Type: FUNCTION; Schema: public; Owner: postgres | |
| -- | |
| CREATE FUNCTION public.create_lottery_draw(p_name text, p_description text, p_prize_amount numeric, p_start_date timestamp without time zone, p_end_date timestamp without time zone, p_draw_date timestamp without time zone, p_admin_user_id bigint) RETURNS bigint | |
| LANGUAGE plpgsql | |
| AS $$ | |
| DECLARE | |
| v_draw_id BIGINT; | |
| BEGIN | |
| -- Validate admin user | |
| IF NOT validate_admin_user(p_admin_user_id) THEN | |
| RAISE EXCEPTION 'Unauthorized: User is not an admin'; | |
| END IF; | |
| -- Insert new draw | |
| INSERT INTO "LotteryDraws" ( | |
| name, | |
| description, | |
| prize_amount, | |
| start_date, | |
| end_date, | |
| draw_date, | |
| admin_user_id | |
| ) VALUES ( | |
| p_name, | |
| p_description, | |
| p_prize_amount, | |
| p_start_date, | |
| p_end_date, | |
| p_draw_date, | |
| p_admin_user_id | |
| ) RETURNING id INTO v_draw_id; | |
| RETURN v_draw_id; | |
| END; | |
| $$; | |
| ALTER FUNCTION public.create_lottery_draw(p_name text, p_description text, p_prize_amount numeric, p_start_date timestamp without time zone, p_end_date timestamp without time zone, p_draw_date timestamp without time zone, p_admin_user_id bigint) OWNER TO postgres; | |
| -- | |
| -- Name: generate_all_initial_tickets(); Type: FUNCTION; Schema: public; Owner: postgres | |
| -- | |
| CREATE FUNCTION public.generate_all_initial_tickets() RETURNS void | |
| LANGUAGE plpgsql | |
| AS $$ | |
| DECLARE | |
| r RECORD; | |
| BEGIN | |
| -- For each wallet's trading volume in February | |
| FOR r IN ( | |
| WITH feb_trades AS ( | |
| SELECT | |
| t."walletId" as wallet_id, | |
| hw.address, | |
| SUM(t.value_total_usd) as total_volume | |
| FROM "Trade" t | |
| JOIN "Wallet" hw ON t."walletId" = hw.id | |
| WHERE t.executed_at >= '2025-02-01' | |
| AND t.executed_at < '2025-03-01' | |
| GROUP BY t."walletId", hw.address | |
| HAVING SUM(t.value_total_usd) >= 100 -- Only generate for those with at least 1 ticket | |
| ) | |
| SELECT | |
| ft.wallet_id, | |
| cw.user_id, | |
| ft.total_volume | |
| FROM feb_trades ft | |
| JOIN xshot_core_prod.public."Wallet" cw ON LOWER(cw.address) = LOWER(ft.address) | |
| ) LOOP | |
| -- Generate tickets for this wallet | |
| PERFORM generate_initial_tickets(r.wallet_id, r.user_id, r.total_volume); | |
| END LOOP; | |
| END; | |
| $$; | |
| ALTER FUNCTION public.generate_all_initial_tickets() OWNER TO postgres; | |
| -- | |
| -- Name: generate_hex_string(integer); Type: FUNCTION; Schema: public; Owner: postgres | |
| -- | |
| CREATE FUNCTION public.generate_hex_string(length integer) RETURNS text | |
| LANGUAGE plpgsql | |
| AS $$ | |
| DECLARE | |
| chars text[] := ARRAY['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f']; | |
| result text := ''; | |
| i integer; | |
| BEGIN | |
| FOR i IN 1..length LOOP | |
| result := result || chars[1+random()*(array_length(chars, 1)-1)]; | |
| END LOOP; | |
| RETURN result; | |
| END; | |
| $$; | |
| ALTER FUNCTION public.generate_hex_string(length integer) OWNER TO postgres; | |
| -- | |
| -- Name: generate_initial_tickets(bigint, bigint, numeric); Type: FUNCTION; Schema: public; Owner: postgres | |
| -- | |
| CREATE FUNCTION public.generate_initial_tickets(p_wallet_id bigint, p_user_id bigint, p_volume_usd numeric) RETURNS void | |
| LANGUAGE plpgsql | |
| AS $_$ | |
| DECLARE | |
| ticket_count INTEGER; | |
| i INTEGER; | |
| new_hash TEXT; | |
| BEGIN | |
| -- Calculate number of tickets ($100 = 1 ticket) | |
| ticket_count := FLOOR(p_volume_usd / 100); | |
| -- Generate tickets | |
| FOR i IN 1..ticket_count LOOP | |
| -- Generate 8 groups of 4 hex characters | |
| new_hash := ''; | |
| FOR j IN 1..8 LOOP | |
| new_hash := new_hash || generate_hex_string(4); | |
| END LOOP; | |
| -- Insert ticket | |
| INSERT INTO "LotteryTickets" ( | |
| user_id, | |
| wallet_id, | |
| ticket_hash, | |
| trade_volume_usd, | |
| created_at | |
| ) VALUES ( | |
| p_user_id, | |
| p_wallet_id, | |
| new_hash, | |
| p_volume_usd, | |
| CURRENT_TIMESTAMP | |
| ); | |
| END LOOP; | |
| END; | |
| $_$; | |
| ALTER FUNCTION public.generate_initial_tickets(p_wallet_id bigint, p_user_id bigint, p_volume_usd numeric) OWNER TO postgres; | |
| -- | |
| -- Name: generate_ticket_hash(); Type: FUNCTION; Schema: public; Owner: postgres | |
| -- | |
| CREATE FUNCTION public.generate_ticket_hash() RETURNS character | |
| LANGUAGE plpgsql | |
| AS $$ | |
| DECLARE | |
| chars TEXT[] := ARRAY['0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F']; | |
| result TEXT := ''; | |
| i INTEGER; | |
| BEGIN | |
| FOR i IN 1..32 LOOP | |
| result := result || chars[1 + floor(random() * 16)]; | |
| END LOOP; | |
| RETURN result; | |
| END; | |
| $$; | |
| ALTER FUNCTION public.generate_ticket_hash() OWNER TO postgres; | |
| -- | |
| -- Name: generate_unique_ticket_hash(); Type: FUNCTION; Schema: public; Owner: postgres | |
| -- | |
| CREATE FUNCTION public.generate_unique_ticket_hash() RETURNS text | |
| LANGUAGE plpgsql | |
| AS $$ | |
| DECLARE | |
| new_hash TEXT; | |
| collision INTEGER; | |
| BEGIN | |
| LOOP | |
| new_hash := ''; | |
| FOR i IN 1..32 LOOP | |
| new_hash := new_hash || substr('0123456789ABCDEF', ceil(random() * 16)::integer, 1); | |
| END LOOP; | |
| SELECT COUNT(*) INTO collision | |
| FROM "LotteryTickets" | |
| WHERE ticket_hash = new_hash; | |
| IF collision = 0 THEN | |
| RETURN new_hash; | |
| END IF; | |
| END LOOP; | |
| END; | |
| $$; | |
| ALTER FUNCTION public.generate_unique_ticket_hash() OWNER TO postgres; | |
| -- | |
| -- Name: generate_user_tickets(bigint, bigint); Type: FUNCTION; Schema: public; Owner: postgres | |
| -- | |
| CREATE FUNCTION public.generate_user_tickets(p_user_id bigint, p_draw_id bigint) RETURNS TABLE(tickets_generated bigint, total_volume numeric, ticket_numbers bigint[]) | |
| LANGUAGE plpgsql | |
| AS $$ | |
| DECLARE | |
| v_volume_per_ticket NUMERIC; | |
| v_max_tickets BIGINT; | |
| v_draw_record RECORD; | |
| v_volume NUMERIC; | |
| v_trade_count BIGINT; | |
| v_eligible_tickets BIGINT; | |
| v_generated_count BIGINT := 0; | |
| v_ticket_numbers BIGINT[] := ARRAY[]::BIGINT[]; | |
| BEGIN | |
| -- Get configuration values | |
| SELECT value::NUMERIC INTO v_volume_per_ticket | |
| FROM "LotteryConfig" | |
| WHERE key = 'volume_per_ticket'; | |
| SELECT value::BIGINT INTO v_max_tickets | |
| FROM "LotteryConfig" | |
| WHERE key = 'max_tickets_per_user'; | |
| -- Get draw information and validate | |
| SELECT * INTO v_draw_record | |
| FROM "LotteryDraws" | |
| WHERE id = p_draw_id AND status = 'PENDING'; | |
| IF NOT FOUND THEN | |
| RAISE EXCEPTION 'Invalid or non-pending draw'; | |
| END IF; | |
| -- Calculate trading volume and tickets | |
| WITH user_wallets AS ( | |
| SELECT w.id as wallet_id | |
| FROM dblink(get_core_db_connection(), | |
| format('SELECT id, address FROM "Wallet" WHERE user_id = %L', p_user_id)) | |
| AS w(id bigint, address text) | |
| ), | |
| trading_stats AS ( | |
| SELECT | |
| COALESCE(SUM(t.value_total_usd), 0) as total_volume, | |
| COUNT(*) as trade_count | |
| FROM "Trade" t | |
| WHERE t."walletId" IN (SELECT wallet_id FROM user_wallets) | |
| AND t.executed_at BETWEEN v_draw_record.start_date AND v_draw_record.end_date | |
| ) | |
| SELECT | |
| total_volume, | |
| trade_count | |
| INTO v_volume, v_trade_count | |
| FROM trading_stats; | |
| -- Calculate eligible tickets | |
| v_eligible_tickets := LEAST(FLOOR(v_volume / v_volume_per_ticket), v_max_tickets); | |
| -- Generate new tickets | |
| FOR i IN 1..v_eligible_tickets LOOP | |
| INSERT INTO "LotteryTickets" ( | |
| draw_id, | |
| user_id, | |
| ticket_hash, | |
| ticket_number, | |
| qualifying_volume, | |
| status | |
| ) VALUES ( | |
| p_draw_id, | |
| p_user_id, | |
| generate_unique_ticket_hash(), | |
| get_next_ticket_number(p_draw_id), | |
| v_volume / v_eligible_tickets, | |
| 'PENDING' | |
| ) RETURNING ticket_number INTO v_ticket_numbers[i]; | |
| v_generated_count := v_generated_count + 1; | |
| END LOOP; | |
| -- Update user stats | |
| INSERT INTO "UserLotteryStats" ( | |
| user_id, | |
| total_tickets_all_time, | |
| total_volume_all_time, | |
| total_trades_all_time | |
| ) VALUES ( | |
| p_user_id, | |
| v_eligible_tickets, | |
| v_volume, | |
| v_trade_count | |
| ) | |
| ON CONFLICT (user_id) DO UPDATE SET | |
| total_tickets_all_time = "UserLotteryStats".total_tickets_all_time + v_eligible_tickets, | |
| total_volume_all_time = "UserLotteryStats".total_volume_all_time + v_volume, | |
| total_trades_all_time = "UserLotteryStats".total_trades_all_time + v_trade_count, | |
| updated_at = CURRENT_TIMESTAMP; | |
| RETURN QUERY | |
| SELECT | |
| v_generated_count, | |
| v_volume, | |
| v_ticket_numbers; | |
| END; | |
| $$; | |
| ALTER FUNCTION public.generate_user_tickets(p_user_id bigint, p_draw_id bigint) OWNER TO postgres; | |
| -- | |
| -- Name: get_core_db_connection(); Type: FUNCTION; Schema: public; Owner: postgres | |
| -- | |
| CREATE FUNCTION public.get_core_db_connection() RETURNS text | |
| LANGUAGE plpgsql | |
| AS $$ | |
| BEGIN | |
| RETURN 'dbname=xshot_core_prod'; | |
| END; | |
| $$; | |
| ALTER FUNCTION public.get_core_db_connection() OWNER TO postgres; | |
| -- | |
| -- Name: get_next_ticket_number(bigint); Type: FUNCTION; Schema: public; Owner: postgres | |
| -- | |
| CREATE FUNCTION public.get_next_ticket_number(p_draw_id bigint) RETURNS bigint | |
| LANGUAGE plpgsql | |
| AS $$ | |
| DECLARE | |
| next_number BIGINT; | |
| BEGIN | |
| SELECT COALESCE(MAX(ticket_number), 0) + 1 | |
| INTO next_number | |
| FROM "LotteryTickets" | |
| WHERE draw_id = p_draw_id; | |
| RETURN next_number; | |
| END; | |
| $$; | |
| ALTER FUNCTION public.get_next_ticket_number(p_draw_id bigint) OWNER TO postgres; | |
| -- | |
| -- Name: get_user_lottery_stats(bigint); Type: FUNCTION; Schema: public; Owner: postgres | |
| -- | |
| CREATE FUNCTION public.get_user_lottery_stats(p_user_id bigint) RETURNS TABLE(total_tickets bigint, revealed_tickets bigint, total_volume_usd numeric, total_trades bigint, win_probability numeric) | |
| LANGUAGE plpgsql | |
| AS $$ | |
| BEGIN | |
| RETURN QUERY | |
| WITH trading_volume AS ( | |
| SELECT | |
| t."walletId", | |
| COUNT(*) as trade_count, | |
| SUM(COALESCE(t.value_total_usd, 0)) as volume_usd, | |
| FLOOR(SUM(COALESCE(t.value_total_usd, 0)) / 100) as ticket_count | |
| FROM "Trade" t | |
| WHERE t.executed_at >= '2025-01-01' | |
| AND t.executed_at < '2025-03-01' | |
| GROUP BY t."walletId" | |
| ), | |
| wallet_users AS ( | |
| SELECT | |
| hw.id as holdings_wallet_id, | |
| cw.user_id | |
| FROM "Wallet" hw | |
| LEFT JOIN dblink('dbname=xshot_core_prod', | |
| 'SELECT address, user_id FROM "Wallet"' | |
| ) AS cw(address text, user_id bigint) | |
| ON LOWER(hw.address) = LOWER(cw.address) | |
| WHERE cw.user_id = p_user_id | |
| ), | |
| user_stats AS ( | |
| SELECT | |
| SUM(tv.trade_count) as trades, | |
| SUM(tv.volume_usd) as volume, | |
| SUM(tv.ticket_count) as tickets | |
| FROM wallet_users wu | |
| JOIN trading_volume tv ON tv."walletId" = wu.holdings_wallet_id | |
| ), | |
| total_pool AS ( | |
| SELECT | |
| SUM(ticket_count) as total_pool_tickets | |
| FROM trading_volume tv | |
| JOIN wallet_users wu ON tv."walletId" = wu.holdings_wallet_id | |
| ) | |
| SELECT | |
| us.tickets::BIGINT, | |
| (SELECT COUNT(*) FROM "LotteryTickets" WHERE user_id = p_user_id)::BIGINT, | |
| us.volume, | |
| us.trades::BIGINT, | |
| CASE | |
| WHEN tp.total_pool_tickets > 0 | |
| THEN us.tickets::NUMERIC / tp.total_pool_tickets::NUMERIC | |
| ELSE 0 | |
| END | |
| FROM user_stats us, total_pool tp; | |
| END; | |
| $$; | |
| ALTER FUNCTION public.get_user_lottery_stats(p_user_id bigint) OWNER TO postgres; | |
| -- | |
| -- Name: get_user_lottery_stats(bigint, bigint); Type: FUNCTION; Schema: public; Owner: postgres | |
| -- | |
| CREATE FUNCTION public.get_user_lottery_stats(p_user_id bigint, p_draw_id bigint DEFAULT NULL::bigint) RETURNS TABLE(total_tickets bigint, revealed_tickets bigint, pending_tickets bigint, total_volume numeric, win_probability numeric, special_tickets bigint, current_draw_name text, draw_end_date timestamp without time zone, prize_amount numeric) | |
| LANGUAGE plpgsql | |
| AS $$ | |
| BEGIN | |
| -- If no draw_id specified, get the latest active draw | |
| IF p_draw_id IS NULL THEN | |
| SELECT id INTO p_draw_id | |
| FROM "LotteryDraws" | |
| WHERE status = 'PENDING' | |
| ORDER BY draw_date ASC | |
| LIMIT 1; | |
| END IF; | |
| RETURN QUERY | |
| WITH ticket_stats AS ( | |
| SELECT | |
| COUNT(*) as total, | |
| COUNT(*) FILTER (WHERE status = 'REVEALED') as revealed, | |
| COUNT(*) FILTER (WHERE status = 'PENDING') as pending, | |
| COUNT(*) FILTER (WHERE ticket_number % 100 = 0 | |
| OR qualifying_volume > 10000) as special, | |
| SUM(qualifying_volume) as volume | |
| FROM "LotteryTickets" | |
| WHERE user_id = p_user_id | |
| AND draw_id = p_draw_id | |
| ), | |
| draw_stats AS ( | |
| SELECT | |
| d.name, | |
| d.end_date, | |
| d.prize_amount, | |
| COUNT(*) as total_pool_tickets | |
| FROM "LotteryDraws" d | |
| JOIN "LotteryTickets" t ON t.draw_id = d.id | |
| WHERE d.id = p_draw_id | |
| GROUP BY d.id | |
| ) | |
| SELECT | |
| ts.total, | |
| ts.revealed, | |
| ts.pending, | |
| ts.volume, | |
| CASE | |
| WHEN ds.total_pool_tickets > 0 THEN | |
| ts.total::NUMERIC / ds.total_pool_tickets | |
| ELSE 0 | |
| END, | |
| ts.special, | |
| ds.name, | |
| ds.end_date, | |
| ds.prize_amount | |
| FROM ticket_stats ts | |
| CROSS JOIN draw_stats ds; | |
| END; | |
| $$; | |
| ALTER FUNCTION public.get_user_lottery_stats(p_user_id bigint, p_draw_id bigint) OWNER TO postgres; | |
| -- | |
| -- Name: mask_user_id(bigint); Type: FUNCTION; Schema: public; Owner: postgres | |
| -- | |
| CREATE FUNCTION public.mask_user_id(user_id bigint) RETURNS text | |
| LANGUAGE plpgsql | |
| AS $$ | |
| DECLARE | |
| id_str TEXT; | |
| BEGIN | |
| id_str := user_id::TEXT; | |
| IF LENGTH(id_str) <= 4 THEN | |
| RETURN id_str; | |
| ELSE | |
| RETURN LEFT(id_str, 2) || '...' || RIGHT(id_str, 1); | |
| END IF; | |
| END; | |
| $$; | |
| ALTER FUNCTION public.mask_user_id(user_id bigint) OWNER TO postgres; | |
| -- | |
| -- Name: reveal_ticket(bigint); Type: FUNCTION; Schema: public; Owner: postgres | |
| -- | |
| CREATE FUNCTION public.reveal_ticket(p_user_id bigint) RETURNS TABLE(ticket_hash character, remaining_tickets bigint) | |
| LANGUAGE plpgsql | |
| AS $$ | |
| DECLARE | |
| v_ticket_hash CHAR(32); | |
| v_total_tickets BIGINT; | |
| v_revealed_tickets BIGINT; | |
| BEGIN | |
| -- Get total possible tickets from trade volume using the view | |
| SELECT total_tickets | |
| INTO v_total_tickets | |
| FROM lottery_stats | |
| WHERE user_id = p_user_id; | |
| -- Get count of already revealed tickets | |
| SELECT COUNT(*) | |
| INTO v_revealed_tickets | |
| FROM "LotteryTickets" | |
| WHERE user_id = p_user_id; | |
| -- Check if user can reveal more tickets | |
| IF v_revealed_tickets >= v_total_tickets THEN | |
| RETURN QUERY SELECT NULL::CHAR(32), (v_total_tickets - v_revealed_tickets); | |
| RETURN; | |
| END IF; | |
| -- Generate and insert new ticket | |
| v_ticket_hash := generate_ticket_hash(); | |
| INSERT INTO "LotteryTickets" ( | |
| user_id, | |
| ticket_hash, | |
| status, | |
| revealed_at | |
| ) VALUES ( | |
| p_user_id, | |
| v_ticket_hash, | |
| 'revealed', | |
| CURRENT_TIMESTAMP | |
| ); | |
| RETURN QUERY SELECT | |
| v_ticket_hash, | |
| (v_total_tickets - v_revealed_tickets - 1); | |
| END; | |
| $$; | |
| ALTER FUNCTION public.reveal_ticket(p_user_id bigint) OWNER TO postgres; | |
| -- | |
| -- Name: validate_admin_user(bigint); Type: FUNCTION; Schema: public; Owner: postgres | |
| -- | |
| CREATE FUNCTION public.validate_admin_user(user_id bigint) RETURNS boolean | |
| LANGUAGE plpgsql | |
| AS $$ | |
| DECLARE | |
| is_admin BOOLEAN; | |
| BEGIN | |
| SELECT EXISTS INTO is_admin | |
| FROM dblink(get_core_db_connection(), | |
| format('SELECT 1 FROM "User" WHERE id = %L AND (is_admin = true OR is_lottery_admin = true)', user_id)) | |
| AS t(exists boolean); | |
| RETURN is_admin; | |
| END; | |
| $$; | |
| ALTER FUNCTION public.validate_admin_user(user_id bigint) OWNER TO postgres; | |
| -- | |
| -- Name: validate_lottery_config(); Type: FUNCTION; Schema: public; Owner: postgres | |
| -- | |
| CREATE FUNCTION public.validate_lottery_config() RETURNS trigger | |
| LANGUAGE plpgsql | |
| AS $$ | |
| BEGIN | |
| IF NOT validate_admin_user(NEW.updated_by) THEN | |
| RAISE EXCEPTION 'Invalid admin user ID'; | |
| END IF; | |
| RETURN NEW; | |
| END; | |
| $$; | |
| ALTER FUNCTION public.validate_lottery_config() OWNER TO postgres; | |
| -- | |
| -- Name: validate_lottery_draw(); Type: FUNCTION; Schema: public; Owner: postgres | |
| -- | |
| CREATE FUNCTION public.validate_lottery_draw() RETURNS trigger | |
| LANGUAGE plpgsql | |
| AS $$ | |
| BEGIN | |
| IF NOT validate_admin_user(NEW.admin_user_id) THEN | |
| RAISE EXCEPTION 'Invalid admin user ID'; | |
| END IF; | |
| RETURN NEW; | |
| END; | |
| $$; | |
| ALTER FUNCTION public.validate_lottery_draw() OWNER TO postgres; | |
| -- | |
| -- Name: validate_lottery_ticket(); Type: FUNCTION; Schema: public; Owner: postgres | |
| -- | |
| CREATE FUNCTION public.validate_lottery_ticket() RETURNS trigger | |
| LANGUAGE plpgsql | |
| AS $$ | |
| BEGIN | |
| IF NOT validate_user_exists(NEW.user_id) THEN | |
| RAISE EXCEPTION 'Invalid user ID'; | |
| END IF; | |
| RETURN NEW; | |
| END; | |
| $$; | |
| ALTER FUNCTION public.validate_lottery_ticket() OWNER TO postgres; | |
| -- | |
| -- Name: validate_user_exists(bigint); Type: FUNCTION; Schema: public; Owner: postgres | |
| -- | |
| CREATE FUNCTION public.validate_user_exists(user_id bigint) RETURNS boolean | |
| LANGUAGE plpgsql | |
| AS $$ | |
| DECLARE | |
| exists_in_core BOOLEAN; | |
| BEGIN | |
| SELECT EXISTS INTO exists_in_core | |
| FROM dblink(get_core_db_connection(), | |
| format('SELECT 1 FROM "User" WHERE id = %L', user_id)) | |
| AS t(exists boolean); | |
| RETURN exists_in_core; | |
| END; | |
| $$; | |
| ALTER FUNCTION public.validate_user_exists(user_id bigint) OWNER TO postgres; | |
| SET default_tablespace = ''; | |
| SET default_table_access_method = heap; | |
| -- | |
| -- Name: Balance; Type: TABLE; Schema: public; Owner: postgres | |
| -- | |
| CREATE TABLE public."Balance" ( | |
| id bigint NOT NULL, | |
| amount numeric(65,30) NOT NULL, | |
| amount_usd numeric(65,30) NOT NULL, | |
| currency text NOT NULL, | |
| chain integer NOT NULL, | |
| "walletId" bigint NOT NULL | |
| ); | |
| ALTER TABLE public."Balance" OWNER TO postgres; | |
| -- | |
| -- Name: Balance_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres | |
| -- | |
| CREATE SEQUENCE public."Balance_id_seq" | |
| START WITH 1 | |
| INCREMENT BY 1 | |
| NO MINVALUE | |
| NO MAXVALUE | |
| CACHE 1; | |
| ALTER SEQUENCE public."Balance_id_seq" OWNER TO postgres; | |
| -- | |
| -- Name: Balance_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres | |
| -- | |
| ALTER SEQUENCE public."Balance_id_seq" OWNED BY public."Balance".id; | |
| -- | |
| -- Name: LiveData; Type: TABLE; Schema: public; Owner: postgres | |
| -- | |
| CREATE TABLE public."LiveData" ( | |
| id bigint NOT NULL, | |
| price_native numeric(65,30) NOT NULL, | |
| price_usd numeric(65,30) NOT NULL, | |
| marketcap_native numeric(65,30) NOT NULL, | |
| marketcap_usd numeric(65,30) NOT NULL, | |
| has_liquidity boolean DEFAULT true NOT NULL, | |
| supply numeric(65,30) NOT NULL, | |
| volume_24h numeric(65,30), | |
| "tokenId" bigint NOT NULL, | |
| "updatedAt" timestamp(3) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL | |
| ); | |
| ALTER TABLE public."LiveData" OWNER TO postgres; | |
| -- | |
| -- Name: LiveData_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres | |
| -- | |
| CREATE SEQUENCE public."LiveData_id_seq" | |
| START WITH 1 | |
| INCREMENT BY 1 | |
| NO MINVALUE | |
| NO MAXVALUE | |
| CACHE 1; | |
| ALTER SEQUENCE public."LiveData_id_seq" OWNER TO postgres; | |
| -- | |
| -- Name: LiveData_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres | |
| -- | |
| ALTER SEQUENCE public."LiveData_id_seq" OWNED BY public."LiveData".id; | |
| -- | |
| -- Name: LotteryConfig; Type: TABLE; Schema: public; Owner: postgres | |
| -- | |
| CREATE TABLE public."LotteryConfig" ( | |
| id bigint NOT NULL, | |
| key text NOT NULL, | |
| value text NOT NULL, | |
| description text, | |
| updated_by bigint NOT NULL, | |
| updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP | |
| ); | |
| ALTER TABLE public."LotteryConfig" OWNER TO postgres; | |
| -- | |
| -- Name: LotteryConfig_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres | |
| -- | |
| CREATE SEQUENCE public."LotteryConfig_id_seq" | |
| START WITH 1 | |
| INCREMENT BY 1 | |
| NO MINVALUE | |
| NO MAXVALUE | |
| CACHE 1; | |
| ALTER SEQUENCE public."LotteryConfig_id_seq" OWNER TO postgres; | |
| -- | |
| -- Name: LotteryConfig_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres | |
| -- | |
| ALTER SEQUENCE public."LotteryConfig_id_seq" OWNED BY public."LotteryConfig".id; | |
| -- | |
| -- Name: LotteryDraws; Type: TABLE; Schema: public; Owner: postgres | |
| -- | |
| CREATE TABLE public."LotteryDraws" ( | |
| id bigint NOT NULL, | |
| name text NOT NULL, | |
| description text, | |
| prize_amount numeric(65,30) NOT NULL, | |
| start_date timestamp without time zone NOT NULL, | |
| end_date timestamp without time zone NOT NULL, | |
| draw_date timestamp without time zone, | |
| status public."DrawStatus" DEFAULT 'PENDING'::public."DrawStatus" NOT NULL, | |
| winner_ticket_id bigint, | |
| admin_user_id bigint NOT NULL, | |
| created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP, | |
| updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP | |
| ); | |
| ALTER TABLE public."LotteryDraws" OWNER TO postgres; | |
| -- | |
| -- Name: LotteryDraws_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres | |
| -- | |
| CREATE SEQUENCE public."LotteryDraws_id_seq" | |
| START WITH 1 | |
| INCREMENT BY 1 | |
| NO MINVALUE | |
| NO MAXVALUE | |
| CACHE 1; | |
| ALTER SEQUENCE public."LotteryDraws_id_seq" OWNER TO postgres; | |
| -- | |
| -- Name: LotteryDraws_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres | |
| -- | |
| ALTER SEQUENCE public."LotteryDraws_id_seq" OWNED BY public."LotteryDraws".id; | |
| -- | |
| -- Name: LotteryTickets; Type: TABLE; Schema: public; Owner: postgres | |
| -- | |
| CREATE TABLE public."LotteryTickets" ( | |
| id bigint NOT NULL, | |
| draw_id bigint NOT NULL, | |
| user_id bigint NOT NULL, | |
| ticket_hash character(32) NOT NULL, | |
| ticket_number bigint NOT NULL, | |
| status public.ticket_status DEFAULT 'PENDING'::public.ticket_status NOT NULL, | |
| qualifying_volume numeric(65,30) NOT NULL, | |
| revealed_at timestamp with time zone, | |
| created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP | |
| ); | |
| ALTER TABLE public."LotteryTickets" OWNER TO postgres; | |
| -- | |
| -- Name: LotteryTickets_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres | |
| -- | |
| CREATE SEQUENCE public."LotteryTickets_id_seq" | |
| START WITH 1 | |
| INCREMENT BY 1 | |
| NO MINVALUE | |
| NO MAXVALUE | |
| CACHE 1; | |
| ALTER SEQUENCE public."LotteryTickets_id_seq" OWNER TO postgres; | |
| -- | |
| -- Name: LotteryTickets_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres | |
| -- | |
| ALTER SEQUENCE public."LotteryTickets_id_seq" OWNED BY public."LotteryTickets".id; | |
| -- | |
| -- Name: Token; Type: TABLE; Schema: public; Owner: postgres | |
| -- | |
| CREATE TABLE public."Token" ( | |
| id bigint NOT NULL, | |
| address text NOT NULL, | |
| name text NOT NULL, | |
| symbol text NOT NULL, | |
| chain integer NOT NULL, | |
| decimals integer NOT NULL, | |
| tags text[] DEFAULT ARRAY[]::text[], | |
| "updatedAt" timestamp(3) without time zone NOT NULL | |
| ); | |
| ALTER TABLE public."Token" OWNER TO postgres; | |
| -- | |
| -- Name: Token_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres | |
| -- | |
| CREATE SEQUENCE public."Token_id_seq" | |
| START WITH 1 | |
| INCREMENT BY 1 | |
| NO MINVALUE | |
| NO MAXVALUE | |
| CACHE 1; | |
| ALTER SEQUENCE public."Token_id_seq" OWNER TO postgres; | |
| -- | |
| -- Name: Token_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres | |
| -- | |
| ALTER SEQUENCE public."Token_id_seq" OWNED BY public."Token".id; | |
| -- | |
| -- Name: Trade; Type: TABLE; Schema: public; Owner: postgres | |
| -- | |
| CREATE TABLE public."Trade" ( | |
| id bigint NOT NULL, | |
| tx_hash text NOT NULL, | |
| chain integer NOT NULL, | |
| operation public."TradeOperation" NOT NULL, | |
| value_single_usd numeric(65,30) NOT NULL, | |
| value_single_native numeric(65,30) NOT NULL, | |
| value_total_usd numeric(65,30) NOT NULL, | |
| value_total_native numeric(65,30) NOT NULL, | |
| amount_in numeric(65,30) NOT NULL, | |
| amount_out numeric(65,30) NOT NULL, | |
| token_in text NOT NULL, | |
| token_out text NOT NULL, | |
| executed_at timestamp(3) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, | |
| "walletId" bigint NOT NULL, | |
| "tokenId" bigint NOT NULL | |
| ); | |
| ALTER TABLE public."Trade" OWNER TO postgres; | |
| -- | |
| -- Name: TradePreview; Type: TABLE; Schema: public; Owner: postgres | |
| -- | |
| CREATE TABLE public."TradePreview" ( | |
| id bigint NOT NULL, | |
| chain integer NOT NULL, | |
| current_price numeric(65,30) NOT NULL, | |
| current_mcap numeric(65,30) NOT NULL, | |
| average_entry_price numeric(65,30) NOT NULL, | |
| average_entry_mcap numeric(65,30) NOT NULL, | |
| balance numeric(65,30) NOT NULL, | |
| balance_usd numeric(65,30) NOT NULL, | |
| buys_amount numeric(65,30) NOT NULL, | |
| buys_total numeric(65,30) NOT NULL, | |
| sells_amount numeric(65,30) NOT NULL, | |
| sells_total numeric(65,30) NOT NULL, | |
| pnl_percentage numeric(65,30) NOT NULL, | |
| pnl_amount numeric(65,30) NOT NULL, | |
| "walletId" bigint NOT NULL, | |
| "tokenId" bigint NOT NULL, | |
| hidden boolean DEFAULT false NOT NULL, | |
| "updatedAt" timestamp(3) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL | |
| ); | |
| ALTER TABLE public."TradePreview" OWNER TO postgres; | |
| -- | |
| -- Name: TradePreview_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres | |
| -- | |
| CREATE SEQUENCE public."TradePreview_id_seq" | |
| START WITH 1 | |
| INCREMENT BY 1 | |
| NO MINVALUE | |
| NO MAXVALUE | |
| CACHE 1; | |
| ALTER SEQUENCE public."TradePreview_id_seq" OWNER TO postgres; | |
| -- | |
| -- Name: TradePreview_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres | |
| -- | |
| ALTER SEQUENCE public."TradePreview_id_seq" OWNED BY public."TradePreview".id; | |
| -- | |
| -- Name: Trade_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres | |
| -- | |
| CREATE SEQUENCE public."Trade_id_seq" | |
| START WITH 1 | |
| INCREMENT BY 1 | |
| NO MINVALUE | |
| NO MAXVALUE | |
| CACHE 1; | |
| ALTER SEQUENCE public."Trade_id_seq" OWNER TO postgres; | |
| -- | |
| -- Name: Trade_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres | |
| -- | |
| ALTER SEQUENCE public."Trade_id_seq" OWNED BY public."Trade".id; | |
| -- | |
| -- Name: UserLotteryStats; Type: TABLE; Schema: public; Owner: postgres | |
| -- | |
| CREATE TABLE public."UserLotteryStats" ( | |
| id bigint NOT NULL, | |
| user_id bigint NOT NULL, | |
| total_tickets_all_time bigint DEFAULT 0 NOT NULL, | |
| total_volume_all_time numeric(65,30) DEFAULT 0 NOT NULL, | |
| total_trades_all_time bigint DEFAULT 0 NOT NULL, | |
| total_wins bigint DEFAULT 0 NOT NULL, | |
| last_win_date timestamp with time zone, | |
| updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP | |
| ); | |
| ALTER TABLE public."UserLotteryStats" OWNER TO postgres; | |
| -- | |
| -- Name: UserLotteryStats_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres | |
| -- | |
| CREATE SEQUENCE public."UserLotteryStats_id_seq" | |
| START WITH 1 | |
| INCREMENT BY 1 | |
| NO MINVALUE | |
| NO MAXVALUE | |
| CACHE 1; | |
| ALTER SEQUENCE public."UserLotteryStats_id_seq" OWNER TO postgres; | |
| -- | |
| -- Name: UserLotteryStats_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres | |
| -- | |
| ALTER SEQUENCE public."UserLotteryStats_id_seq" OWNED BY public."UserLotteryStats".id; | |
| -- | |
| -- Name: Wallet; Type: TABLE; Schema: public; Owner: postgres | |
| -- | |
| CREATE TABLE public."Wallet" ( | |
| id bigint NOT NULL, | |
| address text NOT NULL | |
| ); | |
| ALTER TABLE public."Wallet" OWNER TO postgres; | |
| -- | |
| -- Name: Wallet_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres | |
| -- | |
| CREATE SEQUENCE public."Wallet_id_seq" | |
| START WITH 1 | |
| INCREMENT BY 1 | |
| NO MINVALUE | |
| NO MAXVALUE | |
| CACHE 1; | |
| ALTER SEQUENCE public."Wallet_id_seq" OWNER TO postgres; | |
| -- | |
| -- Name: Wallet_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres | |
| -- | |
| ALTER SEQUENCE public."Wallet_id_seq" OWNED BY public."Wallet".id; | |
| -- | |
| -- Name: _prisma_migrations; Type: TABLE; Schema: public; Owner: postgres | |
| -- | |
| CREATE TABLE public._prisma_migrations ( | |
| id character varying(36) NOT NULL, | |
| checksum character varying(64) NOT NULL, | |
| finished_at timestamp with time zone, | |
| migration_name character varying(255) NOT NULL, | |
| logs text, | |
| rolled_back_at timestamp with time zone, | |
| started_at timestamp with time zone DEFAULT now() NOT NULL, | |
| applied_steps_count integer DEFAULT 0 NOT NULL | |
| ); | |
| ALTER TABLE public._prisma_migrations OWNER TO postgres; | |
| -- | |
| -- Name: public_lottery_stats; Type: VIEW; Schema: public; Owner: postgres | |
| -- | |
| CREATE VIEW public.public_lottery_stats AS | |
| SELECT d.id AS draw_id, | |
| d.name AS draw_name, | |
| d.prize_amount, | |
| d.status, | |
| count(DISTINCT t.user_id) AS participating_users, | |
| count(t.id) AS total_tickets, | |
| sum(t.qualifying_volume) AS total_volume, | |
| d.draw_date | |
| FROM (public."LotteryDraws" d | |
| LEFT JOIN public."LotteryTickets" t ON ((d.id = t.draw_id))) | |
| GROUP BY d.id, d.name, d.prize_amount, d.status, d.draw_date; | |
| ALTER VIEW public.public_lottery_stats OWNER TO postgres; | |
| -- | |
| -- Name: Balance id; Type: DEFAULT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."Balance" ALTER COLUMN id SET DEFAULT nextval('public."Balance_id_seq"'::regclass); | |
| -- | |
| -- Name: LiveData id; Type: DEFAULT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."LiveData" ALTER COLUMN id SET DEFAULT nextval('public."LiveData_id_seq"'::regclass); | |
| -- | |
| -- Name: LotteryConfig id; Type: DEFAULT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."LotteryConfig" ALTER COLUMN id SET DEFAULT nextval('public."LotteryConfig_id_seq"'::regclass); | |
| -- | |
| -- Name: LotteryDraws id; Type: DEFAULT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."LotteryDraws" ALTER COLUMN id SET DEFAULT nextval('public."LotteryDraws_id_seq"'::regclass); | |
| -- | |
| -- Name: LotteryTickets id; Type: DEFAULT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."LotteryTickets" ALTER COLUMN id SET DEFAULT nextval('public."LotteryTickets_id_seq"'::regclass); | |
| -- | |
| -- Name: Token id; Type: DEFAULT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."Token" ALTER COLUMN id SET DEFAULT nextval('public."Token_id_seq"'::regclass); | |
| -- | |
| -- Name: Trade id; Type: DEFAULT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."Trade" ALTER COLUMN id SET DEFAULT nextval('public."Trade_id_seq"'::regclass); | |
| -- | |
| -- Name: TradePreview id; Type: DEFAULT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."TradePreview" ALTER COLUMN id SET DEFAULT nextval('public."TradePreview_id_seq"'::regclass); | |
| -- | |
| -- Name: UserLotteryStats id; Type: DEFAULT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."UserLotteryStats" ALTER COLUMN id SET DEFAULT nextval('public."UserLotteryStats_id_seq"'::regclass); | |
| -- | |
| -- Name: Wallet id; Type: DEFAULT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."Wallet" ALTER COLUMN id SET DEFAULT nextval('public."Wallet_id_seq"'::regclass); | |
| -- | |
| -- Name: Balance Balance_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."Balance" | |
| ADD CONSTRAINT "Balance_pkey" PRIMARY KEY (id); | |
| -- | |
| -- Name: LiveData LiveData_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."LiveData" | |
| ADD CONSTRAINT "LiveData_pkey" PRIMARY KEY (id); | |
| -- | |
| -- Name: LotteryConfig LotteryConfig_key_key; Type: CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."LotteryConfig" | |
| ADD CONSTRAINT "LotteryConfig_key_key" UNIQUE (key); | |
| -- | |
| -- Name: LotteryConfig LotteryConfig_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."LotteryConfig" | |
| ADD CONSTRAINT "LotteryConfig_pkey" PRIMARY KEY (id); | |
| -- | |
| -- Name: LotteryDraws LotteryDraws_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."LotteryDraws" | |
| ADD CONSTRAINT "LotteryDraws_pkey" PRIMARY KEY (id); | |
| -- | |
| -- Name: LotteryTickets LotteryTickets_draw_id_ticket_number_key; Type: CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."LotteryTickets" | |
| ADD CONSTRAINT "LotteryTickets_draw_id_ticket_number_key" UNIQUE (draw_id, ticket_number); | |
| -- | |
| -- Name: LotteryTickets LotteryTickets_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."LotteryTickets" | |
| ADD CONSTRAINT "LotteryTickets_pkey" PRIMARY KEY (id); | |
| -- | |
| -- Name: LotteryTickets LotteryTickets_ticket_hash_key; Type: CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."LotteryTickets" | |
| ADD CONSTRAINT "LotteryTickets_ticket_hash_key" UNIQUE (ticket_hash); | |
| -- | |
| -- Name: Token Token_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."Token" | |
| ADD CONSTRAINT "Token_pkey" PRIMARY KEY (id); | |
| -- | |
| -- Name: TradePreview TradePreview_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."TradePreview" | |
| ADD CONSTRAINT "TradePreview_pkey" PRIMARY KEY (id); | |
| -- | |
| -- Name: Trade Trade_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."Trade" | |
| ADD CONSTRAINT "Trade_pkey" PRIMARY KEY (id); | |
| -- | |
| -- Name: UserLotteryStats UserLotteryStats_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."UserLotteryStats" | |
| ADD CONSTRAINT "UserLotteryStats_pkey" PRIMARY KEY (id); | |
| -- | |
| -- Name: UserLotteryStats UserLotteryStats_user_id_key; Type: CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."UserLotteryStats" | |
| ADD CONSTRAINT "UserLotteryStats_user_id_key" UNIQUE (user_id); | |
| -- | |
| -- Name: Wallet Wallet_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."Wallet" | |
| ADD CONSTRAINT "Wallet_pkey" PRIMARY KEY (id); | |
| -- | |
| -- Name: _prisma_migrations _prisma_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public._prisma_migrations | |
| ADD CONSTRAINT _prisma_migrations_pkey PRIMARY KEY (id); | |
| -- | |
| -- Name: LiveData_tokenId_key; Type: INDEX; Schema: public; Owner: postgres | |
| -- | |
| CREATE UNIQUE INDEX "LiveData_tokenId_key" ON public."LiveData" USING btree ("tokenId"); | |
| -- | |
| -- Name: Token_address_chain_key; Type: INDEX; Schema: public; Owner: postgres | |
| -- | |
| CREATE UNIQUE INDEX "Token_address_chain_key" ON public."Token" USING btree (address, chain); | |
| -- | |
| -- Name: TradePreview_walletId_tokenId_key; Type: INDEX; Schema: public; Owner: postgres | |
| -- | |
| CREATE UNIQUE INDEX "TradePreview_walletId_tokenId_key" ON public."TradePreview" USING btree ("walletId", "tokenId"); | |
| -- | |
| -- Name: Wallet_address_key; Type: INDEX; Schema: public; Owner: postgres | |
| -- | |
| CREATE UNIQUE INDEX "Wallet_address_key" ON public."Wallet" USING btree (address); | |
| -- | |
| -- Name: idx_lottery_draws_status; Type: INDEX; Schema: public; Owner: postgres | |
| -- | |
| CREATE INDEX idx_lottery_draws_status ON public."LotteryDraws" USING btree (status); | |
| -- | |
| -- Name: idx_lottery_tickets_draw_id; Type: INDEX; Schema: public; Owner: postgres | |
| -- | |
| CREATE INDEX idx_lottery_tickets_draw_id ON public."LotteryTickets" USING btree (draw_id); | |
| -- | |
| -- Name: idx_lottery_tickets_status; Type: INDEX; Schema: public; Owner: postgres | |
| -- | |
| CREATE INDEX idx_lottery_tickets_status ON public."LotteryTickets" USING btree (status); | |
| -- | |
| -- Name: idx_lottery_tickets_user_id; Type: INDEX; Schema: public; Owner: postgres | |
| -- | |
| CREATE INDEX idx_lottery_tickets_user_id ON public."LotteryTickets" USING btree (user_id); | |
| -- | |
| -- Name: LotteryConfig validate_lottery_config_trigger; Type: TRIGGER; Schema: public; Owner: postgres | |
| -- | |
| CREATE TRIGGER validate_lottery_config_trigger BEFORE INSERT OR UPDATE ON public."LotteryConfig" FOR EACH ROW EXECUTE FUNCTION public.validate_lottery_config(); | |
| -- | |
| -- Name: LotteryDraws validate_lottery_draw_trigger; Type: TRIGGER; Schema: public; Owner: postgres | |
| -- | |
| CREATE TRIGGER validate_lottery_draw_trigger BEFORE INSERT OR UPDATE ON public."LotteryDraws" FOR EACH ROW EXECUTE FUNCTION public.validate_lottery_draw(); | |
| -- | |
| -- Name: LotteryTickets validate_lottery_ticket_trigger; Type: TRIGGER; Schema: public; Owner: postgres | |
| -- | |
| CREATE TRIGGER validate_lottery_ticket_trigger BEFORE INSERT OR UPDATE ON public."LotteryTickets" FOR EACH ROW EXECUTE FUNCTION public.validate_lottery_ticket(); | |
| -- | |
| -- Name: Balance Balance_walletId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."Balance" | |
| ADD CONSTRAINT "Balance_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES public."Wallet"(id) ON UPDATE CASCADE ON DELETE RESTRICT; | |
| -- | |
| -- Name: LiveData LiveData_tokenId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."LiveData" | |
| ADD CONSTRAINT "LiveData_tokenId_fkey" FOREIGN KEY ("tokenId") REFERENCES public."Token"(id) ON UPDATE CASCADE ON DELETE CASCADE; | |
| -- | |
| -- Name: LotteryTickets LotteryTickets_draw_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."LotteryTickets" | |
| ADD CONSTRAINT "LotteryTickets_draw_id_fkey" FOREIGN KEY (draw_id) REFERENCES public."LotteryDraws"(id); | |
| -- | |
| -- Name: TradePreview TradePreview_tokenId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."TradePreview" | |
| ADD CONSTRAINT "TradePreview_tokenId_fkey" FOREIGN KEY ("tokenId") REFERENCES public."Token"(id) ON UPDATE CASCADE ON DELETE CASCADE; | |
| -- | |
| -- Name: TradePreview TradePreview_walletId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."TradePreview" | |
| ADD CONSTRAINT "TradePreview_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES public."Wallet"(id) ON UPDATE CASCADE ON DELETE CASCADE; | |
| -- | |
| -- Name: Trade Trade_tokenId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."Trade" | |
| ADD CONSTRAINT "Trade_tokenId_fkey" FOREIGN KEY ("tokenId") REFERENCES public."Token"(id) ON UPDATE CASCADE ON DELETE CASCADE; | |
| -- | |
| -- Name: Trade Trade_walletId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."Trade" | |
| ADD CONSTRAINT "Trade_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES public."Wallet"(id) ON UPDATE CASCADE ON DELETE CASCADE; | |
| -- | |
| -- PostgreSQL database dump complete | |
| -- | |
| === ./schema_xshot_core_prod.sql === | |
| -- | |
| -- PostgreSQL database dump | |
| -- | |
| -- Dumped from database version 16.4 (Ubuntu 16.4-1.pgdg24.04+1) | |
| -- Dumped by pg_dump version 16.6 (Debian 16.6-1.pgdg120+1) | |
| SET statement_timeout = 0; | |
| SET lock_timeout = 0; | |
| SET idle_in_transaction_session_timeout = 0; | |
| SET client_encoding = 'UTF8'; | |
| SET standard_conforming_strings = on; | |
| SELECT pg_catalog.set_config('search_path', '', false); | |
| SET check_function_bodies = false; | |
| SET xmloption = content; | |
| SET client_min_messages = warning; | |
| SET row_security = off; | |
| -- | |
| -- Name: Chain; Type: TYPE; Schema: public; Owner: postgres | |
| -- | |
| CREATE TYPE public."Chain" AS ENUM ( | |
| 'BSC', | |
| 'ETH', | |
| 'BASE', | |
| 'SOL', | |
| 'ARB', | |
| 'BLAST', | |
| 'TRON' | |
| ); | |
| ALTER TYPE public."Chain" OWNER TO postgres; | |
| -- | |
| -- Name: WalletType; Type: TYPE; Schema: public; Owner: postgres | |
| -- | |
| CREATE TYPE public."WalletType" AS ENUM ( | |
| 'EVM', | |
| 'SOL', | |
| 'TRON' | |
| ); | |
| ALTER TYPE public."WalletType" OWNER TO postgres; | |
| SET default_tablespace = ''; | |
| SET default_table_access_method = heap; | |
| -- | |
| -- Name: Benefit; Type: TABLE; Schema: public; Owner: postgres | |
| -- | |
| CREATE TABLE public."Benefit" ( | |
| id integer NOT NULL, | |
| channel_id bigint, | |
| text text NOT NULL, | |
| tokens_required numeric(65,30) DEFAULT 500000 NOT NULL | |
| ); | |
| ALTER TABLE public."Benefit" OWNER TO postgres; | |
| -- | |
| -- Name: Benefit_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres | |
| -- | |
| CREATE SEQUENCE public."Benefit_id_seq" | |
| AS integer | |
| START WITH 1 | |
| INCREMENT BY 1 | |
| NO MINVALUE | |
| NO MAXVALUE | |
| CACHE 1; | |
| ALTER SEQUENCE public."Benefit_id_seq" OWNER TO postgres; | |
| -- | |
| -- Name: Benefit_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres | |
| -- | |
| ALTER SEQUENCE public."Benefit_id_seq" OWNED BY public."Benefit".id; | |
| -- | |
| -- Name: ChainSetting; Type: TABLE; Schema: public; Owner: postgres | |
| -- | |
| CREATE TABLE public."ChainSetting" ( | |
| id bigint NOT NULL, | |
| user_id bigint NOT NULL, | |
| gas integer DEFAULT 5 NOT NULL, | |
| chain public."Chain" NOT NULL, | |
| price_threshold numeric(65,30) DEFAULT 0 NOT NULL, | |
| last_buy_price numeric(65,30) DEFAULT 0.15 NOT NULL, | |
| last_sell_percent integer DEFAULT 50 NOT NULL, | |
| last_buy_slippage double precision DEFAULT '-1'::integer NOT NULL, | |
| last_sell_slippage double precision DEFAULT '-1'::integer NOT NULL, | |
| marked_slippages integer[] DEFAULT ARRAY['-1'::integer, 20], | |
| quick_buy_keyboard numeric(65,30)[] DEFAULT ARRAY[0.15::numeric(65,30), 0.3::numeric(65,30), 0.25::numeric(65,30), 0.5::numeric(65,30)], | |
| fee_due numeric(65,30) DEFAULT 0 NOT NULL, | |
| fee_to_receive numeric(65,30) DEFAULT 0 NOT NULL, | |
| total_fees_made numeric(65,30) DEFAULT 0 NOT NULL | |
| ); | |
| ALTER TABLE public."ChainSetting" OWNER TO postgres; | |
| -- | |
| -- Name: ChainSetting_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres | |
| -- | |
| CREATE SEQUENCE public."ChainSetting_id_seq" | |
| START WITH 1 | |
| INCREMENT BY 1 | |
| NO MINVALUE | |
| NO MAXVALUE | |
| CACHE 1; | |
| ALTER SEQUENCE public."ChainSetting_id_seq" OWNER TO postgres; | |
| -- | |
| -- Name: ChainSetting_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres | |
| -- | |
| ALTER SEQUENCE public."ChainSetting_id_seq" OWNED BY public."ChainSetting".id; | |
| -- | |
| -- Name: Post; Type: TABLE; Schema: public; Owner: postgres | |
| -- | |
| CREATE TABLE public."Post" ( | |
| id integer NOT NULL, | |
| title text NOT NULL, | |
| content text NOT NULL, | |
| published boolean DEFAULT true NOT NULL, | |
| author_id bigint NOT NULL, | |
| media integer, | |
| created_at timestamp(3) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL | |
| ); | |
| ALTER TABLE public."Post" OWNER TO postgres; | |
| -- | |
| -- Name: PostButton; Type: TABLE; Schema: public; Owner: postgres | |
| -- | |
| CREATE TABLE public."PostButton" ( | |
| id integer NOT NULL, | |
| post_id integer NOT NULL, | |
| text text NOT NULL, | |
| url text NOT NULL, | |
| "row" integer NOT NULL | |
| ); | |
| ALTER TABLE public."PostButton" OWNER TO postgres; | |
| -- | |
| -- Name: PostButton_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres | |
| -- | |
| CREATE SEQUENCE public."PostButton_id_seq" | |
| AS integer | |
| START WITH 1 | |
| INCREMENT BY 1 | |
| NO MINVALUE | |
| NO MAXVALUE | |
| CACHE 1; | |
| ALTER SEQUENCE public."PostButton_id_seq" OWNER TO postgres; | |
| -- | |
| -- Name: PostButton_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres | |
| -- | |
| ALTER SEQUENCE public."PostButton_id_seq" OWNED BY public."PostButton".id; | |
| -- | |
| -- Name: Post_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres | |
| -- | |
| CREATE SEQUENCE public."Post_id_seq" | |
| AS integer | |
| START WITH 1 | |
| INCREMENT BY 1 | |
| NO MINVALUE | |
| NO MAXVALUE | |
| CACHE 1; | |
| ALTER SEQUENCE public."Post_id_seq" OWNER TO postgres; | |
| -- | |
| -- Name: Post_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres | |
| -- | |
| ALTER SEQUENCE public."Post_id_seq" OWNED BY public."Post".id; | |
| -- | |
| -- Name: User; Type: TABLE; Schema: public; Owner: postgres | |
| -- | |
| CREATE TABLE public."User" ( | |
| id bigint NOT NULL, | |
| created_at timestamp(3) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, | |
| is_admin boolean DEFAULT false NOT NULL, | |
| timezone text DEFAULT 'UTC+00:00'::text NOT NULL, | |
| ref_link boolean DEFAULT false NOT NULL, | |
| ref_to_id bigint, | |
| accepted_tos boolean DEFAULT false NOT NULL, | |
| is_lottery_admin boolean DEFAULT false | |
| ); | |
| ALTER TABLE public."User" OWNER TO postgres; | |
| -- | |
| -- Name: Wallet; Type: TABLE; Schema: public; Owner: postgres | |
| -- | |
| CREATE TABLE public."Wallet" ( | |
| id bigint NOT NULL, | |
| user_id bigint NOT NULL, | |
| address text NOT NULL, | |
| alias text, | |
| pkey text NOT NULL, | |
| created_at timestamp(3) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, | |
| type public."WalletType" DEFAULT 'EVM'::public."WalletType" NOT NULL, | |
| abs_id bigint | |
| ); | |
| ALTER TABLE public."Wallet" OWNER TO postgres; | |
| -- | |
| -- Name: Wallet_id_seq; Type: SEQUENCE; Schema: public; Owner: postgres | |
| -- | |
| CREATE SEQUENCE public."Wallet_id_seq" | |
| START WITH 1 | |
| INCREMENT BY 1 | |
| NO MINVALUE | |
| NO MAXVALUE | |
| CACHE 1; | |
| ALTER SEQUENCE public."Wallet_id_seq" OWNER TO postgres; | |
| -- | |
| -- Name: Wallet_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: postgres | |
| -- | |
| ALTER SEQUENCE public."Wallet_id_seq" OWNED BY public."Wallet".id; | |
| -- | |
| -- Name: _prisma_migrations; Type: TABLE; Schema: public; Owner: postgres | |
| -- | |
| CREATE TABLE public._prisma_migrations ( | |
| id character varying(36) NOT NULL, | |
| checksum character varying(64) NOT NULL, | |
| finished_at timestamp with time zone, | |
| migration_name character varying(255) NOT NULL, | |
| logs text, | |
| rolled_back_at timestamp with time zone, | |
| started_at timestamp with time zone DEFAULT now() NOT NULL, | |
| applied_steps_count integer DEFAULT 0 NOT NULL | |
| ); | |
| ALTER TABLE public._prisma_migrations OWNER TO postgres; | |
| -- | |
| -- Name: Benefit id; Type: DEFAULT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."Benefit" ALTER COLUMN id SET DEFAULT nextval('public."Benefit_id_seq"'::regclass); | |
| -- | |
| -- Name: ChainSetting id; Type: DEFAULT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."ChainSetting" ALTER COLUMN id SET DEFAULT nextval('public."ChainSetting_id_seq"'::regclass); | |
| -- | |
| -- Name: Post id; Type: DEFAULT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."Post" ALTER COLUMN id SET DEFAULT nextval('public."Post_id_seq"'::regclass); | |
| -- | |
| -- Name: PostButton id; Type: DEFAULT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."PostButton" ALTER COLUMN id SET DEFAULT nextval('public."PostButton_id_seq"'::regclass); | |
| -- | |
| -- Name: Wallet id; Type: DEFAULT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."Wallet" ALTER COLUMN id SET DEFAULT nextval('public."Wallet_id_seq"'::regclass); | |
| -- | |
| -- Name: Benefit Benefit_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."Benefit" | |
| ADD CONSTRAINT "Benefit_pkey" PRIMARY KEY (id); | |
| -- | |
| -- Name: ChainSetting ChainSetting_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."ChainSetting" | |
| ADD CONSTRAINT "ChainSetting_pkey" PRIMARY KEY (id); | |
| -- | |
| -- Name: PostButton PostButton_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."PostButton" | |
| ADD CONSTRAINT "PostButton_pkey" PRIMARY KEY (id); | |
| -- | |
| -- Name: Post Post_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."Post" | |
| ADD CONSTRAINT "Post_pkey" PRIMARY KEY (id); | |
| -- | |
| -- Name: User User_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."User" | |
| ADD CONSTRAINT "User_pkey" PRIMARY KEY (id); | |
| -- | |
| -- Name: Wallet Wallet_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."Wallet" | |
| ADD CONSTRAINT "Wallet_pkey" PRIMARY KEY (id); | |
| -- | |
| -- Name: _prisma_migrations _prisma_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public._prisma_migrations | |
| ADD CONSTRAINT _prisma_migrations_pkey PRIMARY KEY (id); | |
| -- | |
| -- Name: ChainSetting_chain_user_id_key; Type: INDEX; Schema: public; Owner: postgres | |
| -- | |
| CREATE UNIQUE INDEX "ChainSetting_chain_user_id_key" ON public."ChainSetting" USING btree (chain, user_id); | |
| -- | |
| -- Name: ChainSetting ChainSetting_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."ChainSetting" | |
| ADD CONSTRAINT "ChainSetting_user_id_fkey" FOREIGN KEY (user_id) REFERENCES public."User"(id) ON UPDATE CASCADE ON DELETE CASCADE; | |
| -- | |
| -- Name: PostButton PostButton_post_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."PostButton" | |
| ADD CONSTRAINT "PostButton_post_id_fkey" FOREIGN KEY (post_id) REFERENCES public."Post"(id) ON UPDATE CASCADE ON DELETE CASCADE; | |
| -- | |
| -- Name: Post Post_author_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."Post" | |
| ADD CONSTRAINT "Post_author_id_fkey" FOREIGN KEY (author_id) REFERENCES public."User"(id) ON UPDATE CASCADE ON DELETE CASCADE; | |
| -- | |
| -- Name: User User_ref_to_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."User" | |
| ADD CONSTRAINT "User_ref_to_id_fkey" FOREIGN KEY (ref_to_id) REFERENCES public."User"(id) ON UPDATE CASCADE ON DELETE SET NULL; | |
| -- | |
| -- Name: Wallet Wallet_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: postgres | |
| -- | |
| ALTER TABLE ONLY public."Wallet" | |
| ADD CONSTRAINT "Wallet_user_id_fkey" FOREIGN KEY (user_id) REFERENCES public."User"(id) ON UPDATE CASCADE ON DELETE CASCADE; | |
| -- | |
| -- PostgreSQL database dump complete | |
| -- | |
| === ./src_contents_and_structure.txt === | |
| === ./<q === | |
| <script> | |
| import { slide } from 'svelte/transition'; | |
| import SlotMachine from './SlotMachine.svelte'; | |
| export let revealed = false; | |
| export let hash = ''; | |
| export let onReveal = () => {}; | |
| let isSpinning = false; | |
| let showSlot = false; | |
| // If we come in with revealed=true from the parent, | |
| // immediately show the slot with final value: | |
| $: if (revealed && !showSlot) { | |
| showSlot = true; | |
| isSpinning = false; | |
| } | |
| async function handleReveal() { | |
| if (revealed || isSpinning) return; | |
| showSlot = true; | |
| isSpinning = true; | |
| const result = await onReveal(); | |
| if (result) { | |
| hash = result; | |
| // after a short timeout, mark spinning as false | |
| setTimeout(() => { | |
| isSpinning = false; | |
| }, 2000); | |
| } else { | |
| // revert if reveal failed | |
| isSpinning = false; | |
| showSlot = false; | |
| } | |
| } | |
| </script> | |
| <div | |
| class="relative bg-gradient-to-br from-purple-600/30 to-pink-600/30 | |
| p-4 rounded-xl shadow-lg transform transition-all duration-300 | |
| hover:scale-105 cursor-pointer overflow-hidden w-full text-left" | |
| on:click={handleReveal} | |
| aria-label={revealed ? 'Revealed ticket' : 'Click to reveal ticket'} | |
| disabled={revealed} | |
| > | |
| {#if showSlot} | |
| <!-- Already revealing or revealed --> | |
| <div transition:slide> | |
| <SlotMachine | |
| spinning={isSpinning} | |
| finalValue={hash} | |
| on:spinComplete={() => revealed = true} | |
| /> | |
| </div> | |
| {:else} | |
| <!-- Not revealed yet --> | |
| <div class="text-center" in:slide> | |
| <div class="text-3xl mb-2">🎟️</div> | |
| <div class="text-sm text-gray-300"> | |
| Click to Reveal! | |
| </div> | |
| </div> | |
| {/if} | |
| </div> | |
| <style> | |
| .revealed { | |
| cursor: default; | |
| } | |
| </style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment