XMR compat: address/account creation

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
This commit is contained in:
The MMGen Project 2026-01-21 09:06:50 +00:00
commit 7b53f4337f
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
5 changed files with 134 additions and 10 deletions

View file

@ -1 +1 @@
16.1.dev24
16.1.dev25

View file

@ -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}')

View file

@ -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', []):

View file

@ -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

View file

@ -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: