Files
Pay2Gateway/app/api/crypto-sweep/route.ts

213 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { NextResponse } from 'next/server';
import { CryptoEngine } from '@/lib/crypto-engine';
import { db } from '@/lib/db';
export async function POST(request: Request) {
try {
const body = await request.json();
const { txId, network, token } = body;
if (!txId) {
return NextResponse.json({ success: false, error: "Transaction ID is required" }, { status: 400 });
}
const selectedNetwork = network || 'POLYGON';
const selectedToken = token || 'USDT';
console.log(`[API] Processing sweep for TX: ${txId} on ${selectedNetwork} with ${selectedToken}`);
// 1. Fetch the transaction from DB
// Search by either the internal UUID (id) or the provider ID (stripe_pi_id)
const result = await db.query('SELECT * FROM transactions WHERE id::text = $1 OR stripe_pi_id = $1', [txId]);
if (result.rows.length === 0) {
return NextResponse.json({ success: false, error: "Transaction not found" }, { status: 404 });
}
const transaction = result.rows[0];
// 1.1 Check if already processed
if (transaction.status === 'succeeded') {
return NextResponse.json({
success: true,
message: "Transaction already processed",
status: 'succeeded'
});
}
const metadata = transaction.metadata || {};
const wallets = metadata.wallets || {};
// 2. Determine which wallet to use
// In the database, metadata.wallets is an object: { EVM: {address, privateKey}, SOLANA: {...}, ... }
const tempWalletConfig = wallets[selectedNetwork] || wallets['EVM'];
if (!tempWalletConfig || (!tempWalletConfig.privateKey && !tempWalletConfig.address)) {
return NextResponse.json({ success: false, error: `No temporary wallet found for ${selectedNetwork}` }, { status: 500 });
}
const depositAddress = typeof tempWalletConfig === 'string' ? tempWalletConfig : tempWalletConfig.address;
const depositPrivateKey = typeof tempWalletConfig === 'string' ? null : tempWalletConfig.privateKey;
if (!depositPrivateKey) {
return NextResponse.json({ success: false, error: "Private key not found for sweep" }, { status: 500 });
}
// 3. Define Platform Addresses & Fee
const settings = await (async () => {
const result = 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 map: Record<string, string> = {};
result.rows.forEach(r => map[r.key] = r.value);
return {
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",
fee: parseFloat(map.default_fee_percent || '1.0')
};
})();
let platformAddress = settings.evm;
if (selectedNetwork === 'SOLANA') platformAddress = settings.sol;
else if (selectedNetwork === 'TRON') platformAddress = settings.tron;
else if (selectedNetwork === 'BITCOIN') platformAddress = settings.btc;
// 4. Define Merchant Address & Fee
const merchantResult = await db.query('SELECT * FROM merchants WHERE id = $1', [transaction.merchant_id]);
const merchant = merchantResult.rows[0];
const feePercent = merchant?.fee_percent !== undefined && merchant?.fee_percent !== null
? parseFloat(merchant.fee_percent)
: settings.fee;
// 5. Initialize Engine and Verify Payment first
const cryptoEngine = new CryptoEngine(selectedNetwork);
// 5.1 Convert Fiat amount to Crypto amount for verification
let expectedCryptoAmount = transaction.amount.toString();
try {
const symbolMap: Record<string, string> = {
'SOL': 'SOLUSDT', 'MATIC': 'MATICUSDT', 'POLYGON': 'MATICUSDT',
'USDC': 'USDCUSDT', 'USDT': 'USDTUSDT', 'TRX': 'TRXUSDT'
};
const pair = symbolMap[selectedToken] || 'SOLUSDT';
// 1. Get USD/TRY rate
const tryRes = await fetch(`https://api.binance.com/api/v3/ticker/price?symbol=USDTTRY`);
const tryData = await tryRes.json();
const usdTryPrice = parseFloat(tryData.price) || 32.5;
// 2. Get Crypto/USD rate
let cryptoUsdPrice = 1.0;
if (pair !== 'USDTUSDT' && pair !== 'USDCUSDT') {
const cryptoRes = await fetch(`https://api.binance.com/api/v3/ticker/price?symbol=${pair}`);
const cryptoData = await cryptoRes.json();
cryptoUsdPrice = parseFloat(cryptoData.price) || 1.0;
}
const priceInTry = cryptoUsdPrice * usdTryPrice;
const currency = (transaction.currency || 'TRY').toUpperCase();
const priceInTarget = currency === 'USD' ? cryptoUsdPrice : priceInTry;
if (priceInTarget > 0) {
const rawExpected = parseFloat(transaction.amount) / priceInTarget;
expectedCryptoAmount = (rawExpected * 0.98).toFixed(6);
console.log(`[Sweep API] Price for ${selectedToken}: ${priceInTarget.toFixed(2)} ${currency} | Expected: ${expectedCryptoAmount}`);
}
} catch (priceErr) {
console.warn("[Sweep API] Price fetch failed, using fallback rates.");
const staticRates: Record<string, number> = { 'SOL': 4000, 'USDT': 33, 'USDC': 33, 'TRX': 5, 'MATIC': 25 };
const rate = staticRates[selectedToken] || 1;
expectedCryptoAmount = (parseFloat(transaction.amount) / rate * 0.95).toFixed(6);
}
const verification = await cryptoEngine.verifyPayment(
depositAddress,
expectedCryptoAmount,
selectedToken
);
if (!verification.success) {
return NextResponse.json({
success: false,
error: `Ödeme henüz doğrulanmadı. Beklenen: ~${expectedCryptoAmount} ${selectedToken}`,
status: 'waiting'
});
}
// 6. Proceed to Sweep (100% to platform treasury)
const sweepResult = await cryptoEngine.sweepFunds(
depositPrivateKey,
platformAddress,
selectedToken
);
if (!sweepResult.success) {
throw new Error("Süpürme işlemi başarısız oldu.");
}
// 6.1 Calculate Merchant's credit
const grossAmount = parseFloat(transaction.amount);
const feeAmount = (grossAmount * feePercent) / 100;
const merchantNetCredit = grossAmount - feeAmount;
// 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.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) {
try {
fetch(transaction.callback_url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
status: 'success',
txId: transaction.stripe_pi_id,
orderRef: transaction.source_ref_id,
hashes: { txHash: sweepResult.txHash }
})
}).catch(e => console.error("Webhook fetch failed:", e.message));
} catch (webhookError) {}
}
return NextResponse.json({
success: true,
message: `Ödeme ${selectedNetwork}ında ${selectedToken} ile doğrulandı.`,
hashes: { txHash: sweepResult.txHash },
merchantCredit: merchantNetCredit
});
} catch (error: any) {
console.error('[API Error]:', error.message);
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
}
}