Browse Source

Label editing in mmgen-txcreate

philemon 8 years ago
parent
commit
a6547be82c
12 changed files with 224 additions and 248 deletions
  1. 14 10
      mmgen/crypto.py
  2. 2 2
      mmgen/main_tool.py
  3. 6 14
      mmgen/main_txcreate.py
  4. 10 4
      mmgen/main_txsign.py
  5. 24 20
      mmgen/obj.py
  6. 9 7
      mmgen/rpc.py
  7. 0 11
      mmgen/seed.py
  8. 3 0
      mmgen/term.py
  9. 40 68
      mmgen/tool.py
  10. 108 34
      mmgen/tw.py
  11. 4 74
      mmgen/util.py
  12. 4 4
      test/test.py

+ 14 - 10
mmgen/crypto.py

@@ -28,8 +28,8 @@ from mmgen.term import get_char
 
 crmsg = {
 	'usr_rand_notice': """
-You've chosen to not fully trust your OS's random number generator and provide
-some additional entropy of your own.  Please type %s symbols on your keyboard.
+Since we don't fully trust our OS's random number generator, we'll provide
+some additional entropy of our own.  Please type %s symbols on your keyboard.
 Type slowly and choose your symbols carefully for maximum randomness.  Try to
 use both upper and lowercase as well as punctuation and numerals.  What you
 type will not be displayed on the screen.  Note that the timings between your
@@ -57,7 +57,6 @@ keystrokes will also be used as a source of randomness.
 def encrypt_seed(seed, key):
 	return encrypt_data(seed, key, iv=1, desc='seed')
 
-
 def decrypt_seed(enc_seed, key, seed_id, key_id):
 
 	vmsg_r('Checking key...')
@@ -91,7 +90,6 @@ def decrypt_seed(enc_seed, key, seed_id, key_id):
 
 	return dec_seed
 
-
 def encrypt_data(data, key, iv=1, desc='data', verify=True):
 
 	# 192-bit seed is 24 bytes -> not multiple of 16.  Must use MODE_CTR
@@ -117,7 +115,6 @@ def encrypt_data(data, key, iv=1, desc='data', verify=True):
 
 	return enc_data
 
-
 def decrypt_data(enc_data, key, iv=1, desc='data'):
 
 	vmsg_r('Decrypting %s with key...' % desc)
@@ -130,7 +127,6 @@ def decrypt_data(enc_data, key, iv=1, desc='data'):
 
 	return c.decrypt(enc_data)
 
-
 def scrypt_hash_passphrase(passwd, salt, hash_preset, buflen=32):
 
 	# Buflen arg is for brainwallets only, which use this function to generate
@@ -141,7 +137,6 @@ def scrypt_hash_passphrase(passwd, salt, hash_preset, buflen=32):
 	import scrypt
 	return scrypt.hash(passwd, salt, 2**N, r, p, buflen=buflen)
 
-
 def make_key(passwd,salt,hash_preset,
 		desc='encryption key',from_what='passphrase',verbose=False):
 
@@ -153,7 +148,6 @@ def make_key(passwd,salt,hash_preset,
 	dmsg('Key: %s' % hexlify(key))
 	return key
 
-
 def _get_random_data_from_user(uchars):
 
 	if opt.quiet: msg('Enter %s random symbols' % uchars)
@@ -189,7 +183,6 @@ def _get_random_data_from_user(uchars):
 
 	return key_data+''.join(fmt_time_data)
 
-
 def get_random(length):
 	from Crypto import Random
 	os_rand = Random.new().read(length)
@@ -206,6 +199,18 @@ def get_random(length):
 	else:
 		return os_rand
 
+def get_hash_preset_from_user(hp=g.hash_preset,desc='data'):
+	p = """Enter hash preset for %s,
+ or hit ENTER to accept the default value ('%s'): """ % (desc,hp)
+	while True:
+		ret = my_raw_input(p)
+		if ret:
+			if ret in g.hash_presets.keys(): return ret
+			else:
+				msg('Invalid input.  Valid choices are %s' %
+						', '.join(sorted(g.hash_presets.keys())))
+				continue
+		else: return hp
 
 # Vars for mmgen_*crypt functions only
 salt_len,sha256_len,nonce_len = 32,32,32
@@ -224,7 +229,6 @@ def mmgen_encrypt(data,desc='data',hash_preset=''):
 				int(hexlify(iv),16), desc=desc)
 	return salt+iv+enc_d
 
-
 def mmgen_decrypt(data,desc='data',hash_preset=''):
 	dstart = salt_len + g.aesctr_iv_len
 	salt,iv,enc_d = data[:salt_len],data[salt_len:dstart],data[dstart:]

+ 2 - 2
mmgen/main_tool.py

@@ -64,5 +64,5 @@ if cmd_args and cmd_args[0] == '--help':
 	sys.exit()
 
 args,kwargs = tool.process_args(g.prog_name, command, cmd_args)
-
-tool.__dict__[command](*args,**kwargs)
+ret = tool.__dict__[command](*args,**kwargs)
+sys.exit(0 if ret in (None,True) else 1) # some commands die, some return False on failure

+ 6 - 14
mmgen/main_txcreate.py

@@ -98,22 +98,14 @@ was specified.
 }
 
 def select_unspent(unspent,prompt):
-
 	while True:
 		reply = my_raw_input(prompt).strip()
-
-		if not reply: continue
-
-		selected = parse_addr_idxs(reply,sep=None)
-
-		if not selected: continue
-
-		if selected[-1] > len(unspent):
-			msg('Inputs must be less than %s' % len(unspent))
-			continue
-
-		return selected
-
+		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):
 

+ 10 - 4
mmgen/main_txsign.py

@@ -164,7 +164,14 @@ def add_keys(tx,src,infiles=None,saved_seeds=None,keyaddr_list=None):
 		vmsg('Added %s wif key%s from %s' % (len(new_keys),suf(new_keys,'k'),desc))
 	return new_keys
 
-# # function unneeded - use bitcoin-cli walletdump instead
+# # functions unneeded - use bitcoin-cli walletdump instead
+# def get_bitcoind_passphrase(prompt):
+# 	if opt.passwd_file:
+# 		pwfile_reuse_warning()
+# 		return get_data_from_file(opt.passwd_file,'passphrase').strip('\r\n')
+# 	else:
+# 		return my_raw_input(prompt, echo=opt.echo_passphrase)
+#
 # 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:
@@ -174,7 +181,7 @@ def add_keys(tx,src,infiles=None,saved_seeds=None,keyaddr_list=None):
 # 		prompt = 'Enter passphrase for bitcoind wallet: '
 # 		while True:
 # 			passwd = get_bitcoind_passphrase(prompt)
-# 			ret = c.walletpassphrase(passwd, 9999,ret_on_error=True)
+# 			ret = c.walletpassphrase(passwd, 9999,on_fail='return')
 # 			if rpc_error(ret):
 # 				if rpc_errmsg(ret,'unencrypted wallet, but walletpassphrase was called'):
 # 					msg('Wallet is unencrypted'); break
@@ -184,12 +191,11 @@ def add_keys(tx,src,infiles=None,saved_seeds=None,keyaddr_list=None):
 # 		ok = tx.sign(c,tx_num_str,keys)
 #
 # 		msg('Locking wallet')
-# 		ret = c.walletlock(ret_on_error=True)
+# 		ret = c.walletlock(on_fail='return')
 # 		if rpc_error(ret):
 # 			msg('Failed to lock wallet')
 #
 # 		return ok
-#
 
 # main(): execution begins here
 

+ 24 - 20
mmgen/obj.py

@@ -187,7 +187,7 @@ class AddrIdx(int,InitErrors):
 			assert type(num) is not float
 			me = int.__new__(cls,num)
 		except:
-			m = "'%s': value cannot be converted to addr idx" % num
+			m = "'%s': value cannot be converted to address index" % num
 		else:
 			if len(str(me)) > cls.max_digits:
 				m = "'%s': too many digits in addr idx" % num
@@ -205,9 +205,11 @@ class AddrIdxList(list,InitErrors):
 		self.arg_chk(type(self),on_fail)
 		assert fmt_str or idx_list
 		if idx_list:
-			return list.__init__(self,sorted(set(idx_list)))
+			# dies on failure
+			return list.__init__(self,sorted(set([AddrIdx(i) for i in idx_list])))
 		elif fmt_str:
-			ret,fs = [],"'%s': value cannot be converted to addr idx"
+			desc = fmt_str
+			ret,fs = [],"'%s': value cannot be converted to address index"
 			from mmgen.util import msg
 			for i in (fmt_str.split(sep)):
 				j = i.split('-')
@@ -228,7 +230,7 @@ class AddrIdxList(list,InitErrors):
 			else:
 				return list.__init__(self,sorted(set(ret))) # fell off end of loop - success
 
-		return self.init_fail(fs % err,on_fail,silent=True)
+			return self.init_fail((fs + ' list') % desc,on_fail)
 
 class Hilite(object):
 
@@ -238,19 +240,21 @@ class Hilite(object):
 	trunc_ok = True
 
 	@classmethod
-	def fmtc(cls,s,width=None,color=False,encl='',trunc_ok=None):
+	def fmtc(cls,s,width=None,color=False,encl='',trunc_ok=None,center=False,nullrepl=''):
 		if width == None: width = cls.width
 		if trunc_ok == None: trunc_ok = cls.trunc_ok
 		assert width > 0
+		if s == '' and nullrepl:
+			s,center = nullrepl,True
+		if center: s = s.center(width)
 		assert type(encl) is str and len(encl) in (0,2)
 		a,b = list(encl) if encl else ('','')
 		if trunc_ok and len(s) > width: s = s[:width]
 		return cls.colorize((a+s+b).ljust(width),color=color)
 
-	def fmt(self,width=None,color=False,encl='',trunc_ok=None):
-		if width == None: width = self.width
-		if trunc_ok == None: trunc_ok = self.trunc_ok
-		return self.fmtc(self,width=width,color=color,encl=encl,trunc_ok=trunc_ok)
+	def fmt(self,*args,**kwargs):
+		assert args == () # forbid invocation w/o keywords
+		return self.fmtc(self,*args,**kwargs)
 
 	@classmethod
 	def hlc(cls,s,color=True):
@@ -266,7 +270,8 @@ class Hilite(object):
 	def colorize(cls,s,color=True):
 		import mmgen.globalvars as g
 		from mmgen.util import red,blue,green,yellow,pink,cyan,gray,orange,magenta
-		return locals()[cls.color](s) if (color or cls.color_always) and g.color else s
+		k = color if type(color) is str else cls.color # hack: override color with str value
+		return locals()[k](s) if (color or cls.color_always) and g.color else s
 
 class BTCAmt(Decimal,Hilite,InitErrors):
 	color = 'yellow'
@@ -337,7 +342,6 @@ class BTCAmt(Decimal,Hilite,InitErrors):
 	def __neg__(self,other,context=None):
 		return type(self)(Decimal.__neg__(self,other,context))
 
-
 class BTCAddr(str,Hilite,InitErrors):
 	color = 'cyan'
 	width = 34
@@ -351,16 +355,15 @@ class BTCAddr(str,Hilite,InitErrors):
 			m = "'%s': value is not a Bitcoin address" % s
 		return cls.init_fail(m,on_fail)
 
-	def fmt(self,width=width,color=False):
-		return self.fmtc(self,width=width,color=color)
-
 	@classmethod
-	def fmtc(cls,s,width=width,color=False):
-		if width >= len(s):
-			s = s.ljust(width)
-		else:
-			s = s[:width-2] +  '..'
-		return cls.colorize(s,color=color)
+	def fmtc(cls,s,**kwargs):
+		# True -> 'cyan': use the str value override hack
+		if 'color' in kwargs and kwargs['color'] == True:
+			kwargs['color'] = cls.color
+		if not 'width' in kwargs: kwargs['width'] = cls.width
+		if kwargs['width'] < len(s):
+			s = s[:kwargs['width']-2] +  '..'
+		return Hilite.fmtc(s,**kwargs)
 
 class SeedID(str,Hilite,InitErrors):
 	color = 'blue'
@@ -431,6 +434,7 @@ class MMGenWalletLabel(MMGenLabel):
 
 class MMGenAddrLabel(MMGenLabel):
 	max_len = 32
+	allowed = [chr(i+32) for i in range(95)]
 	desc = 'address label'
 
 class MMGenTXLabel(MMGenLabel):

+ 9 - 7
mmgen/rpc.py

@@ -56,10 +56,10 @@ class BitcoinRPCConnection(object):
 	# 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))
+	# With on_fail='return', returns 'rpcfail',(resp_object,(die_args))
 	def request(self,cmd,*args,**kwargs):
 
-		cf = { 'timeout':g.http_timeout, 'batch':False, 'ret_on_error':False }
+		cf = { 'timeout':g.http_timeout, 'batch':False, 'on_fail':'die' }
 
 		for k in cf:
 			if k in kwargs and kwargs[k]: cf[k] = kwargs[k]
@@ -72,7 +72,7 @@ class BitcoinRPCConnection(object):
 			p = {'method':cmd,'params':args,'id':1}
 
 		def die_maybe(*args):
-			if cf['ret_on_error']:
+			if cf['on_fail'] == 'return':
 				return 'rpcfail',args
 			else:
 				die(*args[1:])
@@ -88,7 +88,10 @@ class BitcoinRPCConnection(object):
 					return (float,str)[caller.client_version>=120000](obj)
 				return json.JSONEncoder.default(self, obj)
 
-#		pp_msg(json.dumps(p,cls=MyJSONEncoder))
+# Can't do UTF-8 labels yet: httplib only ascii?
+# 		if type(p) != list and p['method'] == 'importaddress':
+# 			dump = json.dumps(p,cls=MyJSONEncoder,ensure_ascii=False)
+# 			print(dump)
 
 		try:
 			c.request('POST', '/', json.dumps(p,cls=MyJSONEncoder), {
@@ -153,7 +156,6 @@ class BitcoinRPCConnection(object):
 		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'
+	return type(ret) is tuple and ret and ret[0] == 'rpcfail'
 
-def rpc_errmsg(ret,e):
-	return (False,True)[ret[1][2].find(e) == -1]
+def rpc_errmsg(ret): return ret[1][2]

+ 0 - 11
mmgen/seed.py

@@ -53,7 +53,6 @@ class Seed(MMGenObject):
 	def get_data(self):
 		return self.data
 
-
 class SeedSource(MMGenObject):
 
 	desc = g.proj_name + ' seed source'
@@ -216,13 +215,11 @@ class SeedSource(MMGenObject):
 		}
 		write_data_to_file(self._filename(),self.fmt_data,**kwargs)
 
-
 class SeedSourceUnenc(SeedSource):
 
 	def _decrypt_retry(self): pass
 	def _encrypt(self): pass
 
-
 class SeedSourceEnc(SeedSource):
 
 	_msg = {
@@ -346,7 +343,6 @@ an empty passphrase, just hit ENTER twice.
 		d.key_id   = make_chksum_8(key)
 		d.enc_seed = encrypt_seed(self.seed.data,key)
 
-
 class Mnemonic (SeedSourceUnenc):
 
 	stdin_ok = True
@@ -466,7 +462,6 @@ class Mnemonic (SeedSourceUnenc):
 	def _filename(self):
 		return '%s[%s].%s' % (self.seed.sid,self.seed.length,self.ext)
 
-
 class SeedFile (SeedSourceUnenc):
 
 	stdin_ok = True
@@ -524,7 +519,6 @@ class SeedFile (SeedSourceUnenc):
 	def _filename(self):
 		return '%s[%s].%s' % (self.seed.sid,self.seed.length,self.ext)
 
-
 class Wallet (SeedSourceEnc):
 
 	fmt_codes = 'wallet','w'
@@ -687,7 +681,6 @@ class Wallet (SeedSourceEnc):
 				self.ext
 			)
 
-
 class Brainwallet (SeedSourceEnc):
 
 	stdin_ok = True
@@ -727,7 +720,6 @@ class Brainwallet (SeedSourceEnc):
 		qmsg('Check this value against your records')
 		return True
 
-
 class IncogWallet (SeedSourceEnc):
 
 	file_mode = 'binary'
@@ -883,7 +875,6 @@ to exit and re-run the program with the '--old-incog-fmt' option.
 		else:
 			return False
 
-
 class IncogWalletHex (IncogWallet):
 
 	file_mode = 'text'
@@ -904,7 +895,6 @@ class IncogWalletHex (IncogWallet):
 		IncogWallet._format(self)
 		self.fmt_data = pretty_hexdump(self.fmt_data)
 
-
 class IncogWalletHidden (IncogWallet):
 
 	desc = 'hidden incognito data'
@@ -931,7 +921,6 @@ harder to find, you're advised to choose a much larger file size than this.
 		'dec_chk': ', hash preset, offset %s seed length'
 	}
 
-
 	def _get_hincog_params(self,wtype):
 		p = getattr(opt,'hidden_incog_'+ wtype +'_params')
 		a,b = p.split(',')

+ 3 - 0
mmgen/term.py

@@ -23,6 +23,9 @@ term.py:  Terminal-handling routines for the MMGen suite
 import os,struct
 from mmgen.common import *
 
+CUR_SHOW = '\033[?25h'
+CUR_HIDE = '\033[?25l'
+
 def _kb_hold_protect_unix():
 
 	fd = sys.stdin.fileno()

+ 40 - 68
mmgen/tool.py

@@ -71,7 +71,6 @@ cmd_data = OrderedDict([
 	('mn_stats',     ["wordlist [str='electrum']"]),
 	('mn_printlist', ["wordlist [str='electrum']"]),
 
-
 	('listaddresses',["addrs [str='']",'minconf [int=1]','showempty [bool=False]','pager [bool=False]','showbtcaddrs [bool=False]']),
 	('getbalance',   ['minconf [int=1]']),
 	('txview',       ['<{} TX file> [str]'.format(pnm),'pager [bool=False]','terse [bool=False]']),
@@ -102,7 +101,7 @@ cmd_help = """
   wif2hex      - convert a private key from WIF to hex format
 
   Wallet/TX operations (bitcoind must be running):
-  getbalance    - like 'bitcoind getbalance' but shows confirmed/unconfirmed,
+  getbalance    - like 'bitcoin-cli getbalance' but shows confirmed/unconfirmed,
                   spendable/unspendable balances for individual {pnm} wallets
   listaddresses - list {pnm} addresses and their balances
   txview        - show raw/signed {pnm} transaction in human-readable form
@@ -229,15 +228,12 @@ def are_equal(a,b,dtype=''):
 	else:              return a == b
 
 def print_convert_results(indata,enc,dec,dtype):
-
 	error = (True,False)[are_equal(indata,dec,dtype)]
-
 	if error or opt.verbose:
 		Msg('Input:         %s' % repr(indata))
 		Msg('Encoded data:  %s' % repr(enc))
 		Msg('Recoded data:  %s' % repr(dec))
 	else: Msg(enc)
-
 	if error:
 		die(3,"Error! Recoded data doesn't match input!")
 
@@ -364,23 +360,15 @@ def str2id6(s):  Msg(make_chksum_6(''.join(s.split())))
 # List MMGen addresses and their balances:
 def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=False):
 
