new.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2024 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.new: Bitcoin new transaction class
  12. """
  13. from ....tx.new import New as TxNew
  14. from ....obj import MMGenTxID
  15. from ....util import msg, fmt, make_chksum_6, die, suf
  16. from ....color import pink, yellow
  17. from .base import Base
  18. class New(Base, TxNew):
  19. usr_fee_prompt = 'Enter transaction fee: '
  20. fee_fail_fs = 'Network fee estimation for {c} confirmations failed ({t})'
  21. no_chg_msg = 'Warning: Change address will be deleted as transaction produces no change'
  22. def process_data_output_arg(self, arg):
  23. if any(arg.startswith(pfx) for pfx in ('data:', 'hexdata:')):
  24. if hasattr(self, '_have_op_return_data'):
  25. die(1, 'Transaction may have at most one OP_RETURN data output!')
  26. self._have_op_return_data = True
  27. from .op_return_data import OpReturnData
  28. OpReturnData(self.proto, arg) # test data for validity
  29. return arg
  30. @property
  31. def relay_fee(self):
  32. kb_fee = self.proto.coin_amt(self.rpc.cached['networkinfo']['relayfee'])
  33. ret = kb_fee * self.estimate_size() / 1024
  34. self.cfg._util.vmsg(f'Relay fee: {kb_fee} {self.coin}/kB, for transaction: {ret} {self.coin}')
  35. return ret
  36. @property
  37. def network_estimated_fee_label(self):
  38. return 'Network-estimated ({}, {} conf{})'.format(
  39. self.cfg.fee_estimate_mode.upper(),
  40. pink(str(self.cfg.fee_estimate_confs)),
  41. suf(self.cfg.fee_estimate_confs))
  42. def warn_fee_estimate_fail(self, fe_type):
  43. if not hasattr(self, '_fee_estimate_fail_warning_shown'):
  44. msg(self.fee_fail_fs.format(
  45. c = self.cfg.fee_estimate_confs,
  46. t = fe_type))
  47. self._fee_estimate_fail_warning_shown = True
  48. async def get_rel_fee_from_network(self):
  49. try:
  50. ret = await self.rpc.call(
  51. 'estimatesmartfee',
  52. self.cfg.fee_estimate_confs,
  53. self.cfg.fee_estimate_mode.upper())
  54. fee_per_kb = self.proto.coin_amt(ret['feerate']) if 'feerate' in ret else None
  55. fe_type = 'estimatesmartfee'
  56. except:
  57. args = self.rpc.daemon.estimatefee_args(self.rpc)
  58. ret = await self.rpc.call('estimatefee', *args)
  59. fee_per_kb = self.proto.coin_amt(ret)
  60. fe_type = 'estimatefee'
  61. if fee_per_kb is None:
  62. self.warn_fee_estimate_fail(fe_type)
  63. return fee_per_kb, fe_type
  64. # given tx size, rel fee and units, return absolute fee
  65. def fee_rel2abs(self, tx_size, units, amt_in_units, unit):
  66. return self.proto.coin_amt(amt_in_units * tx_size, from_unit=units[unit])
  67. # given network fee estimate in BTC/kB, return absolute fee using estimated tx size
  68. def fee_est2abs(self, fee_per_kb, fe_type=None):
  69. tx_size = self.estimate_size()
  70. ret = self.proto.coin_amt('1') * (fee_per_kb * self.cfg.fee_adjust * tx_size / 1024)
  71. if self.cfg.verbose:
  72. msg(fmt(f"""
  73. {fe_type.upper()} fee for {self.cfg.fee_estimate_confs} confirmations: {fee_per_kb} {self.coin}/kB
  74. TX size (estimated): {tx_size} bytes
  75. Fee adjustment factor: {self.cfg.fee_adjust:.2f}
  76. Absolute fee (fee_per_kb * adj_factor * tx_size / 1024): {ret} {self.coin}
  77. """).strip())
  78. return ret
  79. def convert_and_check_fee(self, fee, desc):
  80. abs_fee = self.feespec2abs(fee, self.estimate_size())
  81. if abs_fee is None:
  82. raise ValueError(f'{fee}: cannot convert {self.rel_fee_desc} to {self.coin}'
  83. + ' because transaction size is unknown')
  84. if abs_fee is False:
  85. err = f'{fee!r}: invalid TX fee (not a {self.coin} amount or {self.rel_fee_desc} specification)'
  86. elif abs_fee > self.proto.max_tx_fee:
  87. err = f'{abs_fee} {self.coin}: {desc} fee too large (maximum fee: {self.proto.max_tx_fee} {self.coin})'
  88. elif abs_fee < self.relay_fee:
  89. err = f'{abs_fee} {self.coin}: {desc} fee too small (less than relay fee of {self.relay_fee} {self.coin})'
  90. else:
  91. return abs_fee
  92. msg(err)
  93. return False
  94. async def get_input_addrs_from_inputs_opt(self):
  95. # Bitcoin full node, call doesn't go to the network, so just call listunspent with addrs=[]
  96. return []
  97. def update_change_output(self, funds_left):
  98. if funds_left == 0: # TODO: test
  99. msg(self.no_chg_msg)
  100. self.outputs.pop(self.chg_idx)
  101. else:
  102. self.update_output_amt(self.chg_idx, funds_left)
  103. def check_fee(self):
  104. fee = self.sum_inputs() - self.sum_outputs()
  105. if fee > self.proto.max_tx_fee:
  106. c = self.proto.coin
  107. die('MaxFeeExceeded', f'Transaction fee of {fee} {c} too high! (> {self.proto.max_tx_fee} {c})')
  108. def final_inputs_ok_msg(self, funds_left):
  109. return 'Transaction produces {} {} in change'.format(funds_left.hl(), self.coin)
  110. def check_chg_addr_is_wallet_addr(self):
  111. if len([o for o in self.outputs if not o.data]) > 1 and not self.chg_output.mmid:
  112. from ....ui import confirm_or_raise
  113. confirm_or_raise(
  114. cfg = self.cfg,
  115. message = yellow('Change address is not an MMGen wallet address!'),
  116. action = 'Are you sure this is what you want?')
  117. async def create_serialized(self, locktime=None, bump=None):
  118. if not bump:
  119. self.inputs.sort_bip69()
  120. # Set all sequence numbers to the same value, in conformity with the behavior of most modern wallets:
  121. do_rbf = self.proto.cap('rbf') and not self.cfg.no_rbf
  122. seqnum_val = self.proto.max_int - (2 if do_rbf else 1 if locktime else 0)
  123. for i in self.inputs:
  124. i.sequence = seqnum_val
  125. self.outputs.sort_bip69()
  126. inputs_list = [{
  127. 'txid': e.txid,
  128. 'vout': e.vout,
  129. 'sequence': e.sequence
  130. } for e in self.inputs]
  131. outputs_dict = dict((e.addr, e.amt) if e.addr else ('data', e.data.hex()) for e in self.outputs)
  132. ret = await self.rpc.call('createrawtransaction', inputs_list, outputs_dict)
  133. if locktime and not bump:
  134. msg(f'Setting nLockTime to {self.info.strfmt_locktime(locktime)}!')
  135. assert isinstance(locktime, int), 'locktime value not an integer'
  136. self.locktime = locktime
  137. ret = ret[:-8] + bytes.fromhex(f'{locktime:08x}')[::-1].hex()
  138. # TxID is set only once!
  139. self.txid = MMGenTxID(make_chksum_6(bytes.fromhex(ret)).upper())
  140. self.update_serialized(ret)