Merge pull request #35 from bitfeed-project/transaction-details

Transaction detail overlay
This commit is contained in:
Mononaut 2022-03-12 00:05:02 +00:00 committed by GitHub
commit eb8323a55d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 613 additions and 30 deletions

View File

@ -1,12 +1,12 @@
{
"name": "bitfeed-client",
"version": "2.1.5",
"version": "2.2.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "bitfeed-client",
"version": "2.1.5",
"version": "2.2.1",
"dependencies": {
"@babel/core": "^7.16.5",
"@babel/preset-env": "^7.16.5",
@ -21,6 +21,7 @@
"d3-color": "^3.0.1",
"d3-interpolate": "^3.0.1",
"dotenv": "^10.0.0",
"hash.js": "^1.1.7",
"locale-currency": "0.0.2",
"qrcode": "^1.5.0",
"rollup": "^2.62.0",
@ -2834,6 +2835,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hash.js": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"dependencies": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@ -3161,6 +3171,11 @@
"node": ">=4"
}
},
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
},
"node_modules/minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@ -6383,6 +6398,15 @@
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz",
"integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw=="
},
"hash.js": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"requires": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
}
},
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@ -6628,6 +6652,11 @@
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="
},
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",

View File

@ -20,6 +20,7 @@
"d3-color": "^3.0.1",
"d3-interpolate": "^3.0.1",
"dotenv": "^10.0.0",
"hash.js": "^1.1.7",
"locale-currency": "0.0.2",
"qrcode": "^1.5.0",
"rollup": "^2.62.0",

View File

