new.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, a command-line 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 import new as TxBase
  14. from ....obj import MMGenTxID
  15. from ....util import msg,fmt,make_chksum_6,die
  16. from .base import Base
  17. class New(Base,TxBase.New):
  18. usr_fee_prompt = 'Enter transaction fee: '
  19. fee_fail_fs = 'Network fee estimation for {c} confirmations failed ({t})'
  20. no_chg_msg = 'Warning: Change address will be deleted as transaction produces no change'
  21. @property
  22. def relay_fee(self):
  23. kb_fee = self.proto.coin_amt(self.rpc.cached['networkinfo']['relayfee'])
  24. ret = kb_fee * self.estimate_size() / 1024
  25. self.cfg._util.vmsg(f'Relay fee: {kb_fee} {self.coin}/kB, for transaction: {ret} {self.coin}')
  26. return ret
  27. async def get_rel_fee_from_network(self):
  28. try:
  29. ret = await self.rpc.call(
  30. 'estimatesmartfee',
  31. self.cfg.fee_estimate_confs,
  32. self.cfg.fee_estimate_mode.upper() )
  33. fee_per_kb = ret['feerate'] if 'feerate' in ret else -2
  34. fe_type = 'estimatesmartfee'
  35. except:
  36. args = self.rpc.daemon.estimatefee_args(self.rpc)
  37. fee_per_kb = await self.rpc.call('estimatefee',*args)
  38. fe_type = 'estimatefee'
  39. return fee_per_kb,fe_type
  40. # given tx size, rel fee and units, return absolute fee
  41. def fee_rel2abs(self,tx_size,units,amt,unit):
  42. if tx_size:
  43. return self.proto.coin_amt(amt * tx_size * getattr(self.proto.coin_amt,units[unit]))
  44. else:
  45. return None
  46. # given network fee estimate in BTC/kB, return absolute fee using estimated tx size
  47. def fee_est2abs(self,fee_per_kb,fe_type=None):
  48. from decimal import Decimal
  49. tx_size = self.estimate_size()
  50. ret = self.proto.coin_amt(
  51. fee_per_kb * Decimal(self.cfg.fee_adjust) * tx_size / 1024,
  52. from_decimal = True )
  53. if self.cfg.verbose:
  54. msg(fmt(f"""
  55. {fe_type.upper()} fee for {self.cfg.fee_estimate_confs} confirmations: {fee_per_kb} {self.coin}/kB
  56. TX size (estimated): {tx_size} bytes
  57. Fee adjustment factor: {self.cfg.fee_adjust:.2f}
  58. Absolute fee (fee_per_kb * adj_factor * tx_size / 1024): {ret} {self.coin}
  59. """).strip())
  60. return ret
  61. def convert_and_check_fee(self,fee,desc):
  62. abs_fee = self.feespec2abs(fee,self.estimate_size())
  63. if abs_fee is None:
  64. raise ValueError(f'{fee}: cannot convert {self.rel_fee_desc} to {self.coin}'
  65. + ' because transaction size is unknown')
  66. if abs_fee is False:
  67. err = f'{fee!r}: invalid TX fee (not a {self.coin} amount or {self.rel_fee_desc} specification)'
  68. elif abs_fee > self.proto.max_tx_fee:
  69. err = f'{abs_fee} {self.coin}: {desc} fee too large (maximum fee: {self.proto.max_tx_fee} {self.coin})'
  70. elif abs_fee < self.relay_fee:
  71. err = f'{abs_fee} {self.coin}: {desc} fee too small (less than relay fee of {self.relay_fee} {self.coin})'
  72. else:
  73. return abs_fee
  74. msg(err)
  75. return False
  76. async def get_input_addrs_from_cmdline(self):
  77. # Bitcoin full node, call doesn't go to the network, so just call listunspent with addrs=[]
  78. return []
  79. def update_change_output(self,funds_left):
  80. if funds_left == 0: # TODO: test
  81. msg(self.no_chg_msg)
  82. self.outputs.pop(self.chg_idx)
  83. else:
  84. self.update_output_amt(
  85. self.chg_idx,
  86. self.proto.coin_amt(funds_left) )
  87. def check_fee(self):
  88. fee = self.sum_inputs() - self.sum_outputs()
  89. if fee > self.proto.max_tx_fee:
  90. c = self.proto.coin
  91. die( 'MaxFeeExceeded', f'Transaction fee of {fee} {c} too high! (> {self.proto.max_tx_fee} {c})' )
  92. def final_inputs_ok_msg(self,funds_left):
  93. return 'Transaction produces {} {} in change'.format(
  94. self.proto.coin_amt(funds_left).hl(),
  95. self.coin
  96. )
  97. async def create_serialized(self,locktime=None,bump=None):
  98. if not bump:
  99. self.inputs.sort_bip69()
  100. # Set all sequence numbers to the same value, in conformity with the behavior of most modern wallets:
  101. do_rbf = self.proto.cap('rbf') and not self.cfg.no_rbf
  102. seqnum_val = self.proto.max_int - (2 if do_rbf else 1 if locktime else 0)
  103. for i in self.inputs:
  104. i.sequence = seqnum_val
  105. self.outputs.sort_bip69()
  106. inputs_list = [{
  107. 'txid': e.txid,
  108. 'vout': e.vout,
  109. 'sequence': e.sequence
  110. } for e in self.inputs ]
  111. outputs_dict = {e.addr:e.amt for e in self.outputs}
  112. ret = await self.rpc.call( 'createrawtransaction', inputs_list, outputs_dict )
  113. if locktime and not bump:
  114. msg(f'Setting nLockTime to {self.info.strfmt_locktime(locktime)}!')
  115. assert isinstance(locktime,int), 'locktime value not an integer'
  116. self.locktime = locktime
  117. ret = ret[:-8] + bytes.fromhex(f'{locktime:08x}')[::-1].hex()
  118. # TxID is set only once!
  119. self.txid = MMGenTxID(make_chksum_6(bytes.fromhex(ret)).upper())
  120. self.update_serialized(ret)