view.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  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.view: Monero protocol base class for tracking wallet view classes
  12. """
  13. from collections import namedtuple
  14. from ....obj import ImmutableAttr
  15. from ....color import red, green
  16. from ....addr import MoneroIdx
  17. from ....amt import CoinAmtChk
  18. from ....seed import SeedID
  19. from ....xmrwallet import op as xmrwallet_op
  20. from ....tw.view import TwView
  21. from ....tw.unspent import TwUnspentOutputs
  22. class MoneroTwView:
  23. is_account_based = True
  24. item_desc = 'account'
  25. nice_addr_w = {'addr': 20}
  26. total = None
  27. sort_disp = {
  28. 'addr': 'Addr',
  29. 'age': 'Age',
  30. 'amt': 'Amt',
  31. 'twmmid': 'MMGenID'}
  32. # NB: For account-based views, ALL sort keys MUST begin with acct_sort_key!
  33. sort_funcs = {
  34. 'addr': lambda i: '{}:{}'.format(i.twmmid.obj.acct_sort_key, i.addr),
  35. 'age': lambda i: '{}:{:020}'.format(i.twmmid.obj.acct_sort_key, 0 - i.confs),
  36. 'amt': lambda i: '{}:{:050}'.format(i.twmmid.obj.acct_sort_key, i.amt.to_unit('atomic')),
  37. 'twmmid': lambda i: i.twmmid.sort_key} # sort_key begins with acct_sort_key
  38. class MoneroTwViewItem(TwUnspentOutputs.MMGenTwUnspentOutput):
  39. valid_attrs = {'amt', 'unlocked_amt', 'comment', 'twmmid', 'addr', 'confs', 'is_used', 'skip'}
  40. unlocked_amt = ImmutableAttr(CoinAmtChk, include_proto=True)
  41. is_used = ImmutableAttr(bool)
  42. class rpc:
  43. caps = ()
  44. is_remote = False
  45. async def get_rpc_data(self):
  46. from mmgen.tw.shared import TwMMGenID, TwLabel
  47. op = xmrwallet_op('dump_data', self.cfg, None, None, compat_call=True)
  48. await op.restart_wallet_daemon()
  49. wallets_data = await op.main()
  50. if wallets_data:
  51. self.sid = SeedID(sid=wallets_data[0]['seed_id'])
  52. self.total = self.unlocked_total = self.proto.coin_amt('0')
  53. def gen_addrs():
  54. bd = namedtuple('address_balance_data', ['bal', 'unlocked_bal', 'blocks_to_unlock'])
  55. for wdata in wallets_data:
  56. bals_data = {i: {} for i in range(len(wdata['data'].accts_data['subaddress_accounts']))}
  57. for d in wdata['data'].bals_data.get('per_subaddress', []):
  58. bals_data[d['account_index']].update({
  59. d['address_index']: bd(
  60. d['balance'],
  61. d['unlocked_balance'],
  62. d['blocks_to_unlock'])})
  63. for acct_idx, acct_data in enumerate(wdata['data'].addrs_data):
  64. for addr_data in acct_data['addresses']:
  65. addr_idx = addr_data['address_index']
  66. addr_bals = bals_data[acct_idx].get(addr_idx)
  67. bal = self.proto.coin_amt(
  68. addr_bals.bal if addr_bals else 0,
  69. from_unit = 'atomic')
  70. unlocked_bal = self.proto.coin_amt(
  71. addr_bals.unlocked_bal if addr_bals else 0,
  72. from_unit = 'atomic')
  73. if bal or self.include_empty:
  74. self.total += bal
  75. self.unlocked_total += unlocked_bal
  76. mmid = '{}:M:{}-{}/{}'.format(
  77. wdata['seed_id'],
  78. wdata['wallet_num'],
  79. acct_idx,
  80. addr_idx)
  81. btu = addr_bals.blocks_to_unlock if addr_bals else 0
  82. if not btu and bal != unlocked_bal:
  83. btu = 12
  84. yield (TwMMGenID(self.proto, mmid), {
  85. 'addr': addr_data['address'],
  86. 'amt': bal,
  87. 'unlocked_amt': unlocked_bal,
  88. 'recvd': bal,
  89. 'is_used': addr_data['used'],
  90. 'confs': 11 - btu,
  91. 'lbl': TwLabel(self.proto, mmid + ' ' + addr_data['label'])})
  92. return dict(gen_addrs())
  93. def gen_data(self, rpc_data, lbl_id):
  94. return (
  95. self.MoneroTwViewItem(
  96. self.proto,
  97. twmmid = twmmid,
  98. addr = data['addr'],
  99. confs = data['confs'],
  100. is_used = data['is_used'],
  101. comment = data['lbl'].comment,
  102. amt = data['amt'],
  103. unlocked_amt = data['unlocked_amt'])
  104. for twmmid, data in rpc_data.items())
  105. def get_disp_data(self, input_data=None):
  106. data = self.data if input_data is None else input_data
  107. chk_fail_msg = 'For account-based views, ALL sort keys MUST begin with acct_sort_key!'
  108. ad = namedtuple('accts_data', ['idx', 'acct_idx', 'total', 'unlocked_total', 'data'])
  109. bd = namedtuple('accts_data_data', ['disp_data_idx', 'data'])
  110. def gen_accts_data():
  111. idx, acct_idx = (None, None)
  112. total, unlocked_total, d_acc = (0, 0, {})
  113. chk_acc = [] # check for out-of-order accounts (developer idiot-proofing)
  114. for n, d in enumerate(data):
  115. m = d.twmmid.obj
  116. if idx != m.idx or acct_idx != m.acct_idx:
  117. if idx:
  118. yield ad(idx, acct_idx, total, unlocked_total, d_acc)
  119. idx = m.idx
  120. acct_idx = m.acct_idx
  121. total = d.amt
  122. unlocked_total = d.unlocked_amt
  123. d_acc = {m.addr_idx: bd(n, d)}
  124. chk_acc.append((idx, acct_idx))
  125. else:
  126. total += d.amt
  127. unlocked_total += d.unlocked_amt
  128. d_acc[m.addr_idx] = bd(n, d)
  129. if idx:
  130. assert len(set(chk_acc)) == len(chk_acc), chk_fail_msg
  131. yield ad(idx, acct_idx, total, unlocked_total, d_acc)
  132. self.accts_data = tuple(gen_accts_data())
  133. return data
  134. class display_type:
  135. class squeezed(TwUnspentOutputs.display_type.squeezed):
  136. cols = ('addr_idx', 'addr', 'comment', 'amt')
  137. colhdr_fmt_method = None
  138. fmt_method = 'gen_display'
  139. class detail(TwUnspentOutputs.display_type.detail):
  140. cols = ('addr_idx', 'addr', 'amt', 'comment')
  141. colhdr_fmt_method = None
  142. fmt_method = 'gen_display'
  143. line_fmt_method = 'squeezed_format_line'
  144. def get_column_widths(self, data, *, wide):
  145. return self.column_widths_data(
  146. widths = { # fixed cols
  147. 'addr_idx': MoneroIdx.max_digits,
  148. 'used': 4 if 'used' in self.display_type.squeezed.cols else 0,
  149. 'amt': self.amt_widths['amt'],
  150. 'spc': len(self.display_type.squeezed.cols)},
  151. maxws = { # expandable cols
  152. 'addr': max(len(d.addr) for d in data),
  153. 'comment': max(d.comment.screen_width for d in data)},
  154. minws = {
  155. 'addr': 16,
  156. 'comment': len('Comment')},
  157. maxws_nice = self.nice_addr_w)
  158. def gen_display(self, data, cw, fs, color, fmt_method):
  159. yes, no = (red('Used'), green('New ')) if color else ('Used', 'New ')
  160. fs_acct = '{:>4} {:6} {:7} {}'
  161. # 30 = 4(col1) + 6(col2) + 7(col3) + 8(iwidth) + 1(len('.')) + 4(spc)
  162. rfill = ' ' * (self.term_width - self.proto.coin_amt.max_prec - 30)
  163. yield fs_acct.format('', 'Wallet', 'Account', ' Balance').ljust(self.term_width)
  164. for n, d in enumerate(self.accts_data):
  165. yield fs_acct.format(
  166. str(n + 1) + ')',
  167. d.idx.fmt(6, color=color),
  168. d.acct_idx.fmt(7, color=color),
  169. d.total.fmt2(
  170. 8, # iwidth
  171. color = color,
  172. color_override = None if d.total == d.unlocked_total else 'orange'
  173. )) + rfill
  174. for v in d.data.values():
  175. yield fmt_method(None, v.data, cw, fs, color, yes, no)
  176. def squeezed_format_line(self, n, d, cw, fs, color, yes, no):
  177. return fs.format(
  178. I = d.twmmid.obj.addr_idx.fmt(cw.addr_idx, color=color),
  179. a = d.addr.fmt(self.addr_view_pref, cw.addr, color=color),
  180. u = yes if d.is_used else no,
  181. c = d.comment.fmt2(cw.comment, color=color, nullrepl='-'),
  182. A = d.amt.fmt2(
  183. cw.iwidth,
  184. color = color,
  185. color_override = None if d.amt == d.unlocked_amt else 'orange',
  186. prec = self.disp_prec))
  187. async def get_idx_from_user(self):
  188. if res := await self.get_idx(f'{self.item_desc} number', self.accts_data):
  189. return await self.get_idx(
  190. 'address index',
  191. self.accts_data[res.idx - 1].data,
  192. is_addr_idx = True)
  193. class action(TwView.action):
  194. async def a_sync_wallets(self, parent):
  195. from ....util import msg, msg_r, ymsg
  196. from ....tw.view import CUR_HOME, ERASE_ALL
  197. msg('')
  198. try:
  199. op = xmrwallet_op('sync', parent.cfg, None, None, compat_call=True)
  200. except Exception as e:
  201. if type(e).__name__ == 'SocketError':
  202. import asyncio
  203. ymsg(str(e))
  204. await asyncio.sleep(2)
  205. msg_r(CUR_HOME + ERASE_ALL)
  206. return False
  207. raise
  208. await op.restart_wallet_daemon()
  209. await op.main()
  210. await parent.get_data()
  211. if parent.scroll:
  212. msg_r(CUR_HOME + ERASE_ALL)