mirror of
https://github.com/Retropex/bitfeed.git
synced 2025-05-12 19:20:46 +02:00
Basic block explorer & entry/exit transitions
This commit is contained in:
parent
b5bcaf2377
commit
a827ac036b
@ -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,8 +5,8 @@
|
||||
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 } from '../stores.js'
|
||||
import { formatCurrency } from '../utils/fx.js'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
@ -47,8 +47,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime (time) {
|
||||
return timeFormat.format(time)
|
||||
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 +89,12 @@
|
||||
}
|
||||
|
||||
function hideBlock () {
|
||||
analytics.trackEvent('viz', 'block', 'hide')
|
||||
dispatch('hideBlock')
|
||||
if (block && block.height != $latestBlockHeight) {
|
||||
dispatch('quitExploring')
|
||||
} else {
|
||||
analytics.trackEvent('viz', 'block', 'hide')
|
||||
dispatch('hideBlock')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -229,15 +248,15 @@
|
||||
|
||||
{#each [block] as block (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 +279,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,7 +292,7 @@
|
||||
</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) }}" >
|
||||
<button class="close-button standalone" on:click={hideBlock} out:fly={flyOut} in:fly={flyIn} >
|
||||
<Icon icon={closeIcon} color="var(--palette-x)" />
|
||||
</button>
|
||||
{/if}
|
||||
|
@ -207,7 +207,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'
|
||||
|
@ -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>
|
||||
|
@ -110,6 +110,10 @@
|
||||
$blockVisible = false
|
||||
}
|
||||
|
||||
function quitExploring () {
|
||||
if (txController) txController.resumeLatest()
|
||||
}
|
||||
|
||||
function fakeBlock () {
|
||||
const block = txController.simulateBlock()
|
||||
// txController.addBlock(new BitcoinBlock({
|
||||
@ -492,7 +496,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" />
|
||||
|
@ -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 } 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,15 @@ export default class TxController {
|
||||
colorMode.subscribe(mode => {
|
||||
this.setColorMode(mode)
|
||||
})
|
||||
explorerBlockData.subscribe(blockData => {
|
||||
console.log('explorerBlock changed: ', blockData)
|
||||
if (blockData) {
|
||||
this.exploreBlock(blockData)
|
||||
} else {
|
||||
this.resumeLatest()
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
getVertexData () {
|
||||
@ -54,7 +67,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 +77,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 +94,9 @@ export default class TxController {
|
||||
if (this.blockScene) {
|
||||
this.blockScene.setColorMode(mode)
|
||||
}
|
||||
if (this.explorerBlockScene) {
|
||||
this.explorerBlockScene.setColorMode(mode)
|
||||
}
|
||||
}
|
||||
|
||||
applyHighlighting () {
|
||||
@ -84,6 +104,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 +148,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 +215,15 @@ 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) {
|
||||
blockTransitionDirection.set(null)
|
||||
currentBlock.set(block)
|
||||
}
|
||||
} else {
|
||||
this.poolScene.scrollLock = true
|
||||
for (let i = 0; i < block.txns.length; i++) {
|
||||
@ -205,21 +262,87 @@ export default class TxController {
|
||||
this.poolScene.scrollLock = false
|
||||
this.poolScene.layoutAll()
|
||||
}, 5500)
|
||||
|
||||
blockTransitionDirection.set(null)
|
||||
currentBlock.set(block)
|
||||
}
|
||||
|
||||
currentBlock.set(block)
|
||||
this.block = block
|
||||
|
||||
return block
|
||||
}
|
||||
|
||||
exploreBlock (blockData) {
|
||||
const block = 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()
|
||||
}
|
||||
}
|
||||
@ -228,8 +351,8 @@ export default class TxController {
|
||||
if (this.blockScene) {
|
||||
this.blockScene.expire()
|
||||
}
|
||||
this.block = null
|
||||
currentBlock.set(null)
|
||||
if (this.blockVisibleUnsub) this.blockVisibleUnsub()
|
||||
}
|
||||
|
||||
destroyTx (id) {
|
||||
@ -243,7 +366,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 +378,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) {
|
||||
@ -268,9 +393,16 @@ export default class TxController {
|
||||
this.selectedTx = selected
|
||||
selectedTx.set(this.selectedTx)
|
||||
if (sameTx && this.selectedTx) {
|
||||
detailTx.set(this.selectedTx)
|
||||
overlay.set('tx')
|
||||
if (!this.selectedTx.is_inflated) {
|
||||
loading.increment()
|
||||
await searchTx(this.selectedTx.id)
|
||||
loading.decrement()
|
||||
} else {
|
||||
detailTx.set(this.selectedTx)
|
||||
overlay.set('tx')
|
||||
}
|
||||
}
|
||||
console.log(this.selectedTx)
|
||||
this.selectionLocked = !!this.selectedTx && !(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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,42 +7,58 @@ 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.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,16 +75,16 @@ 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: {
|
||||
@ -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] }}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -119,6 +119,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 +253,11 @@ export default class TxBlockScene extends TxMondrianPoolScene {
|
||||
// }
|
||||
}
|
||||
|
||||
initialLayout () {
|
||||
initialLayout (exited) {
|
||||
this.prepareAll()
|
||||
setTimeout(() => {
|
||||
this.layoutAll()
|
||||
if (exited) this.exitRight()
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
@ -199,7 +279,7 @@ export default class TxBlockScene extends TxMondrianPoolScene {
|
||||
}
|
||||
}
|
||||
|
||||
expire () {
|
||||
expire (delay=3000) {
|
||||
this.expired = true
|
||||
this.hide()
|
||||
setTimeout(() => {
|
||||
@ -208,6 +288,13 @@ export default class TxBlockScene extends TxMondrianPoolScene {
|
||||
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) {
|
||||
|
@ -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)
|
||||
|
@ -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,7 +137,34 @@ 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()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
export async function searchTx (txid, input, output) {
|
||||
try {
|
||||
const searchResult = await fetchTx(txid)
|
||||
if (searchResult) {
|
||||
@ -149,6 +182,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
|
||||
}
|
||||
}
|
||||
|
4
server/.gitignore
vendored
4
server/.gitignore
vendored
@ -31,4 +31,8 @@ bitcoin_stream-*.tar
|
||||
!log/.gitkeep
|
||||
*.log
|
||||
|
||||
# Blocks
|
||||
!data/block/.gitkeep
|
||||
/data/block/**
|
||||
|
||||
.envrc
|
||||
|
0
server/data/.gitkeep
Normal file → Executable file
0
server/data/.gitkeep
Normal file → Executable 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
|
||||
|
@ -5,11 +5,12 @@ 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
|
||||
|
||||
plug Corsica, origins: "*", allow_headers: :all
|
||||
plug Plug.Static,
|
||||
at: "/",
|
||||
at: "data",
|
||||
from: :bitcoin_stream
|
||||
plug :match
|
||||
plug Plug.Parsers,
|
||||
@ -18,21 +19,32 @@ 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")
|
||||
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");
|
||||
@ -44,13 +56,33 @@ defmodule BitcoinStream.Router 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);
|
||||
if hash == last_id do
|
||||
payload = BlockData.get_json_block(:block_data);
|
||||
{:ok, payload}
|
||||
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}
|
||||
true -> :err
|
||||
else
|
||||
err ->
|
||||
IO.inspect(err);
|
||||
:err
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user