added vanity address claims

This commit is contained in:
viraladmin 2026-07-26 12:30:50 -06:00
parent c9dfdd0386
commit d059af2cea
5 changed files with 93 additions and 21 deletions

View File

@ -71,9 +71,10 @@ but normal installations do not need it.
- An address ending in `.cltc` configures testnet and displays `CLTC`.
- An address ending in `.clc` configures mainnet and displays `CLC`.
The faucet address must be the registered canonical 40-character short
address. Faucet claims also require canonical short addresses; vanity aliases
are not accepted by the claim form.
The faucet address itself must be the registered canonical 40-character short
address. Faucet recipients may enter either a canonical short address or a
registered vanity address. Vanity inputs are resolved through RPC, and the
canonical owner address is used for the transfer and 24-hour claim ledger.
Point the faucet at its dedicated wallet and provide that wallet's decryption
key:

39
api.php
View File

@ -51,14 +51,14 @@ try {
json_response(['success' => false, 'error' => 'Request body must be JSON.'], 400);
}
$requestedAddress = normalize_requested_address(
$requestedInput = normalize_requested_address(
(string) ($body['address'] ?? ''),
$network['suffix']
);
if ($requestedAddress === null) {
if ($requestedInput === null) {
json_response([
'success' => false,
'error' => "Enter a valid registered {$network['network']} wallet address.",
'error' => "Enter a valid {$network['network']} wallet or vanity address.",
], 422);
}
@ -70,19 +70,38 @@ try {
$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,
$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,

View File

@ -66,7 +66,7 @@
</div>
<form id="faucet-form">
<label for="wallet-address">Registered wallet address</label>
<label for="wallet-address">Registered wallet or vanity address</label>
<div class="address-control">
<input
id="wallet-address"
@ -79,7 +79,7 @@
>
<span id="input-symbol" class="input-marker" aria-hidden="true">CLTC</span>
</div>
<p id="address-note" class="field-note">The wallet must already be registered on testnet.</p>
<p id="address-note" class="field-note">The wallet or vanity address must already be registered on testnet.</p>
<button type="submit" class="request-button">
<span id="request-label">Request 25 CLTC</span>

51
lib.php
View File

@ -142,12 +142,59 @@ function normalized_client_ip(string $remoteAddress): ?string
return substr(bin2hex($packed), 0, 16) . '::/64';
}
function normalize_requested_address(string $address, string $suffix): ?string
/**
* Normalize a claim input without contacting the node.
*
* Canonical addresses are returned as-is. Vanity names are left-padded to
* the fixed 20-byte payload required by the vanity-owner RPC command.
*/
function normalize_requested_address(string $address, string $suffix): ?array
{
$address = strtolower(trim($address));
$escapedSuffix = preg_quote($suffix, '/');
$canonical = "[a-f0-9]{40}\\.$escapedSuffix";
return preg_match("/^$canonical$/", $address) === 1 ? $address : null;
if (preg_match("/^$canonical$/", $address) === 1) {
return ['type' => 'canonical', 'address' => $address];
}
$separator = strrpos($address, '.');
if ($separator === false) {
return null;
}
$payload = substr($address, 0, $separator);
$network = substr($address, $separator + 1);
if (
$network !== $suffix
|| preg_match('/^[a-z]{1,20}$/', $payload) !== 1
) {
return null;
}
return [
'type' => 'vanity',
'address' => str_pad($payload, 20, ' ', STR_PAD_LEFT) . '.' . $suffix,
];
}
/**
* Resolve either an ordinary short address or registered vanity address to
* the canonical short address used by transfers and faucet cooldown records.
*/
function resolve_requested_address(array $requested, string $suffix): ?string
{
if ($requested['type'] === 'canonical') {
return $requested['address'];
}
$owner = strtolower(trim(faucet_rpc()['client']->vanityOwner($requested['address'])));
if ($owner === '' || str_starts_with($owner, 'error:')) {
return null;
}
$escapedSuffix = preg_quote($suffix, '/');
return preg_match("/^[a-f0-9]{40}\\.$escapedSuffix$/", $owner) === 1
? $owner
: null;
}
function atomic_to_decimal(int $atomic): string

View File

@ -58,7 +58,7 @@ function applyStatus(status) {
document.querySelector("#intro-network").textContent = network;
document.querySelector("#input-symbol").textContent = symbol;
document.querySelector("#address-note").textContent =
`The wallet must already be registered on ${network}.`;
`The wallet or vanity address must already be registered on ${network}.`;
document.querySelector("#request-label").textContent =
`Request ${displayClaim} ${symbol}`;
document.querySelector("#rule-amount").textContent = displayClaim;
@ -115,16 +115,21 @@ form.addEventListener("submit", async (event) => {
const value = address.value.trim();
const suffix = faucetState.network === "mainnet" ? "clc" : "cltc";
const escapedSuffix = suffix.replace(".", "\\.");
const valid = new RegExp(
const canonicalAddress = new RegExp(
`^[a-f0-9]{40}\\.${escapedSuffix}$`,
"i",
).test(value);
);
const vanityAddress = new RegExp(
`^[a-z]{1,20}\\.${escapedSuffix}$`,
"i",
);
const valid = canonicalAddress.test(value) || vanityAddress.test(value);
message.classList.add("visible");
message.classList.remove("success");
if (!valid) {
message.textContent =
`Enter a valid registered Contractless wallet address ending in .${suffix}.`;
`Enter a valid registered Contractless wallet or vanity address ending in .${suffix}.`;
return;
}