sweep.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  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. xmrwallet.ops.sweep: Monero wallet ops for the MMGen Suite
  12. """
  13. from ...util import msg, msg_r, gmsg, die, fmt_dict, make_timestr
  14. from ...proto.xmr.rpc import MoneroWalletRPCClient
  15. from ...proto.xmr.daemon import MoneroWalletDaemon
  16. from ...ui import keypress_confirm
  17. from .. import tx_priorities
  18. from ..rpc import MoneroWalletRPC
  19. from .spec import OpMixinSpec
  20. from .wallet import OpWallet
  21. class OpSweep(OpMixinSpec, OpWallet):
  22. spec_id = 'sweep_spec'
  23. spec_key = ((1, 'source'), (3, 'dest'))
  24. opts = (
  25. 'no_relay',
  26. 'tx_relay_daemon',
  27. 'watch_only',
  28. 'priority',
  29. 'skip_empty_accounts',
  30. 'skip_empty_addresses')
  31. sweep_type = 'single-address'
  32. def check_uopts(self):
  33. if self.cfg.tx_relay_daemon and (self.cfg.no_relay or self.cfg.autosign):
  34. die(1, '--tx-relay-daemon makes no sense in this context!')
  35. if self.cfg.priority and self.cfg.priority not in list(tx_priorities):
  36. die(1, '{}: invalid parameter for --priority (valid params: {})'.format(
  37. self.cfg.priority,
  38. fmt_dict(tx_priorities, fmt='square_compact')))
  39. def init_tx_relay_daemon(self):
  40. m = self.parse_tx_relay_opt()
  41. wd2 = MoneroWalletDaemon(
  42. cfg = self.cfg,
  43. proto = self.proto,
  44. wallet_dir = self.cfg.wallet_dir or '.',
  45. test_suite = self.cfg.test_suite,
  46. monerod_addr = m[1],
  47. proxy = m[2])
  48. if self.cfg.test_suite:
  49. wd2.usr_daemon_args = ['--daemon-ssl-allow-any-cert']
  50. wd2.start()
  51. self.c = MoneroWalletRPCClient(
  52. cfg = self.cfg,
  53. daemon = wd2)
  54. def create_tx(self, h, wallet_data):
  55. def create_new_addr_maybe(h, account, label):
  56. if keypress_confirm(self.cfg, f'\nCreate new address for account #{account}?'):
  57. return h.create_new_addr(account, label)
  58. elif not keypress_confirm(self.cfg, f'Sweep to last existing address of account #{account}?'):
  59. die(1, 'Exiting at user request')
  60. return None
  61. dest_addr_chk = None
  62. if self.dest is None: # sweep to same account
  63. dest_acct = self.account
  64. dest_addr_chk = create_new_addr_maybe(
  65. h, self.account, f'{self.name} from this account [{make_timestr()}]')
  66. if dest_addr_chk:
  67. wallet_data = h.get_wallet_data(print=False)
  68. dest_addr, dest_addr_idx = h.get_last_addr(self.account, wallet_data, display=not dest_addr_chk)
  69. if dest_addr_chk:
  70. h.print_acct_addrs(wallet_data, self.account)
  71. elif self.dest_acct is None: # sweep to wallet
  72. h.close_wallet('source')
  73. h2 = MoneroWalletRPC(self, self.dest)
  74. h2.open_wallet('destination')
  75. wallet_data2 = h2.get_wallet_data()
  76. wf = self.get_wallet_fn(self.dest)
  77. if keypress_confirm(self.cfg, f'\nCreate new account for wallet {wf.name!r}?'):
  78. dest_acct, dest_addr = h2.create_acct(
  79. label = f'{self.name} from {self.source.idx}:{self.account} [{make_timestr()}]')
  80. dest_addr_idx = 0
  81. h2.get_wallet_data()
  82. elif keypress_confirm(self.cfg, f'Sweep to last existing account of wallet {wf.name!r}?'):
  83. dest_acct, dest_addr_chk = h2.get_last_acct(wallet_data2.accts_data)
  84. dest_addr, dest_addr_idx = h2.get_last_addr(dest_acct, wallet_data2, display=False)
  85. else:
  86. die(1, 'Exiting at user request')
  87. h2.close_wallet('destination')
  88. h.open_wallet('source', refresh=False)
  89. else: # sweep to specific account of wallet
  90. def get_dest_addr_params(h, wallet_data, dest_acct, label):
  91. self.check_account_exists(wallet_data.accts_data, dest_acct)
  92. h.print_acct_addrs(wallet_data, dest_acct)
  93. dest_addr_chk = create_new_addr_maybe(h, dest_acct, label)
  94. if dest_addr_chk:
  95. wallet_data = h.get_wallet_data(print=False)
  96. dest_addr, dest_addr_idx = h.get_last_addr(dest_acct, wallet_data, display=not dest_addr_chk)
  97. if dest_addr_chk:
  98. h.print_acct_addrs(wallet_data, dest_acct)
  99. return dest_addr, dest_addr_idx, dest_addr_chk
  100. dest_acct = self.dest_acct
  101. if self.dest == self.source:
  102. dest_addr, dest_addr_idx, dest_addr_chk = get_dest_addr_params(
  103. h, wallet_data, dest_acct,
  104. f'{self.name} from account #{self.account} [{make_timestr()}]')
  105. else:
  106. h.close_wallet('source')
  107. h2 = MoneroWalletRPC(self, self.dest)
  108. h2.open_wallet('destination')
  109. dest_addr, dest_addr_idx, dest_addr_chk = get_dest_addr_params(
  110. h2, h2.get_wallet_data(), dest_acct,
  111. f'{self.name} from {self.source.idx}:{self.account} [{make_timestr()}]')
  112. h2.close_wallet('destination')
  113. h.open_wallet('source', refresh=False)
  114. assert dest_addr_chk in (None, dest_addr), (
  115. f'dest_addr: ({dest_addr}) != dest_addr_chk: ({dest_addr_chk})')
  116. msg(f'\n Creating {self.name} transaction...')
  117. return (h, h.make_sweep_tx(self.account, dest_acct, dest_addr_idx, dest_addr, wallet_data.addrs_data))
  118. @property
  119. def add_desc(self):
  120. return (
  121. r' to new address' if self.dest is None else
  122. f' to new account in wallet {self.dest.idx}' if self.dest_acct is None else
  123. f' to account #{self.dest_acct} of wallet {self.dest.idx}') + f' ({self.sweep_type} sweep)'
  124. def check_account_exists(self, accts_data, idx):
  125. max_acct = len(accts_data['subaddress_accounts']) - 1
  126. if self.account > max_acct:
  127. die(2, f'{self.account}: requested account index out of bounds (>{max_acct})')
  128. async def main(self):
  129. gmsg(
  130. f'\n{self.stem.capitalize()}ing account #{self.account}'
  131. f' of wallet {self.source.idx}{self.add_desc}')
  132. h = MoneroWalletRPC(self, self.source)
  133. h.open_wallet('source')
  134. wallet_data = h.get_wallet_data(skip_empty_ok=True)
  135. self.check_account_exists(wallet_data.accts_data, self.account)
  136. h.print_acct_addrs(wallet_data, self.account)
  137. h, new_tx = self.create_tx(h, wallet_data)
  138. msg('\n' + new_tx.get_info(indent=' '))
  139. if self.cfg.tx_relay_daemon:
  140. self.display_tx_relay_info(indent=' ')
  141. msg('Saving TX data to file')
  142. new_tx.write(delete_metadata=True)
  143. if self.cfg.no_relay or self.cfg.autosign:
  144. return True
  145. if keypress_confirm(self.cfg, f'Relay {self.name} transaction?'):
  146. if self.cfg.tx_relay_daemon:
  147. await h.stop_wallet('source')
  148. msg('')
  149. self.init_tx_relay_daemon()
  150. h = MoneroWalletRPC(self, self.source)
  151. h.open_wallet('TX-relay-configured source', refresh=False)
  152. msg_r(f'\n Relaying {self.name} transaction...')
  153. h.relay_tx(new_tx.data.metadata)
  154. gmsg('\nAll done')
  155. return True
  156. else:
  157. die(1, '\nExiting at user request')
  158. class OpSweepAll(OpSweep):
  159. stem = 'sweep'
  160. sweep_type = 'all-address'
  161. class OpTransfer(OpSweep):
  162. stem = 'transferr'
  163. spec_id = 'transfer_spec'
  164. spec_key = ((1, 'source'),)
  165. @property
  166. def add_desc(self):
  167. return f': {self.amount} XMR to {self.dest_addr}'
  168. def create_tx(self, h, wallet_data):
  169. msg(f'\n Creating {self.name} transaction...')
  170. return (h, h.make_transfer_tx(self.account, self.dest_addr, self.amount))