dynamically update blockexplorer compoenent with websockets

This commit is contained in:
dni
2026-07-13 09:09:41 +02:00
parent e3ce6b9fef
commit 843afd33e5
3 changed files with 184 additions and 42 deletions
+1
View File
@@ -864,6 +864,7 @@ window.localisation.en = {
inputs: 'Inputs',
outputs: 'Outputs',
confirmations: 'Confirmations',
confirmed: 'Confirmed',
unconfirmed: 'Unconfirmed',
no_transactions: 'No transactions found',
address: 'Address'
+56 -2
View File
@@ -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)
}
+127 -40
View File
@@ -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>