From 285f3d512ea33ca18ffca87ae1cbff952a6cf792 Mon Sep 17 00:00:00 2001 From: alwaysmesmerizingyou Date: Sat, 14 Nov 2026 15:39:48 -0800 Subject: [PATCH] added donation leaderboard --- index.html | 1 + misc/project/support-us.md | 8 +- src/js/api/donations.js | 495 +++++++++++++++++++++++++++++++++++++ src/js/main.js | 1 + 4 files changed, 504 insertions(+), 1 deletion(-) create mode 100644 src/js/api/donations.js diff --git a/index.html b/index.html index bda06c9..e3bffbf 100644 --- a/index.html +++ b/index.html @@ -186,6 +186,7 @@ + diff --git a/misc/project/support-us.md b/misc/project/support-us.md index 7b4144a..5c6b5bf 100644 --- a/misc/project/support-us.md +++ b/misc/project/support-us.md @@ -19,4 +19,10 @@ All profits generated through Vyla will go directly to its developers as compens | ETH | Ethereum | `0xe647BC3401073b6EBFc426ca504f607078845dd1` | | BTC | Bitcoin | `bc1qp69euu24h47r68sznta5vzpcp0rudtm3gnxau8` | | SOL | Solana | `BHx5JYyGgGvgRaQKe8kCh4Fym8LpiUxtexmv7SBV6pVQ` | -| LTC | Litecoin | `LgkLXSgEX3yt8K5oaEmL6aPoCAAQBz6Bur` | \ No newline at end of file +| LTC | Litecoin | `LgkLXSgEX3yt8K5oaEmL6aPoCAAQBz6Bur` | + +## Donation leaderboard + +
+ +

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.

\ No newline at end of file diff --git a/src/js/api/donations.js b/src/js/api/donations.js new file mode 100644 index 0000000..6db314f --- /dev/null +++ b/src/js/api/donations.js @@ -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 "<$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 = '' + esc(label) + ""; + if (donor.name) { + donorHtml += '
' + esc(donationShortAddress(donor.address)) + "
"; + } + + return "" + + "" + (index + 1) + "" + + "" + donorHtml + "" + + "" + donor.donations + "" + + "" + donationAssetsHtml(donor.assets) + "" + + "" + donationFormatUsd(donor.usd, pricesAvailable && donor.usdKnown) + "" + + "" + esc(donationFormatDate(donor.lastDate)) + "" + + ""; + }).join(""); + + var html = '
' + + "" + + "" + rows + "
RankDonorDonationsAssetsUSD est.Last donation
"; + + if (failedNetworks.length) { + html += '

Could not load: ' + esc(failedNetworks.join(", ")) + ".

"; + } + return html; +} + +function renderDonationLeaderboard() { + var box = document.getElementById("donation-leaderboard"); + if (!box) return; + + var wallets = getDonationWallets(); + if (!wallets.length) { + box.innerHTML = '

No donation wallets configured.

'; + return; + } + + box.innerHTML = '

Loading donation leaderboard...

'; + + 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 + ? '

Donation data is temporarily unavailable.

' + : '

No confirmed donations yet.

'; + return; + } + + var pricesAvailable = Object.keys(prices).length > 0; + box.innerHTML = donationLeaderboardHtml(leaders, pricesAvailable, failedNetworks); + }); + }).catch(function () { + box.innerHTML = '

Donation data is temporarily unavailable.

'; + }); +} diff --git a/src/js/main.js b/src/js/main.js index 2596ebc..add5b34 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -1485,6 +1485,7 @@ function loadPage() { if (hasApiToken) insertApiCard(); renderChangelog(); renderContributors(); + if (typeof renderDonationLeaderboard === "function") renderDonationLeaderboard(); rememberPage(route); if (window.location.hash) {