Browse Source

proto.xmr.tw.addresses: implement account-based display

The MMGen Project 6 days ago
parent
commit
acee3606af

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

@@ -29,3 +29,14 @@ class MoneroTwAddresses(MoneroTwView, TwAddresses):
 		'R': 'a_sync_wallets'}
 		'R': 'a_sync_wallets'}
 	removed_key_mappings = {
 	removed_key_mappings = {
 		'D': 'i_addr_delete'}
 		'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 - 131
mmgen/proto/xmr/tw/unspent.py

@@ -12,11 +12,6 @@
 proto.xmr.tw.unspent: Monero protocol tracking wallet unspent outputs class
 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 ....tw.unspent import TwUnspentOutputs
 
 
 from .view import MoneroTwView
 from .view import MoneroTwView
@@ -25,10 +20,8 @@ class MoneroTwUnspentOutputs(MoneroTwView, TwUnspentOutputs):
 
 
 	hdr_lbl = 'spendable accounts'
 	hdr_lbl = 'spendable accounts'
 	desc = 'spendable accounts'
 	desc = 'spendable accounts'
-	item_desc = 'account'
 	include_empty = False
 	include_empty = False
-	total = None
-	nice_addr_w = {'addr': 20}
+	has_used = False
 
 
 	prompt_fs_in = [
 	prompt_fs_in = [
 		'Sort options: [a]mount, [A]ge, a[d]dr, [M]mgen addr, [r]everse',
 		'Sort options: [a]mount, [A]ge, a[d]dr, [M]mgen addr, [r]everse',
@@ -38,126 +31,3 @@ class MoneroTwUnspentOutputs(MoneroTwView, TwUnspentOutputs):
 	extra_key_mappings = {
 	extra_key_mappings = {
 		'R': 'a_sync_wallets',
 		'R': 'a_sync_wallets',
 		'A': 's_age'}
 		'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}  {}'
-		# 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, 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)

+ 139 - 1
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 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 ....seed import SeedID
+from ....xmrwallet import op as xmrwallet_op
 from ....tw.view import TwView
 from ....tw.view import TwView
+from ....tw.unspent import TwUnspentOutputs
 
 
 class MoneroTwView:
 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:
 	class rpc:
 		caps = ()
 		caps = ()
 		is_remote = False
 		is_remote = False
@@ -80,6 +107,117 @@ class MoneroTwView:
 
 
 		return dict(gen_addrs())
 		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):
 	class action(TwView.action):
 
 
 		async def a_sync_wallets(self, parent):
 		async def a_sync_wallets(self, parent):

+ 6 - 1
test/cmdtest_d/xmr_autosign.py

@@ -529,6 +529,7 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 		('fund_alice_sub3',          'sending funds to Alice’s subaddress #3 (wallet #2)'),
 		('fund_alice_sub3',          'sending funds to Alice’s subaddress #3 (wallet #2)'),
 		('alice_twview2',            'viewing Alice’s tracking wallets (reload, sort options)'),
 		('alice_twview2',            'viewing Alice’s tracking wallets (reload, sort options)'),
 		('alice_twview3',            'viewing Alice’s tracking wallets (check balances)'),
 		('alice_twview3',            'viewing Alice’s tracking wallets (check balances)'),
+		('alice_listaddresses2',     'listing Alice’s addresses (sort options)'),
 	)
 	)
 
 
 	def __init__(self, cfg, trunner, cfgs, spawn):
 	def __init__(self, cfg, trunner, cfgs, spawn):
@@ -585,6 +586,7 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 		return self._alice_twops(
 		return self._alice_twops(
 			'listaddresses',
 			'listaddresses',
 			lbl_addr_num = 2,
 			lbl_addr_num = 2,
+			lbl_addr_idx_num = 0,
 			lbl_add_timestr = True,
 			lbl_add_timestr = True,
 			menu = 'R',
 			menu = 'R',
 			expect_str = r'Primary account.*1\.234567891234')
 			expect_str = r'Primary account.*1\.234567891234')
@@ -608,9 +610,12 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 			'twview',
 			'twview',
 			expect_arr = [
 			expect_arr = [
 				'Total XMR: 3.722345649021 [3.729999970119]',
 				'Total XMR: 3.722345649021 [3.729999970119]',
-				'1  0.026296296417',
+				'1         0.026296296417',
 				'0.007654321098'])
 				'0.007654321098'])
 
 
+	def alice_listaddresses2(self):
+		return self._alice_twops('listaddresses', menu='aAdMELLuuuraAdMeEuu')
+
 	def _alice_twops(
 	def _alice_twops(
 			self,
 			self,
 			op,
 			op,