mirror of
https://github.com/Retropex/bitfeed.git
synced 2025-05-12 19:20:46 +02:00
Handle coinbase & OP_RETURN in tx detail view
This commit is contained in:
parent
987c2c801b
commit
4ee6f277da
@ -54,12 +54,19 @@ $: {
|
||||
function expandAddresses(items) {
|
||||
return items.map(item => {
|
||||
let address = 'unknown'
|
||||
let title = null
|
||||
if (item.script_pub_key) {
|
||||
address = SPKToAddress(item.script_pub_key) || ""
|
||||
if (address === 'OP_RETURN') {
|
||||
title = item.script_pub_key.substring(2).match(/../g).reduce((parsed, hexChar) => {
|
||||
return parsed + String.fromCharCode(parseInt(hexChar, 16))
|
||||
}, "")
|
||||
}
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
address
|
||||
address,
|
||||
title
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -68,24 +75,34 @@ let inputs = []
|
||||
let outputs = []
|
||||
$: {
|
||||
if ($detailTx && $detailTx.inputs) {
|
||||
inputs = expandAddresses($detailTx.inputs)
|
||||
if ($detailTx.isCoinbase) {
|
||||
inputs = [{
|
||||
address: 'coinbase',
|
||||
value: $detailTx.value
|
||||
}]
|
||||
} else {
|
||||
inputs = expandAddresses($detailTx.inputs)
|
||||
}
|
||||
} else inputs = []
|
||||
if ($detailTx && $detailTx.outputs) {
|
||||
outputs = expandAddresses($detailTx.outputs)
|
||||
if ($detailTx.isCoinbase) {
|
||||
outputs = expandAddresses($detailTx.outputs)
|
||||
} else {
|
||||
outputs = [{address: 'fee', value: $detailTx.fee}, ...expandAddresses($detailTx.outputs)]
|
||||
}
|
||||
} else outputs = []
|
||||
}
|
||||
|
||||
let sankeyLines
|
||||
let sankeyHeight
|
||||
$: {
|
||||
if ($detailTx && $detailTx.inputs && $detailTx.outputs) {
|
||||
sankeyHeight = Math.max($detailTx.inputs.length, $detailTx.outputs.length + 1) * rowHeight
|
||||
sankeyLines = calcSankeyLines($detailTx.inputs, $detailTx.outputs, $detailTx.fee || null, $detailTx.value, sankeyHeight, svgWidth, flowWeight)
|
||||
if ($detailTx && inputs && outputs) {
|
||||
sankeyHeight = Math.max(inputs.length, outputs.length) * rowHeight
|
||||
sankeyLines = calcSankeyLines(inputs, outputs, $detailTx.fee || null, $detailTx.value, sankeyHeight, svgWidth, flowWeight)
|
||||
}
|
||||
}
|
||||
|
||||
function calcSankeyLines(inputs, outputs, fee, value, totalHeight, svgWidth, flowWeight) {
|
||||
const feeAndOutputs = [{ value: fee }, ...outputs]
|
||||
const total = fee + value
|
||||
const mergeOffset = (totalHeight - flowWeight) / 2
|
||||
let cumThick = 0
|
||||
@ -126,7 +143,7 @@ function calcSankeyLines(inputs, outputs, fee, value, totalHeight, svgWidth, flo
|
||||
cumThick = 0
|
||||
xOffset = 0
|
||||
|
||||
const outLines = feeAndOutputs.map((output, index) => {
|
||||
const outLines = outputs.map((output, index) => {
|
||||
const weight = (output.value / total) * flowWeight
|
||||
const height = ((index + 0.5) * rowHeight)
|
||||
const step = (weight / 2)
|
||||
@ -151,7 +168,7 @@ function calcSankeyLines(inputs, outputs, fee, value, totalHeight, svgWidth, flo
|
||||
|
||||
cumThick += weight
|
||||
|
||||
return { line, weight, index, total: feeAndOutputs.length }
|
||||
return { line, weight, index, total: outputs.length }
|
||||
})
|
||||
outLines.forEach(line => {
|
||||
line.line[1].x -= xOffset
|
||||
@ -222,7 +239,7 @@ function getMiterOffset (weight, dy, dx) {
|
||||
|
||||
.pane {
|
||||
background: var(--palette-b);
|
||||
padding: .5em 1em;
|
||||
padding: 16px;
|
||||
border-radius: .5em;
|
||||
margin: 0 0 1em;
|
||||
|
||||
@ -236,6 +253,10 @@ function getMiterOffset (weight, dy, dx) {
|
||||
font-size: 0.8em;
|
||||
color: var(--grey);
|
||||
}
|
||||
|
||||
.value.coinbase-sig {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -245,6 +266,7 @@ function getMiterOffset (weight, dy, dx) {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
width: auto;
|
||||
color: var(--palette-x);
|
||||
|
||||
.operator {
|
||||
font-size: 2em;
|
||||
@ -333,7 +355,13 @@ function getMiterOffset (weight, dy, dx) {
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 460px) {
|
||||
@media (min-width: 411px) and (max-width: 479px) {
|
||||
.flow-diagram {
|
||||
font-size: 0.7em;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 410px) {
|
||||
.flow-diagram {
|
||||
display: block;
|
||||
|
||||
@ -352,30 +380,57 @@ function getMiterOffset (weight, dy, dx) {
|
||||
<div class="icon-button" class:disabled={$highlightingFull} on:click={() => highlight($detailTx.id)} title="Add transaction to watchlist">
|
||||
<Icon icon={BookmarkIcon}/>
|
||||
</div>
|
||||
<h2>Transaction <span class="tx-id">{ $detailTx.id }</span></h2>
|
||||
<div class="pane fee-calc">
|
||||
<div class="field">
|
||||
<span class="label">fee</span>
|
||||
<span class="value" style="color: {feeColor};">{ numberFormat.format($detailTx.fee) } sats</span>
|
||||
{#if $detailTx.isCoinbase }
|
||||
<h2>Coinbase <span class="tx-id">{ $detailTx.id }</span></h2>
|
||||
<div class="pane fee-calc">
|
||||
<div class="field">
|
||||
<span class="label">block subsidy</span>
|
||||
<span class="value">{ formatBTC($detailTx.coinbase.subsidy) }</span>
|
||||
</div>
|
||||
<span class="operator">+</span>
|
||||
<div class="field">
|
||||
<span class="label">fees</span>
|
||||
<span class="value">{ formatBTC($detailTx.coinbase.fees) }</span>
|
||||
</div>
|
||||
<span class="operator">=</span>
|
||||
<div class="field">
|
||||
<span class="label">total reward</span>
|
||||
<span class="value">{ formatBTC($detailTx.value) }</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="operator">/</span>
|
||||
<div class="field">
|
||||
<span class="label">size</span>
|
||||
<span class="value" style="color: {feeColor};">{ numberFormat.format($detailTx.vbytes) } vbytes</span>
|
||||
</div>
|
||||
<span class="operator">=</span>
|
||||
<div class="field">
|
||||
<span class="label">fee rate</span>
|
||||
<span class="value" style="color: {feeColor};">{ numberFormat.format($detailTx.feerate.toFixed(2)) } sats/vbyte</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pane total-value">
|
||||
<div class="field">
|
||||
<span class="label">Total value</span>
|
||||
<span class="value" style="color: {feeColor};">{ formatBTC($detailTx.value) }</span>
|
||||
<div class="pane fee-calc">
|
||||
<div class="field">
|
||||
<span class="label">coinbase</span>
|
||||
<span class="value coinbase-sig">{ $detailTx.coinbase.sigAscii }</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<h2>Transaction <span class="tx-id">{ $detailTx.id }</span></h2>
|
||||
<div class="pane fee-calc">
|
||||
<div class="field">
|
||||
<span class="label">fee</span>
|
||||
<span class="value" style="color: {feeColor};">{ numberFormat.format($detailTx.fee) } sats</span>
|
||||
</div>
|
||||
<span class="operator">/</span>
|
||||
<div class="field">
|
||||
<span class="label">size</span>
|
||||
<span class="value" style="color: {feeColor};">{ numberFormat.format($detailTx.vbytes) } vbytes</span>
|
||||
</div>
|
||||
<span class="operator">=</span>
|
||||
<div class="field">
|
||||
<span class="label">fee rate</span>
|
||||
<span class="value" style="color: {feeColor};">{ numberFormat.format($detailTx.feerate.toFixed(2)) } sats/vbyte</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pane total-value">
|
||||
<div class="field">
|
||||
<span class="label">total value</span>
|
||||
<span class="value" style="color: {feeColor};">{ formatBTC($detailTx.value) }</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<h2>Inputs & Outputs</h2>
|
||||
<div class="pane flow-diagram" style="grid-template-columns: minmax(0px, 1fr) {svgWidth}px minmax(0px, 1fr);">
|
||||
@ -389,7 +444,7 @@ function getMiterOffset (weight, dy, dx) {
|
||||
{/each}
|
||||
</div>
|
||||
<div class="column diagram">
|
||||
{#if sankeyLines && $pageWidth > 460}
|
||||
{#if sankeyLines && $pageWidth > 410}
|
||||
<svg class="sankey" height="{sankeyHeight}px" width="{svgWidth}px">
|
||||
<defs>
|
||||
{#each sankeyLines as line, index}
|
||||
@ -416,14 +471,10 @@ function getMiterOffset (weight, dy, dx) {
|
||||
{/if}
|
||||
</div>
|
||||
<div class="column outputs">
|
||||
<p class="header">{$detailTx.outputs.length} output{$detailTx.outputs.length > 1 ? 's' : ''}</p>
|
||||
<div class="entry fee">
|
||||
<p class="address">fee</p>
|
||||
<p class="amount">{ formatBTC($detailTx.fee) }</p>
|
||||
</div>
|
||||
<p class="header">{$detailTx.outputs.length} output{$detailTx.outputs.length > 1 ? 's' : ''} {#if !$detailTx.isCoinbase}+ fee{/if}</p>
|
||||
{#each outputs as output}
|
||||
<div class="entry">
|
||||
<p class="address" title={output.address}><span class="truncatable">{output.address.slice(0,-6)}</span><span class="suffix">{output.address.slice(-6)}</span></p>
|
||||
<p class="address" title={output.title || output.address}><span class="truncatable">{output.address.slice(0,-6)}</span><span class="suffix">{output.address.slice(-6)}</span></p>
|
||||
<p class="amount">{ formatBTC(output.value) }</p>
|
||||
</div>
|
||||
{/each}
|
||||
|
@ -13,12 +13,13 @@ export default class BitcoinBlock {
|
||||
this.bytes = bytes // OTW size of this block in bytes
|
||||
this.txnCount = txn_count
|
||||
this.txns = txns
|
||||
this.coinbase = new BitcoinTx(this.txns[0])
|
||||
this.coinbase = new BitcoinTx(this.txns[0], true)
|
||||
if (fees) {
|
||||
this.fees = fees
|
||||
} else {
|
||||
this.fees = null
|
||||
}
|
||||
this.coinbase.setBlock(this)
|
||||
this.height = this.coinbase.coinbase.height
|
||||
this.miner_sig = this.coinbase.coinbase.sigAscii
|
||||
|
||||
@ -29,7 +30,7 @@ export default class BitcoinBlock {
|
||||
this.minFeerate = this.txnCount > 1 ? Infinity : 0
|
||||
this.avgFeerate = 0
|
||||
this.txns.forEach(txn => {
|
||||
if (!BitcoinTx.prototype.isCoinbase(txn)) {
|
||||
if (txn.id !== this.coinbase.id) {
|
||||
const txFeerate = txn.fee / txn.vbytes
|
||||
this.maxFeerate = Math.max(this.maxFeerate, txFeerate)
|
||||
this.minFeerate = Math.min(this.minFeerate, txFeerate)
|
||||
|
@ -3,34 +3,34 @@ import config from '../config.js'
|
||||
import { mixColor, pink, bluegreen, orange, teal, green, purple } from '../utils/color.js'
|
||||
|
||||
export default class BitcoinTx {
|
||||
constructor (data, vertexArray) {
|
||||
constructor (data, vertexArray, isCoinbase = false) {
|
||||
this.vertexArray = vertexArray
|
||||
this.setData(data)
|
||||
this.setData(data, isCoinbase)
|
||||
this.view = new TxView(this)
|
||||
}
|
||||
|
||||
isCoinbase (txn) {
|
||||
if (txn.inputs && txn.inputs.length === 1 && txn.inputs[0].prev_txid === "0000000000000000000000000000000000000000000000000000000000000000") {
|
||||
const cbInfo = txn.inputs[0].script_sig
|
||||
// number of bytes encoding the block height
|
||||
const height_bytes = parseInt(cbInfo.substring(0,2), 16)
|
||||
// extract the specified number of bytes, reverse the endianness (reverse pairs of hex characters), parse as a hex string
|
||||
const height = parseInt(cbInfo.substring(2,2 + (height_bytes * 2)).match(/../g).reverse().join(''),16)
|
||||
// save remaining bytes as free data
|
||||
const sig = cbInfo.substring(2 + (height_bytes * 2))
|
||||
const sigAscii = sig.match(/../g).reduce((parsed, hexChar) => {
|
||||
return parsed + String.fromCharCode(parseInt(hexChar, 16))
|
||||
}, "")
|
||||
setCoinbaseData (block) {
|
||||
const cbInfo = this.inputs[0].script_sig
|
||||
// number of bytes encoding the block height
|
||||
const height_bytes = parseInt(cbInfo.substring(0,2), 16)
|
||||
// extract the specified number of bytes, reverse the endianness (reverse pairs of hex characters), parse as a hex string
|
||||
const height = parseInt(cbInfo.substring(2,2 + (height_bytes * 2)).match(/../g).reverse().join(''),16)
|
||||
// save remaining bytes as free data
|
||||
const sig = cbInfo.substring(2 + (height_bytes * 2))
|
||||
const sigAscii = sig.match(/../g).reduce((parsed, hexChar) => {
|
||||
return parsed + String.fromCharCode(parseInt(hexChar, 16))
|
||||
}, "")
|
||||
|
||||
return {
|
||||
height,
|
||||
sig,
|
||||
sigAscii
|
||||
}
|
||||
} else return false
|
||||
this.coinbase = {
|
||||
height,
|
||||
sig,
|
||||
sigAscii,
|
||||
fees: block.fees,
|
||||
subsidy: this.value - (block.fees || 0)
|
||||
}
|
||||
}
|
||||
|
||||
setData ({ version, inflated, id, value, fee, vbytes, inputs, outputs, time, block }) {
|
||||
setData ({ version, inflated, id, value, fee, vbytes, inputs, outputs, time, block }, isCoinbase=false) {
|
||||
this.version = version
|
||||
this.is_inflated = !!inflated
|
||||
this.id = id
|
||||
@ -53,8 +53,8 @@ export default class BitcoinTx {
|
||||
this.highlight = false
|
||||
|
||||
// is a coinbase transaction?
|
||||
this.coinbase = this.isCoinbase(this)
|
||||
if (this.coinbase || !this.is_inflated || (this.fee < 0)) {
|
||||
this.isCoinbase = isCoinbase
|
||||
if (this.isCoinbase || !this.is_inflated || (this.fee < 0)) {
|
||||
this.fee = null
|
||||
this.feerate = null
|
||||
}
|
||||
@ -92,6 +92,10 @@ export default class BitcoinTx {
|
||||
setBlock (block) {
|
||||
this.block = block
|
||||
this.state = this.block ? 'block' : 'pool'
|
||||
if (this.block && this.block.coinbase && this.id == this.block.coinbase.id) {
|
||||
this.isCoinbase = true
|
||||
this.setCoinbaseData(this.block)
|
||||
}
|
||||
}
|
||||
|
||||
onEnterScene () {
|
||||
|
@ -45,6 +45,9 @@ export function SPKToAddress (spk) {
|
||||
const payload = "05" + spk.slice(4, -2)
|
||||
const checksum = hash(hash(payload)).slice(0, 8)
|
||||
return binary_to_base58(hexToUintArray(payload + checksum))
|
||||
} else if (spk.startsWith('6a')) {
|
||||
// OP_RETURN
|
||||
return 'OP_RETURN'
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user