Browse Source

[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

philemon 8 years ago
parent
commit
6bdb70b3e9
14 changed files with 788 additions and 555 deletions
  1. 24 0
      mmgen-txdo
  2. 4 3
      mmgen/addr.py
  3. 9 18
      mmgen/color.py
  4. 1 0
      mmgen/globalvars.py
  5. 5 225
      mmgen/main_txcreate.py
  6. 83 0
      mmgen/main_txdo.py
  7. 2 21
      mmgen/main_txsend.py
  8. 14 165
      mmgen/main_txsign.py
  9. 18 0
      mmgen/obj.py
  10. 78 49
      mmgen/tx.py
  11. 245 0
      mmgen/txcreate.py
  12. 183 0
      mmgen/txsign.py
  13. 11 7
      setup.py
  14. 111 67
      test/test.py

+ 24 - 0
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 <mmgen-py@yandex.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+mmgen-txdo: Create, sign and broadcast an online MMGen transaction
+"""
+
+from mmgen.main import launch
+launch("txdo")

+ 4 - 3
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)]

+ 9 - 18
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]

+ 1 - 0
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     = '<mmgen@tuta.io>'

+ 5 - 225
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]  <addr,amt> ... [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 <seed ID>:<index>.
-
-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(),
+	'notes': '\n' + txcreate_notes
 }
 
-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)

+ 83 - 0
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 <mmgen-py@yandex.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+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]  <addr,amt> ... [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)

+ 2 - 21
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)

+ 14 - 165
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] <transaction file>... [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
-	)
-}
-
-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()
+	'notes': '\n' + txsign_notes
 }
 
-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)

+ 18 - 0
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'

+ 78 - 49
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
+			self.btc_txid = BitcoinTxID(tx_data.pop(-1),on_fail='return')
+			if not self.btc_txid:
+				do_err('Bitcoin TxID')
+
+		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:
+					self.label = MMGenTXLabel(comment,on_fail='return')
+					if not self.label:
+						do_err('comment')
+		else:
 			comment = ''
+
+		if len(tx_data) == 4:
+			metadata,self.hex,inputs_data,outputs_data = tx_data
 		else:
-			err_str = 'number of lines'
+			do_err('number of lines')
 
-		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'
-				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'
-
-		if err_str:
-			msg(err_fmt % err_str)
-			sys.exit(2)
+		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')

+ 245 - 0
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 <mmgen-py@yandex.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+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 <seed ID>:<index>.
+
+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

+ 183 - 0
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 <mmgen-py@yandex.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+"""
+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)

+ 11 - 7
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'
 		]
 	)

+ 111 - 67
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, 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()
-
-		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()
+		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)
+
+	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