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