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.
This commit is contained in:
parent
d79338a11b
commit
59980e34a2
7 changed files with 149 additions and 85 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
135
mmgen/tx.py
135
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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue