PHP WebShell

Текущая директория: /var/www/bitcardoApp/tmp

Просмотр файла: btc_send_test.php

<?php
// user/crypto/btc_send_test.php
declare(strict_types=1);

@ini_set('display_errors', '1');
error_reporting(E_ALL);

if (session_status() === PHP_SESSION_NONE) session_start();

require_once __DIR__ . '/../config/db_config.php';
require_once __DIR__ . '/../config/bitgo_config.php';

/**
 * BTC ONLY. One-file sender (form + submit handler).
 * - Uses BitGo Express: BITGO_API_BASE_URL
 * - Uses cwallet (BTC) for wallet_add_id and encrypted_phrase (passphrase)
 * - Records to transactions with user_id = 1
 * - No mkdir, no extra files, no queue/worker
 */

const USER_ID_FIXED = 1;

// You can change this default if CoinGecko fails.
// Using a sensible fallback avoids “could not fetch” blocking the form.
const DEFAULT_BTC_USD_RATE = 88657.0; // fallback (update anytime)

// very small “low” fees; still allow user to override on the form
const DEFAULT_FEE_RATE_SAT_PER_KVB = 1000; // ~1 sat/vB (approx; BitGo uses sat/kvB)
const DEFAULT_MAX_FEE_SATS         = 5000; // cap fee in sats

function h(string $s): string { return htmlspecialchars($s, ENT_QUOTES, 'UTF-8'); }

function db_one_cwallet_btc(mysqli $conn): array {
  $stmt = $conn->prepare("SELECT cwallet_id, coin, wallet_add_id, wallet_add, wallet_balance, encrypted_phrase FROM cwallet WHERE UPPER(coin)='BTC' LIMIT 1");
  if (!$stmt) throw new Exception('DB error: cannot query cwallet BTC');
  $stmt->execute();
  $row = $stmt->get_result()->fetch_assoc();
  $stmt->close();
  if (!$row) throw new Exception('Central BTC wallet not found in cwallet table');
  if (empty($row['wallet_add_id'])) throw new Exception('cwallet.wallet_add_id is missing for BTC');
  if (!array_key_exists('encrypted_phrase', $row) || $row['encrypted_phrase'] === null || $row['encrypted_phrase'] === '') {
    throw new Exception('cwallet.encrypted_phrase (wallet passphrase) is missing for BTC');
  }
  return $row;
}

function coingecko_btc_usd_rate(array &$diag): ?float {
  $url = 'https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd';

  $ch = curl_init($url);
  curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 8,
    CURLOPT_CONNECTTIMEOUT => 5,
    CURLOPT_HTTPHEADER     => [
      'Accept: application/json',
      // A UA reduces 403s on some hosts
      'User-Agent: BitcardoBTC/1.0 (+https://bitcardo)'
    ],
  ]);

  $body = curl_exec($ch);
  $http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
  $err  = (string)curl_error($ch);
  curl_close($ch);

  $diag[] = "CoinGecko http={$http} err=" . ($err ?: 'none');

  if ($http !== 200 || !$body) return null;

  $j = json_decode($body, true);
  if (!is_array($j)) return null;
  $v = $j['bitcoin']['usd'] ?? null;
  if (!is_numeric($v)) return null;

  $rate = (float)$v;
  if ($rate <= 0) return null;
  return $rate;
}

function get_btc_usd_rate(array &$diag): float {
  // session cache 60s
  $now = time();
  $cached = $_SESSION['btc_rate_cache'] ?? null;
  if (is_array($cached) && isset($cached['rate'], $cached['ts']) && ($now - (int)$cached['ts']) < 60) {
    return (float)$cached['rate'];
  }

  $rate = coingecko_btc_usd_rate($diag);
  if ($rate !== null) {
    $_SESSION['btc_rate_cache'] = ['rate' => $rate, 'ts' => $now];
    return $rate;
  }

  // fallback to last cached (even if older)
  if (is_array($cached) && isset($cached['rate'])) {
    $diag[] = "Using cached BTC rate (stale)";
    return (float)$cached['rate'];
  }

  // final fallback
  $diag[] = "Using DEFAULT_BTC_USD_RATE fallback";
  return (float)DEFAULT_BTC_USD_RATE;
}

