| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- #!/usr/bin/env python3
- #
- # MMGen Wallet, a terminal-based cryptocurrency wallet
- # Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
- # Licensed under the GNU General Public License, Version 3:
- # https://www.gnu.org/licenses
- # Public project repositories:
- # https://github.com/mmgen/mmgen-wallet
- # https://gitlab.com/mmgen/mmgen-wallet
- """
- proto.btc.tx.info: Bitcoin transaction info class
- """
- from ....tx.info import TxInfo
- from ....util import fmt, die
- from ....color import red, green, blue, pink
- from ....addr import MMGenID
- class TxInfo(TxInfo):
- sort_orders = ('addr', 'raw')
- txinfo_hdr_fs = '{hdr}\n ID={i} ({a} {c}) RBF={r} Sig={s} Locktime={l}\n'
- txinfo_hdr_fs_short = 'TX {i} ({a} {c}) RBF={r} Sig={s} Locktime={l}\n'
- txinfo_ftr_fs = fmt("""
- Input amount: {i} {d}
- Spend amount: {s} {d}
- Change: {C} {d}
- Fee: {a} {c}{r}
- """)
- def format_rel_fee(self):
- tx = self.tx
- return ' ({} {}, {} of spend amount)'.format(
- pink(tx.fee_abs2rel(tx.fee)),
- tx.rel_fee_disp,
- pink('{:0.6f}%'.format(tx.fee / tx.send_amt * 100))
- )
- def format_abs_fee(self, iwidth, /, *, color=None):
- return self.tx.fee.fmt(iwidth, color=color)
- def format_verbose_footer(self):
- tx = self.tx
- tsize = len(tx.serialized) // 2 if tx.serialized else 'unknown'
- out = f'Transaction size: Vsize {tx.estimate_size()} (estimated), Total {tsize}'
- if tx.name in ('Signed', 'OnlineSigned'):
- wsize = tx.deserialized.witness_size
- out += f', Base {tsize-wsize}, Witness {wsize}'
- return out + '\n'
- def format_body(self, blockcount, nonmm_str, max_mmwid, enl, *, terse, sort):
- if sort not in self.sort_orders:
- die(1, '{!r}: invalid transaction view sort order. Valid options: {}'.format(
- sort,
- ','.join(self.sort_orders)))
- def get_mmid_fmt(e, is_input):
- if e.mmid:
- return e.mmid.fmt2(
- max_mmwid,
- encl = '()',
- color = True,
- append_chars = ('', ' (chg)')[bool(not is_input and e.is_chg and terse)],
- append_color = 'green')
- else:
- return MMGenID.fmtc(
- '[vault address]' if not is_input and e.is_vault else nonmm_str,
- max_mmwid,
- color = True)
- def format_io(desc):
- io = getattr(tx, desc)
- is_input = desc == 'inputs'
- yield desc.capitalize() + ':\n' + enl
- confs_per_day = 60*60*24 // tx.proto.avg_bdi
- io_sorted = {
- 'addr': lambda: sorted(
- io, # prepend '+' (sorts before '0') to ensure non-MMGen addrs are displayed first
- key = lambda o: (o.mmid.sort_key if o.mmid else f'+{o.addr}') + f'{o.amt:040.20f}'),
- 'raw': lambda: io
- }[sort]
- def data_disp(data):
- return f'OP_RETURN data ({len(data)} bytes)'
- if terse:
- iwidth = max(len(str(int(e.amt))) for e in io)
- 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)
- for n, e in enumerate(io_sorted()):
- yield '{:3} {} {} {} {}\n'.format(
- n+1,
- e.addr.fmt(vp1, addr_w, color=True) if e.addr else blue(data_disp(e.data).ljust(addr_w)),
- get_mmid_fmt(e, is_input) if e.addr else ''.ljust(max_mmwid),
- e.amt.fmt(iwidth, color=True),
- tx.dcoin)
- if have_bch and e.addr:
- yield '{:3} [{}]\n'.format('', e.addr.hl(vp2, color=False))
- else:
- col1_w = len(str(len(io))) + 1
- for n, e in enumerate(io_sorted()):
- mmid_fmt = get_mmid_fmt(e, is_input)
- if is_input and blockcount:
- confs = e.confs + blockcount - tx.blockcount
- days = int(confs // confs_per_day)
- def gen():
- if is_input:
- yield (n+1, 'tx,vout:', f'{e.txid.hl()},{red(str(e.vout))}')
- yield ('', 'address:', f'{e.addr.hl(vp1)} {mmid_fmt}')
- if have_bch:
- yield ('', '', f'[{e.addr.hl(vp2, color=False)}]')
- else:
- yield (
- n + 1,
- 'address:',
- (f'{e.addr.hl(vp1)} {mmid_fmt}' if e.addr else e.data.hl(add_label=True)))
- if have_bch and e.addr:
- yield ('', '', f'[{e.addr.hl(vp2, color=False)}]')
- if e.comment:
- yield ('', 'comment:', e.comment.hl())
- yield ('', 'amount:', f'{e.amt.hl()} {tx.dcoin}')
- if is_input and blockcount:
- yield ('', 'confirmations:', f'{confs} (around {days} days)')
- if not is_input and e.is_chg:
- yield ('', 'change:', green('True'))
- yield '\n'.join('{:>{w}} {:<8} {}'.format(*d, w=col1_w) for d in gen()) + '\n\n'
- tx = self.tx
- if self.cfg._proto.coin == 'BCH':
- have_bch = True
- vp1 = 1 if not self.cfg.cashaddr else not self.cfg._proto.cashaddr
- vp2 = (vp1 + 1) % 2
- else:
- have_bch = False
- vp1 = 0
- return (
- 'Inputs/Outputs sort order: {}'.format({
- 'raw': pink('UNSORTED'),
- 'addr': pink('ADDRESS')
- }[sort])
- + ('\n\n', '\n')[terse]
- + ''.join(format_io('inputs'))
- + ''.join(format_io('outputs')))
- def strfmt_locktime(self, locktime=None, *, terse=False):
- # Locktime itself is an unsigned 4-byte integer which can be parsed two ways:
- #
- # If less than 500 million, locktime is parsed as a block height. The transaction can be
- # added to any block which has this height or higher.
- # MMGen note: s/this height or higher/a higher block height/
- #
- # If greater than or equal to 500 million, locktime is parsed using the Unix epoch time
- # format (the number of seconds elapsed since 1970-01-01T00:00 UTC). The transaction can be
- # added to any block whose block time is greater than the locktime.
- num = locktime or self.tx.locktime
- if num is None:
- return '(None)'
- elif num.bit_length() > 32:
- die(2, f'{num!r}: invalid nLockTime value (integer size greater than 4 bytes)!')
- elif num >= 500_000_000:
- import time
- return ' '.join(time.strftime('%c', time.gmtime(num)).split()[1:])
- elif num > 0:
- return '{}{}'.format(('block height ', '')[terse], num)
- else:
- die(2, f'{num!r}: invalid nLockTime value!')
|