base.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
  4. # Copyright (C)2013-2022 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
  9. # https://gitlab.com/mmgen/mmgen
  10. """
  11. tx.base: base transaction class
  12. """
  13. from ..globalvars import *
  14. from ..objmethods import MMGenObject
  15. from ..obj import (
  16. ImmutableAttr,
  17. ListItemAttr,
  18. MMGenListItem,
  19. MMGenTxComment,
  20. TwComment,
  21. CoinTxID,
  22. HexStr,
  23. NonNegativeInt
  24. )
  25. from ..addr import MMGenID,CoinAddr
  26. from ..util import msg,ymsg,fmt,remove_dups,make_timestamp,die
  27. from ..opts import opt
  28. class MMGenTxIO(MMGenListItem):
  29. vout = ListItemAttr(NonNegativeInt)
  30. amt = ImmutableAttr(None)
  31. comment = ListItemAttr(TwComment,reassign_ok=True)
  32. mmid = ListItemAttr(MMGenID,include_proto=True)
  33. addr = ImmutableAttr(CoinAddr,include_proto=True)
  34. confs = ListItemAttr(int) # confs of type long exist in the wild, so convert
  35. txid = ListItemAttr(CoinTxID)
  36. have_wif = ListItemAttr(bool,typeconv=False,delete_ok=True)
  37. invalid_attrs = {'proto','tw_copy_attrs'}
  38. def __init__(self,proto,**kwargs):
  39. self.__dict__['proto'] = proto
  40. MMGenListItem.__init__(self,**kwargs)
  41. @property
  42. def mmtype(self):
  43. """
  44. Attempt to determine input or output’s MMGenAddrType. For non-MMGen
  45. addresses, infer the type from the address format, returning None for
  46. P2PKH, which could be either 'L' or 'C'.
  47. """
  48. return (
  49. str(self.mmid.mmtype) if self.mmid else
  50. 'B' if self.addr.addr_fmt == 'bech32' else
  51. 'S' if self.addr.addr_fmt == 'p2sh' else
  52. None )
  53. class conv_funcs:
  54. def amt(self,value):
  55. return self.proto.coin_amt(value)
  56. class MMGenTxIOList(list,MMGenObject):
  57. def __init__(self,parent,data=None):
  58. self.parent = parent
  59. if data:
  60. assert isinstance(data,list), 'MMGenTxIOList_check1'
  61. data = data
  62. else:
  63. data = list()
  64. list.__init__(self,data)
  65. class Base(MMGenObject):
  66. desc = 'transaction'
  67. comment = None
  68. txid = None
  69. coin_txid = None
  70. timestamp = None
  71. blockcount = None
  72. coin = None
  73. dcoin = None
  74. locktime = None
  75. chain = None
  76. signed = False
  77. non_mmgen_inputs_msg = f"""
  78. This transaction includes inputs with non-{g.proj_name} addresses. When
  79. signing the transaction, private keys for the addresses listed below must
  80. be supplied using the --keys-from-file option. The key file must contain
  81. one key per line. Please note that this transaction cannot be autosigned,
  82. as autosigning does not support the use of key files.
  83. Non-{g.proj_name} addresses found in inputs:
  84. {{}}
  85. """
  86. class Input(MMGenTxIO):
  87. scriptPubKey = ListItemAttr(HexStr)
  88. sequence = ListItemAttr(int,typeconv=False)
  89. tw_copy_attrs = { 'scriptPubKey','vout','amt','comment','mmid','addr','confs','txid' }
  90. class Output(MMGenTxIO):
  91. is_chg = ListItemAttr(bool,typeconv=False)
  92. class InputList(MMGenTxIOList):
  93. desc = 'transaction inputs'
  94. class OutputList(MMGenTxIOList):
  95. desc = 'transaction outputs'
  96. def __init__(self,*args,**kwargs):
  97. self.inputs = self.InputList(self)
  98. self.outputs = self.OutputList(self)
  99. self.name = type(self).__name__
  100. self.proto = kwargs.get('proto')
  101. self.twctl = kwargs.get('twctl')
  102. @property
  103. def coin(self):
  104. return self.proto.coin
  105. @property
  106. def dcoin(self):
  107. return self.proto.dcoin
  108. def check_correct_chain(self):
  109. if hasattr(self,'rpc'):
  110. if self.chain != self.rpc.chain:
  111. die( 'TransactionChainMismatch',
  112. f'Transaction is for {self.chain}, but coin daemon chain is {self.rpc.chain}!' )
  113. def sum_inputs(self):
  114. return sum(e.amt for e in self.inputs)
  115. def sum_outputs(self,exclude=None):
  116. if exclude == None:
  117. olist = self.outputs
  118. else:
  119. olist = self.outputs[:exclude] + self.outputs[exclude+1:]
  120. if not olist:
  121. return self.proto.coin_amt('0')
  122. return self.proto.coin_amt(sum(e.amt for e in olist))
  123. def _chg_output_ops(self,op):
  124. is_chgs = [x.is_chg for x in self.outputs]
  125. if is_chgs.count(True) == 1:
  126. return (
  127. is_chgs.index(True) if op == 'idx' else
  128. self.outputs[is_chgs.index(True)] )
  129. elif is_chgs.count(True) == 0:
  130. return None
  131. else:
  132. raise ValueError('more than one change output!')
  133. @property
  134. def chg_idx(self):
  135. return self._chg_output_ops('idx')
  136. @property
  137. def chg_output(self):
  138. return self._chg_output_ops('output')
  139. def add_timestamp(self):
  140. self.timestamp = make_timestamp()
  141. def add_blockcount(self):
  142. self.blockcount = self.rpc.blockcount
  143. # returns True if comment added or changed, False otherwise
  144. def add_comment(self,infile=None):
  145. if infile:
  146. from ..fileutil import get_data_from_file
  147. self.comment = MMGenTxComment(get_data_from_file(infile,'transaction comment'))
  148. else:
  149. from ..ui import keypress_confirm,line_input
  150. if keypress_confirm(
  151. prompt = 'Edit transaction comment?' if self.comment else 'Add a comment to transaction?',
  152. default_yes = False ):
  153. res = MMGenTxComment(line_input('Comment: ',insert_txt=self.comment))
  154. if not res:
  155. ymsg('Warning: comment is empty')
  156. changed = res != self.comment
  157. self.comment = res
  158. return changed
  159. else:
  160. return False
  161. def get_non_mmaddrs(self,desc):
  162. return remove_dups(
  163. (i.addr for i in getattr(self,desc) if not i.mmid),
  164. edesc = 'non-MMGen address',
  165. quiet = True )
  166. def check_non_mmgen_inputs(self,caller,non_mmaddrs=None):
  167. non_mmaddrs = non_mmaddrs or self.get_non_mmaddrs('inputs')
  168. if non_mmaddrs:
  169. indent = ' '
  170. fs = fmt(self.non_mmgen_inputs_msg,strip_char='\t',indent=indent).strip()
  171. m = fs.format('\n '.join(non_mmaddrs))
  172. if caller in ('txdo','txsign'):
  173. if not opt.keys_from_file:
  174. die( 'UserOptError', f'\n{indent}ERROR: {m}\n' )
  175. else:
  176. msg(f'\n{indent}WARNING: {m}\n')
  177. if not opt.yes:
  178. from ..ui import keypress_confirm
  179. if not keypress_confirm('Continue?',default_yes=True):
  180. die(1,'Exiting at user request')