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>

Выполнить команду


Для локальной разработки. Не используйте в интернете!