Browse Source

modularize wallet classes

- all code has been relocated from `wallet.py` to individual modules under
  `wallet`, with each wallet type having its own module

- the fully rewritten initialization code can be found in `__init__.py` and
  `base.py`
The MMGen Project 3 years ago
parent
commit
9649f5b723

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-13.1.dev016
+13.1.dev017

+ 2 - 2
mmgen/filename.py

@@ -30,7 +30,7 @@ class Filename(MMGenObject):
 
 	def __init__(self,fn,base_class=None,subclass=None,proto=None,write=False):
 		"""
-		'base_class' - a base class with an 'ext_to_type' method
+		'base_class' - a base class with an 'ext_to_cls' method
 		'subclass'   - a subclass with an 'ext' attribute
 
 		One or the other must be provided, but not both.
@@ -52,7 +52,7 @@ class Filename(MMGenObject):
 			die(3,f'Class {(subclass or base_class).__name__!r} does not support the Filename API')
 
 		if base_class:
-			subclass = base_class.ext_to_type(self.ext,proto)
+			subclass = base_class.ext_to_cls( self.ext, proto )
 			if not subclass:
 				die( 'BadFileExtension', f'{self.ext!r}: not a recognized file extension for {base_class}' )
 

+ 2 - 2
mmgen/fileutil.py

@@ -116,9 +116,9 @@ def get_seed_file(cmd_args,nargs,invoked_as=None):
 
 	from .opts import opt
 	from .filename import find_file_in_dir
-	from .wallet import MMGenWallet
+	from .wallet.mmgen import wallet
 
-	wf = find_file_in_dir(MMGenWallet,g.data_dir)
+	wf = find_file_in_dir(wallet,g.data_dir)
 
 	wd_from_opt = bool(opt.hidden_incog_input_params or opt.in_fmt) # have wallet data from opt?
 

+ 2 - 2
mmgen/help.py

@@ -81,8 +81,8 @@ def help_notes_func(proto,po,k):
 			])
 
 		def fmt_codes():
-			from .wallet import Wallet
-			return '\n  '.join( Wallet.format_fmt_codes().splitlines() )
+			from .wallet import format_fmt_codes
+			return '\n  '.join( format_fmt_codes().splitlines() )
 
 		def coin_id():
 			return proto.coin_id

+ 2 - 3
mmgen/main_wallet.py

@@ -22,8 +22,7 @@ mmgen/main_wallet:  Entry point for MMGen wallet-related scripts
 
 import os
 from .common import *
-from .wallet import Wallet,MMGenWallet
-from .filename import find_file_in_dir
+from .wallet import Wallet,get_wallet_cls
 
 usage = '[opts] [infile]'
 nargs = 1
@@ -234,7 +233,7 @@ if invoked_as == 'passchg' and ss_in.infile.dirname == g.data_dir:
 elif invoked_as == 'gen' and not opt.outdir and not opt.stdout:
 	from .filename import find_file_in_dir
 	if (
-		not find_file_in_dir( MMGenWallet, g.data_dir )
+		not find_file_in_dir( get_wallet_cls('mmgen'), g.data_dir )
 		and keypress_confirm(
 			'Make this wallet your default and move it to the data directory?',
 			default_yes = True ) ):

+ 9 - 8
mmgen/opts.py

@@ -469,23 +469,23 @@ def check_usr_opts(usr_opts): # Raises an exception if any check fails
 			end ))
 
 	def chk_in_fmt(key,val,desc):
-		from .wallet import Wallet,IncogWallet,Brainwallet,IncogWalletHidden
-		sstype = Wallet.fmt_code_to_type(val)
-		if not sstype:
+		from .wallet import get_wallet_data
+		wd = get_wallet_data(fmt_code=val)
+		if not wd:
 			opt_unrecognized(key,val)
 		if key == 'out_fmt':
 			p = 'hidden_incog_output_params'
 
-			if sstype == IncogWalletHidden and not getattr(opt,p):
+			if wd.type == 'incog_hidden' and not getattr(opt,p):
 				die( 'UserOptError',
 					'Hidden incog format output requested.  ' +
 					f'You must supply a file and offset with the {fmt_opt(p)!r} option' )
 
-			if issubclass(sstype,IncogWallet) and opt.old_incog_fmt:
+			if wd.base_type == 'incog_base' and opt.old_incog_fmt:
 				opt_display(key,val,beg='Selected',end=' ')
 				opt_display('old_incog_fmt',beg='conflicts with',end=':\n')
 				die( 'UserOptError', 'Export to old incog wallet format unsupported' )
-			elif issubclass(sstype,Brainwallet):
+			elif wd.type == 'brain':
 				die( 'UserOptError', 'Output to brainwallet format unsupported' )
 
 	chk_out_fmt = chk_in_fmt
@@ -515,8 +515,9 @@ def check_usr_opts(usr_opts): # Raises an exception if any check fails
 
 		if hasattr(opt,key2):
 			val2 = getattr(opt,key2)
-			from .wallet import IncogWalletHidden
-			if val2 and val2 not in IncogWalletHidden.fmt_codes:
+			from .wallet import get_wallet_data
+			wd = get_wallet_data('incog_hidden')
+			if val2 and val2 not in wd.fmt_codes:
 				die( 'UserOptError', f'Option conflict:\n  {fmt_opt(key)}, with\n  {fmt_opt(key2)}={val2}' )
 
 	chk_hidden_incog_output_params = chk_hidden_incog_input_params

+ 1 - 1
mmgen/tx/__init__.py

@@ -44,7 +44,7 @@ def _get_cls_info(clsname,modname,args,kwargs):
 		from ..util import get_extension
 		from .completed import Completed
 		ext = get_extension( kwargs['filename'] )
-		cls = Completed.ext_to_type( ext, proto )
+		cls = Completed.ext_to_cls( ext, proto )
 		if not cls:
 			die(1,f'{ext!r}: unrecognized file extension for CompletedTX')
 		clsname = cls.__name__

+ 1 - 1
mmgen/tx/completed.py

@@ -53,7 +53,7 @@ class Completed(Base):
 		return MMGenTxFile(self)
 
 	@classmethod
-	def ext_to_type(cls,ext,proto):
+	def ext_to_cls(cls,ext,proto):
 		"""
 		see twctl:import_token()
 		"""

+ 4 - 5
mmgen/txsign.py

@@ -24,7 +24,7 @@ from .common import *
 from .obj import MMGenList
 from .addr import MMGenAddrType
 from .addrlist import AddrIdxList,KeyAddrList
-from .wallet import Wallet,WalletUnenc,WalletEnc,MMGenWallet
+from .wallet import Wallet,get_wallet_extensions,get_wallet_cls
 
 saved_seeds = {}
 
@@ -117,13 +117,12 @@ def get_tx_files(opt,args):
 
 def get_seed_files(opt,args):
 	# favor unencrypted seed sources first, as they don't require passwords
-	u,e = WalletUnenc,WalletEnc
-	ret = _pop_matching_fns(args,u.get_extensions())
+	ret = _pop_matching_fns( args, get_wallet_extensions('unenc') )
 	from .filename import find_file_in_dir
-	wf = find_file_in_dir(MMGenWallet,g.data_dir) # Make this the first encrypted ss in the list
+	wf = find_file_in_dir(get_wallet_cls('mmgen'),g.data_dir) # Make this the first encrypted ss in the list
 	if wf:
 		ret.append(wf)
-	ret += _pop_matching_fns(args,e.get_extensions())
+	ret += _pop_matching_fns( args, get_wallet_extensions('enc') )
 	if not (ret or opt.mmgen_keys_from_file or opt.keys_from_file): # or opt.use_wallet_dat
 		die(1,'You must specify a seed or key source!')
 	return ret

+ 2 - 3
mmgen/util.py

@@ -453,9 +453,8 @@ def compare_or_die(val1, desc1, val2, desc2, e='Error'):
 	return True
 
 def check_wallet_extension(fn):
-	from .wallet import Wallet
-	if not Wallet.ext_to_type(get_extension(fn)):
-		die( 'BadFileExtension', f'{fn!r}: unrecognized seed source file extension' )
+	from .wallet import get_wallet_data
+	get_wallet_data( ext=get_extension(fn), die_on_fail=True ) # raises exception on failure
 
 def make_full_path(outdir,outfile):
 	return os.path.normpath(os.path.join(outdir, os.path.basename(outfile)))

+ 0 - 1176
mmgen/wallet.py

@@ -1,1176 +0,0 @@
-#!/usr/bin/env python3
-#
-# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
-# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-"""
-wallet.py:  Wallet classes and methods for the MMGen suite
-"""
-
-import os
-
-from .common import *
-from .obj import *
-from .baseconv import *
-from .seed import Seed
-import mmgen.crypto as crypto
-
-def check_usr_seed_len(seed_len):
-	if opt.seed_len and opt.seed_len != seed_len:
-		die(1,f'ERROR: requested seed length ({opt.seed_len}) doesn’t match seed length of source ({seed_len})')
-
-class WalletMeta(type):
-	wallet_classes = set() # one-instance class, so store data in class attr
-	def __init__(cls,name,bases,namespace):
-		cls.wallet_classes.add(cls)
-		cls.wallet_classes -= set(bases)
-
-class Wallet(MMGenObject,metaclass=WalletMeta):
-
-	desc = g.proj_name + ' seed source'
-	file_mode = 'text'
-	filename_api = True
-	stdin_ok = False
-	ask_tty = True
-	no_tty  = False
-	op = None
-	_msg = {}
-
-	class WalletData(MMGenObject): pass
-
-	def __new__(cls,
-		fn            = None,
-		ss            = None,
-		seed_bin      = None,
-		seed          = None,
-		passchg       = False,
-		in_data       = None,
-		ignore_in_fmt = False,
-		in_fmt        = None,
-		passwd_file   = None ):
-
-		in_fmt = in_fmt or opt.in_fmt
-
-		if opt.out_fmt:
-			out_cls = cls.fmt_code_to_type(opt.out_fmt)
-			if not out_cls:
-				die(1,f'{opt.out_fmt!r}: unrecognized output format')
-		else:
-			out_cls = None
-
-		def die_on_opt_mismatch(opt,sstype):
-			compare_or_die(
-				cls.fmt_code_to_type(opt).__name__, 'input format requested on command line',
-				sstype.__name__, 'input file format' )
-
-		if seed or seed_bin:
-			me = super(cls,cls).__new__(out_cls or MMGenWallet) # default to MMGenWallet
-			me.seed = seed or Seed(seed_bin=seed_bin)
-			me.op = 'new'
-		elif ss:
-			me = super(cls,cls).__new__((ss.__class__ if passchg else out_cls) or MMGenWallet)
-			me.seed = ss.seed
-			me.ss_in = ss
-			me.op = ('conv','pwchg_new')[bool(passchg)]
-		elif fn or opt.hidden_incog_input_params:
-			from .filename import Filename
-			if fn:
-				f = Filename(fn,base_class=cls)
-			else:
-				# permit comma in filename
-				fn = ','.join(opt.hidden_incog_input_params.split(',')[:-1])
-				f = Filename(fn,subclass=IncogWalletHidden)
-			if in_fmt and not ignore_in_fmt:
-				die_on_opt_mismatch(in_fmt,f.subclass)
-			me = super(cls,cls).__new__(f.subclass)
-			me.infile = f
-			me.op = ('old','pwchg_old')[bool(passchg)]
-		elif in_fmt:
-			me = super(cls,cls).__new__(cls.fmt_code_to_type(in_fmt))
-			me.op = ('old','pwchg_old')[bool(passchg)]
-		else: # called with no arguments: initialize with random seed
-			me = super(cls,cls).__new__(out_cls or MMGenWallet)
-			me.seed = Seed(None)
-			me.op = 'new'
-
-		return me
-
-	def __init__(self,
-		fn            = None,
-		ss            = None,
-		seed_bin      = None,
-		seed          = None,
-		passchg       = False,
-		in_data       = None,
-		ignore_in_fmt = False,
-		in_fmt        = None,
-		passwd_file   = None ):
-
-		self.passwd_file = passwd_file or opt.passwd_file
-		self.ssdata = self.WalletData()
-		self.msg = {}
-		self.in_data = in_data
-
-		for c in reversed(self.__class__.__mro__):
-			if hasattr(c,'_msg'):
-				self.msg.update(c._msg)
-
-		if hasattr(self,'seed'):
-			self._encrypt()
-			return
-		elif hasattr(self,'infile') or self.in_data or not g.stdin_tty:
-			self._deformat_once()
-			self._decrypt_retry()
-		else:
-			if not self.stdin_ok:
-				die(1,f'Reading from standard input not supported for {self.desc} format')
-			self._deformat_retry()
-			self._decrypt_retry()
-
-		qmsg('Valid {} for Seed ID {}{}'.format(
-			self.desc,
-			self.seed.sid.hl(),
-			(f', seed length {self.seed.bitlen}' if self.seed.bitlen != 256 else '')
-		))
-
-	def _get_data(self):
-		if hasattr(self,'infile'):
-			from .fileutil import get_data_from_file
-			self.fmt_data = get_data_from_file(self.infile.name,self.desc,binary=self.file_mode=='binary')
-		elif self.in_data:
-			self.fmt_data = self.in_data
-		else:
-			self.fmt_data = self._get_data_from_user(self.desc)
-
-	def _get_data_from_user(self,desc):
-		return get_data_from_user(desc)
-
-	def _deformat_once(self):
-		self._get_data()
-		if not self._deformat():
-			die(2,'Invalid format for input data')
-
-	def _deformat_retry(self):
-		while True:
-			self._get_data()
-			if self._deformat():
-				break
-			msg('Trying again...')
-
-	def _decrypt_retry(self):
-		while True:
-			if self._decrypt():
-				break
-			if self.passwd_file:
-				die(2,'Passphrase from password file, so exiting')
-			msg('Trying again...')
-
-	@classmethod
-	def get_extensions(cls):
-		return [c.ext for c in cls.wallet_classes if hasattr(c,'ext')]
-
-	@classmethod
-	def fmt_code_to_type(cls,fmt_code):
-		if fmt_code:
-			for c in cls.wallet_classes:
-				if fmt_code in getattr(c,'fmt_codes',[]):
-					return c
-		return None
-
-	@classmethod
-	def ext_to_type(cls,ext,proto=None):
-		for c in cls.wallet_classes:
-			if ext == getattr(c,'ext',None):
-				return c
-
-	@classmethod
-	def format_fmt_codes(cls):
-		d = [(c.__name__,('.'+c.ext if c.ext else str(c.ext)),','.join(c.fmt_codes))
-					for c in cls.wallet_classes
-				if hasattr(c,'fmt_codes')]
-		w = max(len(i[0]) for i in d)
-		ret = [f'{a:<{w}}  {b:<9} {c}' for a,b,c in [
-			('Format','FileExt','Valid codes'),
-			('------','-------','-----------')
-			] + sorted(d)]
-		return '\n'.join(ret) + ('','-α')[g.debug_utf8] + '\n'
-
-	def get_fmt_data(self):
-		self._format()
-		return self.fmt_data
-
-	def write_to_file(self,outdir='',desc=''):
-		self._format()
-		kwargs = {
-			'desc':     desc or self.desc,
-			'ask_tty':  self.ask_tty,
-			'no_tty':   self.no_tty,
-			'binary':   self.file_mode == 'binary'
-		}
-		# write_data_to_file(): outfile with absolute path overrides opt.outdir
-		if outdir:
-			of = os.path.abspath(os.path.join(outdir,self._filename()))
-		from .fileutil import write_data_to_file
-		write_data_to_file(of if outdir else self._filename(),self.fmt_data,**kwargs)
-
-class WalletUnenc(Wallet):
-
-	def _decrypt_retry(self): pass
-	def _encrypt(self): pass
-
-	def _filename(self):
-		s = self.seed
-		return '{}[{}]{x}.{}'.format(
-			s.fn_stem,
-			s.bitlen,
-			self.ext,
-			x='-α' if g.debug_utf8 else '')
-
-	def _choose_seedlen(self,desc,ok_lens,subtype):
-
-		from .term import get_char
-		def choose_len():
-			prompt = self.choose_seedlen_prompt
-			while True:
-				r = get_char('\r'+prompt)
-				if is_int(r) and 1 <= int(r) <= len(ok_lens):
-					break
-			msg_r(('\r','\n')[g.test_suite] + ' '*len(prompt) + '\r')
-			return ok_lens[int(r)-1]
-
-		msg('{} {}'.format(
-			blue(f'{capfirst(desc)} type:'),
-			yellow(subtype)
-		))
-
-		while True:
-			usr_len = choose_len()
-			prompt = self.choose_seedlen_confirm.format(usr_len)
-			if keypress_confirm(prompt,default_yes=True,no_nl=not g.test_suite):
-				return usr_len
-
-class WalletEnc(Wallet):
-
-	_msg = {
-		'choose_passphrase': """
-			You must choose a passphrase to encrypt your new {} with.
-			A key will be generated from your passphrase using a hash preset of '{}'.
-			Please note that no strength checking of passphrases is performed.
-			For an empty passphrase, just hit ENTER twice.
-		"""
-	}
-
-	def _get_hash_preset_from_user(self,hp,add_desc=''):
-		prompt = 'Enter {}hash preset for {}{}{},\nor hit ENTER to {} value ({!r}): '.format(
-			('old ' if self.op=='pwchg_old' else 'new ' if self.op=='pwchg_new' else ''),
-			('','new ')[self.op=='new'],
-			self.desc,
-			('',' '+add_desc)[bool(add_desc)],
-			('accept the default','reuse the old')[self.op=='pwchg_new'],
-			hp )
-		return crypto.get_hash_preset_from_user( hash_preset=hp, prompt=prompt )
-
-	def _get_hash_preset(self,add_desc=''):
-		if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'hash_preset'):
-			old_hp = self.ss_in.ssdata.hash_preset
-			if opt.keep_hash_preset:
-				hp = old_hp
-				qmsg(f'Reusing hash preset {hp!r} at user request')
-			elif opt.hash_preset:
-				hp = opt.hash_preset
-				qmsg(f'Using hash preset {hp!r} requested on command line')
-			else: # Prompt, using old value as default
-				hp = self._get_hash_preset_from_user(old_hp,add_desc)
-			if (not opt.keep_hash_preset) and self.op == 'pwchg_new':
-				qmsg('Hash preset {}'.format( 'unchanged' if hp == old_hp else f'changed to {hp!r}' ))
-		elif opt.hash_preset:
-			hp = opt.hash_preset
-			qmsg(f'Using hash preset {hp!r} requested on command line')
-		else:
-			hp = self._get_hash_preset_from_user(g.dfl_hash_preset,add_desc)
-		self.ssdata.hash_preset = hp
-
-	def _get_new_passphrase(self):
-		self.ssdata.passwd = crypto.get_new_passphrase(
-			data_desc = ('new ' if self.op in ('new','conv') else '') + self.desc,
-			hash_preset = self.ssdata.hash_preset,
-			passwd_file = self.passwd_file,
-			pw_desc = ('new ' if self.op=='pwchg_new' else '') + 'passphrase' )
-		return self.ssdata.passwd
-
-	def _get_passphrase(self,add_desc=''):
-		self.ssdata.passwd = crypto.get_passphrase(
-			data_desc = self.desc + (f' {add_desc}' if add_desc else ''),
-			passwd_file = self.passwd_file,
-			pw_desc = ('old ' if self.op == 'pwchg_old' else '') + 'passphrase' )
-
-	def _get_first_pw_and_hp_and_encrypt_seed(self):
-		d = self.ssdata
-		self._get_hash_preset()
-
-		if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'passwd'):
-			old_pw = self.ss_in.ssdata.passwd
-			if opt.keep_passphrase:
-				d.passwd = old_pw
-				qmsg('Reusing passphrase at user request')
-			else:
-				pw = self._get_new_passphrase()
-				if self.op == 'pwchg_new':
-					qmsg('Passphrase {}'.format( 'unchanged' if pw == old_pw else 'changed' ))
-		else:
-			self._get_new_passphrase()
-
-		from hashlib import sha256
-		d.salt     = sha256( crypto.get_random(128) ).digest()[:crypto.salt_len]
-		key        = crypto.make_key( d.passwd, d.salt, d.hash_preset )
-		d.key_id   = make_chksum_8(key)
-		d.enc_seed = crypto.encrypt_seed( self.seed.data, key )
-
-class Mnemonic(WalletUnenc):
-
-	stdin_ok = True
-	wclass = 'mnemonic'
-	conv_cls = baseconv
-	choose_seedlen_prompt = 'Choose a mnemonic length: 1) 12 words, 2) 18 words, 3) 24 words: '
-	choose_seedlen_confirm = 'Mnemonic length of {} words chosen. OK?'
-
-	@property
-	def mn_lens(self):
-		return sorted(self.conv_cls(self.wl_id).seedlen_map_rev)
-
-	def _get_data_from_user(self,desc):
-
-		if not g.stdin_tty:
-			return get_data_from_user(desc)
-
-		from .mn_entry import mn_entry # import here to catch cfg var errors
-		mn_len = self._choose_seedlen(self.wclass,self.mn_lens,self.mn_type)
-		return mn_entry(self.wl_id).get_mnemonic_from_user(mn_len)
-
-	def _format(self):
-
-		hexseed = self.seed.hexdata
-
-		bc = self.conv_cls(self.wl_id)
-		mn  = bc.fromhex( hexseed, 'seed' )
-		rev = bc.tohex( mn, 'seed' )
-
-		# Internal error, so just die on fail
-		compare_or_die( rev, 'recomputed seed', hexseed, 'original', e='Internal error' )
-
-		self.ssdata.mnemonic = mn
-		self.fmt_data = ' '.join(mn) + '\n'
-
-	def _deformat(self):
-
-		bc = self.conv_cls(self.wl_id)
-		mn = self.fmt_data.split()
-
-		if len(mn) not in self.mn_lens:
-			msg('Invalid mnemonic ({} words).  Valid numbers of words: {}'.format(
-				len(mn),
-				', '.join(map(str,self.mn_lens)) ))
-			return False
-
-		for n,w in enumerate(mn,1):
-			if w not in bc.digits:
-				msg(f'Invalid mnemonic: word #{n} is not in the {self.wl_id.upper()} wordlist')
-				return False
-
-		hexseed = bc.tohex( mn, 'seed' )
-		rev     = bc.fromhex( hexseed, 'seed' )
-
-		if len(hexseed) * 4 not in Seed.lens:
-			msg('Invalid mnemonic (produces too large a number)')
-			return False
-
-		# Internal error, so just die
-		compare_or_die( ' '.join(rev), 'recomputed mnemonic', ' '.join(mn), 'original', e='Internal error' )
-
-		self.seed = Seed(bytes.fromhex(hexseed))
-		self.ssdata.mnemonic = mn
-
-		check_usr_seed_len(self.seed.bitlen)
-
-		return True
-
-class MMGenMnemonic(Mnemonic):
-
-	fmt_codes = ('mmwords','words','mnemonic','mnem','mn','m')
-	desc = 'MMGen native mnemonic data'
-	mn_type = 'MMGen native'
-	ext = 'mmwords'
-	wl_id = 'mmgen'
-
-class BIP39Mnemonic(Mnemonic):
-
-	fmt_codes = ('bip39',)
-	desc = 'BIP39 mnemonic data'
-	mn_type = 'BIP39'
-	ext = 'bip39'
-	wl_id = 'bip39'
-
-	def __init__(self,*args,**kwargs):
-		from .bip39 import bip39
-		self.conv_cls = bip39
-		super().__init__(*args,**kwargs)
-
-class MMGenSeedFile(WalletUnenc):
-
-	stdin_ok = True
-	fmt_codes = ('mmseed','seed','s')
-	desc = 'seed data'
-	ext = 'mmseed'
-
-	def _format(self):
-		b58seed = baseconv('b58').frombytes(self.seed.data,pad='seed',tostr=True)
-		self.ssdata.chksum = make_chksum_6(b58seed)
-		self.ssdata.b58seed = b58seed
-		self.fmt_data = '{} {}\n'.format(
-			self.ssdata.chksum,
-			split_into_cols(4,b58seed) )
-
-	def _deformat(self):
-		desc = self.desc
-		ld = self.fmt_data.split()
-
-		if not (7 <= len(ld) <= 12): # 6 <= padded b58 data (ld[1:]) <= 11
-			msg(f'Invalid data length ({len(ld)}) in {desc}')
-			return False
-
-		a,b = ld[0],''.join(ld[1:])
-
-		if not is_chksum_6(a):
-			msg(f'{a!r}: invalid checksum format in {desc}')
-			return False
-
-		if not is_b58_str(b):
-			msg(f'{b!r}: not a base 58 string, in {desc}')
-			return False
-
-		vmsg_r(f'Validating {desc} checksum...')
-
-		if not compare_chksums(a,'file',make_chksum_6(b),'computed',verbose=True):
-			return False
-
-		ret = baseconv('b58').tobytes(b,pad='seed')
-
-		if ret == False:
-			msg(f'Invalid base-58 encoded seed: {val}')
-			return False
-
-		self.seed = Seed(ret)
-		self.ssdata.chksum = a
-		self.ssdata.b58seed = b
-
-		check_usr_seed_len(self.seed.bitlen)
-
-		return True
-
-class DieRollSeedFile(WalletUnenc):
-
-	stdin_ok = True
-	fmt_codes = ('b6d','die','dieroll')
-	desc = 'base6d die roll seed data'
-	ext = 'b6d'
-	conv_cls = baseconv
-	wclass = 'dieroll'
-	wl_id = 'b6d'
-	mn_type = 'base6d'
-	choose_seedlen_prompt = 'Choose a seed length: 1) 128 bits, 2) 192 bits, 3) 256 bits: '
-	choose_seedlen_confirm = 'Seed length of {} bits chosen. OK?'
-	user_entropy_prompt = 'Would you like to provide some additional entropy from the keyboard?'
-	interactive_input = False
-
-	def _format(self):
-		d = baseconv('b6d').frombytes(self.seed.data,pad='seed',tostr=True) + '\n'
-		self.fmt_data = block_format(d,gw=5,cols=5)
-
-	def _deformat(self):
-
-		d = remove_whitespace(self.fmt_data)
-		bc = baseconv('b6d')
-		rmap = bc.seedlen_map_rev
-
-		if not len(d) in rmap:
-			die( 'SeedLengthError', '{!r}: invalid length for {} (must be one of {})'.format(
-				len(d),
-				self.desc,
-				list(rmap) ))
-
-		# truncate seed to correct length, discarding high bits
-		seed_len = rmap[len(d)]
-		seed_bytes = bc.tobytes( d, pad='seed' )[-seed_len:]
-
-		if self.interactive_input and opt.usr_randchars:
-			if keypress_confirm(self.user_entropy_prompt):
-				seed_bytes = crypto.add_user_random(
-					rand_bytes = seed_bytes,
-					desc       = 'gathered from your die rolls' )
-				self.desc += ' plus user-supplied entropy'
-
-		self.seed = Seed(seed_bytes)
-		self.ssdata.hexseed = seed_bytes.hex()
-
-		check_usr_seed_len(self.seed.bitlen)
-		return True
-
-	def _get_data_from_user(self,desc):
-
-		if not g.stdin_tty:
-			return get_data_from_user(desc)
-
-		bc = baseconv('b6d')
-
-		seed_bitlens = [ n*8 for n in sorted(bc.seedlen_map) ]
-		seed_bitlen = self._choose_seedlen( self.wclass, seed_bitlens, self.mn_type )
-		nDierolls = bc.seedlen_map[seed_bitlen // 8]
-
-		m = """
-			For a {sb}-bit seed you must roll the die {nd} times.  After each die roll,
-			enter the result on the keyboard as a digit.  If you make an invalid entry,
-			you'll be prompted to re-enter it.
-		"""
-		msg('\n'+fmt(m.strip()).format(sb=seed_bitlen,nd=nDierolls)+'\n')
-
-		CUR_HIDE = '\033[?25l'
-		CUR_SHOW = '\033[?25h'
-		cr = '\n' if g.test_suite else '\r'
-		prompt_fs = f'\b\b\b   {cr}Enter die roll #{{}}: {CUR_SHOW}'
-		clear_line = '' if g.test_suite else '\r' + ' ' * 25
-		invalid_msg = CUR_HIDE + cr + 'Invalid entry' + ' ' * 11
-
-		from .term import get_char
-		def get_digit(n):
-			p = prompt_fs
-			sleep = g.short_disp_timeout
-			while True:
-				ch = get_char(p.format(n),num_chars=1,sleep=sleep)
-				if ch in bc.digits:
-					msg_r(CUR_HIDE + ' OK')
-					return ch
-				else:
-					msg_r(invalid_msg)
-					sleep = g.err_disp_timeout
-					p = clear_line + prompt_fs
-
-		dierolls,n = [],1
-		while len(dierolls) < nDierolls:
-			dierolls.append(get_digit(n))
-			n += 1
-
-		msg('Die rolls successfully entered' + CUR_SHOW)
-		self.interactive_input = True
-
-		return ''.join(dierolls)
-
-class PlainHexSeedFile(WalletUnenc):
-
-	stdin_ok = True
-	fmt_codes = ('hex','rawhex','plainhex')
-	desc = 'plain hexadecimal seed data'
-	ext = 'hex'
-
-	def _format(self):
-		self.fmt_data = self.seed.hexdata + '\n'
-
-	def _deformat(self):
-		desc = self.desc
-		d = self.fmt_data.strip()
-
-		if not is_hex_str_lc(d):
-			msg(f'{d!r}: not a lowercase hexadecimal string, in {desc}')
-			return False
-
-		if not len(d)*4 in Seed.lens:
-			msg(f'Invalid data length ({len(d)}) in {desc}')
-			return False
-
-		self.seed = Seed(bytes.fromhex(d))
-		self.ssdata.hexseed = d
-
-		check_usr_seed_len(self.seed.bitlen)
-
-		return True
-
-class MMGenHexSeedFile(WalletUnenc):
-
-	stdin_ok = True
-	fmt_codes = ('seedhex','hexseed','mmhex')
-	desc = 'hexadecimal seed data with checksum'
-	ext = 'mmhex'
-
-	def _format(self):
-		h = self.seed.hexdata
-		self.ssdata.chksum = make_chksum_6(h)
-		self.ssdata.hexseed = h
-		self.fmt_data = '{} {}\n'.format(
-			self.ssdata.chksum,
-			split_into_cols(4,h) )
-
-	def _deformat(self):
-		desc = self.desc
-		d = self.fmt_data.split()
-		try:
-			d[1]
-			chk,hstr = d[0],''.join(d[1:])
-		except:
-			msg(f'{self.fmt_data.strip()!r}: invalid {desc}')
-			return False
-
-		if not len(hstr)*4 in Seed.lens:
-			msg(f'Invalid data length ({len(hstr)}) in {desc}')
-			return False
-
-		if not is_chksum_6(chk):
-			msg(f'{chk!r}: invalid checksum format in {desc}')
-			return False
-
-		if not is_hex_str(hstr):
-			msg(f'{hstr!r}: not a hexadecimal string, in {desc}')
-			return False
-
-		vmsg_r(f'Validating {desc} checksum...')
-
-		if not compare_chksums(chk,'file',make_chksum_6(hstr),'computed',verbose=True):
-			return False
-
-		self.seed = Seed(bytes.fromhex(hstr))
-		self.ssdata.chksum = chk
-		self.ssdata.hexseed = hstr
-
-		check_usr_seed_len(self.seed.bitlen)
-
-		return True
-
-class MMGenWallet(WalletEnc):
-
-	fmt_codes = ('wallet','w')
-	desc = g.proj_name + ' wallet'
-	ext = 'mmdat'
-
-	def __init__(self,*args,**kwargs):
-		if opt.label:
-			self.label = MMGenWalletLabel(
-				opt.label,
-				msg = "Error in option '--label'" )
-		else:
-			self.label = None
-		super().__init__(*args,**kwargs)
-
-	# logic identical to _get_hash_preset_from_user()
-	def _get_label_from_user(self,old_lbl=''):
-		prompt = 'Enter a wallet label, or hit ENTER {}: '.format(
-			'to reuse the label {}'.format(old_lbl.hl(encl="''")) if old_lbl else
-			'for no label' )
-		while True:
-			ret = line_input(prompt)
-			if ret:
-				lbl = get_obj(MMGenWalletLabel,s=ret)
-				if lbl:
-					return lbl
-				else:
-					msg('Invalid label.  Trying again...')
-			else:
-				return old_lbl or MMGenWalletLabel('No Label')
-
-	# logic identical to _get_hash_preset()
-	def _get_label(self):
-		if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'label'):
-			old_lbl = self.ss_in.ssdata.label
-			if opt.keep_label:
-				lbl = old_lbl
-				qmsg('Reusing label {} at user request'.format( lbl.hl(encl="''") ))
-			elif self.label:
-				lbl = self.label
-				qmsg('Using label {} requested on command line'.format( lbl.hl(encl="''") ))
-			else: # Prompt, using old value as default
-				lbl = self._get_label_from_user(old_lbl)
-			if (not opt.keep_label) and self.op == 'pwchg_new':
-				qmsg('Label {}'.format( 'unchanged' if lbl == old_lbl else f'changed to {lbl!r}' ))
-		elif self.label:
-			lbl = self.label
-			qmsg('Using label {} requested on command line'.format( lbl.hl(encl="''") ))
-		else:
-			lbl = self._get_label_from_user()
-		self.ssdata.label = lbl
-
-	def _encrypt(self):
-		self._get_first_pw_and_hp_and_encrypt_seed()
-		self._get_label()
-		d = self.ssdata
-		d.pw_status = ('NE','E')[len(d.passwd)==0]
-		d.timestamp = make_timestamp()
-
-	def _format(self):
-		d = self.ssdata
-		s = self.seed
-		bc = baseconv('b58')
-		slt_fmt  = bc.frombytes(d.salt,pad='seed',tostr=True)
-		es_fmt = bc.frombytes(d.enc_seed,pad='seed',tostr=True)
-		lines = (
-			d.label,
-			'{} {} {} {} {}'.format( s.sid.lower(), d.key_id.lower(), s.bitlen, d.pw_status, d.timestamp ),
-			'{}: {} {} {}'.format( d.hash_preset, *crypto.get_hash_params(d.hash_preset) ),
-			'{} {}'.format( make_chksum_6(slt_fmt), split_into_cols(4,slt_fmt) ),
-			'{} {}'.format( make_chksum_6(es_fmt),  split_into_cols(4,es_fmt) )
-		)
-		chksum = make_chksum_6(' '.join(lines).encode())
-		self.fmt_data = '\n'.join((chksum,)+lines) + '\n'
-
-	def _deformat(self):
-
-		def check_master_chksum(lines,desc):
-
-			if len(lines) != 6:
-				msg(f'Invalid number of lines ({len(lines)}) in {desc} data')
-				return False
-
-			if not is_chksum_6(lines[0]):
-				msg(f'Incorrect master checksum ({lines[0]}) in {desc} data')
-				return False
-
-			chk = make_chksum_6(' '.join(lines[1:]))
-			if not compare_chksums(lines[0],'master',chk,'computed',
-						hdr='For wallet master checksum',verbose=True):
-				return False
-
-			return True
-
-		lines = self.fmt_data.splitlines()
-		if not check_master_chksum(lines,self.desc):
-			return False
-
-		d = self.ssdata
-		d.label = MMGenWalletLabel(lines[1])
-
-		d1,d2,d3,d4,d5 = lines[2].split()
-		d.seed_id = d1.upper()
-		d.key_id  = d2.upper()
-		check_usr_seed_len(int(d3))
-		d.pw_status,d.timestamp = d4,d5
-
-		hpdata = lines[3].split()
-
-		d.hash_preset = hp = hpdata[0][:-1]  # a string!
-		qmsg(f'Hash preset of wallet: {hp!r}')
-		if opt.hash_preset and opt.hash_preset != hp:
-			qmsg(f'Warning: ignoring user-requested hash preset {opt.hash_preset!r}')
-
-		hash_params = tuple(map(int,hpdata[1:]))
-
-		if hash_params != crypto.get_hash_params(d.hash_preset):
-			msg(f'Hash parameters {" ".join(hash_params)!r} don’t match hash preset {d.hash_preset!r}')
-			return False
-
-		lmin,foo,lmax = sorted(baseconv('b58').seedlen_map_rev) # 22,33,44
-		for i,key in (4,'salt'),(5,'enc_seed'):
-			l = lines[i].split(' ')
-			chk = l.pop(0)
-			b58_val = ''.join(l)
-
-			if len(b58_val) < lmin or len(b58_val) > lmax:
-				msg(f'Invalid format for {key} in {self.desc}: {l}')
-				return False
-
-			if not compare_chksums(chk,key,
-					make_chksum_6(b58_val),'computed checksum',verbose=True):
-				return False
-
-			val = baseconv('b58').tobytes(b58_val,pad='seed')
-			if val == False:
-				msg(f'Invalid base 58 number: {b58_val}')
-				return False
-
-			setattr(d,key,val)
-
-		return True
-
-	def _decrypt(self):
-		d = self.ssdata
-		# Needed for multiple transactions with {}-txsign
-		self._get_passphrase(
-			add_desc = os.path.basename(self.infile.name) if opt.quiet else '' )
-		key = crypto.make_key( d.passwd, d.salt, d.hash_preset )
-		ret = crypto.decrypt_seed( d.enc_seed, key, d.seed_id, d.key_id )
-		if ret:
-			self.seed = Seed(ret)
-			return True
-		else:
-			return False
-
-	def _filename(self):
-		s = self.seed
-		d = self.ssdata
-		return '{}-{}[{},{}]{x}.{}'.format(
-				s.fn_stem,
-				d.key_id,
-				s.bitlen,
-				d.hash_preset,
-				self.ext,
-				x='-α' if g.debug_utf8 else '')
-
-class Brainwallet(WalletEnc):
-
-	stdin_ok = True
-	fmt_codes = ('mmbrain','brainwallet','brain','bw')
-	desc = 'brainwallet'
-	ext = 'mmbrain'
-	# brainwallet warning message? TODO
-
-	def get_bw_params(self):
-		# already checked
-		a = opt.brain_params.split(',')
-		return int(a[0]),a[1]
-
-	def _deformat(self):
-		self.brainpasswd = ' '.join(self.fmt_data.split())
-		return True
-
-	def _decrypt(self):
-		d = self.ssdata
-		if opt.brain_params:
-			"""
-			Don't set opt.seed_len!  When using multiple wallets, BW seed len might differ from others
-			"""
-			bw_seed_len,d.hash_preset = self.get_bw_params()
-		else:
-			if not opt.seed_len:
-				qmsg(f'Using default seed length of {yellow(str(Seed.dfl_len))} bits\n'
-					+ 'If this is not what you want, use the --seed-len option' )
-			self._get_hash_preset()
-			bw_seed_len = opt.seed_len or Seed.dfl_len
-		qmsg_r('Hashing brainwallet data.  Please wait...')
-		# Use buflen arg of scrypt.hash() to get seed of desired length
-		seed = crypto.scrypt_hash_passphrase(
-			self.brainpasswd.encode(),
-			b'',
-			d.hash_preset,
-			buflen = bw_seed_len // 8 )
-		qmsg('Done')
-		self.seed = Seed(seed)
-		msg(f'Seed ID: {self.seed.sid}')
-		qmsg('Check this value against your records')
-		return True
-
-	def _format(self):
-		raise NotImplementedError('Brainwallet not supported as an output format')
-
-	def _encrypt(self):
-		raise NotImplementedError('Brainwallet not supported as an output format')
-
-class IncogWalletBase(WalletEnc):
-
-	_msg = {
-		'check_incog_id': """
-  Check the generated Incog ID above against your records.  If it doesn't
-  match, then your incognito data is incorrect or corrupted.
-	""",
-		'record_incog_id': """
-  Make a record of the Incog ID but keep it secret.  You will use it to
-  identify your incog wallet data in the future.
-	""",
-		'incorrect_incog_passphrase_try_again': """
-Incorrect passphrase, hash preset, or maybe old-format incog wallet.
-Try again? (Y)es, (n)o, (m)ore information:
-""".strip(),
-		'confirm_seed_id': """
-If the Seed ID above is correct but you're seeing this message, then you need
-to exit and re-run the program with the '--old-incog-fmt' option.
-""".strip(),
-		'dec_chk': " {} hash preset"
-	}
-
-	def _make_iv_chksum(self,s):
-		from hashlib import sha256
-		return sha256(s).hexdigest()[:8].upper()
-
-	def _get_incog_data_len(self,seed_len):
-		return (
-			crypto.aesctr_iv_len
-			+ crypto.salt_len
-			+ (0 if opt.old_incog_fmt else crypto.hincog_chk_len)
-			+ seed_len//8 )
-
-	def _incog_data_size_chk(self):
-		# valid sizes: 56, 64, 72
-		dlen = len(self.fmt_data)
-		seed_len = opt.seed_len or Seed.dfl_len
-		valid_dlen = self._get_incog_data_len(seed_len)
-		if dlen == valid_dlen:
-			return True
-		else:
-			if opt.old_incog_fmt:
-				msg('WARNING: old-style incognito format requested.  Are you sure this is correct?')
-			msg(f'Invalid incognito data size ({dlen} bytes) for this seed length ({seed_len} bits)')
-			msg(f'Valid data size for this seed length: {valid_dlen} bytes')
-			for sl in Seed.lens:
-				if dlen == self._get_incog_data_len(sl):
-					die(1,f'Valid seed length for this data size: {sl} bits')
-			msg(f'This data size ({dlen} bytes) is invalid for all available seed lengths')
-			return False
-
-	def _encrypt (self):
-		self._get_first_pw_and_hp_and_encrypt_seed()
-		if opt.old_incog_fmt:
-			die(1,'Writing old-format incog wallets is unsupported')
-		d = self.ssdata
-		# IV is used BOTH to initialize counter and to salt password!
-		d.iv = crypto.get_random( crypto.aesctr_iv_len )
-		d.iv_id = self._make_iv_chksum(d.iv)
-		msg(f'New Incog Wallet ID: {d.iv_id}')
-		qmsg('Make a record of this value')
-		vmsg(self.msg['record_incog_id'])
-
-		d.salt = crypto.get_random( crypto.salt_len )
-		key = crypto.make_key( d.passwd, d.salt, d.hash_preset, 'incog wallet key' )
-		from hashlib import sha256
-		chk = sha256(self.seed.data).digest()[:8]
-		d.enc_seed = crypto.encrypt_data(
-			chk + self.seed.data,
-			key,
-			crypto.aesctr_dfl_iv,
-			'seed' )
-
-		d.wrapper_key = crypto.make_key( d.passwd, d.iv, d.hash_preset, 'incog wrapper key' )
-		d.key_id = make_chksum_8(d.wrapper_key)
-		vmsg(f'Key ID: {d.key_id}')
-		d.target_data_len = self._get_incog_data_len(self.seed.bitlen)
-
-	def _format(self):
-		d = self.ssdata
-		self.fmt_data = d.iv + crypto.encrypt_data(
-			d.salt + d.enc_seed,
-			d.wrapper_key,
-			d.iv,
-			self.desc )
-
-	def _filename(self):
-		s = self.seed
-		d = self.ssdata
-		return '{}-{}-{}[{},{}]{x}.{}'.format(
-				s.fn_stem,
-				d.key_id,
-				d.iv_id,
-				s.bitlen,
-				d.hash_preset,
-				self.ext,
-				x='-α' if g.debug_utf8 else '')
-
-	def _deformat(self):
-
-		if not self._incog_data_size_chk():
-			return False
-
-		d = self.ssdata
-		d.iv             = self.fmt_data[0:crypto.aesctr_iv_len]
-		d.incog_id       = self._make_iv_chksum(d.iv)
-		d.enc_incog_data = self.fmt_data[crypto.aesctr_iv_len:]
-		msg(f'Incog Wallet ID: {d.incog_id}')
-		qmsg('Check this value against your records')
-		vmsg(self.msg['check_incog_id'])
-
-		return True
-
-	def _verify_seed_newfmt(self,data):
-		chk,seed = data[:8],data[8:]
-		from hashlib import sha256
-		if sha256(seed).digest()[:8] == chk:
-			qmsg('Passphrase{} are correct'.format( self.msg['dec_chk'].format('and') ))
-			return seed
-		else:
-			msg('Incorrect passphrase{}'.format( self.msg['dec_chk'].format('or') ))
-			return False
-
-	def _verify_seed_oldfmt(self,seed):
-		m = f'Seed ID: {make_chksum_8(seed)}.  Is the Seed ID correct?'
-		if keypress_confirm(m, True):
-			return seed
-		else:
-			return False
-
-	def _decrypt(self):
-		d = self.ssdata
-		self._get_hash_preset(add_desc=d.incog_id)
-		self._get_passphrase(add_desc=d.incog_id)
-
-		# IV is used BOTH to initialize counter and to salt password!
-		key = crypto.make_key( d.passwd, d.iv, d.hash_preset, 'wrapper key' )
-		dd = crypto.decrypt_data( d.enc_incog_data, key, d.iv, 'incog data' )
-
-		d.salt     = dd[0:crypto.salt_len]
-		d.enc_seed = dd[crypto.salt_len:]
-
-		key = crypto.make_key( d.passwd, d.salt, d.hash_preset, 'main key' )
-		qmsg(f'Key ID: {make_chksum_8(key)}')
-
-		verify_seed = getattr(self,'_verify_seed_'+
-						('newfmt','oldfmt')[bool(opt.old_incog_fmt)])
-
-		seed = verify_seed( crypto.decrypt_seed(d.enc_seed, key, '', '') )
-
-		if seed:
-			self.seed = Seed(seed)
-			msg(f'Seed ID: {self.seed.sid}')
-			return True
-		else:
-			return False
-
-class IncogWallet(IncogWalletBase):
-
-	desc = 'incognito data'
-	fmt_codes = ('mmincog','incog','icg','i')
-	ext = 'mmincog'
-	file_mode = 'binary'
-	no_tty = True
-
-class IncogWalletHex(IncogWalletBase):
-
-	desc = 'hex incognito data'
-	fmt_codes = ('mmincox','incox','incog_hex','xincog','ix','xi')
-	ext = 'mmincox'
-	file_mode = 'text'
-	no_tty = False
-
-	def _deformat(self):
-		ret = decode_pretty_hexdump(self.fmt_data)
-		if ret:
-			self.fmt_data = ret
-			return super()._deformat()
-		else:
-			return False
-
-	def _format(self):
-		super()._format()
-		self.fmt_data = pretty_hexdump(self.fmt_data)
-
-class IncogWalletHidden(IncogWalletBase):
-
-	desc = 'hidden incognito data'
-	fmt_codes = ('incog_hidden','hincog','ih','hi')
-	ext = None
-	file_mode = 'binary'
-	no_tty = True
-
-	_msg = {
-		'choose_file_size': """
-You must choose a size for your new hidden incog data.  The minimum size is
-{} bytes, which puts the incog data right at the end of the file. Since you
-probably want to hide your data somewhere in the middle of the file where it's
-harder to find, you're advised to choose a much larger file size than this.
-	""".strip(),
-		'check_incog_id': """
-  Check generated Incog ID above against your records.  If it doesn't
-  match, then your incognito data is incorrect or corrupted, or you
-  may have specified an incorrect offset.
-	""",
-		'record_incog_id': """
-  Make a record of the Incog ID but keep it secret.  You will used it to
-  identify the incog wallet data in the future and to locate the offset
-  where the data is hidden in the event you forget it.
-	""",
-		'dec_chk': ', hash preset, offset {} seed length'
-	}
-
-	def _get_hincog_params(self,wtype):
-		a = getattr(opt,'hidden_incog_'+ wtype +'_params').split(',')
-		return ','.join(a[:-1]),int(a[-1]) # permit comma in filename
-
-	def _check_valid_offset(self,fn,action):
-		d = self.ssdata
-		m = ('Input','Destination')[action=='write']
-		if fn.size < d.hincog_offset + d.target_data_len:
-			die(1,'{} file {!r} has length {}, too short to {} {} bytes of data at offset {}'.format(
-				m,
-				fn.name,
-				fn.size,
-				action,
-				d.target_data_len,
-				d.hincog_offset ))
-
-	def _get_data(self):
-		d = self.ssdata
-		d.hincog_offset = self._get_hincog_params('input')[1]
-
-		qmsg(f'Getting hidden incog data from file {self.infile.name!r}')
-
-		# Already sanity-checked:
-		d.target_data_len = self._get_incog_data_len(opt.seed_len or Seed.dfl_len)
-		self._check_valid_offset(self.infile,'read')
-
-		flgs = os.O_RDONLY|os.O_BINARY if g.platform == 'win' else os.O_RDONLY
-		fh = os.open(self.infile.name,flgs)
-		os.lseek(fh,int(d.hincog_offset),os.SEEK_SET)
-		self.fmt_data = os.read(fh,d.target_data_len)
-		os.close(fh)
-		qmsg(f'Data read from file {self.infile.name!r} at offset {d.hincog_offset}')
-
-	# overrides method in Wallet
-	def write_to_file(self):
-		d = self.ssdata
-		self._format()
-		compare_or_die(d.target_data_len, 'target data length',
-				len(self.fmt_data),'length of formatted ' + self.desc)
-
-		k = ('output','input')[self.op=='pwchg_new']
-		fn,d.hincog_offset = self._get_hincog_params(k)
-
-		if opt.outdir and not os.path.dirname(fn):
-			fn = os.path.join(opt.outdir,fn)
-
-		check_offset = True
-		try:
-			os.stat(fn)
-		except:
-			if keypress_confirm(
-					f'Requested file {fn!r} does not exist.  Create?',
-					default_yes = True ):
-				min_fsize = d.target_data_len + d.hincog_offset
-				msg(self.msg['choose_file_size'].format(min_fsize))
-				while True:
-					fsize = parse_bytespec(line_input('Enter file size: '))
-					if fsize >= min_fsize:
-						break
-					msg(f'File size must be an integer no less than {min_fsize}')
-
-				from .tool.fileutil import tool_cmd
-				tool_cmd().rand2file(fn,str(fsize))
-				check_offset = False
-			else:
-				die(1,'Exiting at user request')
-
-		from .filename import Filename
-		f = Filename(fn,subclass=type(self),write=True)
-
-		dmsg('{} data len {}, offset {}'.format(
-			capfirst(self.desc),
-			d.target_data_len,
-			d.hincog_offset ))
-
-		if check_offset:
-			self._check_valid_offset(f,'write')
-			if not opt.quiet:
-				confirm_or_raise( '', f'alter file {f.name!r}' )
-
-		flgs = os.O_RDWR|os.O_BINARY if g.platform == 'win' else os.O_RDWR
-		fh = os.open(f.name,flgs)
-		os.lseek(fh, int(d.hincog_offset), os.SEEK_SET)
-		os.write(fh, self.fmt_data)
-		os.close(fh)
-		msg('{} written to file {!r} at offset {}'.format(
-			capfirst(self.desc),
-			f.name,
-			d.hincog_offset ))

+ 158 - 0
mmgen/wallet/__init__.py

@@ -0,0 +1,158 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+wallet.__init__: wallet class initializer
+"""
+
+import importlib
+from collections import namedtuple
+
+from ..globalvars import g
+from ..opts import opt
+from ..util import die,get_extension
+from ..objmethods import MMGenObject
+from ..seed import Seed
+
+_wd = namedtuple('wallet_data', ['type','name','ext','base_type','enc','fmt_codes'])
+_pd = namedtuple('partial_wallet_data',['name','ext','base_type','enc','fmt_codes'])
+
+wallet_data = {
+	'bip39':       _pd('BIP39Mnemonic',    'bip39',  'mnemonic',  False,('bip39',)),
+	'brain':       _pd('Brainwallet',      'mmbrain',None,        True, ('mmbrain','brainwallet','brain','bw')),
+	'dieroll':     _pd('DieRollWallet',    'b6d',    None,        False,('b6d','die','dieroll')),
+	'incog':       _pd('IncogWallet',      'mmincog','incog_base',True, ('mmincog','incog','icg','i')),
+	'incog_hex':   _pd('IncogWalletHex',   'mmincox','incog_base',True, ('mmincox','incox','incog_hex','ix','xi')),
+	'incog_hidden':_pd('IncogWalletHidden',None,     'incog_base',True, ('incog_hidden','hincog','ih','hi')),
+	'mmgen':       _pd('MMGenWallet',      'mmdat',  None,        True, ('wallet','w')),
+	'mmhex':       _pd('MMGenHexSeedFile', 'mmhex',  None,        False,('seedhex','hexseed','mmhex')),
+	'plainhex':    _pd('PlainHexSeedFile', 'hex',    None,        False,('hex','rawhex','plainhex')),
+	'seed':        _pd('MMGenSeedFile',    'mmseed', None,        False,('mmseed','seed','s')),
+	'words':       _pd('MMGenMnemonic',    'mmwords','mnemonic',  False,('mmwords','words','mnemonic','mn','m')),
+}
+
+def get_wallet_data(*args,**kwargs):
+
+	if args:
+		return _wd( args[0], *wallet_data[args[0]] )
+
+	for key in ('fmt_code','ext'):
+		if key in kwargs:
+			val = kwargs[key]
+			break
+	else:
+		die('{!r}: unrecognized argument'.format( list(kwargs.keys())[0] ))
+
+	if key == 'fmt_code':
+		for k,v in wallet_data.items():
+			if val in v.fmt_codes:
+				return _wd(k,*v)
+	else:
+		for k,v in wallet_data.items():
+			if val == getattr(v,key):
+				return _wd(k,*v)
+
+	if 'die_on_fail' in kwargs:
+		die( *{
+			'ext':      ('BadFileExtension', f'{val!r}: unrecognized wallet file extension'),
+			'fmt_code': (3, f'{val!r}: unrecognized wallet format code'),
+			'type':     (3, f'{val!r}: unrecognized wallet type'),
+		}[key] )
+
+def get_wallet_cls(*args,**kwargs):
+	return getattr(
+		importlib.import_module( 'mmgen.wallet.{}'.format(
+			args[0] if args else get_wallet_data(*args,**kwargs).type)
+		),
+		'wallet' )
+
+def get_wallet_extensions(key):
+	return {
+		'enc':   [v.ext for v in wallet_data.values() if v.enc],
+		'unenc': [v.ext for v in wallet_data.values() if not v.enc]
+	}[key]
+
+def format_fmt_codes():
+	d = [(
+			v.name,
+			('.' + v.ext if v.ext else 'None'),
+			','.join(v.fmt_codes)
+		) for v in wallet_data.values()]
+	w = max(len(i[0]) for i in d)
+	ret = [f'{a:<{w}}  {b:<9} {c}' for a,b,c in [
+		('Format','FileExt','Valid codes'),
+		('------','-------','-----------')
+		] + sorted(d) ]
+	return '\n'.join(ret) + ('','-α')[g.debug_utf8] + '\n'
+
+def _get_me(modname):
+	return MMGenObject.__new__( getattr( importlib.import_module(f'mmgen.wallet.{modname}'), 'wallet' ) )
+
+def Wallet(
+	fn            = None,
+	ss            = None,
+	seed_bin      = None,
+	seed          = None,
+	passchg       = False,
+	in_data       = None,
+	ignore_in_fmt = False,
+	in_fmt        = None,
+	passwd_file   = None ):
+
+	in_fmt = in_fmt or opt.in_fmt
+
+	if opt.out_fmt:
+		ss_out = get_wallet_data(fmt_code=opt.out_fmt)
+		if not ss_out:
+			die(1,f'{opt.out_fmt!r}: unrecognized output format')
+	else:
+		ss_out = None
+
+	if seed or seed_bin:
+		me = _get_me( ss_out.type if ss_out else 'mmgen' ) # default to native wallet format
+		me.seed = seed or Seed(seed_bin=seed_bin)
+		me.op = 'new'
+	elif ss:
+		me = _get_me( ss.type if passchg else ss_out.type if ss_out else 'mmgen' )
+		me.seed = ss.seed
+		me.ss_in = ss
+		me.op = 'pwchg_new' if passchg else 'conv'
+	elif fn or opt.hidden_incog_input_params:
+		if fn:
+			wd = get_wallet_data(ext=get_extension(fn),die_on_fail=True)
+			if in_fmt and (not ignore_in_fmt) and in_fmt not in wd.fmt_codes:
+				die(1,f'{in_fmt}: --in-fmt parameter does not match extension of input file')
+			me = _get_me( wd.type )
+		else:
+			fn = ','.join(opt.hidden_incog_input_params.split(',')[:-1]) # permit comma in filename
+			me = _get_me( 'incog_hidden' )
+		from ..filename import Filename
+		me.infile = Filename( fn, subclass=type(me) )
+		me.op = 'pwchg_old' if passchg else 'old'
+	elif in_fmt:
+		me = _get_me( get_wallet_data(fmt_code=in_fmt).type )
+		me.op = 'pwchg_old' if passchg else 'old'
+	else: # called with no arguments: initialize with random seed
+		me = _get_me( ss_out.type if ss_out else 'mmgen' ) # default to native wallet format
+		me.seed = Seed()
+		me.op = 'new'
+
+	me.__init__(
+		fn            = fn,
+		ss            = ss,
+		seed_bin      = seed_bin,
+		seed          = seed,
+		passchg       = passchg,
+		in_data       = in_data,
+		ignore_in_fmt = ignore_in_fmt,
+		in_fmt        = in_fmt,
+		passwd_file   = passwd_file )
+
+	return me

