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 = `
| Date |
Amount |
Payer |
Tx Hash |
${payments.map((payment) => `
| ${escapeLoanText(payment.date || "--")} |
${escapeLoanText(payment.amount || "0")} ${escapeLoanText(contract.loan_asset || "")} |
${escapeLoanText(loanShort(payment.payer || "", 8, 8))} |
${escapeLoanText(loanShort(payment.txid || "", 8, 8))} |
`).join("")}
`;
}
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);
}
});