online.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  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.online: online signed transaction class
  12. """
  13. import sys, time, asyncio
  14. from ..util import msg, Msg, gmsg, ymsg, make_timestr, die
  15. from ..color import pink, yellow
  16. from .signed import Signed, AutomountSigned
  17. class OnlineSigned(Signed):
  18. @property
  19. def status(self):
  20. from . import _base_proto_subclass
  21. return _base_proto_subclass('Status', 'status', {'proto': self.proto})(self)
  22. def check_swap_expiry(self):
  23. from ..util2 import format_elapsed_hr
  24. expiry = self.swap_quote_expiry
  25. now = int(time.time())
  26. t_rem = expiry - now
  27. clr = yellow if t_rem < 0 else pink
  28. msg('Swap quote {a} {b} [{c}]'.format(
  29. a = clr('expired' if t_rem < 0 else 'expires'),
  30. b = clr(format_elapsed_hr(expiry, now=now, future_msg='from now')),
  31. c = make_timestr(expiry)))
  32. return t_rem >= 0
  33. def confirm_send(self, idxs):
  34. from ..ui import confirm_or_raise
  35. confirm_or_raise(
  36. cfg = self.cfg,
  37. message = '' if self.cfg.quiet else
  38. 'Once this transaction is sent, there’s no taking it back!',
  39. action = 'broadcast this transaction to the {} {} network'.format(
  40. self.proto.coin,
  41. self.proto.network.upper()),
  42. expect = 'YES')
  43. msg('Sending transaction')
  44. if len(idxs) > 1 and getattr(self, 'coin_txid2', None) and self.is_swap:
  45. ymsg('Warning: two transactions (approval and router) will be broadcast to the network')
  46. async def post_send(self, asi):
  47. from . import SentTX
  48. tx2 = await SentTX(cfg=self.cfg, data=self.__dict__, automount=bool(asi))
  49. tx2.add_sent_timestamp()
  50. tx2.add_blockcount()
  51. tx2.file.write(
  52. outdir = asi.txauto_dir if asi else None,
  53. ask_overwrite = False,
  54. ask_write = False)
  55. async def send(self, cfg, asi):
  56. status_exitval = None
  57. sent_status = None
  58. all_ok = True
  59. idxs = ['', '2']
  60. if cfg.txhex_idx:
  61. if getattr(self, 'coin_txid2', None):
  62. if cfg.txhex_idx in ('1', '2'):
  63. idxs = ['' if cfg.txhex_idx == '1' else cfg.txhex_idx]
  64. else:
  65. die(1, f'{cfg.txhex_idx}: invalid parameter for --txhex-idx (must be 1 or 2)')
  66. else:
  67. die(1, 'Transaction has only one part, so --txhex-idx makes no sense')
  68. if not (cfg.status or cfg.receipt or cfg.dump_hex or cfg.test):
  69. self.confirm_send(idxs)
  70. for idx in idxs:
  71. if coin_txid := getattr(self, f'coin_txid{idx}', None):
  72. txhex = getattr(self, f'serialized{idx}')
  73. if cfg.status:
  74. cfg._util.qmsg(f'{self.proto.coin} txid: {coin_txid.hl()}')
  75. if cfg.verbose:
  76. await self.post_network_send(coin_txid)
  77. status_exitval = await self.status.display(idx=idx)
  78. elif cfg.receipt:
  79. if res := await self.get_receipt(coin_txid, receipt_only=True):
  80. import json
  81. Msg(json.dumps(res, indent=4))
  82. else:
  83. msg(f'Unable to get receipt for TX {coin_txid.hl()}')
  84. elif cfg.dump_hex:
  85. from ..fileutil import write_data_to_file
  86. write_data_to_file(
  87. cfg,
  88. cfg.dump_hex + idx,
  89. txhex + '\n',
  90. desc = 'serialized transaction hex data',
  91. ask_overwrite = False,
  92. ask_tty = False)
  93. elif cfg.tx_proxy:
  94. if idx != '' and not cfg.test_suite:
  95. await asyncio.sleep(2)
  96. from .tx_proxy import send_tx
  97. msg('{} TX: {}'.format('Testing' if cfg.test else 'Sending', coin_txid.hl()))
  98. if ret := send_tx(cfg, txhex):
  99. if ret != coin_txid:
  100. ymsg(f'Warning: txid mismatch (after sending) ({ret} != {coin_txid})')
  101. sent_status = 'confirm_post_send'
  102. if cfg.test:
  103. break
  104. elif cfg.test:
  105. if await self.test_sendable(txhex):
  106. gmsg('Transaction can be sent')
  107. else:
  108. ymsg('Transaction cannot be sent')
  109. else: # node send
  110. msg(f'Sending TX: {coin_txid.hl()}')
  111. if cfg.bogus_send:
  112. msg(f'BOGUS transaction NOT sent: {coin_txid.hl()}')
  113. else:
  114. if idx != '':
  115. await asyncio.sleep(1)
  116. ret = await self.send_with_node(txhex)
  117. msg(f'Transaction sent: {coin_txid.hl()}')
  118. if ret != coin_txid:
  119. die('TxIDMismatch', f'txid mismatch (after sending) ({ret} != {coin_txid})')
  120. sent_status = 'no_confirm_post_send'
  121. if cfg.wait and sent_status:
  122. res = await self.post_network_send(coin_txid)
  123. if all_ok:
  124. all_ok = res
  125. if not cfg.txhex_idx and sent_status and all_ok:
  126. from ..ui import keypress_confirm
  127. if sent_status == 'no_confirm_post_send' or not asi or keypress_confirm(
  128. cfg, 'Mark transaction as sent on removable device?'):
  129. await self.post_send(asi)
  130. if status_exitval is not None:
  131. if cfg.verbose:
  132. self.info.view_with_prompt('View transaction details?', pause=False)
  133. sys.exit(status_exitval)
  134. class AutomountOnlineSigned(AutomountSigned, OnlineSigned):
  135. pass
  136. class Sent(OnlineSigned):
  137. desc = 'sent transaction'
  138. ext = 'subtx'
  139. class AutomountSent(AutomountOnlineSigned):
  140. desc = 'sent automount transaction'
  141. ext = 'asubtx'