diff --git a/src/components/p2p/CreateAd.tsx b/src/components/p2p/CreateAd.tsx index 6ce2d8b..f67ab96 100644 --- a/src/components/p2p/CreateAd.tsx +++ b/src/components/p2p/CreateAd.tsx @@ -1,17 +1,15 @@ -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { useAuth } from '@/contexts/AuthContext'; -import { useWallet } from '@/contexts/WalletContext'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; -import { toast } from 'sonner'; import { Loader2 } from 'lucide-react'; -import { supabase } from '@/lib/supabase'; import { getPaymentMethods, validatePaymentDetails, + createFiatOffer, type PaymentMethod, type FiatCurrency, type CryptoToken @@ -23,7 +21,6 @@ interface CreateAdProps { export function CreateAd({ onAdCreated }: CreateAdProps) { const { user } = useAuth(); - const { address } = useWallet(); const [paymentMethods, setPaymentMethods] = useState([]); const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(null); @@ -75,16 +72,11 @@ export function CreateAd({ onAdCreated }: CreateAdProps) { }; const handleCreateAd = async () => { - console.log('🔥 handleCreateAd called', { address, user: user?.id }); - - if (!address || !user) { - toast.error('Please connect your wallet and log in'); - console.log('❌ No address or user', { address, user }); + if (!user) { return; } if (!selectedPaymentMethod) { - toast.error('Please select a payment method'); return; } @@ -95,8 +87,6 @@ export function CreateAd({ onAdCreated }: CreateAdProps) { ); if (!validation.valid) { - const firstError = Object.values(validation.errors)[0]; - toast.error(firstError); return; } @@ -105,63 +95,32 @@ export function CreateAd({ onAdCreated }: CreateAdProps) { const fiatAmt = parseFloat(fiatAmount); if (!cryptoAmt || cryptoAmt <= 0) { - toast.error('Invalid crypto amount'); return; } if (!fiatAmt || fiatAmt <= 0) { - toast.error('Invalid fiat amount'); - return; - } - - if (selectedPaymentMethod.min_trade_amount && fiatAmt < selectedPaymentMethod.min_trade_amount) { - toast.error(`Minimum trade amount: ${selectedPaymentMethod.min_trade_amount} ${fiatCurrency}`); - return; - } - - if (selectedPaymentMethod.max_trade_amount && fiatAmt > selectedPaymentMethod.max_trade_amount) { - toast.error(`Maximum trade amount: ${selectedPaymentMethod.max_trade_amount} ${fiatCurrency}`); return; } setLoading(true); try { - // Insert offer into Supabase - // Note: payment_details_encrypted is stored as JSON string (encryption handled server-side in prod) - const { data, error } = await supabase - .from('p2p_fiat_offers') - .insert({ - seller_id: user.id, - seller_wallet: address, - ad_type: adType, - token, - amount_crypto: cryptoAmt, - remaining_amount: cryptoAmt, - fiat_currency: fiatCurrency, - fiat_amount: fiatAmt, - payment_method_id: selectedPaymentMethod.id, - payment_details_encrypted: JSON.stringify(paymentDetails), - time_limit_minutes: timeLimit, - min_order_amount: minOrderAmount ? parseFloat(minOrderAmount) : null, - max_order_amount: maxOrderAmount ? parseFloat(maxOrderAmount) : null, - status: 'open' - }) - .select() - .single(); + // Use createFiatOffer which handles escrow locking via internal ledger + await createFiatOffer({ + token, + amountCrypto: cryptoAmt, + fiatCurrency, + fiatAmount: fiatAmt, + paymentMethodId: selectedPaymentMethod.id, + paymentDetails, + timeLimitMinutes: timeLimit, + minOrderAmount: minOrderAmount ? parseFloat(minOrderAmount) : undefined, + maxOrderAmount: maxOrderAmount ? parseFloat(maxOrderAmount) : undefined, + }); - if (error) { - console.error('❌ Supabase error:', error); - toast.error(error.message || 'Failed to create offer'); - return; - } - - console.log('✅ Offer created successfully:', data); - toast.success('Ad created successfully!'); onAdCreated(); } catch (error) { if (import.meta.env.DEV) console.error('Create ad error:', error); - toast.error('Failed to create offer'); } finally { setLoading(false); } diff --git a/src/lib/p2p-fiat.ts b/src/lib/p2p-fiat.ts index d24a175..3d5af0a 100644 --- a/src/lib/p2p-fiat.ts +++ b/src/lib/p2p-fiat.ts @@ -2,13 +2,12 @@ * P2P Fiat Trading System - Mobile Version (OKX-Style Internal Ledger) * * @module p2p-fiat - * @description P2P fiat-to-crypto trading with internal ledger escrow + * @description Enterprise-level P2P fiat-to-crypto trading with internal ledger escrow * - * Security Model: - * - Authentication via Telegram initData (cryptographically signed) - * - Blockchain transactions ONLY occur at deposit/withdraw (backend) + * OKX Model Implementation: + * - Blockchain transactions ONLY occur at deposit/withdraw (via Edge Functions) * - P2P trades use internal database balance transfers - * - No client-side blockchain transactions + * - No blockchain transactions during actual P2P trading */ import { toast } from 'sonner'; @@ -18,14 +17,6 @@ import { supabase } from '@/lib/supabase'; // TYPES // ===================================================== -export type FiatCurrency = - | 'TRY' | 'IQD' | 'IRR' | 'EUR' | 'USD' | 'GBP' - | 'SEK' | 'CHF' | 'NOK' | 'DKK' | 'AUD' | 'CAD'; - -export type CryptoToken = 'HEZ' | 'PEZ'; -export type OfferStatus = 'open' | 'paused' | 'locked' | 'completed' | 'cancelled'; -export type TradeStatus = 'pending' | 'payment_sent' | 'completed' | 'cancelled' | 'disputed' | 'refunded'; - export interface PaymentMethod { id: string; currency: FiatCurrency; @@ -38,6 +29,7 @@ export interface PaymentMethod { min_trade_amount: number; max_trade_amount?: number; processing_time_minutes: number; + display_order: number; } export interface ValidationRule { @@ -47,6 +39,26 @@ export interface ValidationRule { required?: boolean; } +// Fiat currencies including Kurdish Diaspora countries +export type FiatCurrency = + | 'TRY' // Turkish Lira (Turkey - 15M+ Kurds) + | 'IQD' // Iraqi Dinar (Kurdistan Region - 6M+ Kurds) + | 'IRR' // Iranian Rial (Rojhilat - 8M+ Kurds) + | 'EUR' // Euro (Germany, France, Netherlands, Belgium, Austria) + | 'USD' // US Dollar + | 'GBP' // British Pound (UK - 50K+ Kurds) + | 'SEK' // Swedish Krona (Sweden - 100K+ Kurds) + | 'CHF' // Swiss Franc (Switzerland - 30K+ Kurds) + | 'NOK' // Norwegian Krone (Norway - 30K+ Kurds) + | 'DKK' // Danish Krone (Denmark - 25K+ Kurds) + | 'AUD' // Australian Dollar (Australia - 20K+ Kurds) + | 'CAD'; // Canadian Dollar (Canada - 30K+ Kurds) + +export type CryptoToken = 'HEZ' | 'PEZ'; + +export type OfferStatus = 'open' | 'paused' | 'locked' | 'completed' | 'cancelled'; +export type TradeStatus = 'pending' | 'payment_sent' | 'completed' | 'cancelled' | 'disputed' | 'refunded'; + export interface P2PFiatOffer { id: string; seller_id: string; @@ -66,6 +78,7 @@ export interface P2PFiatOffer { min_buyer_reputation: number; status: OfferStatus; remaining_amount: number; + escrow_tx_hash?: string; created_at: string; expires_at: string; } @@ -100,8 +113,32 @@ export interface P2PReputation { trust_level: 'new' | 'basic' | 'intermediate' | 'advanced' | 'verified'; verified_merchant: boolean; avg_payment_time_minutes?: number; + avg_confirmation_time_minutes?: number; + fast_trader?: boolean; } +export interface CreateOfferParams { + token: CryptoToken; + amountCrypto: number; + fiatCurrency: FiatCurrency; + fiatAmount: number; + paymentMethodId: string; + paymentDetails: Record; + timeLimitMinutes?: number; + minOrderAmount?: number; + maxOrderAmount?: number; +} + +export interface AcceptOfferParams { + offerId: string; + buyerWallet: string; + amount?: number; +} + +// ===================================================== +// INTERNAL BALANCE TYPES (OKX-Style) +// ===================================================== + export interface InternalBalance { token: CryptoToken; available_balance: number; @@ -125,15 +162,52 @@ export interface DepositWithdrawRequest { created_at: string; } +export interface BalanceTransaction { + id: string; + user_id: string; + token: string; + transaction_type: 'deposit' | 'withdraw' | 'escrow_lock' | 'escrow_release' | 'escrow_refund' | 'trade_receive' | 'admin_adjustment'; + amount: number; + balance_before: number; + balance_after: number; + reference_type?: string; + reference_id?: string; + description?: string; + created_at: string; +} + // ===================================================== -// VALIDATION +// CONSTANTS // ===================================================== -export interface ValidationRule { - pattern?: string; - minLength?: number; - maxLength?: number; - required?: boolean; +const PLATFORM_ESCROW_ADDRESS = '5DFwqK698vL4gXHEcanaewnAqhxJ2rjhAogpSTHw3iwGDwd3'; + +const DEFAULT_PAYMENT_DEADLINE_MINUTES = 30; +const DEFAULT_CONFIRMATION_DEADLINE_MINUTES = 60; + +// ===================================================== +// PAYMENT METHODS +// ===================================================== + +/** + * Fetch available payment methods for a currency + */ +export async function getPaymentMethods(currency: FiatCurrency): Promise { + try { + const { data, error } = await supabase + .from('payment_methods') + .select('*') + .eq('currency', currency) + .eq('is_active', true) + .order('display_order'); + + if (error) throw error; + return data || []; + } catch (error) { + console.error('Get payment methods error:', error); + toast.error('Failed to load payment methods'); + return []; + } } /** @@ -176,28 +250,574 @@ export function validatePaymentDetails( } // ===================================================== -// PAYMENT METHODS +// ENCRYPTION (AES-256-GCM - OKX-Level Security) // ===================================================== -export async function getPaymentMethods(currency: FiatCurrency): Promise { - try { - const { data, error } = await supabase - .from('payment_methods') - .select('*') - .eq('currency', currency) - .eq('is_active', true) - .order('display_order'); +const IV_LENGTH = 12; // 96 bits for GCM - if (error) throw error; - return data || []; +/** + * Derive encryption key from a password/secret + */ +async function getEncryptionKey(): Promise { + const encoder = new TextEncoder(); + const keyMaterial = await crypto.subtle.importKey( + 'raw', + encoder.encode('p2p-payment-encryption-v1-pezkuwi'), + 'PBKDF2', + false, + ['deriveBits', 'deriveKey'] + ); + + return crypto.subtle.deriveKey( + { + name: 'PBKDF2', + salt: encoder.encode('pezkuwi-p2p-salt'), + iterations: 100000, + hash: 'SHA-256' + }, + keyMaterial, + { name: 'AES-GCM', length: 256 }, + false, + ['encrypt', 'decrypt'] + ); +} + +/** + * Encrypt payment details using AES-256-GCM + */ +async function encryptPaymentDetails(details: Record): Promise { + try { + const key = await getEncryptionKey(); + const encoder = new TextEncoder(); + const data = encoder.encode(JSON.stringify(details)); + + // Generate random IV + const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH)); + + // Encrypt + const encrypted = await crypto.subtle.encrypt( + { name: 'AES-GCM', iv }, + key, + data + ); + + // Combine IV + ciphertext and encode as base64 + const combined = new Uint8Array(iv.length + encrypted.byteLength); + combined.set(iv); + combined.set(new Uint8Array(encrypted), iv.length); + + return btoa(String.fromCharCode(...combined)); } catch (error) { - console.error('Get payment methods error:', error); - return []; + console.error('Encryption failed:', error); + // Fallback to base64 for backwards compatibility (temporary) + return btoa(JSON.stringify(details)); + } +} + +/** + * Decrypt payment details using AES-256-GCM + */ +export async function decryptPaymentDetails(encrypted: string): Promise> { + try { + const key = await getEncryptionKey(); + + // Decode base64 + const combined = Uint8Array.from(atob(encrypted), c => c.charCodeAt(0)); + + // Extract IV and ciphertext + const iv = combined.slice(0, IV_LENGTH); + const ciphertext = combined.slice(IV_LENGTH); + + // Decrypt + const decrypted = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv }, + key, + ciphertext + ); + + const decoder = new TextDecoder(); + return JSON.parse(decoder.decode(decrypted)); + } catch { + // Fallback: try to decode as plain base64 (for old data) + try { + return JSON.parse(atob(encrypted)); + } catch { + return {}; + } } } // ===================================================== -// OFFERS +// AUDIT LOGGING +// ===================================================== + +async function logAction( + entityType: string, + entityId: string, + action: string, + details: Record +): Promise { + try { + const { data: user } = await supabase.auth.getUser(); + + await supabase.from('p2p_audit_log').insert({ + user_id: user.user?.id, + action, + entity_type: entityType, + entity_id: entityId, + details + }); + } catch (error) { + console.error('Audit log error:', error); + } +} + +// ===================================================== +// CREATE OFFER +// ===================================================== + +/** + * Create a new P2P fiat offer (OKX-Style Internal Ledger) + * + * Steps: + * 1. Lock crypto from internal balance (NO blockchain tx) + * 2. Create offer record in Supabase + * 3. Update escrow tracking + */ +export async function createFiatOffer(params: CreateOfferParams): Promise { + const { + token, + amountCrypto, + fiatCurrency, + fiatAmount, + paymentMethodId, + paymentDetails, + timeLimitMinutes = DEFAULT_PAYMENT_DEADLINE_MINUTES, + minOrderAmount, + maxOrderAmount + } = params; + + try { + // Get current user + const { data: userData } = await supabase.auth.getUser(); + const userId = userData.user?.id; + if (!userId) throw new Error('Not authenticated'); + + toast.info('Locking crypto from your balance...'); + + // 1. Lock crypto from internal balance (NO blockchain tx!) + const { data: lockResult, error: lockError } = await supabase.rpc('lock_escrow_internal', { + p_user_id: userId, + p_token: token, + p_amount: amountCrypto + }); + + if (lockError) throw lockError; + + // Parse result + const lockResponse = typeof lockResult === 'string' ? JSON.parse(lockResult) : lockResult; + + if (!lockResponse.success) { + throw new Error(lockResponse.error || 'Failed to lock balance'); + } + + toast.success('Balance locked successfully'); + + // 2. Encrypt payment details (AES-256-GCM) + const encryptedDetails = await encryptPaymentDetails(paymentDetails); + + // 3. Create offer in Supabase + const { data: offer, error: offerError } = await supabase + .from('p2p_fiat_offers') + .insert({ + seller_id: userId, + seller_wallet: '', // No longer needed with internal ledger + token, + amount_crypto: amountCrypto, + fiat_currency: fiatCurrency, + fiat_amount: fiatAmount, + price_per_unit: fiatAmount / amountCrypto, + payment_method_id: paymentMethodId, + payment_details_encrypted: encryptedDetails, + min_order_amount: minOrderAmount, + max_order_amount: maxOrderAmount, + time_limit_minutes: timeLimitMinutes, + status: 'open', + remaining_amount: amountCrypto, + escrow_locked_at: new Date().toISOString() + }) + .select() + .single(); + + if (offerError) throw offerError; + + // 4. Audit log + await logAction('offer', offer.id, 'create_offer', { + token, + amount_crypto: amountCrypto, + fiat_currency: fiatCurrency, + fiat_amount: fiatAmount, + escrow_type: 'internal_ledger' + }); + + toast.success(`Offer created! Selling ${amountCrypto} ${token} for ${fiatAmount} ${fiatCurrency}`); + + return offer.id; + } catch (error: unknown) { + console.error('Create offer error:', error); + const message = error instanceof Error ? error.message : 'Failed to create offer'; + toast.error(message); + throw error; + } +} + +// ===================================================== +// ACCEPT OFFER +// ===================================================== + +/** + * Accept a P2P fiat offer (buyer) + */ +export async function acceptFiatOffer(params: AcceptOfferParams): Promise { + const { offerId, amount } = params; + + try { + // 1. Get current user + const { data: user } = await supabase.auth.getUser(); + if (!user.user) throw new Error('Not authenticated'); + + // 2. Get offer to determine amount if not specified + const { data: offer, error: offerError } = await supabase + .from('p2p_fiat_offers') + .select('remaining_amount, min_buyer_completed_trades, min_buyer_reputation') + .eq('id', offerId) + .single(); + + if (offerError) throw offerError; + if (!offer) throw new Error('Offer not found'); + + const tradeAmount = amount || offer.remaining_amount; + + // 3. Check buyer reputation requirements + if (offer.min_buyer_completed_trades > 0 || offer.min_buyer_reputation > 0) { + const { data: reputation } = await supabase + .from('p2p_reputation') + .select('completed_trades, reputation_score') + .eq('user_id', user.user.id) + .single(); + + if (!reputation) { + throw new Error('Seller requires experienced buyers'); + } + if (reputation.completed_trades < offer.min_buyer_completed_trades) { + throw new Error(`Minimum ${offer.min_buyer_completed_trades} completed trades required`); + } + if (reputation.reputation_score < offer.min_buyer_reputation) { + throw new Error(`Minimum reputation score ${offer.min_buyer_reputation} required`); + } + } + + // 4. Call atomic database function (prevents race condition) + const { data: result, error: rpcError } = await supabase.rpc('accept_p2p_offer', { + p_offer_id: offerId, + p_buyer_id: user.user.id, + p_buyer_wallet: params.buyerWallet, + p_amount: tradeAmount + }); + + if (rpcError) throw rpcError; + + // Parse result + const response = typeof result === 'string' ? JSON.parse(result) : result; + + if (!response.success) { + throw new Error(response.error || 'Failed to accept offer'); + } + + // 5. Audit log + await logAction('trade', response.trade_id, 'accept_offer', { + offer_id: offerId, + crypto_amount: response.crypto_amount, + fiat_amount: response.fiat_amount + }); + + toast.success('Trade started! Send payment within time limit.'); + + return response.trade_id; + } catch (error: unknown) { + console.error('Accept offer error:', error); + const message = error instanceof Error ? error.message : 'Failed to accept offer'; + toast.error(message); + throw error; + } +} + +// ===================================================== +// MARK PAYMENT SENT (Buyer) +// ===================================================== + +/** + * Buyer marks payment as sent + */ +export async function markPaymentSent( + tradeId: string, + paymentProofUrl?: string +): Promise { + try { + // Update trade + const confirmationDeadline = new Date(Date.now() + DEFAULT_CONFIRMATION_DEADLINE_MINUTES * 60 * 1000); + + const { error } = await supabase + .from('p2p_fiat_trades') + .update({ + buyer_marked_paid_at: new Date().toISOString(), + buyer_payment_proof_url: paymentProofUrl, + status: 'payment_sent', + confirmation_deadline: confirmationDeadline.toISOString() + }) + .eq('id', tradeId); + + if (error) throw error; + + // Audit log + await logAction('trade', tradeId, 'mark_payment_sent', { + payment_proof_url: paymentProofUrl + }); + + toast.success('Payment marked as sent. Waiting for seller confirmation...'); + } catch (error: unknown) { + console.error('Mark payment sent error:', error); + const message = error instanceof Error ? error.message : 'Failed to mark payment as sent'; + toast.error(message); + throw error; + } +} + +// ===================================================== +// CONFIRM PAYMENT RECEIVED (Seller) +// ===================================================== + +/** + * Seller confirms payment received and releases crypto (OKX-Style Internal Ledger) + */ +export async function confirmPaymentReceived(tradeId: string): Promise { + try { + // 1. Get current user (seller) + const { data: userData } = await supabase.auth.getUser(); + const sellerId = userData.user?.id; + if (!sellerId) throw new Error('Not authenticated'); + + // 2. Get trade details + const { data: trade, error: tradeError } = await supabase + .from('p2p_fiat_trades') + .select('*, p2p_fiat_offers(token)') + .eq('id', tradeId) + .single(); + + if (tradeError) throw tradeError; + if (!trade) throw new Error('Trade not found'); + + // Verify caller is the seller + if (trade.seller_id !== sellerId) { + throw new Error('Only seller can confirm payment'); + } + + if (trade.status !== 'payment_sent') { + throw new Error('Payment has not been marked as sent'); + } + + toast.info('Releasing crypto to buyer...'); + + // 3. Release escrow internally (NO blockchain tx!) + const { data: releaseResult, error: releaseError } = await supabase.rpc('release_escrow_internal', { + p_from_user_id: trade.seller_id, + p_to_user_id: trade.buyer_id, + p_token: trade.p2p_fiat_offers.token, + p_amount: trade.crypto_amount, + p_reference_type: 'trade', + p_reference_id: tradeId + }); + + if (releaseError) throw releaseError; + + // Parse result + const releaseResponse = typeof releaseResult === 'string' ? JSON.parse(releaseResult) : releaseResult; + + if (!releaseResponse.success) { + throw new Error(releaseResponse.error || 'Failed to release escrow'); + } + + // 4. Update trade status + const { error: updateError } = await supabase + .from('p2p_fiat_trades') + .update({ + seller_confirmed_at: new Date().toISOString(), + escrow_released_at: new Date().toISOString(), + status: 'completed', + completed_at: new Date().toISOString() + }) + .eq('id', tradeId); + + if (updateError) throw updateError; + + // 5. Update reputations + await updateReputations(trade.seller_id, trade.buyer_id, tradeId); + + // 6. Audit log + await logAction('trade', tradeId, 'confirm_payment', { + released_amount: trade.crypto_amount, + token: trade.p2p_fiat_offers.token, + escrow_type: 'internal_ledger' + }); + + toast.success('Payment confirmed! Crypto released to buyer\'s balance.'); + } catch (error: unknown) { + console.error('Confirm payment error:', error); + const message = error instanceof Error ? error.message : 'Failed to confirm payment'; + toast.error(message); + throw error; + } +} + +// ===================================================== +// CANCEL TRADE +// ===================================================== + +/** + * Cancel a trade (buyer only, before payment sent) + */ +export async function cancelTrade(tradeId: string, reason?: string): Promise { + try { + const { data: user } = await supabase.auth.getUser(); + if (!user.user) throw new Error('Not authenticated'); + + // 1. Get trade details + const { data: trade, error: tradeError } = await supabase + .from('p2p_fiat_trades') + .select('*') + .eq('id', tradeId) + .single(); + + if (tradeError) throw tradeError; + if (!trade) throw new Error('Trade not found'); + + // Only allow cancellation if pending + if (trade.status !== 'pending') { + throw new Error('Trade cannot be cancelled at this stage'); + } + + // 2. Update trade status + const { error: updateError } = await supabase + .from('p2p_fiat_trades') + .update({ + status: 'cancelled', + cancelled_by: user.user.id, + cancel_reason: reason, + }) + .eq('id', tradeId); + + if (updateError) throw updateError; + + // 3. Restore offer remaining amount + const { data: offer } = await supabase + .from('p2p_fiat_offers') + .select('remaining_amount') + .eq('id', trade.offer_id) + .single(); + + if (offer) { + await supabase + .from('p2p_fiat_offers') + .update({ + remaining_amount: offer.remaining_amount + trade.crypto_amount, + status: 'open', + }) + .eq('id', trade.offer_id); + } + + // 4. Audit log + await logAction('trade', tradeId, 'cancel_trade', { + cancelled_by: user.user.id, + reason, + }); + + toast.success('Trade cancelled successfully'); + } catch (error: unknown) { + console.error('Cancel trade error:', error); + const message = error instanceof Error ? error.message : 'Failed to cancel trade'; + toast.error(message); + throw error; + } +} + +// ===================================================== +// HELPER FUNCTIONS +// ===================================================== + +async function updateReputations(sellerId: string, buyerId: string, tradeId: string): Promise { + try { + await supabase.rpc('update_p2p_reputation', { + p_seller_id: sellerId, + p_buyer_id: buyerId, + p_trade_id: tradeId + }); + } catch (error) { + console.error('Update reputation error:', error); + } +} + +/** + * Update user reputation after trade completion + */ +export async function updateUserReputation( + userId: string, + tradeCompleted: boolean +): Promise { + try { + // Get current reputation + const { data: currentRep } = await supabase + .from('p2p_reputation') + .select('*') + .eq('user_id', userId) + .single(); + + if (currentRep) { + // Update existing reputation + await supabase + .from('p2p_reputation') + .update({ + total_trades: currentRep.total_trades + 1, + completed_trades: tradeCompleted + ? currentRep.completed_trades + 1 + : currentRep.completed_trades, + cancelled_trades: tradeCompleted + ? currentRep.cancelled_trades + : currentRep.cancelled_trades + 1, + reputation_score: tradeCompleted + ? Math.min(100, currentRep.reputation_score + 1) + : Math.max(0, currentRep.reputation_score - 2), + }) + .eq('user_id', userId); + } else { + // Create new reputation record + await supabase.from('p2p_reputation').insert({ + user_id: userId, + total_trades: 1, + completed_trades: tradeCompleted ? 1 : 0, + cancelled_trades: tradeCompleted ? 0 : 1, + reputation_score: tradeCompleted ? 50 : 48, + trust_level: 'new', + verified_merchant: false, + fast_trader: false, + }); + } + } catch (error) { + console.error('Update reputation error:', error); + } +} + +// ===================================================== +// QUERY FUNCTIONS // ===================================================== export async function getActiveOffers( @@ -226,75 +846,6 @@ export async function getActiveOffers( } } -export async function acceptFiatOffer(params: { - offerId: string; - buyerWallet: string; - amount?: number; -}): Promise { - const { offerId, buyerWallet, amount } = params; - - try { - const { data: user } = await supabase.auth.getUser(); - if (!user.user) throw new Error('Not authenticated'); - - const { data: offer } = await supabase - .from('p2p_fiat_offers') - .select('remaining_amount, min_buyer_completed_trades, min_buyer_reputation') - .eq('id', offerId) - .single(); - - if (!offer) throw new Error('Offer not found'); - - const tradeAmount = amount || offer.remaining_amount; - - // Check reputation requirements - if (offer.min_buyer_completed_trades > 0 || offer.min_buyer_reputation > 0) { - const { data: reputation } = await supabase - .from('p2p_reputation') - .select('completed_trades, reputation_score') - .eq('user_id', user.user.id) - .single(); - - if (!reputation) { - throw new Error('Seller requires experienced buyers'); - } - if (reputation.completed_trades < offer.min_buyer_completed_trades) { - throw new Error(`Minimum ${offer.min_buyer_completed_trades} completed trades required`); - } - if (reputation.reputation_score < offer.min_buyer_reputation) { - throw new Error(`Minimum reputation score ${offer.min_buyer_reputation} required`); - } - } - - // Atomic accept - const { data: result, error: rpcError } = await supabase.rpc('accept_p2p_offer', { - p_offer_id: offerId, - p_buyer_id: user.user.id, - p_buyer_wallet: buyerWallet, - p_amount: tradeAmount - }); - - if (rpcError) throw rpcError; - - const response = typeof result === 'string' ? JSON.parse(result) : result; - - if (!response.success) { - throw new Error(response.error || 'Failed to accept offer'); - } - - toast.success('Trade started! Send payment within time limit.'); - return response.trade_id; - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Failed to accept offer'; - toast.error(message); - throw error; - } -} - -// ===================================================== -// TRADES -// ===================================================== - export async function getUserTrades(userId: string): Promise { try { const { data, error } = await supabase @@ -311,6 +862,25 @@ export async function getUserTrades(userId: string): Promise { } } +export async function getUserReputation(userId: string): Promise { + try { + const { data, error } = await supabase + .from('p2p_reputation') + .select('*') + .eq('user_id', userId) + .single(); + + if (error && error.code !== 'PGRST116') throw error; + return data; + } catch (error) { + console.error('Get user reputation error:', error); + return null; + } +} + +/** + * Get a specific trade by ID + */ export async function getTradeById(tradeId: string): Promise { try { const { data, error } = await supabase @@ -327,155 +897,13 @@ export async function getTradeById(tradeId: string): Promise { - try { - const confirmationDeadline = new Date(Date.now() + 60 * 60 * 1000); // 60 min - - const { error } = await supabase - .from('p2p_fiat_trades') - .update({ - buyer_marked_paid_at: new Date().toISOString(), - buyer_payment_proof_url: paymentProofUrl, - status: 'payment_sent', - confirmation_deadline: confirmationDeadline.toISOString() - }) - .eq('id', tradeId); - - if (error) throw error; - toast.success('Payment marked as sent. Waiting for seller confirmation...'); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Failed to mark payment'; - toast.error(message); - throw error; - } -} - -export async function confirmPaymentReceived(tradeId: string): Promise { - try { - const { data: userData } = await supabase.auth.getUser(); - const sellerId = userData.user?.id; - if (!sellerId) throw new Error('Not authenticated'); - - const { data: trade } = await supabase - .from('p2p_fiat_trades') - .select('*, p2p_fiat_offers(token)') - .eq('id', tradeId) - .single(); - - if (!trade) throw new Error('Trade not found'); - if (trade.seller_id !== sellerId) throw new Error('Only seller can confirm'); - if (trade.status !== 'payment_sent') throw new Error('Payment not marked as sent'); - - toast.info('Releasing crypto to buyer...'); - - // Release escrow internally - const { data: result, error: releaseError } = await supabase.rpc('release_escrow_internal', { - p_from_user_id: trade.seller_id, - p_to_user_id: trade.buyer_id, - p_token: trade.p2p_fiat_offers.token, - p_amount: trade.crypto_amount, - p_reference_type: 'trade', - p_reference_id: tradeId - }); - - if (releaseError) throw releaseError; - - const response = typeof result === 'string' ? JSON.parse(result) : result; - if (!response.success) throw new Error(response.error || 'Failed to release'); - - // Update trade status - await supabase - .from('p2p_fiat_trades') - .update({ - seller_confirmed_at: new Date().toISOString(), - status: 'completed', - completed_at: new Date().toISOString() - }) - .eq('id', tradeId); - - toast.success('Payment confirmed! Crypto released to buyer.'); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Failed to confirm'; - toast.error(message); - throw error; - } -} - -export async function cancelTrade(tradeId: string, reason?: string): Promise { - try { - const { data: user } = await supabase.auth.getUser(); - if (!user.user) throw new Error('Not authenticated'); - - const { data: trade } = await supabase - .from('p2p_fiat_trades') - .select('*') - .eq('id', tradeId) - .single(); - - if (!trade) throw new Error('Trade not found'); - if (trade.status !== 'pending') throw new Error('Cannot cancel at this stage'); - - await supabase - .from('p2p_fiat_trades') - .update({ - status: 'cancelled', - cancelled_by: user.user.id, - cancel_reason: reason - }) - .eq('id', tradeId); - - // Restore offer amount - const { data: offer } = await supabase - .from('p2p_fiat_offers') - .select('remaining_amount') - .eq('id', trade.offer_id) - .single(); - - if (offer) { - await supabase - .from('p2p_fiat_offers') - .update({ - remaining_amount: offer.remaining_amount + trade.crypto_amount, - status: 'open' - }) - .eq('id', trade.offer_id); - } - - toast.success('Trade cancelled'); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Failed to cancel'; - toast.error(message); - throw error; - } -} - // ===================================================== -// REPUTATION -// ===================================================== - -export async function getUserReputation(userId: string): Promise { - try { - const { data, error } = await supabase - .from('p2p_reputation') - .select('*') - .eq('user_id', userId) - .single(); - - if (error && error.code !== 'PGRST116') throw error; - return data; - } catch (error) { - console.error('Get reputation error:', error); - return null; - } -} - -// ===================================================== -// INTERNAL BALANCE (OKX-Style) +// INTERNAL BALANCE FUNCTIONS (OKX-Style) // ===================================================== +/** + * Get user's internal balances for P2P trading + */ export async function getInternalBalances(): Promise { try { const { data: userData } = await supabase.auth.getUser(); @@ -487,18 +915,27 @@ export async function getInternalBalances(): Promise { }); if (error) throw error; - return typeof data === 'string' ? JSON.parse(data) : (data || []); + + // Parse the JSON result + const balances = typeof data === 'string' ? JSON.parse(data) : data; + return balances || []; } catch (error) { - console.error('Get balances error:', error); + console.error('Get internal balances error:', error); return []; } } +/** + * Get user's internal balance for a specific token + */ export async function getInternalBalance(token: CryptoToken): Promise { const balances = await getInternalBalances(); return balances.find(b => b.token === token) || null; } +/** + * Request a withdrawal from internal balance to external wallet + */ export async function requestWithdraw( token: CryptoToken, amount: number, @@ -509,11 +946,17 @@ export async function requestWithdraw( const userId = userData.user?.id; if (!userId) throw new Error('Not authenticated'); + // Validate amount if (amount <= 0) throw new Error('Amount must be greater than 0'); - if (!walletAddress || walletAddress.length < 40) throw new Error('Invalid wallet address'); + + // Validate wallet address (basic check) + if (!walletAddress || walletAddress.length < 40) { + throw new Error('Invalid wallet address'); + } toast.info('Processing withdrawal request...'); + // Call the database function const { data, error } = await supabase.rpc('request_withdraw', { p_user_id: userId, p_token: token, @@ -523,18 +966,27 @@ export async function requestWithdraw( if (error) throw error; + // Parse result const result = typeof data === 'string' ? JSON.parse(data) : data; - if (!result.success) throw new Error(result.error || 'Request failed'); - toast.success(`Withdrawal request submitted! ${amount} ${token} will be sent.`); + if (!result.success) { + throw new Error(result.error || 'Withdrawal request failed'); + } + + toast.success(`Withdrawal request submitted! ${amount} ${token} will be sent to your wallet.`); + return result.request_id; } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Request failed'; + console.error('Request withdraw error:', error); + const message = error instanceof Error ? error.message : 'Withdrawal request failed'; toast.error(message); throw error; } } +/** + * Get user's deposit/withdraw request history + */ export async function getDepositWithdrawHistory(): Promise { try { const { data, error } = await supabase @@ -546,13 +998,34 @@ export async function getDepositWithdrawHistory(): Promise { + try { + const { data, error } = await supabase + .from('p2p_balance_transactions') + .select('*') + .order('created_at', { ascending: false }) + .limit(limit); + + if (error) throw error; + return data || []; + } catch (error) { + console.error('Get balance transaction history error:', error); + return []; + } +} + +/** + * Get platform escrow wallet address (for deposits) + */ export async function getPlatformWalletAddress(): Promise { - const DEFAULT_ADDRESS = '5DFwqK698vL4gXHEcanaewnAqhxJ2rjhAogpSTHw3iwGDwd3'; try { const { data, error } = await supabase .from('p2p_config') @@ -560,9 +1033,10 @@ export async function getPlatformWalletAddress(): Promise { .eq('key', 'platform_escrow_wallet') .single(); - if (error) return DEFAULT_ADDRESS; - return data?.value || DEFAULT_ADDRESS; - } catch { - return DEFAULT_ADDRESS; + if (error) throw error; + return data?.value || PLATFORM_ESCROW_ADDRESS; + } catch (error) { + console.error('Get platform wallet address error:', error); + return PLATFORM_ESCROW_ADDRESS; } }