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

NOTE: Age sort is meaningful only for addresses with 10 or fewer confirmations.
This commit is contained in:
The MMGen Project 2025-11-27 11:34:10 +00:00
commit be17d06708
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
5 changed files with 102 additions and 19 deletions

View file

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

View file

@ -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):

View file

@ -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())

View file

@ -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)

View file

@ -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,