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, 'message'=>'method_not_allowed']);
}
if (empty($_SESSION['user_id'])) {
  json_out(401, ['ok'=>false, 'message'=>'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;
}

/* ---------------- display rounding helper ----------------
   UI formats USD with 2dp; for very small fees (SOL/TRX) it becomes $0.00.
   We force a minimum visible network fee when fee > 0 by rounding UP to 2dp,
   and by applying a floor of $0.01 for external network fees where applicable.
*/
function round_up_dp(float $v, int $dp = 2): float {
  if ($v <= 0) return 0.0;
  $m = 10 ** $dp;
  return ceil($v * $m) / $m;
}

/* ---------------- 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','TRX'=>'TRXUSD',
    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 {
  $sym = strtoupper($coin);
  if ($sym === 'USDT') return 1.0;
  $pair = 't'.$sym.'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 {
  $sym = strtoupper($coin);
  if ($sym === 'USDT') return 1.0;
  $inst = $sym.'-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','TRX'=>'trx-tron'];
  $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);
  if ($coin === 'USDT') return 1.0;

  $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;
}

/* ---------------- coin helpers ---------------- */
function is_usdt_trc20(string $coin): bool {
  $c = strtoupper(trim($coin));
  // treat USDT as TRC20 in this system (single USDT wallet represents USDT-TRC20)
  return ($c === 'USDT-TRC20' || $c === 'USDT_TRC20' || $c === 'USDTTRC20' || $c === 'USDT');
}
function is_tron_family(string $coin): bool {
  $c = strtoupper(trim($coin));
  return ($c === 'TRX' || is_usdt_trc20($c));
}
function platform_fee_coin_key(string $coin): string {
  $c = strtoupper(trim($coin));
  return is_usdt_trc20($c) ? 'USDT' : $c;
}
function coin_decimals(string $coin): int {
  $c = strtoupper(trim($coin));
  if (is_usdt_trc20($c)) return 6;
  return match ($c) {
    'BTC'=>8,'ETH'=>18,'SOL'=>9,'USDT'=>6,'TRX'=>6, default=>8,
  };
}

/* ---------------- address validation (return only "Invalid Address") ---------------- */
function valid_base58(string $s): bool {
  return (bool)preg_match('/^[1-9A-HJ-NP-Za-km-z]+$/', $s);
}
function valid_btc_address(string $a): bool {
  $a = trim($a);
  if ($a === '') return false;
  $l = strtolower($a);
  if (str_starts_with($l, 'bc1') || str_starts_with($l, 'tb1')) {
    return (bool)preg_match('/^(bc1|tb1)[0-9a-z]{11,71}$/', $l);
  }
  if (strlen($a) < 26 || strlen($a) > 35) return false;
  return valid_base58($a);
}
function valid_tron_address(string $a): bool {
  $a = trim($a);
  if (strlen($a) < 30 || strlen($a) > 36) return false;
  if ($a[0] !== 'T') return false;
  return valid_base58($a);
}
function valid_sol_address(string $a): bool {
  $a = trim($a);
  if (strlen($a) < 32 || strlen($a) > 44) return false;
  return valid_base58($a);
}
function is_valid_address(string $coin, string $addr): bool {
  $c = strtoupper(trim($coin));
  if (is_tron_family($c)) return valid_tron_address($addr);
  return match ($c) {
    'BTC' => valid_btc_address($addr),
    'SOL' => valid_sol_address($addr),
    default => (strlen(trim($addr)) >= 20),
  };
}

/* ---------------- fee rule picker ---------------- */
function pick_platform_fee_rule(mysqli $conn, string $coinKey, float $amountUsd): array {
  $c = strtoupper($coinKey);
  $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];
}

/* ---------------- base-unit helpers ---------------- */
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);
}
function extract_bitgo_fee_base(array $bd): ?string {
  $candidates = [
    $bd['fee'] ?? null,
    $bd['estimatedFee'] ?? null,
    $bd['txInfo']['fee'] ?? null,
    $bd['feeInfo']['fee'] ?? null,
    $bd['txRequest']['feeInfo']['fee'] ?? null,
    $bd['txRequest']['txInfo']['fee'] ?? null,
  ];
  foreach ($candidates as $v) {
    if ($v === null) continue;
    if (is_numeric($v)) return (string)$v;
  }
  return null;
}

