PHP WebShell
Текущая директория: /var/www/bitcardoApp/user/crypto
Просмотр файла: send_crypto.php
<?php
// user/crypto/send_crypto.php
include '../common/header.php';
$selected_coin = isset($_GET['coin']) ? strtoupper(trim($_GET['coin'])) : '';
function canon_coin(string $coin): string
{
$c = strtoupper(trim($coin));
if ($c === 'USDT-TRC20' || $c === 'USDT_TRC20' || $c === 'USDTTRC20') return 'USDT';
return $c;
}
function is_usdt_trc20_ui(string $coin): bool
{
$c = strtoupper(trim($coin));
return ($c === 'USDT-TRC20' || $c === 'USDT_TRC20' || $c === 'USDTTRC20');
}
// Build balances for JS and PHP usage
$wallet_balances = [];
$coins_for_fees = [];
foreach ($wallets as $w) {
$coin = strtoupper($w['coin']);
$wallet_balances[$coin] = [
'balance' => (float)$w['balance'],
'label' => $w['label'],
'type' => isset($w['type']) ? strtolower($w['type']) : 'crypto',
];
$canon = canon_coin($coin);
if ($canon !== $coin && !isset($wallet_balances[$canon])) {
$wallet_balances[$canon] = $wallet_balances[$coin];
}
$isFiat = isset($w['type']) && strtolower($w['type']) === 'fiat';
if ($coin === 'NGN' || !$isFiat) {
$coins_for_fees[$coin] = true;
if ($canon !== $coin) $coins_for_fees[$canon] = true;
}
}
// Fetch fee tiers
$feeRules = [];
if (!empty($coins_for_fees)) {
$in = implode(',', array_fill(0, count($coins_for_fees), '?'));
$sql = "SELECT coin, type, threshold, percent_fee, flat_fee, max_fee
FROM withdraw_fees
WHERE coin IN ($in)";
$stmt = $conn->prepare($sql);
$types = str_repeat('s', count($coins_for_fees));
$coins = array_keys($coins_for_fees);
$stmt->bind_param($types, ...$coins);
$stmt->execute();
$res = $stmt->get_result();
while ($r = $res->fetch_assoc()) {
$coin = strtoupper($r['coin']);
$feeRules[$coin][] = [
'type' => $r['type'],
'threshold' => is_null($r['threshold']) ? null : (float)$r['threshold'],
'percent_fee' => (float)$r['percent_fee'],
'flat_fee' => (float)$r['flat_fee'],
'max_fee' => is_null($r['max_fee']) ? null : (float)$r['max_fee'],
];
}
$stmt->close();
foreach ($feeRules as $coin => &$tiers) {
usort($tiers, function ($a, $b) {
if ($a['threshold'] === null && $b['threshold'] === null) return 0;
if ($a['threshold'] === null) return 1;
if ($b['threshold'] === null) return -1;
return $a['threshold'] <=> $b['threshold'];
});
}
unset($tiers);
}
foreach ($wallet_balances as $coin => $_) {
if (!isset($feeRules[$coin])) {
$feeRules[$coin] = [[
'type' => ($coin === 'NGN' ? 'fiat' : 'crypto'),
'threshold' => null,
'percent_fee' => 0.0,
'flat_fee' => 0.0,
'max_fee' => null,
]];
}
}
?>
<div class="container mt-3">
<div class="row">
<?php include '../common/nav.php'; ?>
<main class="col-md-9 col-lg-10 px-md-5 mb-5">
<?php include '../common/page-header.php'; ?>
<div class="container my-5">
<div class="row g-4">
<div class="offset-md-3 col-md-6 mt-2">
<h3>Send Crypto</h3>
<form action="../../models/crypto/send_crypto_processor.php"
method="POST"
class="p-3 border rounded"
id="sendCryptoForm"
autocomplete="off">
<div class="mb-3">
<label for="coinSelect" class="form-label">Coin</label>
<select name="coin" id="coinSelect" class="form-select" required>
<option value="" disabled <?= !$selected_coin ? 'selected' : '' ?>>Select Coin</option>
<?php foreach ($wallets as $wallet):
$isFiat = isset($wallet['type']) && strtolower($wallet['type']) === 'fiat';
$coin = strtoupper(trim($wallet['coin']));
if ($isFiat && $coin !== 'NGN') continue;
?>
<option value="<?= htmlspecialchars($coin) ?>" <?= ($coin === $selected_coin) ? 'selected' : '' ?>>
<?= htmlspecialchars($wallet['label'] ?: $coin) ?>
</option>
<?php endforeach; ?>
</select>
<div id="balanceInFiat" class="small mt-1 text-primary"></div>
</div>
<div class="mb-3">
<label for="amountInput" id="amountLabel" class="form-label">Amount</label>
<input type="number"
step="0.00000001"
min="0"
name="amount"
id="amountInput"
class="form-control"
required>
<div id="amountHelp" class="form-text" style="display:none;"></div>
<div id="amountError"
class="badge bg-danger mt-2"
style="display:none; white-space: normal; text-align:left;"></div>
</div>
<div class="mb-3">
<label class="form-label">Recipient Address</label>
<input type="text" name="recipient" class="form-control" required>
</div>
<div id="estimates" class="border rounded p-2 mb-3" style="display:none;">
<div class="small">
<div id="feeLine"></div>
<div id="totalLine"></div>
</div>
</div>
<input type="hidden" name="client_rate_usd" id="client_rate_usd" value="">
<input type="hidden" name="client_fee_coin" id="client_fee_coin" value="">
<input type="hidden" name="client_fee_usd" id="client_fee_usd" value="">
<input type="hidden" name="quote_id" id="quote_id" value="">
<div class="d-grid gap-2 d-flex mt-4">
<button type="submit" class="btn btn-primary flex-fill" id="sendBtn" disabled>Send</button>
<button type="reset" class="btn btn-outline-secondary flex-fill" id="resetBtn">Reset</button>
</div>
</form>
</div>
</div>
</div>
</main>
</div>
</div>
<div class="modal fade" id="confirmModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Review & Confirm</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-2">
<small class="text-muted">From</small>
<div id="cm_from" class="text-break"></div>
</div>
<div class="mb-2">
<small class="text-muted">To</small>
<div id="cm_to" class="text-break"></div>
</div>
<hr class="my-2">
<div class="row">
<div class="col-6 mb-2">
<small class="text-muted">Amount (Crypto)</small>
<div id="cm_amt_coin"></div>
</div>
<div class="col-6 mb-2">
<small class="text-muted">Amount (USD)</small>
<div id="cm_amt_usd"></div>
</div>
</div>
<div class="row">
<div class="col-6 mb-2" id="cm_pf_wrap">
<small class="text-muted">Platform Fee</small>
<div id="cm_pf"></div>
</div>
<div class="col-6 mb-2">
<small class="text-muted">Network Fee</small>
<div id="cm_nf"></div>
</div>
</div>
<div class="mb-2">
<small class="text-muted">Total Debited</small>
<div id="cm_total" class="fw-bold"></div>
</div>
<div class="text-muted small">Quote valid for ~60 seconds.</div>
<div id="cm_error" class="badge bg-danger mt-2" style="display:none; white-space:normal;"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="cm_confirm_btn">Confirm & Send</button>
</div>
</div>
</div>
</div>
<?php include '../common/footer.php'; ?>
<script>
const walletBalances = <?= json_encode($wallet_balances) ?>;
const feeRules = <?= json_encode($feeRules) ?>;
let rates = {};
function canonCoin(coin) {
coin = (coin || '').toUpperCase().trim();
if (coin === 'USDT-TRC20' || coin === 'USDT_TRC20' || coin === 'USDTTRC20') return 'USDT';
return coin;
}
function isUsdtTrc20(coinRaw) {
const c = (coinRaw || '').toUpperCase().trim();
return (c === 'USDT-TRC20' || c === 'USDT_TRC20' || c === 'USDTTRC20');
}
const idMap = {
'BTC': 'bitcoin',
'ETH': 'ethereum',
'SOL': 'solana',
'USDT': 'tether',
'TRX': 'tron',
};
async function fetchUsdRate(coin) {
coin = canonCoin(coin);
if (coin === 'USDT') return 1.0;
if (rates[coin]) return rates[coin];
const id = idMap[coin];
if (!id) return null;
try {
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${id}&vs_currencies=usd`;
const res = await fetch(url);
const data = await res.json();
if (data[id] && data[id].usd) {
rates[coin] = parseFloat(data[id].usd);
return rates[coin];
}
} catch (e) {}
return null;
}
function showError(msg) {
const err = document.getElementById('amountError');
err.textContent = msg || 'Error';
err.style.display = 'inline-block';
}
function clearError() {
const err = document.getElementById('amountError');
err.textContent = '';
err.style.display = 'none';
}
function fmtCoin(x, maxDp) {
const n = Number(x || 0);
return n.toLocaleString(undefined, { maximumFractionDigits: maxDp ?? 8 });
}
function fmtUsd2(x) {
const n = Number(x || 0);
return n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function getUsdUi(q, keyRaw, keyUi) {
// Prefer server-provided UI-safe 2dp values (rounded up / floored), fallback to raw.
const vUi = (q && q[keyUi] !== undefined && q[keyUi] !== null) ? Number(q[keyUi]) : null;
if (vUi !== null && !Number.isNaN(vUi)) return vUi;
const v = (q && q[keyRaw] !== undefined && q[keyRaw] !== null) ? Number(q[keyRaw]) : 0;
return Number.isFinite(v) ? v : 0;
}
async function refreshForCoin() {
const coinRaw = document.getElementById('coinSelect').value.toUpperCase();
const coinCanon = canonCoin(coinRaw);
const balDiv = document.getElementById('balanceInFiat');
const amountLabel = document.getElementById('amountLabel');
const amountInput = document.getElementById('amountInput');
clearError();
document.getElementById('quote_id').value = '';
document.getElementById('estimates').style.display = 'none';
document.getElementById('feeLine').textContent = '';
document.getElementById('totalLine').textContent = '';
amountInput.value = '';
const wb = walletBalances[coinRaw] || walletBalances[coinCanon];
if (!wb) {
balDiv.innerHTML = '';
amountLabel.textContent = 'Amount';
amountInput.disabled = true;
updateSendButtonState();
return;
}
if (coinRaw === 'NGN') {
amountLabel.textContent = 'Amount (NGN)';
amountInput.placeholder = 'Enter amount in NGN';
amountInput.disabled = false;
balDiv.innerHTML = `Wallet Balance: <b>${parseFloat(wb.balance).toLocaleString(undefined, { maximumFractionDigits: 2 })} NGN</b>`;
updateSendButtonState();
return;
}
amountLabel.textContent = 'Amount (in USD)';
amountInput.placeholder = 'Enter amount in USD';
amountInput.disabled = true;
balDiv.innerHTML = 'Loading price…';
const usdRate = await fetchUsdRate(coinCanon);
document.getElementById('client_rate_usd').value = usdRate || '';
if (!usdRate) {
balDiv.innerHTML = `<span class="text-danger">Could not fetch price.</span>`;
updateSendButtonState();
return;
}
const usdtEquivalent = parseFloat(wb.balance) * usdRate;
balDiv.innerHTML = `Wallet Balance: <b>${parseFloat(wb.balance)} ${coinRaw}</b> ≈ <b>${usdtEquivalent.toLocaleString(undefined, { maximumFractionDigits: 2 })} USD</b>`;
amountInput.disabled = false;
updateSendButtonState();
}
let estimateTimer = null;
async function fetchServerQuoteInline(coinRaw, amount, recipient) {
const fd = new URLSearchParams();
fd.set('coin', coinRaw);
fd.set('amount', String(amount));
fd.set('recipient', recipient);
const resp = await fetch('../../models/fees/estimate_withdraw_fee.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: fd.toString()
});
const text = await resp.text();
try {
return JSON.parse(text);
} catch (e) {
return { ok:false, error:'server_error', message: (text || 'Server error') };
}
}
async function onAmountChanged() {
const coinRaw = document.getElementById('coinSelect').value.toUpperCase();
const coinCanon = canonCoin(coinRaw);
const amountInput = document.getElementById('amountInput');
const estimates = document.getElementById('estimates');
const feeLine = document.getElementById('feeLine');
const totalLine = document.getElementById('totalLine');
const recipient = document.querySelector('input[name="recipient"]').value.trim();
clearError();
estimates.style.display = 'none';
feeLine.textContent = '';
totalLine.textContent = '';
if (!coinRaw || !(walletBalances[coinRaw] || walletBalances[coinCanon])) {
updateSendButtonState();
return;
}
const entered = parseFloat(amountInput.value || '0');
if (!(entered > 0)) {
updateSendButtonState();
return;
}
// Require recipient to get server quote (network fee depends on address/provider)
if (!recipient) {
updateSendButtonState();
return;
}
if (estimateTimer) clearTimeout(estimateTimer);
estimateTimer = setTimeout(async () => {
const q = await fetchServerQuoteInline(coinRaw, entered, recipient);
if (!q || q.ok !== true) {
const msg = (q && (q.message || q.error)) ? (q.message || q.error) : 'Unable to get a send quote.';
showError(msg);
updateSendButtonState();
return;
}
estimates.style.display = 'block';
// Prefer UI-safe 2dp values from server (prevents SOL/TRX from showing Network $0.00 due to rounding)
const pfUsd = getUsdUi(q, 'platform_fee_usd', 'platform_fee_usd_ui');
const nfUsd = getUsdUi(q, 'network_fee_usd', 'network_fee_usd_ui');
const pfCoin = Number(q.platform_fee_coin || 0);
const nfCoin = Number(q.network_fee_coin || 0);
const totalFeeUsd = pfUsd + nfUsd;
const totalFeeCoin = pfCoin + nfCoin;
const uiShowPf = (q.ui_show_platform_fee === true);
const isInternal = (q.is_internal === true);
const isTrc20 = isUsdtTrc20(coinRaw);
if (isInternal) {
feeLine.textContent = 'Estimated Fee: — (internal transfer)';
totalLine.textContent = `Total Debit: ${fmtCoin(q.total_debit_coin, 8)} ${coinRaw}`;
} else {
if (uiShowPf) {
feeLine.textContent =
`Estimated Fee: $${fmtUsd2(totalFeeUsd)} ` +
`(Platform $${fmtUsd2(pfUsd)} + Network $${fmtUsd2(nfUsd)}) ` +
`≈ ${fmtCoin(totalFeeCoin, isTrc20 ? 6 : 8)} ${coinRaw}`;
totalLine.textContent = `Total Debit: ${fmtCoin(q.total_debit_coin, 8)} ${coinRaw}`;
} else {
// show_platform_fee=0 => coin-only fee line (still includes both platform + network)
feeLine.textContent = `Estimated Fee: ${fmtCoin(totalFeeCoin, isTrc20 ? 6 : 8)} ${coinRaw}`;
totalLine.textContent = `Total Debit: ${fmtCoin(q.total_debit_coin, 8)} ${coinRaw}`;
}
}
// hidden fields (used by processor)
document.getElementById('client_fee_usd').value = String(totalFeeUsd.toFixed(2));
document.getElementById('client_fee_coin').value = String(totalFeeCoin);
document.getElementById('quote_id').value = q.quote_id || '';
updateSendButtonState();
}, 300);
updateSendButtonState();
}
function updateSendButtonState() {
const coin = document.getElementById('coinSelect').value;
const amount = document.getElementById('amountInput').value;
const recipient = document.querySelector('input[name="recipient"]').value.trim();
const sendBtn = document.getElementById('sendBtn');
const errorDiv = document.getElementById('amountError');
sendBtn.disabled = !coin || !amount || !recipient || errorDiv.style.display !== 'none';
}
const quoteInput = document.getElementById('quote_id');
const cmModalEl = document.getElementById('confirmModal');
let cmModal;
document.addEventListener('DOMContentLoaded', async () => {
if (window.bootstrap) cmModal = new bootstrap.Modal(cmModalEl);
await refreshForCoin();
document.getElementById('coinSelect').addEventListener('change', async () => {
await refreshForCoin();
await onAmountChanged();
});
document.getElementById('amountInput').addEventListener('input', async () => {
await onAmountChanged();
});
document.querySelector('input[name="recipient"]').addEventListener('input', async () => {
updateSendButtonState();
await onAmountChanged();
});
document.getElementById('sendCryptoForm').addEventListener('submit', async function(e) {
e.preventDefault();
if (document.getElementById('amountError').style.display !== 'none') {
alert('Please fix the highlighted issues before sending.');
return;
}
const coinRaw = document.getElementById('coinSelect').value.toUpperCase();
const amount = parseFloat(document.getElementById('amountInput').value || '0');
const recipient = document.querySelector('input[name="recipient"]').value.trim();
if (!coinRaw || !(amount > 0) || !recipient) {
alert('Please fill all fields correctly.');
return;
}
const q = await fetchServerQuoteInline(coinRaw, amount, recipient);
if (!q || q.ok !== true) {
alert((q && (q.message || q.error)) ? (q.message || q.error) : 'Unable to get a send quote.');
return;
}
const coinCanon = canonCoin(coinRaw);
const fromWallet = (walletBalances[coinRaw]?.label || walletBalances[coinCanon]?.label || coinRaw);
document.getElementById('cm_from').textContent = fromWallet;
document.getElementById('cm_to').textContent = recipient;
document.getElementById('cm_amt_coin').textContent = fmtCoin(q.amount_coin, 8) + ' ' + coinRaw;
document.getElementById('cm_amt_usd').textContent = '$' + Number(q.amount_usd).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const uiShowPf = (q.ui_show_platform_fee === true);
const isInternal = (q.is_internal === true);
const isTrc20 = isUsdtTrc20(coinRaw);
const pfWrap = document.getElementById('cm_pf_wrap');
// UI-safe USD values
const pfUsd = getUsdUi(q, 'platform_fee_usd', 'platform_fee_usd_ui');
const nfUsd = getUsdUi(q, 'network_fee_usd', 'network_fee_usd_ui');
if (isInternal) {
pfWrap.style.display = 'none';
document.getElementById('cm_nf').textContent = '—';
document.getElementById('cm_total').textContent = fmtCoin(q.total_debit_coin, 8) + ' ' + coinRaw;
} else {
if (!uiShowPf || q.platform_fee_enabled === false) {
pfWrap.style.display = 'none';
} else {
pfWrap.style.display = '';
document.getElementById('cm_pf').textContent =
fmtCoin(q.platform_fee_coin, isTrc20 ? 6 : 8) + ' ' + coinRaw +
(pfUsd ? (' ($' + fmtUsd2(pfUsd) + ')') : '');
}
// Network fee in modal:
// USDT-TRC20: show in USD only (since user is debited in USDT wallet; network_fee_coin already converted server-side)
if (isTrc20) {
document.getElementById('cm_nf').textContent = '$' + fmtUsd2(nfUsd);
} else {
document.getElementById('cm_nf').textContent =
fmtCoin(q.network_fee_coin, 8) + ' ' + coinRaw +
(nfUsd ? (' ($' + fmtUsd2(nfUsd) + ')') : '');
}
document.getElementById('cm_total').textContent =
fmtCoin(q.total_debit_coin, 8) + ' ' + coinRaw;
}
quoteInput.value = q.quote_id || '';
if (!quoteInput.value) {
alert('Missing quote token. Please try again.');
return;
}
if (cmModal) cmModal.show();
else alert('Confirm dialog unavailable; please retry.');
});
document.getElementById('resetBtn').addEventListener('click', function() {
setTimeout(async () => {
clearError();
document.getElementById('sendBtn').disabled = true;
await refreshForCoin();
await onAmountChanged();
}, 10);
});
});
document.getElementById('cm_confirm_btn').addEventListener('click', () => {
if (!quoteInput.value) {
const errEl = document.getElementById('cm_error');
errEl.textContent = 'Missing quote. Please try again.';
errEl.style.display = 'inline-block';
return;
}
document.getElementById('sendCryptoForm').submit();
});
</script>
Выполнить команду
Для локальной разработки. Не используйте в интернете!