prune.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2024 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-wallet
  9. # https://gitlab.com/mmgen/mmgen-wallet
  10. """
  11. tw.prune: Tracking wallet pruned listaddresses class for the MMGen suite
  12. """
  13. from ..util import msg, msg_r, rmsg, ymsg
  14. from ..color import red, green, gray, yellow
  15. from ..obj import ListItemAttr
  16. from .addresses import TwAddresses
  17. from .view import CUR_HOME, ERASE_ALL
  18. class TwAddressesPrune(TwAddresses):
  19. mod_subpath = 'tw.prune'
  20. class TwAddress(TwAddresses.TwAddress):
  21. valid_attrs = TwAddresses.TwAddress.valid_attrs | {'tag'}
  22. tag = ListItemAttr(bool, typeconv=False, reassign_ok=True)
  23. async def __init__(self, *args, warn_used=False, **kwargs):
  24. self.warn_used = warn_used
  25. await super().__init__(*args, **kwargs)
  26. def gen_display(self, data, cw, fs, color, fmt_method):
  27. id_save = data[0].al_id
  28. yes, no = red('Yes '), green('No ')
  29. for n, d in enumerate(data, 1):
  30. if id_save != d.al_id:
  31. id_save = d.al_id
  32. yield ''.ljust(self.term_width)
  33. yield (
  34. gray(fmt_method(n, d, cw, fs, False, 'Yes ', 'No ')) if d.tag else
  35. fmt_method(n, d, cw, fs, True, yes, no))
  36. def do_prune(self):
  37. def gen():
  38. for n, d in enumerate(self.data, 1):
  39. if d.tag:
  40. pruned.append(n)
  41. if d.amt:
  42. rmsg(f'Warning: pruned address {d.twmmid.addr} has a balance!')
  43. elif self.warn_used and d.recvd:
  44. ymsg(f'Warning: pruned address {d.twmmid.addr} is used!')
  45. else:
  46. yield d
  47. pruned = []
  48. self.reverse = False
  49. self.do_sort('twmmid')
  50. self.data = list(gen())
  51. return pruned
  52. class action(TwAddresses.action):
  53. def get_addrnums(self, parent, desc):
  54. prompt = f'Enter a range or space-separated list of addresses to {desc}: '
  55. from ..ui import line_input
  56. msg('')
  57. while True:
  58. reply = line_input(parent.cfg, prompt).strip()
  59. if reply:
  60. from ..addrlist import AddrIdxList
  61. from ..obj import get_obj
  62. selected = get_obj(AddrIdxList, fmt_str=','.join(reply.split()))
  63. if selected:
  64. if selected[-1] <= len(parent.disp_data):
  65. return selected
  66. msg(f'Address number must be <= {len(parent.disp_data)}')
  67. else:
  68. return []
  69. def query_user(self, desc, addrnum, e):
  70. from collections import namedtuple
  71. md = namedtuple('mdata', ['wmsg', 'prompt'])
  72. m = {
  73. 'amt': md(
  74. red('Address #{a} ({b}) has a balance of {c}!'.format(
  75. a = addrnum,
  76. b = e.twmmid.addr,
  77. c = e.amt.hl2(color=False, unit=True))),
  78. '[p]rune anyway, [P]rune all with balance, [s]kip, [S]kip all with balance: ',
  79. ),
  80. 'used': md(
  81. yellow('Address #{a} ({b}) is used!'.format(
  82. a = addrnum,
  83. b = e.twmmid.addr)),
  84. '[p]rune anyway, [P]rune all used, [s]kip, [S]kip all used: ',
  85. ),
  86. }
  87. from ..term import get_char
  88. valid_res = 'pPsS'
  89. msg(m[desc].wmsg)
  90. while True:
  91. res = get_char(m[desc].prompt, immed_chars=valid_res)
  92. if res in valid_res:
  93. msg('')
  94. return {
  95. # auto, prune
  96. 'p': (False, True),
  97. 'P': (True, True),
  98. 's': (False, False),
  99. 'S': (True, False),
  100. }[res]
  101. else:
  102. msg('\nInvalid keypress')
  103. async def a_prune(self, parent):
  104. def do_entry(desc, n, addrnum, e):
  105. if auto[desc]:
  106. return False
  107. else:
  108. auto[desc], prune = self.query_user(desc, addrnum, e)
  109. dfl[desc] = auto[desc] and prune
  110. skip_all_used = auto['used'] and not dfl['used']
  111. if auto[desc]: # we’ve switched to auto mode, so go back and fix up all previous entries
  112. for idx in addrnums[:n]:
  113. e = parent.disp_data[idx-1]
  114. if skip_all_used and e.recvd:
  115. e.tag = False
  116. elif desc == 'amt' and e.amt:
  117. e.tag = prune
  118. elif desc == 'used' and (e.recvd and not e.amt):
  119. e.tag = prune
  120. # skipping all used addrs implies skipping all addrs with balances
  121. if skip_all_used:
  122. auto['amt'] = True
  123. dfl['amt'] = False
  124. return prune
  125. addrnums = self.get_addrnums(parent, 'prune')
  126. dfl = {'amt': False, 'used': False} # default prune policy for given property (has amt, is used)
  127. auto = {'amt': False, 'used': False} # whether to ask the user, or apply default policy automatically
  128. for n, addrnum in enumerate(addrnums):
  129. e = parent.disp_data[addrnum-1]
  130. if e.amt and not dfl['amt']:
  131. e.tag = do_entry('amt', n, addrnum, e)
  132. elif parent.warn_used and (e.recvd and not e.amt) and not dfl['used']:
  133. e.tag = do_entry('used', n, addrnum, e)
  134. else:
  135. e.tag = True
  136. if parent.scroll:
  137. msg_r(CUR_HOME + ERASE_ALL)
  138. async def a_unprune(self, parent):
  139. for addrnum in self.get_addrnums(parent, 'unprune'):
  140. parent.disp_data[addrnum-1].tag = False
  141. if parent.scroll:
  142. msg_r(CUR_HOME + ERASE_ALL)
  143. async def a_clear_prune_list(self, parent):
  144. for d in parent.data:
  145. d.tag = False