Browse Source

proto.xmr.tw.view: add age sort, locked balance display

NOTE: Age sort is meaningful only for addresses with 10 or fewer confirmations.
The MMGen Project 1 week ago
parent
commit
be17d06708
5 changed files with 102 additions and 19 deletions
  1. 1 1
      mmgen/data/version
  2. 22 10
      mmgen/proto/xmr/tw/unspent.py
  3. 24 7
      mmgen/proto/xmr/tw/view.py
  4. 7 1
      mmgen/tw/view.py
  5. 48 0
      test/cmdtest_d/xmr_autosign.py

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-16.1.dev16
+16.1.dev17

+ 22 - 10
mmgen/proto/xmr/tw/unspent.py

@@ -31,12 +31,13 @@ class MoneroTwUnspentOutputs(MoneroTwView, TwUnspentOutputs):
 	nice_addr_w = {'addr': 20}
 
 	prompt_fs_in = [
-		'Sort options: [a]mount, a[d]dr, [M]mgen addr, [r]everse',
+		'Sort options: [a]mount, [A]ge, a[d]dr, [M]mgen addr, [r]everse',
 		'Display options: r[e]draw screen',
 		'View/Print: pager [v]iew, [w]ide pager view, [p]rint to file{s}',
 		'Actions: [q]uit menu, add [l]abel, [R]efresh balances:']
 	extra_key_mappings = {
-		'R': 'a_sync_wallets'}
+		'R': 'a_sync_wallets',
+		'A': 's_age'}
 
 	sort_disp = {
 		'addr':   'Addr',
@@ -47,7 +48,7 @@ class MoneroTwUnspentOutputs(MoneroTwView, TwUnspentOutputs):
 	# 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: i.twmmid.sort_key, # dummy (age sort not supported)
+		'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
 
@@ -63,32 +64,36 @@ class MoneroTwUnspentOutputs(MoneroTwView, TwUnspentOutputs):
 					addr    = data['addr'],
 					confs   = data['confs'],
 					comment = data['lbl'].comment,
-					amt     = data['amt'])
+					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', 'data'])
+		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, total, d_acc = (None, None, 0, {})
+			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, d_acc)
+						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, d_acc)
+				yield ad(idx, acct_idx, total, unlocked_total, d_acc)
 		self.accts_data = tuple(gen_accts_data())
 		return super().get_disp_data()
 
@@ -129,7 +134,10 @@ class MoneroTwUnspentOutputs(MoneroTwView, TwUnspentOutputs):
 				str(n + 1) + ')',
 				d.idx.fmt(6, color=color),
 				d.acct_idx.fmt(7, color=color),
-				d.total.hl(color=color)).ljust(self.term_width)
+				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)
 
@@ -138,7 +146,11 @@ class MoneroTwUnspentOutputs(MoneroTwView, TwUnspentOutputs):
 			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.fmt(cw.iwidth, color=color, prec=self.disp_prec))
+			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):

+ 24 - 7
mmgen/proto/xmr/tw/view.py

@@ -12,6 +12,8 @@
 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 ....seed import SeedID
 from ....tw.view import TwView
@@ -32,33 +34,48 @@ class MoneroTwView:
 		if wallets_data:
 			self.sid = SeedID(sid=wallets_data[0]['seed_id'])
 
-		self.total = self.proto.coin_amt('0')
+		self.total = self.unlocked_total = self.proto.coin_amt('0')
 
 		def gen_addrs():
+			bd = namedtuple('address_balance_data', ['bal', 'unlocked_bal', 'blocks_to_unlock'])
 			for wdata in wallets_data:
 				bals_data = {i: {} for i in range(len(wdata['data'].accts_data['subaddress_accounts']))}
 
 				for d in wdata['data'].bals_data.get('per_subaddress', []):
-					bals_data[d['account_index']].update({d['address_index']: d['unlocked_balance']})
+					bals_data[d['account_index']].update({
+						d['address_index']: bd(
+							d['balance'],
+							d['unlocked_balance'],
+							d['blocks_to_unlock'])})
 
 				for acct_idx, acct_data in enumerate(wdata['data'].addrs_data):
 					for addr_data in acct_data['addresses']:
 						addr_idx = addr_data['address_index']
-						self.total += (bal := self.proto.coin_amt(
-							bals_data[acct_idx].get(addr_idx, 0),
-							from_unit = 'atomic'))
-						if self.include_empty or bal:
+						addr_bals = bals_data[acct_idx].get(addr_idx)
+						bal = self.proto.coin_amt(
+							addr_bals.bal if addr_bals else 0,
+							from_unit = 'atomic')
+						unlocked_bal = self.proto.coin_amt(
+							addr_bals.unlocked_bal if addr_bals else 0,
+							from_unit = 'atomic')
+						if bal or self.include_empty:
+							self.total += bal
+							self.unlocked_total += unlocked_bal
 							mmid = '{}:M:{}-{}/{}'.format(
 								wdata['seed_id'],
 								wdata['wallet_num'],
 								acct_idx,
 								addr_idx)
