const transferSaveButton = document.querySelector("[data-transfer-save]"); const transferBroadcastButton = document.querySelector("[data-transfer-broadcast]"); const ATOMIC_UNITS_PER_COIN = 100000000n; const NON_BASE_TRANSFER_MIN_FEE_ATOMIC = 100000000n; let transferBalancesRefreshPromise = null; function setTransferMessage(message, type = "") { setScopedTransactionMessage("Transfer", transferMessage, message, type); } function balanceAtomicValue(balance) { try { return BigInt(balance?.balance || "0"); } catch (_) { return 0n; } } function transferAssetKey(balance) { return `${balance?.asset || ""}|${Number(balance?.nft_series || 0)}`; } function transferAssetLabel(balance) { const amount = formatAtomicBalance(balance?.balance || "0"); const series = Number(balance?.nft_series || 0); if (series > 0) { return `${balance.asset} #${series} (${amount})`; } return `${balance.asset} (${amount})`; } function decimalToAtomic(value) { const text = String(value || "").trim(); if (!/^\d+(\.\d{0,8})?$/.test(text)) { return null; } const [whole, fraction = ""] = text.split("."); const paddedFraction = fraction.padEnd(8, "0"); return BigInt(whole || "0") * ATOMIC_UNITS_PER_COIN + BigInt(paddedFraction || "0"); } function atomicToDecimal(value) { const atomic = BigInt(value || 0); const whole = atomic / ATOMIC_UNITS_PER_COIN; const fraction = String(atomic % ATOMIC_UNITS_PER_COIN).padStart(8, "0"); return `${whole}.${fraction}`; } function isBaseTransferAsset(asset) { return String(asset || "").trim().toUpperCase() === activeBaseCoin(); } function minimumTransferFee(amount, asset) { if (isBaseTransferAsset(asset)) { return (amount + 99n) / 100n; } return NON_BASE_TRANSFER_MIN_FEE_ATOMIC; } function baseCoinBalanceAtomic() { const baseCoin = activeBaseCoin(); const baseBalance = (Array.isArray(activeWalletBalances) ? activeWalletBalances : []).find((balance) => { return String(balance.asset || "").trim().toUpperCase() === baseCoin && !Number(balance.nft_series || 0); }); return balanceAtomicValue(baseBalance); } function selectedTransferAsset() { if (!transferAssetSelect?.value) { return null; } const option = transferAssetSelect.selectedOptions?.[0]; if (!option) { return null; } return { asset: option.dataset.asset || "", nftSeries: Number(option.dataset.nftSeries || 0), balance: BigInt(option.dataset.balance || "0") }; } function updateTransferDerivedFields() { const selected = selectedTransferAsset(); const sender = activeWalletAddressShort || activeWalletAddressHex || "--"; if (transferSender) { transferSender.textContent = sender; } if (!selected) { if (transferCoin) { transferCoin.textContent = "--"; } if (transferNftSeries) { transferNftSeries.textContent = "0"; } if (transferBalance) { transferBalance.textContent = "--"; } if (transferAmountInput) { transferAmountInput.disabled = false; } if (transferFeeInput) { transferFeeInput.value = ""; } return; } if (transferCoin) { transferCoin.textContent = selected.asset || "--"; } if (transferNftSeries) { transferNftSeries.textContent = String(selected.nftSeries || 0); } if (transferBalance) { const seriesLabel = selected.nftSeries > 0 ? ` #${selected.nftSeries}` : ""; transferBalance.textContent = `${formatAtomicBalance(String(selected.balance))} ${selected.asset}${seriesLabel}`; } if (transferAmountInput) { if (selected.nftSeries > 0) { transferAmountInput.value = "1"; transferAmountInput.disabled = true; } else { transferAmountInput.disabled = false; } } updateTransferFee(); } function updateTransferFee() { if (!transferFeeInput) { return; } const selected = selectedTransferAsset(); if (!selected) { transferFeeInput.value = ""; return; } const amount = selected.nftSeries > 0 ? 1n : decimalToAtomic(transferAmountInput?.value); if (amount === null || amount <= 0n) { transferFeeInput.value = ""; return; } transferFeeInput.value = atomicToDecimal(minimumTransferFee(amount, selected.asset)); } function refreshTransferAssetOptions() { if (!transferAssetSelect) { return; } const previous = transferAssetSelect.value; transferAssetSelect.innerHTML = ""; const spendableBalances = (Array.isArray(activeWalletBalances) ? activeWalletBalances : []) .filter((balance) => (balance.asset || "").trim() && balanceAtomicValue(balance) > 0n) .sort((left, right) => { const leftSeries = Number(left.nft_series || 0); const rightSeries = Number(right.nft_series || 0); if ((left.asset || "") !== (right.asset || "")) { return (left.asset || "").localeCompare(right.asset || ""); } return leftSeries - rightSeries; }); if (!spendableBalances.length) { const option = document.createElement("option"); option.value = ""; option.textContent = walletLoaded ? "No spendable assets found" : "Load a wallet first"; transferAssetSelect.appendChild(option); updateTransferDerivedFields(); return; } spendableBalances.forEach((balance) => { const option = document.createElement("option"); option.value = transferAssetKey(balance); option.textContent = transferAssetLabel(balance); option.dataset.asset = balance.asset || ""; option.dataset.nftSeries = String(Number(balance.nft_series || 0)); option.dataset.balance = String(balance.balance || "0"); transferAssetSelect.appendChild(option); }); if ([...transferAssetSelect.options].some((option) => option.value === previous)) { transferAssetSelect.value = previous; } updateTransferDerivedFields(); } async function refreshTransferBalancesIfNeeded() { if (activeWalletBalances.length || !walletLoaded || !registrationRegistered) { refreshTransferAssetOptions(); return; } if (transferBalancesRefreshPromise) { return transferBalancesRefreshPromise; } const invoke = window.__TAURI__?.core?.invoke; const broadcastNode = activeBroadcastNode(); if (!invoke || !broadcastNode) { refreshTransferAssetOptions(); return; } transferBalancesRefreshPromise = (async () => { setTransferMessage("Loading spendable assets..."); try { const balances = await invoke("get_active_wallet_balances", { broadcastNode }); activeWalletBalances = Array.isArray(balances) ? balances : []; refreshTransferAssetOptions(); setTransferMessage(""); } catch (error) { refreshTransferAssetOptions(); setTransferMessage(String(error), "error"); throw error; } finally { transferBalancesRefreshPromise = null; } })(); return transferBalancesRefreshPromise; } function parseTransferPaymentUri(value) { const text = String(value || "").trim(); let uri; try { uri = new URL(text); } catch (_) { throw new Error("Enter a valid Contractless payment URI."); } if (uri.protocol.toLowerCase() !== "contractless:") { throw new Error("Payment URI must begin with contractless:."); } const encodedAddress = uri.hostname || uri.pathname.replace(/^\/+/, ""); let receiver; try { receiver = decodeURIComponent(encodedAddress).trim(); } catch (_) { throw new Error("Payment URI contains an invalid receiver address."); } if (!receiver) { throw new Error("Payment URI does not contain a receiver address."); } const asset = (uri.searchParams.get("asset") || activeBaseCoin()).trim(); if (!asset) { throw new Error("Payment URI does not contain a valid asset."); } const amount = (uri.searchParams.get("amount") || "").trim(); if (amount) { const atomicAmount = decimalToAtomic(amount); if (atomicAmount === null || atomicAmount <= 0n) { throw new Error("Payment URI contains an invalid amount."); } } const label = (uri.searchParams.get("label") || "").trim(); return { receiver, asset, amount, label }; } async function loadTransferPaymentUri(value) { const payment = parseTransferPaymentUri(value); openTransactionType("Transfer"); await refreshTransferBalancesIfNeeded(); const matchingOption = [...(transferAssetSelect?.options || [])].find((option) => { return ( String(option.dataset.asset || "").trim().toLowerCase() === payment.asset.toLowerCase() && Number(option.dataset.nftSeries || 0) === 0 ); }); if (!matchingOption) { throw new Error(`The active wallet does not have a spendable ${payment.asset} balance.`); } transferAssetSelect.value = matchingOption.value; transferReceiverInput.value = payment.receiver; transferAmountInput.value = payment.amount; if (typeof addPaymentUriAddressBookEntry === "function") { await addPaymentUriAddressBookEntry(payment.receiver, payment.label); } updateTransferDerivedFields(); updateTransferFee(); setTransferMessage(""); } function validateTransferForm() { const selected = selectedTransferAsset(); if (!walletLoaded) { return "Load a wallet before creating a transfer."; } if (!registrationRegistered) { return "Address registration is required before creating a transfer."; } if (!selected) { return "Select a coin, token, or NFT to transfer."; } const receiver = String(transferReceiverInput?.value || "").trim(); if (!receiver) { return "Enter a receiver address."; } if ( receiver.toLowerCase() === String(activeWalletAddressHex || "").toLowerCase() || receiver.toLowerCase() === String(activeWalletAddressShort || "").toLowerCase() ) { return "Receiver cannot be the active wallet address."; } const amount = selected.nftSeries > 0 ? 1n : decimalToAtomic(transferAmountInput?.value); if (amount === null || amount <= 0n) { return "Enter a valid amount with up to 8 decimal places."; } if (selected.nftSeries > 0 && String(transferAmountInput?.value || "").trim() !== "1") { return "NFT transfers must send exactly 1 item."; } if (amount > selected.balance) { return "Amount exceeds the available balance for this asset."; } const expectedFee = minimumTransferFee(amount, selected.asset); const fee = decimalToAtomic(transferFeeInput?.value || "0"); if (fee === null) { return "Transaction fee could not be calculated."; } if (fee < expectedFee) { return `Transaction fee must be at least ${atomicToDecimal(expectedFee)} ${activeBaseCoin()}.`; } const baseBalance = baseCoinBalanceAtomic(); if (isBaseTransferAsset(selected.asset)) { if (amount + fee > selected.balance) { return "Amount plus transaction fee exceeds the available base coin balance."; } } else if (fee > baseBalance) { return `This transfer needs ${atomicToDecimal(fee)} ${activeBaseCoin()} for the transaction fee.`; } return ""; } function transferPayload() { const selected = selectedTransferAsset(); const value = selected?.nftSeries > 0 ? 1n : decimalToAtomic(transferAmountInput?.value); const fee = decimalToAtomic(transferFeeInput?.value || "0"); return { receiver: String(transferReceiverInput?.value || "").trim(), coin: selected?.asset || "", nftSeries: Number(selected?.nftSeries || 0), value: String(value || 0n), txfee: String(fee || 0n) }; } function setTransferButtonsDisabled(disabled) { if (transferSaveButton) { transferSaveButton.disabled = disabled; } if (transferBroadcastButton) { transferBroadcastButton.disabled = disabled; } } function clearTransferForm() { if (transferReceiverInput) { transferReceiverInput.value = ""; } if (transferAmountInput) { transferAmountInput.value = ""; } if (transferAssetSelect) { transferAssetSelect.selectedIndex = -1; } updateTransferDerivedFields(); } async function handleTransferAction(action) { const error = validateTransferForm(); if (error) { setTransferMessage(error, "error"); return; } const invoke = window.__TAURI__?.core?.invoke; if (!invoke) { setTransferMessage("Wallet bridge is not available.", "error"); return; } const walletKey = await requestSigningWalletKey(); if (guiRuntimeSettings.require_key_before_signing !== false && !walletKey) { setTransferMessage("Signing cancelled."); return; } setTransferButtonsDisabled(true); try { if (action === "save") { const broadcastNode = activeBroadcastNode(); if (!broadcastNode) { throw new Error("Select a broadcast node before saving."); } setTransferMessage("Saving transfer..."); const review = await invoke("save_transfer_transaction", { ...transferPayload(), broadcastNode, walletKey }); clearTransferForm(); setTransferMessage(`Transfer saved to unbroadcast transactions: ${review.file_name}`, "success"); if (typeof refreshUnbroadcastTransactions === "function") { refreshUnbroadcastTransactions(); } } else { const broadcastNode = activeBroadcastNode(); if (!broadcastNode) { throw new Error("Select a broadcast node before broadcasting."); } setTransferMessage("Broadcasting transfer..."); const result = await invoke("broadcast_transfer_transaction", { ...transferPayload(), broadcastNode, walletKey }); clearTransferForm(); setTransferMessage(result.response || "Transfer broadcast.", "success"); } } catch (error) { setTransferMessage(String(error), "error"); } finally { setTransferButtonsDisabled(false); } } function prepareTransferForm() { setTransferMessage(""); refreshTransferBalancesIfNeeded().catch(() => { // The form already displays the RPC error; keep navigation responsive. }); } transferAssetSelect?.addEventListener("change", () => { updateTransferDerivedFields(); setTransferMessage(""); }); transferReceiverInput?.addEventListener("input", () => setTransferMessage("")); transferAmountInput?.addEventListener("input", () => { updateTransferFee(); setTransferMessage(""); }); transferSaveButton?.addEventListener("click", () => handleTransferAction("save")); transferBroadcastButton?.addEventListener("click", () => handleTransferAction("broadcast")); topbarPaymentUriInput?.addEventListener("input", () => { topbarPaymentUriInput.setCustomValidity(""); }); topbarPaymentUriForm?.addEventListener("submit", async (event) => { event.preventDefault(); topbarPaymentUriInput?.setCustomValidity(""); try { await loadTransferPaymentUri(topbarPaymentUriInput?.value); if (topbarPaymentUriInput) { topbarPaymentUriInput.value = ""; } } catch (error) { const message = error instanceof Error ? error.message : String(error); if (topbarPaymentUriInput) { topbarPaymentUriInput.setCustomValidity(message); topbarPaymentUriInput.reportValidity(); } if (currentView === "send") { setTransferMessage(message, "error"); } } });