fix: auto-verify deposit after TX sign, remove manual verify step

The manual "Verify Deposit" step required users to click a button after
signing. Hash was already captured automatically, making the manual step
redundant and risky (modal close = hash lost). Now verification starts
immediately after TX is signed, with spinner UI and retry on failure.
This commit is contained in:
2026-02-26 15:30:30 +03:00
parent 768d637fdc
commit f55a522eba
7 changed files with 112 additions and 72 deletions
+88 -72
View File
@@ -25,7 +25,6 @@ import {
Copy, Copy,
CheckCircle2, CheckCircle2,
AlertTriangle, AlertTriangle,
ExternalLink,
QrCode, QrCode,
Wallet Wallet
} from 'lucide-react'; } from 'lucide-react';
@@ -45,7 +44,7 @@ interface DepositModalProps {
onSuccess?: () => void; onSuccess?: () => void;
} }
type DepositStep = 'select' | 'send' | 'verify' | 'success'; type DepositStep = 'select' | 'send' | 'verifying' | 'success';
export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps) { export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -61,7 +60,7 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
const [blockNumber, setBlockNumber] = useState<number | undefined>(); const [blockNumber, setBlockNumber] = useState<number | undefined>();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [verifying, setVerifying] = useState(false); const [verifyError, setVerifyError] = useState('');
// Fetch platform wallet address on mount // Fetch platform wallet address on mount
useEffect(() => { useEffect(() => {
@@ -83,7 +82,7 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
setBlockNumber(undefined); setBlockNumber(undefined);
setLoading(false); setLoading(false);
setCopied(false); setCopied(false);
setVerifying(false); setVerifyError('');
}; };
const handleClose = () => { const handleClose = () => {
@@ -145,14 +144,18 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
if (hash) { if (hash) {
setTxHash(hash); setTxHash(hash);
// Capture approximate block number for faster verification // Capture approximate block number for faster verification
let blockNum: number | undefined;
try { try {
const header = await assetHubApi.rpc.chain.getHeader(); const header = await assetHubApi.rpc.chain.getHeader();
setBlockNumber(header.number.toNumber()); blockNum = header.number.toNumber();
setBlockNumber(blockNum);
} catch { } catch {
// Non-critical - verification will still work via search // Non-critical - verification will still work via search
} }
setStep('verify'); setStep('verifying');
toast.success(t('p2pDeposit.txSent')); toast.success(t('p2pDeposit.txSent'));
// Auto-verify immediately — fire and forget
handleVerifyDeposit(hash, blockNum);
} }
} catch (error: unknown) { } catch (error: unknown) {
console.error('Deposit transaction error:', error); console.error('Deposit transaction error:', error);
@@ -163,8 +166,11 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
} }
}; };
const handleVerifyDeposit = async () => { const handleVerifyDeposit = async (hash?: string, blockNum?: number) => {
if (!txHash) { const verifyHash = hash || txHash;
const verifyBlockNumber = blockNum !== undefined ? blockNum : blockNumber;
if (!verifyHash) {
toast.error(t('p2pDeposit.enterTxHash')); toast.error(t('p2pDeposit.enterTxHash'));
return; return;
} }
@@ -175,11 +181,10 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
return; return;
} }
setVerifying(true); setVerifyError('');
setStep('verifying');
try { try {
// Call the Edge Function for secure deposit verification
// Use fetch directly to read response body on all status codes
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY; const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
const res = await fetch(`${supabaseUrl}/functions/v1/verify-deposit`, { const res = await fetch(`${supabaseUrl}/functions/v1/verify-deposit`, {
@@ -190,12 +195,12 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
'apikey': supabaseKey, 'apikey': supabaseKey,
}, },
body: JSON.stringify({ body: JSON.stringify({
txHash, txHash: verifyHash,
token, token,
expectedAmount: depositAmount, expectedAmount: depositAmount,
walletAddress: selectedAccount?.address, walletAddress: selectedAccount?.address,
identityId, identityId,
...(blockNumber ? { blockNumber } : {}) ...(verifyBlockNumber ? { blockNumber: verifyBlockNumber } : {})
}) })
}); });
@@ -211,9 +216,7 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
} catch (error) { } catch (error) {
console.error('Verify deposit error:', error); console.error('Verify deposit error:', error);
const message = error instanceof Error ? error.message : t('p2pDeposit.verificationFailed'); const message = error instanceof Error ? error.message : t('p2pDeposit.verificationFailed');
toast.error(message); setVerifyError(message);
} finally {
setVerifying(false);
} }
}; };
@@ -351,67 +354,80 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
</div> </div>
); );
case 'verify': case 'verifying':
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<Alert> {verifyError ? (
<CheckCircle2 className="h-4 w-4 text-green-500" /> <>
<AlertDescription> <div className="text-center space-y-4">
{t('p2pDeposit.txSentVerify')} <div className="w-16 h-16 mx-auto rounded-full bg-destructive/10 flex items-center justify-center">
</AlertDescription> <AlertTriangle className="h-8 w-8 text-destructive" />
</Alert> </div>
<div>
<div className="space-y-2"> <h3 className="text-lg font-semibold text-destructive">
<Label>{t('p2pDeposit.txHash')}</Label> {t('p2pDeposit.verifyFailed')}
<div className="flex gap-2"> </h3>
<Input <p className="text-sm text-muted-foreground mt-2">{verifyError}</p>
value={txHash} </div>
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">{t('p2pDeposit.tokenLabel')}</p>
<p className="font-semibold">{token}</p>
</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">{t('p2pDeposit.tokenLabel')}</p>
<p className="font-semibold">{token}</p>
</div>
<div>
<p className="text-muted-foreground">{t('p2pDeposit.amountLabel')}</p>
<p className="font-semibold">{amount}</p>
</div>
</div>
{txHash && (
<div className="mt-3 pt-3 border-t">
<p className="text-muted-foreground text-xs">TX</p>
<p className="font-mono text-xs break-all">{txHash}</p>
</div>
)}
</div>
<div className="flex gap-2">
<Button variant="outline" className="flex-1" onClick={handleClose}>
{t('cancel')}
</Button>
<Button className="flex-1" onClick={() => handleVerifyDeposit()}>
{t('p2pDeposit.retry')}
</Button>
</div>
</>
) : (
<div className="text-center space-y-4 py-4">
<Loader2 className="h-12 w-12 animate-spin mx-auto text-primary" />
<div> <div>
<p className="text-muted-foreground">{t('p2pDeposit.amountLabel')}</p> <h3 className="text-lg font-semibold">{t('p2pDeposit.autoVerifying')}</h3>
<p className="font-semibold">{amount}</p> <p className="text-sm text-muted-foreground mt-1">
{t('p2pDeposit.autoVerifyingDesc')}
</p>
</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">{t('p2pDeposit.tokenLabel')}</p>
<p className="font-semibold">{token}</p>
</div>
<div>
<p className="text-muted-foreground">{t('p2pDeposit.amountLabel')}</p>
<p className="font-semibold">{amount}</p>
</div>
</div>
{txHash && (
<div className="mt-3 pt-3 border-t">
<p className="text-muted-foreground text-xs">TX</p>
<p className="font-mono text-xs break-all">{txHash}</p>
</div>
)}
</div> </div>
</div> </div>
</div> )}
<DialogFooter>
<Button variant="outline" onClick={handleClose}>
{t('cancel')}
</Button>
<Button
onClick={handleVerifyDeposit}
disabled={verifying || !txHash}
>
{verifying ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
{t('p2pDeposit.verifying')}
</>
) : (
t('p2pDeposit.verifyDeposit')
)}
</Button>
</DialogFooter>
</div> </div>
); );
@@ -457,7 +473,7 @@ export function DepositModal({ isOpen, onClose, onSuccess }: DepositModalProps)
<DialogDescription> <DialogDescription>
{step === 'select' && t('p2pDeposit.selectStep')} {step === 'select' && t('p2pDeposit.selectStep')}
{step === 'send' && t('p2pDeposit.sendStep')} {step === 'send' && t('p2pDeposit.sendStep')}
{step === 'verify' && t('p2pDeposit.verifyStep')} {step === 'verifying' && t('p2pDeposit.autoVerifying')}
</DialogDescription> </DialogDescription>
)} )}
</DialogHeader> </DialogHeader>
+4
View File
@@ -1248,6 +1248,10 @@ export default {
'p2pDeposit.tokenLabel': 'الرمز', 'p2pDeposit.tokenLabel': 'الرمز',
'p2pDeposit.amountLabel': 'المبلغ', 'p2pDeposit.amountLabel': 'المبلغ',
'p2pDeposit.done': 'تم', 'p2pDeposit.done': 'تم',
'p2pDeposit.autoVerifying': 'جاري التحقق من الإيداع...',
'p2pDeposit.autoVerifyingDesc': 'جاري البحث عن معاملتك في البلوكتشين...',
'p2pDeposit.verifyFailed': 'فشل التحقق',
'p2pDeposit.retry': 'حاول مرة أخرى',
'p2pWithdraw.title': 'سحب من رصيد P2P', 'p2pWithdraw.title': 'سحب من رصيد P2P',
'p2pWithdraw.formStep': 'اسحب عملة رقمية من رصيد P2P إلى محفظة خارجية', 'p2pWithdraw.formStep': 'اسحب عملة رقمية من رصيد P2P إلى محفظة خارجية',
+4
View File
@@ -1238,6 +1238,10 @@ export default {
'p2pDeposit.tokenLabel': 'تۆکن', 'p2pDeposit.tokenLabel': 'تۆکن',
'p2pDeposit.amountLabel': 'بڕ', 'p2pDeposit.amountLabel': 'بڕ',
'p2pDeposit.done': 'تەواو', 'p2pDeposit.done': 'تەواو',
'p2pDeposit.autoVerifying': 'دانان لە پشتڕاستکردنەوەدایە...',
'p2pDeposit.autoVerifyingDesc': 'لە بلۆکچەین بۆ مامەڵەکەت دەگەڕێت...',
'p2pDeposit.verifyFailed': 'پشتڕاستکردنەوە سەرکەوتوو نەبوو',
'p2pDeposit.retry': 'دووبارە هەوڵ بدەوە',
'p2pWithdraw.title': 'دەرهێنان لە باڵانسی P2P', 'p2pWithdraw.title': 'دەرهێنان لە باڵانسی P2P',
'p2pWithdraw.formStep': 'لە باڵانسی P2P کریپتۆ دەربهێنە بۆ جزدانی دەرەکی', 'p2pWithdraw.formStep': 'لە باڵانسی P2P کریپتۆ دەربهێنە بۆ جزدانی دەرەکی',
+4
View File
@@ -1591,6 +1591,10 @@ export default {
'p2pDeposit.tokenLabel': 'Token', 'p2pDeposit.tokenLabel': 'Token',
'p2pDeposit.amountLabel': 'Amount', 'p2pDeposit.amountLabel': 'Amount',
'p2pDeposit.done': 'Done', 'p2pDeposit.done': 'Done',
'p2pDeposit.autoVerifying': 'Verifying deposit...',
'p2pDeposit.autoVerifyingDesc': 'Searching for your transaction on blockchain...',
'p2pDeposit.verifyFailed': 'Verification failed',
'p2pDeposit.retry': 'Try Again',
// WithdrawModal // WithdrawModal
'p2pWithdraw.title': 'Withdraw from P2P Balance', 'p2pWithdraw.title': 'Withdraw from P2P Balance',
+4
View File
@@ -1261,6 +1261,10 @@ export default {
'p2pDeposit.tokenLabel': 'توکن', 'p2pDeposit.tokenLabel': 'توکن',
'p2pDeposit.amountLabel': 'مقدار', 'p2pDeposit.amountLabel': 'مقدار',
'p2pDeposit.done': 'انجام شد', 'p2pDeposit.done': 'انجام شد',
'p2pDeposit.autoVerifying': 'واریز در حال تأیید...',
'p2pDeposit.autoVerifyingDesc': 'در حال جستجوی تراکنش شما در بلاکچین...',
'p2pDeposit.verifyFailed': 'تأیید ناموفق بود',
'p2pDeposit.retry': 'تلاش مجدد',
// WithdrawModal // WithdrawModal
'p2pWithdraw.title': 'برداشت از موجودی P2P', 'p2pWithdraw.title': 'برداشت از موجودی P2P',
+4
View File
@@ -1253,6 +1253,10 @@ export default {
'p2pDeposit.tokenLabel': 'Token', 'p2pDeposit.tokenLabel': 'Token',
'p2pDeposit.amountLabel': 'Miqdar', 'p2pDeposit.amountLabel': 'Miqdar',
'p2pDeposit.done': 'Temam', 'p2pDeposit.done': 'Temam',
'p2pDeposit.autoVerifying': 'Danîn tê piştrastkirin...',
'p2pDeposit.autoVerifyingDesc': 'Li blockchain kirrûbirê te tê lêgerîn...',
'p2pDeposit.verifyFailed': 'Piştrastkirin bi ser neket',
'p2pDeposit.retry': 'Dîsa Biceribîne',
// WithdrawModal // WithdrawModal
'p2pWithdraw.title': 'Ji Hesabê P2P Vekişîne', 'p2pWithdraw.title': 'Ji Hesabê P2P Vekişîne',
+4
View File
@@ -1247,6 +1247,10 @@ export default {
'p2pDeposit.tokenLabel': 'Token', 'p2pDeposit.tokenLabel': 'Token',
'p2pDeposit.amountLabel': 'Miktar', 'p2pDeposit.amountLabel': 'Miktar',
'p2pDeposit.done': 'Tamam', 'p2pDeposit.done': 'Tamam',
'p2pDeposit.autoVerifying': 'Yatırım doğrulanıyor...',
'p2pDeposit.autoVerifyingDesc': "Blockchain'de işleminiz aranıyor...",
'p2pDeposit.verifyFailed': 'Doğrulama başarısız oldu',
'p2pDeposit.retry': 'Tekrar Dene',
// WithdrawModal // WithdrawModal
'p2pWithdraw.title': 'P2P Bakiyeden Çekim', 'p2pWithdraw.title': 'P2P Bakiyeden Çekim',