+ 136 - 0
mmgen/wallet/base.py

@@ -0,0 +1,136 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+wallet.base: wallet base class
+"""
+
+import os
+
+from ..globalvars import g
+from ..opts import opt
+from ..util import msg,qmsg,die,get_data_from_user
+from ..objmethods import MMGenObject
+from . import Wallet,wallet_data,get_wallet_cls
+
+class WalletMeta(type):
+
+	def __init__(cls,name,bases,namespace):
+		cls.type = cls.__module__.split('.')[-1]
+		if cls.type in wallet_data:
+			for k,v in wallet_data[cls.type]._asdict().items():
+				setattr(cls,k,v)
+
+class wallet(MMGenObject,metaclass=WalletMeta):
+
+	desc = 'MMGen seed source'
+	file_mode = 'text'
+	filename_api = True
+	stdin_ok = False
+	ask_tty = True
+	no_tty  = False
+	op = None
+
+	class WalletData(MMGenObject):
+		pass
+
+	def __init__(self,
+		fn            = None,
+		ss            = None,
+		seed_bin      = None,
+		seed          = None,
+		passchg       = False,
+		in_data       = None,
+		ignore_in_fmt = False,
+		in_fmt        = None,
+		passwd_file   = None ):
+
+		self.passwd_file = passwd_file or opt.passwd_file
+		self.ssdata = self.WalletData()
+		self.msg = {}
+		self.in_data = in_data
+
+		for c in reversed(self.__class__.__mro__):
+			if hasattr(c,'_msg'):
+				self.msg.update(c._msg)
+
+		if hasattr(self,'seed'):
+			self._encrypt()
+			return
+		elif hasattr(self,'infile') or self.in_data or not g.stdin_tty:
+			self._deformat_once()
+			self._decrypt_retry()
+		else:
+			if not self.stdin_ok:
+				die(1,f'Reading from standard input not supported for {self.desc} format')
+			self._deformat_retry()
+			self._decrypt_retry()
+
+		qmsg('Valid {} for Seed ID {}{}'.format(
+			self.desc,
+			self.seed.sid.hl(),
+			(f', seed length {self.seed.bitlen}' if self.seed.bitlen != 256 else '')
+		))
+
+	def _get_data(self):
+		if hasattr(self,'infile'):
+			from ..fileutil import get_data_from_file
+			self.fmt_data = get_data_from_file(self.infile.name,self.desc,binary=self.file_mode=='binary')
+		elif self.in_data:
+			self.fmt_data = self.in_data
+		else:
+			self.fmt_data = self._get_data_from_user(self.desc)
+
+	def _get_data_from_user(self,desc):
+		return get_data_from_user(desc)
+
+	def _deformat_once(self):
+		self._get_data()
+		if not self._deformat():
+			die(2,'Invalid format for input data')
+
+	def _deformat_retry(self):
+		while True:
+			self._get_data()
+			if self._deformat():
+				break
+			msg('Trying again...')
+
+	@classmethod
+	def ext_to_cls(cls,ext,proto):
+		return get_wallet_cls(ext=ext)
+
+	def get_fmt_data(self):
+		self._format()
+		return self.fmt_data
+
+	def write_to_file(self,outdir='',desc=''):
+		self._format()
+		kwargs = {
+			'desc':     desc or self.desc,
+			'ask_tty':  self.ask_tty,
+			'no_tty':   self.no_tty,
+			'binary':   self.file_mode == 'binary'
+		}
+
+		if outdir:
+			# write_data_to_file(): outfile with absolute path overrides opt.outdir
+			of = os.path.abspath(os.path.join(outdir,self._filename()))
+
+		from ..fileutil import write_data_to_file
+		write_data_to_file(
+			of if outdir else self._filename(),
+			self.fmt_data,
+			**kwargs )
+
+	def check_usr_seed_len(self,bitlen=None):
+		chk = bitlen or self.seed.bitlen
+		if opt.seed_len and opt.seed_len != chk:
+			die(1,f'ERROR: requested seed length ({opt.seed_len}) doesn’t match seed length of source ({chk})')

+ 26 - 0
mmgen/wallet/bip39.py

@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+wallet.bip39: BIP39 mnemonic wallet class
+"""
+
+from .mnemonic import wallet
+
+class wallet(wallet):
+
+	desc = 'BIP39 mnemonic data'
+	mn_type = 'BIP39'
+	wl_id = 'bip39'
+
+	def __init__(self,*args,**kwargs):
+		from ..bip39 import bip39
+		self.conv_cls = bip39
+		super().__init__(*args,**kwargs)

