shared.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  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.shared: Shared methods for the cmdtest.py test suite
  20. """
  21. from mmgen.util import get_extension
  22. from mmgen.wallet import get_wallet_cls
  23. from mmgen.addrlist import AddrList
  24. from mmgen.passwdlist import PasswordList
  25. from ..include.common import cfg, cmp_or_die, strip_ansi_escapes, joinpath, silence, end_silence
  26. from .include.common import ref_bw_file, ref_bw_hash_preset, ref_dir
  27. class CmdTestShared:
  28. 'shared methods for the cmdtest.py test suite'
  29. @property
  30. def segwit_mmtype(self):
  31. return ('segwit', 'bech32')[bool(cfg.bech32)] if self.segwit else None
  32. @property
  33. def segwit_arg(self):
  34. return ['--type=' + self.segwit_mmtype] if self.segwit_mmtype else []
  35. def txcreate_ui_common(
  36. self,
  37. t,
  38. caller = None,
  39. menu = [],
  40. inputs = '1',
  41. file_desc = 'Unsigned transaction',
  42. input_sels_prompt = 'to spend',
  43. bad_input_sels = False,
  44. interactive_fee = '',
  45. fee_desc = 'transaction fee',
  46. fee_info_pat = None,
  47. add_comment = '',
  48. view = 't',
  49. save = True,
  50. return_early = False,
  51. tweaks = [],
  52. used_chg_addr_resp = None,
  53. auto_chg_addr = None):
  54. txdo = (caller or self.test_name)[:4] == 'txdo'
  55. expect_pat = r'\[q\]uit menu, .*?:.'
  56. delete_pat = r'Enter account number .*:.'
  57. confirm_pat = r'OK\?.*:.'
  58. if used_chg_addr_resp is not None:
  59. t.expect('reuse harms your privacy.*:.*', used_chg_addr_resp, regex=True)
  60. if auto_chg_addr is not None:
  61. e1 = 'Choose a change address:.*Enter a number> '
  62. e2 = fr'Using .*{auto_chg_addr}.* as.*address'
  63. res = t.expect([e1, e2], regex=True)
  64. if res == 0:
  65. choice = [s.split(')')[0].lstrip() for s in t.p.match[0].split('\n') if auto_chg_addr in s][0]
  66. t.send(f'{choice}\n')
  67. t.expect(e2, regex=True)
  68. t.send('y')
  69. pat = expect_pat
  70. for choice in menu + ['q']:
  71. t.expect(pat, choice, regex=True)
  72. if self.proto.base_proto == 'Ethereum':
  73. pat = confirm_pat if pat == delete_pat else delete_pat if choice == 'D' else expect_pat
  74. if bad_input_sels:
  75. for r in ('x', '3-1', '9999'):
  76. t.expect(input_sels_prompt+': ', r+'\n')
  77. t.expect(input_sels_prompt+': ', inputs+'\n')
  78. have_est_fee = t.expect([f'{fee_desc}: ', 'OK? (Y/n): ']) == 1
  79. if have_est_fee and not interactive_fee:
  80. t.send('y')
  81. else:
  82. if have_est_fee:
  83. t.send('n')
  84. t.expect(f'{fee_desc}: ', interactive_fee+'\n')
  85. else:
  86. t.send(interactive_fee+'\n')
  87. if fee_info_pat:
  88. t.expect(fee_info_pat, regex=True)
  89. t.expect('OK? (Y/n): ', 'y')
  90. t.expect('(Y/n): ', '\n') # chg amt OK prompt
  91. if 'confirm_non_mmgen' in tweaks:
  92. t.expect('Continue? (Y/n)', '\n')
  93. if 'confirm_chg_non_mmgen' in tweaks:
  94. t.expect('to confirm: ', 'YES\n')
  95. t.do_comment(add_comment)
  96. if return_early:
  97. return t
  98. t.view_tx(view)
  99. if not txdo:
  100. t.expect('(y/N): ', ('n', 'y')[save])
  101. t.written_to_file(file_desc)
  102. return t
  103. def txsign_ui_common(
  104. self,
  105. t,
  106. caller = None,
  107. view = 't',
  108. add_comment = '',
  109. file_desc = 'Signed transaction',
  110. ni = False,
  111. save = True,
  112. do_passwd = False,
  113. passwd = None,
  114. has_label = False):
  115. txdo = (caller or self.test_name)[:4] == 'txdo'
  116. if do_passwd and txdo:
  117. t.passphrase('MMGen wallet', passwd or self.wpasswd)
  118. if not (ni or txdo):
  119. t.view_tx(view)
  120. if do_passwd:
  121. t.passphrase('MMGen wallet', passwd or self.wpasswd)
  122. t.do_comment(add_comment, has_label=has_label)
  123. t.expect('(Y/n): ', ('n', 'y')[save])
  124. t.written_to_file(file_desc)
  125. return t
  126. def txsend_ui_common(
  127. self,
  128. t,
  129. caller = None,
  130. view = 'n',
  131. add_comment = '',
  132. file_desc = 'Sent transaction',
  133. confirm_send = True,
  134. bogus_send = True,
  135. test = False,
  136. quiet = False,
  137. contract_addr = None,
  138. has_label = False):
  139. txdo = (caller or self.test_name)[:4] == 'txdo'
  140. if not txdo:
  141. t.license() # MMGEN_NO_LICENSE is set, so does nothing
  142. t.view_tx(view)
  143. t.do_comment(add_comment, has_label=has_label)
  144. if not test:
  145. self._do_confirm_send(t, quiet=quiet, confirm_send=confirm_send)
  146. if bogus_send:
  147. txid = ''
  148. t.expect('BOGUS transaction NOT sent')
  149. elif test == 'tx_proxy':
  150. t.expect('can be sent')
  151. return True
  152. else:
  153. m = 'TxID: ' if test else 'Transaction sent: '
  154. txid = strip_ansi_escapes(t.expect_getend(m))
  155. assert len(txid) == 64, f'{txid!r}: Incorrect txid length!'
  156. if not test:
  157. if contract_addr:
  158. _ = strip_ansi_escapes(t.expect_getend('Contract address: '))
  159. assert _ == contract_addr, f'Contract address mismatch: {_} != {contract_addr}'
  160. t.written_to_file(file_desc)
  161. return txid
  162. def txbump_ui_common(self, t, *, fee, fee_desc='transaction fee', bad_fee=None):
  163. t.expect('(Y/n): ', 'n') # network-estimated fee OK?
  164. if bad_fee:
  165. t.expect(f'{fee_desc}: ', f'{bad_fee}\n')
  166. t.expect(f'{fee_desc}: ', f'{fee}\n')
  167. t.expect('(Y/n): ', 'y') # fee OK?
  168. t.expect('(Y/n): ', 'y') # signoff
  169. t.expect('(y/N): ', 'n') # edit comment
  170. t.expect('(y/N): ', 'y') # save TX?
  171. t.written_to_file('Fee-bumped transaction')
  172. return t
  173. def txsign_end(self, t, tnum=None, has_label=False):
  174. t.expect('Signing transaction')
  175. t.do_comment(False, has_label=has_label)
  176. t.expect(r'Save signed transaction.*?\? \(Y/n\): ', 'y', regex=True)
  177. t.written_to_file('Signed transaction' + (' #' + tnum if tnum else ''))
  178. return t
  179. def txsign(
  180. self,
  181. wf,
  182. txfile,
  183. save = True,
  184. has_label = False,
  185. extra_opts = [],
  186. extra_desc = '',
  187. view = 'n',
  188. dfl_wallet = False):
  189. opts = extra_opts + ['-d', self.tmpdir, txfile] + ([wf] if wf else [])
  190. wcls = get_wallet_cls(ext = 'mmdat' if dfl_wallet else get_extension(wf))
  191. t = self.spawn(
  192. 'mmgen-txsign',
  193. opts,
  194. extra_desc,
  195. no_passthru_opts = ['coin'],
  196. exit_val = None if save or (wcls.enc and wcls.type != 'brain') else 1)
  197. t.license()
  198. t.view_tx(view)
  199. if wcls.enc and wcls.type != 'brain':
  200. t.passphrase(wcls.desc, self.wpasswd)
  201. if save:
  202. self.txsign_end(t, has_label=has_label)
  203. else:
  204. t.do_comment(False, has_label=has_label)
  205. t.expect('Save signed transaction? (Y/n): ', 'n')
  206. t.expect('not saved')
  207. return t
  208. def ref_brain_chk(self, bw_file=ref_bw_file):
  209. wf = joinpath(ref_dir, bw_file)
  210. add_args = [f'-l{self.seed_len}', f'-p{ref_bw_hash_preset}']
  211. return self.walletchk(wf, add_args=add_args, sid=self.ref_bw_seed_id)
  212. def walletchk(
  213. self,
  214. wf,
  215. wcls = None,
  216. add_args = [],
  217. sid = None,
  218. extra_desc = '',
  219. dfl_wallet = False):
  220. hp = self.hash_preset if hasattr(self, 'hash_preset') else '1'
  221. wcls = wcls or get_wallet_cls(ext=get_extension(wf))
  222. t = self.spawn(
  223. 'mmgen-walletchk',
  224. ([] if dfl_wallet else ['-i', wcls.fmt_codes[0]])
  225. + self.testnet_opt
  226. + add_args + ['-p', hp]
  227. + ([wf] if wf else []),
  228. extra_desc = extra_desc,
  229. no_passthru_opts = True)
  230. if wcls.type != 'incog_hidden':
  231. t.expect(f"Getting {wcls.desc} from file ‘")
  232. if wcls.enc and wcls.type != 'brain':
  233. t.passphrase(wcls.desc, self.wpasswd)
  234. t.expect(['Passphrase is OK', 'Passphrase.* are correct'], regex=True)
  235. chksum = t.expect_getend(f'Valid {wcls.desc} for Seed ID ')[:8]
  236. if sid:
  237. cmp_or_die(chksum, sid)
  238. return t
  239. def addrgen(
  240. self,
  241. wf,
  242. check_ref = False,
  243. ftype = 'addr',
  244. id_str = None,
  245. extra_opts = [],
  246. mmtype = None,
  247. stdout = False,
  248. dfl_wallet = False,
  249. no_passthru_opts = False):
  250. list_type = ftype[:4]
  251. passgen = list_type == 'pass'
  252. if not mmtype and not passgen:
  253. mmtype = self.segwit_mmtype
  254. t = self.spawn(
  255. f'mmgen-{list_type}gen',
  256. ['-d', self.tmpdir] + extra_opts +
  257. ([], ['--type='+str(mmtype)])[bool(mmtype)] +
  258. ([], ['--stdout'])[stdout] +
  259. ([], [wf])[bool(wf)] +
  260. ([], [id_str])[bool(id_str)] +
  261. [getattr(self, f'{list_type}_idx_list')],
  262. extra_desc = f'({mmtype})' if mmtype in ('segwit', 'bech32') else '',
  263. no_passthru_opts = no_passthru_opts)
  264. t.license()
  265. wcls = get_wallet_cls(ext = 'mmdat' if dfl_wallet else get_extension(wf))
  266. t.passphrase(wcls.desc, self.wpasswd)
  267. t.expect('Passphrase is OK')
  268. desc = ('address', 'password')[passgen]
  269. chksum = strip_ansi_escapes(t.expect_getend(rf'Checksum for {desc} data .*?: ', regex=True))
  270. if check_ref:
  271. chksum_chk = (
  272. self.chk_data[self.test_name] if passgen else
  273. self.chk_data[self.test_name][self.fork][self.proto.testnet])
  274. cmp_or_die(chksum, chksum_chk, desc=f'{ftype}list data checksum')
  275. if passgen:
  276. t.expect('Encrypt password list? (y/N): ', 'N')
  277. if stdout:
  278. t.read()
  279. else:
  280. fn = t.written_to_file('Password list' if passgen else 'Addresses')
  281. cls = PasswordList if passgen else AddrList
  282. silence()
  283. al = cls(cfg, self.proto, infile=fn, skip_chksum_msg=True) # read back the file we’ve written
  284. end_silence()
  285. cmp_or_die(al.chksum, chksum, desc=f'{ftype}list data checksum from file')
  286. return t
  287. def keyaddrgen(self, wf, check_ref=False, extra_opts=[], mmtype=None):
  288. if not mmtype:
  289. mmtype = self.segwit_mmtype
  290. args = ['-d', self.tmpdir, self.usr_rand_arg, wf, self.addr_idx_list]
  291. t = self.spawn('mmgen-keygen',
  292. ([f'--type={mmtype}'] if mmtype else []) + extra_opts + args,
  293. extra_desc = f'({mmtype})' if mmtype in ('segwit', 'bech32') else '')
  294. t.license()
  295. wcls = get_wallet_cls(ext=get_extension(wf))
  296. t.passphrase(wcls.desc, self.wpasswd)
  297. chksum = t.expect_getend(r'Checksum for key-address data .*?: ', regex=True)
  298. if check_ref:
  299. chksum_chk = self.chk_data[self.test_name][self.fork][self.proto.testnet]
  300. cmp_or_die(chksum, chksum_chk, desc='key-address list data checksum')
  301. t.expect('Encrypt key list? (y/N): ', 'y')
  302. t.usr_rand(self.usr_rand_chars)
  303. t.hash_preset('new key-address list', '1')
  304. t.passphrase_new('new key-address list', self.kapasswd)
  305. t.written_to_file('Encrypted secret keys')
  306. return t
  307. def _do_confirm_send(self, t, quiet=False, confirm_send=True, sure=True):
  308. if sure:
  309. t.expect('Are you sure you want to broadcast this')
  310. m = ('YES, I REALLY WANT TO DO THIS', 'YES')[quiet]
  311. t.expect(f'{m!r} to confirm: ', ('', m)[confirm_send]+'\n')