Browse Source

New object-oriented TX code

philemon 8 years ago
parent
commit
3f2a866c47
12 changed files with 745 additions and 434 deletions
  1. 5 2
      mmgen/globalvars.py
  2. 73 68
      mmgen/main_txcreate.py
  3. 14 21
      mmgen/main_txsend.py
  4. 88 131
      mmgen/main_txsign.py
  5. 4 3
      mmgen/obj.py
  6. 1 1
      mmgen/opts.py
  7. 38 22
      mmgen/rpc.py
  8. 11 10
      mmgen/tool.py
  9. 365 171
      mmgen/tx.py
  10. 29 4
      mmgen/util.py
  11. 116 0
      scripts/tx-old2new.py
  12. 1 1
      test/tooltest.py

+ 5 - 2
mmgen/globalvars.py

@@ -35,6 +35,8 @@ no_license           = os.getenv('MMGEN_NOLICENSE')
 bogus_wallet_data    = os.getenv('MMGEN_BOGUS_WALLET_DATA')
 disable_hold_protect = os.getenv('MMGEN_DISABLE_HOLD_PROTECT')
 
+btc_amt_decimal_places = 8
+
 from decimal import Decimal
 tx_fee        = Decimal('0.0003')
 max_tx_fee    = Decimal('0.01')
@@ -77,8 +79,9 @@ seedfile_exts = (
 	wallet_ext, seed_ext, mn_ext, brain_ext, incog_ext, incog_hex_ext
 )
 
-rawtx_ext           = 'raw'
-sigtx_ext           = 'sig'
+rawtx_ext           = 'rawtx'
+sigtx_ext           = 'sigtx'
+txid_ext            = 'txid'
 addrfile_ext        = 'addrs'
 addrfile_chksum_ext = 'chk'
 keyfile_ext         = 'keys'

+ 73 - 68
mmgen/main_txcreate.py

@@ -127,7 +127,7 @@ def format_unspent_outputs_for_printing(out,sort_info,total):
 
 	return \
 'Unspent outputs ({} UTC)\nSort order: {}\n\n{}\n\nTotal BTC: {}\n'.format(
-		make_timestr(), ' '.join(sort_info), '\n'.join(pout), total
+		make_timestr(), ' '.join(sort_info), '\n'.join(pout), normalize_btc_amt(total)
 	)
 
 
@@ -146,7 +146,7 @@ def sort_and_view(unspent):
 	sort,group,show_days,show_mmaddr,reverse = 'age',False,False,True,True
 	unspent.sort(key=s_age,reverse=reverse) # Reverse age sort by default
 
-	total = trim_exponent(sum([i['amount'] for i in unspent]))
+	total = sum([i['amount'] for i in unspent])
 	max_acct_len = max([len(i['mmid']+' '+i['comment']) for i in unspent])
 
 	hdr_fmt   = 'UNSPENT OUTPUTS (sort order: %s)  Total BTC: %s'
@@ -188,7 +188,7 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
 				elif sort == 'txid' and a['txid'] == b['txid']:        b['skip'] = 'txid'
 
 		for i in unsp:
-			amt = str(trim_exponent(i['amount']))
+			amt = str(normalize_btc_amt(i['amount']))
 			lfill = 3 - len(amt.split('.')[0]) if '.' in amt else 3 - len(amt)
 			i['amt'] = ' '*lfill + amt
 			i['days'] = int(i['confirmations'] * g.mins_per_block / (60*24))
@@ -216,7 +216,7 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
 		if group and (sort == 'address' or sort == 'txid'):
 			sort_info.append('grouped')
 
-		out  = [hdr_fmt % (' '.join(sort_info), total), table_hdr]
+		out  = [hdr_fmt % (' '.join(sort_info), normalize_btc_amt(total)), table_hdr]
 		out += [fs % (str(n+1)+')',i['tx'],i['vout'],i['addr'],i['amt'],i['age'])
 					for n,i in enumerate(unsp)]
 
@@ -301,12 +301,12 @@ def mmaddr2btcaddr_unspent(unspent,mmaddr):
 def mmaddr2btcaddr(c,mmaddr,ail_w,ail_f):
 
 	# assume mmaddr has already been checked
-	btcaddr = ail_w.mmaddr2btcaddr(mmaddr)
+	btc_addr = ail_w.mmaddr2btcaddr(mmaddr)
 
-	if not btcaddr:
+	if not btc_addr:
 		if ail_f:
-			btcaddr = ail_f.mmaddr2btcaddr(mmaddr)
-			if btcaddr:
+			btc_addr = ail_f.mmaddr2btcaddr(mmaddr)
+			if btc_addr:
 				msg(wmsg['addr_in_addrfile_only'].format(mmgenaddr=mmaddr))
 				if not keypress_confirm('Continue anyway?'):
 					sys.exit(1)
@@ -315,15 +315,31 @@ def mmaddr2btcaddr(c,mmaddr,ail_w,ail_f):
 		else:
 			die(2,wmsg['addr_not_found_no_addrfile'].format(pnm=pnm,mmgenaddr=mmaddr))
 
-	return btcaddr
+	return btc_addr
 
 
-def make_b2m_map(inputs_data,tx_out,ail_w,ail_f):
-	d = dict([(d['address'], (d['mmid'],d['comment']))
-				for d in inputs_data if d['mmid']])
-	d.update(ail_w.make_reverse_dict(tx_out.keys()))
-	d.update(ail_f.make_reverse_dict(tx_out.keys()))
-	return d
+def get_fee_estimate():
+	if 'tx_fee' in opt.set_by_user:
+		return None
+	else:
+		ret = c.estimatefee(opt.tx_confs)
+		if ret != -1:
+			return 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
 
 def get_fee_estimate():
 	if 'tx_fee' in opt.set_by_user:
@@ -372,16 +388,15 @@ def get_tx_size_and_fee(inputs,outputs):
 
 cmd_args = opts.init(opts_data)
 
-if opt.comment_file:
-	comment = get_tx_comment_from_file(opt.comment_file)
+tx = MMGenTX()
+
+if opt.comment_file: tx.add_comment(opt.comment_file)
 
 c = bitcoin_connection()
 
 if not opt.info:
 	do_license_msg(immed=True)
 
-	tx_out,change_addr = {},''
-
 	addrfiles = [a for a in cmd_args if get_extension(a) == g.addrfile_ext]
 	cmd_args = set(cmd_args) - set(addrfiles)
 
@@ -397,27 +412,27 @@ if not opt.info:
 		if ',' in a:
 			a1,a2 = split2(a,',')
 			if is_btc_addr(a1):
-				btcaddr = a1
+				btc_addr = a1
 			elif is_mmgen_addr(a1):
-				btcaddr = mmaddr2btcaddr(c,a1,ail_w,ail_f)
+				btc_addr = mmaddr2btcaddr(c,a1,ail_w,ail_f)
 			else:
 				die(2,"%s: unrecognized subargument in argument '%s'" % (a1,a))
 
-			ret = normalize_btc_amt(a2)
-			if ret:
-				tx_out[btcaddr] = ret
+			btc_amt = convert_to_btc_amt(a2)
+			if btc_amt:
+				tx.add_output(btc_addr,btc_amt)
 			else:
 				die(2,"%s: invalid amount in argument '%s'" % (a2,a))
 		elif is_mmgen_addr(a) or is_btc_addr(a):
-			if change_addr:
+			if tx.change_addr:
 				die(2,'ERROR: More than one change address specified: %s, %s' %
 						(change_addr, a))
-			change_addr = a if is_btc_addr(a) else mmaddr2btcaddr(c,a,ail_w,ail_f)
-			tx_out[change_addr] = 0
+			tx.change_addr = a if is_btc_addr(a) else mmaddr2btcaddr(c,a,ail_w,ail_f)
+			tx.add_output(tx.change_addr,Decimal('0'))
 		else:
 			die(2,'%s: unrecognized argument' % a)
 
-	if not tx_out:
+	if not tx.outputs:
 		die(2,'At least one output must be specified on the command line')
 
 	if opt.tx_fee > g.max_tx_fee:
@@ -440,13 +455,14 @@ for o in us:
 	del o['account']
 unspent = sort_and_view(us)
 
-total = trim_exponent(sum([i['amount'] for i in unspent]))
+total = sum([i['amount'] for i in unspent])
 
-msg('Total unspent: %s BTC (%s outputs)' % (total, len(unspent)))
+msg('Total unspent: %s BTC (%s outputs)' % (normalize_btc_amt(total), len(unspent)))
 if opt.info: sys.exit()
 