+ 67 - 0
mmgen/wallet/brain.py

@@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+wallet.brain: brainwallet wallet class
+"""
+
+from ..opts import opt
+from ..util import msg,qmsg,qmsg_r
+from ..color import yellow
+from .enc import wallet
+from .seed import Seed
+import mmgen.crypto as crypto
+
+class wallet(wallet):
+
+	stdin_ok = True
+	desc = 'brainwallet'
+	# brainwallet warning message? TODO
+
+	def get_bw_params(self):
+		# already checked
+		a = opt.brain_params.split(',')
+		return int(a[0]),a[1]
+
+	def _deformat(self):
+		self.brainpasswd = ' '.join(self.fmt_data.split())
+		return True
+
+	def _decrypt(self):
+		d = self.ssdata
+		if opt.brain_params:
+			"""
+			Don't set opt.seed_len!  When using multiple wallets, BW seed len might differ from others
+			"""
+			bw_seed_len,d.hash_preset = self.get_bw_params()
+		else:
+			if not opt.seed_len:
+				qmsg(f'Using default seed length of {yellow(str(Seed.dfl_len))} bits\n'
+					+ 'If this is not what you want, use the --seed-len option' )
+			self._get_hash_preset()
+			bw_seed_len = opt.seed_len or Seed.dfl_len
+		qmsg_r('Hashing brainwallet data.  Please wait...')
+		# Use buflen arg of scrypt.hash() to get seed of desired length
+		seed = crypto.scrypt_hash_passphrase(
+			self.brainpasswd.encode(),
+			b'',
+			d.hash_preset,
+			buflen = bw_seed_len // 8 )
+		qmsg('Done')
+		self.seed = Seed(seed)
+		msg(f'Seed ID: {self.seed.sid}')
+		qmsg('Check this value against your records')
+		return True
+
+	def _format(self):
+		raise NotImplementedError('Brainwallet not supported as an output format')
+
+	def _encrypt(self):
+		raise NotImplementedError('Brainwallet not supported as an output format')

+ 114 - 0
mmgen/wallet/dieroll.py

@@ -0,0 +1,114 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+wallet.dieroll: dieroll wallet class
+"""
+
+from ..globalvars import g
+from ..opts import opt
+from ..util import msg,msg_r,die,fmt,block_format,remove_whitespace,keypress_confirm
+from ..seed import Seed
+from ..baseconv import baseconv
+from .unenc import wallet
+
+class wallet(wallet):
+
+	stdin_ok = True
+	desc = 'base6d die roll seed data'
+	conv_cls = baseconv
+	wl_id = 'b6d'
+	mn_type = 'base6d'
+	choose_seedlen_prompt = 'Choose a seed length: 1) 128 bits, 2) 192 bits, 3) 256 bits: '
+	choose_seedlen_confirm = 'Seed length of {} bits chosen. OK?'
+	user_entropy_prompt = 'Would you like to provide some additional entropy from the keyboard?'
+	interactive_input = False
+
+	def _format(self):
+		d = baseconv('b6d').frombytes(self.seed.data,pad='seed',tostr=True) + '\n'
+		self.fmt_data = block_format(d,gw=5,cols=5)
+
+	def _deformat(self):
+
+		d = remove_whitespace(self.fmt_data)
+		bc = baseconv('b6d')
+		rmap = bc.seedlen_map_rev
+
+		if not len(d) in rmap:
+			die( 'SeedLengthError', '{!r}: invalid length for {} (must be one of {})'.format(
+				len(d),
+				self.desc,
+				list(rmap) ))
+
+		# truncate seed to correct length, discarding high bits
+		seed_len = rmap[len(d)]
+		seed_bytes = bc.tobytes( d, pad='seed' )[-seed_len:]
+
+		if self.interactive_input and opt.usr_randchars:
+			if keypress_confirm(self.user_entropy_prompt):
+				from ..crypto import add_user_random
+				seed_bytes = add_user_random(
+					rand_bytes = seed_bytes,
+					desc       = 'gathered from your die rolls' )
+				self.desc += ' plus user-supplied entropy'
+
+		self.seed = Seed(seed_bytes)
+		self.ssdata.hexseed = seed_bytes.hex()
+
+		self.check_usr_seed_len()
+		return True
+
+	def _get_data_from_user(self,desc):
+
+		if not g.stdin_tty:
+			return get_data_from_user(desc)
+
+		bc = baseconv('b6d')
+
+		seed_bitlen = self._choose_seedlen([ n*8 for n in sorted(bc.seedlen_map) ])
+		nDierolls = bc.seedlen_map[seed_bitlen // 8]
+
+		message = """
+			For a {sb}-bit seed you must roll the die {nd} times.  After each die roll,
+			enter the result on the keyboard as a digit.  If you make an invalid entry,
+			you'll be prompted to re-enter it.
+		"""
+		msg('\n'+fmt(message.strip()).format(sb=seed_bitlen,nd=nDierolls)+'\n')
+
+		CUR_HIDE = '\033[?25l'
+		CUR_SHOW = '\033[?25h'
+		cr = '\n' if g.test_suite else '\r'
+		prompt_fs = f'\b\b\b   {cr}Enter die roll #{{}}: {CUR_SHOW}'
+		clear_line = '' if g.test_suite else '\r' + ' ' * 25
+		invalid_msg = CUR_HIDE + cr + 'Invalid entry' + ' ' * 11
+
+		from ..term import get_char
+		def get_digit(n):
+			p = prompt_fs
+			sleep = g.short_disp_timeout
+			while True:
+				ch = get_char(p.format(n),num_chars=1,sleep=sleep)
+				if ch in bc.digits:
+					msg_r(CUR_HIDE + ' OK')
+					return ch
+				else:
+					msg_r(invalid_msg)
+					sleep = g.err_disp_timeout
+					p = clear_line + prompt_fs
+
+		dierolls,n = [],1
+		while len(dierolls) < nDierolls:
+			dierolls.append(get_digit(n))
+			n += 1
+
+		msg('Die rolls successfully entered' + CUR_SHOW)
+		self.interactive_input = True
+
+		return ''.join(dierolls)

+ 96 - 0
mmgen/wallet/enc.py

@@ -0,0 +1,96 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+wallet.enc: encrypted wallet base class
+"""
+
+from ..globalvars import g
+from ..opts import opt
+from ..util import msg,qmsg,make_chksum_8
+
+import mmgen.crypto as crypto
+
+from .base import wallet
+
+class wallet(wallet):
+
+	def _decrypt_retry(self):
+		while True:
+			if self._decrypt():
+				break
+			if self.passwd_file:
+				die(2,'Passphrase from password file, so exiting')
+			msg('Trying again...')
+
+	def _get_hash_preset_from_user(self,hp,add_desc=''):
+		prompt = 'Enter {}hash preset for {}{}{},\nor hit ENTER to {} value ({!r}): '.format(
+			('old ' if self.op=='pwchg_old' else 'new ' if self.op=='pwchg_new' else ''),
+			('','new ')[self.op=='new'],
+			self.desc,
+			('',' '+add_desc)[bool(add_desc)],
+			('accept the default','reuse the old')[self.op=='pwchg_new'],
+			hp )
+		return crypto.get_hash_preset_from_user( hash_preset=hp, prompt=prompt )
+
+	def _get_hash_preset(self,add_desc=''):
+		if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'hash_preset'):
+			old_hp = self.ss_in.ssdata.hash_preset
+			if opt.keep_hash_preset:
+				hp = old_hp
+				qmsg(f'Reusing hash preset {hp!r} at user request')
+			elif opt.hash_preset:
+				hp = opt.hash_preset
+				qmsg(f'Using hash preset {hp!r} requested on command line')
+			else: # Prompt, using old value as default
+				hp = self._get_hash_preset_from_user(old_hp,add_desc)
+			if (not opt.keep_hash_preset) and self.op == 'pwchg_new':
+				qmsg('Hash preset {}'.format( 'unchanged' if hp == old_hp else f'changed to {hp!r}' ))
+		elif opt.hash_preset:
+			hp = opt.hash_preset
+			qmsg(f'Using hash preset {hp!r} requested on command line')
+		else:
+			hp = self._get_hash_preset_from_user(g.dfl_hash_preset,add_desc)
+		self.ssdata.hash_preset = hp
+
+	def _get_new_passphrase(self):
+		return crypto.get_new_passphrase(
+			data_desc = ('new ' if self.op in ('new','conv') else '') + self.desc,
+			hash_preset = self.ssdata.hash_preset,
+			passwd_file = self.passwd_file,
+			pw_desc = ('new ' if self.op=='pwchg_new' else '') + 'passphrase' )
+
+	def _get_passphrase(self,add_desc=''):
+		return crypto.get_passphrase(
+			data_desc = self.desc + (f' {add_desc}' if add_desc else ''),
+			passwd_file = self.passwd_file,
+			pw_desc = ('old ' if self.op == 'pwchg_old' else '') + 'passphrase' )
+
+	def _get_first_pw_and_hp_and_encrypt_seed(self):
+		d = self.ssdata
+		self._get_hash_preset()
+
+		if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'passwd'):
+			old_pw = self.ss_in.ssdata.passwd
+			if opt.keep_passphrase:
+				d.passwd = old_pw
+				qmsg('Reusing passphrase at user request')
+			else:
+				d.passwd = self._get_new_passphrase()
+				if self.op == 'pwchg_new':
+					qmsg('Passphrase {}'.format( 'unchanged' if d.passwd == old_pw else 'changed' ))
+		else:
+			d.passwd = self._get_new_passphrase()
+
+		from hashlib import sha256
+		d.salt     = sha256( crypto.get_random(128) ).digest()[:crypto.salt_len]
+		key        = crypto.make_key( d.passwd, d.salt, d.hash_preset )
+		d.key_id   = make_chksum_8(key)
+		d.enc_seed = crypto.encrypt_seed( self.seed.data, key )

+ 21 - 0
mmgen/wallet/incog.py

@@ -0,0 +1,21 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+wallet.incog: incognito wallet class
+"""
+
+from .incog_base import wallet
+
+class wallet(wallet):
+
+	desc = 'incognito data'
+	file_mode = 'binary'
+	no_tty = True

+ 169 - 0
mmgen/wallet/incog_base.py

@@ -0,0 +1,169 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+wallet.incog_base: incognito wallet base class
+"""
+
+from ..globalvars import g
+from ..opts import opt
+from ..seed import Seed
+from ..util import msg,vmsg,qmsg,make_chksum_8,keypress_confirm
+from .enc import wallet
+import mmgen.crypto as crypto
+
+class wallet(wallet):
+
+	_msg = {
+		'check_incog_id': """
+  Check the generated Incog ID above against your records.  If it doesn't
+  match, then your incognito data is incorrect or corrupted.
+	""",
+		'record_incog_id': """
+  Make a record of the Incog ID but keep it secret.  You will use it to
+  identify your incog wallet data in the future.
+	""",
+		'decrypt_params': " {} hash preset"
+	}
+
+	def _make_iv_chksum(self,s):
+		from hashlib import sha256
+		return sha256(s).hexdigest()[:8].upper()
+
+	def _get_incog_data_len(self,seed_len):
+		return (
+			crypto.aesctr_iv_len
+			+ crypto.salt_len
+			+ (0 if opt.old_incog_fmt else crypto.hincog_chk_len)
+			+ seed_len//8 )
+
+	def _incog_data_size_chk(self):
+		# valid sizes: 56, 64, 72
+		dlen = len(self.fmt_data)
+		seed_len = opt.seed_len or Seed.dfl_len
+		valid_dlen = self._get_incog_data_len(seed_len)
+		if dlen == valid_dlen:
+			return True
+		else:
+			if opt.old_incog_fmt:
+				msg('WARNING: old-style incognito format requested.  Are you sure this is correct?')
+			msg(f'Invalid incognito data size ({dlen} bytes) for this seed length ({seed_len} bits)')
+			msg(f'Valid data size for this seed length: {valid_dlen} bytes')
+			for sl in Seed.lens:
+				if dlen == self._get_incog_data_len(sl):
+					die(1,f'Valid seed length for this data size: {sl} bits')
+			msg(f'This data size ({dlen} bytes) is invalid for all available seed lengths')
+			return False
+
+	def _encrypt (self):
+		self._get_first_pw_and_hp_and_encrypt_seed()
+		if opt.old_incog_fmt:
+			die(1,'Writing old-format incog wallets is unsupported')
+		d = self.ssdata
+		# IV is used BOTH to initialize counter and to salt password!
+		d.iv = crypto.get_random( crypto.aesctr_iv_len )
+		d.iv_id = self._make_iv_chksum(d.iv)
+		msg(f'New Incog Wallet ID: {d.iv_id}')
+		qmsg('Make a record of this value')
+		vmsg('\n  ' + self.msg['record_incog_id'].strip()+'\n')
+
+		d.salt = crypto.get_random( crypto.salt_len )
+		key = crypto.make_key( d.passwd, d.salt, d.hash_preset, 'incog wallet key' )
+		from hashlib import sha256
+		chk = sha256(self.seed.data).digest()[:8]
+		d.enc_seed = crypto.encrypt_data(
+			chk + self.seed.data,
+			key,
+			crypto.aesctr_dfl_iv,
+			'seed' )
+
+		d.wrapper_key = crypto.make_key( d.passwd, d.iv, d.hash_preset, 'incog wrapper key' )
+		d.key_id = make_chksum_8(d.wrapper_key)
+		vmsg(f'Key ID: {d.key_id}')
+		d.target_data_len = self._get_incog_data_len(self.seed.bitlen)
+
+	def _format(self):
+		d = self.ssdata
+		self.fmt_data = d.iv + crypto.encrypt_data(
+			d.salt + d.enc_seed,
+			d.wrapper_key,
+			d.iv,
+			self.desc )
+
+	def _filename(self):
+		s = self.seed
+		d = self.ssdata
+		return '{}-{}-{}[{},{}]{x}.{}'.format(
+				s.fn_stem,
+				d.key_id,
+				d.iv_id,
+				s.bitlen,
+				d.hash_preset,
+				self.ext,
+				x='-α' if g.debug_utf8 else '')
+
+	def _deformat(self):
+
+		if not self._incog_data_size_chk():
+			return False
+
+		d = self.ssdata
+		d.iv             = self.fmt_data[0:crypto.aesctr_iv_len]
+		d.incog_id       = self._make_iv_chksum(d.iv)
+		d.enc_incog_data = self.fmt_data[crypto.aesctr_iv_len:]
+		msg(f'Incog Wallet ID: {d.incog_id}')
+		qmsg('Check this value against your records')
+		vmsg('\n  ' + self.msg['check_incog_id'].strip()+'\n')
+
+		return True
+
+	def _verify_seed_newfmt(self,data):
+		chk,seed = data[:8],data[8:]
+		from hashlib import sha256
+		if sha256(seed).digest()[:8] == chk:
+			qmsg('Passphrase{} are correct'.format( self.msg['decrypt_params'].format('and') ))
+			return seed
+		else:
+			msg('Incorrect passphrase{}'.format( self.msg['decrypt_params'].format('or') ))
+			return False
+
+	def _verify_seed_oldfmt(self,seed):
+		m = f'Seed ID: {make_chksum_8(seed)}.  Is the Seed ID correct?'
+		if keypress_confirm(m, True):
+			return seed
+		else:
+			return False
+
+	def _decrypt(self):
+		d = self.ssdata
+		self._get_hash_preset(add_desc=d.incog_id)
+		d.passwd = self._get_passphrase(add_desc=d.incog_id)
+
+		# IV is used BOTH to initialize counter and to salt password!
+		key = crypto.make_key( d.passwd, d.iv, d.hash_preset, 'wrapper key' )
+		dd = crypto.decrypt_data( d.enc_incog_data, key, d.iv, 'incog data' )
+
+		d.salt     = dd[0:crypto.salt_len]
+		d.enc_seed = dd[crypto.salt_len:]
+
+		key = crypto.make_key( d.passwd, d.salt, d.hash_preset, 'main key' )
+		qmsg(f'Key ID: {make_chksum_8(key)}')
+
+		verify_seed = getattr(self,'_verify_seed_'+
+						('newfmt','oldfmt')[bool(opt.old_incog_fmt)])
+
+		seed = verify_seed( crypto.decrypt_seed(d.enc_seed, key, '', '') )
+
+		if seed:
+			self.seed = Seed(seed)
+			msg(f'Seed ID: {self.seed.sid}')
+			return True
+		else:
+			return False

+ 34 - 0
mmgen/wallet/incog_hex.py

@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+wallet.incog_hex: hexadecimal incognito wallet class
+"""
+
+from ..util import pretty_hexdump,decode_pretty_hexdump
+from .incog_base import wallet
+
+class wallet(wallet):
+
+	desc = 'hex incognito data'
+	file_mode = 'text'
+	no_tty = False
+
+	def _deformat(self):
+		ret = decode_pretty_hexdump(self.fmt_data)
+		if ret:
+			self.fmt_data = ret
+			return super()._deformat()
+		else:
+			return False
+
+	def _format(self):
+		super()._format()
+		self.fmt_data = pretty_hexdump(self.fmt_data)

+ 149 - 0
mmgen/wallet/incog_hidden.py

@@ -0,0 +1,149 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+wallet.incog_hidden: hidden incognito wallet class
+"""
+
+import os
+
+from ..globalvars import g
+from ..opts import opt
+from ..seed import Seed
+from ..util import (
+	msg,
+	dmsg,
+	qmsg,
+	die,
+	compare_or_die,
+	keypress_confirm,
+	parse_bytespec,
+	line_input,
+	capfirst,
+	confirm_or_raise
+)
+from .incog_base import wallet
+
+class wallet(wallet):
+
+	desc = 'hidden incognito data'
+	file_mode = 'binary'
+	no_tty = True
+
+	_msg = {
+		'choose_file_size': """
+  You must choose a size for your new hidden incog data.  The minimum size
+  is {} bytes, which puts the incog data right at the end of the file.
+  Since you probably want to hide your data somewhere in the middle of the
+  file where it's harder to find, you're advised to choose a much larger file
+  size than this.
+	""",
+		'check_incog_id': """
+  Check generated Incog ID above against your records.  If it doesn't match,
+  then your incognito data is incorrect or corrupted, or you may have speci-
+  fied an incorrect offset.
+	""",
+		'record_incog_id': """
+  Make a record of the Incog ID but keep it secret.  You will used it to
+  identify the incog wallet data in the future and to locate the offset
+  where the data is hidden in the event you forget it.
+	""",
+		'decrypt_params': ', hash preset, offset {} seed length'
+	}
+
+	def _get_hincog_params(self,wtype):
+		a = getattr(opt,'hidden_incog_'+ wtype +'_params').split(',')
+		return ','.join(a[:-1]),int(a[-1]) # permit comma in filename
+
+	def _check_valid_offset(self,fn,action):
+		d = self.ssdata
+		m = ('Input','Destination')[action=='write']
+		if fn.size < d.hincog_offset + d.target_data_len:
+			die(1,'{} file {!r} has length {}, too short to {} {} bytes of data at offset {}'.format(
+				m,
+				fn.name,
+				fn.size,
+				action,
+				d.target_data_len,
+				d.hincog_offset ))
+
+	def _get_data(self):
+		d = self.ssdata
+		d.hincog_offset = self._get_hincog_params('input')[1]
+
+		qmsg(f'Getting hidden incog data from file {self.infile.name!r}')
+
+		# Already sanity-checked:
+		d.target_data_len = self._get_incog_data_len(opt.seed_len or Seed.dfl_len)
+		self._check_valid_offset(self.infile,'read')
+
+		flgs = os.O_RDONLY|os.O_BINARY if g.platform == 'win' else os.O_RDONLY
+		fh = os.open(self.infile.name,flgs)
+		os.lseek(fh,int(d.hincog_offset),os.SEEK_SET)
+		self.fmt_data = os.read(fh,d.target_data_len)
+		os.close(fh)
+		qmsg(f'Data read from file {self.infile.name!r} at offset {d.hincog_offset}')
+
+	# overrides method in Wallet
+	def write_to_file(self):
+		d = self.ssdata
+		self._format()
+		compare_or_die(d.target_data_len, 'target data length',
+				len(self.fmt_data),'length of formatted ' + self.desc)
+
+		k = ('output','input')[self.op=='pwchg_new']
+		fn,d.hincog_offset = self._get_hincog_params(k)
+
+		if opt.outdir and not os.path.dirname(fn):
+			fn = os.path.join(opt.outdir,fn)
+
+		check_offset = True
+		try:
+			os.stat(fn)
+		except:
+			if keypress_confirm(
+					f'Requested file {fn!r} does not exist.  Create?',
+					default_yes = True ):
+				min_fsize = d.target_data_len + d.hincog_offset
+				msg('\n  ' + self.msg['choose_file_size'].strip().format(min_fsize)+'\n')
+				while True:
+					fsize = parse_bytespec(line_input('Enter file size: '))
+					if fsize >= min_fsize:
+						break
+					msg(f'File size must be an integer no less than {min_fsize}')
+
+				from ..tool.fileutil import tool_cmd
+				tool_cmd().rand2file(fn,str(fsize))
+				check_offset = False
+			else:
+				die(1,'Exiting at user request')
+
+		from ..filename import Filename
+		f = Filename(fn,subclass=type(self),write=True)
+
+		dmsg('{} data len {}, offset {}'.format(
+			capfirst(self.desc),
+			d.target_data_len,
+			d.hincog_offset ))
+
+		if check_offset:
+			self._check_valid_offset(f,'write')
+			if not opt.quiet:
+				confirm_or_raise( '', f'alter file {f.name!r}' )
+
+		flgs = os.O_RDWR|os.O_BINARY if g.platform == 'win' else os.O_RDWR
+		fh = os.open(f.name,flgs)
+		os.lseek(fh, int(d.hincog_offset), os.SEEK_SET)
+		os.write(fh, self.fmt_data)
+		os.close(fh)
+		msg('{} written to file {!r} at offset {}'.format(
+			capfirst(self.desc),
+			f.name,
+			d.hincog_offset ))

+ 191 - 0
mmgen/wallet/mmgen.py

@@ -0,0 +1,191 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+wallet.mmgen: MMGen native wallet class
+"""
+
+import os
+
+from ..globalvars import g
+from ..opts import opt
+from ..seed import Seed
+from ..util import msg,qmsg,line_input,make_timestamp,make_chksum_6,split_into_cols,is_chksum_6,compare_chksums
+from ..obj import MMGenWalletLabel,get_obj
+from ..baseconv import baseconv
+
+import mmgen.crypto as crypto
+
+from .enc import wallet
+
+class wallet(wallet):
+
+	desc = 'MMGen wallet'
+
+	def __init__(self,*args,**kwargs):
+		if opt.label:
+			self.label = MMGenWalletLabel(
+				opt.label,
+				msg = "Error in option '--label'" )
+		else:
+			self.label = None
+		super().__init__(*args,**kwargs)
+
+	# logic identical to _get_hash_preset_from_user()
+	def _get_label_from_user(self,old_lbl=''):
+		prompt = 'Enter a wallet label, or hit ENTER {}: '.format(
+			'to reuse the label {}'.format(old_lbl.hl(encl="''")) if old_lbl else
+			'for no label' )
+		while True:
+			ret = line_input(prompt)
+			if ret:
+				lbl = get_obj(MMGenWalletLabel,s=ret)
+				if lbl:
+					return lbl
+				else:
+					msg('Invalid label.  Trying again...')
+			else:
+				return old_lbl or MMGenWalletLabel('No Label')
+
+	# logic identical to _get_hash_preset()
+	def _get_label(self):
+		if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'label'):
+			old_lbl = self.ss_in.ssdata.label
+			if opt.keep_label:
+				lbl = old_lbl
+				qmsg('Reusing label {} at user request'.format( lbl.hl(encl="''") ))
+			elif self.label:
+				lbl = self.label
+				qmsg('Using label {} requested on command line'.format( lbl.hl(encl="''") ))
+			else: # Prompt, using old value as default
+				lbl = self._get_label_from_user(old_lbl)
+			if (not opt.keep_label) and self.op == 'pwchg_new':
+				qmsg('Label {}'.format( 'unchanged' if lbl == old_lbl else f'changed to {lbl!r}' ))
+		elif self.label:
+			lbl = self.label
+			qmsg('Using label {} requested on command line'.format( lbl.hl(encl="''") ))
+		else:
+			lbl = self._get_label_from_user()
+		self.ssdata.label = lbl
+
+	def _encrypt(self):
+		self._get_first_pw_and_hp_and_encrypt_seed()
+		self._get_label()
+		d = self.ssdata
+		d.pw_status = ('NE','E')[len(d.passwd)==0]
+		d.timestamp = make_timestamp()
+
+	def _format(self):
+		d = self.ssdata
+		s = self.seed
+		bc = baseconv('b58')
+		slt_fmt  = bc.frombytes(d.salt,pad='seed',tostr=True)
+		es_fmt = bc.frombytes(d.enc_seed,pad='seed',tostr=True)
+		lines = (
+			d.label,
+			'{} {} {} {} {}'.format( s.sid.lower(), d.key_id.lower(), s.bitlen, d.pw_status, d.timestamp ),
+			'{}: {} {} {}'.format( d.hash_preset, *crypto.get_hash_params(d.hash_preset) ),
+			'{} {}'.format( make_chksum_6(slt_fmt), split_into_cols(4,slt_fmt) ),
+			'{} {}'.format( make_chksum_6(es_fmt),  split_into_cols(4,es_fmt) )
+		)
+		chksum = make_chksum_6(' '.join(lines).encode())
+		self.fmt_data = '\n'.join((chksum,)+lines) + '\n'
+
+	def _deformat(self):
+
+		def check_master_chksum(lines,desc):
+
+			if len(lines) != 6:
+				msg(f'Invalid number of lines ({len(lines)}) in {desc} data')
+				return False
+
+			if not is_chksum_6(lines[0]):
+				msg(f'Incorrect master checksum ({lines[0]}) in {desc} data')
+				return False
+
+			chk = make_chksum_6(' '.join(lines[1:]))
+			if not compare_chksums(lines[0],'master',chk,'computed',
+						hdr='For wallet master checksum',verbose=True):
+				return False
+
+			return True
+
+		lines = self.fmt_data.splitlines()
+		if not check_master_chksum(lines,self.desc):
+			return False
+
+		d = self.ssdata
+		d.label = MMGenWalletLabel(lines[1])
+
+		d1,d2,d3,d4,d5 = lines[2].split()
+		d.seed_id = d1.upper()
+		d.key_id  = d2.upper()
+		self.check_usr_seed_len(int(d3))
+		d.pw_status,d.timestamp = d4,d5
+
+		hpdata = lines[3].split()
+
+		d.hash_preset = hp = hpdata[0][:-1]  # a string!
+		qmsg(f'Hash preset of wallet: {hp!r}')
+		if opt.hash_preset and opt.hash_preset != hp:
+			qmsg(f'Warning: ignoring user-requested hash preset {opt.hash_preset!r}')
+
+		hash_params = tuple(map(int,hpdata[1:]))
+
+		if hash_params != crypto.get_hash_params(d.hash_preset):
+			msg(f'Hash parameters {" ".join(hash_params)!r} don’t match hash preset {d.hash_preset!r}')
+			return False
+
+		lmin,foo,lmax = sorted(baseconv('b58').seedlen_map_rev) # 22,33,44
+		for i,key in (4,'salt'),(5,'enc_seed'):
+			l = lines[i].split(' ')
+			chk = l.pop(0)
+			b58_val = ''.join(l)
+
+			if len(b58_val) < lmin or len(b58_val) > lmax:
+				msg(f'Invalid format for {key} in {self.desc}: {l}')
+				return False
+
+			if not compare_chksums(chk,key,
+					make_chksum_6(b58_val),'computed checksum',verbose=True):
+				return False
+
+			val = baseconv('b58').tobytes(b58_val,pad='seed')
+			if val == False:
+				msg(f'Invalid base 58 number: {b58_val}')
+				return False
+
+			setattr(d,key,val)
+
+		return True
+
+	def _decrypt(self):
+		d = self.ssdata
+		# Needed for multiple transactions with {}-txsign
+		d.passwd = self._get_passphrase(
+			add_desc = os.path.basename(self.infile.name) if opt.quiet else '' )
+		key = crypto.make_key( d.passwd, d.salt, d.hash_preset )
+		ret = crypto.decrypt_seed( d.enc_seed, key, d.seed_id, d.key_id )
+		if ret:
+			self.seed = Seed(ret)
+			return True
+		else:
+			return False
+
+	def _filename(self):
+		s = self.seed
+		d = self.ssdata
+		return '{}-{}[{},{}]{x}.{}'.format(
+				s.fn_stem,
+				d.key_id,
+				s.bitlen,
+				d.hash_preset,
+				self.ext,
+				x='-α' if g.debug_utf8 else '')

