mirror of
https://github.com/janoside/btc-rpc-explorer.git
synced 2026-08-13 12:33:13 +02:00
-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
458 lines
No EOL
18 KiB
Text
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 ≤ 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();
|
|
}
|
|
});
|
|
}); |