From 4f631a1068517fbc1c21b8a85c45c6e3468c2d6c Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Fri, 11 Jun 2021 13:20:51 +0000 Subject: [PATCH] mmgen-xmrwallet: new `transfer` operation --- mmgen/main_xmrwallet.py | 30 ++++++++++++++------ mmgen/xmrwallet.py | 51 ++++++++++++++++++++++++++++------ test/test_py_d/ts_xmrwallet.py | 47 ++++++++++++++++++++++++++++--- 3 files changed, 107 insertions(+), 21 deletions(-) diff --git a/mmgen/main_xmrwallet.py b/mmgen/main_xmrwallet.py index 359a6abb..f76eb12b 100755 --- a/mmgen/main_xmrwallet.py +++ b/mmgen/main_xmrwallet.py @@ -29,9 +29,10 @@ opts_data = { 'desc': """Perform various Monero wallet operations for addresses in an MMGen XMR key-address file""", 'usage2': [ - '[opts] create [wallets]', - '[opts] sync [wallets]', - '[opts] sweep ', + '[opts] create [wallets]', + '[opts] sync [wallets]', + '[opts] transfer ', + '[opts] sweep ', ], 'options': """ -h, --help Print this help message @@ -61,10 +62,12 @@ may point to a 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 -sync - sync wallet for all or specified addresses in key-address file -sweep - sweep funds in specified wallet:account to new address in same - account or new account in another wallet +create - create wallet for all or specified addresses in key-address file +sync - sync wallet for all or specified addresses in key-address file +transfer - transfer specified XMR amount to specified address from specified + wallet:account +sweep - sweep funds in specified wallet:account to new address in same + account or new account in another wallet CREATE AND SYNC OPERATION NOTES @@ -75,6 +78,17 @@ key-address file, each corresponding to a Monero wallet to be created or synced. If omitted, all wallets are operated upon. + TRANSFER OPERATION NOTES + +The transfer operation takes a `transfer specifier` arg with the following +format: + + SOURCE:ACCOUNT:ADDRESS,AMOUNT + +where SOURCE is a wallet index; ACCOUNT the source account index; and ADDRESS +and AMOUNT the destination Monero address and XMR amount, respectively. + + SWEEP OPERATION NOTES The sweep operation takes a `sweep specifier` arg with the following format: @@ -126,7 +140,7 @@ if op in ('create','sync'): opts.usage() if cmd_args: wallets = cmd_args[0] -elif op == 'sweep': +elif op in ('transfer','sweep'): if len(cmd_args) != 1: opts.usage() spec = cmd_args[0] diff --git a/mmgen/xmrwallet.py b/mmgen/xmrwallet.py index ff6ccf17..60275848 100755 --- a/mmgen/xmrwallet.py +++ b/mmgen/xmrwallet.py @@ -26,11 +26,13 @@ from .common import * from .addr import KeyAddrList,AddrIdxList from .rpc import MoneroRPCClientRaw, MoneroWalletRPCClient from .daemon import MoneroWalletDaemon +from .protocol import _b58a 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)), + 'transfer_spec': e('SOURCE_WALLET_NUM:ACCOUNT:ADDRESS,AMOUNT', rf'(\d+):(\d+):([{_b58a}]+),([0-9.]+)'), 'sweep_spec': e('SOURCE_WALLET_NUM:ACCOUNT[,DEST_WALLET_NUM]', r'(\d+):(\d+)(?:,(\d+))?'), })( namedtuple('uarg_info_entry',['annot','pat']), @@ -39,7 +41,7 @@ xmrwallet_uarg_info = ( class MoneroWalletOps: - ops = ('create','sync','sweep') + ops = ('create','sync','transfer','sweep') class base: @@ -155,6 +157,20 @@ class MoneroWalletOps: hl_amt(fee), )) + async def make_transfer_tx(self,account,addr,amt): + res = await self.c.call( + 'transfer', + account_index = account, + destinations = [{ + 'amount': amt.to_unit('atomic'), + 'address': addr + }], + do_not_relay = True, + get_tx_metadata = True + ) + self.display_tx( res['tx_hash'], res['amount'], res['fee'] ) + return res['tx_metadata'] + async def make_sweep_tx(self,account,addr): res = await self.c.call( 'sweep_all', @@ -438,6 +454,11 @@ class MoneroWalletOps: self.addr_data = list(gen()) self.account = int(m[2]) + if self.name == 'transfer': + from mmgen.obj import CoinAddr + self.dest_addr = CoinAddr(self.proto,m[3]) + self.amount = self.proto.coin_amt(m[4]) + def post_init(self): if uarg.tx_relay_daemon: @@ -467,7 +488,8 @@ class MoneroWalletOps: async def process_wallets(self): gmsg(f'\n{self.desc}ing account #{self.account} of wallet {self.source.idx}' + ( - ' to new address' if self.dest is None + f': {self.amount} XMR to {self.dest_addr}' if self.name == 'transfer' + else ' to new address' if self.dest == None else f' to new account in wallet {self.dest.idx}' )) h = self.rpc(self,self.source) @@ -481,7 +503,9 @@ class MoneroWalletOps: await h.get_addrs(accts_data,self.account) - if self.dest == None: + if self.name == 'transfer': + new_addr = self.dest_addr + elif 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}?'): @@ -507,12 +531,14 @@ class MoneroWalletOps: await h2.close_wallet('destination') await h.open_wallet('source') - msg('\n Creating sweep transaction: balance of wallet {}, account #{} => {}'.format( - self.source.idx, - self.account, - cyan(new_addr), - )) - tx_metadata = await h.make_sweep_tx(self.account,new_addr) + msg_r(f'\n Creating {self.name} transaction: wallet {self.source.idx}, account #{self.account}') + + if self.name == 'transfer': + msg(f', {self.amount} XMR => {cyan(new_addr)}') + tx_metadata = await h.make_transfer_tx(self.account,new_addr,self.amount) + elif self.name == 'sweep': + msg(f' => {cyan(new_addr)}') + tx_metadata = await h.make_sweep_tx(self.account,new_addr) if keypress_confirm(f'\nRelay {self.name} transaction?'): w_desc = 'source' @@ -533,3 +559,10 @@ class MoneroWalletOps: die(1,'\nExiting at user request') return True + + class transfer(sweep): + name = 'transfer' + desc = 'Transfer' + past = 'transferred' + spec_id = 'transfer_spec' + spec_key = ( (1,'source'), ) diff --git a/test/test_py_d/ts_xmrwallet.py b/test/test_py_d/ts_xmrwallet.py index e831807d..8ac6cdb8 100755 --- a/test/test_py_d/ts_xmrwallet.py +++ b/test/test_py_d/ts_xmrwallet.py @@ -61,6 +61,13 @@ class TestSuiteXMRWallet(TestSuiteBase): ('sweep_to_address_noproxy', 'sweeping to new address (via TX relay)'), ('mine_blocks', 'mining blocks'), + + ('transfer_to_miner_proxy', 'transferring funds to Miner (via TX relay + proxy)'), + ('mine_blocks_extra', 'mining blocks'), + ('sync_wallet_2', 'syncing Alice’s wallet #2'), + + ('transfer_to_miner_noproxy', 'transferring funds to Miner (via TX relay)'), + ('mine_blocks', 'mining blocks'), ) def __init__(self,trunner,cfgs,spawn): @@ -197,7 +204,7 @@ class TestSuiteXMRWallet(TestSuiteBase): 'wd_rpc', ]) for user,sid,shift,kal_range in ( # kal_range must be None, a single digit, or a single hyphenated range - ('miner', '98831F3A', 130, '1'), + ('miner', '98831F3A', 130, '1-2'), ('bob', '1378FC64', 140, None), ('alice', 'FE3C6545', 150, '1-4'), ): @@ -320,6 +327,9 @@ class TestSuiteXMRWallet(TestSuiteBase): def sync_wallets_selected(self): return self.sync_wallets(wallets='1,3-4') + def sync_wallet_2(self): + return self.sync_wallets(wallets='2') + def sync_wallets(self,wallets=None): data = self.users['alice'] dir_opt = [f'--outdir={data.udir}'] @@ -376,6 +386,19 @@ class TestSuiteXMRWallet(TestSuiteBase): self.set_dest('alice',2,1,lambda x: x > 1,'unlocked balance > 1') return ret + def transfer_to_miner_proxy(self): + addr = read_from_file(self.users['miner'].addrfile_fs.format(2)) + amt = '0.135' + ret = self.do_op('transfer','alice',f'2:1:{addr},{amt}',self.tx_relay_daemon_proxy_parm) + self.set_dest('miner',2,0,lambda x: str(x) == amt,f'unlocked balance == {amt}') + return ret + + def transfer_to_miner_noproxy(self): + addr = read_from_file(self.users['miner'].addrfile_fs.format(2)) + ret = self.do_op('transfer','alice',f'2:1:{addr},0.0995',self.tx_relay_daemon_parm) + self.set_dest('miner',2,0,lambda x: str(x) == '0.2345','unlocked balance == 0.2345') + return ret + # wallet methods async def open_wallet_user(self,user,wnum): @@ -420,7 +443,7 @@ class TestSuiteXMRWallet(TestSuiteBase): ret = await self.users['miner'].md_rpc.call('stop_mining') return self.get_status(ret) - async def mine_blocks(self,random_txs=None): + async def mine_blocks(self,random_txs=None,extra_blocks=None): """ - open destination wallet - optionally create and broadcast random TXs @@ -447,6 +470,18 @@ class TestSuiteXMRWallet(TestSuiteBase): else: die(2,'Restart attempt limit exceeded') + async def mine_extra_blocks(): + h_start = await get_height() + imsg_r(f'[+{extra_blocks} blocks]: ') + oqmsg_r('|') + while True: + await asyncio.sleep(2) + h = await get_height() + imsg_r(f'{h} ') + oqmsg_r('+') + if h - h_start > extra_blocks: + break + async def send_random_txs(): from mmgen.tool import tool_api t = tool_api() @@ -480,8 +515,7 @@ class TestSuiteXMRWallet(TestSuiteBase): self.do_msg(extra_desc=f'+{random_txs} random TXs' if random_txs else None) - if self.dest.user != 'miner': - await self.open_wallet_user(self.dest.user,self.dest.wnum) + await self.open_wallet_user(self.dest.user,self.dest.wnum) if random_txs: await send_random_txs() @@ -494,6 +528,8 @@ class TestSuiteXMRWallet(TestSuiteBase): while True: ub = await get_balance(self.dest) if self.dest.test(ub): + if extra_blocks: + await mine_extra_blocks() imsg('') oqmsg_r('+') print_balance(self.dest,ub) @@ -515,6 +551,9 @@ class TestSuiteXMRWallet(TestSuiteBase): async def mine_blocks_tx(self): return await self.mine_blocks(random_txs=self.dfl_random_txs) + async def mine_blocks_extra(self): + return await self.mine_blocks(extra_blocks=100) # TODO: 100 is arbitrary value + # util methods def get_status(self,ret):