+	# TODO - move some or all of this code to AddrList
 	usr_addr_list = []
 	if addrs:
-		sid,idxs = split2(addrs,':')
-		if not idxs:
-			s1 = "'%s': invalid address argument\n"
-			s2 = "Address argument format: <%s Seed ID>':'<index or range>"
-			die(1, (s1+s2) % (addrs,g.proj_name))
-		if not is_mmgen_seed_id(sid):
-			die(1,'%s: invalid %s Seed ID' % (g.proj_name,sid))
-		tmp = parse_addr_idxs(idxs)
-		if not tmp: return False
-		usr_addr_list = ['%s:%s' % (sid,i) for i in tmp]
+		sid_s,idxs = split2(addrs,':')
+		sid = SeedID(sid=sid_s)
+		usr_addr_list = ['{}:{}'.format(sid,a) for a in AddrIdxList(idxs)]
 
 	c = bitcoin_connection()
-
 	addrs = {} # reusing variable name!
-	from mmgen.obj import BTCAmt
 	total = BTCAmt('0')
 	for d in c.listunspent(0):
 		mmaddr,comment = split2(d['account'])
@@ -392,7 +380,7 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
 					die(2,'duplicate BTC address ({}) for this MMGen address! ({})'.format(
 							(d['address'], addrs[key][2])))
 			else:
