mirror of
https://github.com/Retropex/bitfeed.git
synced 2025-05-12 19:20:46 +02:00
Merge pull request #43 from bitfeed-project/block-explorer
Block explorer
This commit is contained in:
commit
6fb1bed616
@ -4,6 +4,8 @@ map $sent_http_content_type $expires {
|
||||
application/javascript max;
|
||||
}
|
||||
|
||||
proxy_cache_path /var/cache/nginx/bitfeed levels=1:2 keys_zone=bitfeed:10m max_size=500m inactive=1w use_temp_path=off;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
@ -22,6 +24,11 @@ server {
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_cache bitfeed;
|
||||
proxy_cache_revalidate on;
|
||||
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
||||
proxy_cache_background_update on;
|
||||
proxy_cache_lock on;
|
||||
proxy_pass http://wsmonobackend;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
@ -5,9 +5,10 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Icon from '../components/Icon.svelte'
|
||||
import closeIcon from '../assets/icon/cil-x-circle.svg'
|
||||
import { shortBtcFormat, longBtcFormat, timeFormat, numberFormat } from '../utils/format.js'
|
||||
import { exchangeRates, settings, blocksEnabled } from '../stores.js'
|
||||
import { shortBtcFormat, longBtcFormat, dateFormat, numberFormat } from '../utils/format.js'
|
||||
import { exchangeRates, settings, blocksEnabled, latestBlockHeight, blockTransitionDirection, loading } from '../stores.js'
|
||||
import { formatCurrency } from '../utils/fx.js'
|
||||
import { searchBlockHeight } from '../utils/search.js'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@ -47,8 +48,37 @@
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime (time) {
|
||||
return timeFormat.format(time)
|
||||
let hasPrevBlock
|
||||
let hasNextBlock
|
||||
$: {
|
||||
if (block) {
|
||||
if (block.height > 0) hasPrevBlock = true
|
||||
else hasPrevBlock = false
|
||||
if (block.height < $latestBlockHeight) hasNextBlock = true
|
||||
else hasNextBlock = false
|
||||
} else {
|
||||
hasPrevBlock = false
|
||||
hasNextBlock = false
|
||||
}
|
||||
}
|
||||
|
||||
let flyIn
|
||||
let flyOut
|
||||
$: {
|
||||
if ($blockTransitionDirection && $blockTransitionDirection === 'right') {
|
||||
flyIn = { x: 100, easing: linear, delay: 1000, duration: 1000 }
|
||||
flyOut = { x: -100, easing: linear, delay: 0, duration: 1000 }
|
||||
} else if ($blockTransitionDirection && $blockTransitionDirection === 'left') {
|
||||
flyIn = { x: -100, easing: linear, delay: 1000, duration: 1000 }
|
||||
flyOut = { x: 100, easing: linear, delay: 0, duration: 1000 }
|
||||
} else {
|
||||
flyIn = { y: (restoring ? -50 : 50), duration: (restoring ? 500 : 1000), easing: linear, delay: (restoring ? 0 : newBlockDelay) }
|
||||
flyOut = { y: -50, duration: 2000, easing: linear }
|
||||
}
|
||||
}
|
||||
|
||||
function formatDateTime (time) {
|
||||
return dateFormat.format(time)
|
||||
}
|
||||
|
||||
function formatBTC (sats) {
|
||||
@ -74,8 +104,34 @@
|
||||
}
|
||||
|
||||
function hideBlock () {
|
||||
analytics.trackEvent('viz', 'block', 'hide')
|
||||
dispatch('hideBlock')
|
||||
if (block && block.height != $latestBlockHeight) {
|
||||
dispatch('quitExploring')
|
||||
} else {
|
||||
analytics.trackEvent('viz', 'block', 'hide')
|
||||
dispatch('hideBlock')
|
||||
}
|
||||
}
|
||||
|
||||
async function explorePrevBlock (e) {
|
||||
e.preventDefault()
|
||||
if (!$loading && block) {
|
||||
$loading = true
|
||||
await searchBlockHeight(block.height - 1)
|
||||
$loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function exploreNextBlock (e) {
|
||||
e.preventDefault()
|
||||
if (!$loading && block) {
|
||||
if (block.height + 1 < $latestBlockHeight) {
|
||||
$loading = true
|
||||
await searchBlockHeight(block.height + 1)
|
||||
$loading = false
|
||||
} else {
|
||||
dispatch('quitExploring')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -172,6 +228,42 @@
|
||||
}
|
||||
}
|
||||
|
||||
.explore-button {
|
||||
position: absolute;
|
||||
bottom: 10%;
|
||||
padding: .75em;
|
||||
pointer-events: all;
|
||||
|
||||
&.prev {
|
||||
right: 100%
|
||||
}
|
||||
&.next {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
.outline {
|
||||
stroke: white;
|
||||
stroke-width: 32;
|
||||
stroke-linecap: butt;
|
||||
stroke-linejoin: miter;
|
||||
fill: white;
|
||||
fill-opacity: 0;
|
||||
transition: fill-opacity 300ms;
|
||||
}
|
||||
|
||||
&.right {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.chevron .outline {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-aspect-ratio: 1/1) {
|
||||
.block-info {
|
||||
bottom: unset;
|
||||
@ -227,17 +319,17 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
{#each [block] as block (block)}
|
||||
{#key block}
|
||||
{#if block != null && visible && $blocksEnabled }
|
||||
<div class="block-info" out:fly="{{ y: -50, duration: 2000, easing: linear }}" in:fly="{{ y: (restoring ? -50 : 50), duration: (restoring ? 500 : 1000), easing: linear, delay: (restoring ? 0 : newBlockDelay) }}">
|
||||
<div class="block-info" out:fly={flyOut} in:fly={flyIn}>
|
||||
<!-- <span class="data-field">Hash: { block.id }</span> -->
|
||||
<div class="full-size">
|
||||
<div class="data-row">
|
||||
<span class="data-field title-field" title="{block.miner_sig}"><b>Latest Block: </b>{ numberFormat.format(block.height) }</span>
|
||||
<span class="data-field title-field" title="{block.miner_sig}"><b>{#if block.height == $latestBlockHeight}Latest {/if}Block: </b>{ numberFormat.format(block.height) }</span>
|
||||
<button class="data-field close-button" on:click={hideBlock}><Icon icon={closeIcon} color="var(--palette-x)" /></button>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-field">Mined { formatTime(block.time) }</span>
|
||||
<span class="data-field" title="block timestamp">{ formatDateTime(block.time) }</span>
|
||||
<span class="data-field">{ formattedBlockValue }</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
@ -260,7 +352,7 @@
|
||||
<button class="data-field close-button" on:click={hideBlock}><Icon icon={closeIcon} color="var(--palette-x)" /></button>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
<span class="data-field">Mined { formatTime(block.time) }</span>
|
||||
<span class="data-field">{ formatDateTime(block.time) }</span>
|
||||
<span class="data-field">{ formattedBlockValue }</span>
|
||||
</div>
|
||||
<div class="data-row">
|
||||
@ -273,8 +365,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="close-button standalone" on:click={hideBlock} out:fly="{{ y: -50, duration: 2000, easing: linear }}" in:fly="{{ y: (restoring ? -50 : 50), duration: (restoring ? 500 : 1000), easing: linear, delay: (restoring ? 0 : newBlockDelay) }}" >
|
||||
{#if hasPrevBlock }
|
||||
<a href="/block/height/{block.height - 1}" on:click={explorePrevBlock} class="explore-button prev" out:fly={flyOut} in:fly={flyIn}>
|
||||
<svg class="chevron left" height="1.5em" width="1.5em" viewBox="0 0 512 512">
|
||||
<path d="M 107.628,257.54 327.095,38.078 404,114.989 261.506,257.483 404,399.978 327.086,476.89 Z" class="outline" />
|
||||
</svg>
|
||||
</a>
|
||||
{/if}
|
||||
{#if hasNextBlock }
|
||||
<a href="/block/height/{block.height + 1}" on:click={exploreNextBlock} class="explore-button next" out:fly={flyOut} in:fly={flyIn}>
|
||||
<svg class="chevron right" height="1.5em" width="1.5em" viewBox="0 0 512 512">
|
||||
<path d="M 107.628,257.54 327.095,38.078 404,114.989 261.506,257.483 404,399.978 327.086,476.89 Z" class="outline" />
|
||||
</svg>
|
||||
</a>
|
||||
{/if}
|
||||
<button class="close-button standalone" on:click={hideBlock} out:fly={flyOut} in:fly={flyIn} >
|
||||
<Icon icon={closeIcon} color="var(--palette-x)" />
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
{/key}
|
||||
|
@ -5,10 +5,11 @@ import { numberFormat } from '../utils/format.js'
|
||||
import { logTxSize, byteTxSize } from '../utils/misc.js'
|
||||
import { interpolateHcl } from 'd3-interpolate'
|
||||
import { color, hcl } from 'd3-color'
|
||||
import { hlToHex, orange, teal, green, purple } from '../utils/color.js'
|
||||
import { hlToHex, orange, teal, blue, green, purple } from '../utils/color.js'
|
||||
|
||||
const orangeHex = hlToHex(orange)
|
||||
const tealHex = hlToHex(teal)
|
||||
const blueHex = hlToHex(blue)
|
||||
const greenHex = hlToHex(green)
|
||||
const purpleHex = hlToHex(purple)
|
||||
|
||||
@ -42,7 +43,7 @@ resize()
|
||||
|
||||
onMount(() => {
|
||||
resize()
|
||||
colorScale = generateColorScale(orangeHex, tealHex)
|
||||
colorScale = generateColorScale(orangeHex, blueHex)
|
||||
feeColorScale = generateColorScale(tealHex, purpleHex)
|
||||
})
|
||||
|
||||
@ -207,7 +208,7 @@ function generateColorScale (colorA, colorB) {
|
||||
{:else}
|
||||
<span class="value left">1</span>
|
||||
<img src={feeColorScale} alt="" class="color-scale-img" width="200" height="15">
|
||||
<span class="value right">64+</span>
|
||||
<span class="value right">128+</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,7 +9,7 @@ import AddressIcon from '../assets/icon/cil-wallet.svg'
|
||||
import TxIcon from '../assets/icon/cil-arrow-circle-right.svg'
|
||||
import BlockIcon from '../assets/icon/grid-icon.svg'
|
||||
import { fly } from 'svelte/transition'
|
||||
import { matchQuery, searchTx, searchBlock } from '../utils/search.js'
|
||||
import { matchQuery, searchTx, searchBlockHeight, searchBlockHash } from '../utils/search.js'
|
||||
import { selectedTx, detailTx, overlay, loading } from '../stores.js'
|
||||
|
||||
const queryIcons = {
|
||||
@ -68,8 +68,17 @@ async function searchSubmit (e) {
|
||||
case 'output':
|
||||
searchErr = await searchTx(matchedQuery.txid, null, matchedQuery.output)
|
||||
break;
|
||||
|
||||
case 'blockheight':
|
||||
searchErr = await searchBlockHeight(matchedQuery.height)
|
||||
break;
|
||||
|
||||
case 'blockhash':
|
||||
searchErr = await searchBlockHash(matchedQuery.hash)
|
||||
break;
|
||||
}
|
||||
if (searchErr != null) handleSearchError(searchErr)
|
||||
if (searchErr == null) errorMessage = null
|
||||
else handleSearchError(searchErr)
|
||||
$loading--
|
||||
} else {
|
||||
errorMessage = 'enter a transaction id, block hash or block height'
|
||||
|
@ -7,7 +7,9 @@ import { exchangeRates, settings, sidebarToggle, newHighlightQuery, highlighting
|
||||
import { formatCurrency } from '../utils/fx.js'
|
||||
import { hlToHex, mixColor, teal, purple } from '../utils/color.js'
|
||||
import { SPKToAddress } from '../utils/encodings.js'
|
||||
import api from '../utils/api.js'
|
||||
import { searchTx } from '../utils/search.js'
|
||||
import { fade } from 'svelte/transition'
|
||||
|
||||
function onClose () {
|
||||
$detailTx = null
|
||||
@ -47,7 +49,7 @@ $: {
|
||||
|
||||
let confirmations = 0
|
||||
$: {
|
||||
if ($detailTx && $detailTx.block && $detailTx.block.height && $latestBlockHeight != null) {
|
||||
if ($detailTx && $detailTx.block && $detailTx.block.height != null && $latestBlockHeight != null) {
|
||||
confirmations = (1 + $latestBlockHeight - $detailTx.block.height)
|
||||
}
|
||||
}
|
||||
@ -129,8 +131,8 @@ let highlight = {}
|
||||
$: {
|
||||
if ($highlightInOut && $detailTx && $highlightInOut.txid === $detailTx.id) {
|
||||
highlight = {}
|
||||
if ($highlightInOut.input != null) highlight.in = $highlightInOut.input
|
||||
if ($highlightInOut.output != null) highlight.out = $highlightInOut.output
|
||||
if ($highlightInOut.input != null) highlight.in = parseInt($highlightInOut.input)
|
||||
if ($highlightInOut.output != null) highlight.out = parseInt($highlightInOut.output)
|
||||
} else {
|
||||
highlight = {}
|
||||
}
|
||||
@ -257,15 +259,29 @@ async function clickItem (item) {
|
||||
if (item.rest) {
|
||||
truncate = false
|
||||
} else if (item.prev_txid && item.prev_vout != null) {
|
||||
$loading++
|
||||
await searchTx(item.prev_txid, null, item.prev_vout)
|
||||
$loading--
|
||||
} else if (item.spend && item.spend.txid && item.spend.vin) {
|
||||
$loading++
|
||||
await searchTx(item.spend.txid, item.spend.vin)
|
||||
$loading--
|
||||
// $loading++
|
||||
// await searchTx(item.prev_txid, null, item.prev_vout)
|
||||
// $loading--
|
||||
} else if (item.spend) {
|
||||
// $loading++
|
||||
// await searchTx(item.spend.txid, item.spend.vin)
|
||||
// $loading--
|
||||
}
|
||||
}
|
||||
|
||||
async function goToInput(e, input) {
|
||||
e.preventDefault()
|
||||
$loading++
|
||||
await searchTx(input.prev_txid, null, input.prev_vout)
|
||||
$loading--
|
||||
}
|
||||
|
||||
async function goToOutput(e, output) {
|
||||
e.preventDefault()
|
||||
$loading++
|
||||
await searchTx(output.spend.txid, output.spend.vin)
|
||||
$loading--
|
||||
}
|
||||
</script>
|
||||
|
||||
<style type="text/scss">
|
||||
@ -380,6 +396,7 @@ async function clickItem (item) {
|
||||
margin: 0;
|
||||
|
||||
.entry {
|
||||
position: relative;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -390,7 +407,6 @@ async function clickItem (item) {
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
transition: background 300ms;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: solid 1px var(--grey);
|
||||
@ -420,12 +436,44 @@ async function clickItem (item) {
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.put-link {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1.5em;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.chevron {
|
||||
.outline {
|
||||
stroke: white;
|
||||
stroke-width: 32;
|
||||
stroke-linecap: butt;
|
||||
stroke-linejoin: miter;
|
||||
fill: white;
|
||||
fill-opacity: 0;
|
||||
transition: fill-opacity 300ms;
|
||||
}
|
||||
|
||||
&.right {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.chevron .outline {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.inputs {
|
||||
.entry {
|
||||
align-items: flex-start;
|
||||
padding-left: 10px;
|
||||
padding-left: 1.5em;
|
||||
&.highlight {
|
||||
background: linear-gradient(90deg, var(--bold-a) -100%, transparent 100%);
|
||||
}
|
||||
@ -435,22 +483,27 @@ async function clickItem (item) {
|
||||
.address {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.put-link {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.outputs {
|
||||
.entry {
|
||||
padding-right: 10px;
|
||||
padding-right: 1.5em;
|
||||
border-right: solid 1px transparent;
|
||||
&.unspent {
|
||||
border-right: solid 1px var(--grey);
|
||||
}
|
||||
&.highlight {
|
||||
background: linear-gradient(90deg, transparent 0%, var(--bold-a) 200%);
|
||||
}
|
||||
&.clickable:hover {
|
||||
background: linear-gradient(-90deg, var(--palette-e), transparent);
|
||||
}
|
||||
|
||||
.put-link {
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -582,8 +635,15 @@ async function clickItem (item) {
|
||||
<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 clickable" on:click={() => clickItem(input)}>
|
||||
{#each inputs as input, index}
|
||||
<div class="entry" class:clickable={input.rest} class:highlight={highlight.in != null && highlight.in === index} on:click={() => clickItem(input)}>
|
||||
{#if input.prev_txid }
|
||||
<a href="/tx/{input.prev_txid}:{input.prev_vout}" on:click={(e) => goToInput(e, input)} class="put-link" transition:fade|local={{ duration: 200 }}>
|
||||
<svg class="chevron left" height="1.2em" width="1.2em" viewBox="0 0 512 512">
|
||||
<path d="M 107.628,257.54 327.095,38.078 404,114.989 261.506,257.483 404,399.978 327.086,476.89 Z" class="outline" />
|
||||
</svg>
|
||||
</a>
|
||||
{/if}
|
||||
<p class="address" title={input.title || input.address}><span class="truncatable">{input.address.slice(0,-6)}</span><span class="suffix">{input.address.slice(-6)}</span></p>
|
||||
<p class="amount">{ input.value == null ? '???' : formatBTC(input.value) }</p>
|
||||
</div>
|
||||
@ -619,7 +679,14 @@ async function clickItem (item) {
|
||||
<div class="column outputs">
|
||||
<p class="header">{$detailTx.outputs.length} output{$detailTx.outputs.length > 1 ? 's' : ''} {#if $detailTx.fee}+ fee{/if}</p>
|
||||
{#each outputs as output}
|
||||
<div class="entry" class:clickable={output.rest || output.spend} class:unspent={!output.spend && !output.fee} class:highlight={highlight.out != null && highlight.out === output.index} on:click={() => clickItem(output)}>
|
||||
<div class="entry" class:clickable={output.rest} class:highlight={highlight.out != null && highlight.out === output.index} on:click={() => clickItem(output)}>
|
||||
{#if output.spend}
|
||||
<a href="/tx/{output.spend.vin}:{output.spend.txid}" on:click={(e) => goToOutput(e, output)} class="put-link" transition:fade|local={{ duration: 200 }}>
|
||||
<svg class="chevron right" height="1.2em" width="1.2em" viewBox="0 0 512 512">
|
||||
<path d="M 107.628,257.54 327.095,38.078 404,114.989 261.506,257.483 404,399.978 327.086,476.89 Z" class="outline" />
|
||||
</svg>
|
||||
</a>
|
||||
{/if}
|
||||
<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">{ output.value == null ? '???' : formatBTC(output.value) }</p>
|
||||
</div>
|
||||
|
@ -31,6 +31,17 @@ $: {
|
||||
}
|
||||
}
|
||||
|
||||
let inputCount
|
||||
let outputCount
|
||||
$: {
|
||||
if (tx) {
|
||||
if (tx.inputs) inputCount = tx.inputs.length
|
||||
else inputCount = tx.numInputs || 0
|
||||
if (tx.outputs) outputCount = tx.outputs.length
|
||||
else outputCount = tx.numOutputs || 0
|
||||
}
|
||||
}
|
||||
|
||||
function formatBTC (sats) {
|
||||
return `₿ ${longBtcFormat.format(sats/100000000)}`
|
||||
}
|
||||
@ -137,14 +148,18 @@ function highlight () {
|
||||
<p class="field hash">
|
||||
TxID: { tx.id }
|
||||
</p>
|
||||
{#if tx.inputs && tx.outputs && !tx.coinbase }
|
||||
{#if inputCount && outputCount && !tx.coinbase }
|
||||
<p class="field inputs">
|
||||
<span>{ tx.inputs.length } input{#if tx.inputs.length != 1}s{/if}</span>
|
||||
<span>{ inputCount } input{#if inputCount != 1}s{/if}</span>
|
||||
<span class="arrow"> ⟶ </span>
|
||||
<span>{ tx.outputs.length } output{#if tx.outputs.length != 1}s{/if}</span>
|
||||
<span>{ outputCount } output{#if outputCount != 1}s{/if}</span>
|
||||
</p>
|
||||
{:else if tx.coinbase }
|
||||
<p class="field coinbase">Coinbase: { tx.coinbase.sigAscii }</p>
|
||||
{#if tx.coinbase.sigAscii }
|
||||
<p class="field coinbase">Coinbase: { tx.coinbase.sigAscii }</p>
|
||||
{:else}
|
||||
<p class="field coinbase">Coinbase</p>
|
||||
{/if}
|
||||
<p class="field inputs">{ tx.outputs.length } output{#if tx.outputs.length != 1}s{/if}</p>
|
||||
{/if}
|
||||
<p class="field vbytes">Size: { numberFormat.format(tx.vbytes) } vbytes</p>
|
||||
|
@ -5,8 +5,8 @@
|
||||
import getTxStream from '../controllers/TxStream.js'
|
||||
import { settings, overlay, serverConnected, serverDelay, txCount, mempoolCount,
|
||||
mempoolScreenHeight, frameRate, avgFrameRate, blockVisible, tinyScreen,
|
||||
compactScreen, currentBlock, selectedTx, blockAreaSize, devEvents,
|
||||
devSettings, pageWidth, pageHeight, loading } from '../stores.js'
|
||||
compactScreen, currentBlock, latestBlockHeight, selectedTx, blockAreaSize,
|
||||
devEvents, devSettings, pageWidth, pageHeight, loading } from '../stores.js'
|
||||
import BlockInfo from '../components/BlockInfo.svelte'
|
||||
import SearchBar from '../components/SearchBar.svelte'
|
||||
import TxInfo from '../components/TxInfo.svelte'
|
||||
@ -110,6 +110,10 @@
|
||||
$blockVisible = false
|
||||
}
|
||||
|
||||
function quitExploring () {
|
||||
if (txController) txController.resumeLatest()
|
||||
}
|
||||
|
||||
function fakeBlock () {
|
||||
const block = txController.simulateBlock()
|
||||
// txController.addBlock(new BitcoinBlock({
|
||||
@ -467,6 +471,13 @@
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.alert-bar-wrapper {
|
||||
font-size: 0.8em;
|
||||
width: 18em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<svelte:window on:resize={resize} on:load={resize} on:click={pointerLeave} />
|
||||
@ -492,7 +503,7 @@
|
||||
<div class="spacer" style="flex: {$pageWidth <= 640 ? '1.5' : '1'}"></div>
|
||||
<div class="block-area-outer" style="width: {$blockAreaSize}px; height: {$blockAreaSize}px">
|
||||
<div class="block-area">
|
||||
<BlockInfo block={$currentBlock} visible={$blockVisible && !$tinyScreen} on:hideBlock={hideBlock} />
|
||||
<BlockInfo block={$currentBlock} visible={$blockVisible && !$tinyScreen} on:hideBlock={hideBlock} on:quitExploring={quitExploring} />
|
||||
</div>
|
||||
{#if config.dev && config.debug && $devSettings.guides }
|
||||
<div class="guide-area" />
|
||||
|
@ -164,10 +164,9 @@ function rotateAlerts () {
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
height: 3em;
|
||||
font-size: 0.8em;
|
||||
width: 16em;
|
||||
width: 18em;
|
||||
.alert-wrapper {
|
||||
width: 16em;
|
||||
width: 18em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,8 @@ 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 { overlay, txCount, mempoolCount, mempoolScreenHeight, blockVisible, currentBlock, selectedTx, detailTx, blockAreaSize, highlight, colorMode, blocksEnabled, latestBlockHeight } from '../stores.js'
|
||||
import { searchTx, fetchSpends, addSpends } from '../utils/search.js'
|
||||
import { overlay, txCount, mempoolCount, mempoolScreenHeight, blockVisible, currentBlock, selectedTx, detailTx, blockAreaSize, highlight, colorMode, blocksEnabled, latestBlockHeight, explorerBlockData, blockTransitionDirection, loading } from '../stores.js'
|
||||
import config from "../config.js"
|
||||
|
||||
export default class TxController {
|
||||
@ -18,6 +19,9 @@ export default class TxController {
|
||||
this.blockAreaSize = (width <= 620) ? Math.min(window.innerWidth * 0.7, window.innerHeight / 2.75) : Math.min(window.innerWidth * 0.75, window.innerHeight / 2.5)
|
||||
blockAreaSize.set(this.blockAreaSize)
|
||||
this.blockScene = null
|
||||
this.block = null
|
||||
this.explorerBlockScene = null
|
||||
this.explorerBlock = null
|
||||
this.clearBlockTimeout = null
|
||||
this.txDelay = 0 //config.txDelay
|
||||
this.maxTxDelay = config.txDelay
|
||||
@ -43,6 +47,13 @@ export default class TxController {
|
||||
colorMode.subscribe(mode => {
|
||||
this.setColorMode(mode)
|
||||
})
|
||||
explorerBlockData.subscribe(blockData => {
|
||||
if (blockData) {
|
||||
this.exploreBlock(blockData)
|
||||
} else {
|
||||
this.resumeLatest()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getVertexData () {
|
||||
@ -54,7 +65,8 @@ export default class TxController {
|
||||
}
|
||||
|
||||
getScenes () {
|
||||
if (this.blockScene) return [this.poolScene, this.blockScene]
|
||||
if (this.blockScene && this.explorerBlockScene) return [this.poolScene, this.blockScene, this.explorerBlockScene]
|
||||
else if (this.blockScene) return [this.poolScene, this.blockScene]
|
||||
else return [this.poolScene]
|
||||
}
|
||||
|
||||
@ -63,6 +75,9 @@ export default class TxController {
|
||||
if (this.blockScene) {
|
||||
this.blockScene.layoutAll({ width: this.blockAreaSize, height: this.blockAreaSize })
|
||||
}
|
||||
if (this.explorerBlockScene) {
|
||||
this.explorerBlockScene.layoutAll({ width: this.blockAreaSize, height: this.blockAreaSize })
|
||||
}
|
||||
}
|
||||
|
||||
resize ({ width, height }) {
|
||||
@ -77,6 +92,9 @@ export default class TxController {
|
||||
if (this.blockScene) {
|
||||
this.blockScene.setColorMode(mode)
|
||||
}
|
||||
if (this.explorerBlockScene) {
|
||||
this.explorerBlockScene.setColorMode(mode)
|
||||
}
|
||||
}
|
||||
|
||||
applyHighlighting () {
|
||||
@ -84,6 +102,9 @@ export default class TxController {
|
||||
if (this.blockScene) {
|
||||
this.blockScene.applyHighlighting(this.highlightCriteria)
|
||||
}
|
||||
if (this.explorerBlockScene) {
|
||||
this.explorerBlockScene.applyHighlighting(this.highlightCriteria)
|
||||
}
|
||||
}
|
||||
|
||||
addTx (txData) {
|
||||
@ -125,20 +146,49 @@ export default class TxController {
|
||||
}
|
||||
}
|
||||
|
||||
simulateBlock () {
|
||||
const time = Date.now() / 1000
|
||||
console.log('sim time ', time)
|
||||
this.addBlock({
|
||||
version: 'fake',
|
||||
id: Math.random(),
|
||||
value: 10000,
|
||||
prev_block: 'also_fake',
|
||||
merkle_root: 'merkle',
|
||||
timestamp: time,
|
||||
bits: 'none',
|
||||
txn_count: 20,
|
||||
fees: 100,
|
||||
txns: [{ version: 'fake', inflated: false, id: 'coinbase', value: 625000100, fee: 100, vbytes: 500, inputs:[{ prev_txid: '00000000000000000000000000000', prev_vout: 0, script_sig: '03e0170b04efb72c622f466f756e6472792055534120506f6f6c202364726f70676f6c642f0eb5059f0000000000000000',
|
||||
sequence_no: 0, value: 625000100, script_pub_key: "76a9145e9b23809261178723055968d134a947f47e799f88ac" }], outputs: [{ prev_txid: '00000000000000000000000000000', prev_vout: 0, script_sig: '03e0170b04efb72c622f466f756e6472792055534120506f6f6c202364726f70676f6c642f0eb5059f0000000000000000',
|
||||
sequence_no: 0, value: 625000100, script_pub_key: "76a9145e9b23809261178723055968d134a947f47e799f88ac" }], time: Date.now()
|
||||
}, ...Object.keys(this.txs).filter(() => {
|
||||
return (Math.random() < 0.5)
|
||||
}).map(key => {
|
||||
return {
|
||||
...this.txs[key],
|
||||
inputs: this.txs[key].inputs.map(input => { return {...input, script_pub_key: null, value: null }}),
|
||||
}
|
||||
})]
|
||||
})
|
||||
}
|
||||
|
||||
addBlock (blockData, realtime=true) {
|
||||
// discard duplicate blocks
|
||||
if (!blockData || !blockData.id || this.knownBlocks[blockData.id]) {
|
||||
return
|
||||
}
|
||||
|
||||
const block = new BitcoinBlock(blockData)
|
||||
let block
|
||||
block = new BitcoinBlock(blockData)
|
||||
|
||||
latestBlockHeight.set(block.height)
|
||||
this.knownBlocks[block.id] = true
|
||||
// this.knownBlocks[block.id] = true
|
||||
if (this.clearBlockTimeout) clearTimeout(this.clearBlockTimeout)
|
||||
|
||||
this.expiredTxs = {}
|
||||
|
||||
this.clearBlock()
|
||||
if (!this.explorerBlockScene) this.clearBlock()
|
||||
|
||||
if (this.blocksEnabled) {
|
||||
this.blockScene = new TxBlockScene({ width: this.blockAreaSize, height: this.blockAreaSize, blockId: block.id, controller: this, colorMode: this.colorMode })
|
||||
@ -163,10 +213,14 @@ export default class TxController {
|
||||
this.expiredTxs[block.txns[i].id] = true
|
||||
}
|
||||
console.log(`New block with ${knownCount} known transactions and ${unknownCount} unknown transactions`)
|
||||
this.blockScene.initialLayout()
|
||||
this.blockScene.initialLayout(!!this.explorerBlockScene)
|
||||
setTimeout(() => { this.poolScene.scrollLock = false; this.poolScene.layoutAll() }, 4000)
|
||||
|
||||
blockVisible.set(true)
|
||||
|
||||
if (!this.explorerBlockScene) {
|
||||
currentBlock.set(block)
|
||||
}
|
||||
} else {
|
||||
this.poolScene.scrollLock = true
|
||||
for (let i = 0; i < block.txns.length; i++) {
|
||||
@ -205,31 +259,97 @@ export default class TxController {
|
||||
this.poolScene.scrollLock = false
|
||||
this.poolScene.layoutAll()
|
||||
}, 5500)
|
||||
|
||||
currentBlock.set(block)
|
||||
}
|
||||
|
||||
currentBlock.set(block)
|
||||
this.block = block
|
||||
|
||||
return block
|
||||
}
|
||||
|
||||
exploreBlock (blockData) {
|
||||
const block = blockData.isBlock ? blockData : new BitcoinBlock(blockData)
|
||||
let enterFromRight = false
|
||||
|
||||
// clean up previous block
|
||||
if (this.explorerBlock && this.explorerBlockScene) {
|
||||
const prevBlock = this.explorerBlock
|
||||
const prevBlockScene = this.explorerBlockScene
|
||||
if (prevBlock.height < block.height) {
|
||||
prevBlockScene.exitLeft()
|
||||
enterFromRight = true
|
||||
}
|
||||
else prevBlockScene.exitRight()
|
||||
prevBlockScene.expire(2000)
|
||||
} else if (this.blockScene) {
|
||||
this.blockScene.exitRight()
|
||||
}
|
||||
|
||||
this.explorerBlock = block
|
||||
|
||||
if (this.blocksEnabled) {
|
||||
this.explorerBlockScene = new TxBlockScene({ width: this.blockAreaSize, height: this.blockAreaSize, blockId: block.id, controller: this, colorMode: this.colorMode })
|
||||
for (let i = 0; i < block.txns.length; i++) {
|
||||
const tx = new BitcoinTx({
|
||||
...block.txns[i],
|
||||
block: block
|
||||
}, this.vertexArray)
|
||||
this.txs[tx.id] = tx
|
||||
this.txs[tx.id].applyHighlighting(this.highlightCriteria)
|
||||
this.explorerBlockScene.insert(tx, 0, false)
|
||||
}
|
||||
this.explorerBlockScene.prepareAll()
|
||||
this.explorerBlockScene.layoutAll()
|
||||
if (enterFromRight) {
|
||||
blockTransitionDirection.set('right')
|
||||
this.explorerBlockScene.enterRight()
|
||||
} else {
|
||||
blockTransitionDirection.set('left')
|
||||
this.explorerBlockScene.enterLeft()
|
||||
}
|
||||
}
|
||||
|
||||
blockVisible.set(true)
|
||||
currentBlock.set(block)
|
||||
}
|
||||
|
||||
resumeLatest () {
|
||||
if (this.explorerBlock && this.explorerBlockScene) {
|
||||
const prevBlock = this.explorerBlock
|
||||
const prevBlockScene = this.explorerBlockScene
|
||||
prevBlockScene.exitLeft()
|
||||
prevBlockScene.expire(2000)
|
||||
this.explorerBlockScene = null
|
||||
this.explorerBlock = null
|
||||
}
|
||||
if (this.blockScene && this.block) {
|
||||
blockTransitionDirection.set('right')
|
||||
this.blockScene.enterRight()
|
||||
currentBlock.set(this.block)
|
||||
}
|
||||
}
|
||||
|
||||
hideBlock () {
|
||||
if (this.blockScene) {
|
||||
if (this.blockScene && !this.explorerBlockScene) {
|
||||
blockTransitionDirection.set(null)
|
||||
this.blockScene.hide()
|
||||
}
|
||||
}
|
||||
|
||||
showBlock () {
|
||||
if (this.blockScene) {
|
||||
if (this.blockScene && !this.explorerBlockScene) {
|
||||
this.blockScene.show()
|
||||
}
|
||||
}
|
||||
|
||||
clearBlock () {
|
||||
if (this.blockScene) {
|
||||
this.blockScene.exitLeft()
|
||||
this.blockScene.expire()
|
||||
}
|
||||
this.block = null
|
||||
currentBlock.set(null)
|
||||
if (this.blockVisibleUnsub) this.blockVisibleUnsub()
|
||||
}
|
||||
|
||||
destroyTx (id) {
|
||||
@ -243,7 +363,8 @@ export default class TxController {
|
||||
mouseMove (position) {
|
||||
if (this.poolScene && !this.selectionLocked) {
|
||||
let selected = this.poolScene.selectAt(position)
|
||||
if (!selected && this.blockScene && !this.blockScene.hidden) selected = this.blockScene.selectAt(position)
|
||||
if (!selected && this.blockScene && !this.explorerBlock && !this.blockScene.hidden) selected = this.blockScene.selectAt(position)
|
||||
if (!selected && this.explorerBlockScene && this.explorerBlock && !this.explorerBlockScene.hidden) selected = this.explorerBlockScene.selectAt(position)
|
||||
|
||||
if (selected !== this.selectedTx) {
|
||||
if (this.selectedTx) this.selectedTx.hoverOff()
|
||||
@ -254,10 +375,11 @@ export default class TxController {
|
||||
}
|
||||
}
|
||||
|
||||
mouseClick (position) {
|
||||
async mouseClick (position) {
|
||||
if (this.poolScene) {
|
||||
let selected = this.poolScene.selectAt(position)
|
||||
if (!selected && this.blockScene && !this.blockScene.hidden) selected = this.blockScene.selectAt(position)
|
||||
if (!selected && this.blockScene && !this.explorerBlock && !this.blockScene.hidden) selected = this.blockScene.selectAt(position)
|
||||
if (!selected && this.explorerBlockScene && this.explorerBlock && !this.explorerBlockScene.hidden) selected = this.explorerBlockScene.selectAt(position)
|
||||
|
||||
let sameTx = true
|
||||
if (selected !== this.selectedTx) {
|
||||
@ -266,12 +388,21 @@ export default class TxController {
|
||||
if (selected) selected.hoverOn()
|
||||
}
|
||||
this.selectedTx = selected
|
||||
selectedTx.set(this.selectedTx)
|
||||
if (sameTx && this.selectedTx) {
|
||||
detailTx.set(this.selectedTx)
|
||||
overlay.set('tx')
|
||||
selectedTx.set(selected)
|
||||
if (sameTx && selected) {
|
||||
if (!selected.is_inflated) {
|
||||
loading.increment()
|
||||
await searchTx(selected.id)
|
||||
loading.decrement()
|
||||
} else {
|
||||
const spendResult = await fetchSpends(selected.id)
|
||||
if (spendResult) selected = addSpends(selected, spendResult)
|
||||
detailTx.set(selected)
|
||||
overlay.set('tx')
|
||||
}
|
||||
console.log(selected)
|
||||
}
|
||||
this.selectionLocked = !!this.selectedTx && !(this.selectionLocked && sameTx)
|
||||
this.selectionLocked = !!selected && !(this.selectionLocked && sameTx)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
import BitcoinTx from '../models/BitcoinTx.js'
|
||||
|
||||
export default class BitcoinBlock {
|
||||
constructor ({ version, id, value, prev_block, merkle_root, timestamp, bits, bytes, txn_count, txns, fees }) {
|
||||
constructor ({ version, id, height, value, prev_block, merkle_root, timestamp, bits, bytes, txn_count, txns, fees }) {
|
||||
this.isBlock = true
|
||||
this.version = version
|
||||
this.id = id
|
||||
this.height = height
|
||||
this.value = value
|
||||
this.prev_block = prev_block
|
||||
this.merkle_root = merkle_root
|
||||
@ -14,13 +15,9 @@ export default class BitcoinBlock {
|
||||
this.txnCount = txn_count
|
||||
this.txns = txns
|
||||
this.coinbase = new BitcoinTx(this.txns[0], true)
|
||||
if (fees) {
|
||||
this.fees = fees
|
||||
} else {
|
||||
this.fees = null
|
||||
}
|
||||
this.fees = fees
|
||||
this.coinbase.setBlock(this)
|
||||
this.height = this.coinbase.coinbase.height
|
||||
this.height = this.height || this.coinbase.coinbase.height
|
||||
this.miner_sig = this.coinbase.coinbase.sigAscii
|
||||
|
||||
this.total_vbytes = 0
|
||||
@ -41,4 +38,49 @@ export default class BitcoinBlock {
|
||||
this.avgFeerate = this.fees / this.total_vbytes
|
||||
}
|
||||
}
|
||||
|
||||
setVertexArray (vertexArray) {
|
||||
if (this.txns) {
|
||||
this.txns.forEach(txn => {
|
||||
txn.setVertexArray(vertexArray)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
static fromRPCData (data) {
|
||||
const txns = data.tx.map((tx, index) => { return BitcoinTx.fromRPCData(tx, index == 0) })
|
||||
const value = txns.reduce((acc, tx) => { return acc + tx.fee + tx.value }, 0)
|
||||
const fees = txns.reduce((acc, tx) => { return acc + tx.fee }, 0)
|
||||
|
||||
return {
|
||||
version: data.version,
|
||||
id: data.hash,
|
||||
height: data.height,
|
||||
value: value,
|
||||
prev_block: data.previousblockhash,
|
||||
merkle_root: data.merkleroot,
|
||||
timestamp: data.time,
|
||||
bits: data.bits,
|
||||
bytes: data.size,
|
||||
txn_count: txns.length,
|
||||
txns,
|
||||
fees
|
||||
}
|
||||
}
|
||||
|
||||
static decompress (data) {
|
||||
return {
|
||||
version: data[0],
|
||||
id: data[1],
|
||||
height: data[2],
|
||||
value: data[3],
|
||||
prev_block: data[4],
|
||||
timestamp: data[5],
|
||||
bits: data[6],
|
||||
bytes: data[7],
|
||||
txn_count: data[8].length,
|
||||
txns: data[8].map(txData => BitcoinTx.decompress(txData)),
|
||||
fees: data[9]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,48 +1,64 @@
|
||||
import TxView from './TxView.js'
|
||||
import config from '../config.js'
|
||||
import { subsidyAt } from '../utils/bitcoin.js'
|
||||
import { mixColor, pink, bluegreen, orange, teal, green, purple } from '../utils/color.js'
|
||||
import { mixColor, pink, bluegreen, orange, teal, blue, green, purple } from '../utils/color.js'
|
||||
|
||||
export default class BitcoinTx {
|
||||
constructor (data, vertexArray, isCoinbase = false) {
|
||||
this.vertexArray = vertexArray
|
||||
this.setData(data, isCoinbase)
|
||||
this.view = new TxView(this)
|
||||
if (vertexArray) this.view = new TxView(this)
|
||||
}
|
||||
|
||||
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 parsed_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))
|
||||
}, "")
|
||||
if (this.is_preview) {
|
||||
const subsidy = subsidyAt(block.height)
|
||||
this.coinbase = {
|
||||
height: block.height,
|
||||
fees: this.value - subsidy,
|
||||
subsidy
|
||||
}
|
||||
} else {
|
||||
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 parsed_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 && sig.length) ? sig.match(/../g).reduce((parsed, hexChar) => {
|
||||
return parsed + String.fromCharCode(parseInt(hexChar, 16))
|
||||
}, "") : ""
|
||||
|
||||
const height = block.height == null ? parsed_height : block.height
|
||||
const height = block.height == null ? parsed_height : block.height
|
||||
|
||||
const subsidy = subsidyAt(height)
|
||||
const subsidy = subsidyAt(height)
|
||||
|
||||
this.coinbase = {
|
||||
height,
|
||||
sig,
|
||||
sigAscii,
|
||||
fees: this.value - subsidy,
|
||||
subsidy
|
||||
this.coinbase = {
|
||||
height,
|
||||
sig,
|
||||
sigAscii,
|
||||
fees: this.value - subsidy,
|
||||
subsidy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setData ({ version, inflated, id, value, fee, vbytes, inputs, outputs, time, block }, isCoinbase=false) {
|
||||
setVertexArray (vertexArray) {
|
||||
this.vertexArray = vertexArray
|
||||
this.view = new TxView(this)
|
||||
}
|
||||
|
||||
setData ({ version, inflated, preview, id, value, fee, vbytes, numInputs, inputs, outputs, time, block }, isCoinbase=false) {
|
||||
this.version = version
|
||||
this.is_inflated = !!inflated
|
||||
this.is_preview = !!preview
|
||||
this.id = id
|
||||
this.pixelPosition = { x: 0, y: 0, r: 0}
|
||||
this.screenPosition = { x: 0, y: 0, r: 0}
|
||||
this.gridPosition = { x: 0, y: 0, r: 0}
|
||||
this.inputs = inputs
|
||||
if (numInputs != null) this.numInputs = numInputs
|
||||
this.outputs = outputs
|
||||
this.value = value
|
||||
this.fee = fee
|
||||
@ -59,21 +75,21 @@ export default class BitcoinTx {
|
||||
|
||||
// is a coinbase transaction?
|
||||
this.isCoinbase = isCoinbase
|
||||
if (this.isCoinbase || !this.is_inflated || (this.fee < 0)) {
|
||||
if (this.isCoinbase || this.fee == null || this.fee < 0) {
|
||||
this.fee = null
|
||||
this.feerate = null
|
||||
}
|
||||
|
||||
if (!this.block) this.setBlock(block)
|
||||
|
||||
const feeColor = (this.feerate == null
|
||||
const feeColor = ((this.isCoinbase || this.feerate == null)
|
||||
? orange
|
||||
: mixColor(teal, purple, 1, Math.log2(64), Math.log2(this.feerate))
|
||||
: mixColor(teal, purple, 1, Math.log2(128), Math.log2(this.feerate))
|
||||
)
|
||||
this.colors = {
|
||||
age: {
|
||||
block: { color: orange },
|
||||
pool: { color: orange, endColor: teal, duration: 60000 },
|
||||
pool: { color: orange, endColor: blue, duration: 60000 },
|
||||
},
|
||||
fee: {
|
||||
block: { color: feeColor },
|
||||
@ -147,4 +163,33 @@ export default class BitcoinTx {
|
||||
})
|
||||
this.view.setHighlight(this.highlight, color || pink)
|
||||
}
|
||||
|
||||
static fromRPCData(txData, isCoinbase) {
|
||||
return {
|
||||
version: txData.version,
|
||||
inflated: false,
|
||||
preview: true,
|
||||
id: txData.txid,
|
||||
value: null, // calculated in constructor
|
||||
fee: txData.fee * 100000000,
|
||||
vbytes: txData.vsize,
|
||||
inputs: txData.vin.map(vin => { return { script_sig: vin.coinbase || vin.scriptSig.hex, prev_txid: vin.txid, prev_vout: vin.vout }}),
|
||||
outputs: txData.vout.map(vout => { return { value: vout.value * 100000000, script_pub_key: vout.scriptPubKey.hex }}),
|
||||
}
|
||||
}
|
||||
|
||||
// unpack compact array format tx data
|
||||
static decompress (data, blockData) {
|
||||
return {
|
||||
version: data[0],
|
||||
inflated: false,
|
||||
preview: true,
|
||||
id: data[1],
|
||||
fee: data[2],
|
||||
value: data[3],
|
||||
vbytes: data[4],
|
||||
numInputs: data[5],
|
||||
outputs: data[6].map(vout => { return { value: vout[0], script_pub_key: vout[1] }}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,12 @@
|
||||
import TxMondrianPoolScene from './TxMondrianPoolScene.js'
|
||||
import { settings } from '../stores.js'
|
||||
import { logTxSize, byteTxSize } from '../utils/misc.js'
|
||||
import config from '../config.js'
|
||||
|
||||
let settingsValue
|
||||
settings.subscribe(v => {
|
||||
settingsValue = v
|
||||
})
|
||||
|
||||
export default class TxBlockScene extends TxMondrianPoolScene {
|
||||
constructor ({ width, height, unit = 4, padding = 1, blockId, controller, heightStore, colorMode }) {
|
||||
@ -47,6 +55,12 @@ export default class TxBlockScene extends TxMondrianPoolScene {
|
||||
this.resetLayout()
|
||||
}
|
||||
|
||||
// calculates and returns the size of the tx in multiples of the grid size
|
||||
txSize (tx={ value: 1, vbytes: 1 }) {
|
||||
if (settingsValue.vbytes) return byteTxSize(tx.vbytes, Math.Infinity)
|
||||
else return logTxSize(tx.value, Math.Infinity)
|
||||
}
|
||||
|
||||
setTxOnScreen (tx, pixelPosition) {
|
||||
if (!tx.view.initialised) {
|
||||
tx.view.update({
|
||||
@ -119,6 +133,85 @@ export default class TxBlockScene extends TxMondrianPoolScene {
|
||||
this.prepareTxOnScreen(tx, this.layoutTx(tx, sequence, 0, false))
|
||||
}
|
||||
|
||||
enterTx (tx, right) {
|
||||
tx.view.update({
|
||||
display: {
|
||||
position: {
|
||||
x: tx.screenPosition.x + (right ? window.innerWidth : -window.innerWidth) + ((Math.random()-0.5) * (window.innerHeight/4)),
|
||||
y: tx.screenPosition.y + ((Math.random()-0.5) * (window.innerHeight/4)),
|
||||
r: tx.pixelPosition.r
|
||||
},
|
||||
color: {
|
||||
...tx.getColor('block', this.colorMode).color,
|
||||
alpha: 0
|
||||
}
|
||||
},
|
||||
delay: 0,
|
||||
state: 'ready'
|
||||
})
|
||||
tx.view.update({
|
||||
display: {
|
||||
position: tx.screenPosition,
|
||||
color: {
|
||||
...tx.getColor('block', this.colorMode).color,
|
||||
alpha: 1
|
||||
}
|
||||
},
|
||||
duration: 2000,
|
||||
delay: 0
|
||||
})
|
||||
}
|
||||
|
||||
enter (right) {
|
||||
this.hidden = false
|
||||
const ids = this.getActiveTxList()
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
this.enterTx(this.txs[ids[i]], right)
|
||||
}
|
||||
}
|
||||
|
||||
enterRight () {
|
||||
this.enter(true)
|
||||
}
|
||||
|
||||
enterLeft () {
|
||||
this.enter(false)
|
||||
}
|
||||
|
||||
exitTx (tx, right) {
|
||||
tx.view.update({
|
||||
display: {
|
||||
position: {
|
||||
x: tx.screenPosition.x + (right ? window.innerWidth : -window.innerWidth) + ((Math.random()-0.5) * (window.innerHeight/4)),
|
||||
y: tx.screenPosition.y + ((Math.random()-0.5) * (window.innerHeight/4)),
|
||||
r: tx.pixelPosition.r
|
||||
},
|
||||
color: {
|
||||
...tx.getColor('block', this.colorMode).color,
|
||||
alpha: 0
|
||||
}
|
||||
},
|
||||
delay: 0,
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
|
||||
exit (right) {
|
||||
this.hidden = true
|
||||
const ids = this.getActiveTxList()
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
this.exitTx(this.txs[ids[i]], right)
|
||||
}
|
||||
}
|
||||
|
||||
exitRight () {
|
||||
this.exit(true)
|
||||
}
|
||||
|
||||
exitLeft () {
|
||||
this.exit(false)
|
||||
}
|
||||
|
||||
hideTx (tx) {
|
||||
this.savePixelsToScreenPosition(tx)
|
||||
tx.view.update({
|
||||
@ -174,10 +267,11 @@ export default class TxBlockScene extends TxMondrianPoolScene {
|
||||
// }
|
||||
}
|
||||
|
||||
initialLayout () {
|
||||
initialLayout (exited) {
|
||||
this.prepareAll()
|
||||
setTimeout(() => {
|
||||
this.layoutAll()
|
||||
if (exited) this.exitRight()
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
@ -199,15 +293,21 @@ export default class TxBlockScene extends TxMondrianPoolScene {
|
||||
}
|
||||
}
|
||||
|
||||
expire () {
|
||||
expire (delay=3000) {
|
||||
this.expired = true
|
||||
this.hide()
|
||||
setTimeout(() => {
|
||||
const txIds = this.getTxList()
|
||||
for (let i = 0; i < txIds.length; i++) {
|
||||
if (this.txs[txIds[i]]) this.controller.destroyTx(txIds[i])
|
||||
}
|
||||
this.layout.destroy()
|
||||
}, 3000)
|
||||
}, delay)
|
||||
}
|
||||
|
||||
selectAt (position) {
|
||||
if (this.layout) {
|
||||
const gridPosition = this.screenToGrid({ x: position.x + (this.gridSize/4), y: position.y - (this.gridSize/2) })
|
||||
return this.layout.getTxInGridCell(gridPosition)
|
||||
} else return null
|
||||
}
|
||||
}
|
||||
|
@ -339,7 +339,7 @@ class MondrianLayout {
|
||||
while (this.txMap.length <= offsetY) {
|
||||
this.txMap.push(new Array(this.width).fill(null))
|
||||
}
|
||||
this.txMap[offsetY][coord.x] = null
|
||||
if (this.txMap[offsetY]) this.txMap[offsetY][coord.x] = null
|
||||
}
|
||||
|
||||
getTxInGridCell(coord) {
|
||||
|
@ -29,8 +29,6 @@ export default class TxPoolScene {
|
||||
|
||||
this.scrollRateLimitTimer = null
|
||||
this.initialised = true
|
||||
|
||||
if (config.dev) console.log('pool', this)
|
||||
}
|
||||
|
||||
resize ({ width = this.width, height = this.height }) {
|
||||
|
@ -167,4 +167,6 @@ export const blocksEnabled = derived([settings], ([$settings]) => {
|
||||
|
||||
export const latestBlockHeight = writable(null)
|
||||
export const highlightInOut = writable(null)
|
||||
export const loading = writable(0)
|
||||
export const loading = createCounter()
|
||||
export const explorerBlockData = writable(null)
|
||||
export const blockTransitionDirection = writable(null)
|
||||
|
@ -16,6 +16,7 @@ export const pink = { h: 0.03, l: 0.35 }
|
||||
export const bluegreen = { h: 0.45, l: 0.4 }
|
||||
export const orange = { h: 0.181, l: 0.472 }
|
||||
export const teal = { h: 0.475, l: 0.55 }
|
||||
export const blue = { h: 0.5, l: 0.55 }
|
||||
export const green = { h: 0.37, l: 0.35 }
|
||||
export const purple = { h: 0.95, l: 0.35 }
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import api from './api.js'
|
||||
import BitcoinTx from '../models/BitcoinTx.js'
|
||||
import { detailTx, selectedTx, overlay, highlightInOut } from '../stores.js'
|
||||
import BitcoinBlock from '../models/BitcoinBlock.js'
|
||||
import { detailTx, selectedTx, currentBlock, explorerBlockData, overlay, highlightInOut } from '../stores.js'
|
||||
import { addressToSPK } from './encodings.js'
|
||||
|
||||
// Quick heuristic matching to guess what kind of search a query is for
|
||||
@ -113,6 +114,11 @@ function matchQuery (query) {
|
||||
}
|
||||
export {matchQuery as matchQuery}
|
||||
|
||||
let currentBlockVal
|
||||
currentBlock.subscribe(block => {
|
||||
currentBlockVal = block
|
||||
})
|
||||
|
||||
async function fetchTx (txid) {
|
||||
if (!txid) return
|
||||
const response = await fetch(`${api.uri}/api/tx/${txid}`, {
|
||||
@ -131,10 +137,72 @@ async function fetchTx (txid) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchTx(txid, input, output) {
|
||||
async function fetchBlockByHash (hash) {
|
||||
if (!hash || (currentBlockVal && hash === currentBlockVal.id)) return true
|
||||
// try to fetch static block
|
||||
let response = await fetch(`${api.uri}/api/block/${hash}`, {
|
||||
method: 'GET'
|
||||
})
|
||||
if (!response) throw new Error('null response')
|
||||
if (response && response.status == 200) {
|
||||
const blockData = await response.json()
|
||||
if (blockData) {
|
||||
if (blockData.id) {
|
||||
return new BitcoinBlock(blockData)
|
||||
} else return BitcoinBlock.decompress(blockData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchBlockByHeight (height) {
|
||||
if (height == null) return
|
||||
const response = await fetch(`${api.uri}/api/block/height/${height}`, {
|
||||
method: 'GET'
|
||||
})
|
||||
if (!response) throw new Error('null response')
|
||||
if (response.status == 200) {
|
||||
const hash = await response.json()
|
||||
return fetchBlockByHash(hash)
|
||||
} else {
|
||||
throw new Error(response.status)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSpends (txid) {
|
||||
if (txid == null) return
|
||||
const response = await fetch(`${api.uri}/api/spends/${txid}`, {
|
||||
method: 'GET'
|
||||
})
|
||||
if (!response) throw new Error('null response')
|
||||
if (response.status == 200) {
|
||||
return response.json()
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
export {fetchSpends as fetchSpends}
|
||||
|
||||
function addSpends(tx, spends) {
|
||||
tx.outputs.forEach((output, index) => {
|
||||
if (spends[index]) {
|
||||
output.spend = {
|
||||
txid: spends[index][0],
|
||||
vin: spends[index][1],
|
||||
}
|
||||
} else {
|
||||
output.spend = null
|
||||
}
|
||||
})
|
||||
return tx
|
||||
}
|
||||
export {addSpends as addSpends}
|
||||
|
||||
export async function searchTx (txid, input, output) {
|
||||
try {
|
||||
const searchResult = await fetchTx(txid)
|
||||
let searchResult = await fetchTx(txid)
|
||||
const spendResult = await fetchSpends(txid)
|
||||
if (searchResult) {
|
||||
if (spendResult) searchResult = addSpends(searchResult, spendResult)
|
||||
selectedTx.set(searchResult)
|
||||
detailTx.set(searchResult)
|
||||
overlay.set('tx')
|
||||
@ -149,6 +217,36 @@ export async function searchTx(txid, input, output) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchBlock(hash) {
|
||||
console.log("search block ", hash)
|
||||
export async function searchBlockHash (hash) {
|
||||
try {
|
||||
const searchResult = await fetchBlockByHash(hash)
|
||||
if (searchResult) {
|
||||
if (searchResult.id) {
|
||||
explorerBlockData.set(searchResult)
|
||||
}
|
||||
return null
|
||||
} else {
|
||||
return '500'
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('error fetching block ', err)
|
||||
return err.message
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchBlockHeight (height) {
|
||||
try {
|
||||
const searchResult = await fetchBlockByHeight(height)
|
||||
if (searchResult) {
|
||||
if (searchResult.id) {
|
||||
explorerBlockData.set(searchResult)
|
||||
}
|
||||
return null
|
||||
} else {
|
||||
return '500'
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('error fetching block ', err)
|
||||
return err.message
|
||||
}
|
||||
}
|
||||
|
5
server/.gitignore
vendored
5
server/.gitignore
vendored
@ -31,4 +31,9 @@ bitcoin_stream-*.tar
|
||||
!log/.gitkeep
|
||||
*.log
|
||||
|
||||
# Blocks
|
||||
/data/index/**
|
||||
!data/index/.gitkeep
|
||||
!data/index/spend/.gitkeep
|
||||
|
||||
.envrc
|
||||
|
0
server/data/.gitkeep
Normal file → Executable file
0
server/data/.gitkeep
Normal file → Executable file
0
server/data/index/.gitkeep
Normal file
0
server/data/index/.gitkeep
Normal file
@ -6,9 +6,11 @@ defmodule BitcoinStream.BlockData do
|
||||
@moduledoc """
|
||||
Block data module.
|
||||
|
||||
Maintains a flat-file db of blocks (if enabled)
|
||||
Serves a cached copy of the latest block
|
||||
"""
|
||||
use GenServer
|
||||
use Task, restart: :transient
|
||||
|
||||
def start_link(opts) do
|
||||
Logger.info("Starting block data link");
|
||||
@ -53,4 +55,73 @@ defmodule BitcoinStream.BlockData do
|
||||
def set_json_block(pid, block_id, json) do
|
||||
GenServer.call(pid, {:json, { block_id, json }}, 10000)
|
||||
end
|
||||
|
||||
def clean_block(block) do
|
||||
{txs, value, fees} = clean_txs(block["tx"]);
|
||||
{:ok, [
|
||||
block["version"],
|
||||
block["hash"],
|
||||
block["height"],
|
||||
value,
|
||||
block["previousblockhash"],
|
||||
block["time"],
|
||||
block["bits"],
|
||||
block["size"],
|
||||
txs,
|
||||
fees
|
||||
]}
|
||||
end
|
||||
|
||||
defp clean_txs([], clean, value, fees) do
|
||||
{Enum.reverse(clean), value, fees}
|
||||
end
|
||||
defp clean_txs([tx | rest], clean, value, fees) do
|
||||
{cleantx, txvalue, txfee} = clean_tx(tx)
|
||||
clean_txs(rest, [cleantx | clean], value + txvalue, fees + txfee)
|
||||
end
|
||||
defp clean_txs(txs) do
|
||||
clean_txs(txs, [], 0, 0)
|
||||
end
|
||||
|
||||
defp clean_tx(tx) do
|
||||
total_value = sum_output_values(tx["vout"]);
|
||||
outputs = clean_outputs(tx["vout"]);
|
||||
fee = if tx["fee"] != nil do round(tx["fee"] * 100000000) else 0 end
|
||||
{[
|
||||
tx["version"],
|
||||
tx["txid"],
|
||||
fee,
|
||||
total_value,
|
||||
tx["vsize"],
|
||||
length(tx["vin"]),
|
||||
outputs
|
||||
], total_value, fee}
|
||||
end
|
||||
|
||||
defp clean_outputs([], clean) do
|
||||
Enum.reverse(clean)
|
||||
end
|
||||
defp clean_outputs([out | rest], clean) do
|
||||
clean_outputs(rest, [clean_output(out) | clean])
|
||||
end
|
||||
defp clean_outputs(outputs) do
|
||||
clean_outputs(outputs, [])
|
||||
end
|
||||
|
||||
defp clean_output(output) do
|
||||
[
|
||||
round(output["value"] * 100000000),
|
||||
output["scriptPubKey"]["hex"]
|
||||
]
|
||||
end
|
||||
|
||||
defp sum_output_values([], value) do
|
||||
value
|
||||
end
|
||||
defp sum_output_values([out|rest], value) do
|
||||
sum_output_values(rest, value + round(out["value"] * 100000000))
|
||||
end
|
||||
defp sum_output_values(outputs) do
|
||||
sum_output_values(outputs, 0)
|
||||
end
|
||||
end
|
||||
|
@ -13,6 +13,7 @@ defmodule BitcoinStream.Bridge.Block do
|
||||
alias BitcoinStream.Mempool, as: Mempool
|
||||
alias BitcoinStream.RPC, as: RPC
|
||||
alias BitcoinStream.BlockData, as: BlockData
|
||||
alias BitcoinStream.Index.Spend, as: SpendIndex
|
||||
|
||||
def child_spec(host: host, port: port) do
|
||||
%{
|
||||
|
@ -9,6 +9,7 @@ defmodule BitcoinStream.Bridge.Sequence do
|
||||
"""
|
||||
use GenServer
|
||||
|
||||
alias BitcoinStream.Index.Spend, as: SpendIndex
|
||||
alias BitcoinStream.Mempool, as: Mempool
|
||||
alias BitcoinStream.RPC, as: RPC
|
||||
|
||||
@ -92,13 +93,14 @@ defmodule BitcoinStream.Bridge.Sequence do
|
||||
|
||||
defp loop(socket) do
|
||||
with {:ok, message} <- :chumak.recv_multipart(socket),
|
||||
[_topic, <<hash::binary-size(32), type::binary-size(1), seq::little-size(64)>>, <<_sequence::little-size(32)>>] <- message,
|
||||
txid <- Base.encode16(hash, case: :lower),
|
||||
[_topic, <<id::binary-size(32), type::binary-size(1), rest::binary>>, <<_sequence::little-size(32)>>] <- message,
|
||||
hash <- Base.encode16(id, case: :lower),
|
||||
event <- to_charlist(type) do
|
||||
case event do
|
||||
# Transaction added to mempool
|
||||
'A' ->
|
||||
case Mempool.register(:mempool, txid, seq, true) do
|
||||
<<seq::little-size(64)>> = rest;
|
||||
case Mempool.register(:mempool, hash, seq, true) do
|
||||
false -> false
|
||||
|
||||
{ txn, count } ->
|
||||
@ -107,16 +109,26 @@ defmodule BitcoinStream.Bridge.Sequence do
|
||||
|
||||
# Transaction removed from mempool for non block inclusion reason
|
||||
'R' ->
|
||||
case Mempool.drop(:mempool, txid) do
|
||||
<<seq::little-size(64)>> = rest;
|
||||
case Mempool.drop(:mempool, hash) do
|
||||
count when is_integer(count) ->
|
||||
send_drop_tx(txid, count);
|
||||
send_drop_tx(hash, count);
|
||||
|
||||
_ ->
|
||||
true
|
||||
end
|
||||
|
||||
'D' ->
|
||||
SpendIndex.notify_block_disconnect(:spends, hash);
|
||||
true
|
||||
|
||||
'C' ->
|
||||
SpendIndex.notify_block(:spends, hash);
|
||||
true
|
||||
|
||||
# Don't care about other events
|
||||
_ -> true
|
||||
other ->
|
||||
true
|
||||
end
|
||||
else
|
||||
_ -> false
|
||||
|
@ -191,6 +191,7 @@ defmodule BitcoinStream.Mempool do
|
||||
:registered ->
|
||||
with [] <- :ets.lookup(:block_cache, txid) do # double check tx isn't included in the last block
|
||||
:ets.insert(:mempool_cache, {txid, { txn.inputs, txn.value + txn.fee, txn.inflated }, nil});
|
||||
cache_spends(txid, txn.inputs);
|
||||
get(pid)
|
||||
else
|
||||
_ ->
|
||||
@ -248,6 +249,7 @@ defmodule BitcoinStream.Mempool do
|
||||
[{_txid, _, txn}] when txn != nil ->
|
||||
:ets.insert(:mempool_cache, {txid, { txn.inputs, txn.value + txn.fee, txn.inflated }, nil});
|
||||
:ets.delete(:sync_cache, txid);
|
||||
cache_spends(txid, txn.inputs);
|
||||
if do_count do
|
||||
increment(pid);
|
||||
end
|
||||
@ -295,8 +297,10 @@ defmodule BitcoinStream.Mempool do
|
||||
get(pid)
|
||||
|
||||
# tx fully processed and not already dropped
|
||||
[{_txid, data, _status}] when data != nil ->
|
||||
[{txid, data, _status}] when data != nil ->
|
||||
:ets.delete(:mempool_cache, txid);
|
||||
{inputs, _value, _inflated} = data;
|
||||
uncache_spends(inputs);
|
||||
decrement(pid);
|
||||
get(pid)
|
||||
|
||||
@ -429,6 +433,7 @@ defmodule BitcoinStream.Mempool do
|
||||
inflated_txn <- BitcoinTx.inflate(txn, false) do
|
||||
if inflated_txn.inflated do
|
||||
:ets.insert(:mempool_cache, {txid, { inflated_txn.inputs, inflated_txn.value + inflated_txn.fee, inflated_txn.inflated }, status});
|
||||
cache_spends(txid, inflated_txn.inputs);
|
||||
Logger.debug("repaired #{repaired} mempool txns #{txid}");
|
||||
repaired + 1
|
||||
else
|
||||
@ -493,4 +498,31 @@ defmodule BitcoinStream.Mempool do
|
||||
end
|
||||
end
|
||||
|
||||
defp cache_spend(txid, index, input) do
|
||||
:ets.insert(:spend_cache, {[input.prev_txid, input.prev_vout], [txid, index]})
|
||||
end
|
||||
defp cache_spends(_txid, _index, []) do
|
||||
:ok
|
||||
end
|
||||
defp cache_spends(txid, index, [input | rest]) do
|
||||
cache_spend(txid, index, input);
|
||||
cache_spends(txid, index + 1, rest)
|
||||
end
|
||||
defp cache_spends(txid, inputs) do
|
||||
cache_spends(txid, 0, inputs)
|
||||
end
|
||||
|
||||
defp uncache_spend(input) do
|
||||
:ets.delete(:spend_cache, [input.prev_txid, input.prev_vout])
|
||||
end
|
||||
defp uncache_spends([]) do
|
||||
:ok
|
||||
end
|
||||
defp uncache_spends([input | rest]) do
|
||||
uncache_spend(input);
|
||||
uncache_spends(rest)
|
||||
end
|
||||
defp uncache_spends(inputs) do
|
||||
uncache_spends(inputs)
|
||||
end
|
||||
end
|
||||
|
@ -59,6 +59,36 @@ def decode(block_binary) do
|
||||
end
|
||||
end
|
||||
|
||||
def parse(hex) do
|
||||
with block_binary <- Base.decode16!(hex, [case: :lower]),
|
||||
bytes <- byte_size(block_binary),
|
||||
{:ok, raw_block} <- Bitcoinex.Block.decode(hex),
|
||||
id <- Bitcoinex.Block.block_id(block_binary),
|
||||
{summarised_txns, total_value, total_fees} <- summarise_txns(raw_block.txns)
|
||||
do
|
||||
{:ok, %__MODULE__{
|
||||
version: raw_block.version,
|
||||
prev_block: raw_block.prev_block,
|
||||
merkle_root: raw_block.merkle_root,
|
||||
timestamp: raw_block.timestamp,
|
||||
bits: raw_block.bits,
|
||||
bytes: bytes,
|
||||
txn_count: raw_block.txn_count,
|
||||
txns: summarised_txns,
|
||||
fees: total_fees,
|
||||
value: total_value,
|
||||
id: id
|
||||
}}
|
||||
else
|
||||
{:error, reason} ->
|
||||
Logger.error("Error decoding data for BitcoinBlock: #{reason}")
|
||||
:error
|
||||
_ ->
|
||||
Logger.error("Error decoding data for BitcoinBlock: (unknown reason)")
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp summarise_txns([coinbase | txns]) do
|
||||
# Mempool.is_done returns false while the mempool is still syncing
|
||||
with extended_coinbase <- BitcoinTx.extend(coinbase),
|
||||
@ -87,7 +117,7 @@ defp summarise_txns([next | rest], summarised, total, fees, do_inflate) do
|
||||
if do_inflate do
|
||||
inflated_txn = BitcoinTx.inflate(extended_txn, false)
|
||||
if (inflated_txn.inflated) do
|
||||
Logger.debug("Processing block tx #{length(summarised)}/#{length(summarised) + length(rest) + 1} | #{extended_txn.id}");
|
||||
# Logger.debug("Processing block tx #{length(summarised)}/#{length(summarised) + length(rest) + 1} | #{extended_txn.id}");
|
||||
summarise_txns(rest, [inflated_txn | summarised], total + inflated_txn.value, fees + inflated_txn.fee, true)
|
||||
else
|
||||
summarise_txns(rest, [inflated_txn | summarised], total + inflated_txn.value, nil, false)
|
||||
|
@ -5,11 +5,13 @@ defmodule BitcoinStream.Router do
|
||||
|
||||
alias BitcoinStream.BlockData, as: BlockData
|
||||
alias BitcoinStream.Protocol.Transaction, as: BitcoinTx
|
||||
alias BitcoinStream.BlockData, as: BlockData
|
||||
alias BitcoinStream.RPC, as: RPC
|
||||
alias BitcoinStream.Index.Spend, as: SpendIndex
|
||||
|
||||
plug Corsica, origins: "*", allow_headers: :all
|
||||
plug Plug.Static,
|
||||
at: "/",
|
||||
at: "data",
|
||||
from: :bitcoin_stream
|
||||
plug :match
|
||||
plug Plug.Parsers,
|
||||
@ -18,21 +20,37 @@ defmodule BitcoinStream.Router do
|
||||
json_decoder: Jason
|
||||
plug :dispatch
|
||||
|
||||
match "api/block/height/:height" do
|
||||
case get_block_by_height(height) do
|
||||
{:ok, hash} ->
|
||||
put_resp_header(conn, "cache-control", "public, max-age=3600, immutable")
|
||||
|> send_resp(200, hash)
|
||||
_ ->
|
||||
Logger.debug("Error getting blockhash at height #{height}");
|
||||
send_resp(conn, 404, "Block not found")
|
||||
end
|
||||
end
|
||||
|
||||
match "/api/block/:hash" do
|
||||
case get_block(hash) do
|
||||
{:ok, block} ->
|
||||
put_resp_header(conn, "cache-control", "public, max-age=604800, immutable")
|
||||
{:ok, block, true} ->
|
||||
put_resp_header(conn, "cache-control", "public, max-age=120, immutable")
|
||||
|> send_resp(200, block)
|
||||
|
||||
{:ok, block, false} ->
|
||||
put_resp_header(conn, "cache-control", "public, max-age=31536000, immutable")
|
||||
|> send_resp(200, block)
|
||||
|
||||
_ ->
|
||||
Logger.debug("Error getting block hash");
|
||||
send_resp(conn, 404, "Block not available")
|
||||
Logger.debug("Error getting block with hash #{hash}");
|
||||
send_resp(conn, 404, "Block not found")
|
||||
end
|
||||
end
|
||||
|
||||
match "/api/tx/:hash" do
|
||||
case get_tx(hash) do
|
||||
{:ok, tx} ->
|
||||
put_resp_header(conn, "cache-control", "public, max-age=604800, immutable")
|
||||
put_resp_header(conn, "cache-control", "public, max-age=60, immutable")
|
||||
|> send_resp(200, tx)
|
||||
_ ->
|
||||
Logger.debug("Error getting tx hash");
|
||||
@ -40,17 +58,48 @@ defmodule BitcoinStream.Router do
|
||||
end
|
||||
end
|
||||
|
||||
match "/api/spends/:txid" do
|
||||
case get_tx_spends(txid) do
|
||||
{:ok, spends} ->
|
||||
put_resp_header(conn, "cache-control", "public, max-age=10, immutable")
|
||||
|> send_resp(200, spends)
|
||||
_ ->
|
||||
Logger.debug("Error getting tx spends");
|
||||
send_resp(conn, 200, "[]")
|
||||
end
|
||||
end
|
||||
|
||||
match _ do
|
||||
send_resp(conn, 404, "Not found")
|
||||
end
|
||||
|
||||
defp get_block(last_seen) do
|
||||
defp get_block_by_height(height_str) do
|
||||
with {height, _} <- Integer.parse(height_str),
|
||||
{:ok, 200, blockhash} <- RPC.request(:rpc, "getblockhash", [height]),
|
||||
{:ok, payload} <- Jason.encode(blockhash) do
|
||||
{:ok, payload}
|
||||
else
|
||||
err ->
|
||||
IO.inspect(err)
|
||||
:err
|
||||
end
|
||||
end
|
||||
|
||||
defp get_block(hash) do
|
||||
last_id = BlockData.get_block_id(:block_data);
|
||||
cond do
|
||||
(last_seen == last_id) ->
|
||||
payload = BlockData.get_json_block(:block_data);
|
||||
{:ok, payload}
|
||||
true -> :err
|
||||
if hash == last_id do
|
||||
payload = BlockData.get_json_block(:block_data);
|
||||
{:ok, payload, true}
|
||||
else
|
||||
with {:ok, 200, block} <- RPC.request(:rpc, "getblock", [hash, 2]),
|
||||
{:ok, cleaned} <- BlockData.clean_block(block),
|
||||
{:ok, payload} <- Jason.encode(cleaned) do
|
||||
{:ok, payload, false}
|
||||
else
|
||||
err ->
|
||||
IO.inspect(err);
|
||||
:err
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -64,9 +113,32 @@ defmodule BitcoinStream.Router do
|
||||
{:ok, payload} <- Jason.encode(%{tx: inflated_txn, blockheight: height, blockhash: blockhash, time: time}) do
|
||||
{:ok, payload}
|
||||
else
|
||||
{:ok, 500, nil} ->
|
||||
# specially handle the genesis coinbase transaction
|
||||
with true <- (txid == "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b"),
|
||||
rawtx <- Base.decode16!("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000", [case: :lower]),
|
||||
{:ok, txn } <- BitcoinTx.decode(rawtx),
|
||||
inflated_txn <- BitcoinTx.inflate(txn, false),
|
||||
{:ok, payload} <- Jason.encode(%{tx: inflated_txn, blockheight: 0, blockhash: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", time: 1231006505}) do
|
||||
{:ok, payload}
|
||||
else
|
||||
_ -> :err
|
||||
end
|
||||
|
||||
err ->
|
||||
IO.inspect(err);
|
||||
:err
|
||||
end
|
||||
end
|
||||
|
||||
defp get_tx_spends(txid) do
|
||||
with {:ok, spends} <- SpendIndex.get_tx_spends(:spends, txid),
|
||||
{:ok, payload} <- Jason.encode(spends) do
|
||||
{:ok, payload}
|
||||
else
|
||||
err ->
|
||||
IO.inspect(err)
|
||||
:err
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -13,6 +13,7 @@ defmodule BitcoinStream.Server do
|
||||
{ rpc_pool_size, "" } = Integer.parse(System.get_env("RPC_POOL_SIZE"));
|
||||
log_level = System.get_env("LOG_LEVEL");
|
||||
btc_host = System.get_env("BITCOIN_HOST");
|
||||
indexed = System.get_env("INDEXED")
|
||||
|
||||
case log_level do
|
||||
"debug" ->
|
||||
@ -38,6 +39,7 @@ defmodule BitcoinStream.Server do
|
||||
}},
|
||||
{ BitcoinStream.RPC, [host: btc_host, port: rpc_port, name: :rpc] },
|
||||
{ BitcoinStream.BlockData, [name: :block_data] },
|
||||
{ BitcoinStream.Index.Spend, [name: :spends, indexed: indexed]},
|
||||
Plug.Cowboy.child_spec(
|
||||
scheme: :http,
|
||||
plug: BitcoinStream.Router,
|
||||
|
424
server/lib/spend_index.ex
Normal file
424
server/lib/spend_index.ex
Normal file
@ -0,0 +1,424 @@
|
||||
require Logger
|
||||
|
||||
defmodule BitcoinStream.Index.Spend do
|
||||
|
||||
use GenServer
|
||||
|
||||
alias BitcoinStream.Protocol.Block, as: BitcoinBlock
|
||||
alias BitcoinStream.Protocol.Transaction, as: BitcoinTx
|
||||
alias BitcoinStream.RPC, as: RPC
|
||||
|
||||
def start_link(opts) do
|
||||
Logger.info("Starting Spend Index");
|
||||
{indexed, opts} = Keyword.pop(opts, :indexed);
|
||||
GenServer.start_link(__MODULE__, [indexed], opts)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init([indexed]) do
|
||||
:ets.new(:spend_cache, [:set, :public, :named_table]);
|
||||
if (indexed != nil) do
|
||||
{:ok, dbref} = :rocksdb.open(String.to_charlist("data/index/spend"), [create_if_missing: true]);
|
||||
Process.send_after(self(), :sync, 2000);
|
||||
{:ok, [dbref, indexed, false]}
|
||||
else
|
||||
{:ok, [nil, indexed, false]}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def terminate(_reason, [dbref, indexed, _done]) do
|
||||
if (indexed != nil) do
|
||||
:rocksdb.close(dbref)
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:sync, [dbref, indexed, done]) do
|
||||
if (indexed != nil) do
|
||||
case sync(dbref) do
|
||||
true ->
|
||||
{:noreply, [dbref, indexed, true]}
|
||||
|
||||
_ ->
|
||||
{:noreply, [dbref, indexed, false]}
|
||||
end
|
||||
else
|
||||
{:noreply, [dbref, indexed, done]}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(_event, state) do
|
||||
# if RPC responds after the calling process already timed out, garbled messages get dumped to handle_info
|
||||
# quietly discard
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:get_tx_spends, txid}, _from, [dbref, indexed, done]) do
|
||||
case get_transaction_spends(dbref, txid, (indexed != nil)) do
|
||||
{:ok, spends} ->
|
||||
{:reply, {:ok, spends}, [dbref, indexed, done]}
|
||||
|
||||
err ->
|
||||
Logger.error("failed to fetch tx spends");
|
||||
{:reply, err, [dbref, indexed, done]}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast(:new_block, [dbref, indexed, done]) do
|
||||
if (indexed != nil and done) do
|
||||
case sync(dbref) do
|
||||
true ->
|
||||
{:noreply, [dbref, indexed, true]}
|
||||
|
||||
_ ->
|
||||
{:noreply, [dbref, indexed, false]}
|
||||
end
|
||||
else
|
||||
Logger.info("Already building spend index");
|
||||
{:noreply, [dbref, indexed, false]}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:block_disconnected, hash}, [dbref, indexed, done]) do
|
||||
if (indexed != nil and done) do
|
||||
block_disconnected(dbref, hash)
|
||||
end
|
||||
{:noreply, [dbref, indexed, done]}
|
||||
end
|
||||
|
||||
def get_tx_spends(pid, txid) do
|
||||
GenServer.call(pid, {:get_tx_spends, txid}, 60000)
|
||||
catch
|
||||
:exit, reason ->
|
||||
case reason do
|
||||
{:timeout, _} -> {:error, :timeout}
|
||||
|
||||
_ -> {:error, reason}
|
||||
end
|
||||
|
||||
error -> {:error, error}
|
||||
end
|
||||
|
||||
def notify_block(pid, _hash) do
|
||||
GenServer.cast(pid, :new_block)
|
||||
end
|
||||
|
||||
def notify_block_disconnect(pid, hash) do
|
||||
GenServer.cast(pid, {:block_disconnected, hash})
|
||||
end
|
||||
|
||||
defp wait_for_ibd() do
|
||||
case RPC.get_node_status(:rpc) do
|
||||
:ok -> true
|
||||
|
||||
_ ->
|
||||
Logger.info("Waiting for node to come online and fully sync before synchronizing spend index");
|
||||
RPC.notify_on_ready(:rpc)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_index_height(dbref) do
|
||||
case :rocksdb.get(dbref, "height", []) do
|
||||
{:ok, <<height::integer-size(32)>>} ->
|
||||
height
|
||||
|
||||
:not_found ->
|
||||
-1
|
||||
|
||||
_ ->
|
||||
Logger.error("unexpected leveldb response")
|
||||
end
|
||||
end
|
||||
|
||||
defp get_chain_height() do
|
||||
case RPC.request(:rpc, "getblockcount", []) do
|
||||
{:ok, 200, height} ->
|
||||
height
|
||||
|
||||
_ ->
|
||||
Logger.error("unexpected RPC response");
|
||||
:err
|
||||
end
|
||||
end
|
||||
|
||||
defp get_block(height) do
|
||||
with {:ok, 200, blockhash} <- RPC.request(:rpc, "getblockhash", [height]),
|
||||
{:ok, 200, blockdata} <- RPC.request(:rpc, "getblock", [blockhash, 0]),
|
||||
{:ok, block} <- BitcoinBlock.parse(blockdata) do
|
||||
block
|
||||
end
|
||||
end
|
||||
|
||||
defp get_block_by_hash(hash) do
|
||||
with {:ok, 200, blockdata} <- RPC.request(:rpc, "getblock", [hash, 0]),
|
||||
{:ok, block} <- BitcoinBlock.parse(blockdata) do
|
||||
block
|
||||
end
|
||||
end
|
||||
|
||||
defp index_input(spendkey, input, spends) do
|
||||
case input do
|
||||
# coinbase (skip)
|
||||
%{prev_txid: "0000000000000000000000000000000000000000000000000000000000000000"} ->
|
||||
spends
|
||||
|
||||
%{prev_txid: txid, prev_vout: vout} ->
|
||||
binid = Base.decode16!(txid, [case: :lower])
|
||||
case spends[binid] do
|
||||
nil ->
|
||||
Map.put(spends, binid, [[vout, spendkey]])
|
||||
|
||||
a ->
|
||||
Map.put(spends, binid, [[vout, spendkey] | a])
|
||||
end
|
||||
|
||||
# unexpected input format (should never happen)
|
||||
_ ->
|
||||
spends
|
||||
end
|
||||
end
|
||||
|
||||
defp index_inputs(_binid, [], _vout, spends) do
|
||||
spends
|
||||
end
|
||||
defp index_inputs(binid, [vin | rest], vout, spends) do
|
||||
spends = index_input(binid <> <<vout::integer-size(24)>>, vin, spends);
|
||||
index_inputs(binid, rest, vout+1, spends)
|
||||
end
|
||||
|
||||
defp index_tx(%{id: txid, inputs: inputs}, spends) do
|
||||
binid = Base.decode16!(txid, [case: :lower]);
|
||||
index_inputs(binid, inputs, 0, spends)
|
||||
end
|
||||
|
||||
defp index_txs([], spends) do
|
||||
spends
|
||||
end
|
||||
defp index_txs([tx | rest], spends) do
|
||||
spends = index_tx(tx, spends);
|
||||
index_txs(rest, spends)
|
||||
end
|
||||
|
||||
defp index_block_inputs(dbref, batch, txns) do
|
||||
spends = index_txs(txns, %{});
|
||||
Enum.each(spends, fn {binid, outputs} ->
|
||||
case get_chain_spends(dbref, binid, true) do
|
||||
false ->
|
||||
Logger.error("uninitialised tx in input index: #{Base.encode16(binid, [case: :lower])}")
|
||||
:ok
|
||||
|
||||
prev ->
|
||||
:rocksdb.batch_put(batch, binid, fillBinarySpends(prev, outputs))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp init_block_txs(batch, txns) do
|
||||
Enum.each(txns, fn tx ->
|
||||
size = length(tx.outputs) * 35 * 8;
|
||||
binary_txid = Base.decode16!(tx.id, [case: :lower]);
|
||||
:rocksdb.batch_put(batch, binary_txid, <<0::integer-size(size)>>)
|
||||
end)
|
||||
end
|
||||
|
||||
defp index_block(dbref, height) do
|
||||
with block <- get_block(height),
|
||||
{:ok, batch} <- :rocksdb.batch(),
|
||||
:ok <- init_block_txs(batch, block.txns),
|
||||
:ok <- :rocksdb.write_batch(dbref, batch, []),
|
||||
{:ok, batch} <- :rocksdb.batch(),
|
||||
:ok <- index_block_inputs(dbref, batch, block.txns),
|
||||
:ok <- :rocksdb.write_batch(dbref, batch, []) do
|
||||
:ok
|
||||
else
|
||||
err ->
|
||||
Logger.error("error indexing block");
|
||||
IO.inspect(err);
|
||||
:err
|
||||
end
|
||||
end
|
||||
|
||||
# insert a 35-byte spend key into a binary spend index
|
||||
# (not sure how efficient this method is?)
|
||||
defp fillBinarySpend(bin, index, spendkey) do
|
||||
a_size = 35 * index;
|
||||
<<a::binary-size(a_size), _b::binary-size(35), c::binary>> = bin;
|
||||
<<a::binary, spendkey::binary, c::binary>>
|
||||
end
|
||||
defp fillBinarySpends(bin, []) do
|
||||
bin
|
||||
end
|
||||
defp fillBinarySpends(bin, [[index, spendkey] | rest]) do
|
||||
bin = fillBinarySpend(bin, index, spendkey);
|
||||
fillBinarySpends(bin, rest)
|
||||
end
|
||||
|
||||
# "erase" a spend by zeroing out the spend key
|
||||
defp clearBinarySpend(bin, index, _spendkey) do
|
||||
a_size = 35 * index;
|
||||
b_size = 35 * 8;
|
||||
<<a::binary-size(a_size), _b::binary-size(35), c::binary>> = bin;
|
||||
<<a::binary, <<0::integer-size(b_size)>>, c::binary>>
|
||||
end
|
||||
defp clearBinarySpends(bin, []) do
|
||||
bin
|
||||
end
|
||||
defp clearBinarySpends(bin, [[index, spendkey] | rest]) do
|
||||
bin = clearBinarySpend(bin, index, spendkey);
|
||||
clearBinarySpends(bin, rest)
|
||||
end
|
||||
|
||||
# On start up, check index height (load from leveldb) vs latest block height (load via rpc)
|
||||
# Until index height = block height, process next block
|
||||
defp sync(dbref) do
|
||||
wait_for_ibd();
|
||||
with index_height <- get_index_height(dbref),
|
||||
chain_height <- get_chain_height() do
|
||||
if index_height < chain_height do
|
||||
with :ok <- index_block(dbref, index_height + 1),
|
||||
:ok <- :rocksdb.put(dbref, "height", <<(index_height + 1)::integer-size(32)>>, []) do
|
||||
Logger.info("Built spend index for block #{index_height + 1}");
|
||||
Process.send_after(self(), :sync, 0);
|
||||
else
|
||||
_ ->
|
||||
Logger.error("Failed to build spend index");
|
||||
false
|
||||
end
|
||||
else
|
||||
Logger.info("Spend index fully synced to height #{index_height}");
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp get_chain_spends(dbref, binary_txid, use_index) do
|
||||
case (if use_index do :rocksdb.get(dbref, binary_txid, []) else :not_found end) do
|
||||
{:ok, spends} ->
|
||||
spends
|
||||
|
||||
:not_found ->
|
||||
# uninitialized, try to construct on-the-fly from RPC data
|
||||
txid = Base.encode16(binary_txid);
|
||||
with {:ok, 200, hextx} <- RPC.request(:rpc, "getrawtransaction", [txid]),
|
||||
rawtx <- Base.decode16!(hextx, case: :lower),
|
||||
{:ok, tx } <- BitcoinTx.decode(rawtx) do
|
||||
size = length(tx.outputs) * 35 * 8;
|
||||
<<0::integer-size(size)>>
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
|
||||
_ ->
|
||||
Logger.error("unexpected leveldb response");
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
defp unpack_spends(<<>>, spend_list) do
|
||||
Enum.reverse(spend_list)
|
||||
end
|
||||
# unspent outputs are zeroed out
|
||||
defp unpack_spends(<<0::integer-size(280), rest::binary>>, spend_list) do
|
||||
unpack_spends(rest, [false | spend_list])
|
||||
end
|
||||
defp unpack_spends(<<binary_txid::binary-size(32), index::integer-size(24), rest::binary>>, spend_list) do
|
||||
txid = Base.encode16(binary_txid, [case: :lower]);
|
||||
unpack_spends(rest, [[txid, index] | spend_list])
|
||||
end
|
||||
defp unpack_spends(false) do
|
||||
[]
|
||||
end
|
||||
defp unpack_spends(bin) do
|
||||
unpack_spends(bin, [])
|
||||
end
|
||||
|
||||
defp get_transaction_spends(dbref, txid, use_index) do
|
||||
binary_txid = Base.decode16!(txid, [case: :lower]);
|
||||
chain_spends = get_chain_spends(dbref, binary_txid, use_index);
|
||||
spend_list = unpack_spends(chain_spends);
|
||||
spend_list = add_mempool_spends(txid, spend_list);
|
||||
{:ok, spend_list}
|
||||
end
|
||||
|
||||
defp add_mempool_spends(_txid, _index, [], added) do
|
||||
Enum.reverse(added)
|
||||
end
|
||||
defp add_mempool_spends(txid, index, [false | rest], added) do
|
||||
case :ets.lookup(:spend_cache, [txid, index]) do
|
||||
[] ->
|
||||
add_mempool_spends(txid, index + 1, rest, [false | added])
|
||||
|
||||
[{[_index, _txid], spend}] ->
|
||||
add_mempool_spends(txid, index + 1, rest, [spend | added])
|
||||
end
|
||||
end
|
||||
defp add_mempool_spends(txid, index, [spend | rest], added) do
|
||||
add_mempool_spends(txid, index + 1, rest, [spend | added])
|
||||
end
|
||||
defp add_mempool_spends(txid, spend_list) do
|
||||
add_mempool_spends(txid, 0, spend_list, [])
|
||||
end
|
||||
|
||||
defp stack_dropped_blocks(dbref, hash, undo_stack, min_height) do
|
||||
# while we're below the latest processed height
|
||||
# keep adding blocks to the undo stack until we find an ancestor in the main chain
|
||||
with {:ok, 200, blockdata} <- RPC.request(:rpc, "getblock", [hash, 1]),
|
||||
index_height <- get_index_height(dbref),
|
||||
true <- (blockdata["height"] <= index_height),
|
||||
true <- (blockdata["confirmations"] < 0) do
|
||||
stack_dropped_blocks(dbref, blockdata["previousblockhash"], [hash | undo_stack], blockdata["height"])
|
||||
else
|
||||
_ -> [undo_stack, min_height]
|
||||
end
|
||||
end
|
||||
|
||||
defp drop_block_inputs(dbref, batch, txns) do
|
||||
spends = index_txs(txns, %{});
|
||||
Enum.each(spends, fn {binid, outputs} ->
|
||||
case get_chain_spends(dbref, binid, true) do
|
||||
false ->
|
||||
Logger.error("uninitialised tx in input index: #{Base.encode16(binid, [case: :lower])}")
|
||||
:ok
|
||||
|
||||
prev ->
|
||||
:rocksdb.batch_put(batch, binid, clearBinarySpends(prev, outputs))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp drop_block(dbref, hash) do
|
||||
with block <- get_block_by_hash(hash),
|
||||
{:ok, batch} <- :rocksdb.batch(),
|
||||
:ok <- drop_block_inputs(dbref, batch, block.txns),
|
||||
:ok <- :rocksdb.write_batch(dbref, batch, []) do
|
||||
:ok
|
||||
else
|
||||
err ->
|
||||
Logger.error("error indexing block");
|
||||
IO.inspect(err);
|
||||
:err
|
||||
end
|
||||
end
|
||||
|
||||
defp drop_blocks(_dbref, []) do
|
||||
:ok
|
||||
end
|
||||
defp drop_blocks(dbref, [hash | rest]) do
|
||||
drop_block(dbref, hash);
|
||||
drop_blocks(dbref, rest)
|
||||
end
|
||||
defp block_disconnected(dbref, hash) do
|
||||
[undo_stack, min_height] = stack_dropped_blocks(dbref, hash, [], nil);
|
||||
drop_blocks(dbref, undo_stack);
|
||||
if (min_height != nil) do
|
||||
:rocksdb.put(dbref, "height", <<(min_height - 1)::integer-size(32)>>, [])
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
@ -41,7 +41,8 @@ defmodule BitcoinStream.MixProject do
|
||||
{:plug, "~> 1.13"},
|
||||
{:corsica, "~> 1.0"},
|
||||
{:plug_cowboy, "~> 2.0"},
|
||||
{:jason, "~> 1.1"}
|
||||
{:jason, "~> 1.1"},
|
||||
{:rocksdb, "~> 1.6"}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
@ -9,7 +9,9 @@
|
||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
||||
"cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
|
||||
"decimal": {:hex, :decimal, "1.9.0", "83e8daf59631d632b171faabafb4a9f4242c514b0a06ba3df493951c08f64d07", [:mix], [], "hexpm", "b1f2343568eed6928f3e751cf2dffde95bfaa19dd95d09e8a9ea92ccfd6f7d85"},
|
||||
"eleveldb": {:hex, :eleveldb, "2.2.20", "1fff63a5055bbf4bf821f797ef76065882b193f5e8095f95fcd9287187773b58", [:rebar3], [], "hexpm", "0e67df12ef836a7bcdde9373c59f1ae18b335defd1d66b820d3d4dd7ca1844e2"},
|
||||
"elixometer": {:hex, :elixometer, "1.4.0", "c16f5ac3f369bb1747de059a95e46f86e097d9adb3de5666e997922089c396d1", [:mix], [{:exometer_core, "~> 1.5", [hex: :exometer_core, repo: "hexpm", optional: false]}, {:lager, ">= 3.2.1", [hex: :lager, repo: "hexpm", optional: false]}, {:pobox, "~> 1.2", [hex: :pobox, repo: "hexpm", optional: false]}], "hexpm", "9cd6c8fca17600e3958bbea65c1274ac5baffa03d919b652bdf1f33b5aec64f2"},
|
||||
"exleveldb": {:hex, :exleveldb, "0.14.0", "8e9353bbce38482d6971d254c6b98ceb50f3f179c94732b5d17db1be426fca18", [:mix], [{:eleveldb, "~> 2.2.20", [hex: :eleveldb, repo: "hexpm", optional: false]}], "hexpm", "803cd3b4c826a1e17e7e28f6afe224837a743b475e1a48336f186af3dd8636ad"},
|
||||
"exometer_core": {:hex, :exometer_core, "1.5.7", "ab97e34a5d69ab14e6ae161db4cca5b5e655e635b842f830ee6ab2cbfcfdc30a", [:rebar3], [{:folsom, "0.8.7", [hex: :folsom, repo: "hexpm", optional: false]}, {:hut, "1.2.1", [hex: :hut, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:setup, "2.0.2", [hex: :setup, repo: "hexpm", optional: false]}], "hexpm", "6afbd8f6b1aaf7443d6a5a05bbbcd15285622353550f3077c87176e25be99c1e"},
|
||||
"finch": {:hex, :finch, "0.10.2", "9ad27d68270d879f73f26604bb2e573d40f29bf0e907064a9a337f90a16a0312", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dd8b11b282072cec2ef30852283949c248bd5d2820c88d8acc89402b81db7550"},
|
||||
"folsom": {:hex, :folsom, "0.8.7", "a885f0aeee4c84270954c88a55a5a473d6b2c7493e32ffdc5765412dd555a951", [:rebar3], [{:bear, "0.8.7", [hex: :bear, repo: "hexpm", optional: false]}], "hexpm", "f7b644fc002a75af00b8bfbd3cc5c2bd955e09a118d2982d9a6c04e5646ff367"},
|
||||
@ -33,6 +35,7 @@
|
||||
"plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
|
||||
"pobox": {:hex, :pobox, "1.2.0", "3127cb48f13d18efec7a9ea2622077f4f9c5f067cc1182af1977dacd7a74fdb8", [:rebar3], [], "hexpm", "25d6fcdbe4fedbbf4bcaa459fadee006e75bb3281d4e6c9b2dc0ee93c51920c4"},
|
||||
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||
"rocksdb": {:hex, :rocksdb, "1.7.0", "5d23319998a7fce5ffd5d7824116c905caba7f91baf8eddabd0180f1bb272cef", [:rebar3], [], "hexpm", "a4bdc5dd80ed137161549713062131e8240523787ebe7b51df61cfb48b1786ce"},
|
||||
"setup": {:hex, :setup, "2.0.2", "1203f4cda11306c2e34434244576ded0a7bbfb0908d9a572356c809bd0cdf085", [:rebar3], [], "hexpm", "7d6aaf5281d0b0c40980e128f9dc410dacd03799a8577201d4c8b43e7f97509a"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
|
||||
"telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"},
|
||||
|
Loading…
Reference in New Issue
Block a user