txhistory.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  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. tw.txhistory: Tracking wallet transaction history class for the MMGen suite
  12. """
  13. from collections import namedtuple
  14. from ..util import fmt
  15. from ..obj import NonNegativeInt
  16. from .view import TwView
  17. class TwTxHistory(TwView):
  18. class display_type(TwView.display_type):
  19. class squeezed(TwView.display_type.squeezed):
  20. cols = ('num', 'txid', 'date', 'inputs', 'amt', 'outputs', 'comment')
  21. subhdr_fmt_method = 'gen_squeezed_subheader'
  22. class detail(TwView.display_type.detail):
  23. need_column_widths = False
  24. subhdr_fmt_method = 'gen_detail_subheader'
  25. colhdr_fmt_method = None
  26. item_separator = '\n\n'
  27. has_wallet = False
  28. show_txid = False
  29. show_unconfirmed = False
  30. show_total_amt = False
  31. update_widths_on_age_toggle = True
  32. print_output_types = ('squeezed', 'detail')
  33. filters = ('show_unconfirmed',)
  34. mod_subpath = 'tw.txhistory'
  35. async def __init__(self, cfg, proto, sinceblock=0):
  36. await super().__init__(cfg, proto)
  37. self.sinceblock = NonNegativeInt(sinceblock if sinceblock >= 0 else self.rpc.blockcount + sinceblock)
  38. @property
  39. def no_rpcdata_errmsg(self):
  40. return 'No transaction history {}found!'.format(
  41. f'from block {self.sinceblock} ' if self.sinceblock else '')
  42. def filter_data(self):
  43. return (d for d in self.data if d.confirmations > 0 or self.show_unconfirmed)
  44. def set_amt_widths(self, data):
  45. amts_tuple = namedtuple('amts_data', ['amt'])
  46. return super().set_amt_widths([amts_tuple(d.amt_disp(self.show_total_amt)) for d in data])
  47. def get_column_widths(self, data, wide, interactive):
  48. # var cols: inputs outputs comment [txid]
  49. if not hasattr(self, 'varcol_maxwidths'):
  50. self.varcol_maxwidths = {
  51. 'inputs': max(len(d.vouts_disp(
  52. 'inputs', width=None, color=False, addr_view_pref=self.addr_view_pref)) for d in data),
  53. 'outputs': max(len(d.vouts_disp(
  54. 'outputs', width=None, color=False, addr_view_pref=self.addr_view_pref)) for d in data),
  55. 'comment': max(len(d.comment) for d in data),
  56. }
  57. maxws = self.varcol_maxwidths.copy()
  58. minws = {
  59. 'inputs': 15,
  60. 'outputs': 15,
  61. 'comment': len('Comment'),
  62. }
  63. if self.show_txid:
  64. maxws['txid'] = self.txid_w
  65. minws['txid'] = 8
  66. maxws_nice = {'txid': 20}
  67. else:
  68. maxws['txid'] = 0
  69. minws['txid'] = 0
  70. maxws_nice = {}
  71. widths = { # fixed cols
  72. 'num': max(2, len(str(len(data)))+1),
  73. 'date': self.age_w,
  74. 'amt': self.amt_widths['amt'],
  75. 'spc': 6 + self.show_txid, # 5(6) spaces between cols + 1 leading space in fs
  76. }
  77. return self.compute_column_widths(
  78. widths,
  79. maxws,
  80. minws,
  81. maxws_nice,
  82. wide = wide,
  83. interactive = interactive)
  84. def gen_squeezed_subheader(self, cw, color):
  85. # keep these shorter than min screen width (currently prompt width, or 65 chars)
  86. if self.sinceblock:
  87. yield f'Displaying transactions since block {self.sinceblock.hl(color=color)}'
  88. yield 'Only wallet-related outputs are shown'
  89. yield 'Comment is from first wallet address in outputs or inputs'
  90. if (cw.inputs < self.varcol_maxwidths['inputs'] or
  91. cw.outputs < self.varcol_maxwidths['outputs']):
  92. yield 'Note: screen is too narrow to display all inputs and outputs'
  93. def gen_detail_subheader(self, cw, color):
  94. if self.sinceblock:
  95. yield f'Displaying transactions since block {self.sinceblock.hl(color=color)}'
  96. yield 'Only wallet-related outputs are shown'
  97. def squeezed_col_hdr(self, cw, fs, color):
  98. return fs.format(
  99. n = '',
  100. t = 'TxID',
  101. d = self.age_hdr,
  102. i = 'Inputs',
  103. A = 'Amt({})'.format('TX' if self.show_total_amt else 'Wallet'),
  104. o = 'Outputs',
  105. c = 'Comment')
  106. def gen_squeezed_display(self, data, cw, fs, color, fmt_method):
  107. for n, d in enumerate(data, 1):
  108. yield fs.format(
  109. n = str(n) + ')',
  110. t = d.txid_disp(width=cw.txid, color=color) if hasattr(cw, 'txid') else None,
  111. d = d.age_disp(self.age_fmt, width=self.age_w, color=color),
  112. i = d.vouts_disp('inputs', width=cw.inputs, color=color, addr_view_pref=self.addr_view_pref),
  113. A = d.amt_disp(self.show_total_amt).fmt(iwidth=cw.iwidth, prec=self.disp_prec, color=color),
  114. o = d.vouts_disp('outputs', width=cw.outputs, color=color, addr_view_pref=self.addr_view_pref),
  115. c = d.comment.fmt2(width=cw.comment, color=color, nullrepl='-'))
  116. def gen_detail_display(self, data, cw, fs, color, fmt_method):
  117. fs = fmt("""
  118. {n}
  119. Block: [{d}] {b}
  120. TxID: [{D}] {t}
  121. Value: {A}
  122. Wallet Value: {B}
  123. Fee: {f}
  124. Inputs:
  125. {i}
  126. Outputs ({N}):
  127. {o}
  128. """, strip_char='\t').strip()
  129. for n, d in enumerate(data, 1):
  130. yield fs.format(
  131. n = str(n) + ')',
  132. d = d.age_disp('date_time', width=None, color=None),
  133. b = d.blockheight_disp(color=color),
  134. D = d.txdate_disp('date_time'),
  135. t = d.txid_disp(color=color),
  136. A = d.amt_disp(show_total_amt=True).hl(color=color),
  137. B = d.amt_disp(show_total_amt=False).hl(color=color),
  138. f = d.fee_disp(color=color),
  139. i = d.vouts_list_disp('inputs', color=color, indent=' '*8, addr_view_pref=self.addr_view_pref),
  140. N = d.nOutputs,
  141. o = d.vouts_list_disp('outputs', color=color, indent=' '*8, addr_view_pref=self.addr_view_pref),
  142. )
  143. sort_disp = {
  144. 'age': 'Age',
  145. 'blockheight': 'Block Height',
  146. 'amt': 'Wallet Amt',
  147. 'total_amt': 'TX Amt',
  148. 'txid': 'TxID',
  149. }
  150. sort_funcs = {
  151. 'age': lambda i: '{:010}.{:010}'.format(0xffffffff - abs(i.confirmations), i.time_received or 0),
  152. 'blockheight': lambda i: 0 - abs(i.confirmations), # old/altcoin daemons return no 'blockheight' field
  153. 'amt': lambda i: i.wallet_outputs_total,
  154. 'total_amt': lambda i: i.outputs_total,
  155. 'txid': lambda i: i.txid,
  156. }
  157. async def set_dates(self, _):
  158. pass
  159. @property
  160. def dump_fn_pfx(self):
  161. return 'transaction-history' + (f'-since-block-{self.sinceblock}' if self.sinceblock else '')
  162. class sort_action(TwView.sort_action):
  163. def s_blockheight(self, parent):
  164. parent.do_sort('blockheight')
  165. def s_amt(self, parent):
  166. parent.do_sort('amt')
  167. parent.show_total_amt = False
  168. def s_total_amt(self, parent):
  169. parent.do_sort('total_amt')
  170. parent.show_total_amt = True
  171. class display_action(TwView.display_action):
  172. def d_show_txid(self, parent):
  173. parent.show_txid = not parent.show_txid
  174. def d_show_unconfirmed(self, parent):
  175. parent.show_unconfirmed = not parent.show_unconfirmed
  176. def d_show_total_amt(self, parent):
  177. parent.show_total_amt = not parent.show_total_amt