Browse Source

move MoneroWalletOps class to new `xmrwallet` module

The MMGen Project 3 years ago
parent
commit
3484222185
3 changed files with 522 additions and 497 deletions
  1. 5 497
      mmgen/tool.py
  2. 516 0
      mmgen/xmrwallet.py
  3. 1 0
      setup.py

+ 5 - 497
mmgen/tool.py

@@ -20,8 +20,6 @@
 tool.py:  Routines for the 'mmgen-tool' utility
 """
 
-import re
-from collections import namedtuple
 from .protocol import hash160
 from .common import *
 from .crypto import *
@@ -1012,25 +1010,6 @@ class MMGenToolCmdRPC(MMGenToolCmds):
 			msg("Address '{}' deleted from tracking wallet".format(ret))
 		return ret
 
-from .obj import XMRAmt
-
-def fmtXMRamt(amt):
-	return XMRAmt(amt,from_unit='min_coin_unit').fmt(fs='5.12',color=True)
-
-def hlXMRamt(amt):
-	return XMRAmt(amt,from_unit='min_coin_unit').hl()
-
-def make_uarg_info():
-	e = namedtuple('uarg_info_entry',['annot','pat'])
-	hp = r'(?:[^:]+):(?:\d+)'
-	return {
-		'daemon':          e('HOST:PORT', hp),
-		'tx_relay_daemon': e('HOST:PORT[:PROXY_HOST:PROXY_PORT]', r'({p})(?::({p}))?'.format(p=hp)),
-		'wallets_sweep':   e('SOURCE_WALLET_NUM:ACCOUNT[,DEST_WALLET_NUM]', r'(\d+):(\d+)(?:,(\d+))?'),
-	}
-
-uarg_info = make_uarg_info()
-
 class MMGenToolCmdMonero(MMGenToolCmds):
 	"""
 	Monero wallet operations
@@ -1040,6 +1019,8 @@ class MMGenToolCmdMonero(MMGenToolCmds):
 	a violation of good security practice.
 	"""
 
+	from .xmrwallet import xmrwallet_uarg_info
+
 	def xmrwallet(
 		self,
 		op:                  str,
@@ -1048,8 +1029,8 @@ class MMGenToolCmdMonero(MMGenToolCmds):
 		wallets:             '(integer range or list, or sweep specifier)' = '',
 		start_wallet_daemon  = True,
 		stop_wallet_daemon   = True,
-		daemon:              uarg_info['daemon'].annot = '',
-		tx_relay_daemon:     uarg_info['tx_relay_daemon'].annot = '',
+		daemon:              xmrwallet_uarg_info['daemon'].annot = '',
+		tx_relay_daemon:     xmrwallet_uarg_info['tx_relay_daemon'].annot = '',
 	):
 
 		"""
