Browse Source

minor fixes and cleanups

MMGen 5 years ago
parent
commit
23ae73d908

+ 3 - 2
mmgen/addr.py

@@ -556,7 +556,7 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file
 					pop_list.append(n)
 		for n in reversed(pop_list): self.data.pop(n)
 		if pop_list:
-			vmsg(self.msgs['removed_dup_keys'].format(len(pop_list),suf(removed,'s')))
+			vmsg(self.msgs['removed_dup_keys'].format(len(pop_list),suf(removed)))
 
 	def add_wifs(self,key_list):
 		if not key_list: return
@@ -900,7 +900,8 @@ re-import your addresses.
 
 	def __init__(self,source=None):
 		self.al_ids = {}
-		if source == 'tw': self.add_tw_data()
+		if source == 'tw':
+			self.add_tw_data()
 
 	def seed_ids(self):
 		return list(self.al_ids.keys())

+ 0 - 1
mmgen/altcoin.py

@@ -431,7 +431,6 @@ class CoinInfo(object):
 				line[n] = re.sub(r"'0x(....)'",r'0x\1',line[n])
 				line[n] = re.sub(r' ',r'',line[n]) + ('',',')[n != len(line)-1]
 
-			from mmgen.util import pmsg,pdie
 			if trust != -1:
 				if sym in tt:
 					src = tt[sym]

+ 16 - 15
mmgen/altcoins/eth/contract.py

@@ -26,7 +26,7 @@ from . import rlp
 from mmgen.globalvars import g
 from mmgen.common import *
 from mmgen.obj import MMGenObject,CoinAddr,TokenAddr,CoinTxID,ETHAmt
-from mmgen.util import msg,pmsg
+from mmgen.util import msg
 
 try:
 	assert not g.use_internal_keccak_module
@@ -60,17 +60,16 @@ class Token(MMGenObject): # ERC20
 	def do_call(self,method_sig,method_args='',toUnit=False):
 		data = create_method_id(method_sig) + method_args
 		if g.debug:
-			msg('{}:  {}'.format(method_sig,'\n  '.join(parse_abi(data))))
+			msg('ETH_CALL {}:  {}'.format(method_sig,'\n  '.join(parse_abi(data))))
 		ret = g.rpch.eth_call({ 'to': '0x'+self.addr, 'data': '0x'+data })
 		return int(ret,16) * self.base_unit if toUnit else ret
 
 	def balance(self,acct_addr):
-		return self.do_call('balanceOf(address)',acct_addr.rjust(64,'0'),toUnit=True)
+		return ETHAmt(self.do_call('balanceOf(address)',acct_addr.rjust(64,'0'),toUnit=True))
 
 	def strip(self,s):
 		return ''.join([chr(b) for b in s if 32 <= b <= 127]).strip()
 
-	def total_supply(self): return self.do_call('totalSupply()',toUnit=True)
 	def decimals(self):
 			ret = self.do_call('decimals()')
 			try:
@@ -79,8 +78,15 @@ class Token(MMGenObject): # ERC20
 			except:
 				"RPC call to decimals() failed (returned '{}')".format(ret)
 			return int(b,16) if b else None
-	def name(self):         return self.strip(bytes.fromhex(self.do_call('name()')[2:]))
-	def symbol(self):       return self.strip(bytes.fromhex(self.do_call('symbol()')[2:]))
+
+	def name(self):
+		return self.strip(bytes.fromhex(self.do_call('name()')[2:]))
+
+	def symbol(self):
+		return self.strip(bytes.fromhex(self.do_call('symbol()')[2:]))
+
+	def total_supply(self):
+		return self.do_call('totalSupply()',toUnit=True)
 
 	def info(self):
 		fs = '{:15}{}\n' * 5
@@ -108,8 +114,8 @@ class Token(MMGenObject): # ERC20
 				'startgas': start_gas.toWei(),
 				'gasprice': gasPrice.toWei(),
 				'value':    0,
-				'nonce':   nonce,
-				'data':    bytes.fromhex(data) }
+				'nonce':    nonce,
+				'data':     bytes.fromhex(data) }
 
 	def txsign(self,tx_in,key,from_addr,chain_id=None):
 
@@ -125,8 +131,9 @@ class Token(MMGenObject): # ERC20
 			m = "Sender address '{}' does not match address of key '{}'!"
 			die(3,m.format(from_addr,tx.sender.hex()))
 		if g.debug:
