xmrwallet.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2021 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. xmrwallet.py - MoneroWalletOps class
  20. """
  21. import os,re,time,json
  22. from collections import namedtuple
  23. from .common import *
  24. from .addr import KeyAddrList,AddrIdxList
  25. from .rpc import MoneroRPCClientRaw,MoneroWalletRPCClient,json_encoder
  26. from .daemon import MoneroWalletDaemon
  27. from .protocol import _b58a,init_proto
  28. from .obj import CoinAddr,CoinTxID,SeedID,AddrIdx,Hilite,InitErrors
  29. xmrwallet_uarg_info = (
  30. lambda e,hp: {
  31. 'daemon': e('HOST:PORT', hp),
  32. 'tx_relay_daemon': e('HOST:PORT[:PROXY_HOST:PROXY_PORT]', r'({p})(?::({p}))?'.format(p=hp)),
  33. 'transfer_spec': e('SOURCE_WALLET_NUM:ACCOUNT:ADDRESS,AMOUNT', rf'(\d+):(\d+):([{_b58a}]+),([0-9.]+)'),
  34. 'sweep_spec': e('SOURCE_WALLET_NUM:ACCOUNT[,DEST_WALLET_NUM]', r'(\d+):(\d+)(?:,(\d+))?'),
  35. })(
  36. namedtuple('uarg_info_entry',['annot','pat']),
  37. r'(?:[^:]+):(?:\d+)'
  38. )
  39. class XMRWalletAddrSpec(str,Hilite,InitErrors,MMGenObject):
  40. color = 'cyan'
  41. width = 0
  42. trunc_ok = False
  43. min_len = 5 # 1:0:0
  44. max_len = 14 # 9999:9999:9999
  45. def __new__(cls,arg1,arg2=None,arg3=None):
  46. if type(arg1) == cls:
  47. return arg1
  48. try:
  49. if isinstance(arg1,str):
  50. me = str.__new__(cls,arg1)
  51. m = re.fullmatch('({n}):({n}):({n}|None)'.format(n=r'[0-9]{1,4}'),arg1)
  52. assert m is not None, f'{arg1!r}: invalid XMRWalletAddrSpec'
  53. for e in m.groups():
  54. if len(e) != 1 and e[0] == '0':
  55. die(2,f'{e}: leading zeroes not permitted in XMRWalletAddrSpec element')
  56. me.wallet = AddrIdx(m[1])
  57. me.account = int(m[2])
  58. me.account_address = None if m[3] == 'None' else int(m[3])
  59. else:
  60. me = str.__new__(cls,f'{arg1}:{arg2}:{arg3}')
  61. for arg in [arg1,arg2] + ([] if arg3 is None else [arg3]):
  62. assert isinstance(arg,int), f'{arg}: XMRWalletAddrSpec component not of type int'
  63. assert arg is None or arg <= 9999, f'{arg}: XMRWalletAddrSpec component greater than 9999'
  64. me.wallet = AddrIdx(arg1)
  65. me.account = arg2
  66. me.account_address = arg3
  67. return me
  68. except Exception as e:
  69. return cls.init_fail(e,me)
  70. class MoneroMMGenTX:
  71. class Base:
  72. def make_chksum(self,keys=None):
  73. res = json.dumps(
  74. dict( (k,v) for k,v in self.data._asdict().items() if (not keys or k in keys) ),
  75. cls = json_encoder
  76. )
  77. return make_chksum_6(res)
  78. @property
  79. def base_chksum(self):
  80. return self.make_chksum(
  81. ('op','create_time','network','seed_id','source','dest','amount')
  82. )
  83. @property
  84. def full_chksum(self):
  85. return self.make_chksum(set(self.data._fields) - {'metadata'})
  86. xmrwallet_tx_data = namedtuple('xmrwallet_tx_data',[
  87. 'op',
  88. 'create_time',
  89. 'sign_time',
  90. 'network',
  91. 'seed_id',
  92. 'source',
  93. 'dest',
  94. 'dest_address',
  95. 'txid',
  96. 'amount',
  97. 'fee',
  98. 'blob',
  99. 'metadata',
  100. ])
  101. def get_info(self,indent=''):
  102. d = self.data
  103. if d.dest:
  104. to_entry = f'\n{indent} To: ' + (
  105. 'Wallet {}, account {}, address {}'.format(
  106. d.dest.wallet.hl(),
  107. red(f'#{d.dest.account}'),
  108. red(f'#{d.dest.account_address}')
  109. )
  110. )
  111. return fmt("""
  112. Transaction info [Seed ID: {}. Network: {}]:
  113. TxID: {}
  114. Type: {}
  115. From: Wallet {}, account {}{}
  116. Amt: {} XMR
  117. Fee: {} XMR
  118. Dest: {}
  119. """,strip_char='\t',indent=indent).format(
  120. d.seed_id.hl(), d.network.upper(),
  121. d.txid.hl(),
  122. blue(capfirst(d.op)),
  123. d.source.wallet.hl(),
  124. red(f'#{d.source.account}'),
  125. to_entry if d.dest else '',
  126. d.amount.hl(),
  127. d.fee.hl(),
  128. d.dest_address.hl()
  129. )
  130. def write(self,delete_metadata=False):
  131. dict_data = self.data._asdict()
  132. if delete_metadata:
  133. dict_data['metadata'] = None
  134. out = json.dumps(
  135. { 'MoneroMMGenTX': {
  136. 'base_chksum': self.base_chksum,
  137. 'full_chksum': self.full_chksum,
  138. 'data': dict_data,
  139. }
  140. },
  141. cls = json_encoder,
  142. )
  143. fn = '{}{}-XMR[{!s}]{}.sigtx'.format(
  144. self.base_chksum.upper(),
  145. (lambda s: f'-{s.upper()}' if s else '')(self.full_chksum),
  146. self.data.amount,
  147. (lambda s: '' if s == 'mainnet' else f'.{s}')(self.data.network),
  148. )
  149. write_data_to_file(fn,out,desc='MoneroMMGenTX data',ask_write=True,ask_write_default_yes=False)
  150. class NewSigned(Base):
  151. def __init__(self,*args,**kwargs):
  152. assert not args, 'Non-keyword args not permitted'
  153. d = namedtuple('kwargs_tuple',kwargs)(**kwargs)
  154. proto = init_proto('xmr',network=d.network)
  155. now = int(time.time())
  156. self.data = self.xmrwallet_tx_data(
  157. op = d.op,
  158. create_time = now,
  159. sign_time = now,
  160. network = d.network,
  161. seed_id = SeedID(sid=d.seed_id),
  162. source = XMRWalletAddrSpec(d.source),
  163. dest = None if d.dest is None else XMRWalletAddrSpec(d.dest),
  164. dest_address = CoinAddr(proto,d.dest_address),
  165. txid = CoinTxID(d.txid),
  166. amount = proto.coin_amt(d.amount,from_unit='atomic'),
  167. fee = proto.coin_amt(d.fee,from_unit='atomic'),
  168. blob = d.blob,
  169. metadata = d.metadata,
  170. )
  171. class Signed(Base):
  172. def __init__(self,fn):
  173. self.fn = fn
  174. d_wrap = json.loads(get_data_from_file(fn))['MoneroMMGenTX']
  175. d = self.xmrwallet_tx_data(**d_wrap['data'])
  176. proto = init_proto('xmr',network=d.network)
  177. self.data = self.xmrwallet_tx_data(
  178. op = d.op,
  179. create_time = d.create_time,
  180. sign_time = d.sign_time,
  181. network = d.network,
  182. seed_id = SeedID(sid=d.seed_id),
  183. source = XMRWalletAddrSpec(d.source),
  184. dest = None if d.dest is None else XMRWalletAddrSpec(d.dest),
  185. dest_address = CoinAddr(proto,d.dest_address),
  186. txid = CoinTxID(d.txid),
  187. amount = proto.coin_amt(d.amount),
  188. fee = proto.coin_amt(d.fee),
  189. blob = d.blob,
  190. metadata = d.metadata,
  191. )
  192. for k in ('base_chksum','full_chksum'):
  193. a = getattr(self,k)
  194. b = d_wrap[k]
  195. assert a == b, f'{k} mismatch: {a} != {b}'
  196. class MoneroWalletOps:
  197. ops = ('create','sync','transfer','sweep','relay')
  198. opts = (
  199. 'wallet_dir',
  200. 'daemon',
  201. 'tx_relay_daemon',
  202. 'use_internal_keccak_module',
  203. 'hash_preset',
  204. 'restore_height',
  205. 'no_start_wallet_daemon',
  206. 'no_stop_wallet_daemon',
  207. 'do_not_relay',
  208. )
  209. pat_opts = ('daemon','tx_relay_daemon')
  210. class base(MMGenObject):
  211. opts = ('wallet_dir',)
  212. def __init__(self,uarg_tuple,uopt_tuple):
  213. def gen_classes():
  214. for cls in type(self).__mro__:
  215. yield cls
  216. if cls.__name__ == 'base':
  217. break
  218. classes = tuple(gen_classes())
  219. self.opts = tuple(set(opt for cls in classes for opt in cls.opts))
  220. global uarg, uopt, uarg_info, fmt_amt, hl_amt
  221. uarg = uarg_tuple
  222. uopt = uopt_tuple
  223. uarg_info = xmrwallet_uarg_info
  224. def fmt_amt(amt):
  225. return self.proto.coin_amt(amt,from_unit='min_coin_unit').fmt(fs='5.12',color=True)
  226. def hl_amt(amt):
  227. return self.proto.coin_amt(amt,from_unit='min_coin_unit').hl()
  228. id_cur = None
  229. for cls in classes:
  230. if id(cls.check_uopts) != id_cur:
  231. cls.check_uopts(self)
  232. id_cur = id(cls.check_uopts)
  233. self.proto = init_proto('xmr',testnet=g.testnet)
  234. def check_uopts(self):
  235. def check_pat_opt(name):
  236. val = getattr(uopt,name)
  237. if not re.fullmatch(uarg_info[name].pat,val,re.ASCII):
  238. die(1,'{!r}: invalid value for --{}: it must have format {!r}'.format(
  239. val,
  240. name.replace('_','-'),
  241. uarg_info[name].annot
  242. ))
  243. for opt in uopt._asdict():
  244. if getattr(uopt,opt) and not opt in self.opts:
  245. die(1,'Option --{} not supported for {!r} operation'.format(
  246. opt.replace('_','-'),
  247. uarg.op
  248. ))
  249. for opt in MoneroWalletOps.pat_opts:
  250. if getattr(uopt,opt):
  251. check_pat_opt(opt)
  252. def display_tx_relay_info(self,indent=''):
  253. m = re.fullmatch(uarg_info['tx_relay_daemon'].pat,uopt.tx_relay_daemon,re.ASCII)
  254. msg(fmt(f"""
  255. TX relay info:
  256. Host: {blue(m[1])}
  257. Proxy: {blue(m[2] or 'None')}
  258. """,strip_char='\t',indent=indent))
  259. def post_main(self): pass
  260. def stop_daemons(self): pass
  261. class wallet(base):
  262. opts = (
  263. 'use_internal_keccak_module',
  264. 'hash_preset',
  265. 'daemon',
  266. 'no_start_wallet_daemon',
  267. 'no_stop_wallet_daemon',
  268. )
  269. wallet_exists = True
  270. def __init__(self,uarg_tuple,uopt_tuple):
  271. def wallet_exists(fn):
  272. try: os.stat(fn)
  273. except: return False
  274. else: return True
  275. def check_wallets():
  276. for d in self.addr_data:
  277. fn = self.get_wallet_fn(d)
  278. exists = wallet_exists(fn)
  279. if exists and not self.wallet_exists:
  280. die(1,f'Wallet {fn!r} already exists!')
  281. elif not exists and self.wallet_exists:
  282. die(1,f'Wallet {fn!r} not found!')
  283. super().__init__(uarg_tuple,uopt_tuple)
  284. self.kal = KeyAddrList(self.proto,uarg.infile)
  285. self.create_addr_data()
  286. check_wallets()
  287. self.wd = MoneroWalletDaemon(
  288. proto = self.proto,
  289. wallet_dir = uopt.wallet_dir or '.',
  290. test_suite = g.test_suite,
  291. daemon_addr = uopt.daemon or None,
  292. )
  293. if not uopt.no_start_wallet_daemon:
  294. self.wd.restart()
  295. self.c = MoneroWalletRPCClient(
  296. host = self.wd.host,
  297. port = self.wd.rpc_port,
  298. user = self.wd.user,
  299. passwd = self.wd.passwd
  300. )
  301. def create_addr_data(self):
  302. if uarg.wallets:
  303. idxs = AddrIdxList(uarg.wallets)
  304. self.addr_data = [d for d in self.kal.data if d.idx in idxs]
  305. if len(self.addr_data) != len(idxs):
  306. die(1,f'List {uarg.wallets!r} contains addresses not present in supplied key-address file')
  307. else:
  308. self.addr_data = self.kal.data
  309. def stop_daemons(self):
  310. if not uopt.no_stop_wallet_daemon:
  311. self.wd.stop()
  312. if uopt.tx_relay_daemon and hasattr(self,'wd2'):
  313. self.wd2.stop()
  314. def get_wallet_fn(self,d):
  315. return os.path.join(
  316. uopt.wallet_dir or '.','{}-{}-MoneroWallet{}{}'.format(
  317. self.kal.al_id.sid,
  318. d.idx,
  319. '.testnet' if g.testnet else '',
  320. '-α' if g.debug_utf8 else '' ))
  321. async def main(self):
  322. gmsg('\n{}ing {} wallet{}'.format(self.desc,len(self.addr_data),suf(self.addr_data)))
  323. processed = 0
  324. for n,d in enumerate(self.addr_data): # [d.sec,d.addr,d.wallet_passwd,d.viewkey]
  325. fn = self.get_wallet_fn(d)
  326. gmsg('\n{}ing wallet {}/{} ({})'.format(
  327. self.desc,
  328. n+1,
  329. len(self.addr_data),
  330. os.path.basename(fn),
  331. ))
  332. processed += await self.process_wallet(d,fn)
  333. gmsg('\n{} wallet{} {}'.format(processed,suf(processed),self.past))
  334. return processed
  335. class rpc:
  336. def __init__(self,parent,d):
  337. self.parent = parent
  338. self.c = parent.c
  339. self.d = d
  340. self.fn = parent.get_wallet_fn(d)
  341. async def open_wallet(self,desc,refresh=True):
  342. gmsg_r(f'\n Opening {desc} wallet...')
  343. await self.c.call( # returns {}
  344. 'open_wallet',
  345. filename=os.path.basename(self.fn),
  346. password=self.d.wallet_passwd )
  347. gmsg('done')
  348. if refresh:
  349. gmsg_r(f' Refreshing {desc} wallet...')
  350. ret = await self.c.call('refresh')
  351. gmsg('done')
  352. if ret['received_money']:
  353. msg(' Wallet has received funds')
  354. async def close_wallet(self,desc):
  355. gmsg_r(f'\n Closing {desc} wallet...')
  356. await self.c.call('close_wallet')
  357. gmsg_r('done')
  358. def print_accts(self,data,addrs_data,indent=' '):
  359. d = data['subaddress_accounts']
  360. msg('\n' + indent + f'Accounts of wallet {os.path.basename(self.fn)}:')
  361. fs = indent + ' {:6} {:18} {:<6} {:%s} {}' % max(len(e['label']) for e in d)
  362. msg(fs.format('Index ','Base Address','nAddrs','Label','Unlocked Balance'))
  363. for i,e in enumerate(d):
  364. msg(fs.format(
  365. str(e['account_index']),
  366. e['base_address'][:15] + '...',
  367. len(addrs_data[i]['addresses']),
  368. e['label'],
  369. fmt_amt(e['unlocked_balance']),
  370. ))
  371. async def get_accts(self,print=True):
  372. data = await self.c.call('get_accounts')
  373. addrs_data = [
  374. await self.c.call('get_address',account_index=i)
  375. for i in range(len(data['subaddress_accounts']))
  376. ]
  377. if print:
  378. self.print_accts(data,addrs_data)
  379. return ( data, addrs_data )
  380. async def create_acct(self):
  381. msg('\n Creating new account...')
  382. ret = await self.c.call(
  383. 'create_account',
  384. label = f'Sweep from {self.parent.source.idx}:{self.parent.account}'
  385. )
  386. msg(' Index: {}'.format( pink(str(ret['account_index'])) ))
  387. msg(' Address: {}'.format( cyan(ret['address']) ))
  388. return (ret['account_index'], ret['address'])
  389. def get_last_acct(self,accts_data):
  390. msg('\n Getting last account...')
  391. ret = accts_data['subaddress_accounts'][-1]
  392. msg(' Index: {}'.format( pink(str(ret['account_index'])) ))
  393. msg(' Address: {}'.format( cyan(ret['base_address']) ))
  394. return (ret['account_index'], ret['base_address'])
  395. async def print_addrs(self,accts_data,account):
  396. ret = await self.c.call('get_address',account_index=account)
  397. d = ret['addresses']
  398. msg('\n Addresses of account #{} ({}):'.format(
  399. account,
  400. accts_data['subaddress_accounts'][account]['label']))
  401. fs = ' {:6} {:18} {:%s} {}' % max(len(e['label']) for e in d)
  402. msg(fs.format('Index ','Address','Label','Used'))
  403. for e in d:
  404. msg(fs.format(
  405. str(e['address_index']),
  406. e['address'][:15] + '...',
  407. e['label'],
  408. e['used']
  409. ))
  410. async def create_new_addr(self,account):
  411. msg_r('\n Creating new address: ')
  412. ret = await self.c.call(
  413. 'create_address',
  414. account_index = account,
  415. label = 'Sweep from this account',
  416. )
  417. msg(cyan(ret['address']))
  418. return ret['address']
  419. async def get_last_addr(self,account,display=True):
  420. if display:
  421. msg('\n Getting last address:')
  422. ret = (await self.c.call(
  423. 'get_address',
  424. account_index = account,
  425. ))['addresses']
  426. addr = ret[-1]['address']
  427. if display:
  428. msg(' ' + cyan(addr))
  429. return ( addr, len(ret) - 1 )
  430. async def make_transfer_tx(self,account,addr,amt):
  431. res = await self.c.call(
  432. 'transfer',
  433. account_index = account,
  434. destinations = [{
  435. 'amount': amt.to_unit('atomic'),
  436. 'address': addr
  437. }],
  438. do_not_relay = True,
  439. get_tx_hex = True,
  440. get_tx_metadata = True
  441. )
  442. return MoneroMMGenTX.NewSigned(
  443. op = uarg.op,
  444. network = self.parent.proto.network,
  445. seed_id = self.parent.kal.al_id.sid,
  446. source = XMRWalletAddrSpec(self.parent.source.idx,self.parent.account,None),
  447. dest = None,
  448. dest_address = addr,
  449. txid = res['tx_hash'],
  450. amount = res['amount'],
  451. fee = res['fee'],
  452. blob = res['tx_blob'],
  453. metadata = res['tx_metadata'],
  454. )
  455. async def make_sweep_tx(self,account,dest_acct,dest_addr_idx,addr):
  456. res = await self.c.call(
  457. 'sweep_all',
  458. address = addr,
  459. account_index = account,
  460. do_not_relay = True,
  461. get_tx_hex = True,
  462. get_tx_metadata = True
  463. )
  464. if len(res['tx_hash_list']) > 1:
  465. die(3,'More than one TX required. Cannot perform this sweep')
  466. return MoneroMMGenTX.NewSigned(
  467. op = uarg.op,
  468. network = self.parent.proto.network,
  469. seed_id = self.parent.kal.al_id.sid,
  470. source = XMRWalletAddrSpec(self.parent.source.idx,self.parent.account,None),
  471. dest = XMRWalletAddrSpec(
  472. (self.parent.dest or self.parent.source).idx,
  473. dest_acct,
  474. dest_addr_idx),
  475. dest_address = addr,
  476. txid = res['tx_hash_list'][0],
  477. amount = res['amount_list'][0],
  478. fee = res['fee_list'][0],
  479. blob = res['tx_blob_list'][0],
  480. metadata = res['tx_metadata_list'][0],
  481. )
  482. async def relay_tx(self,tx_hex):
  483. ret = await self.c.call('relay_tx',hex=tx_hex)
  484. try:
  485. msg('\n Relayed {}'.format( CoinTxID(ret['tx_hash']).hl() ))
  486. except:
  487. msg(f'\n Server returned: {ret!s}')
  488. class create(wallet):
  489. name = 'create'
  490. desc = 'Creat'
  491. past = 'created'
  492. wallet_exists = False
  493. opts = ('restore_height',)
  494. def check_uopts(self):
  495. if int(uopt.restore_height) < 0:
  496. die(1,f"{uopt.restore_height}: invalid value for --restore-height (less than zero)")
  497. async def process_wallet(self,d,fn):
  498. msg_r('') # for pexpect
  499. from .baseconv import baseconv
  500. ret = await self.c.call(
  501. 'restore_deterministic_wallet',
  502. filename = os.path.basename(fn),
  503. password = d.wallet_passwd,
  504. seed = baseconv.fromhex(d.sec,'xmrseed',tostr=True),
  505. restore_height = uopt.restore_height,
  506. language = 'English' )
  507. pp_msg(ret) if opt.debug else msg(' Address: {}'.format(ret['address']))
  508. return True
  509. class sync(wallet):
  510. name = 'sync'
  511. desc = 'Sync'
  512. past = 'synced'
  513. opts = ('rescan_blockchain',)
  514. def __init__(self,uarg_tuple,uopt_tuple):
  515. super().__init__(uarg_tuple,uopt_tuple)
  516. host,port = uopt.daemon.split(':') if uopt.daemon else ('localhost',self.wd.daemon_port)
  517. self.dc = MoneroRPCClientRaw(host=host, port=int(port), user=None, passwd=None)
  518. self.accts_data = {}
  519. async def process_wallet(self,d,fn):
  520. chain_height = (await self.dc.call('get_height'))['height']
  521. msg(f' Chain height: {chain_height}')
  522. t_start = time.time()
  523. msg_r(' Opening wallet...')
  524. await self.c.call(
  525. 'open_wallet',
  526. filename=os.path.basename(fn),
  527. password=d.wallet_passwd )
  528. msg('done')
  529. msg_r(' Getting wallet height (be patient, this could take a long time)...')
  530. wallet_height = (await self.c.call('get_height'))['height']
  531. msg_r('\r' + ' '*68 + '\r')
  532. msg(f' Wallet height: {wallet_height} ')
  533. behind = chain_height - wallet_height
  534. if behind > 1000:
  535. msg_r(f' Wallet is {behind} blocks behind chain tip. Please be patient. Syncing...')
  536. ret = await self.c.call('refresh')
  537. if behind > 1000:
  538. msg('done')
  539. if ret['received_money']:
  540. msg(' Wallet has received funds')
  541. for i in range(2):
  542. wallet_height = (await self.c.call('get_height'))['height']
  543. if wallet_height >= chain_height:
  544. break
  545. ymsg(f' Wallet failed to sync (wallet height [{wallet_height}] < chain height [{chain_height}])')
  546. if i or not uopt.rescan_blockchain:
  547. break
  548. msg_r(' Rescanning blockchain, please be patient...')
  549. await self.c.call('rescan_blockchain')
  550. await self.c.call('refresh')
  551. msg('done')
  552. t_elapsed = int(time.time() - t_start)
  553. bn = os.path.basename(fn)
  554. a,b = await self.rpc(self,d).get_accts(print=False)
  555. msg(' Balance: {} Unlocked balance: {}'.format(
  556. hl_amt(a['total_balance']),
  557. hl_amt(a['total_unlocked_balance']),
  558. ))
  559. self.accts_data[bn] = { 'accts': a, 'addrs': b }
  560. msg(' Wallet height: {}'.format(wallet_height))
  561. msg(' Sync time: {:02}:{:02}'.format( t_elapsed//60, t_elapsed%60 ))
  562. await self.c.call('close_wallet')
  563. return wallet_height >= chain_height
  564. def post_main(self):
  565. d = self.accts_data
  566. for n,k in enumerate(d):
  567. ad = self.addr_data[n]
  568. self.rpc(self,ad).print_accts( d[k]['accts'], d[k]['addrs'], indent='')
  569. col1_w = max(map(len,d)) + 1
  570. fs = '{:%s} {} {}' % col1_w
  571. tbals = [0,0]
  572. msg('\n'+fs.format('Wallet','Balance ','Unlocked Balance'))
  573. for k in d:
  574. b = d[k]['accts']['total_balance']
  575. ub = d[k]['accts']['total_unlocked_balance']
  576. msg(fs.format( k + ':', fmt_amt(b), fmt_amt(ub) ))
  577. tbals[0] += b
  578. tbals[1] += ub
  579. msg(fs.format( '-'*col1_w, '-'*18, '-'*18 ))
  580. msg(fs.format( 'TOTAL:', fmt_amt(tbals[0]), fmt_amt(tbals[1]) ))
  581. class sweep(wallet):
  582. name = 'sweep'
  583. desc = 'Sweep'
  584. past = 'swept'
  585. spec_id = 'sweep_spec'
  586. spec_key = ( (1,'source'), (3,'dest') )
  587. opts = ('do_not_relay','tx_relay_daemon')
  588. def create_addr_data(self):
  589. m = re.fullmatch(uarg_info[self.spec_id].pat,uarg.spec,re.ASCII)
  590. if not m:
  591. fs = "{!r}: invalid {!r} arg: for {} operation, it must have format {!r}"
  592. die(1,fs.format( uarg.spec, self.spec_id, self.name, uarg_info[self.spec_id].annot ))
  593. def gen():
  594. for i,k in self.spec_key:
  595. if m[i] == None:
  596. setattr(self,k,None)
  597. else:
  598. idx = int(m[i])
  599. try:
  600. res = [d for d in self.kal.data if d.idx == idx][0]
  601. except:
  602. die(1,'Supplied key-address file does not contain address {}:{}'.format(
  603. self.kal.al_id.sid,
  604. idx ))
  605. else:
  606. setattr(self,k,res)
  607. yield res
  608. self.addr_data = list(gen())
  609. self.account = int(m[2])
  610. if self.name == 'transfer':
  611. self.dest_addr = CoinAddr(self.proto,m[3])
  612. self.amount = self.proto.coin_amt(m[4])
  613. def init_tx_relay_daemon(self):
  614. m = re.fullmatch(uarg_info['tx_relay_daemon'].pat,uopt.tx_relay_daemon,re.ASCII)
  615. self.wd2 = MoneroWalletDaemon(
  616. proto = self.proto,
  617. wallet_dir = uopt.wallet_dir or '.',
  618. test_suite = g.test_suite,
  619. daemon_addr = m[1],
  620. proxy = m[2],
  621. port_shift = 16 )
  622. if g.test_suite:
  623. self.wd2.usr_daemon_args = ['--daemon-ssl-allow-any-cert']
  624. self.wd2.start()
  625. self.c = MoneroWalletRPCClient(
  626. host = self.wd2.host,
  627. port = self.wd2.rpc_port,
  628. user = self.wd2.user,
  629. passwd = self.wd2.passwd
  630. )
  631. async def main(self):
  632. gmsg(f'\n{self.desc}ing account #{self.account} of wallet {self.source.idx}' + (
  633. f': {self.amount} XMR to {self.dest_addr}' if self.name == 'transfer'
  634. else ' to new address' if self.dest == None
  635. else f' to new account in wallet {self.dest.idx}' ))
  636. h = self.rpc(self,self.source)
  637. await h.open_wallet('source')
  638. accts_data = (await h.get_accts())[0]
  639. max_acct = len(accts_data['subaddress_accounts']) - 1
  640. if self.account > max_acct:
  641. die(1,f'{self.account}: requested account index out of bounds (>{max_acct})')
  642. await h.print_addrs(accts_data,self.account)
  643. if self.name == 'transfer':
  644. dest_addr = self.dest_addr
  645. elif self.dest == None:
  646. dest_acct = self.account
  647. if keypress_confirm(f'\nCreate new address for account #{self.account}?'):
  648. dest_addr_chk = await h.create_new_addr(self.account)
  649. elif keypress_confirm(f'Sweep to last existing address of account #{self.account}?'):
  650. dest_addr_chk = None
  651. else:
  652. die(1,'Exiting at user request')
  653. dest_addr,dest_addr_idx = await h.get_last_addr(self.account,display=not dest_addr_chk)
  654. assert dest_addr_chk in (None,dest_addr), 'dest_addr_chk1'
  655. await h.print_addrs(accts_data,self.account)
  656. else:
  657. await h.close_wallet('source')
  658. bn = os.path.basename(self.get_wallet_fn(self.dest))
  659. h2 = self.rpc(self,self.dest)
  660. await h2.open_wallet('destination')
  661. accts_data = (await h2.get_accts())[0]
  662. if keypress_confirm(f'\nCreate new account for wallet {bn!r}?'):
  663. dest_acct,dest_addr = await h2.create_acct()
  664. dest_addr_idx = 0
  665. await h2.get_accts()
  666. elif keypress_confirm(f'Sweep to last existing account of wallet {bn!r}?'):
  667. dest_acct,dest_addr_chk = h2.get_last_acct(accts_data)
  668. dest_addr,dest_addr_idx = await h2.get_last_addr(dest_acct,display=False)
  669. assert dest_addr_chk == dest_addr, 'dest_addr_chk2'
  670. else:
  671. die(1,'Exiting at user request')
  672. await h2.close_wallet('destination')
  673. await h.open_wallet('source',refresh=False)
  674. msg(f'\n Creating {self.name} transaction...')
  675. if self.name == 'transfer':
  676. new_tx = await h.make_transfer_tx(self.account,dest_addr,self.amount)
  677. elif self.name == 'sweep':
  678. new_tx = await h.make_sweep_tx(self.account,dest_acct,dest_addr_idx,dest_addr)
  679. msg('\n' + new_tx.get_info(indent=' '))
  680. if uopt.tx_relay_daemon:
  681. self.display_tx_relay_info(indent=' ')
  682. if uopt.do_not_relay:
  683. msg('Saving TX data to file')
  684. new_tx.write(delete_metadata=True)
  685. elif keypress_confirm(f'Relay {self.name} transaction?'):
  686. w_desc = 'source'
  687. if uopt.tx_relay_daemon:
  688. await h.close_wallet('source')
  689. msg('')
  690. self.init_tx_relay_daemon()
  691. h = self.rpc(self,self.source)
  692. w_desc = 'TX relay source'
  693. await h.open_wallet(w_desc,refresh=False)
  694. msg_r(f'\n Relaying {self.name} transaction...')
  695. await h.relay_tx(new_tx.data.metadata)
  696. await h.close_wallet(w_desc)
  697. gmsg('\n\nAll done')
  698. else:
  699. await h.close_wallet('source')
  700. die(1,'\nExiting at user request')
  701. return True
  702. class transfer(sweep):
  703. name = 'transfer'
  704. desc = 'Transfer'
  705. past = 'transferred'
  706. spec_id = 'transfer_spec'
  707. spec_key = ( (1,'source'), )
  708. class relay(base):
  709. name = 'relay'
  710. desc = 'Relay'
  711. past = 'relayed'
  712. opts = ('tx_relay_daemon',)
  713. def __init__(self,uarg_tuple,uopt_tuple):
  714. super().__init__(uarg_tuple,uopt_tuple)
  715. if uopt.tx_relay_daemon:
  716. m = re.fullmatch(uarg_info['tx_relay_daemon'].pat,uopt.tx_relay_daemon,re.ASCII)
  717. host,port = m[1].split(':')
  718. proxy = m[2]
  719. else:
  720. from .daemon import CoinDaemon
  721. md = CoinDaemon('xmr',test_suite=g.test_suite)
  722. host,port = md.host,md.rpc_port
  723. proxy = None
  724. self.dc = MoneroRPCClientRaw(
  725. host = host,
  726. port = int(port),
  727. user = None,
  728. passwd = None,
  729. proxy = proxy )
  730. self.tx = MoneroMMGenTX.Signed(uarg.infile)
  731. async def main(self):
  732. msg('\n' + self.tx.get_info())
  733. if uopt.tx_relay_daemon:
  734. self.display_tx_relay_info()
  735. if keypress_confirm('Relay transaction?'):
  736. res = await self.dc.call(
  737. 'send_raw_transaction',
  738. tx_as_hex = self.tx.data.blob
  739. )
  740. if res['status'] == 'OK':
  741. msg('Status: ' + green('OK'))
  742. if res['not_relayed']:
  743. ymsg('Transaction not relayed')
  744. return True
  745. else:
  746. raise RPCFailure(repr(res))
  747. else:
  748. die(1,'Exiting at user request')