mmgen-tool listaddresses: fully reimplement
- reimplemented using the new tracking wallet display framework
- command is now interactive, with the same UI as 'twview' and 'txhist'
- the new 'Used' column shows whether an address has received funds
- the new tristate 'showused' filter allows display of only unused, used
or all used addresses
- adding, removal and editing of labels is supported
Testing/demo:
# Run the regtest test partially, leaving coin daemon running:
$ test/test.py -De regtest.label
# Try out the interactive sorting, filtering and label editing features:
$ PYTHONPATH=. MMGEN_TEST_SUITE=1 cmds/mmgen-tool --bob listaddresses interactive=1
# When finished, gracefully shut down the daemon:
$ test/stop-coin-daemons.py btc_rt
This commit is contained in:
parent
8e04c21271
commit
1d392f1731
12 changed files with 507 additions and 298 deletions
|
|
@ -1 +1 @@
|
|||
13.3.dev16
|
||||
13.3.dev17
|
||||
|
|
|
|||
83
mmgen/proto/btc/tw/addresses.py
Executable file
83
mmgen/proto/btc/tw/addresses.py
Executable file
|
|
@ -0,0 +1,83 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
||||
# 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
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
||||
# 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)
|
||||
|
|
@ -66,20 +66,14 @@ class BitcoinTwJSON(TwJSON):
|
|||
@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
|
||||
|
|
|
|||
68
mmgen/proto/eth/tw/addresses.py
Executable file
68
mmgen/proto/eth/tw/addresses.py
Executable file
|
|
@ -0,0 +1,68 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
||||
# 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
|
||||
|
|
@ -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 <mmgen@tuta.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
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
|
||||
|
|
@ -44,58 +44,6 @@ class tool_cmd(tool_cmd_base):
|
|||
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 <seed ID>:[<type>:]<idx list>)' )
|
||||
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 @@ class tool_cmd(tool_cmd_base):
|
|||
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
|
||||
|
|
|
|||
284
mmgen/tw/addresses.py
Executable file
284
mmgen/tw/addresses.py
Executable file
|
|
@ -0,0 +1,284 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
|
||||
# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
|
||||
# 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 <seed ID>:[<type>:]<idx list>)' )
|
||||
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
|
||||
|
|
@ -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 <mmgen@tuta.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
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())
|
||||
|
|
@ -30,7 +30,7 @@ from ..color import nocolor,yellow,green,red,blue
|
|||
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 @@ class TwCommon:
|
|||
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(),
|
||||
|
|
|
|||
|
|
@ -858,13 +858,13 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
|
|||
|
||||
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 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
|
|||
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'])
|
||||
|
|
|
|||
|
|
@ -604,10 +604,10 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
|
|||
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 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
|
|||
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 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
|
|||
|
||||
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 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
|
|||
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 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
|
|||
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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue