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