Browse Source

XMR compat: sweep transactions

Example:

    # Invoking `mmgen-txcreate` without arguments activates the sweep function-
    # ality.  Type ‘S’ (account sweep) or ‘s’ (address sweep) to begin creating
    # the transaction:
    $ mmgen-txcreate --coin=xmr

Testing/demo:

    $ test/cmdtest.py --coin=xmr -e -X alice_twview_chk4 xmr_compat
The MMGen Project 3 days ago
parent
commit
907bdc2bdf

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-16.1.dev25
+16.1.dev26

+ 1 - 1
mmgen/help/help_notes.py

@@ -23,7 +23,7 @@ class help_notes:
 			case 'Bitcoin':
 			case 'Bitcoin':
 				return '[ADDR,AMT ... | DATA_SPEC] ADDR [addr file ...]'
 				return '[ADDR,AMT ... | DATA_SPEC] ADDR [addr file ...]'
 			case 'Monero':
 			case 'Monero':
-				return 'ADDR,AMT'
+				return '[ADDR,AMT]'
 			case _:
 			case _:
 				return 'ADDR,AMT [addr file ...]'
 				return 'ADDR,AMT [addr file ...]'
 
 

+ 5 - 0
mmgen/help/txcreate_examples.py

@@ -22,6 +22,7 @@ def help(proto, cfg):
 	addr = t.privhex2addr('bead' * 16)
 	addr = t.privhex2addr('bead' * 16)
 	sample_addr = addr.views[addr.view_pref]
 	sample_addr = addr.views[addr.view_pref]
 	cmd_base = gc.prog_name + ('' if proto.coin == 'BTC' else f' --coin={proto.coin.lower()}')
 	cmd_base = gc.prog_name + ('' if proto.coin == 'BTC' else f' --coin={proto.coin.lower()}')
+	action = 'Create' if gc.prog_name == 'mmgen-txcreate' else 'Execute'
 
 
 	match proto.base_proto:
 	match proto.base_proto:
 		case 'Bitcoin':
 		case 'Bitcoin':
@@ -64,6 +65,10 @@ EXAMPLES:
   Send 0.123 {proto.coin} to an external {proto.name} address:
   Send 0.123 {proto.coin} to an external {proto.name} address:
 
 
     $ {cmd_base} {sample_addr},0.123
     $ {cmd_base} {sample_addr},0.123
+
+  {action} a sweep transaction:
+
+    $ {cmd_base}
 """
 """
 
 
 		case _:
 		case _:

+ 1 - 1
mmgen/main_txcreate.py

@@ -130,7 +130,7 @@ if cfg.list_assets:
 	Msg('AVAILABLE SWAP ASSETS:\n' + sp.SwapAsset('BTC', 'send').fmt_assets_data(indent='  '))
 	Msg('AVAILABLE SWAP ASSETS:\n' + sp.SwapAsset('BTC', 'send').fmt_assets_data(indent='  '))
 	sys.exit(0)
 	sys.exit(0)
 
 
-if not (cfg.info or cfg.contract_data):
+if not (cfg.info or cfg.contract_data or cfg.coin == 'XMR'):
 	if len(cfg._args) < {'tx': 1, 'swaptx': 2}[target]:
 	if len(cfg._args) < {'tx': 1, 'swaptx': 2}[target]:
 		cfg._usage()
 		cfg._usage()
 
 

+ 36 - 0
mmgen/proto/xmr/tw/unspent.py

@@ -30,4 +30,40 @@ class MoneroTwUnspentOutputs(MoneroTwView, TwUnspentOutputs):
 		self.extra_key_mappings = {
 		self.extra_key_mappings = {
 			'R': 'a_sync_wallets',
 			'R': 'a_sync_wallets',
 			'A': 's_age'}
 			'A': 's_age'}
+		if tx and tx.is_sweep:
+			self.prompt_fs_in[-1] = 'Actions: [q]uit, add [l]abel, r[e]draw, [R]efresh balances:'
+			self.prompt_fs_in.insert(-1, 'Transaction ops: [s]weep to address, [S]weep to account')
+			self.extra_key_mappings.update({
+				's': 'i_addr_sweep',
+				'S': 'i_acct_sweep'})
 		await super().__init__(cfg, proto, minconf=minconf, addrs=addrs, tx=tx)
 		await super().__init__(cfg, proto, minconf=minconf, addrs=addrs, tx=tx)
