unspent.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  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. twuo: Tracking wallet unspent outputs class for the MMGen suite
  20. """
  21. import time
  22. from collections import namedtuple
  23. from ..globalvars import g
  24. from ..color import red,yellow
  25. from ..util import (
  26. msg,
  27. die,
  28. capfirst,
  29. suf,
  30. fmt,
  31. keypress_confirm,
  32. line_input,
  33. base_proto_tw_subclass
  34. )
  35. from ..base_obj import AsyncInit
  36. from ..objmethods import MMGenObject
  37. from ..obj import ImmutableAttr,ListItemAttr,MMGenListItem,TwComment,get_obj,HexStr,CoinTxID,MMGenList
  38. from ..addr import CoinAddr,MMGenID
  39. from ..rpc import rpc_init
  40. from .common import TwCommon,TwMMGenID,get_tw_label
  41. class TwUnspentOutputs(MMGenObject,TwCommon,metaclass=AsyncInit):
  42. def __new__(cls,proto,*args,**kwargs):
  43. return MMGenObject.__new__(base_proto_tw_subclass(cls,proto,'unspent'))
  44. txid_w = 64
  45. print_hdr_fs = '{a} (block #{b}, {c} UTC)\n{d}Sort order: {e}\n{f}\n\nTotal {g}: {h}\n'
  46. no_rpcdata_errmsg = f"""
  47. No spendable outputs found! Import addresses with balances into your
  48. watch-only wallet using 'mmgen-addrimport' and then re-run this program.
  49. """
  50. class MMGenTwUnspentOutput(MMGenListItem):
  51. txid = ListItemAttr(CoinTxID)
  52. vout = ListItemAttr(int,typeconv=False)
  53. amt = ImmutableAttr(None)
  54. amt2 = ListItemAttr(None)
  55. label = 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. self.proto = proto
  72. self.data = MMGenList()
  73. self.show_mmid = True
  74. self.minconf = minconf
  75. self.addrs = addrs
  76. self.rpc = await rpc_init(proto)
  77. from .ctl import TrackingWallet
  78. self.wallet = await TrackingWallet(proto,mode='w')
  79. @property
  80. def total(self):
  81. return sum(i.amt for i in self.data)
  82. def gen_data(self,rpc_data,lbl_id):
  83. for o in rpc_data:
  84. if not lbl_id in o:
  85. continue # coinbase outputs have no account field
  86. l = get_tw_label(self.proto,o[lbl_id])
  87. if l:
  88. o.update({
  89. 'twmmid': l.mmid,
  90. 'label': l.comment or '',
  91. 'amt': self.proto.coin_amt(o['amount']),
  92. 'addr': CoinAddr(self.proto,o['address']),
  93. 'confs': o['confirmations']
  94. })
  95. yield self.MMGenTwUnspentOutput(
  96. self.proto,
  97. **{ k:v for k,v in o.items() if k in self.MMGenTwUnspentOutput.valid_attrs } )
  98. def get_display_constants(self):
  99. data = self.data
  100. for i in data:
  101. i.skip = ''
  102. # allow for 7-digit confirmation nums
  103. col1_w = max(3,len(str(len(data)))+1) # num + ')'
  104. mmid_w = max(len(('',i.twmmid)[i.twmmid.type=='mmgen']) for i in data) or 12 # DEADBEEF:S:1
  105. max_acct_w = max(i.label.screen_width for i in data) + mmid_w + 1
  106. max_btcaddr_w = max(len(i.addr) for i in data)
  107. min_addr_w = self.cols - self.col_adj
  108. addr_w = min(max_btcaddr_w + (0,1+max_acct_w)[self.show_mmid],min_addr_w)
  109. acct_w = min(max_acct_w, max(24,addr_w-10))
  110. btaddr_w = addr_w - acct_w - 1
  111. label_w = acct_w - mmid_w - 1
  112. tx_w = min(self.txid_w,self.cols-addr_w-29-col1_w) # min=6 TODO
  113. txdots = ('','..')[tx_w < self.txid_w]
  114. dc = namedtuple('display_constants',['col1_w','mmid_w','addr_w','btaddr_w','label_w','tx_w','txdots'])
  115. return dc(col1_w,mmid_w,addr_w,btaddr_w,label_w,tx_w,txdots)
  116. def gen_display_output(self,c):
  117. fs = self.display_fs_fs.format( cw=c.col1_w, tw=c.tx_w )
  118. hdr_fs = self.display_hdr_fs_fs.format( cw=c.col1_w, tw=c.tx_w )
  119. yield hdr_fs.format(
  120. n = 'Num',
  121. t = 'TXid'.ljust(c.tx_w - 2) + ' Vout',
  122. a = 'Address'.ljust(c.addr_w),
  123. A = f'Amt({self.proto.dcoin})'.ljust(self.disp_prec+5),
  124. A2 = f' Amt({self.proto.coin})'.ljust(self.disp_prec+4),
  125. c = {
  126. 'confs': 'Confs',
  127. 'block': 'Block',
  128. 'days': 'Age(d)',
  129. 'date': 'Date',
  130. 'date_time': 'Date',
  131. }[self.age_fmt],
  132. ).rstrip()
  133. for n,i in enumerate(self.data):
  134. addr_dots = '|' + '.'*(c.addr_w-1)
  135. mmid_disp = MMGenID.fmtc(
  136. (
  137. '.'*c.mmid_w if i.skip == 'addr' else
  138. i.twmmid if i.twmmid.type == 'mmgen' else
  139. f'Non-{g.proj_name}'
  140. ),
  141. width = c.mmid_w,
  142. color = True )
  143. if self.show_mmid:
  144. addr_out = '{} {}{}'.format((
  145. type(i.addr).fmtc(addr_dots,width=c.btaddr_w,color=True) if i.skip == 'addr' else
  146. i.addr.fmt(width=c.btaddr_w,color=True)
  147. ),
  148. mmid_disp,
  149. (' ' + i.label.fmt(width=c.label_w,color=True)) if c.label_w > 0 else ''
  150. )
  151. else:
  152. addr_out = (
  153. type(i.addr).fmtc(addr_dots,width=c.addr_w,color=True) if i.skip=='addr' else
  154. i.addr.fmt(width=c.addr_w,color=True) )
  155. yield fs.format(
  156. n = str(n+1)+')',
  157. t = (
  158. '' if not i.txid else
  159. ' ' * (c.tx_w-4) + '|...' if i.skip == 'txid' else
  160. i.txid[:c.tx_w-len(c.txdots)] + c.txdots ),
  161. v = i.vout,
  162. a = addr_out,
  163. A = i.amt.fmt(color=True,prec=self.disp_prec),
  164. A2 = (i.amt2.fmt(color=True,prec=self.disp_prec) if i.amt2 is not None else ''),
  165. c = self.age_disp(i,self.age_fmt),
  166. ).rstrip()
  167. def gen_print_output(self,color,show_confs):
  168. addr_w = max(len(i.addr) for i in self.data)
  169. mmid_w = max(len(('',i.twmmid)[i.twmmid.type=='mmgen']) for i in self.data) or 12 # DEADBEEF:S:1
  170. fs = self.print_fs_fs.format(
  171. tw = self.txid_w + 3,
  172. cf = '{c:<8} ' if show_confs else '',
  173. aw = self.proto.coin_amt.max_prec + 5 )
  174. yield fs.format(
  175. n = 'Num',
  176. t = 'Tx ID,Vout',
  177. a = 'Address'.ljust(addr_w),
  178. m = 'MMGen ID'.ljust(mmid_w),
  179. A = f'Amount({self.proto.dcoin})',
  180. A2 = f'Amount({self.proto.coin})',
  181. c = 'Confs', # skipped for eth
  182. b = 'Block', # skipped for eth
  183. D = 'Date',
  184. l = 'Label' )
  185. max_lbl_len = max([len(i.label) for i in self.data if i.label] or [2])
  186. for n,i in enumerate(self.data):
  187. yield fs.format(
  188. n = str(n+1) + ')',
  189. t = '{},{}'.format(
  190. ('|'+'.'*63 if i.skip == 'txid' and self.group else i.txid),
  191. i.vout ),
  192. a = (
  193. '|'+'.' * addr_w if i.skip == 'addr' and self.group else
  194. i.addr.fmt(color=color,width=addr_w) ),
  195. m = MMGenID.fmtc(
  196. (i.twmmid if i.twmmid.type == 'mmgen' else f'Non-{g.proj_name}'),
  197. width = mmid_w,
  198. color = color ),
  199. A = i.amt.fmt(color=color),
  200. A2 = ( i.amt2.fmt(color=color) if i.amt2 is not None else '' ),
  201. c = i.confs,
  202. b = self.rpc.blockcount - (i.confs - 1),
  203. D = self.age_disp(i,'date_time'),
  204. l = i.label.hl(color=color) if i.label else
  205. TwComment.fmtc(
  206. s = '',
  207. color = color,
  208. nullrepl = '-',
  209. width = max_lbl_len )
  210. ).rstrip()
  211. def display_total(self):
  212. msg('\nTotal unspent: {} {} ({} output{})'.format(
  213. self.total.hl(),
  214. self.proto.dcoin,
  215. len(self.data),
  216. suf(self.data) ))
  217. class item_action(TwCommon.item_action):
  218. async def a_balance_refresh(self,uo,idx):
  219. if not keypress_confirm(
  220. f'Refreshing tracking wallet {uo.item_desc} #{idx}. Is this what you want?'):
  221. return 'redo'
  222. await uo.wallet.get_balance( uo.data[idx-1].addr, force_rpc=True )
  223. await uo.get_data()
  224. uo.oneshot_msg = yellow(f'{uo.proto.dcoin} balance for account #{idx} refreshed\n\n')
  225. async def a_addr_delete(self,uo,idx):
  226. if not keypress_confirm(
  227. f'Removing {uo.item_desc} #{idx} from tracking wallet. Is this what you want?'):
  228. return 'redo'
  229. if await uo.wallet.remove_address( uo.data[idx-1].addr ):
  230. await uo.get_data()
  231. uo.oneshot_msg = yellow(f'{capfirst(uo.item_desc)} #{idx} removed\n\n')
  232. else:
  233. import asyncio
  234. await asyncio.sleep(3)
  235. uo.oneshot_msg = red('Address could not be removed\n\n')
  236. async def a_lbl_add(self,uo,idx):
  237. async def do_lbl_add(lbl):
  238. e = uo.data[idx-1]
  239. if await uo.wallet.add_label( e.twmmid, lbl, addr=e.addr ):
  240. await uo.get_data()
  241. uo.oneshot_msg = yellow('Label {} {} #{}\n\n'.format(
  242. ('added to' if lbl else 'removed from'),
  243. uo.item_desc,
  244. idx ))
  245. else:
  246. import asyncio
  247. await asyncio.sleep(3)
  248. uo.oneshot_msg = red('Label could not be added\n\n')
  249. cur_lbl = uo.data[idx-1].label
  250. msg('Current label: {}'.format(cur_lbl.hl() if cur_lbl else '(none)'))
  251. res = line_input(
  252. "Enter label text (or ENTER to return to main menu): ",
  253. insert_txt = cur_lbl )
  254. if res == cur_lbl:
  255. return None
  256. elif res == '':
  257. return (await do_lbl_add(res)) if keypress_confirm(
  258. f'Removing label for {uo.item_desc} #{idx}. Is this what you want?') else 'redo'
  259. else:
  260. return (await do_lbl_add(res)) if get_obj(TwComment,s=res) else 'redo'