You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
bitcoind-ncurses2/block.py

494 lines
15 KiB
Python

# Copyright (c) 2014-2017 esotericnonsense (Daniel Edgecumbe)
# Distributed under the MIT software license, see the accompanying
# file COPYING or https://opensource.org/licenses/mit-license.php
import datetime
# import math
import curses
import asyncio
# from decimal import Decimal
import view
from util import isoformatseconds
class BlockStore(object):
def __init__(self, client):
self._client = client
self._lock = asyncio.Lock()
self._blocks = {} # hash -> raw block (full details)
self._bestblockhash = None
async def get_block(self, blockhash):
with await self._lock:
try:
return self._blocks[blockhash]
except KeyError:
# TODO: handle error if the block doesn't exist at all.
j = await self._client.request("getblock", [blockhash])
self._blocks[blockhash] = j["result"]
return j["result"]
async def get_blockhash(self, height):
# Direct RPC call.
j = await self._client.request("getblockhash", [height])
return j["result"]
async def get_previousblockhash(self, blockhash):
with await self._lock:
try:
return self._blocks[blockhash]["previousblockhash"]
except KeyError:
raise
async def get_nextblockhash(self, blockhash):
with await self._lock:
try:
return self._blocks[blockhash]["nextblockhash"]
except KeyError:
raise
async def get_previousblockhash_n(self, blockhash, n):
if n <= 0:
raise TypeError
# This is based on height.
with await self._lock:
try:
block = self._blocks[blockhash]
except KeyError:
raise
if block["height"] < n:
raise KeyError
j = await self._client.request("getblockhash", [block["height"] - n])
try:
return j["result"]
except KeyError:
raise
async def get_nextblockhash_n(self, blockhash, n):
if n <= 0:
raise TypeError
# This is based on height.
with await self._lock:
try:
block = self._blocks[blockhash]
except KeyError:
raise
try:
bestblock = self._blocks[self._bestblockhash]
except KeyError:
raise
if bestblock["height"] - block["height"] < n:
raise KeyError
j = await self._client.request("getblockhash", [block["height"] + n])
try:
return j["result"]
except KeyError:
raise
async def on_bestblockhash(self, blockhash):
with await self._lock:
self._bestblockhash = blockhash
# Pre-fetch it if necessary and update the previous block
# TODO: think about the locking here.
block = await self.get_block(blockhash)
with await self._lock:
try:
prevblock = self._blocks[block["previousblockhash"]]
except KeyError:
return
if "nextblockhash" in prevblock:
if prevblock["nextblockhash"] == blockhash:
return
raise Exception("BlockStore does not handle re-orgs")
prevblock["nextblockhash"] = blockhash
async def get_bestblockhash(self):
with await self._lock:
if self._bestblockhash is None:
raise KeyError
return self._bestblockhash
class BlockView(view.View):
_mode_name = "block"
def __init__(self, blockstore, txidsetter, modesetter):
self._blockstore = blockstore
self._txidsetter = txidsetter
self._modesetter = modesetter
self._edit_mode = False # Are we in edit mode?
self._edit_buffer = ""
self._hash = None # currently browsed hash.
self._selected_tx = None # (index, blockhash)
self._tx_offset = None # (offset, blockhash)
super().__init__()
async def _set_hash(self, newhash):
# TODO: lock?
self._hash = newhash
self._selected_tx = (0, newhash)
self._tx_offset = (0, newhash)
async def _draw_block(self, block, bestblockhash):
CGREEN = curses.color_pair(1)
CRED = curses.color_pair(3)
CYELLOW = curses.color_pair(5)
CBOLD = curses.A_BOLD
self._pad.addstr(0, 46, "[J/K: browse, HOME/END: quicker, L: best, TAB: manual]", CYELLOW)
self._pad.addstr(0, 1, "Time {}".format(
isoformatseconds(datetime.datetime.utcfromtimestamp(block["time"]))
), CBOLD)
self._pad.addstr(0, 31, "Height {}".format(block["height"]), CBOLD)
self._pad.addstr(1, 1, "Size {} bytes".format(block["size"]), CBOLD)
self._pad.addstr(2, 1, "Weight {} WU".format(block["weight"]), CBOLD)
self._pad.addstr(3, 1, "Diff {:,d}".format(int(block["difficulty"])), CBOLD)
self._pad.addstr(4, 1, "Version 0x{}".format(block["versionHex"]), CBOLD)
self._pad.addstr(1, 31, "Hash {}".format(block["hash"]), CBOLD)
if "previousblockhash" in block:
self._pad.addstr(2, 31, "Prev {}".format(block["previousblockhash"]), CBOLD)
else:
self._pad.addstr(2, 60, "genesis block!", CBOLD + CRED)
if "nextblockhash" in block:
self._pad.addstr(3, 31, "Next {}".format(block["nextblockhash"]), CBOLD)
elif block["hash"] == bestblockhash:
self._pad.addstr(3, 60, "best block!", CBOLD + CGREEN)
self._pad.addstr(4, 31, "Root {}".format(block["merkleroot"]), CBOLD)
async def _draw_transactions(self, block, bestblockhash):
CGREEN = curses.color_pair(1)
CRED = curses.color_pair(3)
CYELLOW = curses.color_pair(5)
CBOLD = curses.A_BOLD
CREVERSE = curses.A_REVERSE
self._pad.addstr(6, 36, "Transactions: {}".format(
len(block["tx"])), CBOLD)
self._pad.addstr(6, 68, "[UP/DOWN: browse, ENTER: select]", CYELLOW)
if self._selected_tx is None or self._tx_offset is None:
# Shouldn't happen
raise Exception
if self._selected_tx[1] != block["hash"] or self._tx_offset[1] != block["hash"]:
# Shouldn't happen
raise Exception
offset = self._tx_offset[0]
if offset > 0:
self._pad.addstr(7, 36, "... ^ ...", CBOLD)
if offset < len(block["tx"]) - 11:
self._pad.addstr(19, 36, "... v ...", CBOLD)
for i, txid in enumerate(block["tx"]):
if i < offset: # this is lazy
continue
if i > offset+10: # this is lazy
break
if i == self._selected_tx[0] and self._hash == self._selected_tx[1]:
self._pad.addstr(8+i-offset, 36, "{}".format(txid), CBOLD + CREVERSE)
else:
self._pad.addstr(8+i-offset, 36, "{}".format(txid))
async def _draw_edit_mode(self):
CGREEN = curses.color_pair(1)
CRED = curses.color_pair(3)
CYELLOW = curses.color_pair(5)
CBOLD = curses.A_BOLD
CREVERSE = curses.A_REVERSE
oy, ox = (20-6)//2, (100-70)//2
self._pad.addstr(oy, ox, " " * 70, CGREEN + CREVERSE)
self._pad.addstr(oy+6, ox, " " * 70, CGREEN + CREVERSE)
for i in range(5):
self._pad.addstr(oy+1+i, ox, " ", CGREEN + CREVERSE)
self._pad.addstr(oy+1+i, ox+69, " ", CGREEN + CREVERSE)
self._pad.addstr(oy+2, ox+2, "enter a block height or hash", CBOLD)
self._pad.addstr(oy+2, ox+53, "[ENTER: search]", CYELLOW)
self._pad.addstr(oy+4, ox+2, "> {}".format(self._edit_buffer),
CRED + CBOLD + CREVERSE if self._edit_mode else 0)
async def _submit_edit_buffer(self):
buf = self._edit_buffer
if len(buf) == 0:
return
if len(buf) < 8 and buf.isdigit():
blockhash = await self._blockstore.get_blockhash(int(buf))
self._edit_mode = False
self._edit_buffer = ""
await self._set_hash(blockhash)
await self._draw_if_visible()
return
if len(buf) == 64:
try:
int(buf, 16)
except ValueError:
# Note that it's invalid somehow
return
self._edit_mode = False
self._edit_buffer = ""
await self._set_hash(buf)
await self._draw_if_visible()
return
# Note that it's invalid somehow
return
async def _draw(self):
self._clear_init_pad()
if self._edit_mode:
await self._draw_edit_mode()
else:
block = None
bestblockhash = None
if self._hash:
block = await self._blockstore.get_block(self._hash)
bestblockhash = await self._blockstore.get_bestblockhash()
if block:
await self._draw_block(block, bestblockhash)
await self._draw_transactions(block, bestblockhash)
self._draw_pad_to_screen()
async def _select_previous_transaction(self):
if self._hash is None:
return # Can't do anything
if self._selected_tx == None or self._selected_tx[1] != self._hash:
return # Can't do anything
if self._tx_offset == None or self._tx_offset[1] != self._hash:
return # Can't do anything
if self._selected_tx[0] == 0:
return # At the beginning already.
if self._selected_tx[0] == self._tx_offset[0]:
self._tx_offset = (self._tx_offset[0] - 1, self._tx_offset[1])
self._selected_tx = (self._selected_tx[0] - 1, self._selected_tx[1])
await self._draw_if_visible()
async def _select_next_transaction(self):
if self._hash is None:
return # Can't do anything
if self._selected_tx == None or self._selected_tx[1] != self._hash:
return # Can't do anything
if self._tx_offset == None or self._tx_offset[1] != self._hash:
return # Can't do anything
try:
block = await self._blockstore.get_block(self._hash)
except KeyError:
return # Can't do anything
if self._selected_tx[0] == len(block["tx"]) - 1:
return # At the end already
if self._selected_tx[0] == self._tx_offset[0] + 10:
self._tx_offset = (self._tx_offset[0] + 1, self._tx_offset[1])
self._selected_tx = (self._selected_tx[0] + 1, self._selected_tx[1])
await self._draw_if_visible()
async def _enter_transaction_view(self):
if self._hash is None:
return # Can't do anything
if self._selected_tx == None or self._selected_tx[1] != self._hash:
return # Can't do anything
if self._tx_offset == None or self._tx_offset[1] != self._hash:
return # This shouldn't matter, but skip anyway
try:
block = await self._blockstore.get_block(self._hash)
except KeyError:
return # Can't do anything
txid = block["tx"][self._selected_tx[0]]
await self._txidsetter(txid)
await self._modesetter("transaction")
async def _select_previous_block(self):
if self._hash is None:
return # Can't do anything
try:
newhash = await self._blockstore.get_previousblockhash(self._hash)
except KeyError:
return # Can't do anything
await self._set_hash(newhash)
await self._draw_if_visible()
async def _select_next_block(self):
if self._hash is None:
return # Can't do anything
try:
newhash = await self._blockstore.get_nextblockhash(self._hash)
except KeyError:
return # Can't do anything
await self._set_hash(newhash)
await self._draw_if_visible()
async def _select_previous_block_n(self, n):
if self._hash is None:
return # Can't do anything
try:
newhash = await self._blockstore.get_previousblockhash_n(self._hash, n)
except KeyError:
return # Can't do anything
await self._set_hash(newhash)
await self._draw_if_visible()
async def _select_next_block_n(self, n):
if self._hash is None:
return # Can't do anything
try:
newhash = await self._blockstore.get_nextblockhash_n(self._hash, n)
except KeyError:
return # Can't do anything
await self._set_hash(newhash)
await self._draw_if_visible()
async def _select_best_block(self):
if self._hash is None:
return # Can't do anything
try:
newhash = await self._blockstore.get_bestblockhash()
except KeyError:
return # Can't do anything
await self._set_hash(newhash)
await self._draw_if_visible()
async def on_bestblockhash(self, key, obj):
try:
bestblockhash = obj["result"]
except KeyError:
return
await self._blockstore.on_bestblockhash(bestblockhash)
# If we have no browse hash, set it to the best.
if self._hash is None:
newhash = bestblockhash
await self._set_hash(newhash)
# Redraw so that we know if it's the best
await self._draw_if_visible()
async def handle_keypress(self, key):
if key == "\t" or key == "KEY_TAB":
self._edit_mode = not self._edit_mode
await self._draw_if_visible()
return None
if self._edit_mode:
if (len(key) == 1 and ord(key) == 127) or key == "KEY_BACKSPACE":
self._edit_buffer = self._edit_buffer[:-1]
await self._draw_if_visible()
return None
elif key == "KEY_RETURN" or key == "\n":
await self._submit_edit_buffer()
await self._draw_if_visible()
return None
elif len(key) == 1:
if len(self._edit_buffer) < 64:
self._edit_buffer += key
await self._draw_if_visible()
return None
else:
if key.lower() == "j":
await self._select_previous_block()
return None
if key.lower() == "k":
await self._select_next_block()
return None
if key == "KEY_HOME":
await self._select_previous_block_n(1000)
return None
if key == "KEY_END":
await self._select_next_block_n(1000)
return None
if key == "KEY_UP":
await self._select_previous_transaction()
return None
if key == "KEY_DOWN":
await self._select_next_transaction()
return None
if key == "KEY_RETURN" or key == "\n":
await self._enter_transaction_view()
return None
if key.lower() == "l":
await self._select_best_block()
return None
return key
async def on_mode_change(self, newmode):
""" Overrides view.View to set the edit mode inactive. """
if newmode != self._mode_name:
self._edit_mode = False
self._visible = False
return
self._visible = True
await self._draw_if_visible()