const LOAN_PAYMENT_FEE_ATOMIC = 1000000n; let loanPaymentContracts = []; let loanPaymentLoadPromise = null; let loanPaymentContractsKey = ""; function setLoanPaymentMessage(message, type = "") { setScopedTransactionMessage("Loan Payment", loanPaymentMessage, message, type); } function selectedLoanPaymentContract() { const hash = String(loanPaymentContractSelect?.value || ""); return loanPaymentContracts.find((contract) => contract.contract === hash) || null; } function loanPaymentPeriodLabel(period) { return { daily: "Daily", weekly: "Weekly", monthly: "Monthly" }[String(period || "").toLowerCase()] || String(period || "Payment"); } function populateLoanPaymentContracts() { if (!loanPaymentContractSelect) { return; } const previous = loanPaymentContractSelect.value; loanPaymentContractSelect.innerHTML = ""; if (loanPaymentContracts.length) { const placeholder = document.createElement("option"); placeholder.value = ""; placeholder.textContent = "Select an active loan"; loanPaymentContractSelect.appendChild(placeholder); } loanPaymentContracts.forEach((contract) => { const option = document.createElement("option"); option.value = contract.contract; const remaining = formatAtomicBalance(contract.remaining_after_pending || "0"); option.textContent = `${contract.loan_asset} loan from ${truncateMiddle(contract.lender, 8, 8)} (${remaining} remaining)`; loanPaymentContractSelect.appendChild(option); }); if (!loanPaymentContracts.length) { const option = document.createElement("option"); option.value = ""; option.textContent = "No active borrower loans"; loanPaymentContractSelect.appendChild(option); } else if ([...loanPaymentContractSelect.options].some((option) => option.value === previous)) { loanPaymentContractSelect.value = previous; } updateLoanPaymentDerivedFields(true); } async function loadLoanPaymentContracts(force = false) { if (loanPaymentLoadPromise) { return loanPaymentLoadPromise; } const invoke = window.__TAURI__?.core?.invoke; const broadcastNode = activeBroadcastNode(); if (!invoke || !broadcastNode || !walletLoaded || !registrationRegistered) { return; } const key = `${activeWalletAddressShort || activeWalletAddressHex}|${broadcastNode}`; if (!force && loanPaymentContractsKey === key) { populateLoanPaymentContracts(); return; } loanPaymentLoadPromise = (async () => { setLoanPaymentMessage("Loading active loans..."); const contracts = await invoke("active_loan_contracts", { broadcastNode }); loanPaymentContracts = Array.isArray(contracts) ? contracts : []; loanPaymentContractsKey = key; populateLoanPaymentContracts(); setLoanPaymentMessage( loanPaymentContracts.length ? "" : "No active loans require payment." ); })().catch((error) => { loanPaymentContracts = []; populateLoanPaymentContracts(); setLoanPaymentMessage(String(error), "error"); throw error; }).finally(() => { loanPaymentLoadPromise = null; }); return loanPaymentLoadPromise; } function expectedLoanPayment(contract) { if (!contract) { return 0n; } const scheduled = BigInt(contract.payment_amount || "0"); const remaining = BigInt(contract.remaining_after_pending || "0"); return scheduled < remaining ? scheduled : remaining; } function updateLoanPaymentWarning(contract, amount) { if (!loanPaymentWarning) { return; } const expected = expectedLoanPayment(contract); const show = contract && amount !== null && amount > 0n && amount < expected; loanPaymentWarning.classList.toggle("hidden", !show); loanPaymentWarning.textContent = show ? `This is less than the expected ${atomicToDecimal(expected)} ${contract.loan_asset} payment. Partial payments are allowed, but the unpaid amount still counts toward the loan's overdue balance.` : ""; } function updateLoanPaymentDerivedFields(setSuggestedAmount = false) { const contract = selectedLoanPaymentContract(); if (!contract) { loanPaymentLender.textContent = "--"; loanPaymentAsset.textContent = "--"; loanPaymentScheduled.textContent = "--"; loanPaymentConfirmed.textContent = "--"; loanPaymentPending.textContent = "--"; loanPaymentRemaining.textContent = "--"; loanPaymentTipInput.value = ""; loanPaymentFeeInput.value = atomicToDecimal(LOAN_PAYMENT_FEE_ATOMIC); updateLoanPaymentWarning(null, null); return; } const expected = expectedLoanPayment(contract); if (setSuggestedAmount || !loanPaymentValueInput.value) { loanPaymentValueInput.value = atomicToDecimal(expected); } const amount = decimalToAtomic(loanPaymentValueInput.value); const tip = amount !== null && amount > 0n ? (amount + 99n) / 100n : 0n; loanPaymentLender.textContent = contract.lender; loanPaymentAsset.textContent = contract.loan_asset; loanPaymentScheduled.textContent = `${loanPaymentPeriodLabel(contract.payment_period)}: ${atomicToDecimal(BigInt(contract.payment_amount || "0"))} ${contract.loan_asset}`; loanPaymentConfirmed.textContent = `${atomicToDecimal(BigInt(contract.confirmed_paid || "0"))} ${contract.loan_asset}`; loanPaymentPending.textContent = `${atomicToDecimal(BigInt(contract.pending_paid || "0"))} ${contract.loan_asset}`; loanPaymentRemaining.textContent = `${atomicToDecimal(BigInt(contract.remaining_after_pending || "0"))} ${contract.loan_asset}`; loanPaymentTipInput.value = atomicToDecimal(tip); loanPaymentFeeInput.value = atomicToDecimal(LOAN_PAYMENT_FEE_ATOMIC); updateLoanPaymentWarning(contract, amount); } function loanPaymentBalance(asset) { return activeWalletBalances.find((balance) => { return String(balance.asset || "").trim().toLowerCase() === String(asset || "").trim().toLowerCase() && Number(balance.nft_series || 0) === 0; }); } function validateLoanPaymentForm() { if (!walletLoaded) { return "Load a wallet before making a loan payment."; } if (!registrationRegistered) { return "Address registration is required before making a loan payment."; } const contract = selectedLoanPaymentContract(); if (!contract) { return "Select an active loan."; } const amount = decimalToAtomic(loanPaymentValueInput?.value); if (amount === null || amount <= 0n) { return "Enter a valid payment amount."; } const remaining = BigInt(contract.remaining_after_pending || "0"); if (amount > remaining) { return `Payment cannot exceed the remaining ${atomicToDecimal(remaining)} ${contract.loan_asset}.`; } const tip = (amount + 99n) / 100n; const available = balanceAtomicValue(loanPaymentBalance(contract.loan_asset)); if (amount + tip > available) { return `This wallet cannot cover the payment and 1% miner tip in ${contract.loan_asset}.`; } const baseBalance = baseCoinBalanceAtomic(); if (isBaseTransferAsset(contract.loan_asset)) { if (amount + tip + LOAN_PAYMENT_FEE_ATOMIC > available) { return `This wallet cannot cover the payment, tip, and ${atomicToDecimal(LOAN_PAYMENT_FEE_ATOMIC)} ${activeBaseCoin()} fee.`; } } else if (baseBalance < LOAN_PAYMENT_FEE_ATOMIC) { return `This payment requires ${atomicToDecimal(LOAN_PAYMENT_FEE_ATOMIC)} ${activeBaseCoin()} for the transaction fee.`; } return ""; } function loanPaymentPayload() { const contract = selectedLoanPaymentContract(); return { broadcastNode: activeBroadcastNode(), contractHash: contract.contract, paybackAmount: String(decimalToAtomic(loanPaymentValueInput.value)) }; } function setLoanPaymentButtonsDisabled(disabled) { document.querySelector("[data-loan-payment-save]")?.toggleAttribute("disabled", disabled); document.querySelector("[data-loan-payment-broadcast]")?.toggleAttribute("disabled", disabled); } function clearLoanPaymentForm() { loanPaymentContractSelect.value = ""; loanPaymentValueInput.value = ""; updateLoanPaymentDerivedFields(false); } async function handleLoanPaymentAction(action) { const validationError = validateLoanPaymentForm(); if (validationError) { setLoanPaymentMessage(validationError, "error"); return; } const invoke = window.__TAURI__?.core?.invoke; if (!invoke) { setLoanPaymentMessage("Wallet bridge is not available.", "error"); return; } const walletKey = await requestSigningWalletKey(); if (guiRuntimeSettings.require_key_before_signing !== false && !walletKey) { setLoanPaymentMessage("Signing cancelled."); return; } setLoanPaymentButtonsDisabled(true); try { if (action === "save") { setLoanPaymentMessage("Saving loan payment transaction..."); const review = await invoke("save_loan_payment_transaction", { ...loanPaymentPayload(), walletKey }); clearLoanPaymentForm(); setLoanPaymentMessage(`Loan payment saved: ${review.file_name}`, "success"); if (typeof refreshUnbroadcastTransactions === "function") { refreshUnbroadcastTransactions(); } } else { setLoanPaymentMessage("Broadcasting loan payment..."); const result = await invoke("broadcast_loan_payment_transaction", { ...loanPaymentPayload(), walletKey }); await loadLoanPaymentContracts(true); clearLoanPaymentForm(); setLoanPaymentMessage(result.response || "Loan payment broadcast.", "success"); } } catch (error) { setLoanPaymentMessage(String(error), "error"); await loadLoanPaymentContracts(true).catch(() => {}); } finally { setLoanPaymentButtonsDisabled(false); } } function prepareLoanPaymentForm() { setLoanPaymentMessage(""); loadLoanPaymentContracts(false).catch(() => { // The form displays the lookup error. }); } function resetLoanPaymentSession() { loanPaymentContracts = []; loanPaymentLoadPromise = null; loanPaymentContractsKey = ""; loanPaymentContractSelect.innerHTML = ""; loanPaymentValueInput.value = ""; updateLoanPaymentDerivedFields(false); setLoanPaymentMessage(""); } loanPaymentContractSelect?.addEventListener("change", () => { setLoanPaymentMessage(""); updateLoanPaymentDerivedFields(true); }); loanPaymentValueInput?.addEventListener("input", () => { setLoanPaymentMessage(""); updateLoanPaymentDerivedFields(false); }); document .querySelector("[data-loan-payment-save]") ?.addEventListener("click", () => handleLoanPaymentAction("save")); document .querySelector("[data-loan-payment-broadcast]") ?.addEventListener("click", () => handleLoanPaymentAction("broadcast"));