285 lines
9.1 KiB
JavaScript
285 lines
9.1 KiB
JavaScript
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))
|
|
};
|
|
}
|
|
|