PHP WebShell

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

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

<?php
// /models/crypto/bitgo_client.php
declare(strict_types=1);

/**
 * BitGo HTTP client helper (cURL)
 * - BTC uses BITGO_API_BASE_URL (your local Express)
 * - SOL uses BITGO_CLOUD_BASE_URL (BitGo Cloud)
 * - TRX/USDT-TRC20 are NOT handled here (TronLink flow) — do not use this for TRX.
 *
 * Usage (example):
 *   require_once __DIR__ . '/../../config/bitgo_config.php';
 *   require_once __DIR__ . '/bitgo_client.php';
 *   $wallet = bitgo_get_wallet('BTC', $walletId);
 */

if (!function_exists('bitgo_coin_key')) {
  function bitgo_coin_key(string $coinUpper): string {
    $c = strtoupper(trim($coinUpper));
    // Map your UI coins to BitGo coin keys.
    // If your Express/cloud uses different keys, adjust here once and all callers benefit.
    return match ($c) {
      'BTC' => 'btc',
      'SOL' => 'sol',
      default => strtolower($c),
    };
  }
}

if (!function_exists('bitgo_api_base_for_coin')) {
  function bitgo_api_base_for_coin(string $coinUpper): string {
    // Prefer bitgo_config.php helper if present; otherwise fallback.
    if (function_exists('bitgo_base_url_for_coin')) {
      return bitgo_base_url_for_coin($coinUpper);
    }
    // Sensible fallback: SOL -> cloud, others -> express
    return (strtoupper($coinUpper) === 'SOL') ? (defined('BITGO_CLOUD_BASE_URL') ? BITGO_CLOUD_BASE_URL : '')
                                              : (defined('BITGO_API_BASE_URL') ? BITGO_API_BASE_URL : '');
  }
}

if (!function_exists('bitgo_request')) {
  /**
   * @return array{ok:bool, code:int, data?:mixed, error?:string, raw?:string}
   */
  function bitgo_request(string $method, string $coinUpper, string $path, ?array $payload = null, int $timeout = 45): array {
    if (!defined('BITGO_ACCESS_TOKEN')) {
      return ['ok'=>false, 'code'=>500, 'error'=>'BITGO_ACCESS_TOKEN not defined'];
    }

    $base = rtrim((string)bitgo_api_base_for_coin($coinUpper), '/');
    if ($base === '') {
      return ['ok'=>false, 'code'=>500, 'error'=>'BitGo base URL missing for coin '.$coinUpper];
    }

    $coinKey = bitgo_coin_key($coinUpper);
    $path = ltrim($path, '/');

    // Allow caller to pass paths with or without /{coinKey}/ prefix.
    // If path already begins with "{coinKey}/", do not double-prefix.
    if (!preg_match('~^' . preg_quote($coinKey, '~') . '/~', $path)) {
      $url = $base . '/' . $coinKey . '/' . $path;
    } else {
      $url = $base . '/' . $path;
    }

    $ch = curl_init($url);
    if ($ch === false) {
      return ['ok'=>false, 'code'=>500, 'error'=>'Failed to init curl'];
    }

    $headers = [
      'Accept: application/json',
      'Content-Type: application/json',
      'Authorization: Bearer ' . BITGO_ACCESS_TOKEN,
    ];

    curl_setopt_array($ch, [
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HEADER         => true,
      CURLOPT_CUSTOMREQUEST  => strtoupper($method),
      CURLOPT_HTTPHEADER     => $headers,
      CURLOPT_TIMEOUT        => $timeout,
    ]);

    if ($payload !== null) {
      $json = json_encode($payload, JSON_UNESCAPED_SLASHES);
      if ($json === false) {
        curl_close($ch);
        return ['ok'=>false, 'code'=>500, 'error'=>'Failed to encode JSON payload'];
      }
      curl_setopt($ch, CURLOPT_POSTFIELDS, $json);
    }

    $resp = curl_exec($ch);
    if ($resp === false) {
      $err = curl_error($ch);
      $no  = curl_errno($ch);
      curl_close($ch);
      return ['ok'=>false, 'code'=>0, 'error'=>"cURL error ({$no}): {$err}"];
    }

    $status     = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $headerSize = (int)curl_getinfo($ch, CURLINFO_HEADER_SIZE);
    curl_close($ch);

    $rawBody = substr($resp, $headerSize);
    $decoded = json_decode($rawBody, true);

    if (!is_array($decoded) && !is_object($decoded)) {
      // Some BitGo errors still return JSON but can be malformed if proxied; keep raw for diagnostics.
      return [
        'ok'   => false,
        'code' => $status ?: 500,
        'error'=> 'Invalid JSON from BitGo',
        'raw'  => $rawBody,
      ];
    }

    if ($status >= 200 && $status < 300) {
      return ['ok'=>true, 'code'=>$status, 'data'=>$decoded, 'raw'=>$rawBody];
    }

    // Extract best-effort error message
    $msg = null;
    if (is_array($decoded)) {
      $msg = $decoded['error'] ?? $decoded['message'] ?? $decoded['name'] ?? null;
    }
    if (!$msg) $msg = 'BitGo request failed';

    return ['ok'=>false, 'code'=>$status ?: 500, 'error'=>$msg, 'data'=>$decoded, 'raw'=>$rawBody];
  }
}