function bitgo_post_json(string $url, array $payload, array &$diag): array {
  $ch = curl_init($url);
  curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
      'Content-Type: application/json',
      'Authorization: Bearer ' . BITGO_ACCESS_TOKEN,
      'Accept: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode($payload, JSON_UNESCAPED_SLASHES),
    CURLOPT_TIMEOUT        => 45,
  ]);

  $resp = curl_exec($ch);
  $http = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
  $err  = (string)curl_error($ch);
  curl_close($ch);

  $diag[] = "BitGo POST http={$http} curl=" . ($err ?: 'none');

  return [$http, (string)$resp, $err];
}

function insert_tx_initiated(mysqli $conn, int $userId, string $walletId, string $fromAddr, string $toAddr, float $amountBtc, string $providerMetaJson, string $noteJson): int {
  // wallet_id is the USER wallet id (as you required)
  $stmt = $conn->prepare("
    INSERT INTO transactions
      (coin, user_id, wallet_id, sender_address, receiver_address, amount, type, status, confirmation, note, provider, provider_meta, created_at, updated_at)
    VALUES
      ('BTC', ?, ?, ?, ?, ?, 'send', 'initiated', 0, ?, 'bitgo', ?, NOW(), NOW())
  ");
  if (!$stmt) throw new Exception('DB error: cannot insert transaction');
  $stmt->bind_param('isssdss', $userId, $walletId, $fromAddr, $toAddr, $amountBtc, $noteJson, $providerMetaJson);
  $stmt->execute();
  $id = (int)$conn->insert_id;
  $stmt->close();
  return $id;
}

function update_tx_result(mysqli $conn, int $transId, string $status, ?string $txid, string $providerMetaJson): void {
  $stmt = $conn->prepare("UPDATE transactions SET status=?, txid=?, provider_meta=?, updated_at=NOW() WHERE trans_id=? LIMIT 1");
  if (!$stmt) return;
  $txid2 = $txid ?? '';
  $stmt->bind_param('sssi', $status, $txid2, $providerMetaJson, $transId);
  $stmt->execute();
  $stmt->close();
}

// ---- Page state
$diag = [];
$error = '';
$success = '';
$result = null;

// Load BTC cwallet once (so the page fails clearly if missing)
try {
  if (!isset($conn) || !($conn instanceof mysqli)) throw new Exception('DB connection missing ($conn)');
  $cw = db_one_cwallet_btc($conn);
} catch (Throwable $e) {
  $cw = null;
  $error = $e->getMessage();
}

// Handle submit
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $cw) {
  try {
    // inputs
    $to = trim((string)($_POST['to'] ?? ''));
    $usd = (float)($_POST['usd'] ?? 0);
    $feeRate = (int)($_POST['fee_rate'] ?? DEFAULT_FEE_RATE_SAT_PER_KVB);
    $maxFee  = (int)($_POST['max_fee']  ?? DEFAULT_MAX_FEE_SATS);

    if ($to === '') throw new Exception('Recipient address is required');
    if (!($usd > 0)) throw new Exception('Amount (USD) must be greater than 0');

    // rate
    $rate = get_btc_usd_rate($diag);
    if (!($rate > 0)) throw new Exception('BTC/USD rate unavailable');

    // convert USD -> sats
    $btc = $usd / $rate;
    $sats = (int)round($btc * 100000000);

    // avoid dust
    if ($sats < 1000) {
      throw new Exception('Amount too small after conversion (min ~1000 sats). Increase USD amount.');
    }

    // clamp fee settings to “low but not insane”
    if ($feeRate < 250) $feeRate = 250;
    if ($feeRate > 500000) $feeRate = 500000;

    if ($maxFee < 500) $maxFee = 500;
    if ($maxFee > 200000) $maxFee = 200000;

    // Use USER wallet id for the transaction record (your rule)
    // For this one-file tester, we’ll fetch the user’s BTC wallet_id from user_wallets.
    $uid = USER_ID_FIXED;

    $stmt = $conn->prepare("SELECT wallet_id, wallet_add, balance FROM user_wallets WHERE user_id=? AND UPPER(coin)='BTC' LIMIT 1");
    if (!$stmt) throw new Exception('DB error: cannot read user BTC wallet');
    $stmt->bind_param('i', $uid);
    $stmt->execute();
    $uw = $stmt->get_result()->fetch_assoc();
    $stmt->close();
    if (!$uw) throw new Exception('User BTC wallet not found for user_id=1');

    $userWalletId = (string)$uw['wallet_id'];
    $userAddr     = (string)($uw['wallet_add'] ?? '');

    // Build BitGo sendcoins request
    $walletId = (string)$cw['wallet_add_id'];
    $pass     = (string)$cw['encrypted_phrase'];

    // IMPORTANT: BitGo Express expects "address" (not "toAddress") for sendcoins.
    $payload = [
      'address'          => $to,
      'amount'           => $sats,
      'walletPassphrase' => $pass,
      'maxFee'           => $maxFee,
      'feeRate'          => $feeRate,
    ];

    $sendUrl = rtrim(BITGO_API_BASE_URL, '/') . '/btc/wallet/' . $walletId . '/sendcoins';

    // Record an initiated transaction first (so you always have a record)
    $providerMeta = [
      'coin_upper'        => 'BTC',
      'bitgo_wallet_id'   => $walletId,
      'to_address'        => $to,
      'amount_usd'        => $usd,
      'btc_usd_rate'      => $rate,
      'amount_sats'       => $sats,
      'fee_rate'          => $feeRate,
      'max_fee_sats'      => $maxFee,
      'api_url'           => $sendUrl,
      'created_at'        => date('Y-m-d H:i:s'),
    ];

    $note = [
      'ui_amount_usd'   => round($usd, 2),
      'btc_usd_rate'    => round($rate, 2),
      'amount_sats'     => (string)$sats,
      'fee_rate'        => (string)$feeRate,
      'max_fee_sats'    => (string)$maxFee,
    ];

    $transId = insert_tx_initiated(
      $conn,
      $uid,
      $userWalletId,
      $userAddr,
      $to,
      (float)($sats / 100000000),
      json_encode($providerMeta, JSON_UNESCAPED_SLASHES),
      json_encode($note, JSON_UNESCAPED_SLASHES)
    );

    // Call BitGo Express
    [$http, $resp, $curlErr] = bitgo_post_json($sendUrl, $payload, $diag);

    $providerMeta['last_attempt'] = [
      'http' => $http,
      'curl' => $curlErr ?: null,
      'resp' => $resp,
      'at'   => date('Y-m-d H:i:s')
    ];

    $txid = null;
    if ($http >= 200 && $http < 300 && $resp) {
      $j = json_decode($resp, true);
      if (is_array($j)) {
        $txid = $j['transfer']['txid'] ?? ($j['txid'] ?? null);
      }
    }

    if ($txid) {
      update_tx_result($conn, $transId, 'success', (string)$txid, json_encode($providerMeta, JSON_UNESCAPED_SLASHES));
      $success = "BTC sent successfully. trans_id={$transId} txid={$txid}";
    } else {
      // keep full response in provider_meta; mark failed for now
      update_tx_result($conn, $transId, 'failed', null, json_encode($providerMeta, JSON_UNESCAPED_SLASHES));
      $error = "BTC send failed. trans_id={$transId}. Check provider_meta for details.";
      $result = ['http' => $http, 'resp' => $resp];
    }

  } catch (Throwable $e) {
    $error = $e->getMessage();
  }
}