-			msg('{}'.format('\n  '.join(parse_abi(data))))
+			msg('TOKEN DATA:')
 			pmsg(tx.to_dict())
+			msg('PARSED ABI DATA:\n  {}'.format('\n  '.join(parse_abi(tx.data.hex()))))
 		return hex_tx,coin_txid
 
 # The following are used for token deployment only:
@@ -145,9 +152,3 @@ class Token(MMGenObject): # ERC20
 								from_addr2=from_addr2)
 		(hex_tx,coin_txid) = self.txsign(tx_in,key,from_addr)
 		return self.txsend(hex_tx)
-
-	def transfer_from(self,from_addr,to_addr,amt,key,start_gas,gasPrice):
-		raise NotImplementedError('method not implemented')
-		return self.transfer(   from_addr,to_addr,amt,key,start_gas,gasPrice,
-								method_sig='transferFrom(address,address,uint256)',
-								from_addr2=from_addr)

+ 2 - 2
mmgen/altcoins/eth/tw.py

@@ -233,11 +233,11 @@ class EthereumTwAddrList(TwAddrList):
 		rpc_init()
 		if g.token: self.token = Token(g.token)
 
-		tw = TrackingWallet().mmid_ordered_dict()
+		tw_dict = TrackingWallet().mmid_ordered_dict()
 		self.total = g.proto.coin_amt('0')
 
 		from mmgen.obj import CoinAddr
-		for mmid,d in list(tw.items()):
+		for mmid,d in list(tw_dict.items()):
 #			if d['confirmations'] < minconf: continue # cannot get confirmations for eth account
 			label = TwLabel(mmid+' '+d['comment'],on_fail='raise')
 			if usr_addr_list and (label.mmid not in usr_addr_list): continue

+ 11 - 11
mmgen/altcoins/eth/tx.py

@@ -44,7 +44,7 @@ class EthereumMMGenTX(MMGenTX):
 	usr_rel_fee = None # not in MMGenTX
 	disable_fee_check = False
 	txobj  = None # ""
-	data = HexStr('')
+	usr_contract_data = HexStr('')
 
 	def __init__(self,*args,**kwargs):
 		super(EthereumMMGenTX,self).__init__(*args,**kwargs)
@@ -53,7 +53,7 @@ class EthereumMMGenTX(MMGenTX):
 		if hasattr(opt,'contract_data') and opt.contract_data:
 			m = "'--contract-data' option may not be used with token transaction"
 			assert not 'Token' in type(self).__name__, m
-			self.data = HexStr(open(opt.contract_data).read().strip())
+			self.usr_contract_data = HexStr(open(opt.contract_data).read().strip())
 			self.disable_fee_check = True
 
 	@classmethod
@@ -120,7 +120,7 @@ class EthereumMMGenTX(MMGenTX):
 					'chainId':  Int(d['chainId']),
 					'data':     HexStr(d['data']) }
 		self.tx_gas = o['startGas'] # approximate, but better than nothing
-		self.data = o['data']
+		self.usr_contract_data = o['data']
 		if o['data'] and not o['to']: self.disable_fee_check = True
 		self.fee = self.fee_rel2abs(o['gasPrice'].toWei())
 		self.txobj = o
@@ -139,7 +139,7 @@ class EthereumMMGenTX(MMGenTX):
 			'startGas': self.start_gas,
 			'nonce': self.get_nonce(),
 			'chainId': Int(g.rpch.request(chain_id_method),16),
-			'data':  self.data,
+			'data':  self.usr_contract_data,
 		}
 
 	# Instead of serializing tx data as with BTC, just create a JSON dump.
@@ -147,7 +147,7 @@ class EthereumMMGenTX(MMGenTX):
 	# thus removing an attack vector
 	def create_raw(self):
 		assert len(self.inputs) == 1,'Transaction has more than one input!'
-		o_ok = (0,1) if self.data else (1,)
+		o_ok = (0,1) if self.usr_contract_data else (1,)
 		o_num = len(self.outputs)
 		assert o_num in o_ok,'Transaction has invalid number of outputs!'.format(o_num)
 		self.make_txobj()
@@ -166,7 +166,7 @@ class EthereumMMGenTX(MMGenTX):
 
 	def process_cmd_args(self,cmd_args,ad_f,ad_w):
 		lc = len(cmd_args)