+ 66 - 0
mmgen/wallet/mmhex.py

@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+wallet.mmhex: MMGen hexadecimal file wallet class
+"""
+
+from ..util import make_chksum_6,split_into_cols
+from ..seed import Seed
+from ..util import msg,vmsg_r,is_chksum_6,is_hex_str,compare_chksums
+from .unenc import wallet
+
+class wallet(wallet):
+
+	stdin_ok = True
+	desc = 'hexadecimal seed data with checksum'
+
+	def _format(self):
+		h = self.seed.hexdata
+		self.ssdata.chksum = make_chksum_6(h)
+		self.ssdata.hexseed = h
+		self.fmt_data = '{} {}\n'.format(
+			self.ssdata.chksum,
+			split_into_cols(4,h) )
+
+	def _deformat(self):
+		desc = self.desc
+		d = self.fmt_data.split()
+		try:
+			d[1]
+			chk,hstr = d[0],''.join(d[1:])
+		except:
+			msg(f'{self.fmt_data.strip()!r}: invalid {desc}')
+			return False
+
+		if not len(hstr)*4 in Seed.lens:
+			msg(f'Invalid data length ({len(hstr)}) in {desc}')
+			return False
+
+		if not is_chksum_6(chk):
+			msg(f'{chk!r}: invalid checksum format in {desc}')
+			return False
+
+		if not is_hex_str(hstr):
+			msg(f'{hstr!r}: not a hexadecimal string, in {desc}')
+			return False
+
+		vmsg_r(f'Validating {desc} checksum...')
+
+		if not compare_chksums(chk,'file',make_chksum_6(hstr),'computed',verbose=True):
+			return False
+
+		self.seed = Seed(bytes.fromhex(hstr))
+		self.ssdata.chksum = chk
+		self.ssdata.hexseed = hstr
+
+		self.check_usr_seed_len()
+
+		return True

+ 86 - 0
mmgen/wallet/mnemonic.py

@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+wallet.mnemonic: MMGen mnemonic wallet base class
+"""
+
+from ..globalvars import g
+from ..baseconv import baseconv
+from ..util import msg,compare_or_die,get_data_from_user
+from ..seed import Seed
+from .unenc import wallet
+
+class wallet(wallet):
+
+	stdin_ok = True
+	conv_cls = baseconv
+	choose_seedlen_prompt = 'Choose a mnemonic length: 1) 12 words, 2) 18 words, 3) 24 words: '
+	choose_seedlen_confirm = 'Mnemonic length of {} words chosen. OK?'
+
+	@property
+	def mn_lens(self):
+		return sorted(self.conv_cls(self.wl_id).seedlen_map_rev)
+
+	def _get_data_from_user(self,desc):
+
+		if not g.stdin_tty:
+			return get_data_from_user(desc)
+
+		from ..mn_entry import mn_entry # import here to catch cfg var errors
+		mn_len = self._choose_seedlen( self.mn_lens )
+		return mn_entry(self.wl_id).get_mnemonic_from_user(mn_len)
+
+	def _format(self):
+
+		hexseed = self.seed.hexdata
+
+		bc = self.conv_cls(self.wl_id)
+		mn  = bc.fromhex( hexseed, 'seed' )
+		rev = bc.tohex( mn, 'seed' )
+
+		# Internal error, so just die on fail
+		compare_or_die( rev, 'recomputed seed', hexseed, 'original', e='Internal error' )
+
+		self.ssdata.mnemonic = mn
+		self.fmt_data = ' '.join(mn) + '\n'
+
+	def _deformat(self):
+
+		bc = self.conv_cls(self.wl_id)
+		mn = self.fmt_data.split()
+
+		if len(mn) not in self.mn_lens:
+			msg('Invalid mnemonic ({} words).  Valid numbers of words: {}'.format(
+				len(mn),
+				', '.join(map(str,self.mn_lens)) ))
+			return False
+
+		for n,w in enumerate(mn,1):
+			if w not in bc.digits:
+				msg(f'Invalid mnemonic: word #{n} is not in the {self.wl_id.upper()} wordlist')
+				return False
+
+		hexseed = bc.tohex( mn, 'seed' )
+		rev     = bc.fromhex( hexseed, 'seed' )
+
+		if len(hexseed) * 4 not in Seed.lens:
+			msg('Invalid mnemonic (produces too large a number)')
+			return False
+
+		# Internal error, so just die
+		compare_or_die( ' '.join(rev), 'recomputed mnemonic', ' '.join(mn), 'original', e='Internal error' )
+
+		self.seed = Seed(bytes.fromhex(hexseed))
+		self.ssdata.mnemonic = mn
+
+		self.check_usr_seed_len()
+
+		return True

+ 44 - 0
mmgen/wallet/plainhex.py

@@ -0,0 +1,44 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+wallet.plainhex: plain hexadecimal wallet class
+"""
+
+from ..util import msg,is_hex_str_lc
+from ..seed import Seed
+from .unenc import wallet
+
+class wallet(wallet):
+
+	stdin_ok = True
+	desc = 'plain hexadecimal seed data'
+
+	def _format(self):
+		self.fmt_data = self.seed.hexdata + '\n'
+
+	def _deformat(self):
+		desc = self.desc
+		d = self.fmt_data.strip()
+
+		if not is_hex_str_lc(d):
+			msg(f'{d!r}: not a lowercase hexadecimal string, in {desc}')
+			return False
+
+		if not len(d)*4 in Seed.lens:
+			msg(f'Invalid data length ({len(d)}) in {desc}')
+			return False
+
+		self.seed = Seed(bytes.fromhex(d))
+		self.ssdata.hexseed = d
+
+		self.check_usr_seed_len()
+
+		return True

+ 68 - 0
mmgen/wallet/seed.py

@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+wallet.seed: seed file wallet class
+"""
+
+from ..util import msg,vmsg_r,make_chksum_6,split_into_cols,is_chksum_6,compare_chksums
+from ..baseconv import baseconv,is_b58_str
+from ..seed import Seed
+from .unenc import wallet
+
+class wallet(wallet):
+
+	stdin_ok = True
+	desc = 'seed data'
+
+	def _format(self):
+		b58seed = baseconv('b58').frombytes(self.seed.data,pad='seed',tostr=True)
+		self.ssdata.chksum = make_chksum_6(b58seed)
+		self.ssdata.b58seed = b58seed
+		self.fmt_data = '{} {}\n'.format(
+			self.ssdata.chksum,
+			split_into_cols(4,b58seed) )
+
+	def _deformat(self):
+		desc = self.desc
+		ld = self.fmt_data.split()
+
+		if not (7 <= len(ld) <= 12): # 6 <= padded b58 data (ld[1:]) <= 11
+			msg(f'Invalid data length ({len(ld)}) in {desc}')
+			return False
+
+		a,b = ld[0],''.join(ld[1:])
+
+		if not is_chksum_6(a):
+			msg(f'{a!r}: invalid checksum format in {desc}')
+			return False
+
+		if not is_b58_str(b):
+			msg(f'{b!r}: not a base 58 string, in {desc}')
+			return False
+
+		vmsg_r(f'Validating {desc} checksum...')
+
+		if not compare_chksums(a,'file',make_chksum_6(b),'computed',verbose=True):
+			return False
+
+		ret = baseconv('b58').tobytes(b,pad='seed')
+
+		if ret == False:
+			msg(f'Invalid base-58 encoded seed: {val}')
+			return False
+
+		self.seed = Seed(ret)
+		self.ssdata.chksum = a
+		self.ssdata.b58seed = b
+
+		self.check_usr_seed_len()
+
+		return True