-send_amt = sum([tx_out[i] for i in tx_out.keys()])
-msg('Total amount to spend: %s' % ('%s BTC'%send_amt,'Unknown')[bool(send_amt)])
+tx.send_amt = tx.sum_outputs()
+
+msg('Total amount to spend: %s' % ('Unknown','%s BTC'%tx.send_amt,)[bool(tx.send_amt)])
 
 while True:
 	sel_nums = select_outputs(unspent,
@@ -465,53 +481,42 @@ while True:
 		if not keypress_confirm('Accept?'):
 			continue
 
-	total_in = trim_exponent(sum([i['amount'] for i in sel_unspent]))
-	tx_size,tx_fee = get_tx_size_and_fee(sel_unspent,tx_out)
-	change = trim_exponent(total_in - (send_amt + tx_fee))
+	tx.copy_inputs(sel_unspent)              # makes tx.inputs
+
+	tx.calculate_size_and_fee(fee_estimate)  # sets tx.size, tx.fee
 
-	if change >= 0:
-		prompt = 'Transaction produces %s BTC in change.  OK?' % change
+	change_amt = tx.sum_inputs() - tx.send_amt - tx.fee
+
+	if change_amt >= 0:
+		prompt = 'Transaction produces %s BTC in change.  OK?' % change_amt
 		if keypress_confirm(prompt,default_yes=True):
 			break
 	else:
-		msg(wmsg['not_enough_btc'] % change)
-
-if change > 0 and not change_addr:
-	die(2,wmsg['throwaway_change'] % change)
+		msg(wmsg['not_enough_btc'] % change_amt)
 
-if change_addr in tx_out and not change:
+if change_amt > 0:
+	if not tx.change_addr:
+		die(2,wmsg['throwaway_change'] % change_amt)
+	tx.add_output(tx.change_addr,change_amt)
+elif tx.change_addr:
 	msg('Warning: Change address will be unused as transaction produces no change')
-	del tx_out[change_addr]
+	tx.del_output(tx.change_addr)
 
-for k,v in tx_out.items(): tx_out[k] = float(v)
+if not tx.send_amt:
+	tx.send_amt = change_amt
 
-if change > 0: tx_out[change_addr] = float(change)
+dmsg('tx: %s' % tx)
 
-tx_in = [{'txid':i['txid'], 'vout':i['vout']} for i in sel_unspent]
+tx.add_comment()   # edits an existing comment
+tx.create_raw(c)   # creates tx.hex, tx.txid
+tx.add_mmaddrs_to_outputs(ail_w,ail_f)
+tx.add_timestamp()
+tx.add_blockcount(c)
 
-dmsg('tx_in:  %s\ntx_out: %s' % (repr(tx_in),repr(tx_out)))
-
-if opt.comment_file:
-	if keypress_confirm('Edit comment?',False):
-		comment = get_tx_comment_from_user(comment)
-else:
-	if keypress_confirm('Add a comment to transaction?',False):
-		comment = get_tx_comment_from_user()
-	else: comment = False
-
-tx_hex = c.createrawtransaction(tx_in,tx_out)
 qmsg('Transaction successfully created')
 
-amt = send_amt or change
-tx_id = make_chksum_6(unhexlify(tx_hex)).upper()
-metadata = tx_id, amt, make_timestamp()
-
-b2m_map = make_b2m_map(sel_unspent,tx_out,ail_w,ail_f)
+dmsg('TX (final): %s' % tx)
 
-prompt_and_view_tx_data(c,'View decoded transaction?',
-		sel_unspent,tx_hex,b2m_map,comment,metadata)
+tx.view_with_prompt('View decoded transaction?')
 
-outfile = 'tx_%s[%s].%s' % (tx_id,amt,g.rawtx_ext)
-data = make_tx_data('{} {} {}'.format(*metadata),
-			tx_hex,sel_unspent,b2m_map,comment)
-write_data_to_file(outfile,data,'transaction',ask_write_default_yes=False)
+tx.write_to_file(ask_write_default_yes=False)

+ 14 - 21
mmgen/main_txsend.py

@@ -44,39 +44,32 @@ else: opts.usage()
 
 do_license_msg()
 
-tx_data = get_lines_from_file(infile,'signed transaction data')
+tx = MMGenTX()
 
-metadata,tx_hex,inputs_data,b2m_map,comment = parse_tx_file(tx_data,infile)
-
-qmsg("Signed transaction file '%s' is valid" % infile)
+tx.parse_tx_file(infile,'signed transaction data')
 
 c = bitcoin_connection()
 
-prompt_and_view_tx_data(c,'View transaction data?',
-	inputs_data,tx_hex,b2m_map,comment,metadata)
+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 keypress_confirm('Edit transaction comment?'):
-	comment = get_tx_comment_from_user(comment)
-	data = make_tx_data('{} {} {}'.format(*metadata), tx_hex,
-				inputs_data, b2m_map, comment)
-	write_data_to_file(infile,data,'signed transaction with edited comment')
+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'
+expect = 'YES, I REALLY WANT TO DO THIS'
 
 if opt.quiet: warn,expect = '','YES'
 
-confirm_or_exit(warn, action, expect)
+confirm_or_exit(warn,action,expect)
 
 msg('Sending transaction')
 
-try:
-	tx_id = c.sendrawtransaction(tx_hex)
-except:
-	die(3,'Unable to send transaction')
-
-msg('Transaction sent: %s' % tx_id)
+tx.send(c,bogus=True)
 
-of = 'tx_{}[{}].txid'.format(*metadata[:2])
-write_data_to_file(of, tx_id+'\n','transaction ID',ask_overwrite=True)
+tx.write_txid_to_file()

+ 88 - 131
mmgen/main_txsign.py

@@ -25,10 +25,11 @@ from mmgen.tx import *
 from mmgen.seed import SeedSource
 
 pnm = g.proj_name
-pnl = pnm.lower()
 
+# Unneeded, as MMGen transactions cannot contain non-MMGen inputs
+# -w, --use-wallet-dat  Get keys from a running bitcoind
 opts_data = {
-	'desc':    'Sign Bitcoin transactions generated by {pnl}-txcreate'.format(pnl=pnl),
+	'desc':    'Sign Bitcoin transactions generated by {pnl}-txcreate'.format(pnl=pnm.lower()),
 	'usage':   '[opts] <transaction file>... [seed source]...',
 	'options': """
 -h, --help            Print this help message.
@@ -64,8 +65,7 @@ opts_data = {
 -I, --info            Display information about the transaction and exit.
 -t, --terse-info      Like '--info', but produce more concise output.
 -v, --verbose         Produce more verbose output
--w, --use-wallet-dat  Get keys from a running bitcoind
-""".format(g=g,pnm=pnm,pnl=pnl),
+""".format(g=g,pnm=pnm,pnl=pnm.lower()),
 	'notes': """
 
 Transactions with either {pnm} or non-{pnm} input addresses may be signed.
@@ -97,7 +97,7 @@ FMT CODES:
   {f}
 """.format(
 		f='\n  '.join(SeedSource.format_fmt_codes().splitlines()),
-		g=g,pnm=pnm,pnl=pnl
+		g=g,pnm=pnm,pnl=pnm.lower()
 	)
 }
 
@@ -114,7 +114,7 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
 
 def get_seed_for_seed_id(seed_id,infiles,saved_seeds):
 
-	if seed_id in saved_seeds.keys():
+	if seed_id in saved_seeds:
 		return saved_seeds[seed_id]
 
 	while True:
@@ -131,8 +131,7 @@ def get_seed_for_seed_id(seed_id,infiles,saved_seeds):
 
 		if ss.seed.sid == seed_id: return ss.seed.data
 
-
-def get_keys_for_mmgen_addrs(mmgen_addrs,infiles,saved_seeds):
+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)))
@@ -148,70 +147,31 @@ def get_keys_for_mmgen_addrs(mmgen_addrs,infiles,saved_seeds):
 		d += [('{}:{}'.format(seed_id,e.idx),e.addr,e.wif) for e in ai.addrdata]
 	return d
 
-
-def sign_transaction(c,tx_hex,tx_num_str,sig_data,keys=None):
-
-	if keys:
-		qmsg('Passing %s key%s to bitcoind' % (len(keys),suf(keys,'k')))
-		dmsg('Keys:\n  %s' % '\n  '.join(keys))
-
-	msg_r('Signing transaction{}...'.format(tx_num_str))
-#	from mmgen.rpc import exceptions
-	try:
-		sig_tx = c.signrawtransaction(tx_hex,sig_data,keys)
-#	except exceptions.InvalidAddressOrKey:
-	except: # TODO
-		die(3,'failed\nInvalid address or key')
-
-	return sig_tx
-
-
-def sign_tx_with_bitcoind_wallet(c,tx_hex,tx_num_str,sig_data,keys):
-
-	try:
-		sig_tx = sign_transaction(c,tx_hex,tx_num_str,sig_data,keys)
-	except:
-#		from mmgen.rpc import exceptions
+# function unneeded, as MMGen transactions cannot contain non-MMGen inputs
+def sign_tx_with_bitcoind_wallet(c,tx,tx_num_str,keys):
+	ok = tx.sign(c,tx_num_str,keys) # returns false on failure
+	if ok:
+		return ok
+	else:
 		msg('Using keys in wallet.dat as per user request')
 		prompt = 'Enter passphrase for bitcoind wallet: '
 		while True:
 			passwd = get_bitcoind_passphrase(prompt)
-
-			try:
-				c.walletpassphrase(passwd, 9999)
-#			except exceptions.WalletPassphraseIncorrect:
-			except: # TODO
-				msg('Passphrase incorrect (or some other error)')
+			ret = c.walletpassphrase(passwd, 9999,ret_on_error=True)
+			if rpc_error(ret):
+				if rpc_errmsg(ret,'unencrypted wallet, but walletpassphrase was called'):
+					msg('Wallet is unencrypted'); break
 			else:
 				msg('Passphrase OK'); break
 
-		sig_tx = sign_transaction(c,tx_hex,tx_num_str,sig_data,keys)
+		ok = tx.sign(c,tx_num_str,keys)
 
 		msg('Locking wallet')
-		try:
-			c.walletlock()
-		except:
+		ret = c.walletlock(ret_on_error=True)
+		if rpc_error(ret):
 			msg('Failed to lock wallet')
 
-	return sig_tx
-
-
-def check_maps_from_seeds(maplist,desc,infiles,saved_seeds,return_keys=False):
-
-	if not maplist: return []
-	qmsg('Checking {pnm} -> BTC address mappings for {w}s (from seed(s))'.format(
-				pnm=pnm,w=desc))
-	d = get_keys_for_mmgen_addrs(maplist.keys(),infiles,saved_seeds)
-#	0=mmaddr 1=addr 2=wif
-	m = dict([(e[0],e[1]) for e in d])
-	for a,b in zip(sorted(m),sorted(maplist)):
-		if a != b:
-			al,bl = 'generated seed:','tx file:'
-			die(3,wmsg['mm2btc_mapping_error'] % (al,a,m[a],bl,b,maplist[b]))
-	if return_keys:
-		ret = [e[2] for e in d]
-		vmsg('Added %s wif key%s from seeds' % (len(ret),suf(ret,'k')))
-		return ret
+		return ok
 
 def missing_keys_errormsg(addrs):
 	Msg("""
@@ -219,7 +179,6 @@ A key file must be supplied (or use the '--use-wallet-dat' option)
 for the following non-{pnm} address{suf}:\n    {l}""".format(
 	pnm=pnm, suf=suf(addrs,'a'), l='\n    '.join(addrs)).strip())
 
-
 def parse_mmgen_keyaddr_file():
 	from mmgen.addr import AddrInfo
 	ai = AddrInfo(opt.mmgen_keys_from_file,has_keys=True)
@@ -229,21 +188,18 @@ def parse_mmgen_keyaddr_file():
 	return dict(
 		[('%s:%s'%(ai.seed_id,e.idx), (e.addr,e.wif)) for e in ai.addrdata])
 
-
-def parse_keylist(from_file):
+def parse_keylist(key_data):
 	fn = opt.keys_from_file
 	from mmgen.crypto import mmgen_decrypt_file_maybe
 	dec = mmgen_decrypt_file_maybe(fn,'non-{} keylist file'.format(pnm))
-	keys_all = remove_comments(dec.splitlines()) # DOS-safe
 	# Key list could be bitcoind dump, so remove first space and everything following
-	keys_all = [k.split()[0] for k in keys_all]
-	keys_all = set(keys_all) # Remove possible dups
+	keys_all = set([line.split()[0] for line in remove_comments(dec.splitlines())]) # DOS-safe
 	dmsg(repr(keys_all))
-	d = from_file['mmdata']
-	kawifs = [d[k][1] for k in d.keys()]
-	keys = [k for k in keys_all if k not in kawifs]
+	ka_keys = [d[k][1] for k in key_data['kafile']]
+	keys = [k for k in keys_all if k not in ka_keys]
 	removed = len(keys_all) - len(keys)
-	if removed: vmsg(wmsg['removed_dups'] % (removed,suf(removed,'k')))
+	if removed:
+		vmsg(wmsg['removed_dups'] % (removed,suf(removed,'k')))
 	addrs = []
 	wif2addr_f = get_wif2addr_f()
 	for n,k in enumerate(keys,1):
@@ -253,41 +209,55 @@ def parse_keylist(from_file):
 
 	return dict(zip(addrs,keys))
 
-
 # Check inputs and outputs maps against key-address file, deleting entries:
-def check_maps_from_kafile(imap,desc,kadata,return_keys=False):
+def check_maps_from_kafile(io_map,desc,kadata,return_keys=False):
 	if not kadata: return []
 	qmsg('Checking {pnm} -> BTC address mappings for {w}s (from key-address file)'.format(pnm=pnm,w=desc))
 	ret = []
-	for k in imap.keys():
-		if k in kadata.keys():
-			if kadata[k][0] == imap[k]:
-				del imap[k]
+	for k in io_map.keys():
+		if k in kadata:
+			if kadata[k][0] == io_map[k]:
+				del io_map[k]
 				ret += [kadata[k][1]]
 			else:
 				kl,il = 'key-address file:','tx file:'
-				die(2,wmsg['mm2btc_mapping_error']%(kl,k,kadata[k][0],il,k,imap[k]))
+				die(2,wmsg['mm2btc_mapping_error']%(kl,k,kadata[k][0],il,k,io_map[k]))
 	if ret: vmsg('Removed %s address%s from %ss map' % (len(ret),suf(ret,'a'),desc))
 	if return_keys:
 		vmsg('Added %s wif key%s from %ss map' % (len(ret),suf(ret,'k'),desc))
 		return ret
 
+# Check inputs and outputs maps against values generated from seeds
+def check_maps_from_seeds(io_map,desc,infiles,saved_seeds,return_keys=False):
+
+	if not io_map: return []
+	qmsg('Checking {pnm} -> BTC address mappings for {w}s (from seed(s))'.format(
+				pnm=pnm,w=desc))
+	d = generate_keys_for_mmgen_addrs(io_map.keys(),infiles,saved_seeds)
+#	0=mmaddr 1=addr 2=wif
+	m = dict([(e[0],e[1]) for e in d])
+	for a,b in zip(sorted(m),sorted(io_map)):
+		if a != b:
+			al,bl = 'generated seed:','tx file:'
+			die(3,wmsg['mm2btc_mapping_error'] % (al,a,m[a],bl,b,io_map[b]))
+	if return_keys:
+		vmsg('Added %s wif key%s from seeds' % (len(d),suf(ret,'k')))
+		return [e[2] for e in d]
 
-def get_keys_from_keylist(kldata,other_addrs):
+def get_keys_from_keylist(kldata,addrs):
 	ret = []
-	for addr in other_addrs[:]:
-		if addr in kldata.keys():
+	for addr in addrs[:]:
+		if addr in kldata:
 			ret += [kldata[addr]]
-			other_addrs.remove(addr)
+			addrs.remove(addr)
 	vmsg('Added %s wif key%s from user-supplied keylist' %
 			(len(ret),suf(ret,'k')))
 	return ret
 
+# main(): execution begins here
 
 infiles = opts.init(opts_data,add_opts=['b16'])
 
-#if opt.from_incog_hex or opt.from_incog_hidden: opt.from_incog = True
-
 if not infiles: opts.usage()
 for i in infiles: check_infile(i)
 
@@ -295,16 +265,21 @@ c = bitcoin_connection()
 
 saved_seeds = {}
 tx_files   = [i for i in infiles if get_extension(i) == g.rawtx_ext]
-seed_files = [i for i in infiles if get_extension(i) != g.rawtx_ext]
+seed_files = [i for i in infiles if get_extension(i) in g.seedfile_exts]
+
+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)
 
-from_file = { 'mmdata':{}, 'kldata':{} }
+key_data = { 'kafile':{}, 'klfile':{} }
 if opt.mmgen_keys_from_file:
-	from_file['mmdata'] = parse_mmgen_keyaddr_file() or {}
+	key_data['kafile'] = parse_mmgen_keyaddr_file() or {}
 if opt.keys_from_file:
-	from_file['kldata'] = parse_keylist(from_file) or {}
+	key_data['klfile'] = parse_keylist(key_data) or {}
 
 tx_num_str = ''
 for tx_num,tx_file in enumerate(tx_files,1):
@@ -312,72 +287,54 @@ for tx_num,tx_file in enumerate(tx_files,1):
 		msg('\nTransaction #%s of %s:' % (tx_num,len(tx_files)))
 		tx_num_str = ' #%s' % tx_num
 
-	m = ('transaction data','')[bool(opt.tx_id)]
-	tx_data = get_lines_from_file(tx_file,m)
+	tx = MMGenTX()
+	tx.parse_tx_file(tx_file,('transaction data','')[bool(opt.tx_id)])
+
+	if tx.check_signed(c):
+		die(1,'Transaction is already signed!')
 
-	metadata,tx_hex,inputs_data,b2m_map,comment = parse_tx_file(tx_data,tx_file)
 	vmsg("Successfully opened transaction file '%s'" % tx_file)
 
-	if opt.tx_id: die(0,metadata[0])
+	if opt.tx_id: die(0,tx.txid)
 
 	if opt.info or opt.terse_info:
-		view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,pause=False,
-				terse=opt.terse_info)
+		tx.view(pause=False,terse=opt.terse_info)
 		sys.exit()
 
-	prompt_and_view_tx_data(c,'View data for transaction{}?'.format(tx_num_str),
-		inputs_data,tx_hex,b2m_map,comment,metadata)
+	tx.view_with_prompt('View data for transaction%s?' % tx_num_str)
 
 	# Start
-	other_addrs = list(set([i['address'] for i in inputs_data if not i['mmid']]))
+	other_addrs = list(set([i['address'] for i in tx.inputs if not i['mmid']]))
 
-	keys = get_keys_from_keylist(from_file['kldata'],other_addrs)
+	# should remove all elements from other_addrs
+	keys = get_keys_from_keylist(key_data['klfile'],other_addrs)
 
 	if other_addrs and not opt.use_wallet_dat:
 		missing_keys_errormsg(other_addrs)
 		sys.exit(2)
 
-	imap = dict([(i['mmid'],i['address']) for i in inputs_data if i['mmid']])
-	omap = dict([(j[0],i) for i,j in b2m_map.items()])
-	sids = set([i[:8] for i in imap.keys()])
+	imap = dict([(i['mmid'],i['address']) for i in tx.inputs if i['mmid']])
+	omap = dict([(tx.outputs[k][1],k) for k in tx.outputs if len(tx.outputs[k]) > 1])
+	sids = set([i[:8] for i in imap])
 
-	keys += check_maps_from_kafile(imap,'input',from_file['mmdata'],True)
-	check_maps_from_kafile(omap,'output',from_file['mmdata'])
+	keys += check_maps_from_kafile(imap,'input',key_data['kafile'],True)
+	check_maps_from_kafile(omap,'output',key_data['kafile'])
 
 	keys += check_maps_from_seeds(imap,'input',seed_files,saved_seeds,True)
 	check_maps_from_seeds(omap,'output',seed_files,saved_seeds)
 
-	extra_sids = set(saved_seeds.keys()) - sids
+	extra_sids = set(saved_seeds) - sids
 	if extra_sids:
 		msg('Unused Seed ID%s: %s' %
 			(suf(extra_sids,'k'),' '.join(extra_sids)))
 
-	# Begin signing
-	sig_data = [
-		{'txid':i['txid'],'vout':i['vout'],'scriptPubKey':i['scriptPubKey']}
-			for i in inputs_data]
+# 	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 opt.use_wallet_dat:
-		sig_tx = sign_tx_with_bitcoind_wallet(
-				c,tx_hex,tx_num_str,sig_data,keys)
-	else:
-		sig_tx = sign_transaction(c,tx_hex,tx_num_str,sig_data,keys)
-
-	if sig_tx['complete']:
-		msg('OK')
-		if keypress_confirm('Edit transaction comment?'):
-			comment = get_tx_comment_from_user(comment)
-		outfile = 'tx_%s[%s].%s' % (metadata[0],metadata[1],g.sigtx_ext)
-		data = make_tx_data(
-				'{} {} {t}'.format(*metadata[:2],
-				t=make_timestamp()),
-				sig_tx['hex'], inputs_data, b2m_map, comment
-			)
-		write_data_to_file(
-			outfile,data,
-			'signed transaction{}'.format(tx_num_str),
-			ask_write_prompt='Save signed transaction?'
-		)
+	if ok:
+		tx.add_comment()   # edits an existing comment
+		tx.write_to_file(ask_write_default_yes=True)
 	else:
-		msg_r('failed\nSome keys were missing.  ')
-		die(3,'Transaction %scould not be signed.' % tx_num_str)
+		die(3,'failed\nSome keys were missing.  Transaction %scould not be signed.' % tx_num_str)

+ 4 - 3
mmgen/obj.py

@@ -20,6 +20,7 @@
 obj.py:  The MMGenObject class and methods
 """
 import mmgen.globalvars as g
+from decimal import Decimal
 
 lvl = 0
 
@@ -38,16 +39,16 @@ class MMGenObject(object):
 
 		def conv(v,col_w):
 			vret = ''
-			if type(v) == str:
+			if type(v) in (str,unicode):
 				if not (set(list(v)) <= set(list(g.printable))):
 					vret = repr(v)
 				else:
 					vret = fix_linebreaks(v,fixed_indent=0)
-			elif type(v) == int or type(v) == long:
+			elif type(v) in (int,long,Decimal):
 				vret = str(v)
 			elif type(v) == dict:
 				sep = '\n{}{}'.format(indent,' '*4)
-				cw = max(len(k) for k in v) + 2
+				cw = (max(len(k) for k in v) if v else 0) + 2
 				t = sep.join(['{:<{w}}: {}'.format(
 					repr(k),
 	(fix_linebreaks(v[k],fixed_indent=0) if type(v[k]) == str else v[k]),

+ 1 - 1
mmgen/opts.py

@@ -125,7 +125,7 @@ def init(opts_data,add_opts=[],opt_filter=None):
 	for k in 'prog_name','desc','usage','options','notes':
 		if k in opts_data: del opts_data[k]
 
-	# Transfer uopts into opt, setting required opts to None if not set by user
+	# Transfer uopts into opt, setting program's opts + required opts to None if not set by user
 	for o in [s.rstrip('=') for s in long_opts] + \
 			g.required_opts + add_opts + skipped_opts:
 		setattr(opt,o,uopts[o] if o in uopts else None)

+ 38 - 22
mmgen/rpc.py

@@ -52,9 +52,12 @@ class BitcoinRPCConnection(object):
 	# Normal mode: call with arg list unrolled, exactly as with 'bitcoin-cli'
 	# Batch mode:  call with list of arg lists as first argument
 	# kwargs are for local use and are not passed to server
+
+	# By default, dies with an error msg on all errors and exceptions
+	# With ret_on_error=True, returns 'rpcfail',(resp_object,(die_args))
 	def request(self,cmd,*args,**kwargs):
 
-		cf = { 'timeout': g.http_timeout, 'batch': False }
+		cf = { 'timeout':g.http_timeout, 'batch':False, 'ret_on_error':False }
 
 		for k in cf:
 			if k in kwargs and kwargs[k]: cf[k] = kwargs[k]
@@ -66,6 +69,12 @@ class BitcoinRPCConnection(object):
 		else:
 			p = {'method':cmd,'params':args,'id':1}
 
+		def die_maybe(*args):
+			if cf['ret_on_error']:
+				return 'rpcfail',args
+			else:
+				die(*args[1:])
+
 		dmsg('=== rpc.py debug ===')
 		dmsg('    RPC POST data ==> %s\n' % p)
 
@@ -75,34 +84,33 @@ class BitcoinRPCConnection(object):
 				'Authorization': 'Basic ' + base64.b64encode(self.auth_str)
 			})
 		except Exception as e:
-			die(2,'%s\nUnable to connect to bitcoind' % e)
+			return die_maybe(None,2,'%s\nUnable to connect to bitcoind' % e)
 
 		r = c.getresponse() # returns HTTPResponse instance
 
-		if r.status == 401:
-			m1 = 'RPC authentication error'
-			m2 = 'Check that rpcuser/rpcpassword in Bitcoin config file are correct'
-			m3 = '(or, alternatively, copy the authentication cookie to Bitcoin data dir'
-			m4 = 'if {} and Bitcoin are running as different users)'.format(g.proj_name)
-			die(1,'\n'.join((m1,m2,m3,m4)))
-		elif r.status != 200:
-			die(1,'RPC error: %s %s\n%s' % (r.status, r.reason, r.read()))
+		if r.status != 200:
+			e1 = r.read()
+			try:
+				e2 = json.loads(e1)['error']['message']
+			except:
+				e2 = str(e1)
+			return die_maybe(r,1,e2)
 
 		r2 = r.read()
 
 		dmsg('    RPC REPLY data ==> %s\n' % r2)
 
 		if not r2:
-			die(2,'Error: empty reply')
+			return die_maybe(r,2,'Error: empty reply')
 
 		r3 = json.loads(r2.decode('utf8'), parse_float=decimal.Decimal)
 		ret = []
 
 		for resp in r3 if cf['batch'] else [r3]:
 			if 'error' in resp and resp['error'] != None:
-				die(1,'Bitcoind returned an error: %s' % resp['error'])
+				return die_maybe(r,1,'Bitcoind returned an error: %s' % resp['error'])
 			elif 'result' not in resp:
-				die(1, 'Missing JSON-RPC result\n' + repr(resps))
+				return die_maybe(r,1, 'Missing JSON-RPC result\n' + repr(resps))
 			else:
 				ret.append(resp['result'])
 
@@ -110,20 +118,28 @@ class BitcoinRPCConnection(object):
 
 
 	rpcmethods = (
+		'createrawtransaction',
+		'backupwallet',
+		'decoderawtransaction',
 		'estimatefee',
-		'getinfo',
-		'getbalance',
 		'getaddressesbyaccount',
-		'listunspent',
-		'listaccounts',
+		'getbalance',
+		'getblock',
+		'getblockcount',
+		'getblockhash',
+		'getinfo',
 		'importaddress',
-		'decoderawtransaction',
-		'createrawtransaction',
-		'signrawtransaction',
+		'listaccounts',
+		'listunspent',
 		'sendrawtransaction',
-		'walletpassphrase',
-		'walletlock',
+		'signrawtransaction',
 	)
 
 	for name in rpcmethods:
 		exec "def {n}(self,*a,**k):return self.request('{n}',*a,**k)\n".format(n=name)
+
+def rpc_error(ret):
+	return ret is list and ret and ret[0] == 'rpcfail'
+
+def rpc_errmsg(ret,e):
+	return (False,True)[ret[1][2].find(e) == -1]

+ 11 - 10
mmgen/tool.py

@@ -432,10 +432,10 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
 	for k in sorted(addrs,key=s_mmgen):
 		if out and k.split('_')[0] != out[-1].split(':')[0]: out.append('')
 		baddr = ' ' + addrs[k][2] if showbtcaddrs else ''
-		out.append(fs % (k.replace('_',':'), baddr, addrs[k][1], trim_exponent(addrs[k][0])))
+		out.append(fs % (k.replace('_',':'), baddr, addrs[k][1], normalize_btc_amt(addrs[k][0])))
 
 	o = (fs + '\n%s\nTOTAL: %s BTC') % (
-			'ADDRESS','','COMMENT','BALANCE', '\n'.join(out), trim_exponent(total)
+			'ADDRESS','','COMMENT','BALANCE', '\n'.join(out), normalize_btc_amt(total)
 		)
 	if pager: do_pager(o)
 	else: Msg(o)
@@ -444,7 +444,9 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
 def getbalance(minconf=1):
 
 	accts = {}
-	for d in bitcoin_connection().listunspent(0):
+	us = bitcoin_connection().listunspent(0)
+#	pp_die(us)
+	for d in us:
 		ma = split2(d['account'])[0]
 		keys = ['TOTAL']
 		if d['spendable']: keys += ['SPENDABLE']
@@ -453,7 +455,7 @@ def getbalance(minconf=1):
 		i = (1,2)[confs >= minconf]
 
 		for key in keys:
-			if key not in accts: accts[key] = [0,0,0]
+			if key not in accts: accts[key] = [Decimal('0')] * 3
 			for j in ([],[0])[confs==0] + [i]:
 				accts[key][j] += d['amount']
 
@@ -461,15 +463,14 @@ def getbalance(minconf=1):
 	mc,lbl = str(minconf),'confirms'
 	Msg(fs.format('Wallet','Unconfirmed','<%s %s'%(mc,lbl),'>=%s %s'%(mc,lbl)))
 	for key in sorted(accts.keys()):
-		Msg(fs.format(key+':', *[str(trim_exponent(a))+' BTC'
-				for a in accts[key]]))
+		line = [str(normalize_btc_amt(a))+' BTC' for a in accts[key]]
+		Msg(fs.format(key+':', *line))
 
 def txview(infile,pager=False,terse=False):
 	c = bitcoin_connection()
-	tx_data = get_lines_from_file(infile,'transaction data')
-
-	metadata,tx_hex,inputs_data,b2m_map,comment = parse_tx_file(tx_data,infile)
-	view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,pager,pause=False,terse=terse)
+	tx = MMGenTX()
+	tx.parse_tx_file(infile,'transaction data')
+	tx.view(pager,pause=False,terse=terse)
 
 def add_label(mmaddr,label,remove=False):
 	if not is_mmgen_addr(mmaddr):

+ 365 - 171
mmgen/tx.py

@@ -22,40 +22,61 @@ tx.py:  Bitcoin transaction routines
 
 import sys, os
 from stat import *
-from binascii import unhexlify
+from binascii import hexlify,unhexlify
 from decimal import Decimal
 from collections import OrderedDict
 
 from mmgen.common import *
 from mmgen.term import do_pager
 
-def trim_exponent(n):
+def normalize_btc_amt(amt):
 	'''Remove exponent and trailing zeros.
 	'''
-	d = Decimal(n)
-	return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize()
+	# to_integral() needed to keep ints > 9 from being shown in exp. notation
+	if is_btc_amt(amt):
+		return amt.quantize(Decimal(1)) if amt == amt.to_integral() else amt.normalize()
+	else:
+		die(2,'%s: not a BTC amount' % amt)
 
-def normalize_btc_amt(amt):
+def is_btc_amt(amt):
+
+	if type(amt) is not Decimal:
+		msg('%s: not a decimal number' % amt)
+		return False
+
+	if amt.as_tuple()[-1] < -g.btc_amt_decimal_places:
+		msg('%s: Too many decimal places in amount' % amt)
+		return False
+
+	return True
+
+def convert_to_btc_amt(amt,return_on_fail=False):
 	# amt must be a string!
 
 	from decimal import Decimal
 	try:
 		ret = Decimal(amt)
 	except:
-		msg('%s: Invalid BTC amount' % amt)
-		return False
+		m = '%s: amount cannot be converted to decimal' % amt
+		if return_on_fail:
+			msg(m); return False
+		else:
+			die(2,m)
 
-	dmsg('Decimal(amt): %s\nAs tuple: %s' % (amt,repr(ret.as_tuple())))
+	dmsg('Decimal(amt): %s' % repr(amt))
 
-	if ret.as_tuple()[-1] < -8:
-		msg('%s: Too many decimal places in amount' % amt)
-		return False
+	if ret.as_tuple()[-1] < -g.btc_amt_decimal_places:
+		m = '%s: Too many decimal places in amount' % amt
+		if return_on_fail:
+			msg(m); return False
+		else:
+			die(2,m)
 
 	if ret == 0:
-		msg('Requested zero BTC amount')
-		return False
+		msg('WARNING: BTC amount is zero')
+
+	return ret
 
-	return trim_exponent(ret)
 
 def parse_mmgen_label(s,check_label_len=False):
 	l = split2(s)
@@ -129,183 +150,356 @@ Only ASCII printable characters are permitted.
 """.strip() % (ch,label))
 			sys.exit(3)
 
-def prompt_and_view_tx_data(c,prompt,inputs_data,tx_hex,adata,comment,metadata):
-
-	prompt += ' (y)es, (N)o, pager (v)iew, (t)erse view'
+def wiftoaddr_keyconv(wif):
+	if wif[0] == '5':
+		from subprocess import check_output
+		return check_output(['keyconv', wif]).split()[1]
+	else:
+		return wiftoaddr(wif)
 
-	reply = prompt_and_get_char(prompt,'YyNnVvTt',enter_ok=True)
+def get_wif2addr_f():
+	if opt.no_keyconv: return wiftoaddr
+	from mmgen.addr import test_for_keyconv
+	return (wiftoaddr,wiftoaddr_keyconv)[bool(test_for_keyconv())]
 
-	if reply and reply in 'YyVvTt':
-		view_tx_data(c,inputs_data,tx_hex,adata,comment,metadata,
-				pager=reply in 'Vv',terse=reply in 'Tt')
 
+def mmaddr2btcaddr_addrdata(mmaddr,addr_data,source=''):
+	seed_id,idx = mmaddr.split(':')
+	if seed_id in addr_data:
+		if idx in addr_data[seed_id]:
+			vmsg('%s -> %s%s' % (mmaddr,addr_data[seed_id][idx][0],
+				' (from %s)' % source if source else ''))
+			return addr_data[seed_id][idx]
 
-def view_tx_data(c,inputs_data,tx_hex,b2m_map,comment,metadata,pager=False,pause=True,terse=False):
+	return '',''
 
-	td = c.decoderawtransaction(tx_hex)
+from mmgen.obj import *
+
+class MMGenTX(MMGenObject):
+	def __init__(self,filename=None):
+		self.inputs      = []
+		self.outputs     = {}
+		self.change_addr = ''
+		self.size        = 0             # size of raw serialized tx
+		self.fee         = Decimal('0')
+		self.send_amt    = Decimal('0')  # total amt minus change
+		self.hex         = ''            # raw serialized hex transaction
+		self.comment     = ''
+		self.txid        = ''
+		self.btc_txid    = ''
+		self.timestamp   = ''
+		self.chksum      = ''
+		self.fmt_data    = ''
+		self.blockcount  = 0
+		self.isSigned    = False
+
+	def add_output(self,btcaddr,amt):
+		self.outputs[btcaddr] = (amt,)
+
+	def del_output(self,btcaddr):
+		del self.outputs[btcaddr]
+
+	def sum_outputs(self):
+		return sum([self.outputs[k][0] for k in self.outputs])
+
+	# returns true if comment added or changed
+	def add_comment(self,infile=None):
+		if infile:
+			s = get_data_from_file(infile,'transaction comment')
+			if is_valid_tx_comment(s):
+				self.comment = s.decode('utf8').strip()
+				return True
+			else:
+				sys.exit(2)
+		else: # get comment from user, or edit existing comment
+			m = ('Add a comment to transaction?','Edit transaction comment?')[bool(self.comment)]
+			if keypress_confirm(m,default_yes=False):
+				while True:
+					s = my_raw_input('Comment: ',insert_txt=self.comment.encode('utf8'))
+					if is_valid_tx_comment(s):
+						csave = self.comment
+						self.comment = s.decode('utf8').strip()
+						return (True,False)[csave == self.comment]
+					else:
+						msg('Invalid comment')
+			return False
 
-	fs = (
-		'TRANSACTION DATA\n\nHeader: [Tx ID: {}] [Amount: {} BTC] [Time: {}]\n\n',
-		'Transaction {} - {} BTC - {} UTC\n'
-	)[bool(terse)]
+	def edit_comment(self):
+		return self.add_comment(self)
 
-	out = fs.format(*metadata)
+	# https://bitcoin.stackexchange.com/questions/1195/how-to-calculate-transaction-size-before-sending
+	def calculate_size_and_fee(self,fee_estimate):
+		self.size = len(self.inputs)*180 + len(self.outputs)*34 + 10
+		if fee_estimate:
+			ftype,fee = 'Calculated','{:.8f}'.format(fee_estimate*opt.tx_fee_adj*self.size / 1024)
+		else:
+			ftype,fee = 'User-selected',opt.tx_fee
+
+		ufee = None
+		if not keypress_confirm('{} TX fee: {} BTC.  OK?'.format(ftype,fee),default_yes=True):
+			while True:
+				ufee = my_raw_input('Enter transaction fee: ')
+				if convert_to_btc_amt(ufee,return_on_fail=True):
+					if Decimal(ufee) > g.max_tx_fee:
+						msg('{} BTC: fee too large (maximum fee: {} BTC)'.format(ufee,g.max_tx_fee))
+					else:
+						fee = ufee
+						break
+		self.fee = convert_to_btc_amt(fee)
+		vmsg('Inputs:{}  Outputs:{}  TX size:{}'.format(
+				len(self.inputs),len(self.outputs),self.size))
+		vmsg('Fee estimate: {} (1024 bytes, {} confs)'.format(fee_estimate,opt.tx_confs))
+		m = ('',' (after %sx adjustment)' % opt.tx_fee_adj)[opt.tx_fee_adj != 1 and not ufee]
+		vmsg('TX fee:       {}{}'.format(self.fee,m))
+
+	def copy_inputs(self,source):
+		copy_keys = 'txid','vout','amount','comment','mmid','address',\
+					'confirmations','scriptPubKey'
+		self.inputs = [dict([(k,d[k] if k in d else '') for k in copy_keys]) for d in source]
+
+	def sum_inputs(self):
+		return sum([i['amount'] for i in self.inputs])
+
+	def create_raw(self,c):
+		inputs,outputs = [],{}
+		def dec2str(d):
+			tmp = {}
+			for k in d:
+				tmp[k] = str(d[k]) if type(d[k]) is Decimal else d[k]
+			return tmp
+		inputs = [dec2str(d) for d in self.inputs]
+		for k in self.outputs:
+			outputs[k] = str(self.outputs[k][0])
+#		mdie(inputs,outputs)
+		self.hex = c.createrawtransaction(inputs,outputs)
+		self.txid = make_chksum_6(unhexlify(self.hex)).upper()
+
+# 	def make_b2m_map(self,ail_w,ail_f):
+# 		d = dict([(d['address'], (d['mmid'],d['comment']))
+# 					for d in self.inputs if d['mmid']])
+# 		d = ail_w.make_reverse_dict(self.outputs.keys())
+# 		d.update(ail_f.make_reverse_dict(self.outputs.keys()))
+# 		self.b2m_map = d
+
+	def add_mmaddrs_to_outputs(self,ail_w,ail_f):
+		d = ail_w.make_reverse_dict(self.outputs.keys())
+		d.update(ail_f.make_reverse_dict(self.outputs.keys()))
+		for k in self.outputs:
+			if k in d:
+				self.outputs[k] += d[k]
+
+	def add_timestamp(self):
+		self.timestamp = make_timestamp()
+
+	def add_blockcount(self,c):
+		self.blockcount = int(c.getblockcount())
+
+	def format(self):
+		from mmgen.bitcoin import b58encode
+		lines = (
+			'{} {} {} {}'.format(
+				self.txid,
+				self.send_amt,
+				self.timestamp,
+				(self.blockcount or 'None')
+			),
+			self.hex,
+			repr(self.inputs),
+			repr(self.outputs)
+		) + ((b58encode(self.comment.encode('utf8')),) if self.comment else ())
+		self.chksum = make_chksum_6(' '.join(lines))
+		self.fmt_data = '\n'.join((self.chksum,) + lines)+'\n'
+
+	# return true or false, don't exit
+	def sign(self,c,tx_num_str,keys=None):
+
+		if keys:
+			qmsg('Passing %s key%s to bitcoind' % (len(keys),suf(keys,'k')))
+			dmsg('Keys:\n  %s' % '\n  '.join(keys))
+
+		sig_data = [dict([(k,d[k]) for k in 'txid','vout','scriptPubKey']) for d in self.inputs]
+		dmsg('Sig data:\n%s' % pp_format(sig_data))
+		dmsg('Raw hex:\n%s' % self.hex)
+
+		msg_r('Signing transaction{}...'.format(tx_num_str))
+		sig_tx = c.signrawtransaction(self.hex,sig_data,keys)
+
+		if sig_tx['complete']:
+			msg('OK')
+			self.hex = sig_tx['hex']
+			self.isSigned = True
+			return True
+		else:
+			msg('failed\nBitcoind returned the following errors:')
+			pp_msg(sig_tx['errors'])
+			return False
+
+	def check_signed(self,c):
+		d = c.decoderawtransaction(self.hex)
+		self.isSigned = bool(d['vin'][0]['scriptSig']['hex'])
+		return self.isSigned
+
+	def send(self,c,bogus=False):
+		if bogus:
+			self.btc_txid = 'deadbeef' * 8
+			m = 'BOGUS transaction NOT sent: %s'
+		else:
+			self.btc_txid = c.sendrawtransaction(self.hex) # exits on failure?
+			m = 'Transaction sent: %s'
+		msg(m % self.btc_txid)
+
+	def write_txid_to_file(self,ask_write=False,ask_write_default_yes=True):
+		fn = '%s[%s].%s' % (self.txid,self.send_amt,g.txid_ext)
+		write_data_to_file(fn,self.btc_txid+'\n','transaction ID',
+			ask_write=ask_write,
+			ask_write_default_yes=ask_write_default_yes)
+
+	def write_to_file(self,ask_write=True,ask_write_default_yes=False):
+		if ask_write == False:
+			ask_write_default_yes=True
+		self.format()
+		fn = '%s[%s].%s' % (self.txid,self.send_amt,
+			(g.rawtx_ext,g.sigtx_ext)[self.isSigned])
+		write_data_to_file(fn,self.fmt_data,'transaction',
+			ask_write=ask_write,
+			ask_write_default_yes=ask_write_default_yes)
+
+	def view_with_prompt(self,prompt=''):
+		prompt += ' (y)es, (N)o, pager (v)iew, (t)erse view'
+		reply = prompt_and_get_char(prompt,'YyNnVvTt',enter_ok=True)
+		if reply and reply in 'YyVvTt':
+			self.view(pager=reply in 'Vv',terse=reply in 'Tt')
+
+	def view(self,pager=False,pause=True,terse=False):
+		o = self.format_view(terse=terse).encode('utf8')
+		if pager: do_pager(o)
+		else:
+			sys.stdout.write(o)
+			from mmgen.term import get_char
+			if pause:
+				get_char('Press any key to continue: ')
+				msg('')
+
+	def format_view(self,terse=False):
+		try:
+			blockcount = bitcoin_connection().getblockcount()
+		except:
+			blockcount = None
+
+		fs = (
+			'TRANSACTION DATA\n\nHeader: [Tx ID: {}] [Amount: {} BTC] [Time: {}]\n\n',
+			'Transaction {} - {} BTC - {} UTC\n'
+		)[bool(terse)]
+
+		out = fs.format(self.txid,self.send_amt,self.timestamp)
+
+		enl = ('\n','')[bool(terse)]
+		if self.comment:
+			out += 'Comment: %s\n%s' % (self.comment,enl)
+		out += 'Inputs:\n' + enl
+
+		nonmm_str = 'non-{pnm} address'.format(pnm=g.proj_name)
+
+		for n,i in enumerate(self.inputs):
+			if blockcount:
+				confirmations = i['confirmations'] + blockcount - self.blockcount
+				days = int(confirmations * g.mins_per_block / (60*24))
+			if not i['mmid']:
+				i['mmid'] = nonmm_str
+			mmid_fmt = ' ({:>{l}})'.format(i['mmid'],l=34-len(i['address']))
+			if terse:
+				out += '  %s: %-54s %s BTC' % (n+1,i['address'] + mmid_fmt,
+						normalize_btc_amt(i['amount']))
+			else:
+				for d in (
+	(n+1, 'tx,vout:',       '%s,%s' % (i['txid'], i['vout'])),
+	('',  'address:',       i['address'] + mmid_fmt),
+	('',  'comment:',       i['comment']),
+	('',  'amount:',        '%s BTC' % normalize_btc_amt(i['amount'])),
+	('',  'confirmations:', '%s (around %s days)' % (confirmations,days) if blockcount else '')
+				):
+					if d[2]: out += ('%3s %-8s %s\n' % d)
+			out += '\n'
+
+		out += 'Outputs:\n' + enl
+		for n,k in enumerate(self.outputs):
+			btcaddr = k
+			v = self.outputs[k]
+			btc_amt,mmid,comment = (v[0],'Non-MMGen address','') if len(v) == 1 else v
+			mmid_fmt = ' ({:>{l}})'.format(mmid,l=34-len(btcaddr))
+			if terse:
+				out += '  %s: %-54s %s BTC' % (n+1, btcaddr+mmid_fmt, normalize_btc_amt(btc_amt))
+			else:
+				for d in (
+						(n+1, 'address:',  btcaddr + mmid_fmt),
+						('',  'comment:',  comment),
+						('',  'amount:',   '%s BTC' % normalize_btc_amt(btc_amt))
+					):
+					if d[2]: out += ('%3s %-8s %s\n' % d)
+			out += '\n'
 
-	enl = ('\n','')[bool(terse)]
-	if comment: out += 'Comment: %s\n%s' % (comment,enl)
-	out += 'Inputs:\n' + enl
+		fs = (
+			'Total input:  %s BTC\nTotal output: %s BTC\nTX fee:       %s BTC\n',
+			'In %s BTC - Out %s BTC - Fee %s BTC\n'
+		)[bool(terse)]
 
-	nonmm_str = 'non-{pnm} address'.format(pnm=g.proj_name)
+		total_in  = self.sum_inputs()
+		total_out = self.sum_outputs()
+		out += fs % (
+			normalize_btc_amt(total_in),
+			normalize_btc_amt(total_out),
+			normalize_btc_amt(total_in-total_out)
+		)
 
-	total_in = 0
-	for n,i in enumerate(td['vin']):
-		for j in inputs_data:
-			if j['txid'] == i['txid'] and j['vout'] == i['vout']:
-				days = int(j['confirmations'] * g.mins_per_block / (60*24))
-				total_in += j['amount']
-				if not j['mmid']: j['mmid'] = nonmm_str
-				mmid_fmt = ' ({:>{l}})'.format(j['mmid'],l=34-len(j['address']))
-				if terse:
-					out += '  %s: %-54s %s BTC' % (n+1,j['address'] + mmid_fmt,
-							trim_exponent(j['amount']))
-				else:
-					for d in (
-	(n+1, 'tx,vout:',       '%s,%s' % (i['txid'], i['vout'])),
-	('',  'address:',       j['address'] + mmid_fmt),
-	('',  'comment:',       j['comment']),
-	('',  'amount:',        '%s BTC' % trim_exponent(j['amount'])),
-	('',  'confirmations:', '%s (around %s days)' % (j['confirmations'], days))
-					):
-						if d[2]: out += ('%3s %-8s %s\n' % d)
-				out += '\n'
-
-				break
-	total_out = 0
-	out += 'Outputs:\n' + enl
-	for n,i in enumerate(td['vout']):
-		btcaddr = i['scriptPubKey']['addresses'][0]
-		mmid,comment=b2m_map[btcaddr] if btcaddr in b2m_map else (nonmm_str,'')
-		mmid_fmt = ' ({:>{l}})'.format(mmid,l=34-len(j['address']))
-		total_out += i['value']
-		if terse:
-			out += '  %s: %-54s %s BTC' % (n+1,btcaddr + mmid_fmt,
-					trim_exponent(i['value']))
-		else:
-			for d in (
-					(n+1, 'address:',  btcaddr + mmid_fmt),
-					('',  'comment:',  comment),
-					('',  'amount:',   trim_exponent(i['value']))
-				):
-				if d[2]: out += ('%3s %-8s %s\n' % d)
-		out += '\n'
-
-	fs = (
-		'Total input:  %s BTC\nTotal output: %s BTC\nTX fee:       %s BTC\n',
-		'In %s BTC - Out %s BTC - Fee %s BTC\n'
-	)[bool(terse)]
-
-	out += fs % (
-		trim_exponent(total_in),
-		trim_exponent(total_out),
-		trim_exponent(total_in-total_out)
-	)
-
-	o = out.encode('utf8')
-	if pager: do_pager(o)
-	else:
-		sys.stdout.write(o)
-		from mmgen.term import get_char
-		if pause:
-			get_char('Press any key to continue: ')
-			msg('')
+		return out
 
+	def parse_tx_file(self,infile,desc):
 
-def parse_tx_file(tx_data,infile):
+		self.parse_tx_data(get_lines_from_file(infile,desc))
 
-	err_str,err_fmt = '','Invalid %s in transaction file'
+	def parse_tx_data(self,tx_data):
 
-	if len(tx_data) == 5:
-		metadata,tx_hex,inputs_data,outputs_data,comment = tx_data
-	elif len(tx_data) == 4:
-		metadata,tx_hex,inputs_data,outputs_data = tx_data
-		comment = ''
-	else:
-		err_str = 'number of lines'
+		err_str,err_fmt = '','Invalid %s in transaction file'
 
-	if not err_str:
-		if len(metadata.split()) != 3:
-			err_str = 'metadata'
+		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:
-			try: unhexlify(tx_hex)
-			except: err_str = 'hex data'
+			err_str = '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:
-				try: inputs_data = eval(inputs_data)
-				except: err_str = 'inputs data'
+				self.txid,send_amt,self.timestamp,blockcount = metadata.split()
+				self.send_amt = Decimal(send_amt)
+				self.blockcount = int(blockcount)
+				try: unhexlify(self.hex)
+				except: err_str = 'hex data'
 				else:
-					try: outputs_data = eval(outputs_data)
-					except: err_str = 'mmgen-to-btc address map data'
+					try: self.inputs = eval(inputs_data)
+					except: err_str = 'inputs data'
 					else:
-						if comment:
-							from mmgen.bitcoin import b58decode
-							comment = b58decode(comment)
-							if comment == False:
-								err_str = 'encoded comment (not base58)'
-							else:
-								if is_valid_tx_comment(comment):
-									comment = comment.decode('utf8')
+						try: self.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:
-									err_str = 'comment'
-
-	if err_str:
-		msg(err_fmt % err_str)
-		sys.exit(2)
-	else:
-		return metadata.split(),tx_hex,inputs_data,outputs_data,comment
-
-
-def wiftoaddr_keyconv(wif):
-	if wif[0] == '5':
-		from subprocess import check_output
-		return check_output(['keyconv', wif]).split()[1]
-	else:
-		return wiftoaddr(wif)
-
-def get_wif2addr_f():
-	if opt.no_keyconv: return wiftoaddr
-	from mmgen.addr import test_for_keyconv
-	return (wiftoaddr,wiftoaddr_keyconv)[bool(test_for_keyconv())]
-
-
-def get_tx_comment_from_file(infile):
-	s = get_data_from_file(infile,'transaction comment')
-	if is_valid_tx_comment(s):
-		return s.decode('utf8').strip()
-	else:
-		sys.exit(2)
-
-def get_tx_comment_from_user(comment=''):
-	try:
-		while True:
-			s = my_raw_input('Comment: ',insert_txt=comment.encode('utf8'))
-			if s == '': return False
-			if is_valid_tx_comment(s):
-				return s.decode('utf8')
-	except KeyboardInterrupt:
-		msg('User interrupt')
-		return False
+									if is_valid_tx_comment(comment):
+										self.comment = comment.decode('utf8')
+									else:
+										err_str = 'comment'
 
-def make_tx_data(metadata_fmt, tx_hex, inputs_data, b2m_map, comment):
-	from mmgen.bitcoin import b58encode
-	s = (b58encode(comment.encode('utf8')),) if comment else ()
-	lines = (metadata_fmt, tx_hex, repr(inputs_data), repr(b2m_map)) + s
-	return '\n'.join(lines)+'\n'
+		if err_str:
+			msg(err_fmt % err_str)
+			sys.exit(2)
 
-def mmaddr2btcaddr_addrdata(mmaddr,addr_data,source=''):
-	seed_id,idx = mmaddr.split(':')
-	if seed_id in addr_data:
-		if idx in addr_data[seed_id]:
-			vmsg('%s -> %s%s' % (mmaddr,addr_data[seed_id][idx][0],
-				' (from %s)' % source if source else ''))
-			return addr_data[seed_id][idx]
 
-	return '',''

+ 29 - 4
mmgen/util.py

@@ -176,12 +176,25 @@ def split_into_cols(col_wid,s):
 def capfirst(s):
 	return s if len(s) == 0 else s[0].upper() + s[1:]
 
-def make_timestamp():
-	tv = time.gmtime(time.time())[:6]
+def decode_timestamp(s):
+# 	with open('/etc/timezone') as f:
+# 		tz_save = f.read().rstrip()
+	os.environ['TZ'] = 'UTC'
+	ts = time.strptime(s,'%Y%m%d_%H%M%S')
+	t = time.mktime(ts)
+# 	os.environ['TZ'] = tz_save
+	return int(t)
+
+def make_timestamp(secs=None):
+	t = int(secs) if secs else time.time()
+	tv = time.gmtime(t)[:6]
 	return '{:04d}{:02d}{:02d}_{:02d}{:02d}{:02d}'.format(*tv)
-def make_timestr():
-	tv = time.gmtime(time.time())[:6]
+
+def make_timestr(secs=None):
+	t = int(secs) if secs else time.time()
+	tv = time.gmtime(t)[:6]
 	return '{:04d}/{:02d}/{:02d} {:02d}:{:02d}:{:02d}'.format(*tv)
+
 def secs_to_hms(secs):
 	return '{:02d}:{:02d}:{:02d}'.format(secs/3600, (secs/60) % 60, secs % 60)
 
@@ -791,3 +804,15 @@ def bitcoin_connection():
 	import mmgen.rpc
 	return mmgen.rpc.BitcoinRPCConnection(
 				host,port,cfg[user],cfg[passwd],auth_cookie=auth_cookie)
+
+def pp_format(d):
+	import pprint
+	return pprint.PrettyPrinter(indent=4).pformat(d)
+
+def pp_die(d):
+	import pprint
+	die(1,pprint.PrettyPrinter(indent=4).pformat(d))
+
+def pp_msg(d):
+	import pprint
+	msg(pprint.PrettyPrinter(indent=4).pformat(d))

+ 116 - 0
scripts/tx-old2new.py

@@ -0,0 +1,116 @@
+#!/usr/bin/env python
+
+import sys,os
+repo_root = os.path.split(os.path.abspath(os.path.dirname(sys.argv[0])))[0]
+sys.path = [repo_root] + sys.path
+
+from mmgen.common import *
+
+from mmgen.tool import *
+from mmgen.tx import *
+from mmgen.bitcoin import *
+from mmgen.seed import *
+from mmgen.term import do_pager
+
+help_data = {
+	'desc':    "Convert MMGen transaction file from old format to new format",
+	'usage':   "<tx file>",
+	'options': """
+-h, --help    Print this help message
+"""
+}
+
+import mmgen.opts
+cmd_args = opts.init(help_data)
+
+if len(cmd_args) != 1: opts.usage()
+
+def parse_tx_file(infile):
+
+	err_str,err_fmt = '','Invalid %s in transaction file'
+	tx_data = get_lines_from_file(infile)
+
+	if len(tx_data) == 5:
+		metadata,tx_hex,inputs_data,outputs_data,comment = tx_data
+	elif len(tx_data) == 4:
+		metadata,tx_hex,inputs_data,outputs_data = tx_data
+		comment = ''
+	else:
+		err_str = 'number of lines'
+
+	if not err_str:
+		if len(metadata.split()) != 3:
+			err_str = 'metadata'
+		else:
+			try: unhexlify(tx_hex)
+			except: err_str = 'hex data'
+			else:
+				try: inputs_data = eval(inputs_data)
+				except: err_str = 'inputs data'
+				else:
+					try: outputs_data = eval(outputs_data)
+					except: err_str = '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:
+								if is_valid_tx_comment(comment):
+									comment = comment.decode('utf8')
+								else:
+									err_str = 'comment'
+
+	if err_str:
+		msg(err_fmt % err_str)
+		sys.exit(2)
+	else:
+		return metadata.split(),tx_hex,inputs_data,outputs_data,comment
+
+def find_block_by_time(c,timestamp):
+	secs = decode_timestamp(timestamp)
+	block_num = c.getblockcount()
+#	print 'secs:',secs, 'last block:',last_block
+	top,bot = block_num,0
+	m = 'Searching for block'
+	msg_r(m)
+	for i in range(40):
+		msg_r('.')
+		bhash = c.getblockhash(block_num)
+		block = c.getblock(bhash)
+#		print 'block_num:',block_num, 'mediantime:',block['mediantime'], 'target:',secs
+		cur_secs = block['mediantime']
+		if cur_secs > secs:
+			top = block_num
+		else:
+			bot = block_num
+		block_num = (top + bot) / 2
+		if top - bot < 2:
+			msg('\nFound: %s ' % block_num)
+			break
+
+	return block_num
+
+tx = MMGenTX()
+
+[tx.txid,send_amt,tx.timestamp],tx.hex,inputs,b2m_map,tx.comment = parse_tx_file(cmd_args[0])
+tx.send_amt = Decimal(send_amt)
+
+c = bitcoin_connection()
+
+tx.blockcount = find_block_by_time(c,tx.timestamp)
+
+dec_tx = c.decoderawtransaction(tx.hex)
+
+tx.copy_inputs(inputs)
+tx.outputs = dict([(i['scriptPubKey']['addresses'][0],(i['value'],)) for i in dec_tx['vout']])
+for k in tx.outputs:
+	if k in b2m_map:
+		tx.outputs[k] += b2m_map[k]
+
+#tx.view_with_prompt('View decoded transaction?')
+#tx.format()
+#print(str(tx))
+# do_pager('OLD:\n' + pp_format(inputs) + '\nNEW:\n' + pp_format(tx.inputs))
+tx.write_to_file(ask_write=False)

+ 1 - 1
test/tooltest.py

@@ -103,7 +103,7 @@ cfg = {
 	'tmpdir':        'test/tmp10',
 	'tmpdir_num':    10,
 	'refdir':        'test/ref',
-	'txfile':        'tx_FFB367[1.234].raw',
+	'txfile':        'FFB367[1.234].rawtx',
 	'addrfile':      '98831F3A[1,31-33,500-501,1010-1011].addrs',
 	'addrfile_chk':  '6FEF 6FB9 7B13 5D91 854A 0BD3',
 }