unspent.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2022 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,MMGenID
  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. def __new__(cls,proto,*args,**kwargs):
  42. return MMGenObject.__new__(proto.base_proto_subclass(cls,'tw.unspent'))
  43. show_mmid = True
  44. no_rpcdata_errmsg = """
  45. No spendable outputs found! Import addresses with balances into your
  46. watch-only wallet using 'mmgen-addrimport' and then re-run this program.
  47. """
  48. update_widths_on_age_toggle = False
  49. print_output_types = ('detail',)
  50. class MMGenTwUnspentOutput(MMGenListItem):
  51. txid = ListItemAttr(CoinTxID)
  52. vout = ListItemAttr(NonNegativeInt)
  53. amt = ImmutableAttr(None)
  54. amt2 = ListItemAttr(None) # the ETH balance for token account
  55. comment = ListItemAttr(TwComment,reassign_ok=True)
  56. twmmid = ImmutableAttr(TwMMGenID,include_proto=True)
  57. addr = ImmutableAttr(CoinAddr,include_proto=True)
  58. confs = ImmutableAttr(int,typeconv=False)
  59. date = ListItemAttr(int,typeconv=False,reassign_ok=True)
  60. scriptPubKey = ImmutableAttr(HexStr)
  61. skip = ListItemAttr(str,typeconv=False,reassign_ok=True)
  62. def __init__(self,proto,**kwargs):
  63. self.__dict__['proto'] = proto
  64. MMGenListItem.__init__(self,**kwargs)
  65. class conv_funcs:
  66. def amt(self,value):
  67. return self.proto.coin_amt(value)
  68. def amt2(self,value):
  69. return self.proto.coin_amt(value)
  70. async def __init__(self,proto,minconf=1,addrs=[]):
  71. await super().__init__(proto)
  72. self.minconf = minconf
  73. self.addrs = addrs
  74. self.min_cols = g.min_screen_width
  75. @property
  76. def total(self):
  77. return sum(i.amt for i in self.data)
  78. def gen_data(self,rpc_data,lbl_id):
  79. for o in rpc_data:
  80. if not lbl_id in o:
  81. continue # coinbase outputs have no account field
  82. l = get_tw_label(self.proto,o[lbl_id])
  83. if l:
  84. o.update({
  85. 'twmmid': l.mmid,
  86. 'comment': l.comment or '',
  87. 'amt': self.proto.coin_amt(o['amount']),
  88. 'addr': CoinAddr(self.proto,o['address']),
  89. 'confs': o['confirmations']
  90. })
  91. yield self.MMGenTwUnspentOutput(
  92. self.proto,
  93. **{ k:v for k,v in o.items() if k in self.MMGenTwUnspentOutput.valid_attrs } )
  94. def filter_data(self):
  95. data = self.data.copy()
  96. for d in data:
  97. d.skip = ''
  98. gkeys = {'addr':'addr','twmmid':'addr','txid':'txid'}
  99. if self.group and self.sort_key in gkeys:
  100. for a,b in [(data[i],data[i+1]) for i in range(len(data)-1)]:
  101. for k in gkeys:
  102. if self.sort_key == k and getattr(a,k) == getattr(b,k):
  103. b.skip = gkeys[k]
  104. return data
  105. def get_column_widths(self,data,wide=False):
  106. # min screen width: 80 cols
  107. # num txid vout addr [mmid] [comment] amt [amt2] date
  108. maxws_nice = {'txid': 12}
  109. if self.show_mmid:
  110. maxws_nice['addr'] = 16
  111. return self.compute_column_widths(
  112. widths = { # fixed cols
  113. 'num': max(2,len(str(len(data)))+1),
  114. 'vout': 4,
  115. 'mmid': max(len(d.twmmid.disp) for d in data) if self.show_mmid else 0,
  116. 'amt': self.disp_prec + 5,
  117. 'amt2': 0,
  118. 'block': self.age_col_params['block'][0] if wide else 0,
  119. 'date_time': self.age_col_params['date_time'][0] if wide else 0,
  120. 'date': self.age_w,
  121. 'spc': 7 if self.show_mmid else 5, # 7(5) spaces in fs
  122. },
  123. maxws = { # expandable cols
  124. 'txid': self.txid_w,
  125. 'addr': max(len(d.addr) for d in data),
  126. 'comment': max(d.comment.screen_width for d in data) if self.show_mmid else 0,
  127. },
  128. minws = {
  129. 'txid': 7,
  130. 'addr': 10,
  131. 'comment': len('Comment') if self.show_mmid else 0,
  132. },
  133. maxws_nice = maxws_nice,
  134. wide = wide,
  135. )
  136. def gen_squeezed_display(self,data,cw,hdr_fs,fs,color):
  137. yield hdr_fs.format(
  138. n = '',
  139. t = 'TxID',
  140. v = 'Vout',
  141. a = 'Address',
  142. m = 'MMGenID',
  143. c = 'Comment',
  144. A = 'Amt({})'.format(self.proto.dcoin),
  145. B = 'Amt({})'.format(self.proto.coin),
  146. d = self.age_hdr )
  147. for n,d in enumerate(data):
  148. yield fs.format(
  149. n = str(n+1) + ')',
  150. t = (CoinTxID.fmtc('|' + '.'*(cw.txid-1),color=color) if d.skip == 'txid'
  151. else d.txid.truncate(width=cw.txid,color=color)) if cw.txid else None,
  152. v = ' ' + d.vout.fmt(width=cw.vout-1,color=color) if cw.vout else None,
  153. a = type(d.addr).fmtc('|' + '.'*(cw.addr-1),width=cw.addr,color=color) if d.skip == 'addr'
  154. else d.addr.fmt(width=cw.addr,color=color),
  155. m = (MMGenID.fmtc('.'*cw.mmid,color=color) if d.skip == 'addr'
  156. else d.twmmid.fmt(width=cw.mmid,color=color)) if cw.mmid else None,
  157. c = d.comment.fmt(width=cw.comment,color=color,nullrepl='-') if cw.comment else None,
  158. A = d.amt.fmt(color=color,prec=self.disp_prec),
  159. B = d.amt2.fmt(color=color,prec=self.disp_prec) if cw.amt2 else None,
  160. d = self.age_disp(d,self.age_fmt),
  161. )
  162. def gen_detail_display(self,data,cw,hdr_fs,fs,color):
  163. yield hdr_fs.format(
  164. n = '',
  165. t = 'TxID',
  166. v = 'Vout',
  167. a = 'Address',
  168. m = 'MMGenID',
  169. A = 'Amt({})'.format(self.proto.dcoin),
  170. B = 'Amt({})'.format(self.proto.coin),
  171. b = 'Block',
  172. D = 'Date/Time',
  173. c = 'Comment' ).rstrip()
  174. for n,d in enumerate(data):
  175. yield fs.format(
  176. n = str(n+1) + ')',
  177. t = d.txid.fmt(color=color) if cw.txid else None,
  178. v = ' ' + d.vout.fmt(width=cw.vout-1,color=color) if cw.vout else None,
  179. a = d.addr.fmt(width=cw.addr,color=color),
  180. m = d.twmmid.fmt(width=cw.mmid,color=color),
  181. A = d.amt.fmt(color=color,prec=self.disp_prec),
  182. B = d.amt2.fmt(color=color,prec=self.disp_prec) if cw.amt2 else None,
  183. b = self.age_disp(d,'block'),
  184. D = self.age_disp(d,'date_time'),
  185. c = d.comment.fmt(width=cw.comment,color=color,nullrepl='-'),
  186. ).rstrip()
  187. def display_total(self):
  188. msg('\nTotal unspent: {} {} ({} output{})'.format(
  189. self.total.hl(),
  190. self.proto.dcoin,
  191. len(self.data),
  192. suf(self.data) ))
  193. async def set_dates(self,us):
  194. if not self.dates_set:
  195. # 'blocktime' differs from 'time', is same as getblockheader['time']
  196. dates = [ o.get('blocktime',0)
  197. for o in await self.rpc.gathered_icall('gettransaction',[(o.txid,True,False) for o in us]) ]
  198. for idx,o in enumerate(us):
  199. o.date = dates[idx]
  200. self.dates_set = True
  201. class action(TwView.action):
  202. def s_twmmid(self,parent):
  203. parent.do_sort('twmmid')
  204. parent.show_mmid = True
  205. def d_mmid(self,parent):
  206. parent.show_mmid = not parent.show_mmid
  207. def d_group(self,parent):
  208. if parent.can_group:
  209. parent.group = not parent.group