-		if lc == 0 and self.data and not 'Token' in type(self).__name__: return
+		if lc == 0 and self.usr_contract_data and not 'Token' in type(self).__name__: return
 		if lc != 1:
 			fs = '{} output{} specified, but Ethereum transactions must have exactly one'
 			die(1,fs.format(lc,suf(lc)))
@@ -264,7 +264,7 @@ class EthereumMMGenTX(MMGenTX):
 
 	def format_view_abs_fee(self):
 		fee = self.fee_rel2abs(self.txobj['gasPrice'].toWei())
-		note = ' (max)' if self.data else ''
+		note = ' (max)' if self.usr_contract_data else ''
 		return fee.hl() + note
 
 	def format_view_rel_fee(self,terse): return ''
@@ -337,7 +337,7 @@ class EthereumMMGenTX(MMGenTX):
 
 		confs = self.is_in_wallet()
 		if confs is not False:
-			if self.data:
+			if self.usr_contract_data:
 				exec_status = type(self).get_exec_status(self.coin_txid)
 				if exec_status == 0:
 					msg('Contract failed to execute!')
@@ -451,12 +451,12 @@ class EthereumTokenMMGenTX(EthereumMMGenTX):
 			o['token_addr'] = TokenAddr(d['token_addr'])
 			o['decimals']   = Int(d['decimals'])
 			t = Token(o['token_addr'],o['decimals'])
-			self.data = o['data'] = t.create_data(o['to'],o['amt'])
+			self.usr_contract_data = o['data'] = t.create_data(o['to'],o['amt'])
 
 	def format_view_body(self,*args,**kwargs):
-		if self.data:
+		if self.usr_contract_data:
 			from .contract import Token
-			self.txobj['token_to'] = Token.transferdata2sendaddr(self.data)
+			self.txobj['token_to'] = Token.transferdata2sendaddr(self.usr_contract_data)
 		return 'Token:     {d} {c}\n{r}'.format(
 			d=self.txobj['token_addr'].hl(),
 			c=blue('(' + g.dcoin + ')'),

+ 5 - 0
mmgen/exception.py

@@ -24,10 +24,15 @@ mmgen.exception: Exception classes for the MMGen suite
 class UserNonConfirmation(Exception):     mmcode = 1
 class BadAgeFormat(Exception):            mmcode = 1
 class BadFilename(Exception):             mmcode = 1
+class SocketError(Exception):             mmcode = 1
+class UserAddressNotInWallet(Exception):  mmcode = 1
 
 # 2: yellow hl, message only
+class InvalidTokenAddress(Exception):     mmcode = 2
 class UnrecognizedTokenSymbol(Exception): mmcode = 2
 class TokenNotInBlockchain(Exception):    mmcode = 2
+class TokenNotInWallet(Exception):        mmcode = 2
+class BadTwComment(Exception):            mmcode = 2
 
 # 3: yellow hl, 'MMGen Error' + exception + message
 class RPCFailure(Exception):              mmcode = 3

+ 1 - 0
mmgen/main.py

@@ -39,6 +39,7 @@ def launch(mod):
 		__import__('mmgen.main_' + mod)
 	except KeyboardInterrupt:
 		sys.stderr.write('\nUser interrupt\n')
+		sys.exit(1) # must exit normally so exit handlers will be called
 	except EOFError:
 		sys.stderr.write('\nEnd of file\n')
 	except Exception as e:

+ 2 - 4
mmgen/main_autosign.py

@@ -170,16 +170,14 @@ def do_umount():
 
 def sign_tx_file(txfile,signed_txs):
 	try:
-		g.testnet = False
-		g.coin = 'BTC'
+		init_coin('BTC',testnet=False)
 		tmp_tx = mmgen.tx.MMGenTX(txfile,metadata_only=True)
 		init_coin(tmp_tx.coin)
 
 		if tmp_tx.chain != 'mainnet':
 			if tmp_tx.chain == 'testnet' or (
 				hasattr(g.proto,'chain_name') and tmp_tx.chain != g.proto.chain_name):
-				g.testnet = True
-				init_coin(tmp_tx.coin)
+				init_coin(tmp_tx.coin,testnet=True)
 
 		if hasattr(g.proto,'chain_name'):
 			m = 'Chains do not match! tx file: {}, proto: {}'

+ 1 - 1
mmgen/main_wallet.py

@@ -157,7 +157,7 @@ if invoked_as in ('conv','passchg','subgen'):
 	gmsg('Processing output wallet')
 
 if invoked_as == 'subgen':
-	ss_out = SeedSource(seed=ss_in.seed.subseed(ss_idx,print_msg=True).data)
+	ss_out = SeedSource(seed_bin=ss_in.seed.subseed(ss_idx,print_msg=True).data)
 else:
 	ss_out = SeedSource(ss=ss_in,passchg=invoked_as=='passchg')
 

+ 2 - 1
mmgen/obj.py

@@ -491,7 +491,8 @@ class BTCAmt(Decimal,Hilite,InitErrors):
 
 	def __str__(self,color=False): # format simply, no exponential notation
 		return self.colorize(
-			str(int(self)) if int(self) == self else self.normalize().__format__('f'),
+				str(int(self)) if int(self) == self else
+				self.normalize().__format__('f'),
 			color=color)
 
 	def __repr__(self):

+ 4 - 2
mmgen/opts.py

@@ -348,13 +348,15 @@ def init(opts_data,add_opts=[],opt_filter=None,parse_only=False):
 
 def opt_is_tx_fee(val,desc):
 	from mmgen.tx import MMGenTX
-	ret = MMGenTX().process_fee_spec(val,224,on_fail='return')
+	tx = MMGenTX()
+	# TODO: size is just a guess; do this check after parsing tx file
+	ret = tx.process_fee_spec(val,224,on_fail='return')
 	# Non-standard startgas: disable fee checking
 	if hasattr(opt,'contract_data') and opt.contract_data: ret = None
 	if hasattr(opt,'tx_gas') and opt.tx_gas:               ret = None
 	if ret == False:
 		msg("'{}': invalid {}\n(not a {} amount or {} specification)".format(
-				val,desc,g.coin.upper(),MMGenTX().rel_fee_desc))
+				val,desc,g.coin.upper(),tx.rel_fee_desc))
 	elif ret != None and ret > g.proto.max_tx_fee:
 		msg("'{}': invalid {}\n({} > max_tx_fee ({} {}))".format(
 				val,desc,ret.fmt(fs='1.1'),g.proto.max_tx_fee,g.coin.upper()))

