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

187 lines
8.0 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 to get the temporary wallet private key
const result = await db.query('SELECT * FROM transactions WHERE 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 (EVM or SOLANA)
const walletType = selectedNetwork === 'SOLANA' ? 'SOLANA' : 'EVM';
const tempWalletConfig = wallets[walletType];
if (!tempWalletConfig || !tempWalletConfig.privateKey) {
return NextResponse.json({ success: false, error: `No temporary wallet found for ${walletType}` }, { status: 500 });
}
// 3. Define Platform Address & Fee (Fetch from dynamic settings)
const settings = await (async () => {
const result = await db.query('SELECT key, value FROM system_settings WHERE key IN (\'sol_platform_address\', \'evm_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 || "5pLH1tqZhx8p8WpZ18yr28N42KXB3FXVPzZ9ceCtpBVe",
evm: map.evm_platform_address || process.env.EVM_PLATFORM_ADDRESS || "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
fee: parseFloat(map.default_fee_percent || '1.0')
};
})();
const platformAddress = selectedNetwork === 'SOLANA' ? settings.sol : settings.evm;
// 4. Define Merchant Address (Fetch from transaction's merchant)
const merchantResult = await db.query('SELECT * FROM merchants WHERE id = $1', [transaction.merchant_id]);
const merchant = merchantResult.rows[0];
// Prioritize merchant's specific fee, fallback to global setting
const feePercent = merchant?.fee_percent !== undefined && merchant?.fee_percent !== null
? parseFloat(merchant.fee_percent)
: settings.fee;
let merchantAddress = merchant?.wallet_address || platformAddress;
// Use merchant's specific vault if available
if (selectedNetwork === 'SOLANA') {
if (merchant?.sol_vault_address) merchantAddress = merchant.sol_vault_address;
} else {
if (merchant?.evm_vault_address) merchantAddress = merchant.evm_vault_address;
}
console.log(`[Sweep] Destination for merchant: ${merchantAddress}`);
// 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();
// Always try to fetch price to ensure we are comparing crypto to crypto
try {
const coinIdMap: Record<string, string> = {
'SOL': 'solana',
'USDC': 'usd-coin',
'USDT': 'tether',
'TRX': 'tron',
'BTC': 'bitcoin'
};
const coinId = coinIdMap[selectedToken] || '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 = (transaction.currency || 'TRY').toLowerCase();
const price = priceData[coinId][currencyKey] || priceData[coinId]['usd'];
if (price) {
// Apply a small tolerance (e.g., 2% for price fluctuations)
const rawExpected = parseFloat(transaction.amount) / price;
expectedCryptoAmount = (rawExpected * 0.98).toFixed(6);
console.log(`[Sweep] Verified Amount: ${transaction.amount} ${transaction.currency} => Expected ~${rawExpected.toFixed(6)} ${selectedToken} (Threshold: ${expectedCryptoAmount})`);
}
} catch (priceErr) {
console.warn("[Sweep] Could not fetch real-time price, using raw amount as fallback:", priceErr);
}
const verification = await cryptoEngine.verifyPayment(
tempWalletConfig.address,
expectedCryptoAmount,
selectedToken
);
if (!verification.success) {
return NextResponse.json({
success: false,
error: `Henüz ödeme algılanmadı. Beklenen: ~${expectedCryptoAmount} ${selectedToken}`,
status: 'waiting'
});
}
// 6. Proceed to Sweep (100% to platform treasury)
const sweepResult = await cryptoEngine.sweepFunds(
tempWalletConfig.privateKey,
platformAddress,
selectedToken
);
if (!sweepResult.success) {
throw new Error("Süpürme işlemi başarısız oldu.");
}
// 6.1 Calculate Merchant's credit (Amount - Platform Fee)
// Note: transaction.amount is the gross amount paid by customer
const grossAmount = parseFloat(transaction.amount);
const feeAmount = (grossAmount * feePercent) / 100;
const merchantNetCredit = grossAmount - feeAmount;
// 6.2 Update Merchant's virtual balance in DB
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]);
// 7. Automated Webhook Notification
if (transaction.callback_url) {
console.log(`[Webhook] Notifying merchant at ${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: any) {
console.error("[Webhook Error]:", webhookError.message);
}
}
return NextResponse.json({
success: true,
message: `Ödeme ${selectedNetwork}ında ${selectedToken} ile başarıyla doğrulandı, hazineye aktarıldı ve merchant bakiyesi güncellendi.`,
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 });
}
}