added donation leaderboard

This commit is contained in:
2026-11-14 15:39:48 -08:00
parent b9b596d14f
commit 285f3d512e
4 changed files with 504 additions and 1 deletions

View File

@@ -186,6 +186,7 @@
<script src="/src/js/glass.js"></script> <script src="/src/js/glass.js"></script>
<script src="/src/js/main.js"></script> <script src="/src/js/main.js"></script>
<script src="/src/js/api/contribute.js"></script> <script src="/src/js/api/contribute.js"></script>
<script src="/src/js/api/donations.js"></script>
<script src="/src/js/api/download.js"></script> <script src="/src/js/api/download.js"></script>
<script src="/src/js/api/release.js"></script> <script src="/src/js/api/release.js"></script>
<script src="/src/js/main/settings.js"></script> <script src="/src/js/main/settings.js"></script>

View File

@@ -19,4 +19,10 @@ All profits generated through Vyla will go directly to its developers as compens
| ETH | Ethereum | `0xe647BC3401073b6EBFc426ca504f607078845dd1` | | ETH | Ethereum | `0xe647BC3401073b6EBFc426ca504f607078845dd1` |
| BTC | Bitcoin | `bc1qp69euu24h47r68sznta5vzpcp0rudtm3gnxau8` | | BTC | Bitcoin | `bc1qp69euu24h47r68sznta5vzpcp0rudtm3gnxau8` |
| SOL | Solana | `BHx5JYyGgGvgRaQKe8kCh4Fym8LpiUxtexmv7SBV6pVQ` | | SOL | Solana | `BHx5JYyGgGvgRaQKe8kCh4Fym8LpiUxtexmv7SBV6pVQ` |
| LTC | Litecoin | `LgkLXSgEX3yt8K5oaEmL6aPoCAAQBz6Bur` | | LTC | Litecoin | `LgkLXSgEX3yt8K5oaEmL6aPoCAAQBz6Bur` |
## Donation leaderboard
<div id="donation-leaderboard"></div>
<p class="muted-line">Confirmed native on-chain transfers are ranked by estimated USD value. USD estimates are provided by CoinGecko and may be delayed. Donors are shown by public wallet address unless a public name is available.</p>

495
src/js/api/donations.js Normal file
View File

