From 59980e34a21cc8be7420225d3b4c883fb6589d77 Mon Sep 17 00:00:00 2001 From: philemon Date: Fri, 8 Aug 2014 20:42:43 +0400 Subject: [PATCH] New user comment field in transaction files (*.raw and *.sig), readline editable in -txcreate, -txsign and -txsend. All UTF-8 characters allowed, <= 72 chars (non-UTF-8 terminals are limited to ASCII). Motivation: saved *.sig files now become valuable for recordkeeping. --- mmgen/config.py | 1 + mmgen/main_txcreate.py | 30 ++++++--- mmgen/main_txsend.py | 16 +++-- mmgen/main_txsign.py | 20 +++--- mmgen/tool.py | 15 +++-- mmgen/tx.py | 135 ++++++++++++++++++++++++++--------------- mmgen/util.py | 15 +++-- 7 files changed, 148 insertions(+), 84 deletions(-) diff --git a/mmgen/config.py b/mmgen/config.py index 65e4f595..39b7bb4e 100755 --- a/mmgen/config.py +++ b/mmgen/config.py @@ -30,6 +30,7 @@ version = '0.7.7' quiet,verbose = False,False min_screen_width = 80 +max_tx_comment_len = 72 from decimal import Decimal tx_fee = Decimal("0.0001") diff --git a/mmgen/main_txcreate.py b/mmgen/main_txcreate.py index 0cf24c0e..09028790 100755 --- a/mmgen/main_txcreate.py +++ b/mmgen/main_txcreate.py @@ -36,6 +36,7 @@ help_data = { 'usage': "[opts] ... [change addr] [addr file] ...", 'options': """ -h, --help Print this help message +-c, --comment-file= f Source the transaction's comment from file 'f' -d, --outdir= d Specify an alternate directory 'd' for output -e, --echo-passphrase Print passphrase to screen when typing it -f, --tx-fee= f Transaction fee (default: {g.tx_fee} BTC) @@ -196,26 +197,35 @@ if g.debug: print "tx_in:", repr(tx_in) print "tx_out:", repr(tx_out) +if 'comment_file' in opts: + comment = get_tx_comment_from_file(opts['comment_file']) + if comment == False: sys.exit(2) + if keypress_confirm("Edit comment?",False): + comment = get_tx_comment_from_user(comment) +else: + if keypress_confirm("Add a comment to transaction?",False): + comment = get_tx_comment_from_user() + else: comment = False + tx_hex = c.createrawtransaction(tx_in,tx_out) qmsg("Transaction successfully created") + prompt = "View decoded transaction? (y)es, (N)o, (v)iew in pager" reply = prompt_and_get_char(prompt,"YyNnVv",enter_ok=True) +amt = send_amt or change +tx_id = make_chksum_6(unhexlify(tx_hex)).upper() +metadata = tx_id, amt, make_timestamp() + if reply and reply in "YyVv": - pager = True if reply in "Vv" else False - view_tx_data(c,[i.__dict__ for i in sel_unspent],tx_hex,b2m_map,pager=pager) + view_tx_data(c,[i.__dict__ for i in sel_unspent],tx_hex,b2m_map, + comment,metadata,True if reply in "Vv" else False) prompt = "Save transaction?" if keypress_confirm(prompt,default_yes=True): - amt = send_amt or change - tx_id = make_chksum_6(unhexlify(tx_hex)).upper() outfile = "tx_%s[%s].%s" % (tx_id,amt,g.rawtx_ext) - data = "{} {} {}\n{}\n{}\n{}\n".format( - tx_id, amt, make_timestamp(), - tx_hex, - repr([i.__dict__ for i in sel_unspent]), - repr(b2m_map) - ) + data = make_tx_data("{} {} {}".format(*metadata), tx_hex, + [i.__dict__ for i in sel_unspent], b2m_map, comment) write_to_file(outfile,data,opts,"transaction",False,True) else: msg("Transaction not saved") diff --git a/mmgen/main_txsend.py b/mmgen/main_txsend.py index 85d2604c..31c36c81 100755 --- a/mmgen/main_txsend.py +++ b/mmgen/main_txsend.py @@ -51,7 +51,7 @@ do_license_msg() tx_data = get_lines_from_file(infile,"signed transaction data") -metadata,tx_hex,inputs_data,b2m_map = parse_tx_data(tx_data,infile) +metadata,tx_hex,inputs_data,b2m_map,comment = parse_tx_data(tx_data,infile) qmsg("Signed transaction file '%s' is valid" % infile) @@ -60,8 +60,16 @@ c = connect_to_bitcoind() prompt = "View transaction data? (y)es, (N)o, (v)iew in pager" reply = prompt_and_get_char(prompt,"YyNnVv",enter_ok=True) if reply and reply in "YyVv": - p = True if reply in "Vv" else False - view_tx_data(c,inputs_data,tx_hex,b2m_map,metadata,pager=p) + view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata, + pager=True if reply in "Vv" else False) + +if keypress_confirm("Edit transaction comment?"): + comment = get_tx_comment_from_user(comment) + data = make_tx_data("{} {} {}".format(*metadata), tx_hex, + inputs_data, b2m_map, comment) + w = "signed transaction with edited comment" + outfile = infile + write_to_file(outfile,data,opts,w,False,True,True) warn = "Once this transaction is sent, there's no taking it back!" what = "broadcast this transaction to the network" @@ -81,5 +89,5 @@ except: msg("Transaction sent: %s" % tx_id) -of = "tx_{}[{}].out".format(*metadata[:2]) +of = "tx_{}[{}].txid".format(*metadata[:2]) write_to_file(of, tx_id+"\n",opts,"transaction ID",True,True) diff --git a/mmgen/main_txsign.py b/mmgen/main_txsign.py index dcbd8c89..2e28cae6 100755 --- a/mmgen/main_txsign.py +++ b/mmgen/main_txsign.py @@ -123,7 +123,7 @@ if 'keys_from_file' in opts: while True: d_dec = mmgen_decrypt(d,"encrypted keylist","",opts) if d_dec: d = d_dec; break - else: msg("Trying again...") + msg("Trying again...") keys_from_file = remove_comments(d.split("\n")) else: keys_from_file = [] @@ -136,7 +136,7 @@ for tx_num,tx_file in enumerate(tx_files,1): m = "" if 'tx_id' in opts else "transaction data" tx_data = get_lines_from_file(tx_file,m) - metadata,tx_hex,inputs_data,b2m_map = parse_tx_data(tx_data,tx_file) + metadata,tx_hex,inputs_data,b2m_map,comment = parse_tx_data(tx_data,tx_file) qmsg("Successfully opened transaction file '%s'" % tx_file) if 'tx_id' in opts: @@ -144,7 +144,7 @@ for tx_num,tx_file in enumerate(tx_files,1): sys.exit(0) if 'info' in opts: - view_tx_data(c,inputs_data,tx_hex,b2m_map,metadata) + view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata) sys.exit(0) # Are inputs mmgen addresses? @@ -177,8 +177,8 @@ for tx_num,tx_file in enumerate(tx_files,1): p = "View data for transaction{}? (y)es, (N)o, (v)iew in pager" reply = prompt_and_get_char(p.format(tx_num_str),"YyNnVv",enter_ok=True) if reply and reply in "YyVv": - p = True if reply in "Vv" else False - view_tx_data(c,inputs_data,tx_hex,b2m_map,metadata,pager=p) + view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata, + True if reply in "Vv" else False) sig_data = [ {"txid":i['txid'],"vout":i['vout'],"scriptPubKey":i['scriptPubKey']} @@ -200,13 +200,11 @@ for tx_num,tx_file in enumerate(tx_files,1): if sig_tx['complete']: msg("OK") + if keypress_confirm("Edit transaction comment?"): + comment = get_tx_comment_from_user(comment) outfile = "tx_%s[%s].%s" % (metadata[0],metadata[1],g.sigtx_ext) - data = "{}\n{}\n{}\n{}\n".format( - " ".join(metadata[:2] + [make_timestamp()]), - sig_tx['hex'], - repr(inputs_data), - repr(b2m_map) - ) + data = make_tx_data("{} {} {t}".format(*metadata[:2], t=make_timestamp()), + sig_tx['hex'], inputs_data, b2m_map, comment) w = "signed transaction{}".format(tx_num_str) write_to_file(outfile,data,opts,w,(not g.quiet),True,False) else: diff --git a/mmgen/tool.py b/mmgen/tool.py index deb1e4cc..dc42c0d6 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -60,7 +60,7 @@ commands = { "str2id6": [' [str]'], "listaddresses":['minconf [int=1]', 'showempty [bool=False]'], "getbalance": ['minconf [int=1]'], - "viewtx": [' [str]'], + "txview": [' [str]','pager [bool=False]'], "check_addrfile": [' [str]'], "find_incog_data": [' [str]',' [str]','keep_searching [bool=False]'], "hexreverse": [' [str]'], @@ -99,7 +99,7 @@ command_help = """ getbalance - like 'bitcoind getbalance' but shows confirmed/unconfirmed, spendable/unspendable balances for individual {pnm} wallets listaddresses - list {pnm} addresses and their balances - viewtx - show raw/signed {pnm} transaction in human-readable form + txview - show raw/signed {pnm} transaction in human-readable form General utilities: bytespec - convert a byte specifier such as '1GB' into a plain integer @@ -372,12 +372,12 @@ def getbalance(minconf=1): for key in sorted(accts.keys()): print fs.format(key+":", *[str(trim_exponent(a))+" BTC" for a in accts[key]]) -def viewtx(infile): +def txview(infile,pager=False): c = connect_to_bitcoind() tx_data = get_lines_from_file(infile,"transaction data") - metadata,tx_hex,inputs_data,b2m_map = parse_tx_data(tx_data,infile) - view_tx_data(c,inputs_data,tx_hex,b2m_map,metadata) + metadata,tx_hex,inputs_data,b2m_map,comment = parse_tx_data(tx_data,infile) + view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,pager) def check_addrfile(infile): parse_addrs_file(infile) @@ -429,7 +429,10 @@ def encrypt(infile,outfile="",hash_preset=''): def decrypt(infile,outfile="",hash_preset=''): enc_d = get_data_from_file(infile,"encrypted data") - dec_d = mmgen_decrypt(enc_d,"user data","",opts) + while True: + dec_d = mmgen_decrypt(enc_d,"user data","",opts) + if dec_d: break + msg("Trying again...") if outfile == '-': write_to_stdout(dec_d,"decrypted data",confirm=True) else: diff --git a/mmgen/tx.py b/mmgen/tx.py index 04c5e56b..a1a0c640 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -32,14 +32,14 @@ from mmgen.term import do_pager,get_char txmsg = { 'not_enough_btc': "Not enough BTC in the inputs for this transaction (%s BTC)", 'throwaway_change': """ -ERROR: This transaction produces change (%s BTC); however, no change -address was specified. +ERROR: This transaction produces change (%s BTC); however, no change address +was specified. """.strip(), 'mixed_inputs': """ -NOTE: This transaction uses a mixture of both mmgen and non-mmgen inputs, -which makes the signing process more complicated. When signing the -transaction, keys for the non-mmgen inputs must be supplied in a separate -file using either the '-k' or '-K' option to '{}-txsign'. +NOTE: This transaction uses a mixture of both mmgen and non-mmgen inputs, which +makes the signing process more complicated. When signing the transaction, keys +for the non-mmgen inputs must be supplied in a separate file using either the +'-k' or '-K' option to '{}-txsign'. Selected mmgen inputs: %s""".format(g.proj_name.lower()), 'too_many_acct_addresses': """ @@ -326,16 +326,15 @@ def parse_mmgen_label(s,check_label_len=False): return tuple(l) -def view_tx_data(c,inputs_data,tx_hex,b2m_map,metadata=[],pager=False): +def view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,pager=False): td = c.decoderawtransaction(tx_hex) out = "TRANSACTION DATA\n\n" - - if metadata: - out += "Header: [Tx ID: {}] [Amount: {} BTC] [Time: {}]\n\n".format(*metadata) - + out += "Header: [Tx ID: {}] [Amount: {} BTC] [Time: {}]\n\n".format(*metadata) + if comment: out += "Comment: %s\n\n" % comment out += "Inputs:\n\n" + total_in = 0 for n,i in enumerate(td['vin']): for j in inputs_data: @@ -377,46 +376,46 @@ def view_tx_data(c,inputs_data,tx_hex,b2m_map,metadata=[],pager=False): out += "Total output: %s BTC\n" % trim_exponent(total_out) out += "TX fee: %s BTC\n" % trim_exponent(total_in-total_out) - if pager: do_pager(out) - else: print "\n"+out + o = out.encode("utf8") + if pager: do_pager(o) + else: print "\n"+o def parse_tx_data(tx_data,infile): - try: + err_str,err_fmt = "","Invalid %s in transaction file" + + if len(tx_data) == 5: + metadata,tx_hex,inputs_data,outputs_data,comment = tx_data + elif len(tx_data) == 4: metadata,tx_hex,inputs_data,outputs_data = tx_data - except: - msg("'%s': not a transaction file" % infile) - sys.exit(2) + comment = "" + else: + err_str = "number of lines" - err_fmt = "Transaction %s is invalid" + if not err_str: + if len(metadata.split()) != 3: + err_str = "metadata" + else: + try: unhexlify(tx_hex) + except: err_str = "hex data" + else: + try: inputs_data = eval(inputs_data) + except: err_str = "inputs data" + else: + try: outputs_data = eval(outputs_data) + except: err_str = "mmgen-to-btc address map data" + else: + if is_valid_tx_comment(comment,True): + comment = comment.decode("utf8") + else: + err_str = "comment" - if len(metadata.split()) != 3: - msg(err_fmt % "metadata") - sys.exit(2) - - try: unhexlify(tx_hex) - except: - msg(err_fmt % "hex data") + if err_str: + msg(err_fmt % err_str) sys.exit(2) else: - if not tx_hex: - msg("Transaction is empty!") - sys.exit(2) - - try: - inputs_data = eval(inputs_data) - except: - msg(err_fmt % "inputs data") - sys.exit(2) - - try: - outputs_data = eval(outputs_data) - except: - msg(err_fmt % "mmgen to btc address map data") - sys.exit(2) - - return metadata.split(),tx_hex,inputs_data,outputs_data + return metadata.split(),tx_hex,inputs_data,outputs_data,comment def select_outputs(unspent,prompt): @@ -687,7 +686,7 @@ def sign_tx_with_bitcoind_wallet(c,tx_hex,tx_num_str,sig_data,keys,opts): def preverify_keys(addrs_in, keys_in, mm_inputs): - addrs,keys,extra_keys = set(addrs_in),set(keys_in),[] + addrs,keys = set(addrs_in),set(keys_in) import mmgen.bitcoin as b @@ -713,16 +712,10 @@ def preverify_keys(addrs_in, keys_in, mm_inputs): if addr in addrs: addrs.remove(addr) if not addrs: break - else: - extra_keys.append(k) except KeyboardInterrupt: msg("\nSkipping") else: msg("") - if extra_keys: - s = "" if len(extra_keys) == 1 else "s" - msg("%s extra key%s found" % (len(extra_keys),s)) - if addrs: mms = dict([(i['address'],i['account'].split()[0]) for i in mm_inputs if i['address'] in addrs]) @@ -732,6 +725,12 @@ def preverify_keys(addrs_in, keys_in, mm_inputs): for a in sorted(addrs): print " %s%s" % (a, " ({})".format(mms[a]) if a in mms else "") sys.exit(2) + else: + extra_keys = len(keys) - len(set(addrs_in)) + if extra_keys > 0: + s = "" if extra_keys == 1 else "s" + msg("%s extra key%s found" % (extra_keys,s)) + def missing_keys_errormsg(other_addrs): @@ -766,3 +765,41 @@ def check_mmgen_to_btc_addr_mappings_addrfile(mmgen_inputs,b2m_map,addrfiles): confirm_or_exit(txmsg['missing_mappings'] % " ".join(missing),"continue") else: qmsg("Address mappings OK") + + +def is_valid_tx_comment(c, verbose=True): + if len(c) > g.max_tx_comment_len: + if verbose: msg("Invalid transaction comment (longer than %s characters)" % + g.max_tx_comment_len) + return False + try: c.decode("utf8") + except: + if verbose: msg("Invalid transaction comment (not UTF-8)") + return False + else: return True + +def get_tx_comment_from_file(infile): + c = get_data_from_file(infile,"transaction comment") + if is_valid_tx_comment(c, verbose=True): + return c.decode("utf8").strip() + else: return False + + +def get_tx_comment_from_user(comment=""): + + try: + while True: + c = my_raw_input("Comment: ", echo=True, + insert_txt=comment.encode("utf8")) + if c == "": return False + if is_valid_tx_comment(c, verbose=True): + return c.decode("utf8") + except KeyboardInterrupt: + msg("User interrupt") + return False + + +def make_tx_data(metadata_fmt, tx_hex, inputs_data, b2m_map, comment): + lines = (metadata_fmt, tx_hex, repr(inputs_data), repr(b2m_map)) + \ + ((comment,) if comment else ()) + return "\n".join(lines).encode("utf8")+"\n" diff --git a/mmgen/util.py b/mmgen/util.py index 9d706546..e7b18b17 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -712,14 +712,21 @@ def get_hash_preset_from_user(hp='3',what="data"): else: return hp -def my_raw_input(prompt,echo=True): - msg_r(prompt) +def my_raw_input(prompt,echo=True,insert_txt=""): + + if not sys.stdout.isatty(): insert_txt = "" + + import readline + def st_hook(): readline.insert_text(insert_txt) + readline.set_startup_hook(st_hook) + + msg_r("" if insert_txt else prompt) kb_hold_protect() if echo: - reply = raw_input("") + reply = raw_input(prompt if insert_txt else "") else: from getpass import getpass - reply = getpass("") + reply = getpass(prompt if insert_txt else "") kb_hold_protect() return reply