Contractless-Faucet/script.js

184 lines
6.2 KiB
JavaScript

const menuButton = document.querySelector(".menu-button");
const navigation = document.querySelector(".site-nav");
const form = document.querySelector("#faucet-form");
const address = document.querySelector("#wallet-address");
const message = document.querySelector("#form-message");
const copyAddress = document.querySelector("#copy-address");
const faucetAddress = document.querySelector("#faucet-address");
const requestButton = form.querySelector(".request-button");
let faucetState = {
network: "testnet",
symbol: "CLTC",
claimAmount: "25.00000000",
online: false,
};
menuButton.addEventListener("click", () => {
const open = navigation.classList.toggle("open");
menuButton.setAttribute("aria-expanded", String(open));
});
function displayAmount(value) {
const numeric = Number(value);
return Number.isFinite(numeric)
? numeric.toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: 8,
})
: value;
}
function waitDescription(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.ceil((seconds % 3600) / 60);
return hours > 0
? `${hours} hour${hours === 1 ? "" : "s"} and ${minutes} minute${minutes === 1 ? "" : "s"}`
: `${minutes} minute${minutes === 1 ? "" : "s"}`;
}
function applyStatus(status) {
const network = status.network || "testnet";
const symbol = status.symbol || "CLTC";
const claimAmount = status.claim_amount || "25.00000000";
const displayClaim = displayAmount(claimAmount);
const networkTitle = network.charAt(0).toUpperCase() + network.slice(1);
faucetState = {
network,
symbol,
claimAmount,
online: Boolean(status.online),
};
document.querySelector("#brand-network").textContent = `${network.toUpperCase()} FAUCET`;
document.querySelector("#network-coordinate").textContent =
`${network.toUpperCase()} / ${displayClaim} ${symbol}`;
document.querySelector("#eyebrow-network").textContent = network;
document.querySelector("#intro-amount").textContent = `${displayClaim} ${symbol}`;
document.querySelector("#intro-network").textContent = network;
document.querySelector("#input-symbol").textContent = symbol;
document.querySelector("#address-note").textContent =
`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;
document.querySelector("#rule-symbol").textContent = `${symbol} per request`;
document.querySelector("#tip-label").textContent = `Send ${symbol} tips to:`;
document.querySelector("#lower-network").textContent = networkTitle;
document.querySelector("#footer-network").textContent =
`${network.toUpperCase()} COINS HAVE NO MONETARY VALUE`;
faucetAddress.textContent =
status.faucet_address || `Faucet ${network} address is not configured`;
document.querySelector("#faucet-balance").textContent =
status.faucet_balance === null
? `-- ${symbol}`
: `${displayAmount(status.faucet_balance)} ${symbol}`;
document.querySelector("#total-claimed").textContent =
`${displayAmount(status.total_claimed || "0")} ${symbol}`;
document.querySelector("#total-addresses").textContent =
Number(status.total_addresses || 0).toLocaleString();
const faucetStatus = document.querySelector("#faucet-status");
faucetStatus.lastChild.textContent = status.online ? " Online" : " Offline";
faucetStatus.classList.toggle("offline", !status.online);
requestButton.disabled = !status.online;
}
async function refreshStatus() {
try {
const response = await fetch("api.php", {
headers: { Accept: "application/json" },
cache: "no-store",
});
const status = await response.json();
if (!response.ok || !status.success) {
throw new Error(status.error || "Status request failed.");
}
applyStatus(status);
} catch {
applyStatus({
network: faucetState.network,
symbol: faucetState.symbol,
claim_amount: faucetState.claimAmount,
online: false,
faucet_address: "",
faucet_balance: null,
total_claimed: "0",
total_addresses: 0,
});
}
}
form.addEventListener("submit", async (event) => {
event.preventDefault();
const value = address.value.trim();
const suffix = faucetState.network === "mainnet" ? "clc" : "cltc";
const escapedSuffix = suffix.replace(".", "\\.");
const canonicalAddress = new RegExp(
`^[a-f0-9]{40}\\.${escapedSuffix}$`,
"i",
);
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 or vanity address ending in .${suffix}.`;
return;
}
requestButton.disabled = true;
message.textContent = "Creating and broadcasting your faucet transaction...";
try {
const response = await fetch("api.php", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({ address: value }),
});
const result = await response.json();
if (!response.ok || !result.success) {
const retry = result.retry_after_seconds
? ` Try again in ${waitDescription(result.retry_after_seconds)}.`
: "";
throw new Error((result.error || "The faucet request failed.") + retry);
}
message.classList.add("success");
message.textContent =
`${displayAmount(result.amount)} ${result.symbol} was broadcast. ` +
`Transaction: ${result.txid}`;
address.value = "";
await refreshStatus();
} catch (error) {
message.textContent =
error instanceof Error ? error.message : "The faucet request failed.";
} finally {
requestButton.disabled = !faucetState.online;
}
});
copyAddress.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(faucetAddress.textContent.trim());
copyAddress.textContent = "Copied";
} catch {
copyAddress.textContent = "Copy failed";
}
setTimeout(() => {
copyAddress.textContent = "Copy address";
}, 1800);
});
refreshStatus();