ts_xmrwallet.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2023 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. test.test_py_d.ts_xmrwallet: xmrwallet tests for the test.py test suite
  20. """
  21. import os,time,re,atexit,asyncio,shutil
  22. from subprocess import run,PIPE
  23. from collections import namedtuple
  24. from mmgen.cfg import gc
  25. from mmgen.util import msg,fmt,async_run,capfirst,is_int,die,list_gen
  26. from mmgen.obj import MMGenRange
  27. from mmgen.amt import XMRAmt
  28. from mmgen.addrlist import ViewKeyAddrList,KeyAddrList,AddrIdxList
  29. from ..include.common import (
  30. cfg,
  31. omsg,
  32. oqmsg_r,
  33. ok,
  34. imsg,
  35. imsg_r,
  36. write_data_to_file,
  37. read_from_file,
  38. silence,
  39. end_silence,
  40. strip_ansi_escapes
  41. )
  42. from .common import get_file_with_ext
  43. from .ts_base import TestSuiteBase
  44. # atexit functions:
  45. def stop_daemons(self):
  46. for v in self.users.values():
  47. if '--restricted-rpc' in v.md.start_cmd:
  48. v.md.stop()
  49. else:
  50. async_run(v.md_rpc.stop_daemon())
  51. def stop_miner_wallet_daemon(self):
  52. async_run(self.users['miner'].wd_rpc.stop_daemon())
  53. def kill_proxy(cls,args):
  54. if gc.platform == 'linux':
  55. omsg(f'Killing SSH SOCKS server at localhost:{cls.socks_port}')
  56. cmd = [ 'pkill', '-f', ' '.join(args) ]
  57. run(cmd)
  58. class TestSuiteXMRWallet(TestSuiteBase):
  59. """
  60. Monero wallet operations
  61. """
  62. networks = ('xmr',)
  63. passthru_opts = ()
  64. tmpdir_nums = [29]
  65. dfl_random_txs = 3
  66. color = True
  67. socks_port = 49237
  68. # Bob’s daemon is stopped via process kill, not RPC, so put Bob last in list:
  69. user_data = (
  70. ('miner', '98831F3A', False, 130, '1-2', []),
  71. ('alice', 'FE3C6545', False, 150, '1-4', []),
  72. ('bob', '1378FC64', False, 140, None, ['--restricted-rpc']),
  73. )
  74. tx_relay_user = 'bob'
  75. datadir_base = os.path.join('test','daemons','xmrtest')
  76. cmd_group = (
  77. ('daemon_version', 'checking daemon version'),
  78. ('gen_kafiles', 'generating key-address files'),
  79. ('create_wallets_miner', 'creating Monero wallets (Miner)'),
  80. ('set_label_miner', 'setting an address label (Miner, primary account)'),
  81. ('mine_initial_coins', 'mining initial coins'),
  82. ('create_wallets_alice', 'creating Monero wallets (Alice)'),
  83. ('fund_alice', 'sending funds'),
  84. ('sync_wallets_all', 'syncing all wallets'),
  85. ('new_account_alice', 'creating a new account (Alice)'),
  86. ('new_account_alice_label', 'creating a new account (Alice, with label)'),
  87. ('new_address_alice', 'creating a new address (Alice)'),
  88. ('new_address_alice_label', 'creating a new address (Alice, with label)'),
  89. ('remove_label_alice', 'removing an address label (Alice, subaddress)'),
  90. ('set_label_alice', 'setting an address label (Alice, subaddress)'),
  91. ('sync_wallets_selected', 'syncing selected wallets'),
  92. ('sweep_to_address_proxy', 'sweeping to new address (via TX relay + proxy)'),
  93. ('sweep_to_account', 'sweeping to new account'),
  94. ('sweep_to_address_noproxy', 'sweeping to new address (via TX relay, no proxy)'),
  95. ('transfer_to_miner_proxy', 'transferring funds to Miner (via TX relay + proxy)'),
  96. ('transfer_to_miner_noproxy', 'transferring funds to Miner (via TX relay, no proxy)'),
  97. ('transfer_to_miner_create1', 'transferring funds to Miner (create TX)'),
  98. ('transfer_to_miner_send1', 'transferring funds to Miner (send TX via proxy)'),
  99. ('transfer_to_miner_create2', 'transferring funds to Miner (create TX)'),
  100. ('transfer_to_miner_send2', 'transferring funds to Miner (send TX, no proxy)'),
  101. ('sweep_create_and_send', 'sweeping to new account (create TX + send TX, in stages)'),
  102. ('list_wallets_all', 'listing wallets'),
  103. ('stop_daemons', 'stopping all wallet and coin daemons'),
  104. )
  105. def __init__(self,trunner,cfgs,spawn):
  106. TestSuiteBase.__init__(self,trunner,cfgs,spawn)
  107. if trunner is None:
  108. return
  109. from mmgen.protocol import init_proto
  110. self.proto = init_proto( cfg, 'XMR', network='mainnet' )
  111. self.extra_opts = ['--wallet-rpc-password=passw0rd']
  112. self.autosign_mountpoint = os.path.join(self.tmpdir,'mmgen_autosign')
  113. self.autosign_xmr_dir = os.path.join(self.tmpdir,'mmgen_autosign','xmr')
  114. self.init_users()
  115. self.init_daemon_args()
  116. self.autosign_opts = []
  117. for v in self.users.values():
  118. run(['mkdir','-p',v.udir])
  119. self.tx_relay_daemon_parm = 'localhost:{}'.format( self.users[self.tx_relay_user].md.rpc_port )
  120. self.tx_relay_daemon_proxy_parm = (
  121. self.tx_relay_daemon_parm + f':127.0.0.1:{self.socks_port}' ) # must be IP, not 'localhost'
  122. if not cfg.no_daemon_stop:
  123. atexit.register(stop_daemons,self)
  124. atexit.register(stop_miner_wallet_daemon,self)
  125. if not cfg.no_daemon_autostart:
  126. stop_daemons(self)
  127. time.sleep(0.2)
  128. if os.path.exists(self.datadir_base):
  129. shutil.rmtree(self.datadir_base)
  130. os.makedirs(self.datadir_base)
  131. self.start_daemons()
  132. self.init_proxy()
  133. self.balance = None
  134. # init methods
  135. @classmethod
  136. def init_proxy(cls,external_call=False):
  137. def port_in_use(port):
  138. import socket
  139. try:
  140. socket.create_connection(('localhost',port)).close()
  141. except:
  142. return False
  143. else:
  144. return True
  145. def start_proxy():
  146. if external_call or not cfg.no_daemon_autostart:
  147. run(a+b2)
  148. omsg(f'SSH SOCKS server started, listening at localhost:{cls.socks_port}')
  149. debug_file = os.path.join('' if external_call else cls.datadir_base,'txrelay-proxy.debug')
  150. a = ['ssh','-x','-o','ExitOnForwardFailure=True','-D',f'localhost:{cls.socks_port}']
  151. b0 = ['-o','PasswordAuthentication=False']
  152. b1 = ['localhost','true']
  153. b2 = ['-fN','-E', debug_file, 'localhost']
  154. if port_in_use(cls.socks_port):
  155. omsg(f'Port {cls.socks_port} already in use. Assuming SSH SOCKS server is running')
  156. else:
  157. cp = run(a+b0+b1,stdout=PIPE,stderr=PIPE)
  158. err = cp.stderr.decode()
  159. if err:
  160. omsg(err)
  161. if cp.returncode == 0:
  162. start_proxy()
  163. elif 'onnection refused' in err:
  164. die(2,fmt("""
  165. The SSH daemon must be running and listening on localhost in order to test
  166. XMR TX relaying via SOCKS proxy. If sshd is not running, please start it.
  167. Otherwise, add the line 'ListenAddress 127.0.0.1' to your sshd_config, and
  168. then restart the daemon.
  169. """,indent=' '))
  170. elif 'ermission denied' in err:
  171. msg(fmt(f"""
  172. In order to test XMR TX relaying via SOCKS proxy, it’s desirable to enable
  173. SSH to localhost without a password, which is not currently supported by
  174. your configuration. Your possible courses of action:
  175. 1. Continue by answering 'y' at this prompt, and enter your system password
  176. at the following prompt;
  177. 2. Exit the test here, add your user SSH public key to your user
  178. 'authorized_keys' file, and restart the test; or
  179. 3. Exit the test here, start the SSH SOCKS proxy manually by entering the
  180. following command, and restart the test:
  181. {' '.join(a+b2)}
  182. """,indent=' ',strip_char='\t'))
  183. from mmgen.ui import keypress_confirm
  184. if keypress_confirm(cfg,'Continue?'):
  185. start_proxy()
  186. else:
  187. die(1,'Exiting at user request')
  188. else:
  189. die(2,fmt(f"""
  190. Please start the SSH SOCKS proxy by entering the following command:
  191. {' '.join(a+b2)}
  192. Then restart the test.
  193. """,indent=' '))
  194. if not (external_call or cfg.no_daemon_stop):
  195. atexit.unregister(kill_proxy)
  196. atexit.register(kill_proxy, cls, a + b2)
  197. return True
  198. def init_users(self):
  199. from mmgen.daemon import CoinDaemon
  200. from mmgen.proto.xmr.daemon import MoneroWalletDaemon
  201. from mmgen.proto.xmr.rpc import MoneroRPCClient,MoneroWalletRPCClient
  202. self.users = {}
  203. tmpdir_num = self.tmpdir_nums[0]
  204. ud = namedtuple('user_data',[
  205. 'sid',
  206. 'mmwords',
  207. 'autosign',
  208. 'udir',
  209. 'datadir',
  210. 'kal_range',
  211. 'kafile',
  212. 'walletfile_fs',
  213. 'addrfile_fs',
  214. 'md',
  215. 'md_rpc',
  216. 'wd',
  217. 'wd_rpc',
  218. 'add_coind_args',
  219. ])
  220. # kal_range must be None, a single digit, or a single hyphenated range
  221. for ( user,
  222. sid,
  223. autosign,
  224. shift,
  225. kal_range,
  226. add_coind_args ) in self.user_data:
  227. tmpdir = os.path.join('test','tmp',str(tmpdir_num))
  228. udir = os.path.join(tmpdir,user)
  229. datadir = os.path.join(self.datadir_base,user)
  230. md = CoinDaemon(
  231. cfg = cfg,
  232. proto = self.proto,
  233. test_suite = True,
  234. port_shift = shift,
  235. opts = ['online'],
  236. datadir = datadir
  237. )
  238. md_rpc = MoneroRPCClient(
  239. cfg = cfg,
  240. proto = self.proto,
  241. host = 'localhost',
  242. port = md.rpc_port,
  243. user = None,
  244. passwd = None,
  245. test_connection = False,
  246. daemon = md,
  247. )
  248. wd = MoneroWalletDaemon(
  249. cfg = cfg,
  250. proto = self.proto,
  251. test_suite = True,
  252. wallet_dir = udir,
  253. user = 'foo',
  254. passwd = 'bar',
  255. port_shift = shift,
  256. monerod_addr = f'127.0.0.1:{md.rpc_port}',
  257. )
  258. wd_rpc = MoneroWalletRPCClient(
  259. cfg = cfg,
  260. daemon = wd,
  261. test_connection = False,
  262. )
  263. if autosign:
  264. kafile_suf = 'vkeys'
  265. fn_stem = 'MoneroWatchOnlyWallet'
  266. kafile_dir = self.autosign_xmr_dir
  267. else:
  268. kafile_suf = 'akeys'
  269. fn_stem = 'MoneroWallet'
  270. kafile_dir = udir
  271. self.users[user] = ud(
  272. sid = sid,
  273. mmwords = f'test/ref/{sid}.mmwords',
  274. autosign = autosign,
  275. udir = udir,
  276. datadir = datadir,
  277. kal_range = kal_range,
  278. kafile = f'{kafile_dir}/{sid}-XMR-M[{kal_range}].{kafile_suf}',
  279. walletfile_fs = f'{udir}/{sid}-{{}}-{fn_stem}',
  280. addrfile_fs = f'{udir}/{sid}-{{}}-{fn_stem}.address.txt',
  281. md = md,
  282. md_rpc = md_rpc,
  283. wd = wd,
  284. wd_rpc = wd_rpc,
  285. add_coind_args = add_coind_args,
  286. )
  287. def init_daemon_args(self):
  288. common_args = ['--p2p-bind-ip=127.0.0.1','--fixed-difficulty=1','--regtest'] # ,'--rpc-ssl-allow-any-cert']
  289. for u in self.users:
  290. other_ports = [self.users[u2].md.p2p_port for u2 in self.users if u2 != u]
  291. node_args = [f'--add-exclusive-node=127.0.0.1:{p}' for p in other_ports]
  292. self.users[u].md.usr_coind_args = (
  293. common_args +
  294. node_args +
  295. self.users[u].add_coind_args )
  296. # cmd_group methods
  297. def daemon_version(self):
  298. rpc_port = self.users['miner'].md.rpc_port
  299. return self.spawn( 'mmgen-tool', ['--coin=xmr', f'--rpc-port={rpc_port}', 'daemon_version'] )
  300. def gen_kafiles(self):
  301. for user,data in self.users.items():
  302. if not data.kal_range:
  303. continue
  304. if data.autosign:
  305. continue
  306. run(['mkdir','-p',data.udir])
  307. run(f'rm -f {data.kafile}',shell=True)
  308. t = self.spawn(
  309. 'mmgen-keygen', [
  310. '-q', '--accept-defaults', '--coin=xmr',
  311. f'--outdir={data.udir}', data.mmwords, data.kal_range
  312. ],
  313. extra_desc = f'({capfirst(user)})' )
  314. t.read()
  315. t.ok()
  316. t.skip_ok = True
  317. return t
  318. def create_wallets_miner(self):
  319. return self.create_wallets('miner')
  320. def create_wallets_alice(self):
  321. return self.create_wallets('alice')
  322. def create_wallets(self,user,wallet=None,add_opts=[],op='create'):
  323. assert wallet is None or is_int(wallet), 'wallet arg'
  324. data = self.users[user]
  325. stem_glob = data.walletfile_fs.format(wallet or '*')
  326. for glob in (
  327. stem_glob,
  328. stem_glob + '.keys',
  329. stem_glob + '.address.txt' ):
  330. run( f'rm -f {glob}', shell=True )
  331. t = self.spawn(
  332. 'mmgen-xmrwallet',
  333. [f'--wallet-dir={data.udir}']
  334. + self.extra_opts
  335. + (self.autosign_opts if data.autosign else [])
  336. + add_opts
  337. + [op]
  338. + ([] if data.autosign else [data.kafile])
  339. + [wallet or data.kal_range]
  340. )
  341. for i in MMGenRange(wallet or data.kal_range).items:
  342. write_data_to_file(
  343. cfg,
  344. self.users[user].addrfile_fs.format(i),
  345. t.expect_getend('Address: '),
  346. quiet = True
  347. )
  348. return t
  349. def new_addr_alice(self,spec,cfg,expect,kafile=None):
  350. data = self.users['alice']
  351. t = self.spawn(
  352. 'mmgen-xmrwallet',
  353. self.extra_opts +
  354. [f'--wallet-dir={data.udir}'] +
  355. [f'--daemon=localhost:{data.md.rpc_port}'] +
  356. (['--no-start-wallet-daemon'] if cfg in ('continue','stop') else []) +
  357. (['--no-stop-wallet-daemon'] if cfg in ('start','continue') else []) +
  358. ['new', (kafile or data.kafile), spec] )
  359. m = re.search( expect, t.read(strip_color=True), re.DOTALL )
  360. assert m, f'no match found for {expect!r}'
  361. return t
  362. na_idx = 1
  363. def new_account_alice(self):
  364. return self.new_addr_alice(
  365. '4',
  366. 'start',
  367. fr'Creating new account.*Index:\s+{self.na_idx}\s')
  368. def new_account_alice_label(self):
  369. return self.new_addr_alice(
  370. '4,Alice’s new account',
  371. 'continue',
  372. fr'Creating new account.*Index:\s+{self.na_idx+1}\s.*Alice’s new account')
  373. def new_address_alice(self):
  374. return self.new_addr_alice(
  375. '4:2',
  376. 'continue',
  377. fr'Account index:\s+2\s+Creating new address' )
  378. def new_address_alice_label(self):
  379. return self.new_addr_alice(
  380. '4:2,Alice’s new address',
  381. 'stop',
  382. fr'Account index:\s+2\s+Creating new address.*Alice’s new address' )
  383. async def mine_initial_coins(self):
  384. self.spawn('', msg_only=True, extra_desc='(opening wallet)')
  385. await self.open_wallet_user('miner',1)
  386. ok()
  387. return await self.mine_chk('miner',1,0,lambda x: x.ub > 20,'unlocked balance > 20')
  388. async def fund_alice(self,wallet=1,check_bal=True):
  389. self.spawn('', msg_only=True, extra_desc='(transferring funds from Miner wallet)')
  390. await self.transfer(
  391. 'miner',
  392. 1234567891234,
  393. read_from_file(self.users['alice'].addrfile_fs.format(wallet)),
  394. )
  395. if not check_bal:
  396. return 'ok'
  397. ok()
  398. bal = '1.234567891234'
  399. return await self.mine_chk(
  400. 'alice',wallet,0,
  401. lambda x: str(x.ub) == bal,f'unlocked balance == {bal}',
  402. random_txs = self.dfl_random_txs
  403. )
  404. def set_label_miner(self):
  405. return self.set_label_user( 'miner', '1:0:0,"Miner’s new primary account label [1:0:0]"' )
  406. def remove_label_alice(self):
  407. return self.set_label_user( 'alice', '4:2:2,""' )
  408. def set_label_alice(self):
  409. return self.set_label_user( 'alice', '4:2:2,"Alice’s new subaddress label [4:2:2]"' )
  410. def set_label_user(self,user,label_spec):
  411. data = self.users[user]
  412. cmd_opts = [f'--wallet-dir={data.udir}', f'--daemon=localhost:{data.md.rpc_port}']
  413. t = self.spawn(
  414. 'mmgen-xmrwallet',
  415. self.extra_opts + cmd_opts + ['label', data.kafile, label_spec]
  416. )
  417. t.expect('(y/N): ','y')
  418. t.expect(['successfully set','successfully removed'])
  419. return t
  420. def sync_wallets_all(self):
  421. return self.sync_wallets('alice',add_opts=['--rescan-blockchain'])
  422. def sync_wallets_selected(self):
  423. return self.sync_wallets('alice',wallets='1-2,4')
  424. def list_wallets_all(self):
  425. return self.sync_wallets('alice',op='list')
  426. def sync_wallets(self,user,op='sync',wallets=None,add_opts=[],bal_chk_func=None):
  427. data = self.users[user]
  428. cmd_opts = list_gen(
  429. [f'--wallet-dir={data.udir}'],
  430. [f'--daemon=localhost:{data.md.rpc_port}'],
  431. )
  432. t = self.spawn(
  433. 'mmgen-xmrwallet',
  434. self.extra_opts
  435. + cmd_opts
  436. + self.autosign_opts
  437. + add_opts
  438. + [op]
  439. + ([] if data.autosign else [data.kafile])
  440. + ([wallets] if wallets else [])
  441. )
  442. wlist = AddrIdxList(wallets) if wallets else MMGenRange(data.kal_range).items
  443. for n,wnum in enumerate(wlist,1):
  444. t.expect('Syncing wallet {}/{} ({})'.format(
  445. n,
  446. len(wlist),
  447. os.path.basename(data.walletfile_fs.format(wnum)),
  448. ))
  449. t.expect('Chain height: ')
  450. t.expect('Wallet height: ')
  451. res = strip_ansi_escapes(t.expect_getend('Balance: '))
  452. if bal_chk_func:
  453. m = re.match( '(\S+) Unlocked balance: (\S+)', res, re.DOTALL )
  454. amts = [XMRAmt(amt) for amt in m.groups()]
  455. assert bal_chk_func(n,*amts), f'balance check for wallet {n} failed!'
  456. return t
  457. def do_op(
  458. self,
  459. op,
  460. user,
  461. arg2,
  462. tx_relay_parm = None,
  463. no_relay = False,
  464. return_amt = False,
  465. reuse_acct = False,
  466. add_desc = None,
  467. do_ret = False):
  468. data = self.users[user]
  469. cmd_opts = list_gen(
  470. [f'--wallet-dir={data.udir}'],
  471. [f'--outdir={data.udir}', not data.autosign],
  472. [f'--daemon=localhost:{data.md.rpc_port}'],
  473. [f'--tx-relay-daemon={tx_relay_parm}', tx_relay_parm],
  474. ['--no-relay', no_relay and not data.autosign],
  475. [f'--autosign-mountpoint={self.autosign_mountpoint}', data.autosign],
  476. )
  477. add_desc = (', ' + add_desc) if add_desc else ''
  478. t = self.spawn(
  479. 'mmgen-xmrwallet',
  480. self.extra_opts
  481. + cmd_opts
  482. + [op]
  483. + ([] if data.autosign else [data.kafile])
  484. + [arg2],
  485. extra_desc = f'({capfirst(user)}{add_desc})' )
  486. if op == 'sign':
  487. return t
  488. if op == 'sweep':
  489. t.expect(
  490. r'Create new {} .* \(y/N\): '.format(('address','account')[',' in arg2]),
  491. ('y','n')[reuse_acct],
  492. regex=True )
  493. if reuse_acct:
  494. t.expect( r'to last existing account .* \(y/N\): ','y', regex=True )
  495. if return_amt:
  496. amt = XMRAmt(strip_ansi_escapes(t.expect_getend('Amount: ')).replace('XMR','').strip())
  497. dtype = 'unsigned' if data.autosign else 'signed'
  498. t.expect(f'Save {dtype} transaction? (y/N): ','y')
  499. t.written_to_file(f'{dtype.capitalize()} transaction')
  500. if not no_relay:
  501. t.expect(f'Relay {op} transaction? (y/N): ','y')
  502. get_file_with_ext(self.users[user].udir,'sigtx',delete_all=True)
  503. t.read()
  504. return t if do_ret else amt if return_amt else t.ok()
  505. def sweep_to_address_proxy(self):
  506. self.do_op('sweep','alice','1:0',self.tx_relay_daemon_proxy_parm)
  507. return self.mine_chk('alice',1,0,lambda x: x.ub > 1,'unlocked balance > 1')
  508. def sweep_to_account(self):
  509. self.do_op('sweep','alice','1:0,2')
  510. return self.mine_chk('alice',2,1,lambda x: x.ub > 1,'unlocked balance > 1')
  511. def sweep_to_address_noproxy(self):
  512. self.do_op('sweep','alice','2:1',self.tx_relay_daemon_parm)
  513. return self.mine_chk('alice',2,1,lambda x: x.ub > 0.9,'unlocked balance > 0.9')
  514. async def transfer_to_miner_proxy(self):
  515. addr = read_from_file(self.users['miner'].addrfile_fs.format(2))
  516. amt = '0.135'
  517. self.do_op('transfer','alice',f'2:1:{addr},{amt}',self.tx_relay_daemon_proxy_parm)
  518. await self.stop_wallet_user('miner')
  519. await self.open_wallet_user('miner',2)
  520. await self.mine_chk('miner',2,0,lambda x: str(x.ub) == amt,f'unlocked balance == {amt}')
  521. ok()
  522. return await self.mine_chk('alice',2,1,lambda x: x.ub > 0.9,'unlocked balance > 0.9')
  523. async def transfer_to_miner_noproxy(self):
  524. addr = read_from_file(self.users['miner'].addrfile_fs.format(2))
  525. self.do_op('transfer','alice',f'2:1:{addr},0.0995',self.tx_relay_daemon_parm)
  526. await self.mine_chk('miner',2,0,lambda x: str(x.ub) == '0.2345','unlocked balance == 0.2345')
  527. ok()
  528. return await self.mine_chk('alice',2,1,lambda x: x.ub > 0.9,'unlocked balance > 0.9')
  529. def transfer_to_miner_create(self,amt):
  530. get_file_with_ext(self.users['alice'].udir,'sigtx',delete_all=True)
  531. addr = read_from_file(self.users['miner'].addrfile_fs.format(2))
  532. return self.do_op('transfer','alice',f'2:1:{addr},{amt}',no_relay=True,do_ret=True)
  533. def transfer_to_miner_create1(self):
  534. return self.transfer_to_miner_create('0.0111')
  535. def transfer_to_miner_create2(self):
  536. return self.transfer_to_miner_create('0.0012')
  537. def relay_tx(self,relay_opt,add_desc=None):
  538. user = 'alice'
  539. data = self.users[user]
  540. add_desc = (', ' + add_desc) if add_desc else ''
  541. t = self.spawn(
  542. 'mmgen-xmrwallet',
  543. self.extra_opts
  544. + [ relay_opt, 'relay', get_file_with_ext(data.udir,'sigtx') ],
  545. extra_desc = f'(relaying TX, {capfirst(user)}{add_desc})' )
  546. t.expect('Relay transaction? ','y')
  547. t.read()
  548. t.ok()
  549. return t
  550. async def transfer_to_miner_send1(self):
  551. self.relay_tx(f'--tx-relay-daemon={self.tx_relay_daemon_proxy_parm}',add_desc='via proxy')
  552. await self.mine_chk('miner',2,0,lambda x: str(x.ub) == '0.2456','unlocked balance == 0.2456')
  553. ok()
  554. return await self.mine_chk('alice',2,1,lambda x: x.ub > 0.9,'unlocked balance > 0.9')
  555. async def transfer_to_miner_send2(self):
  556. self.relay_tx(f'--tx-relay-daemon={self.tx_relay_daemon_parm}',add_desc='no proxy')
  557. await self.mine_chk('miner',2,0,lambda x: str(x.ub) == '0.2468','unlocked balance == 0.2468')
  558. ok()
  559. return await self.mine_chk('alice',2,1,lambda x: x.ub > 0.9,'unlocked balance > 0.9')
  560. async def sweep_create_and_send(self):
  561. bal = XMRAmt('0')
  562. min_bal = XMRAmt('0.9')
  563. for i in range(4):
  564. if i:
  565. ok()
  566. get_file_with_ext(self.users['alice'].udir,'sigtx',delete_all=True)
  567. send_amt = self.do_op(
  568. 'sweep','alice','2:1,3', # '2:1,3'
  569. no_relay = True,
  570. reuse_acct = True,
  571. add_desc = f'TX #{i+1}',
  572. return_amt = True )
  573. ok()
  574. self.relay_tx(f'--tx-relay-daemon={self.tx_relay_daemon_parm}',add_desc=f'send amt: {send_amt} XMR')
  575. await self.mine_chk('alice',2,1,lambda x: 'chk_bal_chg','balance has changed')
  576. ok()
  577. bal_info = await self.mine_chk('alice',3,0,lambda x,y=bal: x.ub > y, f'bal > {bal}',return_bal=True)
  578. bal += bal_info.ub
  579. if bal >= min_bal:
  580. return 'ok'
  581. return False
  582. # wallet methods
  583. async def open_wallet_user(self,user,wnum):
  584. data = self.users[user]
  585. silence()
  586. kal = (ViewKeyAddrList if data.autosign else KeyAddrList)(
  587. cfg = cfg,
  588. proto = self.proto,
  589. addrfile = data.kafile,
  590. skip_chksum_msg = True,
  591. key_address_validity_check = False )
  592. end_silence()
  593. self.users[user].wd.start(silent=not (cfg.exact_output or cfg.verbose))
  594. return data.wd_rpc.call(
  595. 'open_wallet',
  596. filename = os.path.basename(data.walletfile_fs.format(wnum)),
  597. password = kal.entry(wnum).wallet_passwd )
  598. async def stop_wallet_user(self,user):
  599. await self.users[user].wd_rpc.stop_daemon(silent=not (cfg.exact_output or cfg.verbose))
  600. return 'ok'
  601. # mining methods
  602. async def mine5(self):
  603. return await self.mine(5)
  604. async def _get_height(self):
  605. u = self.users['miner']
  606. for _ in range(20):
  607. try:
  608. return u.md_rpc.call('get_last_block_header')['block_header']['height']
  609. except Exception as e:
  610. if 'onnection refused' in str(e):
  611. omsg(f'{e}\nMonerod appears to have crashed. Attempting to restart...')
  612. await asyncio.sleep(5)
  613. u.md.restart()
  614. await asyncio.sleep(5)
  615. await self.start_mining()
  616. else:
  617. raise
  618. else:
  619. die(2,'Restart attempt limit exceeded')
  620. async def mine10(self):
  621. return await self.mine(10)
  622. async def mine30(self):
  623. return await self.mine(30)
  624. async def mine100(self):
  625. return await self.mine(100)
  626. async def mine(self,nblks):
  627. start_height = height = await self._get_height()
  628. imsg(f'Height: {height}')
  629. imsg_r(f'Mining {nblks} blocks...')
  630. await self.start_mining()
  631. while height < start_height + nblks:
  632. await asyncio.sleep(2)
  633. height = await self._get_height()
  634. imsg_r('.')
  635. ret = await self.stop_mining()
  636. imsg('done')
  637. imsg(f'Height: {height}')
  638. return 'ok' if ret == 'OK' else False
  639. async def start_mining(self):
  640. data = self.users['miner']
  641. addr = read_from_file(data.addrfile_fs.format(1)) # mine to wallet #1, account 0
  642. for _ in range(20):
  643. ret = data.md_rpc.call_raw(
  644. 'start_mining',
  645. do_background_mining = False, # run mining in background or foreground
  646. ignore_battery = True, # ignore battery state (on laptop)
  647. miner_address = addr, # account address to mine to
  648. threads_count = 3 ) # number of mining threads to run
  649. status = self.get_status(ret)
  650. if status == 'OK':
  651. return True
  652. elif status == 'BUSY':
  653. await asyncio.sleep(5)
  654. omsg('Daemon busy. Attempting to start mining...')
  655. else:
  656. die(2,f'Monerod returned status {status}')
  657. else:
  658. die(2,'Max retries exceeded')
  659. async def stop_mining(self):
  660. ret = self.users['miner'].md_rpc.call_raw('stop_mining')
  661. return self.get_status(ret)
  662. async def mine_chk(
  663. self,
  664. user,
  665. wnum,
  666. account,
  667. test,
  668. test_desc,
  669. test2 = None,
  670. test2_desc = None,
  671. random_txs = None,
  672. return_bal = False ):
  673. """
  674. - open destination wallet
  675. - optionally create and broadcast random TXs
  676. - start mining
  677. - mine until funds appear in wallet
  678. - stop mining
  679. - close wallet
  680. """
  681. async def send_random_txs():
  682. from mmgen.tool.api import tool_api
  683. t = tool_api(cfg)
  684. t.init_coin('XMR','mainnet')
  685. t.usr_randchars = 0
  686. imsg_r('Sending random transactions: ')
  687. for i in range(random_txs):
  688. await self.transfer(
  689. 'miner',
  690. 123456789,
  691. t.randpair()[1],
  692. )
  693. imsg_r(f'{i+1} ')
  694. oqmsg_r('+')
  695. await asyncio.sleep(0.5)
  696. imsg('')
  697. def print_balance(dest,bal_info):
  698. imsg('Total balances in {}’s wallet {}, account #{}: {} (total), {} (unlocked)'.format(
  699. capfirst(dest.user),
  700. dest.wnum,
  701. dest.account,
  702. bal_info.b.hl(),
  703. bal_info.ub.hl(),
  704. ))
  705. async def get_balance(dest,count):
  706. data = self.users[dest.user]
  707. data.wd_rpc.call('refresh')
  708. if count and not count % 20:
  709. data.wd_rpc.call('rescan_blockchain')
  710. ret = data.wd_rpc.call('get_accounts')['subaddress_accounts'][dest.account]
  711. d_tup = namedtuple('bal_info',['b','ub'])
  712. return d_tup(
  713. b = XMRAmt(ret['balance'],from_unit='atomic'),
  714. ub = XMRAmt(ret['unlocked_balance'],from_unit='atomic')
  715. )
  716. # start execution:
  717. self.do_msg(extra_desc =
  718. (f'sending {random_txs} random TXs, ' if random_txs else '') +
  719. f'mining, checking wallet {user}:{wnum}:{account}' )
  720. dest = namedtuple(
  721. 'dest_info',['user','wnum','account','test','test_desc','test2','test2_desc'])(
  722. user,wnum,account,test,test_desc,test2,test2_desc)
  723. if dest.user != 'miner':
  724. await self.open_wallet_user(dest.user,dest.wnum)
  725. bal_info_start = await get_balance(dest,0)
  726. chk_bal_chg = dest.test(bal_info_start) == 'chk_bal_chg'
  727. if random_txs:
  728. await send_random_txs()
  729. await self.start_mining()
  730. h = await self._get_height()
  731. imsg_r(f'Chain height: {h} ')
  732. max_iterations,height_threshold = (300,80) if gc.platform == 'win' else (50,300)
  733. for count in range(max_iterations):
  734. bal_info = await get_balance(dest,count)
  735. if h > height_threshold and (
  736. dest.test(bal_info) is True or (chk_bal_chg and bal_info.ub != bal_info_start.ub)
  737. ):
  738. imsg('')
  739. oqmsg_r('+')
  740. print_balance(dest,bal_info)
  741. if dest.test2:
  742. assert dest.test2(bal_info) is True, f'test failed: {dest.test2_desc} ({bal_info})'
  743. break
  744. await asyncio.sleep(2)
  745. h = await self._get_height()
  746. if count > 12: # something might have gone wrong, so be more verbose
  747. imsg(f'Height: {h} ')
  748. print_balance(dest,bal_info)
  749. else:
  750. imsg_r(f'{h} ')
  751. oqmsg_r('+')
  752. else:
  753. die(2,f'Timeout exceeded, balance {bal_info.ub!r}')
  754. await self.stop_mining()
  755. if user != 'miner':
  756. await self.stop_wallet_user(dest.user)
  757. return bal_info if return_bal else 'ok'
  758. # util methods
  759. def get_status(self,ret):
  760. if ret['status'] != 'OK':
  761. imsg( 'RPC status: {}'.format( ret['status'] ))
  762. return ret['status']
  763. def do_msg(self,extra_desc=None):
  764. self.spawn(
  765. '',
  766. msg_only = True,
  767. extra_desc = f'({extra_desc})' if extra_desc else None
  768. )
  769. async def transfer(self,user,amt,addr):
  770. return self.users[user].wd_rpc.call('transfer',destinations=[{'amount':amt,'address':addr}])
  771. # daemon start/stop methods
  772. def start_daemons(self):
  773. for v in self.users.values():
  774. run(['mkdir','-p',v.datadir])
  775. v.md.start()
  776. def stop_daemons(self):
  777. self.spawn('', msg_only=True)
  778. if cfg.no_daemon_stop:
  779. omsg('[not stopping daemons at user request]')
  780. else:
  781. omsg('')
  782. stop_daemons(self)
  783. atexit.unregister(stop_daemons)
  784. stop_miner_wallet_daemon(self)
  785. atexit.unregister(stop_miner_wallet_daemon)
  786. return 'silent'