$ledger); $balance = null; if (configuration_ready()) { try { $balance = faucet_balance_atomic(); } catch (Throwable $error) { error_log('[contractless_faucet] RPC status failed: ' . $error->getMessage()); } } json_response([ 'success' => true, 'online' => configuration_ready() && $balance !== null, 'network' => $network['network'], 'symbol' => $network['symbol'], 'faucet_address' => $config['faucet_address'], 'claim_amount' => atomic_to_decimal($config['claim_amount_atomic']), 'cooldown_seconds' => $config['cooldown_seconds'], 'faucet_balance' => $balance === null ? null : atomic_to_decimal($balance), 'total_claimed' => atomic_to_decimal((int) $ledger['total_claimed_atomic']), 'total_addresses' => count($ledger['addresses']), ]); } if ($_SERVER['REQUEST_METHOD'] !== 'POST') { header('Allow: GET, POST'); json_response(['success' => false, 'error' => 'Method not allowed.'], 405); } if (!configuration_ready()) { json_response([ 'success' => false, 'error' => 'The faucet is not configured or is temporarily offline.', ], 503); } $rawBody = file_get_contents('php://input'); if ($rawBody === false || strlen($rawBody) > 4096) { json_response(['success' => false, 'error' => 'Invalid request.'], 400); } $body = json_decode($rawBody, true); if (!is_array($body)) { json_response(['success' => false, 'error' => 'Request body must be JSON.'], 400); } $requestedAddress = normalize_requested_address( (string) ($body['address'] ?? ''), $network['suffix'] ); if ($requestedAddress === null) { json_response([ 'success' => false, 'error' => "Enter a valid registered {$network['network']} wallet address.", ], 422); } // REMOTE_ADDR is authoritative. Forwarded headers are intentionally ignored. $clientIp = normalized_client_ip((string) ($_SERVER['REMOTE_ADDR'] ?? '')); if ($clientIp === null) { json_response(['success' => false, 'error' => 'Could not identify the requesting IP.'], 400); } $now = time(); $nowMs = (int) floor(microtime(true) * 1000); $precheck = with_ledger_lock(true, function (array &$ledger) use ( $requestedAddress, $clientIp, $now, $nowMs, $config ): array { $lastRequest = (int) $ledger['last_request_timestamp_ms']; if ($nowMs - $lastRequest < $config['minimum_request_interval_ms']) { return ['allowed' => false, 'status' => 429, 'error' => 'The faucet is busy. Try again.']; } $ledger['last_request_timestamp_ms'] = $nowMs; $addressClaim = active_claim( $ledger['addresses'], $requestedAddress, $now, $config['cooldown_seconds'] ); $ipClaim = active_claim($ledger['ips'], $clientIp, $now, $config['cooldown_seconds']); $claim = $addressClaim ?? $ipClaim; if ($claim !== null) { return [ 'allowed' => false, 'status' => 429, 'error' => 'This wallet address or IP has already claimed recently.', 'retry_after_seconds' => claim_remaining_seconds( $claim, $now, $config['cooldown_seconds'] ), ]; } return ['allowed' => true]; }); if (!$precheck['allowed']) { json_response(['success' => false] + $precheck, $precheck['status']); } $balance = faucet_balance_atomic(); $requiredBalance = $config['claim_amount_atomic'] + $config['transfer_fee_atomic']; if ($balance === null || $balance < $requiredBalance) { json_response([ 'success' => false, 'error' => 'The faucet is temporarily low on funds. Please try again later.', ], 503); } try { $transaction = create_faucet_transfer($requestedAddress); } catch (InvalidArgumentException $error) { json_response(['success' => false, 'error' => $error->getMessage()], 422); } $canonicalAddress = strtolower(trim((string) $transaction['receiver'])); $requestId = bin2hex(random_bytes(16)); $reservation = with_ledger_lock(true, function (array &$ledger) use ( $canonicalAddress, $clientIp, $now, $requestId, $config ): array { $addressClaim = active_claim( $ledger['addresses'], $canonicalAddress, $now, $config['cooldown_seconds'] ); $ipClaim = active_claim($ledger['ips'], $clientIp, $now, $config['cooldown_seconds']); $claim = $addressClaim ?? $ipClaim; if ($claim !== null) { return [ 'allowed' => false, 'retry_after_seconds' => claim_remaining_seconds( $claim, $now, $config['cooldown_seconds'] ), ]; } $entry = [ 'claimed_timestamp' => $now, 'status' => 'pending', 'request_id' => $requestId, 'address' => $canonicalAddress, 'ip' => $clientIp, ]; $ledger['addresses'][$canonicalAddress] = $entry; $ledger['ips'][$clientIp] = $entry; return ['allowed' => true]; }); if (!$reservation['allowed']) { json_response([ 'success' => false, 'error' => 'This wallet address or IP has already claimed recently.', 'retry_after_seconds' => $reservation['retry_after_seconds'], ], 429); } try { $txid = broadcast_faucet_transfer($transaction); with_ledger_lock(true, function (array &$ledger) use ( $canonicalAddress, $clientIp, $requestId, $txid, $config ): void { foreach ([['addresses', $canonicalAddress], ['ips', $clientIp]] as [$group, $key]) { if (($ledger[$group][$key]['request_id'] ?? '') === $requestId) { $ledger[$group][$key]['status'] = 'completed'; $ledger[$group][$key]['txid'] = $txid; unset($ledger[$group][$key]['request_id']); } } $ledger['total_claimed_atomic'] += $config['claim_amount_atomic']; $ledger['total_claims']++; }); } catch (Throwable $error) { with_ledger_lock(true, function (array &$ledger) use ( $canonicalAddress, $clientIp, $requestId ): void { foreach ([['addresses', $canonicalAddress], ['ips', $clientIp]] as [$group, $key]) { if (($ledger[$group][$key]['request_id'] ?? '') === $requestId) { unset($ledger[$group][$key]); } } }); throw $error; } json_response([ 'success' => true, 'txid' => $txid, 'amount' => atomic_to_decimal($config['claim_amount_atomic']), 'symbol' => $network['symbol'], 'message' => ucfirst($network['network']) . ' coins were broadcast successfully.', ]); } catch (Throwable $error) { error_log('[contractless_faucet] ' . $error->getMessage()); json_response([ 'success' => false, 'error' => 'The faucet could not complete this request. Please try again later.', ], 500); }