Browse Source

mmgen-tool: OO rewrite of Monero ops

The MMGen Project 4 years ago
parent
commit
eec3464357
3 changed files with 211 additions and 175 deletions
  1. 1 0
      mmgen/main_tool.py
  2. 209 174
      mmgen/tool.py
  3. 1 1
      test/test-release.sh

+ 1 - 0
mmgen/main_tool.py

@@ -72,6 +72,7 @@ opts_data = {
                       'compressed', 'segwit', 'bech32', 'zcash_z')
 -v, --verbose         Produce more verbose output
 -X, --cached-balances Use cached balances (Ethereum only)
+-y, --yes             Answer 'yes' to prompts, suppress non-essential output
 """,
 	'notes': """
 

+ 209 - 174
mmgen/tool.py

@@ -1013,34 +1013,17 @@ class MMGenToolCmdMonero(MMGenToolCmds):
 	a violation of good security practice.
 	"""
 
-	_monero_chain_height = None
-	monerod_args = []
-
-	@property
-	def monero_chain_height(self):
-		if self._monero_chain_height == None:
-			from .daemon import CoinDaemon
-			port = CoinDaemon('xmr',test_suite=g.test_suite).rpc_port
-			cmd = ['monerod','--rpc-bind-port={}'.format(port)] + self.monerod_args + ['status']
-
-			from subprocess import run,PIPE,DEVNULL
-			cp = run(cmd,stdout=PIPE,stderr=DEVNULL,check=True)
-			import re
-			m = re.search(r'Height: (\d+)/\d+ ',cp.stdout.decode())
-			if not m:
-				die(1,'Unable to connect to monerod!')
-			self._monero_chain_height = int(m.group(1))
-			msg('Chain height: {}'.format(self._monero_chain_height))
-
-		return self._monero_chain_height
-
 	def xmrwallet(
 		self,
-		op:str,
-		xmr_keyaddrfile:str,
-		blockheight:'(default: current height)' = 0,
-		addrs:'(integer range or list)' = '',
+		op:                  str,
+		xmr_keyaddrfile:     str,
+		blockheight:         '(default: current height)' = 0,
+		addrs:               '(integer range or list)'   = '',
+		start_wallet_daemon: bool                        = True,
+		stop_wallet_daemon:  bool                        = True,
+		monerod_args:        str                         = '',
 	):
+
 		"""
 		perform various Monero wallet operations for addresses in XMR key-address file
 		  Supported operations:
