193 lines
10 KiB
TypeScript
193 lines
10 KiB
TypeScript
|
|
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<string, string> = {};
|
|
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 wallets = metadata.wallets || {};
|
|
|
|
const scanningMatrix = [
|
|
{ network: 'SOLANA', tokens: ['SOL', 'USDT', 'USDC'] },
|
|
{ network: 'POLYGON', tokens: ['MATIC', 'USDT', 'USDC'] },
|
|
{ network: 'TRON', tokens: ['TRX', 'USDT', 'USDC'] }
|
|
];
|
|
|
|
if (metadata.intent_network && metadata.intent_token) {
|
|
const intentIdx = scanningMatrix.findIndex(m => m.network === metadata.intent_network);
|
|
if (intentIdx > -1) {
|
|
const intentItem = scanningMatrix.splice(intentIdx, 1)[0];
|
|
const tokenIdx = intentItem.tokens.indexOf(metadata.intent_token);
|
|
if (tokenIdx > -1) {
|
|
intentItem.tokens.splice(tokenIdx, 1);
|
|
intentItem.tokens.unshift(metadata.intent_token);
|
|
}
|
|
scanningMatrix.unshift(intentItem);
|
|
}
|
|
}
|
|
|
|
let foundPayment = false;
|
|
|
|
for (const scan of scanningMatrix) {
|
|
if (foundPayment) break;
|
|
|
|
const networkId = scan.network;
|
|
const walletConfig = wallets[networkId] || (networkId === 'POLYGON' ? wallets['EVM'] : null);
|
|
|
|
if (!walletConfig) continue;
|
|
|
|
const depositAddress = typeof walletConfig === 'string' ? walletConfig : walletConfig.address;
|
|
const depositPrivateKey = walletConfig.privateKey;
|
|
|
|
if (!depositPrivateKey) continue;
|
|
|
|
// Create engine with explicit network and ensured config
|
|
const cryptoEngine = new CryptoEngine(networkId);
|
|
|
|
for (const tokenSymbol of scan.tokens) {
|
|
if (foundPayment) break;
|
|
|
|
console.log(`[SyncWorker] Checking TX ${tx.id.slice(0,8)} | Network: ${networkId} | Token: ${tokenSymbol} | Address: ${depositAddress}`);
|
|
|
|
let expectedCryptoAmount = tx.amount.toString();
|
|
try {
|
|
const symbolMap: Record<string, string> = {
|
|
'SOL': 'SOLUSDT', 'MATIC': 'MATICUSDT', 'POLYGON': 'MATICUSDT',
|
|
'USDC': 'USDCUSDT', 'USDT': 'USDTUSDT', 'TRX': 'TRXUSDT'
|
|
};
|
|
const pair = symbolMap[tokenSymbol] || '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 = (tx.currency || 'TRY').toUpperCase();
|
|
const priceInTarget = currency === 'USD' ? cryptoUsdPrice : priceInTry;
|
|
|
|
if (priceInTarget > 0) {
|
|
const rawExpected = parseFloat(tx.amount) / priceInTarget;
|
|
expectedCryptoAmount = (rawExpected * 0.98).toFixed(6);
|
|
console.log(`[SyncWorker] Price for ${tokenSymbol}: ${priceInTarget.toFixed(2)} ${currency} | Expected: ${expectedCryptoAmount}`);
|
|
}
|
|
} catch (e) {
|
|
console.warn(`[SyncWorker] Price fetch failed for ${tokenSymbol}, using backup static rates.`);
|
|
// Absolute fallback for safety
|
|
const staticRates: Record<string, number> = { 'SOL': 4000, 'USDT': 33, 'USDC': 33, 'TRX': 5, 'MATIC': 25 };
|
|
const rate = staticRates[tokenSymbol] || 1;
|
|
expectedCryptoAmount = (parseFloat(tx.amount) / rate * 0.95).toFixed(6);
|
|
}
|
|
|
|
const verification = await cryptoEngine.verifyPayment(depositAddress, expectedCryptoAmount, tokenSymbol);
|
|
|
|
if (verification.success) {
|
|
console.log(`[SyncWorker] Found ${tokenSymbol} matching ~${expectedCryptoAmount} on ${networkId}`);
|
|
|
|
let platformAddress = sysSettings.evm;
|
|
if (networkId === 'SOLANA') platformAddress = sysSettings.sol;
|
|
else if (networkId === 'TRON') platformAddress = sysSettings.tron;
|
|
|
|
const sweepResult = await cryptoEngine.sweepFunds(depositPrivateKey, platformAddress, tokenSymbol);
|
|
|
|
if (sweepResult.success) {
|
|
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;
|
|
|
|
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, networkId, tokenSymbol, 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, networkId, tokenSymbol, expectedCryptoAmount]);
|
|
|
|
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', network: networkId, token: tokenSymbol });
|
|
foundPayment = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!foundPayment) results.push({ id: tx.id, status: 'no_payment' });
|
|
|
|
} catch (err: any) {
|
|
console.error(`[SyncWorker] Error processing TX ${tx.id}:`, err);
|
|
results.push({ id: tx.id, status: 'error', error: err.message });
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|