feat: add Solana USDT/USDC support and refine admin payouts UI

This commit is contained in:
mstfyldz
2026-03-13 05:17:04 +03:00
parent 5f0df83686
commit 641498957c
16 changed files with 1335 additions and 120 deletions

1
.gitignore vendored
View File

@@ -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

View File

@@ -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' },

View File

@@ -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 ( Bazlı)</label>
<p className="text-[9px] text-gray-400 italic px-1 -mt-1">Her 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>
);
}

View File

@@ -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
View 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"> (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>
);
}

View 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 });
}
}

View 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 });
}
}

View 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 });
}
}

View File

@@ -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) {

View 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 });
}
}

View File

@@ -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 }

View File

@@ -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}` },

View 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>
);
}

View File

@@ -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": [

View File

@@ -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
View File

@@ -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.