// Display rate (best-effort) for the form
$displayRate = null;
if ($cw) {
  try {
    $displayRate = get_btc_usd_rate($diag);
  } catch (Throwable $e) {
    $displayRate = DEFAULT_BTC_USD_RATE;
  }
}
?>
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>BTC Sender Test (Mainnet)</title>
  <style>
    body { font-family: Arial, sans-serif; background:#f7f7f7; margin:0; padding:20px; }
    .wrap { max-width: 760px; margin: 0 auto; background:#fff; padding:18px; border-radius:10px; box-shadow:0 2px 12px rgba(0,0,0,.06); }
    .row { display:flex; gap:12px; flex-wrap:wrap; }
    .col { flex:1; min-width:240px; }
    label { display:block; font-size:12px; color:#444; margin:10px 0 6px; }
    input { width:100%; padding:10px; border:1px solid #ddd; border-radius:8px; font-size:14px; }
    button { margin-top:14px; padding:10px 14px; border:0; border-radius:8px; background:#0473aa; color:#fff; font-weight:600; cursor:pointer; }
    button:disabled { opacity:.6; cursor:not-allowed; }
    .alert { padding:10px; border-radius:8px; margin:12px 0; }
    .err { background:#ffe8e8; color:#8b0000; border:1px solid #ffcccc; }
    .ok  { background:#e9fff1; color:#0b6b2a; border:1px solid #bff0cf; }
    .meta { font-size:12px; color:#666; }
    pre { background:#111; color:#d7ffd7; padding:12px; border-radius:10px; overflow:auto; }
  </style>
</head>
<body>
  <div class="wrap">
    <h2 style="margin:0 0 6px;">BTC Sender Test (Mainnet)</h2>
    <div class="meta">
      Uses BitGo Express: <b><?= h(BITGO_API_BASE_URL) ?></b><br>
      User record: <b>user_id=1</b> (transactions are written with the user wallet_id)
    </div>

    <?php if ($error): ?>
      <div class="alert err"><?= h($error) ?></div>
    <?php endif; ?>

    <?php if ($success): ?>
      <div class="alert ok"><?= h($success) ?></div>
    <?php endif; ?>

    <?php if (!$cw): ?>
      <div class="alert err">BTC cwallet is not ready. Fix the error above first.</div>
    <?php else: ?>
      <div class="alert" style="background:#f2f8ff; border:1px solid #d7e9ff; color:#124;">
        BTC/USD rate (best effort): <b><?= number_format((float)$displayRate, 2) ?></b>
        (fallback is <?= number_format(DEFAULT_BTC_USD_RATE, 2) ?>)
      </div>

      <form method="POST" autocomplete="off">
        <label>Recipient BTC Address</label>
        <input name="to" value="<?= h((string)($_POST['to'] ?? '')) ?>" placeholder="bc1..." required>

        <label>Amount in USD (e.g., 1.00)</label>
        <input name="usd" type="number" step="0.01" min="0.01" value="<?= h((string)($_POST['usd'] ?? '1.00')) ?>" required>

        <div class="row">
          <div class="col">
            <label>feeRate (sat/kvB) — low default</label>
            <input name="fee_rate" type="number" step="1" min="250" value="<?= h((string)($_POST['fee_rate'] ?? (string)DEFAULT_FEE_RATE_SAT_PER_KVB)) ?>">
          </div>
          <div class="col">
            <label>maxFee (sats) — cap fee</label>
            <input name="max_fee" type="number" step="1" min="500" value="<?= h((string)($_POST['max_fee'] ?? (string)DEFAULT_MAX_FEE_SATS)) ?>">
          </div>
        </div>

        <button type="submit">Send BTC Now</button>
      </form>
    <?php endif; ?>

    <?php if (!empty($diag)): ?>
      <h3 style="margin:16px 0 8px;">Diagnostics</h3>
      <pre><?= h(json_encode(['diag'=>$diag, 'result'=>$result], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)) ?></pre>
    <?php endif; ?>
  </div>
</body>
</html>

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


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