Browse Source

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.

philemon 10 years ago
parent
commit
59980e34a2
7 changed files with 149 additions and 85 deletions
  1. 1 0
      mmgen/config.py
  2. 20 10
      mmgen/main_txcreate.py
  3. 12 4
      mmgen/main_txsend.py
  4. 9 11
      mmgen/main_txsign.py
  5. 9 6
      mmgen/tool.py
  6. 87 50
      mmgen/tx.py
  7. 11 4
      mmgen/util.py

+ 1 - 0
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")

+ 20 - 10
mmgen/main_txcreate.py

@@ -36,6 +36,7 @@ help_data = {
 	'usage':   "[opts]  <addr,amt> ... [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")

+ 12 - 4
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)

+ 9 - 11
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:

+ 9 - 6
mmgen/tool.py

@@ -60,7 +60,7 @@ commands = {
 	"str2id6":      ['<string (spaces are ignored)> [str]'],
 	"listaddresses":['minconf [int=1]', 'showempty [bool=False]'],
 	"getbalance":   ['minconf [int=1]'],
-	"viewtx":       ['<MMGen tx file> [str]'],
+	"txview":       ['<MMGen tx file> [str]','pager [bool=False]'],
 	"check_addrfile": ['<MMGen addr file> [str]'],
 	"find_incog_data": ['<file or device name> [str]','<Incog ID> [str]','keep_searching [bool=False]'],
 	"hexreverse":   ['<hexadecimal string> [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:

+ 87 - 50
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:
-		metadata,tx_hex,inputs_data,outputs_data = tx_data
-	except:
-		msg("'%s': not a transaction file" % infile)
-		sys.exit(2)
-
-	err_fmt = "Transaction %s is invalid"
+	err_str,err_fmt = "","Invalid %s in transaction file"
 
-	if len(metadata.split()) != 3:
-		msg(err_fmt % "metadata")
-		sys.exit(2)
-
-	try: unhexlify(tx_hex)
-	except:
-		msg(err_fmt % "hex data")
-		sys.exit(2)
+	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
+		comment = ""
 	else:
-		if not tx_hex:
-			msg("Transaction is empty!")
-			sys.exit(2)
+		err_str = "number of lines"
 
-	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")
+	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 err_str:
+		msg(err_fmt % err_str)
 		sys.exit(2)
-
-	return metadata.split(),tx_hex,inputs_data,outputs_data
+	else:
+		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"

+ 11 - 4
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