@@ -1048,166 +1031,218 @@ class MMGenToolCmdMonero(MMGenToolCmds):
 		    sync   - sync wallet for all or specified addresses in key-address file
 		"""
 
-		if op == 'sync' and blockheight != 0:
-			die(1,'sync operation does not support blockheight arg')
-
-		return self.monero_wallet_ops(
-			infile = xmr_keyaddrfile,
-			op = op,
-			blockheight = blockheight,
-			addrs = addrs
-		)
-
-	def monero_wallet_ops(self,infile:str,op:str,blockheight=0,addrs='',monerod_args=[]):
-
-		if monerod_args:
-			self.monerod_args = monerod_args
-
-		async def create(n,d,fn,c,m):
-			try: os.stat(fn)
-			except: pass
-			else:
-				ymsg("Wallet '{}' already exists!".format(fn))
-				return False
-			gmsg(m)
-
-			from .baseconv import baseconv
-			ret = await c.call(
-				'restore_deterministic_wallet',
-				filename  = os.path.basename(fn),
-				password  = d.wallet_passwd,
-				seed      = baseconv.fromhex(d.sec,'xmrseed',tostr=True),
-				restore_height = blockheight,
-				language  = 'English' )
-
-			pp_msg(ret) if opt.debug else msg('  Address: {}'.format(ret['address']))
-			return True
-
-		async def sync(n,d,fn,c,m):
-			try:
-				os.stat(fn)
-			except:
-				ymsg("Wallet '{}' does not exist!".format(fn))
-				return False
-
-			chain_height = self.monero_chain_height
-			gmsg(m)
-
-			import time
-			t_start = time.time()
-
-			msg_r('  Opening wallet...')
-			await c.call(
-				'open_wallet',
-				filename=os.path.basename(fn),
-				password=d.wallet_passwd )
-			msg('done')
-
-			msg_r('  Getting wallet height (be patient, this could take a long time)...')
-			wallet_height = (await c.call('get_height'))['height']
-			msg('\r{}\r  Wallet height: {}        '.format(' '*68,wallet_height))
-
-			behind = chain_height - wallet_height
-			if behind > 1000:
-				m = '  Wallet is {} blocks behind chain tip.  Please be patient.  Syncing...'
-				msg_r(m.format(behind))
-
-			ret = await c.call('refresh')
+		class MoneroWalletOps:
+
+			ops = ('create','sync')
+
+			class base:
+
+				wallet_exists = True
+				_monero_chain_height = None
+
+				def __init__(self,start_daemon=True):
+
+					def wallet_exists(fn):
+						try: os.stat(fn)
+						except: return False
+						else: return True
+
+					def check_wallets():
+						for d in addr_data:
+							fn = self.get_wallet_fn(d)
+							exists = wallet_exists(fn)
+							if exists and not self.wallet_exists:
+								die(1,f'Wallet {fn!r} already exists!')
+							elif not exists and self.wallet_exists:
+								die(1,f'Wallet {fn!r} not found!')
+
+					check_wallets()
+
+					from .daemon import MoneroWalletDaemon
+					self.wd = MoneroWalletDaemon(
+						wallet_dir = opt.outdir or '.',
+						test_suite = g.test_suite
+					)
+
+					if start_daemon:
+						self.wd.restart()
+
+					from .rpc import MoneroWalletRPCClient
+					self.c = MoneroWalletRPCClient(
+						host   = self.wd.host,
+						port   = self.wd.rpc_port,
+						user   = self.wd.user,
+						passwd = self.wd.passwd
+					)
+
+					self.bals = {}
+
+				def stop_daemon(self):
+					self.wd.stop()
+
+				def post_process(self): pass
+
+				def get_wallet_fn(self,d):
+					return os.path.join(
+						opt.outdir or '.','{}-{}-MoneroWallet{}'.format(
+							kal.al_id.sid,
+							d.idx,
+							'-α' if g.debug_utf8 else ''))
+
+				def process_wallets(self):
+					gmsg('\n{}ing {} wallet{}'.format(self.desc,len(addr_data),suf(addr_data)))
+					processed = 0
+					for n,d in enumerate(addr_data): # [d.sec,d.wallet_passwd,d.viewkey,d.addr]
+						fn = self.get_wallet_fn(d)
+						gmsg('\n{}ing wallet {}/{} ({})'.format(
+							self.action,
+							n+1,
+							len(addr_data),
+							os.path.basename(fn),
+						))
+						processed += run_session(self.run(d,fn))
+					gmsg('\n{} wallet{} {}'.format(processed,suf(processed),self.past))
+					return processed
+
+				@property
+				def monero_chain_height(self):
+					if self._monero_chain_height == None:
+						from .daemon import CoinDaemon
+						port = CoinDaemon('xmr',test_suite=g.test_suite).rpc_port
+						cmd = ['monerod','--rpc-bind-port={}'.format(port)] + monerod_args.split() + ['status']
+
+						from subprocess import run,PIPE,DEVNULL
+						cp = run(cmd,stdout=PIPE,stderr=DEVNULL,check=True)
+						import re
+						m = re.search(r'Height: (\d+)/\d+ ',cp.stdout.decode())
+						if not m:
+							die(1,'Unable to connect to monerod!')
+						self._monero_chain_height = int(m.group(1))
+						msg('Chain height: {}'.format(self._monero_chain_height))
+
+					return self._monero_chain_height
+
+			class create(base):
+				name    = 'create'
+				desc    = 'Creat'
+				action  = 'Creat'
+				past    = 'created'
+				wallet_exists = False
+
+				async def run(self,d,fn):
+
+					from .baseconv import baseconv
+					ret = await self.c.call(
+						'restore_deterministic_wallet',
+						filename       = os.path.basename(fn),
+						password       = d.wallet_passwd,
+						seed           = baseconv.fromhex(d.sec,'xmrseed',tostr=True),
+						restore_height = blockheight,
+						language       = 'English' )
+
+					pp_msg(ret) if opt.debug else msg('  Address: {}'.format(ret['address']))
+					return True
+
+			class sync(base):
+				name    = 'sync'
+				desc    = 'Sync'
+				action  = 'Sync'
+				past    = 'synced'
+
+				async def run(self,d,fn):
+
+					chain_height = self.monero_chain_height
+
+					import time
+					t_start = time.time()
+
+					msg_r('  Opening wallet...')
+					await self.c.call(
+						'open_wallet',
+						filename=os.path.basename(fn),
+						password=d.wallet_passwd )
+					msg('done')
+
+					msg_r('  Getting wallet height (be patient, this could take a long time)...')
+					wallet_height = (await self.c.call('get_height'))['height']
+					msg_r('\r' + ' '*68 + '\r')
+					msg(f'  Wallet height: {wallet_height}        ')
+
+					behind = chain_height - wallet_height
+					if behind > 1000:
+						msg_r(f'  Wallet is {behind} blocks behind chain tip.  Please be patient.  Syncing...')
+
+					ret = await self.c.call('refresh')
+
+					if behind > 1000:
+						msg('done')
+
+					if ret['received_money']:
+						msg('  Wallet has received funds')
+
+					t_elapsed = int(time.time() - t_start)
+
+					ret = await self.c.call('get_accounts')
+
+					bn = os.path.basename(fn)
+					self.bals[bn] = tuple(ret[k] for k in ('total_balance','total_unlocked_balance'))
+
+					if opt.debug:
+						pp_msg(ret)
+					else:
+						msg('  Balance: {} Unlocked balance: {}'.format(*[fmtXMRamt(bal) for bal in self.bals[bn]]))
+
+					msg('  Wallet height: {}'.format( (await self.c.call('get_height'))['height'] ))
+					msg('  Sync time: {:02}:{:02}'.format( t_elapsed//60, t_elapsed%60 ))
+
+					await self.c.call('close_wallet')
+					return True
+
+				def post_process(self):
+					col1_w = max(map(len,self.bals)) + 1
+					fs = '{:%s} {} {}' % col1_w
+					msg('\n'+fs.format('Wallet','Balance           ','Unlocked Balance  '))
+					tbals = [0,0]
+					for k in self.bals:
+						for i in (0,1):
+							tbals[i] += self.bals[k][i]
+						msg(fs.format(k+':',*[fmtXMRamt(bal) for bal in self.bals[k]]))
+					msg(fs.format('-'*col1_w,'-'*18,'-'*18))
+					msg(fs.format('TOTAL:',*[fmtXMRamt(bal) for bal in tbals]))
+
+		def fmtXMRamt(amt):
+			from .obj import XMRAmt
+			return XMRAmt(amt,from_unit='min_coin_unit').fmt(fs='5.12',color=True)
 
-			if behind > 1000:
-				msg('done')
+		def check_args():
+			assert addr_data, f'No addresses in addrfile within range {addrs!r}'
 
-			if ret['received_money']:
-				msg('  Wallet has received funds')
+			if op not in MoneroWalletOps.ops:
+				die(1,f'{op!r}: unrecognized operation')
 
-			t_elapsed = int(time.time() - t_start)
+			if op == 'sync' and blockheight != 0:
+				die(1,'Sync operation does not support blockheight arg')
 
-			ret = await c.call('get_accounts')
+		# start execution
+		from .protocol import init_proto
 
-			from .obj import XMRAmt
-			bals[fn] = tuple([XMRAmt(ret[k],from_unit='min_coin_unit')
-					for k in ('total_balance','total_unlocked_balance')])
+		kal = KeyAddrList(init_proto('xmr',network='mainnet'),xmr_keyaddrfile)
+		addr_data = [
+			d for d in kal.data if addrs == '' or d.idx in AddrIdxList(addrs)
+		]
 
-			if opt.debug:
-				pp_msg(ret)
-			else:
-				msg('  Balance: {} Unlocked balance: {}'.format(*[b.hl() for b in bals[fn]]))
-
-			msg('  Wallet height: {}'.format((await c.call('get_height'))['height']))
-			msg('  Sync time: {:02}:{:02}'.format(t_elapsed//60,t_elapsed%60))
-
-			await c.call('close_wallet')
-			return True
-
-		async def process_wallets(op):
-			g.accept_defaults = g.accept_defaults or op.accept_defaults
-			from .protocol import init_proto
-			proto = init_proto('xmr',network='mainnet')
-			al = KeyAddrList(proto,infile)
-			data = [d for d in al.data if addrs == '' or d.idx in AddrIdxList(addrs)]
-			dl = len(data)
-			assert dl,"No addresses in addrfile within range '{}'".format(addrs)
-			gmsg('\n{}ing {} wallet{}'.format(op.desc,dl,suf(dl)))
-
-			from .daemon import MoneroWalletDaemon
-			wd = MoneroWalletDaemon(
-				wallet_dir = opt.outdir or '.',
-				test_suite = g.test_suite )
-			wd.restart()
-
-			from .rpc import MoneroWalletRPCClient
-			c = MoneroWalletRPCClient(
-				host = wd.host,
-				port = wd.rpc_port,
-				user = wd.user,
-				passwd = wd.passwd )
-
-			wallets_processed = 0
-			for n,d in enumerate(data): # [d.sec,d.wallet_passwd,d.viewkey,d.addr]
-				fn = os.path.join(
-					opt.outdir or '.','{}-{}-MoneroWallet{}'.format(
-						al.al_id.sid,
-						d.idx,
-						'-α' if g.debug_utf8 else ''))
-
-				info = '\n{}ing wallet {}/{} ({})'.format(op.action,n+1,dl,fn)
-				wallets_processed += await op.func(n,d,fn,c,info)
-
-			wd.stop()
-			gmsg('\n{} wallet{} {}ed'.format(wallets_processed,suf(wallets_processed),op.desc.lower()))
-
-			if wallets_processed and op.name == 'sync':
-				col1_w = max(map(len,bals)) + 1
-				fs = '{:%s} {} {}' % col1_w
-				msg('\n'+fs.format('Wallet','Balance           ','Unlocked Balance  '))
-				from .obj import XMRAmt
-				tbals = [XMRAmt('0'),XMRAmt('0')]
-				for bal in bals:
-					for i in (0,1): tbals[i] += bals[bal][i]
-					msg(fs.format(bal+':',*[XMRAmt(b).fmt(fs='5.12',color=True) for b in bals[bal]]))
-				msg(fs.format('-'*col1_w,'-'*18,'-'*18))
-				msg(fs.format('TOTAL:',*[XMRAmt(b).fmt(fs='5.12',color=True) for b in tbals]))
+		check_args()
 
 		if blockheight < 0:
 			blockheight = 0 # TODO: handle the non-zero case
 
-		bals = {} # locked,unlocked
+		m = getattr(MoneroWalletOps,op)(start_daemon=start_wallet_daemon)
 
-		wo = namedtuple('mwo',['name','desc','action','func','accept_defaults'])
-		op = { # reusing name!
-			'create': wo('create', 'Creat', 'Generat', create, False),
-			'sync':   wo('sync',   'Sync',  'Sync',    sync,   True) }[op]
-		try:
-			run_session(process_wallets(op))
-		except KeyboardInterrupt:
-			rdie(1,'\nUser interrupt\n')
-		except EOFError:
-			rdie(2,'\nEnd of file\n')
-		except Exception as e:
-			try:
-				die(1,'Error: {}'.format(e.args[0]))
-			except:
-				rdie(1,'Error: {!r}'.format(e.args[0]))
+		if m.process_wallets():
+			m.post_process()
+
+		if stop_wallet_daemon:
+			m.stop_daemon()
 
 		return True
 

+ 1 - 1
test/test-release.sh

@@ -340,7 +340,7 @@ else
 	create_tmpdir
 fi
 
-mmgen_tool_xmr="$mmgen_tool -q --accept-defaults --outdir $TMPDIR --monero-wallet-rpc-password=passw0rd"
+mmgen_tool_xmr="$mmgen_tool -q --yes --outdir $TMPDIR --monero-wallet-rpc-password=passw0rd"
 i_xmr='Monero'
 s_xmr='Testing key-address file generation and wallet creation and sync operations for Monero'
 s_xmr='The monerod (mainnet) daemon must be running for the following tests'