PHP WebShell

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

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

<?php
/**
 * BitGo "transfer" webhook handler (multi-coin).
 * Schema aligned to your DB:
 * - user_wallets.wallet_id INT
 * - transactions.wallet_id VARCHAR(255) (store user wallet_id as string)
 * - crypto_deposit_log for admin
 */

@ini_set('display_errors', '0');
header('Content-Type: application/json');

const LOG_MAX_LEN = 20000;
$LOG_FILE = __DIR__ . '/bitgo_transfer.log';

function log_event(string $level, string $message, array $ctx = []): void {
  global $LOG_FILE;
  $line = [
    'ts'  => gmdate('c'),
    'lvl' => $level,
    'ip'  => $_SERVER['REMOTE_ADDR'] ?? 'n/a',
    'ua'  => $_SERVER['HTTP_USER_AGENT'] ?? 'n/a',
    'msg' => $message,
    'ctx' => $ctx,
  ];
  @file_put_contents($LOG_FILE, json_encode($line, JSON_UNESCAPED_SLASHES) . PHP_EOL, FILE_APPEND | LOCK_EX);
}

$DB_CFG = __DIR__ . '/../config/db_config.php';
$BG_CFG = __DIR__ . '/../config/bitgo_config.php';

if (!file_exists($DB_CFG) || !file_exists($BG_CFG)) {
  http_response_code(500);
  echo json_encode(['error' => 'config-missing']);
  exit;
}
require_once $DB_CFG;
require_once $BG_CFG;

if (!isset($conn) || !($conn instanceof mysqli)) {
  http_response_code(500);
  echo json_encode(['error' => 'db-conn']);
  exit;
}

$raw   = file_get_contents('php://input') ?: '';
$event = json_decode($raw, true);
log_event('hit', 'incoming webhook', ['len' => strlen($raw), 'preview' => substr($raw, 0, LOG_MAX_LEN)]);

if (!$event || empty($event['transfer'])) {
  http_response_code(200);
  echo json_encode(['ok' => true, 'ignored' => 'no-transfer']);
  exit;
}

// ---- Signature verify (BitGo uses X-Signature-SHA256 hex) ----
if (defined('BITGO_WEBHOOK_SECRET') && BITGO_WEBHOOK_SECRET !== '') {
  $sig = $_SERVER['HTTP_X_SIGNATURE_SHA256'] ?? '';
  if ($sig === '') {
    log_event('warn', 'missing signature header');
    http_response_code(401);
    echo json_encode(['error' => 'missing-signature']);
    exit;
  }
  $calc = hash_hmac('sha256', $raw, BITGO_WEBHOOK_SECRET); // hex
  if (!hash_equals($calc, $sig)) {
    log_event('warn', 'invalid signature', ['calc' => $calc, 'recv' => $sig]);
    http_response_code(401);
    echo json_encode(['error' => 'invalid-signature']);
    exit;
  }
}

$tr         = $event['transfer'];
$coinTicker = strtolower($tr['coin'] ?? '');
$txid       = $tr['txid'] ?? null;
$type       = $tr['type'] ?? '';
$state      = $tr['state'] ?? '';
$confirms   = (int)($tr['confirmations'] ?? 0);
$entries    = $tr['entries'] ?? [];

if ($type !== 'receive' || !$txid) {
  http_response_code(200);
  echo json_encode(['ok' => true, 'ignored' => 'not-receive-or-missing-txid']);
  exit;
}

$tokenSymbol   = strtoupper($tr['token'] ?? ($tr['tokenInfo']['symbol'] ?? ''));
$tokenDecimals = isset($tr['tokenInfo']['decimals']) ? (int)$tr['tokenInfo']['decimals'] : null;
$assetCoin     = strtoupper($tokenSymbol ?: $coinTicker);

$decimals    = coin_decimals($coinTicker, $assetCoin, $tokenDecimals);
$confirmNeed = confirm_threshold($coinTicker, $assetCoin);

// pick first positive entry
$toAddrRaw = null; $amountBase = '0';
foreach ($entries as $e) {
  $v = (string)($e['value'] ?? '0');
  if (is_positive_value($v)) {
    $toAddrRaw  = (string)($e['address'] ?? '');
    $amountBase = $v;
    break;
  }
}
if (!$toAddrRaw) {
  http_response_code(200);
  echo json_encode(['ok' => true, 'ignored' => 'no-positive-entry']);
  exit;
}

$toAddr = normalize_addr($assetCoin, $toAddrRaw);     // IMPORTANT: SOL stays case-sensitive
$amountHuman = to_human($amountBase, $decimals, 12);

// map address -> user wallet
$userRow = find_user_by_address($conn, $toAddr, $assetCoin);
if (!$userRow) {
  log_event('ignored', 'wallet not found', ['addr' => $toAddr, 'coin' => $assetCoin, 'txid' => $txid]);
  http_response_code(200);
  echo json_encode(['ok' => true, 'ignored' => 'wallet-not-found']);
  exit;
}