-				addrs[key] = [BTCAmt('0'),comment,d['address']]
+				addrs[key] = [BTCAmt('0'),MMGenAddrLabel(comment),BTCAddr(d['address'])]
 			addrs[key][0] += d['amount']
 			total += d['amount']
 
@@ -407,45 +395,47 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
 				key = mmaddr.replace(':','_')
 				if key not in addrs:
 					if showbtcaddrs: save_a.append([acct])
-					addrs[key] = [BTCAmt('0'),comment,'']
+					addrs[key] = [BTCAmt('0'),MMGenAddrLabel(comment),'']
 
 		for acct,addr in zip(save_a,c.getaddressesbyaccount(save_a,batch=True)):
 			if len(addr) != 1:
 				die(2,"Account '%s' has more or less than one BTC address!" % addr)
 			key = split2(acct[0])[0].replace(':','_')
-			addrs[key][2] = addr[0]
+			addrs[key][2] = BTCAddr(addr[0])
 
 	if not addrs:
 		die(0,('No addresses with balances!','No tracked addresses!')[showempty])
 
-	fs = '%-{}s %-{}s %-{}s %s'.format(
-		max(len(k) for k in addrs),
-		(0,36)[showbtcaddrs],
-		max(max(len(addrs[k][1]) for k in addrs) + 1,8) # pad 8 if no comments
-	)
-
-	def s_mmgen(key): # TODO
-		return '{}:{:>0{w}}'.format(w=AddrIdx.max_digits, *key.split('_'))
-
-	out = []
+	fs = ('{mid} {lbl} {amt}','{mid} {addr} {lbl} {amt}')[showbtcaddrs]
+	max_mmid_len = max(len(k) for k in addrs) or 10
+	max_lbl_len =  max(len(addrs[k][1]) for k in addrs) or 7
+	out = [fs.format(
+			mid=MMGenID.fmtc('MMGenID',width=max_mmid_len),
+			addr=BTCAddr.fmtc('ADDRESS'),
+			lbl=MMGenAddrLabel.fmtc('COMMENT',width=max_lbl_len),
+			amt='BALANCE'
+			)]
+
+	old_sid = ''
+	def s_mmgen(k): return '{:>0{w}}'.format(k,w=AddrIdx.max_digits+9) # TODO
 	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], addrs[k][0].fmt('3.0',color=1)))
