288 lines
10 KiB
JavaScript
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"));
|