From 641498957cb494bd87eaf0f4f71d3b810e07ebfc Mon Sep 17 00:00:00 2001 From: mstfyldz Date: Fri, 13 Mar 2026 05:17:04 +0300 Subject: [PATCH] feat: add Solana USDT/USDC support and refine admin payouts UI --- .gitignore | 1 + app/admin/layout.tsx | 4 +- app/admin/merchants/page.tsx | 423 ++++++++++++++++---- app/admin/page.tsx | 4 + app/admin/payouts/page.tsx | 172 ++++++++ app/api/admin/payouts/list/route.ts | 23 ++ app/api/admin/payouts/route.ts | 169 ++++++++ app/api/admin/treasury/balances/route.ts | 72 ++++ app/api/crypto-sweep/route.ts | 31 +- app/api/merchants/[id]/balances/route.ts | 34 ++ app/api/merchants/[id]/route.ts | 42 +- app/api/merchants/route.ts | 33 +- components/admin/PlatformTreasuryWidget.tsx | 131 ++++++ lib/crypto-config.json | 50 ++- lib/crypto-engine.ts | 264 +++++++++++- next-env.d.ts | 2 +- 16 files changed, 1335 insertions(+), 120 deletions(-) create mode 100644 app/admin/payouts/page.tsx create mode 100644 app/api/admin/payouts/list/route.ts create mode 100644 app/api/admin/payouts/route.ts create mode 100644 app/api/admin/treasury/balances/route.ts create mode 100644 app/api/merchants/[id]/balances/route.ts create mode 100644 components/admin/PlatformTreasuryWidget.tsx diff --git a/.gitignore b/.gitignore index 435f4a6..e04317c 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx index 8a48b82..85f6c98 100644 --- a/app/admin/layout.tsx +++ b/app/admin/layout.tsx @@ -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' }, diff --git a/app/admin/merchants/page.tsx b/app/admin/merchants/page.tsx index 2d7bf66..50efabb 100644 --- a/app/admin/merchants/page.tsx +++ b/app/admin/merchants/page.tsx @@ -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([]); const [isLoading, setIsLoading] = useState(true); + const [treasuryData, setTreasuryData] = useState(null); + const [isLoadingTreasury, setIsLoadingTreasury] = useState(true); const [copiedId, setCopiedId] = useState(null); - const [isEditModalOpen, setIsEditModalOpen] = useState(false); - const [editingMerchant, setEditingMerchant] = useState(null); + const [showEditModal, setShowEditModal] = useState(false); + const [showPayoutModal, setShowPayoutModal] = useState(false); + const [selectedMerchant, setSelectedMerchant] = useState(null); + const [payoutAmount, setPayoutAmount] = useState(''); + const [payoutNetwork, setPayoutNetwork] = useState('POLYGON'); + const [payoutCurrency, setPayoutCurrency] = useState('USDT'); + const [merchantBalances, setMerchantBalances] = useState([]); + const [merchantFeePercent, setMerchantFeePercent] = useState(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 + }); 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 (
{/* Header */} @@ -116,6 +194,7 @@ export default function MerchantsPage() {
+ {/* Merchant Cards Grid */}
{isLoading ? ( @@ -164,19 +243,42 @@ export default function MerchantsPage() {
{/* Balance Section */} -
-
-

İçerideki Bakiye

-

- {new Intl.NumberFormat('tr-TR', { style: 'currency', currency: 'TRY' }).format(m.available_balance || 0)} -

-
-
-

Toplam Ödenen

-

- {new Intl.NumberFormat('tr-TR', { style: 'currency', currency: 'TRY' }).format(m.withdrawn_balance || 0)} -

+
+
+
+

İçerideki Bakiye

+

+ {new Intl.NumberFormat('tr-TR', { style: 'currency', currency: 'TRY' }).format(Number(m.available_balance || 0))} +

+
+
+

Toplam Ödenen

+

+ {new Intl.NumberFormat('tr-TR', { style: 'currency', currency: 'TRY' }).format(Number(m.withdrawn_balance || 0))} +

+
+ + {m.balance_breakdown && m.balance_breakdown.length > 0 && ( +
+ {m.balance_breakdown.map((b: any, i: number) => ( +
+
+ + {Number(b.amount).toFixed(4)} {b.token} + +
+ ))} +
+ )} + +
@@ -188,7 +290,7 @@ export default function MerchantsPage() { {m.api_key || '••••••••••••••••'}
-
- -
- {[ - { id: 'stripe', name: 'Stripe' }, - { id: 'cryptomus', name: 'Cryptomus' }, - { id: 'nuvei', name: 'Nuvei' }, - { id: 'paykings', name: 'PayKings' }, - { id: 'securionpay', name: 'SecurionPay' }, - ].map((p) => ( - - ))} -
-
-
@@ -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" />
-

Bu firmaya özel kesinti oranı. Boş bırakılırsa sistem varsayılanı kullanılır.

+

Kesinti oranı. Boş bırakılırsa sistem varsayılanı kullanılır.

- +
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" />
-

Ödeme başarılı olduğunda bu adrese bildirim gönderilecektir.

+
+ +
+ +

Her ağ için payout yapılacak cüzdan adresini girin.

+ + {[ + { 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 => ( +
+ {net.icon} + 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}`} + /> +
+ ))}
)} + + {/* Payout Modal */} + {showPayoutModal && ( +
+
+
+ +
+
+ +
+
+

Ödeme Gönder

+

Treasury'den Merchant Cüzdanına Transfer

+
+
+
+ +
+ {(() => { + 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 ( +
+
+

Alıcı Firma

+

{selectedMerchant?.name}

+
+ +
+
+

Top. Brüt (Hazine)

+

{totalGross.toFixed(4)} {payoutCurrency}

+
+
+

Platform Fee (%{merchantFeePercent})

+

-{totalFee.toFixed(4)} {payoutCurrency}

+
+
+ +
+

Net Hak Ediş (Çekilebilir)

+
+

{available.toFixed(6)}

+

{payoutCurrency}

+
+ {withdrawn > 0 && ( +

Daha önce {withdrawn.toFixed(4)} {payoutCurrency} çekildi.

+ )} +
+ + {merchantBalances.length > 0 && ( +
+ {merchantBalances.filter(b => b.available > 0).map((b, i) => ( + { setPayoutNetwork(b.network); setPayoutCurrency(b.token); }} + > + {b.network}/{b.token}: {b.available.toFixed(4)} + + ))} +
+ )} +
+ ); + })()} + +
+ {/* Simplified Selection Indicator */} +
+

Seçili Ödeme Hattı

+
+
+
+ {payoutNetwork === 'SOLANA' ? '🟢' : payoutNetwork === 'TRON' ? '🔴' : payoutNetwork === 'BITCOIN' ? '🟠' : '🟣'} +
+
+

{payoutNetwork}

+

{payoutCurrency}

+
+
+ Yukarıdaki chiplerden seçin +
+
+ +
+ + {(() => { + const bal = merchantBalances.find(b => b.network === payoutNetwork && b.token === payoutCurrency); + const maxAvailable = bal ? bal.available : 0; + const isOverspend = parseFloat(payoutAmount || '0') > maxAvailable; + return ( + <> +
+ 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 + /> +
+ {maxAvailable > 0 && ( + + )} + {payoutCurrency} +
+
+ {isOverspend && ( +

⚠️ Girilen miktar firma bakiyesini ({maxAvailable.toFixed(6)} {payoutCurrency}) aşıyor!

+ )} + + ); + })()} +

Treasury cüzdanından düşülüp merchant adresine kripto olarak iletilecek.

+
+ +
+ +

+ Bu işlem kalıcıdır. Payout adresi ({payoutNetwork}): + + {(() => { + const netKey = ['POLYGON', 'BSC', 'ETH'].includes(payoutNetwork) ? 'EVM' : payoutNetwork; + const addr = selectedMerchant?.payout_addresses?.[netKey]; + return addr || ADRES EKSİK! Lütfen firma ayarlarından {netKey} adresini girin.; + })()} + +

+
+
+ + +
+
+
+ )} ); } diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 60fa72f..4d41c1d 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -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: { + {/* Platform Treasury Status - High Visibility */} + + {/* Middle Section: Charts */}
diff --git a/app/admin/payouts/page.tsx b/app/admin/payouts/page.tsx new file mode 100644 index 0000000..1397db8 --- /dev/null +++ b/app/admin/payouts/page.tsx @@ -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 ( +
+ {/* Header Area */} +
+
+

Ödemeler (Payouts)

+

Merchantlara yapılan kripto ödeme geçmişi

+
+
+ + {/* Stats Overview (Optional) */} +
+
+
+ +
+
+

Toplam Ödeme

+

{payouts.length}

+
+
+
+
+ +
+
+

Son İşlem

+

+ {payouts.length > 0 ? format(new Date(payouts[0].created_at), 'dd MMM', { locale: tr }) : '-'} +

+
+
+
+
+ +
+
+

Durum

+

Aktif

+
+
+
+ + {/* Payouts Table */} +
+
+ + + + + + + + + + + + + + {payouts.map((p: any) => ( + + + + + + + + + + ))} + +
MerchantMiktar & VarlıkAğ (Network)Hedef AdresTarihDurumBlockchain
+
+ + {p.merchant_name} + + ID: {p.merchant_id.slice(0,8)}... +
+
+
+ {Number(p.amount).toFixed(4)} + {p.currency} +
+
+
+ + {p.network} + +
+
+
+
+ + {p.destination_address.slice(0, 8)}...{p.destination_address.slice(-8)} +
+
+
+ + {format(new Date(p.created_at), 'dd MMM yyyy, HH:mm', { locale: tr })} + + +
+ + {p.status === 'succeeded' ? 'Başarılı' : 'Hatalı'} + +
+
+ {p.tx_hash && p.tx_hash !== 'mock' ? ( + + TX GÖRÜNTÜLE + + + ) : ( + Mock İşlem + )} +
+
+ {payouts.length === 0 && ( +
+
+ +
+

Henüz ödeme yapılmadı

+
+ )} +
+
+ ); +} diff --git a/app/api/admin/payouts/list/route.ts b/app/api/admin/payouts/list/route.ts new file mode 100644 index 0000000..23f8a6e --- /dev/null +++ b/app/api/admin/payouts/list/route.ts @@ -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 }); + } +} diff --git a/app/api/admin/payouts/route.ts b/app/api/admin/payouts/route.ts new file mode 100644 index 0000000..a66ff35 --- /dev/null +++ b/app/api/admin/payouts/route.ts @@ -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 = { + '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 = { + '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 }); + } +} diff --git a/app/api/admin/treasury/balances/route.ts b/app/api/admin/treasury/balances/route.ts new file mode 100644 index 0000000..5963d4e --- /dev/null +++ b/app/api/admin/treasury/balances/route.ts @@ -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 = {}; + 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 = {}; + 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 }); + } +} diff --git a/app/api/crypto-sweep/route.ts b/app/api/crypto-sweep/route.ts index 36c5133..67c5b1c 100644 --- a/app/api/crypto-sweep/route.ts +++ b/app/api/crypto-sweep/route.ts @@ -59,7 +59,7 @@ export async function POST(request: Request) { const map: Record = {}; 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) { diff --git a/app/api/merchants/[id]/balances/route.ts b/app/api/merchants/[id]/balances/route.ts new file mode 100644 index 0000000..45491fc --- /dev/null +++ b/app/api/merchants/[id]/balances/route.ts @@ -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 }); + } +} diff --git a/app/api/merchants/[id]/route.ts b/app/api/merchants/[id]/route.ts index e29616a..d173367 100644 --- a/app/api/merchants/[id]/route.ts +++ b/app/api/merchants/[id]/route.ts @@ -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 } diff --git a/app/api/merchants/route.ts b/app/api/merchants/route.ts index d5b32fa..735dad4 100644 --- a/app/api/merchants/route.ts +++ b/app/api/merchants/route.ts @@ -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}` }, diff --git a/components/admin/PlatformTreasuryWidget.tsx b/components/admin/PlatformTreasuryWidget.tsx new file mode 100644 index 0000000..8bf9c1d --- /dev/null +++ b/components/admin/PlatformTreasuryWidget.tsx @@ -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(null); + const [isLoading, setIsLoading] = useState(true); + const [lastUpdated, setLastUpdated] = useState(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 ( +
+
+
+
+ +
+
+

Platform Hazinesi

+

+ + Sistem Genelindeki On-Chain Likidite +

+
+
+ +
+ {lastUpdated && ( + + Son Güncelleme: {lastUpdated.toLocaleTimeString('tr-TR')} + + )} + +
+
+ +
+
+ {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 ( +
+
+
+ {net.icon} + + {net.id} + +
+ +
+

Ana Varlık

+
+

+ {balance} +

+ {nativeSymbol} +
+
+ + {tokenList.length > 0 && ( +
+ {tokenList.map(([symbol, bal]) => ( +
+ {symbol as string} + {bal as string} +
+ ))} +
+ )} + + {balance === "Error" && ( +
+ Bağlantı Hatası +
+ )} +
+
+ ); + })} +
+ +
+ +

+ Yukarıdaki bakiyeler platformun operasyonel cüzdanlarından (Platform Treasury) anlık olarak çekilir. Ödemeler bu likidite üzerinden karşılanmaktadır. +

+
+
+
+ ); +} diff --git a/lib/crypto-config.json b/lib/crypto-config.json index 5691be4..611bae8 100644 --- a/lib/crypto-config.json +++ b/lib/crypto-config.json @@ -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": [ diff --git a/lib/crypto-engine.ts b/lib/crypto-engine.ts index f68e43a..c6036f2 100644 --- a/lib/crypto-engine.ts +++ b/lib/crypto-engine.ts @@ -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 { + 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. */ diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c..c4b7818 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -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.