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];
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!