+
+	async def get_idx_from_user(self, method_name):
+		if method_name in ('i_acct_sweep', 'i_addr_sweep'):
+			from collections import namedtuple
+			ret = []
+			for acct_desc in {
+					'i_acct_sweep': ['source', 'destination'],
+					'i_addr_sweep': ['source']}[method_name]:
+				if res := await self.get_idx(f'{acct_desc} account number', self.accts_data):
+					ret.append(res)
+				else:
+					return None
+			return namedtuple('usr_idx_data', 'idx acct_addr_idx', defaults=[None])(*ret)
+		else:
+			return await super().get_idx_from_user(method_name)
+
+	class item_action(TwUnspentOutputs.item_action):
+		acct_methods = ('i_acct_sweep', 'i_addr_sweep')
+
+		async def i_acct_sweep(self, parent, idx, acct_addr_idx=None):
+			d = parent.accts_data
+			d1 = d[idx.idx - 1]
+			d2 = d[acct_addr_idx.idx - 1]
+			parent.tx.sweep_spec = f'{d1.idx}:{d1.acct_idx},{d2.idx}:{d2.acct_idx}'
+			return 'quit_view'
+
+		async def i_addr_sweep(self, parent, idx, acct_addr_idx=None):
+			d = parent.accts_data[idx.idx - 1]
+			parent.tx.sweep_spec = f'{d.idx}:{d.acct_idx}'
+			return 'quit_view'

+ 19 - 1
mmgen/proto/xmr/tx/new.py

@@ -18,6 +18,11 @@ from .base import Base
 
 
 class New(Base, TxNew):
 class New(Base, TxNew):
 
 
+	async def create(self, cmd_args, **kwargs):
+		self.is_sweep = not cmd_args
+		self.sweep_spec = None
+		return await super().create(cmd_args, **kwargs)
+
 	async def get_input_addrs_from_inputs_opt(self):
 	async def get_input_addrs_from_inputs_opt(self):
 		return [] # TODO
 		return [] # TODO
 
 
@@ -42,7 +47,20 @@ class New(Base, TxNew):
 
 
 	async def compat_create(self):
 	async def compat_create(self):
 
 
-		if True:
+		if self.is_sweep:
+			if not self.sweep_spec:
+				from ....util import ymsg
+				ymsg('No transaction operation specified. Exiting')
+				return None
+			from ....ui import item_chooser
+			from ....color import pink
+			op = item_chooser(
+				self.cfg,
+				'Choose the sweep operation type',
+				('sweep', 'sweep_all'),
+				lambda s: pink(s.upper())).item
+			spec = self.sweep_spec
+		else:
 			op = 'transfer'
 			op = 'transfer'
 			i = self.inputs[0]
 			i = self.inputs[0]
 			o = self.outputs[0]
 			o = self.outputs[0]

+ 2 - 1
mmgen/tw/unspent.py

@@ -95,7 +95,8 @@ class TwUnspentOutputs(TwView):
 			self.__dict__['proto'] = proto
 			self.__dict__['proto'] = proto
 			MMGenListItem.__init__(self, **kwargs)
 			MMGenListItem.__init__(self, **kwargs)
 
 
-	async def __init__(self, cfg, proto, *, minconf=1, addrs=[]):
+	async def __init__(self, cfg, proto, *, minconf=1, addrs=[], tx=None):
+		self.tx  = tx
 		await super().__init__(cfg, proto)
 		await super().__init__(cfg, proto)
 		self.minconf  = NonNegativeInt(minconf)
 		self.minconf  = NonNegativeInt(minconf)
 		self.addrs    = addrs
 		self.addrs    = addrs

+ 5 - 1
mmgen/tw/view.py

@@ -639,7 +639,9 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 				case ch if ch in key_mappings:
 				case ch if ch in key_mappings:
 					func = action_classes[ch].run
 					func = action_classes[ch].run
 					arg = action_methods[ch]
 					arg = action_methods[ch]
-					await func(self, arg) if isAsync(func) else func(self, arg)
+					ret = await func(self, arg) if isAsync(func) else func(self, arg)
+					if ret == 'quit_view':
+						return cleanup()
 				case 'q':
 				case 'q':
 					return cleanup(add_nl=True)
 					return cleanup(add_nl=True)
 				case _:
 				case _:
@@ -696,6 +698,8 @@ class TwView(MMGenObject, metaclass=AsyncInit):
 			await do_error_msg()
 			await do_error_msg()
 
 
 	async def post_action_cleanup(self, ret):
 	async def post_action_cleanup(self, ret):