+ 1 - 2
mmgen/protocol.py

@@ -21,7 +21,7 @@ protocol.py: Coin protocol functions, classes and methods
 """
 
 import sys,os,hashlib
-from mmgen.util import msg,pmsg,ymsg,Msg,pdie,ydie
+from mmgen.util import msg,ymsg,Msg,ydie
 from mmgen.obj import MMGenObject,BTCAmt,LTCAmt,BCHAmt,B2XAmt,ETHAmt
 from mmgen.globalvars import g
 import mmgen.bech32 as bech32
@@ -520,7 +520,6 @@ def make_init_genonly_altcoins_str(data):
 		if proto+'Protocol' in globals(): continue
 		if coin.lower() in CoinProtocol.coins: continue
 		out += fs.format(coin.lower(),proto,('None',proto+'TestnetProtocol')[coin in tn_coins])
-#	print out
 	return out
 
 def init_coin(coin,testnet=None):

+ 1 - 1
mmgen/regtest.py

@@ -333,4 +333,4 @@ def generate(blocks=1,silent=False):
 	if not out or len(literal_eval(out)) != blocks:
 		rdie(1,'Error generating blocks')
 	p.wait()
-	gmsg('Mined {} block{}'.format(blocks,suf(blocks,'s')))
+	gmsg('Mined {} block{}'.format(blocks,suf(blocks)))

+ 2 - 2
mmgen/rpc.py

@@ -28,7 +28,7 @@ from decimal import Decimal
 def dmsg_rpc(s):
 	if g.debug_rpc: msg(s)
 
-class CoinDaemonRPCConnection(object):
+class CoinDaemonRPCConnection(MMGenObject):
 
 	auth = True
 	db_fs = '    host [{h}] port [{p}] user [{u}] passwd [{pw}] auth_cookie [{c}]\n'
@@ -42,7 +42,7 @@ class CoinDaemonRPCConnection(object):
 		try:
 			socket.create_connection((host,port),timeout=3).close()
 		except:
-			die(1,'Unable to connect to {}:{}'.format(host,port))
+			raise SocketError('Unable to connect to {}:{}'.format(host,port))
 
 		if not self.auth:
 			pass

+ 7 - 3
mmgen/seed.py

@@ -72,6 +72,10 @@ class SeedBase(MMGenObject):
 	def hexdata(self):
 		return self.data.hex()
 
+	@property
+	def fn_stem(self):
+		return self.sid
+
 class SubSeedList(MMGenObject):
 	have_short = True
 	nonce_start = 0
@@ -612,7 +616,7 @@ class SeedSourceUnenc(SeedSource):
 	def _filename(self):
 		s = self.seed
 		return '{}[{}]{x}.{}'.format(
-			s.sid,
+			s.fn_stem,
 			s.bitlen,
 			self.ext,
 			x='-α' if g.debug_utf8 else '')
@@ -1121,7 +1125,7 @@ class Wallet (SeedSourceEnc):
 		s = self.seed
 		d = self.ssdata
 		return '{}-{}[{},{}]{x}.{}'.format(
-				s.sid,
+				s.fn_stem,
 				d.key_id,
 				s.bitlen,
 				d.hash_preset,
@@ -1248,7 +1252,7 @@ to exit and re-run the program with the '--old-incog-fmt' option.
 		s = self.seed
 		d = self.ssdata
 		return '{}-{}-{}[{},{}]{x}.{}'.format(
-				s.sid,
+				s.fn_stem,
 				d.key_id,
 				d.iv_id,
 				s.bitlen,

+ 15 - 13
mmgen/tool.py

@@ -111,7 +111,7 @@ def _process_args(cmd,cmd_args):
 	if flag != 'VAR_ARGS':
 		if len(cmd_args) < len(c_args):
 			m1 = 'Command requires exactly {} non-keyword argument{}'
-			msg(m1.format(len(c_args),suf(c_args,'s')))
+			msg(m1.format(len(c_args),suf(c_args)))
 			_usage(cmd)
 
 		u_args = cmd_args[:len(c_args)]
@@ -459,25 +459,25 @@ class MMGenToolCmdMnemonic(MMGenToolCmdBase):
 		IMPORTANT NOTE: Though MMGen mnemonics use the Electrum wordlist, they're
 		computed using a different algorithm and are NOT Electrum-compatible!
 	"""
