Skip to main content

Built-in Blockchain Explorer

SatSigner includes a powerful built-in blockchain explorer that works with your configured Electrum or Esplora backend. Explore blocks, visualize difficulty adjustments, and analyze Bitcoin’s blockchain without relying on third-party websites.

Overview

The explorer provides:
  • Block Explorer: Fetch and view detailed block information
  • Difficulty Visualization: Interactive spiral visualization of mining epochs
  • Transaction Browser: View transactions within blocks
  • Network Statistics: Real-time blockchain metrics
  • Privacy-First: Uses your own node or configured backend

Block Explorer

Features

View comprehensive block data:
type Block = {
  id: string                 // Block hash
  height: number             // Block height
  merkle_root?: string       // Merkle root
  previousblockhash?: string // Previous block hash
  timestamp: number          // Unix timestamp
  weight: number             // Block weight
  size: number               // Block size (bytes)
  version: number            // Block version
  nonce: number              // Mining nonce
  difficulty: number         // Difficulty target
  tx_count?: number          // Transaction count
  mediantime?: number        // Median time past
}

Implementation

Location: apps/mobile/app/(authenticated)/(tabs)/(signer,explorer,converter)/explorer/block/index.tsx:1

Fetching Blocks (Esplora)

// apps/mobile/app/.../explorer/block/index.tsx:56
async function fetchBlockEsplora(height: number): Promise<Block> {
  const esplora = new Esplora(backendUrl)
  const blockHash = await esplora.getBlockAtHeight(height)
  const block = await esplora.getBlockInfo(blockHash)
  return block
}
Esplora API Endpoints:
  • /block-height/{height} - Get block hash at height
  • /block/{hash} - Get block details

Fetching Blocks (Electrum)

// apps/mobile/app/.../explorer/block/index.tsx:63
async function fetchBlockElectrum(height: number): Promise<Block> {
  const electrum = await ElectrumClient.initClientFromUrl(backendUrl)
  const block = await electrum.getBlock(height)
  electrum.close()
  
  return {
    id: block.getId(),
    height,
    merkle_root: block.merkleRoot?.toString('hex'),
    previousblockhash: block.prevHash?.toString('hex'),
    timestamp: block.timestamp,
    weight: block.weight(),
    size: block.weight() * 4,  // Size = Weight / 4
    version: block.version,
    nonce: block.nonce,
    difficulty: getDifficultyFromBits(block.bits),
    tx_count: block.transactions?.length,
    mediantime: undefined
  }
}

Difficulty Calculation

Convert compact bits format to difficulty:
// apps/mobile/app/.../explorer/block/index.tsx:22
function getDifficultyFromBits(bits: number): number {
  const exponent = bits >>> 24
  const mantissa = bits & 0x007fffff
  let target = BigInt(mantissa)
  const shift = 8 * (exponent - 3)
  
  if (shift >= 0) {
    target = target * (BigInt(1) << BigInt(shift))
  } else {
    target = target / (BigInt(1) << BigInt(-shift))
  }
  
  const maxTarget = BigInt(
    '0x00000000ffff0000000000000000000000000000000000000000000000000000'
  )
  
  return Number(maxTarget) / Number(target)
}

Block Navigation

Navigate between blocks:
// apps/mobile/app/.../explorer/block/index.tsx:115
function nextBlockHeight() {
  setInputHeight(Math.min(maxBlockHeight, Number(inputHeight) + 1).toString())
}

function prevBlockHeight() {
  setInputHeight(Math.max(1, Number(inputHeight) - 1).toString())
}

async function fetchLatestBlock() {
  const tipHeight = await getBlockchainHeight('bitcoin')
  setMaxBlockHeight(tipHeight)
  await fetchBlock(tipHeight)
}

User Interface

The block explorer UI provides:
  • Height Input: Jump to specific block height
  • Navigation Buttons: Previous/Next block
  • Latest Block Button: Fetch blockchain tip
  • Block Details: All block metadata displayed
  • Transaction List: View transactions in block

Difficulty Adjustment Visualizer

Overview

Visualize Bitcoin’s mining difficulty over time with an interactive spiral representation of each difficulty epoch (2016 blocks). Location: apps/mobile/app/(authenticated)/(tabs)/(signer,explorer,converter)/explorer/difficulty.tsx:1

Difficulty Epochs

const BLOCKS_PER_EPOCH = 2016

type BlockDifficulty = {
  height: number          // Block height
  timestamp: number       // Block timestamp
  txCount: number         // Transaction count
  chainWork: string       // Cumulative work
  nonce: number          // Mining nonce
  size: number           // Block size
  weight: number         // Block weight
  cycleHeight: number    // Position in epoch (0-2015)
  timeDifference: number // Time since previous block
}

Fetching Epoch Data

// apps/mobile/app/.../explorer/difficulty.tsx:93
async function fetchData(epoch: number) {
  const fileName = getFileName(epoch)
  const response = await fetch(DATA_LINK + fileName)
  const rawData = await response.json() as DifficultyEpochsData[][]
  const items = rawData[0]
  
  const data = items.map(value => ({
    height: value[0].height,
    timestamp: value[1].time,
    txCount: value[2].nTx,
    chainWork: value[3].chainwork,
    nonce: value[4].nonce,
    size: value[5].size,
    weight: value[6].weight,
    cycleHeight: value[7].block_in_cycle,
    timeDifference: value[8].time_difference
  }) as BlockDifficulty)
  
  setData(data)
}

Current Difficulty Stats

Fetch real-time difficulty adjustment data:
// apps/mobile/app/.../explorer/difficulty.tsx:68
async function fetchDifficultyAdjustment() {
  const response = await mempoolOracle.getDifficultyAdjustment()
  
  // Average block time
  const avgTimeInSeconds = response.timeAvg / 1000
  const avgTimeInMinutes = avgTimeInSeconds / 60
  setAverageBlockTime(`~${avgTimeInMinutes.toFixed(1)} minutes`)
  
  // Time to next adjustment
  const [time, timeUnit] = formatTimeFromNow(response.remainingTime)
  setRemainingTime(`~${time.toFixed(1)} ${timeUnit}`)
}
Displayed Metrics:
  • Average block time for current epoch
  • Time remaining until next difficulty adjustment
  • Current epoch number
  • Epoch date range
  • Block height range

Spiral Visualization

Each epoch is displayed as a spiral:
const CANVAS_WIDTH = SCREEN_WIDTH
const CANVAS_HEIGHT = 0.7 * SCREEN_HEIGHT

<SSSpiralBlocks
  data={data}                        // Block data array
  loading={loading}                   // Loading state
  maxBlocksPerSpiral={BLOCKS_PER_EPOCH}  // 2016 blocks
  canvasWidth={CANVAS_WIDTH}
  canvasHeight={CANVAS_HEIGHT}
  onBlockPress={selectBlock}         // Select block for details
/>
Visual Encoding:
  • Spiral position: Block order in epoch
  • Color: Block properties (time, size, etc.)
  • Tap interaction: View block details

Block Details Modal

Tapping a block shows detailed information:
// apps/mobile/app/.../explorer/difficulty.tsx:263
function BlockDetails({ block }: BlockDetailsProps) {
  return (
    <>
      <SSText>Height: {block.height}</SSText>
      <SSText>Cycle Height: {block.cycleHeight}</SSText>
      <SSText>Transactions: {block.txCount}</SSText>
      <SSText>Size: {block.size} bytes</SSText>
      <SSText>VSize: {Math.trunc(block.weight / 4)}</SSText>
      <SSText>Weight: {block.weight} WU</SSText>
      <SSText>Nonce: {block.nonce}</SSText>
      <SSText>Date: {formatDate(block.timestamp * 1000)}</SSText>
      <SSText>Time Difference: {block.timeDifference}s</SSText>
      <SSText>Chain Work: {block.chainWork}</SSText>
    </>
  )
}

Epoch Navigation

Navigate between difficulty adjustment periods:
// Navigate to specific epoch
function getFileName(index: number) {
  return `rcp_bitcoin_block_data_${(index * BLOCKS_PER_EPOCH)
    .toString()
    .padStart(7, '0')}.json`
}

// Epoch 0: Blocks 0-2015
// Epoch 1: Blocks 2016-4031
// Epoch 426: Blocks 860,000+

Transaction Explorer

Viewing Block Transactions

Location: apps/mobile/app/(authenticated)/(tabs)/(signer,explorer,converter)/explorer/block/[block]/transactions.tsx:1 List all transactions in a block:
type BlockTransaction = {
  txid: string
  version: number
  locktime: number
  vin: Array<{
    txid: string
    vout: number
    sequence: number
    scriptsig?: string
    witness?: string[]
  }>
  vout: Array<{
    scriptpubkey: string
    scriptpubkey_address?: string
    value: number
  }>
  size: number
  weight: number
  fee?: number
  status: {
    confirmed: boolean
    block_height: number
    block_hash: string
    block_time: number
  }
}

Backend Integration

Supported Backends

The explorer works with: Esplora
  • REST API interface
  • JSON responses
  • Public instances available
  • Self-hostable
Electrum
  • Electrum protocol
  • Binary responses
  • Wide server availability
  • Lower bandwidth

Backend Configuration

Select backend in Settings → Network:
type BlockchainConfig = {
  server: {
    backend: 'esplora' | 'electrum'
    url: string
  }
}
Default Configuration:
  • Backend: Esplora
  • URL: Public Esplora instance or custom node

Switching Backends

The explorer automatically adapts:
// apps/mobile/app/.../explorer/block/index.tsx:83
async function fetchBlock(height: number) {
  setLoading(true)
  try {
    const block = backend === 'esplora'
      ? await fetchBlockEsplora(height)
      : await fetchBlockElectrum(height)
    setBlock(block)
    setInputHeight(height.toString())
    return block
  } catch {
    toast.error(`Failed to fetch block ${height}`)
  } finally {
    setLoading(false)
  }
}

Privacy Considerations

Using Your Own Node

Best Practice: Connect to your own node
  • No data leaked to third parties
  • Complete privacy for block lookups
  • No rate limiting
  • Enhanced security

Third-Party Backends

When using public backends:
  • Block lookups are visible to server
  • IP address exposed
  • Consider using Tor (see Privacy Tools)
  • Avoid patterns that leak wallet info

Data Minimization

  • Only fetch blocks you need
  • Don’t explore blocks related to your transactions
  • Use Tor for additional privacy
  • Self-host backend when possible

Performance Optimization

Caching Strategy

// Cache recently viewed blocks
const blockCache = new Map<number, Block>()

async function fetchBlock(height: number) {
  if (blockCache.has(height)) {
    return blockCache.get(height)
  }
  
  const block = await fetchBlockFromBackend(height)
  blockCache.set(height, block)
  
  // Limit cache size
  if (blockCache.size > 100) {
    const firstKey = blockCache.keys().next().value
    blockCache.delete(firstKey)
  }
  
  return block
}

Lazy Loading

  • Transactions loaded on demand
  • Difficulty epochs fetched as needed
  • Progressive rendering for large datasets

Use Cases

Blockchain Analysis

  • Study Bitcoin’s block structure
  • Analyze mining patterns
  • Research difficulty adjustments
  • Educational exploration

Transaction Verification

  • Verify transaction inclusion
  • Check confirmation count
  • Inspect transaction details
  • Validate payment proofs

Mining Research

  • Study block timing distributions
  • Analyze difficulty trends
  • Research orphan rates
  • Monitor network hashrate

Future Enhancements

Planned features:
  • Mempool Explorer: View unconfirmed transactions
  • Address Lookup: Search any Bitcoin address
  • UTXO Analysis: Visualize UTXO set
  • Fee Estimation: Historical fee rate charts
  • Network Graphs: Visualize network topology
  • Script Decoder: Parse and explain Bitcoin scripts

Implementation Reference

Block Explorer: apps/mobile/app/(authenticated)/(tabs)/(signer,explorer,converter)/explorer/block/index.tsx:1 Difficulty Visualizer: apps/mobile/app/(authenticated)/(tabs)/(signer,explorer,converter)/explorer/difficulty.tsx:1 Esplora API: apps/mobile/api/esplora.ts:1 Electrum Client: apps/mobile/api/electrum.ts:1 Blockchain Types: apps/mobile/types/models/Blockchain.ts:1

Resources

Build docs developers (and LLMs) love