-
-	o = (fs + '\n%s\nTOTAL: %s BTC') % (
-			'ADDRESS','','COMMENT',' BALANCE', '\n'.join(out), total.hl()
-		)
+		if old_sid and old_sid != k[:8]: out.append('')
+		old_sid = k[:8]
+		out.append(fs.format(
+			mid=MMGenID(k.replace('_',':')).fmt(width=max_mmid_len,color=True),
+			addr=(addrs[k][2].fmt(color=True) if showbtcaddrs else None),
+			lbl=addrs[k][1].fmt(width=max_lbl_len,color=True,nullrepl='-'),
+			amt=addrs[k][0].fmt('3.0',color=True)))
+
+	out.append('\nTOTAL: %s BTC' % total.hl(color=True))
+	o = '\n'.join(out)
 	if pager: do_pager(o)
 	else: Msg(o)
 
-
 def getbalance(minconf=1):
 
 	accts = {}
-	us = bitcoin_connection().listunspent(0)
-#	pp_die(us)
-	for d in us:
+	for d in bitcoin_connection().listunspent(0):
 		ma = split2(d['account'])[0]
 		keys = ['TOTAL']
 		if d['spendable']: keys += ['SPENDABLE']
@@ -477,30 +467,16 @@ def twview(pager=False,reverse=False,wide=False,sort='age'):
 	out = tw.format_for_printing(color=True) if wide else tw.format_for_display()
 	do_pager(out) if pager else sys.stdout.write(out)
 