-	def _do_random_mn(self,nbytes:int,wordlist_id:str):
+	def _do_random_mn(self,nbytes:int,wordlist:str):
 		assert nbytes in (16,24,32), 'nbytes must be 16, 24 or 32'
 		hexrand = get_random(nbytes).hex()
 		Vmsg('Seed: {}'.format(hexrand))
-		return self.hex2mn(hexrand,wordlist_id=wordlist_id)
+		return self.hex2mn(hexrand,wordlist=wordlist)
 
 	def mn_rand128(self,wordlist=dfl_wl_id):
-		"generate random 128-bit mnemonic"
+		"generate random 128-bit mnemonic seed phrase"
 		return self._do_random_mn(16,wordlist)
 
 	def mn_rand192(self,wordlist=dfl_wl_id):
-		"generate random 192-bit mnemonic"
+		"generate random 192-bit mnemonic seed phrase"
 		return self._do_random_mn(24,wordlist)
 
 	def mn_rand256(self,wordlist=dfl_wl_id):
-		"generate random 256-bit mnemonic"
+		"generate random 256-bit mnemonic seed phrase"
 		return self._do_random_mn(32,wordlist)
 
-	def hex2mn(self,hexstr:'sstr',wordlist_id=dfl_wl_id):
+	def hex2mn(self,hexstr:'sstr',wordlist=dfl_wl_id):
 		"convert a 16, 24 or 32-byte hexadecimal number to a mnemonic"
 		opt.out_fmt = 'words'
 		from mmgen.seed import SeedSource
@@ -553,7 +553,9 @@ class MMGenToolCmdFile(MMGenToolCmdBase):
 		flist.sort_by_age(key=file_sort) # in-place sort
 
 		sep = '—'*77+'\n'
