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

This commit is contained in:
The MMGen Project 2025-11-29 09:12:49 +00:00
commit acee3606af
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
4 changed files with 157 additions and 133 deletions

View file

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

View file

@ -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,126 +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} {}'
# 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)

View file

@ -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,6 +107,117 @@ 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):

View file

@ -529,6 +529,7 @@ 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)'),
)
def __init__(self, cfg, trunner, cfgs, spawn):
@ -585,6 +586,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 +610,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,