From 627d30107696d9d9e99883e09064a0e11d391408 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Tue, 20 Apr 2021 19:36:22 +0000 Subject: [PATCH] mmgen-tool xmrwallet: new 'sweep' command --- mmgen/tool.py | 215 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 213 insertions(+), 2 deletions(-) diff --git a/mmgen/tool.py b/mmgen/tool.py index d8ed76d5..11af7773 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -1020,7 +1020,7 @@ class MMGenToolCmdMonero(MMGenToolCmds): op: str, xmr_keyaddrfile: str, blockheight: '(default: current height)' = 0, - wallets: '(integer range or list)' = '', + wallets: '(integer range or list, or sweep specifier)' = '', start_wallet_daemon = True, stop_wallet_daemon = True, monerod_args = '', @@ -1033,11 +1033,30 @@ class MMGenToolCmdMonero(MMGenToolCmds): 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 + + SWEEP OPERATION NOTES + + For the sweep operation, the parameter to the 'wallets' arg has a different + format, known as a 'sweep specifier': + + SOURCE:ACCOUNT[,DEST] + + where SOURCE and DEST are wallet numbers and ACCOUNT an account index. + + If DEST is omitted, a new address will be created in ACCOUNT of SOURCE and + all funds from ACCOUNT of SOURCE will be swept into it. + + If DEST is included, a new account will be created in DEST and all funds + from ACCOUNT of SOURCE will be swept into the new account. + + The user is prompted before addresses are created or funds are transferred. """ class MoneroWalletOps: - ops = ('create','sync') + ops = ('create','sync','sweep') class base: @@ -1234,6 +1253,198 @@ class MMGenToolCmdMonero(MMGenToolCmds): 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' + + def create_addr_data(self): + import re + m = re.match('(\d+):(\d+)(?:,(\d+))?$',wallets,re.ASCII) + if not m: + die(1, + "{!r}: invalid 'wallets' arg: for sweep operation, it must have format {}".format( + wallets, + 'SOURCE:ACCOUNT[,DEST]' + )) + + 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]) + + 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() + + 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: + 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() + + if keypress_confirm(f'\nCreate new account for wallet {bn!r}?'): + new_addr = await h.create_acct() + await h.get_accts() + elif keypress_confirm(f'Sweep to last existing account of wallet {bn!r}?'): + new_addr = h.get_last_acct(accts_data) + else: + die(1,'Exiting at user request') + + h = xmr_rpc_methods(self,self.source) + 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) + else: + die(1,'Exiting at user request') + + 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') + + def print_accts(self,data,indent=' '): + d = data['subaddress_accounts'] + msg('\n' + indent + f'Accounts of wallet {os.path.basename(self.fn)}:') + fs = indent + ' {:6} {:18} {:%s} {}' % max(len(e['label']) for e in d) + msg(fs.format('Index ','Base Address','Label','Balance')) + for e in d: + msg(fs.format( + str(e['account_index']), + e['base_address'][:15] + '...', + e['label'], + fmtXMRamt(e['balance']), + )) + + async def get_accts(self): + data = await self.c.call('get_accounts') + self.print_accts(data) + return 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 + + 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'], + } + ret = await self.c.call( + 'sweep_all', + address = addr, + account_index = account, + ) + 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]), + )) + return ret + def fmtXMRamt(amt): from .obj import XMRAmt return XMRAmt(amt,from_unit='min_coin_unit').fmt(fs='5.12',color=True)