-		return sep.join([MMGenTX(fn).format_view(terse=terse,sort=tx_sort) for fn in flist.names()]).rstrip()
+		return sep.join(
+			[MMGenTX(fn).format_view(terse=terse,sort=tx_sort) for fn in flist.names()]
+		).rstrip()
 
 class MMGenToolCmdFileCrypt(MMGenToolCmdBase):
 	"""
@@ -787,11 +789,11 @@ class MMGenToolCmdRPC(MMGenToolCmdBase):
 		"view tracking wallet"
 		rpc_init()
 		from mmgen.tw import TwUnspentOutputs
-		tw = TwUnspentOutputs(minconf=minconf)
-		tw.do_sort(sort,reverse=reverse)
-		tw.age_fmt = age_fmt
-		tw.show_mmid = show_mmid
-		return tw.format_for_printing(color=True) if wide else tw.format_for_display()
+		twuo = TwUnspentOutputs(minconf=minconf)
+		twuo.do_sort(sort,reverse=reverse)
+		twuo.age_fmt = age_fmt
+		twuo.show_mmid = show_mmid
+		return twuo.format_for_printing(color=True) if wide else twuo.format_for_display()
 
 	def add_label(self,mmgen_or_coin_addr:str,label:str):
 		"add descriptive label for address in tracking wallet"

+ 14 - 6
mmgen/tw.py

@@ -108,6 +108,16 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 		return sum(i.amt for i in self.unspent)
 
 	def get_unspent_rpc(self):
+		# bitcoin-cli help listunspent:
+		# Arguments:
+		# 1. minconf        (numeric, optional, default=1) The minimum confirmations to filter
+		# 2. maxconf        (numeric, optional, default=9999999) The maximum confirmations to filter
+		# 3. addresses      (json array, optional, default=empty array) A json array of bitcoin addresses
+		# 4. include_unsafe (boolean, optional, default=true) Include outputs that are not safe to spend
+		# 5. query_options  (json object, optional) JSON with query options
+
+		# for now, self.addrs is just an empty list for Bitcoin and friends
+		add_args = (9999999,self.addrs) if self.addrs else ()
 		return g.rpch.listunspent(self.minconf)
 
 	def get_unspent_data(self):
@@ -115,8 +125,6 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 			us_rpc = eval(get_data_from_file(g.bogus_wallet_data)) # testing, so ok
 		else:
 			us_rpc = self.get_unspent_rpc()
-#		write_data_to_file('bogus_unspent.json', repr(us), 'bogus unspent data')
-#		sys.exit(0)
 
 		if not us_rpc: die(0,self.wmsg['no_spendable_outputs'])
 		confs_per_day = 60*60*24 // g.proto.secs_per_block
@@ -288,7 +296,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 		return self.fmt_print
 
 	def display_total(self):
-		fs = '\nTotal unspent: {} {} ({} outputs)'
+		fs = '\nTotal unspent: {} {} ({} output%s)' % suf(self.unspent)
 		msg(fs.format(self.total.hl(),g.dcoin,len(self.unspent)))
 
 	def get_idx_from_user(self,get_label=False):
@@ -469,7 +477,7 @@ class TwAddrList(MMGenDict):
 		if age_fmt not in age_fmts:
 			raise BadAgeFormat("'{}': invalid age format (must be one of {!r})".format(age_fmt,age_fmts))
 		out = ['Chain: '+green(g.chain.upper())] if g.chain != 'mainnet' else []
-		fs = '{{mid}}{} {{cmt}} {{amt}}{}'.format(('',' {addr}')[showbtcaddrs],('',' {age}')[show_age])
+		fs = '{mid}' + ('',' {addr}')[showbtcaddrs] + ' {cmt} {amt}' + ('',' {age}')[show_age]
 		mmaddrs = [k for k in self.keys() if k.type == 'mmgen']
 		max_mmid_len = max(len(k) for k in mmaddrs) + 2 if mmaddrs else 10
 		max_cmt_width = max(max(v['lbl'].comment.screen_width for v in self.values()),7)
@@ -483,7 +491,7 @@ class TwAddrList(MMGenDict):
 				cmt=TwComment.fmtc('COMMENT',width=max_cmt_width+1),
 				amt='BALANCE'.ljust(max_fp_len+4),
 				age=('CONFS','DAYS')[age_fmt=='days'],
-				)]
+				).rstrip()]
 
 		def sort_algo(j):
 			if sort and 'age' in sort:
@@ -515,7 +523,7 @@ class TwAddrList(MMGenDict):
 				cmt=e['lbl'].comment.fmt(width=max_cmt_width,color=True,nullrepl='-'),
 				amt=e['amt'].fmt('4.{}'.format(max(max_fp_len,3)),color=True),
 				age=mmid.confs // (1,confs_per_day)[age_fmt=='days'] if hasattr(mmid,'confs') and mmid.confs != None else '-'
-				))
+				).rstrip())
 
 		return '\n'.join(out + ['\nTOTAL: {} {}'.format(self.total.hl(color=True),g.dcoin)])
 

+ 5 - 4
mmgen/tx.py

@@ -735,7 +735,7 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
 
 		self.check_pubkey_scripts()
 
-		qmsg('Passing {} key{} to {}'.format(len(keys),suf(keys,'s'),g.proto.daemon_name))
+		qmsg('Passing {} key{} to {}'.format(len(keys),suf(keys),g.proto.daemon_name))
 
 		if self.has_segwit_inputs():
 			from mmgen.addr import KeyGenerator,AddrGenerator
@@ -781,12 +781,12 @@ Selected non-{pnm} inputs: {{}}""".strip().format(pnm=g.proj_name,pnl=g.proj_nam
 			msg('OK')
 			return True
 		except Exception as e:
-			if g.traceback:
-				import traceback
-				ymsg('\n'+''.join(traceback.format_exception(*sys.exc_info())))
 			try: m = '{}'.format(e.args[0])
 			except: m = repr(e.args[0])
 			msg('\n'+yellow(m))
+			if g.traceback:
+				import traceback
+				ymsg('\n'+''.join(traceback.format_exception(*sys.exc_info())))
 			return False
 
 	def mark_raw(self):
@@ -1548,6 +1548,7 @@ class MMGenBumpTX(MMGenTX):
 			return False
 		return ret
 
+# NOT MAINTAINED
 class MMGenSplitTX(MMGenTX):
 
 	def get_outputs_from_cmdline(self,mmid): # TODO: check that addr is empty

+ 3 - 3
mmgen/txsign.py

@@ -70,7 +70,7 @@ def get_seed_for_seed_id(sid,infiles,saved_seeds):
 def generate_kals_for_mmgen_addrs(need_keys,infiles,saved_seeds):
 	mmids = [e.mmid for e in need_keys]
 	sids = {i.sid for i in mmids}
-	vmsg('Need seed{}: {}'.format(suf(sids,'s'),' '.join(sids)))
+	vmsg('Need seed{}: {}'.format(suf(sids),' '.join(sids)))
 	d = MMGenList()
 	from mmgen.addr import KeyAddrList
 	for sid in sids:
@@ -104,7 +104,7 @@ def add_keys(tx,src,infiles=None,saved_seeds=None,keyaddr_list=None):
 					else:
 						die(3,wmsg['mapping_error'].format(m1,mmid,f.addr,'tx file:',e.mmid,e.addr))
 	if new_keys:
-		vmsg('Added {} wif key{} from {}'.format(len(new_keys),suf(new_keys,'s'),desc))
+		vmsg('Added {} wif key{} from {}'.format(len(new_keys),suf(new_keys),desc))
 	return new_keys
 
 def _pop_and_return(args,cmplist): # strips found args
@@ -167,6 +167,6 @@ def txsign(tx,seed_files,kl,kal,tx_num_str=''):
 
 	extra_sids = set(saved_seeds) - tx.get_input_sids() - tx.get_output_sids()
 	if extra_sids:
-		msg('Unused Seed ID{}: {}'.format(suf(extra_sids,'s'),' '.join(extra_sids)))
+		msg('Unused Seed ID{}: {}'.format(suf(extra_sids),' '.join(extra_sids)))
 
 	return tx.sign(tx_num_str,keys) # returns True or False

+ 2 - 2
mmgen/util.py

@@ -184,7 +184,7 @@ def dmsg(s):
 	if opt.debug: msg(s)
 
 def suf(arg,suf_type='s'):
-	suf_types = { 's':  ('s',''), 'es': ('es',''), 'y': ('ies','y') }
+	suf_types = { 's': '', 'es': '', 'ies': 'y' }
 	assert suf_type in suf_types
 	t = type(arg)
 	if t == int:
@@ -193,7 +193,7 @@ def suf(arg,suf_type='s'):
 		n = len(arg)
 	else:
 		die(2,'{}: invalid parameter for suf()'.format(arg))
-	return suf_types[suf_type][n==1]
+	return suf_types[suf_type] if n == 1 else suf_type
 
 def get_extension(f):
 	a,b = os.path.splitext(f)

+ 6 - 4
scripts/traceback_run.py

@@ -12,6 +12,7 @@ def traceback_run_init():
 
 	if 'TMUX' in os.environ: del os.environ['TMUX']
 	os.environ['MMGEN_TRACEBACK'] = '1'
+	os.environ['PYTHONPATH'] = '.'
 
 	of = 'my.err'
 	try: os.unlink(of)
@@ -28,11 +29,11 @@ def traceback_run_process_exception():
 
 	exc = l.pop()
 	if exc[:11] == 'SystemExit:': l.pop()
-	if os.getenv('MMGEN_DISABLE_COLOR'):
+	if False: # was: if os.getenv('MMGEN_DISABLE_COLOR'):
 		sys.stdout.write('{}{}'.format(''.join(l),exc))
 	else:
-		red    = lambda s: '\033[31;1m{}\033[0m'.format(s)
-		yellow = lambda s: '\033[33;1m{}\033[0m'.format(s)
+		def red(s): return '\033[31;1m{}\033[0m'.format(s)
+		def yellow(s): return '\033[33;1m{}\033[0m'.format(s)
 		sys.stdout.write('{}{}'.format(yellow(''.join(l)),red(exc)))
 
 	open(traceback_run_outfile,'w').write(''.join(l+[exc]))
@@ -50,7 +51,8 @@ except SystemExit as e:
 	sys.exit(e.code)
 except Exception as e:
 	traceback_run_process_exception()
-	sys.exit(e.mmcode if hasattr(e,'mmcode') else e.code if hasattr(e,'code') else 1)
+	retval = e.mmcode if hasattr(e,'mmcode') else e.code if hasattr(e,'code') else 1
+	sys.exit(retval)
 
 blue = lambda s: s if os.getenv('MMGEN_DISABLE_COLOR') else '\033[34;1m{}\033[0m'.format(s)
 sys.stdout.write(blue('Runtime: {:0.5f} secs\n'.format(time.time() - traceback_run_tstart)))

+ 6 - 13
test/test.py

@@ -69,6 +69,7 @@ def create_shm_dir(data_dir,trash_dir):
 
 import sys,os,time
 
+os.environ['MMGEN_TEST_SUITE'] = '1'
 repo_root = os.path.normpath(os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]),os.pardir)))
 os.chdir(repo_root)
 sys.path.__setitem__(0,repo_root)
