autosign.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
  4. # Copyright (C)2013-2023 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
  9. # https://gitlab.com/mmgen/mmgen
  10. """
  11. autosign: Auto-sign MMGen transactions, message files and XMR wallet output files
  12. """
  13. import sys,os,asyncio
  14. from pathlib import Path
  15. from subprocess import run,PIPE,DEVNULL
  16. from collections import namedtuple
  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
  20. from .wallet import Wallet,get_wallet_cls
  21. from .filename import find_file_in_dir
  22. from .ui import keypress_confirm
  23. class AutosignConfig(Config):
  24. _set_ok = ('usr_randchars','_proto','outdir','passwd_file')
  25. class Signable:
  26. signables = ('transaction','message','xmr_transaction','xmr_wallet_outputs_file')
  27. class base:
  28. clean_all = False
  29. multiple_ok = True
  30. def __init__(self,parent):
  31. self.parent = parent
  32. self.cfg = parent.cfg
  33. self.dir = getattr(parent,self.dir_name)
  34. @property
  35. def unsigned(self):
  36. return self._unprocessed( '_unsigned', self.rawext, self.sigext )
  37. @property
  38. def unsubmitted(self):
  39. return self._unprocessed( '_unsubmitted', self.sigext, self.subext )
  40. def _unprocessed(self,attrname,rawext,sigext):
  41. if not hasattr(self,attrname):
  42. dirlist = sorted(self.dir.iterdir())
  43. names = {f.name for f in dirlist}
  44. setattr(
  45. self,
  46. attrname,
  47. tuple(f for f in dirlist
  48. if f.name.endswith('.' + rawext)
  49. and f.name[:-len(rawext)] + sigext not in names) )
  50. return getattr(self,attrname)
  51. def print_bad_list(self,bad_files):
  52. msg('\n{a}\n{b}'.format(
  53. a = red(f'Failed {self.desc}s:'),
  54. b = ' {}\n'.format('\n '.join(self.gen_bad_list(sorted(bad_files,key=lambda f: f.name))))
  55. ))
  56. class transaction(base):
  57. desc = 'transaction'
  58. rawext = 'rawtx'
  59. sigext = 'sigtx'
  60. dir_name = 'tx_dir'
  61. fail_msg = 'failed to sign'
  62. async def sign(self,f):
  63. from .tx import UnsignedTX
  64. tx1 = UnsignedTX( cfg=self.cfg, filename=f )
  65. if tx1.proto.sign_mode == 'daemon':
  66. from .rpc import rpc_init
  67. tx1.rpc = await rpc_init( self.cfg, tx1.proto )
  68. from .tx.sign import txsign
  69. tx2 = await txsign( self.cfg, tx1, self.parent.wallet_files[:], None, None )
  70. if tx2:
  71. tx2.file.write(ask_write=False)
  72. return tx2
  73. else:
  74. return False
  75. def print_summary(self,txs):
  76. if self.cfg.full_summary:
  77. bmsg('\nAutosign summary:\n')
  78. msg_r('\n'.join(tx.info.format(terse=True) for tx in txs))
  79. return
  80. def gen():
  81. for tx in txs:
  82. non_mmgen = [o for o in tx.outputs if not o.mmid]
  83. if non_mmgen:
  84. yield (tx,non_mmgen)
  85. body = list(gen())
  86. if body:
  87. bmsg('\nAutosign summary:')
  88. fs = '{} {} {}'
  89. t_wid,a_wid = 6,44
  90. def gen():
  91. yield fs.format('TX ID ','Non-MMGen outputs'+' '*(a_wid-17),'Amount')
  92. yield fs.format('-'*t_wid, '-'*a_wid, '-'*7)
  93. for tx,non_mmgen in body:
  94. for nm in non_mmgen:
  95. yield fs.format(
  96. tx.txid.fmt( width=t_wid, color=True ) if nm is non_mmgen[0] else ' '*t_wid,
  97. nm.addr.fmt( width=a_wid, color=True ),
  98. nm.amt.hl() + ' ' + yellow(tx.coin))
  99. msg('\n' + '\n'.join(gen()))
  100. else:
  101. msg('\nNo non-MMGen outputs')
  102. def gen_bad_list(self,bad_files):
  103. for f in bad_files:
  104. yield red(f.name)
  105. class xmr_signable(transaction): # virtual class
  106. def need_daemon_restart(self,m,new_idx):
  107. old_idx = self.parent.xmr_cur_wallet_idx
  108. self.parent.xmr_cur_wallet_idx = new_idx
  109. return old_idx != new_idx or m.wd.state != 'ready'
  110. def print_summary(self,signables):
  111. bmsg('\nAutosign summary:')
  112. msg('\n'.join(s.get_info(indent=' ') for s in signables) + self.summary_footer)
  113. class xmr_transaction(xmr_signable):
  114. dir_name = 'xmr_tx_dir'
  115. desc = 'Monero transaction'
  116. subext = 'subtx'
  117. multiple_ok = False
  118. summary_footer = ''
  119. async def sign(self,f):
  120. from .xmrwallet import MoneroMMGenTX,MoneroWalletOps,xmrwallet_uargs
  121. tx1 = MoneroMMGenTX.Completed( self.parent.xmrwallet_cfg, f )
  122. m = MoneroWalletOps.sign(
  123. self.parent.xmrwallet_cfg,
  124. xmrwallet_uargs(
  125. infile = str(self.parent.wallet_files[0]), # MMGen wallet file
  126. wallets = str(tx1.src_wallet_idx),
  127. spec = None ),
  128. )
  129. tx2 = await m.main( f, restart_daemon=self.need_daemon_restart(m,tx1.src_wallet_idx) )
  130. tx2.write(ask_write=False)
  131. return tx2
  132. class xmr_wallet_outputs_file(xmr_signable):
  133. desc = 'Monero wallet outputs file'
  134. rawext = 'raw'
  135. sigext = 'sig'
  136. dir_name = 'xmr_outputs_dir'
  137. clean_all = True
  138. summary_footer = '\n'
  139. async def sign(self,f):
  140. from .xmrwallet import MoneroWalletOps,xmrwallet_uargs
  141. wallet_idx = MoneroWalletOps.wallet.get_idx_from_fn(f)
  142. m = MoneroWalletOps.export_key_images(
  143. self.parent.xmrwallet_cfg,
  144. xmrwallet_uargs(
  145. infile = str(self.parent.wallet_files[0]), # MMGen wallet file
  146. wallets = str(wallet_idx),
  147. spec = None ),
  148. )
  149. obj = await m.main( f, wallet_idx, restart_daemon=self.need_daemon_restart(m,wallet_idx) )
  150. obj.write()
  151. return obj
  152. class message(base):
  153. desc = 'message file'
  154. rawext = 'rawmsg.json'
  155. sigext = 'sigmsg.json'
  156. dir_name = 'msg_dir'
  157. fail_msg = 'failed to sign or signed incompletely'
  158. async def sign(self,f):
  159. from .msg import UnsignedMsg,SignedMsg
  160. m = UnsignedMsg( self.cfg, infile=f )
  161. await m.sign( wallet_files=self.parent.wallet_files[:] )
  162. m = SignedMsg( self.cfg, data=m.__dict__ )
  163. m.write_to_file(
  164. outdir = self.dir.resolve(),
  165. ask_overwrite = False )
  166. if m.data.get('failed_sids'):
  167. die('MsgFileFailedSID',f'Failed Seed IDs: {fmt_list(m.data["failed_sids"],fmt="bare")}')
  168. return m
  169. def print_summary(self,messages):
  170. gmsg('\nSigned message files:')
  171. for m in messages:
  172. gmsg(' ' + m.signed_filename)
  173. def gen_bad_list(self,bad_files):
  174. for f in bad_files:
  175. sigfile = f.parent / ( f.name[:-len(self.rawext)] + self.sigext )
  176. yield orange(sigfile.name) if sigfile.exists() else red(f.name)
  177. class Autosign:
  178. dfl_mountpoint = '/mnt/mmgen_autosign'
  179. dfl_wallet_dir = '/dev/shm/autosign'
  180. old_dfl_mountpoint = '/mnt/tx'
  181. dfl_dev_disk_path = '/dev/disk/by-label/MMGEN_TX'
  182. fake_dev_disk_path = '/tmp/mmgen-test-suite-dev.disk.by-label.MMGEN_TX'
  183. old_dfl_mountpoint_errmsg = f"""
  184. Mountpoint '{old_dfl_mountpoint}' is no longer supported!
  185. Please rename '{old_dfl_mountpoint}' to '{dfl_mountpoint}'
  186. and update your fstab accordingly.
  187. """
  188. mountpoint_errmsg_fs = """
  189. Mountpoint '{}' does not exist or does not point
  190. to a directory! Please create the mountpoint and add an entry
  191. to your fstab as described in this script’s help text.
  192. """
  193. mn_fmts = {
  194. 'mmgen': 'words',
  195. 'bip39': 'bip39',
  196. }
  197. dfl_mn_fmt = 'mmgen'
  198. have_msg_dir = False
  199. def __init__(self,cfg):
  200. self.cfg = cfg
  201. if cfg.mnemonic_fmt:
  202. if cfg.mnemonic_fmt not in self.mn_fmts:
  203. die(1,'{!r}: invalid mnemonic format (must be one of: {})'.format(
  204. cfg.mnemonic_fmt,
  205. fmt_list( self.mn_fmts, fmt='no_spc' ) ))
  206. self.dev_disk_path = Path(
  207. self.fake_dev_disk_path if cfg.test_suite_xmr_autosign else
  208. self.dfl_dev_disk_path )
  209. self.mountpoint = Path(cfg.mountpoint or self.dfl_mountpoint)
  210. self.wallet_dir = Path(cfg.wallet_dir or self.dfl_wallet_dir)
  211. self.tx_dir = self.mountpoint / 'tx'
  212. self.msg_dir = self.mountpoint / 'msg'
  213. self.keyfile = self.mountpoint / 'autosign.key'
  214. cfg.outdir = str(self.tx_dir)
  215. cfg.passwd_file = str(self.keyfile)
  216. if any(k in cfg._uopts for k in ('help','longhelp')):
  217. return
  218. if 'coin' in cfg._uopts:
  219. die(1,'--coin option not supported with this command. Use --coins instead')
  220. self.coins = cfg.coins.upper().split(',') if cfg.coins else []
  221. if cfg._args and cfg._args[0] == 'clean':
  222. return
  223. if cfg.xmrwallets and not 'XMR' in self.coins:
  224. self.coins.append('XMR')
  225. if not self.coins:
  226. ymsg('Warning: no coins specified, defaulting to BTC')
  227. self.coins = ['BTC']
  228. if 'XMR' in self.coins:
  229. self.xmr_dir = self.mountpoint / 'xmr'
  230. self.xmr_tx_dir = self.mountpoint / 'xmr' / 'tx'
  231. self.xmr_outputs_dir = self.mountpoint / 'xmr' / 'outputs'
  232. self.xmr_cur_wallet_idx = None
  233. async def check_daemons_running(self):
  234. from .protocol import init_proto
  235. for coin in self.coins:
  236. proto = init_proto( self.cfg, coin, testnet=self.cfg.network=='testnet', need_amt=True )
  237. if proto.sign_mode == 'daemon':
  238. self.cfg._util.vmsg(f'Checking {coin} daemon')
  239. from .rpc import rpc_init
  240. from .exception import SocketError
  241. try:
  242. await rpc_init( self.cfg, proto )
  243. except SocketError as e:
  244. from .daemon import CoinDaemon
  245. d = CoinDaemon( self.cfg, proto=proto, test_suite=self.cfg.test_suite )
  246. die(2,
  247. f'\n{e}\nIs the {d.coind_name} daemon ({d.exec_fn}) running '
  248. + 'and listening on the correct port?' )
  249. @property
  250. def wallet_files(self):
  251. if not hasattr(self,'_wallet_files'):
  252. try:
  253. dirlist = self.wallet_dir.iterdir()
  254. except:
  255. die(1,f"Cannot open wallet directory '{self.wallet_dir}'. Did you run ‘mmgen-autosign setup’?")
  256. self._wallet_files = [f for f in dirlist if f.suffix == '.mmdat']
  257. if not self._wallet_files:
  258. die(1,'No wallet files present!')
  259. return self._wallet_files
  260. def do_mount(self,no_xmr_chk=False):
  261. from stat import S_ISDIR,S_IWUSR,S_IRUSR
  262. def check_dir(cdir):
  263. try:
  264. ds = cdir.stat()
  265. assert S_ISDIR(ds.st_mode), f"'{cdir}' is not a directory!"
  266. assert ds.st_mode & S_IWUSR|S_IRUSR == S_IWUSR|S_IRUSR, f"'{cdir}' is not read/write for this user!"
  267. except:
  268. die(1,f"'{cdir}' missing or not read/writable by user!")
  269. if not self.mountpoint.is_dir():
  270. def do_die(m):
  271. die(1,'\n' + yellow(fmt(m.strip(),indent=' ')))
  272. if Path(self.old_dfl_mountpoint).is_dir():
  273. do_die(self.old_dfl_mountpoint_errmsg)
  274. else:
  275. do_die(self.mountpoint_errmsg_fs.format(self.mountpoint))
  276. if not self.mountpoint.is_mount():
  277. if run( ['mount',self.mountpoint], stderr=DEVNULL, stdout=DEVNULL ).returncode == 0:
  278. msg(f"Mounting '{self.mountpoint}'")
  279. elif not self.cfg.test_suite:
  280. die(1,f"Unable to mount device at '{self.mountpoint}'")
  281. self.have_msg_dir = self.msg_dir.is_dir()
  282. check_dir(self.tx_dir)
  283. if self.have_msg_dir:
  284. check_dir(self.msg_dir)
  285. if 'XMR' in self.coins and not no_xmr_chk:
  286. check_dir(self.xmr_tx_dir)
  287. def do_umount(self):
  288. if self.mountpoint.is_mount():
  289. run( ['sync'], check=True )
  290. msg(f"Unmounting '{self.mountpoint}'")
  291. run( ['umount',self.mountpoint], check=True )
  292. bmsg('It is now safe to extract the removable device')
  293. def decrypt_wallets(self):
  294. msg(f"Unlocking wallet{suf(self.wallet_files)} with key from '{self.cfg.passwd_file}'")
  295. fails = 0
  296. for wf in self.wallet_files:
  297. try:
  298. Wallet( self.cfg, wf, ignore_in_fmt=True )
  299. except SystemExit as e:
  300. if e.code != 0:
  301. fails += 1
  302. return False if fails else True
  303. async def sign_all(self,target_name):
  304. target = getattr(Signable,target_name)(self)
  305. if target.unsigned:
  306. good = []
  307. bad = []
  308. if len(target.unsigned) > 1 and not target.multiple_ok:
  309. ymsg(f'Autosign error: only one unsigned {target.desc} transaction allowed at a time!')
  310. target.print_bad_list(target.unsigned)
  311. return False
  312. for f in target.unsigned:
  313. ret = None
  314. try:
  315. ret = await target.sign(f)
  316. except Exception as e:
  317. ymsg(f"An error occurred with {target.desc} '{f.name}':\n {type(e).__name__}: {e!s}")
  318. except:
  319. ymsg(f"An error occurred with {target.desc} '{f.name}'")
  320. good.append(ret) if ret else bad.append(f)
  321. self.cfg._util.qmsg('')
  322. await asyncio.sleep(0.3)
  323. msg(f'{len(good)} {target.desc}{suf(good)} signed')
  324. if bad:
  325. rmsg(f'{len(bad)} {target.desc}{suf(bad)} {target.fail_msg}')
  326. if good and not self.cfg.no_summary:
  327. target.print_summary(good)
  328. if bad:
  329. target.print_bad_list(bad)
  330. return not bad
  331. else:
  332. msg(f'No unsigned {target.desc}s')
  333. await asyncio.sleep(0.5)
  334. return True
  335. async def do_sign(self):
  336. if not self.cfg.stealth_led:
  337. self.led.set('busy')
  338. self.do_mount()
  339. key_ok = self.decrypt_wallets()
  340. if key_ok:
  341. if self.cfg.stealth_led:
  342. self.led.set('busy')
  343. ret1 = await self.sign_all('transaction')
  344. ret2 = await self.sign_all('message') if self.have_msg_dir else True
  345. # import XMR wallet outputs BEFORE signing transactions:
  346. ret3 = await self.sign_all('xmr_wallet_outputs_file') if 'XMR' in self.coins else True
  347. ret4 = await self.sign_all('xmr_transaction') if 'XMR' in self.coins else True
  348. ret = ret1 and ret2 and ret3 and ret4
  349. self.do_umount()
  350. self.led.set(('standby','off','error')[(not ret)*2 or bool(self.cfg.stealth_led)])
  351. return ret
  352. else:
  353. msg('Password is incorrect!')
  354. self.do_umount()
  355. if not self.cfg.stealth_led:
  356. self.led.set('error')
  357. return False
  358. def wipe_existing_key(self):
  359. try: self.keyfile.stat()
  360. except: pass
  361. else:
  362. from .fileutil import shred_file
  363. msg(f"\nShredding existing key '{self.keyfile}'")
  364. shred_file( self.keyfile, verbose=self.cfg.verbose )
  365. def create_key(self):
  366. desc = f"key file '{self.keyfile}'"
  367. msg('Creating ' + desc)
  368. try:
  369. self.keyfile.write_text( os.urandom(32).hex() )
  370. self.keyfile.chmod(0o400)
  371. except:
  372. die(2,'Unable to write ' + desc)
  373. msg('Wrote ' + desc)
  374. def gen_key(self,no_unmount=False):
  375. if not self.get_insert_status():
  376. die(1,'Removable device not present!')
  377. self.do_mount(no_xmr_chk=True)
  378. self.wipe_existing_key()
  379. self.create_key()
  380. if not no_unmount:
  381. self.do_umount()
  382. def setup(self):
  383. def remove_wallet_dir():
  384. msg(f"Deleting '{self.wallet_dir}'")
  385. import shutil
  386. try: shutil.rmtree(self.wallet_dir)
  387. except: pass
  388. def create_wallet_dir():
  389. try: self.wallet_dir.mkdir(parents=True)
  390. except: pass
  391. try: self.wallet_dir.stat()
  392. except: die(2,f"Unable to create wallet directory '{self.wallet_dir}'")
  393. remove_wallet_dir()
  394. create_wallet_dir()
  395. self.gen_key(no_unmount=True)
  396. wf = find_file_in_dir( get_wallet_cls('mmgen'), self.cfg.data_dir )
  397. if wf and keypress_confirm(
  398. cfg = self.cfg,
  399. prompt = f"Default wallet '{wf}' found.\nUse default wallet for autosigning?",
  400. default_yes = True ):
  401. from .cfg import Config
  402. ss_in = Wallet( Config(), wf )
  403. else:
  404. ss_in = Wallet( self.cfg, in_fmt=self.mn_fmts[self.cfg.mnemonic_fmt or self.dfl_mn_fmt] )
  405. ss_out = Wallet( self.cfg, ss=ss_in )
  406. ss_out.write_to_file( desc='autosign wallet', outdir=self.wallet_dir )
  407. @property
  408. def xmrwallet_cfg(self):
  409. if not hasattr(self,'_xmrwallet_cfg'):
  410. from .cfg import Config
  411. self._xmrwallet_cfg = Config({
  412. '_clone': self.cfg,
  413. 'coin': 'xmr',
  414. 'wallet_rpc_user': 'autosign',
  415. 'wallet_rpc_password': 'autosign password',
  416. 'wallet_rpc_port': 23232 if self.cfg.test_suite_xmr_autosign else None,
  417. 'wallet_dir': str(self.wallet_dir),
  418. 'autosign': True,
  419. 'autosign_mountpoint': str(self.mountpoint),
  420. 'outdir': str(self.xmr_dir), # required by vkal.write()
  421. })
  422. return self._xmrwallet_cfg
  423. def xmr_setup(self):
  424. def create_signing_wallets():
  425. from .xmrwallet import MoneroWalletOps,xmrwallet_uargs
  426. if len(self.wallet_files) > 1:
  427. ymsg(f'Warning: more than one wallet file, using the first ({self.wallet_files[0]}) for xmrwallet generation')
  428. m = MoneroWalletOps.create_offline(
  429. self.xmrwallet_cfg,
  430. xmrwallet_uargs(
  431. infile = str(self.wallet_files[0]), # MMGen wallet file
  432. wallets = self.cfg.xmrwallets, # XMR wallet idxs
  433. spec = None ),
  434. )
  435. async_run(m.main())
  436. async_run(m.stop_wallet_daemon())
  437. import shutil
  438. try: shutil.rmtree(self.xmr_outputs_dir)
  439. except: pass
  440. self.xmr_outputs_dir.mkdir(parents=True)
  441. self.xmr_tx_dir.mkdir(exist_ok=True)
  442. self.clean_old_files()
  443. create_signing_wallets()
  444. def clean_old_files(self):
  445. def do_shred(f):
  446. nonlocal count
  447. msg_r('.')
  448. shred_file( f, verbose=self.cfg.verbose )
  449. count += 1
  450. def clean_dir(s_name):
  451. def clean_files(rawext,sigext):
  452. for f in s.dir.iterdir():
  453. if s.clean_all and (f.name.endswith(f'.{rawext}') or f.name.endswith(f'.{sigext}')):
  454. do_shred(f)
  455. elif f.name.endswith(f'.{sigext}'):
  456. raw = f.parent / ( f.name[:-len(sigext)] + rawext )
  457. if raw.is_file():
  458. do_shred(raw)
  459. s = getattr(Signable,s_name)(asi)
  460. msg_r(f"Cleaning directory '{s.dir}'..")
  461. if s.dir.is_dir():
  462. clean_files( s.rawext, s.sigext )
  463. if hasattr(s,'subext'):
  464. clean_files( s.rawext, s.subext )
  465. clean_files( s.sigext, s.subext )
  466. msg('done' if s.dir.is_dir() else 'skipped (no dir)')
  467. asi = get_autosign_obj( self.cfg, 'btc,xmr' )
  468. count = 0
  469. from .fileutil import shred_file
  470. for s_name in Signable.signables:
  471. clean_dir(s_name)
  472. bmsg(f'{count} file{suf(count)} shredded')
  473. def get_insert_status(self):
  474. if self.cfg.no_insert_check:
  475. return True
  476. try: self.dev_disk_path.stat()
  477. except: return False
  478. else: return True
  479. async def do_loop(self):
  480. if not self.cfg.stealth_led:
  481. self.led.set('standby')
  482. testing_xmr = self.cfg.test_suite_xmr_autosign
  483. if testing_xmr:
  484. msg('Waiting for fake device insertion')
  485. n = 1 if testing_xmr else 0
  486. prev_status = False
  487. while True:
  488. status = self.get_insert_status()
  489. if status and not prev_status:
  490. msg('Device insertion detected')
  491. await self.do_sign()
  492. if testing_xmr:
  493. if self.dev_disk_path.exists():
  494. self.dev_disk_path.unlink()
  495. prev_status = status
  496. if not n % 10:
  497. msg_r(f"\r{' '*17}\rWaiting")
  498. await asyncio.sleep(1)
  499. if not testing_xmr:
  500. msg_r('.')
  501. n += 1
  502. def at_exit(self,exit_val,message=None):
  503. if message:
  504. msg(message)
  505. self.led.stop()
  506. sys.exit(0 if self.cfg.test_suite_xmr_autosign else int(exit_val))
  507. def init_exit_handler(self):
  508. def handler(arg1,arg2):
  509. self.at_exit(1,'\nCleaning up...')
  510. import signal
  511. signal.signal( signal.SIGTERM, handler )
  512. signal.signal( signal.SIGINT, handler )
  513. def init_led(self):
  514. from .led import LEDControl
  515. self.led = LEDControl(
  516. enabled = self.cfg.led,
  517. simulate = self.cfg.test_suite_autosign_led_simulate )
  518. self.led.set('off')
  519. def get_autosign_obj(cfg,coins=None):
  520. return Autosign(
  521. AutosignConfig({
  522. 'mountpoint': cfg.autosign_mountpoint or cfg.mountpoint,
  523. 'test_suite': cfg.test_suite,
  524. 'coins': coins if isinstance(coins,str) else ','.join(coins) if coins else 'btc',
  525. })
  526. )