/* ---------------- inputs ---------------- */
$coin     = strtoupper(trim($_POST['coin'] ?? ''));
$amountU  = (float)($_POST['amount'] ?? 0);
$toAddr   = trim($_POST['recipient'] ?? '');

if ($coin === '' || $amountU <= 0 || $toAddr === '') {
  json_out(400, ['ok'=>false,'message'=>'bad_request']);
}

/* Address validation: ONLY "Invalid Address" */
if (!is_valid_address($coin, $toAddr)) {
  json_out(200, ['ok'=>false, 'message'=>'Invalid Address']);
}

/* ---------------- sender wallet (USER ledger only) ----------------
   USDT-TRC20 UI should match user wallet stored as USDT (or variants)
*/
if (is_usdt_trc20($coin)) {
  $stmt = $conn->prepare("SELECT wallet_id, wallet_add, balance, coin FROM user_wallets WHERE user_id = ? AND UPPER(coin) IN ('USDT','USDT-TRC20','USDT_TRC20','USDTTRC20') LIMIT 1");
  if (!$stmt) json_out(500, ['ok'=>false,'message'=>'db_error']);
  $stmt->bind_param('i', $userId);
} else {
  $stmt = $conn->prepare("SELECT wallet_id, wallet_add, balance, coin FROM user_wallets WHERE user_id = ? AND UPPER(coin) = ? LIMIT 1");
  if (!$stmt) json_out(500, ['ok'=>false,'message'=>'db_error']);
  $stmt->bind_param('is', $userId, $coin);
}
$stmt->execute();
$sender = $stmt->get_result()->fetch_assoc();
$stmt->close();
if (!$sender) json_out(404, ['ok'=>false,'message'=>'wallet_not_found']);

$senderBal  = (float)$sender['balance'];
$coinStored = strtoupper((string)$sender['coin']);

/* ---------------- internal vs external ---------------- */
if (is_usdt_trc20($coin)) {
  $stmt = $conn->prepare("SELECT user_id, wallet_id FROM user_wallets WHERE UPPER(wallet_add) = UPPER(?) AND UPPER(coin) IN ('USDT','USDT-TRC20','USDT_TRC20','USDTTRC20') LIMIT 1");
  if (!$stmt) json_out(500, ['ok'=>false,'message'=>'db_error']);
  $stmt->bind_param('s', $toAddr);
} else {
  $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,'message'=>'db_error']);
  $stmt->bind_param('ss', $toAddr, $coinStored);
}
$stmt->execute();
$recv = $stmt->get_result()->fetch_assoc();
$stmt->close();
$isInternal = (bool)$recv;

/* ---------------- FX + amount in coin ---------------- */
$feeCoinKey = platform_fee_coin_key($coin);
$usdRate = ($feeCoinKey === 'USDT') ? 1.0 : fetch_usd_rate($conn, $feeCoinKey);
if (!$usdRate || $usdRate <= 0) {
  json_out(500, ['ok'=>false,'message'=>'missing_rate']);
}
$dec        = coin_decimals($coin);
$amountCoin = round($amountU / $usdRate, $dec);

/* ---------------- site flags ---------------- */
$ckey = strtolower($feeCoinKey);
$platform_fee_enabled = get_site_flag($conn, "platform_fee_{$ckey}", 1) === 1;
$ui_show_platform_fee = get_site_flag($conn, 'show_platform_fee', 1) === 1;

/* ---------------- fees ---------------- */
$platformFeeUsd = 0.0; $platformFeeCoin = 0.0;
$networkFeeUsd  = 0.0; $networkFeeCoin  = 0.0;

