38451-vm/swap.php
2026-02-20 06:34:26 +00:00

366 lines
16 KiB
PHP

<?php
require_once __DIR__ . "/includes/lang.php";
require_once __DIR__ . "/includes/header.php";
$balances = [];
if ($user) {
$stmt = db()->prepare("SELECT symbol, available FROM user_balances WHERE user_id = ?");
$stmt->execute([$user["id"]]);
while ($row = $stmt->fetch()) {
$balances[$row["symbol"]] = (float)$row["available"];
}
}
?>
<div class="container py-4 py-md-5 d-flex justify-content-center">
<div class="card bg-dark border-0 shadow-lg" style="width: 100%; max-width: 480px; border-radius: 28px; background: #0b0e11 !important; border: 1px solid #2b3139 !important; box-shadow: 0 20px 50px rgba(0,0,0,0.5) !important;">
<div class="card-body p-3 p-md-4">
<h3 class="fw-bold mb-4 text-white d-flex align-items-center gap-2">
<i class="bi bi-lightning-charge-fill text-primary"></i>
<?= __("swap") ?>
</h3>
<!-- From -->
<div class="p-3 p-md-4 mb-2 rounded-4" style="background: #161a1e; border: 1px solid #2b3139;">
<div class="d-flex justify-content-between mb-3">
<span class="text-white opacity-50 small fw-bold text-uppercase" style="letter-spacing: 1px;"><?= __("from") ?></span>
<span class="text-white opacity-50 small"><?= __("balance") ?>: <span class="text-white fw-bold" id="from-balance-val">0.00</span></span>
</div>
<div class="d-flex align-items-center">
<input type="number" id="from-amount" class="form-control bg-transparent border-0 text-white p-0 shadow-none w-50 fw-bold" placeholder="0.00" style="color: #fff !important; font-size: clamp(24px, 5vw, 32px) !important;">
<div class="ms-auto dropdown">
<div class="d-flex align-items-center bg-dark p-2 rounded-pill px-2 px-md-3 border border-secondary cursor-pointer shadow-sm dropdown-toggle" data-bs-toggle="dropdown">
<img src="<?php echo getCoinIcon("USDT"); ?>" width="24" height="24" class="me-2 rounded-circle" id="from-coin-img" alt="USDT">
<span class="fw-bold text-white small" id="from-coin-symbol" data-symbol="USDT">USDT</span>
</div>
<ul class="dropdown-menu dropdown-menu-dark bg-dark border-secondary rounded-4 shadow-lg p-2" style="max-height: 300px; overflow-y: auto;">
<?php
$swap_coins = ["BTC", "ETH", "USDT", "BNB", "SOL", "XRP", "ADA", "DOGE", "DOT", "MATIC", "LINK", "SHIB"];
foreach($swap_coins as $sc): ?>
<li><a class="dropdown-item rounded-3 py-2 d-flex align-items-center gap-2" href="#" onclick="selectCoin('from', '<?= $sc ?>', '<?= $sc ?>', '<?= getCoinIcon($sc) ?>')">
<img src="<?= getCoinIcon($sc) ?>" width="20" height="20" alt="<?= $sc ?>"> <?= $sc ?>
</a></li>
<?php endforeach; ?>
</ul>
</div>
</div>
</div>
<!-- Switch Icon -->
<div class="text-center my-n3 position-relative" style="z-index: 2;">
<div class="bg-primary border border-4 border-dark rounded-circle d-inline-flex p-2 shadow-lg" style="cursor: pointer; transition: all 0.3s;" onclick="switchCoins()">
<i class="bi bi-arrow-down-up text-white fs-5"></i>
</div>
</div>
<!-- To -->
<div class="p-3 p-md-4 mt-n1 mb-4 rounded-4" style="background: #161a1e; border: 1px solid #2b3139;">
<div class="d-flex justify-content-between mb-3">
<span class="text-white opacity-50 small fw-bold text-uppercase" style="letter-spacing: 1px;"><?= __("to") ?></span>
<span class="text-white opacity-50 small"><?= __("balance") ?>: <span class="text-white fw-bold" id="to-balance-val">0.00</span></span>
</div>
<div class="d-flex align-items-center">
<input type="number" id="to-amount" class="form-control bg-transparent border-0 text-white p-0 shadow-none w-50 fw-bold" placeholder="0.00" readonly style="color: #fff !important; opacity: 1; font-size: clamp(24px, 5vw, 32px) !important;">
<div class="ms-auto dropdown">
<div class="d-flex align-items-center bg-dark p-2 rounded-pill px-2 px-md-3 border border-secondary cursor-pointer shadow-sm dropdown-toggle" data-bs-toggle="dropdown">
<img src="<?php echo getCoinIcon("BTC"); ?>" width="24" height="24" class="me-2 rounded-circle" id="to-coin-img" alt="BTC">
<span class="fw-bold text-white small" id="to-coin-symbol" data-symbol="BTC">BTC</span>
</div>
<ul class="dropdown-menu dropdown-menu-dark bg-dark border-secondary rounded-4 shadow-lg p-2" style="max-height: 300px; overflow-y: auto;">
<?php foreach($swap_coins as $sc): ?>
<li><a class="dropdown-item rounded-3 py-2 d-flex align-items-center gap-2" href="#" onclick="selectCoin('to', '<?= $sc ?>', '<?= $sc ?>', '<?= getCoinIcon($sc) ?>')">
<img src="<?= getCoinIcon($sc) ?>" width="20" height="20" alt="<?= $sc ?>"> <?= $sc ?>
</a></li>
<?php endforeach; ?>
</ul>
</div>
</div>
</div>
<div class="mb-4 small px-3 py-3 rounded-4" style="background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.05);">
<div class="d-flex justify-content-between text-white opacity-50 mb-2">
<span class="fw-medium"><?= __("rate") ?></span>
<span id="swap-rate" class="text-white fw-bold">--</span>
</div>
<div class="d-flex justify-content-between text-white opacity-50 mb-2">
<span class="fw-medium"><?= __("price_impact") ?></span>
<span class="text-success fw-bold">&lt; 0.01%</span>
</div>
<div class="d-flex justify-content-between text-white opacity-50">
<span class="fw-medium"><?= __("slippage") ?></span>
<span class="text-white fw-bold">0.5%</span>
</div>
</div>
<button id="swap-btn" onclick="executeSwap()" class="btn btn-primary w-100 py-3 rounded-pill fw-bold fs-5 shadow-primary transition-all active-scale" style="background: linear-gradient(135deg, #0062ff, #00d2ff); border: none;"><?= __("swap_now") ?></button>
</div>
</div>
</div>
<script>
const fromInput = document.getElementById("from-amount");
const toInput = document.getElementById("to-amount");
const rateEl = document.getElementById("swap-rate");
const swapBtn = document.getElementById("swap-btn");
const approxLabel = "<?= __("approx") ?>";
const userBalances = <?= json_encode($balances) ?>;
let fromPrice = 1;
let toPrice = 1;
async function fetchPrice(symbol) {
if (symbol === "USDT") return 1;
// Fallback chain for frontend price fetching
try {
// Try CoinCap first as it's CORS-friendly
const resCC = await fetch(`https://api.coincap.io/v2/assets/${symbol.toLowerCase()}`);
if (resCC.ok) {
const dataCC = await resCC.json();
if (dataCC.data && dataCC.data.priceUsd) return parseFloat(dataCC.data.priceUsd);
}
} catch (e) { console.log("CoinCap error, trying OKX..."); }
try {
// Try OKX (might have CORS issues on some browsers)
const response = await fetch(`https://www.okx.com/api/v5/market/ticker?instId=${symbol}-USDT`);
if (response.ok) {
const data = await response.json();
if (data.data && data.data[0]) return parseFloat(data.data[0].last);
}
} catch (e) {
console.error("Price fetch error:", e);
}
return 0;
}
async function updateRates() {
const fromSymbol = document.getElementById("from-coin-symbol").dataset.symbol;
const toSymbol = document.getElementById("to-coin-symbol").dataset.symbol;
fromPrice = await fetchPrice(fromSymbol);
toPrice = await fetchPrice(toSymbol);
updateCalculation();
}
function selectCoin(type, symbol, displayName, icon) {
document.getElementById(type + "-coin-symbol").innerText = displayName;
document.getElementById(type + "-coin-symbol").dataset.symbol = symbol;
document.getElementById(type + "-coin-img").src = icon;
const bal = userBalances[symbol] || 0;
document.getElementById(type + "-balance-val").innerText = bal.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 4});
updateRates();
}
function switchCoins() {
const fromSymbol = document.getElementById("from-coin-symbol").dataset.symbol;
const fromName = document.getElementById("from-coin-symbol").innerText;
const fromIcon = document.getElementById("from-coin-img").src;
const toSymbol = document.getElementById("to-coin-symbol").dataset.symbol;
const toName = document.getElementById("to-coin-symbol").innerText;
const toIcon = document.getElementById("to-coin-img").src;
selectCoin("from", toSymbol, toName, toIcon);
selectCoin("to", fromSymbol, fromName, fromIcon);
}
function updateCalculation() {
const val = parseFloat(fromInput.value) || 0;
const rate = fromPrice / toPrice;
toInput.value = (val * rate).toFixed(8);
const fromName = document.getElementById("from-coin-symbol").innerText;
const toName = document.getElementById("to-coin-symbol").innerText;
if (fromPrice && toPrice) {
rateEl.innerText = `1 ${fromName} ${approxLabel} ${(rate).toFixed(8)} ${toName}`;
} else {
rateEl.innerText = "--";
}
}
async function executeSwap() {
const amount = parseFloat(fromInput.value);
const fromCoin = document.getElementById("from-coin-symbol").dataset.symbol;
const toCoin = document.getElementById("to-coin-symbol").dataset.symbol;
if (!amount || amount <= 0) {
Swal.fire({
icon: 'warning',
title: '<?= __("enter_amount") ?>',
background: '#161a1e',
color: '#fff',
confirmButtonText: '<?= __("confirm") ?>',
confirmButtonColor: '#0062ff',
customClass: {
popup: 'rounded-4 border border-secondary shadow-lg'
}
});
return;
}
if (amount > (userBalances[fromCoin] || 0)) {
Swal.fire({
icon: 'error',
title: '<?= __("insufficient_balance") ?>',
background: '#161a1e',
color: '#fff',
confirmButtonText: '<?= __("confirm") ?>',
confirmButtonColor: '#0062ff',
customClass: {
popup: 'rounded-4 border border-secondary shadow-lg'
}
});
return;
}
// Confirmation Modal
const confirmResult = await Swal.fire({
title: '<?= __("confirm_swap") ?>',
html: `<div class="p-3 text-start">
<div class="mb-2 d-flex justify-content-between"><span><?= __("from") ?>:</span> <span class="fw-bold text-primary">${amount} ${fromCoin}</span></div>
<div class="mb-0 d-flex justify-content-between"><span><?= __("to") ?>:</span> <span class="fw-bold text-success">${toInput.value} ${toCoin}</span></div>
</div>`,
icon: 'question',
showCancelButton: true,
confirmButtonText: '<?= __("confirm") ?>',
cancelButtonText: '<?= __("cancel") ?>',
background: '#161a1e',
color: '#fff',
confirmButtonColor: '#0062ff',
cancelButtonColor: '#2b3139',
customClass: {
popup: 'rounded-4 border border-secondary shadow-lg',
confirmButton: 'rounded-pill px-4',
cancelButton: 'rounded-pill px-4'
}
});
if (!confirmResult.isConfirmed) return;
Swal.fire({
title: '<?= __("processing") ?>',
html: '<?= __("swap_processing_desc") ?>',
allowOutsideClick: false,
background: '#161a1e',
color: '#fff',
didOpen: () => {
Swal.showLoading();
},
customClass: {
popup: 'rounded-4 border border-secondary shadow-lg'
}
});
swapBtn.disabled = true;
const originalText = swapBtn.innerText;
swapBtn.innerText = '<?= __("processing") ?>';
const formData = new FormData();
formData.append("from_coin", fromCoin);
formData.append("to_coin", toCoin);
formData.append("amount", amount);
try {
const response = await fetch("api/swap.php", {
method: "POST",
body: formData
});
const result = await response.json();
if (result.success) {
await Swal.fire({
icon: 'success',
title: '<?= __("success") ?>',
text: '<?= __("swap_success_desc") ?>',
background: '#161a1e',
color: '#fff',
confirmButtonText: '<?= __("confirm") ?>',
confirmButtonColor: '#0062ff',
customClass: {
popup: 'rounded-4 border border-secondary shadow-lg'
}
});
location.reload();
} else {
Swal.fire({
icon: 'error',
title: '<?= __("failed") ?>',
text: result.error || '<?= __("unknown_error") ?>',
background: '#161a1e',
color: '#fff',
confirmButtonText: '<?= __("confirm") ?>',
confirmButtonColor: '#0062ff',
customClass: {
popup: 'rounded-4 border border-secondary shadow-lg'
}
});
swapBtn.disabled = false;
swapBtn.innerText = originalText;
}
} catch (e) {
Swal.fire({
icon: 'error',
title: '<?= __("request_failed") ?>',
background: '#161a1e',
color: '#fff',
confirmButtonText: '<?= __("confirm") ?>',
confirmButtonColor: '#0062ff',
customClass: {
popup: 'rounded-4 border border-secondary shadow-lg'
}
});
swapBtn.disabled = false;
swapBtn.innerText = originalText;
}
}
fromInput.addEventListener("input", updateCalculation);
// Initial setup
document.addEventListener("DOMContentLoaded", () => {
selectCoin("from", "USDT", "USDT", "<?php echo getCoinIcon("USDT"); ?>");
selectCoin("to", "BTC", "BTC", "<?php echo getCoinIcon("BTC"); ?>");
});
// Animate switch icon
document.querySelector(".bg-primary.rounded-circle").addEventListener("click", function() {
this.style.transform = this.style.transform === "rotate(180deg)" ? "rotate(0deg)" : "rotate(180deg)";
});
</script>
<style>
.shadow-primary {
box-shadow: 0 10px 25px rgba(0, 98, 255, 0.4);
}
.cursor-pointer { cursor: pointer; }
#swap-rate { font-family: "Roboto Mono", monospace; }
.active-scale:active { transform: scale(0.98); }
/* Beautiful SweetAlert2 Customization */
.swal2-popup.rounded-4 {
border-radius: 24px !important;
padding: 2rem !important;
background: #161a1e !important;
}
.swal2-title {
font-size: 1.5rem !important;
font-weight: 700 !important;
}
.swal2-confirm.rounded-pill, .swal2-cancel.rounded-pill {
padding: 0.8rem 2rem !important;
font-size: 0.9rem !important;
font-weight: 700 !important;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.swal2-icon {
border-width: 2px !important;
}
</style>
<?php require_once __DIR__ . "/includes/footer.php"; ?>