diff --git a/mmgen/data/version b/mmgen/data/version index a1110e5c..32cf1b79 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -13.3.dev16 +13.3.dev17 diff --git a/mmgen/proto/btc/tw/addresses.py b/mmgen/proto/btc/tw/addresses.py new file mode 100755 index 00000000..b5f5e5e6 --- /dev/null +++ b/mmgen/proto/btc/tw/addresses.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +proto.btc.tw.addresses: Bitcoin base protocol tracking wallet address list class +""" + +from ....tw.addresses import TwAddresses +from ....tw.common import TwLabel,get_obj +from ....util import msg,msg_r +from ....addr import CoinAddr +from ....obj import NonNegativeInt +from .common import BitcoinTwCommon + +class BitcoinTwAddresses(TwAddresses,BitcoinTwCommon): + + has_age = True + prompt = """ +Sort options: [a]mt, [A]ge, [M]mid, [r]everse +Column options: toggle [D]ays/date/confs/block +Filters: show [E]mpty addrs, [u]sed addrs, all [L]abels +View/Print: pager [v]iew, [w]ide view, [p]rint +Actions: [q]uit, r[e]draw, add [l]abel: +""" + key_mappings = { + 'a':'s_amt', + 'A':'s_age', + 'M':'s_twmmid', + 'r':'d_reverse', + 'D':'d_days', + 'e':'d_redraw', + 'E':'d_showempty', + 'u':'d_showused', + 'L':'d_all_labels', + 'q':'a_quit', + 'v':'a_view', + 'w':'a_view_detail', + 'p':'a_print_detail', + 'l':'a_comment_add' } + + squeezed_fs_fs = ' {{n:>{nw}}} {{m:}} {{u:}}%s {{c:}} {{b:}} {{d:}}' + squeezed_hdr_fs_fs = ' {{n:>{nw}}} {{m:{mw}}} {{u:{uw}}}%s {{c:{cw}}} {{b:{bw}}} {{d:}}' + wide_fs_fs = ' {{n:>{nw}}} {{m:}} {{u:}} {{a:}} {{c:}} {{b:}} {{B:<{Bw}}} {{d:}}' + wide_hdr_fs_fs = ' {{n:>{nw}}} {{m:{mw}}} {{u:{uw}}} {{a:{aw}}} {{c:{cw}}} {{b:{bw}}} {{B:{Bw}}} {{d:}}' + + async def get_rpc_data(self): + + msg_r('Getting unspent outputs...') + addrs = await self.get_unspent_by_mmid(self.minconf) + msg('done') + + amt0 = self.proto.coin_amt('0') + self.total = sum((v['amt'] for v in addrs.values()), start=amt0 ) + + msg_r('Getting labels and associated addresses...') + for label,addr in await self.get_addr_label_pairs(): + if label and label.mmid not in addrs: + addrs[label.mmid] = { + 'addr': addr, + 'amt': amt0, + 'recvd': amt0, + 'confs': 0, + 'lbl': label } + msg('done') + + msg_r('Getting received funds data...') + # args: 1:minconf, 2:include_empty, 3:include_watchonly, 4:include_immature_coinbase + for d in await self.rpc.call( 'listreceivedbylabel', 1, False, True ): + label = get_obj( TwLabel, proto=self.proto, text=d['label'] ) + if label: + assert label.mmid in addrs, f'{label.mmid!r} not found in addrlist!' + addrs[label.mmid]['recvd'] = d['amount'] + addrs[label.mmid]['confs'] = d['confirmations'] + msg('done') + + return addrs diff --git a/mmgen/proto/btc/tw/addrs.py b/mmgen/proto/btc/tw/addrs.py deleted file mode 100755 index 2db98902..00000000 --- a/mmgen/proto/btc/tw/addrs.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env python3 -# -# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet -# Copyright (C)2013-2022 The MMGen Project -# Licensed under the GNU General Public License, Version 3: -# https://www.gnu.org/licenses -# Public project repositories: -# https://github.com/mmgen/mmgen -# https://gitlab.com/mmgen/mmgen - -""" -proto.btc.twaddrs: Bitcoin base protocol tracking wallet address list class -""" - -from ....util import msg,die -from ....obj import MMGenList -from ....addr import CoinAddr -from ....rpc import rpc_init -from ....tw.addrs import TwAddrList -from ....tw.common import get_tw_label -from .common import BitcoinTwCommon - -class BitcoinTwAddrList(TwAddrList,BitcoinTwCommon): - - has_age = True - - async def __init__(self,proto,usr_addr_list,minconf,showempty,showcoinaddrs,all_labels,wallet=None): - - self.rpc = await rpc_init(proto) - self.proto = proto - - # get balances with 'listunspent' - self.update( await self.get_unspent_by_mmid(minconf,usr_addr_list) ) - self.total = sum(v['amt'] for v in self.values()) or proto.coin_amt('0') - - # use 'listaccounts' only for empty addresses, as it shows false positive balances - if showempty or all_labels: - for label,addr in await self.get_addr_label_pairs(): - if (not label - or (all_labels and not showempty and not label.comment) - or (usr_addr_list and (label.mmid not in usr_addr_list)) ): - continue - if label.mmid not in self: - self[label.mmid] = { 'amt':proto.coin_amt('0'), 'lbl':label, 'addr':'' } - if showcoinaddrs: - self[label.mmid]['addr'] = CoinAddr(proto,addr) diff --git a/mmgen/proto/btc/tw/json.py b/mmgen/proto/btc/tw/json.py index 6eff0a60..53208286 100755 --- a/mmgen/proto/btc/tw/json.py +++ b/mmgen/proto/btc/tw/json.py @@ -66,20 +66,14 @@ class Export(TwJSON.Export,Base): @property async def addrlist(self): if not hasattr(self,'_addrlist'): - from .addrs import TwAddrList - self._addrlist = await TwAddrList( - proto = self.proto, - usr_addr_list = None, - minconf = 0, - showempty = True, - showcoinaddrs = True, - all_labels = False ) + from .addresses import TwAddresses + self._addrlist = await TwAddresses(self.proto,get_data=True) return self._addrlist - async def get_entries(self): + async def get_entries(self): # TODO: include 'received' field return sorted( - [self.entry_tuple(v['lbl'].mmid, v['addr'], v['amt'], v['lbl'].comment) - for v in (await self.addrlist).values()], + [self.entry_tuple(d.twmmid.obj, d.addr, d.amt, d.comment) + for d in (await self.addrlist).data], key = lambda x: x.mmgen_id.sort_key ) @property diff --git a/mmgen/proto/eth/tw/addresses.py b/mmgen/proto/eth/tw/addresses.py new file mode 100755 index 00000000..1921a9b1 --- /dev/null +++ b/mmgen/proto/eth/tw/addresses.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +proto.eth.tw.addresses: Ethereum base protocol tracking wallet address list class +""" + +from ....tw.addresses import TwAddresses +from ....tw.ctl import TrackingWallet +from ....addr import CoinAddr +from .common import EthereumTwCommon + +class EthereumTwAddresses(TwAddresses,EthereumTwCommon): + + has_age = False + prompt = """ +Sort options: [a]mt, [M]mid, [r]everse +Filters: show [E]mpty addrs, all [L]abels +View/Print: pager [v]iew, [w]ide view, [p]rint +Actions: [q]uit, r[e]draw, [D]elete address, add [l]abel: +""" + key_mappings = { + 'a':'s_amt', + 'M':'s_twmmid', + 'r':'d_reverse', + 'e':'d_redraw', + 'E':'d_showempty', + 'L':'d_all_labels', + 'q':'a_quit', + 'l':'a_comment_add', + 'D':'a_addr_delete', + 'v':'a_view', + 'w':'a_view_detail', + 'p':'a_print_detail' } + + squeezed_fs_fs = ' {{n:>{nw}}} {{m:}}%s {{c:}} {{b:}}' + squeezed_hdr_fs_fs = ' {{n:>{nw}}} {{m:{mw}}}%s {{c:{cw}}} {{b:{bw}}}' + wide_fs_fs = ' {{n:>{nw}}} {{m:}} {{a:}} {{c:}} {{b:}}' + wide_hdr_fs_fs = ' {{n:>{nw}}} {{m:{mw}}} {{a:{aw}}} {{c:{cw}}} {{b:{bw}}}' + + async def get_rpc_data(self): + + amt0 = self.proto.coin_amt('0') + self.total = amt0 + self.minconf = None + addrs = {} + + for label,addr in await self.get_addr_label_pairs(): + bal = await self.wallet.get_balance(addr) + addrs[label.mmid] = { + 'addr': addr, + 'amt': bal, + 'recvd': amt0, + 'confs': 0, + 'lbl': label } + self.total += bal + + return addrs + +class EthereumTokenTwAddresses(EthereumTwAddresses): + pass diff --git a/mmgen/proto/eth/tw/addrs.py b/mmgen/proto/eth/tw/addrs.py deleted file mode 100755 index a058cb9d..00000000 --- a/mmgen/proto/eth/tw/addrs.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 -# -# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution -# Copyright (C)2013-2022 The MMGen Project -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -proto.eth.twaddrs: Ethereum tracking wallet address list class -""" - -from ....tw.addrs import TwAddrList - -class EthereumTwAddrList(TwAddrList): - - has_age = False - - async def __init__(self,proto,usr_addr_list,minconf,showempty,showcoinaddrs,all_labels,wallet=None): - - from ....tw.common import TwLabel - from ....tw.ctl import TrackingWallet - from ....addr import CoinAddr - - self.proto = proto - self.wallet = wallet or await TrackingWallet(self.proto,mode='w') - tw_dict = self.wallet.mmid_ordered_dict - self.total = self.proto.coin_amt('0') - - for mmid,d in list(tw_dict.items()): -# if d['confirmations'] < minconf: continue # cannot get confirmations for eth account - label = TwLabel(self.proto,mmid+' '+d['comment']) - if usr_addr_list and (label.mmid not in usr_addr_list): - continue - bal = await self.wallet.get_balance(d['addr']) - if bal == 0 and not showempty: - if not label.comment or not all_labels: - continue - self[label.mmid] = {'amt': self.proto.coin_amt('0'), 'lbl': label } - if showcoinaddrs: - self[label.mmid]['addr'] = CoinAddr(self.proto,d['addr']) - self[label.mmid]['lbl'].mmid.confs = None - self[label.mmid]['amt'] += bal - self.total += bal - - del self.wallet - -class EthereumTokenTwAddrList(EthereumTwAddrList): - pass diff --git a/mmgen/tool/rpc.py b/mmgen/tool/rpc.py index 7bf9e899..00b41859 100755 --- a/mmgen/tool/rpc.py +++ b/mmgen/tool/rpc.py @@ -44,58 +44,6 @@ async def getbalance(self, from ..tw.bal import TwGetBalance return (await TwGetBalance(self.proto,minconf,quiet)).format() - async def listaddress(self, - mmgen_addr:str, - minconf: 'minimum number of confirmations' = 1, - showcoinaddr: 'display coin address in addition to MMGen ID' = True, - age_fmt: 'format for the Age/Date column ' + options_annot_str(TwCommon.age_fmts) = 'confs' ): - "list the specified MMGen address in the tracking wallet and its balance" - - return await self.listaddresses( - mmgen_addrs = mmgen_addr, - minconf = minconf, - showcoinaddrs = showcoinaddr, - age_fmt = age_fmt ) - - async def listaddresses(self, - mmgen_addrs: 'hyphenated range or comma-separated list of addresses' = '', - minconf: 'minimum number of confirmations' = 1, - pager: 'send output to pager' = False, - showcoinaddrs:'display coin addresses in addition to MMGen IDs' = True, - showempty: 'show addresses with no balances' = True, - all_labels: 'show all addresses with labels' = False, - age_fmt: 'format for the Age/Date column ' + options_annot_str(TwCommon.age_fmts) = 'confs', - sort: 'address sort order ' + options_annot_str(['reverse','age']) = '' ): - "list MMGen addresses in the tracking wallet and their balances" - - show_age = bool(age_fmt) - - if sort: - sort = set(sort.split(',')) - sort_params = {'reverse','age'} - if not sort.issubset( sort_params ): - from ..util import die - die(1,"The sort option takes the following parameters: '{}'".format( "','".join(sort_params) )) - - usr_addr_list = [] - if mmgen_addrs: - a = mmgen_addrs.rsplit(':',1) - if len(a) != 2: - from ..util import die - die(1, - f'{mmgen_addrs}: invalid address list argument ' + - '(must be in form :[:])' ) - from ..addr import MMGenID - from ..addrlist import AddrIdxList - usr_addr_list = [MMGenID(self.proto,f'{a[0]}:{i}') for i in AddrIdxList(a[1])] - - from ..tw.addrs import TwAddrList - al = await TwAddrList( self.proto, usr_addr_list, minconf, showempty, showcoinaddrs, all_labels ) - if not al: - from ..util import die - die(0,('No tracked addresses with balances!','No tracked addresses!')[showempty]) - return await al.format( showcoinaddrs, sort, show_age, age_fmt or 'confs' ) - async def twops(self, obj,pager,reverse,detail,sort,age_fmt,interactive, **kwargs ): @@ -148,6 +96,47 @@ async def txhist(self, return await self.twops( obj,pager,reverse,detail,sort,age_fmt,interactive ) + async def listaddress(self, + mmgen_addr:str, + wide: 'display data in wide tabular format' = False, + minconf: 'minimum number of confirmations' = 1, + showcoinaddr: 'display coin address in addition to MMGen ID' = True, + age_fmt: 'format for the Age/Date column ' + options_annot_str(TwCommon.age_fmts) = 'confs' ): + "list the specified MMGen address in the tracking wallet and its balance" + + return await self.listaddresses( + mmgen_addrs = mmgen_addr, + wide = wide, + minconf = minconf, + showcoinaddrs = showcoinaddr, + age_fmt = age_fmt ) + + async def listaddresses(self, + pager: 'send output to pager' = False, + reverse: 'reverse order of unspent outputs' = False, + wide: 'display data in wide tabular format' = False, + minconf: 'minimum number of confirmations' = 1, + sort: 'address sort order ' + options_annot_str(['reverse','mmid','addr','amt']) = '', + age_fmt: 'format for the Age/Date column ' + options_annot_str(TwCommon.age_fmts) = 'confs', + interactive: 'enable interactive operation' = False, + mmgen_addrs: 'hyphenated range or comma-separated list of addresses' = '', + showcoinaddrs:'display coin addresses in addition to MMGen IDs' = True, + showempty: 'show addresses with no balances' = True, + showused: 'show used addresses (tristate: 0=no, 1=yes, 2=all)' = 1, + all_labels: 'show all addresses with labels' = False ): + "list MMGen addresses in the tracking wallet and their balances" + + assert showused in (0,1,2), f"‘showused’ must have a value of 0, 1 or 2" + + from ..tw.addresses import TwAddresses + obj = await TwAddresses(self.proto,minconf=minconf,mmgen_addrs=mmgen_addrs) + return await self.twops( + obj,pager,reverse,wide,sort,age_fmt,interactive, + showcoinaddrs = showcoinaddrs, + showempty = showempty, + showused = showused, + all_labels = all_labels ) + async def add_label(self,mmgen_or_coin_addr:str,label:str): "add descriptive label for address in tracking wallet" from ..tw.ctl import TrackingWallet diff --git a/mmgen/tw/addresses.py b/mmgen/tw/addresses.py new file mode 100755 index 00000000..7915d247 --- /dev/null +++ b/mmgen/tw/addresses.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +tw.addresses: Tracking wallet listaddresses class for the MMGen suite +""" + +from collections import namedtuple + +from ..util import suf +from ..base_obj import AsyncInit +from ..objmethods import MMGenObject +from ..obj import MMGenList,MMGenListItem,ImmutableAttr,ListItemAttr,TwComment,NonNegativeInt +from ..rpc import rpc_init +from ..addr import CoinAddr,MMGenID +from ..color import red,green +from .common import TwCommon,TwMMGenID + +class TwAddresses(MMGenObject,TwCommon,metaclass=AsyncInit): + + hdr_lbl = 'tracking wallet addresses' + desc = 'address list' + item_desc = 'address' + txid_w = 64 + sort_key = 'twmmid' + age_fmts_interactive = ('confs','block','days','date','date_time') + update_widths_on_age_toggle = True + print_output_types = ('detail',) + filters = ('showempty','showused','all_labels') + showcoinaddrs = True + showempty = True + showused = 1 # tristate: 0:no, 1:yes, 2:all + all_labels = False + no_data_errmsg = 'No addresses in tracking wallet!' + + class TwAddress(MMGenListItem): + valid_attrs = {'twmmid','addr','al_id','confs','comment','amt','recvd','date','skip'} + invalid_attrs = {'proto'} + + twmmid = ImmutableAttr(TwMMGenID,include_proto=True) # contains confs,txid(unused),date(unused),al_id + addr = ImmutableAttr(CoinAddr,include_proto=True) + al_id = ImmutableAttr(str) # set to '_' for non-MMGen addresses + confs = ImmutableAttr(int,typeconv=False) + comment = ListItemAttr(TwComment,reassign_ok=True) + amt = ImmutableAttr(None) + recvd = ImmutableAttr(None) + date = ListItemAttr(int,typeconv=False,reassign_ok=True) + skip = ListItemAttr(str,typeconv=False,reassign_ok=True) + + def __init__(self,proto,**kwargs): + self.__dict__['proto'] = proto + MMGenListItem.__init__(self,**kwargs) + + class conv_funcs: + def amt(self,value): + return self.proto.coin_amt(value) + def recvd(self,value): + return self.proto.coin_amt(value) + + @property + def coinaddr_list(self): + return [d.addr for d in self.data] + + def __new__(cls,proto,*args,**kwargs): + return MMGenObject.__new__(proto.base_proto_subclass(cls,'tw','addresses')) + + async def __init__(self,proto,minconf=1,mmgen_addrs='',wallet=None,get_data=False): + + self.proto = proto + self.minconf = NonNegativeInt(minconf) + self.usr_addr_list = [] + self.rpc = await rpc_init(proto) + + from .ctl import TrackingWallet + self.wallet = wallet or await TrackingWallet(proto,mode='w') + + if mmgen_addrs: + a = mmgen_addrs.rsplit(':',1) + if len(a) != 2: + from ..util import die + die(1, + f'{mmgen_addrs}: invalid address list argument ' + + '(must be in form :[:])' ) + from ..addrlist import AddrIdxList + self.usr_addr_list = [MMGenID(self.proto,f'{a[0]}:{i}') for i in AddrIdxList(a[1])] + + if get_data: + await self.get_data() + + @property + def no_rpcdata_errmsg(self): + return 'No addresses {}found!'.format( + f'with {self.minconf} confirmations ' if self.minconf else '') + + async def gen_data(self,rpc_data,lbl_id): + return ( + self.TwAddress( + self.proto, + twmmid = twmmid, + addr = data['addr'], + al_id = getattr(twmmid.obj,'al_id','_'), + confs = data['confs'], + comment = data['lbl'].comment, + amt = data['amt'], + recvd = data['recvd'], + date = 0, + skip = '' ) + for twmmid,data in rpc_data.items() + ) + + def filter_data(self): + if self.usr_addr_list: + return (d for d in self.data if d.twmmid.obj in self.usr_addr_list) + else: + return (d for d in self.data if + (self.all_labels and d.comment) or + (self.showused == 2 and d.recvd) or + (not (d.recvd and not self.showused) and (d.amt or self.showempty)) + ) + + def get_column_widths(self,data,wide=False): + + return self.compute_column_widths( + widths = { # fixed cols + 'num': max(2,len(str(len(data)))+1), + 'mmid': max(len(d.twmmid.disp) for d in data), + 'used': 4, + 'amt': self.disp_prec + 5, + 'date': self.age_w, + 'spc': 7, # 6 spaces between cols + 1 leading space in fs + }, + maxws = { # expandable cols + 'addr': max(len(d.addr) for d in data), + 'comment': max(d.comment.screen_width for d in data), + }, + minws = { + 'addr': 12, + 'comment': len('Comment'), + }, + maxws_nice = {'addr': 18}, + wide = wide, + ) + + def subheader(self,color): + if self.minconf: + return f'Displaying balances with at least {self.minconf} confirmation{suf(self.minconf)}\n' + else: + return '' + + def gen_squeezed_display(self,data,cw,color): + + fs_parms = { + 'nw': cw.num, + 'mw': cw.mmid, + 'uw': cw.used, + 'aw': cw.addr, + 'cw': cw.comment, + 'bw': cw.amt, + 'dw': cw.date + } + + hdr_fs = (self.squeezed_hdr_fs_fs % ('',' {{a:{aw}}}')[self.showcoinaddrs]).format(**fs_parms) + fs = (self.squeezed_fs_fs % ('',' {{a:}}')[self.showcoinaddrs]).format(**fs_parms) + + yield hdr_fs.format( + n = '', + m = 'MMGenID', + u = 'Used', + a = 'Address', + c = 'Comment', + b = 'Balance', + d = self.age_hdr ) + + yes,no = (red('Yes '),green('No ')) if color else ('Yes ','No ') + id_save = data[0].al_id + + for n,d in enumerate(data,1): + if id_save != d.al_id: + id_save = d.al_id + yield '' + yield fs.format( + n = str(n) + ')', + m = MMGenID.fmtc(d.twmmid.disp,width=cw.mmid,color=True), + u = yes if d.recvd else no, + a = d.addr.fmt(color=True,width=cw.addr), + c = d.comment.fmt(width=cw.comment,color=True,nullrepl='-'), + b = d.amt.fmt(color=True), + d = self.age_disp( d, self.age_fmt ) + ) + + def gen_detail_display(self,data,cw,color): + + fs_parms = { + 'nw': cw.num, + 'mw': cw.mmid, + 'uw': cw.used, + 'aw': cw.addr, + 'cw': cw.comment, + 'bw': cw.amt, + 'Bw': self.age_col_params['block'][0], + 'dw': self.age_col_params['date_time'][0], + } + + hdr_fs = self.wide_hdr_fs_fs.format(**fs_parms) + fs = self.wide_fs_fs.format(**fs_parms) + + yield hdr_fs.format( + n = '', + m = 'MMGenID', + u = 'Used', + a = 'Address', + c = 'Comment', + b = 'Balance', + B = 'Block', + d = 'Date' ) + + yes,no = (red('Yes '),green('No ')) if color else ('Yes ','No ') + id_save = data[0].al_id + + for n,d in enumerate(data,1): + if id_save != d.al_id: + id_save = d.al_id + yield '' + yield fs.format( + n = str(n) + ')', + m = MMGenID.fmtc(d.twmmid.disp,width=fs_parms['mw'],color=color), + u = yes if d.recvd else no, + a = d.addr.fmt(color=color,width=fs_parms['aw']), + c = d.comment.fmt(width=fs_parms['cw'],color=color,nullrepl='-'), + b = d.amt.fmt(color=color), + B = self.age_disp( d, 'block' ), + d = self.age_disp( d, 'date_time' ), + ) + + async def set_dates(self,addrs): + if not self.dates_set: + bc = self.rpc.blockcount + 1 + caddrs = [addr for addr in addrs if addr.confs] + hashes = await self.rpc.gathered_call('getblockhash',[(n,) for n in [bc - a.confs for a in caddrs]]) + dates = [d['time'] for d in await self.rpc.gathered_call('getblockheader',[(h,) for h in hashes])] + for idx,addr in enumerate(caddrs): + addr.date = dates[idx] + self.dates_set = True + + sort_disp = { + 'age': 'AddrListID+Age', + 'amt': 'AddrListID+Amt', + 'twmmid': 'MMGenID', + } + + sort_funcs = { + 'age': lambda d: '{}_{}_{}'.format( + d.al_id, + # Hack, but OK for the foreseeable future: + ('{:>012}'.format(1_000_000_000 - d.confs) if d.confs else '_'), + d.twmmid.sort_key), + 'amt': lambda d: '{}_{}'.format(d.al_id,d.amt), + 'twmmid': lambda d: d.twmmid.sort_key, + } + + @property + def dump_fn_pfx(self): + return 'listaddresses' + (f'-minconf-{self.minconf}' if self.minconf else '') + + class action(TwCommon.action): + + def s_amt(self,parent): + parent.do_sort('amt') + + def d_showempty(self,parent): + parent.showempty = not parent.showempty + + def d_showused(self,parent): + parent.showused = (parent.showused + 1) % 3 + + def d_all_labels(self,parent): + parent.all_labels = not parent.all_labels diff --git a/mmgen/tw/addrs.py b/mmgen/tw/addrs.py deleted file mode 100755 index 1fceb364..00000000 --- a/mmgen/tw/addrs.py +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env python3 -# -# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution -# Copyright (C)2013-2022 The MMGen Project -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -twaddrs: Tracking wallet listaddresses class for the MMGen suite -""" - -from ..color import green -from ..util import msg,die -from ..base_obj import AsyncInit -from ..obj import MMGenDict,TwComment -from ..addr import CoinAddr,MMGenID -from .common import TwCommon - -class TwAddrList(MMGenDict,TwCommon,metaclass=AsyncInit): - - def __new__(cls,proto,*args,**kwargs): - return MMGenDict.__new__(proto.base_proto_subclass(cls,'tw','addrs'),*args,**kwargs) - - def raw_list(self): - return [((k if k.type == 'mmgen' else 'Non-MMGen'),self[k]['addr'],self[k]['amt']) for k in self] - - def coinaddr_list(self): - return [self[k]['addr'] for k in self] - - async def format(self,showcoinaddrs,sort,show_age,age_fmt): - if not self.has_age: - show_age = False - if age_fmt not in self.age_fmts: - die( 'BadAgeFormat', f'{age_fmt!r}: invalid age format (must be one of {self.age_fmts!r})' ) - fs = '{mid}' + ('',' {addr}')[showcoinaddrs] + ' {cmt} {amt}' + ('',' {age}')[show_age] - mmaddrs = [k for k in self.keys() if k.type == 'mmgen'] - max_mmid_len = max(len(k) for k in mmaddrs) + 2 if mmaddrs else 10 - max_cmt_width = max(max(v['lbl'].comment.screen_width for v in self.values()),7) - addr_width = max(len(self[mmid]['addr']) for mmid in self) - - max_fp_len = max([len(a.split('.')[1]) for a in [str(v['amt']) for v in self.values()] if '.' in a] or [1]) - - def sort_algo(j): - if sort and 'age' in sort: - return '{}_{:>012}_{}'.format( - j.obj.rsplit(':',1)[0], - # Hack, but OK for the foreseeable future: - (1000000000-(j.confs or 0) if hasattr(j,'confs') else 0), - j.sort_key) - else: - return j.sort_key - - mmids = sorted(self,key=sort_algo,reverse=bool(sort and 'reverse' in sort)) - if show_age: - await self.set_dates( [o for o in mmids if hasattr(o,'confs')] ) - - def gen_output(): - - if self.proto.chain_name != 'mainnet': - yield 'Chain: '+green(self.proto.chain_name.upper()) - - yield fs.format( - mid=MMGenID.fmtc('MMGenID',width=max_mmid_len), - addr=(CoinAddr.fmtc('ADDRESS',width=addr_width) if showcoinaddrs else None), - cmt=TwComment.fmtc('COMMENT',width=max_cmt_width+1), - amt='BALANCE'.ljust(max_fp_len+4), - age=age_fmt.upper(), - ).rstrip() - - al_id_save = None - for mmid in mmids: - if mmid.type == 'mmgen': - if al_id_save and al_id_save != mmid.obj.al_id: - yield '' - al_id_save = mmid.obj.al_id - mmid_disp = mmid - else: - if al_id_save: - yield '' - al_id_save = None - mmid_disp = 'Non-MMGen' - e = self[mmid] - yield fs.format( - mid=MMGenID.fmtc(mmid_disp,width=max_mmid_len,color=True), - addr=(e['addr'].fmt(color=True,width=addr_width) if showcoinaddrs else None), - cmt=e['lbl'].comment.fmt(width=max_cmt_width,color=True,nullrepl='-'), - amt=e['amt'].fmt('4.{}'.format(max(max_fp_len,3)),color=True), - age=self.age_disp(mmid,age_fmt) if show_age and hasattr(mmid,'confs') else '-' - ).rstrip() - - yield '\nTOTAL: {} {}'.format( - self.total.hl(color=True), - self.proto.dcoin ) - - return '\n'.join(gen_output()) diff --git a/mmgen/tw/common.py b/mmgen/tw/common.py index 92815d33..4b366d03 100755 --- a/mmgen/tw/common.py +++ b/mmgen/tw/common.py @@ -30,7 +30,7 @@ from ..util import msg,msg_r,fmt,die,capfirst,make_timestr from ..addr import MMGenID -# mixin class for TwUnspentOutputs,TwAddrList,TwTxHistory: +# mixin class for TwUnspentOutputs,TwAddresses,TwTxHistory: class TwCommon: dates_set = False @@ -250,10 +250,10 @@ def get_freews(cols,varws,varw,minw): def header(self,color): Blue,Green = (blue,green) if color else (nocolor,nocolor) - Yes,No = (green('yes'),red('no')) if color else ('yes','no') + Yes,No,All = (green('yes'),red('no'),yellow('all')) if color else ('yes','no','all') def fmt_filter(k): - return '{}:{}'.format(k,{0:No,1:Yes}[getattr(self,k)]) + return '{}:{}'.format(k,{0:No,1:Yes,2:All}[getattr(self,k)]) return '{h} (sort order: {s}){f}\nNetwork: {n}\nBlock {b} [{d}]\n{t}'.format( h = self.hdr_lbl.upper(), diff --git a/test/test_py_d/ts_ethdev.py b/test/test_py_d/ts_ethdev.py index 814b9d0f..c9caf248 100755 --- a/test/test_py_d/ts_ethdev.py +++ b/test/test_py_d/ts_ethdev.py @@ -858,13 +858,13 @@ def add_comment(self,comment,addr='98831F3A:E:3'): def chk_comment(self,comment_pat,addr='98831F3A:E:3'): t = self.spawn('mmgen-tool', self.eth_args + ['listaddresses','all_labels=1']) - t.expect(fr'{addr}\b.*\S{{30}}\b.*{comment_pat}\b',regex=True) + t.expect(fr'{addr}\b.*{comment_pat}',regex=True) return t def add_comment1(self): return self.add_comment(comment=tw_comment_zh) - def chk_comment1(self): return self.chk_comment(comment_pat=tw_comment_zh) + def chk_comment1(self): return self.chk_comment(comment_pat=tw_comment_zh[:3]) def add_comment2(self): return self.add_comment(comment=tw_comment_lat_cyr_gr) - def chk_comment2(self): return self.chk_comment(comment_pat=tw_comment_lat_cyr_gr) + def chk_comment2(self): return self.chk_comment(comment_pat=tw_comment_lat_cyr_gr[:3]) def remove_comment(self,addr='98831F3A:E:3'): t = self.spawn('mmgen-tool', self.eth_args + ['remove_label',addr]) @@ -1161,18 +1161,18 @@ def listaddresses1(self): def listaddresses2(self): return self.listaddresses(tool_args=['minconf=999999999']) def listaddresses3(self): - return self.listaddresses(tool_args=['sort=age']) + return self.listaddresses(tool_args=['sort=amt','reverse=1']) def listaddresses4(self): - return self.listaddresses(tool_args=['sort=age','showempty=1']) + return self.listaddresses(tool_args=['sort=age','showempty=0']) def token_listaddresses1(self): return self.listaddresses(args=['--token=mm1']) def token_listaddresses2(self): return self.listaddresses(args=['--token=mm1'],tool_args=['showempty=1']) def token_listaddresses3(self): - return self.listaddresses(args=['--token=mm1'],tool_args=['showempty=1']) + return self.listaddresses(args=['--token=mm1'],tool_args=['showempty=0']) def token_listaddresses4(self): - return self.listaddresses(args=['--token=mm2'],tool_args=['showempty=1']) + return self.listaddresses(args=['--token=mm2'],tool_args=['sort=age','reverse=1']) def twview_cached_balances(self): return self.twview(args=['--cached-balances']) diff --git a/test/test_py_d/ts_regtest.py b/test/test_py_d/ts_regtest.py index 3efa0daf..e8d5d72a 100755 --- a/test/test_py_d/ts_regtest.py +++ b/test/test_py_d/ts_regtest.py @@ -604,10 +604,10 @@ def bob_bal2d(self): return self.user_bal('bob',rtBals[0],args=['minconf=2'],skip_check=True) def bob_bal2e(self): - return self.user_bal('bob',rtBals[0],args=['showempty=1','sort=age']) + return self.user_bal('bob',rtBals[0],args=['showempty=1','sort=amt']) def bob_bal2f(self): - return self.user_bal('bob',rtBals[0],args=['showempty=1','sort=age,reverse']) + return self.user_bal('bob',rtBals[0],args=['showempty=0','sort=twmmid','reverse=1']) def bob_bal3(self): return self.user_bal('bob',rtBals[1]) @@ -1131,16 +1131,18 @@ def alice_add_comment2(self): sid = self._user_sid('alice') return self.user_add_comment('alice',sid+':C:1','Replacement Label') - def _user_chk_comment(self,user,addr,comment): - t = self.spawn('mmgen-tool',['--'+user,'listaddresses','all_labels=1']) - ret = strip_ansi_escapes(t.expect_getend(addr)).strip().split(None,1)[1] - cmp_or_die(ret[:len(comment)],comment) + def _user_chk_comment(self,user,addr,comment,extra_args=[]): + t = self.spawn('mmgen-tool',['--'+user,'listaddresses','all_labels=1']+extra_args) + ret = strip_ansi_escapes(t.expect_getend(addr)).strip().split(None,2)[2] + cmp_or_die( # squeezed display, double-width chars, so truncate to min field width + ret[:3].strip(), + comment[:3].strip()) return t def alice_add_comment_coinaddr(self): mmid = self._user_sid('alice') + (':S:1',':L:1')[self.proto.coin=='BCH'] - t = self.spawn('mmgen-tool',['--alice','listaddress',mmid],no_msg=True) - addr = [i for i in strip_ansi_escapes(t.read()).splitlines() if i.startswith(mmid)][0].split()[1] + t = self.spawn('mmgen-tool',['--alice','listaddress',mmid,'wide=true'],no_msg=True) + addr = [i for i in strip_ansi_escapes(t.read()).splitlines() if re.search(rf'\b{mmid}\b',i)][0].split()[3] return self.user_add_comment('alice',addr,'Label added using coin address of MMGen address') def alice_chk_comment_coinaddr(self): @@ -1182,7 +1184,7 @@ def alice_chk_comment1(self): def alice_chk_comment2(self): sid = self._user_sid('alice') - return self._user_chk_comment('alice',sid+':C:1','Replacement Label') + return self._user_chk_comment('alice',sid+':C:1','Replacement Label',extra_args=['age_fmt=block']) def alice_edit_comment1(self): return self.user_edit_comment('alice','4',tw_comment_lat_cyr_gr) def alice_edit_comment2(self): return self.user_edit_comment('alice','3',tw_comment_zh) @@ -1190,12 +1192,12 @@ def alice_edit_comment2(self): return self.user_edit_comment('alice','3',tw_comm def alice_chk_comment3(self): sid = self._user_sid('alice') mmid = sid + (':S:3',':L:3')[self.proto.coin=='BCH'] - return self._user_chk_comment('alice',mmid,tw_comment_lat_cyr_gr) + return self._user_chk_comment('alice',mmid,tw_comment_lat_cyr_gr,extra_args=['age_fmt=date']) def alice_chk_comment4(self): sid = self._user_sid('alice') mmid = sid + (':S:3',':L:3')[self.proto.coin=='BCH'] - return self._user_chk_comment('alice',mmid,'-') + return self._user_chk_comment('alice',mmid,'-',extra_args=['age_fmt=date_time']) def user_edit_comment(self,user,output,comment): t = self.spawn('mmgen-txcreate',['-B','--'+user,'-i']) @@ -1368,10 +1370,10 @@ def bob_msgverify_export(self): def bob_msgverify_export_single(self): sid = self._user_sid('bob') mmid = f'{sid}:{self.dfl_mmtype}:1' - args = [ '--bob', '--color=0', 'listaddress', mmid ] + args = [ '--bob', '--color=0', 'listaddress', mmid, 'wide=true' ] imsg(f'Running mmgen-tool {fmt_list(args,fmt="bare")}') t = self.spawn('mmgen-tool', args, no_msg=True) - addr = t.expect_getend(mmid).split()[0] + addr = t.expect_getend(mmid).split()[1] t.close() return self.bob_msgverify( addr = addr,