-def add_label(mmaddr,label,remove=False):
-	if not is_mmgen_id(mmaddr):
-		die(1,'{a}: not a valid {pnm} address'.format(pnm=pnm,a=mmaddr))
-	MMGenAddrLabel(label)  # Exits on failure
-
-	from mmgen.addr import AddrData
-	btcaddr = AddrData(source='tw').mmaddr2btcaddr(mmaddr)
-
-	if not btcaddr:
-		die(1,'{pnm} address {a} not found in tracking wallet'.format(
-				pnm=pnm,a=mmaddr))
-
-	c = bitcoin_connection()
-	try:
-		l = ' ' + label if label else ''
-		c.importaddress(btcaddr,mmaddr+l,False) # addr,label,rescan,p2sh
-	except:
-		die(1,'Unable to add label')
-
-	s = '{pnm} address {a} in tracking wallet'.format(a=mmaddr,pnm=pnm)
-	if remove: msg('Removed label from {}'.format(s))
-	else:      msg("Added label '{}' for {}".format(label,s))
+def add_label(mmaddr,label):
+	from mmgen.tw import MMGenTrackingWallet
+	if MMGenTrackingWallet.add_label(mmaddr,label): # returns on failure
+		s = '{pnm} address {a} in tracking wallet'.format(a=mmaddr,pnm=pnm)
+		if label: msg("Added label '{}' for {}".format(label,s))
+		else:     msg('Removed label for {}'.format(s))
+	else:
+		die(1,'Label could not be %s' % ('removed','added')[bool(label)])
 
-def remove_label(mmaddr): add_label(mmaddr,'',remove=True)
+def remove_label(mmaddr): add_label(mmaddr,'')
 
 def addrfile_chksum(infile):
 	from mmgen.addr import AddrList
@@ -544,7 +520,6 @@ def wif2hex(wif,compressed=False):
 def hex2wif(hexpriv,compressed=False):
 	Msg(bitcoin.hextowif(hexpriv,compressed))
 
-
 def encrypt(infile,outfile='',hash_preset=''):
 	data = get_data_from_file(infile,'data for encryption',binary=True)
 	enc_d = mmgen_encrypt(data,'user data',hash_preset)
@@ -553,7 +528,6 @@ def encrypt(infile,outfile='',hash_preset=''):
 
 	write_data_to_file(outfile,enc_d,'encrypted data',binary=True)
 
-
 def decrypt(infile,outfile='',hash_preset=''):
 	enc_d = get_data_from_file(infile,'encrypted data',binary=True)
 	while True:
@@ -568,7 +542,6 @@ def decrypt(infile,outfile='',hash_preset=''):
 
 	write_data_to_file(outfile,dec_d,'decrypted data',binary=True)
 
-
 def find_incog_data(filename,iv_id,keep_searching=False):
 	ivsize,bsize,mod = g.aesctr_iv_len,4096,4096*8
 	n,carry = 0,' '*ivsize
@@ -594,7 +567,6 @@ def find_incog_data(filename,iv_id,keep_searching=False):
 	msg('')
 	os.close(f)
 
-
 def rand2file(outfile, nbytes, threads=4, silent=False):
 	nbytes = parse_nbytes(nbytes)
 	from Crypto import Random

+ 108 - 34
mmgen/tw.py