$userId     = (int)$userRow['user_id'];
$userWalId  = (int)$userRow['wallet_id'];
$uwAddr     = $userRow['wallet_add']; // stored format
$txWalIdStr = (string)$userWalId;

$status     = ($confirms >= $confirmNeed || strtolower($state) === 'confirmed') ? 'confirmed' : 'pending';
$applied    = 0;
$transferId = null;
$senderRaw  = (string)($tr['fromAddress'] ?? $tr['from'] ?? 'external');
$sender     = normalize_addr($assetCoin, $senderRaw);

$networkName = 'BITGO';

$conn->begin_transaction();
try {
  // --- deposit log (admin) ---
  $logRow = find_deposit_log($conn, $networkName, $assetCoin, $txid);
  if ($logRow) {
    if ($confirms > (int)$logRow['confirmations']) {
      $stmt = $conn->prepare("UPDATE crypto_deposit_log SET confirmations=?, updated_at=NOW() WHERE id=?");
      $logId = (int)$logRow['id'];
      $stmt->bind_param('ii', $confirms, $logId);
      $stmt->execute();
      $stmt->close();
    }
  } else {
    $blockNumInt  = 0;
    $amountRawStr = (string)$amountBase;

    $stmt = $conn->prepare("
      INSERT INTO crypto_deposit_log
        (network, coin, user_id, wallet_id, wallet_add, txid, block_num,
         amount_raw, amount_dec, confirmations, credited, created_at, updated_at)
      VALUES
        (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, NOW(), NOW())
    ");
    $stmt->bind_param(
      'ssiississi',
      $networkName,
      $assetCoin,
      $userId,
      $userWalId,
      $uwAddr,
      $txid,
      $blockNumInt,
      $amountRawStr,
      $amountHuman,
      $confirms
    );
    $stmt->execute();
    $stmt->close();
  }

  // --- transactions (user) ---
  $existing = find_tx($conn, $txid, $toAddr, $assetCoin);

  if ($existing) {
    $transId = (int)$existing['trans_id'];
    $stmt = $conn->prepare("UPDATE transactions SET confirmation=?, status=?, updated_at=NOW() WHERE trans_id=?");
    $stmt->bind_param('isi', $confirms, $status, $transId);
    $stmt->execute();
    $stmt->close();

    $alreadyApplied = (int)$existing['applied'] === 1;
    if (!$alreadyApplied && $status === 'confirmed') {
      credit_user_wallet($conn, $userId, $assetCoin, $uwAddr, $amountHuman);
      mark_applied($conn, $transId);
      mark_deposit_log_credited($conn, $networkName, $assetCoin, $txid, $confirms);
      $applied = 1;
    }
  } else {
    $stmt = $conn->prepare("
      INSERT INTO transactions
        (coin, user_id, wallet_id, transfer_id, sender_address, receiver_address,
         amount, type, txid, reference, provider, provider_meta, confirmation, status, applied, created_at, updated_at)
      VALUES
        (?, ?, ?, ?, ?, ?, ?, 'deposit', ?, NULL, 'bitgo', NULL, ?, ?, 0, NOW(), NOW())
    ");
    $stmt->bind_param(
      'sissssssis',
      $assetCoin,
      $userId,
      $txWalIdStr,
      $transferId,
      $sender,
      $toAddr,
      $amountHuman,
      $txid,
      $confirms,
      $status
    );
    $stmt->execute();
    $newId = (int)$stmt->insert_id;
    $stmt->close();

    if ($status === 'confirmed') {
      credit_user_wallet($conn, $userId, $assetCoin, $uwAddr, $amountHuman);
      mark_applied($conn, $newId);
      mark_deposit_log_credited($conn, $networkName, $assetCoin, $txid, $confirms);
      $applied = 1;
    }
  }

  $conn->commit();
} catch (Throwable $e) {
  $conn->rollback();
  log_event('error', 'db-failure', ['txid' => $txid, 'err' => $e->getMessage()]);
  http_response_code(500);
  echo json_encode(['error' => 'db-failure']);
  exit;
}

http_response_code(200);
echo json_encode([
  'ok' => true,
  'coin' => $assetCoin,
  'address' => $uwAddr,
  'amount' => $amountHuman,
  'confirmations' => $confirms,
  'required_conf' => $confirmNeed,
  'status' => $status,
  'applied' => $applied
]);
exit;

/* ================= helpers ================= */

function normalize_addr(string $asset, string $addr): string {
  $addr = trim($addr);
  // SOL base58 is case-sensitive -> DO NOT lowercase.
  if (strtoupper($asset) === 'SOL') return $addr;
  return strtolower($addr);
}

function is_positive_value(string $v): bool {
  if (function_exists('bccomp')) return bccomp($v, '0', 0) === 1;
  return (float)$v > 0.0;
}

function coin_decimals(string $network, string $asset, ?int $tokenDecimals): int {
  $network = strtolower($network);
  if ($tokenDecimals !== null) return $tokenDecimals;
  return match ($network) { 'btc' => 8, 'eth' => 18, 'sol' => 9, 'trx' => 6, default => 8 };
}

function confirm_threshold(string $network, string $asset): int {
  if (function_exists('bitgo_required_confirmations')) {
    return (int) bitgo_required_confirmations($network, $asset);
  }
  $map = ['BTC' => 2, 'ETH' => 12, 'SOL' => 32, 'TRX' => 19];
  $assetU = strtoupper($asset);
  return $map[$assetU] ?? 1;
}

function to_human(string $valueBase, int $decimals, int $scale = 12): string {
  if (!function_exists('bcscale')) {
    $div = 1;
    for ($i = 0; $i < $decimals; $i++) $div *= 10;
    return number_format(((float)$valueBase) / $div, min($scale, $decimals), '.', '');
  }
  bcscale($scale);
  $div = bcpow('10', (string)$decimals);
  $res = bcdiv($valueBase, $div, $scale);
  return rtrim(rtrim($res, '0'), '.') ?: '0';
}

function wallet_scale_for_asset(string $asset): int {
  return match (strtoupper($asset)) {
    'BTC' => 8, 'TRX' => 6, 'SOL' => 9, 'ETH' => 10, default => 10
  };
}

function decimal_clause_for_scale(int $scale): string {
  $s = max(0, min(18, $scale));
  return "DECIMAL(30,$s)";
}

function find_user_by_address(mysqli $conn, string $addrNorm, string $asset): ?array {
  // SOL match must be case-sensitive (base58)
  if (strtoupper($asset) === 'SOL') {
    $sql = "SELECT user_id, wallet_id, wallet_add
              FROM user_wallets
             WHERE BINARY TRIM(wallet_add) = BINARY TRIM(?)
               AND UPPER(coin) = UPPER(?)
             LIMIT 1";
    $stmt = $conn->prepare($sql);
    $stmt->bind_param('ss', $addrNorm, $asset);
    $stmt->execute();
    $res = $stmt->get_result();
    $row = ($res && $res->num_rows) ? $res->fetch_assoc() : null;
    $stmt->close();
    return $row;
  }

  // Others: case-insensitive
  $sql = "SELECT user_id, wallet_id, wallet_add
            FROM user_wallets
           WHERE TRIM(LOWER(wallet_add)) = TRIM(LOWER(?))
             AND UPPER(coin) = UPPER(?)
           LIMIT 1";
  $stmt = $conn->prepare($sql);
  $stmt->bind_param('ss', $addrNorm, $asset);
  $stmt->execute();
  $res = $stmt->get_result();
  $row = ($res && $res->num_rows) ? $res->fetch_assoc() : null;
  $stmt->close();
  return $row;
}

function find_tx(mysqli $conn, string $txid, string $receiverNorm, string $asset): ?array {
  $sql = "SELECT trans_id, applied
            FROM transactions
           WHERE txid = ? AND receiver_address = ? AND coin = ?
           LIMIT 1";
  $stmt = $conn->prepare($sql);
  $stmt->bind_param('sss', $txid, $receiverNorm, $asset);
  $stmt->execute();
  $res = $stmt->get_result();
  $row = ($res && $res->num_rows) ? $res->fetch_assoc() : null;
  $stmt->close();
  return $row;
}

function credit_user_wallet(mysqli $conn, int $userId, string $asset, string $addrStored, string $amountHuman): void {
  $scale = wallet_scale_for_asset($asset);
  $dec   = decimal_clause_for_scale($scale);
  $sql = "
    UPDATE user_wallets
       SET balance    = ROUND((CAST(balance AS $dec) + CAST(? AS $dec)), $scale),
           updated_at = NOW()
     WHERE user_id = ? AND coin = ? AND wallet_add = ?
     LIMIT 1
  ";
  $stmt = $conn->prepare($sql);
  $stmt->bind_param('siss', $amountHuman, $userId, $asset, $addrStored);
  $stmt->execute();
  $stmt->close();
}

function mark_applied(mysqli $conn, int $transId): void {
  $stmt = $conn->prepare("UPDATE transactions SET applied=1, updated_at=NOW() WHERE trans_id=?");
  $stmt->bind_param('i', $transId);
  $stmt->execute();
  $stmt->close();
}

function find_deposit_log(mysqli $conn, string $network, string $coin, string $txid): ?array {
  $sql = "SELECT id, credited, confirmations
          FROM crypto_deposit_log
          WHERE network=? AND coin=? AND txid=?
          LIMIT 1";
  $stmt = $conn->prepare($sql);
  $stmt->bind_param('sss', $network, $coin, $txid);
  $stmt->execute();
  $res = $stmt->get_result();
  $row = ($res && $res->num_rows) ? $res->fetch_assoc() : null;
  $stmt->close();
  return $row;
}

function mark_deposit_log_credited(mysqli $conn, string $network, string $coin, string $txid, int $confirmations): void {
  $stmt = $conn->prepare("
    UPDATE crypto_deposit_log
    SET credited=1, confirmations=?, updated_at=NOW()
    WHERE network=? AND coin=? AND txid=?
  ");
  $stmt->bind_param('isss', $confirmations, $network, $coin, $txid);
  $stmt->execute();
  $stmt->close();
}

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


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