diff --git a/macros.py b/macros.py index 93f8841..8fb1a89 100644 --- a/macros.py +++ b/macros.py @@ -8,7 +8,7 @@ VERSION_STRING = "bitcoind-ncurses v0.2.0-dev" # "monitor", "wallet", "peers", "block", # "tx", "console", "net", "forks", # ] -MODES = ["monitor", "peers", "block", "transaction", "net"] +MODES = ["monitor", "peers", "wallet", "block", "transaction", "net"] DEFAULT_MODE = "monitor" # TX_VERBOSE_MODE controls whether the prevouts for an input are fetched. diff --git a/main.py b/main.py index 08dabdc..b8e7846 100644 --- a/main.py +++ b/main.py @@ -18,6 +18,7 @@ import peers import block import transaction import net +import wallet async def keypress_loop(window, callback, resize_callback): @@ -127,15 +128,21 @@ def create_tasks(client, window, nosplash): ) netview = net.NetView() + walletview = wallet.WalletView( + transactionview.set_txid, + modehandler.set_mode, + ) modehandler.add_callback("monitor", monitorview.on_mode_change) modehandler.add_callback("peers", peerview.on_mode_change) modehandler.add_callback("block", blockview.on_mode_change) modehandler.add_callback("transaction", transactionview.on_mode_change) modehandler.add_callback("net", netview.on_mode_change) + modehandler.add_callback("wallet", walletview.on_mode_change) modehandler.add_keypress_handler("block", blockview.handle_keypress) modehandler.add_keypress_handler("transaction", transactionview.handle_keypress) + modehandler.add_keypress_handler("wallet", walletview.handle_keypress) async def on_nettotals(key, obj): await headerview.on_nettotals(key, obj) @@ -164,6 +171,7 @@ def create_tasks(client, window, nosplash): await blockview.on_window_resize(y, x) await transactionview.on_window_resize(y, x) await netview.on_window_resize(y, x) + await walletview.on_window_resize(y, x) ty, tx = window.getmaxyx() tasks = [ @@ -179,6 +187,8 @@ def create_tasks(client, window, nosplash): on_peerinfo, 5.0), poll_client(client, "getmempoolinfo", monitorview.on_mempoolinfo, 5.0), + poll_client(client, "listsinceblock", + walletview.on_sinceblock, 5.0), poll_client(client, "estimatesmartfee", monitorview.on_estimatesmartfee, 15.0, params=[2]), poll_client(client, "estimatesmartfee", diff --git a/wallet.py b/wallet.py new file mode 100644 index 0000000..35bd50d --- /dev/null +++ b/wallet.py @@ -0,0 +1,191 @@ +# 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 curses +import asyncio + +import view + +class WalletView(view.View): + _mode_name = "wallet" + + def __init__(self, txidsetter, modesetter): + self._txidsetter = txidsetter + self._modesetter = modesetter + + self._wallet = None + self._tx_offset = None # (index, hash of wallet) + self._selected_tx = None # (index, hash of wallet) + + super().__init__() + + async def _draw_wallet(self, wallet): + CGREEN = curses.color_pair(1) + CRED = curses.color_pair(3) + CYELLOW = curses.color_pair(5) + CBOLD = curses.A_BOLD + CREVERSE = curses.A_REVERSE + + if "transactions" in wallet: + offset = self._tx_offset[0] + + self._pad.addstr(0, 1, "Transactions: {}".format( + len(wallet["transactions"])), CBOLD) + self._pad.addstr(0, 68, "[UP/DOWN: browse, ENTER: select]", CYELLOW) + + if offset > 0: + self._pad.addstr(1, 25, "... ^ ...", CBOLD) + + if offset < len(wallet["transactions"]) - 11: + self._pad.addstr(19, 25, "... v ...", CBOLD) + + for i, tx in enumerate(wallet["transactions"]): + if i < offset: + continue + if i > offset+5: + break + + color = CGREEN if tx["amount"] >= 0 else CRED + # if i == self._selected_tx[0] and self._hash == self._selected_tx[1]: + if i == self._selected_tx[0]: + color += CBOLD + CREVERSE + # hackerino + self._pad.addstr(2+((i-offset)*3), 1, " " * 98, color) + self._pad.addstr(2+((i-offset)*3)+1, 1, " " * 98, color) + + self._pad.addstr(2+((i-offset)*3), 1, "{}".format( + datetime.datetime.utcfromtimestamp(tx["timereceived"]).isoformat(timespec="seconds") + ), color) + self._pad.addstr(2+((i-offset)*3), 30, "block: {: 7d}".format(tx["blockindex"]), color) + self._pad.addstr(2+((i-offset)*3), 81, "{: 15.8f} BTC".format( + tx["amount"], + ), color) + self._pad.addstr(2+((i-offset)*3)+1, 1, "{}".format(tx["address"]), color) + self._pad.addstr(2+((i-offset)*3)+1, 36, "{}".format(tx["txid"]), color) + + async def _draw(self): + self._clear_init_pad() + + if self._wallet: + await self._draw_wallet(self._wallet) + + self._draw_pad_to_screen() + + async def on_sinceblock(self, key, obj): + # TODO: if no changes don't reset cursor or do anything? + try: + wallet = obj["result"] + except KeyError: + return + + if self._wallet is not None: + if wallet["lastblock"] == self._wallet["lastblock"]: + # No change. + return + + def sort_wallet_tx(tx): + return (tx["timereceived"], tx["amount"]) + + # Sort it. + wallet["transactions"] = sorted(wallet["transactions"], key=sort_wallet_tx, reverse=True) + + self._wallet = wallet + + # TODO: scan the old and new wallets, select the same tx, update offset. + + if self._selected_tx is None: + self._selected_tx = (0, None) + + if self._tx_offset is None: + self._tx_offset = (0, None) + + await self._draw_if_visible() + + 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 not self._wallet: + return + + 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 + """ + + if not self._wallet: + return + + if self._selected_tx[0] == len(self._wallet["transactions"]) - 1: + return # At the end already + + if self._selected_tx[0] == self._tx_offset[0] + 5: + 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 + """ + + if self._selected_tx == None: + return # Can't do anything + + txid = self._wallet["transactions"][self._selected_tx[0]]["txid"] + + await self._txidsetter(txid) + await self._modesetter("transaction") + + async def handle_keypress(self, key): + assert self._visible + + 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 + + return key