mirror of
https://github.com/Retropex/bitfeed.git
synced 2025-05-12 19:20:46 +02:00
commit
a650445c1f
@ -1,6 +1,7 @@
|
|||||||
map $sent_http_content_type $expires {
|
map $sent_http_content_type $expires {
|
||||||
default off;
|
default off;
|
||||||
text/css max;
|
text/css max;
|
||||||
|
text/json max;
|
||||||
application/javascript max;
|
application/javascript max;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -14,15 +15,10 @@ server {
|
|||||||
|
|
||||||
server_name client;
|
server_name client;
|
||||||
|
|
||||||
location = / {
|
location ~* \.(html)$ {
|
||||||
add_header Cache-Control 'no-cache';
|
add_header Cache-Control 'no-cache';
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ =404;
|
|
||||||
expires $expires;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api {
|
location /api {
|
||||||
proxy_cache bitfeed;
|
proxy_cache bitfeed;
|
||||||
proxy_cache_revalidate on;
|
proxy_cache_revalidate on;
|
||||||
@ -42,6 +38,11 @@ server {
|
|||||||
proxy_set_header Host $http_host;
|
proxy_set_header Host $http_host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
expires $expires;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
upstream wsmonobackend {
|
upstream wsmonobackend {
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rollup -c",
|
"build": "rollup -c",
|
||||||
"dev": "rollup -c -w",
|
"dev": "rollup -c -w",
|
||||||
"start": "sirv public"
|
"start": "sirv public --single"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.16.5",
|
"@babel/core": "^7.16.5",
|
||||||
|
@ -2,9 +2,12 @@
|
|||||||
import TxViz from './components/TxViz.svelte'
|
import TxViz from './components/TxViz.svelte'
|
||||||
import analytics from './utils/analytics.js'
|
import analytics from './utils/analytics.js'
|
||||||
import config from './config.js'
|
import config from './config.js'
|
||||||
import { settings } from './stores.js'
|
import Router from './controllers/Router.js'
|
||||||
|
import {settings} from './stores.js'
|
||||||
|
|
||||||
if (!$settings.noTrack && config.public) analytics.init()
|
if (!$settings.noTrack && config.public) analytics.init()
|
||||||
|
|
||||||
|
const router = new Router(window.location.pathname)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
@ -62,18 +62,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let transitionDirection
|
||||||
let flyIn
|
let flyIn
|
||||||
let flyOut
|
let flyOut
|
||||||
$: {
|
$: {
|
||||||
if ($blockTransitionDirection && $blockTransitionDirection === 'right') {
|
if (!$blockTransitionDirection || !visible || !block || !$blocksEnabled) {
|
||||||
|
transitionDirection = 'up'
|
||||||
|
flyIn = { y: (restoring ? -50 : 50), duration: (restoring ? 500 : 1000), easing: linear, delay: (restoring ? 0 : newBlockDelay) }
|
||||||
|
flyOut = { y: -50, duration: 2000, easing: linear }
|
||||||
|
} else if ($blockTransitionDirection && $blockTransitionDirection === 'right') {
|
||||||
|
transitionDirection = 'right'
|
||||||
flyIn = { x: 100, easing: linear, delay: 1000, duration: 1000 }
|
flyIn = { x: 100, easing: linear, delay: 1000, duration: 1000 }
|
||||||
flyOut = { x: -100, easing: linear, delay: 0, duration: 1000 }
|
flyOut = { x: -100, easing: linear, delay: 0, duration: 1000 }
|
||||||
} else if ($blockTransitionDirection && $blockTransitionDirection === 'left') {
|
} else if ($blockTransitionDirection && $blockTransitionDirection === 'left') {
|
||||||
|
transitionDirection = 'left'
|
||||||
flyIn = { x: -100, easing: linear, delay: 1000, duration: 1000 }
|
flyIn = { x: -100, easing: linear, delay: 1000, duration: 1000 }
|
||||||
flyOut = { x: 100, easing: linear, delay: 0, duration: 1000 }
|
flyOut = { x: 100, easing: linear, delay: 0, duration: 1000 }
|
||||||
} else {
|
} else {
|
||||||
flyIn = { y: (restoring ? -50 : 50), duration: (restoring ? 500 : 1000), easing: linear, delay: (restoring ? 0 : newBlockDelay) }
|
transitionDirection = 'down'
|
||||||
flyOut = { y: -50, duration: 2000, easing: linear }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,9 +121,9 @@
|
|||||||
async function explorePrevBlock (e) {
|
async function explorePrevBlock (e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!$loading && block) {
|
if (!$loading && block) {
|
||||||
$loading = true
|
loading.increment()
|
||||||
await searchBlockHeight(block.height - 1)
|
await searchBlockHeight(block.height - 1)
|
||||||
$loading = false
|
loading.decrement()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,9 +131,9 @@
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!$loading && block) {
|
if (!$loading && block) {
|
||||||
if (block.height + 1 < $latestBlockHeight) {
|
if (block.height + 1 < $latestBlockHeight) {
|
||||||
$loading = true
|
loading.increment()
|
||||||
await searchBlockHeight(block.height + 1)
|
await searchBlockHeight(block.height + 1)
|
||||||
$loading = false
|
loading.decrement()
|
||||||
} else {
|
} else {
|
||||||
dispatch('quitExploring')
|
dispatch('quitExploring')
|
||||||
}
|
}
|
||||||
@ -155,6 +161,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.block-info-container {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.block-info {
|
.block-info {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: calc(100% + 0.25rem);
|
bottom: calc(100% + 0.25rem);
|
||||||
@ -319,68 +333,71 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
{#key block}
|
{#key transitionDirection}
|
||||||
{#if block != null && visible && $blocksEnabled }
|
{#each ((block != null && visible && $blocksEnabled) ? [block] : []) as block (block.id)}
|
||||||
<div class="block-info" out:fly={flyOut} in:fly={flyIn}>
|
<div class="block-info-container" out:fly|local={flyOut} in:fly|local={flyIn}>
|
||||||
<!-- <span class="data-field">Hash: { block.id }</span> -->
|
<div class="block-info">
|
||||||
<div class="full-size">
|
<!-- <span class="data-field">Hash: { block.id }</span> -->
|
||||||
<div class="data-row">
|
<div class="full-size">
|
||||||
<span class="data-field title-field" title="{block.miner_sig}"><b>{#if block.height == $latestBlockHeight}Latest {/if}Block: </b>{ numberFormat.format(block.height) }</span>
|
<div class="data-row">
|
||||||
<button class="data-field close-button" on:click={hideBlock}><Icon icon={closeIcon} color="var(--palette-x)" /></button>
|
<span class="data-field title-field" title="{block.miner_sig}"><b>{#if block.height == $latestBlockHeight}Latest {/if}Block: </b>{ numberFormat.format(block.height) }</span>
|
||||||
</div>
|
<button class="data-field close-button" on:click={hideBlock}><Icon icon={closeIcon} color="var(--palette-x)" /></button>
|
||||||
<div class="data-row">
|
</div>
|
||||||
<span class="data-field" title="block timestamp">{ formatDateTime(block.time) }</span>
|
<div class="data-row">
|
||||||
<span class="data-field">{ formattedBlockValue }</span>
|
<span class="data-field" title="block timestamp">{ formatDateTime(block.time) }</span>
|
||||||
</div>
|
<span class="data-field">{ formattedBlockValue }</span>
|
||||||
<div class="data-row">
|
</div>
|
||||||
<span class="data-field">{ formatBytes(block.bytes) }</span>
|
<div class="data-row">
|
||||||
<span class="data-field">{ formatCount(block.txnCount) } transactions</span>
|
|
||||||
</div>
|
|
||||||
<div class="data-row spacer"> </div>
|
|
||||||
<div class="data-row">
|
|
||||||
<span class="data-field">Avg fee rate</span>
|
|
||||||
{#if block.fees != null}
|
|
||||||
<span class="data-field">{ formatFee(block.avgFeerate) } sats/vbyte</span>
|
|
||||||
{:else}
|
|
||||||
<span class="data-field">unavailable</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="compact">
|
|
||||||
<div class="data-row">
|
|
||||||
<span class="data-field title-field" title="{block.miner_sig}"><b>Latest 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">{ formatDateTime(block.time) }</span>
|
|
||||||
<span class="data-field">{ formattedBlockValue }</span>
|
|
||||||
</div>
|
|
||||||
<div class="data-row">
|
|
||||||
<span class="data-field">{ formatCount(block.txnCount) } transactions</span>
|
|
||||||
{#if block.fees != null}
|
|
||||||
<span class="data-field">{ formatFee(block.avgFeerate) } sats/vb</span>
|
|
||||||
{:else}
|
|
||||||
<span class="data-field">{ formatBytes(block.bytes) }</span>
|
<span class="data-field">{ formatBytes(block.bytes) }</span>
|
||||||
{/if}
|
<span class="data-field">{ formatCount(block.txnCount) } transaction{block.txnCount == 1 ? '' : 's'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="data-row spacer"> </div>
|
||||||
|
<div class="data-row">
|
||||||
|
<span class="data-field">Avg fee rate</span>
|
||||||
|
{#if block.fees != null}
|
||||||
|
<span class="data-field">{ formatFee(block.avgFeerate) } sats/vbyte</span>
|
||||||
|
{:else}
|
||||||
|
<span class="data-field">unavailable</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="compact">
|
||||||
|
<div class="data-row">
|
||||||
|
<span class="data-field title-field" title="{block.miner_sig}"><b>Latest 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">{ formatDateTime(block.time) }</span>
|
||||||
|
<span class="data-field">{ formattedBlockValue }</span>
|
||||||
|
</div>
|
||||||
|
<div class="data-row">
|
||||||
|
<span class="data-field">{ formatCount(block.txnCount) } transactions</span>
|
||||||
|
{#if block.fees != null}
|
||||||
|
<span class="data-field">{ formatFee(block.avgFeerate) } sats/vb</span>
|
||||||
|
{:else}
|
||||||
|
<span class="data-field">{ formatBytes(block.bytes) }</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if hasPrevBlock }
|
||||||
|
<a href="/block/height/{block.height - 1}" on:click={explorePrevBlock} class="explore-button prev">
|
||||||
|
<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">
|
||||||
|
<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}>
|
||||||
|
<Icon icon={closeIcon} color="var(--palette-x)" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{#if hasPrevBlock }
|
{/each}
|
||||||
<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}
|
|
||||||
{/key}
|
{/key}
|
||||||
|
@ -17,7 +17,7 @@ import clipboardIcon from '../assets/icon/cil-clipboard.svg'
|
|||||||
import twitterIcon from '../assets/icon/cib-twitter.svg'
|
import twitterIcon from '../assets/icon/cib-twitter.svg'
|
||||||
import { fade, fly } from 'svelte/transition'
|
import { fade, fly } from 'svelte/transition'
|
||||||
import { durationFormat } from '../utils/format.js'
|
import { durationFormat } from '../utils/format.js'
|
||||||
import { overlay, tiers } from '../stores.js'
|
import { overlay, tiers, urlPath } from '../stores.js'
|
||||||
import QRCode from 'qrcode'
|
import QRCode from 'qrcode'
|
||||||
|
|
||||||
let tab = 'form' // form | invoice | success
|
let tab = 'form' // form | invoice | success
|
||||||
@ -164,6 +164,7 @@ $: {
|
|||||||
}
|
}
|
||||||
$: {
|
$: {
|
||||||
if ($overlay === 'donation') {
|
if ($overlay === 'donation') {
|
||||||
|
$urlPath = '/donate'
|
||||||
startExpiryTimer()
|
startExpiryTimer()
|
||||||
stopPollingInvoice()
|
stopPollingInvoice()
|
||||||
pollingEnabled = true
|
pollingEnabled = true
|
||||||
@ -416,9 +417,13 @@ async function copyInvoice () {
|
|||||||
}
|
}
|
||||||
analytics.trackEvent('donations', 'invoice', 'copy')
|
analytics.trackEvent('donations', 'invoice', 'copy')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onClose () {
|
||||||
|
$urlPath = "/"
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Overlay name="donation" fullSize>
|
<Overlay name="donation" fullSize on:close={onClose}>
|
||||||
<section class="donation-modal">
|
<section class="donation-modal">
|
||||||
<div class="tab-nav">
|
<div class="tab-nav">
|
||||||
<button class="to left" class:disabled={!canTabLeft} on:click={tabLeft}>←</button>
|
<button class="to left" class:disabled={!canTabLeft} on:click={tabLeft}>←</button>
|
||||||
|
@ -54,7 +54,7 @@ async function searchSubmit (e) {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
if (matchedQuery && matchedQuery.query !== 'address') {
|
if (matchedQuery && matchedQuery.query !== 'address') {
|
||||||
$loading++
|
loading.increment()
|
||||||
let searchErr
|
let searchErr
|
||||||
switch(matchedQuery.query) {
|
switch(matchedQuery.query) {
|
||||||
case 'txid':
|
case 'txid':
|
||||||
@ -79,7 +79,7 @@ async function searchSubmit (e) {
|
|||||||
}
|
}
|
||||||
if (searchErr == null) errorMessage = null
|
if (searchErr == null) errorMessage = null
|
||||||
else handleSearchError(searchErr)
|
else handleSearchError(searchErr)
|
||||||
$loading--
|
loading.decrement()
|
||||||
} else {
|
} else {
|
||||||
errorMessage = 'enter a transaction id, block hash or block height'
|
errorMessage = 'enter a transaction id, block hash or block height'
|
||||||
}
|
}
|
||||||
@ -219,7 +219,7 @@ async function searchSubmit (e) {
|
|||||||
|
|
||||||
<div class="input-wrapper" transition:fly={{ y: -25 }}>
|
<div class="input-wrapper" transition:fly={{ y: -25 }}>
|
||||||
<form class="search-form" action="" on:submit={searchSubmit}>
|
<form class="search-form" action="" on:submit={searchSubmit}>
|
||||||
<input class="search-input" type="text" bind:value={query} placeholder="Enter a txid">
|
<input class="search-input" type="text" bind:value={query} placeholder="txid, block id or block height">
|
||||||
<div class="clear-button" class:disabled={query == null || query === ''} on:click={clearInput} title="Clear">
|
<div class="clear-button" class:disabled={query == null || query === ''} on:click={clearInput} title="Clear">
|
||||||
<Icon icon={CrossIcon}/>
|
<Icon icon={CrossIcon}/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,17 +3,18 @@ import Overlay from '../components/Overlay.svelte'
|
|||||||
import Icon from './Icon.svelte'
|
import Icon from './Icon.svelte'
|
||||||
import BookmarkIcon from '../assets/icon/cil-bookmark.svg'
|
import BookmarkIcon from '../assets/icon/cil-bookmark.svg'
|
||||||
import { longBtcFormat, numberFormat, feeRateFormat, dateFormat } from '../utils/format.js'
|
import { longBtcFormat, numberFormat, feeRateFormat, dateFormat } from '../utils/format.js'
|
||||||
import { exchangeRates, settings, sidebarToggle, newHighlightQuery, highlightingFull, detailTx, pageWidth, latestBlockHeight, highlightInOut, loading } from '../stores.js'
|
import { exchangeRates, settings, sidebarToggle, newHighlightQuery, highlightingFull, detailTx, pageWidth, latestBlockHeight, highlightInOut, loading, urlPath, currentBlock, overlay, explorerBlockData } from '../stores.js'
|
||||||
import { formatCurrency } from '../utils/fx.js'
|
import { formatCurrency } from '../utils/fx.js'
|
||||||
import { hlToHex, mixColor, teal, purple } from '../utils/color.js'
|
import { hlToHex, mixColor, teal, purple } from '../utils/color.js'
|
||||||
import { SPKToAddress } from '../utils/encodings.js'
|
import { SPKToAddress } from '../utils/encodings.js'
|
||||||
import api from '../utils/api.js'
|
import api from '../utils/api.js'
|
||||||
import { searchTx } from '../utils/search.js'
|
import { searchTx, searchBlockHash, searchBlockHeight } from '../utils/search.js'
|
||||||
import { fade } from 'svelte/transition'
|
import { fade } from 'svelte/transition'
|
||||||
|
|
||||||
function onClose () {
|
function onClose () {
|
||||||
$detailTx = null
|
$detailTx = null
|
||||||
$highlightInOut = null
|
$highlightInOut = null
|
||||||
|
$urlPath = "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatBTC (sats) {
|
function formatBTC (sats) {
|
||||||
@ -258,29 +259,41 @@ function getMiterOffset (weight, dy, dx) {
|
|||||||
async function clickItem (item) {
|
async function clickItem (item) {
|
||||||
if (item.rest) {
|
if (item.rest) {
|
||||||
truncate = false
|
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) {
|
|
||||||
// $loading++
|
|
||||||
// await searchTx(item.spend.txid, item.spend.vin)
|
|
||||||
// $loading--
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function goToInput(e, input) {
|
async function goToInput(e, input) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
$loading++
|
loading.increment()
|
||||||
await searchTx(input.prev_txid, null, input.prev_vout)
|
await searchTx(input.prev_txid, null, input.prev_vout)
|
||||||
$loading--
|
loading.decrement()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function goToOutput(e, output) {
|
async function goToOutput(e, output) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
$loading++
|
loading.increment()
|
||||||
await searchTx(output.spend.txid, output.spend.vin)
|
await searchTx(output.spend.txid, output.spend.vin)
|
||||||
$loading--
|
loading.decrement()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function goToBlock(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
// ignore click if it was triggered while selecting text, or if we don't have a block to go to
|
||||||
|
if (!$detailTx || !$detailTx.block || !!window.getSelection().toString()) return
|
||||||
|
|
||||||
|
let hash = $detailTx.block.hash || $detailTx.block.id
|
||||||
|
let height = $detailTx.block.height
|
||||||
|
if (hash === $currentBlock.id) {
|
||||||
|
$overlay = null
|
||||||
|
} else if (height == $latestBlockHeight) {
|
||||||
|
$explorerBlockData = null
|
||||||
|
$overlay = null
|
||||||
|
} else if (hash) {
|
||||||
|
loading.increment()
|
||||||
|
await searchBlockHash($detailTx.block.hash || $detailTx.block.id)
|
||||||
|
loading.decrement()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -361,6 +374,14 @@ async function goToOutput(e, output) {
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: text;
|
||||||
|
&:hover {
|
||||||
|
background: var(--palette-a);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.fields {
|
.fields {
|
||||||
@ -561,7 +582,7 @@ async function goToOutput(e, output) {
|
|||||||
{/if}
|
{/if}
|
||||||
<h2><span class="title">{#if $detailTx.isCoinbase }Coinbase{:else}Transaction{/if}</span> <span class="tx-id">{ $detailTx.id }</span></h2>
|
<h2><span class="title">{#if $detailTx.isCoinbase }Coinbase{:else}Transaction{/if}</span> <span class="tx-id">{ $detailTx.id }</span></h2>
|
||||||
{#if $detailTx.block}
|
{#if $detailTx.block}
|
||||||
<div class="pane fields">
|
<a class="pane fields clickable" href="/block/{$detailTx.block.hash || $detailTx.block.id}" draggable="false" on:click={goToBlock}>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<span class="label">confirmed</span>
|
<span class="label">confirmed</span>
|
||||||
<span class="value" style="color: {feeColor};">{ dateFormat.format($detailTx.block.time) }</span>
|
<span class="value" style="color: {feeColor};">{ dateFormat.format($detailTx.block.time) }</span>
|
||||||
@ -571,7 +592,7 @@ async function goToOutput(e, output) {
|
|||||||
<span class="label">block height</span>
|
<span class="label">block height</span>
|
||||||
<span class="value" style="color: {feeColor};">{ numberFormat.format($detailTx.block.height) }</span>
|
<span class="value" style="color: {feeColor};">{ numberFormat.format($detailTx.block.height) }</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $detailTx.isCoinbase}
|
{#if $detailTx.isCoinbase}
|
||||||
<div class="pane fields">
|
<div class="pane fields">
|
||||||
|
104
client/src/controllers/Router.js
Normal file
104
client/src/controllers/Router.js
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { urlPath, settings, loading, detailTx, highlightInOut, explorerBlockData, overlay } from '../stores.js'
|
||||||
|
import { searchTx, searchBlockHash, searchBlockHeight } from '../utils/search.js'
|
||||||
|
|
||||||
|
export default class Router {
|
||||||
|
constructor (initialPath = '/') {
|
||||||
|
this.path = initialPath
|
||||||
|
this.apply(initialPath)
|
||||||
|
urlPath.subscribe(val => {
|
||||||
|
if (val != null) {
|
||||||
|
this.pushHistory(val)
|
||||||
|
this.path = val
|
||||||
|
}
|
||||||
|
})
|
||||||
|
window.addEventListener('popstate', e => {
|
||||||
|
if (e && e.state && e.state.path) {
|
||||||
|
this.path = e.state.path
|
||||||
|
this.apply(e.state.path)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pushHistory (path, replace = false) {
|
||||||
|
if (replace) {
|
||||||
|
window.history.replaceState({path}, "", path, window.history.state)
|
||||||
|
} else if (path !== this.path) {
|
||||||
|
window.history.pushState({path}, "", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearPath () {
|
||||||
|
urlPath.set("/")
|
||||||
|
}
|
||||||
|
|
||||||
|
apply (path) {
|
||||||
|
const parts = path.split("/")
|
||||||
|
if (path === '/') {
|
||||||
|
detailTx.set(null)
|
||||||
|
highlightInOut.set(null)
|
||||||
|
urlPath.set("/")
|
||||||
|
explorerBlockData.set(null)
|
||||||
|
overlay.set(null)
|
||||||
|
} else {
|
||||||
|
switch (parts[1]) {
|
||||||
|
case 'block':
|
||||||
|
if (parts[2] === "height") {
|
||||||
|
try {
|
||||||
|
const height = parseInt(parts[3])
|
||||||
|
this.goToBlockHeight(height)
|
||||||
|
} catch (err) {
|
||||||
|
// ??
|
||||||
|
}
|
||||||
|
} else if (parts[2]) {
|
||||||
|
this.goToBlock(parts[2])
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'tx':
|
||||||
|
if (parts[2]) {
|
||||||
|
this.goToTransaction(parts[2])
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'donate':
|
||||||
|
overlay.set('donation')
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async goToBlock (blockhash) {
|
||||||
|
loading.increment()
|
||||||
|
await searchBlockHash(blockhash)
|
||||||
|
loading.decrement()
|
||||||
|
}
|
||||||
|
|
||||||
|
async goToBlockHeight (height) {
|
||||||
|
loading.increment()
|
||||||
|
await searchBlockHeight(height)
|
||||||
|
loading.decrement()
|
||||||
|
}
|
||||||
|
|
||||||
|
async goToTransaction (q) {
|
||||||
|
loading.increment()
|
||||||
|
const parts = q.split(":")
|
||||||
|
let txid, input, output
|
||||||
|
if (parts.length) {
|
||||||
|
if (parts[0].length == 64) {
|
||||||
|
txid = parts[0]
|
||||||
|
output = parseInt(parts[1])
|
||||||
|
if (isNaN(output)) output = null
|
||||||
|
} else if (parts[1].length == 64) {
|
||||||
|
txid = parts[1]
|
||||||
|
input = parseInt(parts[0])
|
||||||
|
if (isNaN(input)) input = null
|
||||||
|
} else {
|
||||||
|
// invalid
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
txid = q
|
||||||
|
}
|
||||||
|
await searchTx(txid, input, output)
|
||||||
|
loading.decrement()
|
||||||
|
}
|
||||||
|
}
|
@ -6,8 +6,9 @@ import BitcoinBlock from '../models/BitcoinBlock.js'
|
|||||||
import TxSprite from '../models/TxSprite.js'
|
import TxSprite from '../models/TxSprite.js'
|
||||||
import { FastVertexArray } from '../utils/memory.js'
|
import { FastVertexArray } from '../utils/memory.js'
|
||||||
import { searchTx, fetchSpends, addSpends } from '../utils/search.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 { overlay, txCount, mempoolCount, mempoolScreenHeight, blockVisible, currentBlock, selectedTx, detailTx, blockAreaSize, highlight, colorMode, blocksEnabled, latestBlockHeight, explorerBlockData, blockTransitionDirection, loading, urlPath } from '../stores.js'
|
||||||
import config from "../config.js"
|
import config from "../config.js"
|
||||||
|
import { tick } from 'svelte';
|
||||||
|
|
||||||
export default class TxController {
|
export default class TxController {
|
||||||
constructor ({ width, height }) {
|
constructor ({ width, height }) {
|
||||||
@ -188,6 +189,22 @@ export default class TxController {
|
|||||||
|
|
||||||
this.expiredTxs = {}
|
this.expiredTxs = {}
|
||||||
|
|
||||||
|
if (this.explorerBlockScene && this.explorerBlock && this.explorerBlock.id === block.id) {
|
||||||
|
this.block = this.explorerBlock
|
||||||
|
this.blockScene = this.explorerBlockScene
|
||||||
|
this.explorerBlockScene = null
|
||||||
|
this.explorerBlock = null
|
||||||
|
urlPath.set("/")
|
||||||
|
|
||||||
|
for (let i = 0; i < block.txns.length; i++) {
|
||||||
|
this.txs[block.txns[i].id].setData(block.txns[i])
|
||||||
|
this.poolScene.remove(block.txns[i].id)
|
||||||
|
}
|
||||||
|
this.poolScene.layoutAll()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.explorerBlockScene) this.clearBlock()
|
if (!this.explorerBlockScene) this.clearBlock()
|
||||||
|
|
||||||
if (this.blocksEnabled) {
|
if (this.blocksEnabled) {
|
||||||
@ -268,8 +285,14 @@ export default class TxController {
|
|||||||
return block
|
return block
|
||||||
}
|
}
|
||||||
|
|
||||||
exploreBlock (blockData) {
|
async exploreBlock (blockData) {
|
||||||
const block = blockData.isBlock ? blockData : new BitcoinBlock(blockData)
|
const block = blockData.isBlock ? blockData : new BitcoinBlock(blockData)
|
||||||
|
|
||||||
|
if (this.block && this.block.id === block.id) {
|
||||||
|
this.showBlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let enterFromRight = false
|
let enterFromRight = false
|
||||||
|
|
||||||
// clean up previous block
|
// clean up previous block
|
||||||
@ -311,10 +334,11 @@ export default class TxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
blockVisible.set(true)
|
blockVisible.set(true)
|
||||||
|
await tick()
|
||||||
currentBlock.set(block)
|
currentBlock.set(block)
|
||||||
}
|
}
|
||||||
|
|
||||||
resumeLatest () {
|
async resumeLatest () {
|
||||||
if (this.explorerBlock && this.explorerBlockScene) {
|
if (this.explorerBlock && this.explorerBlockScene) {
|
||||||
const prevBlock = this.explorerBlock
|
const prevBlock = this.explorerBlock
|
||||||
const prevBlockScene = this.explorerBlockScene
|
const prevBlockScene = this.explorerBlockScene
|
||||||
@ -322,17 +346,20 @@ export default class TxController {
|
|||||||
prevBlockScene.expire(2000)
|
prevBlockScene.expire(2000)
|
||||||
this.explorerBlockScene = null
|
this.explorerBlockScene = null
|
||||||
this.explorerBlock = null
|
this.explorerBlock = null
|
||||||
|
urlPath.set("/")
|
||||||
}
|
}
|
||||||
if (this.blockScene && this.block) {
|
if (this.blockScene && this.block) {
|
||||||
blockTransitionDirection.set('right')
|
blockTransitionDirection.set('right')
|
||||||
|
await tick()
|
||||||
this.blockScene.enterRight()
|
this.blockScene.enterRight()
|
||||||
currentBlock.set(this.block)
|
currentBlock.set(this.block)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hideBlock () {
|
async hideBlock () {
|
||||||
if (this.blockScene && !this.explorerBlockScene) {
|
if (this.blockScene && !this.explorerBlockScene) {
|
||||||
blockTransitionDirection.set(null)
|
blockTransitionDirection.set(null)
|
||||||
|
await tick()
|
||||||
this.blockScene.hide()
|
this.blockScene.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -397,6 +424,7 @@ export default class TxController {
|
|||||||
} else {
|
} else {
|
||||||
const spendResult = await fetchSpends(selected.id)
|
const spendResult = await fetchSpends(selected.id)
|
||||||
if (spendResult) selected = addSpends(selected, spendResult)
|
if (spendResult) selected = addSpends(selected, spendResult)
|
||||||
|
urlPath.set(`/tx/${selected.id}`)
|
||||||
detailTx.set(selected)
|
detailTx.set(selected)
|
||||||
overlay.set('tx')
|
overlay.set('tx')
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,7 @@ export default class TxBlockScene extends TxMondrianPoolScene {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.savePixelsToScreenPosition(tx, 0, this.hidden ? 50 : 0)
|
this.savePixelsToScreenPosition(tx, 0, (this.hidden && !this.exited) ? 50 : 0)
|
||||||
if (this.hidden) {
|
if (this.hidden) {
|
||||||
tx.view.update({
|
tx.view.update({
|
||||||
display: {
|
display: {
|
||||||
@ -164,6 +164,7 @@ export default class TxBlockScene extends TxMondrianPoolScene {
|
|||||||
|
|
||||||
enter (right) {
|
enter (right) {
|
||||||
this.hidden = false
|
this.hidden = false
|
||||||
|
this.exited = false
|
||||||
const ids = this.getActiveTxList()
|
const ids = this.getActiveTxList()
|
||||||
for (let i = 0; i < ids.length; i++) {
|
for (let i = 0; i < ids.length; i++) {
|
||||||
this.enterTx(this.txs[ids[i]], right)
|
this.enterTx(this.txs[ids[i]], right)
|
||||||
@ -198,6 +199,7 @@ export default class TxBlockScene extends TxMondrianPoolScene {
|
|||||||
|
|
||||||
exit (right) {
|
exit (right) {
|
||||||
this.hidden = true
|
this.hidden = true
|
||||||
|
this.exited = true
|
||||||
const ids = this.getActiveTxList()
|
const ids = this.getActiveTxList()
|
||||||
for (let i = 0; i < ids.length; i++) {
|
for (let i = 0; i < ids.length; i++) {
|
||||||
this.exitTx(this.txs[ids[i]], right)
|
this.exitTx(this.txs[ids[i]], right)
|
||||||
|
@ -170,3 +170,5 @@ export const highlightInOut = writable(null)
|
|||||||
export const loading = createCounter()
|
export const loading = createCounter()
|
||||||
export const explorerBlockData = writable(null)
|
export const explorerBlockData = writable(null)
|
||||||
export const blockTransitionDirection = writable(null)
|
export const blockTransitionDirection = writable(null)
|
||||||
|
|
||||||
|
export const urlPath = writable(null)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import api from './api.js'
|
import api from './api.js'
|
||||||
import BitcoinTx from '../models/BitcoinTx.js'
|
import BitcoinTx from '../models/BitcoinTx.js'
|
||||||
import BitcoinBlock from '../models/BitcoinBlock.js'
|
import BitcoinBlock from '../models/BitcoinBlock.js'
|
||||||
import { detailTx, selectedTx, currentBlock, explorerBlockData, overlay, highlightInOut } from '../stores.js'
|
import { detailTx, selectedTx, currentBlock, explorerBlockData, overlay, highlightInOut, urlPath } from '../stores.js'
|
||||||
import { addressToSPK } from './encodings.js'
|
import { addressToSPK } from './encodings.js'
|
||||||
|
|
||||||
// Quick heuristic matching to guess what kind of search a query is for
|
// Quick heuristic matching to guess what kind of search a query is for
|
||||||
@ -198,6 +198,13 @@ function addSpends(tx, spends) {
|
|||||||
export {addSpends as addSpends}
|
export {addSpends as addSpends}
|
||||||
|
|
||||||
export async function searchTx (txid, input, output) {
|
export async function searchTx (txid, input, output) {
|
||||||
|
if (input != null) {
|
||||||
|
urlPath.set(`/tx/${input}:${txid}`)
|
||||||
|
} else if (output != null) {
|
||||||
|
urlPath.set(`/tx/${txid}:${output}`)
|
||||||
|
} else {
|
||||||
|
urlPath.set(`/tx/${txid}`)
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
let searchResult = await fetchTx(txid)
|
let searchResult = await fetchTx(txid)
|
||||||
const spendResult = await fetchSpends(txid)
|
const spendResult = await fetchSpends(txid)
|
||||||
@ -218,6 +225,8 @@ export async function searchTx (txid, input, output) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function searchBlockHash (hash) {
|
export async function searchBlockHash (hash) {
|
||||||
|
urlPath.set(`/block/${hash}`)
|
||||||
|
overlay.set(null)
|
||||||
try {
|
try {
|
||||||
const searchResult = await fetchBlockByHash(hash)
|
const searchResult = await fetchBlockByHash(hash)
|
||||||
if (searchResult) {
|
if (searchResult) {
|
||||||
@ -235,6 +244,8 @@ export async function searchBlockHash (hash) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function searchBlockHeight (height) {
|
export async function searchBlockHeight (height) {
|
||||||
|
urlPath.set(`/block/height/${height}`)
|
||||||
|
overlay.set(null)
|
||||||
try {
|
try {
|
||||||
const searchResult = await fetchBlockByHeight(height)
|
const searchResult = await fetchBlockByHeight(height)
|
||||||
if (searchResult) {
|
if (searchResult) {
|
||||||
|
@ -85,6 +85,7 @@ defmodule BitcoinStream.Index.Spend do
|
|||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_cast({:block_disconnected, hash}, [dbref, indexed, done]) do
|
def handle_cast({:block_disconnected, hash}, [dbref, indexed, done]) do
|
||||||
|
Logger.info("block disconnected: #{hash}");
|
||||||
if (indexed != nil and done) do
|
if (indexed != nil and done) do
|
||||||
block_disconnected(dbref, hash)
|
block_disconnected(dbref, hash)
|
||||||
end
|
end
|
||||||
|
Loading…
Reference in New Issue
Block a user