PHP WebShell
Текущая директория: /var/www/bitcardoApp/models/fees
Просмотр файла: estimate_withdraw_fee.php
<?php
// models/fees/estimate_withdraw_fee.php
declare(strict_types=1);
@ini_set('display_errors', '0');
header('Content-Type: application/json');
session_start();
require_once __DIR__ . '/../../config/db_config.php';
require_once __DIR__ . '/../../config/bitgo_config.php';
function json_out(int $code, array $data) {
http_response_code($code);
echo json_encode($data, JSON_UNESCAPED_SLASHES);
exit;
}
if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'POST') {
json_out(405, ['ok'=>false, 'error'=>'method_not_allowed']);
}
if (empty($_SESSION['user_id'])) {
json_out(401, ['ok'=>false, 'error'=>'unauthorized']);
}
$userId = (int)$_SESSION['user_id'];
/* ---------------- site_settings helpers ---------------- */
function get_site_flag(mysqli $conn, string $key, int $default = 1): int {
$val = null;
$stmt = $conn->prepare("SELECT setting_value FROM site_settings WHERE setting_key = ? LIMIT 1");
if ($stmt) {
$stmt->bind_param('s', $key);
$stmt->execute();
$stmt->bind_result($v);
if ($stmt->fetch()) $val = $v;
$stmt->close();
}
if ($val === null) return $default;
$val = is_numeric($val) ? (int)$val : (strtolower((string)$val) === 'true' ? 1 : 0);
return ($val === 1) ? 1 : 0;
}
/* ---------------- tiny HTTP helper ---------------- */
function http_get_json(string $url, float $timeout = 1.8): ?array {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => $timeout,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_USERAGENT => 'bitcardo/quote/1.0'
]);
$res = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code !== 200 || !$res) return null;
$d = json_decode($res, true);
return (json_last_error() === JSON_ERROR_NONE) ? $d : null;
}
/* ---------------- keyless price providers ---------------- */
function price_from_kraken(string $coin): ?float {
$pair = match (strtoupper($coin)) {
'BTC'=>'XBTUSD','ETH'=>'ETHUSD','SOL'=>'SOLUSD','USDT'=>'USDTUSD',
default => strtoupper($coin).'USD',
};
$d = http_get_json("https://api.kraken.com/0/public/Ticker?pair={$pair}");
if (!$d || !empty($d['error'])) return null;
if (empty($d['result']) || !is_array($d['result'])) return null;
$first = reset($d['result']);
$price = is_array($first) && isset($first['c'][0]) ? (float)$first['c'][0] : null;
return ($price && $price > 0) ? $price : null;
}
function price_from_binance(string $coin): ?float {
$sym = strtoupper($coin);
if ($sym === 'USDT') return 1.0;
$pair = $sym.'USDT';
$d = http_get_json("https://api.binance.com/api/v3/ticker/price?symbol={$pair}");
if (!$d || empty($d['price'])) return null;
$p = (float)$d['price']; return $p > 0 ? $p : null;
}
function price_from_bitfinex(string $coin): ?float {
$pair = 't'.strtoupper($coin).'USD';
$d = http_get_json("https://api-pub.bitfinex.com/v2/ticker/{$pair}");
if (!$d || !is_array($d) || count($d) < 7) return null;
$last = (float)$d[6]; return $last > 0 ? $last : null;
}
function price_from_okx(string $coin): ?float {
$inst = strtoupper($coin).'-USD';
$d = http_get_json("https://www.okx.com/api/v5/market/ticker?instId={$inst}");
if (!$d || ($d['code'] ?? '') !== '0' || empty($d['data'][0]['last'])) return null;
$p = (float)$d['data'][0]['last']; return $p > 0 ? $p : null;
}
function price_from_gemini(string $coin): ?float {
$sym = strtoupper($coin);
if ($sym === 'USDT') return 1.0;
$d = http_get_json("https://api.gemini.com/v1/pricefeed");
if (!$d || !is_array($d)) return null;
$pair = $sym.'USD';
foreach ($d as $row) {
if (($row['pair'] ?? null) === $pair && !empty($row['price'])) {
$p = (float)$row['price']; if ($p > 0) return $p;
}
}
return null;
}
function price_from_coinpaprika(string $coin): ?float {
$map = ['BTC'=>'btc-bitcoin','ETH'=>'eth-ethereum','SOL'=>'sol-solana','USDT'=>'usdt-tether'];
$id = $map[strtoupper($coin)] ?? null;
if (!$id) return null;
$d = http_get_json("https://api.coinpaprika.com/v1/tickers/{$id}");
$p = $d['quotes']['USD']['price'] ?? null; $p = $p !== null ? (float)$p : null;
return ($p && $p > 0) ? $p : null;
}
function fetch_usd_rate(mysqli $conn, string $coin): ?float {
$coin = strtoupper($coin);
$key = "fx_usd_quote_{$coin}";
if (function_exists('apcu_fetch')) {
$cached = apcu_fetch($key, $ok);
if ($ok && is_numeric($cached) && $cached > 0) return (float)$cached;
}
$providers = ['price_from_kraken','price_from_binance','price_from_bitfinex','price_from_okx','price_from_gemini','price_from_coinpaprika'];
foreach ($providers as $fn) {
$p = $fn($coin);
if ($p && $p > 0) {
if (function_exists('apcu_store')) apcu_store($key, $p, 20);
return $p;
}
}
$stmt = $conn->prepare("SELECT sell_rate FROM coin_rates WHERE coin = ? LIMIT 1");
if ($stmt) {
$stmt->bind_param('s', $coin);
$stmt->execute();
$stmt->bind_result($rateUsd);
$rate = $stmt->fetch() ? (float)$rateUsd : null;
$stmt->close();
if ($rate && $rate > 0) {
if (function_exists('apcu_store')) apcu_store($key, $rate, 20);
return $rate;
}
}
return null;
}
/* ---------------- helpers ---------------- */
function coin_decimals(string $coin): int {
return match (strtoupper($coin)) {
'BTC'=>8,'ETH'=>18,'SOL'=>9,'USDT'=>6, default=>8,
};
}
function pick_platform_fee_rule(mysqli $conn, string $coin, float $amountUsd): array {
$c = strtoupper($coin);
$stmt = $conn->prepare("
SELECT percent_fee, flat_fee, max_fee
FROM withdraw_fees
WHERE coin = ?
AND type = 'crypto'
AND (threshold IS NULL OR ? <= threshold)
ORDER BY threshold ASC
LIMIT 1
");
if (!$stmt) return ['percent'=>0.0,'flat'=>0.0,'cap'=>null];
$stmt->bind_param('sd', $c, $amountUsd);
$stmt->execute();
$stmt->bind_result($percent, $flat, $cap);
$found = $stmt->fetch();
$stmt->close();
if (!$found) {
$stmt = $conn->prepare("
SELECT percent_fee, flat_fee, max_fee
FROM withdraw_fees
WHERE coin = ? AND type = 'crypto'
ORDER BY threshold DESC
LIMIT 1
");
if ($stmt) {
$stmt->bind_param('s', $c);
$stmt->execute();
$stmt->bind_result($percent, $flat, $cap);
$found = $stmt->fetch();
$stmt->close();
}
}
if (!$found) return ['percent'=>0.0,'flat'=>0.0,'cap'=>null];
return ['percent'=>(float)$percent,'flat'=>(float)$flat,'cap'=>$cap !== null ? (float)$cap : null];
}
function dec_to_base_units(string $coinStr, int $decimals): string {
if (strpos($coinStr, 'e') !== false || strpos($coinStr, 'E') !== false) {
$coinStr = sprintf('%.' . $decimals . 'F', (float)$coinStr);
}
if (function_exists('bcpow') && function_exists('bcmul')) {
$coinStr = rtrim(rtrim($coinStr, '0'), '.');
if ($coinStr === '' || $coinStr === '-0') $coinStr = '0';
$pow = bcpow('10', (string)$decimals, 0);
return bcmul($coinStr, $pow, 0);
}
return (string)(int)round(((float)$coinStr) * (10 ** $decimals), 0);
}
function base_to_dec(string $baseStr, int $decimals): float {
if (function_exists('bcdiv')) {
return (float) bcdiv($baseStr, bcpow('10', (string)$decimals, 0), $decimals);
}
return ((int)$baseStr) / (10 ** $decimals);
}
/* ---------------- inputs ---------------- */
$coin = strtoupper(trim($_POST['coin'] ?? ''));
$amountU = (float)($_POST['amount'] ?? 0); // USD typed in UI for crypto
$toAddr = trim($_POST['recipient'] ?? '');
if ($coin === '' || $amountU <= 0 || $toAddr === '') {
json_out(400, ['ok'=>false,'error'=>'bad_request','message'=>'coin, amount (USD), recipient required']);
}
/* ---------------- sender wallet ---------------- */
$stmt = $conn->prepare("SELECT wallet_id, wallet_add, balance FROM user_wallets WHERE user_id = ? AND UPPER(coin) = ? LIMIT 1");
if (!$stmt) json_out(500, ['ok'=>false, 'error'=>'db_error','stage'=>'find_sender']);
$stmt->bind_param('is', $userId, $coin);
$stmt->execute();
$sender = $stmt->get_result()->fetch_assoc();
$stmt->close();
if (!$sender) json_out(404, ['ok'=>false,'error'=>'wallet_not_found']);
$senderBal = (float)$sender['balance'];
/* ---------------- live FX + coin amount ---------------- */
$usdRate = fetch_usd_rate($conn, $coin);
if (!$usdRate || $usdRate <= 0) {
json_out(500, ['ok'=>false, 'error'=>'missing_rate','message'=>"No USD rate for $coin"]);
}
$dec = coin_decimals($coin);
$amountCoin = round($amountU / $usdRate, $dec);
/* ---------------- site settings flags ----------------- */
$ckey = strtolower($coin);
$charge_pf_key = "platform_fee_{$ckey}";
$platform_fee_enabled = get_site_flag($conn, $charge_pf_key, 1) === 1;
/* GLOBAL show/hide for all coins */
$ui_show_platform_fee = get_site_flag($conn, 'show_platform_fee', 1) === 1;
/* ---------------- internal vs external ---------------- */
$stmt = $conn->prepare("SELECT user_id, wallet_id FROM user_wallets WHERE UPPER(wallet_add) = UPPER(?) AND UPPER(coin) = ? LIMIT 1");
if (!$stmt) json_out(500, ['ok'=>false,'error'=>'db_error','stage'=>'find_receiver']);
$stmt->bind_param('ss', $toAddr, $coin);
$stmt->execute();
$recv = $stmt->get_result()->fetch_assoc();
$stmt->close();
$isInternal = (bool)$recv;
/* ---------------- fees ---------------- */
$platformFeeUsd = 0.0; $platformFeeCoin = 0.0;
$networkFeeUsd = 0.0; $networkFeeCoin = 0.0;
if (!$isInternal) {
if ($platform_fee_enabled) {
$rule = pick_platform_fee_rule($conn, $coin, $amountU);
$pct = (float)$rule['percent'];
$flatUsd = (float)$rule['flat'];
$capUsd = $rule['cap'];
$platformFeeUsd = ($pct/100.0)*$amountU + $flatUsd;
if ($capUsd !== null && $capUsd > 0) $platformFeeUsd = min($platformFeeUsd, (float)$capUsd);
$platformFeeCoin = round($platformFeeUsd / $usdRate, $dec);
}
// Build exact with BitGo to get the precise network fee
$stmt = $conn->prepare("SELECT wallet_add_id FROM cwallet WHERE coin = ? LIMIT 1");
if (!$stmt) json_out(500, ['ok'=>false,'error'=>'db_error','stage'=>'cwallet']);
$stmt->bind_param('s', $coin);
$stmt->execute();
$cw = $stmt->get_result()->fetch_assoc();
$stmt->close();
if (!$cw) json_out(500, ['ok'=>false, 'error'=>'cwallet_missing', 'message'=>"Central wallet not found for {$coin}"]);
$walletId = $cw['wallet_add_id'];
$amountBase = dec_to_base_units(sprintf('%.' . $dec . 'F', $amountCoin), $dec);
// platform spendable check
$wUrl = rtrim(BITGO_API_BASE_URL, '/') . '/' . strtolower($coin) . '/wallet/' . $walletId;
$wh = curl_init($wUrl);
curl_setopt_array($wh, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_HTTPHEADER => ['Authorization: Bearer '.BITGO_ACCESS_TOKEN]
]);
$wres = curl_exec($wh);
$wcode= curl_getinfo($wh, CURLINFO_HTTP_CODE);
curl_close($wh);
$spendable = null;
if ($wcode === 200 && $wres) {
$wd = json_decode($wres, true);
if (isset($wd['spendableBalance'])) $spendable = (string)$wd['spendableBalance'];
}
// build tx (not send) to get exact fee in base units
$bUrl = rtrim(BITGO_API_BASE_URL, '/') . '/' . strtolower($coin) . '/wallet/' . $walletId . '/tx/build';
$payload = json_encode([
'recipients' => [
['address' => $toAddr, 'amount' => (int)$amountBase]
]
]);
$ch = curl_init($bUrl);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer ' . BITGO_ACCESS_TOKEN
],
CURLOPT_POSTFIELDS => $payload,
CURLOPT_TIMEOUT => 20,
]);
$bres = curl_exec($ch);
$bcode= curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if (!($bcode === 200 && $bres)) {
json_out(502, ['ok'=>false,'error'=>'bitgo_build_failed','message'=>$bres ?: "HTTP {$bcode}"]);
}
$bd = json_decode($bres, true);
$feeRate = $bd['feeRate'] ?? null;
$feeBase = $bd['fee'] ?? ($bd['estimatedFee'] ?? ($bd['txInfo']['fee'] ?? null));
if ($feeBase === null) json_out(502, ['ok'=>false,'error'=>'bitgo_fee_missing','message'=>'Build did not return fee']);
$networkFeeBase = (string)$feeBase;
$networkFeeCoin = base_to_dec($networkFeeBase, $dec);
$networkFeeUsd = $networkFeeCoin * $usdRate;
if ($spendable !== null) {
$need = (string) ((int)$amountBase + (int)$networkFeeBase);
if ((int)$spendable < (int)$need) {
json_out(400, ['ok'=>false,'error'=>'platform_insufficient','message'=>'Platform wallet cannot cover amount + network fee']);
}
}
}
$totalFeeCoin = round($platformFeeCoin + $networkFeeCoin, $dec);
$totalDebitCoin = round($amountCoin + $totalFeeCoin, $dec);
if ($totalDebitCoin > $senderBal) {
json_out(400, ['ok'=>false,'error'=>'insufficient_funds','message'=>'Total (amount + fees) exceeds your wallet balance']);
}
/* ---------------- persist short-lived quote ------------- */
$quoteId = bin2hex(random_bytes(8));
$_SESSION['send_quote'] = $_SESSION['send_quote'] ?? [];
$_SESSION['send_quote'][$quoteId] = [
'user_id' => $userId,
'coin' => $coin,
'recipient' => $toAddr,
'usd_rate' => $usdRate,
'amount_usd' => $amountU,
'amount_coin' => $amountCoin,
'decimals' => $dec,
'is_internal' => $isInternal,
'platform_fee_enabled' => $platform_fee_enabled,
'ui_show_platform_fee' => $ui_show_platform_fee, // GLOBAL
'platform_fee_coin' => $platformFeeCoin,
'platform_fee_usd' => $platformFeeUsd,
'network_fee_coin' => $networkFeeCoin,
'network_fee_usd' => $networkFeeUsd,
'network_fee_base' => $networkFeeBase ?? '0',
'fee_rate' => $feeRate ?? null,
'amount_base' => isset($amountBase) ? (string)$amountBase : null,
'total_debit_coin' => $totalDebitCoin,
'created_at' => time(),
'expires_at' => time() + 60,
];
/* ---------------- response to UI ----------------------- */
json_out(200, [
'ok' => true,
'quote_id' => $quoteId,
'is_internal' => $isInternal,
'coin' => $coin,
'amount_usd' => round($amountU, 2),
'amount_coin' => (float)$amountCoin,
'platform_fee_enabled'=> $platform_fee_enabled,
'ui_show_platform_fee'=> $ui_show_platform_fee, // GLOBAL flag surfaced to UI
'platform_fee_coin' => (float)$platformFeeCoin,
'platform_fee_usd' => (float)$platformFeeUsd,
'network_fee_coin' => (float)$networkFeeCoin,
'network_fee_usd' => (float)$networkFeeUsd,
'total_debit_coin' => (float)$totalDebitCoin,
]);
Выполнить команду
Для локальной разработки. Не используйте в интернете!