mmgen-txcreate: automatic change address selection by address type
This patch introduces an improvement to commit cbe749813 (automatic change
address selection)
Instead of supplying a Seed ID + address type, users may now specify the
address type alone. If the tracking wallet contains more than one unused
address matching the user’s criteria, the user is prompted to choose a specific
change address.
As with plain auto change address selection, this feature is entirely opt-in.
Sample invocations:
# old invocation:
$ mmgen-txcreate -q 35N9FntsNwy98TmjFHyCpsBVDVUs5wDPfB,0.123 01ABCDEF:B
# new invocation:
$ mmgen-txcreate -q 35N9FntsNwy98TmjFHyCpsBVDVUs5wDPfB,0.123 B
# or, alternatively:
$ mmgen-txcreate -q 35N9FntsNwy98TmjFHyCpsBVDVUs5wDPfB,0.123 bech32
Testing/demo:
# run the regtest test partially, leaving the coin daemon running:
$ test/test.py -D regtest.auto_chg
# run the auto chg addrtype test, displaying script output:
$ test/test.py -SAe regtest:bob_auto_chg_addrtype1
# view the addresses in Bob’s tracking wallet, verifying that the first
# unused ones in each grouping were chosen as change outputs:
$ PYTHONPATH=. MMGEN_TEST_SUITE=1 cmds/mmgen-tool --bob listaddresses interactive=1
# When finished, gracefully shut down the daemon:
$ test/stop-coin-daemons.py btc_rt
This commit is contained in:
parent
ba291d4799
commit
045fdefd73
9 changed files with 137 additions and 17 deletions
|
|
@ -79,6 +79,9 @@ class MMGenAddrType(str,Hilite,InitErrors,MMGenObject):
|
|||
def get_names(cls):
|
||||
return [v.name for v in cls.mmtypes.values()]
|
||||
|
||||
def is_mmgen_addrtype(proto,id_str):
|
||||
return get_obj( MMGenAddrType, proto=proto, id_str=id_str, silent=True, return_bool=True )
|
||||
|
||||
class MMGenPasswordType(MMGenAddrType):
|
||||
mmtypes = {
|
||||
'P': ati('password', 'password', None, None, None, None, None, 'Password generated from MMGen seed')
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
13.3.dev22
|
||||
13.3.dev23
|
||||
|
|
|
|||
|
|
@ -161,15 +161,24 @@ EXAMPLES:
|
|||
|
||||
$ {g.prog_name} {sample_addr},0.123 01ABCDEF:{mmtype}
|
||||
|
||||
Same as above, but select the change address automatically by address type:
|
||||
|
||||
$ {g.prog_name} {sample_addr},0.123 {mmtype}
|
||||
|
||||
Same as above, but reduce verbosity and specify fee of 20 satoshis
|
||||
per byte:
|
||||
|
||||
$ {g.prog_name} -q -f 20s {sample_addr},0.123 01ABCDEF:{mmtype}
|
||||
$ {g.prog_name} -q -f 20s {sample_addr},0.123 {mmtype}
|
||||
|
||||
Send entire balance of selected inputs minus fee to an external {proto.name}
|
||||
address:
|
||||
|
||||
$ {g.prog_name} {sample_addr}
|
||||
|
||||
Send entire balance of selected inputs minus fee to first unused wallet
|
||||
address of specified type:
|
||||
|
||||
$ {g.prog_name} {mmtype}
|
||||
"""
|
||||
|
||||
def txcreate():
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ opts_data = {
|
|||
'sets': [('yes', True, 'quiet', True)],
|
||||
'text': {
|
||||
'desc': f'Create a transaction with outputs to specified coin or {g.proj_name} addresses',
|
||||
'usage': '[opts] [<addr,amt> ...] <change addr or addrlist ID> [addr file ...]',
|
||||
'usage': '[opts] [<addr,amt> ...] <change addr, addrlist ID or addr type> [addr file ...]',
|
||||
'options': """
|
||||
-h, --help Print this help message
|
||||
--, --longhelp Print help message for long options (common options)
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ opts_data = {
|
|||
'sets': [('yes', True, 'quiet', True)],
|
||||
'text': {
|
||||
'desc': f'Create, sign and send an {g.proj_name} transaction',
|
||||
'usage': '[opts] [<addr,amt> ...] <change addr or addrlist ID> [addr file ...] [seed source ...]',
|
||||
'usage': '[opts] [<addr,amt> ...] <change addr, addrlist ID or addr type> [addr file ...] [seed source ...]',
|
||||
'options': """
|
||||
-h, --help Print this help message
|
||||
--, --longhelp Print help message for long options (common options)
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@
|
|||
tw.addresses: Tracking wallet listaddresses class for the MMGen suite
|
||||
"""
|
||||
|
||||
from ..util import suf
|
||||
from ..util import msg,suf,is_int
|
||||
from ..objmethods import MMGenObject
|
||||
from ..obj import MMGenListItem,ImmutableAttr,ListItemAttr,TwComment,NonNegativeInt
|
||||
from ..addr import CoinAddr,MMGenID
|
||||
from ..addr import CoinAddr,MMGenID,MMGenAddrType
|
||||
from ..color import red,green
|
||||
from .view import TwView
|
||||
from .shared import TwMMGenID
|
||||
|
|
@ -242,6 +242,39 @@ class TwAddresses(TwView):
|
|||
def dump_fn_pfx(self):
|
||||
return 'listaddresses' + (f'-minconf-{self.minconf}' if self.minconf else '')
|
||||
|
||||
@property
|
||||
def sid_ranges(self):
|
||||
|
||||
def gen_sid_ranges():
|
||||
|
||||
from collections import namedtuple
|
||||
sid_range = namedtuple('sid_range',['bot','top'])
|
||||
|
||||
sid_save = None
|
||||
bot = None
|
||||
|
||||
for n,e in enumerate(self.data):
|
||||
if e.twmmid.type == 'mmgen':
|
||||
if e.twmmid.obj.sid != sid_save:
|
||||
if sid_save:
|
||||
yield (sid_save, sid_range(bot, n-1))
|
||||
sid_save = e.twmmid.obj.sid
|
||||
bot = n
|
||||
else:
|
||||
break
|
||||
else:
|
||||
n += 1
|
||||
|
||||
if sid_save:
|
||||
yield (sid_save, sid_range(bot, n-1))
|
||||
|
||||
assert self.sort_key == 'twmmid'
|
||||
|
||||
if not hasattr(self,'_sid_ranges'):
|
||||
self._sid_ranges = dict(gen_sid_ranges())
|
||||
|
||||
return self._sid_ranges
|
||||
|
||||
def is_used(self,coinaddr):
|
||||
for e in self.data:
|
||||
if e.addr == coinaddr:
|
||||
|
|
@ -249,7 +282,7 @@ class TwAddresses(TwView):
|
|||
else: # addr not in tracking wallet
|
||||
return None
|
||||
|
||||
def get_change_address(self,al_id):
|
||||
def get_change_address(self,al_id,bot=None,top=None):
|
||||
"""
|
||||
Get lowest-indexed unused address in tracking wallet for requested AddrListID.
|
||||
Return values on failure:
|
||||
|
|
@ -282,7 +315,9 @@ class TwAddresses(TwView):
|
|||
assert self.sort_key == 'twmmid'
|
||||
|
||||
data = self.data
|
||||
start = get_start( bot=0, top=len(data) - 1 )
|
||||
start = get_start(
|
||||
bot = 0 if bot is None else bot,
|
||||
top = len(data) - 1 if top is None else top )
|
||||
|
||||
if start is not None:
|
||||
for d in data[start:]:
|
||||
|
|
@ -292,6 +327,40 @@ class TwAddresses(TwView):
|
|||
else:
|
||||
return False
|
||||
|
||||
def get_change_address_by_addrtype(self,mmtype):
|
||||
"""
|
||||
Find the lowest-indexed change addresses in tracking wallet of given address type,
|
||||
present them in a menu and return a single change address chosen by the user.
|
||||
|
||||
Return values on failure:
|
||||
None: no addresses in wallet of requested address type
|
||||
False: no unused addresses in wallet of requested address type
|
||||
"""
|
||||
|
||||
def choose_address(addrs):
|
||||
from ..ui import line_input
|
||||
prompt = '\nChoose a change address:\n\n{m}\n\nEnter a number> '.format(
|
||||
m = '\n'.join(f'{n:3}) {a.twmmid.hl()}' for n,a in enumerate(addrs,1))
|
||||
)
|
||||
while True:
|
||||
res = line_input(prompt)
|
||||
if is_int(res) and 0 < int(res) <= len(addrs):
|
||||
return addrs[int(res)-1]
|
||||
msg(f'{res}: invalid entry')
|
||||
|
||||
assert isinstance(mmtype,MMGenAddrType)
|
||||
|
||||
res = [self.get_change_address( f'{sid}:{mmtype}', r.bot, r.top ) for sid,r in self.sid_ranges.items()]
|
||||
|
||||
if any(res):
|
||||
res = list(filter(None,res))
|
||||
if len(res) == 1:
|
||||
return res[0]
|
||||
else:
|
||||
return choose_address(res)
|
||||
elif False in res:
|
||||
return False
|
||||
|
||||
class action(TwView.action):
|
||||
|
||||
def s_amt(self,parent):
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ from ..obj import get_obj,MMGenList
|
|||
from ..util import msg,qmsg,fmt,die,suf,remove_dups,get_extension
|
||||
from ..addr import (
|
||||
is_mmgen_id,
|
||||
MMGenAddrType,
|
||||
CoinAddr,
|
||||
is_mmgen_addrtype,
|
||||
is_coin_addr,
|
||||
is_addrlist_id
|
||||
)
|
||||
|
|
@ -181,7 +183,7 @@ class New(Base):
|
|||
coin_addr = mmaddr2coinaddr(arg,ad_w,ad_f,self.proto)
|
||||
elif is_coin_addr(self.proto,arg):
|
||||
coin_addr = CoinAddr(self.proto,arg)
|
||||
elif is_addrlist_id(self.proto,arg):
|
||||
elif is_mmgen_addrtype(self.proto,arg) or is_addrlist_id(self.proto,arg):
|
||||
if self.proto.base_proto_coin != 'BTC':
|
||||
die(2,f'Change addresses not supported for {self.proto.name} protocol')
|
||||
|
||||
|
|
@ -190,14 +192,21 @@ class New(Base):
|
|||
al.reverse = False
|
||||
al.do_sort('twmmid')
|
||||
|
||||
res = al.get_change_address(arg)
|
||||
if is_mmgen_addrtype(self.proto,arg):
|
||||
arg = MMGenAddrType(self.proto,arg)
|
||||
res = al.get_change_address_by_addrtype(arg)
|
||||
desc = 'of address type'
|
||||
else:
|
||||
res = al.get_change_address(arg)
|
||||
desc = 'from address list'
|
||||
|
||||
if res:
|
||||
coin_addr = res.addr
|
||||
self.chg_autoselected = True
|
||||
else:
|
||||
die(2,'Tracking wallet contains no {t}addresses from address list {a!r}'.format(
|
||||
die(2,'Tracking wallet contains no {t}addresses {d} {a!r}'.format(
|
||||
t = ('unused ','')[res is None],
|
||||
d = desc,
|
||||
a = arg ))
|
||||
else:
|
||||
die(2,f'{arg_in}: invalid command-line argument')
|
||||
|
|
|
|||
|
|
@ -355,9 +355,14 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
|
|||
('bob_auto_chg2', 'creating an automatic change address transaction (B)'),
|
||||
('bob_auto_chg3', 'creating an automatic change address transaction (S)'),
|
||||
('bob_auto_chg4', 'creating an automatic change address transaction (single address)'),
|
||||
('bob_auto_chg_addrtype1', 'creating an automatic change address transaction by addrtype (C)'),
|
||||
('bob_auto_chg_addrtype2', 'creating an automatic change address transaction by addrtype (B)'),
|
||||
('bob_auto_chg_addrtype3', 'creating an automatic change address transaction by addrtype (S)'),
|
||||
('bob_auto_chg_addrtype4', 'creating an automatic change address transaction by addrtype (single address)'),
|
||||
('bob_auto_chg_bad1', 'error handling for auto change address transaction (bad ID FFFFFFFF:C)'),
|
||||
('bob_auto_chg_bad2', 'error handling for auto change address transaction (bad ID 00000000:C)'),
|
||||
('bob_auto_chg_bad3', 'error handling for auto change address transaction (no unused addresses)'),
|
||||
('bob_auto_chg_bad4', 'error handling for auto change address transaction by addrtype (no unused addresses)'),
|
||||
),
|
||||
}
|
||||
|
||||
|
|
@ -1417,16 +1422,17 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
|
|||
outputs_cl = [sid+':C:5,0.0135', sid+':L:4'],
|
||||
outputs_list = '1' )
|
||||
|
||||
def _bob_auto_chg(self,al_id,include_dest=True):
|
||||
def _bob_auto_chg(self,arg,include_dest=True,choices=1):
|
||||
dest = [self.burn_addr+',0.01'] if include_dest else []
|
||||
t = self.spawn(
|
||||
'mmgen-txcreate',
|
||||
['-d',self.tr.trash_dir,'-B','--bob', al_id] + dest)
|
||||
['-d',self.tr.trash_dir,'-B','--bob', arg] + dest)
|
||||
return self.txcreate_ui_common(t,
|
||||
menu = [],
|
||||
inputs = '1',
|
||||
interactive_fee = '20s',
|
||||
auto_chg_al_id = al_id )
|
||||
auto_chg_arg = arg,
|
||||
auto_chg_choices = choices )
|
||||
|
||||
def bob_auto_chg1(self):
|
||||
return self._bob_auto_chg(self._user_sid('bob') + ':C')
|
||||
|
|
@ -1444,6 +1450,22 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
|
|||
def bob_auto_chg4(self):
|
||||
return self._bob_auto_chg( self._user_sid('bob') + ':C', include_dest=False )
|
||||
|
||||
def bob_auto_chg_addrtype1(self):
|
||||
return self._bob_auto_chg( 'C', choices=3 )
|
||||
|
||||
def bob_auto_chg_addrtype2(self):
|
||||
if not self.proto.cap('segwit'):
|
||||
return 'skip'
|
||||
return self._bob_auto_chg( 'B', choices=1 )
|
||||
|
||||
def bob_auto_chg_addrtype3(self):
|
||||
if not self.proto.cap('segwit'):
|
||||
return 'skip'
|
||||
return self._bob_auto_chg( 'S', choices=1 )
|
||||
|
||||
def bob_auto_chg_addrtype4(self):
|
||||
return self._bob_auto_chg( 'C', choices=3, include_dest=False )
|
||||
|
||||
def _bob_auto_chg_bad(self,al_id,expect):
|
||||
t = self.spawn(
|
||||
'mmgen-txcreate',
|
||||
|
|
@ -1467,6 +1489,11 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
|
|||
self._user_sid('bob') + ':L',
|
||||
'contains no unused addresses from address list' )
|
||||
|
||||
def bob_auto_chg_bad4(self):
|
||||
return self._bob_auto_chg_bad(
|
||||
'L',
|
||||
'contains no unused addresses of address type' )
|
||||
|
||||
def stop(self):
|
||||
if opt.no_daemon_stop:
|
||||
self.spawn('',msg_only=True)
|
||||
|
|
|
|||
|
|
@ -48,7 +48,8 @@ class TestSuiteShared(object):
|
|||
save = True,
|
||||
tweaks = [],
|
||||
used_chg_addr_resp = None,
|
||||
auto_chg_al_id = None ):
|
||||
auto_chg_arg = None,
|
||||
auto_chg_choices = 1 ):
|
||||
|
||||
txdo = (caller or self.test_name)[:4] == 'txdo'
|
||||
|
||||
|
|
@ -59,8 +60,10 @@ class TestSuiteShared(object):
|
|||
if used_chg_addr_resp is not None:
|
||||
t.expect('reuse harms your privacy.*:.*',used_chg_addr_resp,regex=True)
|
||||
|
||||
if auto_chg_al_id is not None:
|
||||
t.expect(fr'Using .*{auto_chg_al_id}:\d+\D.* as.*address','y',regex=True)
|
||||
if auto_chg_arg is not None:
|
||||
if auto_chg_choices > 1:
|
||||
t.expect('Enter a number> ',f'{auto_chg_choices}\n')
|
||||
t.expect(fr'Using .*{auto_chg_arg}:\d+\D.* as.*address','y',regex=True)
|
||||
|
||||
pat = expect_pat
|
||||
for choice in menu + ['q']:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue