| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 |
- #!/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
- """
- tx.base: base transaction class
- """
- from ..cfg import gc
- from ..objmethods import MMGenObject
- from ..obj import (
- ImmutableAttr,
- ListItemAttr,
- MMGenListItem,
- MMGenTxComment,
- TwComment,
- CoinTxID,
- HexStr,
- NonNegativeInt
- )
- from ..amt import CoinAmtChk
- from ..addr import MMGenID, CoinAddr
- from ..util import msg, ymsg, fmt, remove_dups, make_timestamp, die, cached_property
- class MMGenTxIO(MMGenListItem):
- vout = ListItemAttr(NonNegativeInt)
- amt = ImmutableAttr(CoinAmtChk, include_proto=True)
- comment = ListItemAttr(TwComment, reassign_ok=True)
- mmid = ListItemAttr(MMGenID, include_proto=True)
- addr = ImmutableAttr(CoinAddr, include_proto=True)
- confs = ListItemAttr(int) # confs of type long exist in the wild, so convert
- txid = ListItemAttr(CoinTxID)
- have_wif = ListItemAttr(bool, typeconv=False, delete_ok=True)
- invalid_attrs = {'proto', 'tw_copy_attrs'}
- def __init__(self, proto, **kwargs):
- self.__dict__['proto'] = proto
- MMGenListItem.__init__(self, **kwargs)
- @property
- def mmtype(self):
- """
- Attempt to determine input or output’s MMGenAddrType. For non-MMGen
- addresses, infer the type from the address format, returning None for
- P2PKH, which could be either 'L' or 'C'.
- """
- return (
- str(self.mmid.mmtype) if self.mmid else
- 'B' if self.addr.addr_fmt == 'bech32' else
- 'S' if self.addr.addr_fmt == 'p2sh' else
- None
- ) if self.addr else None
- class MMGenTxIOList(list, MMGenObject):
- def __init__(self, parent, data=None):
- self.parent = parent
- if data:
- assert isinstance(data, list), 'MMGenTxIOList_check1'
- else:
- data = []
- list.__init__(self, data)
- class Base(MMGenObject):
- desc = 'transaction'
- comment = None
- txid = None
- coin_txid = None
- timestamp = None
- sent_timestamp = None
- blockcount = None
- locktime = None
- chain = None
- signed = False
- is_bump = False
- is_swap = False
- swap_attrs = {
- 'swap_proto': None,
- 'swap_quote_expiry': None,
- 'swap_recv_addr_mmid': None,
- 'swap_recv_asset_spec': None,
- 'swap_memo': None,
- 'token_vault_addr': None,
- 'serialized2': None,
- 'coin_txid2': CoinTxID}
- file_format = 'json'
- non_mmgen_inputs_msg = f"""
- This transaction includes inputs with non-{gc.proj_name} addresses. When
- signing the transaction, private keys for the addresses listed below must
- be supplied using the --keys-from-file option. The key file must contain
- one key per line.
- Non-{gc.proj_name} addresses found in inputs:
- {{}}
- """
- class Input(MMGenTxIO):
- scriptPubKey = ListItemAttr(HexStr)
- sequence = ListItemAttr(int, typeconv=False)
- tw_copy_attrs = {'scriptPubKey', 'vout', 'amt', 'comment', 'mmid', 'addr', 'confs', 'txid'}
- class Output(MMGenTxIO):
- addr = ListItemAttr(CoinAddr, include_proto=True) # ImmutableAttr in parent cls
- is_chg = ListItemAttr(bool, typeconv=False)
- is_vault = ListItemAttr(bool, typeconv=False)
- data = ListItemAttr(None, typeconv=False) # placeholder
- class InputList(MMGenTxIOList):
- desc = 'transaction inputs'
- class OutputList(MMGenTxIOList):
- desc = 'transaction outputs'
- def __init__(self, *args, **kwargs):
- self.cfg = kwargs['cfg']
- self.inputs = self.InputList(self)
- self.outputs = self.OutputList(self)
- self.name = type(self).__name__
- self.proto = kwargs['proto']
- self.twctl = kwargs.get('twctl')
- self.is_token = 'Token' in self.name
- @property
- def coin(self):
- return self.proto.coin
- @property
- def dcoin(self):
- return self.proto.dcoin
- @property
- def info(self):
- from .info import init_info
- return init_info(self.cfg, self)
- def check_correct_chain(self):
- if hasattr(self, 'rpc'):
- if self.chain != self.rpc.chain:
- die('TransactionChainMismatch',
- f'Transaction is for {self.chain}, but coin daemon chain is {self.rpc.chain}!')
- def sum_inputs(self):
- return sum(e.amt for e in self.inputs)
- def sum_outputs(self, *, exclude=None):
- if exclude is None:
- olist = self.outputs
- else:
- olist = self.outputs[:exclude] + self.outputs[exclude+1:]
- if not olist:
- return self.proto.coin_amt('0')
- return sum(e.amt for e in olist)
- def _chg_output_ops(self, op, attr):
- is_chgs = [getattr(x, attr) for x in self.outputs]
- if is_chgs.count(True) == 1:
- return is_chgs.index(True) if op == 'idx' else self.outputs[is_chgs.index(True)]
- elif is_chgs.count(True) == 0:
- return None
- else:
- raise ValueError('more than one change output!')
- @property
- def chg_idx(self):
- return self._chg_output_ops('idx', 'is_chg')
- @property
- def chg_output(self):
- return self._chg_output_ops('output', 'is_chg')
- def add_timestamp(self):
- self.timestamp = make_timestamp()
- def add_sent_timestamp(self):
- self.sent_timestamp = make_timestamp()
- def add_blockcount(self):
- self.blockcount = self.rpc.blockcount
- # returns True if comment added or changed, False otherwise
- def add_comment(self, *, infile=None):
- if infile:
- from ..fileutil import get_data_from_file
- self.comment = MMGenTxComment(
- get_data_from_file(self.cfg, infile, desc='transaction comment'))
- else:
- from ..ui import keypress_confirm, line_input
- if keypress_confirm(
- self.cfg,
- prompt = 'Edit transaction comment?' if self.comment else 'Add a comment to transaction?',
- default_yes = False):
- res = MMGenTxComment(line_input(self.cfg, 'Comment: ', insert_txt=self.comment))
- if not res:
- ymsg('Warning: comment is empty')
- changed = res != self.comment
- self.comment = res
- return changed
- else:
- return False
- def get_non_mmaddrs(self, desc):
- return remove_dups(
- (i.addr for i in getattr(self, desc) if not i.mmid),
- edesc = 'non-MMGen address',
- quiet = True)
- def check_non_mmgen_inputs(self, *, caller, non_mmaddrs=None):
- assert caller in ('txcreate', 'txdo', 'txsign', 'autosign')
- non_mmaddrs = non_mmaddrs or self.get_non_mmaddrs('inputs')
- if non_mmaddrs:
- indent = ' '
- fs = fmt(self.non_mmgen_inputs_msg, strip_char='\t', indent=indent).strip()
- m = fs.format('\n '.join(non_mmaddrs))
- if caller in ('txdo', 'txsign'):
- if not self.cfg.keys_from_file:
- die('UserOptError', f'\n{indent}ERROR: {m}\n')
- else:
- msg(f'\n{indent}WARNING: {m}\n')
- if not (caller == 'autosign' or self.cfg.yes):
- from ..ui import keypress_confirm
- keypress_confirm(self.cfg, 'Continue?', default_yes=True, do_exit=True)
- # swap methods:
- @cached_property
- def swap_proto_mod(self):
- from .new_swap import get_swap_proto_mod
- return get_swap_proto_mod(self.swap_proto)
- @cached_property
- def send_asset(self):
- spec = self.proto.coin + (f'.{self.proto.tokensym}' if self.proto.tokensym else '')
- return self.swap_proto_mod.SwapAsset(spec, 'send')
- @cached_property
- def recv_asset(self):
- if hasattr(self, 'swap_recv_asset_spec'):
- return self.swap_proto_mod.SwapAsset(self.swap_recv_asset_spec, 'recv')
- else: # backwards-compatibility workaround
- from ..swap.asset import SwapAsset
- x = '[unknown]'
- return SwapAsset._ad(x, x, x, x, x)
- # token methods:
- @property
- def token_op(self):
- return 'approve' if self.is_swap else 'transfer'
|