+ 57 - 0
mmgen/wallet/unenc.py

@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+wallet.unenc: unencrypted wallet base class
+"""
+
+from ..globalvars import g
+from ..color import blue,yellow
+from ..util import msg,msg_r,capfirst,is_int,keypress_confirm
+from .base import wallet
+
+class wallet(wallet):
+
+	def _decrypt_retry(self):
+		pass
+
+	def _encrypt(self):
+		pass
+
+	def _filename(self):
+		s = self.seed
+		return '{}[{}]{x}.{}'.format(
+			s.fn_stem,
+			s.bitlen,
+			self.ext,
+			x='-α' if g.debug_utf8 else '')
+
+	def _choose_seedlen(self,ok_lens):
+
+		from ..term import get_char
+		def choose_len():
+			prompt = self.choose_seedlen_prompt
+			while True:
+				r = get_char('\r'+prompt)
+				if is_int(r) and 1 <= int(r) <= len(ok_lens):
+					break
+			msg_r(('\r','\n')[g.test_suite] + ' '*len(prompt) + '\r')
+			return ok_lens[int(r)-1]
+
+		msg('{} {}'.format(
+			blue(f'{capfirst(self.base_type or self.type)} type:'),
+			yellow(self.mn_type)
+		))
+
+		while True:
+			usr_len = choose_len()
+			prompt = self.choose_seedlen_confirm.format(usr_len)
+			if keypress_confirm(prompt,default_yes=True,no_nl=not g.test_suite):
+				return usr_len

+ 21 - 0
mmgen/wallet/words.py

@@ -0,0 +1,21 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+wallet.words: MMGen mnemonic wallet class
+"""
+
+from .mnemonic import wallet
+
+class wallet(wallet):
+
+	desc = 'MMGen native mnemonic data'
+	mn_type = 'MMGen native'
+	wl_id = 'mmgen'

