From 7b53f4337fc1499652953394d7304b82a41f4047 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Wed, 21 Jan 2026 09:06:50 +0000 Subject: [PATCH] XMR compat: address/account creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Example: # Press ‘N’ or ‘n’ at the prompt to create new account or address: $ mmgen-tool --coin=xmr listaddresses interactive=1 Testing/demo: $ test/cmdtest.py --coin=xmr -e xmr_compat --- mmgen/data/version | 2 +- mmgen/proto/xmr/tw/addresses.py | 61 ++++++++++++++++++++++++++++++- mmgen/proto/xmr/tw/view.py | 8 ++--- mmgen/xmrwallet/ops/new.py | 9 +++-- test/cmdtest_d/xmr_autosign.py | 64 +++++++++++++++++++++++++++++++-- 5 files changed, 134 insertions(+), 10 deletions(-) diff --git a/mmgen/data/version b/mmgen/data/version index 0965cd04..7396a3cd 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -16.1.dev24 +16.1.dev25 diff --git a/mmgen/proto/xmr/tw/addresses.py b/mmgen/proto/xmr/tw/addresses.py index b41f9bc2..c9734f23 100755 --- a/mmgen/proto/xmr/tw/addresses.py +++ b/mmgen/proto/xmr/tw/addresses.py @@ -16,6 +16,30 @@ from ....tw.addresses import TwAddresses from .view import MoneroTwView +async def add_new_address(parent, spec, ok_msg): + from ....ui import line_input, keypress_confirm + from ....color import green, yellow + from ....xmrwallet import op as xmrwallet_op + lbl = line_input( + parent.cfg, + 'Enter label text for new address (or ENTER for default label): ') + add_timestr = keypress_confirm(parent.cfg, 'Add timestamp to label?') + op = xmrwallet_op( + 'new', + parent.cfg, + None, + None, + spec = spec + (',' + lbl if lbl else ''), + compat_call = True) + op.c.call('close_wallet') + if await op.main(add_timestr=add_timestr): + await parent.get_data() + parent.oneshot_msg = green(ok_msg) + return 'redraw' + else: + parent.oneshot_msg = yellow('Operation cancelled') + return False + class MoneroTwAddresses(MoneroTwView, TwAddresses): include_empty = True @@ -23,8 +47,10 @@ class MoneroTwAddresses(MoneroTwView, TwAddresses): prompt_fs_repl = {'XMR': ( (1, 'Filters: show [E]mpty addrs, [u]sed addrs, all [L]abels'), - (3, 'Actions: [q]uit menu, add [l]abel, [R]efresh balances:'))} + (3, 'Actions: [q]uit menu, add [l]abel, [N]ew acct, [n]ew addr, [R]efresh bals:'))} extra_key_mappings = { + 'N': 'a_acct_new', + 'n': 'i_addr_new', 'u': 'd_showused', 'R': 'a_sync_wallets'} removed_key_mappings = { @@ -40,3 +66,36 @@ class MoneroTwAddresses(MoneroTwView, TwAddresses): def get_disp_data(self): return MoneroTwView.get_disp_data(self, input_data=tuple(TwAddresses.get_disp_data(self))) + + class action(MoneroTwView.action): + + async def a_acct_new(self, parent): + from ....obj import Int + from ....util import suf + from ....addr import MMGenID + from ....ui import item_chooser + def wallet_id(wnum): + return MMGenID(proto=parent.proto, id_str='{}:M:{}'.format(parent.sid, wnum)) + res = item_chooser( + parent.cfg, + 'Choose a wallet to add a new account to', + [(d['wallet_num'], len(d['data'].accts_data['subaddress_accounts'])) + for d in parent.dump_data], + lambda d: '{a} [{b} account{c}]'.format( + a = wallet_id(d[0]).hl(), + b = Int(d[1]).hl(), + c = suf(d[1]))) + return await add_new_address( + parent, + str(res.item[0]), + f'New account added to wallet {wallet_id(res.item[0]).hl()}') + + class item_action(TwAddresses.item_action): + acct_methods = ('i_addr_new') + + async def i_addr_new(self, parent, idx, acct_addr_idx=None): + e = parent.accts_data[idx-1] + return await add_new_address( + parent, + f'{e.idx}:{e.acct_idx}', + f'New address added to wallet {e.idx}, account #{e.acct_idx}') diff --git a/mmgen/proto/xmr/tw/view.py b/mmgen/proto/xmr/tw/view.py index b96db7ff..87bcb92c 100755 --- a/mmgen/proto/xmr/tw/view.py +++ b/mmgen/proto/xmr/tw/view.py @@ -57,16 +57,16 @@ class MoneroTwView: op = xmrwallet_op('dump_data', self.cfg, None, None, compat_call=True) await op.restart_wallet_daemon() - wallets_data = await op.main() + self.dump_data = await op.main() - if wallets_data: - self.sid = SeedID(sid=wallets_data[0]['seed_id']) + if self.dump_data: + self.sid = SeedID(sid=self.dump_data[0]['seed_id']) self.total = self.unlocked_total = self.proto.coin_amt('0') def gen_addrs(): bd = namedtuple('address_balance_data', ['bal', 'unlocked_bal', 'blocks_to_unlock']) - for wdata in wallets_data: + for wdata in self.dump_data: bals_data = {i: {} for i in range(len(wdata['data'].accts_data['subaddress_accounts']))} for d in wdata['data'].bals_data.get('per_subaddress', []): diff --git a/mmgen/xmrwallet/ops/new.py b/mmgen/xmrwallet/ops/new.py index 005ffa4f..b17d6215 100755 --- a/mmgen/xmrwallet/ops/new.py +++ b/mmgen/xmrwallet/ops/new.py @@ -27,14 +27,16 @@ class OpNew(OpMixinSpec, OpWallet): spec_key = ((1, 'source'),) wallet_offline = True - async def main(self): + async def main(self, add_timestr=True): h = MoneroWalletRPC(self, self.source) h.open_wallet('Monero', refresh=False) desc = 'account' if self.account is None else 'address' label = TwComment( None if self.label == '' else - '{} [{}]'.format(self.label or f'xmrwallet new {desc}', make_timestr())) + '{a}{b}'.format( + a = self.label or (f'new {desc}' if self.compat_call else f'xmrwallet new {desc}'), + b = ' [{}]'.format(make_timestr()) if add_timestr else '')) wallet_data = h.get_wallet_data() @@ -58,11 +60,14 @@ class OpNew(OpMixinSpec, OpWallet): if desc == 'address': h.print_acct_addrs(wallet_data, self.account) + ret = True else: ymsg('\nOperation cancelled by user request') + ret = False # wallet must be left open: otherwise the 'stop_wallet' RPC call used to stop the daemon will fail if self.cfg.no_stop_wallet_daemon: h.close_wallet('Monero') msg('') + return ret diff --git a/test/cmdtest_d/xmr_autosign.py b/test/cmdtest_d/xmr_autosign.py index b38ad1d0..efa75106 100755 --- a/test/cmdtest_d/xmr_autosign.py +++ b/test/cmdtest_d/xmr_autosign.py @@ -508,6 +508,7 @@ class CmdTestXMRCompat(CmdTestXMRAutosign): Monero autosigning operations (compat mode) """ menu_prompt = 'efresh balances:\b' + listaddresses_menu_prompt = 'efresh bals:\b' extra_daemons = ['ltc'] cmd_group = ( @@ -548,6 +549,14 @@ class CmdTestXMRCompat(CmdTestXMRAutosign): ('wait_loop_start_ltc', 'starting autosign wait loop in XMR compat mode [--coins=ltc,xmr]'), ('alice_txsend1', 'sending the transaction'), ('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)'), + ('alice_newacct3', 'adding account to Alice’s tracking wallet'), + ('alice_newacct4', 'adding account to Alice’s tracking wallet (dfl label, no timestr)'), + ('alice_newaddr1', 'adding address to Alice’s tracking wallet'), + ('alice_newaddr2', 'adding address to Alice’s tracking wallet (no timestr)'), + ('alice_newaddr3', 'adding address to Alice’s tracking wallet (dfl label)'), + ('alice_newaddr4', 'adding address to Alice’s tracking wallet (dfl label, no timestr)'), ('stop_daemons', 'stopping all wallet and coin daemons'), ) @@ -600,6 +609,46 @@ class CmdTestXMRCompat(CmdTestXMRAutosign): addr_data = data['MoneroMMGenWalletDumpFile']['data']['wallet_metadata'][1]['addresses'] return await self.fund_alice(addr=addr_data[addr_num-1]['address'], amt=amt) + def alice_newacct1(self): + return self._alice_newacct(2, lbl_text='New Test Account', add_timestr=True) + + def alice_newacct2(self): + return self._alice_newacct(1, lbl_text='New Test Account') + + def alice_newacct3(self): + return self._alice_newacct(2, add_timestr=True) + + def alice_newacct4(self): + return self._alice_newacct(2) + + def _alice_newacct(self, wallet_num, lbl_text='', add_timestr=False): + return self._alice_twops( + 'listaddresses', + newacct_wallet_num = wallet_num, + lbl_text = lbl_text, + add_timestr = add_timestr, + expect_str = (lbl_text or 'new account ') + (r'.*\[20.*\]' if add_timestr else '')) + + def alice_newaddr1(self): + return self._alice_newaddr(1, lbl_text='New Test Address', add_timestr=True) + + def alice_newaddr2(self): + return self._alice_newaddr(1, lbl_text='New Test Address') + + def alice_newaddr3(self): + return self._alice_newaddr(2, add_timestr=True) + + def alice_newaddr4(self): + return self._alice_newaddr(2) + + def _alice_newaddr(self, acct_num, lbl_text='', add_timestr=False): + return self._alice_twops( + 'listaddresses', + newaddr_acct_num = acct_num, + lbl_text = lbl_text, + add_timestr = add_timestr, + expect_str = (lbl_text or 'new address ') + (r'.*\[20.*\]' if add_timestr else '')) + def alice_listaddresses(self): return self._alice_twops('listaddresses', menu='R') @@ -646,6 +695,8 @@ class CmdTestXMRCompat(CmdTestXMRAutosign): *, lbl_acct_num = None, lbl_addr_idx = None, + newacct_wallet_num = None, + newaddr_acct_num = None, lbl_text = '', add_timestr = False, menu = '', @@ -660,16 +711,25 @@ class CmdTestXMRCompat(CmdTestXMRAutosign): + self.autosign_opts + [op] + (['interactive=1'] if interactive else [])) - menu_prompt = self.menu_prompt - have_lbl = lbl_acct_num + menu_prompt = self.listaddresses_menu_prompt if op == 'listaddresses' else self.menu_prompt + have_lbl = lbl_acct_num or newacct_wallet_num or newaddr_acct_num + have_new_addr = newacct_wallet_num or newaddr_acct_num if interactive: if lbl_acct_num: t.expect(menu_prompt, 'l') t.expect('main menu): ', str(lbl_acct_num)) t.expect('main menu): ', str(lbl_addr_idx)) + elif newacct_wallet_num: + t.expect(menu_prompt, 'N') + t.expect('number> ', f'{newacct_wallet_num}\n') + elif newaddr_acct_num: + t.expect(menu_prompt, 'n') + t.expect('main menu): ', str(newaddr_acct_num)) if have_lbl: t.expect(': ', lbl_text + '\n') # add label t.expect('(y/N): ', ('y' if add_timestr else 'n')) # add timestr + if have_new_addr: + t.expect('(y/N): ', 'y') for ch in menu: t.expect(menu_prompt, ch) if expect_str: