bump.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2025 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. tx.bump: transaction bump class
  12. """
  13. from .new_swap import NewSwap
  14. from .completed import Completed
  15. from ..util import msg, ymsg, is_int, die
  16. from ..color import pink
  17. class Bump(Completed, NewSwap):
  18. desc = 'fee-bumped transaction'
  19. ext = 'rawtx'
  20. bump_output_idx = None
  21. is_bump = True
  22. def __init__(self, *, check_sent, new_outputs, **kwargs):
  23. super().__init__(**kwargs)
  24. self.new_outputs = new_outputs
  25. self.orig_rel_fee = self.get_orig_rel_fee()
  26. if new_outputs:
  27. if self.is_swap:
  28. self.is_swap = False
  29. for attr in self.swap_attrs:
  30. setattr(self, attr, None)
  31. self.outputs = self.OutputList(self)
  32. self.cfg = kwargs['cfg'] # must use current cfg opts, not those from orig_tx
  33. if not self.is_replaceable():
  34. die(1, f'Transaction {self.txid} is not replaceable')
  35. # If sending, require original tx to be sent
  36. if check_sent and not self.coin_txid:
  37. die(1, f'Transaction {self.txid!r} was not broadcast to the network')
  38. self.coin_txid = ''
  39. self.sent_timestamp = None
  40. async def get_inputs(self, outputs_sum):
  41. return True
  42. def check_bumped_fee_ok(self, abs_fee):
  43. from ..amt import RelFeeAmt
  44. orig = RelFeeAmt(self.orig_rel_fee)
  45. new = RelFeeAmt(self.fee_abs2rel(abs_fee))
  46. if new <= orig:
  47. fs = 'New fee ({b!s} {d}) <= original fee ({a!s} {d})\nPlease choose a higher fee'
  48. ymsg(fs.format(a=orig, b=new, d=self.rel_fee_disp))
  49. return False
  50. return True
  51. async def create_feebump(self, silent):
  52. from ..rpc import rpc_init
  53. self.rpc = await rpc_init(self.cfg, self.proto)
  54. msg('Creating replacement transaction')
  55. self.check_sufficient_funds_for_bump()
  56. output_idx = self.choose_output()
  57. await self.set_gas()
  58. if not silent:
  59. msg('Minimum fee for new transaction: {} {} ({} {})'.format(
  60. self.min_fee.hl(),
  61. self.proto.coin,
  62. pink(self.fee_abs2rel(self.min_fee)),
  63. self.rel_fee_disp))
  64. if self.is_swap:
  65. self.recv_proto = self.check_swap_memo().proto
  66. self.process_swap_options()
  67. fee_hint = await self.update_vault_output(self.send_amt)
  68. else:
  69. fee_hint = None
  70. self.usr_fee = self.get_usr_fee_interactive(
  71. fee = self.cfg.fee or fee_hint,
  72. desc = 'User-selected' if self.cfg.fee else 'Recommended' if fee_hint else None)
  73. self.bump_fee(output_idx, self.usr_fee)
  74. assert self.fee <= self.proto.max_tx_fee
  75. if not self.cfg.yes:
  76. self.add_comment() # edits an existing comment
  77. await self.create_serialized()
  78. self.add_timestamp()
  79. self.add_blockcount()
  80. self.cfg._util.qmsg('Fee successfully increased')
  81. def check_sufficient_funds_for_bump(self):
  82. if not [o.amt for o in self.outputs if o.amt >= self.min_fee]:
  83. die(1,
  84. 'Transaction cannot be bumped.\n' +
  85. f'All outputs contain less than the minimum fee ({self.min_fee} {self.coin})')
  86. def choose_output(self):
  87. def check_sufficient_funds(o_amt):
  88. if o_amt < self.min_fee:
  89. msg(f'Minimum fee ({self.min_fee} {self.coin}) is greater than output amount ({o_amt} {self.coin})')
  90. return False
  91. return True
  92. if len(self.nondata_outputs) == 1:
  93. if check_sufficient_funds(self.nondata_outputs[0].amt):
  94. self.bump_output_idx = 0
  95. return 0
  96. else:
  97. die(1, 'Insufficient funds to bump transaction')
  98. init_reply = self.cfg.output_to_reduce
  99. chg_idx = self.chg_idx
  100. while True:
  101. if init_reply is None:
  102. from ..ui import line_input
  103. m = 'Choose an output to deduct the fee from (Hit ENTER for the change output): '
  104. reply = line_input(self.cfg, m) or 'c'
  105. else:
  106. reply, init_reply = init_reply, None
  107. if chg_idx is None and not is_int(reply):
  108. msg('Output must be an integer')
  109. elif chg_idx is not None and not is_int(reply) and reply != 'c':
  110. msg("Output must be an integer, or 'c' for the change output")
  111. else:
  112. idx = chg_idx if reply == 'c' else (int(reply) - 1)
  113. if idx < 0 or idx >= len(self.outputs):
  114. msg(f'Output must be in the range 1-{len(self.outputs)}')
  115. else:
  116. o_amt = self.outputs[idx].amt
  117. cm = ' (change output)' if chg_idx == idx else ''
  118. prompt = f'Fee will be deducted from output {idx+1}{cm} ({o_amt} {self.coin})'
  119. if check_sufficient_funds(o_amt):
  120. if self.cfg.yes:
  121. msg(prompt)
  122. else:
  123. from ..ui import keypress_confirm
  124. if not keypress_confirm(self.cfg, prompt+'. OK?', default_yes=True):
  125. continue
  126. self.bump_output_idx = idx
  127. return idx