unspent.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2025 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. proto.xmr.tw.unspent: Monero protocol tracking wallet unspent outputs class
  12. """
  13. from collections import namedtuple
  14. from ....obj import ImmutableAttr
  15. from ....addr import MoneroIdx
  16. from ....amt import CoinAmtChk
  17. from ....tw.unspent import TwUnspentOutputs
  18. from .view import MoneroTwView
  19. class MoneroTwUnspentOutputs(MoneroTwView, TwUnspentOutputs):
  20. hdr_lbl = 'spendable accounts'
  21. desc = 'spendable accounts'
  22. item_desc = 'account'
  23. include_empty = False
  24. total = None
  25. nice_addr_w = {'addr': 20}
  26. prompt_fs_in = [
  27. 'Sort options: [a]mount, [A]ge, a[d]dr, [M]mgen addr, [r]everse',
  28. 'Display options: r[e]draw screen',
  29. 'View/Print: pager [v]iew, [w]ide pager view, [p]rint to file{s}',
  30. 'Actions: [q]uit menu, add [l]abel, [R]efresh balances:']
  31. extra_key_mappings = {
  32. 'R': 'a_sync_wallets',
  33. 'A': 's_age'}
  34. sort_disp = {
  35. 'addr': 'Addr',
  36. 'age': 'Age',
  37. 'amt': 'Amt',
  38. 'twmmid': 'MMGenID'}
  39. # NB: For account-based views, ALL sort keys MUST begin with acct_sort_key!
  40. sort_funcs = {
  41. 'addr': lambda i: '{}:{}'.format(i.twmmid.obj.acct_sort_key, i.addr),
  42. 'age': lambda i: '{}:{:020}'.format(i.twmmid.obj.acct_sort_key, 0 - i.confs),
  43. 'amt': lambda i: '{}:{:050}'.format(i.twmmid.obj.acct_sort_key, i.amt.to_unit('atomic')),
  44. 'twmmid': lambda i: i.twmmid.sort_key} # sort_key begins with acct_sort_key
  45. class MoneroMMGenTwUnspentOutput(TwUnspentOutputs.MMGenTwUnspentOutput):
  46. valid_attrs = {'amt', 'unlocked_amt', 'comment', 'twmmid', 'addr', 'confs', 'skip'}
  47. unlocked_amt = ImmutableAttr(CoinAmtChk, include_proto=True)
  48. def gen_data(self, rpc_data, lbl_id):
  49. return (
  50. self.MoneroMMGenTwUnspentOutput(
  51. self.proto,
  52. twmmid = twmmid,
  53. addr = data['addr'],
  54. confs = data['confs'],
  55. comment = data['lbl'].comment,
  56. amt = data['amt'],
  57. unlocked_amt = data['unlocked_amt'])
  58. for twmmid, data in rpc_data.items())
  59. def get_disp_data(self):
  60. chk_fail_msg = 'For account-based views, ALL sort keys MUST begin with acct_sort_key!'
  61. ad = namedtuple('accts_data', ['idx', 'acct_idx', 'total', 'unlocked_total', 'data'])
  62. bd = namedtuple('accts_data_data', ['disp_data_idx', 'data'])
  63. def gen_accts_data():
  64. idx, acct_idx = (None, None)
  65. total, unlocked_total, d_acc = (0, 0, {})
  66. chk_acc = [] # check for out-of-order accounts (developer idiot-proofing)
  67. for n, d in enumerate(self.data):
  68. m = d.twmmid.obj
  69. if idx != m.idx or acct_idx != m.acct_idx:
  70. if idx:
  71. yield ad(idx, acct_idx, total, unlocked_total, d_acc)
  72. chk_acc.append((m.idx, m.acct_idx))
  73. idx = m.idx
  74. acct_idx = m.acct_idx
  75. total = d.amt
  76. unlocked_total = d.unlocked_amt
  77. d_acc = {m.addr_idx: bd(n, d)}
  78. else:
  79. total += d.amt
  80. unlocked_total += d.unlocked_amt
  81. d_acc[m.addr_idx] = bd(n, d)
  82. if idx:
  83. assert len(set(chk_acc)) == len(chk_acc), chk_fail_msg
  84. yield ad(idx, acct_idx, total, unlocked_total, d_acc)
  85. self.accts_data = tuple(gen_accts_data())
  86. return super().get_disp_data()
  87. class display_type(TwUnspentOutputs.display_type):
  88. class squeezed(TwUnspentOutputs.display_type.squeezed):
  89. cols = ('addr_idx', 'addr', 'comment', 'amt')
  90. colhdr_fmt_method = None
  91. fmt_method = 'gen_display'
  92. class detail(TwUnspentOutputs.display_type.detail):
  93. cols = ('addr_idx', 'addr', 'amt', 'comment')
  94. colhdr_fmt_method = None
  95. fmt_method = 'gen_display'
  96. line_fmt_method = 'squeezed_format_line'
  97. def get_column_widths(self, data, *, wide, interactive):
  98. return self.compute_column_widths(
  99. widths = { # fixed cols
  100. 'addr_idx': MoneroIdx.max_digits,
  101. 'amt': self.amt_widths['amt'],
  102. 'spc': 4}, # 1 leading space plus 3 spaces between 4 cols
  103. maxws = { # expandable cols
  104. 'addr': max(len(d.addr) for d in data),
  105. 'comment': max(d.comment.screen_width for d in data)},
  106. minws = {
  107. 'addr': 16,
  108. 'comment': len('Comment')},
  109. maxws_nice = self.nice_addr_w,
  110. wide = wide,
  111. interactive = interactive)
  112. def gen_display(self, data, cw, fs, color, fmt_method):
  113. fs_acct = '{:>4} {:6} {:7} {}'
  114. # 30 = 4(col1) + 6(col2) + 7(col3) + 8(iwidth) + 1(len('.')) + 4(spc)
  115. rfill = ' ' * (self.term_width - self.proto.coin_amt.max_prec - 30)
  116. yield fs_acct.format('', 'Wallet', 'Account', ' Balance').ljust(self.term_width)
  117. for n, d in enumerate(self.accts_data):
  118. yield fs_acct.format(
  119. str(n + 1) + ')',
  120. d.idx.fmt(6, color=color),
  121. d.acct_idx.fmt(7, color=color),
  122. d.total.fmt2(
  123. 8, # iwidth
  124. color = color,
  125. color_override = None if d.total == d.unlocked_total else 'orange'
  126. )) + rfill
  127. for v in d.data.values():
  128. yield fmt_method(None, v.data, cw, fs, color, None, None)
  129. def squeezed_format_line(self, n, d, cw, fs, color, yes, no):
  130. return fs.format(
  131. I = d.twmmid.obj.addr_idx.fmt(cw.addr_idx, color=color),
  132. a = d.addr.fmt(self.addr_view_pref, cw.addr, color=color),
  133. c = d.comment.fmt2(cw.comment, color=color, nullrepl='-'),
  134. A = d.amt.fmt2(
  135. cw.iwidth,
  136. color = color,
  137. color_override = None if d.amt == d.unlocked_amt else 'orange',
  138. prec = self.disp_prec))
  139. async def get_idx_from_user(self):
  140. if res := await self.get_idx(f'{self.item_desc} number', self.accts_data):
  141. return await self.get_idx(
  142. 'address index',
  143. self.accts_data[res.idx - 1].data,
  144. is_addr_idx = True)