@@ -1087,481 +1068,8 @@ class MMGenToolCmdMonero(MMGenToolCmds):
 		    The user is prompted before addresses are created or funds are transferred.
 		"""
 
-		class MoneroWalletOps:
-
-			ops = ('create','sync','sweep')
-
-			class base:
+		from .xmrwallet import MoneroWalletOps
 
-				wallet_exists = True
-				tx_relay = False
-
-				def check_uargs(self):
-
-					def check_host_arg(name):
-						val = getattr(uarg,name)
-						if not re.fullmatch(uarg_info[name].pat,val,re.ASCII):
-							die(1,'{!r}: invalid {!r} parameter: it must have format {!r}'.format(
-								val, name, uarg_info[name].annot ))
-
-					if uarg.op != 'create' and uarg.restore_height != 0:
-						die(1,"'restore_height' arg is supported only for create operation")
-
-					if uarg.restore_height < 0:
-						die(1,f"{uarg.restore_height}: invalid 'restore_height' arg (<0)")
-
-					if uarg.daemon:
-						check_host_arg('daemon')
-
-					if uarg.tx_relay_daemon:
-						if not self.tx_relay:
-							die(1,f"'tx_relay_daemon' arg is not recognized for operation {uarg.op!r}")
-						check_host_arg('tx_relay_daemon')
-
-				def __init__(self,uarg_tuple):
-
-					def wallet_exists(fn):
-						try: os.stat(fn)
-						except: return False
-						else: return True
-
-					def check_wallets():
-						for d in self.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!')
-
-					global uarg
-					uarg = uarg_tuple
-
-					self.check_uargs()
-
-					from .protocol import init_proto
-					self.kal = KeyAddrList(init_proto('xmr',network='mainnet'),uarg.xmr_keyaddrfile)
-					self.create_addr_data()
-
-					check_wallets()
-
-					from .daemon import MoneroWalletDaemon
-					self.wd = MoneroWalletDaemon(
-						wallet_dir = opt.outdir or '.',
-						test_suite = g.test_suite,
-						daemon_addr = uarg.daemon or None,
-					)
-
-					if uarg.start_wallet_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.post_init()
-
-				def create_addr_data(self):
-					if uarg.wallets:
-						idxs = AddrIdxList(uarg.wallets)
-						self.addr_data = [d for d in self.kal.data if d.idx in idxs]
-						if len(self.addr_data) != len(idxs):
-							die(1,f'List {uarg.wallets!r} contains addresses not present in supplied key-address file')
-					else:
-						self.addr_data = self.kal.data
-
-				def stop_daemons(self):
-					if uarg.stop_wallet_daemon:
-						self.wd.stop()
-						if uarg.tx_relay_daemon:
-							self.wd2.stop()
-
-				def post_init(self): pass
-				def post_process(self): pass
-
-				def get_wallet_fn(self,d):
-					return os.path.join(
-						opt.outdir or '.','{}-{}-MoneroWallet{}'.format(
-							self.kal.al_id.sid,
-							d.idx,
-							'-α' if g.debug_utf8 else ''))
-
-				async def process_wallets(self):
-					gmsg('\n{}ing {} wallet{}'.format(self.desc,len(self.addr_data),suf(self.addr_data)))
-					processed = 0
-					for n,d in enumerate(self.addr_data): # [d.sec,d.addr,d.wallet_passwd,d.viewkey]
-						fn = self.get_wallet_fn(d)
-						gmsg('\n{}ing wallet {}/{} ({})'.format(
-							self.desc,
-							n+1,
-							len(self.addr_data),
-							os.path.basename(fn),
-						))
-						processed += await self.run(d,fn)
-					gmsg('\n{} wallet{} {}'.format(processed,suf(processed),self.past))
-					return processed
-
-			class create(base):
-				name    = 'create'
-				desc    = '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 = uarg.restore_height,
-						language       = 'English' )
-
-					pp_msg(ret) if opt.debug else msg('  Address: {}'.format(ret['address']))
-					return True
-
-			class sync(base):
-				name    = 'sync'
-				desc    = 'Sync'
-				past    = 'synced'
-
-				async def run(self,d,fn):
-
-					chain_height = (await self.dc.call('get_info'))['height']
-					msg(f'  Chain height: {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)
-
-					bn = os.path.basename(fn)
-
-					a,b = await xmr_rpc_methods(self,d).get_accts(print=False)
-
-					msg('  Balance: {} Unlocked balance: {}'.format(
-						hlXMRamt(a['total_balance']),
-						hlXMRamt(a['total_unlocked_balance']),
-					))
-
-					self.accts_data[bn] = { 'accts': a, 'addrs': b }
-
-					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_init(self):
-					host,port = uarg.daemon.split(':') if uarg.daemon else ('localhost',self.wd.daemon_port)
-					from .rpc import MoneroRPCClient
-					self.dc = MoneroRPCClient(host=host, port=int(port), user=None, passwd=None)
-					self.accts_data = {}
-
-				def post_process(self):
-					d = self.accts_data
-
-					for n,k in enumerate(d):
-						ad = self.addr_data[n]
-						xmr_rpc_methods(self,ad).print_accts( d[k]['accts'], d[k]['addrs'], indent='')
-
-					col1_w = max(map(len,d)) + 1
-					fs = '{:%s} {} {}' % col1_w
-					tbals = [0,0]
-					msg('\n'+fs.format('Wallet','Balance           ','Unlocked Balance'))
-
-					for k in d:
-						b  = d[k]['accts']['total_balance']
-						ub = d[k]['accts']['total_unlocked_balance']
-						msg(fs.format( k + ':', fmtXMRamt(b), fmtXMRamt(ub) ))
-						tbals[0] += b
-						tbals[1] += ub
-
-					msg(fs.format( '-'*col1_w, '-'*18, '-'*18 ))
-					msg(fs.format( 'TOTAL:', fmtXMRamt(tbals[0]), fmtXMRamt(tbals[1]) ))
-
-			class sweep(base):
-				name    = 'sweep'
-				desc    = 'Sweep'
-				past    = 'swept'
-				tx_relay = True
-
-				def create_addr_data(self):
-					m = re.fullmatch(uarg_info['wallets_sweep'].pat,uarg.wallets,re.ASCII)
-					if not m:
-						fs = "{!r}: invalid 'wallets' arg: for sweep operation, it must have format {!r}"
-						die(1,fs.format( uarg.wallets, uarg_info['wallets_sweep'].annot ))
-
-					def gen():
-						for i,k in ( (1,'source'), (3,'dest') ):
-							if m[i] == None:
-								setattr(self,k,None)
-							else:
-								idx = int(m[i])
-								try:
-									res = [d for d in self.kal.data if d.idx == idx][0]
-								except:
-									die(1,'Supplied key-address file does not contain address {}:{}'.format(
-										self.kal.al_id.sid,
-										idx ))
-								else:
-									setattr(self,k,res)
-									yield res
-
-					self.addr_data = list(gen())
-					self.account = int(m[2])
-
-				def post_init(self):
-
-					if uarg.tx_relay_daemon:
-						m = re.fullmatch(uarg_info['tx_relay_daemon'].pat,uarg.tx_relay_daemon,re.ASCII)
-
-						from .daemon import MoneroWalletDaemon
-						self.wd2 = MoneroWalletDaemon(
-							wallet_dir = opt.outdir or '.',
-							test_suite = g.test_suite,
-							daemon_addr = m[1],
-							proxy = m[2],
-							rpc_port_shift = 16,
-						)
-
-						if uarg.start_wallet_daemon:
-							self.wd2.restart()
-
-						from .rpc import MoneroWalletRPCClient
-						self.c2 = MoneroWalletRPCClient(
-							host   = self.wd2.host,
-							port   = self.wd2.rpc_port,
-							user   = self.wd2.user,
-							passwd = self.wd2.passwd
-						)
-
-				async def process_wallets(self):
-					gmsg(f'\nSweeping account #{self.account} of wallet {self.source.idx}' + (
-						' to new address' if self.dest is None else
-						f' to new account in wallet {self.dest.idx}' ))
-
-					h = xmr_rpc_methods(self,self.source)
-
-					await h.open_wallet('source')
-					accts_data = (await h.get_accts())[0]
-
-					max_acct = len(accts_data['subaddress_accounts']) - 1
-					if self.account > max_acct:
-						die(1,f'{self.account}: requested account index out of bounds (>{max_acct})')
-
-					await h.get_addrs(accts_data,self.account)
-
-					if self.dest == None:
-						if keypress_confirm(f'\nCreate new address for account #{self.account}?'):
-							new_addr = await h.create_new_addr(self.account)
-						elif keypress_confirm(f'Sweep to last existing address of account #{self.account}?'):
-							new_addr = await h.get_last_addr(self.account)
-						else:
-							die(1,'Exiting at user request')
-						await h.get_addrs(accts_data,self.account)
-					else:
-						await h.close_wallet('source')
-						bn = os.path.basename(self.get_wallet_fn(self.dest))
-						h2 = xmr_rpc_methods(self,self.dest)
-						await h2.open_wallet('destination')
-						accts_data = (await h2.get_accts())[0]
-
-						if keypress_confirm(f'\nCreate new account for wallet {bn!r}?'):
-							new_addr = await h2.create_acct()
-							await h2.get_accts()
-						elif keypress_confirm(f'Sweep to last existing account of wallet {bn!r}?'):
-							new_addr = h2.get_last_acct(accts_data)
-						else:
-							die(1,'Exiting at user request')
-
-						await h2.close_wallet('destination')
-						await h.open_wallet('source')
-
-					msg('\nCreating sweep transaction: balance of wallet {}, account #{} => {}'.format(
-						self.source.idx,
-						self.account,
-						cyan(new_addr),
-					))
-					sweep_tx = await h.make_sweep_tx(self.account,new_addr)
-
-					if keypress_confirm('Relay sweep transaction?'):
-						w_desc = 'source'
-						if uarg.tx_relay_daemon:
-							await h.close_wallet('source')
-							self.c = self.c2
-							h = xmr_rpc_methods(self,self.source)
-							w_desc = 'TX relay source'
-							await h.open_wallet(w_desc)
-						msg(f'\n    Relaying sweep transaction...')
-						await h.relay_sweep_tx( sweep_tx['tx_metadata_list'][0] )
-						await h.close_wallet(w_desc)
-
-						gmsg('\n\nAll done')
-					else:
-						await h.close_wallet('source')
-						die(1,'\nExiting at user request')
-
-					return True
-
-		class xmr_rpc_methods:
-
-			def __init__(self,parent,d):
-				self.parent = parent
-				self.c = parent.c
-				self.d = d
-				self.fn = parent.get_wallet_fn(d)
-
-			async def open_wallet(self,desc):
-				gmsg_r(f'\n  Opening {desc} wallet...')
-				ret = await self.c.call( # returns {}
-					'open_wallet',
-					filename=os.path.basename(self.fn),
-					password=self.d.wallet_passwd )
-				gmsg('done')
-
-			async def close_wallet(self,desc):
-				gmsg_r(f'\n  Closing {desc} wallet...')
-				await self.c.call('close_wallet')
-				gmsg_r('done')
-
-			def print_accts(self,data,addrs_data,indent='    '):
-				d = data['subaddress_accounts']
-				msg('\n' + indent + f'Accounts of wallet {os.path.basename(self.fn)}:')
-				fs = indent + '  {:6}  {:18}  {:<6} {:%s}  {}' % max(len(e['label']) for e in d)
-				msg(fs.format('Index ','Base Address','nAddrs','Label','Balance'))
-				for i,e in enumerate(d):
-					msg(fs.format(
-						str(e['account_index']),
-						e['base_address'][:15] + '...',
-						len(addrs_data[i]['addresses']),
-						e['label'],
-						fmtXMRamt(e['balance']),
-					))
-
-			async def get_accts(self,print=True):
-				data = await self.c.call('get_accounts')
-				addrs_data = [
-					await self.c.call('get_address',account_index=i)
-						for i in range(len(data['subaddress_accounts']))
-				]
-				if print:
-					self.print_accts(data,addrs_data)
-				return ( data, addrs_data )
-
-			async def create_acct(self):
-				msg('\n    Creating new account...')
-				ret = await self.c.call(
-					'create_account',
-					label = f'Sweep from {self.parent.source.idx}:{self.parent.account}'
-				)
-				msg('      Index:   {}'.format( pink(str(ret['account_index'])) ))
-				msg('      Address: {}'.format( cyan(ret['address']) ))
-				return ret['address']
-
-			def get_last_acct(self,accts_data):
-				msg('\n    Getting last account...')
-				data = accts_data['subaddress_accounts'][-1]
-				msg('      Index:   {}'.format( pink(str(data['account_index'])) ))
-				msg('      Address: {}'.format( cyan(data['base_address']) ))
-				return data['base_address']
-
-			async def get_addrs(self,accts_data,account):
-				ret = await self.c.call('get_address',account_index=account)
-				d = ret['addresses']
-				msg('\n      Addresses of account #{} ({}):'.format(
-					account,
-					accts_data['subaddress_accounts'][account]['label']))
-				fs = '        {:6}  {:18}  {:%s}  {}' % max(len(e['label']) for e in d)
-				msg(fs.format('Index ','Address','Label','Used'))
-				for e in d:
-					msg(fs.format(
-						str(e['address_index']),
-						e['address'][:15] + '...',
-						e['label'],
-						e['used']
-					))
-				return ret
-
-			async def create_new_addr(self,account):
-				msg_r('\n    Creating new address: ')
-				ret = await self.c.call(
-					'create_address',
-					account_index = account,
-					label         = 'Sweep from this account',
-				)
-				msg(cyan(ret['address']))
-				return ret['address']
-
-			async def get_last_addr(self,account):
-				msg('\n    Getting last address:')
-				ret = (await self.c.call(
-					'get_address',
-					account_index = account,
-				))['addresses'][-1]['address']
-				msg('      ' + cyan(ret))
-				return ret
-
-			def display_sweep_tx(self,data):
-				from .obj import CoinTxID
-				msg('    TxID:   {}\n    Amount: {}\n    Fee:    {}'.format(
-					CoinTxID(data['tx_hash_list'][0]).hl(),
-					hlXMRamt(data['amount_list'][0]),
-					hlXMRamt(data['fee_list'][0]),
-				))
-
-			async def make_sweep_tx(self,account,addr):
-				ret = await self.c.call(
-					'sweep_all',
-					address = addr,
-					account_index = account,
-					do_not_relay = True,
-					get_tx_metadata = True
-				)
-				self.display_sweep_tx(ret)
-				return ret
-
-			def display_txid(self,data):
-				from .obj import CoinTxID
-				msg('    Relayed {}'.format( CoinTxID(data['tx_hash']).hl() ))
-
-			async def relay_sweep_tx(self,tx_hex):
-				ret = await self.c.call('relay_tx',hex=tx_hex)
-				try:
-					self.display_txid(ret)
-				except:
-					print(ret)
-
-		# start execution
 		if op not in MoneroWalletOps.ops:
 			die(1,f'{op!r}: unrecognized operation')
 

+ 516 - 0
mmgen/xmrwallet.py

@@ -0,0 +1,516 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2021 The MMGen Project <mmgen@tuta.io>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+xmrwallet.py - MoneroWalletOps class
+"""
+
+import os,re
+from collections import namedtuple
+from .common import *
+from .addr import KeyAddrList,AddrIdxList
+from .rpc import MoneroRPCClient, MoneroWalletRPCClient
+from .daemon import MoneroWalletDaemon
+
+xmrwallet_uarg_info = (
+	lambda e,hp: {
+		'daemon':          e('HOST:PORT', hp),
+		'tx_relay_daemon': e('HOST:PORT[:PROXY_HOST:PROXY_PORT]', r'({p})(?::({p}))?'.format(p=hp)),
+		'wallets_sweep':   e('SOURCE_WALLET_NUM:ACCOUNT[,DEST_WALLET_NUM]', r'(\d+):(\d+)(?:,(\d+))?'),
+	})(
+		namedtuple('uarg_info_entry',['annot','pat']),
+		r'(?:[^:]+):(?:\d+)'
+	)
+
+class MoneroWalletOps:
+
+	ops = ('create','sync','sweep')
+
+	class base:
+
+		class rpc:
+
+			def __init__(self,parent,d):
+				self.parent = parent
+				self.c = parent.c
+				self.d = d
+				self.fn = parent.get_wallet_fn(d)
+
+			async def open_wallet(self,desc):
+				gmsg_r(f'\n  Opening {desc} wallet...')
+				ret = await self.c.call( # returns {}
+					'open_wallet',
+					filename=os.path.basename(self.fn),
+					password=self.d.wallet_passwd )
+				gmsg('done')
+
+			async def close_wallet(self,desc):
+				gmsg_r(f'\n  Closing {desc} wallet...')
+				await self.c.call('close_wallet')
+				gmsg_r('done')
+
+			def print_accts(self,data,addrs_data,indent='    '):
+				d = data['subaddress_accounts']
+				msg('\n' + indent + f'Accounts of wallet {os.path.basename(self.fn)}:')
+				fs = indent + '  {:6}  {:18}  {:<6} {:%s}  {}' % max(len(e['label']) for e in d)
+				msg(fs.format('Index ','Base Address','nAddrs','Label','Balance'))
+				for i,e in enumerate(d):
+					msg(fs.format(
+						str(e['account_index']),
+						e['base_address'][:15] + '...',
+						len(addrs_data[i]['addresses']),
+						e['label'],
+						fmt_amt(e['balance']),
+					))
+
+			async def get_accts(self,print=True):
+				data = await self.c.call('get_accounts')
+				addrs_data = [
+					await self.c.call('get_address',account_index=i)
+						for i in range(len(data['subaddress_accounts']))
+				]
+				if print:
+					self.print_accts(data,addrs_data)
+				return ( data, addrs_data )
+
+			async def create_acct(self):
+				msg('\n    Creating new account...')
+				ret = await self.c.call(
+					'create_account',
+					label = f'Sweep from {self.parent.source.idx}:{self.parent.account}'
+				)
+				msg('      Index:   {}'.format( pink(str(ret['account_index'])) ))
+				msg('      Address: {}'.format( cyan(ret['address']) ))
+				return ret['address']
+
+			def get_last_acct(self,accts_data):
+				msg('\n    Getting last account...')
+				data = accts_data['subaddress_accounts'][-1]
+				msg('      Index:   {}'.format( pink(str(data['account_index'])) ))
+				msg('      Address: {}'.format( cyan(data['base_address']) ))
+				return data['base_address']
+
+			async def get_addrs(self,accts_data,account):
+				ret = await self.c.call('get_address',account_index=account)
+				d = ret['addresses']
+				msg('\n      Addresses of account #{} ({}):'.format(
+					account,
+					accts_data['subaddress_accounts'][account]['label']))
+				fs = '        {:6}  {:18}  {:%s}  {}' % max(len(e['label']) for e in d)
+				msg(fs.format('Index ','Address','Label','Used'))
+				for e in d:
+					msg(fs.format(
+						str(e['address_index']),
+						e['address'][:15] + '...',
+						e['label'],
+						e['used']
+					))
+				return ret
+
+			async def create_new_addr(self,account):
+				msg_r('\n    Creating new address: ')
+				ret = await self.c.call(
+					'create_address',
+					account_index = account,
+					label         = 'Sweep from this account',
+				)
+				msg(cyan(ret['address']))
+				return ret['address']
+
+			async def get_last_addr(self,account):
+				msg('\n    Getting last address:')
+				ret = (await self.c.call(
+					'get_address',
+					account_index = account,
+				))['addresses'][-1]['address']
+				msg('      ' + cyan(ret))
+				return ret
+
+			def display_sweep_tx(self,data):
+				from .obj import CoinTxID
+				msg('    TxID:   {}\n    Amount: {}\n    Fee:    {}'.format(
+					CoinTxID(data['tx_hash_list'][0]).hl(),
+					hl_amt(data['amount_list'][0]),
+					hl_amt(data['fee_list'][0]),
+				))
+
+			async def make_sweep_tx(self,account,addr):
+				ret = await self.c.call(
+					'sweep_all',
+					address = addr,
+					account_index = account,
+					do_not_relay = True,
+					get_tx_metadata = True
+				)
+				self.display_sweep_tx(ret)
+				return ret
+
+			def display_txid(self,data):
+				from .obj import CoinTxID
+				msg('    Relayed {}'.format( CoinTxID(data['tx_hash']).hl() ))
+
+			async def relay_sweep_tx(self,tx_hex):
+				ret = await self.c.call('relay_tx',hex=tx_hex)
+				try:
+					self.display_txid(ret)
+				except:
+					print(ret)
+
+		wallet_exists = True
+		tx_relay = False
+
+		def check_uargs(self):
+
+			def check_host_arg(name):
+				val = getattr(uarg,name)
+				if not re.fullmatch(uarg_info[name].pat,val,re.ASCII):
+					die(1,'{!r}: invalid {!r} parameter: it must have format {!r}'.format(
+						val, name, uarg_info[name].annot ))
+
+			if uarg.op != 'create' and uarg.restore_height != 0:
+				die(1,"'restore_height' arg is supported only for create operation")
+
+			if uarg.restore_height < 0:
+				die(1,f"{uarg.restore_height}: invalid 'restore_height' arg (<0)")
+
+			if uarg.daemon:
+				check_host_arg('daemon')
+
+			if uarg.tx_relay_daemon:
+				if not self.tx_relay:
+					die(1,f"'tx_relay_daemon' arg is not recognized for operation {uarg.op!r}")
+				check_host_arg('tx_relay_daemon')
+
+		def __init__(self,uarg_tuple):
+
+			def wallet_exists(fn):
+				try: os.stat(fn)
+				except: return False
+				else: return True
+
+			def check_wallets():
+				for d in self.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!')
+
+			global uarg, uarg_info, fmt_amt, hl_amt
+
+			uarg = uarg_tuple
+			uarg_info = xmrwallet_uarg_info
+
+			from .obj import XMRAmt
+			def fmt_amt(amt):
+				return XMRAmt(amt,from_unit='min_coin_unit').fmt(fs='5.12',color=True)
+			def hl_amt(amt):
+				return XMRAmt(amt,from_unit='min_coin_unit').hl()
+
+			self.check_uargs()
+
+			from .protocol import init_proto
+			self.kal = KeyAddrList(init_proto('xmr',network='mainnet'),uarg.xmr_keyaddrfile)
+			self.create_addr_data()
+
+			check_wallets()
+
+			self.wd = MoneroWalletDaemon(
+				wallet_dir = opt.outdir or '.',
+				test_suite = g.test_suite,
+				daemon_addr = uarg.daemon or None,
+			)
+
+			if uarg.start_wallet_daemon:
+				self.wd.restart()
+
+			self.c = MoneroWalletRPCClient(
+				host   = self.wd.host,
+				port   = self.wd.rpc_port,
+				user   = self.wd.user,
+				passwd = self.wd.passwd
+			)
+
+			self.post_init()
+
+		def create_addr_data(self):
+			if uarg.wallets:
+				idxs = AddrIdxList(uarg.wallets)
+				self.addr_data = [d for d in self.kal.data if d.idx in idxs]
+				if len(self.addr_data) != len(idxs):
+					die(1,f'List {uarg.wallets!r} contains addresses not present in supplied key-address file')
+			else:
+				self.addr_data = self.kal.data
+
+		def stop_daemons(self):
+			if uarg.stop_wallet_daemon:
+				self.wd.stop()
+				if uarg.tx_relay_daemon:
+					self.wd2.stop()
+
+		def post_init(self): pass
+		def post_process(self): pass
+
+		def get_wallet_fn(self,d):
+			return os.path.join(
+				opt.outdir or '.','{}-{}-MoneroWallet{}'.format(
+					self.kal.al_id.sid,
+					d.idx,
+					'-α' if g.debug_utf8 else ''))
+
+		async def process_wallets(self):
+			gmsg('\n{}ing {} wallet{}'.format(self.desc,len(self.addr_data),suf(self.addr_data)))
+			processed = 0
+			for n,d in enumerate(self.addr_data): # [d.sec,d.addr,d.wallet_passwd,d.viewkey]
+				fn = self.get_wallet_fn(d)
+				gmsg('\n{}ing wallet {}/{} ({})'.format(
+					self.desc,
+					n+1,
+					len(self.addr_data),
+					os.path.basename(fn),
+				))
+				processed += await self.run(d,fn)
+			gmsg('\n{} wallet{} {}'.format(processed,suf(processed),self.past))
+			return processed
+
+	class create(base):
+		name    = 'create'
+		desc    = '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 = uarg.restore_height,
+				language       = 'English' )
+
+			pp_msg(ret) if opt.debug else msg('  Address: {}'.format(ret['address']))
+			return True
+
+	class sync(base):
+		name    = 'sync'
+		desc    = 'Sync'
+		past    = 'synced'
+
+		async def run(self,d,fn):
+
+			chain_height = (await self.dc.call('get_info'))['height']
+			msg(f'  Chain height: {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)
+
+			bn = os.path.basename(fn)
+
+			a,b = await self.rpc(self,d).get_accts(print=False)
+
+			msg('  Balance: {} Unlocked balance: {}'.format(
+				hl_amt(a['total_balance']),
+				hl_amt(a['total_unlocked_balance']),
+			))
+
+			self.accts_data[bn] = { 'accts': a, 'addrs': b }
+
+			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_init(self):
+			host,port = uarg.daemon.split(':') if uarg.daemon else ('localhost',self.wd.daemon_port)
+			self.dc = MoneroRPCClient(host=host, port=int(port), user=None, passwd=None)
+			self.accts_data = {}
+
+		def post_process(self):
+			d = self.accts_data
+
+			for n,k in enumerate(d):
+				ad = self.addr_data[n]
+				self.rpc(self,ad).print_accts( d[k]['accts'], d[k]['addrs'], indent='')
+
+			col1_w = max(map(len,d)) + 1
+			fs = '{:%s} {} {}' % col1_w
+			tbals = [0,0]
+			msg('\n'+fs.format('Wallet','Balance           ','Unlocked Balance'))
+
+			for k in d:
+				b  = d[k]['accts']['total_balance']
+				ub = d[k]['accts']['total_unlocked_balance']
+				msg(fs.format( k + ':', fmt_amt(b), fmt_amt(ub) ))
+				tbals[0] += b
+				tbals[1] += ub
+
+			msg(fs.format( '-'*col1_w, '-'*18, '-'*18 ))
+			msg(fs.format( 'TOTAL:', fmt_amt(tbals[0]), fmt_amt(tbals[1]) ))
+
+	class sweep(base):
+		name    = 'sweep'
+		desc    = 'Sweep'
+		past    = 'swept'
+		tx_relay = True
+
+		def create_addr_data(self):
+			m = re.fullmatch(uarg_info['wallets_sweep'].pat,uarg.wallets,re.ASCII)
+			if not m:
+				fs = "{!r}: invalid 'wallets' arg: for sweep operation, it must have format {!r}"
+				die(1,fs.format( uarg.wallets, uarg_info['wallets_sweep'].annot ))
+
+			def gen():
+				for i,k in ( (1,'source'), (3,'dest') ):
+					if m[i] == None:
+						setattr(self,k,None)
+					else:
+						idx = int(m[i])
+						try:
+							res = [d for d in self.kal.data if d.idx == idx][0]
+						except:
+							die(1,'Supplied key-address file does not contain address {}:{}'.format(
+								self.kal.al_id.sid,
+								idx ))
+						else:
+							setattr(self,k,res)
+							yield res
+
+			self.addr_data = list(gen())
+			self.account = int(m[2])
+
+		def post_init(self):
+
+			if uarg.tx_relay_daemon:
+				m = re.fullmatch(uarg_info['tx_relay_daemon'].pat,uarg.tx_relay_daemon,re.ASCII)
+
+				self.wd2 = MoneroWalletDaemon(
+					wallet_dir = opt.outdir or '.',
+					test_suite = g.test_suite,
+					daemon_addr = m[1],
+					proxy = m[2],
+					rpc_port_shift = 16,
+				)
+
+				if uarg.start_wallet_daemon:
+					self.wd2.restart()
+
+				self.c2 = MoneroWalletRPCClient(
+					host   = self.wd2.host,
+					port   = self.wd2.rpc_port,
+					user   = self.wd2.user,
+					passwd = self.wd2.passwd
+				)
+
+		async def process_wallets(self):
+			gmsg(f'\nSweeping account #{self.account} of wallet {self.source.idx}' + (
+				' to new address' if self.dest is None else
+				f' to new account in wallet {self.dest.idx}' ))
+
+			h = self.rpc(self,self.source)
+
+			await h.open_wallet('source')
+			accts_data = (await h.get_accts())[0]
+
+			max_acct = len(accts_data['subaddress_accounts']) - 1
+			if self.account > max_acct:
+				die(1,f'{self.account}: requested account index out of bounds (>{max_acct})')
+
+			await h.get_addrs(accts_data,self.account)
+
+			if self.dest == None:
+				if keypress_confirm(f'\nCreate new address for account #{self.account}?'):
+					new_addr = await h.create_new_addr(self.account)
+				elif keypress_confirm(f'Sweep to last existing address of account #{self.account}?'):
+					new_addr = await h.get_last_addr(self.account)
+				else:
+					die(1,'Exiting at user request')
+				await h.get_addrs(accts_data,self.account)
+			else:
+				await h.close_wallet('source')
+				bn = os.path.basename(self.get_wallet_fn(self.dest))
+				h2 = self.rpc(self,self.dest)
+				await h2.open_wallet('destination')
+				accts_data = (await h2.get_accts())[0]
+
+				if keypress_confirm(f'\nCreate new account for wallet {bn!r}?'):
+					new_addr = await h2.create_acct()
+					await h2.get_accts()
+				elif keypress_confirm(f'Sweep to last existing account of wallet {bn!r}?'):
+					new_addr = h2.get_last_acct(accts_data)
+				else:
+					die(1,'Exiting at user request')
+
+				await h2.close_wallet('destination')
+				await h.open_wallet('source')
+
+			msg('\nCreating sweep transaction: balance of wallet {}, account #{} => {}'.format(
+				self.source.idx,
+				self.account,
+				cyan(new_addr),
+			))
+			sweep_tx = await h.make_sweep_tx(self.account,new_addr)
+
+			if keypress_confirm('Relay sweep transaction?'):
+				w_desc = 'source'
+				if uarg.tx_relay_daemon:
+					await h.close_wallet('source')
+					self.c = self.c2
+					h = self.rpc(self,self.source)
+					w_desc = 'TX relay source'
+					await h.open_wallet(w_desc)
+				msg(f'\n    Relaying sweep transaction...')
+				await h.relay_sweep_tx( sweep_tx['tx_metadata_list'][0] )
+				await h.close_wallet(w_desc)
+
+				gmsg('\n\nAll done')
+			else:
+				await h.close_wallet('source')
+				die(1,'\nExiting at user request')
+
+			return True
+

+ 1 - 0
setup.py

@@ -148,6 +148,7 @@ setup(
 			'mmgen.txsign',
 			'mmgen.util',
 			'mmgen.wallet',
+			'mmgen.xmrwallet',
 
 			'mmgen.altcoins.__init__',