diff --git a/mmgen/data/version b/mmgen/data/version index 7396a3cd..fdd016f6 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -16.1.dev25 +16.1.dev26 diff --git a/mmgen/help/help_notes.py b/mmgen/help/help_notes.py index c97d1b29..c4b9f867 100755 --- a/mmgen/help/help_notes.py +++ b/mmgen/help/help_notes.py @@ -23,7 +23,7 @@ class help_notes: case 'Bitcoin': return '[ADDR,AMT ... | DATA_SPEC] ADDR [addr file ...]' case 'Monero': - return 'ADDR,AMT' + return '[ADDR,AMT]' case _: return 'ADDR,AMT [addr file ...]' diff --git a/mmgen/help/txcreate_examples.py b/mmgen/help/txcreate_examples.py index d5df9b73..7218545b 100755 --- a/mmgen/help/txcreate_examples.py +++ b/mmgen/help/txcreate_examples.py @@ -22,6 +22,7 @@ def help(proto, cfg): addr = t.privhex2addr('bead' * 16) sample_addr = addr.views[addr.view_pref] 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: case 'Bitcoin': @@ -64,6 +65,10 @@ EXAMPLES: Send 0.123 {proto.coin} to an external {proto.name} address: $ {cmd_base} {sample_addr},0.123 + + {action} a sweep transaction: + + $ {cmd_base} """ case _: diff --git a/mmgen/main_txcreate.py b/mmgen/main_txcreate.py index c837ad2c..8fe67f06 100755 --- a/mmgen/main_txcreate.py +++ b/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=' ')) 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]: cfg._usage() diff --git a/mmgen/proto/xmr/tw/unspent.py b/mmgen/proto/xmr/tw/unspent.py index a44a1da2..58458f6b 100755 --- a/mmgen/proto/xmr/tw/unspent.py +++ b/mmgen/proto/xmr/tw/unspent.py @@ -30,4 +30,40 @@ class MoneroTwUnspentOutputs(MoneroTwView, TwUnspentOutputs): self.extra_key_mappings = { 'R': 'a_sync_wallets', '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) + + 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' diff --git a/mmgen/proto/xmr/tx/new.py b/mmgen/proto/xmr/tx/new.py index 6879fe08..fd9c534a 100755 --- a/mmgen/proto/xmr/tx/new.py +++ b/mmgen/proto/xmr/tx/new.py @@ -18,6 +18,11 @@ from .base import Base 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): return [] # TODO @@ -42,7 +47,20 @@ class New(Base, TxNew): 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' i = self.inputs[0] o = self.outputs[0] diff --git a/mmgen/tw/unspent.py b/mmgen/tw/unspent.py index 74667121..054b0ea4 100755 --- a/mmgen/tw/unspent.py +++ b/mmgen/tw/unspent.py @@ -95,7 +95,8 @@ class TwUnspentOutputs(TwView): self.__dict__['proto'] = proto 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) self.minconf = NonNegativeInt(minconf) self.addrs = addrs diff --git a/mmgen/tw/view.py b/mmgen/tw/view.py index c330c7da..946f8269 100755 --- a/mmgen/tw/view.py +++ b/mmgen/tw/view.py @@ -639,7 +639,9 @@ class TwView(MMGenObject, metaclass=AsyncInit): case ch if ch in key_mappings: func = action_classes[ch].run 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': return cleanup(add_nl=True) case _: @@ -696,6 +698,8 @@ class TwView(MMGenObject, metaclass=AsyncInit): await do_error_msg() 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')): # error messages could leave screen in messy state, so do complete redraw: msg_r( diff --git a/mmgen/tx/new.py b/mmgen/tx/new.py index 4ba550c4..ac1c0812 100755 --- a/mmgen/tx/new.py +++ b/mmgen/tx/new.py @@ -79,6 +79,7 @@ def parse_fee_spec(proto, fee_arg): class New(Base): fee_is_approximate = False + is_sweep = False msg_wallet_low_coin = 'Wallet has insufficient funds for this transaction ({} {} needed)' msg_no_change_output = """ 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: 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) if self.is_swap: cmd_args = await self.process_swap_cmdline_args(cmd_args, addrfile_args) @@ -475,7 +476,8 @@ class New(Base): self.cfg, self.proto, 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() self.twctl = self.twuo.twctl @@ -485,6 +487,11 @@ class New(Base): if not (self.is_bump or self.cfg.inputs): 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: self.twuo.display_total() diff --git a/mmgen/xmrwallet/ops/sweep.py b/mmgen/xmrwallet/ops/sweep.py index 34495c50..d28d8b3c 100755 --- a/mmgen/xmrwallet/ops/sweep.py +++ b/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})') 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) diff --git a/test/cmdtest_d/xmr_autosign.py b/test/cmdtest_d/xmr_autosign.py index 69fb9adf..9d6b3703 100755 --- a/test/cmdtest_d/xmr_autosign.py +++ b/test/cmdtest_d/xmr_autosign.py @@ -548,6 +548,16 @@ class CmdTestXMRCompat(CmdTestXMRAutosign): ('alice_txcreate3', 'recreating the transaction'), ('wait_loop_start_ltc', 'starting autosign wait loop in XMR compat mode [--coins=ltc,xmr]'), ('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'), ('alice_newacct1', 'adding account to Alice’s tracking wallet (dfl label)'), ('alice_newacct2', 'adding account to Alice’s tracking wallet (no timestr)'), @@ -686,6 +696,15 @@ class CmdTestXMRCompat(CmdTestXMRAutosign): '1 0.026296296417', '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): return self._alice_twops( 'twview', @@ -756,6 +775,12 @@ class CmdTestXMRCompat(CmdTestXMRAutosign): def alice_txcreate1(self): 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 def _alice_txabort(self): @@ -770,7 +795,7 @@ class CmdTestXMRCompat(CmdTestXMRAutosign): add_opts = self.alice_daemon_opts, wait_signed = True) - alice_txsend1 = _alice_txsend + alice_txsend1 = alice_txsend2 = alice_txsend3 = _alice_txsend def wait_signed1(self): self.spawn(msg_only=True) @@ -788,6 +813,8 @@ class CmdTestXMRCompat(CmdTestXMRAutosign): menu = '', acct_num = None, wait_signed = False, + sweep_type = None, + sweep_menu = '', signable_desc = 'transaction'): if wait_signed: self._wait_signed(signable_desc) @@ -796,7 +823,13 @@ class CmdTestXMRCompat(CmdTestXMRAutosign): if '--abort' in opts: t.expect('(y/N): ', 'y') 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': t.expect(self.menu_prompt, ch) t.expect('to spend from: ', f'{acct_num}\n')