|
|
|
# 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
|
|
|
|
from macros import TX_VERBOSE_MODE
|
|
|
|
|
|
|
|
|
|
|
|
class TransactionStore(object):
|
|
|
|
def __init__(self, client):
|
|
|
|
self._client = client
|
|
|
|
|
|
|
|
self._lock = asyncio.Lock()
|
|
|
|
|
|
|
|
self._transactions = {} # txid -> raw transaction
|
|
|
|
|
|
|
|
async def get_transaction(self, txid):
|
|
|
|
with await self._lock:
|
|
|
|
try:
|
|
|
|
return self._transactions[txid]
|
|
|
|
except KeyError:
|
|
|
|
# TODO: handle error if the transaction doesn't exist at all.
|
|
|
|
j = await self._client.request("getrawtransaction", [txid, True])
|
|
|
|
self._transactions[txid] = j["result"]
|
|
|
|
return j["result"]
|
|
|
|
|
|
|
|
|
|
|
|
class TransactionView(view.View):
|
|
|
|
_mode_name = "transaction"
|
|
|
|
|
|
|
|
def __init__(self, transactionstore):
|
|
|
|
self._transactionstore = transactionstore
|
|
|
|
|
|
|
|
self._edit_mode = False # Are we in edit mode?
|
|
|
|
self._edit_buffer = ""
|
|
|
|
|
|
|
|
self._txid = None # currently browsed txid.
|
|
|
|
self._selected_input = None # (index, txid)
|
|
|
|
self._input_offset = None # (offset, txid)
|
|
|
|
self._selected_output = None # (index, txid)
|
|
|
|
self._output_offset = None # (offset, txid)
|
|
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
|
|
async def _set_txid(self, txid, vout=None):
|
|
|
|
# TODO: lock?
|
|
|
|
self._txid = txid
|
|
|
|
self._selected_input = (0, txid)
|
|
|
|
self._input_offset = (0, txid)
|
|
|
|
if vout is not None: # A specific input was selected, go there.
|
|
|
|
self._selected_output = (vout, txid)
|
|
|
|
self._output_offset = (vout, txid)
|
|
|
|
else:
|
|
|
|
self._selected_output = (0, txid)
|
|
|
|
self._output_offset = (0, txid)
|
|
|
|
|
|
|
|
async def set_txid(self, txid):
|
|
|
|
# Externally setting vout is not permitted/necessary.
|
|
|
|
await self._set_txid(txid)
|
|
|
|
|
|
|
|
async def _draw_transaction(self, transaction):
|
|
|
|
CGREEN = curses.color_pair(1)
|
|
|
|
CRED = curses.color_pair(3)
|
|
|
|
CYELLOW = curses.color_pair(5)
|
|
|
|
CBOLD = curses.A_BOLD
|
|
|
|
|
|
|
|
self._pad.addstr(0, 1, "time {}".format(
|
|
|
|
datetime.datetime.utcfromtimestamp(transaction["time"]).isoformat(timespec="seconds")
|
|
|
|
), CBOLD)
|
|
|
|
self._pad.addstr(1, 1, "size {}b".format(transaction["size"]), CBOLD)
|
|
|
|
self._pad.addstr(1, 15, "vsize {}b".format(transaction["vsize"]), CBOLD)
|
|
|
|
self._pad.addstr(2, 1, "locktime {}".format(transaction["locktime"]), CBOLD)
|
|
|
|
self._pad.addstr(2, 23, "v{}".format(transaction["version"]), CBOLD)
|
|
|
|
|
|
|
|
self._pad.addstr(0, 31, "txid {}".format(transaction["txid"]), CBOLD)
|
|
|
|
self._pad.addstr(1, 31, "hash {}".format(transaction["hash"]), CBOLD)
|
|
|
|
if "blockhash" in transaction:
|
|
|
|
self._pad.addstr(2, 30, "block {}".format(transaction["blockhash"]), CBOLD)
|
|
|
|
else:
|
|
|
|
self._pad.addstr(2, 58, "unconfirmed transaction!", CBOLD + CRED)
|
|
|
|
|
|
|
|
# height and weight would be nice.
|
|
|
|
# neither are directly accessible.
|
|
|
|
|
|
|
|
async def _draw_inputs(self, transaction, inouts):
|
|
|
|
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(4, 1, "Inputs: {}".format(len(transaction["vin"])), CRED + CBOLD)
|
|
|
|
self._pad.addstr(4, 68, "[UP/DOWN: browse, ENTER: select]", CYELLOW)
|
|
|
|
|
|
|
|
if self._selected_input is None or self._input_offset is None:
|
|
|
|
# Shouldn't happen
|
|
|
|
raise Exception
|
|
|
|
|
|
|
|
if self._selected_input[1] != transaction["txid"] or self._input_offset[1] != transaction["txid"]:
|
|
|
|
# Shouldn't happen
|
|
|
|
raise Exception
|
|
|
|
|
|
|
|
offset = self._input_offset[0]
|
|
|
|
if offset > 0:
|
|
|
|
self._pad.addstr(5, 36, "... ^ ...", CRED + CBOLD)
|
|
|
|
if offset < len(transaction["vin"]) - 5:
|
|
|
|
self._pad.addstr(11, 36, "... v ...", CRED + CBOLD)
|
|
|
|
for i, inp in enumerate(transaction["vin"]):
|
|
|
|
if i < offset: # this is lazy
|
|
|
|
continue
|
|
|
|
if i > offset+4: # this is lazy
|
|
|
|
break
|
|
|
|
|
|
|
|
# Sequence numbers, perhaps?
|
|
|
|
if "coinbase" in inp:
|
|
|
|
inputstr = inp["coinbase"][:76]
|
|
|
|
elif inouts is not None: # TX_VERBOSE_MODE
|
|
|
|
# Find the vout
|
|
|
|
inout = inouts[i]
|
|
|
|
inputstr = "{}".format(str(inout)[:40])
|
|
|
|
spk = inout["scriptPubKey"]
|
|
|
|
if "addresses" in spk:
|
|
|
|
if len(spk["addresses"]) > 1:
|
|
|
|
inoutstring = "<{} addresses>".format(len(spk["addresses"]))
|
|
|
|
elif len(spk["addresses"]) == 1:
|
|
|
|
inoutstring = spk["addresses"][0].rjust(34)
|
|
|
|
else:
|
|
|
|
raise Exception("addresses present in scriptPubKey, but 0 addresses")
|
|
|
|
elif "asm" in spk:
|
|
|
|
inoutstring = spk["asm"][:34]
|
|
|
|
else:
|
|
|
|
inoutstring = "???"
|
|
|
|
|
|
|
|
inputstr = "{:05d} {} {: 15.8f} BTC".format(i, inoutstring, inout["value"])
|
|
|
|
else:
|
|
|
|
inputstr = "{:05d} {}:{:05d}".format(i, inp["txid"], inp["vout"])
|
|
|
|
|
|
|
|
if i == self._selected_input[0] and self._txid == self._selected_input[1]:
|
|
|
|
inputcolor = CRED + CBOLD + CREVERSE
|
|
|
|
else:
|
|
|
|
inputcolor = CRED
|
|
|
|
|
|
|
|
self._pad.addstr(6+i-offset, 24, inputstr, inputcolor)
|
|
|
|
|
|
|
|
async def _draw_outputs(self, transaction):
|
|
|
|
# TODO: can we reuse code from _draw_inputs?
|
|
|
|
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(12, 1, "[PGUP/PGDN: browse]", CYELLOW)
|
|
|
|
out_total = sum(out["value"] for out in transaction["vout"])
|
|
|
|
self._pad.addstr(12, 64, "Outputs: {: 5d} ({: 15.8f} BTC)".format(len(transaction["vout"]), out_total), CGREEN + CBOLD)
|
|
|
|
|
|
|
|
if self._selected_output is None or self._output_offset is None:
|
|
|
|
# Shouldn't happen
|
|
|
|
raise Exception
|
|
|
|
|
|
|
|
if self._selected_output[1] != transaction["txid"] or self._output_offset[1] != transaction["txid"]:
|
|
|
|
# Shouldn't happen
|
|
|
|
raise Exception
|
|
|
|
|
|
|
|
offset = self._output_offset[0]
|
|
|
|
if offset > 0:
|
|
|
|
self._pad.addstr(13, 36, "... ^ ...", CGREEN + CBOLD)
|
|
|
|
if offset < len(transaction["vout"]) - 5:
|
|
|
|
self._pad.addstr(19, 36, "... v ...", CGREEN + CBOLD)
|
|
|
|
for i, out in enumerate(transaction["vout"]):
|
|
|
|
if i < offset: # this is lazy
|
|
|
|
continue
|
|
|
|
if i > offset+4: # this is lazy
|
|
|
|
break
|
|
|
|
|
|
|
|
# A 1 million BTC output would be rather surprising. Pad to six.
|
|
|
|
spk = out["scriptPubKey"]
|
|
|
|
if "addresses" in spk:
|
|
|
|
if len(spk["addresses"]) > 1:
|
|
|
|
outstring = "<{} addresses>".format(len(spk["addresses"]))
|
|
|
|
elif len(spk["addresses"]) == 1:
|
|
|
|
outstring = spk["addresses"][0].rjust(34)
|
|
|
|
else:
|
|
|
|
raise Exception("addresses present in scriptPubKey, but 0 addresses")
|
|
|
|
elif "asm" in spk:
|
|
|
|
outstring = spk["asm"][:80]
|
|
|
|
else:
|
|
|
|
outstring = "???"
|
|
|
|
|
|
|
|
if i == self._selected_output[0] and self._txid == self._selected_output[1]:
|
|
|
|
outputcolor = CGREEN + CBOLD + CREVERSE
|
|
|
|
else:
|
|
|
|
outputcolor = CGREEN
|
|
|
|
|
|
|
|
self._pad.addstr(14+i-offset, 1, "{:05d} {} {: 15.8f} BTC".format(i, outstring, out["value"]), outputcolor)
|
|
|
|
|
|
|
|
async def _draw_no_transaction(self):
|
|
|
|
CRED = curses.color_pair(3)
|
|
|
|
CBOLD = curses.A_BOLD
|
|
|
|
self._pad.addstr(0, 1, "no transaction loaded", CRED + CBOLD)
|
|
|
|
self._pad.addstr(1, 1, "enter block or wallet view and select a transaction", CRED)
|
|
|
|
self._pad.addstr(2, 1, "note that most transactions will be unavailable if -txindex is not enabled on your node", CRED)
|
|
|
|
|
|
|
|
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 transaction id (txid)", 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) == 64:
|
|
|
|
try:
|
|
|
|
int(buf, 16)
|
|
|
|
except ValueError:
|
|
|
|
# Note that it's invalid somehow
|
|
|
|
return
|
|
|
|
|
|
|
|
self._edit_mode = False
|
|
|
|
self._edit_buffer = ""
|
|
|
|
await self._set_txid(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:
|
|
|
|
transaction = None
|
|
|
|
inouts = None
|
|
|
|
if self._txid:
|
|
|
|
transaction = await self._transactionstore.get_transaction(self._txid)
|
|
|
|
if TX_VERBOSE_MODE:
|
|
|
|
inouts = []
|
|
|
|
for vin in transaction["vin"]:
|
|
|
|
if not "txid" in vin:
|
|
|
|
# It's a coinbase
|
|
|
|
inouts = None
|
|
|
|
break
|
|
|
|
prevtx = await self._transactionstore.get_transaction(vin["txid"])
|
|
|
|
inouts.append(prevtx["vout"][vin["vout"]])
|
|
|
|
|
|
|
|
if transaction:
|
|
|
|
await self._draw_transaction(transaction)
|
|
|
|
if "vin" in transaction:
|
|
|
|
await self._draw_inputs(transaction, inouts)
|
|
|
|
if "vout" in transaction:
|
|
|
|
await self._draw_outputs(transaction)
|
|
|
|
else:
|
|
|
|
await self._draw_no_transaction()
|
|
|
|
|
|
|
|
self._draw_pad_to_screen()
|
|
|
|
|
|
|
|
async def _select_previous_input(self):
|
|
|
|
if self._txid is None:
|
|
|
|
return # Can't do anything
|
|
|
|
|
|
|
|
if self._selected_input == None or self._selected_input[1] != self._txid:
|
|
|
|
return # Can't do anything
|
|
|
|
|
|
|
|
if self._input_offset == None or self._input_offset[1] != self._txid:
|
|
|
|
return # Can't do anything
|
|
|
|
|
|
|
|
if self._selected_input[0] == 0:
|
|
|
|
return # At the beginning already.
|
|
|
|
|
|
|
|
if self._selected_input[0] == self._input_offset[0]:
|
|
|
|
self._input_offset = (self._input_offset[0] - 1, self._input_offset[1])
|
|
|
|
|
|
|
|
self._selected_input = (self._selected_input[0] - 1, self._selected_input[1])
|
|
|
|
|
|
|
|
await self._draw_if_visible()
|
|
|
|
|
|
|
|
async def _select_next_input(self):
|
|
|
|
if self._txid is None:
|
|
|
|
return # Can't do anything
|
|
|
|
|
|
|
|
if self._selected_input == None or self._selected_input[1] != self._txid:
|
|
|
|
return # Can't do anything
|
|
|
|
|
|
|
|
if self._input_offset == None or self._input_offset[1] != self._txid:
|
|
|
|
return # Can't do anything
|
|
|
|
|
|
|
|
try:
|
|
|
|
transaction = await self._transactionstore.get_transaction(self._txid)
|
|
|
|
except KeyError:
|
|
|
|
return # Can't do anything
|
|
|
|
|
|
|
|
if self._selected_input[0] == len(transaction["vin"]) - 1:
|
|
|
|
return # At the end already
|
|
|
|
|
|
|
|
if self._selected_input[0] == self._input_offset[0] + 4:
|
|
|
|
self._input_offset = (self._input_offset[0] + 1, self._input_offset[1])
|
|
|
|
|
|
|
|
self._selected_input = (self._selected_input[0] + 1, self._selected_input[1])
|
|
|
|
|
|
|
|
await self._draw_if_visible()
|
|
|
|
|
|
|
|
async def _select_input_as_transaction(self):
|
|
|
|
if self._txid is None:
|
|
|
|
return # Can't do anything
|
|
|
|
|
|
|
|
if self._selected_input == None or self._selected_input[1] != self._txid:
|
|
|
|
return # Can't do anything
|
|
|
|
|
|
|
|
if self._input_offset == None or self._input_offset[1] != self._txid:
|
|
|
|
return # This shouldn't matter, but skip anyway
|
|
|
|
|
|
|
|
try:
|
|
|
|
transaction = await self._transactionstore.get_transaction(self._txid)
|
|
|
|
except KeyError:
|
|
|
|
return # Can't do anything
|
|
|
|
|
|
|
|
inp = transaction["vin"][self._selected_input[0]]
|
|
|
|
# Sequence numbers, perhaps?
|
|
|
|
if "coinbase" in inp:
|
|
|
|
return # Can't do anything
|
|
|
|
else:
|
|
|
|
await self._set_txid(inp["txid"], vout=inp["vout"])
|
|
|
|
|
|
|
|
await self._draw_if_visible()
|
|
|
|
|
|
|
|
async def _select_previous_output(self):
|
|
|
|
if self._txid is None:
|
|
|
|
return # Can't do anything
|
|
|
|
|
|
|
|
if self._selected_output == None or self._selected_output[1] != self._txid:
|
|
|
|
return # Can't do anything
|
|
|
|
|
|
|
|
if self._output_offset == None or self._output_offset[1] != self._txid:
|
|
|
|
return # Can't do anything
|
|
|
|
|
|
|
|
if self._selected_output[0] == 0:
|
|
|
|
return # At the beginning already.
|
|
|
|
|
|
|
|
if self._selected_output[0] == self._output_offset[0]:
|
|
|
|
self._output_offset = (self._output_offset[0] - 1, self._output_offset[1])
|
|
|
|
|
|
|
|
self._selected_output = (self._selected_output[0] - 1, self._selected_output[1])
|
|
|
|
|
|
|
|
await self._draw_if_visible()
|
|
|
|
|
|
|
|
async def _select_next_output(self):
|
|
|
|
if self._txid is None:
|
|
|
|
return # Can't do anything
|
|
|
|
|
|
|
|
if self._selected_output == None or self._selected_output[1] != self._txid:
|
|
|
|
return # Can't do anything
|
|
|
|
|
|
|
|
if self._output_offset == None or self._output_offset[1] != self._txid:
|
|
|
|
return # Can't do anything
|
|
|
|
|
|
|
|
try:
|
|
|
|
transaction = await self._transactionstore.get_transaction(self._txid)
|
|
|
|
except KeyError:
|
|
|
|
return # Can't do anything
|
|
|
|
|
|
|
|
if self._selected_output[0] == len(transaction["vout"]) - 1:
|
|
|
|
return # At the end already
|
|
|
|
|
|
|
|
if self._selected_output[0] == self._output_offset[0] + 4:
|
|
|
|
self._output_offset = (self._output_offset[0] + 1, self._output_offset[1])
|
|
|
|
|
|
|
|
self._selected_output = (self._selected_output[0] + 1, self._selected_output[1])
|
|
|
|
|
|
|
|
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 == "KEY_UP":
|
|
|
|
await self._select_previous_input()
|
|
|
|
return None
|
|
|
|
|
|
|
|
if key == "KEY_DOWN":
|
|
|
|
await self._select_next_input()
|
|
|
|
return None
|
|
|
|
|
|
|
|
if key.lower() == "j" or key == "KEY_PPAGE":
|
|
|
|
await self._select_previous_output()
|
|
|
|
return None
|
|
|
|
|
|
|
|
if key.lower() == "k" or key == "KEY_NPAGE":
|
|
|
|
await self._select_next_output()
|
|
|
|
return None
|
|
|
|
|
|
|
|
if key == "KEY_RETURN" or key == "\n":
|
|
|
|
await self._select_input_as_transaction()
|
|
|
|
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()
|