diff --git a/package.json b/package.json index 5ef5bf5..f3a448d 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/components/wallet/DepositUSDTModal.tsx b/src/components/wallet/DepositUSDTModal.tsx new file mode 100644 index 0000000..b488457 --- /dev/null +++ b/src/components/wallet/DepositUSDTModal.tsx @@ -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('trc20'); + const [depositCode, setDepositCode] = useState(''); + const [copied, setCopied] = useState<'address' | 'memo' | null>(null); + const [deposits, setDeposits] = useState([]); + 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 ( +
+
+ {/* Header */} +
+
+
+ +
+
+

USDT Depo Bike

+

Bo wUSDT li Asset Hub

+
+
+
+ + +
+
+ + {showHistory ? ( + /* Deposit History */ +
+

Dîroka Depoyan

+ {deposits.length === 0 ? ( +

Hîn depo tune

+ ) : ( + deposits.map((deposit) => ( +
+
+
+
+ {deposit.network === 'trc20' ? '🔴' : '⚪'} + {deposit.amount} USDT +
+

+ {new Date(deposit.created_at).toLocaleDateString('ku')} +

+
+ + {getStatusText(deposit.status)} + +
+ {deposit.tx_hash && ( + 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" + > + + TX bibîne + + )} +
+ )) + )} + +
+ ) : ( +
+ {/* Network Selection */} +
+ +
+ {NETWORKS.map((net) => ( + + ))} +
+
+ + {/* Important Notice */} +
+
+ +
+

Girîng!

+
    +
  • + Kêmtirîn: {network.minAmount} USDT +
  • +
  • Memo/Not qada de koda xwe binivîse
  • +
  • Tenê USDT bişîne, tokenên din winda dibin
  • +
+
+
+
+ + {/* Deposit Address */} +
+ {/* Address */} +
+ +
+ + {network.address || 'Amade nîne'} + + +
+
+ + {/* Memo/Reference */} +
+ +
+ {isLoading ? ( +
+ +
+ ) : ( + + {depositCode || '---'} + + )} + +
+

+ ⚠️ Vê kodê di memo/not de binivîse, wekî din depoya te nayê nas kirin! +

+
+
+ + {/* Steps */} +
+

Çawa Depo Bikim?

+
    +
  1. + 1. + Navnîşan û memo kopî bike +
  2. +
  3. + 2. + Di cîzdana xwe de ({network.name}) USDT bişîne navnîşana jorîn +
  4. +
  5. + 3. + + MEMO qada de koda xwe binivîse! + +
  6. +
  7. + 4. + Piştî {network.confirmations} pejirandinê, wUSDT dê li Asset Hub be +
  8. +
+
+ + {/* Processing Time */} +

+ Dema pêvajoyê: ~5-30 hûrdem li gorî torê +

+
+ )} +
+
+ ); +} diff --git a/src/components/wallet/TokensCard.tsx b/src/components/wallet/TokensCard.tsx index 43ef195..4d26911 100644 --- a/src/components/wallet/TokensCard.tsx +++ b/src/components/wallet/TokensCard.tsx @@ -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('--'); const [peopleHezBalance, setPeopleHezBalance] = useState('--'); 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) { )} ) : ( - onSendToken && - token.balance !== '--' && - parseFloat(token.balance) > 0 && ( - - ) +
+ {/* Deposit button for USDT */} + {token.displaySymbol === 'USDT' && ( + + )} + {onSendToken && + token.balance !== '--' && + parseFloat(token.balance) > 0 && ( + + )} +
)} @@ -935,6 +952,13 @@ export function TokensCard({ onSendToken }: Props) { {/* Fund Fees Modal for XCM Teleport */} setShowFundFeesModal(false)} /> + + {/* Deposit USDT Modal */} + setShowDepositModal(false)} + userId={null} + /> ); } diff --git a/src/version.json b/src/version.json index be7af22..9a143b5 100644 --- a/src/version.json +++ b/src/version.json @@ -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 } diff --git a/supabase/functions/get-deposit-code/index.ts b/supabase/functions/get-deposit-code/index.ts new file mode 100644 index 0000000..69ab366 --- /dev/null +++ b/supabase/functions/get-deposit-code/index.ts @@ -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 { + 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' }, + }); + } +}); diff --git a/supabase/functions/get-deposits/index.ts b/supabase/functions/get-deposits/index.ts new file mode 100644 index 0000000..bb67afa --- /dev/null +++ b/supabase/functions/get-deposits/index.ts @@ -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 { + 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' }, + }); + } +}); diff --git a/supabase/migrations/20260208_deposit_system.sql b/supabase/migrations/20260208_deposit_system.sql new file mode 100644 index 0000000..09181a5 --- /dev/null +++ b/supabase/migrations/20260208_deposit_system.sql @@ -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');