@@ -0,0 +1,495 @@
var DONATION_CHAINS = {
ETH: {
id: "ethereum",
decimals: 18,
explorer: "https://eth.blockscout.com",
coinGecko: "ethereum"
},
BTC: {
id: "bitcoin",
decimals: 8,
explorer: "https://mempool.space",
coinGecko: "bitcoin"
},
SOL: {
id: "solana",
decimals: 9,
explorer: "https://explorer.solana.com",
coinGecko: "solana"
},
LTC: {
id: "litecoin",
decimals: 8,
explorer: "https://litecoinspace.org",
coinGecko: "litecoin"
}
};
var DONATION_COIN_ORDER = ["ETH", "BTC", "SOL", "LTC"];
var DONATION_MAX_PAGES = 4;
var DONATION_SOLANA_SIGNATURE_LIMIT = 50;
var DONATION_SOLANA_BATCH_SIZE = 5;
var DONATION_LEADERBOARD_LIMIT = 20;
function donationFetchJson(url, options) {
return fetch(url, options || {}).then(function (res) {
if (!res.ok) throw new Error("HTTP " + res.status);
return res.json();
});
}
function donationShortAddress(address) {
address = String(address || "");
if (address.length <= 14) return address;
return address.slice(0, 6) + "…" + address.slice(-4);
}
function donationFormatAmount(amount, coin) {
var decimals = coin === "BTC" || coin === "LTC" ? 8 : 6;
return amount.toLocaleString(undefined, {
maximumFractionDigits: decimals
});
}
function donationFormatUsd(amount, available) {
if (!available) return "—";
if (amount > 0 && amount < 0.01) return "&lt;$0.01";
return "$" + amount.toLocaleString(undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
}
function donationFormatDate(timestamp) {
if (!timestamp) return "—";
return new Date(timestamp).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric"
});
}
function getDonationWallets() {
var main = document.getElementById("main-content");
if (!main) return [];
var wallets = [];
var rows = main.querySelectorAll("table tbody tr");
for (var i = 0; i < rows.length; i++) {
var cells = rows[i].querySelectorAll("td");
if (cells.length < 3) continue;
var coin = cells[0].textContent.trim().toUpperCase();
var chain = DONATION_CHAINS[coin];
var code = cells[2].querySelector("code");
var address = code ? code.textContent.trim() : "";
if (!chain || !address) continue;
wallets.push({
coin: coin,
network: cells[1].textContent.trim(),
address: address,
chain: chain.id,
explorer: chain.explorer
});
}
return wallets;
}
function fetchEthereumDonations(wallet) {
var endpoint = "https://eth.blockscout.com/api/v2/addresses/" +
encodeURIComponent(wallet.address) + "/transactions";
var params = new URLSearchParams({ filter: "to" });
var transactions = [];
var pages = 0;
var address = wallet.address.toLowerCase();
function nextPage() {
if (pages >= DONATION_MAX_PAGES) return Promise.resolve(transactions);
pages++;
return donationFetchJson(endpoint + "?" + params.toString()).then(function (data) {
if (data && Array.isArray(data.items)) {
transactions = transactions.concat(data.items);
}
if (!data || !data.next_page_params) return transactions;
params = new URLSearchParams();
Object.keys(data.next_page_params).forEach(function (key) {
var value = data.next_page_params[key];
if (value !== null && value !== undefined) params.set(key, String(value));
});
return nextPage();
});
}
return nextPage().then(function () {
var donations = [];
transactions.forEach(function (tx) {
if (!tx || tx.status !== "ok" || !tx.to || !tx.to.hash) return;
if (tx.to.hash.toLowerCase() !== address) return;
var wei;
try {
wei = BigInt(tx.value || "0");
} catch (e) {
return;
}
var amount = Number(wei) / Math.pow(10, 18);
if (!(amount > 0)) return;
var from = tx.from || {};
donations.push({
coin: wallet.coin,
network: wallet.network,
chain: wallet.chain,
explorer: wallet.explorer,
txid: tx.hash,
date: Date.parse(tx.timestamp) || 0,
amount: amount,
donorAddress: from.hash || "",
donorName: from.name || from.ens_domain_name || ""
});
});
return donations;
});
}
function fetchUtxoDonations(wallet) {
var base = wallet.coin === "BTC" ? "https://mempool.space" : "https://litecoinspace.org";
var addressPath = "/api/address/" + encodeURIComponent(wallet.address);
var path = addressPath + "/txs/chain";
var transactions = [];
var pages = 0;
function nextPage() {
if (pages >= DONATION_MAX_PAGES) return Promise.resolve(transactions);
pages++;
return donationFetchJson(base + path).then(function (data) {
if (!Array.isArray(data) || !data.length) return transactions;
transactions = transactions.concat(data);
if (data.length < 25) return transactions;
path = addressPath + "/txs/chain/" + encodeURIComponent(data[data.length - 1].txid);
return nextPage();
});
}
return nextPage().then(function () {
var donations = [];
transactions.forEach(function (tx) {
if (!tx || !tx.status || !tx.status.confirmed) return;
var inputs = tx.vin || [];
var outputs = tx.vout || [];
var spendsAddress = inputs.some(function (input) {
return input.prevout && input.prevout.scriptpubkey_address === wallet.address;
});
if (spendsAddress) return;
var received = outputs.reduce(function (sum, output) {
return output.scriptpubkey_address === wallet.address ? sum + (output.value || 0) : sum;
}, 0);
if (!(received > 0)) return;
var inputTotals = {};
var totalInput = 0;
inputs.forEach(function (input) {
var prevout = input.prevout;
if (!prevout || !prevout.scriptpubkey_address) return;
var value = prevout.value || 0;
inputTotals[prevout.scriptpubkey_address] = (inputTotals[prevout.scriptpubkey_address] || 0) + value;
totalInput += value;
});
var senders = Object.keys(inputTotals);
if (!senders.length) return;
senders.forEach(function (sender) {
var share = totalInput > 0 ? inputTotals[sender] / totalInput : 1 / senders.length;
donations.push({
coin: wallet.coin,
network: wallet.network,
chain: wallet.chain,
explorer: wallet.explorer,
txid: tx.txid,
date: (tx.status.block_time || 0) * 1000,
amount: (received * share) / 100000000,
donorAddress: sender,
donorName: ""
});
});
});
return donations;
});
}
function solanaRpcCall(method, params) {
return donationFetchJson("https://solana-rpc.publicnode.com", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: method,
params: params
})
}).then(function (data) {
if (data.error) throw new Error(data.error.message || "Solana RPC error");
return data.result;
});
}
function solanaInstructions(transaction) {
var instructions = [];
var message = transaction && transaction.transaction && transaction.transaction.message;
if (message && Array.isArray(message.instructions)) {
instructions = instructions.concat(message.instructions);
}
if (transaction && transaction.meta && Array.isArray(transaction.meta.innerInstructions)) {
transaction.meta.innerInstructions.forEach(function (group) {
if (group && Array.isArray(group.instructions)) {
instructions = instructions.concat(group.instructions);
}
});
}
return instructions;
}
function fetchSolanaTransaction(signatureItem, wallet) {
return solanaRpcCall("getTransaction", [
signatureItem.signature,
{
encoding: "jsonParsed",
commitment: "confirmed",
maxSupportedTransactionVersion: 1
}
]).then(function (transaction) {
if (!transaction || !transaction.meta || transaction.meta.err) return [];
var donations = [];
solanaInstructions(transaction).forEach(function (instruction) {
var parsed = instruction.parsed;
if (!parsed || parsed.type !== "transfer") return;
if (instruction.program !== "system" && instruction.programId !== "11111111111111111111111111111111") return;
var info = parsed.info || {};
if (info.destination !== wallet.address) return;
var lamports = Number(info.lamports || 0);
if (!(lamports > 0)) return;
donations.push({
coin: wallet.coin,
network: wallet.network,
chain: wallet.chain,
explorer: wallet.explorer,
txid: signatureItem.signature,
date: (transaction.blockTime || signatureItem.blockTime || 0) * 1000,
amount: lamports / 1000000000,
donorAddress: info.source || "",
donorName: ""
});
});
return donations;
}).catch(function () {
return [];
});
}
function fetchSolanaDonations(wallet) {
return solanaRpcCall("getSignaturesForAddress", [
wallet.address,
{
limit: DONATION_SOLANA_SIGNATURE_LIMIT,
commitment: "confirmed"
}
]).then(function (signatures) {
if (!Array.isArray(signatures)) return [];
var valid = signatures.filter(function (signature) {
return signature && signature.signature && !signature.err;
});
var batches = [];
for (var i = 0; i < valid.length; i += DONATION_SOLANA_BATCH_SIZE) {
batches.push(valid.slice(i, i + DONATION_SOLANA_BATCH_SIZE));
}
var donations = [];
return batches.reduce(function (promise, batch) {
return promise.then(function () {
return Promise.all(batch.map(function (signatureItem) {
return fetchSolanaTransaction(signatureItem, wallet);
})).then(function (results) {
results.forEach(function (result) {
donations = donations.concat(result);
});
});
});
}, Promise.resolve()).then(function () {
return donations;
});
});
}
function fetchWalletDonations(wallet) {
if (wallet.coin === "ETH") return fetchEthereumDonations(wallet);
if (wallet.coin === "SOL") return fetchSolanaDonations(wallet);
return fetchUtxoDonations(wallet);
}
function fetchDonationPrices(wallets) {
var ids = [];
wallets.forEach(function (wallet) {
var id = DONATION_CHAINS[wallet.coin].coinGecko;
if (ids.indexOf(id) === -1) ids.push(id);
});
if (!ids.length) return Promise.resolve({});
return donationFetchJson(
"https://api.coingecko.com/api/v3/simple/price?ids=" + ids.join(",") + "&vs_currencies=usd"
).then(function (data) {
var prices = {};
wallets.forEach(function (wallet) {
var id = DONATION_CHAINS[wallet.coin].coinGecko;
if (data[id] && data[id].usd) prices[wallet.coin] = data[id].usd;
});
return prices;
}).catch(function () {
return {};
});
}
function donationAssetsHtml(assets) {
var coins = DONATION_COIN_ORDER.filter(function (coin) {
return assets[coin] > 0;
});
if (!coins.length) return "—";
return coins.map(function (coin) {
return esc(donationFormatAmount(assets[coin], coin) + " " + coin);
}).join(" · ");
}
function aggregateDonationLeaderboard(donations, prices) {
var donors = {};
var keys = [];
donations.forEach(function (donation) {
if (!donation.donorAddress) return;
var key = donation.chain + ":" + donation.donorAddress.toLowerCase();
if (!donors[key]) {
donors[key] = {
chain: donation.chain,
explorer: donation.explorer,
address: donation.donorAddress,
name: "",
donations: 0,
usd: 0,
usdKnown: false,
assets: {},
lastDate: 0
};
keys.push(key);
}
var donor = donors[key];
var price = prices[donation.coin] || 0;
donor.donations++;
donor.assets[donation.coin] = (donor.assets[donation.coin] || 0) + donation.amount;
donor.usd += donation.amount * price;
if (price > 0) donor.usdKnown = true;
donor.lastDate = Math.max(donor.lastDate, donation.date || 0);
if (!donor.name && donation.donorName) donor.name = donation.donorName;
});
return keys.map(function (key) {
return donors[key];
}).sort(function (a, b) {
if (b.usd !== a.usd) return b.usd - a.usd;
if (b.donations !== a.donations) return b.donations - a.donations;
return b.lastDate - a.lastDate;
}).slice(0, DONATION_LEADERBOARD_LIMIT);
}
function donationLeaderboardHtml(leaders, pricesAvailable, failedNetworks) {
var rows = leaders.map(function (donor, index) {
var label = donor.name || donationShortAddress(donor.address);
var addressUrl = donor.explorer + "/address/" + encodeURIComponent(donor.address);
var donorHtml = '<a href="' + addressUrl + '" target="_blank" rel="noopener">' + esc(label) + "</a>";
if (donor.name) {
donorHtml += '<div class="muted-line">' + esc(donationShortAddress(donor.address)) + "</div>";
}
return "<tr>" +
"<td>" + (index + 1) + "</td>" +
"<td>" + donorHtml + "</td>" +
"<td>" + donor.donations + "</td>" +
"<td>" + donationAssetsHtml(donor.assets) + "</td>" +
"<td>" + donationFormatUsd(donor.usd, pricesAvailable && donor.usdKnown) + "</td>" +
"<td>" + esc(donationFormatDate(donor.lastDate)) + "</td>" +
"</tr>";
}).join("");
var html = '<div class="table-wrap"><table>' +
"<thead><tr><th>Rank</th><th>Donor</th><th>Donations</th><th>Assets</th><th>USD est.</th><th>Last donation</th></tr></thead>" +
"<tbody>" + rows + "</tbody></table></div>";
if (failedNetworks.length) {
html += '<p class="muted-line">Could not load: ' + esc(failedNetworks.join(", ")) + ".</p>";
}
return html;
}
function renderDonationLeaderboard() {
var box = document.getElementById("donation-leaderboard");
if (!box) return;
var wallets = getDonationWallets();
if (!wallets.length) {
box.innerHTML = '<p class="muted-line">No donation wallets configured.</p>';
return;
}
box.innerHTML = '<p class="muted-line">Loading donation leaderboard...</p>';
var walletResults = wallets.map(function (wallet) {
return fetchWalletDonations(wallet).then(function (donations) {
return { wallet: wallet, donations: donations, error: null };
}).catch(function (error) {
return { wallet: wallet, donations: [], error: error };
});
});
Promise.all(walletResults).then(function (results) {
var hasDonations = results.some(function (result) {
return result.donations.length > 0;
});
var pricePromise = hasDonations ? fetchDonationPrices(wallets) : Promise.resolve({});
return pricePromise.then(function (prices) {
var donations = [];
var failedNetworks = [];
results.forEach(function (result) {
if (result.error) {
failedNetworks.push(result.wallet.network);
} else {
donations = donations.concat(result.donations);
}
});
var leaders = aggregateDonationLeaderboard(donations, prices);
if (!leaders.length) {
box.innerHTML = failedNetworks.length
? '<p class="muted-line">Donation data is temporarily unavailable.</p>'
: '<p class="muted-line">No confirmed donations yet.</p>';
return;
}
var pricesAvailable = Object.keys(prices).length > 0;
box.innerHTML = donationLeaderboardHtml(leaders, pricesAvailable, failedNetworks);
});
}).catch(function () {
box.innerHTML = '<p class="muted-line">Donation data is temporarily unavailable.</p>';
});
}

View File

@@ -1485,6 +1485,7 @@ function loadPage() {
if (hasApiToken) insertApiCard(); if (hasApiToken) insertApiCard();
renderChangelog(); renderChangelog();
renderContributors(); renderContributors();
if (typeof renderDonationLeaderboard === "function") renderDonationLeaderboard();
rememberPage(route); rememberPage(route);
if (window.location.hash) { if (window.location.hash) {