+ 1 - 0
setup.cfg

@@ -54,6 +54,7 @@ packages =
 	mmgen.share
 	mmgen.tool
 	mmgen.tx
+	mmgen.wallet
 
 scripts =
 	cmds/mmgen-addrgen

+ 2 - 1
test/overlay/__init__.py

@@ -51,7 +51,8 @@ def overlay_setup(repo_root):
 				'mmgen.proto',
 				'mmgen.share',
 				'mmgen.tool',
-				'mmgen.tx' ):
+				'mmgen.tx',
+				'mmgen.wallet' ):
 			process_srcdir(d)
 
 	return overlay_dir

+ 7 - 7
test/test_py_d/ts_input.py

@@ -13,7 +13,7 @@ ts_input.py: user input tests for the MMGen test.py test suite
 from ..include.common import *
 from .ts_base import *
 from .input import *
-from mmgen.wallet import Wallet
+from mmgen.wallet import get_wallet_cls
 
 class TestSuiteInput(TestSuiteBase):
 	'user input'
@@ -207,19 +207,19 @@ class TestSuiteInput(TestSuiteBase):
 		return t
 
 	def _user_seed_entry(self,fmt,usr_rand=False,out_fmt=None,entry_mode='full',mn=None):
-		wcls = Wallet.fmt_code_to_type(fmt)
+		wcls = get_wallet_cls(fmt_code=fmt)
 		wf = os.path.join(ref_dir,f'FE3C6545.{wcls.ext}')
-		if wcls.wclass == 'mnemonic':
+		if wcls.base_type == 'mnemonic':
 			mn = mn or read_from_file(wf).strip().split()
-		elif wcls.wclass == 'dieroll':
+		elif wcls.type == 'dieroll':
 			mn = mn or list(remove_whitespace(read_from_file(wf)))
 			for idx,val in ((5,'x'),(18,'0'),(30,'7'),(44,'9')):
 				mn.insert(idx,val)
 		t = self.spawn('mmgen-walletconv',['-r10','-S','-i',fmt,'-o',out_fmt or fmt])
-		t.expect(f'{capfirst(wcls.wclass)} type:.*{wcls.mn_type}',regex=True)
+		t.expect(f'{capfirst(wcls.base_type or wcls.type)} type:.*{wcls.mn_type}',regex=True)
 		t.expect(wcls.choose_seedlen_prompt,'1')
 		t.expect('(Y/n): ','y')
-		if wcls.wclass == 'mnemonic':
+		if wcls.base_type == 'mnemonic':
 			t.expect('Type a number.*: ','6',regex=True)
 			t.expect('invalid')
 			from mmgen.mn_entry import mn_entry
@@ -229,7 +229,7 @@ class TestSuiteInput(TestSuiteBase):
 			mode = strip_ansi_escapes(t.p.match.group(1)).lower()
 			assert mode == mne.em.name.lower(), f'{mode} != {mne.em.name.lower()}'
 			stealth_mnemonic_entry(t,mne,mn,entry_mode=entry_mode)
-		elif wcls.wclass == 'dieroll':
+		elif wcls.type == 'dieroll':
 			user_dieroll_entry(t,mn)
 			if usr_rand:
 				t.expect(wcls.user_entropy_prompt,'y')

+ 20 - 17
test/test_py_d/ts_main.py

@@ -23,7 +23,9 @@ ts_main.py: Basic operations tests for the test.py test suite
 from mmgen.globalvars import g
 from mmgen.opts import opt
 from mmgen.fileutil import get_data_from_file,write_data_to_file
-from mmgen.wallet import Wallet,MMGenWallet,MMGenMnemonic,IncogWallet,MMGenSeedFile
+from mmgen.wallet import get_wallet_cls
+from mmgen.wallet.mmgen import wallet as MMGenWallet
+from mmgen.wallet.incog import wallet as IncogWallet
 from mmgen.rpc import rpc_init
 from ..include.common import *
 from .common import *
@@ -277,8 +279,8 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 		return t
 
 	def subwalletgen_mnemonic(self,wf):
-		icls = Wallet.ext_to_type(get_extension(wf))
-		ocls = MMGenMnemonic
+		icls = get_wallet_cls(ext=get_extension(wf))
+		ocls = get_wallet_cls('words')
 		args = [self.usr_rand_arg,'-p1','-d',self.tr.trash_dir,'-o',ocls.fmt_codes[0],wf,'3L']
 		t = self.spawn('mmgen-subwalletgen', args)
 		t.license()
@@ -579,33 +581,34 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 		t.license()
 
 		if not pf:
-			icls = Wallet.ext_to_type(get_extension(wf))
+			icls = get_wallet_cls(ext=get_extension(wf))
 			t.passphrase(icls.desc,self.wpasswd)
 
-		ocls = Wallet.fmt_code_to_type(out_fmt)
+		ocls = get_wallet_cls(fmt_code=out_fmt)
 
-		if issubclass(ocls,WalletEnc) and ocls != Brainwallet:
+		if ocls.enc and ocls.type != 'brain':
 			t.passphrase_new('new '+ocls.desc,self.wpasswd)
 			t.usr_rand(self.usr_rand_chars)
 
-		if ocls.__name__.startswith('Incog'):
+		if ocls.type.startswith('incog'):
 			m = 'Encrypting random data generated by your operating system with key'
 			t.expect(m)
 			t.expect(m)
 			incog_id = t.expect_getend('New Incog Wallet ID: ')
 			t.expect(m)
 
-		if ocls == IncogWalletHidden:
+		if ocls.type == 'incog_hidden':
 			self.write_to_tmpfile(incog_id_fn,incog_id)
 			t.hincog_create(hincog_bytes)
-		elif ocls == MMGenWallet:
+		elif ocls.type == 'mmgen':
 			t.label()
+
 		return t.written_to_file(capfirst(ocls.desc),oo=True),t
 
 	def export_seed(self,wf,out_fmt='seed',pf=None):
 		f,t = self._walletconv_export(wf,out_fmt=out_fmt,pf=pf)
 		silence()
-		wcls = Wallet.fmt_code_to_type(out_fmt)
+		wcls = get_wallet_cls(fmt_code=out_fmt)
 		msg('==> {}: {}'.format(
 			wcls.desc,
 			cyan(get_data_from_file(f,wcls.desc)) ))
@@ -636,8 +639,8 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 		return self.export_incog(wf,out_fmt='hi',add_args=add_args)
 
 	def addrgen_seed(self,wf,foo,in_fmt='seed'):
-		wcls = Wallet.fmt_code_to_type(in_fmt)
-		stdout = wcls == MMGenSeedFile # capture output to screen once
+		wcls = get_wallet_cls(fmt_code=in_fmt)
+		stdout = wcls.type == 'seed' # capture output to screen once
 		t = self.spawn(
 			'mmgen-addrgen',
 			(['-S'] if stdout else []) +
@@ -665,7 +668,7 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 				([],[wf])[bool(wf)] + [self.addr_idx_list])
 		t.license()
 		t.expect_getend('Incog Wallet ID: ')
-		wcls = Wallet.fmt_code_to_type(in_fmt)
+		wcls = get_wallet_cls(fmt_code=in_fmt)
 		t.hash_preset(wcls.desc,'1')
 		t.passphrase(rf'{wcls.desc} \w{{8}}',self.wpasswd)
 		vmsg('Comparing generated checksum with checksum from address file')
@@ -707,7 +710,7 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 		t = self.spawn('mmgen-txsign', ['-d',self.tmpdir,txf1,wf1,txf2,wf2])
 		t.license()
 		for cnum,wf in (('1',wf1),('2',wf2)):
-			wcls = Wallet.ext_to_type(get_extension(wf))
+			wcls = get_wallet_cls(ext=get_extension(wf))
 			t.view_tx('n')
 			t.passphrase(wcls.desc,self.cfgs[cnum]['wpasswd'])
 			self.txsign_end(t,cnum)
@@ -730,7 +733,7 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 		t.license()
 		t.view_tx('n')
 		for cnum,wf in (('1',wf1),('3',wf2)):
-			wcls = Wallet.ext_to_type(get_extension(wf))
+			wcls = get_wallet_cls(ext=get_extension(wf))
 			t.passphrase(wcls.desc,self.cfgs[cnum]['wpasswd'])
 		self.txsign_end(t)
 		return t
@@ -743,7 +746,7 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 		bwf = joinpath(self.tmpdir,self.bw_filename)
 		make_brainwallet_file(bwf)
 		seed_len = str(self.seed_len)
-		args = ['-d',self.tmpdir,'-p1',self.usr_rand_arg,'-l'+seed_len,'-ib']
+		args = ['-d',self.tmpdir,'-p1',self.usr_rand_arg,'-l'+seed_len,'-ibw']
 		t = self.spawn('mmgen-walletconv', args + [bwf])
 		t.license()
 		wcls = MMGenWallet
@@ -830,7 +833,7 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 		t = self.spawn('mmgen-txsign', add_args + ['-d',self.tmpdir,'-k',non_mm_file,txf,wf])
 		t.license()
 		t.view_tx('n')
-		wcls = Wallet.ext_to_type(get_extension(wf))
+		wcls = get_wallet_cls(ext=get_extension(wf))
 		t.passphrase(wcls.desc,self.cfgs['20']['wpasswd'])
 		if bad_vsize:
 			t.expect('Estimated transaction vsize')

+ 2 - 2
test/test_py_d/ts_ref.py

@@ -23,7 +23,7 @@ ts_ref.py: Reference file tests for the test.py test suite
 import os
 from mmgen.globalvars import g
 from mmgen.opts import opt
-from mmgen.wallet import MMGenMnemonic
+from mmgen.wallet import get_wallet_cls
 from ..include.common import *
 from .common import *
 
@@ -163,7 +163,7 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared):
 
 	def ref_words_to_subwallet_chk(self,ss_idx):
 		wf = dfl_words_file
-		ocls = MMGenMnemonic
+		ocls = get_wallet_cls('words')
 		args = ['-d',self.tr.trash_dir,'-o',ocls.fmt_codes[-1],wf,ss_idx]
 
 		t = self.spawn('mmgen-subwalletgen',args,extra_desc='(generate subwallet)')

