632 lines
22 KiB
JavaScript
632 lines
22 KiB
JavaScript
|
|
function setUnbroadcastListMessage(message, type = "") {
|
||
|
|
if (!unbroadcastListMessage) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
unbroadcastListMessage.textContent = message;
|
||
|
|
unbroadcastListMessage.classList.toggle("error", type === "error");
|
||
|
|
unbroadcastListMessage.classList.toggle("success", type === "success");
|
||
|
|
unbroadcastListMessage.classList.toggle("hidden", !message);
|
||
|
|
}
|
||
|
|
|
||
|
|
function setUnbroadcastReviewMessage(message, type = "") {
|
||
|
|
if (!unbroadcastReviewMessage) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
unbroadcastReviewMessage.textContent = message;
|
||
|
|
unbroadcastReviewMessage.classList.toggle("error", type === "error");
|
||
|
|
unbroadcastReviewMessage.classList.toggle("success", type === "success");
|
||
|
|
unbroadcastReviewMessage.classList.toggle("hidden", !message);
|
||
|
|
}
|
||
|
|
|
||
|
|
function unbroadcastStatusLabel(item) {
|
||
|
|
switch (item?.status) {
|
||
|
|
case "ready":
|
||
|
|
return "Ready";
|
||
|
|
case "needs_active_signature":
|
||
|
|
return "Needs Your Signature";
|
||
|
|
case "needs_other_signature":
|
||
|
|
return "Needs Other Signature";
|
||
|
|
case "invalid_hash":
|
||
|
|
return "Invalid Hash";
|
||
|
|
case "invalid_signature":
|
||
|
|
return "Invalid Signature";
|
||
|
|
default:
|
||
|
|
return item?.status || "Unknown";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function truncateMiddle(value, left = 12, right = 12) {
|
||
|
|
const text = String(value || "");
|
||
|
|
if (text.length <= left + right + 3) {
|
||
|
|
return text;
|
||
|
|
}
|
||
|
|
return `${text.slice(0, left)}...${text.slice(-right)}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function clearUnbroadcastReview() {
|
||
|
|
activeUnbroadcastTransaction = null;
|
||
|
|
if (unbroadcastReviewGrid) {
|
||
|
|
unbroadcastReviewGrid.innerHTML = "";
|
||
|
|
}
|
||
|
|
if (unbroadcastJsonPreview) {
|
||
|
|
unbroadcastJsonPreview.textContent = "";
|
||
|
|
}
|
||
|
|
setUnbroadcastReviewMessage("");
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderUnbroadcastList(items = []) {
|
||
|
|
if (!transactionList) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
transactionList.innerHTML = "";
|
||
|
|
if (!items.length) {
|
||
|
|
setUnbroadcastListMessage("No unbroadcast transactions found.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
setUnbroadcastListMessage("");
|
||
|
|
items.forEach((item) => {
|
||
|
|
const button = document.createElement("button");
|
||
|
|
button.className = "local-transaction";
|
||
|
|
button.type = "button";
|
||
|
|
button.dataset.unbroadcastFile = item.file_name;
|
||
|
|
button.classList.toggle(
|
||
|
|
"active",
|
||
|
|
activeUnbroadcastTransaction?.file_name === item.file_name
|
||
|
|
);
|
||
|
|
|
||
|
|
const type = document.createElement("span");
|
||
|
|
type.className = "tx-kind";
|
||
|
|
type.textContent = item.type_name || "Transaction";
|
||
|
|
|
||
|
|
const summary = document.createElement("strong");
|
||
|
|
summary.textContent = item.summary || item.file_name;
|
||
|
|
|
||
|
|
const status = document.createElement("span");
|
||
|
|
status.textContent = item.status_message || unbroadcastStatusLabel(item);
|
||
|
|
|
||
|
|
const file = document.createElement("span");
|
||
|
|
file.className = "muted-file";
|
||
|
|
file.textContent = item.txid ? truncateMiddle(item.txid) : item.file_name;
|
||
|
|
|
||
|
|
button.append(type, summary, status, file);
|
||
|
|
button.addEventListener("click", () => loadUnbroadcastTransaction(item.file_name));
|
||
|
|
transactionList.appendChild(button);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
async function refreshUnbroadcastTransactions() {
|
||
|
|
if (unbroadcastRefreshInFlight) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const invoke = window.__TAURI__?.core?.invoke;
|
||
|
|
if (!invoke) {
|
||
|
|
setUnbroadcastListMessage("Tauri bridge is not available.", "error");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
unbroadcastRefreshInFlight = true;
|
||
|
|
setUnbroadcastListMessage("Loading unbroadcast transactions...");
|
||
|
|
try {
|
||
|
|
const items = await invoke("list_unbroadcast_transactions");
|
||
|
|
renderUnbroadcastList(Array.isArray(items) ? items : []);
|
||
|
|
} catch (error) {
|
||
|
|
transactionList.innerHTML = "";
|
||
|
|
setUnbroadcastListMessage(String(error), "error");
|
||
|
|
} finally {
|
||
|
|
unbroadcastRefreshInFlight = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function appendReviewField(label, value, wide = false) {
|
||
|
|
if (!unbroadcastReviewGrid) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const row = document.createElement("div");
|
||
|
|
row.classList.toggle("wide", wide);
|
||
|
|
|
||
|
|
const key = document.createElement("span");
|
||
|
|
key.textContent = label;
|
||
|
|
|
||
|
|
const val = document.createElement("b");
|
||
|
|
val.textContent = value === null || value === undefined || value === "" ? "--" : String(value);
|
||
|
|
|
||
|
|
row.append(key, val);
|
||
|
|
unbroadcastReviewGrid.appendChild(row);
|
||
|
|
}
|
||
|
|
|
||
|
|
function primitiveTransactionFields(transaction) {
|
||
|
|
return Object.entries(transaction || {}).filter(([, value]) => {
|
||
|
|
return value === null || ["string", "number", "boolean"].includes(typeof value);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function atomicReviewAmount(value) {
|
||
|
|
try {
|
||
|
|
return atomicToDecimal(BigInt(value || 0));
|
||
|
|
} catch (_) {
|
||
|
|
return String(value || "0");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function swapReviewAsset(transaction, side) {
|
||
|
|
const ticker = String(transaction?.[`ticker${side}`] || "").trim();
|
||
|
|
const series = Number(transaction?.[`nft_series${side}`] || 0);
|
||
|
|
return series > 0 ? `${ticker} #${series}` : ticker;
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderSwapReviewFields(review) {
|
||
|
|
const transaction = review.transaction || {};
|
||
|
|
const active = String(activeWalletAddressShort || activeWalletAddressHex || "").trim();
|
||
|
|
const sender1 = String(transaction.sender1 || "").trim();
|
||
|
|
const sender2 = String(transaction.sender2 || "").trim();
|
||
|
|
const activeIsSender1 = active === sender1;
|
||
|
|
const activeIsSender2 = active === sender2;
|
||
|
|
const expiration = Number(transaction.offer_expiration || 0);
|
||
|
|
|
||
|
|
if (activeIsSender1 || activeIsSender2) {
|
||
|
|
const sendSide = activeIsSender2 ? 2 : 1;
|
||
|
|
const receiveSide = activeIsSender2 ? 1 : 2;
|
||
|
|
const counterparty = activeIsSender2 ? sender1 : sender2;
|
||
|
|
appendReviewField("Your Role", activeIsSender1 ? "Offer Creator (Sender 1)" : "Counterparty (Sender 2)");
|
||
|
|
appendReviewField(
|
||
|
|
activeIsSender1 ? "You Offer" : "You Send",
|
||
|
|
`${atomicReviewAmount(transaction[`value${sendSide}`])} ${swapReviewAsset(transaction, sendSide)}`
|
||
|
|
);
|
||
|
|
appendReviewField(
|
||
|
|
activeIsSender1 ? "You Request" : "You Receive",
|
||
|
|
`${atomicReviewAmount(transaction[`value${receiveSide}`])} ${swapReviewAsset(transaction, receiveSide)}`
|
||
|
|
);
|
||
|
|
appendReviewField(
|
||
|
|
"Your Miner Tip",
|
||
|
|
`${atomicReviewAmount(transaction[`tip${sendSide}`])} ${swapReviewAsset(transaction, sendSide)}`
|
||
|
|
);
|
||
|
|
appendReviewField(
|
||
|
|
"Your Fee",
|
||
|
|
`${atomicReviewAmount(transaction[`txfee${sendSide}`])} ${activeBaseCoin()}`
|
||
|
|
);
|
||
|
|
appendReviewField("Counterparty", counterparty, true);
|
||
|
|
} else {
|
||
|
|
appendReviewField("Viewer Role", "Not a signer");
|
||
|
|
appendReviewField("Sender 1", sender1, true);
|
||
|
|
appendReviewField(
|
||
|
|
"Sender 1 Offers",
|
||
|
|
`${atomicReviewAmount(transaction.value1)} ${swapReviewAsset(transaction, 1)}`
|
||
|
|
);
|
||
|
|
appendReviewField("Sender 2", sender2, true);
|
||
|
|
appendReviewField(
|
||
|
|
"Sender 2 Offers",
|
||
|
|
`${atomicReviewAmount(transaction.value2)} ${swapReviewAsset(transaction, 2)}`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
appendReviewField(
|
||
|
|
"Sender 1 Signature",
|
||
|
|
transaction.signature1
|
||
|
|
? (activeIsSender1 ? "Your signature is present" : "Signature supplied")
|
||
|
|
: "Missing"
|
||
|
|
);
|
||
|
|
appendReviewField(
|
||
|
|
"Sender 2 Signature",
|
||
|
|
transaction.signature2
|
||
|
|
? (activeIsSender2 ? "Your signature is present" : "Signature supplied")
|
||
|
|
: (activeIsSender2 ? "Awaiting your signature" : "Awaiting sender2 signature")
|
||
|
|
);
|
||
|
|
appendReviewField(
|
||
|
|
"Expires",
|
||
|
|
expiration ? new Date(expiration * 1000).toLocaleString() : "--"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function loanReviewPeriod(period) {
|
||
|
|
return {
|
||
|
|
d: "Daily",
|
||
|
|
w: "Weekly",
|
||
|
|
m: "Monthly"
|
||
|
|
}[String(period || "").trim()] || "Unknown";
|
||
|
|
}
|
||
|
|
|
||
|
|
function loanReviewCollateral(transaction) {
|
||
|
|
return String(transaction?.collateral || "").trim();
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderLoanReviewFields(review) {
|
||
|
|
const transaction = review.transaction || {};
|
||
|
|
const active = String(activeWalletAddressShort || activeWalletAddressHex || "").trim();
|
||
|
|
const lender = String(transaction.lender || "").trim();
|
||
|
|
const borrower = String(transaction.borrower || "").trim();
|
||
|
|
const activeIsLender = active === lender;
|
||
|
|
const activeIsBorrower = active === borrower;
|
||
|
|
const startTimestamp = Number(transaction.timestamp || transaction.time || 0);
|
||
|
|
const paymentCount = Number(transaction.payment_number || 0);
|
||
|
|
const paymentAmount = BigInt(transaction.payment_amount || 0);
|
||
|
|
const scheduledTotal = paymentAmount * BigInt(paymentCount);
|
||
|
|
|
||
|
|
appendReviewField(
|
||
|
|
"Your Role",
|
||
|
|
activeIsLender ? "Offer Creator (Lender)" : activeIsBorrower ? "Borrower" : "Not a signer"
|
||
|
|
);
|
||
|
|
appendReviewField("Lender", lender, true);
|
||
|
|
appendReviewField("Borrower", borrower, true);
|
||
|
|
appendReviewField(
|
||
|
|
"Loan",
|
||
|
|
`${atomicReviewAmount(transaction.loan_amount)} ${String(transaction.loan_coin || "").trim()}`
|
||
|
|
);
|
||
|
|
appendReviewField(
|
||
|
|
"Collateral",
|
||
|
|
`${atomicReviewAmount(transaction.collateral_amount)} ${loanReviewCollateral(transaction)}`
|
||
|
|
);
|
||
|
|
appendReviewField(
|
||
|
|
"Start Date (UTC)",
|
||
|
|
startTimestamp ? new Date(startTimestamp * 1000).toLocaleString([], { timeZone: "UTC" }) : "--"
|
||
|
|
);
|
||
|
|
appendReviewField("Payment Period", loanReviewPeriod(transaction.payment_period));
|
||
|
|
appendReviewField("Number of Payments", paymentCount);
|
||
|
|
appendReviewField(
|
||
|
|
"Amount per Payment",
|
||
|
|
`${atomicReviewAmount(transaction.payment_amount)} ${String(transaction.loan_coin || "").trim()}`
|
||
|
|
);
|
||
|
|
appendReviewField(
|
||
|
|
"Total Scheduled Repayment",
|
||
|
|
`${atomicReviewAmount(scheduledTotal)} ${String(transaction.loan_coin || "").trim()}`
|
||
|
|
);
|
||
|
|
appendReviewField("Missed Payments Allowed", Number(transaction.grace_period || 0));
|
||
|
|
appendReviewField(
|
||
|
|
"Maximum Overdue Value",
|
||
|
|
`${atomicReviewAmount(transaction.max_late_value)} ${String(transaction.loan_coin || "").trim()}`
|
||
|
|
);
|
||
|
|
appendReviewField(
|
||
|
|
"Lender Fee",
|
||
|
|
`${atomicReviewAmount(transaction.txfee)} ${activeBaseCoin()}`
|
||
|
|
);
|
||
|
|
appendReviewField(
|
||
|
|
"Collateral Claim Rule",
|
||
|
|
"The missed-payment allowance and maximum overdue value must both be exceeded.",
|
||
|
|
true
|
||
|
|
);
|
||
|
|
appendReviewField(
|
||
|
|
"Lender Signature",
|
||
|
|
transaction.signature1
|
||
|
|
? (activeIsLender ? "Your signature is present" : "Signature supplied")
|
||
|
|
: (activeIsLender ? "Awaiting your signature" : "Missing")
|
||
|
|
);
|
||
|
|
appendReviewField(
|
||
|
|
"Borrower Signature",
|
||
|
|
transaction.signature2
|
||
|
|
? (activeIsBorrower ? "Your signature is present" : "Signature supplied")
|
||
|
|
: (activeIsBorrower ? "Awaiting your signature" : "Awaiting borrower signature")
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderUnbroadcastReview(review) {
|
||
|
|
activeUnbroadcastTransaction = review;
|
||
|
|
if (transactionTitle) {
|
||
|
|
transactionTitle.textContent = `${review.type_name || "Transaction"} Review`;
|
||
|
|
}
|
||
|
|
if (transactionCopy) {
|
||
|
|
transactionCopy.textContent = review.status_message || "Inspect this local transaction before broadcasting.";
|
||
|
|
}
|
||
|
|
if (transactionList) {
|
||
|
|
transactionList.classList.add("hidden");
|
||
|
|
}
|
||
|
|
formPanel?.classList.add("hidden");
|
||
|
|
reviewPanel?.classList.remove("hidden");
|
||
|
|
|
||
|
|
if (unbroadcastReviewGrid) {
|
||
|
|
unbroadcastReviewGrid.innerHTML = "";
|
||
|
|
}
|
||
|
|
appendReviewField("Type", review.type_name);
|
||
|
|
appendReviewField("Status", unbroadcastStatusLabel(review));
|
||
|
|
appendReviewField("File", review.file_name, true);
|
||
|
|
appendReviewField("Transaction Hash", review.txid || review.transaction?.hash || "--", true);
|
||
|
|
appendReviewField("Summary", review.summary, true);
|
||
|
|
|
||
|
|
if (Number(review.transaction_type) === 6) {
|
||
|
|
renderSwapReviewFields(review);
|
||
|
|
} else if (Number(review.transaction_type) === 7) {
|
||
|
|
renderLoanReviewFields(review);
|
||
|
|
} else {
|
||
|
|
primitiveTransactionFields(review.transaction)
|
||
|
|
.filter(([key]) => !["txtype", "hash"].includes(key))
|
||
|
|
.forEach(([key, value]) => {
|
||
|
|
const isSignature = key.toLowerCase().includes("signature");
|
||
|
|
appendReviewField(
|
||
|
|
key,
|
||
|
|
isSignature ? (value ? truncateMiddle(value, 18, 18) : "Missing") : value,
|
||
|
|
String(value || "").length > 28
|
||
|
|
);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (unbroadcastJsonPreview) {
|
||
|
|
unbroadcastJsonPreview.textContent = JSON.stringify(review.transaction || {}, null, 2);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (unbroadcastSignButton) {
|
||
|
|
unbroadcastSignButton.disabled = !review.can_sign;
|
||
|
|
unbroadcastSignButton.classList.toggle("hidden", !review.can_sign);
|
||
|
|
unbroadcastSignButton.textContent =
|
||
|
|
[6, 7].includes(Number(review.transaction_type))
|
||
|
|
? "Sign and Broadcast"
|
||
|
|
: "Sign Transaction";
|
||
|
|
}
|
||
|
|
if (unbroadcastBroadcastButton) {
|
||
|
|
unbroadcastBroadcastButton.disabled = !review.can_broadcast;
|
||
|
|
}
|
||
|
|
setUnbroadcastReviewMessage(review.status_message || "");
|
||
|
|
}
|
||
|
|
|
||
|
|
async function validateImportedSwapBalance(review) {
|
||
|
|
if (Number(review?.transaction_type) !== 6) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const transaction = review.transaction || {};
|
||
|
|
const active = String(activeWalletAddressShort || activeWalletAddressHex || "").trim();
|
||
|
|
if (active !== String(transaction.sender2 || "").trim()) {
|
||
|
|
throw new Error(`This swap requires sender2 wallet ${transaction.sender2}.`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const invoke = window.__TAURI__?.core?.invoke;
|
||
|
|
const broadcastNode = activeBroadcastNode();
|
||
|
|
const [balances, nfts] = await Promise.all([
|
||
|
|
invoke("get_active_wallet_balances", { broadcastNode }),
|
||
|
|
invoke("nft_list", { broadcastNode })
|
||
|
|
]);
|
||
|
|
activeWalletBalances = Array.isArray(balances) ? balances : [];
|
||
|
|
const nftKeys = new Set((Array.isArray(nfts) ? nfts : []).map((nft) => {
|
||
|
|
return `${String(nft.nft_name || "").trim().toLowerCase()}|${Number(nft.series || 0)}`;
|
||
|
|
}));
|
||
|
|
const asset = String(transaction.ticker2 || "").trim();
|
||
|
|
const series = Number(transaction.nft_series2 || 0);
|
||
|
|
const isNft = nftKeys.has(`${asset.toLowerCase()}|${series}`);
|
||
|
|
const balance = activeWalletBalances.find((row) => {
|
||
|
|
return (
|
||
|
|
String(row.asset || "").trim().toLowerCase() === asset.toLowerCase() &&
|
||
|
|
Number(row.nft_series || 0) === series
|
||
|
|
);
|
||
|
|
});
|
||
|
|
const available = balanceAtomicValue(balance);
|
||
|
|
const amount = BigInt(transaction.value2 || 0);
|
||
|
|
const tip = BigInt(transaction.tip2 || 0);
|
||
|
|
const fee = BigInt(transaction.txfee2 || 0);
|
||
|
|
if (isNft && tip !== 0n) {
|
||
|
|
throw new Error("NFT swap tips must be zero.");
|
||
|
|
}
|
||
|
|
if (!isNft && tip < (amount + 99n) / 100n) {
|
||
|
|
throw new Error("The counterparty miner tip must be at least 1% of the swap amount.");
|
||
|
|
}
|
||
|
|
if (amount + tip > available) {
|
||
|
|
throw new Error(`This wallet does not have enough ${swapReviewAsset(transaction, 2)} for the swap.`);
|
||
|
|
}
|
||
|
|
const baseBalance = baseCoinBalanceAtomic();
|
||
|
|
if (isBaseTransferAsset(asset)) {
|
||
|
|
if (amount + tip + fee > available) {
|
||
|
|
throw new Error(`This wallet cannot cover the requested amount, tip, and ${activeBaseCoin()} fee.`);
|
||
|
|
}
|
||
|
|
} else if (fee > baseBalance) {
|
||
|
|
throw new Error(`This wallet does not have enough ${activeBaseCoin()} for the swap fee.`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function validateImportedLoanBalance(review) {
|
||
|
|
if (Number(review?.transaction_type) !== 7) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
const transaction = review.transaction || {};
|
||
|
|
const active = String(activeWalletAddressShort || activeWalletAddressHex || "").trim();
|
||
|
|
const borrower = String(transaction.borrower || "").trim();
|
||
|
|
if (active !== borrower) {
|
||
|
|
throw new Error(`This loan requires borrower wallet ${borrower}.`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const invoke = window.__TAURI__?.core?.invoke;
|
||
|
|
const broadcastNode = activeBroadcastNode();
|
||
|
|
const balances = await invoke("get_active_wallet_balances", { broadcastNode });
|
||
|
|
activeWalletBalances = Array.isArray(balances) ? balances : [];
|
||
|
|
|
||
|
|
const collateral = loanReviewCollateral(transaction);
|
||
|
|
const split = collateral.match(/^(.*)_(\d{1,5})$/);
|
||
|
|
const asset = split ? split[1] : collateral;
|
||
|
|
const series = split ? Number(split[2]) : 0;
|
||
|
|
const balance = activeWalletBalances.find((row) => {
|
||
|
|
return (
|
||
|
|
String(row.asset || "").trim().toLowerCase() === asset.toLowerCase()
|
||
|
|
&& Number(row.nft_series || 0) === series
|
||
|
|
);
|
||
|
|
});
|
||
|
|
const available = balanceAtomicValue(balance);
|
||
|
|
const required = BigInt(transaction.collateral_amount || 0);
|
||
|
|
if (required > available) {
|
||
|
|
const label = series > 0 ? `${asset} #${series}` : asset;
|
||
|
|
throw new Error(
|
||
|
|
`This wallet requires ${atomicReviewAmount(required)} ${label} as collateral, but only ${atomicReviewAmount(available)} is available.`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function importUnbroadcastTransaction() {
|
||
|
|
const invoke = window.__TAURI__?.core?.invoke;
|
||
|
|
const broadcastNode = activeBroadcastNode();
|
||
|
|
if (!invoke || !broadcastNode) {
|
||
|
|
setUnbroadcastListMessage("Select a broadcast node before importing.", "error");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (!walletLoaded) {
|
||
|
|
setUnbroadcastListMessage("Load a wallet before importing a transaction.", "error");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
setUnbroadcastListMessage("Validating imported transaction...");
|
||
|
|
try {
|
||
|
|
const review = await invoke("import_unbroadcast_transaction", { broadcastNode });
|
||
|
|
if (!review) {
|
||
|
|
setUnbroadcastListMessage("");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
renderUnbroadcastReview(review);
|
||
|
|
refreshUnbroadcastTransactions();
|
||
|
|
} catch (error) {
|
||
|
|
setUnbroadcastListMessage(String(error), "error");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadUnbroadcastTransaction(fileName) {
|
||
|
|
if (unbroadcastReviewInFlight) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const invoke = window.__TAURI__?.core?.invoke;
|
||
|
|
if (!invoke) {
|
||
|
|
setUnbroadcastListMessage("Tauri bridge is not available.", "error");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
unbroadcastReviewInFlight = true;
|
||
|
|
setUnbroadcastListMessage("");
|
||
|
|
try {
|
||
|
|
const review = await invoke("load_unbroadcast_transaction", { fileName });
|
||
|
|
renderUnbroadcastReview(review);
|
||
|
|
refreshUnbroadcastTransactions();
|
||
|
|
} catch (error) {
|
||
|
|
setUnbroadcastListMessage(String(error), "error");
|
||
|
|
} finally {
|
||
|
|
unbroadcastReviewInFlight = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function showUnbroadcastList() {
|
||
|
|
if (transactionTitle) {
|
||
|
|
transactionTitle.textContent = "Unbroadcast Transactions";
|
||
|
|
}
|
||
|
|
if (transactionCopy) {
|
||
|
|
transactionCopy.textContent = "Transactions saved locally and waiting for review.";
|
||
|
|
}
|
||
|
|
transactionList?.classList.remove("hidden");
|
||
|
|
formPanel?.classList.add("hidden");
|
||
|
|
reviewPanel?.classList.add("hidden");
|
||
|
|
clearUnbroadcastReview();
|
||
|
|
refreshUnbroadcastTransactions();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function signActiveUnbroadcastTransaction() {
|
||
|
|
if (!activeUnbroadcastTransaction?.file_name) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const invoke = window.__TAURI__?.core?.invoke;
|
||
|
|
const broadcastNode = activeBroadcastNode();
|
||
|
|
if (!invoke || !broadcastNode) {
|
||
|
|
setUnbroadcastReviewMessage("Select a broadcast node before signing.", "error");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
await validateImportedSwapBalance(activeUnbroadcastTransaction);
|
||
|
|
await validateImportedLoanBalance(activeUnbroadcastTransaction);
|
||
|
|
} catch (error) {
|
||
|
|
setUnbroadcastReviewMessage(String(error), "error");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const walletKey = await requestSigningWalletKey();
|
||
|
|
if (guiRuntimeSettings.require_key_before_signing !== false && !walletKey) {
|
||
|
|
setUnbroadcastReviewMessage("Signing cancelled.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
unbroadcastSignButton.disabled = true;
|
||
|
|
const transactionType = Number(activeUnbroadcastTransaction.transaction_type);
|
||
|
|
const signAndBroadcast = [6, 7].includes(transactionType);
|
||
|
|
const signingType = transactionType === 7 ? "loan" : transactionType === 6 ? "swap" : "transaction";
|
||
|
|
setUnbroadcastReviewMessage(`Signing ${signingType}...`);
|
||
|
|
try {
|
||
|
|
const review = await invoke("sign_unbroadcast_transaction", {
|
||
|
|
walletKey,
|
||
|
|
broadcastNode,
|
||
|
|
fileName: activeUnbroadcastTransaction.file_name
|
||
|
|
});
|
||
|
|
renderUnbroadcastReview(review);
|
||
|
|
refreshUnbroadcastTransactions();
|
||
|
|
if (signAndBroadcast) {
|
||
|
|
const typeName = transactionType === 7 ? "Loan" : "Swap";
|
||
|
|
setUnbroadcastReviewMessage(`${typeName} signed. Broadcasting...`);
|
||
|
|
await broadcastActiveUnbroadcastTransaction();
|
||
|
|
} else {
|
||
|
|
setUnbroadcastReviewMessage("Transaction signed.", "success");
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
setUnbroadcastReviewMessage(String(error), "error");
|
||
|
|
} finally {
|
||
|
|
unbroadcastSignButton.disabled = !activeUnbroadcastTransaction?.can_sign;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function broadcastActiveUnbroadcastTransaction() {
|
||
|
|
if (!activeUnbroadcastTransaction?.file_name) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const invoke = window.__TAURI__?.core?.invoke;
|
||
|
|
const broadcastNode = activeBroadcastNode();
|
||
|
|
if (!invoke || !broadcastNode) {
|
||
|
|
setUnbroadcastReviewMessage("No broadcast node is selected.", "error");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
unbroadcastBroadcastButton.disabled = true;
|
||
|
|
setUnbroadcastReviewMessage("Broadcasting transaction...");
|
||
|
|
try {
|
||
|
|
const broadcastedTransaction = activeUnbroadcastTransaction?.transaction;
|
||
|
|
const result = await invoke("broadcast_unbroadcast_transaction", {
|
||
|
|
broadcastNode,
|
||
|
|
fileName: activeUnbroadcastTransaction.file_name
|
||
|
|
});
|
||
|
|
if (typeof applyActiveVanityDisplayFromTransaction === "function") {
|
||
|
|
applyActiveVanityDisplayFromTransaction(broadcastedTransaction);
|
||
|
|
}
|
||
|
|
const successMessage = result?.response || "Broadcast completed.";
|
||
|
|
transactionTitle.textContent = "Unbroadcast Transactions";
|
||
|
|
transactionCopy.textContent = "Transactions saved locally and waiting for review.";
|
||
|
|
transactionList?.classList.remove("hidden");
|
||
|
|
formPanel?.classList.add("hidden");
|
||
|
|
reviewPanel?.classList.add("hidden");
|
||
|
|
clearUnbroadcastReview();
|
||
|
|
await refreshUnbroadcastTransactions();
|
||
|
|
setUnbroadcastListMessage(successMessage, "success");
|
||
|
|
} catch (error) {
|
||
|
|
setUnbroadcastReviewMessage(String(error), "error");
|
||
|
|
} finally {
|
||
|
|
unbroadcastBroadcastButton.disabled = !activeUnbroadcastTransaction?.can_broadcast;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function deleteActiveUnbroadcastTransaction() {
|
||
|
|
if (!activeUnbroadcastTransaction?.file_name) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const invoke = window.__TAURI__?.core?.invoke;
|
||
|
|
if (!invoke) {
|
||
|
|
setUnbroadcastReviewMessage("Tauri bridge is not available.", "error");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const fileName = activeUnbroadcastTransaction.file_name;
|
||
|
|
setUnbroadcastReviewMessage("Deleting local copy...");
|
||
|
|
try {
|
||
|
|
await invoke("delete_unbroadcast_transaction", { fileName });
|
||
|
|
showUnbroadcastList();
|
||
|
|
} catch (error) {
|
||
|
|
setUnbroadcastReviewMessage(String(error), "error");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
unbroadcastBackButton?.addEventListener("click", showUnbroadcastList);
|
||
|
|
unbroadcastSignButton?.addEventListener("click", signActiveUnbroadcastTransaction);
|
||
|
|
unbroadcastBroadcastButton?.addEventListener("click", broadcastActiveUnbroadcastTransaction);
|
||
|
|
unbroadcastDeleteButton?.addEventListener("click", deleteActiveUnbroadcastTransaction);
|
||
|
|
document
|
||
|
|
.querySelector("[data-import-transaction]")
|
||
|
|
?.addEventListener("click", importUnbroadcastTransaction);
|