Browse Source

mmgen-tool xmrwallet: add 'daemon', 'tx_relay_daemon' opts

- If 'daemon' is specified, RPC wallet connects to this monerod instead of the
  default at localhost:18081

- If 'tx_relay_daemon' is specified, transactions are relayed via this daemon
  instead of the local one.  This option supports proxying via SOCKS (Tor)
The MMGen Project 3 years ago
parent
commit
0c42587978
2 changed files with 136 additions and 38 deletions
  1. 16 2
      mmgen/daemon.py
  2. 120 36
      mmgen/tool.py

+ 16 - 2
mmgen/daemon.py

@@ -230,6 +230,8 @@ class MoneroWalletDaemon(Daemon):
 			host           = None,
 			user           = None,
 			passwd         = None,
+			daemon_addr    = None,
+			proxy          = None,
 			rpc_port_shift = None ):
 
 		super().__init__()
@@ -245,7 +247,10 @@ class MoneroWalletDaemon(Daemon):
 		self.pidfile = os.path.join(self.datadir,id_str+'.pid')
 		self.logfile = os.path.join(self.datadir,id_str+'.log')
 
-		self.daemon_port = CoinDaemon('xmr',test_suite=test_suite).rpc_port
+		self.proxy = proxy
+		self.daemon_addr = daemon_addr
+		if not daemon_addr:
+			self.daemon_port = CoinDaemon('xmr',test_suite=test_suite).rpc_port
 
 		if self.platform == 'win':
 			self.use_pidfile = False
@@ -267,11 +272,20 @@ class MoneroWalletDaemon(Daemon):
 	def start_cmd(self):
 		cmd = [
 			'monero-wallet-rpc',
-			'--daemon-port={}'.format(self.daemon_port),
+			'--untrusted-daemon',
 			'--rpc-bind-port={}'.format(self.rpc_port),
 			'--wallet-dir='+self.wallet_dir,
 			'--log-file='+self.logfile,
 			'--rpc-login={}:{}'.format(self.user,self.passwd) ]
+
+		if self.daemon_addr:
+			cmd.append(f'--daemon-address={self.daemon_addr}')
+		else:
+			cmd.append(f'--daemon-port={self.daemon_port}')
+
+		if self.proxy:
+			cmd.append(f'--proxy={self.proxy}')
+
 		if self.platform == 'linux':
 			cmd += ['--pidfile={}'.format(self.pidfile)]
 			cmd += [] if 'no_daemonize' in self.flags else ['--detach']

+ 120 - 36
mmgen/tool.py

@@ -1024,11 +1024,20 @@ class MMGenToolCmdMonero(MMGenToolCmds):
 		wallets:             '(integer range or list, or sweep specifier)' = '',
 		start_wallet_daemon  = True,
 		stop_wallet_daemon   = True,
