add blocks
This commit is contained in:
@@ -7,10 +7,12 @@ from lnbits.settings import settings
|
|||||||
from lnbits.utils.electrum import (
|
from lnbits.utils.electrum import (
|
||||||
AddressResponse,
|
AddressResponse,
|
||||||
BlockHeader,
|
BlockHeader,
|
||||||
|
BlockInfo,
|
||||||
ElectrumClient,
|
ElectrumClient,
|
||||||
ElectrumError,
|
ElectrumError,
|
||||||
FeeResponse,
|
FeeResponse,
|
||||||
Transaction,
|
Transaction,
|
||||||
|
parse_block_header,
|
||||||
parse_raw_tx,
|
parse_raw_tx,
|
||||||
scripthash_from_address,
|
scripthash_from_address,
|
||||||
)
|
)
|
||||||
@@ -33,6 +35,24 @@ def _client() -> ElectrumClient:
|
|||||||
return ElectrumClient(settings.lnbits_blockexplorer_electrum_url)
|
return ElectrumClient(settings.lnbits_blockexplorer_electrum_url)
|
||||||
|
|
||||||
|
|
||||||
|
@blockexplorer_router.get("/blocks")
|
||||||
|
async def api_blocks() -> list[BlockInfo]:
|
||||||
|
_check_enabled()
|
||||||
|
try:
|
||||||
|
async with _client() as c:
|
||||||
|
tip = await c.get_tip()
|
||||||
|
start = max(0, tip.height - 4)
|
||||||
|
headers = await c.get_block_headers(start, tip.height - start + 1)
|
||||||
|
raw = bytes.fromhex(headers.hex)
|
||||||
|
blocks = [
|
||||||
|
parse_block_header(raw[i * 80 : (i + 1) * 80].hex(), start + i)
|
||||||
|
for i in range(headers.count)
|
||||||
|
]
|
||||||
|
return list(reversed(blocks))
|
||||||
|
except ElectrumError as e:
|
||||||
|
raise HTTPException(HTTPStatus.SERVICE_UNAVAILABLE, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
@blockexplorer_router.get("/tip")
|
@blockexplorer_router.get("/tip")
|
||||||
async def api_tip() -> BlockHeader:
|
async def api_tip() -> BlockHeader:
|
||||||
_check_enabled()
|
_check_enabled()
|
||||||
|
|||||||
@@ -852,6 +852,7 @@ window.localisation.en = {
|
|||||||
blockexplorer_search_label: 'Search by TXID or Address',
|
blockexplorer_search_label: 'Search by TXID or Address',
|
||||||
blockexplorer_search_hint:
|
blockexplorer_search_hint:
|
||||||
'64-char hex = transaction · anything else = Bitcoin address',
|
'64-char hex = transaction · anything else = Bitcoin address',
|
||||||
|
recent_blocks: 'Recent Blocks',
|
||||||
chain_tip: 'Chain Tip',
|
chain_tip: 'Chain Tip',
|
||||||
block_height: 'Block Height',
|
block_height: 'Block Height',
|
||||||
block_fee: 'block fee',
|
block_fee: 'block fee',
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ window.PageBlockExplorer = {
|
|||||||
loading: false,
|
loading: false,
|
||||||
tip: null,
|
tip: null,
|
||||||
fees: null,
|
fees: null,
|
||||||
|
blocks: [],
|
||||||
|
selectedBlock: null,
|
||||||
|
blockDialog: false,
|
||||||
txResult: null,
|
txResult: null,
|
||||||
addressResult: null,
|
addressResult: null,
|
||||||
currentAddress: ''
|
currentAddress: ''
|
||||||
@@ -18,12 +21,48 @@ window.PageBlockExplorer = {
|
|||||||
label: blocks + '-block fee',
|
label: blocks + '-block fee',
|
||||||
rate: (rate * 100000).toFixed(1) + ' sat/vB'
|
rate: (rate * 100000).toFixed(1) + ' sat/vB'
|
||||||
}))
|
}))
|
||||||
|
},
|
||||||
|
formattedBlocks() {
|
||||||
|
const now = Math.floor(Date.now() / 1000)
|
||||||
|
return this.blocks.map(b => ({
|
||||||
|
...b,
|
||||||
|
shortHash: b.hash.slice(0, 8) + '...' + b.hash.slice(-4),
|
||||||
|
timeAgo: this._timeAgo(now - b.timestamp),
|
||||||
|
utcTime: new Date(b.timestamp * 1000).toUTCString(),
|
||||||
|
difficulty: this._difficulty(b.bits)
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
await Promise.all([this.loadTip(), this.loadFees()])
|
await Promise.all([this.loadTip(), this.loadFees(), this.loadBlocks()])
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
_timeAgo(seconds) {
|
||||||
|
if (seconds < 60) return seconds + 's ago'
|
||||||
|
if (seconds < 3600) return Math.floor(seconds / 60) + 'm ago'
|
||||||
|
return Math.floor(seconds / 3600) + 'h ago'
|
||||||
|
},
|
||||||
|
_difficulty(bitsHex) {
|
||||||
|
const exp = parseInt(bitsHex.slice(0, 2), 16)
|
||||||
|
const mantissa = parseInt(bitsHex.slice(2), 16)
|
||||||
|
const diff1 = 0xffff * Math.pow(2, 208)
|
||||||
|
const target = mantissa * Math.pow(2, 8 * (exp - 3))
|
||||||
|
const d = diff1 / target
|
||||||
|
if (d >= 1e12) return (d / 1e12).toFixed(2) + 'T'
|
||||||
|
if (d >= 1e9) return (d / 1e9).toFixed(2) + 'G'
|
||||||
|
if (d >= 1e6) return (d / 1e6).toFixed(2) + 'M'
|
||||||
|
return d.toFixed(0)
|
||||||
|
},
|
||||||
|
openBlock(b) {
|
||||||
|
this.selectedBlock = b
|
||||||
|
this.blockDialog = true
|
||||||
|
},
|
||||||
|
async loadBlocks() {
|
||||||
|
try {
|
||||||
|
const r = await LNbits.api.request('GET', '/blockexplorer/api/v1/blocks')
|
||||||
|
this.blocks = r.data
|
||||||
|
} catch (_) {}
|
||||||
|
},
|
||||||
async loadTip() {
|
async loadTip() {
|
||||||
try {
|
try {
|
||||||
const r = await LNbits.api.request('GET', '/blockexplorer/api/v1/tip')
|
const r = await LNbits.api.request('GET', '/blockexplorer/api/v1/tip')
|
||||||
@@ -56,10 +95,7 @@ window.PageBlockExplorer = {
|
|||||||
},
|
},
|
||||||
async loadTx(txid) {
|
async loadTx(txid) {
|
||||||
try {
|
try {
|
||||||
const r = await LNbits.api.request(
|
const r = await LNbits.api.request('GET', '/blockexplorer/api/v1/tx/' + txid)
|
||||||
'GET',
|
|
||||||
'/blockexplorer/api/v1/tx/' + txid
|
|
||||||
)
|
|
||||||
this.txResult = r.data
|
this.txResult = r.data
|
||||||
this.addressResult = null
|
this.addressResult = null
|
||||||
this.query = txid
|
this.query = txid
|
||||||
|
|||||||
@@ -1,6 +1,78 @@
|
|||||||
<template id="page-blockexplorer">
|
<template id="page-blockexplorer">
|
||||||
<div class="row q-col-gutter-md justify-center">
|
<div class="row q-col-gutter-md">
|
||||||
<div class="col-12 col-md-8 q-gutter-y-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="row q-gutter-sm">
|
||||||
|
<q-card
|
||||||
|
v-for="b in formattedBlocks"
|
||||||
|
:key="b.height"
|
||||||
|
flat
|
||||||
|
class="bg-primary text-white cursor-pointer"
|
||||||
|
v-ripple
|
||||||
|
@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-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" v-text="'diff ' + b.difficulty"></div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
|
||||||
|
<!-- Block detail dialog -->
|
||||||
|
<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-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>
|
||||||
|
</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>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
<div class="row q-col-gutter-md">
|
||||||
|
<div class="col-6 col-sm-3">
|
||||||
|
<div class="text-caption text-grey">Version</div>
|
||||||
|
<div v-text="'0x' + selectedBlock.version.toString(16)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-sm-3">
|
||||||
|
<div class="text-caption text-grey">Bits</div>
|
||||||
|
<div v-text="selectedBlock.bits"></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-sm-3">
|
||||||
|
<div class="text-caption text-grey">Difficulty</div>
|
||||||
|
<div v-text="selectedBlock.difficulty"></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 col-sm-3">
|
||||||
|
<div class="text-caption text-grey">Nonce</div>
|
||||||
|
<div v-text="selectedBlock.nonce.toLocaleString()"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn flat v-close-popup v-text="$t('close')"></q-btn>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
<q-card>
|
<q-card>
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="text-h6" v-text="$t('block_explorer')"></div>
|
<div class="text-h6" v-text="$t('block_explorer')"></div>
|
||||||
@@ -14,68 +86,29 @@
|
|||||||
@keyup.enter="search"
|
@keyup.enter="search"
|
||||||
>
|
>
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
<q-btn
|
<q-btn flat round dense icon="search" :loading="loading" @click="search" />
|
||||||
flat
|
|
||||||
round
|
|
||||||
dense
|
|
||||||
icon="search"
|
|
||||||
:loading="loading"
|
|
||||||
@click="search"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
|
|
||||||
<!-- Chain tip + fee summary -->
|
|
||||||
<q-card v-if="tip">
|
|
||||||
<q-card-section>
|
|
||||||
<div class="text-subtitle1 q-mb-sm" v-text="$t('chain_tip')"></div>
|
|
||||||
<div class="row q-col-gutter-md">
|
|
||||||
<div class="col-auto">
|
|
||||||
<div
|
|
||||||
class="text-caption text-grey"
|
|
||||||
v-text="$t('block_height')"
|
|
||||||
></div>
|
|
||||||
<div class="text-h6" v-text="tip.height.toLocaleString()"></div>
|
|
||||||
</div>
|
|
||||||
<template v-if="feeList.length">
|
|
||||||
<div class="col-auto" v-for="f in feeList" :key="f.label">
|
|
||||||
<div class="text-caption text-grey" v-text="f.label"></div>
|
|
||||||
<div class="text-body2" v-text="f.rate"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</q-card-section>
|
|
||||||
</q-card>
|
|
||||||
|
|
||||||
<!-- Transaction result -->
|
<!-- Transaction result -->
|
||||||
<q-card v-if="txResult">
|
<q-card v-if="txResult">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="text-subtitle1 q-mb-sm" v-text="$t('transaction')"></div>
|
<div class="text-subtitle1 q-mb-sm" v-text="$t('transaction')"></div>
|
||||||
<div class="q-mb-xs">
|
<div class="q-mb-xs">
|
||||||
<span class="text-caption text-grey">txid: </span>
|
<span class="text-caption text-grey">txid: </span>
|
||||||
<code style="word-break: break-all" v-text="txResult.txid"></code>
|
<code class="text-caption be-wrap" v-text="txResult.txid"></code>
|
||||||
</div>
|
</div>
|
||||||
<div class="row q-col-gutter-md q-mb-sm">
|
<div class="row q-col-gutter-md q-mb-sm">
|
||||||
<div class="col-auto" v-if="txResult.blockheight">
|
<div class="col-auto" v-if="txResult.blockheight">
|
||||||
<div
|
<div class="text-caption text-grey" v-text="$t('block_height')"></div>
|
||||||
class="text-caption text-grey"
|
|
||||||
v-text="$t('block_height')"
|
|
||||||
></div>
|
|
||||||
<div v-text="txResult.blockheight.toLocaleString()"></div>
|
<div v-text="txResult.blockheight.toLocaleString()"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto" v-if="txResult.confirmations !== undefined">
|
<div class="col-auto" v-if="txResult.confirmations !== undefined">
|
||||||
|
<div class="text-caption text-grey" v-text="$t('confirmations')"></div>
|
||||||
<div
|
<div
|
||||||
class="text-caption text-grey"
|
v-text="txResult.confirmations > 0 ? txResult.confirmations : $t('unconfirmed')"
|
||||||
v-text="$t('confirmations')"
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
v-text="
|
|
||||||
txResult.confirmations > 0
|
|
||||||
? txResult.confirmations
|
|
||||||
: $t('unconfirmed')
|
|
||||||
"
|
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto" v-if="txResult.vsize || txResult.size">
|
<div class="col-auto" v-if="txResult.vsize || txResult.size">
|
||||||
@@ -96,13 +129,9 @@
|
|||||||
<q-list dense separator>
|
<q-list dense separator>
|
||||||
<q-item v-for="(vin, i) in txResult.vin" :key="i">
|
<q-item v-for="(vin, i) in txResult.vin" :key="i">
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label
|
<q-item-label v-if="vin.coinbase" class="text-grey" v-text="$t('coinbase')">
|
||||||
v-if="vin.coinbase"
|
|
||||||
class="text-grey"
|
|
||||||
v-text="$t('coinbase')"
|
|
||||||
>
|
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label v-else style="word-break: break-all">
|
<q-item-label v-else class="be-wrap">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@click.prevent="loadTx(vin.txid)"
|
@click.prevent="loadTx(vin.txid)"
|
||||||
@@ -123,9 +152,7 @@
|
|||||||
<q-item v-for="(vout, i) in txResult.vout" :key="i">
|
<q-item v-for="(vout, i) in txResult.vout" :key="i">
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label>
|
<q-item-label>
|
||||||
<template
|
<template v-if="vout.scriptPubKey && vout.scriptPubKey.address">
|
||||||
v-if="vout.scriptPubKey && vout.scriptPubKey.address"
|
|
||||||
>
|
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@click.prevent="loadAddress(vout.scriptPubKey.address)"
|
@click.prevent="loadAddress(vout.scriptPubKey.address)"
|
||||||
@@ -137,10 +164,7 @@
|
|||||||
<span v-text="vout.scriptPubKey.type"></span>
|
<span v-text="vout.scriptPubKey.type"></span>
|
||||||
</template>
|
</template>
|
||||||
</q-item-label>
|
</q-item-label>
|
||||||
<q-item-label
|
<q-item-label caption v-text="vout.value + ' BTC'"></q-item-label>
|
||||||
caption
|
|
||||||
v-text="vout.value + ' BTC'"
|
|
||||||
></q-item-label>
|
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
</q-list>
|
</q-list>
|
||||||
@@ -152,46 +176,20 @@
|
|||||||
<q-card v-if="addressResult">
|
<q-card v-if="addressResult">
|
||||||
<q-card-section>
|
<q-card-section>
|
||||||
<div class="text-subtitle1 q-mb-xs" v-text="$t('address')"></div>
|
<div class="text-subtitle1 q-mb-xs" v-text="$t('address')"></div>
|
||||||
<code
|
<div class="text-caption q-mb-sm"><code class="be-wrap" v-text="currentAddress"></code></div>
|
||||||
class="q-mb-sm"
|
|
||||||
style="word-break: break-all; display: block"
|
|
||||||
v-text="currentAddress"
|
|
||||||
></code>
|
|
||||||
<div class="row q-col-gutter-md q-mb-md">
|
<div class="row q-col-gutter-md q-mb-md">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<div
|
<div class="text-caption text-grey" v-text="$t('confirmed_balance')"></div>
|
||||||
class="text-caption text-grey"
|
<div v-text="addressResult.balance.confirmed.toLocaleString() + ' sat'"></div>
|
||||||
v-text="$t('confirmed_balance')"
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
v-text="
|
|
||||||
addressResult.balance.confirmed.toLocaleString() + ' sat'
|
|
||||||
"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="col-auto" v-if="addressResult.balance.unconfirmed !== 0">
|
||||||
class="col-auto"
|
<div class="text-caption text-grey" v-text="$t('unconfirmed_balance')"></div>
|
||||||
v-if="addressResult.balance.unconfirmed !== 0"
|
<div v-text="addressResult.balance.unconfirmed.toLocaleString() + ' sat'"></div>
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="text-caption text-grey"
|
|
||||||
v-text="$t('unconfirmed_balance')"
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
v-text="
|
|
||||||
addressResult.balance.unconfirmed.toLocaleString() + ' sat'
|
|
||||||
"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="text-subtitle2 q-mb-xs"
|
class="text-subtitle2 q-mb-xs"
|
||||||
v-text="
|
v-text="$t('transaction_history') + ' (' + addressResult.history.length + ')'"
|
||||||
$t('transaction_history') +
|
|
||||||
' (' +
|
|
||||||
addressResult.history.length +
|
|
||||||
')'
|
|
||||||
"
|
|
||||||
></div>
|
></div>
|
||||||
<q-list dense separator>
|
<q-list dense separator>
|
||||||
<q-item
|
<q-item
|
||||||
@@ -202,18 +200,10 @@
|
|||||||
@click="loadTx(h.tx_hash)"
|
@click="loadTx(h.tx_hash)"
|
||||||
>
|
>
|
||||||
<q-item-section>
|
<q-item-section>
|
||||||
<q-item-label
|
<q-item-label class="text-primary be-wrap" v-text="h.tx_hash"></q-item-label>
|
||||||
class="text-primary"
|
|
||||||
style="word-break: break-all"
|
|
||||||
v-text="h.tx_hash"
|
|
||||||
></q-item-label>
|
|
||||||
<q-item-label
|
<q-item-label
|
||||||
caption
|
caption
|
||||||
v-text="
|
v-text="h.height > 0 ? $t('block_height') + ': ' + h.height.toLocaleString() : $t('unconfirmed')"
|
||||||
h.height > 0
|
|
||||||
? $t('block_height') + ': ' + h.height.toLocaleString()
|
|
||||||
: $t('unconfirmed')
|
|
||||||
"
|
|
||||||
></q-item-label>
|
></q-item-label>
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section side>
|
<q-item-section side>
|
||||||
@@ -229,5 +219,34 @@
|
|||||||
</q-card-section>
|
</q-card-section>
|
||||||
</q-card>
|
</q-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Right column: chain tip + fees -->
|
||||||
|
<div class="col-12 col-md-5 q-gutter-y-md">
|
||||||
|
<q-card v-if="tip">
|
||||||
|
<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>
|
||||||
|
<template v-if="feeList.length">
|
||||||
|
<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-section>
|
||||||
|
<q-item-section side>
|
||||||
|
<q-item-label class="text-body2" v-text="f.rate"></q-item-label>
|
||||||
|
</q-item-section>
|
||||||
|
</q-item>
|
||||||
|
</q-list>
|
||||||
|
</template>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.be-wrap { word-break: break-all; }
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -251,6 +251,39 @@ class AddressResponse(BaseModel):
|
|||||||
history: list[HistoryEntry]
|
history: list[HistoryEntry]
|
||||||
|
|
||||||
|
|
||||||
|
class BlockInfo(BaseModel):
|
||||||
|
height: int
|
||||||
|
hash: str
|
||||||
|
timestamp: int
|
||||||
|
version: int
|
||||||
|
bits: str
|
||||||
|
nonce: int
|
||||||
|
prev_hash: str
|
||||||
|
merkle_root: str
|
||||||
|
|
||||||
|
|
||||||
|
def parse_block_header(header_hex: str, height: int) -> BlockInfo:
|
||||||
|
"""Parse an 80-byte block header hex string into a BlockInfo model."""
|
||||||
|
data = bytes.fromhex(header_hex)
|
||||||
|
version = struct.unpack_from("<I", data, 0)[0]
|
||||||
|
prev_hash = data[4:36][::-1].hex()
|
||||||
|
merkle_root = data[36:68][::-1].hex()
|
||||||
|
timestamp = struct.unpack_from("<I", data, 68)[0]
|
||||||
|
bits = format(struct.unpack_from("<I", data, 72)[0], "08x")
|
||||||
|
nonce = struct.unpack_from("<I", data, 76)[0]
|
||||||
|
block_hash = hashlib.sha256(hashlib.sha256(data).digest()).digest()[::-1].hex()
|
||||||
|
return BlockInfo(
|
||||||
|
height=height,
|
||||||
|
hash=block_hash,
|
||||||
|
timestamp=timestamp,
|
||||||
|
version=version,
|
||||||
|
bits=bits,
|
||||||
|
nonce=nonce,
|
||||||
|
prev_hash=prev_hash,
|
||||||
|
merkle_root=merkle_root,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def parse_raw_tx(hex_str: str) -> Transaction:
|
def parse_raw_tx(hex_str: str) -> Transaction:
|
||||||
"""Parse a raw transaction hex string into a Transaction model."""
|
"""Parse a raw transaction hex string into a Transaction model."""
|
||||||
data = bytes.fromhex(hex_str)
|
data = bytes.fromhex(hex_str)
|
||||||
|
|||||||
Reference in New Issue
Block a user