proto.xmr.tw.unspent: implement account-based view
Example:
$ mmgen-tool --coin=xmr twview interactive=1
Testing/demo:
$ test/cmdtest.py -ne xmr_compat
This commit is contained in:
parent
46c6710a0b
commit
1132d0ff6b
8 changed files with 172 additions and 35 deletions
|
|
@ -146,6 +146,7 @@ class MMGenID(HiliteStr, InitErrors, MMGenObject):
|
|||
me = str.__new__(cls, id_str)
|
||||
idx, ext = idx.split('-', 1)
|
||||
me.acct_idx, me.addr_idx = [MoneroIdx(e) for e in ext.split('/', 1)]
|
||||
me.acct_id = f'{sid}:{mmtype}:{idx}:{me.acct_idx}'
|
||||
else:
|
||||
ext = None
|
||||
me = str.__new__(cls, f'{sid}:{mmtype}:{idx}')
|
||||
|
|
@ -154,14 +155,16 @@ class MMGenID(HiliteStr, InitErrors, MMGenObject):
|
|||
me.idx = AddrIdx(idx)
|
||||
me.al_id = str.__new__(AddrListID, me.sid + ':' + me.mmtype) # checks already done
|
||||
if ext:
|
||||
me.sort_key = '{}:{}:{:0{w1}}:{:0{w2}}:{:0{w2}}'.format(
|
||||
me.acct_sort_key = '{}:{}:{:0{w1}}:{:0{w2}}'.format(
|
||||
me.sid,
|
||||
me.mmtype,
|
||||
me.idx,
|
||||
me.acct_idx,
|
||||
me.addr_idx,
|
||||
w1 = me.idx.max_digits,
|
||||
w2 = MoneroIdx.max_digits)
|
||||
me.sort_key = me.acct_sort_key + ':{:0{w2}}'.format(
|
||||
me.addr_idx,
|
||||
w2 = MoneroIdx.max_digits)
|
||||
else:
|
||||
me.sort_key = '{}:{}:{:0{w}}'.format(me.sid, me.mmtype, me.idx, w=me.idx.max_digits)
|
||||
me.proto = proto
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
16.1.dev15
|
||||
16.1.dev16
|
||||
|
|
|
|||
|
|
@ -20,3 +20,12 @@ class MoneroTwAddresses(MoneroTwView, TwAddresses):
|
|||
|
||||
include_empty = True
|
||||
has_used = True
|
||||
|
||||
prompt_fs_repl = {'XMR': (
|
||||
(1, 'Filters: show [E]mpty addrs, [u]sed addrs, all [L]abels'),
|
||||
(3, 'Actions: [q]uit menu, add [l]abel, [R]efresh balances:'))}
|
||||
extra_key_mappings = {
|
||||
'u': 'd_showused',
|
||||
'R': 'a_sync_wallets'}
|
||||
removed_key_mappings = {
|
||||
'D': 'i_addr_delete'}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,122 @@
|
|||
proto.xmr.tw.unspent: Monero protocol tracking wallet unspent outputs class
|
||||
"""
|
||||
|
||||
from ....tw.addresses import TwAddresses
|
||||
from collections import namedtuple
|
||||
|
||||
from ....tw.unspent import TwUnspentOutputs
|
||||
from ....addr import MMGenID, MoneroIdx
|
||||
from ....color import red, green
|
||||
|
||||
from .view import MoneroTwView
|
||||
|
||||
class MoneroTwUnspentOutputs(MoneroTwView, TwAddresses):
|
||||
class MoneroTwUnspentOutputs(MoneroTwView, TwUnspentOutputs):
|
||||
|
||||
hdr_lbl = 'spendable accounts'
|
||||
desc = 'address balances'
|
||||
desc = 'spendable accounts'
|
||||
item_desc = 'account'
|
||||
account_based = True
|
||||
include_empty = False
|
||||
total = None
|
||||
nice_addr_w = {'addr': 20}
|
||||
|
||||
prompt_fs_repl = {'XMR': (
|
||||
(1, 'Display options: r[e]draw screen'),
|
||||
(3, 'Actions: [q]uit menu, add [l]abel, [R]efresh balances:'))}
|
||||
extra_key_mappings = {
|
||||
'R': 'a_sync_wallets'}
|
||||
|
||||
sort_disp = {
|
||||
'addr': 'Addr',
|
||||
'age': 'Age',
|
||||
'amt': 'Amt',
|
||||
'twmmid': 'MMGenID'}
|
||||
|
||||
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)
|
||||
'amt': lambda i: '{}:{:050}'.format(i.twmmid.obj.acct_sort_key, i.amt.to_unit('atomic')),
|
||||
'twmmid': lambda i: i.twmmid.sort_key}
|
||||
|
||||
def gen_data(self, rpc_data, lbl_id):
|
||||
return (
|
||||
self.MMGenTwUnspentOutput(
|
||||
self.proto,
|
||||
twmmid = twmmid,
|
||||
addr = data['addr'],
|
||||
confs = data['confs'],
|
||||
comment = data['lbl'].comment,
|
||||
amt = data['amt'])
|
||||
for twmmid, data in rpc_data.items())
|
||||
|
||||
def get_disp_data(self):
|
||||
ad = namedtuple('accts_data', ['total', 'data'])
|
||||
bd = namedtuple('accts_data_data', ['disp_data_idx', 'data'])
|
||||
def gen_accts_data():
|
||||
acct_id_save, total, d_acc = (None, 0, {})
|
||||
for n, d in enumerate(self.data):
|
||||
m = d.twmmid.obj
|
||||
if acct_id_save != m.acct_id:
|
||||
if acct_id_save:
|
||||
yield (acct_id_save, ad(total, d_acc))
|
||||
acct_id_save = m.acct_id
|
||||
total = d.amt
|
||||
d_acc = {m.addr_idx: bd(n, d)}
|
||||
else:
|
||||
total += d.amt
|
||||
d_acc[m.addr_idx] = bd(n, d)
|
||||
if acct_id_save:
|
||||
yield (acct_id_save, ad(total, d_acc))
|
||||
self.accts_data = dict(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):
|
||||
yes, no = (red('Yes '), green('No ')) if color else ('Yes ', 'No ')
|
||||
fs_acct = '{:>4} {} {:%s} {}' % MoneroIdx.max_digits
|
||||
wallet_wid = 15
|
||||
yield ' Wallet Account Balance'.ljust(self.term_width)
|
||||
for n, (k, v) in enumerate(self.accts_data.items()):
|
||||
mmid, acct_idx = k.rsplit(':', 1)
|
||||
m = list(v.data.values())[0].data.twmmid.obj
|
||||
yield fs_acct.format(
|
||||
str(n + 1) + ')',
|
||||
MMGenID.hlc(mmid.ljust(wallet_wid), color=color),
|
||||
m.acct_idx.fmt(MoneroIdx.max_digits, color=color),
|
||||
v.total.hl(color=color)).ljust(self.term_width)
|
||||
for v in v.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),
|
||||
c = d.comment.fmt2(cw.comment, color=color, nullrepl='-'),
|
||||
A = d.amt.fmt(cw.iwidth, color=color, prec=self.disp_prec))
|
||||
|
|
|
|||
|
|
@ -21,15 +21,6 @@ class MoneroTwView:
|
|||
caps = ()
|
||||
is_remote = False
|
||||
|
||||
prompt_fs_repl = {'XMR': (
|
||||
(1, 'Filters: show [E]mpty addrs, [u]sed addrs, all [L]abels'),
|
||||
(3, 'Actions: [q]uit menu, add [l]abel, [R]efresh balances:'))}
|
||||
extra_key_mappings = {
|
||||
'u': 'd_showused',
|
||||
'R': 'a_sync_wallets'}
|
||||
removed_key_mappings = {
|
||||
'D': 'i_addr_delete'}
|
||||
|
||||
async def get_rpc_data(self):
|
||||
from mmgen.tw.shared import TwMMGenID, TwLabel
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ from ..obj import (
|
|||
TwComment,
|
||||
CoinTxID,
|
||||
NonNegativeInt)
|
||||
from ..addr import CoinAddr
|
||||
from ..addr import CoinAddr, MoneroIdx
|
||||
from ..amt import CoinAmtChk
|
||||
from .shared import TwMMGenID, TwLabel, get_tw_label
|
||||
from .view import TwView
|
||||
|
|
@ -166,6 +166,8 @@ class TwUnspentOutputs(TwView):
|
|||
'amt2': self.amt_widths.get('amt2', 0),
|
||||
'block': self.age_col_params['block'][0] if wide else 0,
|
||||
'date_time': self.age_col_params['date_time'][0] if wide else 0,
|
||||
'addr_idx': MoneroIdx.max_digits,
|
||||
'acct_idx': MoneroIdx.max_digits,
|
||||
'date': self.age_w,
|
||||
'spc': self.disp_spc + (2 * show_mmid) + self.has_amt2},
|
||||
maxws = { # expandable cols
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ from ..cfg import gv
|
|||
from ..objmethods import MMGenObject
|
||||
from ..obj import get_obj, MMGenIdx, MMGenList
|
||||
from ..color import nocolor, yellow, orange, green, red, blue
|
||||
from ..util import msg, msg_r, fmt, die, capfirst, suf, make_timestr, isAsync
|
||||
from ..util import msg, msg_r, fmt, die, capfirst, suf, make_timestr, isAsync, is_int
|
||||
from ..rpc import rpc_init
|
||||
from ..base_obj import AsyncInit
|
||||
|
||||
|
|
@ -80,6 +80,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|||
def do(method, data, cw, fs, color, fmt_method):
|
||||
return [l.rstrip() for l in method(data, cw, fs, color, fmt_method)]
|
||||
|
||||
account_based = False
|
||||
has_wallet = True
|
||||
has_amt2 = False
|
||||
dates_set = False
|
||||
|
|
@ -119,6 +120,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|||
'comment': fp('c', True, False, ' {c:%s}', ' {c}'),
|
||||
'amt': fp('A', True, False, ' {A:%s}', ' {A}'),
|
||||
'amt2': fp('B', True, False, ' {B:%s}', ' {B}'),
|
||||
'addr_idx': fp('I', True, False, ' {I:%s}', ' {I}'),
|
||||
'date': fp('d', True, True, ' {d:%s}', ' {d:<%s}'),
|
||||
'date_time': fp('D', True, True, ' {D:%s}', ' {D:%s}'),
|
||||
'block': fp('b', True, True, ' {b:%s}', ' {b:<%s}'),
|
||||
|
|
@ -700,18 +702,19 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|||
if not parent.disp_data:
|
||||
return
|
||||
|
||||
async def do_error_msg(data):
|
||||
async def do_error_msg(data, is_addr_idx):
|
||||
msg_r(
|
||||
'Choice must be a single number between {n} and {m} inclusive{s}'.format(
|
||||
n = 1,
|
||||
m = len(data),
|
||||
n = list(data.keys())[0] if is_addr_idx else 1,
|
||||
m = list(data.keys())[-1] if is_addr_idx else len(data),
|
||||
s = ' ' if parent.scroll else ''))
|
||||
if parent.scroll:
|
||||
await asyncio.sleep(1.5)
|
||||
msg_r(CUR_UP(1) + '\r' + ERASE_ALL)
|
||||
|
||||
async def get_idx(desc, data):
|
||||
async def get_idx(desc, data, *, is_addr_idx=False):
|
||||
from ..ui import line_input
|
||||
ur = namedtuple('usr_idx_data', ['idx', 'addr_idx'])
|
||||
while True:
|
||||
msg_r(parent.blank_prompt if parent.scroll else '\n')
|
||||
usr_ret = line_input(
|
||||
|
|
@ -721,13 +724,24 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|||
if parent.scroll:
|
||||
msg_r(CUR_UP(1) + '\r' + ''.ljust(parent.term_width))
|
||||
return None
|
||||
idx = get_obj(MMGenIdx, n=usr_ret, silent=True)
|
||||
if idx and idx <= len(data):
|
||||
return idx
|
||||
await do_error_msg(data)
|
||||
if is_addr_idx:
|
||||
if is_int(usr_ret) and int(usr_ret) in data:
|
||||
return ur(MMGenIdx(data[int(usr_ret)].disp_data_idx + 1), int(usr_ret))
|
||||
else:
|
||||
idx = get_obj(MMGenIdx, n=usr_ret, silent=True)
|
||||
if idx and idx <= len(data):
|
||||
return ur(idx, None)
|
||||
await do_error_msg(data, is_addr_idx)
|
||||
|
||||
async def get_idx_from_user():
|
||||
return await get_idx(f'{parent.item_desc} number', parent.disp_data)
|
||||
if parent.account_based:
|
||||
if res := await get_idx(f'{parent.item_desc} number', parent.accts_data):
|
||||
return await get_idx(
|
||||
'address index',
|
||||
list(parent.accts_data.values())[res.idx - 1].data,
|
||||
is_addr_idx = True)
|
||||
else:
|
||||
return await get_idx(f'{parent.item_desc} number', parent.disp_data)
|
||||
|
||||
while True:
|
||||
# action_method return values:
|
||||
|
|
@ -736,8 +750,8 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|||
# None: action aborted by user or no action performed
|
||||
# 'redo': user will be re-prompted for item number
|
||||
# 'redraw': action successfully performed, screen will be redrawn
|
||||
if idx := await get_idx_from_user():
|
||||
ret = await action_method(parent, idx)
|
||||
if usr_ret := await get_idx_from_user():
|
||||
ret = await action_method(parent, usr_ret.idx, usr_ret.addr_idx)
|
||||
else:
|
||||
ret = None
|
||||
if ret != 'redo':
|
||||
|
|
@ -750,7 +764,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|||
CUR_HOME + ERASE_ALL +
|
||||
await parent.format(display_type='squeezed', interactive=True, scroll=True))
|
||||
|
||||
async def i_balance_refresh(self, parent, idx):
|
||||
async def i_balance_refresh(self, parent, idx, addr_idx=None):
|
||||
if not parent.keypress_confirm(
|
||||
f'Refreshing tracking wallet {parent.item_desc} #{idx}. OK?'):
|
||||
return 'redo'
|
||||
|
|
@ -767,7 +781,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|||
if res == 0:
|
||||
return 'redraw' # zeroing balance may mess up display
|
||||
|
||||
async def i_addr_delete(self, parent, idx):
|
||||
async def i_addr_delete(self, parent, idx, addr_idx=None):
|
||||
if not parent.keypress_confirm(
|
||||
'Removing {} {} from tracking wallet. OK?'.format(
|
||||
parent.item_desc, red(f'#{idx}'))):
|
||||
|
|
@ -781,7 +795,7 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|||
parent.oneshot_msg = red('Address could not be removed')
|
||||
return False
|
||||
|
||||
async def i_comment_add(self, parent, idx):
|
||||
async def i_comment_add(self, parent, idx, addr_idx=None):
|
||||
|
||||
async def do_comment_add(comment_in):
|
||||
from ..obj import TwComment
|
||||
|
|
@ -810,8 +824,12 @@ class TwView(MMGenObject, metaclass=AsyncInit):
|
|||
return False
|
||||
|
||||
entry = parent.disp_data[idx-1]
|
||||
desc = f'{parent.item_desc} #{idx}'
|
||||
color_desc = f'{parent.item_desc} {red("#" + str(idx))}'
|
||||
if addr_idx is None:
|
||||
desc = f'{parent.item_desc} #{idx}'
|
||||
color_desc = f'{parent.item_desc} {red("#" + str(idx))}'
|
||||
else:
|
||||
desc = f'address #{addr_idx}'
|
||||
color_desc = f'address {red("#" + str(addr_idx))}'
|
||||
|
||||
cur_comment = parent.disp_data[idx-1].comment
|
||||
msg('Current label: {}'.format(cur_comment.hl() if cur_comment else '(none)'))
|
||||
|
|
|
|||
|
|
@ -508,6 +508,8 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
|
|||
('gen_kafile_miner', 'generating key-address file for Miner'),
|
||||
('create_wallet_miner', 'creating Monero wallet for Miner'),
|
||||
('mine_initial_coins', 'mining initial coins'),
|
||||
('fund_alice2', 'sending funds to Alice (wallet #2)'),
|
||||
('check_bal_alice2', 'mining, checking balance (wallet #2)'),
|
||||
('fund_alice1', 'sending funds to Alice (wallet #1)'),
|
||||
('mine_blocks', 'mining some blocks'),
|
||||
('alice_listaddresses', 'performing operations on Alice’s tracking wallets (listaddresses)'),
|
||||
|
|
@ -537,13 +539,15 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
|
|||
return self._alice_twops('listaddresses', 2, 'y', r'Primary account.*1\.234567891234')
|
||||
|
||||
def alice_twview(self):
|
||||
return self._alice_twops('twview', 1, 'n', r'New Label.*2\.469135782468')
|
||||
return self._alice_twops('twview', 1, 'n', r'New Label.*2\.469135782468', addr_idx_num=0)
|
||||
|
||||
def _alice_twops(self, op, addr_num, add_timestr_resp, expect_str):
|
||||
def _alice_twops(self, op, addr_num, add_timestr_resp, expect_str, *, addr_idx_num=None):
|
||||
self.insert_device_online()
|
||||
t = self.spawn('mmgen-tool', self.alice_opts + self.autosign_opts + [op, 'interactive=1'])
|
||||
t.expect(self.menu_prompt, 'l')
|
||||
t.expect('main menu): ', str(addr_num))
|
||||
if addr_idx_num is not None:
|
||||
t.expect('main menu): ', str(addr_idx_num))
|
||||
t.expect(': ', 'New Label\n')
|
||||
t.expect('(y/N): ', add_timestr_resp)
|
||||
t.expect(self.menu_prompt, 'R')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue