move MoneroWalletOps class to new xmrwallet module
This commit is contained in:
parent
9dc8efe161
commit
3484222185
3 changed files with 522 additions and 497 deletions
502
mmgen/tool.py
502
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:
|
||||
from .xmrwallet import MoneroWalletOps
|
||||
|
||||
ops = ('create','sync','sweep')
|
||||
|
||||
class base:
|
||||
|
||||
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
mmgen/xmrwallet.py
Executable file
516
mmgen/xmrwallet.py
Executable file
|
|
@ -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
setup.py
1
setup.py
|
|
@ -148,6 +148,7 @@ setup(
|
|||
'mmgen.txsign',
|
||||
'mmgen.util',
|
||||
'mmgen.wallet',
|
||||
'mmgen.xmrwallet',
|
||||
|
||||
'mmgen.altcoins.__init__',
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue