bump.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  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 orange, 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. elif self.is_swap and self.is_token:
  34. die(1,
  35. orange('Fee-bumping of token swap transactions currently not supported.\n') +
  36. orange('To bump the transaction, supply new outputs on the command line'))
  37. if not self.is_replaceable():
  38. die(1, f'Transaction {self.txid} is not replaceable')
  39. # If sending, require original tx to be sent
  40. if check_sent and not self.coin_txid:
  41. die(1, f'Transaction {self.txid!r} was not broadcast to the network')
  42. self.coin_txid = ''
  43. self.sent_timestamp = None
  44. async def get_inputs(self, outputs_sum):
  45. return True
  46. def check_bumped_fee_ok(self, abs_fee):
  47. from ..amt import RelFeeAmt
  48. orig = RelFeeAmt(self.orig_rel_fee)
  49. new = RelFeeAmt(self.fee_abs2rel(abs_fee))
  50. if new <= orig:
  51. fs = 'New fee ({b!s} {d}) <= original fee ({a!s} {d})\nPlease choose a higher fee'
  52. ymsg(fs.format(a=orig, b=new, d=self.rel_fee_disp))
  53. return False
  54. return True
  55. async def create_feebump(self, silent):
  56. from ..rpc import rpc_init
  57. self.rpc = await rpc_init(self.cfg, self.proto)
  58. msg('Creating replacement transaction')
  59. self.check_sufficient_funds_for_bump()
  60. output_idx = self.choose_output()
  61. await self.set_gas(force=True)
  62. if not silent:
  63. msg('Minimum fee for new transaction: {} {} ({} {})'.format(
  64. self.min_fee.hl(),
  65. self.proto.coin,
  66. pink(self.fee_abs2rel(self.min_fee)),
  67. self.rel_fee_disp))
  68. if self.is_swap:
  69. self.recv_proto = self.check_swap_memo().proto
  70. self.swap_cfg = self.swap_proto_mod.SwapCfg(self.cfg)
  71. fee_hint = await self.update_vault_output(self.send_amt)
  72. else:
  73. fee_hint = None
  74. self.usr_fee = self.get_usr_fee_interactive(
  75. fee = self.cfg.fee or fee_hint,
  76. desc = 'User-selected' if self.cfg.fee else 'Recommended' if fee_hint else None)
  77. self.bump_fee(output_idx, self.usr_fee)
  78. assert self.fee <= self.proto.max_tx_fee
  79. if not self.cfg.yes:
  80. self.add_comment() # edits an existing comment
  81. await self.create_serialized()
  82. self.add_timestamp()
  83. self.add_blockcount()
  84. self.cfg._util.qmsg('Fee successfully increased')
  85. def check_sufficient_funds_for_bump(self):
  86. if not [o.amt for o in self.outputs if o.amt >= self.min_fee]:
  87. die(1,
  88. 'Transaction cannot be bumped.\n' +
  89. f'All outputs contain less than the minimum fee ({self.min_fee} {self.coin})')
  90. def choose_output(self):
  91. def check_sufficient_funds(o_amt):
  92. if o_amt < self.min_fee:
  93. msg(f'Minimum fee ({self.min_fee} {self.coin}) is greater than output amount ({o_amt} {self.coin})')
  94. return False
  95. return True
  96. if len(self.nondata_outputs) == 1:
  97. if check_sufficient_funds(self.nondata_outputs[0].amt):
  98. self.bump_output_idx = 0
  99. return 0
  100. else:
  101. die(1, 'Insufficient funds to bump transaction')
  102. init_reply = self.cfg.output_to_reduce
  103. chg_idx = self.chg_idx
  104. while True:
  105. if init_reply is None:
  106. from ..ui import line_input
  107. m = 'Choose an output to deduct the fee from (Hit ENTER for the change output): '
  108. reply = line_input(self.cfg, m) or 'c'
  109. else:
  110. reply, init_reply = init_reply, None
  111. if chg_idx is None and not is_int(reply):
  112. msg('Output must be an integer')
  113. elif chg_idx is not None and not is_int(reply) and reply != 'c':
  114. msg("Output must be an integer, or 'c' for the change output")
  115. else:
  116. idx = chg_idx if reply == 'c' else (int(reply) - 1)
  117. if idx < 0 or idx >= len(self.outputs):
  118. msg(f'Output must be in the range 1-{len(self.outputs)}')
  119. else:
  120. o_amt = self.outputs[idx].amt
  121. cm = ' (change output)' if chg_idx == idx else ''
  122. prompt = f'Fee will be deducted from output {idx+1}{cm} ({o_amt} {self.coin})'
  123. if check_sufficient_funds(o_amt):
  124. if self.cfg.yes:
  125. msg(prompt)
  126. else:
  127. from ..ui import keypress_confirm
  128. if not keypress_confirm(self.cfg, prompt+'. OK?', default_yes=True):
  129. continue
  130. self.bump_output_idx = idx
  131. return idx