unspent.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2023 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. tw.unspent: Tracking wallet unspent outputs class for the MMGen suite
  20. """
  21. from ..globalvars import g
  22. from ..util import msg,suf,fmt
  23. from ..objmethods import MMGenObject
  24. from ..obj import (
  25. ImmutableAttr,
  26. ListItemAttr,
  27. MMGenListItem,
  28. TwComment,
  29. HexStr,
  30. CoinTxID,
  31. NonNegativeInt )
  32. from ..addr import CoinAddr
  33. from .shared import TwMMGenID,get_tw_label
  34. from .view import TwView
  35. class TwUnspentOutputs(TwView):
  36. class display_type(TwView.display_type):
  37. class squeezed(TwView.display_type.squeezed):
  38. cols = ('num','txid','vout','addr','mmid','comment','amt','amt2','date')
  39. class detail(TwView.display_type.detail):
  40. cols = ('num','txid','vout','addr','mmid','amt','amt2','block','date_time','comment')
  41. show_mmid = True
  42. no_rpcdata_errmsg = """
  43. No spendable outputs found! Import addresses with balances into your
  44. watch-only wallet using 'mmgen-addrimport' and then re-run this program.
  45. """
  46. update_widths_on_age_toggle = False
  47. print_output_types = ('detail',)
  48. mod_subpath = 'tw.unspent'
  49. class MMGenTwUnspentOutput(MMGenListItem):
  50. txid = ListItemAttr(CoinTxID)
  51. vout = ListItemAttr(NonNegativeInt)
  52. amt = ImmutableAttr(None)
  53. amt2 = ListItemAttr(None) # the ETH balance for token account
  54. comment = ListItemAttr(TwComment,reassign_ok=True)
  55. twmmid = ImmutableAttr(TwMMGenID,include_proto=True)
  56. addr = ImmutableAttr(CoinAddr,include_proto=True)
  57. confs = ImmutableAttr(int,typeconv=False)
  58. date = ListItemAttr(int,typeconv=False,reassign_ok=True)
  59. scriptPubKey = ImmutableAttr(HexStr)
  60. skip = ListItemAttr(str,typeconv=False,reassign_ok=True)
  61. def __init__(self,proto,**kwargs):
  62. self.__dict__['proto'] = proto
  63. MMGenListItem.__init__(self,**kwargs)
  64. class conv_funcs:
  65. def amt(self,value):
  66. return self.proto.coin_amt(value)
  67. def amt2(self,value):
  68. return self.proto.coin_amt(value)
  69. async def __init__(self,proto,minconf=1,addrs=[]):
  70. await super().__init__(proto)
  71. self.minconf = minconf
  72. self.addrs = addrs
  73. self.min_cols = g.min_screen_width
  74. @property
  75. def total(self):
  76. return sum(i.amt for i in self.data)
  77. def gen_data(self,rpc_data,lbl_id):
  78. for o in rpc_data:
  79. if not lbl_id in o:
  80. continue # coinbase outputs have no account field
  81. l = get_tw_label(self.proto,o[lbl_id])
  82. if l:
  83. o.update({
  84. 'twmmid': l.mmid,
  85. 'comment': l.comment or '',
  86. 'amt': self.proto.coin_amt(o['amount']),
  87. 'addr': CoinAddr(self.proto,o['address']),
  88. 'confs': o['confirmations']
  89. })
  90. yield self.MMGenTwUnspentOutput(
  91. self.proto,
  92. **{ k:v for k,v in o.items() if k in self.MMGenTwUnspentOutput.valid_attrs } )
  93. def filter_data(self):
  94. data = self.data.copy()
  95. for d in data:
  96. d.skip = ''
  97. gkeys = {'addr':'addr','twmmid':'addr','txid':'txid'}
  98. if self.group and self.sort_key in gkeys:
  99. for a,b in [(data[i],data[i+1]) for i in range(len(data)-1)]:
  100. for k in gkeys:
  101. if self.sort_key == k and getattr(a,k) == getattr(b,k):
  102. b.skip = gkeys[k]
  103. return data
  104. def get_column_widths(self,data,wide,interactive):
  105. show_mmid = self.show_mmid or wide
  106. # num txid vout addr [mmid] [comment] amt [amt2] date
  107. return self.compute_column_widths(
  108. widths = { # fixed cols
  109. 'num': max(2,len(str(len(data)))+1),
  110. 'vout': 4,
  111. 'mmid': max(len(d.twmmid.disp) for d in data) if show_mmid else 0,
  112. 'amt': self.amt_widths['amt'],
  113. 'amt2': self.amt_widths.get('amt2',0),
  114. 'block': self.age_col_params['block'][0] if wide else 0,
  115. 'date_time': self.age_col_params['date_time'][0] if wide else 0,
  116. 'date': self.age_w,
  117. 'spc': 7 if show_mmid else 5, # 7(5) spaces in fs
  118. },
  119. maxws = { # expandable cols
  120. 'txid': self.txid_w,
  121. 'addr': max(len(d.addr) for d in data),
  122. 'comment': max(d.comment.screen_width for d in data) if show_mmid else 0,
  123. },
  124. minws = {
  125. 'txid': 7,
  126. 'addr': 10,
  127. 'comment': len('Comment') if show_mmid else 0,
  128. },
  129. maxws_nice = {'txid':12, 'addr':16} if show_mmid else {'txid':12},
  130. wide = wide,
  131. interactive = interactive,
  132. )
  133. def squeezed_col_hdr(self,cw,fs,color):
  134. return fs.format(
  135. n = '',
  136. t = 'TxID',
  137. v = 'Vout',
  138. a = 'Address',
  139. m = 'MMGenID',
  140. c = 'Comment',
  141. A = 'Amt({})'.format(self.proto.dcoin),
  142. B = 'Amt({})'.format(self.proto.coin),
  143. d = self.age_hdr )
  144. def detail_col_hdr(self,cw,fs,color):
  145. return fs.format(
  146. n = '',
  147. t = 'TxID',
  148. v = 'Vout',
  149. a = 'Address',
  150. m = 'MMGenID',
  151. A = 'Amt({})'.format(self.proto.dcoin),
  152. B = 'Amt({})'.format(self.proto.coin),
  153. b = 'Block',
  154. D = 'Date/Time',
  155. c = 'Comment' )
  156. def gen_squeezed_display(self,data,cw,fs,color,fmt_method):
  157. for n,d in enumerate(data):
  158. yield fs.format(
  159. n = str(n+1) + ')',
  160. t = (d.txid.fmtc( '|' + '.'*(cw.txid-1), width=cw.txid, color=color ) if d.skip == 'txid'
  161. else d.txid.truncate( width=cw.txid, color=color )) if cw.txid else None,
  162. v = ' ' + d.vout.fmt( width=cw.vout-1, color=color ) if cw.vout else None,
  163. a = d.addr.fmtc( '|' + '.'*(cw.addr-1), width=cw.addr, color=color ) if d.skip == 'addr'
  164. else d.addr.fmt( width=cw.addr, color=color ),
  165. m = (d.twmmid.fmtc( '.'*cw.mmid, width=cw.mmid, color=color ) if d.skip == 'addr'
  166. else d.twmmid.fmt( width=cw.mmid, color=color )) if cw.mmid else None,
  167. c = d.comment.fmt2( width=cw.comment, color=color, nullrepl='-' ) if cw.comment else None,
  168. A = d.amt.fmt( color=color, iwidth=cw.iwidth, prec=self.disp_prec ),
  169. B = d.amt2.fmt( color=color, iwidth=cw.iwidth2, prec=self.disp_prec ) if cw.amt2 else None,
  170. d = self.age_disp(d,self.age_fmt),
  171. )
  172. def gen_detail_display(self,data,cw,fs,color,fmt_method):
  173. for n,d in enumerate(data):
  174. yield fs.format(
  175. n = str(n+1) + ')',
  176. t = d.txid.fmt( width=cw.txid, color=color ) if cw.txid else None,
  177. v = ' ' + d.vout.fmt( width=cw.vout-1, color=color ) if cw.vout else None,
  178. a = d.addr.fmt( width=cw.addr, color=color ),
  179. m = d.twmmid.fmt( width=cw.mmid, color=color ),
  180. A = d.amt.fmt( color=color, iwidth=cw.iwidth, prec=self.disp_prec ),
  181. B = d.amt2.fmt( color=color, iwidth=cw.iwidth2, prec=self.disp_prec ) if cw.amt2 else None,
  182. b = self.age_disp(d,'block'),
  183. D = self.age_disp(d,'date_time'),
  184. c = d.comment.fmt2( width=cw.comment, color=color, nullrepl='-' ))
  185. def display_total(self):
  186. msg('\nTotal unspent: {} {} ({} output{})'.format(
  187. self.total.hl(),
  188. self.proto.dcoin,
  189. len(self.data),
  190. suf(self.data) ))
  191. async def set_dates(self,us):
  192. if not self.dates_set:
  193. # 'blocktime' differs from 'time', is same as getblockheader['time']
  194. dates = [ o.get('blocktime',0)
  195. for o in await self.rpc.gathered_icall('gettransaction',[(o.txid,True,False) for o in us]) ]
  196. for idx,o in enumerate(us):
  197. o.date = dates[idx]
  198. self.dates_set = True
  199. class sort_action(TwView.sort_action):
  200. def s_twmmid(self,parent):
  201. parent.do_sort('twmmid')
  202. parent.show_mmid = True
  203. class display_action(TwView.display_action):
  204. def d_mmid(self,parent):
  205. parent.show_mmid = not parent.show_mmid
  206. def d_group(self,parent):
  207. if parent.can_group:
  208. parent.group = not parent.group