From 6bdb70b3e93448839a1a4fb7fa307d3dcb6cd8b2 Mon Sep 17 00:00:00 2001 From: philemon Date: Mon, 12 Dec 2016 00:30:23 +0300 Subject: [PATCH] [tx]: new command: mmgen-txdo - create, sign and send tx in one operation [tx]: tx file format change: txid now appended to file after tx is broadcast --- mmgen-txdo | 24 ++++ mmgen/addr.py | 7 +- mmgen/color.py | 27 ++--- mmgen/globalvars.py | 1 + mmgen/main_txcreate.py | 230 +------------------------------------- mmgen/main_txdo.py | 83 ++++++++++++++ mmgen/main_txsend.py | 23 +--- mmgen/main_txsign.py | 179 +++--------------------------- mmgen/obj.py | 18 +++ mmgen/tx.py | 127 ++++++++++++--------- mmgen/txcreate.py | 245 +++++++++++++++++++++++++++++++++++++++++ mmgen/txsign.py | 183 ++++++++++++++++++++++++++++++ setup.py | 18 +-- test/test.py | 176 ++++++++++++++++++----------- 14 files changed, 787 insertions(+), 554 deletions(-) create mode 100755 mmgen-txdo mode change 100644 => 100755 mmgen/color.py create mode 100755 mmgen/main_txdo.py create mode 100755 mmgen/txcreate.py create mode 100755 mmgen/txsign.py diff --git a/mmgen-txdo b/mmgen-txdo new file mode 100755 index 00000000..e00b5897 --- /dev/null +++ b/mmgen-txdo @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C)2013-2016 Philemon +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program. If not, see . + +""" +mmgen-txdo: Create, sign and broadcast an online MMGen transaction +""" + +from mmgen.main import launch +launch("txdo") diff --git a/mmgen/addr.py b/mmgen/addr.py index 6f46041b..a97f480a 100755 --- a/mmgen/addr.py +++ b/mmgen/addr.py @@ -181,10 +181,10 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file adata = self.generate(seed,addr_idxs) elif addrlist: # data from flat address list sid = None - adata = [AddrListEntry(addr=a) for a in addrlist] + adata = [AddrListEntry(addr=a) for a in set(addrlist)] elif keylist: # data from flat key list sid,do_chksum = None,False - adata = [AddrListEntry(wif=k) for k in keylist] + adata = [AddrListEntry(wif=k) for k in set(keylist)] elif seed or addr_idxs: die(3,'Must specify both seed and addr indexes') elif sid or adata: @@ -329,13 +329,14 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file vmsg(self.msgs['removed_dups'] % (len(pop_list),suf(removed,'k'))) def add_wifs(self,al_key): + if not al_key: return for d in self.data: for e in al_key.data: if e.addr and e.wif and e.addr == d.addr: d.wif = e.wif def list_missing(self,key): - return [d for d in self.data if not getattr(d,key)] + return [d.addr for d in self.data if not getattr(d,key)] def get(self,key): return [getattr(d,key) for d in self.data if getattr(d,key)] diff --git a/mmgen/color.py b/mmgen/color.py old mode 100644 new mode 100755 index b1bdcc1a..1df53e4b --- a/mmgen/color.py +++ b/mmgen/color.py @@ -20,14 +20,13 @@ color.py: color routines for the MMGen suite """ -import os - # If 88- or 256-color support is compiled, the following apply. # P s = 3 8 ; 5 ; P s -> Set foreground color to the second P s . # P s = 4 8 ; 5 ; P s -> Set background color to the second P s . +import os if os.environ['TERM'][-8:] == '256color': - _blk,_red,_grn,_yel,_blu,_mag,_cya,_bright,_dim,_ybright,_ydim,_pnk,_orng,_gry = [ - '\033[38;5;%s;1m' % c for c in 232,210,121,229,75,90,122,231,245,187,243,218,215,246] + _blk,_red,_grn,_yel,_blu,_mag,_cya,_bright,_dim,_ybright,_ydim,_pnk,_orng,_gry,_pur = [ + '\033[38;5;%s;1m' % c for c in 232,210,121,229,75,90,122,231,245,187,243,218,215,246,147] _redbg = '\033[38;5;232;48;5;210;1m' _grnbg = '\033[38;5;232;48;5;121;1m' _grybg = '\033[38;5;231;48;5;240;1m' @@ -35,9 +34,10 @@ if os.environ['TERM'][-8:] == '256color': else: _blk,_red,_grn,_yel,_blu,_mag,_cya,_reset,_grnbg = \ ['\033[%sm' % c for c in '30;1','31;1','32;1','33;1','34;1','35;1','36;1','0','30;102'] - _gry=_orng=_pnk=_redbg=_ybright=_ydim=_bright=_dim=_grybg=_mag # TODO + _gry=_orng=_pnk=_redbg=_ybright=_ydim=_bright=_dim=_grybg=_mag=_pur # TODO -clr_red=clr_grn=clr_grnbg=clr_yel=clr_cya=clr_blu=clr_pnk=clr_orng=clr_gry=clr_mag=clr_reset='' +_colors = 'red','grn','grnbg','yel','cya','blu','pnk','orng','gry','mag','pur','reset' +for c in _colors: globals()['clr_'+c] = '' def nocolor(s): return s def red(s): return clr_red+s+clr_reset @@ -50,18 +50,9 @@ def pink(s): return clr_pnk+s+clr_reset def orange(s): return clr_orng+s+clr_reset def gray(s): return clr_gry+s+clr_reset def magenta(s): return clr_mag+s+clr_reset +def purple(s): return clr_pur+s+clr_reset def init_color(enable_color=True): - global clr_red,clr_grn,clr_grnbg,clr_yel,clr_cya,clr_blu,clr_pnk,clr_orng,clr_gry,clr_mag,clr_reset if enable_color: - clr_red = _red - clr_grn = _grn - clr_grnbg = _grnbg - clr_yel = _yel - clr_cya = _cya - clr_blu = _blu - clr_pnk = _pnk - clr_orng = _orng - clr_gry = _gry - clr_mag = _mag - clr_reset = _reset + for c in _colors: + globals()['clr_'+c] = globals()['_'+c] diff --git a/mmgen/globalvars.py b/mmgen/globalvars.py index d52fc96b..330fc5b3 100755 --- a/mmgen/globalvars.py +++ b/mmgen/globalvars.py @@ -40,6 +40,7 @@ class g(object): release_date = 'December 2016' proj_name = 'MMGen' + proj_url = 'https://github.com/mmgen/mmgen' prog_name = os.path.basename(sys.argv[0]) author = 'Philemon' email = '' diff --git a/mmgen/main_txcreate.py b/mmgen/main_txcreate.py index 1964c624..a8d73cbf 100755 --- a/mmgen/main_txcreate.py +++ b/mmgen/main_txcreate.py @@ -21,14 +21,10 @@ mmgen-txcreate: Create a Bitcoin transaction to and from MMGen- or non-MMGen inputs and outputs """ -from mmgen.common import * -from mmgen.tx import * -from mmgen.tw import * - -pnm = g.proj_name +from mmgen.txcreate import * opts_data = { - 'desc': 'Create a BTC transaction with outputs to specified addresses', + 'desc': 'Create a transaction with outputs to specified Bitcoin or {g.proj_name} addresses'.format(g=g), 'usage': '[opts] ... [change addr] [addr file] ...', 'options': """ -h, --help Print this help message @@ -44,226 +40,10 @@ opts_data = { -q, --quiet Suppress warnings; overwrite files without prompting -v, --verbose Produce more verbose output """.format(g=g), - 'notes': """ - -The transaction's outputs are specified on the command line, while its inputs -are chosen from a list of the user's unpent outputs via an interactive menu. - -If the transaction fee is not specified by the user, it will be calculated -using bitcoind's "estimatefee" function for the default (or user-specified) -number of confirmations. If "estimatefee" fails, the global default fee of -{g.tx_fee} BTC will be used. - -Dynamic fees will be multiplied by the value of '--tx-fee-adj', if specified. - -Ages of transactions are approximate based on an average block discovery -interval of {g.mins_per_block} minutes. - -All addresses on the command line can be either Bitcoin addresses or {pnm} -addresses of the form :. - -To send the value of all inputs (minus TX fee) to a single output, specify -one address with no amount on the command line. -""".format(g=g,pnm=pnm) + 'notes': '\n' + txcreate_notes } -wmsg = { - 'addr_in_addrfile_only': """ -Warning: output address {mmgenaddr} is not in the tracking wallet, which means -its balance will not be tracked. You're strongly advised to import the address -into your tracking wallet before broadcasting this transaction. -""".strip(), - 'addr_not_found': """ -No data for {pnm} address {mmgenaddr} 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(), - 'addr_not_found_no_addrfile': """ -No data for {pnm} address {mmgenaddr} could be found in the tracking wallet. -Please import this address into your tracking wallet or supply an address file -for it on the command line. -""".strip(), - 'non_mmgen_inputs': """ -NOTE: This transaction includes non-{pnm} inputs, which makes the signing -process more complicated. When signing the transaction, keys for non-{pnm} -inputs must be supplied to '{pnl}-txsign' in a file with the '--keys-from-file' -option. -Selected non-{pnm} inputs: %s -""".strip().format(pnm=pnm,pnl=pnm.lower()), - 'not_enough_btc': """ -Not enough BTC in the inputs for this transaction (%s BTC) -""".strip(), - 'throwaway_change': """ -ERROR: This transaction produces change (%s BTC); however, no change address -was specified. -""".strip(), -} - -def select_unspent(unspent,prompt): - while True: - reply = my_raw_input(prompt).strip() - if reply: - selected = AddrIdxList(fmt_str=','.join(reply.split()),on_fail='return') - if selected: - if selected[-1] <= len(unspent): - return selected - msg('Unspent output number must be <= %s' % len(unspent)) - -def mmaddr2baddr(c,mmaddr,ad_w,ad_f): - - # assume mmaddr has already been checked - btc_addr = ad_w.mmaddr2btcaddr(mmaddr) - - if not btc_addr: - if ad_f: - btc_addr = ad_f.mmaddr2btcaddr(mmaddr) - if btc_addr: - msg(wmsg['addr_in_addrfile_only'].format(mmgenaddr=mmaddr)) - if not keypress_confirm('Continue anyway?'): - sys.exit(1) - else: - die(2,wmsg['addr_not_found'].format(pnm=pnm,mmgenaddr=mmaddr)) - else: - die(2,wmsg['addr_not_found_no_addrfile'].format(pnm=pnm,mmgenaddr=mmaddr)) - - return BTCAddr(btc_addr) - - -def get_fee_estimate(): - if 'tx_fee' in opt.set_by_user: # TODO - return None - else: - ret = c.estimatefee(opt.tx_confs) - if ret != -1: - return BTCAmt(ret) - else: - m = """ -Fee estimation failed! -Your possible courses of action (from best to worst): - 1) Re-run script with a different '--tx-confs' parameter (now '{c}') - 2) Re-run script with the '--tx-fee' option (specify fee manually) - 3) Accept the global default fee of {f} BTC -Accept the global default fee of {f} BTC? -""".format(c=opt.tx_confs,f=opt.tx_fee).strip() - if keypress_confirm(m): - return None - else: - die(1,'Exiting at user request') - -# main(): execution begins here - cmd_args = opts.init(opts_data) - -tx = MMGenTX() - -if opt.comment_file: tx.add_comment(opt.comment_file) - -c = bitcoin_connection() - -if not opt.info: - do_license_msg(immed=True) - - from mmgen.addr import AddrList,AddrData - addrfiles = [a for a in cmd_args if get_extension(a) == AddrList.ext] - cmd_args = set(cmd_args) - set(addrfiles) - - ad_f = AddrData() - for a in addrfiles: - check_infile(a) - ad_f.add(AddrList(a)) - - ad_w = AddrData(source='tw') - - for a in cmd_args: - if ',' in a: - a1,a2 = a.split(',',1) - if is_btc_addr(a1): - btc_addr = BTCAddr(a1) - elif is_mmgen_id(a1): - btc_addr = mmaddr2baddr(c,a1,ad_w,ad_f) - else: - die(2,"%s: unrecognized subargument in argument '%s'" % (a1,a)) - tx.add_output(btc_addr,BTCAmt(a2)) - elif is_mmgen_id(a) or is_btc_addr(a): - if tx.change_addr: - die(2,'ERROR: More than one change address specified: %s, %s' % - (change_addr, a)) - tx.change_addr = mmaddr2baddr(c,a,ad_w,ad_f) if is_mmgen_id(a) else BTCAddr(a) - tx.add_output(tx.change_addr,BTCAmt('0')) - else: - die(2,'%s: unrecognized argument' % a) - - if not tx.outputs: - die(2,'At least one output must be specified on the command line') - - if opt.tx_fee > tx.max_fee: - die(2,'Transaction fee too large: %s > %s' % (opt.tx_fee,tx.max_fee)) - - fee_estimate = get_fee_estimate() - -tw = MMGenTrackingWallet(minconf=opt.minconf) -tw.view_and_sort() -tw.display_total() - -if opt.info: sys.exit() - -tx.send_amt = tx.sum_outputs() - -msg('Total amount to spend: %s' % ('Unknown','%s BTC'%tx.send_amt.hl())[bool(tx.send_amt)]) - -while True: - sel_nums = select_unspent(tw.unspent, - 'Enter a range or space-separated list of outputs to spend: ') - msg('Selected output%s: %s' % ( - ('s','')[len(sel_nums)==1], - ' '.join(str(i) for i in sel_nums) - )) - sel_unspent = [tw.unspent[i-1] for i in sel_nums] - - non_mmaddrs = [i for i in sel_unspent if i.mmid == None] - if non_mmaddrs: - msg(wmsg['non_mmgen_inputs'] % ', '.join(set(sorted([a.addr.hl() for a in non_mmaddrs])))) - if not keypress_confirm('Accept?'): - continue - - tx.copy_inputs_from_tw(sel_unspent) # makes tx.inputs - - tx.calculate_size_and_fee(fee_estimate) # sets tx.size, tx.fee - - change_amt = tx.sum_inputs() - tx.send_amt - tx.fee - - if change_amt >= 0: - prompt = 'Transaction produces %s BTC in change. OK?' % change_amt.hl() - if keypress_confirm(prompt,default_yes=True): - break - else: - msg(wmsg['not_enough_btc'] % change_amt) - -if change_amt > 0: - change_amt = BTCAmt(change_amt) - if not tx.change_addr: - die(2,wmsg['throwaway_change'] % change_amt) - tx.del_output(tx.change_addr) - tx.add_output(BTCAddr(tx.change_addr),change_amt) -elif tx.change_addr: - msg('Warning: Change address will be unused as transaction produces no change') - tx.del_output(tx.change_addr) - -if not tx.send_amt: - tx.send_amt = change_amt - -dmsg('tx: %s' % tx) - -tx.add_comment() # edits an existing comment -tx.create_raw(c) # creates tx.hex, tx.txid -tx.add_mmaddrs_to_outputs(ad_w,ad_f) -tx.add_timestamp() -tx.add_blockcount(c) - -qmsg('Transaction successfully created') - -dmsg('TX (final): %s' % tx) - -tx.view_with_prompt('View decoded transaction?') - +do_license_msg() +tx = txcreate(opt,cmd_args,do_info=opt.info) tx.write_to_file(ask_write_default_yes=False) diff --git a/mmgen/main_txdo.py b/mmgen/main_txdo.py new file mode 100755 index 00000000..96779bd9 --- /dev/null +++ b/mmgen/main_txdo.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C)2013-2016 Philemon +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +mmgen-txdo: Create, sign and broadcast an online MMGen transaction +""" + +from mmgen.txcreate import * +from mmgen.txsign import * + +opts_data = { + 'desc': 'Create, sign and send an {g.proj_name} transaction'.format(g=g), + 'usage': '[opts] ... [change addr] [addr file] ... [seed source] ...', + 'options': """ +-h, --help Print this help message +--, --longhelp Print help message for long options (common options) +-a, --tx-fee-adj= f Adjust transaction fee by factor 'f' (see below) +-b, --brain-params=l,p Use seed length 'l' and hash preset 'p' for brainwallet + input +-B, --no-blank Don't blank screen before displaying unspent outputs +-c, --comment-file= f Source the transaction's comment from file 'f' +-C, --tx-confs= c Desired number of confirmations (default: {g.tx_confs}) +-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 (but see below)) +-H, --hidden-incog-input-params=f,o Read hidden incognito data from file + 'f' at offset 'o' (comma-separated) +-i, --in-fmt= f Input is from wallet format 'f' (see FMT CODES below) +-l, --seed-len= l Specify wallet seed length of 'l' bits. This option + is required only for brainwallet and incognito inputs + with non-standard (< {g.seed_len}-bit) seed lengths. +-k, --keys-from-file=f Provide additional keys for non-{pnm} addresses +-K, --key-generator= m Use method 'm' for public key generation + Options: {kgs} (default: {kg}) +-m, --minconf=n Minimum number of confirmations required to spend outputs (default: 1) +-M, --mmgen-keys-from-file=f Provide keys for {pnm} addresses in a key- + address file (output of '{pnl}-keygen'). Permits + online signing without an {pnm} seed source. The + key-address file is also used to verify {pnm}-to-BTC + mappings, so the user should record its checksum. +-O, --old-incog-fmt Specify old-format incognito input +-p, --hash-preset= p Use the scrypt hash parameters defined by preset 'p' + for password hashing (default: '{g.hash_preset}') +-P, --passwd-file= f Get {pnm} wallet or bitcoind passphrase from file 'f' +-q, --quiet Suppress warnings; overwrite files without prompting +-v, --verbose Produce more verbose output +-z, --show-hash-presets Show information on available hash presets +""".format(g=g,pnm=pnm,pnl=pnm.lower(), + kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]), + kg=g.key_generator), + 'notes': '\n' + txcreate_notes + txsign_notes +} + +cmd_args = opts.init(opts_data) +seed_files = get_seed_files(opt,cmd_args) +c = bitcoin_connection() +do_license_msg() + +kal = get_keyaddrlist(opt) +kl = get_keylist(opt) +if kl and kal: kl.remove_dups(kal,key='wif') + +tx = txcreate(opt,cmd_args,caller='txdo') +txsign(opt,c,tx,seed_files,kl,kal) +tx.write_to_file(ask_write=False) + +if tx.send(opt,c): + tx.write_to_file(ask_write=False) diff --git a/mmgen/main_txsend.py b/mmgen/main_txsend.py index 7daa0063..b9927c7c 100755 --- a/mmgen/main_txsend.py +++ b/mmgen/main_txsend.py @@ -41,34 +41,15 @@ if len(cmd_args) == 1: infile = cmd_args[0]; check_infile(infile) else: opts.usage() -# Begin execution - do_license_msg() - tx = MMGenTX(infile) - c = bitcoin_connection() - if not tx.check_signed(c): die(1,'Transaction has no signature!') - qmsg("Signed transaction file '%s' is valid" % infile) - tx.view_with_prompt('View transaction data?') - if tx.add_comment(): # edits an existing comment, returns true if changed tx.write_to_file(ask_write_default_yes=True) -warn = "Once this transaction is sent, there's no taking it back!" -action = 'broadcast this transaction to the network' -expect = 'YES, I REALLY WANT TO DO THIS' - -if opt.quiet: warn,expect = '','YES' - -confirm_or_exit(warn,action,expect) - -msg('Sending transaction') - -tx.send(c,bogus=False) - -tx.write_txid_to_file() +if tx.send(opt,c): + tx.write_to_file(ask_write=False) diff --git a/mmgen/main_txsign.py b/mmgen/main_txsign.py index a5d355a7..81e91020 100755 --- a/mmgen/main_txsign.py +++ b/mmgen/main_txsign.py @@ -20,15 +20,9 @@ mmgen-txsign: Sign a transaction generated by 'mmgen-txcreate' """ -from mmgen.common import * -from mmgen.seed import * -from mmgen.tx import * -from mmgen.addr import * +from mmgen.txsign import * -pnm = g.proj_name - -# -w is unneeded - use bitcoin-cli walletdump instead -# -w, --use-wallet-dat Get keys from a running bitcoind +# -w, --use-wallet-dat (keys from running bitcoind) removed: use bitcoin-cli walletdump instead opts_data = { 'desc': 'Sign Bitcoin transactions generated by {pnl}-txcreate'.format(pnl=pnm.lower()), 'usage': '[opts] ... [seed source]...', @@ -67,104 +61,9 @@ opts_data = { g=g,pnm=pnm,pnl=pnm.lower(), kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]), kg=g.key_generator), - 'notes': """ - -Transactions may contain both {pnm} or non-{pnm} input addresses. - -To sign non-{pnm} inputs, a bitcoind wallet dump or flat key list is used -as the key source ('--keys-from-file' option). - -To sign {pnm} inputs, key data is generated from a seed as with the -{pnl}-addrgen and {pnl}-keygen commands. Alternatively, a key-address file -may be used (--mmgen-keys-from-file option). - -Multiple wallets or other seed files can be listed on the command line in -any order. If the seeds required to sign the transaction's inputs are not -found in these files (or in the default wallet), the user will be prompted -for seed data interactively. - -To prevent an attacker from crafting transactions with bogus {pnm}-to-Bitcoin -address mappings, all outputs to {pnm} addresses are verified with a seed -source. Therefore, seed files or a key-address file for all {pnm} outputs -must also be supplied on the command line if the data can't be found in the -default wallet. - -Seed source files must have the canonical extensions listed in the 'FileExt' -column below: - - {f} -""".format( - f='\n '.join(SeedSource.format_fmt_codes().splitlines()), - pnm=pnm,pnl=pnm.lower(), - w=Wallet,s=SeedFile,m=Mnemonic,b=Brainwallet,x=IncogWalletHex,h=IncogWallet - ) + 'notes': '\n' + txsign_notes } -wmsg = { - 'mapping_error': """ -{pnm} -> BTC address mappings differ! -%-23s %s -> %s -%-23s %s -> %s -""".strip().format(pnm=pnm), - 'missing_keys_error': """ -A key file must be supplied for the following non-{pnm} address%s:\n %s -""".format(pnm=pnm).strip() -} - -def get_seed_for_seed_id(seed_id,infiles,saved_seeds): - - if seed_id in saved_seeds: - return saved_seeds[seed_id] - - while True: - if infiles: - ss = SeedSource(infiles.pop(0),ignore_in_fmt=True) - elif opt.in_fmt: - qmsg('Need seed data for Seed ID %s' % seed_id) - ss = SeedSource() - msg('User input produced Seed ID %s' % ss.seed.sid) - else: - die(2,'ERROR: No seed source found for Seed ID: %s' % seed_id) - - saved_seeds[ss.seed.sid] = ss.seed - if ss.seed.sid == seed_id: return ss.seed - -def generate_keys_for_mmgen_addrs(mmgen_addrs,infiles,saved_seeds): - seed_ids = set([i[:8] for i in mmgen_addrs]) - vmsg('Need seed%s: %s' % (suf(seed_ids,'k'),' '.join(seed_ids))) - d = [] - from mmgen.addr import KeyAddrList - for seed_id in seed_ids: - # Returns only if seed is found - seed = get_seed_for_seed_id(seed_id,infiles,saved_seeds) - addr_idxs = AddrIdxList(idx_list=[int(i[9:]) for i in mmgen_addrs if i[:8] == seed_id]) - d += KeyAddrList(seed=seed,addr_idxs=addr_idxs,do_chksum=False).flat_list() - return d - -def add_keys(tx,src,infiles=None,saved_seeds=None,keyaddr_list=None): - need_keys = [e for e in getattr(tx,src) if e.mmid and not e.have_wif] - if not need_keys: return [] - desc,m1 = ('key-address file','From key-address file:') if keyaddr_list else \ - ('seed(s)','Generated from seed:') - qmsg('Checking {} -> BTC address mappings for {} (from {})'.format(pnm,src,desc)) - d = keyaddr_list.flat_list() if keyaddr_list else \ - generate_keys_for_mmgen_addrs([e.mmid for e in need_keys],infiles,saved_seeds) - new_keys = [] - for e in need_keys: - for f in d: - if f.mmid == e.mmid: - if f.addr == e.addr: - e.have_wif = True - if src == 'inputs': - new_keys.append(f.wif) - else: - die(3,wmsg['mapping_error'] % (m1,f.mmid,f.addr,'tx file:',e.mmid,e.addr)) - if new_keys: - vmsg('Added %s wif key%s from %s' % (len(new_keys),suf(new_keys,'k'),desc)) - return new_keys - -# main(): execution begins here - infiles = opts.init(opts_data,add_opts=['b16']) if not infiles: opts.usage() @@ -172,85 +71,35 @@ for i in infiles: check_infile(i) c = bitcoin_connection() -saved_seeds = {} -tx_files = [i for i in infiles if get_extension(i) == MMGenTX.raw_ext] -seed_files = [i for i in infiles if get_extension(i) in SeedSource.get_extensions()] - -from mmgen.filename import find_file_in_dir -wf = find_file_in_dir(Wallet,g.data_dir) -if wf: seed_files.append(wf) - -if not tx_files: - die(1,'You must specify a raw transaction file!') -if not (seed_files or opt.mmgen_keys_from_file or opt.keys_from_file): # or opt.use_wallet_dat): - die(1,'You must specify a seed or key source!') - if not opt.info and not opt.terse_info: do_license_msg(immed=True) -kal,kl = None,None -if opt.mmgen_keys_from_file: - kal = KeyAddrList(opt.mmgen_keys_from_file) +tx_files = get_tx_files(opt,infiles) +seed_files = get_seed_files(opt,infiles) -if opt.keys_from_file: - l = get_lines_from_file(opt.keys_from_file,'key-address data',trim_comments=True) - kl = KeyAddrList(keylist=[m.split()[0] for m in l]) # accept bitcoind wallet dumps - if kal: kl.remove_dups(kal,key='wif') - kl.generate_addrs() +kal = get_keyaddrlist(opt) +kl = get_keylist(opt) +if kl and kal: kl.remove_dups(kal,key='wif') tx_num_str = '' for tx_num,tx_file in enumerate(tx_files,1): if len(tx_files) > 1: msg('\nTransaction #%s of %s:' % (tx_num,len(tx_files))) tx_num_str = ' #%s' % tx_num - tx = MMGenTX(tx_file) if tx.check_signed(c): die(1,'Transaction is already signed!') - vmsg("Successfully opened transaction file '%s'" % tx_file) - if opt.tx_id: die(0,tx.txid) + if opt.tx_id: + msg(tx.txid); continue if opt.info or opt.terse_info: - tx.view(pause=False,terse=opt.terse_info) - sys.exit() + tx.view(pause=False,terse=opt.terse_info); continue tx.view_with_prompt('View data for transaction%s?' % tx_num_str) - # Start - keys = [] - non_mm_addrs = tx.get_non_mmaddrs('inputs') - if non_mm_addrs: - tmp = KeyAddrList(addrlist=non_mm_addrs,do_chksum=False) - tmp.add_wifs(kl) - m = tmp.list_missing('wif') - if m: die(2,wmsg['missing_keys_error'] % (suf(m,'es'),'\n '.join(m))) - keys += tmp.get_wifs() - - if opt.mmgen_keys_from_file: - keys += add_keys(tx,'inputs',keyaddr_list=kal) - add_keys(tx,'outputs',keyaddr_list=kal) - - keys += add_keys(tx,'inputs',seed_files,saved_seeds) - add_keys(tx,'outputs',seed_files,saved_seeds) - - tx.delete_attrs('inputs','have_wif') - tx.delete_attrs('outputs','have_wif') - - extra_sids = set(saved_seeds) - tx.get_input_sids() - if extra_sids: - msg('Unused Seed ID%s: %s' % - (suf(extra_sids,'k'),' '.join(extra_sids))) - -# if opt.use_wallet_dat: -# ok = sign_tx_with_bitcoind_wallet(c,tx,tx_num_str,keys) -# else: - ok = tx.sign(c,tx_num_str,keys) - - if ok: - tx.add_comment() # edits an existing comment - tx.write_to_file(ask_write_default_yes=True,add_desc=tx_num_str) - else: - die(3,'failed\nSome keys were missing. Transaction %scould not be signed.' % tx_num_str) + txsign(opt,c,tx,seed_files,kl,kal,tx_num_str) + tx.add_comment() # edits an existing comment + tx.write_to_file(ask_write_default_yes=True,add_desc=tx_num_str) diff --git a/mmgen/obj.py b/mmgen/obj.py index 134fe55f..b77d6037 100755 --- a/mmgen/obj.py +++ b/mmgen/obj.py @@ -406,6 +406,24 @@ class MMGenID(str,Hilite,InitErrors): m = "'%s': value cannot be converted to MMGenID" % s return cls.init_fail(m,on_fail) +class MMGenTxID(str,Hilite,InitErrors): + color = 'red' + width = 6 + trunc_ok = False + hexcase = 'upper' + def __new__(cls,s,on_fail='die'): + cls.arg_chk(cls,on_fail) + from string import hexdigits + if len(s) == cls.width and set(s) <= set(getattr(hexdigits,cls.hexcase)()): + return str.__new__(cls,s) + m = "'{}': value cannot be converted to {}".format(s,cls.__name__) + return cls.init_fail(m,on_fail) + +class BitcoinTxID(MMGenTxID): + color = 'purple' + width = 64 + hexcase = 'lower' + class MMGenLabel(unicode,Hilite,InitErrors): color = 'pink' diff --git a/mmgen/tx.py b/mmgen/tx.py index ec77d378..bd123fda 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -115,7 +115,7 @@ class MMGenTX(MMGenObject): i = [{'txid':e.txid,'vout':e.vout} for e in self.inputs] o = dict([(e.addr,e.amt) for e in self.outputs]) self.hex = c.createrawtransaction(i,o) - self.txid = make_chksum_6(unhexlify(self.hex)).upper() + self.txid = MMGenTxID(make_chksum_6(unhexlify(self.hex)).upper()) # returns true if comment added or changed def add_comment(self,infile=None): @@ -201,7 +201,7 @@ class MMGenTX(MMGenObject): def format(self): from mmgen.bitcoin import b58encode - lines = ( + lines = [ '{} {} {} {}'.format( self.txid, self.send_amt, @@ -211,10 +211,14 @@ class MMGenTX(MMGenObject): self.hex, repr([e.__dict__ for e in self.inputs]), repr([e.__dict__ for e in self.outputs]) - ) + ((b58encode(self.label.encode('utf8')),) if self.label else ()) + ] + if self.label: + lines.append(b58encode(self.label.encode('utf8'))) + if self.btc_txid: + if not self.label: lines.append('-') # keep old tx files backwards compatible + lines.append(self.btc_txid) self.chksum = make_chksum_6(' '.join(lines)) - self.fmt_data = '\n'.join((self.chksum,) + lines)+'\n' - + self.fmt_data = '\n'.join([self.chksum] + lines)+'\n' def get_non_mmaddrs(self,desc): return list(set([i.addr for i in getattr(self,desc) if not i.mmid])) @@ -257,14 +261,30 @@ class MMGenTX(MMGenObject): if ret: self.mark_signed() return ret - def send(self,c,bogus=False): - if bogus: - self.btc_txid = 'deadbeef' * 8 + def send(self,opt,c,prompt_user=True): + if prompt_user: + m1 = ("Once this transaction is sent, there's no taking it back!",'')[bool(opt.quiet)] + m2 = 'broadcast this transaction to the network' + m3 = ('YES, I REALLY WANT TO DO THIS','YES')[bool(opt.quiet)] + confirm_or_exit(m1,m2,m3) + + msg('Sending transaction') + if os.getenv('MMGEN_BOGUS_SEND'): + ret = 'deadbeef' * 8 m = 'BOGUS transaction NOT sent: %s' else: - self.btc_txid = c.sendrawtransaction(self.hex) # exits on failure? + ret = c.sendrawtransaction(self.hex) # exits on failure? m = 'Transaction sent: %s' - msg(m % self.btc_txid) + + if ret: + self.btc_txid = BitcoinTxID(ret,on_fail='return') + if self.btc_txid: + self.desc = 'sent transaction' + msg(m % self.btc_txid.hl()) + return True + + msg('Sending of transaction {} failed'.format(self.txid)) + return False def write_txid_to_file(self,ask_write=False,ask_write_default_yes=True): fn = '%s[%s].%s' % (self.txid,self.send_amt,self.txid_ext) @@ -304,14 +324,17 @@ class MMGenTX(MMGenObject): except: blockcount = None - fs = ( - 'TRANSACTION DATA\n\nHeader: [Tx ID: {}] [Amount: {} BTC] [Time: {}]\n\n', + hdr_fs = ( + 'TRANSACTION DATA\n\nHeader: [Tx ID: {}] [Amount: {} BTC] [Time: {}]\n', 'Transaction {} - {} BTC - {} UTC\n' )[bool(terse)] - out = fs.format(self.txid,self.send_amt.hl(),self.timestamp) + out = hdr_fs.format(self.txid.hl(),self.send_amt.hl(),self.timestamp) enl = ('\n','')[bool(terse)] + if self.btc_txid: out += 'Bitcoin TxID: {}\n'.format(self.btc_txid.hl()) + out += enl + if self.label: out += 'Comment: %s\n%s' % (self.label.hl(),enl) out += 'Inputs:\n' + enl @@ -373,44 +396,50 @@ class MMGenTX(MMGenObject): def parse_tx_data(self,tx_data): - err_str,err_fmt = '','Invalid %s in transaction file' + def do_err(s): die(2,'Invalid %s in transaction file' % s) + + if len(tx_data) < 5: do_err('number of lines') + + self.chksum = tx_data.pop(0) + if self.chksum != make_chksum_6(' '.join(tx_data)): + do_err('checksum') if len(tx_data) == 6: - self.chksum,metadata,self.hex,inputs_data,outputs_data,comment = tx_data - elif len(tx_data) == 5: - self.chksum,metadata,self.hex,inputs_data,outputs_data = tx_data - comment = '' - else: - err_str = 'number of lines' + self.btc_txid = BitcoinTxID(tx_data.pop(-1),on_fail='return') + if not self.btc_txid: + do_err('Bitcoin TxID') - if not err_str: - if self.chksum != make_chksum_6(' '.join(tx_data[1:])): - err_str = 'checksum' - elif len(metadata.split()) != 4: - err_str = 'metadata' - else: - self.txid,send_amt,self.timestamp,blockcount = metadata.split() - self.send_amt = BTCAmt(send_amt) - self.blockcount = int(blockcount) - try: unhexlify(self.hex) - except: err_str = 'hex data' + if len(tx_data) == 5: + c = tx_data.pop(-1) + if c != '-': + from mmgen.bitcoin import b58decode + comment = b58decode(c) + if comment == False: + do_err('encoded comment (not base58)') else: - try: self.inputs = self.decode_io('inputs',eval(inputs_data)) - except: err_str = 'inputs data' - else: - try: self.outputs = self.decode_io('outputs',eval(outputs_data)) - except: err_str = 'btc-to-mmgen address map data' - else: - if comment: - from mmgen.bitcoin import b58decode - comment = b58decode(comment) - if comment == False: - err_str = 'encoded comment (not base58)' - else: - self.label = MMGenTXLabel(comment,on_fail='return') - if not self.label: - err_str = 'comment' + self.label = MMGenTXLabel(comment,on_fail='return') + if not self.label: + do_err('comment') + else: + comment = '' - if err_str: - msg(err_fmt % err_str) - sys.exit(2) + if len(tx_data) == 4: + metadata,self.hex,inputs_data,outputs_data = tx_data + else: + do_err('number of lines') + + if len(metadata.split()) != 4: do_err('metadata') + + self.txid,send_amt,self.timestamp,blockcount = metadata.split() + self.txid = MMGenTxID(self.txid) + self.send_amt = BTCAmt(send_amt) + self.blockcount = int(blockcount) + + try: unhexlify(self.hex) + except: do_err('hex data') + + try: self.inputs = self.decode_io('inputs',eval(inputs_data)) + except: do_err('inputs data') + + try: self.outputs = self.decode_io('outputs',eval(outputs_data)) + except: do_err('btc-to-mmgen address map data') diff --git a/mmgen/txcreate.py b/mmgen/txcreate.py new file mode 100755 index 00000000..142ae32b --- /dev/null +++ b/mmgen/txcreate.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python +# +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C)2013-2016 Philemon +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +txcreate: Create a Bitcoin transaction to and from MMGen- or non-MMGen inputs + and outputs +""" + +from mmgen.common import * +from mmgen.tx import * +from mmgen.tw import * + +pnm = g.proj_name + +txcreate_notes = """ +The transaction's outputs are specified on the command line, while its inputs +are chosen from a list of the user's unpent outputs via an interactive menu. + +If the transaction fee is not specified by the user, it will be calculated +using bitcoind's "estimatefee" function for the default (or user-specified) +number of confirmations. If "estimatefee" fails, the global default fee of +{g.tx_fee} BTC will be used. + +Dynamic fees will be multiplied by the value of '--tx-fee-adj', if specified. + +Ages of transactions are approximate based on an average block discovery +interval of {g.mins_per_block} minutes. + +All addresses on the command line can be either Bitcoin addresses or {pnm} +addresses of the form :. + +To send the value of all inputs (minus TX fee) to a single output, specify +one address with no amount on the command line. +""".format(g=g,pnm=pnm) + +wmsg = { + 'addr_in_addrfile_only': """ +Warning: output address {mmgenaddr} is not in the tracking wallet, which means +its balance will not be tracked. You're strongly advised to import the address +into your tracking wallet before broadcasting this transaction. +""".strip(), + 'addr_not_found': """ +No data for {pnm} address {mmgenaddr} 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(), + 'addr_not_found_no_addrfile': """ +No data for {pnm} address {mmgenaddr} could be found in the tracking wallet. +Please import this address into your tracking wallet or supply an address file +for it on the command line. +""".strip(), + 'non_mmgen_inputs': """ +NOTE: This transaction includes non-{pnm} inputs, which makes the signing +process more complicated. When signing the transaction, keys for non-{pnm} +inputs must be supplied to '{pnl}-txsign' in a file with the '--keys-from-file' +option. +Selected non-{pnm} inputs: %s +""".strip().format(pnm=pnm,pnl=pnm.lower()), + 'not_enough_btc': """ +Not enough BTC in the inputs for this transaction (%s BTC) +""".strip(), + 'throwaway_change': """ +ERROR: This transaction produces change (%s BTC); however, no change address +was specified. +""".strip(), +} + +def select_unspent(unspent,prompt): + while True: + reply = my_raw_input(prompt).strip() + if reply: + selected = AddrIdxList(fmt_str=','.join(reply.split()),on_fail='return') + if selected: + if selected[-1] <= len(unspent): + return selected + msg('Unspent output number must be <= %s' % len(unspent)) + +def mmaddr2baddr(c,mmaddr,ad_w,ad_f): + + # assume mmaddr has already been checked + btc_addr = ad_w.mmaddr2btcaddr(mmaddr) + + if not btc_addr: + if ad_f: + btc_addr = ad_f.mmaddr2btcaddr(mmaddr) + if btc_addr: + msg(wmsg['addr_in_addrfile_only'].format(mmgenaddr=mmaddr)) + if not keypress_confirm('Continue anyway?'): + sys.exit(1) + else: + die(2,wmsg['addr_not_found'].format(pnm=pnm,mmgenaddr=mmaddr)) + else: + die(2,wmsg['addr_not_found_no_addrfile'].format(pnm=pnm,mmgenaddr=mmaddr)) + + return BTCAddr(btc_addr) + +def get_fee_estimate(): + if 'tx_fee' in opt.set_by_user: # TODO + return None + else: + ret = c.estimatefee(opt.tx_confs) + if ret != -1: + return BTCAmt(ret) + else: + m = """ +Fee estimation failed! +Your possible courses of action (from best to worst): + 1) Re-run script with a different '--tx-confs' parameter (now '{c}') + 2) Re-run script with the '--tx-fee' option (specify fee manually) + 3) Accept the global default fee of {f} BTC +Accept the global default fee of {f} BTC? +""".format(c=opt.tx_confs,f=opt.tx_fee).strip() + if keypress_confirm(m): + return None + else: + die(1,'Exiting at user request') + +def txcreate(opt,cmd_args,do_info=False,caller='txcreate'): + + tx = MMGenTX() + + if opt.comment_file: tx.add_comment(opt.comment_file) + + c = bitcoin_connection() + + if not do_info: + from mmgen.addr import AddrList,AddrData + addrfiles = [a for a in cmd_args if get_extension(a) == AddrList.ext] + cmd_args = set(cmd_args) - set(addrfiles) + + ad_f = AddrData() + for a in addrfiles: + check_infile(a) + ad_f.add(AddrList(a)) + + ad_w = AddrData(source='tw') + + for a in cmd_args: + if ',' in a: + a1,a2 = a.split(',',1) + if is_btc_addr(a1): + btc_addr = BTCAddr(a1) + elif is_mmgen_id(a1): + btc_addr = mmaddr2baddr(c,a1,ad_w,ad_f) + else: + die(2,"%s: unrecognized subargument in argument '%s'" % (a1,a)) + tx.add_output(btc_addr,BTCAmt(a2)) + elif is_mmgen_id(a) or is_btc_addr(a): + if tx.change_addr: + die(2,'ERROR: More than one change address specified: %s, %s' % + (change_addr, a)) + tx.change_addr = mmaddr2baddr(c,a,ad_w,ad_f) if is_mmgen_id(a) else BTCAddr(a) + tx.add_output(tx.change_addr,BTCAmt('0')) + else: + die(2,'%s: unrecognized argument' % a) + + if not tx.outputs: + die(2,'At least one output must be specified on the command line') + + if opt.tx_fee > tx.max_fee: + die(2,'Transaction fee too large: %s > %s' % (opt.tx_fee,tx.max_fee)) + + fee_estimate = get_fee_estimate() + + tw = MMGenTrackingWallet(minconf=opt.minconf) + tw.view_and_sort() + tw.display_total() + + if do_info: sys.exit() + + tx.send_amt = tx.sum_outputs() + + msg('Total amount to spend: %s' % ('Unknown','%s BTC'%tx.send_amt.hl())[bool(tx.send_amt)]) + + while True: + sel_nums = select_unspent(tw.unspent, + 'Enter a range or space-separated list of outputs to spend: ') + msg('Selected output%s: %s' % ( + ('s','')[len(sel_nums)==1], + ' '.join(str(i) for i in sel_nums) + )) + sel_unspent = [tw.unspent[i-1] for i in sel_nums] + + non_mmaddrs = [i for i in sel_unspent if i.mmid == None] + if non_mmaddrs and caller != 'txdo': + msg(wmsg['non_mmgen_inputs'] % ', '.join(set(sorted([a.addr.hl() for a in non_mmaddrs])))) + if not keypress_confirm('Accept?'): + continue + + tx.copy_inputs_from_tw(sel_unspent) # makes tx.inputs + + tx.calculate_size_and_fee(fee_estimate) # sets tx.size, tx.fee + + change_amt = tx.sum_inputs() - tx.send_amt - tx.fee + + if change_amt >= 0: + prompt = 'Transaction produces %s BTC in change. OK?' % change_amt.hl() + if keypress_confirm(prompt,default_yes=True): + break + else: + msg(wmsg['not_enough_btc'] % change_amt) + + if change_amt > 0: + change_amt = BTCAmt(change_amt) + if not tx.change_addr: + die(2,wmsg['throwaway_change'] % change_amt) + tx.del_output(tx.change_addr) + tx.add_output(BTCAddr(tx.change_addr),change_amt) + elif tx.change_addr: + msg('Warning: Change address will be unused as transaction produces no change') + tx.del_output(tx.change_addr) + + if not tx.send_amt: + tx.send_amt = change_amt + + dmsg('tx: %s' % tx) + + tx.add_comment() # edits an existing comment + tx.create_raw(c) # creates tx.hex, tx.txid + tx.add_mmaddrs_to_outputs(ad_w,ad_f) + tx.add_timestamp() + tx.add_blockcount(c) + + qmsg('Transaction successfully created') + + dmsg('TX (final): %s' % tx) + + tx.view_with_prompt('View decoded transaction?') + + return tx diff --git a/mmgen/txsign.py b/mmgen/txsign.py new file mode 100755 index 00000000..aabebde0 --- /dev/null +++ b/mmgen/txsign.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python +# +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C)2013-2016 Philemon +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +txsign: Sign a transaction generated by 'mmgen-txcreate' +""" + +from mmgen.common import * +from mmgen.seed import * +from mmgen.tx import * +from mmgen.addr import * + +pnm = g.proj_name + +txsign_notes = """ +Transactions may contain both {pnm} or non-{pnm} input addresses. + +To sign non-{pnm} inputs, a bitcoind wallet dump or flat key list is used +as the key source ('--keys-from-file' option). + +To sign {pnm} inputs, key data is generated from a seed as with the +{pnl}-addrgen and {pnl}-keygen commands. Alternatively, a key-address file +may be used (--mmgen-keys-from-file option). + +Multiple wallets or other seed files can be listed on the command line in +any order. If the seeds required to sign the transaction's inputs are not +found in these files (or in the default wallet), the user will be prompted +for seed data interactively. + +To prevent an attacker from crafting transactions with bogus {pnm}-to-Bitcoin +address mappings, all outputs to {pnm} addresses are verified with a seed +source. Therefore, seed files or a key-address file for all {pnm} outputs +must also be supplied on the command line if the data can't be found in the +default wallet. + +Seed source files must have the canonical extensions listed in the 'FileExt' +column below: + + {f} +""".format(f='\n '.join(SeedSource.format_fmt_codes().splitlines()), + pnm=pnm,pnl=pnm.lower()) + +wmsg = { + 'mapping_error': """ +{pnm} -> BTC address mappings differ! +%-23s %s -> %s +%-23s %s -> %s +""".strip().format(pnm=pnm), + 'missing_keys_error': """ +ERROR: a key file must be supplied for the following non-{pnm} address%s:\n %s +""".format(pnm=pnm).strip() +} + +saved_seeds = {} + +def get_seed_for_seed_id(seed_id,infiles,saved_seeds): + + if seed_id in saved_seeds: + return saved_seeds[seed_id] + + while True: + if infiles: + ss = SeedSource(infiles.pop(0),ignore_in_fmt=True) + elif opt.in_fmt: + qmsg('Need seed data for Seed ID %s' % seed_id) + ss = SeedSource() + msg('User input produced Seed ID %s' % ss.seed.sid) + else: + die(2,'ERROR: No seed source found for Seed ID: %s' % seed_id) + + saved_seeds[ss.seed.sid] = ss.seed + if ss.seed.sid == seed_id: return ss.seed + +def generate_keys_for_mmgen_addrs(mmgen_addrs,infiles,saved_seeds): + seed_ids = set([i[:8] for i in mmgen_addrs]) + vmsg('Need seed%s: %s' % (suf(seed_ids,'k'),' '.join(seed_ids))) + d = [] + from mmgen.addr import KeyAddrList + for seed_id in seed_ids: + # Returns only if seed is found + seed = get_seed_for_seed_id(seed_id,infiles,saved_seeds) + addr_idxs = AddrIdxList(idx_list=[int(i[9:]) for i in mmgen_addrs if i[:8] == seed_id]) + d += KeyAddrList(seed=seed,addr_idxs=addr_idxs,do_chksum=False).flat_list() + return d + +def add_keys(tx,src,infiles=None,saved_seeds=None,keyaddr_list=None): + need_keys = [e for e in getattr(tx,src) if e.mmid and not e.have_wif] + if not need_keys: return [] + desc,m1 = ('key-address file','From key-address file:') if keyaddr_list else \ + ('seed(s)','Generated from seed:') + qmsg('Checking {} -> BTC address mappings for {} (from {})'.format(pnm,src,desc)) + d = keyaddr_list.flat_list() if keyaddr_list else \ + generate_keys_for_mmgen_addrs([e.mmid for e in need_keys],infiles,saved_seeds) + new_keys = [] + for e in need_keys: + for f in d: + if f.mmid == e.mmid: + if f.addr == e.addr: + e.have_wif = True + if src == 'inputs': + new_keys.append(f.wif) + else: + die(3,wmsg['mapping_error'] % (m1,f.mmid,f.addr,'tx file:',e.mmid,e.addr)) + if new_keys: + vmsg('Added %s wif key%s from %s' % (len(new_keys),suf(new_keys,'k'),desc)) + return new_keys + +def get_tx_files(opt,args): # strips found args + def is_tx(i): return get_extension(i) == MMGenTX.raw_ext + ret = [args.pop(i) for i in range(len(args)-1,-1,-1) if is_tx(args[i])] + if not ret: + die(1,'You must specify a raw transaction file!') + return list(reversed(ret)) + +def get_seed_files(opt,args): # strips found args + def is_seed(i): return get_extension(i) in SeedSource.get_extensions() + ret = [args.pop(i) for i in range(len(args)-1,-1,-1) if is_seed(args[i])] + from mmgen.filename import find_file_in_dir + wf = find_file_in_dir(Wallet,g.data_dir) + if wf: ret.append(wf) + if not (ret or opt.mmgen_keys_from_file or opt.keys_from_file): # or opt.use_wallet_dat): + die(1,'You must specify a seed or key source!') + return list(reversed(ret)) + +def get_keyaddrlist(opt): + ret = None + if opt.mmgen_keys_from_file: + ret = KeyAddrList(opt.mmgen_keys_from_file) + return ret + +def get_keylist(opt): + ret = None + if opt.keys_from_file: + l = get_lines_from_file(opt.keys_from_file,'key-address data',trim_comments=True) + ret = KeyAddrList(keylist=[m.split()[0] for m in l]) # accept bitcoind wallet dumps + ret.generate_addrs() + return ret + +def txsign(opt,c,tx,seed_files,kl,kal,tx_num_str=''): + # Start + keys = [] + non_mm_addrs = tx.get_non_mmaddrs('inputs') + if non_mm_addrs: + tmp = KeyAddrList(addrlist=non_mm_addrs,do_chksum=False) + tmp.add_wifs(kl) + m = tmp.list_missing('wif') + if m: die(2,wmsg['missing_keys_error'] % (suf(m,'es'),'\n '.join(m))) + keys += tmp.get_wifs() + + if opt.mmgen_keys_from_file: + keys += add_keys(tx,'inputs',keyaddr_list=kal) + add_keys(tx,'outputs',keyaddr_list=kal) + + keys += add_keys(tx,'inputs',seed_files,saved_seeds) + add_keys(tx,'outputs',seed_files,saved_seeds) + + tx.delete_attrs('inputs','have_wif') + tx.delete_attrs('outputs','have_wif') + + extra_sids = set(saved_seeds) - tx.get_input_sids() + if extra_sids: + msg('Unused Seed ID%s: %s' % + (suf(extra_sids,'k'),' '.join(extra_sids))) + + if tx.sign(c,tx_num_str,keys): + return tx + else: + die(3,'failed\nSome keys were missing. Transaction %scould not be signed.' % tx_num_str) diff --git a/setup.py b/setup.py index 5b6a5216..8fd8cb06 100755 --- a/setup.py +++ b/setup.py @@ -59,12 +59,12 @@ setup( name = 'mmgen', description = 'A complete Bitcoin offline/online wallet solution for the command line', version = g.version, - author = 'Philemon', - author_email = 'mmgen-py@yandex.com', - url = 'https://github.com/mmgen/mmgen', + author = g.author, + author_email = g.email, + url = g.proj_url, license = 'GNU GPL v3', platforms = 'Linux, MS Windows, Raspberry Pi', - keywords = 'Bitcoin, cryptocurrency, wallet, cold storage, offline, online, spending, open-source, command-line, Python, Bitcoin Core, bitcoind, hd, deterministic, hierarchical, secure, anonymous, Electrum, seed, mnemonic, brainwallet, Scrypt, utility, script, scriptable, blockchain, raw, transaction, permissionless, console, terminal, curses, ansi, color, tmux, remote, client, daemon, RPC, json, entropy', + keywords = 'Bitcoin, cryptocurrency, wallet, cold storage, offline, online, spending, open-source, command-line, Python, Bitcoin Core, bitcoind, hd, deterministic, hierarchical, secure, anonymous, Electrum, seed, mnemonic, brainwallet, Scrypt, utility, script, scriptable, blockchain, raw, transaction, permissionless, console, terminal, curses, ansi, color, tmux, remote, client, daemon, RPC, json, entropy, xterm, rxvt, PowerShell, MSYS, MinGW, mswin', cmdclass = { 'build_ext': my_build_ext, 'install_data': my_install_data }, ext_modules = [module1], data_files = [('share/mmgen', [ @@ -96,13 +96,16 @@ setup( 'mmgen.util', 'mmgen.main', + 'mmgen.main_wallet', 'mmgen.main_addrgen', 'mmgen.main_addrimport', - 'mmgen.main_tool', 'mmgen.main_txcreate', - 'mmgen.main_txsend', 'mmgen.main_txsign', - 'mmgen.main_wallet', + 'mmgen.main_txsend', + 'mmgen.main_txdo', + 'mmgen.txcreate', + 'mmgen.txsign', + 'mmgen.main_tool', 'mmgen.share.__init__', 'mmgen.share.Opts', @@ -118,6 +121,7 @@ setup( 'mmgen-txcreate', 'mmgen-txsign', 'mmgen-txsend', + 'mmgen-txdo', 'mmgen-tool' ] ) diff --git a/test/test.py b/test/test.py index b6863cd6..7b537d11 100755 --- a/test/test.py +++ b/test/test.py @@ -95,28 +95,34 @@ sample_text = \ # Laggy flash media cause pexpect to crash, so create a temporary directory # under '/dev/shm' and put datadir and temp files here. -if g.platform == 'win': - data_dir = os.path.join('test','data_dir') - try: os.listdir(data_dir) - except: pass +shortopts = ''.join([e[1:] for e in sys.argv if len(e) > 1 and e[0] == '-' and e[1] != '-']) +shortopts = ['-'+e for e in list(shortopts)] +data_dir = os.path.join('test','data_dir') +if not any(e in ('--skip-deps','--resume','-S','-r') for e in sys.argv+shortopts): + if g.platform == 'win': + try: os.listdir(data_dir) + except: pass + else: + import shutil + shutil.rmtree(data_dir) + os.mkdir(data_dir,0755) else: - import shutil - shutil.rmtree(data_dir) - os.mkdir(data_dir,0755) -else: - d,pfx = '/dev/shm','mmgen-test-' - try: - import subprocess - subprocess.call('rm -rf %s/%s*'%(d,pfx),shell=True) - except Exception as e: - die(2,'Unable to delete directory tree %s/%s* (%s)'%(d,pfx,e)) - try: - import tempfile - shm_dir = tempfile.mkdtemp('',pfx,d) - except Exception as e: - die(2,'Unable to create temporary directory in %s (%s)'%(d,e)) - data_dir = os.path.join(shm_dir,'data_dir') - os.mkdir(data_dir,0755) + d,pfx = '/dev/shm','mmgen-test-' + try: + import subprocess + subprocess.call('rm -rf %s/%s*'%(d,pfx),shell=True) + except Exception as e: + die(2,'Unable to delete directory tree %s/%s* (%s)'%(d,pfx,e)) + try: + import tempfile + shm_dir = tempfile.mkdtemp('',pfx,d) + except Exception as e: + die(2,'Unable to create temporary directory in %s (%s)'%(d,e)) + dd = os.path.join(shm_dir,'data_dir') + os.mkdir(dd,0755) + try: os.unlink(data_dir) + except: pass + os.symlink(dd,data_dir) opts_data = { # 'sets': [('interactive',bool,'verbose',None)], @@ -397,6 +403,8 @@ cmd_group['main'] = OrderedDict([ ['txcreate', (1,'transaction creation', [[['addrs'],1]],1)], ['txsign', (1,'transaction signing', [[['mmdat','rawtx',pwfile],1]],1)], ['txsend', (1,'transaction sending', [[['sigtx'],1]])], + # txdo must go after txsign + ['txdo', (1,'online transaction', [[['sigtx','mmdat'],1]])], ['export_seed', (1,'seed export to mmseed format', [[['mmdat'],1]])], ['export_mnemonic', (1,'seed export to mmwords format', [[['mmdat'],1]])], @@ -431,6 +439,7 @@ cmd_group['main'] = OrderedDict([ ['addrgen4', (4,'address generation (4)', [[['mmdat'],4]])], ['txcreate4', (4,'tx creation with inputs and outputs from four seed sources, key-address file and non-MMGen inputs and outputs', [[['addrs'],1],[['addrs'],2],[['addrs'],3],[['addrs'],4],[['addrs','akeys.mmenc'],14]])], ['txsign4', (4,'tx signing with inputs and outputs from incog file, mnemonic file, wallet, brainwallet, key-address file and non-MMGen inputs and outputs', [[['mmincog'],1],[['mmwords'],2],[['mmdat'],3],[['mmbrain','rawtx'],4],[['akeys.mmenc'],14]])], + ['txdo4', (4,'tx creation,signing and sending with inputs and outputs from four seed sources, key-address file and non-MMGen inputs and outputs', [[['addrs'],1],[['addrs'],2],[['addrs'],3],[['addrs'],4],[['addrs','akeys.mmenc'],14],[['mmincog'],1],[['mmwords'],2],[['mmdat'],3],[['mmbrain','rawtx'],4],[['akeys.mmenc'],14]])], # must go after txsign4 ]) cmd_group['tool'] = OrderedDict([ @@ -594,6 +603,7 @@ ia = bool(opt.interactive) os.environ['MMGEN_DISABLE_COLOR'] = '1' os.environ['MMGEN_NO_LICENSE'] = '1' os.environ['MMGEN_MIN_URANDCHARS'] = '3' +os.environ['MMGEN_BOGUS_SEND'] = '1' if opt.debug_scripts: os.environ['MMGEN_DEBUG'] = '1' @@ -1341,13 +1351,7 @@ class MMGenTestSuite(object): vmsg('This is a simulation, so no addresses were actually imported into the tracking\nwallet') ok() - def txcreate(self,name,addrfile): - self.txcreate_common(name,sources=['1']) - - def txcreate_dfl_wallet(self,name,addrfile): - self.txcreate_common(name,sources=['15']) - - def txcreate_common(self,name,sources=['1'],non_mmgen_input='',do_label=False): + def txcreate_common(self,name,sources=['1'],non_mmgen_input='',do_label=False,txdo_args=[],add_args=[]): if opt.verbose or opt.exact_output: sys.stderr.write(green('Generating fake tracking wallet info\n')) silence() @@ -1396,14 +1400,23 @@ class MMGenTestSuite(object): end_silence() if opt.verbose or opt.exact_output: sys.stderr.write('\n') - add_args = ([],['-q'])[ia] if ia: + add_args += ['-q'] m = '\nAnswer the interactive prompts as follows:\n' + \ " 'y', 'y', 'q', '1-9', ENTER, ENTER, ENTER, ENTER, 'y'" msg(grnbg(m)) - t = MMGenExpect(name,'mmgen-txcreate',['-f','0.0001'] + add_args + cmd_args) + bwd_msg = 'MMGEN_BOGUS_WALLET_DATA=%s' % unspent_data_file + if opt.print_cmdline: msg(bwd_msg) + if opt.log: log_fd.write(bwd_msg + ' ') + t = MMGenExpect(name,'mmgen-'+('txcreate','txdo')[bool(txdo_args)],['-f','0.0001'] + add_args + cmd_args + txdo_args) if ia: return t.license() + + if txdo_args and add_args: # txdo4 + t.hash_preset('key-address data','1') + t.passphrase('key-address data',cfgs['14']['kapasswd']) + t.expect('Check key-to-address validity? (y/N): ','y') + for num in tx_data: t.expect_getend('Getting address data from file ') chk=t.expect_getend(r'Checksum for address data .*?: ',regex=True) @@ -1425,7 +1438,7 @@ class MMGenTestSuite(object): if non_mmgen_input: outputs_list.append(len(tx_data)*(addrs_per_wallet+1) + 1) t.expect('Enter a range or space-separated list of outputs to spend: ', ' '.join([str(i) for i in outputs_list])+'\n') - if non_mmgen_input: t.expect('Accept? (y/N): ','y') + if non_mmgen_input and not txdo_args: t.expect('Accept? (y/N): ','y') t.expect('OK? (Y/n): ','y') # fee OK? t.expect('OK? (Y/n): ','y') # change OK? if do_label: @@ -1434,10 +1447,22 @@ class MMGenTestSuite(object): else: t.expect('Add a comment to transaction? (y/N): ','\n') t.tx_view() + if txdo_args: return t t.expect('Save transaction? (y/N): ','y') t.written_to_file('Transaction') ok() + def txcreate(self,name,addrfile): + self.txcreate_common(name,sources=['1']) + + def txdo(self,name,addrfile,wallet): + t = self.txcreate_common(name,sources=['1'],txdo_args=[wallet]) + self.txsign(name,'','',pf='',save=True,has_label=False,txdo_handle=t) + self.txsend(name,'',txdo_handle=t) + + def txcreate_dfl_wallet(self,name,addrfile): + self.txcreate_common(name,sources=['15']) + def txsign_end(self,t,tnum=None,has_label=False): t.expect('Signing transaction') cprompt = ('Add a comment to transaction','Edit transaction comment')[has_label] @@ -1446,16 +1471,21 @@ class MMGenTestSuite(object): add = ' #' + tnum if tnum else '' t.written_to_file('Signed transaction' + add, oo=True) - def txsign(self,name,txfile,wf,pf='',save=True,has_label=False): + def txsign(self,name,txfile,wf,pf='',save=True,has_label=False,txdo_handle=None): add_args = ([],['-q','-P',pf])[ia] if ia: m = '\nAnswer the interactive prompts as follows:\n ENTER, ENTER, ENTER' msg(grnbg(m)) - t = MMGenExpect(name,'mmgen-txsign', add_args+['-d',cfg['tmpdir'],txfile]+([],[wf])[bool(wf)]) - if ia: return - t.license() - t.tx_view() + if txdo_handle: + t = txdo_handle + if ia: return + else: + t = MMGenExpect(name,'mmgen-txsign', add_args+['-d',cfg['tmpdir'],txfile]+([],[wf])[bool(wf)]) + if ia: return + t.license() + t.tx_view() t.passphrase('MMGen wallet',cfg['wpasswd']) + if txdo_handle: return if save: self.txsign_end(t,has_label=has_label) else: @@ -1467,15 +1497,19 @@ class MMGenTestSuite(object): def txsign_dfl_wallet(self,name,txfile,pf='',save=True,has_label=False): return self.txsign(name,txfile,wf=None,pf=pf,save=save,has_label=has_label) - def txsend(self,name,sigfile): - t = MMGenExpect(name,'mmgen-txsend', ['-d',cfg['tmpdir'],sigfile]) - t.license() - t.tx_view() - t.expect('Add a comment to transaction? (y/N): ','\n') + def txsend(self,name,sigfile,txdo_handle=None): + if txdo_handle: + t = txdo_handle + else: + t = MMGenExpect(name,'mmgen-txsend', ['-d',cfg['tmpdir'],sigfile]) + t.license() + t.tx_view() + t.expect('Add a comment to transaction? (y/N): ','\n') t.expect('broadcast this transaction to the network?') - t.expect("'YES, I REALLY WANT TO DO THIS' to confirm: ",'\n') - t.expect('Exiting at user request') - vmsg('This is a simulation; no transaction was sent') + m = 'YES, I REALLY WANT TO DO THIS' + t.expect("'%s' to confirm: " % m,m+'\n') + t.expect('BOGUS transaction NOT sent') + t.written_to_file('Transaction ID') ok() def walletconv_export(self,name,wf,desc,uargs=[],out_fmt='w',pf=None,out_pw=False): @@ -1668,21 +1702,30 @@ class MMGenTestSuite(object): def txcreate4(self,name,f1,f2,f3,f4,f5,f6): self.txcreate_common(name,sources=['1','2','3','4','14'],non_mmgen_input='4',do_label=1) - def txsign4(self,name,f1,f2,f3,f4,f5,f6): + def txdo4(self,name,f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f11,f12): non_mm_fn = os.path.join(cfg['tmpdir'],non_mmgen_fn) - a = ['-d',cfg['tmpdir'],'-i','brain','-b'+cfg['bw_params'],'-p1','-k',non_mm_fn,'-M',f6,f1,f2,f3,f4,f5] - t = MMGenExpect(name,'mmgen-txsign',a) - t.license() + add_args = ['-d',cfg['tmpdir'],'-i','brain','-b'+cfg['bw_params'],'-p1','-k',non_mm_fn,'-M',f12] + t = self.txcreate_common(name,sources=['1','2','3','4','14'],non_mmgen_input='4',do_label=1,txdo_args=[f7,f8,f9,f10],add_args=add_args) + self.txsign4(name,f7,f8,f9,f10,f11,f12,txdo_handle=t) + self.txsend(name,'',txdo_handle=t) - t.hash_preset('key-address data','1') - t.passphrase('key-address data',cfgs['14']['kapasswd']) - t.expect('Check key-to-address validity? (y/N): ','y') - - t.tx_view() + def txsign4(self,name,f1,f2,f3,f4,f5,f6,txdo_handle=None): + if txdo_handle: + t = txdo_handle + else: + non_mm_fn = os.path.join(cfg['tmpdir'],non_mmgen_fn) + a = ['-d',cfg['tmpdir'],'-i','brain','-b'+cfg['bw_params'],'-p1','-k',non_mm_fn,'-M',f6,f1,f2,f3,f4,f5] + t = MMGenExpect(name,'mmgen-txsign',a) + t.license() + t.hash_preset('key-address data','1') + t.passphrase('key-address data',cfgs['14']['kapasswd']) + t.expect('Check key-to-address validity? (y/N): ','y') + t.tx_view() for cnum,desc in ('1','incognito data'),('3','MMGen wallet'): t.passphrase(('%s' % desc),cfgs[cnum]['wpasswd']) + if txdo_handle: return self.txsign_end(t,has_label=True) ok() @@ -2044,19 +2087,20 @@ class MMGenTestSuite(object): for k in ('walletgen','addrgen','keyaddrgen'): locals()[k+'14'] = locals()[k] # create temporary dirs -if g.platform == 'win': - for cfg in sorted(cfgs): - mk_tmpdir(cfgs[cfg]['tmpdir']) -else: - for cfg in sorted(cfgs): - src = os.path.join(shm_dir,cfgs[cfg]['tmpdir'].split('/')[-1]) - mk_tmpdir(src) - try: - os.unlink(cfgs[cfg]['tmpdir']) - except OSError as e: - if e.errno != 2: raise - finally: - os.symlink(src,cfgs[cfg]['tmpdir']) +if not opt.resume and not opt.skip_deps: + if g.platform == 'win': + for cfg in sorted(cfgs): + mk_tmpdir(cfgs[cfg]['tmpdir']) + else: + for cfg in sorted(cfgs): + src = os.path.join(shm_dir,cfgs[cfg]['tmpdir'].split('/')[-1]) + mk_tmpdir(src) + try: + os.unlink(cfgs[cfg]['tmpdir']) + except OSError as e: + if e.errno != 2: raise + finally: + os.symlink(src,cfgs[cfg]['tmpdir']) have_dfl_wallet = False