info.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  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.btc.tx.info: Bitcoin transaction info class
  12. """
  13. from ....tx.info import TxInfo
  14. from ....util import fmt, die
  15. from ....color import red, green, blue, pink
  16. from ....addr import MMGenID
  17. class TxInfo(TxInfo):
  18. sort_orders = ('addr', 'raw')
  19. txinfo_hdr_fs = '{hdr}\n ID={i} ({a} {c}) RBF={r} Sig={s} Locktime={l}\n'
  20. txinfo_hdr_fs_short = 'TX {i} ({a} {c}) RBF={r} Sig={s} Locktime={l}\n'
  21. txinfo_ftr_fs = fmt("""
  22. Input amount: {i} {d}
  23. Spend amount: {s} {d}
  24. Change: {C} {d}
  25. Fee: {a} {c}{r}
  26. """)
  27. def format_rel_fee(self):
  28. tx = self.tx
  29. return ' ({} {}, {} of spend amount)'.format(
  30. pink(tx.fee_abs2rel(tx.fee)),
  31. tx.rel_fee_disp,
  32. pink('{:0.6f}%'.format(tx.fee / tx.send_amt * 100))
  33. )
  34. def format_abs_fee(self, iwidth, /, *, color=None):
  35. return self.tx.fee.fmt(iwidth, color=color)
  36. def format_verbose_footer(self):
  37. tx = self.tx
  38. tsize = len(tx.serialized) // 2 if tx.serialized else 'unknown'
  39. out = f'Transaction size: Vsize {tx.estimate_size()} (estimated), Total {tsize}'
  40. if tx.name in ('Signed', 'OnlineSigned'):
  41. wsize = tx.deserialized.witness_size
  42. out += f', Base {tsize-wsize}, Witness {wsize}'
  43. return out + '\n'
  44. def format_body(self, blockcount, nonmm_str, max_mmwid, enl, *, terse, sort):
  45. if sort not in self.sort_orders:
  46. die(1, '{!r}: invalid transaction view sort order. Valid options: {}'.format(
  47. sort,
  48. ','.join(self.sort_orders)))
  49. def get_mmid_fmt(e, is_input):
  50. if e.mmid:
  51. return e.mmid.fmt2(
  52. max_mmwid,
  53. encl = '()',
  54. color = True,
  55. append_chars = ('', ' (chg)')[bool(not is_input and e.is_chg and terse)],
  56. append_color = 'green')
  57. else:
  58. return MMGenID.fmtc(
  59. '[vault address]' if not is_input and e.is_vault else nonmm_str,
  60. max_mmwid,
  61. color = True)
  62. def format_io(desc):
  63. io = getattr(tx, desc)
  64. is_input = desc == 'inputs'
  65. yield desc.capitalize() + ':\n' + enl
  66. confs_per_day = 60*60*24 // tx.proto.avg_bdi
  67. io_sorted = {
  68. 'addr': lambda: sorted(
  69. io, # prepend '+' (sorts before '0') to ensure non-MMGen addrs are displayed first
  70. key = lambda o: (o.mmid.sort_key if o.mmid else f'+{o.addr}') + f'{o.amt:040.20f}'),
  71. 'raw': lambda: io
  72. }[sort]
  73. def data_disp(data):
  74. return f'OP_RETURN data ({len(data)} bytes)'
  75. if terse:
  76. iwidth = max(len(str(int(e.amt))) for e in io)
  77. addr_w = max((len(e.addr.views[vp1]) if e.addr else len(data_disp(e.data))) for f in (tx.inputs, tx.outputs) for e in f)
  78. for n, e in enumerate(io_sorted()):
  79. yield '{:3} {} {} {} {}\n'.format(
  80. n+1,
  81. e.addr.fmt(vp1, addr_w, color=True) if e.addr else blue(data_disp(e.data).ljust(addr_w)),
  82. get_mmid_fmt(e, is_input) if e.addr else ''.ljust(max_mmwid),
  83. e.amt.fmt(iwidth, color=True),
  84. tx.dcoin)
  85. if have_bch and e.addr:
  86. yield '{:3} [{}]\n'.format('', e.addr.hl(vp2, color=False))
  87. else:
  88. col1_w = len(str(len(io))) + 1
  89. for n, e in enumerate(io_sorted()):
  90. mmid_fmt = get_mmid_fmt(e, is_input)
  91. if is_input and blockcount:
  92. confs = e.confs + blockcount - tx.blockcount
  93. days = int(confs // confs_per_day)
  94. def gen():
  95. if is_input:
  96. yield (n+1, 'tx,vout:', f'{e.txid.hl()},{red(str(e.vout))}')
  97. yield ('', 'address:', f'{e.addr.hl(vp1)} {mmid_fmt}')
  98. if have_bch:
  99. yield ('', '', f'[{e.addr.hl(vp2, color=False)}]')
  100. else:
  101. yield (
  102. n + 1,
  103. 'address:',
  104. (f'{e.addr.hl(vp1)} {mmid_fmt}' if e.addr else e.data.hl(add_label=True)))
  105. if have_bch and e.addr:
  106. yield ('', '', f'[{e.addr.hl(vp2, color=False)}]')
  107. if e.comment:
  108. yield ('', 'comment:', e.comment.hl())
  109. yield ('', 'amount:', f'{e.amt.hl()} {tx.dcoin}')
  110. if is_input and blockcount:
  111. yield ('', 'confirmations:', f'{confs} (around {days} days)')
  112. if not is_input and e.is_chg:
  113. yield ('', 'change:', green('True'))
  114. yield '\n'.join('{:>{w}} {:<8} {}'.format(*d, w=col1_w) for d in gen()) + '\n\n'
  115. tx = self.tx
  116. if self.cfg._proto.coin == 'BCH':
  117. have_bch = True
  118. vp1 = 1 if not self.cfg.cashaddr else not self.cfg._proto.cashaddr
  119. vp2 = (vp1 + 1) % 2
  120. else:
  121. have_bch = False
  122. vp1 = 0
  123. return (
  124. 'Inputs/Outputs sort order: {}'.format({
  125. 'raw': pink('UNSORTED'),
  126. 'addr': pink('ADDRESS')
  127. }[sort])
  128. + ('\n\n', '\n')[terse]
  129. + ''.join(format_io('inputs'))
  130. + ''.join(format_io('outputs')))
  131. def strfmt_locktime(self, locktime=None, *, terse=False):
  132. # Locktime itself is an unsigned 4-byte integer which can be parsed two ways:
  133. #
  134. # If less than 500 million, locktime is parsed as a block height. The transaction can be
  135. # added to any block which has this height or higher.
  136. # MMGen note: s/this height or higher/a higher block height/
  137. #
  138. # If greater than or equal to 500 million, locktime is parsed using the Unix epoch time
  139. # format (the number of seconds elapsed since 1970-01-01T00:00 UTC). The transaction can be
  140. # added to any block whose block time is greater than the locktime.
  141. num = locktime or self.tx.locktime
  142. if num is None:
  143. return '(None)'
  144. elif num.bit_length() > 32:
  145. die(2, f'{num!r}: invalid nLockTime value (integer size greater than 4 bytes)!')
  146. elif num >= 500_000_000:
  147. import time
  148. return ' '.join(time.strftime('%c', time.gmtime(num)).split()[1:])
  149. elif num > 0:
  150. return '{}{}'.format(('block height ', '')[terse], num)
  151. else:
  152. die(2, f'{num!r}: invalid nLockTime value!')