dynamically update blockexplorer compoenent with websockets
This commit is contained in:
@@ -864,6 +864,7 @@ window.localisation.en = {
|
||||
inputs: 'Inputs',
|
||||
outputs: 'Outputs',
|
||||
confirmations: 'Confirmations',
|
||||
confirmed: 'Confirmed',
|
||||
unconfirmed: 'Unconfirmed',
|
||||
no_transactions: 'No transactions found',
|
||||
address: 'Address'
|
||||
|
||||
@@ -10,6 +10,7 @@ window.PageBlockExplorer = {
|
||||
selectedBlock: null,
|
||||
blockDialog: false,
|
||||
txResult: null,
|
||||
txStatus: null,
|
||||
addressResult: null,
|
||||
currentAddress: ''
|
||||
}
|
||||
@@ -35,8 +36,46 @@ window.PageBlockExplorer = {
|
||||
},
|
||||
async created() {
|
||||
await Promise.all([this.loadTip(), this.loadFees(), this.loadBlocks()])
|
||||
this._blockWsActive = true
|
||||
this._connectBlocksWs()
|
||||
},
|
||||
beforeUnmount() {
|
||||
this._blockWsActive = false
|
||||
if (this._blockWs) this._blockWs.close()
|
||||
if (this._searchWs) this._searchWs.close()
|
||||
},
|
||||
methods: {
|
||||
_wsUrl(path) {
|
||||
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
return `${proto}//${window.location.host}/blockexplorer/api/v1${path}`
|
||||
},
|
||||
_connectBlocksWs() {
|
||||
const ws = new WebSocket(this._wsUrl('/ws/blocks'))
|
||||
ws.onmessage = e => {
|
||||
const block = JSON.parse(e.data)
|
||||
const rest = this.blocks.filter(b => b.height !== block.height)
|
||||
this.blocks = [block, ...rest].slice(0, 5)
|
||||
}
|
||||
ws.onerror = () => ws.close()
|
||||
ws.onclose = () => {
|
||||
if (this._blockWsActive) setTimeout(() => this._connectBlocksWs(), 5000)
|
||||
}
|
||||
this._blockWs = ws
|
||||
},
|
||||
_connectSearchWs(path, onMessage) {
|
||||
if (this._searchWs) {
|
||||
this._searchWs.close()
|
||||
this._searchWs = null
|
||||
}
|
||||
const ws = new WebSocket(this._wsUrl(path))
|
||||
ws.onmessage = e => {
|
||||
try {
|
||||
onMessage(JSON.parse(e.data))
|
||||
} catch (_) {}
|
||||
}
|
||||
ws.onerror = () => ws.close()
|
||||
this._searchWs = ws
|
||||
},
|
||||
_timeAgo(seconds) {
|
||||
if (seconds < 60) return seconds + 's ago'
|
||||
if (seconds < 3600) return Math.floor(seconds / 60) + 'm ago'
|
||||
@@ -59,7 +98,10 @@ window.PageBlockExplorer = {
|
||||
},
|
||||
async loadBlocks() {
|
||||
try {
|
||||
const r = await LNbits.api.request('GET', '/blockexplorer/api/v1/blocks')
|
||||
const r = await LNbits.api.request(
|
||||
'GET',
|
||||
'/blockexplorer/api/v1/blocks'
|
||||
)
|
||||
this.blocks = r.data
|
||||
} catch (_) {}
|
||||
},
|
||||
@@ -81,6 +123,7 @@ window.PageBlockExplorer = {
|
||||
const q = this.query.trim()
|
||||
if (!q) return
|
||||
this.txResult = null
|
||||
this.txStatus = null
|
||||
this.addressResult = null
|
||||
this.loading = true
|
||||
try {
|
||||
@@ -95,10 +138,17 @@ window.PageBlockExplorer = {
|
||||
},
|
||||
async loadTx(txid) {
|
||||
try {
|
||||
const r = await LNbits.api.request('GET', '/blockexplorer/api/v1/tx/' + txid)
|
||||
const r = await LNbits.api.request(
|
||||
'GET',
|
||||
'/blockexplorer/api/v1/tx/' + txid
|
||||
)
|
||||
this.txResult = r.data
|
||||
this.txStatus = null
|
||||
this.addressResult = null
|
||||
this.query = txid
|
||||
this._connectSearchWs(`/ws/tx/${txid}`, data => {
|
||||
if (!data.error) this.txStatus = data
|
||||
})
|
||||
} catch (e) {
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
@@ -111,8 +161,12 @@ window.PageBlockExplorer = {
|
||||
)
|
||||
this.addressResult = r.data
|
||||
this.txResult = null
|
||||
this.txStatus = null
|
||||
this.currentAddress = address
|
||||
this.query = address
|
||||
this._connectSearchWs(`/ws/address/${address}`, data => {
|
||||
if (!data.error) this.addressResult = data
|
||||
})
|
||||
} catch (e) {
|
||||
LNbits.utils.notifyApiError(e)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<template id="page-blockexplorer">
|
||||
<div class="row q-col-gutter-md">
|
||||
|
||||
<!-- Left column: blocks + search + results -->
|
||||
<div class="col-12 col-md-7 q-gutter-y-md">
|
||||
|
||||
<!-- Recent blocks (mempool-style squares) -->
|
||||
<q-card v-if="blocks.length">
|
||||
<q-card-section>
|
||||
<div class="text-subtitle1 q-mb-md" v-text="$t('recent_blocks')"></div>
|
||||
<div
|
||||
class="text-subtitle1 q-mb-md"
|
||||
v-text="$t('recent_blocks')"
|
||||
></div>
|
||||
<div class="row q-gutter-sm">
|
||||
<q-card
|
||||
v-for="b in formattedBlocks"
|
||||
@@ -18,9 +19,14 @@
|
||||
@click="openBlock(b)"
|
||||
>
|
||||
<q-card-section class="q-pa-sm">
|
||||
<div class="text-subtitle1 text-weight-bold" v-text="'#' + b.height.toLocaleString()"></div>
|
||||
<div
|
||||
class="text-subtitle1 text-weight-bold"
|
||||
v-text="'#' + b.height.toLocaleString()"
|
||||
></div>
|
||||
<div class="text-caption q-mt-xs" v-text="b.timeAgo"></div>
|
||||
<div class="text-caption q-mt-sm"><code v-text="b.shortHash"></code></div>
|
||||
<div class="text-caption q-mt-sm">
|
||||
<code v-text="b.shortHash"></code>
|
||||
</div>
|
||||
<div class="text-caption" v-text="'diff ' + b.difficulty"></div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
@@ -32,21 +38,33 @@
|
||||
<q-dialog v-model="blockDialog">
|
||||
<q-card v-if="selectedBlock">
|
||||
<q-card-section class="bg-primary text-white q-pb-sm">
|
||||
<div class="text-h6" v-text="'Block #' + selectedBlock.height.toLocaleString()"></div>
|
||||
<div
|
||||
class="text-h6"
|
||||
v-text="'Block #' + selectedBlock.height.toLocaleString()"
|
||||
></div>
|
||||
<div class="text-caption" v-text="selectedBlock.utcTime"></div>
|
||||
</q-card-section>
|
||||
<q-card-section>
|
||||
<div class="q-mb-md">
|
||||
<div class="text-caption text-grey q-mb-xs">Hash</div>
|
||||
<code class="text-caption be-wrap" v-text="selectedBlock.hash"></code>
|
||||
<code
|
||||
class="text-caption be-wrap"
|
||||
v-text="selectedBlock.hash"
|
||||
></code>
|
||||
</div>
|
||||
<div class="q-mb-md">
|
||||
<div class="text-caption text-grey q-mb-xs">Previous block</div>
|
||||
<code class="text-caption be-wrap" v-text="selectedBlock.prev_hash"></code>
|
||||
<code
|
||||
class="text-caption be-wrap"
|
||||
v-text="selectedBlock.prev_hash"
|
||||
></code>
|
||||
</div>
|
||||
<div class="q-mb-lg">
|
||||
<div class="text-caption text-grey q-mb-xs">Merkle root</div>
|
||||
<code class="text-caption be-wrap" v-text="selectedBlock.merkle_root"></code>
|
||||
<code
|
||||
class="text-caption be-wrap"
|
||||
v-text="selectedBlock.merkle_root"
|
||||
></code>
|
||||
</div>
|
||||
<div class="row q-col-gutter-md">
|
||||
<div class="col-6 col-sm-3">
|
||||
@@ -86,7 +104,14 @@
|
||||
@keyup.enter="search"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-btn flat round dense icon="search" :loading="loading" @click="search" />
|
||||
<q-btn
|
||||
flat
|
||||
round
|
||||
dense
|
||||
icon="search"
|
||||
:loading="loading"
|
||||
@click="search"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</q-card-section>
|
||||
@@ -95,29 +120,38 @@
|
||||
<!-- Transaction result -->
|
||||
<q-card v-if="txResult">
|
||||
<q-card-section>
|
||||
<div class="text-subtitle1 q-mb-sm" v-text="$t('transaction')"></div>
|
||||
<div class="q-mb-xs">
|
||||
<div class="row items-center q-mb-sm q-gutter-sm">
|
||||
<div class="text-subtitle1" v-text="$t('transaction')"></div>
|
||||
<q-badge
|
||||
v-if="txStatus"
|
||||
:color="txStatus.confirmed ? 'positive' : 'orange'"
|
||||
:label="txStatus.confirmed ? $t('confirmed') : $t('unconfirmed')"
|
||||
/>
|
||||
<q-spinner-dots v-else size="1em" color="grey" />
|
||||
</div>
|
||||
<div class="q-mb-sm">
|
||||
<span class="text-caption text-grey">txid: </span>
|
||||
<code class="text-caption be-wrap" v-text="txResult.txid"></code>
|
||||
</div>
|
||||
<div class="row q-col-gutter-md q-mb-sm">
|
||||
<div class="col-auto" v-if="txResult.blockheight">
|
||||
<div class="text-caption text-grey" v-text="$t('block_height')"></div>
|
||||
<div v-text="txResult.blockheight.toLocaleString()"></div>
|
||||
</div>
|
||||
<div class="col-auto" v-if="txResult.confirmations !== undefined">
|
||||
<div class="text-caption text-grey" v-text="$t('confirmations')"></div>
|
||||
<div class="col-auto" v-if="txStatus && txStatus.height">
|
||||
<div
|
||||
v-text="txResult.confirmations > 0 ? txResult.confirmations : $t('unconfirmed')"
|
||||
class="text-caption text-grey"
|
||||
v-text="$t('block_height')"
|
||||
></div>
|
||||
<div v-text="txStatus.height.toLocaleString()"></div>
|
||||
</div>
|
||||
<div class="col-auto" v-if="txStatus && txStatus.fee !== null">
|
||||
<div class="text-caption text-grey">fee</div>
|
||||
<div v-text="txStatus.fee + ' sat'"></div>
|
||||
</div>
|
||||
<div class="col-auto" v-if="txResult.vsize || txResult.size">
|
||||
<div class="text-caption text-grey">vsize</div>
|
||||
<div v-text="(txResult.vsize || txResult.size) + ' vB'"></div>
|
||||
</div>
|
||||
<div class="col-auto" v-if="txResult.fee !== undefined">
|
||||
<div class="text-caption text-grey">fee</div>
|
||||
<div v-text="txResult.fee + ' sat'"></div>
|
||||
<div class="col-auto" v-if="txResult.weight">
|
||||
<div class="text-caption text-grey">weight</div>
|
||||
<div v-text="txResult.weight + ' WU'"></div>
|
||||
</div>
|
||||
</div>
|
||||
<q-expansion-item
|
||||
@@ -129,7 +163,11 @@
|
||||
<q-list dense separator>
|
||||
<q-item v-for="(vin, i) in txResult.vin" :key="i">
|
||||
<q-item-section>
|
||||
<q-item-label v-if="vin.coinbase" class="text-grey" v-text="$t('coinbase')">
|
||||
<q-item-label
|
||||
v-if="vin.coinbase"
|
||||
class="text-grey"
|
||||
v-text="$t('coinbase')"
|
||||
>
|
||||
</q-item-label>
|
||||
<q-item-label v-else class="be-wrap">
|
||||
<a
|
||||
@@ -152,7 +190,9 @@
|
||||
<q-item v-for="(vout, i) in txResult.vout" :key="i">
|
||||
<q-item-section>
|
||||
<q-item-label>
|
||||
<template v-if="vout.scriptPubKey && vout.scriptPubKey.address">
|
||||
<template
|
||||
v-if="vout.scriptPubKey && vout.scriptPubKey.address"
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
@click.prevent="loadAddress(vout.scriptPubKey.address)"
|
||||
@@ -164,7 +204,10 @@
|
||||
<span v-text="vout.scriptPubKey.type"></span>
|
||||
</template>
|
||||
</q-item-label>
|
||||
<q-item-label caption v-text="vout.value + ' BTC'"></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="vout.value + ' BTC'"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
@@ -176,20 +219,44 @@
|
||||
<q-card v-if="addressResult">
|
||||
<q-card-section>
|
||||
<div class="text-subtitle1 q-mb-xs" v-text="$t('address')"></div>
|
||||
<div class="text-caption q-mb-sm"><code class="be-wrap" v-text="currentAddress"></code></div>
|
||||
<div class="text-caption q-mb-sm">
|
||||
<code class="be-wrap" v-text="currentAddress"></code>
|
||||
</div>
|
||||
<div class="row q-col-gutter-md q-mb-md">
|
||||
<div class="col-auto">
|
||||
<div class="text-caption text-grey" v-text="$t('confirmed_balance')"></div>
|
||||
<div v-text="addressResult.balance.confirmed.toLocaleString() + ' sat'"></div>
|
||||
<div
|
||||
class="text-caption text-grey"
|
||||
v-text="$t('confirmed_balance')"
|
||||
></div>
|
||||
<div
|
||||
v-text="
|
||||
addressResult.balance.confirmed.toLocaleString() + ' sat'
|
||||
"
|
||||
></div>
|
||||
</div>
|
||||
<div class="col-auto" v-if="addressResult.balance.unconfirmed !== 0">
|
||||
<div class="text-caption text-grey" v-text="$t('unconfirmed_balance')"></div>
|
||||
<div v-text="addressResult.balance.unconfirmed.toLocaleString() + ' sat'"></div>
|
||||
<div
|
||||
class="col-auto"
|
||||
v-if="addressResult.balance.unconfirmed !== 0"
|
||||
>
|
||||
<div
|
||||
class="text-caption text-grey"
|
||||
v-text="$t('unconfirmed_balance')"
|
||||
></div>
|
||||
<div
|
||||
v-text="
|
||||
addressResult.balance.unconfirmed.toLocaleString() + ' sat'
|
||||
"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="text-subtitle2 q-mb-xs"
|
||||
v-text="$t('transaction_history') + ' (' + addressResult.history.length + ')'"
|
||||
v-text="
|
||||
$t('transaction_history') +
|
||||
' (' +
|
||||
addressResult.history.length +
|
||||
')'
|
||||
"
|
||||
></div>
|
||||
<q-list dense separator>
|
||||
<q-item
|
||||
@@ -200,10 +267,17 @@
|
||||
@click="loadTx(h.tx_hash)"
|
||||
>
|
||||
<q-item-section>
|
||||
<q-item-label class="text-primary be-wrap" v-text="h.tx_hash"></q-item-label>
|
||||
<q-item-label
|
||||
class="text-primary be-wrap"
|
||||
v-text="h.tx_hash"
|
||||
></q-item-label>
|
||||
<q-item-label
|
||||
caption
|
||||
v-text="h.height > 0 ? $t('block_height') + ': ' + h.height.toLocaleString() : $t('unconfirmed')"
|
||||
v-text="
|
||||
h.height > 0
|
||||
? $t('block_height') + ': ' + h.height.toLocaleString()
|
||||
: $t('unconfirmed')
|
||||
"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
@@ -226,16 +300,28 @@
|
||||
<q-card-section>
|
||||
<div class="text-subtitle1 q-mb-sm" v-text="$t('chain_tip')"></div>
|
||||
<div class="text-caption text-grey" v-text="$t('block_height')"></div>
|
||||
<div class="text-h6 q-mb-md" v-text="tip.height.toLocaleString()"></div>
|
||||
<div
|
||||
class="text-h6 q-mb-md"
|
||||
v-text="tip.height.toLocaleString()"
|
||||
></div>
|
||||
<template v-if="feeList.length">
|
||||
<div class="text-subtitle2 q-mb-sm" v-text="$t('fee_estimates')"></div>
|
||||
<div
|
||||
class="text-subtitle2 q-mb-sm"
|
||||
v-text="$t('fee_estimates')"
|
||||
></div>
|
||||
<q-list dense>
|
||||
<q-item v-for="f in feeList" :key="f.label" class="q-px-none">
|
||||
<q-item-section>
|
||||
<q-item-label class="text-caption text-grey" v-text="f.label"></q-item-label>
|
||||
<q-item-label
|
||||
class="text-caption text-grey"
|
||||
v-text="f.label"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
<q-item-section side>
|
||||
<q-item-label class="text-body2" v-text="f.rate"></q-item-label>
|
||||
<q-item-label
|
||||
class="text-body2"
|
||||
v-text="f.rate"
|
||||
></q-item-label>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
</q-list>
|
||||
@@ -243,10 +329,11 @@
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.be-wrap { word-break: break-all; }
|
||||
.be-wrap {
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user