$networkFeeTrx      = 0.0;
$networkFeeTrxUsd   = 0.0;
$networkFeeTrxSun   = '0';

$networkFeeBase = '0';
$feeRate = null;
$amountBase = null;
$provider = 'internal';

if (!$isInternal) {

  if ($platform_fee_enabled) {
    $rule = pick_platform_fee_rule($conn, $feeCoinKey, $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);
  }

  if (is_tron_family($coin)) {
    $provider = 'tron';

    $trxUsd = fetch_usd_rate($conn, 'TRX') ?: 0.0;

    // Network fee must not show as $0 in UI.
    // TRX may be "free" with bandwidth, but in practice users often pay something.
    // Use a small but non-zero estimate for TRX transfers; keep USDT-TRC20 as energy-based estimate.
    if (strtoupper($coin) === 'TRX') {
      $networkFeeTrx = 0.35; // small realistic UI estimate (non-zero)
    } else {
      $networkFeeTrx = 8.0;  // UI-only low estimate for USDT-TRC20 energy
    }

    $networkFeeTrxUsd = ($trxUsd > 0) ? ($networkFeeTrx * $trxUsd) : 0.0;

    // Ensure network fee is visible at 2dp (>= $0.01) for UI purposes
    if ($networkFeeTrxUsd > 0 && round($networkFeeTrxUsd, 2) <= 0.0) {
      $networkFeeTrxUsd = 0.01;
      if ($trxUsd > 0) $networkFeeTrx = $networkFeeTrxUsd / $trxUsd;
    }

    $networkFeeTrxSun = dec_to_base_units(sprintf('%.6F', $networkFeeTrx), 6);

    if (strtoupper($coin) === 'TRX') {
      // TRX send: fee is TRX
      $networkFeeCoin = round($networkFeeTrx, 6);
      $networkFeeUsd  = $networkFeeTrxUsd;
    } else {
      // USDT-TRC20 send: show USD network fee, and represent it in USDT units (USDT≈USD).
      $networkFeeUsd  = $networkFeeTrxUsd;
      $networkFeeCoin = round(($usdRate > 0 ? ($networkFeeUsd / $usdRate) : 0.0), $dec);
    }

  } else {
    $provider = 'bitgo';

    $stmt = $conn->prepare("SELECT wallet_add_id FROM cwallet WHERE UPPER(coin) = UPPER(?) LIMIT 1");
    if (!$stmt) json_out(500, ['ok'=>false,'message'=>'db_error']);
    $stmt->bind_param('s', $feeCoinKey);
    $stmt->execute();
    $cw = $stmt->get_result()->fetch_assoc();
    $stmt->close();

    $walletId = $cw['wallet_add_id'] ?? null;

    $amountBase = dec_to_base_units(sprintf('%.' . $dec . 'F', $amountCoin), $dec);

    // SOL: previously the fee was so tiny it rounded to $0.00 in UI.
    // Use a low-but-visible estimate (>= $0.01) while keeping a sane SOL amount.
    if ($feeCoinKey === 'SOL') {
      $estSolFee = 0.00005; // low estimate
      $estUsd = $estSolFee * $usdRate;

      if ($estUsd > 0 && round($estUsd, 2) <= 0.0) {
        $estUsd = 0.01;
        $estSolFee = $estUsd / $usdRate;
      }

      $networkFeeCoin = round($estSolFee, 9);
      $networkFeeUsd  = $networkFeeCoin * $usdRate;
      $networkFeeBase = dec_to_base_units(sprintf('%.9F', $networkFeeCoin), 9);
      $feeRate = null;

    } else {
      if ($walletId) {
        $coinPath = strtolower($feeCoinKey);
        $baseUrl  = bitgo_base_url_for_coin($feeCoinKey);
        $bUrl = rtrim($baseUrl, '/') . '/' . $coinPath . '/wallet/' . $walletId . '/tx/build';

        $payload = json_encode([
          'recipients' => [
            ['address' => $toAddr, 'amount' => (int)$amountBase]
          ]
        ], JSON_UNESCAPED_SLASHES);

        $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) {
          $bd = json_decode($bres, true);
          if (is_array($bd)) {
            $feeRate = $bd['feeRate'] ?? null;
            $feeBase = extract_bitgo_fee_base($bd);
            if ($feeBase !== null) {
              $networkFeeBase = (string)$feeBase;
              $networkFeeCoin = base_to_dec($networkFeeBase, $dec);
              $networkFeeUsd  = $networkFeeCoin * $usdRate;
            }
          }
        }
      }

      // If build failed or fee missing: low fallback (do not block user)
      if ($networkFeeCoin <= 0) {
        if ($feeCoinKey === 'BTC') {
          $networkFeeCoin = 0.00000500; // 500 sats (UI estimate)
        } else {
          $networkFeeCoin = 0.00000001;
        }
        $networkFeeUsd  = $networkFeeCoin * $usdRate;
        $networkFeeBase = dec_to_base_units(sprintf('%.' . $dec . 'F', $networkFeeCoin), $dec);
        $feeRate = null;
      }
    }
  }
}