+		if ret == 'quit_view':
+			return ret
 		if self.scroll and (ret is False or ret in ('redraw', 'erase')):
 		if self.scroll and (ret is False or ret in ('redraw', 'erase')):
 			# error messages could leave screen in messy state, so do complete redraw:
 			# error messages could leave screen in messy state, so do complete redraw:
 			msg_r(
 			msg_r(

+ 9 - 2
mmgen/tx/new.py

@@ -79,6 +79,7 @@ def parse_fee_spec(proto, fee_arg):
 class New(Base):
 class New(Base):
 
 
 	fee_is_approximate = False
 	fee_is_approximate = False
+	is_sweep = False
 	msg_wallet_low_coin = 'Wallet has insufficient funds for this transaction ({} {} needed)'
 	msg_wallet_low_coin = 'Wallet has insufficient funds for this transaction ({} {} needed)'
 	msg_no_change_output = """
 	msg_no_change_output = """
 		ERROR: No change address specified.  If you wish to create a transaction with
 		ERROR: No change address specified.  If you wish to create a transaction with
@@ -455,7 +456,7 @@ class New(Base):
 		if self.cfg.comment_file:
 		if self.cfg.comment_file:
 			self.add_comment(infile=self.cfg.comment_file)
 			self.add_comment(infile=self.cfg.comment_file)
 
 
-		if not do_info:
+		if cmd_args and not do_info:
 			cmd_args, addrfile_args = self.get_addrfiles_from_cmdline(cmd_args)
 			cmd_args, addrfile_args = self.get_addrfiles_from_cmdline(cmd_args)
 			if self.is_swap:
 			if self.is_swap:
 				cmd_args = await self.process_swap_cmdline_args(cmd_args, addrfile_args)
 				cmd_args = await self.process_swap_cmdline_args(cmd_args, addrfile_args)
@@ -475,7 +476,8 @@ class New(Base):
 				self.cfg,
 				self.cfg,
 				self.proto,
 				self.proto,
 				minconf = self.cfg.minconf,
 				minconf = self.cfg.minconf,
-				addrs = await self.get_input_addrs_from_inputs_opt())
+				addrs = await self.get_input_addrs_from_inputs_opt(),
+				tx = self if self.is_sweep else None)
 			await self.twuo.get_data()
 			await self.twuo.get_data()
 			self.twctl = self.twuo.twctl
 			self.twctl = self.twuo.twctl
 
 
@@ -485,6 +487,11 @@ class New(Base):
 		if not (self.is_bump or self.cfg.inputs):
 		if not (self.is_bump or self.cfg.inputs):
 			await self.twuo.view_filter_and_sort()
 			await self.twuo.view_filter_and_sort()
 
 
+		if self.is_sweep:
+			del self.twctl
+			del self.twuo.twctl
+			return await self.compat_create()
+
 		if not self.is_bump:
 		if not self.is_bump:
 			self.twuo.display_total()
 			self.twuo.display_total()
 
 

+ 3 - 4
mmgen/xmrwallet/ops/sweep.py

@@ -169,10 +169,9 @@ class OpSweep(OpMixinSpec, OpWallet):
 			die(2, f'{self.account}: requested account index out of bounds (>{max_acct})')
 			die(2, f'{self.account}: requested account index out of bounds (>{max_acct})')
 
 
 	async def main(self):
 	async def main(self):
-		if not self.compat_call:
-			gmsg(
-				f'\n{self.stem.capitalize()}ing account #{self.account}'
-				f' of wallet {self.source.idx}{self.add_desc}')
+		gmsg(
+			f'\n{self.stem.capitalize()}ing account #{self.account}'
+			f' of wallet {self.source.idx}{self.add_desc}')
 
 
 		h = MoneroWalletRPC(self, self.source)
 		h = MoneroWalletRPC(self, self.source)
 
 

+ 35 - 2
test/cmdtest_d/xmr_autosign.py

@@ -548,6 +548,16 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 		('alice_txcreate3',          'recreating the transaction'),
 		('alice_txcreate3',          'recreating the transaction'),
 		('wait_loop_start_ltc',      'starting autosign wait loop in XMR compat mode [--coins=ltc,xmr]'),
 		('wait_loop_start_ltc',      'starting autosign wait loop in XMR compat mode [--coins=ltc,xmr]'),
 		('alice_txsend1',            'sending the transaction'),
 		('alice_txsend1',            'sending the transaction'),
