mirror of
https://github.com/pezkuwichain/pezkuwi-telegram-miniapp.git
synced 2026-06-13 09:21:10 +00:00
feat: add USDT deposit system with TRC20 and Polkadot support
This commit is contained in:
+2
-1
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
⚠️ Vê 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 dê 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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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' },
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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');
|
||||
Reference in New Issue
Block a user