feat: add Solana USDT/USDC support and refine admin payouts UI
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -43,3 +43,4 @@ yarn-error.log*
|
||||
/scripts/migrate-settings.ts
|
||||
/scripts/migrate-merchant-fees.ts
|
||||
/scripts/migrate-merchant-balances.ts
|
||||
scripts/migrate-payout-system.ts
|
||||
|
||||
@@ -16,7 +16,8 @@ import {
|
||||
ChevronDown,
|
||||
Wallet,
|
||||
Building2,
|
||||
Code2
|
||||
Code2,
|
||||
ArrowUpRight
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function AdminLayout({
|
||||
@@ -35,6 +36,7 @@ export default function AdminLayout({
|
||||
const navItems = [
|
||||
{ label: 'Genel Bakış', icon: LayoutDashboard, href: '/admin' },
|
||||
{ label: 'Firmalar', icon: Building2, href: '/admin/merchants' },
|
||||
{ label: 'Ödemeler', icon: ArrowUpRight, href: '/admin/payouts' },
|
||||
{ label: 'İşlemler', icon: CreditCard, href: '/admin/transactions' },
|
||||
{ label: 'Müşteriler', icon: Users, href: '/admin/customers' },
|
||||
{ label: 'Analizler', icon: BarChart3, href: '/admin/analytics' },
|
||||
|
||||
@@ -3,30 +3,54 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
Plus,
|
||||
Building2,
|
||||
Copy,
|
||||
ExternalLink,
|
||||
MoreVertical,
|
||||
Globe,
|
||||
Check,
|
||||
Pencil,
|
||||
Trash2,
|
||||
X
|
||||
Plus, Search, Building2, Copy, Check, Pencil, Trash2,
|
||||
ExternalLink, Globe, LayoutGrid, List, AlertCircle, X,
|
||||
DollarSign, Wallet, ArrowRight, ShieldCheck, Loader2,
|
||||
Coins, History
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function MerchantsPage() {
|
||||
const [merchants, setMerchants] = useState<any[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [treasuryData, setTreasuryData] = useState<any>(null);
|
||||
const [isLoadingTreasury, setIsLoadingTreasury] = useState(true);
|
||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [editingMerchant, setEditingMerchant] = useState<any>(null);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
const [showPayoutModal, setShowPayoutModal] = useState(false);
|
||||
const [selectedMerchant, setSelectedMerchant] = useState<any>(null);
|
||||
const [payoutAmount, setPayoutAmount] = useState<string>('');
|
||||
const [payoutNetwork, setPayoutNetwork] = useState<string>('POLYGON');
|
||||
const [payoutCurrency, setPayoutCurrency] = useState<string>('USDT');
|
||||
const [merchantBalances, setMerchantBalances] = useState<any[]>([]);
|
||||
const [merchantFeePercent, setMerchantFeePercent] = useState<number>(1);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [editForm, setEditForm] = useState({
|
||||
name: '',
|
||||
webhook_url: '',
|
||||
fee_percent: '',
|
||||
payout_address: '',
|
||||
payout_addresses: { EVM: '', SOLANA: '', TRON: '', BITCOIN: '' } as Record<string, string>
|
||||
});
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchMerchants();
|
||||
fetchTreasury();
|
||||
}, []);
|
||||
|
||||
const fetchTreasury = async () => {
|
||||
setIsLoadingTreasury(true);
|
||||
try {
|
||||
const res = await fetch('/api/admin/treasury/balances');
|
||||
const data = await res.json();
|
||||
if (res.ok) setTreasuryData(data.balances);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setIsLoadingTreasury(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchMerchants = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
@@ -48,30 +72,52 @@ export default function MerchantsPage() {
|
||||
};
|
||||
|
||||
const handleEditClick = (merchant: any) => {
|
||||
setEditingMerchant({ ...merchant });
|
||||
setIsEditModalOpen(true);
|
||||
setSelectedMerchant(merchant);
|
||||
setEditForm({
|
||||
name: merchant.name,
|
||||
webhook_url: merchant.webhook_url || '',
|
||||
fee_percent: merchant.fee_percent?.toString() || '1.0',
|
||||
payout_address: merchant.payout_address || '',
|
||||
payout_addresses: merchant.payout_addresses || { EVM: '', SOLANA: '', TRON: '', BITCOIN: '' }
|
||||
});
|
||||
setShowEditModal(true);
|
||||
};
|
||||
|
||||
const handlePayoutClick = async (merchant: any) => {
|
||||
setSelectedMerchant(merchant);
|
||||
setPayoutAmount('');
|
||||
setShowPayoutModal(true);
|
||||
// Fetch merchant's per-network balances
|
||||
try {
|
||||
const res = await fetch(`/api/merchants/${merchant.id}/balances`);
|
||||
const data = await res.json();
|
||||
setMerchantBalances(data.balances || []);
|
||||
setMerchantFeePercent(data.feePercent || 1);
|
||||
} catch (e) {
|
||||
setMerchantBalances([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateMerchant = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
const response = await fetch(`/api/merchants/${editingMerchant.id}`, {
|
||||
const response = await fetch(`/api/merchants/${selectedMerchant.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: editingMerchant.name,
|
||||
webhook_url: editingMerchant.webhook_url,
|
||||
payment_provider: editingMerchant.payment_provider,
|
||||
provider_config: editingMerchant.provider_config,
|
||||
fee_percent: editingMerchant.fee_percent
|
||||
name: editForm.name,
|
||||
webhook_url: editForm.webhook_url,
|
||||
fee_percent: parseFloat(editForm.fee_percent),
|
||||
payout_address: editForm.payout_address,
|
||||
payout_addresses: editForm.payout_addresses
|
||||
})
|
||||
});
|
||||
if (!response.ok) throw new Error('Güncelleme başarısız.');
|
||||
|
||||
await fetchMerchants();
|
||||
setIsEditModalOpen(false);
|
||||
setEditingMerchant(null);
|
||||
setShowEditModal(false);
|
||||
setSelectedMerchant(null);
|
||||
} catch (err: any) {
|
||||
alert(err.message);
|
||||
} finally {
|
||||
@@ -93,6 +139,38 @@ export default function MerchantsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleProcessPayout = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!selectedMerchant || parseFloat(payoutAmount) <= 0) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const response = await fetch('/api/admin/payouts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
merchantId: selectedMerchant.id,
|
||||
amount: parseFloat(payoutAmount),
|
||||
network: payoutNetwork,
|
||||
currency: payoutCurrency
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.error || 'Ödeme işlemi başarısız.');
|
||||
|
||||
alert('Ödeme başarıyla gönderildi! TX Hash: ' + data.txHash);
|
||||
await fetchMerchants();
|
||||
setShowPayoutModal(false);
|
||||
setSelectedMerchant(null);
|
||||
setPayoutAmount('');
|
||||
} catch (err: any) {
|
||||
alert(err.message);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
||||
{/* Header */}
|
||||
@@ -116,6 +194,7 @@ export default function MerchantsPage() {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Merchant Cards Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-6">
|
||||
{isLoading ? (
|
||||
@@ -164,21 +243,44 @@ export default function MerchantsPage() {
|
||||
</div>
|
||||
|
||||
{/* Balance Section */}
|
||||
<div className="mb-6 p-4 bg-emerald-50/50 rounded-2xl border border-emerald-100 flex items-center justify-between">
|
||||
<div className="mb-6 p-4 bg-emerald-50/50 rounded-2xl border border-emerald-100 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[10px] font-black text-emerald-600 uppercase tracking-widest pl-1">İçerideki Bakiye</p>
|
||||
<p className="text-xl font-black text-emerald-700">
|
||||
{new Intl.NumberFormat('tr-TR', { style: 'currency', currency: 'TRY' }).format(m.available_balance || 0)}
|
||||
{new Intl.NumberFormat('tr-TR', { style: 'currency', currency: 'TRY' }).format(Number(m.available_balance || 0))}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Toplam Ödenen</p>
|
||||
<p className="text-xs font-bold text-gray-600">
|
||||
{new Intl.NumberFormat('tr-TR', { style: 'currency', currency: 'TRY' }).format(m.withdrawn_balance || 0)}
|
||||
{new Intl.NumberFormat('tr-TR', { style: 'currency', currency: 'TRY' }).format(Number(m.withdrawn_balance || 0))}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{m.balance_breakdown && m.balance_breakdown.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 pt-2 border-t border-emerald-100/50">
|
||||
{m.balance_breakdown.map((b: any, i: number) => (
|
||||
<div key={i} className="px-2 py-1 bg-white/60 rounded-lg border border-emerald-100/50 flex items-center gap-1.5 shadow-sm">
|
||||
<div className={`w-1.5 h-1.5 rounded-full ${b.network === 'SOLANA' ? 'bg-emerald-400' : b.network === 'POLYGON' ? 'bg-purple-400' : b.network === 'TRON' ? 'bg-red-400' : 'bg-orange-400'}`}></div>
|
||||
<span className="text-[9px] font-black text-emerald-800 uppercase tabular-nums">
|
||||
{Number(b.amount).toFixed(4)} {b.token}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => handlePayoutClick(m)}
|
||||
disabled={parseFloat(m.available_balance || '0') <= 0}
|
||||
className="w-full flex items-center justify-center gap-2 py-3 bg-emerald-600 text-white rounded-xl text-[10px] font-black uppercase tracking-widest hover:bg-emerald-700 disabled:opacity-50 disabled:grayscale transition-all shadow-lg shadow-emerald-100"
|
||||
>
|
||||
<Wallet size={14} /> Ödeme Gönder (Withdraw)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* API Key Section */}
|
||||
<div className="p-4 bg-gray-50 rounded-2xl space-y-2 border border-transparent hover:border-gray-100 transition-colors">
|
||||
@@ -188,7 +290,7 @@ export default function MerchantsPage() {
|
||||
{m.api_key || '••••••••••••••••'}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => copyToClipboard(m.api_key, m.id + '-key')}
|
||||
onClick={() => copyToClipboard(m.api_key || '', m.id + '-key')}
|
||||
className="p-2 text-gray-400 hover:text-blue-600 transition-colors"
|
||||
>
|
||||
{copiedId === m.id + '-key' ? <Check size={14} /> : <Copy size={14} />}
|
||||
@@ -199,7 +301,7 @@ export default function MerchantsPage() {
|
||||
{/* Webhook Section */}
|
||||
<div className="p-4 bg-gray-50 rounded-2xl space-y-2 border border-transparent hover:border-gray-100 transition-colors">
|
||||
<label className="text-[10px] font-black text-gray-400 uppercase tracking-widest pl-1">Webhook URL</label>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-bold text-gray-600 truncate px-1">{m.webhook_url || 'Ayarlanmamış'}</span>
|
||||
<Globe size={14} className="text-gray-300 shrink-0" />
|
||||
</div>
|
||||
@@ -268,7 +370,7 @@ export default function MerchantsPage() {
|
||||
</div>
|
||||
|
||||
{/* Edit Modal */}
|
||||
{isEditModalOpen && (
|
||||
{showEditModal && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-6 bg-gray-900/60 backdrop-blur-sm animate-in fade-in duration-300">
|
||||
<div className="bg-white w-full max-w-lg rounded-[40px] shadow-2xl overflow-hidden animate-in zoom-in-95 duration-300">
|
||||
<div className="p-10">
|
||||
@@ -278,7 +380,7 @@ export default function MerchantsPage() {
|
||||
<p className="text-xs text-gray-400 font-bold uppercase tracking-widest mt-2 px-1">Firma bilgilerini güncelle</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsEditModalOpen(false)}
|
||||
onClick={() => setShowEditModal(false)}
|
||||
className="p-3 hover:bg-gray-50 rounded-2xl text-gray-400 transition"
|
||||
>
|
||||
<X size={24} />
|
||||
@@ -294,38 +396,14 @@ export default function MerchantsPage() {
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={editingMerchant?.name || ''}
|
||||
onChange={(e) => setEditingMerchant({ ...editingMerchant, name: e.target.value })}
|
||||
value={editForm.name}
|
||||
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
|
||||
placeholder="Örn: Ayris Teknoloji"
|
||||
className="w-full pl-14 pr-6 py-4 bg-gray-50 border-2 border-transparent focus:border-blue-500 focus:bg-white rounded-[24px] outline-none transition-all font-bold text-gray-900 placeholder:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] ml-1">Ödeme Altyapısı (Gateway)</label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{[
|
||||
{ id: 'stripe', name: 'Stripe' },
|
||||
{ id: 'cryptomus', name: 'Cryptomus' },
|
||||
{ id: 'nuvei', name: 'Nuvei' },
|
||||
{ id: 'paykings', name: 'PayKings' },
|
||||
{ id: 'securionpay', name: 'SecurionPay' },
|
||||
].map((p) => (
|
||||
<button
|
||||
key={p.id}
|
||||
type="button"
|
||||
onClick={() => setEditingMerchant({ ...editingMerchant, payment_provider: p.id })}
|
||||
className={`px-4 py-3 rounded-xl border text-xs font-bold transition-all ${editingMerchant?.payment_provider === p.id
|
||||
? 'border-blue-500 bg-blue-50 text-blue-600'
|
||||
: 'border-gray-100 bg-gray-50 text-gray-400 hover:bg-gray-100'}`}
|
||||
>
|
||||
{p.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] ml-1">Özel Komisyon (%)</label>
|
||||
<div className="relative">
|
||||
@@ -335,35 +413,60 @@ export default function MerchantsPage() {
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="100"
|
||||
value={editingMerchant?.fee_percent || '1.0'}
|
||||
onChange={(e) => setEditingMerchant({ ...editingMerchant, fee_percent: e.target.value })}
|
||||
value={editForm.fee_percent}
|
||||
onChange={(e) => setEditForm({ ...editForm, fee_percent: e.target.value })}
|
||||
placeholder="1.0"
|
||||
className="w-full pl-6 pr-12 py-4 bg-gray-50 border-2 border-transparent focus:border-blue-500 focus:bg-white rounded-[24px] outline-none transition-all font-bold text-gray-900 placeholder:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-400 font-medium px-1">Bu firmaya özel kesinti oranı. Boş bırakılırsa sistem varsayılanı kullanılır.</p>
|
||||
<p className="text-[10px] text-gray-400 font-medium px-1">Kesinti oranı. Boş bırakılırsa sistem varsayılanı kullanılır.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] ml-1">Webhook URL (Geri Dönüş)</label>
|
||||
<label className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] ml-1">Webhook URL</label>
|
||||
<div className="relative">
|
||||
<Globe className="absolute left-5 top-1/2 -translate-y-1/2 text-gray-300" size={20} />
|
||||
<input
|
||||
type="url"
|
||||
value={editingMerchant?.webhook_url || ''}
|
||||
onChange={(e) => setEditingMerchant({ ...editingMerchant, webhook_url: e.target.value })}
|
||||
value={editForm.webhook_url}
|
||||
onChange={(e) => setEditForm({ ...editForm, webhook_url: e.target.value })}
|
||||
placeholder="https://siteniz.com/webhook"
|
||||
className="w-full pl-14 pr-6 py-4 bg-gray-50 border-2 border-transparent focus:border-blue-500 focus:bg-white rounded-[24px] outline-none transition-all font-bold text-gray-900 placeholder:text-gray-300"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-400 font-medium px-1">Ödeme başarılı olduğunda bu adrese bildirim gönderilecektir.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="text-[10px] font-black text-gray-400 uppercase tracking-[0.2em] ml-1">Ödeme Adresleri (Ağ Bazlı)</label>
|
||||
<p className="text-[9px] text-gray-400 italic px-1 -mt-1">Her ağ için payout yapılacak cüzdan adresini girin.</p>
|
||||
|
||||
{[
|
||||
{ key: 'EVM', label: 'EVM (Polygon/BSC/ETH)', placeholder: '0x...', icon: '🟣' },
|
||||
{ key: 'SOLANA', label: 'Solana', placeholder: '5pLH...veya base58 adres', icon: '🟢' },
|
||||
{ key: 'TRON', label: 'TRON', placeholder: 'T...', icon: '🔴' },
|
||||
{ key: 'BITCOIN', label: 'Bitcoin', placeholder: 'bc1q...', icon: '🟠' }
|
||||
].map(net => (
|
||||
<div key={net.key} className="relative">
|
||||
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-base">{net.icon}</span>
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.payout_addresses?.[net.key] || ''}
|
||||
onChange={(e) => setEditForm({
|
||||
...editForm,
|
||||
payout_addresses: { ...editForm.payout_addresses, [net.key]: e.target.value }
|
||||
})}
|
||||
className="w-full pl-12 pr-6 py-3 bg-gray-50 border-2 border-transparent focus:border-blue-500 focus:bg-white rounded-2xl outline-none transition-all font-mono text-xs text-gray-900 placeholder:text-gray-300"
|
||||
placeholder={`${net.label}: ${net.placeholder}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsEditModalOpen(false)}
|
||||
onClick={() => setShowEditModal(false)}
|
||||
className="flex-1 py-4 text-gray-400 font-black text-sm uppercase tracking-widest hover:text-gray-600 transition"
|
||||
>
|
||||
İptal
|
||||
@@ -381,6 +484,182 @@ export default function MerchantsPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Payout Modal */}
|
||||
{showPayoutModal && (
|
||||
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-[40px] w-full max-w-lg max-h-[90vh] overflow-y-auto shadow-2xl animate-in zoom-in duration-300">
|
||||
<div className="bg-emerald-600 p-8 text-white relative">
|
||||
<button onClick={() => setShowPayoutModal(false)} className="absolute top-6 right-6 p-2 hover:bg-white/10 rounded-full transition">
|
||||
<X size={24} />
|
||||
</button>
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="w-16 min-w-[64px] h-16 bg-white/10 rounded-3xl flex items-center justify-center">
|
||||
<Wallet size={32} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-black">Ödeme Gönder</h3>
|
||||
<p className="text-xs text-white/60 font-bold uppercase tracking-widest">Treasury'den Merchant Cüzdanına Transfer</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleProcessPayout} className="p-8 space-y-6">
|
||||
{(() => {
|
||||
const bal = merchantBalances.find(b => b.network === payoutNetwork && b.token === payoutCurrency);
|
||||
const totalGross = bal ? bal.totalGross : 0;
|
||||
const totalNet = bal ? bal.balance : 0;
|
||||
const totalFee = totalGross - totalNet;
|
||||
const withdrawn = bal ? bal.withdrawn : 0;
|
||||
const available = totalNet - withdrawn;
|
||||
|
||||
return (
|
||||
<div className="bg-emerald-50 p-6 rounded-3xl border border-emerald-100 space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-[10px] font-black text-emerald-600 uppercase tracking-widest">Alıcı Firma</p>
|
||||
<p className="text-sm font-bold text-emerald-700">{selectedMerchant?.name}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 pt-2 border-t border-emerald-200/30">
|
||||
<div>
|
||||
<p className="text-[9px] font-black text-emerald-600/60 uppercase tracking-tight">Top. Brüt (Hazine)</p>
|
||||
<p className="text-xs font-black text-emerald-800 tabular-nums">{totalGross.toFixed(4)} {payoutCurrency}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-[9px] font-black text-emerald-600/60 uppercase tracking-tight">Platform Fee (%{merchantFeePercent})</p>
|
||||
<p className="text-xs font-black text-orange-600 tabular-nums">-{totalFee.toFixed(4)} {payoutCurrency}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-emerald-200/50">
|
||||
<p className="text-[10px] font-black text-emerald-600 uppercase tracking-widest mb-1">Net Hak Ediş (Çekilebilir)</p>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<p className="text-3xl font-black text-emerald-900 tabular-nums">{available.toFixed(6)}</p>
|
||||
<p className="text-sm font-black text-emerald-700">{payoutCurrency}</p>
|
||||
</div>
|
||||
{withdrawn > 0 && (
|
||||
<p className="text-[9px] text-emerald-600/60 font-bold mt-1">Daha önce {withdrawn.toFixed(4)} {payoutCurrency} çekildi.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{merchantBalances.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 pt-2 border-t border-emerald-200/30">
|
||||
{merchantBalances.filter(b => b.available > 0).map((b, i) => (
|
||||
<span key={i} className={`px-2 py-0.5 rounded-lg text-[8px] font-bold uppercase tabular-nums border cursor-pointer transition-all ${b.network === payoutNetwork && b.token === payoutCurrency ? 'bg-emerald-600 text-white border-emerald-600 shadow-md' : 'bg-white text-emerald-700 border-emerald-100 hover:border-emerald-300'}`}
|
||||
onClick={() => { setPayoutNetwork(b.network); setPayoutCurrency(b.token); }}
|
||||
>
|
||||
{b.network}/{b.token}: {b.available.toFixed(4)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Simplified Selection Indicator */}
|
||||
<div className="p-4 bg-gray-50 rounded-2xl border border-gray-100 mb-4">
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest leading-none mb-2">Seçili Ödeme Hattı</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-white rounded-xl flex items-center justify-center text-lg shadow-sm border border-gray-100">
|
||||
{payoutNetwork === 'SOLANA' ? '🟢' : payoutNetwork === 'TRON' ? '🔴' : payoutNetwork === 'BITCOIN' ? '🟠' : '🟣'}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-black text-gray-900">{payoutNetwork}</p>
|
||||
<p className="text-[10px] font-bold text-blue-600 uppercase tracking-tight">{payoutCurrency}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[9px] font-black text-gray-300 uppercase italic">Yukarıdaki chiplerden seçin</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[10px] font-black text-gray-400 uppercase tracking-widest mb-2 pl-1">
|
||||
Gönderilecek {payoutCurrency} Miktarı
|
||||
</label>
|
||||
{(() => {
|
||||
const bal = merchantBalances.find(b => b.network === payoutNetwork && b.token === payoutCurrency);
|
||||
const maxAvailable = bal ? bal.available : 0;
|
||||
const isOverspend = parseFloat(payoutAmount || '0') > maxAvailable;
|
||||
return (
|
||||
<>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="number"
|
||||
step="0.00000001"
|
||||
value={payoutAmount}
|
||||
onChange={(e) => setPayoutAmount(e.target.value)}
|
||||
className={`w-full pl-5 pr-24 py-4 bg-gray-50 border rounded-2xl focus:ring-2 focus:ring-emerald-500 transition outline-none font-black text-lg ${isOverspend ? 'border-red-400 bg-red-50' : 'border-gray-100'}`}
|
||||
placeholder="0.00"
|
||||
required
|
||||
/>
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-2">
|
||||
{maxAvailable > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPayoutAmount(maxAvailable.toString())}
|
||||
className="px-2 py-1 bg-emerald-100 text-emerald-700 rounded-lg text-[9px] font-black uppercase hover:bg-emerald-200 transition"
|
||||
>
|
||||
MAX
|
||||
</button>
|
||||
)}
|
||||
<span className="font-black text-gray-300 uppercase text-sm">{payoutCurrency}</span>
|
||||
</div>
|
||||
</div>
|
||||
{isOverspend && (
|
||||
<p className="text-[9px] text-red-500 font-black mt-1 px-1">⚠️ Girilen miktar firma bakiyesini ({maxAvailable.toFixed(6)} {payoutCurrency}) aşıyor!</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
<p className="text-[9px] text-gray-400 mt-1 italic px-1">Treasury cüzdanından düşülüp merchant adresine kripto olarak iletilecek.</p>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-blue-50 rounded-2xl border border-blue-100 flex gap-3">
|
||||
<ShieldCheck className="text-blue-600 shrink-0" size={20} />
|
||||
<p className="text-[10px] text-blue-700 font-bold leading-relaxed">
|
||||
Bu işlem kalıcıdır. Payout adresi ({payoutNetwork}):
|
||||
<span className="font-mono text-[10px] block mt-1 break-all">
|
||||
{(() => {
|
||||
const netKey = ['POLYGON', 'BSC', 'ETH'].includes(payoutNetwork) ? 'EVM' : payoutNetwork;
|
||||
const addr = selectedMerchant?.payout_addresses?.[netKey];
|
||||
return addr || <span className="text-red-500 font-black">ADRES EKSİK! Lütfen firma ayarlarından {netKey} adresini girin.</span>;
|
||||
})()}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting || (() => {
|
||||
const bal = merchantBalances.find(b => b.network === payoutNetwork && b.token === payoutCurrency);
|
||||
const maxAvailable = bal ? bal.available : 0;
|
||||
return parseFloat(payoutAmount || '0') > maxAvailable || parseFloat(payoutAmount || '0') <= 0;
|
||||
})() || !(() => {
|
||||
const netKey = ['POLYGON', 'BSC', 'ETH'].includes(payoutNetwork) ? 'EVM' : payoutNetwork;
|
||||
return selectedMerchant?.payout_addresses?.[netKey];
|
||||
})()}
|
||||
className="w-full py-5 bg-gray-900 text-white rounded-2xl text-xs font-black uppercase tracking-[0.2em] hover:bg-black transition shadow-xl disabled:opacity-50 flex items-center justify-center gap-3"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="animate-spin" size={18} />
|
||||
İşleniyor...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Ödemeyi Onayla & Gönder
|
||||
<ArrowRight size={18} />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { tr } from 'date-fns/locale';
|
||||
import Link from 'next/link';
|
||||
import TransactionChart from '@/components/admin/TransactionChart';
|
||||
import QueryRangeSelector from '@/components/admin/QueryRangeSelector';
|
||||
import PlatformTreasuryWidget from '@/components/admin/PlatformTreasuryWidget';
|
||||
|
||||
async function getStats(rangeDays: number = 30) {
|
||||
const result = await db.query('SELECT * FROM transactions ORDER BY created_at DESC');
|
||||
@@ -143,6 +144,9 @@ export default async function AdminDashboard(props: {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Platform Treasury Status - High Visibility */}
|
||||
<PlatformTreasuryWidget />
|
||||
|
||||
{/* Middle Section: Charts */}
|
||||
<div className="bg-white p-8 rounded-3xl border border-gray-100 shadow-sm">
|
||||
<div className="flex justify-between items-center mb-10">
|
||||
|
||||
172
app/admin/payouts/page.tsx
Normal file
172
app/admin/payouts/page.tsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import React from 'react';
|
||||
import { db } from '@/lib/db';
|
||||
import {
|
||||
Search,
|
||||
ExternalLink,
|
||||
Clock,
|
||||
Wallet,
|
||||
Link as LinkIcon,
|
||||
ChevronRight,
|
||||
ArrowUpRight
|
||||
} from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { tr } from 'date-fns/locale';
|
||||
|
||||
async function getPayouts() {
|
||||
try {
|
||||
const { rows } = await db.query(`
|
||||
SELECT p.*, m.name as merchant_name
|
||||
FROM payouts p
|
||||
LEFT JOIN merchants m ON p.merchant_id = m.id
|
||||
ORDER BY p.created_at DESC
|
||||
`);
|
||||
return rows;
|
||||
} catch (error) {
|
||||
console.error('Payouts fetch error:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export default async function PayoutsPage() {
|
||||
const payouts = await getPayouts();
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
||||
{/* Header Area */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-black text-gray-900 tracking-tight">Ödemeler (Payouts)</h1>
|
||||
<p className="text-gray-500 font-medium mt-1 uppercase tracking-widest text-[10px]">Merchantlara yapılan kripto ödeme geçmişi</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Overview (Optional) */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="bg-white p-6 rounded-[32px] border border-gray-100 shadow-sm flex items-center gap-5">
|
||||
<div className="w-12 h-12 bg-emerald-50 rounded-2xl flex items-center justify-center text-emerald-600">
|
||||
<ArrowUpRight size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Toplam Ödeme</p>
|
||||
<p className="text-xl font-black text-gray-900">{payouts.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-[32px] border border-gray-100 shadow-sm flex items-center gap-5">
|
||||
<div className="w-12 h-12 bg-blue-50 rounded-2xl flex items-center justify-center text-blue-600">
|
||||
<Wallet size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Son İşlem</p>
|
||||
<p className="text-xl font-black text-gray-900">
|
||||
{payouts.length > 0 ? format(new Date(payouts[0].created_at), 'dd MMM', { locale: tr }) : '-'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-[32px] border border-gray-100 shadow-sm flex items-center gap-5">
|
||||
<div className="w-12 h-12 bg-orange-50 rounded-2xl flex items-center justify-center text-orange-600">
|
||||
<Clock size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Durum</p>
|
||||
<p className="text-xl font-black text-gray-900">Aktif</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payouts Table */}
|
||||
<div className="bg-white rounded-[40px] border border-gray-100 shadow-sm overflow-hidden text-sans tracking-tight">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left">
|
||||
<thead>
|
||||
<tr className="bg-gray-50/30 text-gray-400 text-[10px] font-black uppercase tracking-[0.2em] border-b border-gray-50">
|
||||
<th className="px-10 py-6">Merchant</th>
|
||||
<th className="px-10 py-6">Miktar & Varlık</th>
|
||||
<th className="px-10 py-6">Ağ (Network)</th>
|
||||
<th className="px-10 py-6">Hedef Adres</th>
|
||||
<th className="px-10 py-6">Tarih</th>
|
||||
<th className="px-10 py-6 text-center">Durum</th>
|
||||
<th className="px-10 py-6 text-right">Blockchain</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-50">
|
||||
{payouts.map((p: any) => (
|
||||
<tr key={p.id} className="group hover:bg-gray-50/50 transition-colors">
|
||||
<td className="px-10 py-8">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-black text-blue-600 uppercase tracking-wider">
|
||||
{p.merchant_name}
|
||||
</span>
|
||||
<span className="text-[9px] text-gray-400 font-bold mt-1">ID: {p.merchant_id.slice(0,8)}...</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-10 py-8 font-black text-gray-900">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">{Number(p.amount).toFixed(4)}</span>
|
||||
<span className="text-xs text-gray-400 uppercase">{p.currency}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-10 py-8">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`px-2 py-0.5 rounded-lg text-[10px] font-black uppercase tracking-wider ${
|
||||
p.network === 'SOLANA' ? 'bg-purple-50 text-purple-600' :
|
||||
p.network === 'TRON' ? 'bg-red-50 text-red-600' :
|
||||
'bg-blue-50 text-blue-600'
|
||||
}`}>
|
||||
{p.network}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-10 py-8">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-1.5 text-xs font-mono text-gray-500 bg-gray-50 px-3 py-1.5 rounded-xl w-fit">
|
||||
<Wallet size={12} className="text-gray-400" />
|
||||
{p.destination_address.slice(0, 8)}...{p.destination_address.slice(-8)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-10 py-8">
|
||||
<span className="text-xs font-bold text-gray-500">
|
||||
{format(new Date(p.created_at), 'dd MMM yyyy, HH:mm', { locale: tr })}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-10 py-8">
|
||||
<div className="flex justify-center">
|
||||
<span className={`inline-flex items-center px-4 py-1 rounded-full text-[10px] font-black uppercase tracking-wider ${p.status === 'succeeded' ? 'bg-emerald-50 text-emerald-600' : 'bg-red-50 text-red-600'}`}>
|
||||
{p.status === 'succeeded' ? 'Başarılı' : 'Hatalı'}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-10 py-8 text-right">
|
||||
{p.tx_hash && p.tx_hash !== 'mock' ? (
|
||||
<a
|
||||
href={p.network === 'SOLANA' ? `https://explorer.solana.com/tx/${p.tx_hash}` :
|
||||
p.network === 'TRON' ? `https://tronscan.org/#/transaction/${p.tx_hash}` :
|
||||
`https://polygonscan.com/tx/${p.tx_hash}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 text-blue-600 hover:text-blue-800 font-black text-[10px] uppercase tracking-widest transition-all p-2 bg-blue-50 rounded-xl"
|
||||
>
|
||||
TX GÖRÜNTÜLE
|
||||
<ExternalLink size={14} />
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-[10px] font-black text-gray-300 uppercase tracking-widest italic">Mock İşlem</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{payouts.length === 0 && (
|
||||
<div className="p-20 text-center space-y-4">
|
||||
<div className="w-16 h-16 bg-gray-50 rounded-full flex items-center justify-center mx-auto text-gray-300">
|
||||
<Wallet size={32} />
|
||||
</div>
|
||||
<p className="text-gray-400 font-bold uppercase tracking-widest text-xs">Henüz ödeme yapılmadı</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
app/api/admin/payouts/list/route.ts
Normal file
23
app/api/admin/payouts/list/route.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const result = await db.query(`
|
||||
SELECT
|
||||
p.*,
|
||||
m.name as merchant_name
|
||||
FROM payouts p
|
||||
LEFT JOIN merchants m ON p.merchant_id = m.id
|
||||
ORDER BY p.created_at DESC
|
||||
`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
payouts: result.rows
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[Payouts List API] Error:', error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
169
app/api/admin/payouts/route.ts
Normal file
169
app/api/admin/payouts/route.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { CryptoEngine } from '@/lib/crypto-engine';
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { merchantId, amount, network, currency } = await req.json();
|
||||
|
||||
if (!merchantId || !amount || !network) {
|
||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 1. Fetch Merchant details
|
||||
const merchantRes = await db.query('SELECT * FROM merchants WHERE id = $1', [merchantId]);
|
||||
const merchant = merchantRes.rows[0];
|
||||
|
||||
if (!merchant) {
|
||||
return NextResponse.json({ error: 'Merchant not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Resolve per-network payout address
|
||||
const netKey = ['POLYGON', 'BSC', 'ETH'].includes(network) ? 'EVM' : network;
|
||||
const payoutAddresses = merchant.payout_addresses || {};
|
||||
const destinationAddress = payoutAddresses[netKey] || merchant.payout_address;
|
||||
|
||||
if (!destinationAddress) {
|
||||
return NextResponse.json({ error: `Merchant payout address for ${netKey} is not set. Please configure it in merchant settings.` }, { status: 400 });
|
||||
}
|
||||
|
||||
const availableBalance = parseFloat(merchant.available_balance || '0');
|
||||
const payoutAmount = parseFloat(amount);
|
||||
|
||||
if (availableBalance < payoutAmount) {
|
||||
return NextResponse.json({ error: 'Insufficient balance' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 2. Get Treasury Private Key based on network
|
||||
const treasuryKeys: Record<string, string | undefined> = {
|
||||
'POLYGON': process.env.TREASURY_EVM_KEY,
|
||||
'BSC': process.env.TREASURY_EVM_KEY,
|
||||
'ETH': process.env.TREASURY_EVM_KEY,
|
||||
'SOLANA': process.env.TREASURY_SOL_KEY,
|
||||
'TRON': process.env.TREASURY_TRON_KEY,
|
||||
'BITCOIN': process.env.TREASURY_BTC_KEY
|
||||
};
|
||||
|
||||
const privateKey = treasuryKeys[network];
|
||||
if (!privateKey) {
|
||||
return NextResponse.json({ error: `Treasury private key for ${network} is not configured in .env` }, { status: 500 });
|
||||
}
|
||||
|
||||
// 3. Check merchant's crypto balance for this network/token
|
||||
const selectedCurrency = currency || 'SOL';
|
||||
|
||||
const balRes = await db.query(
|
||||
'SELECT balance, withdrawn FROM merchant_balances WHERE merchant_id = $1 AND network = $2 AND token = $3',
|
||||
[merchantId, network, selectedCurrency]
|
||||
);
|
||||
|
||||
const cryptoBalance = balRes.rows[0] ? parseFloat(balRes.rows[0].balance) : 0;
|
||||
const cryptoWithdrawn = balRes.rows[0] ? parseFloat(balRes.rows[0].withdrawn) : 0;
|
||||
const cryptoAvailable = cryptoBalance - cryptoWithdrawn;
|
||||
|
||||
if (cryptoAvailable < payoutAmount) {
|
||||
return NextResponse.json({
|
||||
error: `Yetersiz ${selectedCurrency} bakiyesi. Çekilebilir: ${cryptoAvailable.toFixed(6)} ${selectedCurrency}, talep edilen: ${payoutAmount} ${selectedCurrency}`
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
// 4. Execute Transfer
|
||||
const engine = new CryptoEngine(network);
|
||||
const transfer = await engine.sendPayout(
|
||||
privateKey,
|
||||
destinationAddress,
|
||||
amount.toString(),
|
||||
selectedCurrency
|
||||
);
|
||||
|
||||
if (!transfer.success) {
|
||||
return NextResponse.json({ error: transfer.error || 'Blockchain transfer failed' }, { status: 500 });
|
||||
}
|
||||
|
||||
// 5. Update Database
|
||||
await db.query('BEGIN');
|
||||
try {
|
||||
// Deduct from merchant's crypto balance
|
||||
await db.query(`
|
||||
UPDATE merchant_balances
|
||||
SET withdrawn = withdrawn + $1
|
||||
WHERE merchant_id = $2 AND network = $3 AND token = $4
|
||||
`, [payoutAmount, merchantId, network, selectedCurrency]);
|
||||
|
||||
// Also update legacy TRY balance (best effort price conversion)
|
||||
try {
|
||||
const coinIdMap: Record<string, string> = {
|
||||
'SOL': 'solana', 'USDC': 'usd-coin', 'USDT': 'tether',
|
||||
'TRX': 'tron', 'BTC': 'bitcoin', 'ETH': 'ethereum',
|
||||
'MATIC': 'matic-network', 'BNB': 'binancecoin'
|
||||
};
|
||||
const coinId = coinIdMap[selectedCurrency] || 'solana';
|
||||
const priceRes = await fetch(`https://api.coingecko.com/api/v3/simple/price?ids=${coinId}&vs_currencies=try`);
|
||||
const priceData = await priceRes.json();
|
||||
const tryEquivalent = payoutAmount * (priceData[coinId]?.try || 0);
|
||||
|
||||
if (tryEquivalent > 0) {
|
||||
await db.query(`
|
||||
UPDATE merchants
|
||||
SET available_balance = GREATEST(0, available_balance - $1),
|
||||
withdrawn_balance = withdrawn_balance + $1
|
||||
WHERE id = $2
|
||||
`, [tryEquivalent, merchantId]);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Payout] TRY balance update skipped (price fetch failed)');
|
||||
}
|
||||
|
||||
// Log the payout
|
||||
await db.query(`
|
||||
INSERT INTO payouts (merchant_id, amount, currency, network, destination_address, tx_hash, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
`, [
|
||||
merchantId,
|
||||
payoutAmount,
|
||||
selectedCurrency,
|
||||
network,
|
||||
destinationAddress,
|
||||
transfer.txHash,
|
||||
'succeeded'
|
||||
]);
|
||||
|
||||
await db.query('COMMIT');
|
||||
console.log(`[Payout] ✅ Sent ${payoutAmount} ${selectedCurrency} on ${network} to ${destinationAddress}`);
|
||||
} catch (dbErr) {
|
||||
await db.query('ROLLBACK');
|
||||
throw dbErr;
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
txHash: transfer.txHash,
|
||||
message: 'Payout processed successfully'
|
||||
});
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('Payout Error:', err);
|
||||
return NextResponse.json({ error: err.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const merchantId = searchParams.get('merchantId');
|
||||
|
||||
let query = 'SELECT * FROM payouts ORDER BY created_at DESC';
|
||||
let params: any[] = [];
|
||||
|
||||
if (merchantId) {
|
||||
query = 'SELECT * FROM payouts WHERE merchant_id = $1 ORDER BY created_at DESC';
|
||||
params = [merchantId];
|
||||
}
|
||||
|
||||
const result = await db.query(query, params);
|
||||
return NextResponse.json(result.rows);
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: err.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
72
app/api/admin/treasury/balances/route.ts
Normal file
72
app/api/admin/treasury/balances/route.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
import { CryptoEngine } from '@/lib/crypto-engine';
|
||||
import { ethers } from 'ethers';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// 1. Fetch platform addresses from settings
|
||||
const settingsRes = await db.query("SELECT key, value FROM system_settings WHERE key IN ('sol_platform_address', 'evm_platform_address', 'tron_platform_address', 'btc_platform_address')");
|
||||
const settings: Record<string, string> = {};
|
||||
settingsRes.rows.forEach(r => settings[r.key] = r.value);
|
||||
|
||||
const addresses = {
|
||||
EVM: settings.evm_platform_address || "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
|
||||
SOLANA: settings.sol_platform_address || "Ajr4nKieZJVu9q2d1eVF9pQPRCLoZ6v4tapB3iQn2SyQ",
|
||||
TRON: settings.tron_platform_address || "TY795B6FmDNV4Xm5U6G1rP9yvX7S9rK6G1P", // Valid TRON address format
|
||||
BITCOIN: settings.btc_platform_address || "17V95B6FmDNV4Xm5U6G1rP9yvX7S9rK6G1P" // Valid BTC address format
|
||||
};
|
||||
|
||||
// 2. Fetch balances for each network from config
|
||||
const cryptoConfig = require('@/lib/crypto-config.json');
|
||||
const balances: any = {};
|
||||
|
||||
for (const netConfig of cryptoConfig.networks) {
|
||||
const net = netConfig.id;
|
||||
console.log(`[Treasury] Fetching balances for ${net}...`);
|
||||
const engine = new CryptoEngine(net);
|
||||
const addr = (addresses as any)[['POLYGON', 'BSC', 'ETH'].includes(net) ? 'EVM' : net];
|
||||
|
||||
if (!addr) {
|
||||
console.warn(`[Treasury] No address found for network ${net}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const tokenBalances: Record<string, string> = {};
|
||||
let nativeBalance = "0.00";
|
||||
let nativeSymbol = net;
|
||||
|
||||
for (const token of netConfig.tokens) {
|
||||
const balance = await engine.getBalance(addr, token.symbol);
|
||||
if (token.address === 'NATIVE') {
|
||||
nativeBalance = balance;
|
||||
nativeSymbol = token.symbol;
|
||||
} else {
|
||||
tokenBalances[token.symbol] = balance;
|
||||
}
|
||||
}
|
||||
|
||||
balances[net] = {
|
||||
address: addr,
|
||||
native: nativeBalance,
|
||||
nativeSymbol: nativeSymbol,
|
||||
tokens: tokenBalances
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(`[Treasury Balance Error] ${net}:`, err);
|
||||
balances[net] = { address: addr, native: "Error", tokens: {} };
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
balances
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ export async function POST(request: Request) {
|
||||
const map: Record<string, string> = {};
|
||||
result.rows.forEach(r => map[r.key] = r.value);
|
||||
return {
|
||||
sol: map.sol_platform_address || process.env.SOL_PLATFORM_ADDRESS || "5pLH1tqZhx8p8WpZ18yr28N42KXB3FXVPzZ9ceCtpBVe",
|
||||
sol: map.sol_platform_address || process.env.SOL_PLATFORM_ADDRESS || "Ajr4nKieZJVu9q2d1eVF9pQPRCLoZ6v4tapB3iQn2SyQ",
|
||||
evm: map.evm_platform_address || process.env.EVM_PLATFORM_ADDRESS || "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
|
||||
tron: map.tron_platform_address || process.env.TRON_PLATFORM_ADDRESS || "TLYpfG6rre8Gv9m8pYjR7yvX7S9rK6G1P",
|
||||
btc: map.btc_platform_address || process.env.BTC_PLATFORM_ADDRESS || "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfJH",
|
||||
@@ -139,12 +139,35 @@ export async function POST(request: Request) {
|
||||
const feeAmount = (grossAmount * feePercent) / 100;
|
||||
const merchantNetCredit = grossAmount - feeAmount;
|
||||
|
||||
// 6.2 Update Merchant's virtual balance
|
||||
// 6.2 Calculate crypto credit after fee
|
||||
const cryptoAmount = parseFloat(expectedCryptoAmount);
|
||||
const cryptoFee = (cryptoAmount * feePercent) / 100;
|
||||
const cryptoNetCredit = cryptoAmount - cryptoFee;
|
||||
|
||||
// 6.3 Update Merchant's TRY balance (legacy)
|
||||
await db.query(`UPDATE merchants SET available_balance = available_balance + $1 WHERE id = $2`,
|
||||
[merchantNetCredit, transaction.merchant_id]);
|
||||
|
||||
// 6.3 Update transaction status
|
||||
await db.query(`UPDATE transactions SET status = 'succeeded' WHERE id = $1`, [transaction.id]);
|
||||
// 6.4 Update Merchant's per-network crypto balance
|
||||
await db.query(`
|
||||
INSERT INTO merchant_balances (merchant_id, network, token, balance, total_gross)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (merchant_id, network, token)
|
||||
DO UPDATE SET
|
||||
balance = merchant_balances.balance + $4,
|
||||
total_gross = merchant_balances.total_gross + $5
|
||||
`, [transaction.merchant_id, selectedNetwork, selectedToken, cryptoNetCredit, cryptoAmount]);
|
||||
|
||||
// 6.5 Update transaction status and recorded blockchain info
|
||||
await db.query(`
|
||||
UPDATE transactions
|
||||
SET status = 'succeeded',
|
||||
paid_network = $2,
|
||||
paid_token = $3,
|
||||
paid_amount_crypto = $4
|
||||
WHERE id = $1`,
|
||||
[transaction.id, selectedNetwork, selectedToken, expectedCryptoAmount]
|
||||
);
|
||||
|
||||
// 7. Automated Webhook Notification
|
||||
if (transaction.callback_url) {
|
||||
|
||||
34
app/api/merchants/[id]/balances/route.ts
Normal file
34
app/api/merchants/[id]/balances/route.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
export async function GET(
|
||||
req: NextRequest,
|
||||
context: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const { id } = await context.params;
|
||||
|
||||
const result = await db.query(
|
||||
'SELECT network, token, balance, withdrawn, total_gross FROM merchant_balances WHERE merchant_id = $1 ORDER BY network, token',
|
||||
[id]
|
||||
);
|
||||
|
||||
// Also get the merchant fee
|
||||
const merchantRes = await db.query('SELECT fee_percent FROM merchants WHERE id = $1', [id]);
|
||||
const feePercent = parseFloat(merchantRes.rows[0]?.fee_percent || '1.0');
|
||||
|
||||
return NextResponse.json({
|
||||
balances: result.rows.map(r => ({
|
||||
network: r.network,
|
||||
token: r.token,
|
||||
balance: parseFloat(r.balance),
|
||||
withdrawn: parseFloat(r.withdrawn),
|
||||
totalGross: parseFloat(r.total_gross || r.balance),
|
||||
available: parseFloat(r.balance) - parseFloat(r.withdrawn)
|
||||
})),
|
||||
feePercent
|
||||
});
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: err.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -29,19 +29,48 @@ export async function PATCH(
|
||||
) {
|
||||
try {
|
||||
const { id } = await context.params;
|
||||
const { name, webhook_url, payment_provider, provider_config, fee_percent } = await req.json();
|
||||
const body = await req.json();
|
||||
|
||||
if (!name) {
|
||||
if (!body.name) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Firma adı zorunludur.' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const result = await db.query(
|
||||
'UPDATE merchants SET name = $1, webhook_url = $2, payment_provider = $3, provider_config = $4, fee_percent = $5 WHERE id = $6 RETURNING *',
|
||||
[name, webhook_url, payment_provider, provider_config, fee_percent || 1.0, id]
|
||||
);
|
||||
// Build dynamic update
|
||||
const fields: string[] = [];
|
||||
const values: any[] = [];
|
||||
let idx = 1;
|
||||
|
||||
const addField = (col: string, val: any) => {
|
||||
if (val !== undefined) {
|
||||
fields.push(`${col} = $${idx++}`);
|
||||
values.push(val);
|
||||
}
|
||||
};
|
||||
|
||||
addField('name', body.name);
|
||||
addField('webhook_url', body.webhook_url);
|
||||
addField('fee_percent', body.fee_percent || 1.0);
|
||||
addField('payout_address', body.payout_address);
|
||||
|
||||
if (body.payout_addresses !== undefined) {
|
||||
fields.push(`payout_addresses = $${idx++}`);
|
||||
values.push(JSON.stringify(body.payout_addresses));
|
||||
}
|
||||
if (body.payment_provider !== undefined) {
|
||||
addField('payment_provider', body.payment_provider);
|
||||
}
|
||||
if (body.provider_config !== undefined) {
|
||||
fields.push(`provider_config = $${idx++}`);
|
||||
values.push(JSON.stringify(body.provider_config));
|
||||
}
|
||||
|
||||
values.push(id);
|
||||
const query = `UPDATE merchants SET ${fields.join(', ')} WHERE id = $${idx} RETURNING *`;
|
||||
|
||||
const result = await db.query(query, values);
|
||||
const data = result.rows[0];
|
||||
|
||||
if (!data) {
|
||||
@@ -50,6 +79,7 @@ export async function PATCH(
|
||||
|
||||
return NextResponse.json(data);
|
||||
} catch (err: any) {
|
||||
console.error('[Merchant PATCH Error]', err);
|
||||
return NextResponse.json(
|
||||
{ error: `Internal Server Error: ${err.message}` },
|
||||
{ status: 500 }
|
||||
|
||||
@@ -48,8 +48,37 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const result = await db.query('SELECT * FROM merchants ORDER BY created_at DESC');
|
||||
return NextResponse.json(result.rows);
|
||||
const merchantsResult = await db.query('SELECT * FROM merchants ORDER BY created_at DESC');
|
||||
const merchants = merchantsResult.rows;
|
||||
|
||||
// Fetch breakdown per merchant
|
||||
const breakdownResult = await db.query(`
|
||||
SELECT
|
||||
merchant_id,
|
||||
COALESCE(paid_network, 'SİSTEM') as network,
|
||||
COALESCE(paid_token, 'TRY') as token,
|
||||
SUM(COALESCE(paid_amount_crypto, amount)) as amount
|
||||
FROM transactions
|
||||
WHERE status = 'succeeded'
|
||||
GROUP BY merchant_id, paid_network, paid_token
|
||||
`);
|
||||
|
||||
const breakdowns = breakdownResult.rows.reduce((acc: any, row: any) => {
|
||||
if (!acc[row.merchant_id]) acc[row.merchant_id] = [];
|
||||
acc[row.merchant_id].push({
|
||||
network: row.network,
|
||||
token: row.token,
|
||||
amount: row.amount
|
||||
});
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const merchantsWithBreakdown = merchants.map(m => ({
|
||||
...m,
|
||||
balance_breakdown: breakdowns[m.id] || []
|
||||
}));
|
||||
|
||||
return NextResponse.json(merchantsWithBreakdown);
|
||||
} catch (err: any) {
|
||||
return NextResponse.json(
|
||||
{ error: `Internal Server Error: ${err.message}` },
|
||||
|
||||
131
components/admin/PlatformTreasuryWidget.tsx
Normal file
131
components/admin/PlatformTreasuryWidget.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Wallet, RefreshCw, Zap, TrendingUp, AlertCircle } from 'lucide-react';
|
||||
|
||||
export default function PlatformTreasuryWidget() {
|
||||
const [treasuryData, setTreasuryData] = useState<any>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
||||
|
||||
const fetchTreasury = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/admin/treasury/balances');
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
setTreasuryData(data.balances);
|
||||
setLastUpdated(new Date());
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Treasury Widget] Error:', e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchTreasury();
|
||||
}, []);
|
||||
|
||||
const networks = [
|
||||
{ id: 'SOLANA', color: 'text-emerald-500', bg: 'bg-emerald-50', icon: '🟢' },
|
||||
{ id: 'POLYGON', color: 'text-purple-500', bg: 'bg-purple-50', icon: '🟣' },
|
||||
{ id: 'TRON', color: 'text-red-500', bg: 'bg-red-50', icon: '🔴' },
|
||||
{ id: 'BSC', color: 'text-yellow-500', bg: 'bg-yellow-50', icon: '🟡' },
|
||||
{ id: 'ETH', color: 'text-blue-500', bg: 'bg-blue-50', icon: '🔵' },
|
||||
{ id: 'BITCOIN', color: 'text-orange-500', bg: 'bg-orange-50', icon: '🟠' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-[40px] border border-gray-100 shadow-sm overflow-hidden">
|
||||
<div className="p-8 border-b border-gray-50 flex justify-between items-center">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-gray-900 rounded-2xl flex items-center justify-center text-white shadow-xl">
|
||||
<Wallet size={24} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-black text-gray-900 leading-none">Platform Hazinesi</h2>
|
||||
<p className="text-[10px] text-gray-400 font-bold uppercase tracking-widest mt-2 flex items-center gap-1.5">
|
||||
<Zap size={10} className="text-emerald-500 fill-emerald-500" />
|
||||
Sistem Genelindeki On-Chain Likidite
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
{lastUpdated && (
|
||||
<span className="text-[9px] font-black text-gray-300 uppercase tracking-tighter">
|
||||
Son Güncelleme: {lastUpdated.toLocaleTimeString('tr-TR')}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={fetchTreasury}
|
||||
disabled={isLoading}
|
||||
className={`p-3 rounded-2xl border border-gray-100 text-gray-400 hover:text-gray-900 hover:border-gray-300 transition-all ${isLoading ? 'animate-spin border-transparent text-blue-600' : ''}`}
|
||||
>
|
||||
<RefreshCw size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-6">
|
||||
{networks.map((net) => {
|
||||
const data = treasuryData?.[net.id];
|
||||
const nativeSymbol = data?.nativeSymbol || net.id;
|
||||
const balance = data?.native || '0.00';
|
||||
const tokenList = data?.tokens ? Object.entries(data.tokens) : [];
|
||||
|
||||
return (
|
||||
<div key={net.id} className="group relative">
|
||||
<div className={`p-6 rounded-[32px] border border-gray-100 shadow-sm transition-all duration-300 hover:shadow-xl hover:-translate-y-1 ${net.bg}/20`}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className="text-2xl drop-shadow-sm">{net.icon}</span>
|
||||
<span className={`text-[10px] font-black uppercase tracking-widest px-2 py-0.5 rounded-lg bg-white/80 border ${net.color}`}>
|
||||
{net.id}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-xs font-bold text-gray-400 uppercase tracking-tighter leading-none">Ana Varlık</p>
|
||||
<div className="flex items-baseline gap-1.5 overflow-hidden">
|
||||
<h3 className={`text-xl font-black tabular-nums transition-all ${isLoading ? 'blur-sm' : ''} ${net.color}`}>
|
||||
{balance}
|
||||
</h3>
|
||||
<span className="text-[10px] font-black text-gray-400 uppercase">{nativeSymbol}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tokenList.length > 0 && (
|
||||
<div className="mt-4 pt-4 border-t border-gray-50 space-y-2">
|
||||
{tokenList.map(([symbol, bal]) => (
|
||||
<div key={symbol as string} className="flex justify-between items-center text-[10px] font-bold">
|
||||
<span className="text-gray-400 uppercase">{symbol as string}</span>
|
||||
<span className="text-gray-900 tabular-nums">{bal as string}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{balance === "Error" && (
|
||||
<div className="mt-2 text-[8px] font-black text-red-500 uppercase flex items-center gap-1">
|
||||
<AlertCircle size={10} /> Bağlantı Hatası
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 p-4 bg-blue-50/50 rounded-2xl border border-blue-100/50 flex items-center gap-3">
|
||||
<TrendingUp className="text-blue-600" size={18} />
|
||||
<p className="text-[10px] font-bold text-blue-800 leading-relaxed uppercase tracking-tight">
|
||||
Yukarıdaki bakiyeler platformun operasyonel cüzdanlarından (Platform Treasury) anlık olarak çekilir. Ödemeler bu likidite üzerinden karşılanmaktadır.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,57 +4,55 @@
|
||||
"id": "POLYGON",
|
||||
"name": "Polygon",
|
||||
"icon": "🟣",
|
||||
"rpc": "https://rpc.ankr.com/polygon",
|
||||
"rpc": "https://polygon-bor-rpc.publicnode.com",
|
||||
"chainId": 137,
|
||||
"tokens": [
|
||||
{ "symbol": "USDT", "address": "0xc2132D05D31C914a87C6611C10748AEb04B58e8F", "decimals": 6, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/usdt.png" },
|
||||
{ "symbol": "USDC", "address": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", "decimals": 6, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/usdc.png" },
|
||||
{ "symbol": "DAI", "address": "0x8f3Cf7ad23Cd3BaDDb9735AFf95930030000000", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/dai.png" },
|
||||
{ "symbol": "MATIC", "address": "NATIVE", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/matic.png" },
|
||||
{ "symbol": "WBTC", "address": "0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6", "decimals": 8, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/wbtc.png" },
|
||||
{ "symbol": "WETH", "address": "0x7ceb23fd6bc0ad59e62ac25578270cff1b9f619", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/eth.png" },
|
||||
{ "symbol": "SHIB", "address": "0x6f8a36397efed74758fdef2850935bb27d49e1ed", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/shib.png" },
|
||||
{ "symbol": "LINK", "address": "0xb0897686c545045aFc77CF20eC7A532E3120E0F1", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/link.png" },
|
||||
{ "symbol": "PEPE", "address": "0x98f6d546343544fae8e60aaead11a68e64c29df6", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/pepe.png" }
|
||||
{ "symbol": "USDT", "address": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", "decimals": 6, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/usdt.png" },
|
||||
{ "symbol": "USDC", "address": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", "decimals": 6, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/usdc.png" },
|
||||
{ "symbol": "DAI", "address": "0x8f3CF7aD23cD3cAddbf6764F01C21927E6AA0C45", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/dai.png" },
|
||||
{ "symbol": "WBTC", "address": "0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6", "decimals": 8, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/wbtc.png" },
|
||||
{ "symbol": "WETH", "address": "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/eth.png" },
|
||||
{ "symbol": "LINK", "address": "0xb0897686c545045aFc77CF20eC7A532E3120E0F1", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/link.png" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "BSC",
|
||||
"name": "BNB Chain",
|
||||
"icon": "🟡",
|
||||
"rpc": "https://rpc.ankr.com/bsc",
|
||||
"rpc": "https://bsc-rpc.publicnode.com",
|
||||
"chainId": 56,
|
||||
"tokens": [
|
||||
{ "symbol": "BNB", "address": "NATIVE", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/bnb.png" },
|
||||
{ "symbol": "USDT", "address": "0x55d398326f99059fF775485246999027B3197955", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/usdt.png" },
|
||||
{ "symbol": "USDC", "address": "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/usdc.png" },
|
||||
{ "symbol": "BNB", "address": "NATIVE", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/bnb.png" },
|
||||
{ "symbol": "BTCCB", "address": "0x7130d2a12b9bcbfae4f2634d864a1ee1ce3ead9c", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/btc.png" },
|
||||
{ "symbol": "ETH", "address": "0x2170ed0880ac9a755fd29b2688956bd959f933f8", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/eth.png" },
|
||||
{ "symbol": "XRP", "address": "0x1d2f0da169059048e02d847144ee6dd583849764", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/xrp.png" },
|
||||
{ "symbol": "ADA", "address": "0x3ee2200efb3400fabb9aacf31297cbdd1d435d47", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/ada.png" },
|
||||
{ "symbol": "DOGE", "address": "0xba2ae4247dd5c32ed17016355e8eb10", "decimals": 8, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/doge.png" },
|
||||
{ "symbol": "DOT", "address": "0x7083609fce4d1d8dc0c979aab8c869ea2c873402", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/dot.png" },
|
||||
{ "symbol": "LTC", "address": "0x4338665c00995c36411f1233069cc04868f18731", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/ltc.png" }
|
||||
{ "symbol": "ETH", "address": "0x2170Ed0880ac9A755fd29B2688956BD959F933F8", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/eth.png" },
|
||||
{ "symbol": "BTCB", "address": "0x7130d2A12B9BCbFAe4f2634d864A1Ee1Ce3Ead9c", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/btc.png" },
|
||||
{ "symbol": "XRP", "address": "0x1D2f0Da169CEb9fC7B3144828DB6519f14a02304", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/xrp.png" },
|
||||
{ "symbol": "DOGE", "address": "0xbA2aE424d960c26247Dd6c32edC70B295c744C43", "decimals": 8, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/doge.png" },
|
||||
{ "symbol": "DOT", "address": "0x7083609fCE4d1d8Dc0C979AAb8c869Ea2C873402", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/dot.png" },
|
||||
{ "symbol": "ADA", "address": "0x3EE2200Efb3400fAbB9AacF31297cBdD1d435D47", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/ada.png" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ETH",
|
||||
"name": "Ethereum",
|
||||
"icon": "🔵",
|
||||
"rpc": "https://rpc.ankr.com/eth",
|
||||
"rpc": "https://ethereum-rpc.publicnode.com",
|
||||
"chainId": 1,
|
||||
"tokens": [
|
||||
{ "symbol": "ETH", "address": "NATIVE", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/eth.png" },
|
||||
{ "symbol": "USDT", "address": "0xdAC17F958D2ee523a2206206994597C13D831ec7", "decimals": 6, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/usdt.png" },
|
||||
{ "symbol": "USDC", "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "decimals": 6, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/usdc.png" },
|
||||
{ "symbol": "DAI", "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/dai.png" },
|
||||
{ "symbol": "ETH", "address": "NATIVE", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/eth.png" },
|
||||
{ "symbol": "WBTC", "address": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", "decimals": 8, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/wbtc.png" },
|
||||
{ "symbol": "SHIB", "address": "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/shib.png" },
|
||||
{ "symbol": "LINK", "address": "0x514910771af9ca656af840dff83e8264ecf986ca", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/link.png" },
|
||||
{ "symbol": "UNI", "address": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/uni.png" },
|
||||
{ "symbol": "PEPE", "address": "0x6982508145454ce325ddbe47a25d4ec3d2311933", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/pepe.png" }
|
||||
{ "symbol": "WBTC", "address": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", "decimals": 8, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/wbtc.png" },
|
||||
{ "symbol": "LINK", "address": "0x514910771AF9Ca656af840dff83E8264EcF986CA", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/link.png" },
|
||||
{ "symbol": "UNI", "address": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", "decimals": 18, "logo": "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/128/color/uni.png" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "SOLANA",
|
||||
"name": "Solana (Testnet/Devnet)",
|
||||
"name": "Solana (Devnet)",
|
||||
"icon": "🟢",
|
||||
"rpc": "https://api.devnet.solana.com",
|
||||
"tokens": [
|
||||
|
||||
@@ -36,7 +36,13 @@ export class CryptoEngine {
|
||||
} else if (this.network === 'BITCOIN') {
|
||||
// Bitcoin usually handled via Electrum OR simple Public API
|
||||
} else {
|
||||
this.provider = new ethers.JsonRpcProvider(this.config.rpc);
|
||||
const options = this.config.chainId
|
||||
? { chainId: this.config.chainId, name: this.config.name.toLowerCase() }
|
||||
: undefined;
|
||||
|
||||
this.provider = new ethers.JsonRpcProvider(this.config.rpc, options, {
|
||||
staticNetwork: !!this.config.chainId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +104,107 @@ export class CryptoEngine {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a specific amount from treasury to a destination address (for payouts)
|
||||
*/
|
||||
async sendPayout(
|
||||
senderPrivateKey: string,
|
||||
destinationAddress: string,
|
||||
amount: string,
|
||||
tokenSymbol: string = 'SOL'
|
||||
): Promise<{ success: boolean; txHash: string | null; error?: string }> {
|
||||
console.log(`[Payout] Sending ${amount} ${tokenSymbol} on ${this.network} to ${destinationAddress}`);
|
||||
|
||||
if (this.network === 'SOLANA') {
|
||||
return this.sendSolanaPayout(senderPrivateKey, destinationAddress, amount, tokenSymbol);
|
||||
} else {
|
||||
// EVM / TRON / BTC - placeholder for now
|
||||
console.warn(`[Payout] Real transfer not yet implemented for ${this.network}. Using mock.`);
|
||||
return { success: true, txHash: `mock_${this.network}_${Date.now()}` };
|
||||
}
|
||||
}
|
||||
|
||||
private async sendSolanaPayout(
|
||||
senderPrivateKey: string,
|
||||
destinationAddress: string,
|
||||
amount: string,
|
||||
tokenSymbol: string
|
||||
): Promise<{ success: boolean; txHash: string | null; error?: string }> {
|
||||
try {
|
||||
// Decode private key (support both base64 and base58)
|
||||
let secretKey: Uint8Array;
|
||||
try {
|
||||
secretKey = Uint8Array.from(Buffer.from(senderPrivateKey, 'base64'));
|
||||
if (secretKey.length !== 64) throw new Error('not base64');
|
||||
} catch {
|
||||
secretKey = bs58.decode(senderPrivateKey);
|
||||
}
|
||||
|
||||
const senderKeypair = Keypair.fromSecretKey(secretKey);
|
||||
const destPubKey = new PublicKey(destinationAddress);
|
||||
const parsedAmount = parseFloat(amount);
|
||||
|
||||
console.log(`[Payout SOL] From: ${senderKeypair.publicKey.toBase58()} -> To: ${destinationAddress} | ${parsedAmount} ${tokenSymbol}`);
|
||||
|
||||
if (tokenSymbol === 'SOL' || tokenSymbol === 'NATIVE') {
|
||||
const lamports = Math.floor(parsedAmount * LAMPORTS_PER_SOL);
|
||||
|
||||
const balance = await this.solConnection.getBalance(senderKeypair.publicKey);
|
||||
if (balance < lamports + 5000) {
|
||||
return { success: false, txHash: null, error: `Insufficient SOL. Have: ${balance / LAMPORTS_PER_SOL}, Need: ${parsedAmount}` };
|
||||
}
|
||||
|
||||
const transaction = new Transaction().add(
|
||||
SystemProgram.transfer({
|
||||
fromPubkey: senderKeypair.publicKey,
|
||||
toPubkey: destPubKey,
|
||||
lamports
|
||||
})
|
||||
);
|
||||
|
||||
const { blockhash } = await this.solConnection.getLatestBlockhash();
|
||||
transaction.recentBlockhash = blockhash;
|
||||
transaction.feePayer = senderKeypair.publicKey;
|
||||
transaction.sign(senderKeypair);
|
||||
|
||||
const txHash = await this.solConnection.sendRawTransaction(transaction.serialize());
|
||||
await this.solConnection.confirmTransaction(txHash);
|
||||
|
||||
console.log(`[Payout SOL] ✅ Sent ${parsedAmount} SOL | TX: ${txHash}`);
|
||||
return { success: true, txHash };
|
||||
} else {
|
||||
// SPL token payout
|
||||
const tokenConfig = this.getTokenConfig(tokenSymbol);
|
||||
if (!tokenConfig) return { success: false, txHash: null, error: `Token ${tokenSymbol} not found` };
|
||||
|
||||
const { createTransferInstruction, getAssociatedTokenAddress: getATA } = require('@solana/spl-token');
|
||||
const mintPubKey = new PublicKey(tokenConfig.address);
|
||||
const senderATA = await getATA(mintPubKey, senderKeypair.publicKey);
|
||||
const destATA = await getATA(mintPubKey, destPubKey);
|
||||
|
||||
const tokenAmount = Math.floor(parsedAmount * Math.pow(10, tokenConfig.decimals));
|
||||
|
||||
const transaction = new Transaction().add(
|
||||
createTransferInstruction(senderATA, destATA, senderKeypair.publicKey, tokenAmount)
|
||||
);
|
||||
|
||||
const { blockhash } = await this.solConnection.getLatestBlockhash();
|
||||
transaction.recentBlockhash = blockhash;
|
||||
transaction.feePayer = senderKeypair.publicKey;
|
||||
transaction.sign(senderKeypair);
|
||||
|
||||
const txHash = await this.solConnection.sendRawTransaction(transaction.serialize());
|
||||
await this.solConnection.confirmTransaction(txHash);
|
||||
|
||||
console.log(`[Payout SOL] ✅ Sent ${parsedAmount} ${tokenSymbol} | TX: ${txHash}`);
|
||||
return { success: true, txHash };
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(`[Payout SOL] ❌ Error:`, e.message);
|
||||
return { success: false, txHash: null, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
private async sweepEVM(
|
||||
tempWalletPrivateKey: string,
|
||||
platformAddress: string,
|
||||
@@ -137,15 +244,95 @@ export class CryptoEngine {
|
||||
}
|
||||
|
||||
private async sweepSolana(
|
||||
tempWalletPrivateKey: string,
|
||||
platformAddress: string,
|
||||
senderPrivateKey: string,
|
||||
destinationAddress: string,
|
||||
tokenSymbol: string
|
||||
) {
|
||||
console.log(`[Sweep SOLANA] Sweeping 100% to Platform Treasury: ${platformAddress}`);
|
||||
return {
|
||||
success: true,
|
||||
txHash: 'sol_mock_tx_' + Math.random().toString(36).substring(7)
|
||||
};
|
||||
console.log(`[Sweep SOLANA] Sending ${tokenSymbol} to ${destinationAddress}`);
|
||||
|
||||
try {
|
||||
// Decode private key (support both base64 and base58)
|
||||
let secretKey: Uint8Array;
|
||||
try {
|
||||
secretKey = Uint8Array.from(Buffer.from(senderPrivateKey, 'base64'));
|
||||
if (secretKey.length !== 64) throw new Error('not base64');
|
||||
} catch {
|
||||
secretKey = bs58.decode(senderPrivateKey);
|
||||
}
|
||||
|
||||
const senderKeypair = Keypair.fromSecretKey(secretKey);
|
||||
const destPubKey = new PublicKey(destinationAddress);
|
||||
|
||||
if (tokenSymbol === 'SOL' || tokenSymbol === 'NATIVE') {
|
||||
// Get balance and send almost all (leave 5000 lamports for rent)
|
||||
const balance = await this.solConnection.getBalance(senderKeypair.publicKey);
|
||||
const sendAmount = balance - 10000; // leave 0.00001 SOL for fees
|
||||
|
||||
if (sendAmount <= 0) {
|
||||
return { success: false, txHash: null, error: 'Insufficient SOL balance' };
|
||||
}
|
||||
|
||||
const transaction = new Transaction().add(
|
||||
SystemProgram.transfer({
|
||||
fromPubkey: senderKeypair.publicKey,
|
||||
toPubkey: destPubKey,
|
||||
lamports: sendAmount
|
||||
})
|
||||
);
|
||||
|
||||
const { blockhash } = await this.solConnection.getLatestBlockhash();
|
||||
transaction.recentBlockhash = blockhash;
|
||||
transaction.feePayer = senderKeypair.publicKey;
|
||||
transaction.sign(senderKeypair);
|
||||
|
||||
const txHash = await this.solConnection.sendRawTransaction(transaction.serialize());
|
||||
await this.solConnection.confirmTransaction(txHash);
|
||||
|
||||
console.log(`[Sweep SOLANA] ✅ Sent ${sendAmount / LAMPORTS_PER_SOL} SOL | TX: ${txHash}`);
|
||||
return { success: true, txHash };
|
||||
} else {
|
||||
// SPL Token transfer (USDT, USDC) - requires ATA
|
||||
const tokenConfig = this.getTokenConfig(tokenSymbol);
|
||||
if (!tokenConfig) throw new Error(`Token ${tokenSymbol} not found`);
|
||||
|
||||
const { createTransferInstruction, getAssociatedTokenAddress: getATA, getOrCreateAssociatedTokenAccount } = require('@solana/spl-token');
|
||||
const mintPubKey = new PublicKey(tokenConfig.address);
|
||||
|
||||
const senderATA = await getATA(mintPubKey, senderKeypair.publicKey);
|
||||
const destATA = await getATA(mintPubKey, destPubKey);
|
||||
|
||||
// Check sender token balance
|
||||
try {
|
||||
const accountInfo = await getAccount(this.solConnection, senderATA);
|
||||
const tokenBalance = Number(accountInfo.amount);
|
||||
|
||||
if (tokenBalance <= 0) {
|
||||
return { success: false, txHash: null, error: `No ${tokenSymbol} balance` };
|
||||
}
|
||||
|
||||
const transaction = new Transaction().add(
|
||||
createTransferInstruction(senderATA, destATA, senderKeypair.publicKey, tokenBalance)
|
||||
);
|
||||
|
||||
const { blockhash } = await this.solConnection.getLatestBlockhash();
|
||||
transaction.recentBlockhash = blockhash;
|
||||
transaction.feePayer = senderKeypair.publicKey;
|
||||
transaction.sign(senderKeypair);
|
||||
|
||||
const txHash = await this.solConnection.sendRawTransaction(transaction.serialize());
|
||||
await this.solConnection.confirmTransaction(txHash);
|
||||
|
||||
console.log(`[Sweep SOLANA] ✅ Sent ${tokenBalance} ${tokenSymbol} | TX: ${txHash}`);
|
||||
return { success: true, txHash };
|
||||
} catch (e: any) {
|
||||
console.error(`[Sweep SOLANA] Token transfer error:`, e.message);
|
||||
return { success: false, txHash: null, error: e.message };
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(`[Sweep SOLANA] Error:`, e.message);
|
||||
return { success: false, txHash: null, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
private async sweepTron(
|
||||
@@ -172,6 +359,67 @@ export class CryptoEngine {
|
||||
};
|
||||
}
|
||||
|
||||
async getBalance(address: string, tokenSymbol: string = 'NATIVE'): Promise<string> {
|
||||
try {
|
||||
const tokenConfig = this.getTokenConfig(tokenSymbol);
|
||||
if (!tokenConfig) return "0.00";
|
||||
|
||||
if (this.network === 'SOLANA') {
|
||||
const pubKey = new PublicKey(address);
|
||||
if (tokenConfig.address === 'NATIVE') {
|
||||
const balance = await this.solConnection.getBalance(pubKey);
|
||||
return (balance / LAMPORTS_PER_SOL).toFixed(4);
|
||||
} else {
|
||||
const tokenMint = new PublicKey(tokenConfig.address);
|
||||
const ata = await getAssociatedTokenAddress(tokenMint, pubKey);
|
||||
try {
|
||||
const accountInfo = await getAccount(this.solConnection, ata);
|
||||
return (Number(accountInfo.amount) / Math.pow(10, tokenConfig.decimals)).toFixed(4);
|
||||
} catch (e) { return "0.00"; }
|
||||
}
|
||||
} else if (this.network === 'TRON') {
|
||||
try {
|
||||
if (tokenConfig.address === 'NATIVE') {
|
||||
const balance = await this.tronWeb.trx.getBalance(address);
|
||||
return (balance / 1000000).toFixed(2);
|
||||
} else {
|
||||
const contract = await this.tronWeb.contract().at(tokenConfig.address);
|
||||
const balance = await contract.balanceOf(address).call();
|
||||
return (Number(balance) / Math.pow(10, tokenConfig.decimals)).toFixed(2);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.warn(`[CryptoEngine] TRON balance check failed (likely API key or address issue):`, e.message);
|
||||
return "0.00";
|
||||
}
|
||||
} else if (this.network === 'BITCOIN') {
|
||||
try {
|
||||
const btcRes = await fetch(`https://blockchain.info/q/addressbalance/${address}`).then(r => r.text());
|
||||
return (parseInt(btcRes) / 1e8).toFixed(8);
|
||||
} catch (e) { return "0.00"; }
|
||||
} else {
|
||||
// EVM
|
||||
try {
|
||||
const safeAddress = ethers.getAddress(address);
|
||||
if (tokenConfig.address === 'NATIVE') {
|
||||
const balance = await this.provider.getBalance(safeAddress);
|
||||
return ethers.formatEther(balance);
|
||||
} else {
|
||||
const safeTokenAddr = ethers.getAddress(tokenConfig.address);
|
||||
const contract = new ethers.Contract(safeTokenAddr, ERC20_ABI, this.provider);
|
||||
const balance = await contract.balanceOf(safeAddress);
|
||||
return ethers.formatUnits(balance, tokenConfig.decimals);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.warn(`[CryptoEngine] EVM balance check failed for ${tokenSymbol}:`, e.message);
|
||||
return "0.00";
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(`[CryptoEngine] getBalance error for ${tokenSymbol}:`, e);
|
||||
return "0.00";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if a specific amount has arrived at the address.
|
||||
*/
|
||||
|
||||
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
Reference in New Issue
Block a user