/* ---------------- totals + ONLY user-side debit checks ----------------
   IMPORTANT: Do NOT block submission due to platform wallet insufficiency.
*/
$totalFeeCoin   = round($platformFeeCoin + $networkFeeCoin, $dec);
$totalDebitCoin = round($amountCoin + $totalFeeCoin, $dec);

if ($totalDebitCoin > $senderBal) {
  json_out(400, ['ok'=>false,'message'=>'insufficient_funds']);
}

/* ---------------- 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,
  'platform_fee_coin'    => $platformFeeCoin,
  'platform_fee_usd'     => $platformFeeUsd,
  'network_fee_coin'     => $networkFeeCoin,
  'network_fee_usd'      => $networkFeeUsd,

  // TRON gas (for worker bookkeeping; UI should rely on network_fee_usd/coin for USDT)
  'network_fee_trx'      => $networkFeeTrx,
  'network_fee_trx_usd'  => $networkFeeTrxUsd,
  'network_fee_trx_sun'  => $networkFeeTrxSun,

  'network_fee_base'     => $networkFeeBase ?? '0',
  'fee_rate'             => $feeRate ?? null,
  'amount_base'          => $amountBase !== null ? (string)$amountBase : null,
  'total_debit_coin'     => $totalDebitCoin,
  'provider'             => $provider,
  'created_at'           => time(),
  'expires_at'           => time() + 60,
];

/* ---------------- response to UI ----------------------- */
$out = [
  'ok'                  => true,
  'quote_id'            => $quoteId,
  'is_internal'         => $isInternal,
  'coin'                => $coin,
  'provider'            => $provider,
  'amount_usd'          => round($amountU, 2),
  'amount_coin'         => (float)$amountCoin,
  'platform_fee_enabled'=> $platform_fee_enabled,
  'ui_show_platform_fee'=> $ui_show_platform_fee,
  '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,
];

// Optional: provide UI-friendly 2dp USD fee fields (won't break existing UI)
$out['platform_fee_usd_ui'] = round_up_dp((float)$platformFeeUsd, 2);
$out['network_fee_usd_ui']  = round_up_dp((float)$networkFeeUsd, 2);

if (!$isInternal && is_usdt_trc20($coin)) {
  // keep these for backend/worker context, but UI should show network_fee_usd and network_fee_coin
  $out['network_fee_trx']     = (float)$networkFeeTrx;
  $out['network_fee_trx_usd'] = (float)$networkFeeTrxUsd;
  $out['requires_trx_fee_wallet'] = 0; // platform pays TRX gas (operationally), but user is debited in USDT here
}

json_out(200, $out);

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


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