xmrwallet.py 27 KB

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