From 0c4258797821b708b7051e9b4aba284041f46ca8 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Fri, 7 May 2021 16:49:01 +0000 Subject: [PATCH] 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) --- mmgen/daemon.py | 18 +++++- mmgen/tool.py | 156 +++++++++++++++++++++++++++++++++++++----------- 2 files changed, 136 insertions(+), 38 deletions(-) diff --git a/mmgen/daemon.py b/mmgen/daemon.py index a64ea9d5..cfa2cbfb 100755 --- a/mmgen/daemon.py +++ b/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'] diff --git a/mmgen/tool.py b/mmgen/tool.py index 9ecc4ad4..67c5c2ee 100755 --- a/mmgen/tool.py +++ b/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)()