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

1528 lines
54 KiB
JavaScript

function decodedTransactionType(transaction) {
if (!transaction || typeof transaction !== "object" || Array.isArray(transaction)) {
return 0;
}
return Number(
transaction.unsigned?.txtype ??
transaction.unsigned_transfer?.txtype ??
transaction.unsigned_create_token?.txtype ??
transaction.unsigned_create_nft?.txtype ??
transaction.unsigned_marketing?.txtype ??
transaction.unsigned_swap?.txtype ??
transaction.unsigned_loan_contract?.txtype ??
transaction.unsigned_contract_payment?.txtype ??
transaction.unsigned_collateral_claim?.txtype ??
transaction.unsigned_burn?.txtype ??
transaction.unsigned_issue_token?.txtype ??
transaction.unsigned_vanity_address?.txtype ??
0
);
}
function txTypeKey(type) {
return {
0: "genesis",
1: "reward",
2: "transfer",
3: "token_create",
4: "nft_create",
5: "marketing",
6: "swap",
7: "loan_create",
8: "loan_payment",
9: "collateral_claim",
10: "burn",
11: "token_issue",
12: "vanity"
}[Number(type)] || "transaction";
}
function minerFeeDisplay(minerEarnings) {
const fee = (Array.isArray(minerEarnings) ? minerEarnings : [])
.filter((earning) => Number(earning?.earning_type) === 0)
.reduce((total, earning) => total + BigInt(earning?.amount || 0), 0n);
return signedBaseFee("+", fee);
}
function minerTipRows(base, minerEarnings) {
return (Array.isArray(minerEarnings) ? minerEarnings : [])
.map((earning) => {
const amount = earning?.amount ?? 0;
const asset = assetWithSeries(earning?.asset || activeBaseCoin(), Number(earning?.nft_series || 0));
if (Number(earning?.earning_type) === 1) {
return dashboardRow(base, "Miner Tip", asset, "Network", signedAmount("+", amount), "--");
}
return null;
})
.filter((row) => row && row.amount !== "+0");
}
function loanSummaryFromLinkedTransactions(linkedTransactions = {}, contractHash = "") {
const direct = linkedTransactions?.[contractHash];
const directLoan = parseLoanContractSummary(direct?.transaction || direct);
if (directLoan) {
return directLoan;
}
for (const linked of Object.values(linkedTransactions || {})) {
const loan = parseLoanContractSummary(linked?.transaction || linked);
if (loan) {
return loan;
}
}
return null;
}
function parseDecodedDashboardTransactionRows(
txid,
blockHeight,
transaction,
linkedTransactions = {},
minerEarnings = []
) {
const type = decodedTransactionType(transaction);
const base = {
txid,
block_height: blockHeight || null,
status: walletStatusForBlock(blockHeight)
};
const fallback = [dashboardRow(base, txTypeName(type), activeBaseCoin(), shortenAddress(txid), "")];
try {
if (type === 1) {
const value = transaction.unsigned?.value || 0;
return [
dashboardRow(
base,
"Mining Reward",
activeBaseCoin(),
"Network",
signedAmount("+", value),
minerFeeDisplay(minerEarnings)
),
...minerTipRows(base, minerEarnings)
];
}
if (type === 2) {
const tx = transaction.unsigned_transfer;
const asset = assetWithSeries(tx.coin, tx.nft_series);
if (isActiveWalletAddress(tx.sender)) {
return [dashboardRow(base, "Transfer Out", asset, addressLabel(tx.receiver), signedAmount("-", tx.value), signedBaseFee("-", tx.txfee || 0))];
}
if (isActiveWalletAddress(tx.receiver)) {
return [dashboardRow(base, "Transfer In", asset, addressLabel(tx.sender), signedAmount("+", tx.value))];
}
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(tx.sender), signedAmount("+", tx.txfee || 0))];
}
if (type === 3) {
const tx = transaction.unsigned_create_token;
if (isActiveWalletAddress(tx.creator)) {
return [dashboardRow(base, "Token Created", tx.ticker || "Token", "Self", signedAmount("+", tx.number), signedBaseFee("-", tx.txfee || 0))];
}
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(tx.creator), signedAmount("+", tx.txfee || 0))];
}
if (type === 4) {
const tx = transaction.unsigned_create_nft;
if (isActiveWalletAddress(tx.creator)) {
return [dashboardRow(base, "NFT Created", assetWithSeries(tx.nft_name || "NFT", tx.series), "Self", `+${tx.count || 0}`, signedBaseFee("-", tx.txfee || 0))];
}
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(tx.creator), signedAmount("+", tx.txfee || 0))];
}
if (type === 5) {
const tx = transaction.unsigned_marketing;
if (isActiveWalletAddress(tx.advertiser)) {
return [dashboardRow(base, "Marketing", "Advertisement", "Network", "", signedBaseFee("-", tx.txfee || 0))];
}
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(tx.advertiser), signedAmount("+", tx.txfee || 0))];
}
if (type === 6) {
const tx = transaction.unsigned_swap;
const asset1 = assetWithSeries(tx.ticker1, tx.nft_series1);
const asset2 = assetWithSeries(tx.ticker2, tx.nft_series2);
if (isActiveWalletAddress(tx.sender1)) {
return [
dashboardRow(base, "Swap Out", asset1, addressLabel(tx.sender2), signedAmount("-", tx.value1), signedBaseFee("-", tx.txfee1 || 0)),
dashboardRow(base, "Miner Tip Paid", asset1, addressLabel(tx.sender2), signedAmount("-", tx.tip1 || 0), "--"),
dashboardRow(base, "Swap In", asset2, addressLabel(tx.sender2), signedAmount("+", tx.value2))
].filter((row) => row.amount !== "0" && row.amount !== "-0");
}
if (isActiveWalletAddress(tx.sender2)) {
return [
dashboardRow(base, "Swap Out", asset2, addressLabel(tx.sender1), signedAmount("-", tx.value2), signedBaseFee("-", tx.txfee2 || 0)),
dashboardRow(base, "Miner Tip Paid", asset2, addressLabel(tx.sender1), signedAmount("-", tx.tip2 || 0), "--"),
dashboardRow(base, "Swap In", asset1, addressLabel(tx.sender1), signedAmount("+", tx.value1))
].filter((row) => row.amount !== "0" && row.amount !== "-0");
}
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), "Swap", signedAmount("+", (tx.txfee1 || 0) + (tx.txfee2 || 0)))];
}
if (type === 7) {
const tx = transaction.unsigned_loan_contract;
if (isActiveWalletAddress(tx.lender)) {
return [dashboardRow(base, "Loan Created", tx.loan_coin, addressLabel(tx.borrower), signedAmount("-", tx.loan_amount), signedBaseFee("-", tx.txfee || 0))];
}
if (isActiveWalletAddress(tx.borrower)) {
return [
dashboardRow(base, "Loan Received", tx.loan_coin, addressLabel(tx.lender), signedAmount("+", tx.loan_amount)),
dashboardRow(base, "Collateral Locked", tx.collateral, addressLabel(tx.lender), signedAmount("-", tx.collateral_amount))
];
}
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(tx.lender), signedAmount("+", tx.txfee || 0))];
}
if (type === 8) {
const tx = transaction.unsigned_contract_payment;
const loan = loanSummaryFromLinkedTransactions(linkedTransactions, tx.contract_hash);
if (isActiveWalletAddress(tx.address)) {
const asset = loan?.loanCoin || "Loan";
const counterparty = loan?.lender ? addressLabel(loan.lender) : shortenAddress(tx.contract_hash);
return [
dashboardRow(base, "Loan Payment Sent", asset, counterparty, signedAmount("-", tx.payback_amount), signedBaseFee("-", tx.txfee || 0)),
dashboardRow(base, "Miner Tip Paid", asset, counterparty, signedAmount("-", tx.tip || 0), "--")
].filter((row) => row.amount !== "0" && row.amount !== "-0");
}
if (loan?.lender && isActiveWalletAddress(loan.lender)) {
return [dashboardRow(base, "Loan Payment Received", loan.loanCoin, addressLabel(tx.address), signedAmount("+", tx.payback_amount))];
}
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(tx.address), signedAmount("+", tx.txfee || 0))];
}
if (type === 9) {
const tx = transaction.unsigned_collateral_claim;
if (isActiveWalletAddress(tx.address)) {
return [dashboardRow(base, "Collateral Claimed", "Collateral", shortenAddress(tx.contract_hash), "", signedBaseFee("-", tx.txfee || 0))];
}
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(tx.address), signedAmount("+", tx.txfee || 0))];
}
if (type === 10) {
const tx = transaction.unsigned_burn;
if (isActiveWalletAddress(tx.address)) {
return [dashboardRow(base, "Burn", assetWithSeries(tx.coin, tx.nft_series), "Network", signedAmount("-", tx.value), signedBaseFee("-", tx.txfee || 0))];
}
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(tx.address), signedAmount("+", tx.txfee || 0))];
}
if (type === 11) {
const tx = transaction.unsigned_issue_token;
if (isActiveWalletAddress(tx.creator)) {
return [dashboardRow(base, "Token Issued", tx.ticker || "Token", "Self", signedAmount("+", tx.number), signedBaseFee("-", tx.txfee || 0))];
}
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(tx.creator), signedAmount("+", tx.txfee || 0))];
}
if (type === 12) {
const tx = transaction.unsigned_vanity_address;
if (isActiveWalletAddress(tx.address)) {
return [dashboardRow(base, "Vanity Registered", "Vanity", "Self", "", signedBaseFee("-", tx.txfee || tx.fee || 0))];
}
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(tx.address), signedAmount("+", tx.txfee || tx.fee || 0))];
}
} catch (_) {
return fallback;
}
return fallback;
}
function parseDashboardTransactionRows(
txid,
blockHeight,
transaction,
linkedTransactions = {},
minerEarnings = []
) {
if (transaction && typeof transaction === "object" && !Array.isArray(transaction)) {
return parseDecodedDashboardTransactionRows(
txid,
blockHeight,
transaction,
linkedTransactions,
minerEarnings
);
}
const bytes = Array.from(transaction || []);
const type = Number(bytes[0] || 0);
const base = {
txid,
block_height: blockHeight,
status: statusForBlock(blockHeight)
};
const fallback = [dashboardRow(base, txTypeName(type), activeBaseCoin(), shortenAddress(txid), "")];
try {
if (type === 1) {
const value = readU64LE(bytes, 5);
return [
dashboardRow(
base,
"Mining Reward",
activeBaseCoin(),
"Network",
signedAmount("+", value),
minerFeeDisplay(minerEarnings)
),
...minerTipRows(base, minerEarnings)
];
}
if (type === 2) {
const value = readU64LE(bytes, 5);
const asset = assetWithSeries(bytesToText(bytes.slice(13, 28)), readU32LE(bytes, 28));
const sender = bytesToAddressHex(bytes.slice(32, 54));
const receiver = bytesToAddressHex(bytes.slice(54, 76));
const fee = readU64LE(bytes, 76);
if (isActiveAddress(sender)) {
return [dashboardRow(base, "Transfer Out", asset, addressLabel(receiver), signedAmount("-", value), signedBaseFee("-", fee))];
}
if (isActiveAddress(receiver)) {
return [dashboardRow(base, "Transfer In", asset, addressLabel(sender), signedAmount("+", value))];
}
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(sender), signedAmount("+", fee))];
}
if (type === 3) {
const creator = bytesToAddressHex(bytes.slice(5, 27));
const ticker = bytesToText(bytes.slice(27, 42));
const number = readU64LE(bytes, 42);
const fee = readU64LE(bytes, 51);
if (isActiveAddress(creator)) {
return [dashboardRow(base, "Token Created", ticker || "Token", "Self", signedAmount("+", number), signedBaseFee("-", fee))];
}
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(creator), signedAmount("+", fee))];
}
if (type === 4) {
const creator = bytesToAddressHex(bytes.slice(5, 27));
const series = Number(bytes[27] || 0);
const nftName = bytesToText(bytes.slice(28, 43)) || "NFT";
const count = readU32LE(bytes, 143);
const fee = readU64LE(bytes, 247);
if (isActiveAddress(creator)) {
return [dashboardRow(base, "NFT Created", assetWithSeries(nftName, series), "Self", `+${count}`, signedBaseFee("-", fee))];
}
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(creator), signedAmount("+", fee))];
}
if (type === 5) {
const advertiser = bytesToAddressHex(bytes.slice(165, 187));
const fee = readU64LE(bytes, 187);
if (isActiveAddress(advertiser)) {
return [dashboardRow(base, "Marketing", "Advertisement", "Network", "", signedBaseFee("-", fee))];
}
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(advertiser), signedAmount("+", fee))];
}
if (type === 6) {
const ticker1 = bytesToText(bytes.slice(9, 24));
const nftSeries1 = readU32LE(bytes, 24);
const value1 = readU64LE(bytes, 28);
const ticker2 = bytesToText(bytes.slice(36, 51));
const nftSeries2 = readU32LE(bytes, 51);
const value2 = readU64LE(bytes, 55);
const sender1 = bytesToAddressHex(bytes.slice(63, 85));
const sender2 = bytesToAddressHex(bytes.slice(85, 107));
const tip1 = readU64LE(bytes, 107);
const tip2 = readU64LE(bytes, 115);
const txfee1 = readU64LE(bytes, 123);
const txfee2 = readU64LE(bytes, 131);
const asset1 = assetWithSeries(ticker1, nftSeries1);
const asset2 = assetWithSeries(ticker2, nftSeries2);
if (isActiveAddress(sender1)) {
return [
dashboardRow(base, "Swap Out", asset1, addressLabel(sender2), signedAmount("-", value1), signedBaseFee("-", txfee1)),
dashboardRow(base, "Miner Tip Paid", asset1, addressLabel(sender2), signedAmount("-", tip1), "--"),
dashboardRow(base, "Swap In", asset2, addressLabel(sender2), signedAmount("+", value2))
].filter((row) => row.amount !== "0" && row.amount !== "-0");
}
if (isActiveAddress(sender2)) {
return [
dashboardRow(base, "Swap Out", asset2, addressLabel(sender1), signedAmount("-", value2), signedBaseFee("-", txfee2)),
dashboardRow(base, "Miner Tip Paid", asset2, addressLabel(sender1), signedAmount("-", tip2), "--"),
dashboardRow(base, "Swap In", asset1, addressLabel(sender1), signedAmount("+", value1))
].filter((row) => row.amount !== "0" && row.amount !== "-0");
}
return [
dashboardRow(base, "Fee Earned", activeBaseCoin(), "Swap", signedAmount("+", txfee1 + txfee2)),
dashboardRow(base, "Tip Earned", asset1, "Swap", signedAmount("+", tip1)),
dashboardRow(base, "Tip Earned", asset2, "Swap", signedAmount("+", tip2))
].filter((row) => row.amount !== "+0");
}
if (type === 7) {
const loanCoin = bytesToText(bytes.slice(5, 20));
const loanAmount = readU64LE(bytes, 20);
const lender = bytesToAddressHex(bytes.slice(28, 50));
const collateral = bytesToText(bytes.slice(50, 71));
const collateralAmount = readU64LE(bytes, 71);
const borrower = bytesToAddressHex(bytes.slice(79, 101));
const fee = readU64LE(bytes, 120);
if (isActiveAddress(lender)) {
return [dashboardRow(base, "Loan Created", loanCoin, addressLabel(borrower), signedAmount("-", loanAmount), signedBaseFee("-", fee))];
}
if (isActiveAddress(borrower)) {
return [
dashboardRow(base, "Loan Received", loanCoin, addressLabel(lender), signedAmount("+", loanAmount)),
dashboardRow(base, "Collateral Locked", collateral, addressLabel(lender), signedAmount("-", collateralAmount))
];
}
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(lender), signedAmount("+", fee))];
}
if (type === 8) {
const paybackAmount = readU64LE(bytes, 5);
const contractHash = bytesToHex(bytes.slice(13, 45));
const borrower = bytesToAddressHex(bytes.slice(45, 67));
const tip = readU64LE(bytes, 67);
const fee = readU64LE(bytes, 75);
const loan = loanSummaryFromLinkedTransactions(linkedTransactions, contractHash);
if (isActiveAddress(borrower)) {
const asset = loan?.loanCoin || "Loan";
const counterparty = loan?.lender ? addressLabel(loan.lender) : `${contractHash.slice(0, 8)}...${contractHash.slice(-8)}`;
return [
dashboardRow(base, "Loan Payment Sent", asset, counterparty, signedAmount("-", paybackAmount), signedBaseFee("-", fee)),
dashboardRow(base, "Miner Tip Paid", asset, counterparty, signedAmount("-", tip), "--")
].filter((row) => row.amount !== "0" && row.amount !== "-0");
}
if (loan?.lender && isActiveAddress(loan.lender)) {
return [dashboardRow(base, "Loan Payment Received", loan.loanCoin, addressLabel(borrower), signedAmount("+", paybackAmount))];
}
return [
dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(borrower), signedAmount("+", fee)),
dashboardRow(base, "Tip Earned", loan?.loanCoin || "Loan", addressLabel(borrower), signedAmount("+", tip))
].filter((row) => row.amount !== "0" && row.amount !== "+0");
}
if (type === 9) {
const contractHash = bytesToHex(bytes.slice(5, 37));
const claimant = bytesToAddressHex(bytes.slice(37, 59));
const fee = readU64LE(bytes, 59);
if (isActiveAddress(claimant)) {
return [dashboardRow(base, "Collateral Claimed", "Collateral", `${contractHash.slice(0, 8)}...${contractHash.slice(-8)}`, "", signedBaseFee("-", fee))];
}
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(claimant), signedAmount("+", fee))];
}
if (type === 10) {
const address = bytesToAddressHex(bytes.slice(5, 27));
const asset = assetWithSeries(bytesToText(bytes.slice(27, 42)), readU32LE(bytes, 42));
const value = readU64LE(bytes, 46);
const fee = readU64LE(bytes, 54);
if (isActiveAddress(address)) {
return [dashboardRow(base, "Burn", asset, "Network", signedAmount("-", value), signedBaseFee("-", fee))];
}
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(address), signedAmount("+", fee))];
}
if (type === 11) {
const creator = bytesToAddressHex(bytes.slice(5, 27));
const ticker = bytesToText(bytes.slice(27, 42));
const number = readU64LE(bytes, 42);
const fee = readU64LE(bytes, 50);
if (isActiveAddress(creator)) {
return [dashboardRow(base, "Token Issued", ticker || "Token", "Self", signedAmount("+", number), signedBaseFee("-", fee))];
}
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(creator), signedAmount("+", fee))];
}
if (type === 12) {
const address = bytesToAddressHex(bytes.slice(5, 27));
const fee = readU64LE(bytes, 49);
if (isActiveAddress(address)) {
return [dashboardRow(base, "Vanity Registered", "Vanity", "Self", "", signedBaseFee("-", fee))];
}
return [dashboardRow(base, "Fee Earned", activeBaseCoin(), addressLabel(address), signedAmount("+", fee))];
}
} catch (_) {
return fallback;
}
return fallback;
}
function decodeLightweightTransaction(bytesLike) {
const bytes = Array.from(bytesLike || []);
const type = Number(bytes[0] || 0);
const time = () => readU32LE(bytes, 1);
if (type === 1) {
return { unsigned: { txtype: type, timestamp: time(), value: Number(readU64LE(bytes, 5)) } };
}
if (type === 2) {
return {
unsigned_transfer: {
txtype: type,
time: time(),
value: Number(readU64LE(bytes, 5)),
coin: bytesToText(bytes.slice(13, 28)),
nft_series: readU32LE(bytes, 28),
sender: bytesToAddressHex(bytes.slice(32, 54)),
receiver: bytesToAddressHex(bytes.slice(54, 76)),
txfee: Number(readU64LE(bytes, 76))
}
};
}
if (type === 3) {
return {
unsigned_create_token: {
txtype: type,
time: time(),
creator: bytesToAddressHex(bytes.slice(5, 27)),
ticker: bytesToText(bytes.slice(27, 42)),
number: Number(readU64LE(bytes, 42)),
hard_limit: Number(bytes[50] || 0),
txfee: Number(readU64LE(bytes, 51))
}
};
}
if (type === 4) {
return {
unsigned_create_nft: {
txtype: type,
time: time(),
creator: bytesToAddressHex(bytes.slice(5, 27)),
series: Number(bytes[27] || 0),
nft_name: bytesToText(bytes.slice(28, 43)),
item_ipfs: bytesToText(bytes.slice(43, 143)),
count: readU32LE(bytes, 143),
description: bytesToText(bytes.slice(147, 247)),
txfee: Number(readU64LE(bytes, 247))
}
};
}
if (type === 5) {
return {
unsigned_marketing: {
txtype: type,
time: time(),
ad_data: bytesToText(bytes.slice(5, 165)),
advertiser: bytesToAddressHex(bytes.slice(165, 187)),
txfee: Number(readU64LE(bytes, 187))
}
};
}
if (type === 6) {
return {
unsigned_swap: {
txtype: type,
time: time(),
ticker1: bytesToText(bytes.slice(9, 24)),
nft_series1: readU32LE(bytes, 24),
value1: Number(readU64LE(bytes, 28)),
ticker2: bytesToText(bytes.slice(36, 51)),
nft_series2: readU32LE(bytes, 51),
value2: Number(readU64LE(bytes, 55)),
sender1: bytesToAddressHex(bytes.slice(63, 85)),
sender2: bytesToAddressHex(bytes.slice(85, 107)),
tip1: Number(readU64LE(bytes, 107)),
tip2: Number(readU64LE(bytes, 115)),
txfee1: Number(readU64LE(bytes, 123)),
txfee2: Number(readU64LE(bytes, 131))
}
};
}
if (type === 7) {
return {
unsigned_loan_contract: {
txtype: type,
time: time(),
loan_coin: bytesToText(bytes.slice(5, 20)),
loan_amount: Number(readU64LE(bytes, 20)),
lender: bytesToAddressHex(bytes.slice(28, 50)),
collateral: bytesToText(bytes.slice(50, 71)),
collateral_amount: Number(readU64LE(bytes, 71)),
borrower: bytesToAddressHex(bytes.slice(79, 101)),
payment_period: bytesToText(bytes.slice(101, 102)),
payment_number: Number(bytes[102] || 0),
payment_amount: Number(readU64LE(bytes, 103)),
grace_period: Number(bytes[111] || 0),
max_late_value: Number(readU64LE(bytes, 112)),
txfee: Number(readU64LE(bytes, 120))
}
};
}
if (type === 8) {
return {
unsigned_contract_payment: {
txtype: type,
time: time(),
payback_amount: Number(readU64LE(bytes, 5)),
contract_hash: bytesToHex(bytes.slice(13, 45)),
address: bytesToAddressHex(bytes.slice(45, 67)),
tip: Number(readU64LE(bytes, 67)),
txfee: Number(readU64LE(bytes, 75))
}
};
}
if (type === 9) {
return {
unsigned_collateral_claim: {
txtype: type,
time: time(),
contract_hash: bytesToHex(bytes.slice(5, 37)),
address: bytesToAddressHex(bytes.slice(37, 59)),
txfee: Number(readU64LE(bytes, 59))
}
};
}
if (type === 10) {
return {
unsigned_burn: {
txtype: type,
time: time(),
address: bytesToAddressHex(bytes.slice(5, 27)),
coin: bytesToText(bytes.slice(27, 42)),
nft_series: readU32LE(bytes, 42),
value: Number(readU64LE(bytes, 46)),
txfee: Number(readU64LE(bytes, 54))
}
};
}
if (type === 11) {
return {
unsigned_issue_token: {
txtype: type,
time: time(),
creator: bytesToAddressHex(bytes.slice(5, 27)),
ticker: bytesToText(bytes.slice(27, 42)),
number: Number(readU64LE(bytes, 42)),
txfee: Number(readU64LE(bytes, 50))
}
};
}
if (type === 12) {
return {
unsigned_vanity_address: {
txtype: type,
time: time(),
address: bytesToAddressHex(bytes.slice(5, 27)),
vanity_address: bytesToAddressHex(bytes.slice(27, 49)),
fee: Number(readU64LE(bytes, 49)),
txfee: Number(readU64LE(bytes, 49))
}
};
}
return { unknown: { txtype: type } };
}
function normalizeTransactionCache(cache) {
if (!cache || typeof cache !== "object" || Array.isArray(cache)) {
return { transactions: {}, ordered_txids: [] };
}
if (cache.transactions && typeof cache.transactions === "object" && !Array.isArray(cache.transactions)) {
return {
...cache,
transactions: { ...cache.transactions },
ordered_txids: normalizeOrderedTxids(cache)
};
}
const transactions = {};
Object.entries(cache).forEach(([txid, transaction]) => {
if (transaction && typeof transaction === "object") {
transactions[txid] = { txid, ...transaction };
}
});
return {
transactions,
ordered_txids: []
};
}
function normalizeOrderedTxids(cache) {
const txids = Array.isArray(cache?.ordered_txids)
? cache.ordered_txids
: (Array.isArray(cache?.address_history_txids) ? cache.address_history_txids : []);
const seen = new Set();
return txids.filter((txid) => {
if (!txid || seen.has(txid)) {
return false;
}
seen.add(txid);
return true;
});
}
function addOrderedTxids(cache, txids) {
cache.ordered_txids = normalizeOrderedTxids(cache);
const known = new Set(cache.ordered_txids || []);
txids.forEach((txid) => {
if (txid && !known.has(txid)) {
known.add(txid);
cache.ordered_txids.push(txid);
}
});
}
function replaceOrderedTxids(cache, txids) {
cache.ordered_txids = normalizeOrderedTxids({ ordered_txids: txids });
}
function normalizeTransactionIndex(index) {
const source = index && typeof index === "object" && !Array.isArray(index) ? index : {};
const orderedTxids = Array.isArray(source.ordered_txids) ? source.ordered_txids.filter(Boolean) : [];
const txidToFile = source.txid_to_file && typeof source.txid_to_file === "object" ? { ...source.txid_to_file } : {};
const txidToType = source.txid_to_type && typeof source.txid_to_type === "object" ? { ...source.txid_to_type } : {};
const typeToTxids = source.type_to_txids && typeof source.type_to_txids === "object" ? { ...source.type_to_txids } : {};
const nextId = Math.max(1, Number(source.next_id || 1));
return {
next_id: nextId,
ordered_txids: orderedTxids,
txid_to_file: txidToFile,
txid_to_type: txidToType,
type_to_txids: typeToTxids,
address_history_last_checked_at: source.address_history_last_checked_at,
address_history_synced_at: source.address_history_synced_at,
updated_at: source.updated_at
};
}
function transactionRecordFileName(id) {
return `${String(id).padStart(8, "0")}.json`;
}
function addIndexTransaction(index, txid, txtype) {
if (!index.txid_to_file[txid]) {
index.txid_to_file[txid] = transactionRecordFileName(index.next_id);
index.next_id += 1;
}
if (!index.ordered_txids.includes(txid)) {
index.ordered_txids.push(txid);
}
index.txid_to_type[txid] = Number(txtype || 0);
const typeKey = String(Number(txtype || 0));
const txidsForType = Array.isArray(index.type_to_txids[typeKey]) ? index.type_to_txids[typeKey] : [];
if (!txidsForType.includes(txid)) {
txidsForType.push(txid);
}
index.type_to_txids[typeKey] = txidsForType;
return index.txid_to_file[txid];
}
function transactionRecordFromRpc(record) {
const transactionBytes = Array.from(record?.transaction_bytes || []);
const txtype = Number(record?.transaction_type ?? transactionBytes[0] ?? 0);
const block = Number(record?.block_height || 0) || null;
const transaction = decodeLightweightTransaction(transactionBytes);
return {
type: txTypeKey(txtype),
txtype,
block,
status: walletStatusForBlock(block),
transaction,
miner_earnings: Array.isArray(record?.miner_earnings) ? record.miner_earnings : []
};
}
function chainStatusForBlock(blockHeight, currentHeight = latestKnownBlockHeight) {
const height = Number(blockHeight || 0);
const tip = Number(currentHeight || 0);
if (!height || !tip || tip < height) {
return CHAIN_STATUS_PENDING;
}
return tip - height >= CONFIRMED_AFTER_BLOCKS ? CHAIN_STATUS_FINALIZED : CHAIN_STATUS_PENDING;
}
function updateTransactionCacheChainStatus(cache, currentHeight = latestKnownBlockHeight) {
Object.entries(cache.transactions || {}).forEach(([, record]) => {
if (!record || typeof record !== "object") {
return;
}
record.status = walletStatusForBlock(blockValueFromRecord(record), currentHeight);
});
cache.finalized_through_height = Math.max(0, Number(currentHeight || 0) - CONFIRMED_AFTER_BLOCKS);
return cache;
}
function recentTransactionField(transaction, names, fallback = "") {
for (const name of names) {
const value = transaction?.[name];
if (value !== undefined && value !== null && String(value).trim() !== "") {
return String(value);
}
}
return fallback;
}
function transactionRecordsForTxids(cache, txids) {
if (!cache || typeof cache !== "object") {
return [];
}
return (txids || [])
.map((txid) => {
const transaction = cache.transactions?.[txid];
if (!transaction || typeof transaction !== "object") {
return null;
}
return { txid, ...transaction };
})
.filter(Boolean);
}
function normalizeRecentTransactionRecords(records, options = {}) {
const source = Array.isArray(records) ? records : [];
const normalized = source
.filter((transaction) => transaction && typeof transaction === "object")
.flatMap((transaction, recordIndex) => {
if (!transaction.transaction && Array.isArray(transaction.rows)) {
return transaction.rows.map((row, rowIndex) => ({
...row,
txid: row.txid || transaction.txid,
block_height: row.block_height ?? blockValueFromRecord(transaction),
__order: recordIndex + rowIndex / 1000
}));
}
const blockHeight = blockValueFromRecord(transaction);
return parseDashboardTransactionRows(
transaction.txid,
blockHeight,
transaction.transaction,
transaction.linked_transactions || {},
transaction.miner_earnings || []
).map((row, rowIndex) => ({
...row,
status: walletStatusForBlock(blockHeight),
__order: recordIndex + rowIndex / 1000
}));
})
.map((transaction) => ({
...transaction,
status: walletStatusForBlock(Number(recentTransactionField(transaction, ["block_height", "blockHeight", "height"], "0")))
}));
const sorted = options.preserveOrder ? normalized : normalized.sort((left, right) => {
const leftHeight = Number(recentTransactionField(left, ["block_height", "blockHeight", "height"], "0"));
const rightHeight = Number(recentTransactionField(right, ["block_height", "blockHeight", "height"], "0"));
return rightHeight - leftHeight;
});
return sorted
.slice(0, 5)
.map(({ __order: _order, ...transaction }) => transaction);
}
function normalizeCachedRecentTransactions(cache) {
if (!cache || typeof cache !== "object") {
return [];
}
if (cache.transactions && typeof cache.transactions === "object" && !Array.isArray(cache.transactions)) {
return normalizeRecentTransactionRecords(transactionRecordsForTxids(cache, cache.ordered_txids || []));
}
if (Array.isArray(cache)) {
return normalizeRecentTransactionRecords(cache);
}
if (Array.isArray(cache.transactions)) {
return normalizeRecentTransactionRecords(cache.transactions);
}
return [];
}
function renderRecentTransactions(transactions) {
clearRecentTransactionsRows();
if (!recentTransactionsBody) {
return;
}
transactions.forEach((transaction) => {
const type = recentTransactionField(transaction, ["type", "tx_type", "transactionType"], "Transaction");
const asset = recentTransactionField(transaction, ["asset", "coin", "token"], activeBaseCoin());
const fallbackCounterparty = shortenAddress(recentTransactionField(transaction, ["txid"], "Unknown"));
const counterparty = recentTransactionField(transaction, ["counterparty", "to", "from", "address"], fallbackCounterparty);
const status = recentTransactionField(transaction, ["status"], "Confirmed");
const amount = recentTransactionField(transaction, ["amount", "value", "display_amount"], "");
const statusClass = status.trim().toLowerCase() === "pending" ? "pending" : "confirmed";
const row = document.createElement("tr");
row.innerHTML = `
<td></td>
<td></td>
<td></td>
<td><span class="table-status ${statusClass}"></span></td>
<td></td>
`;
row.children[0].textContent = type;
row.children[1].textContent = asset;
row.children[2].textContent = counterparty;
row.querySelector(".table-status").textContent = status;
row.children[4].textContent = amount;
recentTransactionsBody.appendChild(row);
});
}
async function renderRecentTransactionsFromCache() {
if (!walletLoaded || !registrationRegistered) {
return [];
}
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke) {
clearRecentTransactionsRows();
setRecentTransactionsState("Transaction cache requires the desktop app build.");
return [];
}
const readId = ++recentTransactionsCacheReadId;
try {
await refreshCurrentBlockHeight();
const cache = await readTransactionCacheObject();
if (readId !== recentTransactionsCacheReadId) {
return [];
}
updateTransactionCacheChainStatus(cache);
const transactions = normalizeCachedRecentTransactions(cache);
renderRecentTransactions(transactions);
setRecentTransactionsState(transactions.length ? "" : "No cached transactions yet.");
return transactions;
} catch (_) {
if (readId !== recentTransactionsCacheReadId) {
return [];
}
clearRecentTransactionsRows();
setRecentTransactionsState("No cached transactions yet.");
return [];
}
}
async function readTransactionCacheObject(walletToken = activeWalletCacheToken()) {
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke || !isActiveWalletCacheToken(walletToken)) {
return { transactions: {}, ordered_txids: [] };
}
const index = await readTransactionIndexObject(walletToken);
return readTransactionCacheRecords(index, index.ordered_txids, walletToken);
}
async function readTransactionCacheRecords(index, txids, walletToken = activeWalletCacheToken()) {
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke || !isActiveWalletCacheToken(walletToken)) {
return { transactions: {}, ordered_txids: [] };
}
const normalizedIndex = normalizeTransactionIndex(index);
const cache = { ...normalizedIndex, transactions: {} };
const recordTxids = normalizeOrderedTxids({ ordered_txids: Array.isArray(txids) ? txids : [] });
cache.ordered_txids = recordTxids;
for (const txid of recordTxids) {
if (!isActiveWalletCacheToken(walletToken)) {
return { transactions: {}, ordered_txids: [] };
}
const fileName = normalizedIndex.txid_to_file?.[txid];
if (!fileName) {
continue;
}
try {
const recordJson = await invoke("read_local_transaction_record", { fileName });
const record = JSON.parse(recordJson || "{}");
if (record && typeof record === "object" && Object.keys(record).length) {
cache.transactions[txid] = repairCachedTransactionRecord(txid, record);
}
} catch (_) {
continue;
}
}
return cache;
}
async function readTransactionIndexObject(walletToken = activeWalletCacheToken()) {
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke || !isActiveWalletCacheToken(walletToken)) {
return normalizeTransactionIndex({});
}
const indexJson = await invoke("read_local_transaction_index");
if (!isActiveWalletCacheToken(walletToken)) {
return normalizeTransactionIndex({});
}
return normalizeTransactionIndex(JSON.parse(indexJson || "{}"));
}
function activeWalletCacheToken() {
return activeWalletAddressShort || activeWalletAddressHex || activeWalletPath || "";
}
function isActiveWalletCacheToken(token) {
return Boolean(walletLoaded && token && token === activeWalletCacheToken());
}
async function refreshCurrentBlockHeight() {
const invoke = window.__TAURI__?.core?.invoke;
const broadcastNode = activeBroadcastNode();
if (!invoke || !broadcastNode || !walletLoaded) {
return latestKnownBlockHeight;
}
try {
latestKnownBlockHeight = await invokeWalletRpcWithBackoff(
"Block height lookup",
() => invoke("current_block_height", { broadcastNode }),
() => walletLoaded
);
} catch (_) {
latestKnownBlockHeight = 0;
}
return latestKnownBlockHeight;
}
function repairCachedTransactionRecord(txid, record) {
if (!record || typeof record !== "object") {
return record;
}
const block = blockValueFromRecord(record);
const transaction = record.transaction && !Array.isArray(record.transaction)
? record.transaction
: record.decoded_transaction;
const transactionType = Number(record.txtype ?? record.transaction_type ?? decodedTransactionType(transaction));
return {
txid,
type: record.type || txTypeKey(transactionType),
txtype: transactionType,
block,
status: walletStatusForBlock(block),
transaction: transaction || record.transaction,
linked_transactions: record.linked_transactions || {},
miner_earnings: Array.isArray(record.miner_earnings) ? record.miner_earnings : []
};
}
function linkedTransactionIdsFor(transaction) {
if (transaction && typeof transaction === "object" && !Array.isArray(transaction)) {
const contractHash = transaction.unsigned_contract_payment?.contract_hash;
return contractHash ? [contractHash] : [];
}
const bytes = Array.from(transaction || []);
const type = Number(bytes[0] || 0);
if (type === 8) {
return [bytesToHex(bytes.slice(13, 45))];
}
return [];
}
function cachedRecordNeedsRefresh(record) {
if (!record || typeof record !== "object") {
return true;
}
if (!record.transaction || Array.isArray(record.transaction)) {
return true;
}
const linkedIds = linkedTransactionIdsFor(record.transaction);
return linkedIds.some((txid) => !record.linked_transactions?.[txid]);
}
async function hydrateLinkedTransactionsForRecord(
txid,
record,
invoke,
broadcastNode,
cache = { transactions: {} },
shouldContinue = () => true,
linkedLookupDelayMs = 0,
commandName = "transaction_by_txid"
) {
if (!record || !record.transaction || !cachedRecordNeedsRefresh(record)) {
return record;
}
const linkedTransactions = { ...(record.linked_transactions || {}) };
for (const linkedTxid of linkedTransactionIdsFor(record.transaction)) {
if (linkedTransactions[linkedTxid]?.transaction) {
continue;
}
const cachedLinked = cache.transactions?.[linkedTxid] || cache.transactions?.[txid]?.linked_transactions?.[linkedTxid];
if (cachedLinked?.transaction && !Array.isArray(cachedLinked.transaction)) {
linkedTransactions[linkedTxid] = repairCachedTransactionRecord(linkedTxid, cachedLinked);
continue;
}
if (linkedLookupDelayMs > 0) {
await sleepMs(linkedLookupDelayMs);
}
if (!shouldContinue()) {
throw new Error("Linked transaction lookup cancelled");
}
const linked = await invokeWalletRpcWithBackoff(
"Linked transaction lookup",
() => invoke(commandName, {
broadcastNode,
txid: linkedTxid
}),
shouldContinue
);
linkedTransactions[linkedTxid] = repairCachedTransactionRecord(linkedTxid, {
txid: linkedTxid,
block: linked.block_height || null,
transaction: linked.transaction,
transaction_type: linked.transaction_type,
miner_earnings: Array.isArray(linked.miner_earnings) ? linked.miner_earnings : []
});
}
return repairCachedTransactionRecord(txid, {
...record,
linked_transactions: linkedTransactions
});
}
function sanitizeTransactionCacheForWrite(cache) {
const clean = {
transactions: {},
ordered_txids: normalizeOrderedTxids(cache),
finalized_through_height: cache.finalized_through_height || 0,
address_history_last_checked_at: cache.address_history_last_checked_at,
address_history_synced_at: cache.address_history_synced_at
};
Object.entries(cache.transactions || {}).forEach(([txid, record]) => {
if (!record || typeof record !== "object") {
return;
}
const normalized = repairCachedTransactionRecord(txid, record);
if (!normalized?.transaction || Array.isArray(normalized.transaction)) {
return;
}
const linkedTransactions = {};
Object.entries(normalized.linked_transactions || {}).forEach(([linkedTxid, linkedRecord]) => {
if (linkedRecord && typeof linkedRecord === "object") {
const linkedNormalized = repairCachedTransactionRecord(linkedTxid, linkedRecord);
if (linkedNormalized?.transaction && !Array.isArray(linkedNormalized.transaction)) {
linkedTransactions[linkedTxid] = linkedNormalized;
}
}
});
clean.transactions[txid] = {
...normalized,
linked_transactions: linkedTransactions
};
});
updateTransactionCacheChainStatus(clean);
return clean;
}
async function writeTransactionCacheObject(cache, walletToken = activeWalletCacheToken()) {
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke || !isActiveWalletCacheToken(walletToken)) {
return;
}
const index = normalizeTransactionIndex(cache);
const indexJson = JSON.stringify({
...index,
updated_at: new Date().toISOString()
});
if (!isActiveWalletCacheToken(walletToken)) {
return;
}
await invoke("write_local_transaction_index", {
indexJson
});
}
async function writeTransactionRecordFile(fileName, record, walletToken = activeWalletCacheToken()) {
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke || !isActiveWalletCacheToken(walletToken)) {
return;
}
await invoke("write_local_transaction_record", {
fileName,
recordJson: JSON.stringify(record)
});
}
async function mutateTransactionCache(mutator, walletToken = activeWalletCacheToken()) {
if (!isActiveWalletCacheToken(walletToken)) {
return { transactions: {}, ordered_txids: [] };
}
const previous = transactionCacheMutationChain;
let releaseMutation;
transactionCacheMutationChain = new Promise((resolve) => {
releaseMutation = resolve;
});
await previous;
try {
if (!isActiveWalletCacheToken(walletToken)) {
return { transactions: {}, ordered_txids: [] };
}
let cache = await readTransactionCacheObject(walletToken);
Object.entries(cache.transactions).forEach(([txid, record]) => {
cache.transactions[txid] = repairCachedTransactionRecord(txid, record);
});
updateTransactionCacheChainStatus(cache);
cache = (await mutator(cache)) || cache;
if (!isActiveWalletCacheToken(walletToken)) {
return { transactions: {}, ordered_txids: [] };
}
updateTransactionCacheChainStatus(cache);
await writeTransactionCacheObject(cache, walletToken);
return cache;
} finally {
releaseMutation();
}
}
async function fetchTransactionRecord(
invoke,
broadcastNode,
txid,
cache,
shouldContinue = () => true,
linkedLookupDelayMs = DASHBOARD_INITIAL_TX_FETCH_DELAY_MS,
commandName = "transaction_by_txid"
) {
const tx = await invokeWalletRpcWithBackoff(
"Transaction lookup",
() => invoke(commandName, {
broadcastNode,
txid
}),
shouldContinue
);
const linkedTransactions = {};
const record = repairCachedTransactionRecord(txid, {
txid,
block: tx.block_height || null,
transaction: tx.transaction,
transaction_type: tx.transaction_type,
linked_transactions: linkedTransactions,
miner_earnings: Array.isArray(tx.miner_earnings) ? tx.miner_earnings : []
});
return hydrateLinkedTransactionsForRecord(
txid,
record,
invoke,
broadcastNode,
cache,
shouldContinue,
linkedLookupDelayMs,
commandName
);
}
async function syncLatestDashboardTransactions() {
if (!walletLoaded || !registrationRegistered || currentView !== "dashboard" || latestTransactionsSyncInFlight) {
return;
}
const invoke = window.__TAURI__?.core?.invoke;
const broadcastNode = activeBroadcastNode();
if (!invoke || !broadcastNode) {
return;
}
const syncId = ++latestTransactionsSyncId;
recentTransactionsCacheReadId += 1;
latestTransactionsSyncInFlight = true;
try {
const hasVisibleTransactions = latestDashboardTransactionRecords.size > 0;
if (!hasVisibleTransactions) {
setRecentTransactionsState("Checking latest transactions...", "loading");
}
if (hasVisibleTransactions) {
await refreshCurrentBlockHeight();
}
const shouldContinue = () => (
syncId === latestTransactionsSyncId &&
registrationRegistered &&
walletLoaded &&
currentView === "dashboard"
);
const latestRecords = await invokeWalletRpcWithBackoff(
"Latest address transaction lookup",
() => invoke("latest_address_transactions", {
broadcastNode,
limit: DASHBOARD_RECENT_LIMIT
}),
shouldContinue
);
if (syncId !== latestTransactionsSyncId || !registrationRegistered || currentView !== "dashboard") {
return;
}
const txids = latestRecords.map((record) => record.txid).filter(Boolean);
if (!latestRecords.length) {
latestDashboardTxids = [];
latestDashboardTransactionRecords = new Map();
clearRecentTransactionsRows();
setRecentTransactionsState("No transactions found.");
return;
}
const memoryCache = { transactions: {} };
for (const record of latestRecords) {
if (!record?.txid || !Array.isArray(record.transaction_bytes)) {
continue;
}
const chainStatus = chainStatusForBlock(record.block_height);
let transactionRecord = repairCachedTransactionRecord(record.txid, {
txid: record.txid,
block: record.block_height || null,
transaction: record.transaction_bytes,
transaction_type: record.transaction_type,
miner_earnings: Array.isArray(record.miner_earnings) ? record.miner_earnings : []
});
transactionRecord = await hydrateLinkedTransactionsForRecord(
record.txid,
transactionRecord,
invoke,
broadcastNode,
memoryCache,
shouldContinue,
0
);
if (!shouldContinue()) {
return;
}
const rows = parseDashboardTransactionRows(
record.txid,
record.block_height,
transactionRecord.transaction,
transactionRecord.linked_transactions || {},
record.miner_earnings || []
).map((row) => ({
...row,
chain_status: chainStatus
}));
memoryCache.transactions[record.txid] = {
txid: record.txid,
block_height: record.block_height,
chain_status: chainStatus,
transaction: record.transaction_bytes,
transaction_bytes: record.transaction_bytes,
transaction_type: record.transaction_type,
miner_earnings: Array.isArray(record.miner_earnings) ? record.miner_earnings : [],
linked_transactions: transactionRecord.linked_transactions || {},
rows,
parser_version: 3
};
}
latestDashboardTxids = txids;
latestDashboardTransactionRecords = new Map(
txids
.map((txid) => [txid, memoryCache.transactions[txid]])
.filter(([, record]) => record)
);
renderRecentTransactions(normalizeRecentTransactionRecords(
transactionRecordsForTxids(memoryCache, txids),
{ preserveOrder: true }
));
setRecentTransactionsState("");
if (!hasVisibleTransactions) {
refreshCurrentBlockHeight()
.then(() => {
if (
syncId === latestTransactionsSyncId &&
registrationRegistered &&
walletLoaded &&
currentView === "dashboard"
) {
const latestCache = { transactions: Object.fromEntries(latestDashboardTransactionRecords) };
updateTransactionCacheChainStatus(latestCache);
latestDashboardTransactionRecords = new Map(Object.entries(latestCache.transactions));
renderRecentTransactions(normalizeRecentTransactionRecords(
transactionRecordsForTxids(latestCache, latestDashboardTxids),
{ preserveOrder: true }
));
}
})
.catch(() => {});
}
} catch (error) {
const message = String(error);
if (syncId === latestTransactionsSyncId) {
if (latestDashboardTransactionRecords.size === 0) {
clearRecentTransactionsRows();
setRecentTransactionsState(message || "Unable to load latest transactions right now.");
}
}
} finally {
if (
syncId === latestTransactionsSyncId &&
latestDashboardTransactionRecords.size === 0 &&
recentTransactionsState?.classList.contains("loading")
) {
setRecentTransactionsState("Unable to load latest transactions right now.");
}
latestTransactionsSyncInFlight = false;
}
}
function resetLatestDashboardTransactionsMemory() {
latestDashboardTxids = [];
latestDashboardTransactionRecords = new Map();
}
function shouldPollLatestDashboardTransactions() {
return walletLoaded && registrationRegistered && currentView === "dashboard";
}
function startLatestDashboardTransactionsPolling() {
if (latestDashboardTransactionsTimer || !shouldPollLatestDashboardTransactions()) {
return;
}
syncLatestDashboardTransactions();
latestDashboardTransactionsTimer = setInterval(() => {
syncLatestDashboardTransactions();
}, DASHBOARD_LATEST_REFRESH_MS);
}
function stopLatestDashboardTransactionsPolling() {
latestTransactionsSyncId += 1;
if (latestDashboardTransactionsTimer) {
clearInterval(latestDashboardTransactionsTimer);
latestDashboardTransactionsTimer = null;
}
}
function updateLatestDashboardTransactionsPolling() {
if (shouldPollLatestDashboardTransactions()) {
startLatestDashboardTransactionsPolling();
return;
}
stopLatestDashboardTransactionsPolling();
}
function shouldContinueBackgroundTransactionSync(syncId, walletToken = activeWalletCacheToken()) {
return (
syncId === backgroundTransactionSyncId &&
walletLoaded &&
registrationRegistered &&
isActiveWalletCacheToken(walletToken)
);
}
function stopBackgroundTransactionSync() {
backgroundTransactionSyncId += 1;
}
function startBackgroundTransactionSync() {
if (!walletLoaded || !registrationRegistered) {
return;
}
const walletToken = activeWalletCacheToken();
const syncId = ++backgroundTransactionSyncId;
runBackgroundTransactionSync(syncId, 0, walletToken);
}
async function runBackgroundTransactionSync(syncId, failureCount = 0, walletToken = activeWalletCacheToken()) {
const invoke = window.__TAURI__?.core?.invoke;
if (!invoke) {
return;
}
try {
while (shouldContinueBackgroundTransactionSync(syncId, walletToken)) {
const broadcastNode = activeBroadcastNode();
if (!broadcastNode) {
await sleepMs(BACKGROUND_SYNC_IDLE_MS);
continue;
}
const currentHeight = await refreshCurrentBlockHeight();
if (!currentHeight) {
await sleepMs(BACKGROUND_SYNC_IDLE_MS);
continue;
}
const cache = await readTransactionIndexObject(walletToken);
const skip = normalizeOrderedTxids(cache).length;
const shouldContinue = () => shouldContinueBackgroundTransactionSync(syncId, walletToken);
const records = await invokeWalletRpcWithBackoff(
"Paged address history lookup",
() => invoke("paged_address_history", {
broadcastNode,
skip,
limit: BACKGROUND_HISTORY_PAGE_SIZE
}),
shouldContinue
);
if (!shouldContinueBackgroundTransactionSync(syncId, walletToken)) {
return;
}
if (!records.length) {
cache.address_history_last_checked_at = new Date().toISOString();
cache.address_history_synced_at = new Date().toISOString();
await writeTransactionCacheObject(cache, walletToken);
await sleepMs(BACKGROUND_SYNC_IDLE_MS);
continue;
}
for (const rpcRecord of records) {
if (!shouldContinueBackgroundTransactionSync(syncId, walletToken)) {
return;
}
const txid = rpcRecord?.txid;
if (!txid || cache.txid_to_file?.[txid]) {
continue;
}
let record = transactionRecordFromRpc(rpcRecord);
record = await hydrateLinkedTransactionsForRecord(
txid,
record,
invoke,
broadcastNode,
{ transactions: {} },
shouldContinue,
BACKGROUND_TX_FETCH_DELAY_MS
);
const fileName = addIndexTransaction(cache, txid, record.txtype);
await writeTransactionRecordFile(fileName, record, walletToken);
if (typeof syncAddressBookFromTransactionRecord === "function") {
await syncAddressBookFromTransactionRecord(txid, record);
}
}
cache.address_history_synced_at = new Date().toISOString();
await writeTransactionCacheObject(cache, walletToken);
if (typeof flushAddressBookWrite === "function") {
await flushAddressBookWrite();
}
failureCount = 0;
}
} catch (_) {
if (shouldContinueBackgroundTransactionSync(syncId, walletToken)) {
const backoff = Math.min(BACKGROUND_SYNC_IDLE_MS * 2 ** failureCount, 60000);
await sleepMs(backoff);
if (shouldContinueBackgroundTransactionSync(syncId, walletToken)) {
runBackgroundTransactionSync(syncId, failureCount + 1, walletToken);
return;
}
}
}
}