10 Commits be17d06708 ... f9e1cf479b

Author SHA1 Message Date
  The MMGen Project f9e1cf479b xmrwallet: pass `compat_call` in uargs tuple 6 days ago
  The MMGen Project 32595f621b whitespace, string formatting, minor fixes and cleanups 6 days ago
  The MMGen Project ffbcd2b615 autosign: `die_wrong_num_txs()`: cleanup 6 days ago
  The MMGen Project 3d4158e81f xmrwallet.file.MoneroMMGenTX: new `is_*` attributes 6 days ago
  The MMGen Project acee3606af proto.xmr.tw.addresses: implement account-based display 6 days ago
  The MMGen Project 36d82c5f91 xmrwallet: whitespace 6 days ago
  The MMGen Project c6fe8fac16 tw.addresses: whitespace 6 days ago
  The MMGen Project 9c13bdd1f1 tw.addresses: improve line filtering logic 6 days ago
  The MMGen Project 7a7add8d80 proto.xmr.tw.unspent: fix right fill for account line 6 days ago
  The MMGen Project efb66dec9f proto.xmr.tw.view.action.a_sync_wallets: handle SocketError 6 days ago

+ 19 - 16
mmgen/autosign.py

@@ -272,13 +272,16 @@ class Signable:
 			return getattr(self, attrname)
 
 		def die_wrong_num_txs(self, tx_type, *, msg=None, desc=None, show_dir=False):
-			num_txs = len(getattr(self, tx_type))
+			match len(getattr(self, tx_type)): # num_txs
+				case 0: subj, suf, pred = ('No', 's', 'present')
+				case 1: subj, suf, pred = ('One', '', 'already present')
+				case _: subj, suf, pred = ('More than one', '', 'already present')
 			die('AutosignTXError', '{m}{a} {b} transaction{c} {d} {e}!'.format(
 				m = msg + '\n' if msg else '',
-				a = 'One' if num_txs == 1 else 'More than one' if num_txs else 'No',
+				a = subj,
 				b = desc or tx_type,
-				c = suf(num_txs),
-				d = 'already present' if num_txs else 'present',
+				c = suf,
+				d = pred,
 				e = f'in ‘{getattr(self.parent, self.dir_name)}’'
 					if show_dir else 'on removable device'))
 
@@ -336,6 +339,7 @@ class Signable:
 
 	class xmr_signable: # mixin class
 		automount = True
+		summary_footer = ''
 
 		def need_daemon_restart(self, m, new_idx):
 			old_idx = self.parent.xmr_cur_wallet_idx
@@ -346,14 +350,6 @@ class Signable:
 			bmsg('\nAutosign summary:')
 			msg('\n'.join(s.get_info(indent='  ') for s in signables) + self.summary_footer)
 
-	class xmr_transaction(xmr_signable, automount_transaction):
-		dir_name = 'xmr_tx_dir'
-		desc = 'Monero transaction'
-		rawext = 'rawtx'
-		sigext = 'sigtx'
-		subext = 'subtx'
-		summary_footer = ''
-
 		async def sign(self, f):
 			from . import xmrwallet
 			from .xmrwallet.file.tx import MoneroMMGenTX
@@ -367,6 +363,13 @@ class Signable:
 			tx2.write(ask_write=False)
 			return tx2
 
+	class xmr_transaction(xmr_signable, automount_transaction):
+		dir_name = 'xmr_tx_dir'
+		desc = 'Monero non-compat transaction'
+		rawext = 'rawtx'
+		sigext = 'sigtx'
+		subext = 'subtx'
+
 	class xmr_wallet_outputs_file(xmr_signable, base):
 		desc = 'Monero wallet outputs file'
 		rawext = 'raw'
@@ -517,10 +520,10 @@ class Autosign:
 		if sys.platform == 'darwin': # test suite uses ‘fixed-up’ shm_dir
 			from .platform.darwin.util import MacOSRamDisk
 			self.ramdisk = MacOSRamDisk(
-					cfg,
-					self.macOS_ramdisk_name,
-					self._get_macOS_ramdisk_size(),
-					path = self.shm_dir)
+				cfg,
+				self.macOS_ramdisk_name,
+				self._get_macOS_ramdisk_size(),
+				path = self.shm_dir)
 
 		self.keyfile = self.mountpoint / 'autosign.key'
 

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-16.1.dev17
+16.1.dev18

+ 11 - 0
mmgen/proto/xmr/tw/addresses.py

@@ -29,3 +29,14 @@ class MoneroTwAddresses(MoneroTwView, TwAddresses):
 		'R': 'a_sync_wallets'}
 	removed_key_mappings = {
 		'D': 'i_addr_delete'}
+
+	class display_type:
+
+		class squeezed(MoneroTwView.display_type.squeezed):
+			cols = ('addr_idx', 'addr', 'used', 'comment', 'amt')
+
+		class detail(MoneroTwView.display_type.detail):
+			cols = ('addr_idx', 'addr', 'used', 'amt', 'comment')
+
+	def get_disp_data(self):
+		return MoneroTwView.get_disp_data(self, input_data=tuple(TwAddresses.get_disp_data(self)))

+ 1 - 128
mmgen/proto/xmr/tw/unspent.py

