addresses.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
  4. # Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
  5. # Licensed under the GNU General Public License, Version 3:
  6. # https://www.gnu.org/licenses
  7. # Public project repositories:
  8. # https://github.com/mmgen/mmgen
  9. # https://gitlab.com/mmgen/mmgen
  10. """
  11. tw.addresses: Tracking wallet listaddresses class for the MMGen suite
  12. """
  13. from ..util import suf
  14. from ..objmethods import MMGenObject
  15. from ..obj import MMGenListItem,ImmutableAttr,ListItemAttr,TwComment,NonNegativeInt
  16. from ..addr import CoinAddr,MMGenID
  17. from ..color import red,green
  18. from .view import TwView
  19. from .shared import TwMMGenID
  20. class TwAddresses(TwView):
  21. hdr_lbl = 'tracking wallet addresses'
  22. desc = 'address list'
  23. item_desc = 'address'
  24. sort_key = 'twmmid'
  25. update_widths_on_age_toggle = True
  26. print_output_types = ('detail',)
  27. filters = ('showempty','showused','all_labels')
  28. showcoinaddrs = True
  29. showempty = True
  30. showused = 1 # tristate: 0:no, 1:yes, 2:all
  31. all_labels = False
  32. no_data_errmsg = 'No addresses in tracking wallet!'
  33. class display_type(TwView.display_type):
  34. class squeezed(TwView.display_type.squeezed):
  35. cols = ('num','mmid','used','addr','comment','amt','date')
  36. class detail(TwView.display_type.detail):
  37. cols = ('num','mmid','used','addr','comment','amt','block','date_time')
  38. class TwAddress(MMGenListItem):
  39. valid_attrs = {'twmmid','addr','al_id','confs','comment','amt','recvd','date','skip'}
  40. invalid_attrs = {'proto'}
  41. twmmid = ImmutableAttr(TwMMGenID,include_proto=True) # contains confs,txid(unused),date(unused),al_id
  42. addr = ImmutableAttr(CoinAddr,include_proto=True)
  43. al_id = ImmutableAttr(str) # set to '_' for non-MMGen addresses
  44. confs = ImmutableAttr(int,typeconv=False)
  45. comment = ListItemAttr(TwComment,reassign_ok=True)
  46. amt = ImmutableAttr(None)
  47. recvd = ImmutableAttr(None)
  48. date = ListItemAttr(int,typeconv=False,reassign_ok=True)
  49. skip = ListItemAttr(str,typeconv=False,reassign_ok=True)
  50. def __init__(self,proto,**kwargs):
  51. self.__dict__['proto'] = proto
  52. MMGenListItem.__init__(self,**kwargs)
  53. class conv_funcs:
  54. def amt(self,value):
  55. return self.proto.coin_amt(value)
  56. def recvd(self,value):
  57. return self.proto.coin_amt(value)
  58. @property
  59. def coinaddr_list(self):
  60. return [d.addr for d in self.data]
  61. def __new__(cls,proto,*args,**kwargs):
  62. return MMGenObject.__new__(proto.base_proto_subclass(cls,'tw','addresses'))
  63. async def __init__(self,proto,minconf=1,mmgen_addrs='',get_data=False):
  64. await super().__init__(proto)
  65. self.minconf = NonNegativeInt(minconf)
  66. if mmgen_addrs:
  67. a = mmgen_addrs.rsplit(':',1)
  68. if len(a) != 2:
  69. from ..util import die
  70. die(1,
  71. f'{mmgen_addrs}: invalid address list argument ' +
  72. '(must be in form <seed ID>:[<type>:]<idx list>)' )
  73. from ..addrlist import AddrIdxList
  74. self.usr_addr_list = [MMGenID(self.proto,f'{a[0]}:{i}') for i in AddrIdxList(a[1])]
  75. else:
  76. self.usr_addr_list = []
  77. if get_data:
  78. await self.get_data()
  79. @property
  80. def no_rpcdata_errmsg(self):
  81. return 'No addresses {}found!'.format(
  82. f'with {self.minconf} confirmations ' if self.minconf else '')
  83. async def gen_data(self,rpc_data,lbl_id):
  84. return (
  85. self.TwAddress(
  86. self.proto,
  87. twmmid = twmmid,
  88. addr = data['addr'],
  89. al_id = getattr(twmmid.obj,'al_id','_'),
  90. confs = data['confs'],
  91. comment = data['lbl'].comment,
  92. amt = data['amt'],
  93. recvd = data['recvd'],
  94. date = 0,
  95. skip = '' )
  96. for twmmid,data in rpc_data.items()
  97. )
  98. def filter_data(self):
  99. if self.usr_addr_list:
  100. return (d for d in self.data if d.twmmid.obj in self.usr_addr_list)
  101. else:
  102. return (d for d in self.data if
  103. (self.all_labels and d.comment) or
  104. (self.showused == 2 and d.recvd) or
  105. (not (d.recvd and not self.showused) and (d.amt or self.showempty))
  106. )
  107. def get_column_widths(self,data,wide=False):
  108. return self.compute_column_widths(
  109. widths = { # fixed cols
  110. 'num': max(2,len(str(len(data)))+1),
  111. 'mmid': max(len(d.twmmid.disp) for d in data),
  112. 'used': 4,
  113. 'amt': self.disp_prec + 5,
  114. 'date': self.age_w if self.has_age else 0,
  115. 'block': self.age_col_params['block'][0] if wide and self.has_age else 0,
  116. 'date_time': self.age_col_params['date_time'][0] if wide and self.has_age else 0,
  117. 'spc': 7, # 6 spaces between cols + 1 leading space in fs
  118. },
  119. maxws = { # expandable cols
  120. 'addr': max(len(d.addr) for d in data) if self.showcoinaddrs else 0,
  121. 'comment': max(d.comment.screen_width for d in data),
  122. },
  123. minws = {
  124. 'addr': 12 if self.showcoinaddrs else 0,
  125. 'comment': len('Comment'),
  126. },
  127. maxws_nice = {'addr': 18},
  128. wide = wide,
  129. )
  130. def subheader(self,color):
  131. if self.minconf:
  132. return f'Displaying balances with at least {self.minconf} confirmation{suf(self.minconf)}\n'
  133. else:
  134. return ''
  135. def gen_squeezed_display(self,data,cw,hdr_fs,fs,color):
  136. yield hdr_fs.format(
  137. n = '',
  138. m = 'MMGenID',
  139. u = 'Used',
  140. a = 'Address',
  141. c = 'Comment',
  142. A = 'Balance',
  143. d = self.age_hdr )
  144. yes,no = (red('Yes '),green('No ')) if color else ('Yes ','No ')
  145. id_save = data[0].al_id
  146. for n,d in enumerate(data,1):
  147. if id_save != d.al_id:
  148. id_save = d.al_id
  149. yield ''
  150. yield fs.format(
  151. n = str(n) + ')',
  152. m = d.twmmid.fmt(width=cw.mmid,color=color),
  153. u = yes if d.recvd else no,
  154. a = d.addr.fmt(color=color,width=cw.addr),
  155. c = d.comment.fmt(width=cw.comment,color=color,nullrepl='-'),
  156. A = d.amt.fmt(color=color,prec=self.disp_prec),
  157. d = self.age_disp( d, self.age_fmt )
  158. )
  159. def gen_detail_display(self,data,cw,hdr_fs,fs,color):
  160. yield hdr_fs.format(
  161. n = '',
  162. m = 'MMGenID',
  163. u = 'Used',
  164. a = 'Address',
  165. c = 'Comment',
  166. A = 'Balance',
  167. b = 'Block',
  168. D = 'Date/Time' ).rstrip()
  169. yes,no = (red('Yes '),green('No ')) if color else ('Yes ','No ')
  170. id_save = data[0].al_id
  171. for n,d in enumerate(data,1):
  172. if id_save != d.al_id:
  173. id_save = d.al_id
  174. yield ''
  175. yield fs.format(
  176. n = str(n) + ')',
  177. m = d.twmmid.fmt(width=cw.mmid,color=color),
  178. u = yes if d.recvd else no,
  179. a = d.addr.fmt(color=color,width=cw.addr),
  180. c = d.comment.fmt(width=cw.comment,color=color,nullrepl='-'),
  181. A = d.amt.fmt(color=color,prec=self.disp_prec),
  182. b = self.age_disp( d, 'block' ),
  183. D = self.age_disp( d, 'date_time' ),
  184. ).rstrip()
  185. async def set_dates(self,addrs):
  186. if not self.dates_set:
  187. bc = self.rpc.blockcount + 1
  188. caddrs = [addr for addr in addrs if addr.confs]
  189. hashes = await self.rpc.gathered_call('getblockhash',[(n,) for n in [bc - a.confs for a in caddrs]])
  190. dates = [d['time'] for d in await self.rpc.gathered_call('getblockheader',[(h,) for h in hashes])]
  191. for idx,addr in enumerate(caddrs):
  192. addr.date = dates[idx]
  193. self.dates_set = True
  194. sort_disp = {
  195. 'age': 'AddrListID+Age',
  196. 'amt': 'AddrListID+Amt',
  197. 'twmmid': 'MMGenID',
  198. }
  199. sort_funcs = {
  200. 'age': lambda d: '{}_{}_{}'.format(
  201. d.al_id,
  202. # Hack, but OK for the foreseeable future:
  203. ('{:>012}'.format(1_000_000_000 - d.confs) if d.confs else '_'),
  204. d.twmmid.sort_key),
  205. 'amt': lambda d: '{}_{}'.format(d.al_id,d.amt),
  206. 'twmmid': lambda d: d.twmmid.sort_key,
  207. }
  208. @property
  209. def dump_fn_pfx(self):
  210. return 'listaddresses' + (f'-minconf-{self.minconf}' if self.minconf else '')
  211. class action(TwView.action):
  212. def s_amt(self,parent):
  213. parent.do_sort('amt')
  214. def d_showempty(self,parent):
  215. parent.showempty = not parent.showempty
  216. def d_showused(self,parent):
  217. parent.showused = (parent.showused + 1) % 3
  218. def d_all_labels(self,parent):
  219. parent.all_labels = not parent.all_labels