feat: add USDT deposit system with TRC20 and Polkadot support

This commit is contained in:
2026-02-08 01:14:21 +03:00
parent 12792277f9
commit 456bbf1dd2
7 changed files with 823 additions and 17 deletions
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "pezkuwi-telegram-miniapp",
"version": "1.0.168",
"version": "1.0.169",
"type": "module",
"description": "Pezkuwichain Telegram Mini App - Forum, Announcements, Rewards",
"author": "Pezkuwichain Team",
@@ -76,6 +76,7 @@
"postcss": "^8.4.47",
"prettier": "^3.8.1",
"tailwindcss": "^3.4.11",
"tronweb": "^6.2.0",
"typescript": "^5.5.3",
"vite": "^5.4.1",
"vitest": "^4.0.18"
+390
View File
@@ -0,0 +1,390 @@
/**
* Deposit USDT Modal
* Allows users to deposit USDT (TRC20 or Polkadot) to get wUSDT on Asset Hub
*/
import { useState, useEffect } from 'react';
import {
X,
Copy,
CheckCircle,
ExternalLink,
AlertCircle,
Loader2,
History,
Plus,
} from 'lucide-react';
import { useTelegram } from '@/hooks/useTelegram';
import { supabase } from '@/lib/supabase';
type Network = 'trc20' | 'polkadot';
interface NetworkInfo {
id: Network;
name: string;
description: string;
address: string;
explorer: string;
icon: string;
minAmount: number;
confirmations: number;
}
const NETWORKS: NetworkInfo[] = [
{
id: 'trc20',
name: 'TRC20 (TRON)',
description: 'USDT li ser tora TRON',
address: import.meta.env.VITE_DEPOSIT_TRON_ADDRESS || '',
explorer: 'https://tronscan.org/#/transaction/',
icon: '🔴',
minAmount: 10,
confirmations: 20,
},
{
id: 'polkadot',
name: 'Polkadot Asset Hub',
description: 'USDT li ser Polkadot',
address: import.meta.env.VITE_DEPOSIT_POLKADOT_ADDRESS || '',
explorer: 'https://assethub-polkadot.subscan.io/extrinsic/',
icon: '⚪',
minAmount: 10,
confirmations: 1,
},
];
interface Deposit {
id: string;
network: Network;
amount: number;
status: string;
tx_hash: string | null;
created_at: string;
}
interface Props {
isOpen: boolean;
onClose: () => void;
userId: string | null;
}
export function DepositUSDTModal({ isOpen, onClose }: Props) {
const { hapticImpact, showAlert } = useTelegram();
const [selectedNetwork, setSelectedNetwork] = useState<Network>('trc20');
const [depositCode, setDepositCode] = useState<string>('');
const [copied, setCopied] = useState<'address' | 'memo' | null>(null);
const [deposits, setDeposits] = useState<Deposit[]>([]);
const [showHistory, setShowHistory] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const network = NETWORKS.find((n) => n.id === selectedNetwork) || NETWORKS[0];
// Fetch user's deposit code via edge function
useEffect(() => {
const fetchDepositCode = async () => {
if (!isOpen) return;
const initData = window.Telegram?.WebApp?.initData;
if (!initData) {
setDepositCode('---');
return;
}
setIsLoading(true);
try {
const { data, error } = await supabase.functions.invoke('get-deposit-code', {
body: { initData },
});
if (error) {
console.error('Error fetching deposit code:', error);
setDepositCode('---');
} else if (data?.code) {
setDepositCode(data.code);
}
} catch (err) {
console.error('Error fetching deposit code:', err);
setDepositCode('---');
} finally {
setIsLoading(false);
}
};
fetchDepositCode();
}, [isOpen]);
// Fetch deposits history
useEffect(() => {
const fetchDeposits = async () => {
if (!isOpen) return;
const initData = window.Telegram?.WebApp?.initData;
if (!initData) return;
try {
const { data, error } = await supabase.functions.invoke('get-deposits', {
body: { initData },
});
if (!error && data?.deposits) {
setDeposits(data.deposits as Deposit[]);
}
} catch {
// Ignore - deposits history is optional
}
};
fetchDeposits();
}, [isOpen]);
const copyToClipboard = async (text: string, type: 'address' | 'memo') => {
try {
await navigator.clipboard.writeText(text);
setCopied(type);
hapticImpact('light');
setTimeout(() => setCopied(null), 2000);
} catch {
showAlert('Kopî nekir');
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'completed':
return 'text-green-400';
case 'confirming':
return 'text-yellow-400';
case 'failed':
return 'text-red-400';
default:
return 'text-muted-foreground';
}
};
const getStatusText = (status: string) => {
switch (status) {
case 'pending':
return 'Li benda';
case 'confirming':
return 'Tê pejirandin';
case 'completed':
return 'Qediya';
case 'failed':
return 'Neserketî';
case 'expired':
return 'Dema wê derbas bû';
default:
return status;
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-end justify-center bg-black/60 backdrop-blur-sm">
<div className="w-full max-w-md bg-background rounded-t-3xl p-6 pb-8 animate-slide-up max-h-[90vh] overflow-y-auto">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="p-2 bg-green-500/20 rounded-full">
<Plus className="w-5 h-5 text-green-400" />
</div>
<div>
<h2 className="text-lg font-semibold">USDT Depo Bike</h2>
<p className="text-xs text-muted-foreground">Bo wUSDT li Asset Hub</p>
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => setShowHistory(!showHistory)}
className={`p-2 rounded-full ${showHistory ? 'text-cyan-400 bg-cyan-400/10' : 'text-muted-foreground hover:text-white'}`}
>
<History className="w-5 h-5" />
</button>
<button
onClick={onClose}
className="p-2 text-muted-foreground hover:text-white rounded-full"
>
<X className="w-5 h-5" />
</button>
</div>
</div>
{showHistory ? (
/* Deposit History */
<div className="space-y-3">
<h3 className="text-sm font-medium text-muted-foreground">Dîroka Depoyan</h3>
{deposits.length === 0 ? (
<p className="text-center text-muted-foreground py-8">Hîn depo tune</p>
) : (
deposits.map((deposit) => (
<div key={deposit.id} className="bg-muted/50 rounded-xl p-3">
<div className="flex justify-between items-start">
<div>
<div className="flex items-center gap-2">
<span>{deposit.network === 'trc20' ? '🔴' : '⚪'}</span>
<span className="font-mono">{deposit.amount} USDT</span>
</div>
<p className="text-xs text-muted-foreground mt-1">
{new Date(deposit.created_at).toLocaleDateString('ku')}
</p>
</div>
<span className={`text-sm ${getStatusColor(deposit.status)}`}>
{getStatusText(deposit.status)}
</span>
</div>
{deposit.tx_hash && (
<a
href={`${NETWORKS.find((n) => n.id === deposit.network)?.explorer}${deposit.tx_hash}`}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-blue-400 flex items-center gap-1 mt-2"
>
<ExternalLink className="w-3 h-3" />
TX bibîne
</a>
)}
</div>
))
)}
<button
onClick={() => setShowHistory(false)}
className="w-full py-3 bg-muted rounded-xl text-center"
>
Vegere
</button>
</div>
) : (
<div className="space-y-4">
{/* Network Selection */}
<div>
<label className="text-sm text-muted-foreground mb-2 block">Torê Hilbijêre</label>
<div className="flex gap-2">
{NETWORKS.map((net) => (
<button
key={net.id}
onClick={() => {
setSelectedNetwork(net.id);
hapticImpact('light');
}}
className={`flex-1 p-3 rounded-xl border transition-all ${
selectedNetwork === net.id
? 'border-green-500 bg-green-500/10'
: 'border-border bg-muted/50'
}`}
>
<div className="text-xl mb-1">{net.icon}</div>
<div className="text-sm font-medium">{net.name}</div>
<div className="text-xs text-muted-foreground">{net.description}</div>
</button>
))}
</div>
</div>
{/* Important Notice */}
<div className="p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
<div className="flex gap-2">
<AlertCircle className="w-5 h-5 text-yellow-400 flex-shrink-0" />
<div className="text-sm text-yellow-400">
<p className="font-medium">Girîng!</p>
<ul className="list-disc list-inside text-xs mt-1 space-y-1">
<li>
Kêmtirîn: <strong>{network.minAmount} USDT</strong>
</li>
<li>Memo/Not qada de koda xwe binivîse</li>
<li>Tenê USDT bişîne, tokenên din winda dibin</li>
</ul>
</div>
</div>
</div>
{/* Deposit Address */}
<div className="bg-muted/50 rounded-xl p-4 space-y-4">
{/* Address */}
<div>
<label className="text-xs text-muted-foreground">Navnîşana Depoyê</label>
<div className="flex items-center gap-2 mt-1">
<code className="flex-1 text-sm font-mono bg-background p-2 rounded-lg break-all">
{network.address || 'Amade nîne'}
</code>
<button
onClick={() => copyToClipboard(network.address, 'address')}
disabled={!network.address}
className="p-2 bg-background rounded-lg hover:bg-muted transition-colors"
>
{copied === 'address' ? (
<CheckCircle className="w-5 h-5 text-green-400" />
) : (
<Copy className="w-5 h-5" />
)}
</button>
</div>
</div>
{/* Memo/Reference */}
<div>
<label className="text-xs text-muted-foreground">Memo / Referans (PÊWÎST)</label>
<div className="flex items-center gap-2 mt-1">
{isLoading ? (
<div className="flex-1 p-2 bg-background rounded-lg flex justify-center">
<Loader2 className="w-5 h-5 animate-spin" />
</div>
) : (
<code className="flex-1 text-lg font-mono font-bold bg-background p-2 rounded-lg text-center text-green-400">
{depositCode || '---'}
</code>
)}
<button
onClick={() => copyToClipboard(depositCode, 'memo')}
disabled={!depositCode || depositCode === '---'}
className="p-2 bg-background rounded-lg hover:bg-muted transition-colors"
>
{copied === 'memo' ? (
<CheckCircle className="w-5 h-5 text-green-400" />
) : (
<Copy className="w-5 h-5" />
)}
</button>
</div>
<p className="text-xs text-red-400 mt-1">
kodê di memo/not de binivîse, wekî din depoya te nayê nas kirin!
</p>
</div>
</div>
{/* Steps */}
<div className="bg-muted/30 rounded-xl p-4">
<h4 className="text-sm font-medium mb-3">Çawa Depo Bikim?</h4>
<ol className="text-xs text-muted-foreground space-y-2">
<li className="flex gap-2">
<span className="text-green-400 font-bold">1.</span>
<span>Navnîşan û memo kopî bike</span>
</li>
<li className="flex gap-2">
<span className="text-green-400 font-bold">2.</span>
<span>Di cîzdana xwe de ({network.name}) USDT bişîne navnîşana jorîn</span>
</li>
<li className="flex gap-2">
<span className="text-green-400 font-bold">3.</span>
<span>
<strong>MEMO qada de koda xwe binivîse!</strong>
</span>
</li>
<li className="flex gap-2">
<span className="text-green-400 font-bold">4.</span>
<span>Piştî {network.confirmations} pejirandinê, wUSDT li Asset Hub be</span>
</li>
</ol>
</div>
{/* Processing Time */}
<p className="text-center text-xs text-muted-foreground">
Dema pêvajoyê: ~5-30 hûrdem li gorî torê
</p>
</div>
)}
</div>
</div>
);
}
+37 -13
View File
@@ -31,6 +31,7 @@ import {
getConnectionState,
} from '@/lib/rpc-manager';
import { FundFeesModal } from './FundFeesModal';
import { DepositUSDTModal } from './DepositUSDTModal';
// Asset IDs matching pwap/web configuration
const ASSET_IDS = {
@@ -280,6 +281,7 @@ export function TokensCard({ onSendToken }: Props) {
const [assetHubHezBalance, setAssetHubHezBalance] = useState<string>('--');
const [peopleHezBalance, setPeopleHezBalance] = useState<string>('--');
const [showFundFeesModal, setShowFundFeesModal] = useState(false);
const [showDepositModal, setShowDepositModal] = useState(false);
// Fetch prices from CoinGecko
const fetchPrices = useCallback(async () => {
@@ -909,19 +911,34 @@ export function TokensCard({ onSendToken }: Props) {
)}
</div>
) : (
onSendToken &&
token.balance !== '--' &&
parseFloat(token.balance) > 0 && (
<button
onClick={() => {
hapticImpact('light');
onSendToken(token);
}}
className="p-2 text-muted-foreground hover:text-white hover:bg-white/10 rounded-lg"
>
<Send className="w-4 h-4" />
</button>
)
<div className="flex items-center gap-1">
{/* Deposit button for USDT */}
{token.displaySymbol === 'USDT' && (
<button
onClick={() => {
hapticImpact('light');
setShowDepositModal(true);
}}
className="p-2 text-green-400 hover:text-green-300 hover:bg-green-500/10 rounded-lg"
title="USDT Depo Bike"
>
<Plus className="w-4 h-4" />
</button>
)}
{onSendToken &&
token.balance !== '--' &&
parseFloat(token.balance) > 0 && (
<button
onClick={() => {
hapticImpact('light');
onSendToken(token);
}}
className="p-2 text-muted-foreground hover:text-white hover:bg-white/10 rounded-lg"
>
<Send className="w-4 h-4" />
</button>
)}
</div>
)}
</div>
</div>
@@ -935,6 +952,13 @@ export function TokensCard({ onSendToken }: Props) {
{/* Fund Fees Modal for XCM Teleport */}
<FundFeesModal isOpen={showFundFeesModal} onClose={() => setShowFundFeesModal(false)} />
{/* Deposit USDT Modal */}
<DepositUSDTModal
isOpen={showDepositModal}
onClose={() => setShowDepositModal(false)}
userId={null}
/>
</div>
);
}
+3 -3
View File
@@ -1,5 +1,5 @@
{
"version": "1.0.168",
"buildTime": "2026-02-07T21:21:39.509Z",
"buildNumber": 1770499299510
"version": "1.0.169",
"buildTime": "2026-02-07T22:14:22.046Z",
"buildNumber": 1770502462047
}
@@ -0,0 +1,170 @@
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
import { createHmac } from 'https://deno.land/std@0.177.0/node/crypto.ts';
const ALLOWED_ORIGIN = 'https://telegram.pezkuwichain.io';
function getCorsHeaders(): Record<string, string> {
return {
'Access-Control-Allow-Origin': ALLOWED_ORIGIN,
'Access-Control-Allow-Headers':
'authorization, x-client-info, apikey, content-type, x-supabase-client-platform',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
};
}
interface TelegramUser {
id: number;
first_name: string;
last_name?: string;
username?: string;
}
function validateInitData(initData: string, botToken: string): TelegramUser | null {
try {
const params = new URLSearchParams(initData);
const hash = params.get('hash');
if (!hash) return null;
params.delete('hash');
const sortedParams = Array.from(params.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${key}=${value}`)
.join('\n');
const secretKey = createHmac('sha256', 'WebAppData').update(botToken).digest();
const calculatedHash = createHmac('sha256', secretKey).update(sortedParams).digest('hex');
if (calculatedHash !== hash) return null;
const authDate = parseInt(params.get('auth_date') || '0');
const now = Math.floor(Date.now() / 1000);
if (now - authDate > 86400) return null;
const userStr = params.get('user');
if (!userStr) return null;
return JSON.parse(userStr) as TelegramUser;
} catch {
return null;
}
}
function generateDepositCode(): string {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
let result = 'PEZ-';
for (let i = 0; i < 8; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
serve(async (req) => {
const corsHeaders = getCorsHeaders();
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
try {
const body = await req.json();
const { initData } = body;
if (!initData) {
return new Response(JSON.stringify({ error: 'Missing initData' }), {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
const botToken = Deno.env.get('TELEGRAM_BOT_TOKEN');
if (!botToken) {
return new Response(JSON.stringify({ error: 'Server configuration error' }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
const telegramUser = validateInitData(initData, botToken);
if (!telegramUser) {
return new Response(JSON.stringify({ error: 'Invalid Telegram data' }), {
status: 401,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
const supabase = createClient(supabaseUrl, supabaseServiceKey, {
auth: { autoRefreshToken: false, persistSession: false },
});
// Get or create user
let userId: string;
const { data: existingUser } = await supabase
.from('tg_users')
.select('id')
.eq('telegram_id', telegramUser.id)
.single();
if (existingUser) {
userId = existingUser.id;
} else {
const { data: newUser, error: createError } = await supabase
.from('tg_users')
.insert({
telegram_id: telegramUser.id,
username: telegramUser.username || null,
first_name: telegramUser.first_name,
last_name: telegramUser.last_name || null,
})
.select('id')
.single();
if (createError || !newUser) {
return new Response(JSON.stringify({ error: 'Failed to create user' }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
userId = newUser.id;
}
// Get or create deposit code
const { data: existingCode } = await supabase
.from('tg_user_deposit_codes')
.select('code')
.eq('user_id', userId)
.single();
if (existingCode) {
return new Response(JSON.stringify({ code: existingCode.code }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
// Create new code
const newCode = generateDepositCode();
const { error: insertError } = await supabase
.from('tg_user_deposit_codes')
.insert({ user_id: userId, code: newCode });
if (insertError) {
return new Response(JSON.stringify({ error: 'Failed to create deposit code' }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
return new Response(JSON.stringify({ code: newCode }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
} catch {
return new Response(JSON.stringify({ error: 'Internal server error' }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
});
+125
View File
@@ -0,0 +1,125 @@
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
import { createHmac } from 'https://deno.land/std@0.177.0/node/crypto.ts';
const ALLOWED_ORIGIN = 'https://telegram.pezkuwichain.io';
function getCorsHeaders(): Record<string, string> {
return {
'Access-Control-Allow-Origin': ALLOWED_ORIGIN,
'Access-Control-Allow-Headers':
'authorization, x-client-info, apikey, content-type, x-supabase-client-platform',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
};
}
interface TelegramUser {
id: number;
first_name: string;
last_name?: string;
username?: string;
}
function validateInitData(initData: string, botToken: string): TelegramUser | null {
try {
const params = new URLSearchParams(initData);
const hash = params.get('hash');
if (!hash) return null;
params.delete('hash');
const sortedParams = Array.from(params.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, value]) => `${key}=${value}`)
.join('\n');
const secretKey = createHmac('sha256', 'WebAppData').update(botToken).digest();
const calculatedHash = createHmac('sha256', secretKey).update(sortedParams).digest('hex');
if (calculatedHash !== hash) return null;
const authDate = parseInt(params.get('auth_date') || '0');
const now = Math.floor(Date.now() / 1000);
if (now - authDate > 86400) return null;
const userStr = params.get('user');
if (!userStr) return null;
return JSON.parse(userStr) as TelegramUser;
} catch {
return null;
}
}
serve(async (req) => {
const corsHeaders = getCorsHeaders();
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
try {
const body = await req.json();
const { initData } = body;
if (!initData) {
return new Response(JSON.stringify({ error: 'Missing initData' }), {
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
const botToken = Deno.env.get('TELEGRAM_BOT_TOKEN');
if (!botToken) {
return new Response(JSON.stringify({ error: 'Server configuration error' }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
const telegramUser = validateInitData(initData, botToken);
if (!telegramUser) {
return new Response(JSON.stringify({ error: 'Invalid Telegram data' }), {
status: 401,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
const supabase = createClient(supabaseUrl, supabaseServiceKey, {
auth: { autoRefreshToken: false, persistSession: false },
});
// Get user
const { data: user } = await supabase
.from('tg_users')
.select('id')
.eq('telegram_id', telegramUser.id)
.single();
if (!user) {
return new Response(JSON.stringify({ deposits: [] }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
// Get deposits
const { data: deposits } = await supabase
.from('tg_deposits')
.select('id, network, amount, status, tx_hash, created_at')
.eq('user_id', user.id)
.order('created_at', { ascending: false })
.limit(20);
return new Response(JSON.stringify({ deposits: deposits || [] }), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
} catch {
return new Response(JSON.stringify({ error: 'Internal server error' }), {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
});
}
});
@@ -0,0 +1,96 @@
-- Deposit System Tables
-- For USDT deposits (TRC20 and Polkadot)
-- User deposit codes (unique memo for each user)
CREATE TABLE IF NOT EXISTS tg_user_deposit_codes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES tg_users(id) ON DELETE CASCADE,
code VARCHAR(12) NOT NULL UNIQUE,
created_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT unique_user_deposit_code UNIQUE (user_id)
);
-- Deposit records
CREATE TABLE IF NOT EXISTS tg_deposits (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES tg_users(id) ON DELETE CASCADE,
network VARCHAR(20) NOT NULL CHECK (network IN ('trc20', 'polkadot')),
amount DECIMAL(20, 6) NOT NULL,
tx_hash VARCHAR(100),
memo VARCHAR(50),
status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'confirming', 'completed', 'failed', 'expired')),
wusdt_tx_hash VARCHAR(100),
error_message TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
confirmed_at TIMESTAMPTZ,
processed_at TIMESTAMPTZ
);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_deposits_user_id ON tg_deposits(user_id);
CREATE INDEX IF NOT EXISTS idx_deposits_status ON tg_deposits(status);
CREATE INDEX IF NOT EXISTS idx_deposits_network ON tg_deposits(network);
CREATE INDEX IF NOT EXISTS idx_deposits_tx_hash ON tg_deposits(tx_hash);
CREATE INDEX IF NOT EXISTS idx_deposit_codes_code ON tg_user_deposit_codes(code);
-- Function to generate unique deposit code
CREATE OR REPLACE FUNCTION generate_deposit_code()
RETURNS VARCHAR(12) AS $$
DECLARE
chars VARCHAR(36) := 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
result VARCHAR(12) := 'PEZ-';
i INTEGER;
BEGIN
FOR i IN 1..8 LOOP
result := result || substr(chars, floor(random() * length(chars) + 1)::integer, 1);
END LOOP;
RETURN result;
END;
$$ LANGUAGE plpgsql;
-- Trigger to auto-generate deposit code for new users
CREATE OR REPLACE FUNCTION create_user_deposit_code()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO tg_user_deposit_codes (user_id, code)
VALUES (NEW.id, generate_deposit_code())
ON CONFLICT (user_id) DO NOTHING;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trigger_create_deposit_code ON tg_users;
CREATE TRIGGER trigger_create_deposit_code
AFTER INSERT ON tg_users
FOR EACH ROW
EXECUTE FUNCTION create_user_deposit_code();
-- Generate codes for existing users
INSERT INTO tg_user_deposit_codes (user_id, code)
SELECT id, generate_deposit_code()
FROM tg_users
WHERE id NOT IN (SELECT user_id FROM tg_user_deposit_codes)
ON CONFLICT DO NOTHING;
-- RLS Policies
ALTER TABLE tg_user_deposit_codes ENABLE ROW LEVEL SECURITY;
ALTER TABLE tg_deposits ENABLE ROW LEVEL SECURITY;
-- Users can read their own deposit code
CREATE POLICY "Users can view own deposit code"
ON tg_user_deposit_codes FOR SELECT
USING (auth.uid() = user_id);
-- Users can view their own deposits
CREATE POLICY "Users can view own deposits"
ON tg_deposits FOR SELECT
USING (auth.uid() = user_id);
-- Service role can do everything (for backend)
CREATE POLICY "Service role full access to deposit codes"
ON tg_user_deposit_codes FOR ALL
USING (auth.role() = 'service_role');
CREATE POLICY "Service role full access to deposits"
ON tg_deposits FOR ALL
USING (auth.role() = 'service_role');