if (!function_exists('bitgo_get_wallet')) {
  function bitgo_get_wallet(string $coinUpper, string $walletId): array {
    return bitgo_request('GET', $coinUpper, 'wallet/' . rawurlencode($walletId));
  }
}

if (!function_exists('bitgo_build_tx')) {
  /**
   * Builds an unsigned tx (or prebuild) depending on coin/wallet policy.
   * Typical payload:
   *  [
   *    'recipients' => [['address' => '...', 'amount' => '12345']], // satoshis/lamports etc depending on coin
   *    'numBlocks' => 2, // optional
   *    'feeRate' => ...  // optional
   *  ]
   */
  function bitgo_build_tx(string $coinUpper, string $walletId, array $payload): array {
    return bitgo_request('POST', $coinUpper, 'wallet/' . rawurlencode($walletId) . '/tx/build', $payload);
  }
}

if (!function_exists('bitgo_send_tx')) {
  /**
   * Sends tx through BitGo wallet.
   * Typical payload:
   *  [
   *    'recipients' => [['address' => '...', 'amount' => '12345']],
   *    'walletPassphrase' => '...',
   *    'comment' => '...',
   *  ]
   */
  function bitgo_send_tx(string $coinUpper, string $walletId, array $payload): array {
    return bitgo_request('POST', $coinUpper, 'wallet/' . rawurlencode($walletId) . '/send', $payload);
  }
}

if (!function_exists('bitgo_extract_fee_from_build')) {
  /**
   * Robust fee parser for BitGo build/send responses.
   * Returns fee in base units (string) if found, else null.
   */
  function bitgo_extract_fee_from_build($data): ?string {
    if (!is_array($data)) return null;

    // Common locations (varies across coins and BitGo versions)
    $candidates = [
      $data['feeInfo']['fee']            ?? null,
      $data['feeInfo']['feeString']      ?? null,
      $data['fee']                       ?? null,
      $data['feeString']                 ?? null,
      $data['txInfo']['fee']             ?? null,
      $data['txInfo']['feeString']       ?? null,
      $data['transfer']['fee']           ?? null,
      $data['transfer']['feeString']     ?? null,
      $data['prebuild']['feeInfo']['fee']       ?? null,
      $data['prebuild']['feeInfo']['feeString'] ?? null,
      $data['prebuild']['fee']                 ?? null,
      $data['prebuild']['feeString']           ?? null,
    ];

    foreach ($candidates as $v) {
      if ($v === null) continue;
      if (is_int($v) || is_float($v)) return (string)$v;
      if (is_string($v) && $v !== '') return $v;
    }

    // Some builds provide fee as array of options
    // e.g. feeInfo: { fee: { low: '...', standard:'...', high:'...' } }
    if (isset($data['feeInfo']['fee']) && is_array($data['feeInfo']['fee'])) {
      foreach (['standard','medium','low','high'] as $k) {
        if (isset($data['feeInfo']['fee'][$k])) return (string)$data['feeInfo']['fee'][$k];
      }
      // take first numeric-ish
      foreach ($data['feeInfo']['fee'] as $vv) {
        if (is_string($vv) && $vv !== '') return $vv;
        if (is_int($vv) || is_float($vv)) return (string)$vv;
      }
    }

    return null;
  }
}

if (!function_exists('bitgo_estimate_withdraw_fee')) {
  /**
   * Convenience: build tx and extract fee. Does NOT send.
   * Returns:
   *  ['ok'=>true, 'fee'=> '...', 'build'=> <full build response>]
   *  or ['ok'=>false, 'error'=>..., 'diag'=>...]
   */
  function bitgo_estimate_withdraw_fee(string $coinUpper, string $walletId, array $buildPayload): array {
    $res = bitgo_build_tx($coinUpper, $walletId, $buildPayload);
    if (!$res['ok']) {
      return [
        'ok'   => false,
        'error'=> $res['error'] ?? 'Build failed',
        'code' => $res['code'] ?? 500,
        'diag' => $res['data'] ?? null,
      ];
    }

    $data = $res['data'];
    $fee  = bitgo_extract_fee_from_build(is_array($data) ? $data : null);

    if ($fee === null) {
      return [
        'ok'   => false,
        'error'=> 'Build did not return fee',
        'code' => 502,
        'diag' => $data, // keep full build response for troubleshooting
      ];
    }

    return ['ok'=>true, 'fee'=>$fee, 'build'=>$data];
  }
}

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


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