@@ -46,7 +46,26 @@ No spendable outputs found!  Import addresses with balances into your
 watch-only wallet using '{}-addrimport' and then re-run this program.
 """.strip().format(g.proj_name)
 	}
+	sort_keys = 'addr','age','amt','txid','mmid'
+
 	def __init__(self):
+		self.unspent      = []
+		self.fmt_display  = ''
+		self.fmt_print    = ''
+		self.cols         = None
+		self.reverse      = False
+		self.group        = False
+		self.show_days    = True
+		self.show_mmid    = True
+		self.get_data()
+		self.sort_key     = 'age'
+		self.do_sort()
+		self.total        = self.get_total_btc()
+
+	def get_total_btc(self):
+		return sum([i.amt for i in self.unspent])
+
+	def get_data(self):
 		if g.bogus_wallet_data: # for debugging purposes only
 			us_rpc = eval(get_data_from_file(g.bogus_wallet_data))
 		else:
@@ -56,27 +75,15 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 
 		if not us_rpc: die(2,self.wmsg['no_spendable_outputs'])
 		for o in us_rpc:
-			o['mmid'],o['label'] = parse_tw_acct_label(o['account'])
+			o['mmid'],o['label'] = parse_tw_acct_label(o['account']) if 'account' in o else ('','')
 			o['days'] = int(o['confirmations'] * g.mins_per_block / (60*24))
 			o['amt'] = o['amount'] # TODO
 			o['addr'] = o['address']
 			o['confs'] = o['confirmations']
-		us = [MMGenTWOutput(**dict([(k,v) for k,v in o.items() if k in MMGenTWOutput.attrs and o[k] not in (None,'')])) for o in us_rpc]
-#		die(1,''.join([str(i)+'\n' for i in us]))
+		self.unspent = [MMGenTWOutput(**dict([(k,v) for k,v in o.items() if k in MMGenTWOutput.attrs and o[k] not in (None,'')])) for o in us_rpc]
 #		die(1,''.join([pp_format(i)+'\n' for i in us_rpc]))
+#		die(1,''.join([str(i)+'\n' for i in self.unspent]))
 
-		self.unspent  = us
-		self.fmt_display  = ''
-		self.fmt_print    = ''
-		self.cols         = None
-		self.reverse      = False
-		self.group        = False
-		self.show_days    = True
-		self.show_mmid    = True
-		self.do_sort('age')
-		self.total        = sum([i.amt for i in self.unspent])
-
-	sort_keys = 'addr','age','amt','txid','mmid'
 	def s_addr(self,i):  return i.addr
 	def s_age(self,i):   return 0 - i.confs
 	def s_amt(self,i):   return i.amt
@@ -87,18 +94,20 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 				*i.mmid.split(':'), w=AddrIdx.max_digits)
 		else: return 'G' + (i.label or '')
 
-	def do_sort(self,key,reverse=None):
+	def do_sort(self,key=None,reverse=None):
+		if not key: key = self.sort_key
+		assert key
+		self.sort_key = key
 		if key not in self.sort_keys:
 			fs = "'{}': invalid sort key.  Valid keys: [{}]"
 			die(2,fs.format(key,' '.join(self.sort_keys)))
 		if reverse == None: reverse = self.reverse
-		self.sort = key
 		self.unspent.sort(key=getattr(self,'s_'+key),reverse=reverse)
 
 	def sort_info(self,include_group=True):
 		ret = ([],['Reverse'])[self.reverse]
-		ret.append(self.sort.capitalize().replace('Mmid','MMGenID'))
-		if include_group and self.group and (self.sort in ('addr','txid','mmid')):
+		ret.append(self.sort_key.capitalize().replace('Mmid','MMGenID'))
+		if include_group and self.group and (self.sort_key in ('addr','txid','mmid')):
 			ret.append('Grouped')
 		return ret
 
@@ -122,7 +131,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 			if i.label == None: i.label = ''
 			i.skip = ''
 
-		mmid_w = max(len(i.mmid or '') for i in unsp)
+		mmid_w = max(len(i.mmid or '') for i in unsp) or 10
 		max_acct_len = max([len((i.mmid or '')+i.label)+1 for i in unsp])
 		addr_w = min(34+((1+max_acct_len) if self.show_mmid else 0),self.cols-46) + 6
 		acct_w   = min(max_acct_len, max(24,int(addr_w-10)))
@@ -138,10 +147,10 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 			'Amt(BTC) ',
 			('Conf.','Age(d)')[self.show_days])
 
-		if self.group and (self.sort in ('addr','txid','mmid')):
+		if self.group and (self.sort_key in ('addr','txid','mmid')):
 			for a,b in [(unsp[i],unsp[i+1]) for i in range(len(unsp)-1)]:
 				for k in ('addr','txid','mmid'):
-					if self.sort == k and getattr(a,k) == getattr(b,k):
+					if self.sort_key == k and getattr(a,k) == getattr(b,k):
 						b.skip = (k,'addr')[k=='mmid']
 
 		hdr_fmt   = 'UNSPENT OUTPUTS (sort order: %s)  Total BTC: %s'
@@ -156,7 +165,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 				addr_out = '%s %s' % (
 					type(i.addr).fmtc(addr_dots,width=btaddr_w,color=True) if i.skip == 'addr' \
 						else i.addr.fmt(width=btaddr_w,color=True),
-					'{} {}'.format(mmid_disp,i.label.fmt(width=label_w,color=True))
+					'{} {}'.format(mmid_disp,i.label.fmt(width=label_w,color=True) if label_w > 0 else '')
 				)
 			else:
 				addr_out = type(i.addr).fmtc(addr_dots,width=addr_w,color=True) if i.skip=='addr' \
@@ -177,12 +186,16 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 		out = [fs % ('Num','Tx ID,Vout','Address'.ljust(34),'MMGen ID'.ljust(15),
 			'Amount(BTC)','Conf.','Age(d)', 'Label')]
 
+		max_lbl_len = max(len(i.label) for i in self.unspent if i.label) or 1
 		for n,i in enumerate(self.unspent):
 			addr = '=' if i.skip == 'addr' and self.group else i.addr.fmt(color=color)
 			tx = ' ' * 63 + '=' if i.skip == 'txid' and self.group else str(i.txid)
 			s = fs % (str(n+1)+')', tx+','+str(i.vout),addr,
-					(i.mmid.fmt(14,color=color) if i.mmid else ''.ljust(14)),
-					i.amt.fmt(color=color),i.confs,i.days,i.label.hl(color=color) if i.label else '')
+					(i.mmid.fmt(width=14,color=color) if i.mmid else
+						MMGenID.fmtc('',width=14,nullrepl='-',color=color)),
+					i.amt.fmt(color=color),i.confs,i.days,
+					i.label.hl(color=color) if i.label else
+						MMGenAddrLabel.fmtc('',color=color,nullrepl='-',width=max_lbl_len))
 			out.append(s.rstrip())
 
 		fs = 'Unspent outputs ({} UTC)\nSort order: {}\n\n{}\n\nTotal BTC: {}\n'
@@ -197,6 +210,30 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 		fs = '\nTotal unspent: %s BTC (%s outputs)'
 		msg(fs % (self.total.hl(),len(self.unspent)))
 
+	def get_idx_and_label_from_user(self):
+		msg('')
+		while True:
+			ret = my_raw_input("Enter unspent output number (or 'q' to return to main menu): ")
+			if ret == 'q': return None,None
+			n = AddrIdx(ret,on_fail='silent') # hacky way to test and convert to integer
+			if not n or n < 1 or n > len(self.unspent):
+				msg('Choice must be a single number between 1 and %s' % len(self.unspent))
+			elif not self.unspent[n-1].mmid:
+				msg('Address #%s is not an %s address. No label can be added to it' %
+						(n,g.proj_name))
+			else:
+				while True:
+					s = my_raw_input("Enter label text (or 'q' to return to main menu): ")
+					if s == 'q':
+						return None,None
+					elif s == '':
+						if keypress_confirm(
+							"Removing label for address #%s.  Is this what you want?" % n):
+							return n,s
+					elif s:
+						if MMGenAddrLabel(s,on_fail='return'):
+							return n,s
+
 	def view_and_sort(self):
 		from mmgen.term import do_pager
 		prompt = """