@@ -386,7 +387,7 @@ def clean(usr_dirs=None):
 		else:
 			die(1,'{}: invalid directory number'.format(d))
 	if dirlist:
-		iqmsg(green('Cleaned tmp director{} {}'.format(suf(dirlist,'y'),' '.join(dirlist))))
+		iqmsg(green('Cleaned tmp director{} {}'.format(suf(dirlist,'ies'),' '.join(dirlist))))
 	cleandir(data_dir)
 	cleandir(trash_dir)
 	iqmsg(green("Cleaned directories '{}'".format("' '".join([data_dir,trash_dir]))))
@@ -429,8 +430,6 @@ def set_environ_for_spawned_scripts():
 	os.environ['MMGEN_NO_LICENSE'] = '1'
 	os.environ['MMGEN_MIN_URANDCHARS'] = '3'
 	os.environ['MMGEN_BOGUS_SEND'] = '1'
-	# Tell spawned programs they're running in the test suite
-	os.environ['MMGEN_TEST_SUITE'] = '1'
 
 def set_restore_term_at_exit():
 	import termios,atexit
@@ -880,23 +879,17 @@ try:
 	tr = TestSuiteRunner(data_dir,trash_dir)
 	tr.run_tests(usr_args)
 except KeyboardInterrupt:
