From ca8657d4cde29fa3ba4d0a1d2b212d58b6316fcc Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Tue, 18 Apr 2023 18:35:57 +0000 Subject: [PATCH] autosign,xmrwallet: various fixes and cleanups --- mmgen/autosign.py | 28 ++++++++++++-------- mmgen/cfg.py | 4 ++- mmgen/rpc.py | 2 +- mmgen/xmrwallet.py | 48 +++++++++++++++++++++++----------- test/test_py_d/ts_autosign.py | 12 ++++++--- test/test_py_d/ts_base.py | 4 ++- test/test_py_d/ts_xmrwallet.py | 17 ++++++++++++ 7 files changed, 82 insertions(+), 33 deletions(-) diff --git a/mmgen/autosign.py b/mmgen/autosign.py index ae4e6d0e..058a11b0 100755 --- a/mmgen/autosign.py +++ b/mmgen/autosign.py @@ -188,9 +188,9 @@ class Autosign: if 'coin' in cfg._uopts: die(1,'--coin option not supported with this command. Use --coins instead') - if cfg.coins: - self.coins = cfg.coins.upper().split(',') - else: + self.coins = cfg.coins.upper().split(',') if cfg.coins else [] + + if not self.coins: ymsg('Warning: no coins specified, defaulting to BTC') self.coins = ['BTC'] @@ -227,6 +227,16 @@ class Autosign: def do_mount(self): + from stat import S_ISDIR,S_IWUSR,S_IRUSR + + def check_dir(cdir): + try: + ds = os.stat(cdir) + assert S_ISDIR(ds.st_mode), f'{cdir!r} is not a directory!' + assert ds.st_mode & S_IWUSR|S_IRUSR == S_IWUSR|S_IRUSR, f'{cdir!r} is not read/write for this user!' + except: + die(1,f'{cdir!r} missing or not read/writable by user!') + if not os.path.isdir(self.mountpoint): def do_die(m): die(1,'\n' + yellow(fmt(m.strip(),indent=' '))) @@ -241,14 +251,10 @@ class Autosign: self.have_msg_dir = os.path.isdir(self.msg_dir) - from stat import S_ISDIR,S_IWUSR,S_IRUSR - for cdir in [self.tx_dir] + ([self.msg_dir] if self.have_msg_dir else []): - try: - ds = os.stat(cdir) - assert S_ISDIR(ds.st_mode), f'{cdir!r} is not a directory!' - assert ds.st_mode & S_IWUSR|S_IRUSR == S_IWUSR|S_IRUSR, f'{cdir!r} is not read/write for this user!' - except: - die(1,f'{cdir!r} missing or not read/writable by user!') + check_dir(self.tx_dir) + + if self.have_msg_dir: + check_dir(self.msg_dir) def do_umount(self): if os.path.ismount(self.mountpoint): diff --git a/mmgen/cfg.py b/mmgen/cfg.py index 5efa3304..4be9b92d 100755 --- a/mmgen/cfg.py +++ b/mmgen/cfg.py @@ -405,7 +405,9 @@ class Config(Lockable): # Step 1: get user-supplied configuration data from a) command line, or b) first argument # to constructor; save to self._uopts: if opts_data or parsed_opts or process_opts: - assert cfg is None + assert cfg is None, ( + 'Config(): ‘cfg’ cannot be used simultaneously with ' + + '‘opts_data’, ‘parsed_opts’ or ‘process_opts’' ) from mmgen.opts import UserOpts UserOpts( cfg = self, diff --git a/mmgen/rpc.py b/mmgen/rpc.py index f39d87e4..dfc25c63 100755 --- a/mmgen/rpc.py +++ b/mmgen/rpc.py @@ -25,7 +25,7 @@ from decimal import Decimal from collections import namedtuple from .cfg import gc -from .util import msg,die,fmt,fmt_list,pp_fmt +from .util import msg,die,fmt,fmt_list,pp_fmt,oneshot_warning from .base_obj import AsyncInit from .obj import NonNegativeInt from .objmethods import Hilite,InitErrors,MMGenObject diff --git a/mmgen/xmrwallet.py b/mmgen/xmrwallet.py index e373eb6f..b95b1bc1 100755 --- a/mmgen/xmrwallet.py +++ b/mmgen/xmrwallet.py @@ -115,6 +115,9 @@ class MoneroMMGenTX: class Base: + def __init__(self): + self.name = type(self).__name__ + def make_chksum(self,keys=None): res = json.dumps( dict( (k,v) for k,v in self.data._asdict().items() if (not keys or k in keys) ), @@ -195,7 +198,7 @@ class MoneroMMGenTX: pmid = pink(pmid.hex()) if pmid else None ) - def write(self,delete_metadata=False): + def write(self,delete_metadata=False,ask_write=True,ask_overwrite=True): dict_data = self.data._asdict() if delete_metadata: dict_data['metadata'] = None @@ -224,16 +227,16 @@ class MoneroMMGenTX: outfile = fn, data = out, desc = self.desc, - ask_write = True, - ask_write_default_yes = False ) + ask_write = ask_write, + ask_write_default_yes = not ask_write, + ask_overwrite = ask_overwrite ) - class NewSigned(Base): - signed = True - desc = 'signed transaction data' - ext = 'sigtx' + class New(Base): def __init__(self,*args,**kwargs): + super().__init__() + assert not args, 'Non-keyword args not permitted' d = namedtuple('kwargs_tuple',kwargs)(**kwargs) @@ -259,11 +262,17 @@ class MoneroMMGenTX: metadata = d.metadata, ) + class NewSigned(New): + desc = 'signed transaction data' + ext = 'sigtx' + signed = True + class Completed(Base): - name = 'completed' def __init__(self,cfg,fn): + super().__init__() + self.cfg = cfg self.fn = fn @@ -276,6 +285,10 @@ class MoneroMMGenTX: d = self.xmrwallet_tx_data(**d_wrap['data']) + if self.name != 'Completed': + assert fn.endswith('.'+self.ext), 'TX filename {fn!r} has incorrect extension (not {self.ext!r})' + assert getattr(d,self.req_field), f'{self.name} TX missing required field {self.req_field!r}' + proto = init_proto( cfg, 'xmr', network=d.network, need_amt=True ) self.data = self.xmrwallet_tx_data( @@ -293,13 +306,17 @@ class MoneroMMGenTX: blob = d.blob, metadata = d.metadata, ) + for k in ('base_chksum','full_chksum'): a = getattr(self,k) b = d_wrap[k] assert a == b, f'{k} mismatch: {a} != {b}' class Signed(Completed): - name = 'signed' + desc = 'signed transaction' + ext = 'sigtx' + signed = True + req_field = 'blob' class MoneroWalletOps: @@ -505,6 +522,7 @@ class MoneroWalletOps: def __init__(self,parent,d): self.parent = parent + self.cfg = parent.cfg self.c = parent.c self.d = d self.fn = parent.get_wallet_fn(d) @@ -637,7 +655,7 @@ class MoneroWalletOps: get_tx_metadata = True ) return MoneroMMGenTX.NewSigned( - cfg = self.parent.cfg, + cfg = self.cfg, op = self.parent.name, network = self.parent.proto.network, seed_id = self.parent.kal.al_id.sid, @@ -665,7 +683,7 @@ class MoneroWalletOps: die(3,'More than one TX required. Cannot perform this sweep') return MoneroMMGenTX.NewSigned( - cfg = self.parent.cfg, + cfg = self.cfg, op = self.parent.name, network = self.parent.proto.network, seed_id = self.parent.kal.al_id.sid, @@ -858,7 +876,7 @@ class MoneroWalletOps: else: idx = int(m[i]) try: - res = [d for d in self.kal.data if d.idx == idx][0] + res = self.kal.entry(idx) except: die(1,'Supplied key-address file does not contain address {}:{}'.format( self.kal.al_id.sid, @@ -1095,6 +1113,8 @@ class MoneroWalletOps: super().__init__(cfg,uarg_tuple) + self.tx = MoneroMMGenTX.Signed( self.cfg, uarg.infile ) + if self.cfg.tx_relay_daemon: m = re.fullmatch( uarg_info['tx_relay_daemon'].pat, @@ -1120,8 +1140,6 @@ class MoneroWalletOps: test_connection = False, # relay is presumably a public node, so avoid extra connections proxy = proxy ) - self.tx = MoneroMMGenTX.Signed( self.cfg, uarg.infile ) - async def main(self): msg('\n' + self.tx.get_info()) @@ -1150,6 +1168,6 @@ class MoneroWalletOps: '\n'.join( tx.get_info() for tx in sorted( - (MoneroMMGenTX.Signed( self.cfg, fn ) for fn in uarg.infile), + (MoneroMMGenTX.Completed( self.cfg, fn ) for fn in uarg.infile), key = lambda x: x.data.sign_time ) )) diff --git a/test/test_py_d/ts_autosign.py b/test/test_py_d/ts_autosign.py index 857d256f..22394d78 100755 --- a/test/test_py_d/ts_autosign.py +++ b/test/test_py_d/ts_autosign.py @@ -97,6 +97,7 @@ class TestSuiteAutosignBase(TestSuiteBase): self.asi = Autosign( AutosignConfig({ + 'coins': ','.join(self.coins), 'mountpoint': ( None if self.live else os.path.join(self.tmpdir,self.mountpoint_basename) @@ -298,20 +299,23 @@ class TestSuiteAutosignBase(TestSuiteBase): def do_sign(self,args,have_msg=False): t = self.spawn('mmgen-autosign', self.opts + args ) t.expect( - f'{self.tx_count} transactions signed' if self.tx_count else + f'{self.tx_count} transaction{suf(self.tx_count)} signed' if self.tx_count else 'No unsigned transactions' ) if self.bad_tx_count: - t.expect(f'{self.bad_tx_count} transactions failed to sign') + t.expect(f'{self.bad_tx_count} transaction{suf(self.bad_tx_count)} failed to sign') t.req_exit_val = 1 if have_msg: t.expect( - f'{self.good_msg_count} message files{{0,1}} signed' if self.good_msg_count else + f'{self.good_msg_count} message file{suf(self.good_msg_count)}{{0,1}} signed' + if self.good_msg_count else 'No unsigned message files', regex=True ) if self.bad_msg_count: - t.expect(f'{self.bad_msg_count} message files{{0,1}} failed to sign', regex=True) + t.expect( + f'{self.bad_msg_count} message file{suf(self.bad_msg_count)}{{0,1}} failed to sign', + regex = True ) t.req_exit_val = 1 if 'wait' in args: diff --git a/test/test_py_d/ts_base.py b/test/test_py_d/ts_base.py index 7b199a54..133d291f 100755 --- a/test/test_py_d/ts_base.py +++ b/test/test_py_d/ts_base.py @@ -25,7 +25,7 @@ from mmgen.cfg import gc from ..include.common import * from .common import * -class TestSuiteBase(object): +class TestSuiteBase: 'initializer class for the test.py test suite' base_passthru_opts = ('data_dir','skip_cfg_file') passthru_opts = () @@ -36,6 +36,8 @@ class TestSuiteBase(object): need_daemon = False def __init__(self,trunner,cfgs,spawn): + if hasattr(self,'tr'): # init will be called multiple times for classes with multiple inheritance + return self.proto = cfg._proto self.tr = trunner self.cfgs = cfgs diff --git a/test/test_py_d/ts_xmrwallet.py b/test/test_py_d/ts_xmrwallet.py index 83f24872..f72bf7a9 100755 --- a/test/test_py_d/ts_xmrwallet.py +++ b/test/test_py_d/ts_xmrwallet.py @@ -615,6 +615,23 @@ class TestSuiteXMRWallet(TestSuiteBase): # mining methods + async def mine5(self): + return await self.mine(5) + + async def mine10(self): + return await self.mine(10) + + async def mine60(self): + return await self.mine(60) + + async def mine(self,nsecs): + imsg_r(f'Mining for {nsecs} seconds...') + await self.start_mining() + await asyncio.sleep(nsecs) + ret = await self.stop_mining() + imsg('done') + return ret + async def start_mining(self): data = self.users['miner'] addr = read_from_file(data.addrfile_fs.format(1)) # mine to wallet #1, account 0