+		('mine_blocks_10',           'mining some blocks'),
+		('alice_twview_chk2',        'viewing Alice’s tracking wallets (check balances)'),
+		('alice_txcreate_sweep1',    'creating a sweep transaction (account sweep)'),
+		('alice_txsend2',            'sending the transaction'),
+		('mine_blocks_10',           'mining some blocks'),
+		('alice_twview_chk3',        'viewing Alice’s tracking wallets (check balances)'),
+		('alice_txcreate_sweep2',    'creating a sweep transaction (address sweep)'),
+		('alice_txsend3',            'sending the transaction'),
+		('mine_blocks_10',           'mining some blocks'),
+		('alice_twview_chk4',        'viewing Alice’s tracking wallets (check balances)'),
 		('wait_loop_kill',           'stopping autosign wait loop'),
 		('wait_loop_kill',           'stopping autosign wait loop'),
 		('alice_newacct1',           'adding account to Alice’s tracking wallet (dfl label)'),
 		('alice_newacct1',           'adding account to Alice’s tracking wallet (dfl label)'),
 		('alice_newacct2',           'adding account to Alice’s tracking wallet (no timestr)'),
 		('alice_newacct2',           'adding account to Alice’s tracking wallet (no timestr)'),
@@ -686,6 +696,15 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 			'1         0.026296296417',
 			'1         0.026296296417',
 			'0.007654321098'])
 			'0.007654321098'])
 
 
+	def alice_twview_chk2(self):
+		return self._alice_twview_chk(['Total XMR: 3.715053370119'], sync=True)
+
+	def alice_twview_chk3(self):
+		return self._alice_twview_chk(['Total XMR: 3.713242570119', '1.232757091234'], sync=True)
+
+	def alice_twview_chk4(self):
+		return self._alice_twview_chk(['Total XMR: 3.709050970119', '1.254861787651'], sync=True)
+
 	def _alice_twview_chk(self, expect_arr, sync=False):
 	def _alice_twview_chk(self, expect_arr, sync=False):
 		return self._alice_twops(
 		return self._alice_twops(
 			'twview',
 			'twview',
@@ -756,6 +775,12 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 	def alice_txcreate1(self):
 	def alice_txcreate1(self):
 		return self._alice_txops('txcreate', [f'{self.burn_addr},0.012345'], acct_num=1)
 		return self._alice_txops('txcreate', [f'{self.burn_addr},0.012345'], acct_num=1)
 
 
+	def alice_txcreate_sweep1(self):
+		return self._alice_txops('txcreate', menu='S', sweep_menu='23', sweep_type='sweep')
+
+	def alice_txcreate_sweep2(self):
+		return self._alice_txops('txcreate', menu='s', sweep_menu='2', sweep_type='sweep_all')
+
 	alice_txcreate3 = alice_txcreate2 = alice_txcreate1
 	alice_txcreate3 = alice_txcreate2 = alice_txcreate1
 
 
 	def _alice_txabort(self):
 	def _alice_txabort(self):
@@ -770,7 +795,7 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 			add_opts    = self.alice_daemon_opts,
 			add_opts    = self.alice_daemon_opts,
 			wait_signed = True)
 			wait_signed = True)
 
 
-	alice_txsend1 = _alice_txsend
+	alice_txsend1 = alice_txsend2 = alice_txsend3 = _alice_txsend
 
 
 	def wait_signed1(self):
 	def wait_signed1(self):
 		self.spawn(msg_only=True)
 		self.spawn(msg_only=True)
@@ -788,6 +813,8 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 			menu = '',
 			menu = '',
 			acct_num = None,
 			acct_num = None,
 			wait_signed = False,
 			wait_signed = False,
+			sweep_type = None,
+			sweep_menu = '',
 			signable_desc = 'transaction'):
 			signable_desc = 'transaction'):
 		if wait_signed:
 		if wait_signed:
 			self._wait_signed(signable_desc)
 			self._wait_signed(signable_desc)
@@ -796,7 +823,13 @@ class CmdTestXMRCompat(CmdTestXMRAutosign):
 		if '--abort' in opts:
 		if '--abort' in opts:
 			t.expect('(y/N): ', 'y')
 			t.expect('(y/N): ', 'y')
 		elif op == 'txcreate':
 		elif op == 'txcreate':
-			if True:
+			if sweep_type:
+				t.expect(self.menu_prompt, menu)
+				for ch in sweep_menu:
+					t.expect('main menu): ', ch)
+				t.expect('number> ', {'sweep': '1', 'sweep_all': '2'}[sweep_type])
+				t.expect('(y/N): ', 'y') # create new address?
+			else:
 				for ch in menu + 'q':
 				for ch in menu + 'q':
 					t.expect(self.menu_prompt, ch)
 					t.expect(self.menu_prompt, ch)
 				t.expect('to spend from: ', f'{acct_num}\n')
 				t.expect('to spend from: ', f'{acct_num}\n')