info.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  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. tx.info: transaction info class
  12. """
  13. import importlib
  14. from ..cfg import gc
  15. from ..color import red, green, cyan, orange, blue, yellow, magenta
  16. from ..util import msg, msg_r, decode_timestamp, make_timestr
  17. from ..util2 import format_elapsed_hr
  18. class TxInfo:
  19. def __init__(self, cfg, tx):
  20. self.cfg = cfg
  21. self.tx = tx
  22. def format(self, *, terse=False, sort='addr'):
  23. tx = self.tx
  24. if tx.is_swap:
  25. sort = 'raw'
  26. if tx.proto.base_proto == 'Ethereum':
  27. blockcount = None
  28. else:
  29. try:
  30. blockcount = tx.rpc.blockcount
  31. except:
  32. blockcount = None
  33. def get_max_mmwid(io):
  34. sel_f = (
  35. (lambda o: len(o.mmid) + 2) if io == tx.inputs else # 2 = len('()')
  36. (lambda o: len(o.mmid) + (2, 8)[bool(o.is_chg)])) # 6 = len(' (chg)')
  37. return max(max([sel_f(o) for o in io if o.mmid] or [0]), len(nonmm_str))
  38. nonmm_str = f'(non-{gc.proj_name} address)'
  39. max_mmwid = max(get_max_mmwid(tx.inputs), get_max_mmwid(tx.outputs))
  40. def gen_view():
  41. yield (self.txinfo_hdr_fs_short if terse else self.txinfo_hdr_fs).format(
  42. hdr = cyan(('SWAP ' if tx.is_swap else '') + 'TRANSACTION DATA'),
  43. i = tx.txid.hl(),
  44. a = tx.send_amt.hl(),
  45. c = tx.dcoin,
  46. r = green('True') if tx.is_replaceable() else red('False'),
  47. s = green('True') if tx.signed else red('False'),
  48. l = (
  49. orange(self.strfmt_locktime(terse=True)) if tx.locktime else
  50. green('None')))
  51. for attr, label in [('timestamp', 'Created:'), ('sent_timestamp', 'Sent:')]:
  52. if (val := getattr(tx, attr)) is not None:
  53. _ = decode_timestamp(val)
  54. yield f' {label:8} {make_timestr(_)} ({format_elapsed_hr(_)})\n'
  55. if tx.chain != 'mainnet': # if mainnet has a coin-specific name, display it
  56. yield green(f' Chain: {tx.chain.upper()}') + '\n'
  57. if tx.coin_txid:
  58. yield f' {tx.coin} TxID: {tx.coin_txid.hl()}\n'
  59. if tx.is_swap:
  60. from ..swap.proto.thorchain import Memo, name
  61. data = tx.swap_memo.encode() if tx.proto.is_evm else tx.data_output.data
  62. if Memo.is_partial_memo(data):
  63. recv_mmid = getattr(tx, 'swap_recv_addr_mmid', None)
  64. p = Memo.parse(data.decode('ascii'))
  65. yield ' {} {}\n'.format(magenta('DEX Protocol:'), blue(name))
  66. yield ' Swap: {}\n'.format(orange(f'{tx.send_asset.name} => {tx.recv_asset.name}'))
  67. yield ' Dest: {}{}\n'.format(
  68. cyan(p.address),
  69. orange(f' {recv_mmid}') if recv_mmid else '')
  70. if not recv_mmid:
  71. yield yellow(' Warning: swap destination address is not a wallet address!\n')
  72. enl = ('\n', '')[bool(terse)]
  73. yield enl
  74. if tx.comment:
  75. yield f' Comment: {tx.comment.hl()}\n{enl}'
  76. yield self.format_body(
  77. blockcount,
  78. nonmm_str,
  79. max_mmwid,
  80. enl,
  81. terse = terse,
  82. sort = sort)
  83. iwidth = len(str(int(tx.sum_inputs())))
  84. yield self.txinfo_ftr_fs.format(
  85. i = tx.sum_inputs().fmt(iwidth, color=True),
  86. o = tx.sum_outputs().fmt(iwidth, color=True),
  87. C = tx.change.fmt(iwidth, color=True),
  88. s = tx.send_amt.fmt(iwidth, color=True),
  89. a = self.format_abs_fee(iwidth, color=True),
  90. r = self.format_rel_fee(),
  91. d = tx.dcoin,
  92. c = tx.coin)
  93. if tx.cfg.verbose:
  94. yield self.format_verbose_footer()
  95. return ''.join(gen_view())
  96. def view_with_prompt(self, prompt, *, pause=True):
  97. prompt += ' (y)es, (N)o, pager (v)iew, (t)erse view: '
  98. from ..term import get_char
  99. while True:
  100. reply = get_char(prompt, immed_chars='YyNnVvTt').strip('\n\r')
  101. msg('')
  102. if reply == '' or reply in 'Nn':
  103. break
  104. if reply in 'YyVvTt':
  105. self.view(
  106. pager = reply in 'Vv',
  107. pause = pause,
  108. terse = reply in 'Tt')
  109. break
  110. msg('Invalid reply')
  111. def view(self, *, pager=False, pause=True, terse=False):
  112. o = self.format(terse=terse)
  113. if pager:
  114. from ..ui import do_pager
  115. do_pager(o)
  116. else:
  117. msg('')
  118. msg_r(o)
  119. from ..term import get_char
  120. if pause:
  121. get_char('Press any key to continue: ')
  122. msg('')
  123. def init_info(cfg, tx):
  124. return getattr(
  125. importlib.import_module(f'mmgen.proto.{tx.proto.base_proto_coin.lower()}.tx.info'),
  126. ('Token' if tx.proto.tokensym else '') + 'TxInfo')(cfg, tx)