btc-rpc-explorer/views/transaction.pug
Dan Janosik e8eeb184e1
lots of minor frontend/code cleanup:
-mixin for txList
-fee/feeRate badge on tx lists (block, address, mempool browse)
-change to single "net balance change" badge on address tx list
-fix minor issues with sats value display
-cleanup minor issue with "time ago" display tooltip
-cleanup text coloring for various badge mixins
-fix display of negative large values
2023-06-13 11:34:08 -04:00

458 lines
No EOL
18 KiB
Text

extends layout
block headContent
title Transaction #{txid}
style.
.field {
word-wrap: break-word;
}
block content
if (!result || !result.getrawtransaction)
+pageTitle("Transaction", txid, true)
if (block)
+contentSection("Summary")
+summaryRow(2)
+summaryItem("Block")
a(href=`./block/${block.hash}`) ##{block.height.toLocaleString()}
+summaryItem("Date / Time")
if (block.time)
+timestamp(block.time, {agoOptions:{oneElement:true}})
else
| ?
+warningAlert
h6.mb-2 Transaction Details Unavailable
if (blockHeight && global.prunedBlockchain && blockHeight < global.pruneHeight)
.mb-2 Blockchain <b>pruning</b> is enabled on your node. This setting tells your node that after validating transactions it may discard data that is non-essential for future validation needs.
.mb-0 The block that contains this transaction has been pruned, meaning that non-essential data has been deleted, including the details of this transaction.
else if (!global.txindexAvailable)
| !{markdown(noTxIndexMsg)}
if (result && result.getrawtransaction)
+pageTitle("Transaction", txid, true, [["coinbase", "This is a coinbase transaction."]])
+pageTabs(["Details", "Scripts", "JSON"])
- DecimalRounded = Decimal.clone({ precision: 4, rounding: 2 })
- var totalInputValue = new Decimal(0);
if (result.getrawtransaction.vin[0].coinbase)
- totalInputValue = totalInputValue.plus(new Decimal(coinConfig.blockRewardFunction(result.getblock.height, activeBlockchain)));
each txInput, txInputIndex in result.txInputs
if (txInput)
- var vout = txInput;
if (vout.value)
- totalInputValue = totalInputValue.plus(new Decimal(vout.value));
- var totalOutputValue = new Decimal(0);
each vout, voutIndex in result.getrawtransaction.vout
- totalOutputValue = totalOutputValue.plus(new Decimal(vout.value));
div.tab-content
+pageTab("Details", true)
#alert-included-next-block.alert.alert-primary(style="display: none;")
h6 Confirmation: Predicted in Next Block
i.bi-check2.text-success.ms-1
.mb-2 This transaction is currently unconfirmed but, with the current mempool, it's predicted to be confirmed in the next block.
span It's currently transaction #<span id='next-block-index'></span> of <span id='next-block-count'></span>.
if (global.specialTransactions && global.specialTransactions[txid])
- var stInfo = global.specialTransactions[txid];
+funAlert(stInfo.alertBodyHtml, stInfo.summary, stInfo.referenceUrl)
.clearfix
.float-end
if (`${userSettings["txPageShowTechDetails"]}` == "false")
a.btn.btn-primary.btn-sm.me-2(href=`./changeSetting?name=txPageShowTechDetails&value=true`, title="Display technical details for this transaction.", data-bs-toggle="tooltip")
i.bi-plus-square.me-2
| Technical Details
- var isTxConfirmed = true;
if (!result.getrawtransaction.confirmations || result.getrawtransaction.confirmations == 0)
- isTxConfirmed = false;
if (isCoinbaseTx)
- var minerInfo = utils.identifyMiner(result.getrawtransaction, result.getblock.height);
+contentSection("Summary")
+summaryRow((isTxConfirmed ? 2 : 3) + (isCoinbaseTx ? 1 : 2) + (isCoinbaseTx && minerInfo ? 1 : 0))
if (!isTxConfirmed)
+summaryItem("Status")
span.text-danger Unconfirmed
+summaryItem("First Seen", "The time when this transaction entered your node's mempool.")
+timestamp(mempoolDetails.entry.time)
+summaryItem("RBF", "Whether this unconfirmed transaction is replaceable using Replace-By-Fee.")
- var replaceable = false;
each vin, vinIndex in tx.vin
// ref: https://github.com/bitcoin/bips/blob/master/bip-0125.mediawiki#summary
if (vin.sequence < 4294967295 - 1)
- replaceable = true;
if (replaceable)
span.text-success Yes
else if (mempoolDetails.ancestors.length > 0)
span.border-dotted(title="RBF may be signaled explicitly or implicitly. This transaction does not explicitly opt in, but if any of its unconfirmed ancestor transactions are replaceable then this one is too.", data-bs-toggle="tooltip") Not explicit…
a.ms-2(href="https://github.com/bitcoin/bips/blob/master/bip-0125.mediawiki#summary", data-bs-toggle="tooltip", title="More info about RBF", target="_blank")
i.bi-info-circle
else
span No
else
+summaryItem("Block")
a(href=`./block/${result.getrawtransaction.blockhash}`) #{result.getblock.height.toLocaleString()}
small.ms-2.text-muted
| (
+confirmations(tx.confirmations)
| )
+summaryItem("Date / Time")
if (tx.time)
+timestamp(result.getrawtransaction.time, {agoOptions:{oneElement:true}})
if (isCoinbaseTx)
if (minerInfo)
+summaryItem("Miner")
if (minerInfo.type == "address-only")
span.badge.bg-secondary
span(data-bs-toggle="tooltip", title=`Unknown Miner Payout Address: ${minerInfo.name}`) #{utils.ellipsizeMiddle(minerInfo.name, 16)}
+copyTextButton(minerInfo.name)
else if (minerInfo.identifiedBy)
span(data-bs-toggle="tooltip", data-bs-html="true", title=`Identified by: ${minerInfo.identifiedBy}${minerInfo.note ? ("<br/><br/>" + minerInfo.note) : ""}`)
+primaryBadge(minerInfo.name, false)
else
span
+primaryBadge(minerInfo.name, false)
+summaryItem("Fees Collected", "The aggregate fees collected by the miner that created this coinbase transaction.")
- var currencyValue = new Decimal(totalOutputValue).minus(totalInputValue);
+valueDisplay(currencyValue)
else
+summaryItem("Fee Rate", null, "sat/vB")
if (!mempoolDetails && !global.txindexAvailable)
span.border-dotted(title="Determining the fee of confirmed transactions requires txindex to be enabled", data-bs-toggle="tooltip")
i.bi-backspace.text-danger.me-2
| ?
else
- var feePaid = mempoolDetails ? new Decimal(mempoolDetails.entry.fees.base) : new Decimal(totalInputValue).minus(totalOutputValue);
span #{utils.addThousandsSeparators(new DecimalRounded(feePaid).dividedBy(tx.vsize).times(100000000))}
+summaryItem("Total Fee")
if (!mempoolDetails && !global.txindexAvailable)
span.border-dotted(title="Determining the fee of confirmed transactions requires txindex to be enabled", data-bs-toggle="tooltip")
i.bi-backspace.text-danger.me-2
| ?
else
- var feePaid = mempoolDetails ? new Decimal(mempoolDetails.entry.fees.base) : new Decimal(totalInputValue).minus(totalOutputValue);
+valueDisplay(feePaid)
- var daysDestroyed = new Decimal(0);
each xInput, xInputIndex in tx.vin
- var vout = null;
if (result.txInputs && result.txInputs[xInputIndex])
- var vout = result.txInputs[xInputIndex];
if (vout.utxoTime)
- var dt = result.getrawtransaction.time - vout.utxoTime;
- var dtDays = dt / 60 / 60 / 24;
- daysDestroyed = daysDestroyed.plus(new Decimal(vout.value).times(dtDays));
+contentSection("Technical Details", true, "txPageShowTechDetails")
if (isTxConfirmed)
- var blockRewardMax = coinConfig.blockRewardFunction(result.getblock.height, activeBlockchain);
- var valueDestroyed = (isCoinbaseTx && parseFloat(totalOutputValue) < parseFloat(blockRewardMax));
- var showLocktime = false;
if (tx.locktime > 500000000 && result.getrawtransaction.locktime > (new Date().getTime() / 1000))
// future-dated locktime date
- showLocktime = true;
if (tx.locktime > 0 && !isTxConfirmed)
// unconfirmed locktime block height
- showLocktime = true;
+summaryRow(3 + (tx.weight ? 1 : 0) + (valueDestroyed ? 1 : 0) + (showLocktime ? 1 : 0) + (parseInt(daysDestroyed) > 0 ? 1 : 0))
+summaryItem("Version")
| #{result.getrawtransaction.version}
+summaryItem("Size", null, "vB")
span #{tx.vsize.toLocaleString()}
if (tx.size != tx.vsize)
span.small.text-muted.ms-2 (
span.border-dotted(title=`Raw size: ${tx.size.toLocaleString()} B`, data-bs-toggle="tooltip") #{tx.size.toLocaleString()}
| )
+summaryItem("Raw Data", `The raw transaction consisting of ${result.getrawtransaction.hex.length.toLocaleString()} hexadecimal bytes.`, "hex")
| #{utils.ellipsizeMiddle(result.getrawtransaction.hex, 12)}
+copyTextButton(result.getrawtransaction.hex)
if (tx.weight)
+summaryItem("Weight", null, "wu", "Weight Units")
span #{tx.weight.toLocaleString()}
if (valueDestroyed)
+summaryItem("Value Destroyed", "The miner of this transaction's block failed to collect this value. As a result, it is permanently lost.")
- var currencyValue = new Decimal(blockRewardMax).minus(totalOutputValue);
span.text-danger
+valueDisplay(currencyValue)
if (showLocktime)
+summaryItem("Locktime", "A specification of the earliest that a transaction may be mined. If this value is &leq; 500M, the value is interpreted as a block height; otherwise, it is interpreted as a unix timestamp.")
if (tx.locktime < 500000000 && !isTxConfirmed)
span.me-1
a.border-dotted(href=`./block-height/${result.getrawtransaction.locktime}`, title=`May be mined at this block height, or later`, data-bs-toggle="tooltip") #{result.getrawtransaction.locktime.toLocaleString()}
else
+timestamp(result.getrawtransaction.locktime)
if (result.getrawtransaction.locktime > (new Date().getTime() / 1000))
span.border-dotted.ms-1(title=`This future-dated 'locktime' value is only possible because this is a coinbase transaction. Normally, locktime specifies the earliest that a transaction may be mined (included in a block). But, because this transaction was created by the miner in the process of mining a block, it is able to bypass some of the normal validity checks that nodes on the network perform on transactions.`, data-bs-toggle="tooltip")
i.bi-flag.text-tiny.text-warning
if (parseInt(daysDestroyed) > 0)
- var daysDestroyedDesc = `The sum of all input values multiplied by the number of days that input's UTXO existed before being spent. Large values can sometimes be interesting, indicating the spending of long-dormant coins. A 1 ${coinConfig.ticker} UTXO spent after 7 days dormant results in 7 days destroyed, as does a 7 ${coinConfig.ticker} UTXO spent after 1 day dormant.`;
+summaryItem("Days Destroyed", daysDestroyedDesc, `${coinConfig.ticker.toLowerCase().substring(0, 1)}d`, `${coinConfig.name} Days`)
| #{utils.addThousandsSeparators(daysDestroyed.toDP(1))}
- var inputsOutputsSectionTitle = `${tx.vin.length.toLocaleString()} Input${tx.vin.length == 1 ? "" : "s"}, ${tx.vout.length.toLocaleString()} Output${tx.vout.length == 1 ? "" : "s"}`;
+sectionTitleBlock
.clearfix
.float-start #{inputsOutputsSectionTitle}
.float-end
- var hasInputAddresses = false;
- var txInputs = result.txInputs;
if (tx && tx.vin && txInputs)
each txVin, txVinIndex in tx.vin
if (!txVin.coinbase)
- var vout = null;
if (txInputs[txVinIndex])
- var voutAddresses = utils.getVoutAddresses(txInputs[txVinIndex]);
if (voutAddresses.length > 0)
- hasInputAddresses = true;
if (hasInputAddresses)
.d-inline.d-sm-none
a(href="javascript:void(0)", onclick=`$(this).find(".toggle-icon").toggleClass("bi-plus-square").toggleClass("bi-dash-square"); $(".tx-input-address-${tx.txid}").toggle();`)
i.toggle-icon.bi-plus-square.text-muted.text-end
+contentSection
- var tx = result.getrawtransaction;
- var txInputs = result.txInputs;
- var blockHeight = -1;
if (result && result.getblock)
- blockHeight = result.getblock.height;
- var totalIOValues = utils.getTxTotalInputOutputValues(tx, txInputs, blockHeight);
+txIoDetails(tx, txInputs, totalIOValues, blockHeight)
if (mempoolDetails)
if (mempoolDetails.ancestors.length > 0)
- var ancestorDesc = "Ancestor Transactions are transactions whose outputs are being spent by this transaction. All ancestors must be confirmed before this transaction can be confirmed (though they can be confirmed in the same block).";
+sectionTitle(`${mempoolDetails.ancestors.length.toLocaleString()} Unconfirmed Ancestor Transaction${mempoolDetails.ancestors.length == 1 ? "" : "s"}`, false, null, null, null, ancestorDesc)
+contentSection
ol.mb-0
each ancestorTxid, ancestorIndex in mempoolDetails.ancestors
li
a(href=`./tx/${ancestorTxid}`) #{ancestorTxid}
if (mempoolDetails.descendants.length > 0)
- var descendantDesc = "Descendant Transactions are transactions that spend outputs created by this transaction. This transaction (and all other ancestors transactions) must be confirmed before a descendant transaction can be confirmed (though they can be confirmed in the same block).";
+sectionTitle(`${mempoolDetails.descendants.length.toLocaleString()} Unconfirmed Descendant Transaction${mempoolDetails.descendants.length == 1 ? "" : "s"}`, false, null, null, null, descendantDesc)
+contentSection
ol.mb-0
each descendantTxid, descendantIndex in mempoolDetails.descendants
li
a(href=`./tx/${descendantTxid}`) #{descendantTxid}
+pageTab("Scripts")
+contentSection("Input Scripts")
table.table
thead
tr
th(style="width: 50px;")
th.text-card-highlight.text-uppercase.fw-normal Input
tbody
each vin, vinIndex in result.getrawtransaction.vin
tr
th
a.badge.card-highlight.border(data-bs-toggle="tooltip", title=`Input #${vinIndex}`, style="white-space: nowrap;")
i.bi-arrow-right.text-danger.me-1
span.font-monospace #{vinIndex.toLocaleString()}
td.word-wrap.text-break
if (vin.coinbase)
.mb-3(style="line-height: 1.75em;")
+successBadge("coinbase")
.mt-2
+hexDataDisplay(vin.coinbase)
if (vin.scriptSig && vin.scriptSig.asm)
.mb-2
.mb-2
+darkBadge("scriptSig")
.d-inline-block.small.border.rounded-1.p-1.px-2.card-highlight.word-wrap #{vin.scriptSig.asm}
if (vin.txinwitness && vin.txinwitness.length > 0)
.mb-2
+darkBadge("witness")
- var maxWitnessDisplayCount = 100;
each witnessItem, witnessIndex in vin.txinwitness
if (witnessIndex < maxWitnessDisplayCount)
span.me-2(class=(witnessIndex > 0 ? "mt-2" : false))
span.text-tiny.text-muted.me-1 ##{witnessIndex.toLocaleString()}
if (witnessItem.length == 0)
span.text-muted.text-tiny (empty)
else
+hexDataDisplay(witnessItem)
//.d-inline-block.small.border.rounded-1.p-1.px-2.card-highlight.word-wrap(class=(witnessIndex < (vin.txinwitness.length - 1) ? "mb-2" : false)) #{witnessItem}
if (vin.txinwitness.length > maxWitnessDisplayCount)
span.text-muted.text-tiny.fw-bold (#{(vin.txinwitness.length - maxWitnessDisplayCount).toLocaleString()} more witness items)
+contentSection("Output Scripts")
table.table
thead
tr
th(style="width: 50px;")
th.text-card-highlight.text-uppercase.fw-normal Script Pub Key
tbody
each vout, voutIndex in result.getrawtransaction.vout
tr
th
a.badge.card-highlight.border.fw-normal(data-bs-toggle="tooltip", title=`Output #${(voutIndex + 1)}`, style="white-space: nowrap;")
i.bi-arrow-right.text-success.me-1
span.font-monospace #{voutIndex.toLocaleString()}
td.word-wrap
if (vout.scriptPubKey && vout.scriptPubKey.asm)
- var chunks = vout.scriptPubKey.asm.split(" ");
if (chunks[0] == "OP_RETURN")
.mb-2
+primaryBadge(chunks[0])
- var asm = vout.scriptPubKey.asm;
- asm = asm.substring("OP_RETURN ".length);
+hexDataDisplay(asm)
else
each chunk, chunkIndex in chunks
.mb-2
if (chunk.startsWith("OP_"))
+primaryBadge(chunk)
else
+hexDataDisplay(chunk, "hex,utf8,ascii")
+pageTab("JSON")
if (result.getrawtransaction.hex.length <= 50000)
- var pillTabs = ["Transaction"];
if (!mempoolDetails)
- pillTabs.push("Block Header");
- pillTabs.push("UTXOs");
if (mempoolDetails)
- pillTabs.push("Mempool Details");
else
+pillTabs(pillTabs)
.tab-content
+pageTab("Transaction", true)
+contentSection("Transaction")
pre
code.text-json.json(data-lang="json") #{JSON.stringify(result.getrawtransaction, null, 4)}
+pageTab("Block Header")
+contentSection("Block Header")
pre
code.text-json.json(data-lang="json") #{JSON.stringify(result.getblock, null, 4)}
+pageTab("UTXOs")
+contentSection("UTXOs")
pre
code.text-json.json(data-lang="json") #{JSON.stringify(utxos, null, 4)}
if (mempoolDetails)
+pageTab("Mempool Details")
+contentSection("Mempool Details")
pre
code.json(data-lang="json") #{JSON.stringify(mempoolDetails, null, 4)}
//pre #{JSON.stringify(result.txInputs, null, 4)}
else
span This transaction is very large. Displaying it's data here may cause problems. Instead, see it's raw data via the internal API:
br
a(href=`./api/tx/${txid}`) #{txid}
block endOfBody
script.
var txid = "!{txid}";
$(document).ready(function() {
$.ajax({
url: `./api/mining/next-block/includes/${txid}`
}).done(res => {
if (res.included) {
$("#next-block-index").text((res.index + 1).toLocaleString());
$("#next-block-count").text(res.txCount.toLocaleString());
$("#alert-included-next-block").show();
}
});
});