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

288 lines
10 KiB
JavaScript

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"));