PHP WebShell
Текущая директория: /var/www/bitcardoApp/models/crypto
Просмотр файла: buy_crypto.php
<?php
// models/crypto/buy_crypto.php
if (session_status() === PHP_SESSION_NONE) session_start();
require_once __DIR__ . '/../../config/db_config.php'; // expects $conn, $paystackSecret, $paystackPublic
if (!isset($_SESSION['user_id'])) {
if (isset($_GET['action']) || isset($_POST['action'])) {
header('Content-Type: application/json');
echo json_encode(['ok' => false, 'msg' => 'Unauthorized']);
exit;
}
header("Location: ../auth/login.php");
exit;
}
$user_id = (int)$_SESSION['user_id'];
/* ---------------- Helpers ---------------- */
function json_out(array $payload, int $code = 200): void {
http_response_code($code);
header('Content-Type: application/json');
echo json_encode($payload);
exit;
}
function get_user_profile_limits(mysqli $conn, int $user_id): array {
$sql = "
SELECT u.email,
ul.buy_limit,
ul.instant_buy_limit
FROM users u
LEFT JOIN user_level ul ON ul.level_id = u.level_id
WHERE u.user_id = ?
LIMIT 1
";
$stmt = $conn->prepare($sql);
if (!$stmt) return ['email' => null, 'buy_limit' => null, 'instant_buy_limit' => null];
$stmt->bind_param("i", $user_id);
$stmt->execute();
$stmt->bind_result($email, $buy_limit, $instant_buy_limit);
$out = ['email' => null, 'buy_limit' => null, 'instant_buy_limit' => null];
if ($stmt->fetch()) {
$out['email'] = $email ?: null;
$bl = (float)$buy_limit;
$out['buy_limit'] = ($bl > 0) ? $bl : null;
$ibl = (float)$instant_buy_limit;
$out['instant_buy_limit'] = ($ibl > 0) ? $ibl : null;
}
$stmt->close();
return $out;
}
function get_wallet_coin_for_user(mysqli $conn, int $user_id, string $wallet_id): ?string {
$stmt = $conn->prepare("
SELECT coin
FROM user_wallets
WHERE user_id=? AND wallet_id=? AND UPPER(coin) <> 'NGN'
LIMIT 1
");
if (!$stmt) return null;
$stmt->bind_param("is", $user_id, $wallet_id);
$stmt->execute();
$stmt->bind_result($coin_db);
$coin = null;
if ($stmt->fetch()) {
$coin = strtoupper(trim((string)$coin_db));
// normalize variants to match coin_rates.coin
if ($coin === 'USDT-TRC20' || $coin === 'USDT-ERC20') $coin = 'USDT';
if ($coin === 'USDC-TRC20' || $coin === 'USDC-ERC20') $coin = 'USDC';
if ($coin === 'TRON') $coin = 'TRX';
}
$stmt->close();
return $coin;
}
function get_wallet_address_for_user(mysqli $conn, int $user_id, string $wallet_id): ?string {
// uses user_wallets.wallet_add (your schema)
$stmt = $conn->prepare("
SELECT wallet_add
FROM user_wallets
WHERE user_id=? AND wallet_id=?
LIMIT 1
");
if (!$stmt) return null;
$stmt->bind_param("is", $user_id, $wallet_id);
$stmt->execute();
$stmt->bind_result($wallet_add);
$out = null;
if ($stmt->fetch()) {
$out = trim((string)$wallet_add);
if ($out === '') $out = null;
}
$stmt->close();
return $out;
}
function get_coin_buy_pricing(mysqli $conn, string $coin): array {
$coin = strtoupper(trim($coin));
$stmt = $conn->prepare("
SELECT buy_rate, usd_price, min_swap
FROM coin_rates
WHERE UPPER(coin)=UPPER(?)
LIMIT 1
");
if (!$stmt) {
return ['buy_rate' => null, 'usd_price' => null, 'min_swap' => 0.0];
}
$stmt->bind_param("s", $coin);
$stmt->execute();
$stmt->bind_result($buy_rate, $usd_price, $min_swap);
$out = ['buy_rate' => null, 'usd_price' => null, 'min_swap' => 0.0];
if ($stmt->fetch()) {
$br = (float)$buy_rate;
$up = (float)$usd_price;
$ms = (float)$min_swap;
$out['buy_rate'] = ($br > 0) ? $br : null; // NGN per $1
$out['usd_price'] = ($up > 0) ? $up : null; // USD per 1 coin
$out['min_swap'] = ($ms > 0) ? $ms : 0.0; // USD
}
$stmt->close();
return $out;
}
/**
* Paystack fee estimate (NG):
* 1.5% of amount
* +100 NGN if amount > 2500
* cap at 2000 NGN
*/
function estimate_paystack_fee(float $amountNgn): float {
$pct = 0.015;
$extra = ($amountNgn > 2500) ? 100 : 0;
$cap = 2000;
$fee = ($amountNgn * $pct) + $extra;
if ($fee > $cap) $fee = $cap;
return round($fee, 2);
}
/**
* Gross-up so wallet receives netAmount, user pays fee on top.
*/
function gross_up_for_fee(float $netAmount): float {
$gross = $netAmount;
for ($i = 0; $i < 6; $i++) {
$fee = estimate_paystack_fee($gross);
$newGross = $netAmount + $fee;
if (abs($newGross - $gross) < 0.5) {
return round($newGross, 2);
}
$gross = $newGross;
}
return round($gross, 2);
}
function compute_quote(mysqli $conn, int $user_id, string $wallet_id, float $amount_usd): array {
$wallet_id = trim($wallet_id);
if ($wallet_id === '' || !($amount_usd > 0)) {
return ['ok' => true, 'ready' => false];
}
$coin = get_wallet_coin_for_user($conn, $user_id, $wallet_id);
if (!$coin) return ['ok' => true, 'ready' => false, 'error' => 'invalid_wallet'];
$profile = get_user_profile_limits($conn, $user_id);
$buy_limit = $profile['buy_limit'];
$pricing = get_coin_buy_pricing($conn, $coin);
$buy_rate = $pricing['buy_rate'];
$usd_price = $pricing['usd_price'];
$min_swap = (float)$pricing['min_swap'];
$reasons = [];
if ($min_swap > 0 && $amount_usd < $min_swap) $reasons[] = 'min_swap';
if ($buy_limit !== null && $amount_usd > $buy_limit) $reasons[] = 'buy_limit';
if (!$buy_rate || !$usd_price) $reasons[] = 'no_quote';
$ready = empty($reasons);
$ngn_cost = null;
$coin_qty = null;
$gateway_fee = null;
$total_payable = null;
if ($buy_rate && $usd_price) {
$ngn_cost = round($amount_usd * $buy_rate, 2);
$coin_qty = $amount_usd / $usd_price;
$gross = gross_up_for_fee($ngn_cost);
$fee = round(($gross - $ngn_cost), 2);
$gateway_fee = max(0, $fee);
$total_payable = $gross;
}
return [
'ok' => true,
'ready' => $ready,
'coin' => $coin,
'limits' => [
'min_swap_usd' => $min_swap,
'buy_limit_usd' => $buy_limit,
'instant_buy_limit_usd' => $profile['instant_buy_limit'],
],
'pricing' => [
'buy_rate_ngn_per_usd' => $buy_rate,
'usd_price_per_coin' => $usd_price,
],
'preview' => [
'ngn_cost' => $ngn_cost,
'gateway_fee_ngn' => $gateway_fee,
'total_payable_ngn' => $total_payable,
'coin_qty' => $coin_qty,
],
'reasons' => $reasons,
'email' => $profile['email'],
];
}
function insert_initiated_transaction(mysqli $conn, array $t): bool {
$sql = "
INSERT INTO transactions
(coin, user_id, wallet_id, sender_address, receiver_address, amount, type, reference, provider, provider_meta, confirmation, status, applied, note)
VALUES
(?, ?, ?, '', ?, ?, 'buy', ?, 'paystack', ?, 0, 'initiated', 0, ?)
";
$stmt = $conn->prepare($sql);
if (!$stmt) return false;
$stmt->bind_param(
"sissssss",
$t['coin'],
$t['user_id'],
$t['wallet_id'],
$t['receiver_address'],
$t['coin_qty_str'],
$t['reference'],
$t['provider_meta_json'],
$t['note']
);
$ok = $stmt->execute();
$stmt->close();
return $ok;
}
function get_tx_by_reference(mysqli $conn, int $user_id, string $reference): ?array {
$sql = "
SELECT trans_id, coin, user_id, wallet_id, receiver_address, amount, status, applied, provider_meta
FROM transactions
WHERE provider='paystack' AND reference=? AND user_id=?
LIMIT 1
";
$stmt = $conn->prepare($sql);
if (!$stmt) return null;
$stmt->bind_param("si", $reference, $user_id);
$stmt->execute();
$res = $stmt->get_result();
$row = $res ? $res->fetch_assoc() : null;
$stmt->close();
return $row ?: null;
}
function update_tx_status(mysqli $conn, int $trans_id, string $status, int $applied, int $confirmation, string $provider_meta_json, string $note = ''): bool {
$sql = "
UPDATE transactions
SET status=?, applied=?, confirmation=?, provider_meta=?, note=?
WHERE trans_id=?
LIMIT 1
";
$stmt = $conn->prepare($sql);
if (!$stmt) return false;
$stmt->bind_param("siissi", $status, $applied, $confirmation, $provider_meta_json, $note, $trans_id);
$ok = $stmt->execute();
$stmt->close();
return $ok;
}
function credit_wallet(mysqli $conn, int $user_id, string $wallet_id, string $coin_qty_str): bool {
$sql = "
UPDATE user_wallets
SET balance = balance + ?
WHERE user_id=? AND wallet_id=?
LIMIT 1
";
$stmt = $conn->prepare($sql);
if (!$stmt) return false;
$stmt->bind_param("sis", $coin_qty_str, $user_id, $wallet_id);
$ok = $stmt->execute();
$stmt->close();
return $ok;
}
/* ---------------- ACTION ROUTER ---------------- */
$action = $_GET['action'] ?? $_POST['action'] ?? null;
/* 1) QUOTE (JSON) */
if ($action === 'quote') {
$wallet_id = trim($_GET['wallet_id'] ?? '');
$amount_usd = (float)($_GET['amount_usd'] ?? 0);
$q = compute_quote($conn, $user_id, $wallet_id, $amount_usd);
json_out($q);
}
/* 2) INIT PAYSTACK (JSON) */
if ($action === 'init_paystack') {
global $paystackSecret;
$wallet_id = trim($_POST['wallet_id'] ?? '');
$amount_usd = (float)($_POST['amount_usd'] ?? 0);
$q = compute_quote($conn, $user_id, $wallet_id, $amount_usd);
if (empty($q['ready'])) {
$msg = 'Invalid request';
if (!empty($q['reasons']) && in_array('buy_limit', $q['reasons'], true)) {
$msg = 'Buy limit exceeded for Customer Level.';
}
json_out(['ok' => false, 'msg' => $msg, 'data' => $q], 400);
}
$email = $q['email'] ?? null;
if (!$email) $email = 'user@example.com';
$ngn_cost_net = (float)($q['preview']['ngn_cost'] ?? 0);
$gateway_fee_ngn = (float)($q['preview']['gateway_fee_ngn'] ?? 0);
$total_payable = (float)($q['preview']['total_payable_ngn'] ?? 0);
$coin_qty = (float)($q['preview']['coin_qty'] ?? 0);
if (!($ngn_cost_net > 0) || !($total_payable > 0) || !($coin_qty > 0)) {
json_out(['ok' => false, 'msg' => 'Unable to initialize payment'], 400);
}
// Amount in kobo
$amount_kobo = (int) round($total_payable * 100);
// Reference
$reference = 'BCBUY-' . $user_id . '-' . time() . '-' . mt_rand(10000, 99999);
// Store receiver address in transactions.receiver_address (internal only)
$receiver_address = get_wallet_address_for_user($conn, $user_id, $wallet_id) ?? '';
// Store full details internally (NOT sent to Paystack)
$provider_meta = [
'wallet_id' => $wallet_id,
'receiver_address' => $receiver_address,
'coin' => $q['coin'],
'amount_usd' => $amount_usd,
'ngn_cost_net' => $ngn_cost_net,
'gateway_fee_ngn' => $gateway_fee_ngn,
'total_payable_ngn' => $total_payable,
'amount_kobo_expected' => $amount_kobo,
'coin_qty' => $coin_qty,
'buy_rate' => $q['pricing']['buy_rate_ngn_per_usd'] ?? null,
'usd_price' => $q['pricing']['usd_price_per_coin'] ?? null,
'limits' => $q['limits'] ?? [],
'created_at' => date('c'),
];
// Insert initiated tx
$coin_qty_str = number_format($coin_qty, 10, '.', '');
$ins = insert_initiated_transaction($conn, [
'coin' => $q['coin'],
'user_id' => $user_id,
'wallet_id' => $wallet_id,
'receiver_address' => $receiver_address,
'coin_qty_str' => $coin_qty_str,
'reference' => $reference,
'provider_meta_json' => json_encode($provider_meta),
'note' => 'Payment initiated',
]);
if (!$ins) {
json_out(['ok' => false, 'msg' => 'Unable to initialize payment'], 500);
}
if (empty($paystackSecret)) {
json_out(['ok' => false, 'msg' => 'Payment is temporarily unavailable'], 500);
}
// ✅ Paystack gets NOTHING in metadata.
// Frontend will pass only: key, email, amount, currency, ref.
json_out([
'ok' => true,
'reference' => $reference,
'email' => $email,
'amount_kobo' => $amount_kobo,
'currency' => 'NGN',
]);
}
/* 3) VERIFY PAYSTACK (JSON) */
if ($action === 'verify_paystack') {
global $paystackSecret;
$reference = trim($_GET['reference'] ?? '');
if ($reference === '') {
json_out(['ok' => false, 'msg' => 'Invalid reference'], 400);
}
$tx = get_tx_by_reference($conn, $user_id, $reference);
if (!$tx) {
json_out(['ok' => false, 'msg' => 'Transaction not found'], 404);
}
// Idempotency
if (in_array($tx['status'], ['completed', 'pending'], true)) {
$meta0 = json_decode($tx['provider_meta'] ?? '', true);
if (!is_array($meta0)) $meta0 = [];
json_out([
'ok' => true,
'already_processed' => true,
'status' => $tx['status'],
'reference' => $reference,
'details' => [
'coin' => $tx['coin'] ?? null,
'wallet_id' => $tx['wallet_id'] ?? null,
'receiver_address' => $tx['receiver_address'] ?? null,
'amount_usd' => $meta0['amount_usd'] ?? null,
'ngn_cost_net' => $meta0['ngn_cost_net'] ?? null,
'gateway_fee_ngn' => $meta0['gateway_fee_ngn'] ?? null,
'total_payable_ngn' => $meta0['total_payable_ngn'] ?? null,
'coin_qty' => $meta0['coin_qty'] ?? null,
]
]);
}
if (empty($paystackSecret)) {
json_out(['ok' => false, 'msg' => 'Payment verification unavailable'], 500);
}
// Verify Paystack (kept simple)
$ch = curl_init('https://api.paystack.co/transaction/verify/' . rawurlencode($reference));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . $paystackSecret,
'Content-Type: application/json',
]);
$resp = curl_exec($ch);
$err = curl_error($ch);
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$meta = json_decode($tx['provider_meta'] ?? '', true);
if (!is_array($meta)) $meta = [];
if ($resp === false) {
$meta['paystack_verify_error'] = ['ok' => false, 'code' => 0, 'error' => $err ?: 'curl_error'];
update_tx_status($conn, (int)$tx['trans_id'], 'failed', 0, 0, json_encode($meta), 'Paystack verify failed');
json_out(['ok' => false, 'msg' => 'Verification failed'], 400);
}
$json = json_decode($resp, true);
if (!is_array($json) || empty($json['status']) || empty($json['data']) || $code < 200 || $code >= 300) {
$meta['paystack_verify_error'] = ['ok' => false, 'code' => $code, 'raw' => $resp];
update_tx_status($conn, (int)$tx['trans_id'], 'failed', 0, 0, json_encode($meta), 'Paystack verify failed');
json_out(['ok' => false, 'msg' => 'Verification failed'], 400);
}
$data = $json['data'];
$ps_status = $data['status'] ?? '';
$ps_amount = (int)($data['amount'] ?? 0);
$ps_currency = $data['currency'] ?? 'NGN';
$expected_kobo = (int)($meta['amount_kobo_expected'] ?? 0);
if ($ps_status !== 'success' || $ps_currency !== 'NGN' || $expected_kobo <= 0 || $ps_amount !== $expected_kobo) {
$meta['paystack_verify'] = $json;
update_tx_status($conn, (int)$tx['trans_id'], 'failed', 0, 0, json_encode($meta), 'Payment not successful');
json_out(['ok' => false, 'msg' => 'Payment not successful'], 400);
}
// Re-check buy limit after payment
$amount_usd = (float)($meta['amount_usd'] ?? 0);
$profile_now = get_user_profile_limits($conn, $user_id);
$buy_limit_now = $profile_now['buy_limit'];
$meta['paystack_verify'] = $json;
$meta['verified_at'] = date('c');
$meta['buy_limit_recheck'] = [
'buy_limit_usd' => $buy_limit_now,
'amount_usd' => $amount_usd,
'checked_at' => date('c'),
];
if ($buy_limit_now !== null && $amount_usd > (float)$buy_limit_now) {
update_tx_status($conn, (int)$tx['trans_id'], 'pending', 0, 1, json_encode($meta), 'Paid but buy limit exceeded; pending review.');
json_out([
'ok' => true,
'status' => 'pending',
'reference' => $reference,
'msg' => 'Payment received, but buy limit exceeded for Customer Level. Purchase is pending review.',
'details' => [
'coin' => $tx['coin'] ?? null,
'wallet_id' => $tx['wallet_id'] ?? null,
'receiver_address' => $tx['receiver_address'] ?? null,
'amount_usd' => $meta['amount_usd'] ?? null,
'ngn_cost_net' => $meta['ngn_cost_net'] ?? null,
'gateway_fee_ngn' => $meta['gateway_fee_ngn'] ?? null,
'total_payable_ngn' => $meta['total_payable_ngn'] ?? null,
'coin_qty' => $meta['coin_qty'] ?? null,
]
]);
}
// Instant vs pending credit
$instant_limit = $profile_now['instant_buy_limit'];
$is_instant = false;
if ($instant_limit !== null && $amount_usd > 0) {
$is_instant = ($amount_usd <= (float)$instant_limit);
}
if (!$is_instant) {
update_tx_status($conn, (int)$tx['trans_id'], 'pending', 0, 1, json_encode($meta), 'Paid. Awaiting processing.');
json_out([
'ok' => true,
'status' => 'pending',
'reference' => $reference,
'msg' => 'Payment received. Your purchase is pending confirmation.',
'details' => [
'coin' => $tx['coin'] ?? null,
'wallet_id' => $tx['wallet_id'] ?? null,
'receiver_address' => $tx['receiver_address'] ?? null,
'amount_usd' => $meta['amount_usd'] ?? null,
'ngn_cost_net' => $meta['ngn_cost_net'] ?? null,
'gateway_fee_ngn' => $meta['gateway_fee_ngn'] ?? null,
'total_payable_ngn' => $meta['total_payable_ngn'] ?? null,
'coin_qty' => $meta['coin_qty'] ?? null,
]
]);
}
// Instant credit
$coin_qty = (float)($meta['coin_qty'] ?? 0);
$wallet_id = (string)($meta['wallet_id'] ?? $tx['wallet_id']);
$coin_qty_str = number_format($coin_qty, 10, '.', '');
$credited = credit_wallet($conn, $user_id, $wallet_id, $coin_qty_str);
if (!$credited) {
update_tx_status($conn, (int)$tx['trans_id'], 'pending', 0, 1, json_encode($meta), 'Paid. Wallet credit failed; pending review.');
json_out([
'ok' => true,
'status' => 'pending',
'reference' => $reference,
'msg' => 'Payment received. Your purchase is pending confirmation.',
'details' => [
'coin' => $tx['coin'] ?? null,
'wallet_id' => $tx['wallet_id'] ?? null,
'receiver_address' => $tx['receiver_address'] ?? null,
'amount_usd' => $meta['amount_usd'] ?? null,
'ngn_cost_net' => $meta['ngn_cost_net'] ?? null,
'gateway_fee_ngn' => $meta['gateway_fee_ngn'] ?? null,
'total_payable_ngn' => $meta['total_payable_ngn'] ?? null,
'coin_qty' => $meta['coin_qty'] ?? null,
]
]);
}
update_tx_status($conn, (int)$tx['trans_id'], 'completed', 1, 1, json_encode($meta), 'Paid and credited.');
json_out([
'ok' => true,
'status' => 'completed',
'reference' => $reference,
'msg' => 'Payment successful. Wallet credited.',
'details' => [
'coin' => $tx['coin'] ?? null,
'wallet_id' => $tx['wallet_id'] ?? null,
'receiver_address' => $tx['receiver_address'] ?? null,
'amount_usd' => $meta['amount_usd'] ?? null,
'ngn_cost_net' => $meta['ngn_cost_net'] ?? null,
'gateway_fee_ngn' => $meta['gateway_fee_ngn'] ?? null,
'total_payable_ngn' => $meta['total_payable_ngn'] ?? null,
'coin_qty' => $meta['coin_qty'] ?? null,
]
]);
}
/* ---------------- PAGE DATA (for include in buy.php) ---------------- */
$wallets = [];
$stmt = $conn->prepare("
SELECT wallet_id, coin, label, icon
FROM user_wallets
WHERE user_id=? AND UPPER(coin) <> 'NGN'
ORDER BY coin ASC
");
$stmt->bind_param("i", $user_id);
$stmt->execute();
$res = $stmt->get_result();
while ($row = $res->fetch_assoc()) {
$row['coin'] = strtoupper($row['coin'] ?? '');
$wallets[] = $row;
}
$stmt->close();
$profile = get_user_profile_limits($conn, $user_id);
$buy_limit_usd = $profile['buy_limit'];
Выполнить команду
Для локальной разработки. Не используйте в интернете!