+							btu = addr_bals.blocks_to_unlock if addr_bals else 0
+							if not btu and bal != unlocked_bal:
+								btu = 12
 							yield (TwMMGenID(self.proto, mmid), {
 								'addr':    addr_data['address'],
 								'amt':     bal,
+								'unlocked_amt': unlocked_bal,
 								'recvd':   bal,
 								'is_used': addr_data['used'],
-								'confs':   1,
+								'confs':   11 - btu,
 								'lbl':     TwLabel(self.proto, mmid + ' ' + addr_data['label'])})
 
 		return dict(gen_addrs())

+ 7 - 1
mmgen/tw/view.py

@@ -458,7 +458,13 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 						make_timestr(self.rpc.cur_date))
 
 				if hasattr(self, 'total'):
-					yield 'Total {}: {}'.format(self.proto.dcoin, self.total.hl(color=color))
+					if hasattr(self, 'unlocked_total') and self.total != self.unlocked_total:
+						yield 'Total {}: {} {}'.format(
+							self.proto.dcoin,
+							self.unlocked_total.hl(color=color),
+							self.total.hl3(color_override='orange', encl='[]'))
+					else:
+						yield 'Total {}: {}'.format(self.proto.dcoin, self.total.hl(color=color))
 
 				yield from getattr(self, dt.subhdr_fmt_method)(cw, color)
 

+ 48 - 0
test/cmdtest_d/xmr_autosign.py

@@ -518,6 +518,17 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 		('fund_alice1b',             'sending funds to Alice (wallet #1)'),
 		('mine_blocks_10',           'mining some blocks'),
 		('alice_twview1',            'adding label to Alice’s tracking wallets (twview)'),
+		('new_account_alice',        'adding an account to Alice’s wallet'),
+		('new_address_alice',        'adding an address to Alice’s wallet'),
+		('new_address_alice_label',  'adding an address to Alice’s wallet (with label)'),
+		('alice_dump',               'dumping alice’s wallets to JSON format'),
+		('fund_alice_sub1',          'sending funds to Alice’s subaddress #1 (wallet #2)'),
+		('mine_blocks_1',            'mining a block'),
+		('fund_alice_sub2',          'sending funds to Alice’s subaddress #2 (wallet #2)'),
+		('mine_blocks_1',            'mining a block'),
+		('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)'),
 	)
 
 	def __init__(self, cfg, trunner, cfgs, spawn):
@@ -537,6 +548,9 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 	def create_watchonly_wallets(self):
 		return self._create_wallets()
 
+	async def mine_blocks_1(self):
+		return await self._mine_blocks(1)
+
 	async def mine_blocks_10(self):
 		return await self._mine_blocks(10)
 
@@ -544,6 +558,29 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 		self.spawn(msg_only=True)
 		return await self.mine(n)
 
+	def _new_addr_alice(self, *args):
+		return self.new_addr_alice(*args, do_autosign=True)
+
+	async def alice_dump(self):
+		t = self._xmr_autosign_op('dump')
+		t.read()
+		self.remove_device_online() # device was inserted by _xmr_autosign_op()
+		return t
+
+	async def fund_alice_sub1(self):
+		return await self._fund_alice(1, 9876543210)
+
+	async def fund_alice_sub2(self):
+		return await self._fund_alice(2, 8765432109)
+
+	async def fund_alice_sub3(self):
+		return await self._fund_alice(3, 7654321098)
+
+	async def _fund_alice(self, addr_num, amt):
+		data = json.loads(read_from_file(self.alice_dump_file))
+		addr_data = data['MoneroMMGenWalletDumpFile']['data']['wallet_metadata'][1]['addresses']
+		return await self.fund_alice(addr=addr_data[addr_num-1]['address'], amt=amt)
+
 	def alice_listaddresses1(self):
 		return self._alice_twops(
 			'listaddresses',
@@ -563,6 +600,17 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 			menu = 'R',
 			expect_str = r'New Label.*2\.469135782468')
 
+	def alice_twview2(self):
+		return self._alice_twops('twview', menu='RaAdMraAdMe')
+
+	def alice_twview3(self):
+		return self._alice_twops(
+			'twview',
+			expect_arr = [
+				'Total XMR: 3.722345649021 [3.729999970119]',
+				'1  0.026296296417',
+				'0.007654321098'])
+
 	def _alice_twops(
 			self,
 			op,