+ 9 - 9
test/test_py_d/ts_ref_3seed.py

@@ -23,7 +23,7 @@ ts_ref_3seed.py: Saved and generated reference file tests for 128, 192 and
 
 from mmgen.globalvars import g
 from mmgen.opts import opt
-from mmgen.wallet import *
+from mmgen.wallet import get_wallet_cls
 from ..include.common import *
 from .common import *
 from .ts_base import *
@@ -89,22 +89,22 @@ class TestSuiteRef3Seed(TestSuiteBase,TestSuiteShared):
 		return self.walletchk(wf,pf=None,wcls=ss,sid=self.seed_id)
 
 	def ref_seed_chk(self):
-		return self.ref_ss_chk(ss=MMGenSeedFile)
+		return self.ref_ss_chk(ss=get_wallet_cls('seed'))
 
 	def ref_hex_chk(self):
-		return self.ref_ss_chk(ss=MMGenHexSeedFile)
+		return self.ref_ss_chk(ss=get_wallet_cls('mmhex'))
 
 	def ref_plainhex_chk(self):
-		return self.ref_ss_chk(ss=PlainHexSeedFile)
+		return self.ref_ss_chk(ss=get_wallet_cls('plainhex'))
 
 	def ref_dieroll_chk(self):
-		return self.ref_ss_chk(ss=DieRollSeedFile)
+		return self.ref_ss_chk(ss=get_wallet_cls('dieroll'))
 
 	def ref_mn_chk(self):
-		return self.ref_ss_chk(ss=MMGenMnemonic)
+		return self.ref_ss_chk(ss=get_wallet_cls('words'))
 
 	def ref_bip39_chk(self):
-		return self.ref_ss_chk(ss=BIP39Mnemonic)
+		return self.ref_ss_chk(ss=get_wallet_cls('bip39'))
 
 	def ref_hincog_chk(self,desc='hidden incognito data'):
 		source = TestSuiteWalletConv.sources[str(self.seed_len)]
@@ -141,7 +141,7 @@ class TestSuiteRef3Seed(TestSuiteBase,TestSuiteShared):
 		t = self.spawn('mmgen-walletconv', args + [self.usr_rand_arg])
 		t.license()
 		t.expect('Enter brainwallet: ', ref_wallet_brainpass+'\n')
-		ocls = MMGenWallet
+		ocls = get_wallet_cls('mmgen')
 		t.passphrase_new('new '+ocls.desc,self.wpasswd)
 		t.usr_rand(self.usr_rand_chars)
 		fn = os.path.split(t.written_to_file(capfirst(ocls.desc)))[-1]
@@ -160,7 +160,7 @@ class TestSuiteRef3Seed(TestSuiteBase,TestSuiteShared):
 		wf = self.get_file_with_ext('mmdat')
 		pf = joinpath(self.tmpdir,pwfile)
 		t = self.spawn('mmgen-walletconv',extra_args+['-d','test/trash','-o',ofmt,'-P'+pf,wf])
-		wcls = Wallet.fmt_code_to_type(ofmt)
+		wcls = get_wallet_cls(fmt_code=ofmt)
 		fn = os.path.split(t.written_to_file(capfirst(wcls.desc)))[-1]
 		idx = int(self.test_name[-1]) - 1
 		sid = self.chk_data['sids'][idx]

+ 4 - 3
test/test_py_d/ts_regtest.py

@@ -27,14 +27,15 @@ from mmgen.opts import opt
 from mmgen.util import die,gmsg
 from mmgen.protocol import init_proto
 from mmgen.addrlist import AddrList
-from mmgen.wallet import MMGenWallet
+from mmgen.wallet import Wallet,get_wallet_cls
 from ..include.common import *
 from .common import *
 
 pat_date = r'\b\d\d-\d\d-\d\d\b'
 pat_date_time = r'\b\d\d\d\d-\d\d-\d\d\s+\d\d:\d\d\b'
 
-dfl_wcls = MMGenWallet
+dfl_wcls = get_wallet_cls('mmgen')
+
 rt_pw = 'abc-α'
 rt_data = {
 	'tx_fee': {'btc':'0.0001','bch':'0.001','ltc':'0.01'},
@@ -338,7 +339,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 		return os.path.basename(get_file_with_ext(self._user_dir(user),'mmdat'))[:8]
 
 	def _get_user_subsid(self,user,subseed_idx):
-		fn = get_file_with_ext(self._user_dir(user),MMGenWallet.ext)
+		fn = get_file_with_ext(self._user_dir(user),dfl_wcls.ext)
 		silence()
 		w = Wallet( fn=fn, passwd_file=os.path.join(self.tmpdir,'wallet_password') )
 		end_silence()

+ 16 - 13
test/test_py_d/ts_seedsplit.py

@@ -22,7 +22,7 @@ ts_seedsplit.py: Seed split/join tests for the test.py test suite
 
 from mmgen.globalvars import g
 from mmgen.opts import opt
-from mmgen.wallet import Wallet,MMGenWallet,IncogWallet,IncogWalletHex,IncogWalletHidden,WalletEnc
+from mmgen.wallet import get_wallet_cls
 
 from .ts_base import *
 
@@ -30,7 +30,7 @@ ref_wf = 'test/ref/98831F3A.bip39'
 ref_sid = '98831F3A'
 wpasswd = 'abc'
 sh1_passwd = 'xyz'
-dfl_wcls = MMGenWallet
+dfl_wcls = get_wallet_cls('mmgen')
 
 class TestSuiteSeedSplit(TestSuiteBase):
 	'splitting and joining seeds'
@@ -111,11 +111,11 @@ class TestSuiteSeedSplit(TestSuiteBase):
 		else:
 			pat = f'master share #{master}'
 		t.expect(pat,regex=True)
-		ocls = Wallet.fmt_code_to_type(ofmt)
-		if issubclass(ocls,WalletEnc):
+		ocls = get_wallet_cls(fmt_code=ofmt)
+		if ocls.enc:
 			t.hash_preset('new '+ocls.desc,'1')
 			t.passphrase_new('new '+ocls.desc,sh1_passwd)
-			if ocls == IncogWalletHidden:
+			if ocls.type == 'incog_hidden':
 				t.hincog_create(1234)
 		t.written_to_file(capfirst(ocls.desc))
 		return t
@@ -134,23 +134,26 @@ class TestSuiteSeedSplit(TestSuiteBase):
 				+ shares)
 		if bad_invocation:
 			return t
-		icls = ( MMGenWallet if 'mmdat' in in_exts
-			else IncogWallet if 'mmincog' in in_exts
-			else IncogWalletHex if 'mmincox' in in_exts
-			else IncogWalletHidden if '-H' in add_args
+		icls = ( dfl_wcls if 'mmdat' in in_exts
+			else get_wallet_cls('incog') if 'mmincog' in in_exts
+			else get_wallet_cls('incog_hex') if 'mmincox' in in_exts
+			else get_wallet_cls('incog_hidden') if '-H' in add_args
 			else None )
-		if icls in (IncogWallet,IncogWalletHex,IncogWalletHidden):
+		if icls.type.startswith('incog'):
 			t.hash_preset(icls.desc,'1')
 		if icls:
 			t.passphrase(icls.desc,sh1_passwd)
 		if master:
 			fs = "master share #{}, split id.*'{}'.*, share count {}"
-			pat = fs.format(master,id_str or 'default',len(shares)+(icls==IncogWalletHidden))
+			pat = fs.format(
+				master,
+				id_str or 'default',
+				len(shares) + (icls.type=='incog_hidden') )
 			t.expect(pat,regex=True)
 		sid_cmp = strip_ansi_escapes(t.expect_getend('Joined Seed ID: '))
 		cmp_or_die(sid,sid_cmp)
-		ocls = Wallet.fmt_code_to_type(ofmt)
-		if ocls == MMGenWallet:
+		ocls = get_wallet_cls(fmt_code=ofmt)
+		if ocls.type == 'mmgen':
 			t.hash_preset('new '+ocls.desc,'1')
 			t.passphrase_new('new '+ocls.desc,wpasswd)
 		t.written_to_file(capfirst(ocls.desc))

+ 9 - 8
test/test_py_d/ts_shared.py

@@ -24,7 +24,8 @@ import os
 from mmgen.globalvars import g
 from mmgen.opts import opt
 from mmgen.util import ymsg
-from mmgen.wallet import Wallet,WalletEnc,Brainwallet,MMGenWallet,IncogWalletHidden
+from mmgen.wallet import get_wallet_cls
+
 from ..include.common import *
 from .common import *
 
@@ -167,8 +168,8 @@ class TestSuiteShared(object):
 		t = self.spawn('mmgen-txsign', opts, extra_desc)
 		t.license()
 		t.view_tx(view)
-		wcls = MMGenWallet if dfl_wallet else Wallet.ext_to_type(get_extension(wf))
-		if issubclass(wcls,WalletEnc) and wcls != Brainwallet:
+		wcls = get_wallet_cls( ext = 'mmdat' if dfl_wallet else get_extension(wf) )
+		if wcls.enc and wcls.type != 'brain':
 			t.passphrase(wcls.desc,self.wpasswd)
 		if save:
 			self.txsign_end(t,has_label=has_label)
@@ -185,15 +186,15 @@ class TestSuiteShared(object):
 
 	def walletchk(self,wf,pf,wcls=None,add_args=[],sid=None,extra_desc='',dfl_wallet=False):
 		hp = self.hash_preset if hasattr(self,'hash_preset') else '1'
-		wcls = wcls or Wallet.ext_to_type(get_extension(wf))
+		wcls = wcls or get_wallet_cls(ext=get_extension(wf))
 		t = self.spawn('mmgen-walletchk',
 				([] if dfl_wallet else ['-i',wcls.fmt_codes[0]])
 				+ add_args + ['-p',hp]
 				+ ([wf] if wf else []),
 				extra_desc=extra_desc)
-		if wcls != IncogWalletHidden:
+		if wcls.type != 'incog_hidden':
 			t.expect(f"Getting {wcls.desc} from file '")
-		if issubclass(wcls,WalletEnc) and wcls != Brainwallet:
+		if wcls.enc and wcls.type != 'brain':
 			t.passphrase(wcls.desc,self.wpasswd)
 			t.expect(['Passphrase is OK', 'Passphrase.* are correct'],regex=True)
 		chk = t.expect_getend(f'Valid {wcls.desc} for Seed ID ')[:8]
@@ -223,7 +224,7 @@ class TestSuiteShared(object):
 				[getattr(self,f'{cmd_pfx}_idx_list')],
 				extra_desc=f'({mmtype})' if mmtype in ('segwit','bech32') else '')
 		t.license()
-		wcls = MMGenWallet if dfl_wallet else Wallet.ext_to_type(get_extension(wf))
+		wcls = get_wallet_cls( ext = 'mmdat' if dfl_wallet else get_extension(wf) )
 		t.passphrase(wcls.desc,self.wpasswd)
 		t.expect('Passphrase is OK')
 		desc = ('address','password')[passgen]
@@ -245,7 +246,7 @@ class TestSuiteShared(object):
 				([],['--type='+str(mmtype)])[bool(mmtype)] + args,
 				extra_desc=f'({mmtype})' if mmtype in ('segwit','bech32') else '')
 		t.license()
-		wcls = Wallet.ext_to_type(get_extension(wf))
+		wcls = get_wallet_cls(ext=get_extension(wf))
 		t.passphrase(wcls.desc,self.wpasswd)
 		chk = t.expect_getend(r'Checksum for key-address data .*?: ',regex=True)
 		if check_ref:

+ 12 - 12
test/test_py_d/ts_wallet.py

@@ -22,7 +22,7 @@ ts_wallet.py: Wallet conversion tests for the test.py test suite
 
 import os
 from mmgen.opts import opt
-from mmgen.wallet import *
+from mmgen.wallet import get_wallet_cls
 from .common import *
 from .ts_base import *
 from .ts_shared import *
@@ -107,7 +107,7 @@ class TestSuiteWalletConv(TestSuiteBase,TestSuiteShared):
 
 	def ref_brain_conv(self):
 		uopts = ['-i','bw','-p','1','-l',str(self.seed_len)]
-		return self.walletconv_in(None,uopts,oo=True,icls=Brainwallet)
+		return self.walletconv_in(None,uopts,oo=True,icls=get_wallet_cls('brain'))
 
 	def ref_incog_conv(self,wfk='ic_wallet',in_fmt='i'):
 		uopts = ['-i',in_fmt,'-p','1','-l',str(self.seed_len)]
@@ -125,7 +125,7 @@ class TestSuiteWalletConv(TestSuiteBase,TestSuiteShared):
 			None,
 			uopts + hi_opt,
 			oo = True,
-			icls = IncogWalletHidden )
+			icls = get_wallet_cls('incog_hidden') )
 
 	def ref_hincog_conv_old(self):
 		return self.ref_hincog_conv(wfk='hic_wallet_old',add_uopts=['-O'])
@@ -173,16 +173,16 @@ class TestSuiteWalletConv(TestSuiteBase,TestSuiteShared):
 
 	# wallet conversion tests
 	def walletconv_in(self,infile,uopts=[],oo=False,icls=None):
-		ocls = MMGenMnemonic
+		ocls = get_wallet_cls('words')
 		opts = ['-d',self.tmpdir,'-o',ocls.fmt_codes[0],self.usr_rand_arg]
 		if_arg = [infile] if infile else []
 		d = '(convert)'
 		t = self.spawn('mmgen-walletconv',opts+uopts+if_arg,extra_desc=d)
 		t.license()
-		icls = icls or Wallet.ext_to_type(get_extension(infile))
-		if icls == Brainwallet:
+		icls = icls or get_wallet_cls(ext=get_extension(infile))
+		if icls.type == 'brain':
 			t.expect('Enter brainwallet: ',ref_wallet_brainpass+'\n')
-		if issubclass(icls,WalletEnc) and icls != Brainwallet:
+		if icls.enc and icls.type != 'brain':
 			t.passphrase(icls.desc,self.wpasswd)
 			if self.test_name[:19] == 'ref_hincog_conv_old':
 				t.expect('Is the Seed ID correct? (Y/n): ','\n')
@@ -198,27 +198,27 @@ class TestSuiteWalletConv(TestSuiteBase,TestSuiteShared):
 								sid        = self.seed_id )
 
 	def walletconv_out(self,out_fmt='w',uopts=[],uopts_chk=[]):
-		wcls = Wallet.fmt_code_to_type(out_fmt)
+		wcls = get_wallet_cls(fmt_code=out_fmt)
 		opts = ['-d',self.tmpdir,'-p1','-o',out_fmt] + uopts
 		infile = joinpath(ref_dir,self.seed_id+'.mmwords')
 		t = self.spawn('mmgen-walletconv',[self.usr_rand_arg]+opts+[infile],extra_desc='(convert)')
 
 		add_args = [f'-l{self.seed_len}']
 		t.license()
-		if issubclass(wcls,WalletEnc) and wcls != Brainwallet:
+		if wcls.enc and wcls.type != 'brain':
 			t.passphrase_new('new '+wcls.desc,self.wpasswd)
 			t.usr_rand(self.usr_rand_chars)
-		if wcls in (IncogWallet,IncogWalletHex,IncogWalletHidden):
+		if wcls.type.startswith('incog'):
 			for i in (1,2,3):
 				t.expect('Encrypting random data generated by your operating system with key')
-		if wcls == IncogWalletHidden:
+		if wcls.type == 'incog_hidden':
 			t.hincog_create(hincog_bytes)
 		if out_fmt == 'w':
 			t.label()
 		wf = t.written_to_file(capfirst(wcls.desc),oo=True)
 		pf = None
 
-		if wcls == IncogWalletHidden:
+		if wcls.type == 'incog_hidden':
 			add_args += uopts_chk
 			wf = None
 		msg('' if opt.profile else ' OK')