From 38e0a954d0701552ae9d23df4fc2aa97a7b77839 Mon Sep 17 00:00:00 2001 From: philemon Date: Sun, 27 Jul 2014 00:33:45 +0400 Subject: [PATCH] * better options handling * code cleanups * new commands in 'mmgen-tool' * tx view in 'mmgen-txsend' --- MANIFEST | 1 - mmgen-addrgen | 108 +++++++-------- mmgen-addrimport | 14 +- mmgen-passchg | 27 ++-- mmgen-pywallet | 15 +- mmgen-tool | 8 +- mmgen-txcreate | 72 +++++----- mmgen-txsend | 47 +++---- mmgen-txsign | 47 +++---- mmgen-walletchk | 22 +-- mmgen-walletgen | 45 +++--- mmgen/Opts.py | 120 +++++++++------- mmgen/addr.py | 61 ++++---- mmgen/config.py | 14 +- mmgen/license.py | 6 +- mmgen/tool.py | 189 +++++++++++++------------ mmgen/tx.py | 354 +++++++++++++++++++++-------------------------- mmgen/util.py | 76 +++++----- setup.py | 2 +- 19 files changed, 573 insertions(+), 655 deletions(-) diff --git a/MANIFEST b/MANIFEST index 1476d3fd..56a9426d 100644 --- a/MANIFEST +++ b/MANIFEST @@ -41,4 +41,3 @@ mmgen/tests/mnemonic.py mmgen/tests/test.py mmgen/tests/util.py mmgen/tests/walletgen.py -test/test.py diff --git a/mmgen-addrgen b/mmgen-addrgen index 010c9c15..c40c82b8 100755 --- a/mmgen-addrgen +++ b/mmgen-addrgen @@ -30,51 +30,51 @@ from mmgen.license import * from mmgen.util import * from mmgen.addr import * -invoked_as = sys.argv[0].split("-")[-1] -gen_what = "addresses" if invoked_as == "addrgen" else "keys" - -if invoked_as == "keygen": - extra_help_data = ( - "\n-A, --no-addresses Print only secret keys, no addresses", - "\n-x, --b16 Print secret keys in hexadecimal too", - "\nBy default, both addresses and secret keys are generated." - ) -else: extra_help_data = ("","","") +gen_what = "addresses" if sys.argv[0].split("-")[-1] == "addrgen" else "keys" help_data = { 'prog_name': sys.argv[0].split("/")[-1], - 'desc': """Generate a list or range of {} from a {} wallet, - mnemonic, seed or password""".format(gen_what,g.proj_name), + 'desc': """Generate a list or range of {} from an {g.proj_name} wallet, + mnemonic, seed or password""".format(gen_what,g=g), 'usage':"[opts] [infile]
", 'options': """ -h, --help Print this help message{} --d, --outdir d Specify an alternate directory 'd' for output +-d, --outdir= d Specify an alternate directory 'd' for output -e, --echo-passphrase Echo passphrase or mnemonic to screen upon entry -H, --show-hash-presets Show information on available hash presets -K, --no-keyconv Use internal libraries for address generation instead of 'keyconv' --l, --seed-len N Length of seed. Options: {} - (default: {}) --p, --hash-preset p Use scrypt.hash() parameters from preset 'p' - when hashing password (default: '{}') --P, --passwd-file f Get passphrase from file 'f' +-l, --seed-len= N Length of seed. Options: {seed_lens} + (default: {g.seed_len}) +-p, --hash-preset= p Use scrypt.hash() parameters from preset 'p' when + hashing password (default: '{g.hash_preset}') +-P, --passwd-file= f Get passphrase from file 'f' -q, --quiet Suppress warnings; overwrite files without prompting --S, --stdout Print {W} to stdout +-S, --stdout Print {what} to stdout -v, --verbose Produce more verbose output{} --b, --from-brain l,p Generate {W} from a user-created password, +-b, --from-brain= l,p Generate {what} from a user-created password, i.e. a "brainwallet", using seed length 'l' and hash preset 'p' (comma-separated) --g, --from-incog Generate {W} from an incognito wallet --X, --from-incog-hex Generate {W} from an incognito hexadecimal wallet --G, --from-incog-hidden f,o,l Generate {W} from incognito data in file +-g, --from-incog Generate {what} from an incognito wallet +-X, --from-incog-hex Generate {what} from incognito hexadecimal wallet +-G, --from-incog-hidden= f,o,l Generate {what} from incognito data in file 'f' at offset 'o', with seed length of 'l' --m, --from-mnemonic Generate {W} from an electrum-like mnemonic --s, --from-seed Generate {W} from a seed in .{S} format +-m, --from-mnemonic Generate {what} from an electrum-like mnemonic +-s, --from-seed Generate {what} from a seed in .{g.seed_ext} format +""".format( + *( + ("\n-A, --no-addresses Print only secret keys, no addresses", + "\n-x, --b16 Print secret keys in hexadecimal too") + if gen_what == "keys" else ("","")), + seed_lens=", ".join([str(i) for i in g.seed_lens]), + what=gen_what, g=g +), + 'notes': """ -Addresses are given in a comma-separated list. Hyphen-separated -ranges are also allowed.{} +Addresses are given in a comma-separated list. Hyphen-separated ranges are +also allowed.{} If available, the external 'keyconv' program will be used for address generation. @@ -96,36 +96,14 @@ the 'p' parameter of the '--from-brain' option The '--from-brain' option also requires the user to specify a seed length (the 'l' parameter) -For a brainwallet passphrase to always generate the same keys and -addresses, the same 'l' and 'p' parameters to '--from-brain' must be used -in all future invocations with that passphrase -""".format( - extra_help_data[0], - ", ".join([str(i) for i in g.seed_lens]), - g.seed_len, - g.hash_preset, - extra_help_data[1], - extra_help_data[2], - W=gen_what, - S=g.seed_ext - ) +For a brainwallet passphrase to always generate the same keys and addresses, +the same 'l' and 'p' parameters to '--from-brain' must be used in all future +invocations with that passphrase +""".format("\n\nBy default, both addresses and secret keys are generated." + if gen_what == "keys" else "") } -short_opts = ["h","A","d:","e", - "H","K","l:","p:", - "P:","q","S","v","x","b:", - "g","X","G:","m","s"] -long_opts = ["help","no_addresses","outdir=","echo_passphrase", - "show_hash_presets","no_keyconv","seed_len=","hash_preset=", - "passwd_file=","quiet","stdout","verbose","b16","from_brain=", - "from_incog","from_incog_hex","from_incog_hidden=", - "from_mnemonic","from_seed"] - -if invoked_as == "addrgen": - for i in "A","x": short_opts.remove(i) - for i in "no_addresses","b16": long_opts.remove(i) - -opts,cmd_args = process_opts(sys.argv,help_data,"".join(short_opts),long_opts) +opts,cmd_args = parse_opts(sys.argv,help_data) if 'show_hash_presets' in opts: show_hash_presets() if 'verbose' in opts: g.verbose = True @@ -135,8 +113,6 @@ if 'from_incog_hex' in opts or 'from_incog_hidden' in opts: opts['gen_what'] = gen_what -check_opts(opts,long_opts) - if g.debug: show_opts_and_cmd_args(opts,cmd_args) if len(cmd_args) == 1 and ( @@ -158,12 +134,12 @@ if not addr_list: sys.exit(2) do_license_msg() # Interact with user: -if invoked_as == "keygen" and not g.quiet: +if gen_what == "keys" and not g.quiet: confirm_or_exit(cmessages['unencrypted_secret_keys'], 'continue') # Generate data: -if invoked_as == "addrgen": +if gen_what == "addresses": opts['print_addresses_only'] = True else: if not 'no_addresses' in opts: opts['print_secret'] = True @@ -172,11 +148,16 @@ seed = get_seed_retry(infile,opts) seed_id = make_chksum_8(seed) addr_data = generate_addrs(seed, addr_list, opts) -addr_data_str = format_addr_data(addr_data, seed_id, opts) +addr_data_chksum = make_chksum_8( + " ".join(["{} {}".format(a['num'],a['addr']) for a in addr_data]), + sep=True +) +addr_data_str = format_addr_data( + addr_data, addr_data_chksum, seed_id, addr_list, opts) # Output data: if 'stdout' in opts: - if invoked_as == "keygen" and not g.quiet: + if gen_what == "keys" and not g.quiet: confirm = True else: confirm = False write_to_stdout(addr_data_str,"secret keys",confirm) @@ -184,3 +165,8 @@ elif not sys.stdout.isatty(): write_to_stdout(addr_data_str,"secret keys",confirm=False) else: write_addr_data_to_file(seed, addr_data_str, addr_list, opts) + +msg(""" +Checksum for address data {}[{}]: {} +Remember this checksum or save it to a secure location +""".format(seed_id, fmt_addr_list(addr_list), addr_data_chksum).strip()) diff --git a/mmgen-addrimport b/mmgen-addrimport index 54d5f891..1757315d 100755 --- a/mmgen-addrimport +++ b/mmgen-addrimport @@ -33,25 +33,21 @@ help_data = { 'usage':"[opts] [mmgen address file]", 'options': """ -h, --help Print this help message --l, --addrlist f Import the non-mmgen Bitcoin addresses listed in file 'f' +-l, --addrlist= f Import the non-mmgen Bitcoin addresses listed in file 'f' +-q, --quiet Suppress warnings -r, --rescan Rescan the blockchain. Required if address to import is on the blockchain and has a balance. Rescanning is slow. --q, --quiet Suppress warnings """ } -short_opts = "hl:qr" -long_opts = "help", "addrlist=", "quiet", "rescan" +opts,cmd_args = parse_opts(sys.argv,help_data) -opts,cmd_args = process_opts(sys.argv,help_data,"".join(short_opts),long_opts) if 'quiet' in opts: g.quiet = True if len(cmd_args) != 1 and not 'addrlist' in opts: msg("You must specify an mmgen address list (and/or non-mmgen addresses with the '--addrlist' option)") sys.exit(1) -check_opts(opts,long_opts) - if cmd_args: check_infile(cmd_args[0]) seed_id,addr_data = parse_addrs_file(cmd_args[0]) @@ -59,9 +55,9 @@ else: seed_id,addr_data = "",[] if 'addrlist' in opts: - l = get_lines_from_file(opts['addrlist'],"non-mmgen addresses", + lines = get_lines_from_file(opts['addrlist'],"non-mmgen addresses", remove_comments=True) - addr_data += [(None,i) for i in l] + addr_data += [(None,l) for l in lines] from mmgen.bitcoin import verify_addr qmsg_r("Validating addresses...") diff --git a/mmgen-passchg b/mmgen-passchg index a6117dc3..2b64cbf5 100755 --- a/mmgen-passchg +++ b/mmgen-passchg @@ -26,37 +26,34 @@ from mmgen.util import * import mmgen.config as g help_data = { - 'desc': """Change the passphrase, hash preset or label of a {} + 'desc': """Change the passphrase, hash preset or label of an {} deterministic wallet""".format(g.proj_name), 'usage': "[opts] [filename]", 'options': """ -h, --help Print this help message --d, --outdir d Specify an alternate directory 'd' for output +-d, --outdir= d Specify an alternate directory 'd' for output -H, --show-hash-presets Show information on available hash presets -k, --keep-old-passphrase Keep old passphrase (use when changing hash strength or label only) --L, --label l Change the wallet's label to 'l' --p, --hash-preset p Change scrypt.hash() parameters to preset 'p' - (default: '{}') --P, --passwd-file f Get new passphrase from file 'f' +-L, --label= l Change the wallet's label to 'l' +-p, --hash-preset= p Change scrypt.hash() parameters to preset 'p' + (default: '{g.hash_preset}') +-P, --passwd-file= f Get new passphrase from file 'f' -q, --quiet Suppress warnings; overwrite files without prompting -v, --verbose Produce more verbose output +""".format(g=g), + 'notes': """ -NOTE: The key ID will change if either the passphrase or hash preset - are changed -""".format(g.hash_preset) +NOTE: The key ID will change if either the passphrase or hash preset are + changed +""" } -short_opts = "hd:HkL:p:P:qv" -long_opts = "help","outdir=","show_hash_presets","keep_old_passphrase",\ - "label=","hash_preset=","passwd_file=","quiet","verbose" +opts,cmd_args = parse_opts(sys.argv,help_data) -opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts) if 'quiet' in opts: g.quiet = True if 'verbose' in opts: g.verbose = True -check_opts(opts,long_opts) - if 'show_hash_presets' in opts: show_hash_presets() if len(cmd_args) != 1: diff --git a/mmgen-pywallet b/mmgen-pywallet index 401ffb3b..fff14028 100755 --- a/mmgen-pywallet +++ b/mmgen-pywallet @@ -75,26 +75,21 @@ help_data = { 'usage': "[opts] ", 'options': """ -h, --help Print this help message --d, --outdir d Specify an alternate directory 'd' for output +-d, --outdir= d Specify an alternate directory 'd' for output -e, --echo-passphrase Display passphrase on screen upon entry -j, --json Dump wallet in json format -k, --keys Dump all private keys (flat list) -a, --addrs Dump all addresses (flat list) --K, --keysforaddrs f Dump private keys for addresses listed in file 'f' --P, --passwd-file f Get passphrase from file 'f' +-K, --keysforaddrs= f Dump private keys for addresses listed in file 'f' +-P, --passwd-file= f Get passphrase from file 'f' -S, --stdout Dump to stdout rather than file """ } -short_opts = "hd:ejkaK:P:S" -long_opts = "help","outdir=","echo_passphrase","json","keys","addrs",\ - "keysforaddrs=","passwd_file=","stdout" +opts,cmd_args = parse_opts(sys.argv,help_data) -opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts) -check_opts(opts,long_opts) - -from mmgen.util import check_infile if len(cmd_args) == 1: + from mmgen.util import check_infile check_infile(cmd_args[0]) else: usage(help_data) diff --git a/mmgen-tool b/mmgen-tool index fa8ea6bc..da66e9b8 100755 --- a/mmgen-tool +++ b/mmgen-tool @@ -36,6 +36,8 @@ help_data = { -h, --help Print this help message -q, --quiet Produce quieter output -v, --verbose Produce more verbose output +""", + 'notes': """ COMMANDS:{} Type '{} --help for usage information on a particular @@ -43,11 +45,9 @@ command """.format(command_help,prog_name) } -short_opts = "hqv" -long_opts = "help","quiet","verbose" +opts,cmd_args = parse_opts(sys.argv,help_data) -opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts) -if 'quiet' in opts: g.quiet = True +if 'quiet' in opts: g.quiet = True if 'verbose' in opts: g.verbose = True if len(cmd_args) < 1: diff --git a/mmgen-txcreate b/mmgen-txcreate index ae237279..1d064f6e 100755 --- a/mmgen-txcreate +++ b/mmgen-txcreate @@ -37,34 +37,32 @@ help_data = { 'usage': "[opts] ... [change addr] [addr file] ...", 'options': """ -h, --help Print this help message --d, --outdir d Specify an alternate directory 'd' for output +-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) -i, --info Display unspent outputs and exit -q, --quiet Suppress warnings; overwrite files without prompting - --f, --tx-fee f Transaction fee (default: %s BTC) +""".format(g=g), + 'notes': """ Transaction inputs are chosen from a list of the user's unpent outputs via an interactive menu. Ages of transactions are approximate based on an average block creation -interval of %s minutes. +interval of {g.mins_per_block} minutes. Addresses on the command line can be Bitcoin addresses or MMGen addresses of the form :. To send all inputs (minus TX fee) to a single output, specify one address with no amount on the command line. -""" % (Decimal(g.tx_fee),g.mins_per_block) +""".format(g=g) } -short_opts = "hd:eiqf:" -long_opts = "help","outdir=","echo_passphrase","info","quiet","tx_fee=" +opts,cmd_args = parse_opts(sys.argv,help_data) -opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts) if 'quiet' in opts: g.quiet = True -check_opts(opts,long_opts) if g.debug: show_opts_and_cmd_args(opts,cmd_args) @@ -74,31 +72,39 @@ if not 'info' in opts: tx_out,addr_data,b2m_map,acct_data,change_addr = {},[],{},[],"" - for a in [i for i in cmd_args if match_ext(i,g.addrfile_ext)]: - if match_ext(a,g.addrfile_ext): - check_infile(a) - addr_data.append(parse_addrs_file(a)) + addrfiles = [a for a in cmd_args if get_extension(a) == g.addrfile_ext] + cmd_args = set(cmd_args) - set(addrfiles) - def mm_addr2btc_addr(c,mmadr,acct_data,addr_data,b2m_map): - btcaddr,label = mmgen_addr_to_walletd(c,mmadr,acct_data) + for a in addrfiles: + check_infile(a) + addr_data.append(parse_addrs_file(a)) + + def mmaddr2btcaddr(c,mmaddr,acct_data,addr_data,b2m_map): + # assume mmaddr has already been checked + btcaddr,label = mmaddr2btcaddr_bitcoind(c,mmaddr,acct_data) if not btcaddr: - btcaddr,label = mmgen_addr_to_addr_data(mmadr,addr_data) - b2m_map[btcaddr] = mmadr,label + if addr_data: + btcaddr,label = mmaddr2btcaddr_addrfile(mmaddr,addr_data) + else: + msg(txmsg['addrfile_no_data_msg'] % mmaddr) + sys.exit(2) + + b2m_map[btcaddr] = mmaddr,label return btcaddr - for a in [i for i in cmd_args if not match_ext(i,g.addrfile_ext)]: + for a in cmd_args: if "," in a: a1,a2 = a.split(",") if is_btc_addr(a1): btcaddr = a1 elif is_mmgen_addr(a1): - btcaddr = mm_addr2btc_addr(c,a1,acct_data,addr_data,b2m_map) + btcaddr = mmaddr2btcaddr(c,a1,acct_data,addr_data,b2m_map) else: msg("%s: unrecognized subargument in argument '%s'" % (a1,a)) sys.exit(2) if is_btc_amt(a2): - tx_out[btcaddr] = check_btc_amt(a2) + tx_out[btcaddr] = normalize_btc_amt(a2) else: msg("%s: invalid amount in argument '%s'" % (a2,a)) sys.exit(2) @@ -108,7 +114,7 @@ if not 'info' in opts: (change_addr, a)) sys.exit(2) change_addr = a if is_btc_addr(a) else \ - mm_addr2btc_addr(c,a,acct_data,addr_data,b2m_map) + mmaddr2btcaddr(c,a,acct_data,addr_data,b2m_map) tx_out[change_addr] = 0 else: msg("%s: unrecognized argument" % a) @@ -119,7 +125,7 @@ if not 'info' in opts: sys.exit(2) tx_fee = opts['tx_fee'] if 'tx_fee' in opts else g.tx_fee - tx_fee = check_btc_amt(tx_fee) + tx_fee = normalize_btc_amt(tx_fee) if tx_fee > g.max_tx_fee: msg("Transaction fee too large: %s > %s" % (tx_fee,g.max_tx_fee)) sys.exit(2) @@ -128,21 +134,15 @@ if g.debug: show_opts_and_cmd_args(opts,cmd_args) if not 'info' in opts: do_license_msg(immed=True) -# Begin test -# import mmgen.rpc -# us = eval(get_data_from_file("listunspent.json")) -# End test +#write_to_file("bogus_unspent.json", repr(us)); sys.exit() +if g.bogus_wallet_data: + import mmgen.rpc + us = eval(get_data_from_file("bogus_unspent.json")) +else: + us = c.listunspent() -us = c.listunspent() +if not us: msg(txmsg['no_spendable_outputs']); sys.exit(2) -if not us: - msg(""" -No spendable outputs found! Import addresses with balances into your -watch-only wallet using 'mmgen-addrimport' and then re-run this program.""") - sys.exit(2) - -# write_to_file("listunspent.json",repr(us)) -# sys.exit() unspent = sort_and_view(us) total = trim_exponent(sum([i.amount for i in unspent])) @@ -209,6 +209,6 @@ if reply and reply in "YyVv": prompt = "Save transaction?" if user_confirm(prompt,default_yes=True): - print_tx_to_file(tx_hex,sel_unspent,send_amt or change,b2m_map,opts) + write_tx_to_file(tx_hex,sel_unspent,send_amt or change,b2m_map,opts) else: msg("Transaction not saved") diff --git a/mmgen-txsend b/mmgen-txsend index 3667636c..1f159d50 100755 --- a/mmgen-txsend +++ b/mmgen-txsend @@ -35,46 +35,37 @@ help_data = { 'usage': "[opts] ", 'options': """ -h, --help Print this help message --d, --outdir d Specify an alternate directory 'd' for output +-d, --outdir= d Specify an alternate directory 'd' for output -q, --quiet Suppress warnings; overwrite files without prompting """ } -short_opts = "hd:q" -long_opts = "help","outdir=","quiet" +opts,cmd_args = parse_opts(sys.argv,help_data) -opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts) if 'quiet' in opts: g.quiet = True -check_opts(opts,long_opts) if len(cmd_args) == 1: - infile = cmd_args[0] - check_infile(infile) + infile = cmd_args[0]; check_infile(infile) else: usage(help_data) # Begin execution -try: - metadata,tx_sig = get_lines_from_file(infile,"signed transaction") -except: - msg("Invalid signed transaction file") - sys.exit(3) - -metadata = metadata.split() -if len(metadata) != 3: - msg("Invalid file header") - sys.exit(3) - -from binascii import unhexlify -try: unhexlify(tx_sig) -except: - msg("Invalid signed transaction data") - sys.exit(3) - 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) + qmsg("Signed transaction file '%s' is valid" % infile) +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) + 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" @@ -85,14 +76,12 @@ confirm_or_exit(warn, what, expect) msg("Sending transaction") -c = connect_to_bitcoind() - try: - tx = c.sendrawtransaction(tx_sig) + tx_id = c.sendrawtransaction(tx_hex) except: msg("Unable to send transaction") sys.exit(3) -msg("Transaction sent: %s" % tx) +msg("Transaction sent: %s" % tx_id) -print_sent_tx_to_file(tx,metadata,opts) +write_sent_tx_num_to_file(tx_id,metadata,opts) diff --git a/mmgen-txsign b/mmgen-txsign index 76e85642..4e076c5a 100755 --- a/mmgen-txsign +++ b/mmgen-txsign @@ -33,26 +33,27 @@ help_data = { '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 +-d, --outdir= d Specify an alternate directory 'd' for output -e, --echo-passphrase Print passphrase to screen when typing it -i, --info Display information about the transaction and exit -I, --tx-id Display transaction ID and exit --k, --keys-from-file k Provide additional key data from file 'k' --P, --passwd-file f Get passphrase from file 'f' +-k, --keys-from-file= k Provide additional key data from file 'k' +-P, --passwd-file= f Get passphrase from file 'f' -q, --quiet Suppress warnings; overwrite files without prompting -V, --skip-key-preverify Skip optional key pre-verification step - --b, --from-brain l,p Generate keys from a user-created password, +-b, --from-brain= l,p Generate keys from a user-created password, i.e. a "brainwallet", using seed length 'l' and hash preset 'p' -w, --use-wallet-dat Get keys from a running bitcoind -g, --from-incog Generate keys from an incognito wallet -X, --from-incog-hex Generate keys from an incognito hexadecimal wallet --G, --from-incog-hidden f,o,l Generate keys from incognito data in file +-G, --from-incog-hidden= f,o,l Generate keys from incognito data in file 'f' at offset 'o', with seed length of 'l' -m, --from-mnemonic Generate keys from an electrum-like mnemonic --s, --from-seed Generate keys from a seed in .{} format +-s, --from-seed Generate keys from a seed in .{g.seed_ext} format +""".format(g=g), + 'notes': """ Transactions with either mmgen or non-mmgen input addresses may be signed. For non-mmgen inputs, the bitcoind wallet.dat is used as the key source. @@ -74,34 +75,25 @@ mappings are verified. Therefore, seed material for these addresses must be supplied on the command line. Seed data supplied in files must have the following extensions: - wallet: '.{}' - seed: '.{}' - mnemonic: '.{}' - brainwallet: '.{}' -""".format(g.seed_ext,g.wallet_ext,g.seed_ext,g.mn_ext,g.brain_ext) + wallet: '.{g.wallet_ext}' + seed: '.{g.seed_ext}' + mnemonic: '.{g.mn_ext}' + brainwallet: '.{g.brain_ext}' +""".format(g=g) } -short_opts = "hd:eiIk:P:qVb:wgXG:ms" -long_opts = "help","outdir=","echo_passphrase","info","tx_id",\ - "keys_from_file=","passwd_file=","quiet","skip_key_preverify",\ - "from_brain=","use_wallet_dat",\ - "from_incog","from_incog_hex","from_incog_hidden=",\ - "from_mnemonic","from_seed" - -opts,infiles = process_opts(sys.argv,help_data,short_opts,long_opts) +opts,infiles = parse_opts(sys.argv,help_data) if "quiet" in opts: g.quiet = True if 'from_incog_hex' in opts or 'from_incog_hidden' in opts: opts['from_incog'] = True -check_opts(opts,long_opts) - if not infiles: usage(help_data) for i in infiles: check_infile(i) c = connect_to_bitcoind() -seeds = {} +saved_seeds = {} tx_files = [i for i in set(infiles) if get_extension(i) == g.rawtx_ext] infiles = list(set(infiles) - set(tx_files)) @@ -139,9 +131,10 @@ for tx_file in tx_files: preverify_keys(a, keys) opts['skip_key_preverify'] = True - check_mmgen_to_btc_addr_mappings(inputs_data,b2m_map,infiles,seeds,opts) + check_mmgen_to_btc_addr_mappings(inputs_data,b2m_map,infiles,saved_seeds,opts) - if len(tx_files) > 1: msg("\nTransaction #%s:" % (tx_files.index(tx_file)+1)) + if len(tx_files) > 1: + msg("\nTransaction %s/%s:" % (tx_files.index(tx_file)+1,len(tx_files))) qmsg("Successfully opened transaction file '%s'" % tx_file) prompt = "View transaction data? (y)es, (N)o, (v)iew in pager" @@ -156,7 +149,7 @@ for tx_file in tx_files: if mmgen_addrs: ml = [i['account'].split()[0] for i in mmgen_addrs] - keys += get_keys_for_mmgen_addrs(ml,infiles,seeds,opts) + keys += get_keys_for_mmgen_addrs(ml,infiles,saved_seeds,opts) if 'use_wallet_dat' in opts: sig_tx = sign_tx_with_bitcoind_wallet(c,tx_hex,sig_data,keys,opts) @@ -171,7 +164,7 @@ for tx_file in tx_files: 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) + write_signed_tx_to_file(tx_hex,sig_tx['hex'],metadata,inputs_data,b2m_map,opts) else: msg("failed\nSome keys were missing. Transaction could not be signed.") sys.exit(3) diff --git a/mmgen-walletchk b/mmgen-walletchk index f0ce9770..815bbb19 100755 --- a/mmgen-walletchk +++ b/mmgen-walletchk @@ -27,42 +27,34 @@ from mmgen.util import * help_data = { 'prog_name': sys.argv[0].split("/")[-1], - 'desc': """Check integrity of a %s deterministic wallet, display - its information and export seed and mnemonic data."""\ - % g.proj_name, + 'desc': """Check integrity of an {} deterministic wallet, display + its information, and export seed and mnemonic data. + """.format(g.proj_name), 'usage': "[opts] [filename]", 'options': """ -h, --help Print this help message --d, --outdir d Specify an alternate directory 'd' for output +-d, --outdir= d Specify an alternate directory 'd' for output -e, --echo-passphrase Print passphrase to screen when typing it --P, --passwd-file f Get passphrase from file 'f' +-P, --passwd-file= f Get passphrase from file 'f' -q, --quiet Suppress warnings; overwrite files without prompting -S, --stdout Print seed or mnemonic data to standard output -v, --verbose Produce more verbose output -g, --export-incog Export wallet to incognito format -X, --export-incog-hex Export wallet to incognito hexadecimal format --G, --export-incog-hidden f,o Hide incognito data in existing file 'f' +-G, --export-incog-hidden= f,o Hide incognito data in existing file 'f' at offset 'o' (comma-separated) -m, --export-mnemonic Export the wallet's mnemonic to file -s, --export-seed Export the wallet's seed to file """ } -short_opts = "hd:eP:qSvgXG:ms" -long_opts = "help","outdir=","echo_passphrase","passwd_file=","quiet",\ - "stdout","verbose",\ - "export_incog","export_incog_hex","export_incog_hidden=",\ - "export_mnemonic","export_seed" +opts,cmd_args = parse_opts(sys.argv,help_data) -opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts) if 'quiet' in opts: g.quiet = True if 'verbose' in opts: g.verbose = True if 'export_incog_hidden' in opts or 'export_incog_hex' in opts: opts['export_incog'] = True -# Argument sanity checks and processing: -check_opts(opts,long_opts) - if len(cmd_args) != 1: usage(help_data) check_infile(cmd_args[0]) diff --git a/mmgen-walletgen b/mmgen-walletgen index 75774a41..eecc83ed 100755 --- a/mmgen-walletgen +++ b/mmgen-walletgen @@ -31,35 +31,37 @@ prog_name = sys.argv[0].split("/")[-1] help_data = { 'prog_name': prog_name, - 'desc': "Generate a {} deterministic wallet".format(g.proj_name), + 'desc': "Generate an {} deterministic wallet".format(g.proj_name), 'usage': "[opts] [infile]", 'options': """ -h, --help Print this help message --d, --outdir d Specify an alternate directory 'd' for output +-d, --outdir= d Specify an alternate directory 'd' for output -e, --echo-passphrase Print passphrase to screen when typing it -H, --show-hash-presets Show information on available hash presets --l, --seed-len n Create seed of length 'n'. Options: {} - (default: {}) --L, --label l Label to identify this wallet (32 chars max. +-l, --seed-len= n Create seed of length 'n'. Options: {seed_lens} + (default: {g.seed_len}) +-L, --label= l Label to identify this wallet (32 chars max. Allowed symbols: A-Z, a-z, 0-9, " ", "_", ".") --p, --hash-preset p Use scrypt.hash() parameters from preset 'p' - (default: '{}') --P, --passwd-file f Get passphrase from file 'f' +-p, --hash-preset= p Use scrypt.hash() parameters from preset 'p' + (default: '{g.hash_preset}') +-P, --passwd-file= f Get passphrase from file 'f' -q, --quiet Produce quieter output; overwrite files without prompting --u, --usr-randlen n Get 'n' characters of randomness from the user - (default: {}) +-u, --usr-randlen= n Get 'n' characters of randomness from the user + (default: {g.usr_randlen}) -v, --verbose Produce more verbose output --b, --from-brain l,p Generate wallet from a user-created passphrase, +-b, --from-brain= l,p Generate wallet from a user-created passphrase, i.e. a "brainwallet", using seed length 'l' and hash preset 'p' (comma-separated) -g, --from-incog Generate wallet from an incognito-format wallet -m, --from-mnemonic Generate wallet from an Electrum-like mnemonic --s, --from-seed Generate wallet from a seed in .{S} format +-s, --from-seed Generate wallet from a seed in .{g.seed_ext} format +""".format(seed_lens=",".join([str(i) for i in g.seed_lens]), g=g), + 'notes': """ By default (i.e. when invoked without any of the '--from-' options), -{} generates a wallet based on a random seed. +{prog_name} generates a wallet based on a random seed. Data for the --from- options will be taken from if is specified. Otherwise, the user will be prompted to enter the data. @@ -84,28 +86,15 @@ the '--seed-len' option. For a brainwallet passphrase to always generate the same keys and addresses, the same 'l' and 'p' parameters to '--from-brain' must be used in all future invocations with that passphrase. -""".format( - ",".join([str(i) for i in g.seed_lens]), - g.seed_len, - g.hash_preset, - g.usr_randlen, - prog_name, - S=g.seed_ext, - ) +""".format(prog_name=prog_name) } -short_opts = "hd:eHl:L:p:P:qu:vb:gms" -long_opts = "help","outdir=","echo_passphrase","show_hash_presets","seed_len=",\ - "label=","hash_preset=","passwd_file=","quiet","usr_randlen=","verbose",\ - "from_brain=","from_incog","from_mnemonic","from_seed" +opts,cmd_args = parse_opts(sys.argv,help_data) -opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts) if 'quiet' in opts: g.quiet = True if 'verbose' in opts: g.verbose = True if 'show_hash_presets' in opts: show_hash_presets() -check_opts(opts,long_opts) - if g.debug: show_opts_and_cmd_args(opts,cmd_args) if len(cmd_args) == 1: diff --git a/mmgen/Opts.py b/mmgen/Opts.py index 9b457348..905caa0a 100755 --- a/mmgen/Opts.py +++ b/mmgen/Opts.py @@ -23,19 +23,29 @@ def usage(hd): print "USAGE: %s %s" % (hd['prog_name'], hd['usage']) sys.exit(2) +def print_version_info(progname): + print """ +'{}' version {g.version}. Part of the {g.proj_name} suite. +Copyright (C) {g.Cdates} by {g.author} {g.email}. +""".format(progname, g=g).strip() def print_help(progname,help_data): pn_len = str(len(progname)+2) - print (" %-"+pn_len+"s %s") % (progname.upper()+":", help_data['desc']) - print (" %-"+pn_len+"s %s %s") % ("USAGE:", progname, help_data['usage']) + print (" %-"+pn_len+"s %s") % (progname.upper()+":", help_data['desc'].strip()) + print (" %-"+pn_len+"s %s %s")%("USAGE:", progname, help_data['usage'].strip()) sep = "\n " print " OPTIONS:"+sep+"%s" % sep.join(help_data['options'].strip().split("\n")) + if "notes" in help_data: + print " %s" % "\n ".join(help_data['notes'][1:-1].split("\n")) def process_opts(argv,help_data,short_opts,long_opts): progname = argv[0].split("/")[-1] + if len(argv) == 2 and argv[1] == '--version': # MMGen only! + print_version_info(progname); sys.exit() + if g.debug: print "Short opts: %s" % repr(short_opts) print "Long opts: %s" % repr(long_opts) @@ -69,6 +79,26 @@ def process_opts(argv,help_data,short_opts,long_opts): return opts,args +def parse_opts(argv,help_data): + + lines = help_data['options'].strip().split("\n") + import re + pat = r"^-([a-zA-Z0-9]), --([a-zA-Z0-9-]{1,64})(=*) (.*)" + opt_data = [m.groups() for m in [re.match(pat,l) for l in lines] if m] + + short_opts = "".join([d[0]+(":" if d[2] else "") for d in opt_data if d]) + long_opts = [d[1].replace("-","_")+d[2] for d in opt_data if d] + help_data['options'] = "\n".join( + ["-{0}, --{1}{w} {3}".format(w=" " if m.group(3) else "", *m.groups()) + if m else k for m,k in [(re.match(pat,l),l) for l in lines]] + ) + opts,infiles = process_opts(argv,help_data,short_opts,long_opts) + + if not check_opts(opts,long_opts): sys.exit(1) # MMGen only! + + return opts,infiles + + def show_opts_and_cmd_args(opts,cmd_args): print "Processed options: %s" % repr(opts) print "Cmd args: %s" % repr(cmd_args) @@ -85,51 +115,41 @@ def check_opts(opts,long_opts): if i+"=" in long_opts: set_if_unset_and_typeconvert(opts,i) - for opt in opts.keys(): + for opt,val in opts.items(): - val = opts[opt] what = "parameter for '--%s' option" % opt.replace("_","-") # Check for file existence and readability - for i in 'keys_from_file','addrlist','passwd_file','keysforaddrs': - if opt == i: - check_infile(val) - return + if opt in ('keys_from_file','addrlist','passwd_file','keysforaddrs'): + check_infile(val) + return True if opt == 'outdir': what = "output directory" - import re, os, stat - # TODO Non-portable: - d = re.sub(r'/*$','', val) - opts[opt] = d + import os + if os.path.isdir(val): + if os.access(val, os.W_OK|os.X_OK): + opts[opt] = os.path.normpath(val) + else: + msg("Requested %s '%s' is unwritable by you" % (what,val)) + return False + else: + msg("Requested %s '%s' doen not exist" % (what,val)) + return False - try: mode = os.stat(d).st_mode - except: - msg("Unable to stat requested %s '%s'" % (what,d)) - sys.exit(1) - - if not stat.S_ISDIR(mode): - msg("Requested %s '%s' is not a directory" % (what,d)) - sys.exit(1) - - if not os.access(d, os.W_OK|os.X_OK): - msg("Requested %s '%s' is unwritable by you" % (what,d)) - sys.exit(1) elif opt == 'label': - label = val.strip() - opts[opt] = label - if len(label) > g.max_wallet_label_len: + if len(val) > g.max_wallet_label_len: msg("Label must be %s characters or less" % g.max_wallet_label_len) - sys.exit(1) + return False - for ch in list(label): - if ch not in g.wallet_label_symbols: - msg(""" -"%s": illegal character in label. Only ASCII characters are permitted. -""".strip() % ch) - sys.exit(1) + for ch in list(val): + chs = g.wallet_label_symbols + if ch not in chs: + msg("'%s': ERROR: label contains an illegal symbol" % val) + msg("The following symbols are permitted:\n%s" % "".join(chs)) + return False elif opt == 'export_incog_hidden' or opt == 'from_incog_hidden': try: if opt == 'export_incog_hidden': @@ -138,87 +158,89 @@ def check_opts(opts,long_opts): outfile,offset,seed_len = val.split(",") except: msg("'%s': invalid %s" % (val,what)) - sys.exit(1) + return False try: o = int(offset) except: msg("'%s': invalid 'o' %s (not an integer)" % (offset,what)) - sys.exit(1) + return False if o < 0: msg("'%s': invalid 'o' %s (less than zero)" % (offset,what)) - sys.exit(1) + return False if opt == 'from_incog_hidden': try: sl = int(seed_len) except: msg("'%s': invalid 'l' %s (not an integer)" % (sl,what)) - sys.exit(1) + return False if sl not in g.seed_lens: msg("'%s': invalid 'l' %s (valid choices: %s)" % (sl,what," ".join(str(i) for i in g.seed_lens))) - sys.exit(1) + return False import os, stat try: mode = os.stat(outfile).st_mode except: msg("Unable to stat requested %s '%s'" % (what,outfile)) - sys.exit(1) + return False if not (stat.S_ISREG(mode) or stat.S_ISBLK(mode)): msg("Requested %s '%s' is not a file or block device" % (what,outfile)) - sys.exit(1) + return False ac,m = (os.W_OK,"writ") \ if "export_incog_hidden" in opts else (os.R_OK,"read") if not os.access(outfile, ac): msg("Requested %s '%s' is un%sable by you" % (what,outfile,m)) - sys.exit(1) + return False elif opt == 'from_brain': try: l,p = val.split(",") except: msg("'%s': invalid %s" % (val,what)) - sys.exit(2) + return False try: int(l) except: msg("'%s': invalid 'l' %s (not an integer)" % (l,what)) - sys.exit(1) + return False if int(l) not in g.seed_lens: msg("'%s': invalid 'l' %s. Options: %s" % (l, what, ", ".join([str(i) for i in g.seed_lens]))) - sys.exit(1) + return False if p not in g.hash_presets: hps = ", ".join([i for i in sorted(g.hash_presets.keys())]) msg("'%s': invalid 'p' %s. Options: %s" % (p, what, hps)) - sys.exit(1) + return False elif opt == 'seed_len': if val not in g.seed_lens: msg("'%s': invalid %s. Options: %s" % (val,what,", ".join([str(i) for i in g.seed_lens]))) - sys.exit(2) + return False elif opt == 'hash_preset': if val not in g.hash_presets: msg("'%s': invalid %s. Options: %s" % (val,what,", ".join(sorted(g.hash_presets.keys())))) - sys.exit(2) + return False elif opt == 'usr_randlen': if val > g.max_randlen or val < g.min_randlen: msg("'%s': invalid %s (must be >= %s and <= %s)" % (val,what,g.min_randlen,g.max_randlen)) - sys.exit(2) + return False else: if g.debug: print "check_opts(): No test for opt '%s'" % opt + return True + def set_if_unset_and_typeconvert(opts,opt): diff --git a/mmgen/addr.py b/mmgen/addr.py index 02931553..5589029a 100755 --- a/mmgen/addr.py +++ b/mmgen/addr.py @@ -21,12 +21,25 @@ addr.py: Address generation/display routines for mmgen suite import sys from hashlib import sha256, sha512 +from hashlib import new as hashlib_new from binascii import hexlify, unhexlify from mmgen.bitcoin import numtowif from mmgen.util import msg,qmsg,qmsg_r import mmgen.config as g +addrmsgs = { + 'addrfile_header': """ +# MMGen address file +# +# This file is editable. +# Everything following a hash symbol '#' is a comment and ignored by {}. +# A text label of {} characters or less may be added to the right of each +# address, and it will be appended to the bitcoind wallet label upon import. +# The label may contain any printable ASCII symbol. +""".strip().format(g.proj_name,g.max_addr_label_len) +} + def test_for_keyconv(): """ Test for the presence of 'keyconv' utility on system @@ -112,22 +125,10 @@ def generate_keys(seed, addrnums): return generate_addrs(seed, addrnums, o) -def format_addr_data(addrlist, seed_chksum, opts): - """ - print_addresses(addrs, opts) => None +def format_addr_data(addr_data, addr_data_chksum, seed_id, addr_list, opts): - Print out the addresses and/or keys generated by generate_addresses() - - By default, prints addresses only - - Output can be customized with the following command line options: - print_secret - no_addresses - b16 - """ - - start = addrlist[0]['num'] - end = addrlist[-1]['num'] + start = addr_data[0]['num'] + end = addr_data[-1]['num'] wif_msg = "" if ('b16' in opts and 'print_secret' in opts) \ @@ -139,21 +140,14 @@ def format_addr_data(addrlist, seed_chksum, opts): (5 if 'print_secret' in opts else 1) + len(wif_msg) ) - header = """ -# MMGen address file -# -# This file is editable. -# Everything following a hash symbol '#' is a comment and ignored by {}. -# A text label of {} characters or less may be added to the right of each -# address, and it will be appended to the bitcoind wallet label upon import. -# The label may contain any printable ASCII symbol. -""".strip().format(g.proj_name_cap,g.max_addr_label_len) - data = [] - if not 'stdout' in opts: data.append(header + "\n") - data.append("%s {" % seed_chksum.upper()) + if not 'stdout' in opts: data.append(addrmsgs['addrfile_header'] + "\n") + data.append("# Address data checksum for {}[{}]: {}".format( + seed_id, fmt_addr_list(addr_list), addr_data_chksum)) + data.append("# Record this value to a secure location\n") + data.append("%s {" % seed_id.upper()) - for el in addrlist: + for el in addr_data: col1 = el['num'] if 'no_addresses' in opts: if 'b16' in opts: @@ -179,23 +173,23 @@ def format_addr_data(addrlist, seed_chksum, opts): def fmt_addr_list(addr_list): + addr_list = list(set(sorted(addr_list))) + prev = addr_list[0] ret = prev, for i in addr_list[1:]: - if i == prev + 1: if i == addr_list[-1]: ret += "-", i else: if prev != ret[-1]: ret += "-", prev ret += ",", i - prev = i return "".join([str(i) for i in ret]) -def write_addr_data_to_file(seed, data, addr_list, opts): +def write_addr_data_to_file(seed, addr_data_str, addr_list, opts): if 'print_addresses_only' in opts: ext = g.addrfile_ext elif 'no_addresses' in opts: ext = g.keyfile_ext @@ -211,10 +205,7 @@ def write_addr_data_to_file(seed, data, addr_list, opts): if 'outdir' in opts: outfile = "%s/%s" % (opts['outdir'], outfile) - write_to_file(outfile,data) + write_to_file(outfile,addr_data_str) dtype = "Address" if 'print_addresses_only' in opts else "Key" msg("%s data saved to file '%s'" % (dtype,outfile)) - -if __name__ == "__main__": - print fmt_addr_list(sorted(set([1,3,5,2,8,9,10,12,13,14,16]))) diff --git a/mmgen/config.py b/mmgen/config.py index 8314801f..f8af352f 100755 --- a/mmgen/config.py +++ b/mmgen/config.py @@ -18,15 +18,20 @@ """ config.py: Constants and configuration options for the mmgen suite """ + +author = "Philemon" +email = "" +Cdates = '2013-2014' +version = '0.7.4' + quiet,verbose = False,False min_screen_width = 80 from decimal import Decimal -tx_fee = Decimal("0.001") -max_tx_fee = Decimal("0.1") +tx_fee = Decimal("0.0001") +max_tx_fee = Decimal("0.01") -proj_name = "mmgen" -proj_name_cap = "MMGen" +proj_name = "MMGen" wallet_ext = "mmdat" seed_ext = "mmseed" @@ -59,6 +64,7 @@ keyconv_exec = "keyconv" from os import getenv debug = True if getenv("MMGEN_DEBUG") else False no_license = True if getenv("MMGEN_NOLICENSE") else False +bogus_wallet_data = True if getenv("MMGEN_BOGUS_WALLET_DATA") else False mins_per_block = 8.5 passwd_max_tries = 5 diff --git a/mmgen/license.py b/mmgen/license.py index 7083b06b..5a9e1537 100755 --- a/mmgen/license.py +++ b/mmgen/license.py @@ -21,13 +21,14 @@ license.py: Show the license import sys from mmgen.util import msg, msg_r, get_char +import mmgen.config as g gpl = { 'warning': """ - MMGen Copyright (C) 2013-2014 by Philemon . This + MMGen Copyright (C) {g.Cdates} by {g.author} {g.email}. This program comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under certain conditions. -""", +""".format(g=g), 'prompt': """ Press 'w' for conditions and warranty info, or 'c' to continue: """, @@ -586,7 +587,6 @@ copy of the Program in return for a fee. def do_license_msg(immed=False): - import mmgen.config as g if g.quiet or g.no_license: return msg(gpl['warning']) diff --git a/mmgen/tool.py b/mmgen/tool.py index c7ffeaef..fe30fda9 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -23,6 +23,7 @@ import sys import mmgen.bitcoin as bitcoin from mmgen.util import * +from mmgen.tx import * commands = { # "keyconv_compare": ['wif [str]'], @@ -54,44 +55,47 @@ commands = { "mn_printlist": ['wordlist [str="electrum"]'], "id8": [' [str]'], "id6": [' [str]'], - "listaccounts": ['minconf [int=1]'], + "listaddresses": ['minconf [int=1]', 'showempty [bool=False]'], "getbalance": ['minconf [int=1]'], + "viewtx": [' [str]'], + "check_addrfile": [' [str]'] } command_help = """ -File operations -hexdump - encode data into formatted hexadecimal form (file or stdin) -unhexdump - decode formatted hexadecimal data (file or stdin) + 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) + 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 -hextob58 - convert a hexadecimal number to base 58 -b58tohex - convert a base 58 number to hexadecimal -b58randenc - generate a random 32-byte number and convert it to base 58 -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 + Bitcoin operations: + strtob58 - convert a string to base 58 + hextob58 - convert a hexadecimal number to base 58 + b58tohex - convert a base 58 number to hexadecimal + b58randenc - generate a random 32-byte number and convert it to base 58 + 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): -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 + 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 + Bitcoind operations (bitcoind must be running): + listaddresses - show MMGen addresses and their balances + getbalance - like 'bitcoind getbalance' but shows confirmed/unconfirmed, + spendable/unspendable + viewtx - show raw transaction in human-readable form + check_addrfile - compute checksum and address list for MMGen address file -IMPORTANT NOTE: Though MMGen mnemonics use the Electrum wordlist, they're -computed using a different algorithm and are NOT Electrum-compatible! + IMPORTANT NOTE: Though MMGen mnemonics use the Electrum wordlist, they're + computed using a different algorithm and are NOT Electrum-compatible! """ def tool_usage(prog_name, command): @@ -170,14 +174,10 @@ 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): - d = sys.stdin.read() if infile == "-" else get_data_from_file(infile) - o = pretty_hexdump(d, 2, cols, line_nums) - print o + print pretty_hexdump(get_data_from_file(infile,dash=True), 2, cols, line_nums) def unhexdump(infile): - d = sys.stdin.read() if infile == "-" else get_data_from_file(infile) - o = decode_pretty_hexdump(d) - sys.stdout.write(o) + sys.stdout.write(decode_pretty_hexdump(get_data_from_file(infile,dash=True))) def strtob58(s): enc = bitcoin.b58encode(s) @@ -264,60 +264,79 @@ 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 id8(infile): print make_chksum_8(get_data_from_file(infile,dash=True)) +def id6(infile): print make_chksum_6(get_data_from_file(infile,dash=True)) -def id6(infile): - d = sys.stdin.read() if infile == "-" else get_data_from_file(infile) - print make_chksum_6(d) - - -def listaccounts(minconf=1): +# List MMGen addresses and their balances: +def listaddresses(minconf=1,showempty=False): 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])) + + addrs = {} + for d in c.listunspent(0): + ma,comment = split2(d.account) + if is_mmgen_addr(ma) and d.confirmations >= minconf: + key = "_".join(ma.split(":")) + if key not in addrs: addrs[key] = [0,comment] + addrs[key][0] += d.amount + + # "bitcoind getbalance " can produce a false balance + # (sipa watchonly bitcoind), so use only for empty accounts + if showempty: + # Show accts with not enough confirmations as empty! + # A feature, not a bug! + for (ma,comment),bal in [(split2(a),c.getbalance(a,minconf=minconf)) + for a in c.listaccounts(0)]: + if is_mmgen_addr(ma) and bal == 0: + key = "_".join(ma.split(":")) + if key not in addrs: addrs[key] = [0,comment] + + fs = "%-{}s %-{}s %s".format( + max([len(k) for k in addrs.keys()]), + max([len(str(addrs[k][1])) for k in addrs.keys()]) + ) + print fs % ("ADDRESS","COMMENT","BALANCE") + + def s_mmgen(ma): + return "{}:{:>0{w}}".format(w=g.mmgen_idx_max_digits, *ma.split("_")) + + old_sid = "" + for k in sorted(addrs.keys(),key=s_mmgen): + sid,num = k.split("_") + if old_sid and old_sid != sid: print + old_sid = sid + print fs % (sid+":"+num, addrs[k][1], trim_exponent(addrs[k][0])) + 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") + accts = {} + for d in connect_to_bitcoind().listunspent(0): + ma = split2(d.account)[0] + keys = ["TOTAL"] + if d.spendable: keys += ["SPENDABLE"] + if is_mmgen_addr(ma): keys += [ma.split(":")[0]] + c = d.confirmations + i = 2 if c >= minconf else 1 + + for key in keys: + if key not in accts: accts[key] = [0,0,0] + for j in ([0] if c == 0 else []) + [i]: + accts[key][j] += d.amount + + fs = "{:12} {:<%s} {:<%s} {:<}" % (16,16) + mc,lbl = str(minconf),"confirms" + print fs.format("Wallet","Unconfirmed", + "<%s %s"%(mc,lbl),">=%s %s"%(mc,lbl)) + for key in sorted(accts.keys()): + print fs.format(key+":", *[str(trim_exponent(a))+" BTC" for a in accts[key]]) + +def viewtx(infile): + 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) + +def check_addrfile(infile): parse_addrs_file(infile) diff --git a/mmgen/tx.py b/mmgen/tx.py index 6ac65347..41027d8c 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -37,7 +37,31 @@ 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 the '-k' option to mmgen-txsign. -Selected mmgen inputs: %s""" +Selected mmgen inputs: %s""", +'too_many_acct_addresses': """ +ERROR: More than one address found for account: "%s". +The tracking "wallet.dat" file appears to have been altered by a non-{g.proj_name} +program. Please restore "wallet.dat" from a backup or create a new wallet +and re-import your addresses.""".strip().format(g=g), + 'addrfile_no_data_msg': """ +No data found for MMgen address '%s'. Please import this address into +your tracking wallet, or supply an address file for it on the command line. +""".strip(), + 'addrfile_warn_msg': """ +Warning: no data for address '{mmaddr}' was found in the tracking wallet, so +this information was taken from the user-supplied address file. You're strongly +advised to import this address into your tracking wallet before proceeding with +this transaction. The address will not be tracked until you do so. +""".strip(), + 'addrfile_fail_msg': """ +No data for MMgen address '{mmaddr}' could be found in either the tracking +wallet or the supplied address file. Please import this address into your +tracking wallet, or supply an address file for it on the command line. +""".strip(), + 'no_spendable_outputs': """ +No spendable outputs found! Import addresses with balances into your +watch-only wallet using 'mmgen-addrimport' and then re-run this program. +""".strip() } # Deleted text: @@ -73,6 +97,7 @@ def trim_exponent(n): def is_btc_amt(amt): + # amt must be a string! from decimal import Decimal try: @@ -94,12 +119,11 @@ def is_btc_amt(amt): return trim_exponent(ret) -def check_btc_amt(amt): + +def normalize_btc_amt(amt): ret = is_btc_amt(amt) - if ret: - return ret - else: - sys.exit(3) + if ret: return ret + else: sys.exit(3) def get_bitcoind_cfg_options(cfg_keys): @@ -116,37 +140,24 @@ def get_bitcoind_cfg_options(cfg_keys): msg("Don't know where to look for 'bitcoin.conf'") sys.exit(3) - try: - f = open(cfg_file) - except: - msg("Unable to open file '%s' for reading" % cfg_file) + cfg = dict([(k,v) for k,v in [split2(line.translate(None,"\t "),"=") + for line in get_lines_from_file(cfg_file)] if k in cfg_keys]) + + for k in set(cfg_keys) - set(cfg.keys()): + msg("Configuration option '%s' must be set in %s" % (k,cfg_file)) sys.exit(2) - cfg = {} - - for line in f.readlines(): - s = line.translate(None,"\n\t ").split("=") - for k in cfg_keys: - if s[0] == k: cfg[k] = s[1] - - f.close() - - for k in cfg_keys: - if not k in cfg: - msg("Configuration option '%s' must be set in %s" % (k,cfg_file)) - sys.exit(2) - return cfg -def print_tx_to_file(tx,sel_unspent,send_amt,b2m_map,opts): +def write_tx_to_file(tx,sel_unspent,send_amt,b2m_map,opts): tx_id = make_chksum_6(unhexlify(tx)).upper() 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()) - data = "%s\n%s\n%s\n%s\n" % ( - metadata, tx, + data = "{} {} {}\n{}\n{}\n{}\n".format( + tx_id, send_amt, make_timestamp(), + tx, repr([i.__dict__ for i in sel_unspent]), repr(b2m_map) ) @@ -154,17 +165,22 @@ def print_tx_to_file(tx,sel_unspent,send_amt,b2m_map,opts): msg("Transaction data saved to file '%s'" % outfile) -def print_signed_tx_to_file(tx,sig_tx,metadata,opts): +def write_signed_tx_to_file(tx,sig_tx,metadata,inputs_data,b2m_map,opts): tx_id = make_chksum_6(unhexlify(tx)).upper() 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) + data = "{}\n{}\n{}\n{}\n".format( + " ".join(metadata[:2] + [make_timestamp()]), + sig_tx, + repr(inputs_data), + repr(b2m_map) + ) write_to_file(outfile,data,confirm=False) msg("Signed transaction saved to file '%s'" % outfile) -def print_sent_tx_to_file(tx,metadata,opts): +def write_sent_tx_num_to_file(tx,metadata,opts): outfile = "tx_{}[{}].out".format(*metadata[:2]) if 'outdir' in opts: outfile = "%s/%s" % (opts['outdir'], outfile) @@ -176,7 +192,7 @@ def format_unspent_outputs_for_printing(out,sort_info,total): pfs = " %-4s %-67s %-34s %-12s %-13s %-8s %-10s %s" pout = [pfs % ("Num","TX id,Vout","Address","MMgen ID", - "Amount (BTC)","Confirms","Age (days)", "Comment")] + "Amount (BTC)","Conf.","Age (days)", "Comment")] for n,i in enumerate(out): addr = "=" if i.skip == "addr" and "grouped" in sort_info else i.address @@ -199,7 +215,10 @@ def sort_and_view(unspent): def s_txid(i): return "%s %03s" % (i.txid,i.vout) def s_addr(i): return i.address def s_age(i): return i.confirmations - def s_mmgen(i): return i.account + def s_mmgen(i): + m = parse_mmgen_label(i.account)[0] + if m: return "{}:{:>0{w}}".format(w=g.mmgen_idx_max_digits, *m.split(":")) + else: return "G" + i.account sort,group,show_days,show_mmaddr,reverse = "age",False,False,True,True unspent.sort(key=s_age,reverse=reverse) # Reverse age sort by default @@ -216,7 +235,7 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen "('q' = quit sorting, 'p' = print to file, 'v' = pager view, 'w' = wide view): " from copy import deepcopy - print_to_file_msg = "" + write_to_file_msg = "" msg("") from mmgen.term import get_terminal_size @@ -233,7 +252,7 @@ Display options: show [D]ays, [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" - a = "Age(d)" if show_days else "Confirms" + a = "Age(d)" if show_days else "Conf." table_hdr = fs % ("Num","TX id Vout","","Address", "Amount (BTC)",a) unsp = deepcopy(unspent) @@ -283,14 +302,13 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen 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 = "" + msg("\n".join(out) +"\n\n" + write_to_file_msg + options_msg) + write_to_file_msg = "" - immed_chars = "atDdAMrgmeqpvw" skip_prompt = False while True: - reply = get_char(prompt, immed_chars=immed_chars) + reply = get_char(prompt, immed_chars="atDdAMrgmeqpvw") if reply == 'a': unspent.sort(key=s_amt); sort = "amount" elif reply == 't': unspent.sort(key=s_txid); sort = "txid" @@ -311,7 +329,7 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen data = format_unspent_outputs_for_printing(unsp,sort_info,total) outfile = "listunspent[%s].out" % ",".join(sort_info) write_to_file(outfile, data) - print_to_file_msg = "Data written to '%s'\n\n" % outfile + write_to_file_msg = "Data written to '%s'\n\n" % outfile elif reply == 'v': do_pager("\n".join(out)) continue @@ -332,15 +350,10 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen def parse_mmgen_label(s,check_label_len=False): - - if not s: return "","" - - try: w1,w2 = s.split(None,1) - except: w1,w2 = s,"" - - if not is_mmgen_addr(w1): return "",w1 - if check_label_len: check_addr_label(w2) - return w1,w2 + l = split2(s) + if not is_mmgen_addr(l[0]): return "",s + if check_label_len: check_addr_label(l[1]) + return tuple(l) def view_tx_data(c,inputs_data,tx_hex,b2m_map,metadata=[],pager=False): @@ -401,53 +414,51 @@ def view_tx_data(c,inputs_data,tx_hex,b2m_map,metadata=[],pager=False): out += "TX fee: %s BTC\n" % trim_exponent(total_in-total_out) if pager: do_pager(out) - else: msg("\n"+out) + else: print "\n"+out def parse_tx_data(tx_data,infile): - if len(tx_data) != 4: + 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" - if len(tx_data[0].split()) != 3: + if len(metadata.split()) != 3: msg(err_fmt % "metadata") sys.exit(2) - try: unhexlify(tx_data[1]) + try: unhexlify(tx_hex) except: msg(err_fmt % "hex data") sys.exit(2) else: - if not tx_data: + if not tx_hex: msg("Transaction is empty!") sys.exit(2) try: - inputs_data = eval(tx_data[2]) + inputs_data = eval(inputs_data) except: msg(err_fmt % "inputs data") sys.exit(2) - else: - if not inputs_data: - msg("Transaction has no inputs!") - sys.exit(2) try: - map_data = eval(tx_data[3]) + outputs_data = eval(outputs_data) except: msg(err_fmt % "mmgen to btc address map data") sys.exit(2) - return tx_data[0].split(),tx_data[1],inputs_data,map_data + return metadata.split(),tx_hex,inputs_data,outputs_data def select_outputs(unspent,prompt): while True: - reply = my_raw_input(prompt,allowed_chars="0123456789 -").strip() + reply = my_raw_input(prompt).strip() if not reply: continue @@ -462,111 +473,61 @@ def select_outputs(unspent,prompt): return selected -def is_mmgen_seed(s): +def is_mmgen_seed_id(s): import re - return len(s) == 8 and re.match(r"^[0123456789ABCDEF]*$",s) + return True if re.match(r"^[0123456789ABCDEF]{8}$",s) else False -def is_mmgen_num(s): +def is_mmgen_idx(s): import re - return len(s) <= g.mmgen_idx_max_digits \ - and re.match(r"^[123456789]+[0123456789]*$",s) + m = g.mmgen_idx_max_digits + return True if re.match(r"^[0123456789]{1,"+str(m)+r"}$",s) else False def is_mmgen_addr(s): - import re - return len(s) > 9 and s[8] == ':' \ - and re.match(r"^[0123456789ABCDEF]*$",s[:8]) \ - and len(s[9:]) <= g.mmgen_idx_max_digits \ - and re.match(r"^[123456789]+[0123456789]*$",s[9:]) + seed_id,idx = split2(s,":") + return is_mmgen_seed_id(seed_id) and is_mmgen_idx(idx) def is_btc_addr(s): from mmgen.bitcoin import verify_addr return verify_addr(s) -def btc_addr_to_mmgen_addr(btc_addr,b2m_map): - if btc_addr in b2m_map: - return b2m_map[btc_addr] - return "","" - - -def mmgen_addr_to_walletd(c,mmaddr,acct_data): +def mmaddr2btcaddr_bitcoind(c,mmaddr,acct_data): # We don't want to create a new object, so we'll use append() if not acct_data: for i in c.listaccounts(): acct_data.append(i) - for a in acct_data: - if not a: continue - try: - w1,w2 = a.split(None,1) - except: - w1,w2 = a,"" - if w1 == mmaddr: - acct = a - break - else: - return "","" + for acct in acct_data: + m,comment = parse_mmgen_label(acct) + if m == mmaddr: + addrlist = c.getaddressesbyaccount(acct) + if len(addrlist) == 1: + return addrlist[0],comment + else: + msg(txmsg['too_many_acct_addresses'] % acct); sys.exit(2) - alist = c.getaddressesbyaccount(acct) - - if len(alist) != 1: - msg(""" -ERROR: More than one address found for account: "%s". -The tracking "wallet.dat" file appears to have been altered by a non-%s -program. Please restore "wallet.dat" from a backup or create a new wallet -and re-import your addresses. -""".strip() % (acct,g.proj_name_cap)) - sys.exit(3) - - return alist[0],w2 + return "","" -def mmgen_addr_to_addr_data(m,addr_data): +def mmaddr2btcaddr_addrfile(mmaddr,addr_data): - no_data_msg = """ -No data found for MMgen address '%s'. Please import this address into -your tracking wallet, or supply an address file for it on the command line. -""".strip() % m - warn_msg = """ -Warning: no data for address '%s' exists in the wallet, so it was -taken from the user-supplied address file. You're strongly advised to -import this address into your tracking wallet before proceeding with -this transaction. The address will not be tracked until you do so. -""".strip() % m - fail_msg = """ -No data found for MMgen address '%s' in either wallet or supplied -address file. Please import this address into your tracking wallet, or -supply an address file for it on the command line. -""".strip() % m + mmid,mmidx = mmaddr.split(":") - ID,num = m.split(":") - from binascii import unhexlify - try: unhexlify(ID) - except: pass - else: - try: num = int(num) - except: pass - else: - if not addr_data: - msg(no_data_msg) - sys.exit(2) - for i in addr_data: - if ID == i[0]: - for j in i[1]: - if j[0] == num: - msg(warn_msg) - if not user_confirm("Continue anyway?"): - sys.exit(1) - return j[1],(j[2] if len(j) == 3 else "") - msg(fail_msg) - sys.exit(2) + for ad in addr_data: + if mmid == ad[0]: + for j in ad[1]: + if j[0] == mmidx: + msg(txmsg['addrfile_warn_msg'].format(mmaddr=mmaddr)) + if not user_confirm("Continue anyway?"): + sys.exit(1) + return j[1:] if len(j) == 3 else (j[1],"") - msg("Invalid format: %s" % m) - sys.exit(3) + msg(txmsg['addrfile_fail_msg'].format(mmaddr=mmaddr)) + sys.exit(2) -def check_mmgen_to_btc_addr_mappings(inputs_data,b2m_map,infiles,seeds,opts): +def check_mmgen_to_btc_addr_mappings(inputs_data,b2m_map,infiles,saved_seeds,opts): in_maplist = [(i['account'].split()[0],i['address']) for i in inputs_data if i['account'] and is_mmgen_addr(i['account'].split()[0])] @@ -578,7 +539,7 @@ def check_mmgen_to_btc_addr_mappings(inputs_data,b2m_map,infiles,seeds,opts): mmaddrs = [i[0] for i in maplist] from copy import deepcopy pairs = get_keys_for_mmgen_addrs(mmaddrs, - deepcopy(infiles),seeds,opts,gen_pairs=True) + infiles,saved_seeds,opts,gen_pairs=True) for a,b in zip(sorted(pairs),sorted(maplist)): if a != b: msg(""" @@ -607,6 +568,17 @@ Only ASCII printable characters are permitted. sys.exit(3) +def check_addr_data_hash(seed_id,addr_data): + from hashlib import new as hashlib_new + addr_data_chksum = make_chksum_8( + " ".join(["{} {}".format(*d[:2]) for d in addr_data]), sep=True + ) + from mmgen.addr import fmt_addr_list + fl = fmt_addr_list([int(a[0]) for a in addr_data]) + msg("Computed address data checksum for '{}[{}]': {}".format( + seed_id,fl,addr_data_chksum)) + msg("Check this value against your records") + def parse_addrs_file(f): lines = get_lines_from_file(f,"address data",remove_comments=True) @@ -623,14 +595,14 @@ def parse_addrs_file(f): msg("'%s': invalid first line" % lines[0]) elif cbrace != '}': msg("'%s': invalid last line" % cbrace) - elif not is_mmgen_seed(seed_id): + elif not is_mmgen_seed_id(seed_id): msg("'%s': invalid Seed ID" % seed_id) else: - ret = [] + addr_data = [] for i in lines[1:-1]: d = i.split(None,2) - if not is_mmgen_num(d[0]): + if not is_mmgen_idx(d[0]): msg("'%s': invalid address num. in line: %s" % (d[0],d)) sys.exit(3) @@ -641,9 +613,11 @@ def parse_addrs_file(f): if len(d) == 3: check_addr_label(d[2]) - ret.append(tuple(d)) + addr_data.append(tuple(d)) - return seed_id,ret + check_addr_data_hash(seed_id,addr_data) + + return seed_id,addr_data sys.exit(3) @@ -664,62 +638,46 @@ def sign_transaction(c,tx_hex,sig_data,keys=None): return sig_tx +def get_seed_for_seed_id(seed_id,infiles,saved_seeds,opts): -def get_keys_for_mmgen_addrs(mmgen_addrs,infiles,seeds,opts,gen_pairs=False): + if seed_id in saved_seeds.keys(): + return saved_seeds[seed_id] + + while True: + if infiles: + seed = get_seed_retry(infiles.pop(0),opts) + elif "from_brain" in opts or "from_mnemonic" in opts \ + or "from_seed" in opts or "from_incog" in opts: + msg("Need data for seed ID %s" % seed_id) + seed = get_seed_retry("",opts) + else: + msg("ERROR: No seed source found for seed ID: %s" % seed_id) + sys.exit(2) + + s_id = make_chksum_8(seed) + saved_seeds[s_id] = seed + + if s_id == seed_id: return seed + + +def get_keys_for_mmgen_addrs(mmgen_addrs,infiles,saved_seeds,opts,gen_pairs=False): seed_ids = list(set([i[:8] for i in mmgen_addrs])) seed_ids_save = seed_ids[0:] # deep copy ret = [] - seeds_keys = [i for i in seed_ids if i in seeds] + for seed_id in seed_ids: + # Returns only if seed is found + seed = get_seed_for_seed_id(seed_id,infiles,saved_seeds,opts) - while seed_ids: - if seeds_keys: - seed = seeds[seeds_keys.pop(0)] + addr_ids = [int(i[9:]) for i in mmgen_addrs if i[:8] == seed_id] + from mmgen.addr import generate_keys,generate_addrs + if gen_pairs: + o = {"gen_what":"addresses"} + ret += [("%s:%s" % (seed_id,i['num']),i['addr']) + for i in generate_addrs(seed, addr_ids, o)] else: - infile = False - if infiles: - infile = infiles.pop(0) - seed = get_seed_retry(infile,opts) - elif "from_brain" in opts or "from_mnemonic" in opts \ - or "from_seed" in opts or "from_incog" in opts: - msg("Need data for seed ID %s" % seed_ids[0]) - seed = get_seed_retry("",opts) - else: - b,p,v = ("A seed","","is") if len(seed_ids) == 1 \ - else ("Seed","s","are") - msg("ERROR: %s source%s %s required for the following seed ID%s: %s"% - (b,p,v,p," ".join(seed_ids))) - sys.exit(2) - - seed_id = make_chksum_8(seed) - if seed_id in seed_ids: - seed_ids.remove(seed_id) - addr_ids = [int(i[9:]) for i in mmgen_addrs if i[:8] == seed_id] - seeds[seed_id] = seed - from mmgen.addr import generate_keys,generate_addrs - if gen_pairs: - o = {"gen_what":"addresses"} - ret += [("%s:%s" % (seed_id,i['num']),i['addr']) - for i in generate_addrs(seed, addr_ids, o)] - else: - ret += [i['wif'] for i in generate_keys(seed, addr_ids)] - else: - if seed_id in seed_ids_save: - msg_r("Ignoring duplicate seed source") - if infile: msg(" '%s'" % infile) - else: msg(" for ID %s" % seed_id) - else: - msg("Seed source produced an invalid seed ID (%s)" % seed_id) - if "from_incog" in opts or infile.split(".")[-1] == g.incog_ext: - msg( -"""Incorrect hash preset, password or incognito wallet data - -Trying again...""") - infiles.insert(0,infile) # ugly! - elif infile: - msg("Invalid input file '%s'" % infile) - sys.exit(2) + ret += [i['wif'] for i in generate_keys(seed, addr_ids)] return ret @@ -759,7 +717,7 @@ def preverify_keys(addrs_orig, keys_orig): if len(keys) < len(addrs): msg("ERROR: not enough keys (%s) for number of non-%s addresses (%s)" % - (len(keys),g.proj_name_cap,len(addrs))) + (len(keys),g.proj_name,len(addrs))) sys.exit(2) import mmgen.bitcoin as b @@ -803,7 +761,7 @@ def preverify_keys(addrs_orig, keys_orig): if addrs: s = "" if len(addrs) == 1 else "es" msg("No keys found for the following non-%s address%s:" % - (g.proj_name_cap,s)) + (g.proj_name,s)) print " %s" % "\n ".join(addrs) sys.exit(2) diff --git a/mmgen/util.py b/mmgen/util.py index d9610b1a..c6aadde0 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -43,10 +43,10 @@ def vmsg_r(s): def bail(): sys.exit(9) def get_extension(f): - try: return f.split(".")[-1] - except: return "" + import os + return os.path.splitext(f)[1][1:] -def my_raw_input(prompt,echo=True,allowed_chars=""): +def my_raw_input(prompt,echo=True): try: if echo: reply = raw_input(prompt) @@ -61,33 +61,10 @@ def my_raw_input(prompt,echo=True,allowed_chars=""): return reply -def my_raw_input_old(prompt,echo=True,allowed_chars=""): - - msg_r(prompt) - reply = "" - - while True: - ch = get_char(immed_chars="ALL_EXCEPT_ENTER") - if allowed_chars and ch not in allowed_chars+"\n\r\b"+chr(0x7f): - continue - if echo: - if ch in "\b"+chr(0x7f): # WIP - pass - # reply.pop(0) - else: msg_r(ch) - if ch in "\n\r": - if not echo: msg("") - break - reply += ch - - return reply - - def _get_hash_params(hash_preset): if hash_preset in g.hash_presets: return g.hash_presets[hash_preset] # N,p,r,buflen - else: - # Shouldn't be here + else: # Shouldn't be here msg("%s: invalid 'hash_preset' value" % hash_preset) sys.exit(3) @@ -126,7 +103,7 @@ cmessages = { 'unencrypted_secret_keys': """ This program generates secret keys from your {} seed, outputting them in UNENCRYPTED form. Generate only the key(s) you need and guard them carefully. -""".format(g.proj_name_cap), +""".format(g.proj_name), 'brain_warning': """ ############################## EXPERTS ONLY! ############################## @@ -154,12 +131,10 @@ def confirm_or_exit(message, question, expect="YES"): conf_msg = "Type uppercase '%s' to confirm: " % expect - if question[0].isupper(): - prompt = question + " " + conf_msg - else: - prompt = "Are you sure you want to %s?\n%s" % (question,conf_msg) + p = question+" "+conf_msg if question[0].isupper() else \ + "Are you sure you want to %s?\n%s" % (question,conf_msg) - if my_raw_input(prompt).strip() != expect: + if my_raw_input(p).strip() != expect: msg("Exiting at user request") sys.exit(2) @@ -196,9 +171,10 @@ def prompt_and_get_char(prompt,chars,enter_ok=False,verbose=False): else: msg_r("\r") -def make_chksum_8(s): +def make_chksum_8(s,sep=False): from hashlib import sha256 - return sha256(sha256(s).digest()).hexdigest()[:8].upper() + s = sha256(sha256(s).digest()).hexdigest()[:8].upper() + return "{} {}".format(s[:4],s[4:]) if sep else s def make_chksum_6(s): from hashlib import sha256 @@ -222,12 +198,14 @@ def check_infile(f): msg("Requested input file '%s' is unreadable by you" % f) sys.exit(1) + return True + def _validate_addr_num(n): try: n = int(n) except: - msg("'%s': invalid argument for address" % n) + msg("'%s': address must be an integer" % n) return False if n < 1: @@ -321,7 +299,7 @@ def _get_seed_from_brain_passphrase(words,opts): def encrypt_seed(seed, key, iv=1): """ Encrypt a seed for an {} deterministic wallet - """.format(g.proj_name_cap) + """.format(g.proj_name) # 192-bit seed is 24 bytes -> not multiple of 16. Must use MODE_CTR from Crypto.Cipher import AES @@ -439,6 +417,13 @@ def _display_control_data(label,metadata,hash_preset,salt,enc_seed): ): msg(fs.format(*i)) +def splitN(s,n,sep=None): # always return an n-element list + ret = s.split(sep,n-1) + return ret + ["" for i in range(n-len(ret))] + +def split2(s,sep=None): return splitN(s,2,sep) # always return a 2-element list +def split3(s,sep=None): return splitN(s,3,sep) # always return a 3-element list + def col4(s): nondiv = 1 if len(s) % 4 else 0 return " ".join([s[4*i:4*i+4] for i in range(len(s)/4 + nondiv)]) @@ -578,7 +563,7 @@ def get_data_from_wallet(infile,silent=False): # Don't make this a qmsg: User will be prompted for passphrase and must see # the filename. if not silent: - msg("Getting {} wallet data from file '{}'".format(g.proj_name_cap,infile)) + msg("Getting {} wallet data from file '{}'".format(g.proj_name,infile)) f = open_file_or_exit(infile, 'r') @@ -653,7 +638,8 @@ def get_lines_from_file(infile,what="",remove_comments=False): return lines -def get_data_from_file(infile,what="data"): +def get_data_from_file(infile,what="data",dash=False): + if dash and infile == "-": return sys.stdin.read() qmsg("Getting %s from file '%s'" % (what,infile)) f = open_file_or_exit(infile,'r') data = f.read() @@ -682,7 +668,7 @@ def _get_seed_from_seed_data(words): vmsg("%s data produces seed ID: %s" % (g.seed_ext,make_chksum_8(seed))) return seed else: - msg("Invalid checksum for {} seed".format(g.proj_name_cap)) + msg("Invalid checksum for {} seed".format(g.proj_name)) return False @@ -717,7 +703,7 @@ def get_bitcoind_passphrase(prompt,opts): def get_seed_from_wallet( infile, opts, - prompt="Enter {} wallet passphrase: ".format(g.proj_name_cap), + prompt="Enter {} wallet passphrase: ".format(g.proj_name), silent=False ): @@ -770,7 +756,7 @@ def get_hidden_incog_data(opts): def get_seed_from_incog_wallet( infile, opts, - prompt="Enter %s wallet passphrase: " % g.proj_name_cap, + prompt="Enter {} wallet passphrase: ".format(g.proj_name), silent=False, hex_input=False ): @@ -806,7 +792,7 @@ def get_seed_from_incog_wallet( msg("Configured hash presets: %s" % " ".join(sorted(g.hash_presets))) while True: p = "Enter hash preset for %s wallet (default='%s'): " - hp = my_raw_input(p % (g.proj_name_cap, g.hash_preset)) + hp = my_raw_input(p % (g.proj_name, g.hash_preset)) if not hp: hp = g.hash_preset; break elif hp in g.hash_presets: @@ -890,7 +876,7 @@ def _get_words(infile,what,prompt,opts): def get_seed(infile,opts,silent=False): - ext = infile.split(".")[-1] + ext = get_extension(infile) if ext == g.mn_ext: source = "mnemonic" elif ext == g.brain_ext: source = "brainwallet" @@ -922,7 +908,7 @@ def get_seed(infile,opts,silent=False): if not g.quiet: confirm_or_exit( cmessages['brain_warning'].format( - g.proj_name_cap, *_get_from_brain_opt_params(opts)), + g.proj_name, *_get_from_brain_opt_params(opts)), "continue") prompt = "Enter brainwallet passphrase: " words = _get_words(infile,"brainwallet data",prompt,opts) diff --git a/setup.py b/setup.py index e47580a5..3e571923 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from distutils.core import setup setup( name = 'mmgen', - version = '0.7.4', + version = '0.7.5', author = 'Philemon', author_email = 'mmgen-py@yandex.com', url = 'https://github.com/mmgen/mmgen',