From 67d0c61adb76eccbff3996bfd21b5d235fc40cd6 Mon Sep 17 00:00:00 2001 From: mstfyldz Date: Fri, 13 Mar 2026 05:29:17 +0300 Subject: [PATCH] feat: implement background payment sync worker and admin trigger UI --- app/admin/transactions/page.tsx | 3 + app/api/admin/sync-payments/route.ts | 22 ++++ app/api/transactions/[id]/intent/route.ts | 30 +++++ check_schema.js | 16 +++ components/admin/SyncPaymentsButton.tsx | 60 +++++++++ components/checkout/CryptoCheckout.tsx | 23 ++++ lib/sync-worker.ts | 152 ++++++++++++++++++++++ 7 files changed, 306 insertions(+) create mode 100644 app/api/admin/sync-payments/route.ts create mode 100644 app/api/transactions/[id]/intent/route.ts create mode 100644 check_schema.js create mode 100644 components/admin/SyncPaymentsButton.tsx create mode 100644 lib/sync-worker.ts diff --git a/app/admin/transactions/page.tsx b/app/admin/transactions/page.tsx index 2a25456..8a3655a 100644 --- a/app/admin/transactions/page.tsx +++ b/app/admin/transactions/page.tsx @@ -11,6 +11,7 @@ import { format } from 'date-fns'; import { tr } from 'date-fns/locale'; import TransactionSearch from '@/components/admin/TransactionSearch'; import TransactionStatusFilter from '@/components/admin/TransactionStatusFilter'; +import SyncPaymentsButton from '@/components/admin/SyncPaymentsButton'; async function getTransactions(filters: { merchant_id?: string; q?: string; status?: string }) { let sql = ` @@ -71,6 +72,8 @@ export default async function TransactionsPage(props: {
+
+
+ + ); +} diff --git a/components/checkout/CryptoCheckout.tsx b/components/checkout/CryptoCheckout.tsx index 3062ece..5d3cc0c 100644 --- a/components/checkout/CryptoCheckout.tsx +++ b/components/checkout/CryptoCheckout.tsx @@ -108,6 +108,29 @@ export default function CryptoCheckout({ amount, currency, txId, wallets, onSucc setSelectedToken(network.tokens[0]); }; + // Save payment intent to DB so background sync knows what to check + useEffect(() => { + if (!txId || !selectedNetwork || !selectedToken) return; + + const saveIntent = async () => { + try { + await fetch(`/api/transactions/${txId}/intent`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + network: selectedNetwork.id, + token: selectedToken.symbol + }) + }); + } catch (err) { + console.error("Failed to save payment intent:", err); + } + }; + + const timer = setTimeout(saveIntent, 1000); // Debounce + return () => clearTimeout(timer); + }, [txId, selectedNetwork.id, selectedToken.symbol]); + const handleCopy = () => { navigator.clipboard.writeText(depositAddress); setCopied(true); diff --git a/lib/sync-worker.ts b/lib/sync-worker.ts new file mode 100644 index 0000000..2b4c5f2 --- /dev/null +++ b/lib/sync-worker.ts @@ -0,0 +1,152 @@ + +import { CryptoEngine } from './crypto-engine'; +import { db } from './db'; + +/** + * Background Sync Worker + * Checks blockchain for pending payments and completes them if detected. + */ +export async function syncPendingPayments() { + console.log("[SyncWorker] Starting manual sync..."); + + // 1. Fetch pending transactions that have an intent (or check all crypto transactions) + // We filter for transactions with statuses that indicate they are waiting for payment + // and have a provider type that suggests they are crypto (or just check metadata.wallets) + const result = await db.query(` + SELECT * FROM transactions + WHERE status IN ('waiting', 'pending') + AND (metadata->>'wallets' IS NOT NULL) + ORDER BY created_at DESC + LIMIT 20 + `); + + const pendingTxs = result.rows; + console.log(`[SyncWorker] Found ${pendingTxs.length} pending crypto transactions.`); + + const results = []; + + // 2. Fetch System Settings once + const settingsResult = await db.query('SELECT key, value FROM system_settings WHERE key IN (\'sol_platform_address\', \'evm_platform_address\', \'tron_platform_address\', \'btc_platform_address\', \'default_fee_percent\')'); + const systemMap: Record = {}; + settingsResult.rows.forEach(r => systemMap[r.key] = r.value); + + const sysSettings = { + sol: systemMap.sol_platform_address || process.env.SOL_PLATFORM_ADDRESS || "Ajr4nKieZJVu9q2d1eVF9pQPRCLoZ6v4tapB3iQn2SyQ", + evm: systemMap.evm_platform_address || process.env.EVM_PLATFORM_ADDRESS || "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + tron: systemMap.tron_platform_address || process.env.TRON_PLATFORM_ADDRESS || "TLYpfG6rre8Gv9m8pYjR7yvX7S9rK6G1P", + btc: systemMap.btc_platform_address || process.env.BTC_PLATFORM_ADDRESS || "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfJH", + fee: parseFloat(systemMap.default_fee_percent || '1.0') + }; + + for (const tx of pendingTxs) { + try { + const metadata = tx.metadata || {}; + const intentNetwork = metadata.intent_network || 'POLYGON'; + const intentToken = metadata.intent_token || 'USDT'; + + console.log(`[SyncWorker] Syncing TX ${tx.id} | Intent: ${intentNetwork}/${intentToken}`); + + // Re-use logic from crypto-sweep but optimized for background + const wallets = metadata.wallets || {}; + const tempWalletConfig = wallets[intentNetwork] || wallets['EVM']; + + if (!tempWalletConfig) continue; + + const depositAddress = typeof tempWalletConfig === 'string' ? tempWalletConfig : tempWalletConfig.address; + const depositPrivateKey = tempWalletConfig.privateKey; + + if (!depositPrivateKey) continue; + + const cryptoEngine = new CryptoEngine(intentNetwork); + + // Get expected amount logic + let expectedCryptoAmount = tx.amount.toString(); + try { + const coinIdMap: Record = { + 'SOL': 'solana', 'USDC': 'usd-coin', 'USDT': 'tether', 'TRX': 'tron', 'BTC': 'bitcoin' + }; + const coinId = coinIdMap[intentToken] || 'solana'; + const priceUrl = `https://api.coingecko.com/api/v3/simple/price?ids=${coinId}&vs_currencies=usd,try`; + const priceRes = await fetch(priceUrl); + const priceData = await priceRes.json(); + const currencyKey = (tx.currency || 'TRY').toLowerCase(); + const priceInCurrency = priceData[coinId][currencyKey] || priceData[coinId]['usd']; + + if (priceInCurrency) { + const rawExpected = parseFloat(tx.amount) / priceInCurrency; + expectedCryptoAmount = (rawExpected * 0.98).toFixed(6); + } + } catch (e) {} + + // Verify + const verification = await cryptoEngine.verifyPayment(depositAddress, expectedCryptoAmount, intentToken); + + if (verification.success) { + console.log(`[SyncWorker] Payment DETECTED for TX ${tx.id}. Sweeping...`); + + let platformAddress = sysSettings.evm; + if (intentNetwork === 'SOLANA') platformAddress = sysSettings.sol; + else if (intentNetwork === 'TRON') platformAddress = sysSettings.tron; + else if (intentNetwork === 'BITCOIN') platformAddress = sysSettings.btc; + + // Sweep + const sweepResult = await cryptoEngine.sweepFunds(depositPrivateKey, platformAddress, intentToken); + + if (sweepResult.success) { + // Update Balances & Transaction (Same as crypto-sweep logic) + const merchantResult = await db.query('SELECT * FROM merchants WHERE id = $1', [tx.merchant_id]); + const merchant = merchantResult.rows[0]; + const feePercent = merchant?.fee_percent !== undefined && merchant?.fee_percent !== null ? parseFloat(merchant.fee_percent) : sysSettings.fee; + + const grossAmount = parseFloat(tx.amount); + const feeAmount = (grossAmount * feePercent) / 100; + const merchantNetCredit = grossAmount - feeAmount; + + const cryptoAmount = parseFloat(expectedCryptoAmount); + const cryptoFee = (cryptoAmount * feePercent) / 100; + const cryptoNetCredit = cryptoAmount - cryptoFee; + + // Execute DB updates in transaction if possible, or sequential + await db.query(`UPDATE merchants SET available_balance = available_balance + $1 WHERE id = $2`, [merchantNetCredit, tx.merchant_id]); + + 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 + `, [tx.merchant_id, intentNetwork, intentToken, cryptoNetCredit, cryptoAmount]); + + await db.query(` + UPDATE transactions + SET status = 'succeeded', paid_network = $2, paid_token = $3, paid_amount_crypto = $4 + WHERE id = $1`, [tx.id, intentNetwork, intentToken, expectedCryptoAmount]); + + // Webhook + if (tx.callback_url) { + fetch(tx.callback_url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + status: 'success', + txId: tx.stripe_pi_id, + orderRef: tx.source_ref_id, + hashes: { txHash: sweepResult.txHash } + }) + }).catch(() => {}); + } + + results.push({ id: tx.id, status: 'synced', txHash: sweepResult.txHash }); + } + } else { + results.push({ id: tx.id, status: 'no_payment' }); + } + } catch (err: any) { + console.error(`[SyncWorker] Error processing TX ${tx.id}:`, err.message); + results.push({ id: tx.id, status: 'error', error: err.message }); + } + } + + return results; +}