mirror of
https://gitlab.com/vyla-entertainment/documentation.git
synced 2026-11-14 22:47:26 -05:00
added donation leaderboard
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -20,3 +20,9 @@ All profits generated through Vyla will go directly to its developers as compens
|
|||||||
| 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
495
src/js/api/donations.js
Normal 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 "<$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>';
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user