@@ -206,27 +243,37 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
 		self.display()
 		msg(prompt)
 
-		p = "('q' = quit sorting, 'p' = print to file, 'v' = pager view, 'w' = wide view): "
+		p = "'q'=quit view, 'p'=print to file, 'v'=pager view, 'w'=wide view, 'l'=add label:\b"
 		while True:
 			reply = get_char(p, immed_chars='atDdAMrgmeqpvw')
 			if   reply == 'a': self.do_sort('amt')
-			elif reply == 't': self.do_sort('txid')
-			elif reply == 'D': self.show_days = not self.show_days
-			elif reply == 'd': self.do_sort('addr')
 			elif reply == 'A': self.do_sort('age')
-			elif reply == 'M': self.do_sort('mmid'); self.show_mmid = True
-			elif reply == 'r': self.unspent.reverse(); self.reverse = not self.reverse
+			elif reply == 'd': self.do_sort('addr')
+			elif reply == 'D': self.show_days = not self.show_days
+			elif reply == 'e': msg('\n%s\n%s\n%s' % (self.fmt_display,prompt,p))
 			elif reply == 'g': self.group = not self.group
+			elif reply == 'l':
+				idx,lbl = self.get_idx_and_label_from_user()
+				if idx:
+					e = self.unspent[idx-1]
+					if type(self).add_label(e.mmid,lbl,addr=e.addr):
+						self.get_data()
+						self.do_sort()
+						msg('%s\n%s\n%s' % (self.fmt_display,prompt,p))
+					else:
+						msg('Label could not be added\n%s\n%s' % (prompt,p))
+			elif reply == 'M': self.do_sort('mmid'); self.show_mmid = True
 			elif reply == 'm': self.show_mmid = not self.show_mmid
-			elif reply == 'e': msg("\n%s\n%s\n%s" % (self.fmt_display,prompt,p))
-			elif reply == 'q': return self.unspent
 			elif reply == 'p':
-				of = 'listunspent[%s].out' % ','.join(self.sort_info(include_group=False))
 				msg('')
+				of = 'listunspent[%s].out' % ','.join(self.sort_info(include_group=False)).lower()
 				write_data_to_file(of,self.format_for_printing(),'unspent outputs listing')
 				m = yellow("Data written to '%s'" % of)
 				msg('\n%s\n%s\n\n%s' % (self.fmt_display,m,prompt))
 				continue
+			elif reply == 'q': return self.unspent
+			elif reply == 'r': self.unspent.reverse(); self.reverse = not self.reverse
+			elif reply == 't': self.do_sort('txid')
 			elif reply == 'v':
 				do_pager(self.fmt_display)
 				continue
@@ -240,3 +287,30 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
 			msg('\n')
 			self.display()
 			msg(prompt)
+
+	# returns on failure
+	@classmethod
+	def add_label(cls,mmaddr,label='',addr=None):
+		mmaddr = MMGenID(mmaddr)
+
+		if addr: # called from view_and_sort()
+			if not BTCAddr(addr,on_fail='return'): return False
+		else:
+			from mmgen.addr import AddrData
+			addr = AddrData(source='tw').mmaddr2btcaddr(mmaddr)
+			if not addr:
+				msg('{} address {} not found in tracking wallet'.format(g.proj_name,mmaddr))
+				return False
+
+		label = MMGenAddrLabel(label,on_fail='return')
+		if not label and label != '': return False
+
+		acct = mmaddr + (' ' + label if label else '') # label is ASCII for now
+		# return on failure - args: addr,label,rescan,p2sh
+		ret = bitcoin_connection().importaddress(addr,acct,False,on_fail='return')
+		from mmgen.rpc import rpc_error,rpc_errmsg
+		if rpc_error(ret): msg('From bitcoind: ' + rpc_errmsg(ret))
+		return not rpc_error(ret)
+
+	@classmethod
+	def remove_label(cls,mmaddr): cls.add_label(mmaddr,'')

+ 4 - 74
mmgen/util.py

@@ -17,7 +17,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 """
-util.py:  Low-level routines imported by other modules for the MMGen suite
+util.py:  Low-level routines imported by other modules in the MMGen suite
 """
 
 import sys,os,time,stat,re
@@ -136,12 +136,10 @@ def vmsg(s):
 	if opt.verbose: sys.stderr.write(s + '\n')
 def vmsg_r(s):
 	if opt.verbose: sys.stderr.write(s)
-
 def Vmsg(s):
 	if opt.verbose: sys.stdout.write(s + '\n')
 def Vmsg_r(s):
 	if opt.verbose: sys.stdout.write(s)
-
 def dmsg(s):
 	if opt.debug: sys.stdout.write(s + '\n')
 
@@ -190,7 +188,7 @@ def split_into_cols(col_wid,s):
 	return ' '.join([s[col_wid*i:col_wid*(i+1)]
 					for i in range(len(s)/col_wid+1)]).rstrip()
 
-def capfirst(s):
+def capfirst(s): # different from str.capitalize() - doesn't downcase any uc in string
 	return s if len(s) == 0 else s[0].upper() + s[1:]
 
 def decode_timestamp(s):
@@ -338,7 +336,6 @@ def open_file_or_exit(filename,mode):
 		die(2,"Unable to open file '%s' for %s" % (filename,op))
 	return f
 
-
 def check_file_type_and_access(fname,ftype,blkdev_ok=False):
 
 	a = ((os.R_OK,'read'),(os.W_OK,'writ'))
