feat: automate deposit flow + fix listUsers pagination

- Rewrite DepositWithdrawModal to send TX automatically via assetHubApi
  instead of manual copy-paste-hash flow
- Fix listUsers pagination bug (default 50) in 4 edge functions by
  adding perPage: 1000 - fixes P2P offers not showing for users
- Add new i18n keys for automated deposit states in all 6 languages
This commit is contained in:
2026-02-26 21:53:41 +03:00
parent 0f081545a8
commit 4686453df7
12 changed files with 122 additions and 114 deletions
+78 -104
View File
@@ -1,13 +1,12 @@
import { useState, useCallback } from 'react';
import { useState } from 'react';
import {
X,
Copy,
Check,
ArrowDownToLine,
ArrowUpFromLine,
Loader2,
AlertCircle,
CheckCircle2,
Wallet,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { useAuth } from '@/contexts/AuthContext';
@@ -36,7 +35,7 @@ interface DepositWithdrawModalProps {
onSuccess?: () => void;
}
type DepositStep = 'form' | 'verifying' | 'success' | 'error';
type DepositStep = 'form' | 'sending' | 'verifying' | 'success' | 'error';
export function DepositWithdrawModal({
isOpen,
@@ -46,7 +45,7 @@ export function DepositWithdrawModal({
onSuccess,
}: DepositWithdrawModalProps) {
const { sessionToken } = useAuth();
const { address } = useWallet();
const { address, isConnected, keypair, assetHubApi } = useWallet();
const { t, isRTL } = useTranslation();
const { hapticImpact, hapticNotification } = useTelegram();
@@ -55,14 +54,11 @@ export function DepositWithdrawModal({
// Deposit state
const [depositToken, setDepositToken] = useState<'HEZ' | 'PEZ'>('HEZ');
const [depositAmount, setDepositAmount] = useState('');
const [txHash, setTxHash] = useState('');
const [blockNumber, setBlockNumber] = useState('');
const [depositStep, setDepositStep] = useState<DepositStep>('form');
const [depositError, setDepositError] = useState('');
const [depositResult, setDepositResult] = useState<{ amount: number; token: string } | null>(
null
);
const [copied, setCopied] = useState(false);
// Withdraw state
const [withdrawToken, setWithdrawToken] = useState<'HEZ' | 'PEZ'>('HEZ');
@@ -72,28 +68,10 @@ export function DepositWithdrawModal({
const [withdrawError, setWithdrawError] = useState('');
const [withdrawSuccess, setWithdrawSuccess] = useState(false);
const handleCopyAddress = useCallback(async () => {
try {
await navigator.clipboard.writeText(PLATFORM_WALLET);
setCopied(true);
hapticNotification('success');
setTimeout(() => setCopied(false), 2000);
} catch {
// Fallback for environments where clipboard API is not available
const textArea = document.createElement('textarea');
textArea.value = PLATFORM_WALLET;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
setCopied(true);
hapticNotification('success');
setTimeout(() => setCopied(false), 2000);
}
}, [hapticNotification]);
const canDeposit = isConnected && !!keypair && !!assetHubApi;
const handleVerifyDeposit = async () => {
if (!sessionToken || !txHash || !depositAmount) return;
const handleDeposit = async () => {
if (!sessionToken || !depositAmount || !assetHubApi || !keypair) return;
const amount = parseFloat(depositAmount);
if (isNaN(amount) || amount <= 0) {
@@ -102,17 +80,54 @@ export function DepositWithdrawModal({
}
hapticImpact('medium');
setDepositStep('verifying');
setDepositStep('sending');
setDepositError('');
try {
const result = await verifyDeposit(
sessionToken,
txHash.trim(),
depositToken,
amount,
blockNumber ? parseInt(blockNumber) : undefined
);
const amountPlanck = BigInt(Math.floor(amount * 1e12));
// Create transaction on Asset Hub
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let tx: any;
if (depositToken === 'HEZ') {
// Native token transfer on Asset Hub
// eslint-disable-next-line @typescript-eslint/no-explicit-any
tx = (assetHubApi.tx.balances as any).transferKeepAlive(
PLATFORM_WALLET,
amountPlanck.toString()
);
} else {
// PEZ = asset ID 1 on Asset Hub
// eslint-disable-next-line @typescript-eslint/no-explicit-any
tx = (assetHubApi.tx.assets as any).transfer(1, PLATFORM_WALLET, amountPlanck.toString());
}
// Send TX and wait for finalization
const txHash = await new Promise<string>((resolve, reject) => {
tx.signAndSend(
keypair,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(result: any) => {
if (result.dispatchError) {
if (result.dispatchError.isModule) {
const decoded = assetHubApi!.registry.findMetaError(result.dispatchError.asModule);
reject(new Error(`${decoded.section}.${decoded.name}`));
} else {
reject(new Error(result.dispatchError.toString()));
}
return;
}
if (result.status.isFinalized) {
resolve(result.txHash.toHex());
}
}
).catch(reject);
});
// TX finalized, now verify deposit on backend
setDepositStep('verifying');
const result = await verifyDeposit(sessionToken, txHash, depositToken, amount);
setDepositResult({ amount: result.amount, token: result.token });
setDepositStep('success');
@@ -168,8 +183,6 @@ export function DepositWithdrawModal({
setDepositStep('form');
setDepositError('');
setDepositResult(null);
setTxHash('');
setBlockNumber('');
setDepositAmount('');
};
@@ -247,35 +260,23 @@ export function DepositWithdrawModal({
{/* ── Deposit Tab ── */}
{activeTab === 'deposit' && (
<>
{depositStep === 'form' && (
{!canDeposit ? (
<div className="flex flex-col items-center justify-center py-12 space-y-3">
<Wallet className="w-12 h-12 text-muted-foreground/50" />
<p className="text-sm font-medium text-foreground">
{t('p2p.walletNotConnected')}
</p>
<p className="text-xs text-muted-foreground text-center">
{t('p2p.connectWalletFirst')}
</p>
</div>
) : depositStep === 'form' ? (
<>
{/* Instructions */}
<div className="bg-cyan-500/10 border border-cyan-500/20 rounded-xl p-3">
<p className="text-xs text-cyan-300">{t('p2p.depositInstructions')}</p>
</div>
{/* Platform wallet address */}
<div>
<label className="text-xs text-muted-foreground mb-1 block">
{t('p2p.platformWallet')}
</label>
<div className="flex items-center gap-2 bg-muted rounded-lg p-2.5">
<code className="flex-1 text-xs text-foreground break-all font-mono">
{PLATFORM_WALLET}
</code>
<button
onClick={handleCopyAddress}
className="shrink-0 p-1.5 rounded-md hover:bg-card/50 transition-colors"
>
{copied ? (
<Check className="w-4 h-4 text-green-400" />
) : (
<Copy className="w-4 h-4 text-muted-foreground" />
)}
</button>
</div>
</div>
{/* Token select */}
<div>
<label className="text-xs text-muted-foreground mb-1 block">
@@ -314,47 +315,24 @@ export function DepositWithdrawModal({
/>
</div>
{/* TX Hash */}
<div>
<label className="text-xs text-muted-foreground mb-1 block">
{t('p2p.txHash')}
</label>
<input
type="text"
value={txHash}
onChange={(e) => setTxHash(e.target.value)}
placeholder={t('p2p.txHashPlaceholder')}
className="w-full bg-muted rounded-lg px-3 py-2.5 text-xs text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-cyan-500 font-mono"
/>
</div>
{/* Block Number (optional) */}
<div>
<label className="text-xs text-muted-foreground mb-1 block">
{t('p2p.blockNumber')} ({t('p2p.optional')})
</label>
<input
type="number"
inputMode="numeric"
value={blockNumber}
onChange={(e) => setBlockNumber(e.target.value)}
placeholder="e.g. 1234567"
className="w-full bg-muted rounded-lg px-3 py-2.5 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-cyan-500"
/>
</div>
{/* Verify button */}
{/* Deposit button */}
<button
onClick={handleVerifyDeposit}
disabled={!txHash || !depositAmount || !sessionToken}
onClick={handleDeposit}
disabled={!depositAmount || !sessionToken}
className="w-full py-3 bg-cyan-500 hover:bg-cyan-600 disabled:bg-muted disabled:text-muted-foreground text-white font-medium rounded-xl transition-colors"
>
{t('p2p.verifyDeposit')}
{t('p2p.deposit')}
</button>
</>
)}
{depositStep === 'verifying' && (
) : depositStep === 'sending' ? (
<div className="flex flex-col items-center justify-center py-12 space-y-3">
<Loader2 className="w-10 h-10 text-cyan-400 animate-spin" />
<p className="text-sm text-foreground">{t('p2p.depositSending')}</p>
<p className="text-xs text-muted-foreground text-center">
{t('p2p.depositSendingDesc')}
</p>
</div>
) : depositStep === 'verifying' ? (
<div className="flex flex-col items-center justify-center py-12 space-y-3">
<Loader2 className="w-10 h-10 text-cyan-400 animate-spin" />
<p className="text-sm text-foreground">{t('p2p.verifying')}</p>
@@ -362,9 +340,7 @@ export function DepositWithdrawModal({
{t('p2p.verifyingDesc')}
</p>
</div>
)}
{depositStep === 'success' && depositResult && (
) : depositStep === 'success' && depositResult ? (
<div className="flex flex-col items-center justify-center py-12 space-y-3">
<CheckCircle2 className="w-12 h-12 text-green-400" />
<p className="text-lg font-bold text-foreground">{t('p2p.depositSuccess')}</p>
@@ -378,9 +354,7 @@ export function DepositWithdrawModal({
{t('common.close')}
</button>
</div>
)}
{depositStep === 'error' && (
) : (
<div className="flex flex-col items-center justify-center py-12 space-y-3">
<AlertCircle className="w-12 h-12 text-red-400" />
<p className="text-lg font-bold text-foreground">{t('p2p.depositFailed')}</p>
+6 -1
View File
@@ -408,8 +408,13 @@ const ar: Translations = {
verifyingDesc: 'يتم فحص المعاملة على السلسلة. قد يستغرق هذا حتى ٦٠ ثانية.',
depositSuccess: 'تم الإيداع بنجاح!',
depositFailed: 'فشل الإيداع',
depositInstructions: 'أرسل التوكنات إلى عنوان محفظة المنصة أدناه، ثم الصق هاش المعاملة للتحقق.',
depositInstructions:
'اختر توكن وأدخل المبلغ للإيداع في رصيد P2P الخاص بك. سيتم إرسال المعاملة من محفظتك تلقائياً.',
depositInvalidAmount: 'يرجى إدخال مبلغ صالح',
depositSending: 'جاري إرسال المعاملة...',
depositSendingDesc: 'يتم توقيع وإرسال إيداعك إلى Asset Hub. قد يستغرق هذا حتى 30 ثانية.',
walletNotConnected: 'المحفظة غير متصلة',
connectWalletFirst: 'يرجى فتح قفل محفظتك للإيداع.',
selectToken: 'اختر التوكن',
withdrawAmount: 'مبلغ السحب',
networkFee: 'رسوم الشبكة',
+6 -1
View File
@@ -411,8 +411,13 @@ const ckb: Translations = {
depositSuccess: 'پارەدانان سەرکەوتوو بوو!',
depositFailed: 'پارەدانان سەرنەکەوت',
depositInstructions:
'تۆکن بنێرە بۆ ناونیشانی جزدانی پلاتفۆرمی خوارەوە، پاشان هاشی مامەڵە بلکێنە بۆ پشتڕاستکردنەوە.',
'تۆکنێک هەڵبژێرە و بڕەکە بنووسە بۆ زیادکردن لە باڵانسی P2P. مامەڵەکە بە شێوەیەکی خۆکار لە جزدانەکەت دەنێردرێت.',
depositInvalidAmount: 'تکایە بڕێکی دروست بنووسە',
depositSending: 'مامەڵە دەنێردرێت...',
depositSendingDesc:
'پارەدانانەکەت لە Asset Hub واژووکراوە و دەنێردرێت. ئەمە لەوانەیە تا ٣٠ چرکە بخایەنێت.',
walletNotConnected: 'جزدان پەیوەندی نەکراوە',
connectWalletFirst: 'تکایە بۆ پارەدانان قفڵی جزدانەکەت بکەرەوە.',
selectToken: 'تۆکن هەڵبژێرە',
withdrawAmount: 'بڕی دەرهێنان',
networkFee: 'کرێی تۆڕ',
+6 -1
View File
@@ -410,8 +410,13 @@ const en: Translations = {
depositSuccess: 'Deposit Successful!',
depositFailed: 'Deposit Failed',
depositInstructions:
'Send tokens to the platform wallet address below, then paste the transaction hash to verify.',
'Select a token and enter the amount to deposit to your P2P balance. The transaction will be sent from your wallet automatically.',
depositInvalidAmount: 'Please enter a valid amount',
depositSending: 'Sending transaction...',
depositSendingDesc:
'Signing and sending your deposit to Asset Hub. This may take up to 30 seconds.',
walletNotConnected: 'Wallet not connected',
connectWalletFirst: 'Please unlock your wallet to make deposits.',
selectToken: 'Select Token',
withdrawAmount: 'Withdrawal Amount',
networkFee: 'Network Fee',
+6 -1
View File
@@ -410,8 +410,13 @@ const fa: Translations = {
depositSuccess: 'واریز موفق!',
depositFailed: 'واریز ناموفق',
depositInstructions:
'توکن‌ها را به آدرس کیف پول پلتفرم زیر ارسال کنید، سپس هش تراکنش را برای تایید جایگذاری کنید.',
'یک توکن انتخاب کنید و مقدار را وارد کنید تا به موجودی P2P شما اضافه شود. تراکنش به صورت خودکار از کیف پول شما ارسال خواهد شد.',
depositInvalidAmount: 'لطفا مقدار معتبری وارد کنید',
depositSending: 'در حال ارسال تراکنش...',
depositSendingDesc:
'واریز شما در حال امضا و ارسال به Asset Hub است. این عملیات ممکن است تا ۳۰ ثانیه طول بکشد.',
walletNotConnected: 'کیف پول متصل نیست',
connectWalletFirst: 'لطفا برای واریز، قفل کیف پول خود را باز کنید.',
selectToken: 'انتخاب توکن',
withdrawAmount: 'مقدار برداشت',
networkFee: 'کارمزد شبکه',
+6 -1
View File
@@ -423,8 +423,13 @@ const krd: Translations = {
depositSuccess: 'Depo Serkeftî!',
depositFailed: 'Depo Serneket',
depositInstructions:
'Token bişîne navnîşana cûzdanê ya platformê ya jêrîn, paşê hash ya danûstandinê bişkoje ji bo verastkirin.',
'Ji bo ku hûn li balanseya P2P-ê ya xwe depo bikin, tokenek hilbijêrin û mîqdarê binivîsin. Danûstandin dê ji cûzdanê we bixweber were şandin.',
depositInvalidAmount: 'Ji kerema xwe mîqdarek derbasdar binivîsin',
depositSending: 'Danûstandin tê şandin...',
depositSendingDesc:
'Depoya we li Asset Hub tê îmzekirin û şandin. Ev dibe ku heta 30 saniyeyan bigire.',
walletNotConnected: 'Cûzdan ve negirêdayî ye',
connectWalletFirst: 'Ji bo depokirinê, ji kerema xwe kilîda cûzdanê vekin.',
selectToken: 'Token Hilbijêrin',
withdrawAmount: 'Mîqdara Derxistinê',
networkFee: 'Heqê Torê',
+6 -1
View File
@@ -410,8 +410,13 @@ const tr: Translations = {
depositSuccess: 'Yatırma Başarılı!',
depositFailed: 'Yatırma Başarısız',
depositInstructions:
'Aşağıdaki platform cüzdan adresine token gönderin, ardından doğrulamak için işlem hash yapıştırın.',
'P2P bakiyenize yatırmak için bir token seçin ve miktarı girin. İşlem cüzdanınızdan otomatik olarak gönderilecektir.',
depositInvalidAmount: 'Geçerli bir miktar girin',
depositSending: 'İşlem gönderiliyor...',
depositSendingDesc:
"Yatırma işleminiz Asset Hub'a imzalanıp gönderiliyor. Bu işlem 30 saniyeye kadar sürebilir.",
walletNotConnected: 'Cüzdan bağlı değil',
connectWalletFirst: 'Yatırma yapmak için cüzdanınızın kilidini açın.',
selectToken: 'Token Seçin',
withdrawAmount: 'Çekim Miktarı',
networkFee: 'Ağ Ücreti',
+4
View File
@@ -421,6 +421,10 @@ export interface Translations {
depositFailed: string;
depositInstructions: string;
depositInvalidAmount: string;
depositSending: string;
depositSendingDesc: string;
walletNotConnected: string;
connectWalletFirst: string;
selectToken: string;
withdrawAmount: string;
networkFee: string;
@@ -126,7 +126,7 @@ serve(async (req) => {
const telegramEmail = `telegram_${telegramId}@pezkuwichain.io`;
const {
data: { users: authUsers },
} = await supabase.auth.admin.listUsers();
} = await supabase.auth.admin.listUsers({ perPage: 1000 });
const authUser = authUsers?.find((u) => u.email === telegramEmail);
if (!authUser) {
+1 -1
View File
@@ -138,7 +138,7 @@ serve(async (req) => {
const telegramEmail = `telegram_${telegramId}@pezkuwichain.io`;
const {
data: { users: authUsers },
} = await supabase.auth.admin.listUsers();
} = await supabase.auth.admin.listUsers({ perPage: 1000 });
const authUser = authUsers?.find((u) => u.email === telegramEmail);
if (!authUser) {
@@ -148,7 +148,7 @@ serve(async (req) => {
const telegramEmail = `telegram_${telegramId}@pezkuwichain.io`;
const {
data: { users: existingUsers },
} = await serviceClient.auth.admin.listUsers();
} = await serviceClient.auth.admin.listUsers({ perPage: 1000 });
const authUser = existingUsers?.find((u) => u.email === telegramEmail);
if (!authUser) {
@@ -355,7 +355,7 @@ serve(async (req) => {
// Try to get existing auth user
const {
data: { users: existingUsers },
} = await serviceClient.auth.admin.listUsers();
} = await serviceClient.auth.admin.listUsers({ perPage: 1000 });
let authUser = existingUsers?.find((u) => u.email === telegramEmail);
if (!authUser) {