ct_shared.py 9.1 KB

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