@@ -12,11 +12,6 @@
 proto.xmr.tw.unspent: Monero protocol tracking wallet unspent outputs class
 """
 
-from collections import namedtuple
-
-from ....obj import ImmutableAttr
-from ....addr import MoneroIdx
-from ....amt import CoinAmtChk
 from ....tw.unspent import TwUnspentOutputs
 
 from .view import MoneroTwView
@@ -25,10 +20,8 @@ class MoneroTwUnspentOutputs(MoneroTwView, TwUnspentOutputs):
 
 	hdr_lbl = 'spendable accounts'
 	desc = 'spendable accounts'
-	item_desc = 'account'
 	include_empty = False
-	total = None
-	nice_addr_w = {'addr': 20}
+	has_used = False
 
 	prompt_fs_in = [
 		'Sort options: [a]mount, [A]ge, a[d]dr, [M]mgen addr, [r]everse',
@@ -38,123 +31,3 @@ class MoneroTwUnspentOutputs(MoneroTwView, TwUnspentOutputs):
 	extra_key_mappings = {
 		'R': 'a_sync_wallets',
 		'A': 's_age'}
-
-	sort_disp = {
-		'addr':   'Addr',
-		'age':    'Age',
-		'amt':    'Amt',
-		'twmmid': 'MMGenID'}
-
-	# NB: For account-based views, ALL sort keys MUST begin with acct_sort_key!
-	sort_funcs = {
-		'addr':   lambda i: '{}:{}'.format(i.twmmid.obj.acct_sort_key, i.addr),
-		'age':    lambda i: '{}:{:020}'.format(i.twmmid.obj.acct_sort_key, 0 - i.confs),
-		'amt':    lambda i: '{}:{:050}'.format(i.twmmid.obj.acct_sort_key, i.amt.to_unit('atomic')),
-		'twmmid': lambda i: i.twmmid.sort_key} # sort_key begins with acct_sort_key
-
-	class MoneroMMGenTwUnspentOutput(TwUnspentOutputs.MMGenTwUnspentOutput):
-		valid_attrs = {'amt', 'unlocked_amt', 'comment', 'twmmid', 'addr', 'confs', 'skip'}
-		unlocked_amt = ImmutableAttr(CoinAmtChk, include_proto=True)
-
-	def gen_data(self, rpc_data, lbl_id):
-		return (
-			self.MoneroMMGenTwUnspentOutput(
-					self.proto,
-					twmmid  = twmmid,
-					addr    = data['addr'],
-					confs   = data['confs'],
-					comment = data['lbl'].comment,
-					amt     = data['amt'],
-					unlocked_amt = data['unlocked_amt'])
-				for twmmid, data in rpc_data.items())
-
-	def get_disp_data(self):
-		chk_fail_msg = 'For account-based views, ALL sort keys MUST begin with acct_sort_key!'
-		ad = namedtuple('accts_data', ['idx', 'acct_idx', 'total', 'unlocked_total', 'data'])
-		bd = namedtuple('accts_data_data', ['disp_data_idx', 'data'])
-		def gen_accts_data():
-			idx, acct_idx = (None, None)
-			total, unlocked_total, d_acc = (0, 0, {})
-			chk_acc = [] # check for out-of-order accounts (developer idiot-proofing)
-			for n, d in enumerate(self.data):
-				m = d.twmmid.obj
-				if idx != m.idx or acct_idx != m.acct_idx:
-					if idx:
-						yield ad(idx, acct_idx, total, unlocked_total, d_acc)
-					chk_acc.append((m.idx, m.acct_idx))
-					idx = m.idx
-					acct_idx = m.acct_idx
-					total = d.amt
-					unlocked_total = d.unlocked_amt
-					d_acc = {m.addr_idx: bd(n, d)}
-				else:
-					total += d.amt
-					unlocked_total += d.unlocked_amt
-					d_acc[m.addr_idx] = bd(n, d)
-			if idx:
-				assert len(set(chk_acc)) == len(chk_acc), chk_fail_msg
-				yield ad(idx, acct_idx, total, unlocked_total, d_acc)
-		self.accts_data = tuple(gen_accts_data())
-		return super().get_disp_data()
-
-	class display_type(TwUnspentOutputs.display_type):
-
-		class squeezed(TwUnspentOutputs.display_type.squeezed):
-			cols = ('addr_idx', 'addr', 'comment', 'amt')
-			colhdr_fmt_method = None
-			fmt_method = 'gen_display'
-
-		class detail(TwUnspentOutputs.display_type.detail):
-			cols = ('addr_idx', 'addr', 'amt', 'comment')
-			colhdr_fmt_method = None
-			fmt_method = 'gen_display'
-			line_fmt_method = 'squeezed_format_line'
-
-	def get_column_widths(self, data, *, wide, interactive):
-		return self.compute_column_widths(
-			widths = { # fixed cols
-				'addr_idx': MoneroIdx.max_digits,
-				'amt': self.amt_widths['amt'],
-				'spc': 4}, # 1 leading space plus 3 spaces between 4 cols
-			maxws = { # expandable cols
-				'addr': max(len(d.addr) for d in data),
-				'comment': max(d.comment.screen_width for d in data)},
-			minws = {
-				'addr': 16,
-				'comment': len('Comment')},
-			maxws_nice = self.nice_addr_w,
-			wide = wide,
-			interactive = interactive)
-
-	def gen_display(self, data, cw, fs, color, fmt_method):
-		fs_acct = '{:>4} {:6} {:7}  {}'
-		yield fs_acct.format('', 'Wallet', 'Account', 'Balance').ljust(self.term_width)
-		for n, d in enumerate(self.accts_data):
-			yield fs_acct.format(
-				str(n + 1) + ')',
-				d.idx.fmt(6, color=color),
-				d.acct_idx.fmt(7, color=color),
-				d.total.hl2(
-					color = color,
-					color_override = None if d.total == d.unlocked_total else 'orange'
-				)).ljust(self.term_width)
-			for v in d.data.values():
-				yield fmt_method(None, v.data, cw, fs, color, None, None)
-
-	def squeezed_format_line(self, n, d, cw, fs, color, yes, no):
-		return fs.format(
-			I = d.twmmid.obj.addr_idx.fmt(cw.addr_idx, color=color),
-			a = d.addr.fmt(self.addr_view_pref, cw.addr, color=color),
-			c = d.comment.fmt2(cw.comment, color=color, nullrepl='-'),
-			A = d.amt.fmt2(
-				cw.iwidth,
-				color = color,
-				color_override = None if d.amt == d.unlocked_amt else 'orange',
-				prec = self.disp_prec))
-
-	async def get_idx_from_user(self):
-		if res := await self.get_idx(f'{self.item_desc} number', self.accts_data):
-			return await self.get_idx(
-				'address index',
-				self.accts_data[res.idx - 1].data,
-				is_addr_idx = True)

+ 150 - 3
mmgen/proto/xmr/tw/view.py

@@ -14,12 +14,39 @@ proto.xmr.tw.view: Monero protocol base class for tracking wallet view classes
 
 from collections import namedtuple
 
-from ....xmrwallet import op as xmrwallet_op
+from ....obj import ImmutableAttr
+from ....color import red, green
+from ....addr import MoneroIdx
+from ....amt import CoinAmtChk
 from ....seed import SeedID
+from ....xmrwallet import op as xmrwallet_op
 from ....tw.view import TwView
+from ....tw.unspent import TwUnspentOutputs
 
 class MoneroTwView:
 
+	item_desc = 'account'
+	nice_addr_w = {'addr': 20}
+	total = None
+
+	sort_disp = {
+		'addr':   'Addr',
+		'age':    'Age',
+		'amt':    'Amt',
+		'twmmid': 'MMGenID'}
+
+	# NB: For account-based views, ALL sort keys MUST begin with acct_sort_key!
+	sort_funcs = {
+		'addr':   lambda i: '{}:{}'.format(i.twmmid.obj.acct_sort_key, i.addr),
+		'age':    lambda i: '{}:{:020}'.format(i.twmmid.obj.acct_sort_key, 0 - i.confs),
+		'amt':    lambda i: '{}:{:050}'.format(i.twmmid.obj.acct_sort_key, i.amt.to_unit('atomic')),
+		'twmmid': lambda i: i.twmmid.sort_key} # sort_key begins with acct_sort_key
+
+	class MoneroTwViewItem(TwUnspentOutputs.MMGenTwUnspentOutput):
+		valid_attrs = {'amt', 'unlocked_amt', 'comment', 'twmmid', 'addr', 'confs', 'is_used', 'skip'}
+		unlocked_amt = ImmutableAttr(CoinAmtChk, include_proto=True)
+		is_used = ImmutableAttr(bool)
+
 	class rpc:
 		caps = ()
 		is_remote = False
@@ -80,13 +107,133 @@ class MoneroTwView:
 
 		return dict(gen_addrs())
 
+	def gen_data(self, rpc_data, lbl_id):
+		return (
+			self.MoneroTwViewItem(
+					self.proto,
+					twmmid  = twmmid,
+					addr    = data['addr'],
+					confs   = data['confs'],
+					is_used = data['is_used'],
+					comment = data['lbl'].comment,
+					amt     = data['amt'],
+					unlocked_amt = data['unlocked_amt'])
+				for twmmid, data in rpc_data.items())
+
+	def get_disp_data(self, input_data=None):
+		data = self.data if input_data is None else input_data
+		chk_fail_msg = 'For account-based views, ALL sort keys MUST begin with acct_sort_key!'
+		ad = namedtuple('accts_data', ['idx', 'acct_idx', 'total', 'unlocked_total', 'data'])
+		bd = namedtuple('accts_data_data', ['disp_data_idx', 'data'])
+		def gen_accts_data():
+			idx, acct_idx = (None, None)
+			total, unlocked_total, d_acc = (0, 0, {})
+			chk_acc = [] # check for out-of-order accounts (developer idiot-proofing)
+			for n, d in enumerate(data):
+				m = d.twmmid.obj
+				if idx != m.idx or acct_idx != m.acct_idx:
+					if idx:
+						yield ad(idx, acct_idx, total, unlocked_total, d_acc)
+					idx = m.idx
+					acct_idx = m.acct_idx
+					total = d.amt
+					unlocked_total = d.unlocked_amt
+					d_acc = {m.addr_idx: bd(n, d)}
+					chk_acc.append((idx, acct_idx))
+				else:
+					total += d.amt
+					unlocked_total += d.unlocked_amt
+					d_acc[m.addr_idx] = bd(n, d)
+			if idx:
+				assert len(set(chk_acc)) == len(chk_acc), chk_fail_msg
+				yield ad(idx, acct_idx, total, unlocked_total, d_acc)
+		self.accts_data = tuple(gen_accts_data())
+		return data
+
+	class display_type:
+
+		class squeezed(TwUnspentOutputs.display_type.squeezed):
+			cols = ('addr_idx', 'addr', 'comment', 'amt')
+			colhdr_fmt_method = None
+			fmt_method = 'gen_display'
+
+		class detail(TwUnspentOutputs.display_type.detail):
+			cols = ('addr_idx', 'addr', 'amt', 'comment')
+			colhdr_fmt_method = None
+			fmt_method = 'gen_display'
+			line_fmt_method = 'squeezed_format_line'
+
+	def get_column_widths(self, data, *, wide, interactive):
+		return self.compute_column_widths(
+			widths = { # fixed cols
+				'addr_idx': MoneroIdx.max_digits,
+				'used': 4 if 'used' in self.display_type.squeezed.cols else 0,
+				'amt': self.amt_widths['amt'],
+				'spc': len(self.display_type.squeezed.cols)},
+			maxws = { # expandable cols
+				'addr': max(len(d.addr) for d in data),
+				'comment': max(d.comment.screen_width for d in data)},
+			minws = {
+				'addr': 16,
+				'comment': len('Comment')},
+			maxws_nice = self.nice_addr_w,
+			wide = wide,
+			interactive = interactive)
+
+	def gen_display(self, data, cw, fs, color, fmt_method):
+		yes, no = (red('Used'), green('New ')) if color else ('Used', 'New ')
+		fs_acct = '{:>4} {:6} {:7}  {}'
+		# 30 = 4(col1) + 6(col2) + 7(col3) + 8(iwidth) + 1(len('.')) + 4(spc)
+		rfill = ' ' * (self.term_width - self.proto.coin_amt.max_prec - 30)
+		yield fs_acct.format('', 'Wallet', 'Account', ' Balance').ljust(self.term_width)
+		for n, d in enumerate(self.accts_data):
+			yield fs_acct.format(
+				str(n + 1) + ')',
+				d.idx.fmt(6, color=color),
+				d.acct_idx.fmt(7, color=color),
+				d.total.fmt2(
+					8, # iwidth
+					color = color,
+					color_override = None if d.total == d.unlocked_total else 'orange'
+				)) + rfill
+			for v in d.data.values():
+				yield fmt_method(None, v.data, cw, fs, color, yes, no)
+
+	def squeezed_format_line(self, n, d, cw, fs, color, yes, no):
+		return fs.format(
+			I = d.twmmid.obj.addr_idx.fmt(cw.addr_idx, color=color),
+			a = d.addr.fmt(self.addr_view_pref, cw.addr, color=color),
+			u = yes if d.is_used else no,
+			c = d.comment.fmt2(cw.comment, color=color, nullrepl='-'),
+			A = d.amt.fmt2(
+				cw.iwidth,
+				color = color,
+				color_override = None if d.amt == d.unlocked_amt else 'orange',
+				prec = self.disp_prec))
+
+	async def get_idx_from_user(self):
+		if res := await self.get_idx(f'{self.item_desc} number', self.accts_data):
+			return await self.get_idx(
+				'address index',
+				self.accts_data[res.idx - 1].data,
+				is_addr_idx = True)
+
 	class action(TwView.action):
 
 		async def a_sync_wallets(self, parent):
-			from ....util import msg, msg_r
+			from ....util import msg, msg_r, ymsg
 			from ....tw.view import CUR_HOME, ERASE_ALL
 			msg('')
-			op = xmrwallet_op('sync', parent.cfg, None, None, compat_call=True)
+			try:
+				op = xmrwallet_op('sync', parent.cfg, None, None, compat_call=True)
+			except Exception as e:
+				if type(e).__name__ == 'SocketError':
+					import asyncio
+					ymsg(str(e))
+					await asyncio.sleep(2)
+					msg_r(CUR_HOME + ERASE_ALL)
+					return False
+				raise
 			await op.restart_wallet_daemon()
 			await op.main()
 			await parent.get_data()

+ 1 - 1
mmgen/tool/rpc.py

@@ -141,7 +141,7 @@ class tool_cmd(tool_cmd_base):
 			mmgen_addrs:  'hyphenated range or comma-separated list of addresses' = '',
 			showcoinaddrs:'display coin addresses in addition to MMGen IDs' = True,
 			showempty:    'show addresses with no balances' = True,
-			showused:     'show used addresses (tristate: 0=no, 1=yes, 2=all)' = 1,
+			showused:     'show used addresses (tristate: 0=no, 1=yes, 2=only)' = 1,
 			all_labels:   'show all addresses with labels' = False):
 		"list MMGen addresses in the tracking wallet and their balances"
 

+ 33 - 21
mmgen/tw/addresses.py

@@ -32,7 +32,7 @@ class TwAddresses(TwView):
 	filters = ('showempty', 'showused', 'all_labels')
 	showcoinaddrs = True
 	showempty = True
-	showused = 1 # tristate: 0: no, 1: yes, 2: all
+	showused = 1 # tristate: 0: no, 1: yes, 2: only
 	all_labels = False
 	mod_subpath = 'tw.addresses'
 	has_age = False
@@ -141,12 +141,12 @@ class TwAddresses(TwView):
 		for e in await self.twctl.get_label_addr_pairs():
 			bal = await self.twctl.get_balance(e.coinaddr, block=block)
 			addrs[e.label.mmid] = {
-				'addr':  e.coinaddr,
-				'amt':   bal,
-				'recvd': bal,         # current bal only, CF btc.tw.addresses.get_rpc_data()
+				'addr':    e.coinaddr,
+				'amt':     bal,
+				'recvd':   bal,         # current bal only, CF btc.tw.addresses.get_rpc_data()
 				'is_used': bool(bal) or e.coinaddr in used_addrs,
-				'confs': minconf,
-				'lbl':   e.label}
+				'confs':   minconf,
+				'lbl':     e.label}
 			self.total += bal
 
 		return addrs
@@ -168,30 +168,43 @@ class TwAddresses(TwView):
 
 	def get_disp_data(self):
 		if self.usr_addr_list:
-			return (d for d in self.data if d.twmmid.obj in self.usr_addr_list)
+			for d in self.data:
+				if d.twmmid.obj in self.usr_addr_list:
+					yield d
 		else:
-			return (d for d in self.data if
-				(self.all_labels and d.comment) or
-				(self.showused == 2 and d.is_used) or
-				(not (d.is_used and not self.showused) and (d.amt or self.showempty)))
+			for d in self.data:
+				if self.all_labels and d.comment:
+					yield d
+				else:
+					if not (self.showempty or d.amt):
+						continue
+					match self.showused: # tristate: 0:no, 1:yes, 2:only
+						case 0:
+							if not d.is_used:
+								yield d
+						case 1:
+							yield d
+						case 2:
+							if d.is_used:
+								yield d
 
 	def get_column_widths(self, data, *, wide, interactive):
 
 		return self.compute_column_widths(
 			widths = { # fixed cols
-				'num':  max(2, len(str(len(data)))+1),
-				'mmid': max(len(d.twmmid.disp) for d in data),
-				'used': self.used_w,
-				'amt':  self.amt_widths['amt'],
-				'date': self.age_w if self.has_age else 0,
-				'block': self.age_col_params['block'][0] if wide and self.has_age else 0,
+				'num':       max(2, len(str(len(data)))+1),
+				'mmid':      max(len(d.twmmid.disp) for d in data),
+				'used':      self.used_w,
+				'amt':       self.amt_widths['amt'],
+				'date':      self.age_w if self.has_age else 0,
+				'block':     self.age_col_params['block'][0] if wide and self.has_age else 0,
 				'date_time': self.age_col_params['date_time'][0] if wide and self.has_age else 0,
-				'spc': self.spc_w},
+				'spc':       self.spc_w},
 			maxws = { # expandable cols
 				'addr':    max(len(d.addr) for d in data) if self.showcoinaddrs else 0,
 				'comment': max(d.comment.screen_width for d in data)},
 			minws = {
-				'addr': 12 if self.showcoinaddrs else 0,
+				'addr':    12 if self.showcoinaddrs else 0,
 				'comment': len('Comment')},
 			maxws_nice = {'addr': 18},
 			wide = wide,
@@ -226,8 +239,7 @@ class TwAddresses(TwView):
 			a = d.addr.fmt(self.addr_view_pref, cw.addr, color=color),
 			c = d.comment.fmt2(cw.comment, color=color, nullrepl='-'),
 			A = d.amt.fmt(cw.iwidth, color=color, prec=self.disp_prec),
-			d = self.age_disp(d, self.age_fmt)
-		)
+			d = self.age_disp(d, self.age_fmt))
 
 	def detail_format_line(self, n, d, cw, fs, color, yes, no):
 		return fs.format(

+ 4 - 4
mmgen/tw/view.py

@@ -426,15 +426,15 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 
 				if color:
 					Blue, Green = (blue, green)
-					Yes, No, All = (green('yes'), red('no'), yellow('all'))
+					Yes, No, Only = (green('yes'), red('no'), yellow('only'))
 				else:
 					Blue, Green = (nocolor, nocolor)
-					Yes, No, All = ('yes', 'no', 'all')
+					Yes, No, Only = ('yes', 'no', 'only')
 
 				sort_info = ' '.join(self.sort_info())
 
 				def fmt_filter(k):
-					return '{}:{}'.format(k, {0: No, 1: Yes, 2: All}[getattr(self, k)])
+					return '{}:{}'.format(k, {0: No, 1: Yes, 2: Only}[getattr(self, k)])
 
 				yield '{} (sort order: {}){}'.format(
 					self.hdr_lbl.upper(),
@@ -442,7 +442,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 					spc * (self.cols - len(f'{self.hdr_lbl} (sort order: {sort_info})')))
 
 				if hasattr(self, 'sid'):
-					yield f'Seed ID: {self.sid.hl()}'
+					yield f'Seed ID: {self.sid.hl(color=color)}'
 
 				if self.filters:
 					yield 'Filters: {}{}'.format(

+ 2 - 2
mmgen/tx/util.py

@@ -20,8 +20,8 @@ def get_autosign_obj(cfg):
 			'_clone': cfg,
 			'mountpoint': cfg.autosign_mountpoint,
 			'coins': cfg.coin,
-			'online': not cfg.offline, # used only in online environment (xmrwallet, txcreate, txsend, txbump)
-		}))
+			# used only in online environment (xmrwallet, txcreate, txsend, txbump):
+			'online': not cfg.offline}))
 
 def mount_removable_device(cfg):
 	asi = get_autosign_obj(cfg)

+ 4 - 8
mmgen/xmrwallet/__init__.py

@@ -23,14 +23,13 @@ tx_priorities = {
 	1: 'low',
 	2: 'normal',
 	3: 'high',
-	4: 'highest'
-}
+	4: 'highest'}
 
 uargs = namedtuple('xmrwallet_uargs', [
 	'infile',
 	'wallets',
 	'spec',
-])
+	'compat_call'])
 
 uarg_info = (
 	lambda e, hp: {
@@ -72,8 +71,7 @@ op_names = {
 	'export_outputs_sign': 'export',
 	'import_outputs':      'import',
 	'import_key_images':   'import',
-	'wallet':              'wallet', # virtual class
-}
+	'wallet':              'wallet'} # virtual class
 
 kafile_arg_ops = (
 	'create',
@@ -130,6 +128,4 @@ def op(op, cfg, infile, wallets, *, spec=None, compat_call=False):
 			'daemon': cfg.daemon or cfg.monero_daemon,
 			'watch_only': cfg.watch_only or cfg.autosign or bool(cfg.autosign_mountpoint),
 			'wallet_dir': twctl_cls.get_tw_dir(cfg, cfg._proto)})
-	ret = op_cls(op)(cfg, uargs(infile, wallets, spec))
-	ret.compat_call = compat_call
-	return ret
+	return op_cls(op)(cfg, uargs(infile, wallets, spec, compat_call))

+ 2 - 4
mmgen/xmrwallet/file/__init__.py

@@ -24,8 +24,7 @@ class MoneroMMGenFile:
 	def make_chksum(self, *, keys=None):
 		res = json.dumps(
 			dict((k, v) for k, v in self.data._asdict().items() if (not keys or k in keys)),
-			cls = json_encoder
-		)
+			cls = json_encoder)
 		return make_chksum_N(res, rounds=1, nchars=self.chksum_nchars, upper=False)
 
 	@property
@@ -54,8 +53,7 @@ class MoneroMMGenFile:
 		return json.dumps(
 			{self.data_label: out},
 			cls = json_encoder,
-			indent = 2,
-		)
+			indent = 2)
 
 	def extract_data_from_file(self, cfg, fn):
 		return json.loads(

+ 7 - 14
mmgen/xmrwallet/file/outputs.py

@@ -38,8 +38,7 @@ class MoneroWalletOutputsFile:
 			'outputs_data_hex',
 			'signed_key_images',
 			'sign',
-			'imported',
-		])
+			'imported'])
 
 		def __init__(self, cfg):
 			self.name = type(self).__name__
@@ -62,13 +61,11 @@ class MoneroWalletOutputsFile:
 				wallet_fn.parent) / self.fn_fs.format(
 					a = wallet_fn.name,
 					b = self.base_chksum,
-					c = self.ext,
-				)
+					c = self.ext)
 
 		def get_wallet_fn(self, fn):
 			assert fn.name.endswith(f'.{self.ext}'), (
-				f'{self.name}: filename does not end with {"."+self.ext!r}'
-			)
+				f'{self.name}: filename does not end with {"."+self.ext!r}')
 			return fn.parent / fn.name[:-(len(self.ext)+self.ext_offset+1)]
 
 		def get_info(self, *, indent=''):
@@ -109,8 +106,7 @@ class MoneroWalletOutputsFile:
 				parent     = parent,
 				wallet_fn  = wallet_fn,
 				data       = data,
-				wallet_idx = wallet_idx,
-			)
+				wallet_idx = wallet_idx)
 			self.check_checksums(d_wrap)
 
 		@classmethod
@@ -119,8 +115,7 @@ class MoneroWalletOutputsFile:
 			pat = cls.fn_fs.format(
 				a = wallet_fn.name,
 				b = f'[0-9a-f]{{{cls.chksum_nchars}}}\\',
-				c = cls.ext,
-			)
+				c = cls.ext)
 			matches = [f for f in path.iterdir() if re.match(pat, f.name)]
 			if not matches and ret_on_no_match:
 				return None
@@ -128,8 +123,7 @@ class MoneroWalletOutputsFile:
 				die(2, "{a} matching pattern {b!r} found in '{c}'!".format(
 					a = 'No files' if not matches else 'More than one file',
 					b = pat,
-					c = path
-				))
+					c = path))
 			return matches[0]
 
 	class Unsigned(Completed):
@@ -154,8 +148,7 @@ class MoneroWalletDumpFile:
 		data_tuple = namedtuple('wallet_dump_data', [
 			'seed_id',
 			'wallet_index',
-			'wallet_metadata',
-		])
+			'wallet_metadata'])
 		def get_outfile(self, cfg, wallet_fn):
 			return wallet_fn.parent / f'{wallet_fn.name}.{self.ext}'
 

+ 21 - 14
mmgen/xmrwallet/file/tx.py

@@ -178,6 +178,11 @@ class MoneroMMGenTX:
 				ignore_opt_outdir     = self.cfg.autosign)
 
 	class New(Base):
+		is_new = False
+		is_signing = False
+		is_submitting = False
+		is_complete = False
+		signed = False
 
 		def __init__(self, *args, **kwargs):
 
@@ -200,11 +205,9 @@ class MoneroMMGenTX:
 
 			self.data = self.xmrwallet_tx_data(
 				op             = d.op,
-				create_time    = now if self.name in ('NewSigned', 'NewUnsigned')
-					else getattr(d, 'create_time', None),
-				sign_time      = now if self.name in ('NewSigned', 'NewColdSigned')
-					else getattr(d, 'sign_time', None),
-				submit_time    = now if self.name == 'NewSubmitted' else None,
+				create_time    = now if self.is_new else getattr(d, 'create_time', None),
+				sign_time      = now if self.is_signing else getattr(d, 'sign_time', None),
+				submit_time    = now if self.is_submitting else None,
 				network        = d.network,
 				seed_id        = SeedID(sid=d.seed_id),
 				source         = XMRWalletAddrSpec(d.source),
@@ -212,31 +215,35 @@ class MoneroMMGenTX:
 				dest_address   = CoinAddr(proto, d.dest_address),
 				txid           = CoinTxID(d.txid),
 				amount         = d.amount,
-				priority       = self.cfg.priority if self.name in ('NewSigned', 'NewUnsigned')
-					else d.priority,
+				priority       = self.cfg.priority if self.is_new else d.priority,
 				fee            = d.fee,
 				blob           = d.blob,
 				metadata       = d.metadata,
 				unsigned_txset = d.unsigned_txset,
 				signed_txset   = getattr(d, 'signed_txset', None),
-				complete       = self.name in ('NewSigned', 'NewSubmitted'))
+				complete       = self.is_complete)
 
 	class NewUnsigned(New):
 		desc = 'unsigned transaction'
 		ext = 'rawtx'
-		signed = False
+		is_new = True
 
-	class NewSigned(New):
+	class NewColdSigned(New):
 		desc = 'signed transaction'
 		ext = 'sigtx'
+		is_signing = True
 		signed = True
 
-	class NewColdSigned(NewSigned):
-		pass
+	class NewSigned(NewColdSigned):
+		is_new = True
+		is_complete = True
 
-	class NewSubmitted(NewColdSigned):
+	class NewSubmitted(New):
 		desc = 'submitted transaction'
 		ext = 'subtx'
+		signed = True
+		is_submitting = True
+		is_complete = True
 
 	class Completed(Base):
 		desc = 'transaction'
@@ -268,7 +275,7 @@ class MoneroMMGenTX:
 
 			if self.name not in ('View', 'Completed'):
 				assert fn.name.endswith('.' + self.ext), (
-					'TX file {fn} has incorrect extension (not {self.ext!r})')
+					f'TX file {fn} has incorrect extension (not {self.ext!r})')
 				assert getattr(d, self.req_field), (
 					f'{self.name} TX missing required field {self.req_field!r}')
 				assert bool(d.sign_time) == self.signed, '{a} has {b}sign time!'.format(

+ 2 - 1
mmgen/xmrwallet/include.py

@@ -77,7 +77,8 @@ class XMRWalletAddrSpec(HiliteStr, InitErrors, MMGenObject):
 				me = str.__new__(cls, f'{arg1}:{arg2}:{arg3}')
 				for arg in [arg1, arg2] + ([] if arg3 is None else [arg3]):
 					assert isinstance(arg, int), f'{arg}: XMRWalletAddrSpec component not of type int'
-					assert arg is None or arg <= 9999, f'{arg}: XMRWalletAddrSpec component greater than 9999'
+					assert arg is None or arg <= 9999, (
+						f'{arg}: XMRWalletAddrSpec component greater than 9999')
 				me.wallet = AddrIdx(arg1)
 				me.account = arg2
 				me.account_address = arg3

+ 5 - 6
mmgen/xmrwallet/ops/__init__.py

@@ -41,6 +41,9 @@ class OpBase:
 					break
 
 		self.cfg = cfg
+		self.uargs = uarg_tuple
+		self.compat_call = self.uargs.compat_call
+
 		classes = tuple(gen_classes())
 		self.opts = tuple(set(opt for cls in classes for opt in xmrwallet.opts))
 
@@ -49,8 +52,6 @@ class OpBase:
 
 		global fmt_amt, hl_amt, addr_width
 
-		self.uargs = uarg_tuple
-
 		def fmt_amt(amt):
 			return self.proto.coin_amt(amt, from_unit='atomic').fmt(5, prec=12, color=True)
 		def hl_amt(amt):
@@ -83,15 +84,13 @@ class OpBase:
 				die(1, '{!r}: invalid value for --{}: it must have format {!r}'.format(
 					val,
 					name.replace('_', '-'),
-					uarg_info[name].annot
-				))
+					uarg_info[name].annot))
 
 		for attr in self.cfg.__dict__:
 			if attr in xmrwallet.opts and not attr in self.opts:
 				die(1, 'Option --{} not supported for {!r} operation'.format(
 					attr.replace('_', '-'),
-					self.name,
-				))
+					self.name))
 
 		for opt in xmrwallet.pat_opts:
 			if getattr(self.cfg, opt, None):

+ 1 - 2
mmgen/xmrwallet/ops/export.py

@@ -50,8 +50,7 @@ class OpExportOutputs(OpWallet):
 			parent    = self,
 			wallet_fn = fn,
 			data      = self.c.call('export_outputs', all=True),
-			sign      = self.sign,
-		)
+			sign      = self.sign)
 		m.write()
 		return True
 

+ 4 - 1
mmgen/xmrwallet/ops/import.py

@@ -61,7 +61,10 @@ class OpImportKeyImages(OpWallet):
 		die(2, f'No signed key image files found{rw_msg}!')
 
 	async def process_wallet(self, d, fn, last):
-		keyimage_fn = MoneroWalletOutputsFile.Signed.find_fn_from_wallet_fn(self.cfg, fn, ret_on_no_match=True)
+		keyimage_fn = MoneroWalletOutputsFile.Signed.find_fn_from_wallet_fn(
+			self.cfg,
+			fn,
+			ret_on_no_match = True)
 		if not keyimage_fn:
 			msg(f'No signed key image file found for wallet #{d.idx}')
 			return False

+ 1 - 2
mmgen/xmrwallet/ops/new.py

@@ -47,8 +47,7 @@ class OpNew(OpMixinSpec, OpWallet):
 					a = desc,
 					b = red(str(self.source.idx)),
 					c = '' if desc == 'account' else f', account {red("#"+str(self.account))}',
-					d = 'label ' + pink('‘'+label+'’') if label else 'empty label')
-				):
+					d = 'label ' + pink('‘'+label+'’') if label else 'empty label')):
 
 			if desc == 'address':
 				h.create_new_addr(self.account, label=label)

+ 4 - 6
mmgen/xmrwallet/ops/restore.py

@@ -39,9 +39,9 @@ class OpRestore(OpCreate):
 			match tuple(gen()):
 				case [dump_fn, *rest]:
 					if rest:
-						ymsg(f"Warning: more than one dump file found for '{fn}' - using the first!")
+						ymsg(f'Warning: more than one dump file found for ‘{fn}’ - using the first!')
 				case _:
-					die(1, f"No suitable dump file found for '{fn}'")
+					die(1, f'No suitable dump file found for ‘{fn}’')
 
 			return MoneroWalletDumpFile.Completed(
 				parent = self,
@@ -69,14 +69,12 @@ class OpRestore(OpCreate):
 					self.c.call(
 						'label_address',
 						index = {'major': acct_idx, 'minor': addr_idx},
-						label = addr_data['label'],
-					)
+						label = addr_data['label'])
 
 		def make_format_str():
 			return '    acct {:O>%s}, addr {:O>%s} [{}]' % (
 				len(str(len(data) - 1)),
-				len(str(max(len(acct_data['addresses']) for acct_data in data) - 1))
-			)
+				len(str(max(len(acct_data['addresses']) for acct_data in data) - 1)))
 
 		def check_restored_data():
 			restored_data = h.get_wallet_data(print=False).addrs_data

+ 2 - 4
mmgen/xmrwallet/ops/sign.py

@@ -33,13 +33,11 @@ class OpSign(OpWallet):
 			'sign_transfer',
 			unsigned_txset = tx.data.unsigned_txset,
 			export_raw = True,
-			get_tx_keys = True
-		)
+			get_tx_keys = True)
 		new_tx = MoneroMMGenTX.NewColdSigned(
 			cfg            = self.cfg,
 			txid           = res['tx_hash_list'][0],
 			unsigned_txset = None,
 			signed_txset   = res['signed_txset'],
-			_in_tx         = tx,
-		)
+			_in_tx         = tx)
 		return new_tx

+ 3 - 1
mmgen/xmrwallet/ops/spec.py

@@ -36,7 +36,9 @@ class OpMixinSpec:
 					try:
 						res = self.kal.entry(idx)
 					except:
-						die(1, f'Supplied key-address file does not contain address {self.kal.al_id.sid}:{idx}')
+						die(1,
+							'Supplied key-address file does not contain address '
+							f'{self.kal.al_id.sid}:{idx}')
 					else:
 						setattr(self, k, res)
 						yield res

+ 6 - 10
mmgen/xmrwallet/ops/submit.py

@@ -56,8 +56,7 @@ class OpSubmit(OpWallet):
 			proto       = self.proto,
 			wallet_dir  = self.cfg.wallet_dir or '.',
 			test_suite  = self.cfg.test_suite,
-			monerod_addr = relay_opt[1],
-		)
+			monerod_addr = relay_opt[1])
 
 		u = wd.usr_daemon_args = []
 		if self.cfg.test_suite:
@@ -68,8 +67,7 @@ class OpSubmit(OpWallet):
 		return MoneroWalletRPCClient(
 			cfg             = self.cfg,
 			daemon          = wd,
-			test_connection = False,
-		)
+			test_connection = False)
 
 	async def main(self):
 		tx = self.tx
@@ -104,8 +102,8 @@ class OpSubmit(OpWallet):
 
 		new_tx = MoneroMMGenTX.NewSubmitted(
 			cfg          = self.cfg,
-			_in_tx       = tx,
-		)
+			_in_tx       = tx)
+
 		gmsg('\nOK')
 		new_tx.write(
 			ask_write     = not self.cfg.autosign,
@@ -122,10 +120,8 @@ class OpResubmit(OpSubmit):
 	def get_tx(self):
 		from ...autosign import Signable
 		fns = Signable.xmr_transaction(self.asi).get_submitted()
-		return sorted(
-			(MoneroMMGenTX.Submitted(self.cfg, Path(fn)) for fn in fns),
-				key = lambda x: getattr(x.data, 'submit_time', None) or x.data.create_time
-		)[-1]
+		return sorted((MoneroMMGenTX.Submitted(self.cfg, Path(fn)) for fn in fns),
+			key = lambda x: getattr(x.data, 'submit_time', None) or x.data.create_time)[-1]
 
 class OpAbort(OpBase):
 	opts = ('watch_only', 'autosign')

+ 1 - 2
mmgen/xmrwallet/ops/txview.py

@@ -41,8 +41,7 @@ class OpTxview(OpBase):
 		txs = sorted(
 			(MoneroMMGenTX.View(self.cfg, Path(fn)) for fn in files),
 				# old TX files have no ‘submit_time’ field:
-				key = lambda x: getattr(x.data, 'submit_time', None) or x.data.create_time
-		)
+				key = lambda x: getattr(x.data, 'submit_time', None) or x.data.create_time)
 
 		if self.cfg.autosign:
 			self.asi.do_umount()

+ 3 - 1
mmgen/xmrwallet/ops/view.py

@@ -58,7 +58,9 @@ class OpView(OpSync):
 		wallet_height = self.c.call('get_height')['height']
 		msg(f'  Wallet height: {wallet_height}')
 
-		self.wallets_data[fn.name] = MoneroWalletRPC(self, d).get_wallet_data(print=False, skip_empty_ok=True)
+		self.wallets_data[fn.name] = MoneroWalletRPC(self, d).get_wallet_data(
+			print = False,
+			skip_empty_ok = True)
 
 		if not last:
 			self.c.call('close_wallet')

+ 10 - 4
test/cmdtest_d/xmr_autosign.py

@@ -365,9 +365,9 @@ class CmdTestXMRAutosign(CmdTestXMRWallet, CmdTestAutosignThreaded):
 
 	async def resubmit_transfer_tx1(self):
 		return await self._submit_transfer_tx(
-				relay_parm = self.tx_relay_daemon_proxy_parm,
-				op         = 'resubmit',
-				check_bal  = False)
+			relay_parm = self.tx_relay_daemon_proxy_parm,
+			op         = 'resubmit',
+			check_bal  = False)
 
 	async def submit_transfer_tx2(self):
 		return await self._submit_transfer_tx(relay_parm=self.tx_relay_daemon_parm)
@@ -529,6 +529,8 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 		('fund_alice_sub3',          'sending funds to Alice’s subaddress #3 (wallet #2)'),
 		('alice_twview2',            'viewing Alice’s tracking wallets (reload, sort options)'),
 		('alice_twview3',            'viewing Alice’s tracking wallets (check balances)'),
+		('alice_listaddresses2',     'listing Alice’s addresses (sort options)'),
+		('stop_daemons',             'stopping all wallet and coin daemons'),
 	)
 
 	def __init__(self, cfg, trunner, cfgs, spawn):
@@ -585,6 +587,7 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 		return self._alice_twops(
 			'listaddresses',
 			lbl_addr_num = 2,
+			lbl_addr_idx_num = 0,
 			lbl_add_timestr = True,
 			menu = 'R',
 			expect_str = r'Primary account.*1\.234567891234')
@@ -608,9 +611,12 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 			'twview',
 			expect_arr = [
 				'Total XMR: 3.722345649021 [3.729999970119]',
-				'1  0.026296296417',
+				'1         0.026296296417',
 				'0.007654321098'])
 
+	def alice_listaddresses2(self):
+		return self._alice_twops('listaddresses', menu='aAdMELLuuuraAdMeEuu')
+
 	def _alice_twops(
 			self,
 			op,