+		daemon:              'HOST:PORT' = '',
+		tx_relay_daemon:     'HOST:PORT[:PROXY_HOST:PROXY_PORT]' = '',
 	):
 
 		"""
 		perform various Monero wallet operations for addresses in XMR key-address file
 
+		  Requires a running monerod daemon.  Unless 'daemon' is specified, the daemon
+		  is assumed to be listening on localhost at the default RPC port.
+
+		  If 'tx_relay_daemon' is specified, the monerod daemon at HOST:PORT will be
+		  used to relay any created transactions.  PROXY_HOST:PROXY_PORT, if specified,
+		  may point to a Tor SOCKS proxy, in which case HOST may be a Tor onion address.
+
 		  Supported operations:
 
 		    create - create wallet for all or specified addresses in key-address file
@@ -1061,6 +1070,7 @@ class MMGenToolCmdMonero(MMGenToolCmds):
 			class base:
 
 				wallet_exists = True
+				tx_relay = False
 
 				def __init__(self):
 
@@ -1087,7 +1097,8 @@ class MMGenToolCmdMonero(MMGenToolCmds):
 					from .daemon import MoneroWalletDaemon
 					self.wd = MoneroWalletDaemon(
 						wallet_dir = opt.outdir or '.',
-						test_suite = g.test_suite
+						test_suite = g.test_suite,
+						daemon_addr = daemon or None,
 					)
 
 					if start_wallet_daemon:
@@ -1115,6 +1126,8 @@ class MMGenToolCmdMonero(MMGenToolCmds):
 				def stop_daemons(self):
 					if stop_wallet_daemon:
 						self.wd.stop()
+						if tx_relay_daemon:
+							self.wd2.stop()
 
 				def post_init(self): pass
 				def post_process(self): pass
@@ -1218,12 +1231,9 @@ class MMGenToolCmdMonero(MMGenToolCmds):
 					return True
 
 				def post_init(self):
-					from .daemon import CoinDaemon
-					md = CoinDaemon(network_id='xmr',test_suite=g.test_suite)
-					host,port = (md.host,md.rpc_port)
-
+					host,port = daemon.split(':') if daemon else ('localhost',self.wd.daemon_port)
 					from .rpc import MoneroRPCClient
-					self.dc = MoneroRPCClient(host=host, port=port, user=None, passwd=None)
+					self.dc = MoneroRPCClient(host=host, port=int(port), user=None, passwd=None)
 					self.accts_data = {}
 
 				def post_process(self):
@@ -1252,6 +1262,7 @@ class MMGenToolCmdMonero(MMGenToolCmds):
 				name    = 'sweep'
 				desc    = 'Sweep'
 				past    = 'swept'
+				tx_relay = True
 
 				def create_addr_data(self):
 					m = re.match('(\d+):(\d+)(?:,(\d+))?$',wallets,re.ASCII)
@@ -1281,6 +1292,31 @@ class MMGenToolCmdMonero(MMGenToolCmds):
 					self.addr_data = list(gen())
 					self.account = int(m[2])
 
+				def post_init(self):
+
+					if tx_relay_daemon:
+						m = re.fullmatch(hostproxy_pat,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 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
@@ -1306,31 +1342,48 @@ class MMGenToolCmdMonero(MMGenToolCmds):
 							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))
-						h = xmr_rpc_methods(self,self.dest)
-						await h.open_wallet('destination')
-						accts_data = (await h.get_accts())[0]
+						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 h.create_acct()
-							await h.get_accts()
+							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 = h.get_last_acct(accts_data)
+							new_addr = h2.get_last_acct(accts_data)
 						else:
 							die(1,'Exiting at user request')
 
-						h = xmr_rpc_methods(self,self.source)
+						await h2.close_wallet('destination')
 						await h.open_wallet('source')
 
-					if keypress_confirm(
-						'\nSweep balance of wallet {}, account #{} to {}?'.format(
-							self.source.idx,
-							self.account,
-							cyan(new_addr),
-						)):
-						await h.do_sweep(self.account,new_addr)
+					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 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:
-						die(1,'Exiting at user request')
+						await h.close_wallet('source')
+						die(1,'\nExiting at user request')
+
+					return True
 
 		class xmr_rpc_methods:
 
@@ -1348,6 +1401,11 @@ class MMGenToolCmdMonero(MMGenToolCmds):
 					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)}:')
@@ -1425,26 +1483,36 @@ class MMGenToolCmdMonero(MMGenToolCmds):
 				msg('      ' + cyan(ret))
 				return ret
 
-			async def do_sweep(self,account,addr):
-				msg(f'\n    Sweeping account balance...')
-				ret = { # debug
-					'amount_list': [322222330000],
-					'fee_list': [10600000],
-					'tx_hash_list': ['deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'],
-				}
+			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
 				)
-				from .obj import CoinTxID
-				msg('    TxID:   {}\n    Amount: {}\n    Fee:    {}'.format(
-					CoinTxID(ret['tx_hash_list'][0]).hl(),
-					hlXMRamt(ret['amount_list'][0]),
-					hlXMRamt(ret['fee_list'][0]),
-				))
+				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)
+
 		def fmtXMRamt(amt):
 			from .obj import XMRAmt
 			return XMRAmt(amt,from_unit='min_coin_unit').fmt(fs='5.12',color=True)
@@ -1453,7 +1521,13 @@ class MMGenToolCmdMonero(MMGenToolCmds):
 			from .obj import XMRAmt
 			return XMRAmt(amt,from_unit='min_coin_unit').hl()
 
-		def check_args():
+		def check_args(localvars):
+
+			def check_host_arg(arg_name,pat):
+				val = localvars[arg_name]
+				if not re.fullmatch(pat,val,re.ASCII):
+					annot = MMGenToolCmdMonero.xmrwallet.__annotations__[arg_name]
+					die(1,f'{val!r}: invalid {arg_name!r} parameter: it must have format {annot!r}')
 
 			if blockheight < 0:
 				die(1,f"{blockheight}: invalid 'blockheight' arg (<0)")
@@ -1464,8 +1538,18 @@ class MMGenToolCmdMonero(MMGenToolCmds):
 			if op == 'sync' and blockheight != 0:
 				die(1,'Sync operation does not support blockheight arg')
 
+			if daemon:
+				check_host_arg('daemon',host_pat)
+
+			if tx_relay_daemon:
+				if not getattr(MoneroWalletOps,op).tx_relay:
+					die(1,f"'tx_relay_daemon' arg is not recognized for operation {op!r}")
+				check_host_arg('tx_relay_daemon',hostproxy_pat)
+
 		# start execution
-		check_args()
+		host_pat = r'(?:[^:]+):(?:\d+)'
+		hostproxy_pat = r'({p})(?::({p}))?'.format(p=host_pat)
+		check_args(locals())
 
 		m = getattr(MoneroWalletOps,op)()