autosign.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
  4. # Copyright (C)2013-2024 The MMGen Project <mmgen@tuta.io>
  5. # Licensed under the GNU General Public License, Version 3:
  6. # https://www.gnu.org/licenses
  7. # Public project repositories:
  8. # https://github.com/mmgen/mmgen-wallet
  9. # https://gitlab.com/mmgen/mmgen-wallet
  10. """
  11. autosign: Auto-sign MMGen transactions, message files and XMR wallet output files
  12. """
  13. import sys,os,asyncio
  14. from stat import S_ISDIR,S_IWUSR,S_IRUSR
  15. from pathlib import Path
  16. from subprocess import run,DEVNULL
  17. from .cfg import Config
  18. from .util import msg,msg_r,ymsg,rmsg,gmsg,bmsg,die,suf,fmt,fmt_list,async_run
  19. from .color import yellow,red,orange,brown
  20. from .wallet import Wallet,get_wallet_cls
  21. from .filename import find_file_in_dir
  22. from .ui import keypress_confirm
  23. class Signable:
  24. non_xmr_signables = (
  25. 'transaction',
  26. 'automount_transaction',
  27. 'message')
  28. xmr_signables = ( # order is important!
  29. 'xmr_wallet_outputs_file', # import XMR wallet outputs BEFORE signing transactions
  30. 'xmr_transaction')
  31. class base:
  32. clean_all = False
  33. multiple_ok = True
  34. action_desc = 'signed'
  35. def __init__(self,parent):
  36. self.parent = parent
  37. self.cfg = parent.cfg
  38. self.dir = getattr(parent,self.dir_name)
  39. self.name = type(self).__name__
  40. @property
  41. def submitted(self):
  42. return self._processed('_submitted', self.subext)
  43. def _processed(self, attrname, ext):
  44. if not hasattr(self, attrname):
  45. setattr(self, attrname, tuple(f for f in sorted(self.dir.iterdir()) if f.name.endswith('.'+ext)))
  46. return getattr(self, attrname)
  47. @property
  48. def unsigned(self):
  49. return self._unprocessed( '_unsigned', self.rawext, self.sigext )
  50. @property
  51. def unsubmitted(self):
  52. return self._unprocessed( '_unsubmitted', self.sigext, self.subext )
  53. @property
  54. def unsubmitted_raw(self):
  55. return self._unprocessed( '_unsubmitted_raw', self.rawext, self.subext )
  56. unsent = unsubmitted
  57. unsent_raw = unsubmitted_raw
  58. def _unprocessed(self,attrname,rawext,sigext):
  59. if not hasattr(self,attrname):
  60. dirlist = sorted(self.dir.iterdir())
  61. names = {f.name for f in dirlist}
  62. setattr(
  63. self,
  64. attrname,
  65. tuple(f for f in dirlist
  66. if f.name.endswith('.' + rawext)
  67. and f.name[:-len(rawext)] + sigext not in names) )
  68. return getattr(self,attrname)
  69. def print_bad_list(self,bad_files):
  70. msg('\n{a}\n{b}'.format(
  71. a = red(f'Failed {self.desc}s:'),
  72. b = ' {}\n'.format('\n '.join(self.gen_bad_list(sorted(bad_files,key=lambda f: f.name))))
  73. ))
  74. def die_wrong_num_txs(self, tx_type, msg=None, desc=None, show_dir=False):
  75. num_txs = len(getattr(self, tx_type))
  76. die('AutosignTXError', "{m}{a} {b} transaction{c} {d} {e}!".format(
  77. m = msg + '\n' if msg else '',
  78. a = 'One' if num_txs == 1 else 'More than one' if num_txs else 'No',
  79. b = desc or tx_type,
  80. c = suf(num_txs),
  81. d = 'already present' if num_txs else 'present',
  82. e = f'in ‘{getattr(self.parent, self.dir_name)}’' if show_dir else 'on removable device',
  83. ))
  84. def check_create_ok(self):
  85. if len(self.unsigned):
  86. self.die_wrong_num_txs('unsigned', msg='Cannot create transaction')
  87. if len(self.unsent):
  88. die('AutosignTXError', 'Cannot create transaction: you have an unsent transaction')
  89. def get_unsubmitted(self, tx_type='unsubmitted'):
  90. if len(self.unsubmitted) == 1:
  91. return self.unsubmitted[0]
  92. else:
  93. self.die_wrong_num_txs(tx_type)
  94. def get_unsent(self):
  95. return self.get_unsubmitted('unsent')
  96. def get_submitted(self):
  97. if len(self.submitted) == 0:
  98. self.die_wrong_num_txs('submitted')
  99. else:
  100. return self.submitted
  101. def get_abortable(self):
  102. if len(self.unsent_raw) != 1:
  103. self.die_wrong_num_txs('unsent_raw', desc='unsent')
  104. if len(self.unsent) > 1:
  105. self.die_wrong_num_txs('unsent')
  106. if self.unsent:
  107. if self.unsent[0].stem != self.unsent_raw[0].stem:
  108. die(1, f'{self.unsent[0]}, {self.unsent_raw[0]}: file mismatch')
  109. return self.unsent_raw + self.unsent
  110. def shred_abortable(self):
  111. files = self.get_abortable() # raises AutosignTXError if no unsent TXs available
  112. if keypress_confirm(
  113. self.cfg,
  114. 'The following file{} will be securely deleted:\n{}\nOK?'.format(
  115. suf(files),
  116. fmt_list(map(str, files), fmt='col', indent=' '))):
  117. for f in files:
  118. msg(f'Shredding file ‘{f}’')
  119. from .fileutil import shred_file
  120. shred_file(f)
  121. sys.exit(0)
  122. else:
  123. die(1, 'Exiting at user request')
  124. async def get_last_created(self):
  125. from .tx import CompletedTX
  126. ext = '.' + Signable.automount_transaction.subext
  127. files = [f for f in self.dir.iterdir() if f.name.endswith(ext)]
  128. return sorted(
  129. [await CompletedTX(cfg=self.cfg, filename=str(txfile), quiet_open=True) for txfile in files],
  130. key = lambda x: x.timestamp)[-1]
  131. class transaction(base):
  132. desc = 'non-automount transaction'
  133. rawext = 'rawtx'
  134. sigext = 'sigtx'
  135. dir_name = 'tx_dir'
  136. fail_msg = 'failed to sign'
  137. async def sign(self,f):
  138. from .tx import UnsignedTX
  139. tx1 = UnsignedTX(
  140. cfg = self.cfg,
  141. filename = f,
  142. automount = self.name=='automount_transaction')
  143. if tx1.proto.sign_mode == 'daemon':
  144. from .rpc import rpc_init
  145. tx1.rpc = await rpc_init( self.cfg, tx1.proto, ignore_wallet=True )
  146. from .tx.sign import txsign
  147. tx2 = await txsign(
  148. cfg_parm = self.cfg,
  149. tx = tx1,
  150. seed_files = self.parent.wallet_files[:],
  151. kl = None,
  152. kal = None,
  153. passwd_file = str(self.parent.keyfile))
  154. if tx2:
  155. tx2.file.write(ask_write=False, outdir=self.dir)
  156. return tx2
  157. else:
  158. return False
  159. def print_summary(self,signables):
  160. if self.cfg.full_summary:
  161. bmsg('\nAutosign summary:\n')
  162. msg_r('\n'.join(tx.info.format(terse=True) for tx in signables))
  163. return
  164. def gen():
  165. for tx in signables:
  166. non_mmgen = [o for o in tx.outputs if not o.mmid]
  167. if non_mmgen:
  168. yield (tx,non_mmgen)
  169. body = list(gen())
  170. if body:
  171. bmsg('\nAutosign summary:')
  172. fs = '{} {} {}'
  173. t_wid,a_wid = 6,44
  174. def gen():
  175. yield fs.format('TX ID ','Non-MMGen outputs'+' '*(a_wid-17),'Amount')
  176. yield fs.format('-'*t_wid, '-'*a_wid, '-'*7)
  177. for tx,non_mmgen in body:
  178. for nm in non_mmgen:
  179. yield fs.format(
  180. tx.txid.fmt( width=t_wid, color=True ) if nm is non_mmgen[0] else ' '*t_wid,
  181. nm.addr.fmt( width=a_wid, color=True ),
  182. nm.amt.hl() + ' ' + yellow(tx.coin))
  183. msg('\n' + '\n'.join(gen()))
  184. else:
  185. msg('\nNo non-MMGen outputs')
  186. def gen_bad_list(self,bad_files):
  187. for f in bad_files:
  188. yield red(f.name)
  189. class automount_transaction(transaction):
  190. desc = 'automount transaction'
  191. dir_name = 'txauto_dir'
  192. rawext = 'arawtx'
  193. sigext = 'asigtx'
  194. subext = 'asubtx'
  195. multiple_ok = False
  196. class xmr_signable(transaction): # mixin class
  197. def need_daemon_restart(self,m,new_idx):
  198. old_idx = self.parent.xmr_cur_wallet_idx
  199. self.parent.xmr_cur_wallet_idx = new_idx
  200. return old_idx != new_idx or m.wd.state != 'ready'
  201. def print_summary(self,signables):
  202. bmsg('\nAutosign summary:')
  203. msg('\n'.join(s.get_info(indent=' ') for s in signables) + self.summary_footer)
  204. class xmr_transaction(xmr_signable):
  205. dir_name = 'xmr_tx_dir'
  206. desc = 'Monero transaction'
  207. subext = 'subtx'
  208. multiple_ok = False
  209. summary_footer = ''
  210. async def sign(self,f):
  211. from .xmrwallet import MoneroMMGenTX,MoneroWalletOps,xmrwallet_uargs
  212. tx1 = MoneroMMGenTX.Completed( self.parent.xmrwallet_cfg, f )
  213. m = MoneroWalletOps.sign(
  214. self.parent.xmrwallet_cfg,
  215. xmrwallet_uargs(
  216. infile = str(self.parent.wallet_files[0]), # MMGen wallet file
  217. wallets = str(tx1.src_wallet_idx),
  218. spec = None ),
  219. )
  220. tx2 = await m.main( f, restart_daemon=self.need_daemon_restart(m,tx1.src_wallet_idx) )
  221. tx2.write(ask_write=False)
  222. return tx2
  223. class xmr_wallet_outputs_file(xmr_signable):
  224. desc = 'Monero wallet outputs file'
  225. rawext = 'raw'
  226. sigext = 'sig'
  227. dir_name = 'xmr_outputs_dir'
  228. clean_all = True
  229. summary_footer = '\n'
  230. @property
  231. def unsigned(self):
  232. import json
  233. return tuple(
  234. f for f in super().unsigned
  235. if not json.loads(f.read_text())['MoneroMMGenWalletOutputsFile']['data']['imported'])
  236. async def sign(self,f):
  237. from .xmrwallet import MoneroWalletOps,xmrwallet_uargs
  238. wallet_idx = MoneroWalletOps.wallet.get_idx_from_fn(f)
  239. m = MoneroWalletOps.import_outputs(
  240. self.parent.xmrwallet_cfg,
  241. xmrwallet_uargs(
  242. infile = str(self.parent.wallet_files[0]), # MMGen wallet file
  243. wallets = str(wallet_idx),
  244. spec = None ),
  245. )
  246. obj = await m.main(f, wallet_idx, restart_daemon=self.need_daemon_restart(m,wallet_idx))
  247. obj.write(quiet=not obj.data.sign)
  248. self.action_desc = 'imported and signed' if obj.data.sign else 'imported'
  249. return obj
  250. class message(base):
  251. desc = 'message file'
  252. rawext = 'rawmsg.json'
  253. sigext = 'sigmsg.json'
  254. dir_name = 'msg_dir'
  255. fail_msg = 'failed to sign or signed incompletely'
  256. async def sign(self,f):
  257. from .msg import UnsignedMsg,SignedMsg
  258. m = UnsignedMsg( self.cfg, infile=f )
  259. await m.sign(wallet_files=self.parent.wallet_files[:], passwd_file=str(self.parent.keyfile))
  260. m = SignedMsg( self.cfg, data=m.__dict__ )
  261. m.write_to_file(
  262. outdir = self.dir.resolve(),
  263. ask_overwrite = False )
  264. if m.data.get('failed_sids'):
  265. die('MsgFileFailedSID',f'Failed Seed IDs: {fmt_list(m.data["failed_sids"],fmt="bare")}')
  266. return m
  267. def print_summary(self,signables):
  268. gmsg('\nSigned message files:')
  269. for message in signables:
  270. gmsg(' ' + message.signed_filename)
  271. def gen_bad_list(self,bad_files):
  272. for f in bad_files:
  273. sigfile = f.parent / ( f.name[:-len(self.rawext)] + self.sigext )
  274. yield orange(sigfile.name) if sigfile.exists() else red(f.name)
  275. class Autosign:
  276. dfl_mountpoint = '/mnt/mmgen_autosign'
  277. dfl_wallet_dir = '/dev/shm/autosign'
  278. old_dfl_mountpoint = '/mnt/tx'
  279. dfl_dev_label_dir = '/dev/disk/by-label'
  280. dev_label = 'MMGEN_TX'
  281. old_dfl_mountpoint_errmsg = f"""
  282. Mountpoint '{old_dfl_mountpoint}' is no longer supported!
  283. Please rename '{old_dfl_mountpoint}' to '{dfl_mountpoint}'
  284. and update your fstab accordingly.
  285. """
  286. mountpoint_errmsg_fs = """
  287. Mountpoint '{}' does not exist or does not point
  288. to a directory! Please create the mountpoint and add an entry
  289. to your fstab as described in this script’s help text.
  290. """
  291. mn_fmts = {
  292. 'mmgen': 'words',
  293. 'bip39': 'bip39',
  294. }
  295. dfl_mn_fmt = 'mmgen'
  296. non_xmr_dirs = {
  297. 'tx_dir': 'tx',
  298. 'txauto_dir': 'txauto',
  299. 'msg_dir': 'msg',
  300. }
  301. xmr_dirs = {
  302. 'xmr_dir': 'xmr',
  303. 'xmr_tx_dir': 'xmr/tx',
  304. 'xmr_outputs_dir': 'xmr/outputs',
  305. }
  306. have_xmr = False
  307. xmr_only = False
  308. def init_cfg(self): # see test/overlay/fakemods/mmgen/autosign.py
  309. self.mountpoint = Path(self.cfg.mountpoint or self.dfl_mountpoint)
  310. self.wallet_dir = Path(self.cfg.wallet_dir or self.dfl_wallet_dir)
  311. self.dev_label_path = Path(self.dfl_dev_label_dir) / self.dev_label
  312. self.mount_cmd = 'mount'
  313. self.umount_cmd = 'umount'
  314. def __init__(self,cfg,cmd=None):
  315. if cfg.mnemonic_fmt:
  316. if cfg.mnemonic_fmt not in self.mn_fmts:
  317. die(1,'{!r}: invalid mnemonic format (must be one of: {})'.format(
  318. cfg.mnemonic_fmt,
  319. fmt_list( self.mn_fmts, fmt='no_spc' ) ))
  320. self.cfg = cfg
  321. self.init_cfg()
  322. self.keyfile = self.mountpoint / 'autosign.key'
  323. if any(k in cfg._uopts for k in ('help','longhelp')):
  324. return
  325. if 'coin' in cfg._uopts:
  326. die(1,'--coin option not supported with this command. Use --coins instead')
  327. self.coins = cfg.coins.upper().split(',') if cfg.coins else []
  328. if cfg.xmrwallets and not 'XMR' in self.coins:
  329. self.coins.append('XMR')
  330. if not self.coins and cmd not in ('gen_key','wipe_key'):
  331. ymsg('Warning: no coins specified, defaulting to BTC')
  332. self.coins = ['BTC']
  333. if 'XMR' in self.coins:
  334. self.have_xmr = True
  335. if len(self.coins) == 1:
  336. self.xmr_only = True
  337. self.xmr_cur_wallet_idx = None
  338. self.dirs = {}
  339. self.signables = ()
  340. if not self.xmr_only:
  341. self.dirs |= self.non_xmr_dirs
  342. self.signables += Signable.non_xmr_signables
  343. if self.have_xmr:
  344. self.dirs |= self.xmr_dirs
  345. self.signables += Signable.xmr_signables
  346. for name,path in self.dirs.items():
  347. setattr(self, name, self.mountpoint / path)
  348. async def check_daemons_running(self):
  349. from .protocol import init_proto
  350. for coin in self.coins:
  351. proto = init_proto(self.cfg, coin, network=self.cfg.network, need_amt=True)
  352. if proto.sign_mode == 'daemon':
  353. self.cfg._util.vmsg(f'Checking {coin} daemon')
  354. from .rpc import rpc_init
  355. from .exception import SocketError
  356. try:
  357. await rpc_init( self.cfg, proto, ignore_wallet=True )
  358. except SocketError as e:
  359. from .daemon import CoinDaemon
  360. d = CoinDaemon( self.cfg, proto=proto, test_suite=self.cfg.test_suite )
  361. die(2,
  362. f'\n{e}\nIs the {d.coind_name} daemon ({d.exec_fn}) running '
  363. + 'and listening on the correct port?' )
  364. @property
  365. def wallet_files(self):
  366. if not hasattr(self,'_wallet_files'):
  367. try:
  368. dirlist = self.wallet_dir.iterdir()
  369. except:
  370. die(1,f"Cannot open wallet directory '{self.wallet_dir}'. Did you run ‘mmgen-autosign setup’?")
  371. self._wallet_files = [f for f in dirlist if f.suffix == '.mmdat']
  372. if not self._wallet_files:
  373. die(1,'No wallet files present!')
  374. return self._wallet_files
  375. def do_mount(self, silent=False):
  376. def check_or_create(dirname):
  377. path = getattr(self, dirname)
  378. if path.is_dir():
  379. if not path.stat().st_mode & S_IWUSR|S_IRUSR == S_IWUSR|S_IRUSR:
  380. die(1, f'‘{path}’ is not read/write for this user!')
  381. elif path.exists():
  382. die(1, f'‘{path}’ is not a directory!')
  383. elif path.is_symlink():
  384. die(1, f'‘{path}’ is a symlink not pointing to a directory!')
  385. else:
  386. msg(f'Creating ‘{path}’')
  387. path.mkdir(parents=True)
  388. if not self.mountpoint.is_dir():
  389. def do_die(m):
  390. die(1,'\n' + yellow(fmt(m.strip(),indent=' ')))
  391. if Path(self.old_dfl_mountpoint).is_dir():
  392. do_die(self.old_dfl_mountpoint_errmsg)
  393. else:
  394. do_die(self.mountpoint_errmsg_fs.format(self.mountpoint))
  395. if not self.mountpoint.is_mount():
  396. if run(
  397. self.mount_cmd.split() + [str(self.mountpoint)],
  398. stderr = DEVNULL,
  399. stdout = DEVNULL).returncode == 0:
  400. if not silent:
  401. msg(f"Mounting '{self.mountpoint}'")
  402. else:
  403. die(1,f"Unable to mount device at '{self.mountpoint}'")
  404. for dirname in self.dirs:
  405. check_or_create(dirname)
  406. def do_umount(self,silent=False):
  407. if self.mountpoint.is_mount():
  408. run( ['sync'], check=True )
  409. if not silent:
  410. msg(f"Unmounting '{self.mountpoint}'")
  411. run(self.umount_cmd.split() + [str(self.mountpoint)], check=True)
  412. if not silent:
  413. bmsg('It is now safe to extract the removable device')
  414. def decrypt_wallets(self):
  415. msg(f"Unlocking wallet{suf(self.wallet_files)} with key from ‘{self.keyfile}’")
  416. fails = 0
  417. for wf in self.wallet_files:
  418. try:
  419. Wallet(self.cfg, wf, ignore_in_fmt=True, passwd_file=str(self.keyfile))
  420. except SystemExit as e:
  421. if e.code != 0:
  422. fails += 1
  423. return not fails
  424. async def sign_all(self,target_name):
  425. target = getattr(Signable,target_name)(self)
  426. if target.unsigned:
  427. good = []
  428. bad = []
  429. if len(target.unsigned) > 1 and not target.multiple_ok:
  430. ymsg(f'Autosign error: only one unsigned {target.desc} transaction allowed at a time!')
  431. target.print_bad_list(target.unsigned)
  432. return False
  433. for f in target.unsigned:
  434. ret = None
  435. try:
  436. ret = await target.sign(f)
  437. except Exception as e:
  438. ymsg(f"An error occurred with {target.desc} '{f.name}':\n {type(e).__name__}: {e!s}")
  439. except:
  440. ymsg(f"An error occurred with {target.desc} '{f.name}'")
  441. good.append(ret) if ret else bad.append(f)
  442. self.cfg._util.qmsg('')
  443. await asyncio.sleep(0.3)
  444. msg(brown(f'{len(good)} {target.desc}{suf(good)} {target.action_desc}'))
  445. if bad:
  446. rmsg(f'{len(bad)} {target.desc}{suf(bad)} {target.fail_msg}')
  447. if good and not self.cfg.no_summary:
  448. target.print_summary(good)
  449. if bad:
  450. target.print_bad_list(bad)
  451. return not bad
  452. else:
  453. return f'No unsigned {target.desc}s'
  454. async def do_sign(self):
  455. if not self.cfg.stealth_led:
  456. self.led.set('busy')
  457. self.do_mount()
  458. key_ok = self.decrypt_wallets()
  459. if key_ok:
  460. if self.cfg.stealth_led:
  461. self.led.set('busy')
  462. ret = [await self.sign_all(signable) for signable in self.signables]
  463. for val in ret:
  464. if isinstance(val,str):
  465. msg(val)
  466. if self.cfg.test_suite_autosign_threaded:
  467. await asyncio.sleep(1)
  468. self.do_umount()
  469. self.led.set('error' if not all(ret) else 'off' if self.cfg.stealth_led else 'standby')
  470. return all(ret)
  471. else:
  472. msg('Password is incorrect!')
  473. self.do_umount()
  474. if not self.cfg.stealth_led:
  475. self.led.set('error')
  476. return False
  477. def wipe_encryption_key(self):
  478. if self.keyfile.exists():
  479. from .fileutil import shred_file
  480. ymsg(f'Shredding wallet encryption key ‘{self.keyfile}’')
  481. shred_file(self.keyfile, verbose=self.cfg.verbose)
  482. else:
  483. gmsg('No wallet encryption key on removable device')
  484. def create_key(self):
  485. desc = f"key file '{self.keyfile}'"
  486. msg('Creating ' + desc)
  487. try:
  488. self.keyfile.write_text( os.urandom(32).hex() )
  489. self.keyfile.chmod(0o400)
  490. except:
  491. die(2,'Unable to write ' + desc)
  492. msg('Wrote ' + desc)
  493. def gen_key(self,no_unmount=False):
  494. if not self.device_inserted:
  495. die(1,'Removable device not present!')
  496. self.do_mount()
  497. self.wipe_encryption_key()
  498. self.create_key()
  499. if not no_unmount:
  500. self.do_umount()
  501. def setup(self):
  502. def remove_wallet_dir():
  503. msg(f"Deleting '{self.wallet_dir}'")
  504. import shutil
  505. try:
  506. shutil.rmtree(self.wallet_dir)
  507. except:
  508. pass
  509. def create_wallet_dir():
  510. try:
  511. self.wallet_dir.mkdir(parents=True)
  512. except:
  513. pass
  514. try:
  515. self.wallet_dir.stat()
  516. except:
  517. die(2,f"Unable to create wallet directory '{self.wallet_dir}'")
  518. remove_wallet_dir()
  519. create_wallet_dir()
  520. self.gen_key(no_unmount=True)
  521. wf = find_file_in_dir( get_wallet_cls('mmgen'), self.cfg.data_dir )
  522. if wf and keypress_confirm(
  523. cfg = self.cfg,
  524. prompt = f"Default wallet '{wf}' found.\nUse default wallet for autosigning?",
  525. default_yes = True ):
  526. ss_in = Wallet( Config(), wf )
  527. else:
  528. ss_in = Wallet( self.cfg, in_fmt=self.mn_fmts[self.cfg.mnemonic_fmt or self.dfl_mn_fmt] )
  529. ss_out = Wallet( self.cfg, ss=ss_in, passwd_file=str(self.keyfile) )
  530. ss_out.write_to_file( desc='autosign wallet', outdir=self.wallet_dir )
  531. @property
  532. def xmrwallet_cfg(self):
  533. if not hasattr(self,'_xmrwallet_cfg'):
  534. self._xmrwallet_cfg = Config({
  535. '_clone': self.cfg,
  536. 'coin': 'xmr',
  537. 'wallet_rpc_user': 'autosign',
  538. 'wallet_rpc_password': 'autosign password',
  539. 'wallet_rpc_port': 23232 if self.cfg.test_suite_xmr_autosign else None,
  540. 'wallet_dir': str(self.wallet_dir),
  541. 'autosign': True,
  542. 'autosign_mountpoint': str(self.mountpoint),
  543. 'offline': True,
  544. 'passwd_file': str(self.keyfile),
  545. })
  546. return self._xmrwallet_cfg
  547. def xmr_setup(self):
  548. def create_signing_wallets():
  549. from .xmrwallet import MoneroWalletOps,xmrwallet_uargs
  550. if len(self.wallet_files) > 1:
  551. ymsg(f'Warning: more than one wallet file, using the first ({self.wallet_files[0]}) for xmrwallet generation')
  552. m = MoneroWalletOps.create_offline(
  553. self.xmrwallet_cfg,
  554. xmrwallet_uargs(
  555. infile = str(self.wallet_files[0]), # MMGen wallet file
  556. wallets = self.cfg.xmrwallets, # XMR wallet idxs
  557. spec = None ),
  558. )
  559. async_run(m.main())
  560. async_run(m.stop_wallet_daemon())
  561. self.clean_old_files()
  562. create_signing_wallets()
  563. def clean_old_files(self):
  564. def do_shred(f):
  565. nonlocal count
  566. msg_r('.')
  567. from .fileutil import shred_file
  568. shred_file( f, verbose=self.cfg.verbose )
  569. count += 1
  570. def clean_dir(s_name):
  571. def clean_files(rawext,sigext):
  572. for f in s.dir.iterdir():
  573. if s.clean_all and (f.name.endswith(f'.{rawext}') or f.name.endswith(f'.{sigext}')):
  574. do_shred(f)
  575. elif f.name.endswith(f'.{sigext}'):
  576. raw = f.parent / ( f.name[:-len(sigext)] + rawext )
  577. if raw.is_file():
  578. do_shred(raw)
  579. s = getattr(Signable,s_name)(self)
  580. msg_r(f"Cleaning directory '{s.dir}'..")
  581. if s.dir.is_dir():
  582. clean_files( s.rawext, s.sigext )
  583. if hasattr(s,'subext'):
  584. clean_files( s.rawext, s.subext )
  585. clean_files( s.sigext, s.subext )
  586. msg('done' if s.dir.is_dir() else 'skipped (no dir)')
  587. count = 0
  588. for s_name in self.signables:
  589. clean_dir(s_name)
  590. bmsg(f'{count} file{suf(count)} shredded')
  591. @property
  592. def device_inserted(self):
  593. if self.cfg.no_insert_check:
  594. return True
  595. return self.dev_label_path.exists()
  596. async def main_loop(self):
  597. if not self.cfg.stealth_led:
  598. self.led.set('standby')
  599. threaded = self.cfg.test_suite_autosign_threaded
  600. n = 1 if threaded else 0
  601. prev_status = False
  602. while True:
  603. status = self.device_inserted
  604. if status and not prev_status:
  605. msg('Device insertion detected')
  606. await self.do_sign()
  607. prev_status = status
  608. if not n % 10:
  609. msg_r(f"\r{' '*17}\rWaiting")
  610. await asyncio.sleep(0.2 if threaded else 1)
  611. if not threaded:
  612. msg_r('.')
  613. n += 1
  614. def at_exit(self,exit_val,message=None):
  615. if message:
  616. msg(message)
  617. self.led.stop()
  618. sys.exit(0 if self.cfg.test_suite_autosign_threaded else int(exit_val))
  619. def init_exit_handler(self):
  620. def handler(arg1,arg2):
  621. self.at_exit(1,'\nCleaning up...')
  622. import signal
  623. signal.signal( signal.SIGTERM, handler )
  624. signal.signal( signal.SIGINT, handler )
  625. def init_led(self):
  626. from .led import LEDControl
  627. self.led = LEDControl(
  628. enabled = self.cfg.led,
  629. simulate = self.cfg.test_suite_autosign_led_simulate )
  630. self.led.set('off')