let loansContracts = []; let loansLoadedForKey = ""; let loansLoadInFlight = false; let selectedLoanContractHash = ""; let activeLoansStatusFilter = "all"; let loansStateMessage = ""; function loanFormatAtomic(value) { if (typeof atomicToDecimal === "function") { return atomicToDecimal(BigInt(value || "0")); } if (typeof formatAtomicBalance === "function") { return formatAtomicBalance(value || "0"); } return String(value || "0"); } function loanShort(value, left = 8, right = 8) { if (typeof truncateMiddle === "function") { return truncateMiddle(value, left, right); } const text = String(value || ""); return text.length > left + right + 3 ? `${text.slice(0, left)}...${text.slice(-right)}` : text; } function escapeLoanText(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function activeLoanRole(contract) { const active = String(activeWalletAddressShort || "").trim().toLowerCase(); if (String(contract.lender || "").trim().toLowerCase() === active) { return "lender"; } if (String(contract.borrower || "").trim().toLowerCase() === active) { return "borrower"; } return "unknown"; } function loanRoleLabel(role) { return role === "lender" ? "Lending" : role === "borrower" ? "Borrowing" : "Related"; } function loanCounterparty(contract) { return activeLoanRole(contract) === "lender" ? contract.borrower : contract.lender; } function loanStatusClass(status) { const normalized = String(status || "").toLowerCase(); if (normalized === "delinquent") { return "due"; } if (normalized === "inactive") { return "claimable"; } return "active"; } function loanStatusLabel(status) { const normalized = String(status || "").toLowerCase(); if (normalized === "inactive") { return "Completed"; } return normalized ? normalized.charAt(0).toUpperCase() + normalized.slice(1) : "Unknown"; } function loanPaymentCount(contract) { return Array.isArray(contract.payments) ? contract.payments.length : 0; } function loanAmountLabel(value, asset) { return `${loanFormatAtomic(value)} ${String(asset || "").trim()}`.trim(); } function addUtcMonths(timestampSeconds, monthsToAdd) { const start = new Date(Number(timestampSeconds || 0) * 1000); if (Number.isNaN(start.getTime())) { return null; } const year = start.getUTCFullYear(); const month = start.getUTCMonth() + Number(monthsToAdd || 0); const day = start.getUTCDate(); const lastDay = new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); return new Date(Date.UTC( year, month, Math.min(day, lastDay), start.getUTCHours(), start.getUTCMinutes(), start.getUTCSeconds() )); } function loanDueDateForPayment(contract, paymentIndex) { const startTimestamp = Number(contract.start_timestamp || 0); if (!startTimestamp || !paymentIndex) { return null; } const period = String(contract.payment_period || "").trim().toLowerCase(); if (period === "daily") { return new Date((startTimestamp + paymentIndex * 86_400) * 1000); } if (period === "weekly") { return new Date((startTimestamp + paymentIndex * 7 * 86_400) * 1000); } if (period === "monthly") { return addUtcMonths(startTimestamp, paymentIndex); } return null; } function formatLoanDueDate(date) { if (!(date instanceof Date) || Number.isNaN(date.getTime())) { return "--"; } return date.toLocaleString(undefined, { year: "numeric", month: "short", day: "numeric", hour: "numeric", minute: "2-digit", timeZoneName: "short" }); } function nextLoanPaymentDueLabel(contract) { if (String(contract.status || "").toLowerCase() === "inactive") { return "No remaining payments"; } const paymentAmount = BigInt(contract.payment_amount || "0"); const confirmedPaid = BigInt(contract.confirmed_paid || "0"); const paymentNumber = Number(contract.payment_number || 0); if (!paymentNumber || paymentAmount <= 0n) { return "--"; } const completedPayments = Number(confirmedPaid / paymentAmount); if (completedPayments >= paymentNumber) { return "No remaining payments"; } return formatLoanDueDate(loanDueDateForPayment(contract, completedPayments + 1)); } function sortedLoans() { const query = String(loansSearch?.value || "").trim().toLowerCase(); const roleFilter = loansRoleFilter?.value || "all"; return loansContracts .filter((contract) => { const role = activeLoanRole(contract); const status = String(contract.status || "").toLowerCase(); const searchText = [ contract.contract, contract.lender, contract.borrower, contract.loan_asset, contract.collateral, status, role ].join(" ").toLowerCase(); if (roleFilter !== "all" && role !== roleFilter) { return false; } if (activeLoansStatusFilter !== "all" && status !== activeLoansStatusFilter) { return false; } return !query || searchText.includes(query); }) .sort((left, right) => Number(right.start_timestamp || 0) - Number(left.start_timestamp || 0)); } function renderLoanCard(contract) { const role = activeLoanRole(contract); const selected = contract.contract === selectedLoanContractHash; const counterparty = loanCounterparty(contract); const principal = loanAmountLabel(contract.loan_amount, contract.loan_asset); const remaining = loanAmountLabel(contract.remaining_after_pending, contract.loan_asset); const payments = `${loanPaymentCount(contract)} / ${contract.payment_number || 0}`; const nextDue = nextLoanPaymentDueLabel(contract); return ` `; } function selectedLoanContract() { return loansContracts.find((contract) => contract.contract === selectedLoanContractHash) || null; } function detailItem(label, value) { return `
${escapeLoanText(label)}${escapeLoanText(value || "--")}
`; } function renderLoanPaymentHistory(contract) { if (!loanPaymentHistory) { return; } const payments = Array.isArray(contract?.payments) ? contract.payments : []; if (!payments.length) { loanPaymentHistory.innerHTML = '
No confirmed payments recorded for this loan.
'; return; } loanPaymentHistory.innerHTML = ` ${payments.map((payment) => ` `).join("")}
Date Amount Payer Tx Hash
${escapeLoanText(payment.date || "--")} ${escapeLoanText(payment.amount || "0")} ${escapeLoanText(contract.loan_asset || "")} ${escapeLoanText(loanShort(payment.payer || "", 8, 8))} ${escapeLoanText(loanShort(payment.txid || "", 8, 8))}
`; } function renderLoanDetail() { const contract = selectedLoanContract(); if (!loanDetailGrid) { return; } if (!contract) { loanDetailGrid.innerHTML = detailItem("Selected Loan", "Select a loan to view details."); if (loanPaymentHistory) { loanPaymentHistory.innerHTML = '
No loan selected.
'; } loanCopyContractButton?.toggleAttribute("disabled", true); loanMakePaymentButton?.toggleAttribute("disabled", true); loanClaimCollateralButton?.toggleAttribute("disabled", true); return; } const role = activeLoanRole(contract); const collateral = loanAmountLabel(contract.collateral_amount, contract.collateral); const payment = loanAmountLabel(contract.payment_amount, contract.loan_asset); const confirmed = loanAmountLabel(contract.confirmed_paid, contract.loan_asset); const pending = loanAmountLabel(contract.pending_paid, contract.loan_asset); const remaining = loanAmountLabel(contract.remaining_after_pending, contract.loan_asset); loanDetailGrid.innerHTML = [ detailItem("Contract Hash", contract.contract), detailItem("Role", loanRoleLabel(role)), detailItem("Status", loanStatusLabel(contract.status)), detailItem("Principal", loanAmountLabel(contract.loan_amount, contract.loan_asset)), detailItem("Payment Amount", payment), detailItem("Collateral", collateral), detailItem("Payment Schedule", `${contract.payment_number || 0} ${contract.payment_period || "payments"}`), detailItem("Confirmed Paid", confirmed), detailItem("Pending Total", pending), detailItem("Remaining", remaining), detailItem("Lender", contract.lender), detailItem("Borrower", contract.borrower) ].join(""); renderLoanPaymentHistory(contract); loanCopyContractButton?.toggleAttribute("disabled", false); loanMakePaymentButton?.toggleAttribute( "disabled", !(role === "borrower" && String(contract.status || "").toLowerCase() !== "inactive") ); loanClaimCollateralButton?.toggleAttribute("disabled", String(contract.status || "").toLowerCase() === "inactive"); } function renderLoansPage() { if (!loansList) { return; } const rows = sortedLoans(); if (!selectedLoanContractHash && rows.length) { selectedLoanContractHash = rows[0].contract; } if (selectedLoanContractHash && !loansContracts.some((contract) => contract.contract === selectedLoanContractHash)) { selectedLoanContractHash = rows[0]?.contract || ""; } loansList.innerHTML = rows.map(renderLoanCard).join(""); if (!walletLoaded) { loansState && (loansState.textContent = "Load a wallet to view loans."); } else if (loansLoadInFlight && !loansContracts.length) { loansState && (loansState.textContent = "Loading loans..."); } else if (loansStateMessage) { loansState && (loansState.textContent = loansStateMessage); } else if (!loansContracts.length) { loansState && (loansState.textContent = "No loan contracts found for this wallet."); } else if (!rows.length) { loansState && (loansState.textContent = "No loans match the current filters."); } else { loansState && (loansState.textContent = `${rows.length} loan${rows.length === 1 ? "" : "s"} shown`); } renderLoanDetail(); } async function loadLoansPage({ force = false } = {}) { const invoke = window.__TAURI__?.core?.invoke; const broadcastNode = activeBroadcastNode(); if (!walletLoaded) { loansContracts = []; selectedLoanContractHash = ""; loansStateMessage = "Load a wallet to view loans."; renderLoansPage(); return; } if (!invoke) { loansContracts = []; selectedLoanContractHash = ""; loansStateMessage = "Loan lookup skipped: Tauri invoke is unavailable."; renderLoansPage(); return; } if (!broadcastNode) { loansContracts = []; selectedLoanContractHash = ""; loansStateMessage = "Loan lookup skipped: no broadcast node is selected."; renderLoansPage(); return; } const key = `${activeWalletAddressShort || activeWalletAddressHex}|${broadcastNode}`; if (!force && loansLoadedForKey === key) { renderLoansPage(); return; } if (loansLoadInFlight) { return; } loansLoadInFlight = true; loansStateMessage = "Loading loans..."; renderLoansPage(); try { const contracts = await invokeWalletRpcWithBackoff( "loan contracts", () => invoke("loan_contracts", { broadcastNode }), () => currentView === "loans" && activeBroadcastNode() === broadcastNode ); loansContracts = Array.isArray(contracts) ? contracts : []; loansLoadedForKey = key; selectedLoanContractHash = loansContracts[0]?.contract || ""; loansStateMessage = loansContracts.length ? "" : "Loan contract command returned 0 contracts for this wallet."; } catch (error) { loansContracts = []; selectedLoanContractHash = ""; loansStateMessage = `Loan contract lookup failed: ${String(error)}`; } finally { loansLoadInFlight = false; renderLoansPage(); } } function prepareLoansPage({ force = false } = {}) { renderLoansPage(); loadLoansPage({ force }); } function resetLoansPage() { loansContracts = []; loansLoadedForKey = ""; loansLoadInFlight = false; selectedLoanContractHash = ""; activeLoansStatusFilter = "all"; loansStateMessage = ""; loansSearch && (loansSearch.value = ""); loansRoleFilter && (loansRoleFilter.value = "all"); loansStatusButtons.forEach((button) => { button.classList.toggle("active", button.dataset.loansStatusFilter === "all"); }); renderLoansPage(); } loansSearch?.addEventListener("input", renderLoansPage); loansRoleFilter?.addEventListener("change", renderLoansPage); loansRefreshButton?.addEventListener("click", () => loadLoansPage({ force: true })); loansCreateButton?.addEventListener("click", () => { setActiveView("send"); setActiveTransactionType("Create Loan"); }); loansStatusButtons.forEach((button) => { button.addEventListener("click", () => { activeLoansStatusFilter = button.dataset.loansStatusFilter || "all"; loansStatusButtons.forEach((item) => item.classList.toggle("active", item === button)); selectedLoanContractHash = ""; renderLoansPage(); }); }); loansList?.addEventListener("click", (event) => { const card = event.target.closest("[data-loan-contract]"); if (!card) { return; } selectedLoanContractHash = card.dataset.loanContract || ""; renderLoansPage(); }); loanCopyContractButton?.addEventListener("click", async () => { const contract = selectedLoanContract(); if (!contract) { return; } const copied = typeof writeClipboardText === "function" ? await writeClipboardText(contract.contract) : false; if (!copied) { return; } const original = loanCopyContractButton.textContent; loanCopyContractButton.textContent = "Copied"; setTimeout(() => { loanCopyContractButton.textContent = original; }, 900); }); loanMakePaymentButton?.addEventListener("click", () => { const contract = selectedLoanContract(); if (typeof openTransactionType === "function") { openTransactionType("Loan Payment"); } else { setActiveView("send"); setActiveTransactionType("Loan Payment"); showTransactionForm("Loan Payment"); } const selectContract = () => { if (contract?.contract && loanPaymentContractSelect) { loanPaymentContractSelect.value = contract.contract; updateLoanPaymentDerivedFields(true); } }; if (typeof loadLoanPaymentContracts === "function") { loadLoanPaymentContracts(false).then(selectContract).catch(() => {}); } else { setTimeout(selectContract, 0); } }); loanClaimCollateralButton?.addEventListener("click", () => { const contract = selectedLoanContract(); if (typeof openTransactionType === "function") { openTransactionType("Collateral Claim"); } else { setActiveView("send"); setActiveTransactionType("Collateral Claim"); showTransactionForm("Collateral Claim"); } const selectContract = () => { if (contract?.contract && collateralClaimContractSelect) { collateralClaimContractSelect.value = contract.contract; updateCollateralClaimDetails(); } }; if (typeof loadCollateralClaimContracts === "function") { loadCollateralClaimContracts(false).then(selectContract).catch(() => {}); } else { setTimeout(selectContract, 0); } });