200 lines
9.0 KiB
TypeScript
200 lines
9.0 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
|
||
// 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 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 priceInCurrency = priceData[coinId][currencyKey] || priceData[coinId]['usd'];
|
||
|
||
if (priceInCurrency) {
|
||
// Apply a small tolerance (2%) and cross-convert if needed (simple assumption for demo)
|
||
const price = currencyKey === 'try' ? priceInCurrency : (priceInCurrency * 32.5); // Fallback conversion
|
||
const rawExpected = parseFloat(transaction.amount) / priceInCurrency;
|
||
expectedCryptoAmount = (rawExpected * 0.98).toFixed(6);
|
||
console.log(`[Sweep] ${transaction.amount} ${transaction.currency} => ~${rawExpected.toFixed(6)} ${selectedToken}`);
|
||
}
|
||
} catch (priceErr) {
|
||
console.warn("[Sweep] Price fetch failed, using order amount as fallback");
|
||
}
|
||
|
||
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} ağı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 });
|
||
}
|
||
}
|