mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-23 11:37:55 +00:00
feat(p2p): implement OKX-style internal ledger escrow system
Phase 5 implementation - Internal Ledger Escrow (OKX Model): - No blockchain transactions during P2P trades - Blockchain tx only at deposit/withdraw - Fast and fee-free P2P trading Database: - Add user_internal_balances table - Add p2p_deposit_withdraw_requests table - Add p2p_balance_transactions table - Add lock_escrow_internal(), release_escrow_internal() functions - Add process_deposit(), request_withdraw() functions UI Components: - Add InternalBalanceCard showing available/locked balances - Add DepositModal for crypto deposits to P2P balance - Add WithdrawModal for withdrawals from P2P balance - Integrate balance card into P2PDashboard Backend: - Add process-withdrawal Edge Function - Add verify-deposit Edge Function Updated p2p-fiat.ts: - createFiatOffer() uses internal balance lock - confirmPaymentReceived() uses internal balance transfer - Add internal balance management functions
This commit is contained in:
@@ -9,7 +9,7 @@ import { TradeModal } from './TradeModal';
|
||||
import { MerchantTierBadge } from './MerchantTierBadge';
|
||||
import { getUserReputation, type P2PFiatOffer, type P2PReputation } from '@shared/lib/p2p-fiat';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import type { P2PFilters } from './OrderFilters';
|
||||
import type { P2PFilters } from './types';
|
||||
|
||||
interface AdListProps {
|
||||
type: 'buy' | 'sell' | 'my-ads';
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import {
|
||||
Loader2,
|
||||
Copy,
|
||||
CheckCircle2,
|
||||
AlertTriangle,
|
||||
ExternalLink,
|
||||
QrCode,
|
||||
Wallet
|
||||
} from 'lucide-react';
|
||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
getPlatformWalletAddress,
|
||||
verifyDeposit,
|
||||
type CryptoToken
|
||||
} from '@shared/lib/p2p-fiat';
|
||||
|
||||
interface DepositModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
type DepositStep = 'select' | 'send' | 'verify' | 'success';
|
||||
|
||||
export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps) {
|
||||
const { api, selectedAccount } = usePolkadot();
|
||||
const { balances, signTransaction } = useWallet();
|
||||
|
||||
const [step, setStep] = useState<DepositStep>('select');
|
||||
const [token, setToken] = useState<CryptoToken>('HEZ');
|
||||
const [amount, setAmount] = useState('');
|
||||
const [platformWallet, setPlatformWallet] = useState<string>('');
|
||||
const [txHash, setTxHash] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [verifying, setVerifying] = useState(false);
|
||||
|
||||
// Fetch platform wallet address on mount
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
fetchPlatformWallet();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const fetchPlatformWallet = async () => {
|
||||
const address = await getPlatformWalletAddress();
|
||||
setPlatformWallet(address);
|
||||
};
|
||||
|
||||
const resetModal = () => {
|
||||
setStep('select');
|
||||
setToken('HEZ');
|
||||
setAmount('');
|
||||
setTxHash('');
|
||||
setLoading(false);
|
||||
setCopied(false);
|
||||
setVerifying(false);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
resetModal();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleCopyAddress = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(platformWallet);
|
||||
setCopied(true);
|
||||
toast.success('Address copied to clipboard');
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
toast.error('Failed to copy address');
|
||||
}
|
||||
};
|
||||
|
||||
const getAvailableBalance = () => {
|
||||
if (token === 'HEZ') return balances.HEZ;
|
||||
if (token === 'PEZ') return balances.PEZ;
|
||||
return '0';
|
||||
};
|
||||
|
||||
const handleSendDeposit = async () => {
|
||||
if (!api || !selectedAccount) {
|
||||
toast.error('Please connect your wallet');
|
||||
return;
|
||||
}
|
||||
|
||||
const depositAmount = parseFloat(amount);
|
||||
if (isNaN(depositAmount) || depositAmount <= 0) {
|
||||
toast.error('Please enter a valid amount');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// Build the transfer transaction
|
||||
const DECIMALS = 12;
|
||||
const amountBN = BigInt(Math.floor(depositAmount * 10 ** DECIMALS));
|
||||
|
||||
let tx;
|
||||
if (token === 'HEZ') {
|
||||
// Native transfer
|
||||
tx = api.tx.balances.transferKeepAlive(platformWallet, amountBN);
|
||||
} else {
|
||||
// Asset transfer (PEZ = asset ID 1)
|
||||
const assetId = token === 'PEZ' ? 1 : 0;
|
||||
tx = api.tx.assets.transfer(assetId, platformWallet, amountBN);
|
||||
}
|
||||
|
||||
toast.info('Please sign the transaction in your wallet...');
|
||||
|
||||
// Sign and send
|
||||
const hash = await signTransaction(tx);
|
||||
|
||||
if (hash) {
|
||||
setTxHash(hash);
|
||||
setStep('verify');
|
||||
toast.success('Transaction sent! Please verify your deposit.');
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error('Deposit transaction error:', error);
|
||||
const message = error instanceof Error ? error.message : 'Transaction failed';
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifyDeposit = async () => {
|
||||
if (!txHash) {
|
||||
toast.error('Please enter the transaction hash');
|
||||
return;
|
||||
}
|
||||
|
||||
const depositAmount = parseFloat(amount);
|
||||
if (isNaN(depositAmount) || depositAmount <= 0) {
|
||||
toast.error('Invalid amount');
|
||||
return;
|
||||
}
|
||||
|
||||
setVerifying(true);
|
||||
|
||||
try {
|
||||
const success = await verifyDeposit(txHash, token, depositAmount);
|
||||
|
||||
if (success) {
|
||||
setStep('success');
|
||||
onSuccess?.();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Verify deposit error:', error);
|
||||
} finally {
|
||||
setVerifying(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderStepContent = () => {
|
||||
switch (step) {
|
||||
case 'select':
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label>Select Token</Label>
|
||||
<Select value={token} onValueChange={(v) => setToken(v as CryptoToken)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="HEZ">HEZ (Native)</SelectItem>
|
||||
<SelectItem value="PEZ">PEZ</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Amount to Deposit</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="0.00"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
min="0"
|
||||
step="0.0001"
|
||||
/>
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2 text-sm text-muted-foreground">
|
||||
{token}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Wallet Balance: {parseFloat(getAvailableBalance()).toFixed(4)} {token}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Alert>
|
||||
<Wallet className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
You will send {token} from your connected wallet to the P2P platform escrow.
|
||||
After confirmation, the amount will be credited to your P2P internal balance.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setStep('send')}
|
||||
disabled={!amount || parseFloat(amount) <= 0}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'send':
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="p-4 rounded-lg bg-muted/50 border space-y-4">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-3 rounded-xl bg-primary/10 flex items-center justify-center">
|
||||
<QrCode className="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<p className="text-sm font-medium">Send {amount} {token} to:</p>
|
||||
</div>
|
||||
|
||||
{platformWallet ? (
|
||||
<div className="space-y-2">
|
||||
<div className="p-3 rounded-lg bg-background border font-mono text-xs break-all">
|
||||
{platformWallet}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={handleCopyAddress}
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<CheckCircle2 className="h-4 w-4 mr-2 text-green-500" />
|
||||
Copied!
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-4 w-4 mr-2" />
|
||||
Copy Address
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Skeleton className="h-16 w-full" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Only send {token} on the PezkuwiChain network. Sending other tokens or using
|
||||
other networks will result in permanent loss of funds.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={() => setStep('select')}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1"
|
||||
onClick={handleSendDeposit}
|
||||
disabled={loading || !platformWallet}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Sending...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Send {amount} {token}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'verify':
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Alert>
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
<AlertDescription>
|
||||
Transaction sent! Please verify your deposit to credit your P2P balance.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Transaction Hash</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={txHash}
|
||||
onChange={(e) => setTxHash(e.target.value)}
|
||||
placeholder="0x..."
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => window.open(`https://explorer.pezkuwichain.io/tx/${txHash}`, '_blank')}
|
||||
disabled={!txHash}
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg bg-muted/50 border">
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-muted-foreground">Token</p>
|
||||
<p className="font-semibold">{token}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Amount</p>
|
||||
<p className="font-semibold">{amount}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleVerifyDeposit}
|
||||
disabled={verifying || !txHash}
|
||||
>
|
||||
{verifying ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Verifying...
|
||||
</>
|
||||
) : (
|
||||
'Verify Deposit'
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'success':
|
||||
return (
|
||||
<div className="space-y-6 text-center">
|
||||
<div className="w-20 h-20 mx-auto rounded-full bg-green-500/10 flex items-center justify-center">
|
||||
<CheckCircle2 className="h-10 w-10 text-green-500" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-green-500">
|
||||
Deposit Successful!
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
{amount} {token} has been added to your P2P internal balance.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg bg-muted/50 border">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
You can now create sell offers or trade P2P using your internal balance.
|
||||
No blockchain fees during P2P trades!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleClose} className="w-full">
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && handleClose()}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Wallet className="h-5 w-5" />
|
||||
Deposit to P2P Balance
|
||||
</DialogTitle>
|
||||
{step !== 'success' && (
|
||||
<DialogDescription>
|
||||
{step === 'select' && 'Deposit crypto from your wallet to P2P internal balance'}
|
||||
{step === 'send' && 'Send tokens to the platform escrow wallet'}
|
||||
{step === 'verify' && 'Verify your transaction to credit your balance'}
|
||||
</DialogDescription>
|
||||
)}
|
||||
</DialogHeader>
|
||||
|
||||
{renderStepContent()}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Wallet,
|
||||
ArrowDownToLine,
|
||||
ArrowUpFromLine,
|
||||
RefreshCw,
|
||||
Lock,
|
||||
Unlock
|
||||
} from 'lucide-react';
|
||||
import { getInternalBalances, type InternalBalance } from '@shared/lib/p2p-fiat';
|
||||
|
||||
interface InternalBalanceCardProps {
|
||||
onDeposit?: () => void;
|
||||
onWithdraw?: () => void;
|
||||
}
|
||||
|
||||
export function InternalBalanceCard({ onDeposit, onWithdraw }: InternalBalanceCardProps) {
|
||||
const [balances, setBalances] = useState<InternalBalance[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
const fetchBalances = async () => {
|
||||
try {
|
||||
const data = await getInternalBalances();
|
||||
setBalances(data);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch balances:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchBalances();
|
||||
}, []);
|
||||
|
||||
const handleRefresh = async () => {
|
||||
setIsRefreshing(true);
|
||||
await fetchBalances();
|
||||
};
|
||||
|
||||
const formatBalance = (value: number, decimals: number = 4) => {
|
||||
return value.toLocaleString(undefined, {
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<Skeleton className="h-6 w-32" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-16 w-full" />
|
||||
<Skeleton className="h-16 w-full" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<Wallet className="h-5 w-5" />
|
||||
P2P Internal Balance
|
||||
</CardTitle>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${isRefreshing ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Internal balance for P2P trading. Deposit to start selling.
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{balances.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<Wallet className="h-12 w-12 mx-auto mb-3 opacity-50" />
|
||||
<p className="text-sm">No balance yet</p>
|
||||
<p className="text-xs">Deposit crypto to start P2P trading</p>
|
||||
</div>
|
||||
) : (
|
||||
balances.map((balance) => (
|
||||
<div
|
||||
key={balance.token}
|
||||
className="p-4 rounded-lg bg-muted/50 border"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<span className="text-xs font-bold text-primary">
|
||||
{balance.token.charAt(0)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-semibold">{balance.token}</span>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Total: {formatBalance(balance.total_balance)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<Unlock className="h-4 w-4 text-green-500" />
|
||||
<div>
|
||||
<p className="text-muted-foreground text-xs">Available</p>
|
||||
<p className="font-medium text-green-600">
|
||||
{formatBalance(balance.available_balance)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Lock className="h-4 w-4 text-yellow-500" />
|
||||
<div>
|
||||
<p className="text-muted-foreground text-xs">Locked (Escrow)</p>
|
||||
<p className="font-medium text-yellow-600">
|
||||
{formatBalance(balance.locked_balance)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 pt-3 border-t grid grid-cols-2 gap-2 text-xs text-muted-foreground">
|
||||
<div>
|
||||
<span>Total Deposited: </span>
|
||||
<span className="text-foreground">{formatBalance(balance.total_deposited, 2)}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>Total Withdrawn: </span>
|
||||
<span className="text-foreground">{formatBalance(balance.total_withdrawn, 2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="default"
|
||||
className="flex-1"
|
||||
onClick={onDeposit}
|
||||
>
|
||||
<ArrowDownToLine className="h-4 w-4 mr-2" />
|
||||
Deposit
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={onWithdraw}
|
||||
disabled={balances.every(b => b.available_balance <= 0)}
|
||||
>
|
||||
<ArrowUpFromLine className="h-4 w-4 mr-2" />
|
||||
Withdraw
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -21,52 +21,8 @@ import {
|
||||
Check
|
||||
} from 'lucide-react';
|
||||
|
||||
// Filter options
|
||||
export interface P2PFilters {
|
||||
// Token
|
||||
token: 'HEZ' | 'PEZ' | 'all';
|
||||
|
||||
// Fiat currency
|
||||
fiatCurrency: string | 'all';
|
||||
|
||||
// Payment methods
|
||||
paymentMethods: string[];
|
||||
|
||||
// Amount range
|
||||
minAmount: number | null;
|
||||
maxAmount: number | null;
|
||||
|
||||
// Merchant tier
|
||||
merchantTiers: ('lite' | 'super' | 'diamond')[];
|
||||
|
||||
// Completion rate
|
||||
minCompletionRate: number;
|
||||
|
||||
// Online status
|
||||
onlineOnly: boolean;
|
||||
|
||||
// Verified only
|
||||
verifiedOnly: boolean;
|
||||
|
||||
// Sort
|
||||
sortBy: 'price' | 'completion_rate' | 'trades' | 'newest';
|
||||
sortOrder: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
// Default filters
|
||||
export const DEFAULT_FILTERS: P2PFilters = {
|
||||
token: 'all',
|
||||
fiatCurrency: 'all',
|
||||
paymentMethods: [],
|
||||
minAmount: null,
|
||||
maxAmount: null,
|
||||
merchantTiers: [],
|
||||
minCompletionRate: 0,
|
||||
onlineOnly: false,
|
||||
verifiedOnly: false,
|
||||
sortBy: 'price',
|
||||
sortOrder: 'asc'
|
||||
};
|
||||
// Import types from separate file to avoid react-refresh warning
|
||||
import { type P2PFilters, DEFAULT_FILTERS } from './types';
|
||||
|
||||
// Available fiat currencies
|
||||
const FIAT_CURRENCIES = [
|
||||
|
||||
@@ -8,7 +8,11 @@ import { PlusCircle, Home, ClipboardList, TrendingUp, CheckCircle2, Clock, Store
|
||||
import { AdList } from './AdList';
|
||||
import { CreateAd } from './CreateAd';
|
||||
import { NotificationBell } from './NotificationBell';
|
||||
import { QuickFilterBar, DEFAULT_FILTERS, type P2PFilters } from './OrderFilters';
|
||||
import { QuickFilterBar } from './OrderFilters';
|
||||
import { InternalBalanceCard } from './InternalBalanceCard';
|
||||
import { DepositModal } from './DepositModal';
|
||||
import { WithdrawModal } from './WithdrawModal';
|
||||
import { DEFAULT_FILTERS, type P2PFilters } from './types';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
|
||||
@@ -22,9 +26,16 @@ export function P2PDashboard() {
|
||||
const [showCreateAd, setShowCreateAd] = useState(false);
|
||||
const [userStats, setUserStats] = useState<UserStats>({ activeTrades: 0, completedTrades: 0, totalVolume: 0 });
|
||||
const [filters, setFilters] = useState<P2PFilters>(DEFAULT_FILTERS);
|
||||
const [showDepositModal, setShowDepositModal] = useState(false);
|
||||
const [showWithdrawModal, setShowWithdrawModal] = useState(false);
|
||||
const [balanceRefreshKey, setBalanceRefreshKey] = useState(0);
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
|
||||
const handleBalanceUpdated = () => {
|
||||
setBalanceRefreshKey(prev => prev + 1);
|
||||
};
|
||||
|
||||
// Fetch user stats
|
||||
useEffect(() => {
|
||||
const fetchStats = async () => {
|
||||
@@ -104,42 +115,54 @@ export function P2PDashboard() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
{/* Stats Cards and Balance Card */}
|
||||
{user && (
|
||||
<div className="grid grid-cols-3 gap-4 mb-6">
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardContent className="py-4 flex items-center gap-3">
|
||||
<div className="p-2 bg-yellow-500/20 rounded-lg">
|
||||
<Clock className="w-5 h-5 text-yellow-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-white">{userStats.activeTrades}</p>
|
||||
<p className="text-sm text-gray-400">Active Trades</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardContent className="py-4 flex items-center gap-3">
|
||||
<div className="p-2 bg-green-500/20 rounded-lg">
|
||||
<CheckCircle2 className="w-5 h-5 text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-white">{userStats.completedTrades}</p>
|
||||
<p className="text-sm text-gray-400">Completed</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardContent className="py-4 flex items-center gap-3">
|
||||
<div className="p-2 bg-blue-500/20 rounded-lg">
|
||||
<TrendingUp className="w-5 h-5 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-white">${userStats.totalVolume.toLocaleString()}</p>
|
||||
<p className="text-sm text-gray-400">Volume</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4 mb-6">
|
||||
{/* Internal Balance Card - Takes more space */}
|
||||
<div className="lg:col-span-1">
|
||||
<InternalBalanceCard
|
||||
key={balanceRefreshKey}
|
||||
onDeposit={() => setShowDepositModal(true)}
|
||||
onWithdraw={() => setShowWithdrawModal(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="lg:col-span-3 grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardContent className="py-4 flex items-center gap-3">
|
||||
<div className="p-2 bg-yellow-500/20 rounded-lg">
|
||||
<Clock className="w-5 h-5 text-yellow-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-white">{userStats.activeTrades}</p>
|
||||
<p className="text-sm text-gray-400">Active Trades</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardContent className="py-4 flex items-center gap-3">
|
||||
<div className="p-2 bg-green-500/20 rounded-lg">
|
||||
<CheckCircle2 className="w-5 h-5 text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-white">{userStats.completedTrades}</p>
|
||||
<p className="text-sm text-gray-400">Completed</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardContent className="py-4 flex items-center gap-3">
|
||||
<div className="p-2 bg-blue-500/20 rounded-lg">
|
||||
<TrendingUp className="w-5 h-5 text-blue-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-white">${userStats.totalVolume.toLocaleString()}</p>
|
||||
<p className="text-sm text-gray-400">Volume</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -179,6 +202,20 @@ export function P2PDashboard() {
|
||||
</Tabs>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Deposit Modal */}
|
||||
<DepositModal
|
||||
isOpen={showDepositModal}
|
||||
onClose={() => setShowDepositModal(false)}
|
||||
onSuccess={handleBalanceUpdated}
|
||||
/>
|
||||
|
||||
{/* Withdraw Modal */}
|
||||
<WithdrawModal
|
||||
isOpen={showWithdrawModal}
|
||||
onClose={() => setShowWithdrawModal(false)}
|
||||
onSuccess={handleBalanceUpdated}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,458 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Loader2,
|
||||
CheckCircle2,
|
||||
AlertTriangle,
|
||||
ArrowUpFromLine,
|
||||
Clock,
|
||||
Info
|
||||
} from 'lucide-react';
|
||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
getInternalBalances,
|
||||
requestWithdraw,
|
||||
getDepositWithdrawHistory,
|
||||
type CryptoToken,
|
||||
type InternalBalance,
|
||||
type DepositWithdrawRequest
|
||||
} from '@shared/lib/p2p-fiat';
|
||||
|
||||
interface WithdrawModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
type WithdrawStep = 'form' | 'confirm' | 'success';
|
||||
|
||||
export function WithdrawModal({ isOpen, onClose, onSuccess }: WithdrawModalProps) {
|
||||
const { selectedAccount } = usePolkadot();
|
||||
|
||||
const [step, setStep] = useState<WithdrawStep>('form');
|
||||
const [token, setToken] = useState<CryptoToken>('HEZ');
|
||||
const [amount, setAmount] = useState('');
|
||||
const [walletAddress, setWalletAddress] = useState('');
|
||||
const [balances, setBalances] = useState<InternalBalance[]>([]);
|
||||
const [pendingRequests, setPendingRequests] = useState<DepositWithdrawRequest[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [requestId, setRequestId] = useState<string>('');
|
||||
|
||||
// Network fee estimate (in HEZ)
|
||||
const NETWORK_FEE = 0.01;
|
||||
const MIN_WITHDRAWAL = 0.1;
|
||||
|
||||
// Fetch balances and pending requests on mount
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
fetchData();
|
||||
// Pre-fill wallet address from connected account
|
||||
if (selectedAccount?.address) {
|
||||
setWalletAddress(selectedAccount.address);
|
||||
}
|
||||
}
|
||||
}, [isOpen, selectedAccount]);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [balanceData, historyData] = await Promise.all([
|
||||
getInternalBalances(),
|
||||
getDepositWithdrawHistory()
|
||||
]);
|
||||
setBalances(balanceData);
|
||||
// Filter for pending withdrawal requests
|
||||
setPendingRequests(
|
||||
historyData.filter(r => r.request_type === 'withdraw' && r.status === 'pending')
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Fetch data error:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetModal = () => {
|
||||
setStep('form');
|
||||
setAmount('');
|
||||
setSubmitting(false);
|
||||
setRequestId('');
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
resetModal();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const getAvailableBalance = (): number => {
|
||||
const balance = balances.find(b => b.token === token);
|
||||
return balance?.available_balance || 0;
|
||||
};
|
||||
|
||||
const getLockedBalance = (): number => {
|
||||
const balance = balances.find(b => b.token === token);
|
||||
return balance?.locked_balance || 0;
|
||||
};
|
||||
|
||||
const getMaxWithdrawable = (): number => {
|
||||
const available = getAvailableBalance();
|
||||
// Subtract network fee for HEZ
|
||||
if (token === 'HEZ') {
|
||||
return Math.max(0, available - NETWORK_FEE);
|
||||
}
|
||||
return available;
|
||||
};
|
||||
|
||||
const handleSetMax = () => {
|
||||
const max = getMaxWithdrawable();
|
||||
setAmount(max.toFixed(4));
|
||||
};
|
||||
|
||||
const validateWithdrawal = (): string | null => {
|
||||
const withdrawAmount = parseFloat(amount);
|
||||
|
||||
if (isNaN(withdrawAmount) || withdrawAmount <= 0) {
|
||||
return 'Please enter a valid amount';
|
||||
}
|
||||
|
||||
if (withdrawAmount < MIN_WITHDRAWAL) {
|
||||
return `Minimum withdrawal is ${MIN_WITHDRAWAL} ${token}`;
|
||||
}
|
||||
|
||||
if (withdrawAmount > getMaxWithdrawable()) {
|
||||
return 'Insufficient available balance';
|
||||
}
|
||||
|
||||
if (!walletAddress || walletAddress.length < 40) {
|
||||
return 'Please enter a valid wallet address';
|
||||
}
|
||||
|
||||
// Check for pending requests
|
||||
const hasPendingForToken = pendingRequests.some(r => r.token === token);
|
||||
if (hasPendingForToken) {
|
||||
return `You already have a pending ${token} withdrawal request`;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleContinue = () => {
|
||||
const error = validateWithdrawal();
|
||||
if (error) {
|
||||
toast.error(error);
|
||||
return;
|
||||
}
|
||||
setStep('confirm');
|
||||
};
|
||||
|
||||
const handleSubmitWithdrawal = async () => {
|
||||
const error = validateWithdrawal();
|
||||
if (error) {
|
||||
toast.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
|
||||
try {
|
||||
const withdrawAmount = parseFloat(amount);
|
||||
const id = await requestWithdraw(token, withdrawAmount, walletAddress);
|
||||
setRequestId(id);
|
||||
setStep('success');
|
||||
onSuccess?.();
|
||||
} catch (error) {
|
||||
console.error('Submit withdrawal error:', error);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderFormStep = () => (
|
||||
<div className="space-y-6">
|
||||
{loading ? (
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Token Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label>Select Token</Label>
|
||||
<Select value={token} onValueChange={(v) => setToken(v as CryptoToken)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="HEZ">HEZ (Native)</SelectItem>
|
||||
<SelectItem value="PEZ">PEZ</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Balance Display */}
|
||||
<div className="p-4 rounded-lg bg-muted/50 border">
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="text-muted-foreground">Available</p>
|
||||
<p className="font-semibold text-green-500">
|
||||
{getAvailableBalance().toFixed(4)} {token}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Locked (Escrow)</p>
|
||||
<p className="font-semibold text-yellow-500">
|
||||
{getLockedBalance().toFixed(4)} {token}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Amount Input */}
|
||||
<div className="space-y-2">
|
||||
<Label>Withdrawal Amount</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="0.00"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
min="0"
|
||||
step="0.0001"
|
||||
/>
|
||||
<div className="absolute right-14 top-1/2 -translate-y-1/2 text-sm text-muted-foreground">
|
||||
{token}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 h-7 text-xs"
|
||||
onClick={handleSetMax}
|
||||
>
|
||||
MAX
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Min: {MIN_WITHDRAWAL} {token} | Max: {getMaxWithdrawable().toFixed(4)} {token}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Wallet Address */}
|
||||
<div className="space-y-2">
|
||||
<Label>Destination Wallet Address</Label>
|
||||
<Input
|
||||
value={walletAddress}
|
||||
onChange={(e) => setWalletAddress(e.target.value)}
|
||||
placeholder="5..."
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Only PezkuwiChain addresses are supported
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Network Fee Info */}
|
||||
{token === 'HEZ' && (
|
||||
<Alert>
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Network fee: ~{NETWORK_FEE} HEZ (deducted from withdrawal amount)
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Pending Requests Warning */}
|
||||
{pendingRequests.length > 0 && (
|
||||
<Alert variant="destructive">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
You have {pendingRequests.length} pending withdrawal request(s).
|
||||
Please wait for them to complete.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleContinue}
|
||||
disabled={!amount || parseFloat(amount) <= 0}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderConfirmStep = () => {
|
||||
const withdrawAmount = parseFloat(amount);
|
||||
const receiveAmount = token === 'HEZ' ? withdrawAmount - NETWORK_FEE : withdrawAmount;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Alert>
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
Please review your withdrawal details carefully.
|
||||
This action cannot be undone.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="p-4 rounded-lg bg-muted/50 border space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">Token</span>
|
||||
<span className="font-semibold">{token}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">Withdrawal Amount</span>
|
||||
<span className="font-semibold">{withdrawAmount.toFixed(4)} {token}</span>
|
||||
</div>
|
||||
{token === 'HEZ' && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-muted-foreground">Network Fee</span>
|
||||
<span className="text-yellow-500">-{NETWORK_FEE} HEZ</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-t pt-4 flex justify-between items-center">
|
||||
<span className="text-muted-foreground">You Will Receive</span>
|
||||
<span className="font-bold text-lg text-green-500">
|
||||
{receiveAmount.toFixed(4)} {token}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg bg-muted/30 border">
|
||||
<p className="text-xs text-muted-foreground mb-1">Destination Address</p>
|
||||
<p className="font-mono text-xs break-all">{walletAddress}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Clock className="h-4 w-4" />
|
||||
<span>Processing time: Usually within 5-30 minutes</span>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setStep('form')}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmitWithdrawal}
|
||||
disabled={submitting}
|
||||
>
|
||||
{submitting ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ArrowUpFromLine className="h-4 w-4 mr-2" />
|
||||
Confirm Withdrawal
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSuccessStep = () => (
|
||||
<div className="space-y-6 text-center">
|
||||
<div className="w-20 h-20 mx-auto rounded-full bg-green-500/10 flex items-center justify-center">
|
||||
<CheckCircle2 className="h-10 w-10 text-green-500" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold text-green-500">
|
||||
Withdrawal Request Submitted!
|
||||
</h3>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Your withdrawal request has been submitted for processing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg bg-muted/50 border space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Request ID</span>
|
||||
<Badge variant="outline" className="font-mono text-xs">
|
||||
{requestId.slice(0, 8)}...
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Status</span>
|
||||
<Badge className="bg-yellow-500/20 text-yellow-500 border-yellow-500/30">
|
||||
<Clock className="h-3 w-3 mr-1" />
|
||||
Processing
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground">Amount</span>
|
||||
<span className="font-semibold">{amount} {token}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Alert>
|
||||
<Info className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
You can track your withdrawal status in the transaction history.
|
||||
Funds will arrive in your wallet within 5-30 minutes.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Button onClick={handleClose} className="w-full">
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !open && handleClose()}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<ArrowUpFromLine className="h-5 w-5" />
|
||||
Withdraw from P2P Balance
|
||||
</DialogTitle>
|
||||
{step !== 'success' && (
|
||||
<DialogDescription>
|
||||
{step === 'form' && 'Withdraw crypto from your P2P balance to external wallet'}
|
||||
{step === 'confirm' && 'Review and confirm your withdrawal'}
|
||||
</DialogDescription>
|
||||
)}
|
||||
</DialogHeader>
|
||||
|
||||
{step === 'form' && renderFormStep()}
|
||||
{step === 'confirm' && renderConfirmStep()}
|
||||
{step === 'success' && renderSuccessStep()}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// P2P Filter types and defaults - separate file to avoid react-refresh warning
|
||||
|
||||
export interface P2PFilters {
|
||||
// Token
|
||||
token: 'HEZ' | 'PEZ' | 'all';
|
||||
|
||||
// Fiat currency
|
||||
fiatCurrency: string | 'all';
|
||||
|
||||
// Payment methods
|
||||
paymentMethods: string[];
|
||||
|
||||
// Amount range
|
||||
minAmount: number | null;
|
||||
maxAmount: number | null;
|
||||
|
||||
// Merchant tier
|
||||
merchantTiers: ('lite' | 'super' | 'diamond')[];
|
||||
|
||||
// Completion rate
|
||||
minCompletionRate: number;
|
||||
|
||||
// Online status
|
||||
onlineOnly: boolean;
|
||||
|
||||
// Verified only
|
||||
verifiedOnly: boolean;
|
||||
|
||||
// Sort
|
||||
sortBy: 'price' | 'completion_rate' | 'trades' | 'newest';
|
||||
sortOrder: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export const DEFAULT_FILTERS: P2PFilters = {
|
||||
token: 'all',
|
||||
fiatCurrency: 'all',
|
||||
paymentMethods: [],
|
||||
minAmount: null,
|
||||
maxAmount: null,
|
||||
merchantTiers: [],
|
||||
minCompletionRate: 0,
|
||||
onlineOnly: false,
|
||||
verifiedOnly: false,
|
||||
sortBy: 'price',
|
||||
sortOrder: 'asc'
|
||||
};
|
||||
Reference in New Issue
Block a user