242 lines
8.3 KiB
PHP
242 lines
8.3 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
require __DIR__ . '/lib.php';
|
|
|
|
try {
|
|
$config = faucet_config();
|
|
$network = network_details($config['faucet_address']);
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
|
$ledger = with_ledger_lock(false, static fn(array $ledger): array => $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);
|
|
}
|
|
|
|
$requestedInput = normalize_requested_address(
|
|
(string) ($body['address'] ?? ''),
|
|
$network['suffix']
|
|
);
|
|
if ($requestedInput === null) {
|
|
json_response([
|
|
'success' => false,
|
|
'error' => "Enter a valid {$network['network']} wallet or vanity 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);
|
|
$rateLimit = with_ledger_lock(true, function (array &$ledger) use (
|
|
$nowMs,
|
|
$config
|
|
): bool {
|
|
$lastRequest = (int) $ledger['last_request_timestamp_ms'];
|
|
if ($nowMs - $lastRequest < $config['minimum_request_interval_ms']) {
|
|
return false;
|
|
}
|
|
$ledger['last_request_timestamp_ms'] = $nowMs;
|
|
return true;
|
|
});
|
|
if (!$rateLimit) {
|
|
json_response([
|
|
'success' => false,
|
|
'error' => 'The faucet is busy. Try again.',
|
|
], 429);
|
|
}
|
|
|
|
$requestedAddress = resolve_requested_address($requestedInput, $network['suffix']);
|
|
if ($requestedAddress === null) {
|
|
json_response([
|
|
'success' => false,
|
|
'error' => "The {$network['network']} wallet or vanity address is not registered.",
|
|
], 422);
|
|
}
|
|
|
|
$precheck = with_ledger_lock(true, function (array &$ledger) use (
|
|
$requestedAddress,
|
|
$clientIp,
|
|
$now,
|
|
$config
|
|
): array {
|
|
$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);
|
|
}
|