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 = ` `; 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; } } } }