Skip to content

Instantly share code, notes, and snippets.

@Y0lan
Created February 1, 2025 21:05
Show Gist options
  • Select an option

  • Save Y0lan/bb714a71b9db090febf73d686fbbd385 to your computer and use it in GitHub Desktop.

Select an option

Save Y0lan/bb714a71b9db090febf73d686fbbd385 to your computer and use it in GitHub Desktop.
=== 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