From 242ff6acfa03e981aa5ea91024a374610dd634f7 Mon Sep 17 00:00:00 2001 From: philemon Date: Mon, 21 Jul 2014 10:20:15 +0400 Subject: [PATCH] Version 0.7.4 Additions/improvements: mmgen-txsign - sign multiple transactions in one operation mmgen-addrimport - skip rescanning block chain for new addresses mmgen-tool - new functions: Bitcoind operations: listaccounts - like 'bitcoind listaccounts' but shows MMGen wallet balances too getbalance - like 'bitcoind getbalance' but shows confirmed/unconfirmed, spendable/unspendable MMGen-specific operations: id8 - generate 8-character MMGen ID checksum for file (or stdin) id6 - generate 6-character MMGen ID checksum for file (or stdin) --- mmgen-tool | 5 ++- mmgen-txsend | 5 ++- mmgen-txsign | 113 +++++++++++++++++++++++++----------------------- mmgen/config.py | 2 + mmgen/tool.py | 105 ++++++++++++++++++++++++++++++++++++++------ mmgen/tx.py | 27 +++++++----- mmgen/util.py | 4 +- setup.py | 2 +- 8 files changed, 181 insertions(+), 82 deletions(-) diff --git a/mmgen-tool b/mmgen-tool index 96274092..fa8ea6bc 100755 --- a/mmgen-tool +++ b/mmgen-tool @@ -38,7 +38,8 @@ help_data = { -v, --verbose Produce more verbose output COMMANDS:{} -Type '{} --help for usage information on a particular command +Type '{} --help for usage information on a particular +command """.format(command_help,prog_name) } @@ -62,7 +63,7 @@ if cmd_args and cmd_args[0] == '--help': tool_usage(prog_name, command) sys.exit(0) -args = process_args(prog_name, command, cmd_args) if cmd_args else [] +args = process_args(prog_name, command, cmd_args) #print command + "(" + ", ".join(args) + ")" eval(command + "(" + ", ".join(args) + ")") diff --git a/mmgen-txsend b/mmgen-txsend index c54d1b40..3667636c 100755 --- a/mmgen-txsend +++ b/mmgen-txsend @@ -77,7 +77,10 @@ qmsg("Signed transaction file '%s' is valid" % infile) warn = "Once this transaction is sent, there's no taking it back!" what = "broadcast this transaction to the network" -expect = "yes, i really want to do this".upper() +expect = "YES, I REALLY WANT TO DO THIS" + +if g.quiet: warn,expect = "","YES" + confirm_or_exit(warn, what, expect) msg("Sending transaction") diff --git a/mmgen-txsign b/mmgen-txsign index 80876f68..76e85642 100755 --- a/mmgen-txsign +++ b/mmgen-txsign @@ -29,8 +29,8 @@ from mmgen.util import msg,qmsg help_data = { 'prog_name': sys.argv[0].split("/")[-1], - 'desc': "Sign a Bitcoin transaction generated by mmgen-txcreate", - 'usage': "[opts] [mmgen wallet/seed/words/brain file]...", + 'desc': "Sign Bitcoin transactions generated by mmgen-txcreate", + 'usage': "[opts] ,.. [mmgen wallet/seed/words/brainwallet file]...", 'options': """ -h, --help Print this help message -d, --outdir d Specify an alternate directory 'd' for output @@ -101,70 +101,77 @@ for i in infiles: check_infile(i) c = connect_to_bitcoind() -tx_file = infiles.pop(0) -m = "" if 'tx_id' in opts else "transaction data" -tx_data = get_lines_from_file(tx_file,m) +seeds = {} +tx_files = [i for i in set(infiles) if get_extension(i) == g.rawtx_ext] +infiles = list(set(infiles) - set(tx_files)) -metadata,tx_hex,inputs_data,b2m_map = parse_tx_data(tx_data,tx_file) +if not "info" in opts: do_license_msg(immed=True) -if 'tx_id' in opts: - msg(metadata[0]) - sys.exit(0) +keys_from_file = get_lines_from_file(opts['keys_from_file'],"key data", + remove_comments=True) if 'keys_from_file' in opts else [] -if 'info' in opts: - view_tx_data(c,inputs_data,tx_hex,b2m_map,metadata) - sys.exit(0) +for tx_file in tx_files: + m = "" if 'tx_id' in opts else "transaction data" + tx_data = get_lines_from_file(tx_file,m) -do_license_msg(immed=True) + metadata,tx_hex,inputs_data,b2m_map = parse_tx_data(tx_data,tx_file) + + if 'tx_id' in opts: + msg(metadata[0]) + sys.exit(0) + + if 'info' in opts: + view_tx_data(c,inputs_data,tx_hex,b2m_map,metadata) + sys.exit(0) # Are inputs mmgen addresses? -mmgen_addrs = [i for i in inputs_data if parse_mmgen_label(i['account'])[0]] -other_addrs = [i for i in inputs_data if not parse_mmgen_label(i['account'])[0]] + mmgen_addrs = [i for i in inputs_data if parse_mmgen_label(i['account'])[0]] + other_addrs = [i for i in inputs_data if not parse_mmgen_label(i['account'])[0]] -keys = get_lines_from_file(opts['keys_from_file'],"key data",remove_comments=True) \ - if 'keys_from_file' in opts else [] + keys = keys_from_file -if other_addrs and not keys and not 'use_wallet_dat' in opts: - missing_keys_errormsg(other_addrs) - sys.exit(2) + if other_addrs and not keys and not 'use_wallet_dat' in opts: + missing_keys_errormsg(other_addrs) + sys.exit(2) -if other_addrs and keys and not 'skip_key_preverify' in opts: - a = [i['address'] for i in other_addrs] - preverify_keys(a, keys) + if other_addrs and keys and not 'skip_key_preverify' in opts: + a = [i['address'] for i in other_addrs] + preverify_keys(a, keys) + opts['skip_key_preverify'] = True -seeds = {} -check_mmgen_to_btc_addr_mappings(inputs_data,b2m_map,infiles,seeds,opts) + check_mmgen_to_btc_addr_mappings(inputs_data,b2m_map,infiles,seeds,opts) -qmsg("Successfully opened transaction file '%s'" % tx_file) + if len(tx_files) > 1: msg("\nTransaction #%s:" % (tx_files.index(tx_file)+1)) + qmsg("Successfully opened transaction file '%s'" % tx_file) -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) + 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) -sig_data = [ - {"txid":i['txid'],"vout":i['vout'],"scriptPubKey":i['scriptPubKey']} - for i in inputs_data] + sig_data = [ + {"txid":i['txid'],"vout":i['vout'],"scriptPubKey":i['scriptPubKey']} + for i in inputs_data] -if mmgen_addrs: - ml = [i['account'].split()[0] for i in mmgen_addrs] - keys += get_keys_for_mmgen_addrs(ml,infiles,seeds,opts) + if mmgen_addrs: + ml = [i['account'].split()[0] for i in mmgen_addrs] + keys += get_keys_for_mmgen_addrs(ml,infiles,seeds,opts) - if 'use_wallet_dat' in opts: - sig_tx = sign_tx_with_bitcoind_wallet(c,tx_hex,sig_data,keys,opts) + if 'use_wallet_dat' in opts: + sig_tx = sign_tx_with_bitcoind_wallet(c,tx_hex,sig_data,keys,opts) + else: + sig_tx = sign_transaction(c,tx_hex,sig_data,keys) + elif other_addrs: + if keys: + sig_tx = sign_transaction(c,tx_hex,sig_data,keys) + else: + sig_tx = sign_tx_with_bitcoind_wallet(c,tx_hex,sig_data,keys,opts) + + if sig_tx['complete']: + prompt = "OK\nSave signed transaction?" + if user_confirm(prompt,default_yes=True): + print_signed_tx_to_file(tx_hex,sig_tx['hex'],metadata,opts) else: - sig_tx = sign_transaction(c,tx_hex,sig_data,keys) -elif other_addrs: - if keys: - sig_tx = sign_transaction(c,tx_hex,sig_data,keys) - else: - sig_tx = sign_tx_with_bitcoind_wallet(c,tx_hex,sig_data,keys,opts) - -if sig_tx['complete']: - prompt = "OK\nSave signed transaction?" - if user_confirm(prompt,default_yes=True): - print_signed_tx_to_file(tx_hex,sig_tx['hex'],metadata,opts) -else: - msg("failed\nSome keys were missing. Transaction could not be signed.") - sys.exit(3) + msg("failed\nSome keys were missing. Transaction could not be signed.") + sys.exit(3) diff --git a/mmgen/config.py b/mmgen/config.py index 20c08811..8314801f 100755 --- a/mmgen/config.py +++ b/mmgen/config.py @@ -37,6 +37,8 @@ incog_hex_ext = "mmincox" seedfile_exts = wallet_ext, seed_ext, mn_ext, brain_ext, incog_ext +rawtx_ext = "raw" +sigtx_ext = "sig" addrfile_ext = "addrs" keyfile_ext = "keys" diff --git a/mmgen/tool.py b/mmgen/tool.py index bfc4327a..c7ffeaef 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -51,13 +51,21 @@ commands = { "mn_rand192": ['wordlist [str="electrum"]'], "mn_rand256": ['wordlist [str="electrum"]'], "mn_stats": ['wordlist [str="electrum"]'], - "mn_printlist": ['wordlist [str="electrum"]'] + "mn_printlist": ['wordlist [str="electrum"]'], + "id8": [' [str]'], + "id6": [' [str]'], + "listaccounts": ['minconf [int=1]'], + "getbalance": ['minconf [int=1]'], } command_help = """ -General operations: -hexdump - encode binary data in formatted hexadecimal form -unhexdump - decode formatted hexadecimal data +File operations +hexdump - encode data into formatted hexadecimal form (file or stdin) +unhexdump - decode formatted hexadecimal data (file or stdin) + +MMGen-specific operations +id8 - generate 8-character MMGen ID checksum for file (or stdin) +id6 - generate 6-character MMGen ID checksum for file (or stdin) Bitcoin operations: strtob58 - convert a string to base 58 @@ -68,13 +76,20 @@ randwif - generate a random private key in WIF format randpair - generate a random private key/address pair wif2addr - generate a Bitcoin address from a key in WIF format -Mnemonic operations (choose "electrum" (default), "tirosh" or "all" wordlists): +Mnemonic operations (choose "electrum" (default), "tirosh" or "all" +wordlists): mn_rand128 - generate random 128-bit mnemonic mn_rand192 - generate random 192-bit mnemonic mn_rand256 - generate random 256-bit mnemonic mn_stats - show stats for mnemonic wordlist mn_printlist - print mnemonic wordlist +Bitcoind operations (bitcoind must be running): +listaccounts - like 'bitcoind listaccounts' but shows MMGen wallet balances + too +getbalance - like 'bitcoind getbalance' but shows confirmed/unconfirmed, + spendable/unspendable + IMPORTANT NOTE: Though MMGen mnemonics use the Electrum wordlist, they're computed using a different algorithm and are NOT Electrum-compatible! """ @@ -119,17 +134,23 @@ def process_args(prog_name, command, uargs): ret = [] + def normalize_arg(arg, arg_type): + if arg_type == "bool": + if arg.lower() in ("true","yes","1","on"): return "True" + if arg.lower() in ("false","no","0","off"): return "False" + return arg + for i in range(len(cargs_req)): - arg,arg_type = uargs_req[i], cargs_req[i][1] - if arg == "true" or arg == "false": arg = arg.capitalize() + arg_type = cargs_req[i][1] + arg = normalize_arg(uargs_req[i], arg_type) if arg_type == "str": ret.append('"%s"' % (arg)) elif test_type(arg_type, arg, "#"+str(i+1)): ret.append('%s' % (arg)) for k in uargs_nam.keys(): - arg,arg_type = uargs_nam[k], cargs_nam[k][0] - if arg == "true" or arg == "false": arg = arg.capitalize() + arg_type = cargs_nam[k][0] + arg = normalize_arg(uargs_nam[k], arg_type) if arg_type == "str": ret.append('%s="%s"' % (k, arg)) elif test_type(arg_type, arg, "'"+k+"'"): @@ -149,13 +170,13 @@ def print_convert_results(indata,enc,dec,no_recode=False): msg("WARNING! Recoded number doesn't match input stringwise!") def hexdump(infile, cols=8, line_nums=True): - data = get_data_from_file(infile) - o = pretty_hexdump(data, 2, cols, line_nums) + d = sys.stdin.read() if infile == "-" else get_data_from_file(infile) + o = pretty_hexdump(d, 2, cols, line_nums) print o def unhexdump(infile): - data = get_data_from_file(infile) - o = decode_pretty_hexdump(data) + d = sys.stdin.read() if infile == "-" else get_data_from_file(infile) + o = decode_pretty_hexdump(d) sys.stdout.write(o) def strtob58(s): @@ -242,3 +263,61 @@ def mn_stats(wordlist="electrum"): def mn_printlist(wordlist="electrum"): l = get_wordlist(wordlist) print "%s" % l.strip() + +def id8(infile): + d = sys.stdin.read() if infile == "-" else get_data_from_file(infile) + print make_chksum_8(d) + +def id6(infile): + d = sys.stdin.read() if infile == "-" else get_data_from_file(infile) + print make_chksum_6(d) + + +def listaccounts(minconf=1): + from mmgen.tx import connect_to_bitcoind,trim_exponent,is_mmgen_addr + def s_mmgen(i): + ma = i[0].split(" ")[0] if " " in i[0] else i[0] + if is_mmgen_addr(ma): + mmid,idx = ma.split(":") + return mmid + ":" + ("%04i" % int(idx)) + else: + return "G"+i[0] + + c = connect_to_bitcoind() + data = [(a,c.getbalance(a,minconf)) for a in c.listaccounts()] + data.sort(key=s_mmgen) + col_w = max([len(d[0]) for d in data]) + fs = "%-"+str(col_w)+"s %s" + print fs % ("ACCOUNT","BALANCE") + totals = {} + for d in data: + ma = d[0].split(" ")[0] if " " in d[0] else d[0] + if is_mmgen_addr(ma): + mmid = ma.split(":")[0] + if mmid not in totals: totals[mmid] = 0 + totals[mmid] += d[1] + print fs % ( + d[0] if d[0] else 'TOTAL:', + trim_exponent(d[1]) + ) + print "\nMMGEN WALLET BALANCES" + for k in totals.keys(): + print "%s: %s" % (k, trim_exponent(totals[k])) + +def getbalance(minconf=1): + from mmgen.tx import connect_to_bitcoind,trim_exponent,is_mmgen_addr + c = connect_to_bitcoind() + data = c.listunspent(0) + o = [0,0,0,0,0,0] # su,sb,sc, uu,ub,uc + for d in data: + j = 0 if d.spendable else 3 + if d.confirmations == 0: o[j] += d.amount + k = 1 if d.confirmations < minconf else 2 + o[j+k] += d.amount + + fs = "{}:\n {:<12} unconfirmed\n {:<12} <{M} {C}\n {:<12} >={M} {C}" + for lbl,n in ("Spendable",0),("Unspendable",3): + if sum(o[n:3+n]) == 0: + print "{}: {}".format(lbl,"NONE") + else: + print fs.format(lbl,o[n+0],o[n+1],o[n+2],M=minconf,C="confirmations") diff --git a/mmgen/tx.py b/mmgen/tx.py index ee1e4415..6ac65347 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -141,7 +141,7 @@ def get_bitcoind_cfg_options(cfg_keys): def print_tx_to_file(tx,sel_unspent,send_amt,b2m_map,opts): tx_id = make_chksum_6(unhexlify(tx)).upper() - outfile = "tx_%s[%s].raw" % (tx_id,send_amt) + outfile = "tx_%s[%s].%s" % (tx_id,send_amt,g.rawtx_ext) if 'outdir' in opts: outfile = "%s/%s" % (opts['outdir'], outfile) metadata = "%s %s %s" % (tx_id, send_amt, make_timestamp()) @@ -156,7 +156,7 @@ def print_tx_to_file(tx,sel_unspent,send_amt,b2m_map,opts): def print_signed_tx_to_file(tx,sig_tx,metadata,opts): tx_id = make_chksum_6(unhexlify(tx)).upper() - outfile = "tx_{}[{}].sig".format(*metadata[:2]) + outfile = "tx_%s[%s].%s" % (metadata[0],metadata[1],g.sigtx_ext) if 'outdir' in opts: outfile = "%s/%s" % (opts['outdir'], outfile) data = "%s\n%s\n" % (" ".join(metadata),sig_tx) @@ -174,16 +174,17 @@ def print_sent_tx_to_file(tx,metadata,opts): def format_unspent_outputs_for_printing(out,sort_info,total): - pfs = " %-4s %-67s %-34s %-12s %-13s %-10s %s" + pfs = " %-4s %-67s %-34s %-12s %-13s %-8s %-10s %s" pout = [pfs % ("Num","TX id,Vout","Address","MMgen ID", - "Amount (BTC)","Age (days)", "Comment")] + "Amount (BTC)","Confirms","Age (days)", "Comment")] for n,i in enumerate(out): addr = "=" if i.skip == "addr" and "grouped" in sort_info else i.address tx = " " * 63 + "=" \ if i.skip == "txid" and "grouped" in sort_info else str(i.txid) - s = pfs % (str(n+1)+")", tx+","+str(i.vout),addr,i.mmid,i.amt,i.days,i.label) + s = pfs % (str(n+1)+")", tx+","+str(i.vout),addr, + i.mmid,i.amt,i.confirmations,i.days,i.label) pout.append(s.rstrip()) return \ @@ -200,14 +201,16 @@ def sort_and_view(unspent): def s_age(i): return i.confirmations def s_mmgen(i): return i.account - sort,group,show_mmaddr,reverse = "",False,False,False + sort,group,show_days,show_mmaddr,reverse = "age",False,False,True,True + unspent.sort(key=s_age,reverse=reverse) # Reverse age sort by default + total = trim_exponent(sum([i.amount for i in unspent])) hdr_fmt = "UNSPENT OUTPUTS (sort order: %s) Total BTC: %s" options_msg = """ Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr -Display options: [g]roup, show [m]mgen addr, r[e]draw screen +Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen """.strip() prompt = \ "('q' = quit sorting, 'p' = print to file, 'v' = pager view, 'w' = wide view): " @@ -230,8 +233,8 @@ Display options: [g]roup, show [m]mgen addr, r[e]draw screen addr_w = min(34+((1+max_acct_len) if show_mmaddr else 0),cols-46) tx_w = max(11,min(64, cols-addr_w-32)) fs = " %-4s %-" + str(tx_w) + "s %-2s %-" + str(addr_w) + "s %-13s %-s" - table_hdr = fs % ("Num","TX id Vout","","Address", - "Amount (BTC)","Age(d)") + a = "Age(d)" if show_days else "Confirms" + table_hdr = fs % ("Num","TX id Vout","","Address", "Amount (BTC)",a) unsp = deepcopy(unspent) for i in unsp: i.skip = "" @@ -277,12 +280,13 @@ Display options: [g]roup, show [m]mgen addr, r[e]draw screen out = [hdr_fmt % (" ".join(sort_info), total), table_hdr] for n,i in enumerate(unsp): - out.append(fs % (str(n+1)+")",i.tx,i.vout,i.addr,i.amt,i.days)) + d = i.days if show_days else i.confirmations + out.append(fs % (str(n+1)+")",i.tx,i.vout,i.addr,i.amt,d)) msg("\n".join(out) +"\n\n" + print_to_file_msg + options_msg) print_to_file_msg = "" - immed_chars = "atdAMrgmeqpvw" + immed_chars = "atDdAMrgmeqpvw" skip_prompt = False while True: @@ -290,6 +294,7 @@ Display options: [g]roup, show [m]mgen addr, r[e]draw screen if reply == 'a': unspent.sort(key=s_amt); sort = "amount" elif reply == 't': unspent.sort(key=s_txid); sort = "txid" + elif reply == 'D': show_days = False if show_days else True elif reply == 'd': unspent.sort(key=s_addr); sort = "address" elif reply == 'A': unspent.sort(key=s_age); sort = "age" elif reply == 'M': diff --git a/mmgen/util.py b/mmgen/util.py index fee99430..d9610b1a 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -42,7 +42,9 @@ def vmsg_r(s): def bail(): sys.exit(9) -def get_extension(f): return f.split(".")[-1] +def get_extension(f): + try: return f.split(".")[-1] + except: return "" def my_raw_input(prompt,echo=True,allowed_chars=""): try: diff --git a/setup.py b/setup.py index c414edf9..e47580a5 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from distutils.core import setup setup( name = 'mmgen', - version = '0.7.3b', + version = '0.7.4', author = 'Philemon', author_email = 'mmgen-py@yandex.com', url = 'https://github.com/mmgen/mmgen',