Contractless-GUI-Wallet/frontend/js/dashboard_network.js

285 lines
9.1 KiB
JavaScript
Raw Normal View History

2026-07-01 16:01:30 +00:00
function renderDashboardNetworkInfo(info) {
if (!info || typeof info !== "object") {
return;
}
latestKnownBlockHeight = Number(info.height || latestKnownBlockHeight || 0);
setBridgeStatus(`${networkDisplayName(info)} connected`, "success");
networkInfoHeight && (networkInfoHeight.textContent = formatNumber(info.height));
networkInfoNetwork && (networkInfoNetwork.textContent = networkDisplayName(info));
networkInfoDifficulty && (networkInfoDifficulty.textContent = formatNumber(info.next_block_difficulty));
networkInfoMempool && (networkInfoMempool.textContent = formatNumber(info.total_mempool_transactions));
networkInfoChainTx && (networkInfoChainTx.textContent = formatNumber(info.total_block_transactions));
}
function renderNodeInfo(info, checkedAt = new Date()) {
const broadcastNode = activeBroadcastNode();
nodeInfoNode && (nodeInfoNode.textContent = broadcastNode || "--");
if (!info || typeof info !== "object") {
nodeInfoHeight && (nodeInfoHeight.textContent = "--");
nodeInfoTime && (nodeInfoTime.textContent = "--");
nodeInfoLastChecked && (nodeInfoLastChecked.textContent = "--");
nodeInfoLargestFee && (nodeInfoLargestFee.textContent = "--");
return;
}
nodeInfoHeight && (nodeInfoHeight.textContent = formatNumber(info.height));
nodeInfoTime && (nodeInfoTime.textContent = formatNetworkTime(info.time));
nodeInfoLastChecked && (nodeInfoLastChecked.textContent = checkedAt.toLocaleTimeString());
nodeInfoLargestFee && (nodeInfoLargestFee.textContent = formatLargestFee(info.largest_tx_fee));
}
function resetNetworkInfoDisplays() {
const networkName = activeNetwork === "mainnet" ? "Mainnet" : "Testnet";
networkInfoHeight && (networkInfoHeight.textContent = "--");
networkInfoNetwork && (networkInfoNetwork.textContent = `${networkName} / ${activeBaseCoin()}`);
networkInfoDifficulty && (networkInfoDifficulty.textContent = "--");
networkInfoMempool && (networkInfoMempool.textContent = "--");
networkInfoChainTx && (networkInfoChainTx.textContent = "--");
setBridgeStatus(`${networkName} disconnected`, "error");
renderNodeInfo(null);
}
async function refreshNetworkInfo({ target = "dashboard", force = false } = {}) {
if (!walletLoaded) {
renderNodeInfo(null);
setBridgeStatus(`${activeNetwork === "mainnet" ? "Mainnet" : "Testnet"} disconnected`, "error");
return null;
}
const invoke = window.__TAURI__?.core?.invoke;
const broadcastNode = activeBroadcastNode();
if (!invoke || !broadcastNode) {
setBridgeStatus(`${activeNetwork === "mainnet" ? "Mainnet" : "Testnet"} unavailable`, "error");
return null;
}
if (target === "node" && nodeInfoRefreshInFlight) {
return null;
}
if (target !== "node" && networkInfoRefreshInFlight) {
return null;
}
if (target === "node") {
nodeInfoRefreshInFlight = true;
} else {
networkInfoRefreshInFlight = true;
}
try {
const info = await invokeWalletRpcWithBackoff(
"Network info lookup",
() => invoke("network_info", { broadcastNode }),
() => walletLoaded && activeBroadcastNode() === broadcastNode
);
const checkedAt = new Date();
renderDashboardNetworkInfo(info);
if (target === "node" || force) {
renderNodeInfo(info, checkedAt);
}
return info;
} catch (error) {
setBridgeStatus(`${activeNetwork === "mainnet" ? "Mainnet" : "Testnet"} disconnected`, "error");
if (target === "node") {
nodeInfoLastChecked && (nodeInfoLastChecked.textContent = String(error));
}
return null;
} finally {
if (target === "node") {
nodeInfoRefreshInFlight = false;
} else {
networkInfoRefreshInFlight = false;
}
}
}
function sleepMs(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function walletRpcBackoffDelay(attempt) {
return Math.min(WALLET_RPC_BACKOFF_BASE_MS * 2 ** Math.max(0, attempt - 1), WALLET_RPC_BACKOFF_MAX_MS);
}
async function invokeWalletRpcWithBackoff(label, operation, shouldContinue = () => true) {
let lastError = null;
for (let attempt = 1; attempt <= WALLET_RPC_RETRY_LIMIT; attempt += 1) {
if (!shouldContinue()) {
throw new Error(`${label} cancelled`);
}
try {
return await operation();
} catch (error) {
lastError = error;
if (attempt >= WALLET_RPC_RETRY_LIMIT || !shouldContinue()) {
break;
}
await sleepMs(walletRpcBackoffDelay(attempt));
}
}
throw lastError || new Error(`${label} failed`);
}
function bytesToHex(bytes) {
return Array.from(bytes || [])
.map((byte) => Number(byte).toString(16).padStart(2, "0"))
.join("");
}
function bytesToText(bytes) {
return String.fromCharCode(...Array.from(bytes || [])).trim();
}
function readU32LE(bytes, offset) {
return (
Number(bytes[offset] || 0) |
(Number(bytes[offset + 1] || 0) << 8) |
(Number(bytes[offset + 2] || 0) << 16) |
(Number(bytes[offset + 3] || 0) << 24)
) >>> 0;
}
function readU64LE(bytes, offset) {
let value = 0n;
for (let index = 7; index >= 0; index -= 1) {
value = (value << 8n) + BigInt(Number(bytes[offset + index] || 0));
}
return value;
}
function formatAtomicValue(value) {
const atomic = typeof value === "bigint" ? value : BigInt(value || 0);
const whole = atomic / 100000000n;
const fraction = atomic % 100000000n;
const fractionText = fraction.toString().padStart(8, "0").replace(/0+$/, "");
return fractionText ? `${whole}.${fractionText}` : whole.toString();
}
function bytesToAddressHex(bytes) {
return bytesToHex(bytes).toLowerCase();
}
function addressLabel(addressHex) {
return addressHex ? `${addressHex.slice(0, 8)}...${addressHex.slice(-8)}` : "Unknown";
}
function isActiveAddress(addressHex) {
return Boolean(addressHex && activeWalletAddressHex && addressHex.toLowerCase() === activeWalletAddressHex.toLowerCase());
}
function isActiveShortAddress(address) {
return Boolean(address && activeWalletAddressShort && String(address).trim().toLowerCase() === activeWalletAddressShort.toLowerCase());
}
function isActiveWalletAddress(address) {
const clean = String(address || "").trim().toLowerCase();
if (!clean) {
return false;
}
return clean.includes(".") ? isActiveShortAddress(clean) : isActiveAddress(clean);
}
function assetWithSeries(asset, series) {
const cleanAsset = (asset || "").trim() || activeBaseCoin();
return Number(series || 0) > 0 ? `${cleanAsset} #${series}` : cleanAsset;
}
function signedAmount(sign, value) {
const formatted = formatAtomicValue(value);
if (!formatted || formatted === "0") {
return formatted;
}
return `${sign}${formatted}`;
}
function signedBaseFee(sign, value) {
const amount = signedAmount(sign, value);
return amount && amount !== "0" ? `${amount} ${activeBaseCoin()}` : "--";
}
function statusForBlock(blockHeight, currentHeight = latestKnownBlockHeight) {
if (!currentHeight || !blockHeight || currentHeight < blockHeight) {
return "Pending";
}
return currentHeight - blockHeight >= CONFIRMED_AFTER_BLOCKS ? "Confirmed" : "Pending";
}
function walletStatusForBlock(blockHeight, currentHeight = latestKnownBlockHeight) {
const height = Number(blockHeight || 0);
if (!height) {
return "Unconfirmed";
}
return statusForBlock(height, currentHeight);
}
function blockValueFromRecord(record) {
const block = record?.block ?? record?.block_height ?? record?.blockHeight ?? null;
const numeric = Number(block || 0);
return numeric > 0 ? numeric : null;
}
function dashboardRow(base, type, asset, counterparty, amount, fee = "--") {
return {
...base,
type,
asset,
counterparty,
amount,
fee
};
}
function txTypeName(type) {
return {
0: "Genesis",
1: "Reward",
2: "Transfer",
3: "Create Token",
4: "Create NFT",
5: "Marketing",
6: "Swap",
7: "Create Loan",
8: "Loan Payment",
9: "Collateral Claim",
10: "Burn",
11: "Issue Token",
12: "Vanity Address"
}[type] || "Transaction";
}
function parseLoanContractSummary(transaction) {
if (transaction && typeof transaction === "object" && !Array.isArray(transaction)) {
const unsigned = transaction.unsigned_loan_contract;
if (!unsigned) {
return null;
}
return {
loanCoin: unsigned.loan_coin || "Loan",
loanAmount: unsigned.loan_amount || 0,
lender: unsigned.lender || "",
collateral: unsigned.collateral || "Collateral",
collateralAmount: unsigned.collateral_amount || 0,
borrower: unsigned.borrower || ""
};
}
const bytes = Array.from(transaction || []);
if (Number(bytes[0] || 0) !== 7) {
return null;
}
return {
loanCoin: bytesToText(bytes.slice(5, 20)) || "Loan",
loanAmount: readU64LE(bytes, 20),
lender: bytesToAddressHex(bytes.slice(28, 50)),
collateral: bytesToText(bytes.slice(50, 71)) || "Collateral",
collateralAmount: readU64LE(bytes, 71),
borrower: bytesToAddressHex(bytes.slice(79, 101))
};
}