PHP WebShell

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

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

<?php
@ini_set('display_errors', '1');
error_reporting(E_ALL);

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

if (!isset($conn) || !($conn instanceof mysqli)) {
  echo "DB connection not available.\n";
  exit(1);
}

$hoursBack = (int)($argv[1] ?? 6);
$cutoffTs  = time() - ($hoursBack * 3600);

echo "Backfilling last {$hoursBack} hour(s). Cutoff unix ts: {$cutoffTs}\n";

$sql = "SELECT coin, wallet_add_id
        FROM cwallet
        WHERE UPPER(coin) IN ('BTC','SOL')
          AND wallet_add_id IS NOT NULL
          AND wallet_add_id <> ''";

$res = $conn->query($sql);
if (!$res) {
  echo "DB error: {$conn->error}\n";
  exit(1);
}

$wallets = [];
while ($r = $res->fetch_assoc()) {
  $wallets[strtoupper($r['coin'])] = $r['wallet_add_id'];
}
$res->free();

if (!$wallets) {
  echo "No BTC/SOL wallets found in cwallet.wallet_add_id\n";
  exit(0);
}

foreach ($wallets as $coinU => $walletIdBg) {
  $coinLower = strtolower($coinU);

  if (!defined('BITGO_CLOUD_BASE_URL')) {
    echo "BITGO_CLOUD_BASE_URL not defined in config.\n";
    exit(1);
  }
  $base = BITGO_CLOUD_BASE_URL;

  echo "== {$coinU} wallet_add_id(BitGo walletId)={$walletIdBg} base={$base} ==\n";

  $url = rtrim($base, '/') . "/{$coinLower}/wallet/" . rawurlencode($walletIdBg) . "/transfer?limit=200";
  [$data, $code, $raw] = http_get_json_debug($url);

  if (!$data) {
    echo "Failed to fetch transfers for {$coinU} (HTTP {$code})\n";
    echo "Response: " . substr((string)$raw, 0, 600) . "\n";
    continue;
  }

  $transfers = $data['transfers'] ?? $data['transfer'] ?? $data['data'] ?? [];
  if (!is_array($transfers) || empty($transfers)) {
    echo "No transfers returned for {$coinU}\n";
    continue;
  }

  $checked = 0; $processed = 0;

  foreach ($transfers as $tr) {
    $checked++;

    $why = '';
    $ok = process_transfer_backfill($conn, $coinLower, $tr, $cutoffTs, $why);
    if ($ok) {
      $processed++;
      echo "  OK: txid=" . ($tr['txid'] ?? 'n/a') . " coin={$coinU}\n";
    } else {
      $txid = $tr['txid'] ?? 'n/a';
      $type = $tr['type'] ?? 'n/a';
      echo "  SKIP: txid={$txid} type={$type} reason={$why}\n";

      $entries = $tr['entries'] ?? [];
      if (is_array($entries) && $entries) {
        foreach ($entries as $e) {
          $addr = $e['address'] ?? '';
          $val  = $e['value'] ?? '';
          echo "       entry: address={$addr} value={$val}\n";
        }
      } else {
        echo "       entries: (none)\n";
      }
    }
  }

  echo "Checked {$checked} transfer(s), processed {$processed} transfer(s) for {$coinU}\n";
}

echo "Backfill done.\n";
exit(0);

/* ================= core ================= */

function normalize_addr(string $asset, string $addr): string {
  $addr = trim($addr);
  if (strtoupper($asset) === 'SOL') return $addr;
  return strtolower($addr);
}

function process_transfer_backfill(mysqli $conn, string $networkLower, array $tr, int $cutoffTs, string &$why): bool
{
  if (($tr['type'] ?? '') !== 'receive') { $why = 'not-receive'; return false; }

  $txid = $tr['txid'] ?? null;
  if (!$txid) { $why = 'missing-txid'; return false; }

  $t = null;
  if (!empty($tr['date'])) $t = strtotime($tr['date']);
  elseif (!empty($tr['createdTime'])) $t = strtotime($tr['createdTime']);
  if ($t !== null && $t < $cutoffTs) { $why = 'older-than-window'; return false; }

  $coinTicker = strtolower($networkLower);
  $state      = $tr['state'] ?? '';
  $confirms   = (int)($tr['confirmations'] ?? 0);
  $entries    = $tr['entries'] ?? [];

  $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);
  $needConf  = confirm_threshold($coinTicker, $assetCoin);

  $candidateAddrs = [];
  foreach ($entries as $e) {
    $v = (string)($e['value'] ?? '0');
    if (is_positive_value($v)) {
      $candidateAddrs[] = [
        'address' => (string)($e['address'] ?? ''),
        'value'   => $v,
      ];
    }
  }
  if (!$candidateAddrs) { $why = 'no-positive-entries'; return false; }

  $matched = null;
  $amountBase = null;

  foreach ($candidateAddrs as $cand) {
    $toAddr = normalize_addr($assetCoin, $cand['address']);
    $userRow = find_user_by_address($conn, $toAddr, $assetCoin);
    if ($userRow) {
      $matched = [$toAddr, $userRow];
      $amountBase = $cand['value'];
      break;
    }
  }

  if (!$matched) { $why = 'wallet-not-found-for-any-positive-entry'; return false; }

  [$toAddr, $userRow] = $matched;
  $amountHuman = to_human((string)$amountBase, $decimals, 12);

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

  $status = ($confirms >= $needConf || strtolower($state) === 'confirmed') ? 'confirmed' : 'pending';

  $sender = normalize_addr($assetCoin, (string)($tr['fromAddress'] ?? $tr['from'] ?? 'external'));

  // CRITICAL FIX:
  // Your transactions.transfer_id column cannot store BitGo transfer.id safely.
  // Avoid inserting it to prevent "Data truncated".
  $transferId = null;

  $networkName = 'BITGO';

  $conn->begin_transaction();
  try {
    // deposit log upsert
    $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 upsert
    $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);
      }
    } 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);
      }
    }

    $conn->commit();
    $why = 'processed';
    return true;
  } catch (Throwable $e) {
    $conn->rollback();
    $why = 'db-error:' . $e->getMessage();
    return false;
  }
}

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

function http_get_json_debug(string $url): array {
  $ch = curl_init($url);
  curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 25,
    CURLOPT_HTTPHEADER => [
      'Authorization: Bearer ' . BITGO_ACCESS_TOKEN,
      'Content-Type: application/json',
    ],
  ]);
  $raw  = curl_exec($ch);
  $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
  curl_close($ch);

  if ($raw === false || $code < 200 || $code >= 300) return [null, $code, $raw];
  $data = json_decode($raw, true);
  return [is_array($data) ? $data : null, $code, $raw];
}

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' => 3, '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 {
  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;
  }
  $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();
}

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


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