@ -0,0 +1,485 @@
<script>
import Overlay from '../components/Overlay.svelte'
import Icon from './Icon.svelte'
import BookmarkIcon from '../assets/icon/cil-bookmark.svg'
import { longBtcFormat, numberFormat, feeRateFormat } from '../utils/format.js'
import { exchangeRates, settings, sidebarToggle, newHighlightQuery, highlightingFull, detailTx, pageWidth } from '../stores.js'
import { formatCurrency } from '../utils/fx.js'
import { hlToHex, mixColor, teal, purple } from '../utils/color.js'
import { SPKToAddress } from '../utils/encodings.js'
function onClose () {
$detailTx = null
}
function formatBTC (sats) {
return `₿ ${(sats/100000000).toFixed(8)}`
}
function highlight (query) {
if (!$highlightingFull && query) {
$newHighlightQuery = query
$sidebarToggle = 'search'
}
}
const rowHeight = 60
let svgWidth = 380
let flowWeight = 60
let triangleWidth = 20
$: {
if ($pageWidth) {
if ($pageWidth < 800) {
svgWidth = Math.max($pageWidth - 420, 120)
flowWeight = 0.1875 * (svgWidth - 60)
} else {
svgWidth = 380
flowWeight = 60
}
if ($pageWidth < 600) {
triangleWidth = 10
} else {
triangleWidth = 20
}
}
}
const midColor = hlToHex(mixColor(teal, purple, 1, 3, 2))
let feeColor
$: {
if ($detailTx && $detailTx.feerate != null) {
feeColor = hlToHex(mixColor(teal, purple, 1, Math.log2(64), Math.log2($detailTx.feerate)))
}
}
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,
title
}
})
}
let inputs = []
let outputs = []
$: {
if ($detailTx && $detailTx.inputs) {
if ($detailTx.isCoinbase) {
inputs = [{
address: 'coinbase',
value: $detailTx.value
}]
} else {
inputs = expandAddresses($detailTx.inputs)
}
} else inputs = []
if ($detailTx && $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 && 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 total = fee + value
const mergeOffset = (totalHeight - flowWeight) / 2
let cumThick = 0
let xOffset = 0
const inLines = inputs.map((input, index) => {
const weight = (input.value / total) * flowWeight
const height = ((index + 0.5) * rowHeight)
const step = (weight / 2)
const line = []
const yOffset = 0.5
line.push({ x: triangleWidth, y: height })
line.push({ x: triangleWidth + (0.25 * svgWidth), y: height })
line.push({ x: 0.375 * svgWidth, y: mergeOffset + cumThick + step + yOffset })
line.push({ x: (0.5 * svgWidth) + 1, y: mergeOffset + cumThick + step + yOffset })
const dy = line[1].y - line[2].y
const dx = line[2].x - line[1].x
const miterOffset = getMiterOffset(weight, dy, dx)
xOffset -= miterOffset
line[1].x -= xOffset
line[2].x -= xOffset
xOffset -= miterOffset
// inLines.push({ line, weight })
// inLines.push({ line: [{x: line[1].x + miterOffset, y: line[1].y - (weight / 2)}, {x: line[2].x + miterOffset, y: line[2].y - (weight / 2)}], weight: 1})
cumThick += weight
return { line, weight, index, total: inputs.length, in: true }
})
inLines.forEach(line => {
line.line[1].x += xOffset
line.line[2].x += xOffset
})
cumThick = 0
xOffset = 0
const outLines = outputs.map((output, index) => {
const weight = (output.value / total) * flowWeight
const height = ((index + 0.5) * rowHeight)
const step = (weight / 2)
const line = []
const yOffset = 0.5
line.push({ x: (0.5 * svgWidth) - 1, y: mergeOffset + cumThick + step + yOffset })
line.push({ x: 0.625 * svgWidth, y: mergeOffset + cumThick + step + yOffset })
line.push({ x: svgWidth - triangleWidth - (0.25 * svgWidth), y: height })
line.push({ x: svgWidth - triangleWidth, y: height })
const dy = line[2].y - line[1].y
const dx = line[2].x - line[1].x
const miterOffset = getMiterOffset(weight, dy, dx)
xOffset -= miterOffset
line[1].x += xOffset
line[2].x += xOffset
xOffset -= miterOffset
// outLines.push({ line, weight })
// outLines.push({ line: [{x: line[1].x + miterOffset, y: line[1].y - (weight / 2)}, {x: line[2].x + miterOffset, y: line[2].y - (weight / 2)}], weight: 1})
cumThick += weight
return { line, weight, index, total: outputs.length }
})
outLines.forEach(line => {
line.line[1].x -= xOffset
line.line[2].x -= xOffset
})
return [...inLines, ...outLines].map(line => {
return {
points: line.line.map(point => { return `${point.x},${point.y}`}).join(' '),
weight: line.weight,
index: line.index,
total: line.total,
in: line.in
}
})
}
// given a line weight and corner angle
// return the horizontal distance of a miter from the corner
function getMiterOffset (weight, dy, dx) {
if (dy != 0) {
const angle = Math.atan2(dy, dx)
const u = weight / 2
const a = 0
const b = dy / dx
const c = -u
const d = -u * (Math.cos(angle) + (b * Math.sin(angle)))
return (d - c) / (a - b)
} else return 0
}
</script>
<style type="text/scss">
.tx-detail {
width: 100%;
overflow-x: hidden;
text-align: left;
h2 {
font-size: 1.2em;
word-break: break-word;
}
.tx-id {
font-family: monospace;
font-size: 0.9em;
font-weight: 400;
}
.icon-button {
float: right;
font-size: 24px;
margin: 0;
transition: opacity 300ms, color 300ms, background 300ms;
background: var(--palette-d);
color: var(--bold-a);
cursor: pointer;
padding: 5px;
border-radius: 5px;
&:hover {
background: var(--palette-e);
}
&.disabled {
color: var(--palette-e);
background: none;
}
}
.pane {
background: var(--palette-b);
padding: 16px;
border-radius: .5em;
margin: 0 0 1em;
.field {
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
.label {
font-size: 0.8em;
color: var(--grey);
}
.value.coinbase-sig {
word-break: break-all;
}
}
}
.fee-calc {
align-items: center;
display: flex;
flex-direction: row;
justify-content: center;
width: auto;
color: var(--palette-x);
.operator {
font-size: 2em;
color: var(--grey);
margin: 0 1em;
}
}
.flow-diagram {
display: grid;
grid-template-columns: minmax(0px, 1fr) 380px minmax(0px, 1fr);
.header {
height: 60px;
font-size: 1.1em;
font-weight: 800;
margin: 0;
text-align: center;
}
.column {
width: 100%;
margin: 0;
.entry {
height: 60px;
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: center;
border-top: solid 1px var(--grey);
box-sizing: border-box;
text-align: right;
width: 100%;
overflow: hidden;
&:last-child {
border-bottom: solid 1px var(--grey);
}
.amount, .address {
margin: 0;
font-family: monospace;
font-size: 1.1em;
}
.amount {
white-space: nowrap;
}
.address {
width: 100%;
text-align: right;
span {
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
}
.truncatable {
max-width: calc(100% - 4em);
}
}
}
&.inputs {
.entry {
align-items: flex-start;
.address {
text-align: left;
}
}
}
}
.sankey {
margin-top: 60px;
polyline {
fill: none;
stroke-linecap: butt;
stroke-linejoin: miter;
}
}
}
@media (max-width: 679px) {
.fee-calc {
flex-direction: column;
}
}
@media (min-width: 411px) and (max-width: 479px) {
.flow-diagram {
font-size: 0.7em;
}
}
@media (max-width: 410px) {
.flow-diagram {
display: block;
.column {
width: 100%;
margin: 30px 0;
}
}
}
}
</style>
<Overlay name="tx" on:close={onClose}>
{#if $detailTx}
<section class="tx-detail">
<div class="icon-button" class:disabled={$highlightingFull} on:click={() => highlight($detailTx.id)} title="Add transaction to watchlist">
<Icon icon={BookmarkIcon}/>
</div>
{#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>
<div class="pane fee-calc">
<div class="field">
<span class="label">coinbase</span>
<span class="value coinbase-sig">{ $detailTx.coinbase.sigAscii }</span>
</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 &amp; Outputs</h2>
<div class="pane flow-diagram" style="grid-template-columns: minmax(0px, 1fr) {svgWidth}px minmax(0px, 1fr);">
<div class="column inputs">
<p class="header">{$detailTx.inputs.length} input{$detailTx.inputs.length > 1 ? 's' : ''}</p>
{#each inputs as input}
<div class="entry">
<p class="address" title={input.address}><span class="truncatable">{input.address.slice(0,-6)}</span><span class="suffix">{input.address.slice(-6)}</span></p>
<p class="amount">{ formatBTC(input.value) }</p>
</div>
{/each}
</div>
<div class="column diagram">
{#if sankeyLines && $pageWidth > 410}
<svg class="sankey" height="{sankeyHeight}px" width="{svgWidth}px">
<defs>
{#each sankeyLines as line, index}
<linearGradient id="lg{index}" x1="0%" y1="0%" x2="100%" y2="0%">
{#if line.in}
<stop offset="0%" stop-color={hlToHex(mixColor(teal, purple, 0, Math.max(1,line.total-1), line.index))}/>
<stop offset="100%" stop-color={midColor}/>
{:else}
<stop offset="0%" stop-color={midColor}/>
<stop offset="100%" stop-color={hlToHex(mixColor(purple, teal, 0, Math.max(1,line.total-1), line.index))}/>
{/if}
</linearGradient>
{/each}
</defs>
{#each sankeyLines as line, index }
<polyline points="{line.points}" stroke="url(#lg{index})" style="stroke-width: {line.weight + 1}px;" />
{#if line.in}
<polyline points="0,{line.index * 60} {triangleWidth},{(line.index * 60 )+ 30} 0,{(line.index * 60) + 60}" stroke="var(--grey)" style="stroke-width: 1px;" />
{:else}
<polyline points="{svgWidth},{line.index * 60} {svgWidth - triangleWidth},{(line.index * 60 )+ 30} {svgWidth},{(line.index * 60) + 60}" stroke="var(--grey)" style="stroke-width: 1px;" />
{/if}
{/each}
</svg>
{/if}
</div>
<div class="column outputs">
<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.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}
</div>
</div>
</section>
{/if}
</Overlay>

View File

@ -3,10 +3,11 @@
import TxController from '../controllers/TxController.js'
import TxRender from './TxRender.svelte'
import getTxStream from '../controllers/TxStream.js'
import { settings, overlay, serverConnected, serverDelay, txCount, mempoolCount, mempoolScreenHeight, frameRate, avgFrameRate, blockVisible, tinyScreen, currentBlock, selectedTx, blockAreaSize, devEvents, devSettings } from '../stores.js'
import { settings, overlay, serverConnected, serverDelay, txCount, mempoolCount, mempoolScreenHeight, frameRate, avgFrameRate, blockVisible, tinyScreen, currentBlock, selectedTx, blockAreaSize, devEvents, devSettings, pageWidth } from '../stores.js'
import BlockInfo from '../components/BlockInfo.svelte'
import TxInfo from '../components/TxInfo.svelte'
import Sidebar from '../components/Sidebar.svelte'
import TransactionOverlay from '../components/TransactionOverlay.svelte'
import AboutOverlay from '../components/AboutOverlay.svelte'
import DonationOverlay from '../components/DonationOverlay.svelte'
import SupportersOverlay from '../components/SupportersOverlay.svelte'
@ -77,6 +78,7 @@
})
function resize () {
$pageWidth = window.innerWidth
if (width !== window.innerWidth - 20 || height !== window.innerHeight - 20) {
// don't force resize unless the viewport has actually changed
width = window.innerWidth - 20
@ -477,6 +479,7 @@
<Sidebar />
<TransactionOverlay />
<AboutOverlay />
{#if config.donationsEnabled }
<DonationOverlay />

View File

@ -5,7 +5,7 @@ import BitcoinTx from '../models/BitcoinTx.js'
import BitcoinBlock from '../models/BitcoinBlock.js'
import TxSprite from '../models/TxSprite.js'
import { FastVertexArray } from '../utils/memory.js'
import { txCount, mempoolCount, mempoolScreenHeight, blockVisible, currentBlock, selectedTx, blockAreaSize, highlight, colorMode } from '../stores.js'
import { overlay, txCount, mempoolCount, mempoolScreenHeight, blockVisible, currentBlock, selectedTx, detailTx, blockAreaSize, highlight, colorMode } from '../stores.js'
import config from "../config.js"
export default class TxController {
@ -29,6 +29,9 @@ export default class TxController {
this.lastTxTime = 0
this.txDelay = 0
detailTx.subscribe(tx => {
this.onDetailTxChanged(tx)
})
highlight.subscribe(criteria => {
this.highlightCriteria = criteria
this.applyHighlighting()
@ -202,7 +205,24 @@ export default class TxController {
}
this.selectedTx = selected
selectedTx.set(this.selectedTx)
if (sameTx && this.selectedTx) {
detailTx.set(this.selectedTx)
overlay.set('tx')
}
this.selectionLocked = !!this.selectedTx && !(this.selectionLocked && sameTx)
}
}
onDetailTxChanged (tx) {
if (!tx) {
if (this.selectedTx) {
this.selectedTx.hoverOff()
this.selectedTx = null
}
selectedTx.set(null)
this.selectionLocked = false
}
selectedTx.set(null)
}
}

View File

@ -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)

View File

@ -3,15 +3,14 @@ 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
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
@ -22,15 +21,16 @@ export default class BitcoinTx {
return parsed + String.fromCharCode(parseInt(hexChar, 16))
}, "")
return {
this.coinbase = {
height,
sig,
sigAscii
sigAscii,
fees: block.fees,
subsidy: this.value - (block.fees || 0)
}
} else return false
}
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 () {

View File

@ -92,6 +92,7 @@ export const avgFrameRate = writable(null)
export const blockVisible = writable(false)
export const currentBlock = writable(null)
export const selectedTx = writable(null)
export const detailTx = writable(null)
export const blockAreaSize = writable(0)
export const settingsOpen = writable(false)
@ -149,3 +150,5 @@ export const highlightingFull = writable(false)
const aspectRatio = window.innerWidth / window.innerHeight
let isTinyScreen = (window.innerWidth < 480 && window.innerHeight < 480)
export const tinyScreen = writable(isTinyScreen)
export const pageWidth = writable(window.innerWidth)

View File

@ -1,7 +1,8 @@
import { Buffer } from 'buffer/'
window.Buffer = Buffer
import bech32 from 'bech32-buffer'
import { base58_to_binary } from 'base58-js'
import { base58_to_binary, binary_to_base58 } from 'base58-js'
import { sha256 } from 'hash.js'
// Extract a raw script hash from an address
export function addressToSPK (address) {
@ -25,3 +26,39 @@ export function addressToSPK (address) {
return prefix + Buffer.from(result).toString('hex').slice(2, -8) + postfix
}
}
// Extract an address from a raw scriptpubkey
export function SPKToAddress (spk) {
if (spk.startsWith('5120')) {
// taproot
return (new bech32.BitcoinAddress('bc', 1, hexToUintArray(spk.slice(4)))).encode()
} else if (spk.startsWith('0020') || spk.startsWith('0014')) {
// p2wsh or p2wpkh
return (new bech32.BitcoinAddress('bc', 0, hexToUintArray(spk.slice(4)))).encode()
} else if (spk.startsWith('76a914')) {
// p2pkh
const payload = "00" + spk.slice(6, -4)
const checksum = hash(hash(payload)).slice(0, 8)
return binary_to_base58(hexToUintArray(payload + checksum))
} else if (spk.startsWith('a914')) {
// p2sh
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'
}
}
function hexToUintArray(hex) {
let a = new Uint8Array(hex.length / 2)
for (let i = 0; i < a.length; i++) {
a[i] = parseInt(hex.substr(2 * i, 2), 16)
}
return a
}
function hash (hex) {
return sha256().update(hexToUintArray(hex)).digest('hex')
}