-	die(1,'\nExiting at user request')
+	die(1,'\ntest.py exiting at user request')
 except TestSuiteException as e:
 	ydie(1,e.args[0])
 except TestSuiteFatalException as e:
 	rdie(1,e.args[0])
 except Exception:
 	if opt.traceback:
+		msg(blue('Spawned script exited with error'))
+	else:
 		import traceback
 		print(''.join(traceback.format_exception(*sys.exc_info())))
-		try:
-			os.stat('my.err')
-			t = open('my.err').readlines()
-			if t:
-				msg_r('\n'+yellow(''.join(t[:-1]))+red(t[-1]))
-		except: pass
-		die(1,blue('Test script exited with error'))
-	else:
-		raise
+		msg(blue('Test script exited with error'))
 except:
 	raise

+ 1 - 1
test/test_py_d/ts_autosign.py

@@ -155,8 +155,8 @@ class TestSuiteAutosign(TestSuiteBase):
 
 		def do_autosign(opts,mountpoint):
 			make_wallet(opts)
-
 			copy_files(mountpoint,include_bad_tx=True)
+
 			t = self.spawn('mmgen-autosign',opts+['--full-summary','wait'],extra_desc='(sign - full summary)')
 			t.expect('{} transactions signed'.format(txcount))
 			t.expect('2 transactions failed to sign')