188 lines
8.1 KiB
TypeScript
188 lines
8.1 KiB
TypeScript
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 (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} ağı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 });
|
||
}
|
||
}
|