opts.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. #!/usr/bin/env python
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2017 Philemon <mmgen-py@yandex.com>
  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. opts.py: MMGen-specific options processing after generic processing by share.Opts
  20. """
  21. import sys,os
  22. class opt(object): pass
  23. from mmgen.globalvars import g
  24. import mmgen.share.Opts
  25. from mmgen.util import *
  26. def usage(): Die(2,'USAGE: %s %s' % (g.prog_name, usage_txt))
  27. def die_on_incompatible_opts(incompat_list):
  28. for group in incompat_list:
  29. bad = [k for k in opt.__dict__ if opt.__dict__[k] and k in group]
  30. if len(bad) > 1:
  31. die(1,'Conflicting options: %s' % ', '.join([fmt_opt(b) for b in bad]))
  32. def fmt_opt(o): return '--' + o.replace('_','-')
  33. def _show_hash_presets():
  34. fs = ' {:<7} {:<6} {:<3} {}'
  35. msg('Available parameters for scrypt.hash():')
  36. msg(fs.format('Preset','N','r','p'))
  37. for i in sorted(g.hash_presets.keys()):
  38. msg(fs.format("'%s'" % i, *g.hash_presets[i]))
  39. msg('N = memory usage (power of two), p = iterations (rounds)')
  40. # most, but not all, of these set the corresponding global var
  41. common_opts_data = """
  42. --, --coin=c Choose coin unit. Default: {cu_dfl}. Options: {cu_all}
  43. --, --color=0|1 Disable or enable color output
  44. --, --force-256-color Force 256-color output when color is enabled
  45. --, --bitcoin-data-dir=d Specify Bitcoin data directory location 'd'
  46. --, --data-dir=d Specify {pnm} data directory location 'd'
  47. --, --no-license Suppress the GPL license prompt
  48. --, --rpc-host=h Communicate with bitcoind running on host 'h'
  49. --, --rpc-port=p Communicate with bitcoind listening on port 'p'
  50. --, --rpc-user=user Override 'rpcuser' in bitcoin.conf
  51. --, --rpc-password=pass Override 'rpcpassword' in bitcoin.conf
  52. --, --regtest=0|1 Disable or enable regtest mode
  53. --, --testnet=0|1 Disable or enable testnet
  54. --, --skip-cfg-file Skip reading the configuration file
  55. --, --version Print version information and exit
  56. --, --bob Switch to user "Bob" in MMGen regtest setup
  57. --, --alice Switch to user "Alice" in MMGen regtest setup
  58. """.format(
  59. pnm=g.proj_name,
  60. cu_dfl=g.coin,
  61. cu_all=' '.join(g.coins),
  62. )
  63. def opt_preproc_debug(short_opts,long_opts,skipped_opts,uopts,args):
  64. d = (
  65. ('Cmdline', ' '.join(sys.argv)),
  66. ('Short opts', short_opts),
  67. ('Long opts', long_opts),
  68. ('Skipped opts', skipped_opts),
  69. ('User-selected opts', uopts),
  70. ('Cmd args', args),
  71. )
  72. Msg('\n=== opts.py debug ===')
  73. for e in d: Msg(' {:<20}: {}'.format(*e))
  74. def opt_postproc_debug():
  75. opt.verbose,opt.quiet = True,None
  76. a = [k for k in dir(opt) if k[:2] != '__' and getattr(opt,k) != None]
  77. b = [k for k in dir(opt) if k[:2] != '__' and getattr(opt,k) == None]
  78. Msg(' Opts after processing:')
  79. for k in a:
  80. v = getattr(opt,k)
  81. Msg(' %-18s: %-6s [%s]' % (k,v,type(v).__name__))
  82. Msg(" Opts set to 'None':")
  83. Msg(' %s\n' % '\n '.join(b))
  84. Msg(' Global vars:')
  85. for e in [d for d in dir(g) if d[:2] != '__']:
  86. Msg(' {:<20}: {}'.format(e, getattr(g,e)))
  87. Msg('\n=== end opts.py debug ===\n')
  88. def opt_postproc_initializations():
  89. from mmgen.term import set_terminal_vars
  90. set_terminal_vars()
  91. # testnet data_dir differs from data_dir_root, so check or create
  92. from mmgen.util import msg,die,check_or_create_dir
  93. check_or_create_dir(g.data_dir) # dies on error
  94. from mmgen.color import init_color
  95. init_color(enable_color=g.color,num_colors=('auto',256)[bool(g.force_256_color)])
  96. if g.platform == 'win': start_mscolor()
  97. g.coin = g.coin.upper() # allow user to use lowercase
  98. def set_data_dir_root():
  99. g.data_dir_root = os.path.normpath(os.path.expanduser(opt.data_dir)) if opt.data_dir else \
  100. os.path.join(g.home_dir,'.'+g.proj_name.lower())
  101. # mainnet and testnet share cfg file, as with Core
  102. g.cfg_file = os.path.join(g.data_dir_root,'{}.cfg'.format(g.proj_name.lower()))
  103. def get_data_from_config_file():
  104. from mmgen.util import msg,die,check_or_create_dir
  105. check_or_create_dir(g.data_dir_root) # dies on error
  106. # https://wiki.debian.org/Python:
  107. # Debian (Ubuntu) sys.prefix is '/usr' rather than '/usr/local, so add 'local'
  108. # TODO - test for Windows
  109. # This must match the configuration in setup.py
  110. data = u''
  111. try:
  112. with open(g.cfg_file,'rb') as f: data = f.read().decode('utf8')
  113. except:
  114. cfg_template = os.path.join(*([sys.prefix]
  115. + (['share'],['local','share'])[g.platform=='linux']
  116. + [g.proj_name.lower(),os.path.basename(g.cfg_file)]))
  117. try:
  118. with open(cfg_template,'rb') as f: template_data = f.read()
  119. except:
  120. msg("WARNING: configuration template not found at '{}'".format(cfg_template))
  121. else:
  122. try:
  123. with open(g.cfg_file,'wb') as f: f.write(template_data)
  124. os.chmod(g.cfg_file,0600)
  125. except:
  126. die(2,"ERROR: unable to write to datadir '{}'".format(g.data_dir))
  127. return data
  128. def override_from_cfg_file(cfg_data):
  129. from mmgen.util import die,strip_comments,set_for_type
  130. import re
  131. for n,l in enumerate(cfg_data.splitlines(),1): # DOS-safe
  132. l = strip_comments(l)
  133. if l == '': continue
  134. m = re.match(r'(\w+)\s+(\S+)$',l)
  135. if not m: die(2,"Parse error in file '{}', line {}".format(g.cfg_file,n))
  136. name,val = m.groups()
  137. if name in g.cfg_file_opts:
  138. setattr(g,name,set_for_type(val,getattr(g,name),name,src=g.cfg_file))
  139. else:
  140. die(2,"'{}': unrecognized option in '{}'".format(name,g.cfg_file))
  141. def override_from_env():
  142. from mmgen.util import set_for_type
  143. for name in g.env_opts:
  144. idx,invert_bool = ((6,False),(14,True))[name[:14]=='MMGEN_DISABLE_']
  145. val = os.getenv(name) # os.getenv() returns None if env var is unset
  146. if val: # exclude empty string values too
  147. gname = name[idx:].lower()
  148. setattr(g,gname,set_for_type(val,getattr(g,gname),name,invert_bool))
  149. def init(opts_f,add_opts=[],opt_filter=None):
  150. opts_data = opts_f()
  151. opts_data['long_options'] = common_opts_data
  152. version_info = """
  153. {pgnm_uc} version {g.version}
  154. Part of the {pnm} suite, a Bitcoin cold-storage solution for the command line.
  155. Copyright (C) {g.Cdates} {g.author} {g.email}
  156. """.format(pnm=g.proj_name, g=g, pgnm_uc=g.prog_name.upper()).strip()
  157. uopts,args,short_opts,long_opts,skipped_opts,do_help = \
  158. mmgen.share.Opts.parse_opts(sys.argv,opts_data,opt_filter=opt_filter,defer_help=True)
  159. if g.debug: opt_preproc_debug(short_opts,long_opts,skipped_opts,uopts,args)
  160. # Save this for usage()
  161. global usage_txt
  162. usage_txt = opts_data['usage']
  163. # Transfer uopts into opt, setting program's opts + required opts to None if not set by user
  164. for o in tuple([s.rstrip('=') for s in long_opts] + add_opts + skipped_opts) + \
  165. g.required_opts + g.common_opts:
  166. setattr(opt,o,uopts[o] if o in uopts else None)
  167. if opt.version: Die(0,version_info)
  168. # === Interaction with global vars begins here ===
  169. # NB: user opt --data-dir is actually g.data_dir_root
  170. # cfg file is in g.data_dir_root, wallet and other data are in g.data_dir
  171. # Must set g.data_dir_root and g.cfg_file from cmdline before processing cfg file
  172. set_data_dir_root()
  173. if not opt.skip_cfg_file:
  174. cfg_data = get_data_from_config_file()
  175. override_from_cfg_file(cfg_data)
  176. override_from_env()
  177. # User opt sets global var - do these here, before opt is set from g.global_sets_opt
  178. for k in g.common_opts:
  179. val = getattr(opt,k)
  180. if val != None: setattr(g,k,set_for_type(val,getattr(g,k),'--'+k))
  181. if g.regtest: g.testnet = True # These are equivalent for now
  182. # Global vars are now final, including g.testnet, so we can set g.data_dir
  183. g.data_dir=os.path.normpath(os.path.join(g.data_dir_root,('',g.testnet_name)[g.testnet]))
  184. # If user opt is set, convert its type based on value in mmgen.globalvars (g)
  185. # If unset, set it to default value in mmgen.globalvars (g)
  186. setattr(opt,'set_by_user',[])
  187. for k in g.global_sets_opt:
  188. if k in opt.__dict__ and getattr(opt,k) != None:
  189. # _typeconvert_from_dfl(k)
  190. setattr(opt,k,set_for_type(getattr(opt,k),getattr(g,k),'--'+k))
  191. opt.set_by_user.append(k)
  192. else:
  193. setattr(opt,k,g.__dict__[k])
  194. # Check user-set opts without modifying them
  195. if not check_opts(uopts):
  196. sys.exit(1)
  197. if opt.show_hash_presets:
  198. _show_hash_presets()
  199. sys.exit(0)
  200. if opt.verbose: opt.quiet = None
  201. die_on_incompatible_opts(g.incompatible_opts)
  202. opt_postproc_initializations()
  203. if do_help: # print help screen only after global vars are initialized
  204. opts_data = opts_f()
  205. opts_data['long_options'] = common_opts_data
  206. mmgen.share.Opts.parse_opts(sys.argv,opts_data,opt_filter=opt_filter)
  207. # We don't need this data anymore
  208. del mmgen.share.Opts
  209. del opts_f
  210. for k in ('prog_name','desc','usage','options','notes'):
  211. if k in opts_data: del opts_data[k]
  212. if g.bob or g.alice:
  213. import regtest as rt
  214. rt.user(('alice','bob')[g.bob],quiet=True)
  215. g.testnet = True
  216. g.rpc_host = 'localhost'
  217. g.rpc_port = rt.rpc_port
  218. g.rpc_user = rt.rpc_user
  219. g.rpc_password = rt.rpc_password
  220. g.data_dir = os.path.join(g.home_dir,'.'+g.proj_name.lower(),'regtest')
  221. if g.debug: opt_postproc_debug()
  222. return args
  223. def check_opts(usr_opts): # Returns false if any check fails
  224. def opt_splits(val,sep,n,desc):
  225. sepword = 'comma' if sep == ',' else 'colon' if sep == ':' else "'%s'" % sep
  226. try: l = val.split(sep)
  227. except:
  228. msg("'%s': invalid %s (not %s-separated list)" % (val,desc,sepword))
  229. return False
  230. if len(l) == n: return True
  231. else:
  232. msg("'%s': invalid %s (%s %s-separated items required)" %
  233. (val,desc,n,sepword))
  234. return False
  235. def opt_compares(val,op,target,desc,what=''):
  236. if what: what += ' '
  237. if not eval('%s %s %s' % (val, op, target)):
  238. msg('%s: invalid %s (%snot %s %s)' % (val,desc,what,op,target))
  239. return False
  240. return True
  241. def opt_is_int(val,desc):
  242. try: int(val)
  243. except:
  244. msg("'%s': invalid %s (not an integer)" % (val,desc))
  245. return False
  246. return True
  247. def opt_is_tx_fee(val,desc):
  248. from mmgen.tx import MMGenTX
  249. ret = MMGenTX().convert_fee_spec(val,224,on_fail='return')
  250. if ret == False:
  251. msg("'{}': invalid {} (not a {} amount or satoshis-per-byte specification)".format(
  252. val,desc,g.coin.upper()))
  253. elif ret != None and ret > g.max_tx_fee:
  254. msg("'{}': invalid {} (> max_tx_fee ({} {}))".format(val,desc,g.max_tx_fee,g.coin.upper()))
  255. else:
  256. return True
  257. return False
  258. def opt_is_in_list(val,lst,desc):
  259. if val not in lst:
  260. q,sep = (('',','),("'","','"))[type(lst[0]) == str]
  261. msg('{q}{v}{q}: invalid {w}\nValid choices: {q}{o}{q}'.format(
  262. v=val,w=desc,q=q,
  263. o=sep.join([str(i) for i in sorted(lst)])
  264. ))
  265. return False
  266. return True
  267. def opt_unrecognized(key,val,desc):
  268. msg("'%s': unrecognized %s for option '%s'"
  269. % (val,desc,fmt_opt(key)))
  270. return False
  271. def opt_display(key,val='',beg='For selected',end=':\n'):
  272. s = '%s=%s' % (fmt_opt(key),val) if val else fmt_opt(key)
  273. msg_r("%s option '%s'%s" % (beg,s,end))
  274. global opt
  275. for key,val in [(k,getattr(opt,k)) for k in usr_opts]:
  276. desc = "parameter for '%s' option" % fmt_opt(key)
  277. from mmgen.util import check_infile,check_outfile,check_outdir
  278. # Check for file existence and readability
  279. if key in ('keys_from_file','mmgen_keys_from_file',
  280. 'passwd_file','keysforaddrs','comment_file'):
  281. check_infile(val) # exits on error
  282. continue
  283. if key == 'outdir':
  284. check_outdir(val) # exits on error
  285. # # NEW
  286. elif key in ('in_fmt','out_fmt'):
  287. from mmgen.seed import SeedSource,IncogWallet,Brainwallet,IncogWalletHidden
  288. sstype = SeedSource.fmt_code_to_type(val)
  289. if not sstype:
  290. return opt_unrecognized(key,val,'format code')
  291. if key == 'out_fmt':
  292. p = 'hidden_incog_output_params'
  293. if sstype == IncogWalletHidden and not getattr(opt,p):
  294. die(1,'Hidden incog format output requested. You must supply'
  295. + " a file and offset with the '%s' option" % fmt_opt(p))
  296. if issubclass(sstype,IncogWallet) and opt.old_incog_fmt:
  297. opt_display(key,val,beg='Selected',end=' ')
  298. opt_display('old_incog_fmt',beg='conflicts with',end=':\n')
  299. die(1,'Export to old incog wallet format unsupported')
  300. elif issubclass(sstype,Brainwallet):
  301. die(1,'Output to brainwallet format unsupported')
  302. elif key in ('hidden_incog_input_params','hidden_incog_output_params'):
  303. a = val.split(',')
  304. if len(a) < 2:
  305. opt_display(key,val)
  306. msg('Option requires two comma-separated arguments')
  307. return False
  308. fn,ofs = ','.join(a[:-1]),a[-1] # permit comma in filename
  309. if not opt_is_int(ofs,desc): return False
  310. if key == 'hidden_incog_input_params':
  311. check_infile(fn,blkdev_ok=True)
  312. key2 = 'in_fmt'
  313. else:
  314. try: os.stat(fn)
  315. except:
  316. b = os.path.dirname(fn)
  317. if b: check_outdir(b)
  318. else: check_outfile(fn,blkdev_ok=True)
  319. key2 = 'out_fmt'
  320. if hasattr(opt,key2):
  321. val2 = getattr(opt,key2)
  322. from mmgen.seed import IncogWalletHidden
  323. if val2 and val2 not in IncogWalletHidden.fmt_codes:
  324. die(1,
  325. 'Option conflict:\n %s, with\n %s=%s' % (
  326. fmt_opt(key),fmt_opt(key2),val2
  327. ))
  328. elif key == 'seed_len':
  329. if not opt_is_int(val,desc): return False
  330. if not opt_is_in_list(int(val),g.seed_lens,desc): return False
  331. elif key == 'hash_preset':
  332. if not opt_is_in_list(val,g.hash_presets.keys(),desc): return False
  333. elif key == 'brain_params':
  334. a = val.split(',')
  335. if len(a) != 2:
  336. opt_display(key,val)
  337. msg('Option requires two comma-separated arguments')
  338. return False
  339. d = 'seed length ' + desc
  340. if not opt_is_int(a[0],d): return False
  341. if not opt_is_in_list(int(a[0]),g.seed_lens,d): return False
  342. d = 'hash preset ' + desc
  343. if not opt_is_in_list(a[1],g.hash_presets.keys(),d): return False
  344. elif key == 'usr_randchars':
  345. if val == 0: continue
  346. if not opt_is_int(val,desc): return False
  347. if not opt_compares(val,'>=',g.min_urandchars,desc): return False
  348. if not opt_compares(val,'<=',g.max_urandchars,desc): return False
  349. elif key == 'tx_fee':
  350. if not opt_is_tx_fee(val,desc): return False
  351. elif key == 'tx_confs':
  352. if not opt_is_int(val,desc): return False
  353. if not opt_compares(val,'>=',1,desc): return False
  354. elif key == 'key_generator':
  355. if not opt_compares(val,'<=',len(g.key_generators),desc): return False
  356. if not opt_compares(val,'>',0,desc): return False
  357. elif key == 'coin':
  358. if not opt_is_in_list(val.upper(),g.coins,'coin'): return False
  359. else:
  360. if g.debug: Msg("check_opts(): No test for opt '%s'" % key)
  361. return True