@@ -372,46 +369,9 @@ def check_outfile(f,blkdev_ok=False):
 	return check_file_type_and_access(f,'output file',blkdev_ok=blkdev_ok)
 def check_outdir(f):
 	return check_file_type_and_access(f,'output directory')
-
 def make_full_path(outdir,outfile):
 	return os.path.normpath(os.path.join(outdir, os.path.basename(outfile)))
 
-def _validate_addr_num(n):
-	from mmgen.tx import is_mmgen_idx
-	if is_mmgen_idx(n):
-		return int(n)
-	else:
-		msg("'%s': invalid %s address index" % (n,g.proj_name))
-		return False
-
-def parse_addr_idxs(arg,sep=','):  # TODO - delete
-
-	ret = []
-
-	for i in (arg.split(sep)):
-
-		j = i.split('-')
-
-		if len(j) == 1:
-			i = _validate_addr_num(i)
-			if not i: return False
-			ret.append(i)
-		elif len(j) == 2:
-			beg = _validate_addr_num(j[0])
-			if not beg: return False
-			end = _validate_addr_num(j[1])
-			if not end: return False
-			if end < beg:
-				msg("'%s-%s': invalid range (end is less than beginning)" % (beg,end))
-				return False
-			ret.extend(range(beg,end+1))
-		else:
-			msg("'%s': invalid address range argument" % i)
-			return False
-
-	return sorted(set(ret))
-
-
 def get_new_passphrase(desc,passchg=False):
 
 	w = '{}passphrase for {}'.format(('','new ')[bool(passchg)], desc)
@@ -586,17 +546,13 @@ def get_data_from_file(infile,desc='data',dash=False,silent=False,binary=False):
 	f.close()
 	return data
 
-passwd_file_used = False
-
 def pwfile_reuse_warning():
-	global passwd_file_used
-	if passwd_file_used:
+	if 'passwd_file_used' in globals():
 		qmsg("Reusing passphrase from file '%s' at user request" % opt.passwd_file)
 		return True
-	passwd_file_used = True
+	globals()['passwd_file_used'] = True
 	return False
 
-
 def get_mmgen_passphrase(desc,passchg=False):
 	prompt ='Enter {}passphrase for {}: '.format(('','old ')[bool(passchg)],desc)
 	if opt.passwd_file:
@@ -605,28 +561,6 @@ def get_mmgen_passphrase(desc,passchg=False):
 	else:
 		return ' '.join(get_words_from_user(prompt))
 
-def get_bitcoind_passphrase(prompt):
-	if opt.passwd_file:
-		pwfile_reuse_warning()
-		return get_data_from_file(opt.passwd_file,'passphrase').strip('\r\n')
-	else:
-		return my_raw_input(prompt, echo=opt.echo_passphrase)
-
-
-def get_hash_preset_from_user(hp=g.hash_preset,desc='data'):
-	p = """Enter hash preset for %s,
- or hit ENTER to accept the default value ('%s'): """ % (desc,hp)
-	while True:
-		ret = my_raw_input(p)
-		if ret:
-			if ret in g.hash_presets.keys(): return ret
-			else:
-				msg('Invalid input.  Valid choices are %s' %
-						', '.join(sorted(g.hash_presets.keys())))
-				continue
-		else: return hp
-
-
 def my_raw_input(prompt,echo=True,insert_txt='',use_readline=True):
 
 	try: import readline
@@ -651,7 +585,6 @@ def my_raw_input(prompt,echo=True,insert_txt='',use_readline=True):
 
 	return reply.strip()
 
-
 def keypress_confirm(prompt,default_yes=False,verbose=False):
 
 	from mmgen.term import get_char
@@ -670,7 +603,6 @@ def keypress_confirm(prompt,default_yes=False,verbose=False):
 			if verbose: msg('\nInvalid reply')
 			else: msg_r('\r')
 
-
 def prompt_and_get_char(prompt,chars,enter_ok=False,verbose=False):
 
 	from mmgen.term import get_char
@@ -685,7 +617,6 @@ def prompt_and_get_char(prompt,chars,enter_ok=False,verbose=False):
 		if verbose: msg('\nInvalid reply')
 		else: msg_r('\r')
 
-
 def do_license_msg(immed=False):
 
 	if opt.quiet or g.no_license: return
@@ -708,7 +639,6 @@ def do_license_msg(immed=False):
 			msg_r('\r')
 	msg('')
 
-
 def get_bitcoind_cfg_options(cfg_keys):
 
 	cfg_file = os.path.join(get_homedir(), get_datadir(), 'bitcoin.conf')

+ 4 - 4
test/test.py

@@ -1176,13 +1176,13 @@ class MMGenTestSuite(object):
 		if opt.verbose or opt.exact_output:
 			sys.stderr.write(green('Generating fake tracking wallet info\n'))
 		silence()
-		from mmgen.addr import AddrList,AddrData
+		from mmgen.addr import AddrList,AddrData,AddrIdxList
 		tx_data,ad = {},AddrData()
 		for s in sources:
 			afile = get_file_with_ext('addrs',cfgs[s]['tmpdir'])
 			ai = AddrList(afile)
 			ad.add(ai)
-			aix = parse_addr_idxs(cfgs[s]['addr_idx_list'])
+			aix = AddrIdxList(fmt_str=cfgs[s]['addr_idx_list'])
 			if len(aix) != addrs_per_wallet:
 				errmsg(red('Address index list length != %s: %s' %
 							(addrs_per_wallet,repr(aix))))
@@ -1244,8 +1244,8 @@ class MMGenTestSuite(object):
 
 		for num in tx_data:
 			t.expect('Continue anyway? (y/N): ','y')
-		t.expect(r"'q' = quit sorting, .*?: ",'M', regex=True)
-		t.expect(r"'q' = quit sorting, .*?: ",'q', regex=True)
+		t.expect(r"'q'=quit view, .*?:.",'M', regex=True)
+		t.expect(r"'q'=quit view, .*?:.",'q', regex=True)
 		outputs_list = [(addrs_per_wallet+1)*i + 1 for i in range(len(tx_data))]
 		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: ',