Browse Source

XMR (Monero) key/address generation support and wallet generating utility

- Monero key-address files include spendkey, viewkey and wallet password
- Wallet password is first 16 bytes of SHA256x2(spendkey)
- Generate Monero wallets either by hand with:
    1) `monero-wallet-cli --generate-from-spend-key walletfile`, using the keyaddrfile data,
  or automatically, using:
    2) `mmgen-tool keyaddrlist2monerowallet keyaddrfile`
  The utility will generate a wallet for each key/address pair in the keyaddrfile and encrypt
  it using the password.  The password is supplied via stdin.

- Other feature: 32-byte hexadecimal password generation with `mmgen-passgen --hex`
MMGen 7 years ago
parent
commit
df0385160b

+ 3 - 2
MANIFEST.in

@@ -3,10 +3,11 @@ include doc/wiki/using-mmgen/*
 include test/*.py
 include test/ref/*
 include test/ref/litecoin/*
-include test/ref/zcash/*
-include test/ref/dash/*
 include test/ref/ethereum/*
 include test/ref/ethereum_classic/*
+include test/ref/dash/*
+include test/ref/zcash/*
+include test/ref/monero/*
 
 include scripts/bitcoind-walletunlock.py
 include scripts/compute-file-chksum.py

+ 104 - 37
mmgen/addr.py

@@ -40,15 +40,16 @@ class AddrGenerator(MMGenObject):
 			gen_method = addr_type.gen_method
 		else:
 			raise TypeError,'{}: incorrect argument type for {}()'.format(type(addr_type),cls.__name__)
-		d = {
-			'p2pkh':  AddrGeneratorP2PKH,
-			'segwit': AddrGeneratorSegwit,
+		gen_methods = {
+			'p2pkh':    AddrGeneratorP2PKH,
+			'segwit':   AddrGeneratorSegwit,
 			'ethereum': AddrGeneratorEthereum,
-			'zcash_z': AddrGeneratorZcashZ
+			'zcash_z':  AddrGeneratorZcashZ,
+			'monero':   AddrGeneratorMonero
 		}
-		assert gen_method in d
-		me = super(cls,cls).__new__(d[gen_method])
-		me.desc = d
+		assert gen_method in gen_methods
+		me = super(cls,cls).__new__(gen_methods[gen_method])
+		me.desc = gen_methods
 		return me
 
 class AddrGeneratorP2PKH(AddrGenerator):
@@ -113,6 +114,57 @@ class AddrGeneratorZcashZ(AddrGenerator):
 	def to_segwit_redeem_script(self,pubhex):
 		raise NotImplementedError,'Zcash z-addresses incompatible with Segwit'
 
+class AddrGeneratorMonero(AddrGenerator):
+
+	def b58enc(self,addr_str):
+		enc,l = baseconv.fromhex,len(addr_str)
+		a = ''.join([enc(addr_str[i*8:i*8+8].encode('hex'),'b58',pad=11,tostr=True) for i in range(l/8)])
+		b = enc(addr_str[l-l%8:].encode('hex'),'b58',pad=7,tostr=True)
+		return a + b
+
+	def to_addr(self,sk_hex): # sk_hex instead of pubhex
+
+		# ed25519ll, a low-level ctypes wrapper for Ed25519 digital signatures by
+		# Daniel Holth <dholth@fastmail.fm> - http://bitbucket.org/dholth/ed25519ll/
+		try:
+			from ed25519ll.djbec import scalarmult,edwards,encodepoint,B
+		except:
+			from mmgen.ed25519 import scalarmult,edwards,encodepoint,B
+
+		# Source and license for scalarmultbase function:
+		#   https://github.com/bigreddmachine/MoneroPy/blob/master/moneropy/crypto/ed25519.py
+		# Copyright (c) 2014-2016, The Monero Project
+		# All rights reserved.
+		def scalarmultbase(e):
+			if e == 0: return [0, 1]
+			Q = scalarmult(B, e//2)
+			Q = edwards(Q, Q)
+			if e & 1: Q = edwards(Q, B)
+			return Q
+
+		def hex2int_le(hexstr):
+			return int(hexstr.decode('hex')[::-1].encode('hex'),16)
+
+		vk_hex = self.to_viewkey(sk_hex)
+		pk_str  = encodepoint(scalarmultbase(hex2int_le(sk_hex)))
+		pvk_str = encodepoint(scalarmultbase(hex2int_le(vk_hex)))
+		addr_p1 = g.proto.addr_ver_num['monero'][0].decode('hex') + pk_str + pvk_str
+
+		import sha3
+		return CoinAddr(self.b58enc(addr_p1 + sha3.keccak_256(addr_p1).digest()[:4]))
+
+	def to_wallet_passwd(self,sk_hex):
+		from mmgen.protocol import hash256
+		return WalletPassword(hash256(sk_hex)[:32])
+
+	def to_viewkey(self,sk_hex):
+		assert len(sk_hex) == 64,'{}: incorrect privkey length'.format(len(sk_hex))
+		import sha3
+		return MoneroViewKey(g.proto.preprocess_key(sha3.keccak_256(sk_hex.decode('hex')).hexdigest(),None))
+
+	def to_segwit_redeem_script(self,sk_hex):
+		raise NotImplementedError,'Monero addresses incompatible with Segwit'
+
 class KeyGenerator(MMGenObject):
 
 	def __new__(cls,addr_type,generator=None,silent=False):
@@ -130,7 +182,7 @@ class KeyGenerator(MMGenObject):
 			else:
 				msg('Using (slow) native Python ECDSA library for address generation')
 				return super(cls,cls).__new__(KeyGeneratorPython)
-		elif pubkey_type == 'zcash_z':
+		elif pubkey_type in ('zcash_z','monero'):
 			g.proto.addr_width = 95
 			me = super(cls,cls).__new__(KeyGeneratorDummy)
 			me.desc = 'mmgen-'+pubkey_type
@@ -198,10 +250,11 @@ class KeyGeneratorDummy(KeyGenerator):
 
 class AddrListEntry(MMGenListItem):
 	addr    = MMGenListItemAttr('addr','CoinAddr')
-	viewkey = MMGenListItemAttr('viewkey','ZcashViewKey')
 	idx     = MMGenListItemAttr('idx','AddrIdx') # not present in flat addrlists
 	label   = MMGenListItemAttr('label','TwComment',reassign_ok=True)
 	sec     = MMGenListItemAttr('sec',PrivKey,typeconv=False)
+	viewkey = MMGenListItemAttr('viewkey','ViewKey')
+	wallet_passwd  = MMGenListItemAttr('wallet_passwd','WalletPassword')
 
 class PasswordListEntry(MMGenListItem):
 	passwd = MMGenImmutableAttr('passwd',unicode,typeconv=False) # TODO: create Password type
@@ -214,7 +267,11 @@ class AddrListChksum(str,Hilite):
 	trunc_ok = False
 
 	def __new__(cls,addrlist):
-		lines = [' '.join(addrlist.chksum_rec_f(e)) for e in addrlist.data]
+		ea = addrlist.al_id.mmtype.extra_attrs # add viewkey and passwd to the mix, if present
+		lines = [' '.join(
+					addrlist.chksum_rec_f(e) +
+					tuple(getattr(e,a) for a in ea if getattr(e,a))
+				) for e in addrlist.data]
 		return str.__new__(cls,make_chksum_N(' '.join(lines), nchars=16, sep=True))
 
 class AddrListIDStr(unicode,Hilite):
@@ -341,7 +398,9 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 
 		compressed = self.al_id.mmtype.compressed
 		pubkey_type = self.al_id.mmtype.pubkey_type
-		has_viewkey = self.al_id.mmtype.has_viewkey
+
+		gen_wallet_passwd = type(self) == KeyAddrList and 'wallet_passwd' in self.al_id.mmtype.extra_attrs
+		gen_viewkey       = type(self) == KeyAddrList and 'viewkey' in self.al_id.mmtype.extra_attrs
 
 		if self.gen_addrs:
 			kg = KeyGenerator(self.al_id.mmtype)
@@ -369,8 +428,10 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 			if self.gen_addrs:
 				ph = kg.to_pubhex(e.sec)
 				e.addr = ag.to_addr(ph)
-				if has_viewkey:
+				if gen_viewkey:
 					e.viewkey = ag.to_viewkey(ph)
+				if gen_wallet_passwd:
+					e.wallet_passwd = ag.to_wallet_passwd(ph)
 
 			if type(self) == PasswordList:
 				e.passwd = unicode(self.make_passwd(e.sec)) # TODO - own type
@@ -502,16 +563,17 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 		for e in self.data:
 			c = ' '+e.label if enable_comments and e.label else ''
 			if type(self) == KeyList:
-				out.append(fs.format(e.idx,'wif: {}'.format(e.sec.wif),c))
+				out.append(fs.format(e.idx,'{} {}'.format(self.al_id.mmtype.wif_label,e.sec.wif),c))
 			elif type(self) == PasswordList:
 				out.append(fs.format(e.idx,e.passwd,c))
 			else: # First line with idx
 				out.append(fs.format(e.idx,e.addr,c))
 				if self.has_keys:
-					if self.al_id.mmtype.has_viewkey:
-						out.append(fs.format('','view: '+e.viewkey,c))
-					if opt.b16: out.append(fs.format('', 'hex: '+e.sec,c))
-					out.append(fs.format('','wif: '+e.sec.wif,c))
+					if opt.b16: out.append(fs.format('', 'orig_hex: '+e.sec.orig_hex,c))
+					out.append(fs.format('','{} {}'.format(self.al_id.mmtype.wif_label,e.sec.wif),c))
+					for k in ('viewkey','wallet_passwd'):
+						v = getattr(e,k)
+						if v: out.append(fs.format('','{}: {}'.format(k,v),c))
 
 		out.append('}')
 		self.fmt_data = '\n'.join([l.rstrip() for l in out]) + '\n'
@@ -521,24 +583,30 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 		ret = AddrListList()
 		le = self.entry_type
 
+		def get_line():
+			ret = lines.pop(0).split(None,2)
+			if ret[0] == 'orig_hex:': # hacky
+				return lines.pop(0).split(None,2)
+			return ret
+
 		while lines:
-			l = lines.pop(0)
-			d = l.split(None,2)
+			d = get_line()
 
-			assert is_mmgen_idx(d[0]),"'%s': invalid address num. in line: '%s'" % (d[0],l)
+			assert is_mmgen_idx(d[0]),"'%s': invalid address num. in line: '%s'" % (d[0],' '.join(d))
 			assert self.check_format(d[1]),"'{}': invalid {}".format(d[1],self.data_desc)
 
 			if len(d) != 3: d.append('')
 			a = le(**{'idx':int(d[0]),self.main_attr:d[1],'label':d[2]})
 
-			if self.has_keys:
-				if self.al_id.mmtype.has_viewkey:
-					d = lines.pop(0).split(None,2)
-					assert d[0] == 'view:',"Invalid line in file: '{}'".format(' '.join(d))
-					a.viewkey = ZcashViewKey(d[1])
-				d = lines.pop(0).split(None,2)
-				assert d[0] == 'wif:',"Invalid line in file: '{}'".format(' '.join(d))
+			if self.has_keys: # order: wif,(orig_hex),viewkey,wallet_passwd
+				d = get_line()
+				assert d[0] == self.al_id.mmtype.wif_label,"Invalid line in file: '{}'".format(' '.join(d))
 				a.sec = PrivKey(wif=d[1])
+				for k,dtype in (('viewkey',ViewKey),('wallet_passwd',WalletPassword)):
+					if k in self.al_id.mmtype.extra_attrs:
+						d = get_line()
+						assert d[0] == k+':',"Invalid line in file: '{}'".format(' '.join(d))
+						setattr(a,k,dtype(d[1]))
 
 			ret.append(a)
 
@@ -571,12 +639,7 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 			if not al_mmtype:
 				mmtype = MMGenAddrType('E' if al_coin in ('ETH','ETC') else 'L',on_fail='raise')
 			else:
-				try:
-					mmtype = MMGenAddrType(al_mmtype,on_fail='raise')
-				except:
-					raise ValueError,(
-						u"'{}': invalid address type in address file. Must be one of: {}".format(
-						mmtype.upper(),' '.join([i['name'].upper() for i in MMGenAddrType.mmtypes.values()])))
+				mmtype = MMGenAddrType(al_mmtype,on_fail='raise')
 
 			from mmgen.protocol import CoinProtocol
 			base_coin = CoinProtocol(al_coin or 'BTC',testnet=False).base_coin
@@ -685,7 +748,8 @@ Record this checksum: it will be used to verify the password file in the future
 	pw_fmt      = None
 	pw_info     = {
 		'b58': { 'min_len': 8 , 'max_len': 36 ,'dfl_len': 20, 'desc': 'base-58 password' },
-		'b32': { 'min_len': 10 ,'max_len': 42 ,'dfl_len': 24, 'desc': 'base-32 password' }
+		'b32': { 'min_len': 10 ,'max_len': 42 ,'dfl_len': 24, 'desc': 'base-32 password' },
+		'hex': { 'min_len': 64 ,'max_len': 64 ,'dfl_len': 64, 'desc': 'raw hex password' }
 		}
 	chksum_rec_f = lambda foo,e: (str(e.idx), e.passwd)
 
@@ -751,11 +815,14 @@ Record this checksum: it will be used to verify the password file in the future
 
 	def make_passwd(self,hex_sec):
 		assert self.pw_fmt in self.pw_info
-		# we take least significant part
-		return ''.join(baseconv.fromhex(hex_sec,self.pw_fmt,pad=self.pw_len))[-self.pw_len:]
+		if self.pw_fmt == 'hex':
+			return hex_sec
+		else:
+			# we take least significant part
+			return baseconv.fromhex(hex_sec,self.pw_fmt,pad=self.pw_len,tostr=True)[-self.pw_len:]
 
 	def check_format(self,pw):
-		if not (is_b58_str,is_b32_str)[self.pw_fmt=='b32'](pw):
+		if not {'b58':is_b58_str,'b32':is_b32_str,'hex':is_hex_str}[self.pw_fmt](pw):
 			msg('Password is not a valid {} string'.format(self.pw_fmt))
 			return False
 		if len(pw) != self.pw_len:

+ 57 - 0
mmgen/ed25519.py

@@ -0,0 +1,57 @@
+# The reference Ed25519 software is in the public domain.
+#     Source: https://ed25519.cr.yp.to/python/ed25519.py
+#     Date accessed: 2 Nov. 2016
+
+import hashlib
+
+b = 256
+q = 2**255 - 19
+l = 2**252 + 27742317777372353535851937790883648493
+
+def H(m):
+	return hashlib.sha512(m).digest()
+
+def expmod(b, e, m):
+	if e == 0: return 1
+	t = expmod(b, e//2, m)**2 % m
+	if e & 1: t = (t*b) % m
+	return t
+
+def inv(x):
+	return expmod(x, q-2, q)
+
+d = -121665 * inv(121666)
+I = expmod(2, (q-1)//4, q)
+
+def xrecover(y):
+	xx = (y*y-1) * inv(d*y*y+1)
+	x = expmod(xx, (q+3)//8, q)
+	if (x*x - xx) % q != 0: x = (x*I) % q
+	if x % 2 != 0: x = q-x
+	return x
+
+By = 4 * inv(5)
+Bx = xrecover(By)
+B = [Bx%q, By%q]
+
+def edwards(P, Q):
+	x1 = P[0]
+	y1 = P[1]
+	x2 = Q[0]
+	y2 = Q[1]
+	x3 = (x1*y2+x2*y1) * inv(1+d*x1*x2*y1*y2)
+	y3 = (y1*y2+x1*x2) * inv(1-d*x1*x2*y1*y2)
+	return [x3%q, y3%q]
+
+def scalarmult(P, e):
+	if e == 0: return [0, 1]
+	Q = scalarmult(P, e//2)
+	Q = edwards(Q, Q)
+	if e & 1: Q = edwards(Q, P)
+	return Q
+
+def encodepoint(P):
+	x = P[0]
+	y = P[1]
+	bits = [(y >> i) & 1 for i in range(b-1)] + [x & 1]
+	return b''.join([chr(sum([bits[i * 8 + j] << j for j in range(8)])) for i in range(b//8)])

+ 5 - 2
mmgen/globalvars.py

@@ -44,7 +44,7 @@ class g(object):
 	proj_name = 'MMGen'
 	proj_url  = 'https://github.com/mmgen/mmgen'
 	prog_name = os.path.basename(sys.argv[0])
-	author    = 'Philemon'
+	author    = 'The MMGen Project'
 	email     = '<mmgen@tuta.io>'
 	Cdates    = '2013-2017'
 	keywords  = 'Bitcoin, BTC, cryptocurrency, wallet, cold storage, offline, online, spending, open-source, command-line, Python, Linux, Bitcoin Core, bitcoind, hd, deterministic, hierarchical, secure, anonymous, Electrum, seed, mnemonic, brainwallet, Scrypt, utility, script, scriptable, blockchain, raw, transaction, permissionless, console, terminal, curses, ansi, color, tmux, remote, client, daemon, RPC, json, entropy, xterm, rxvt, PowerShell, MSYS, MinGW, mswin, Armbian, Raspbian, Raspberry Pi, Orange Pi, BCash, BCH, Litecoin, LTC, altcoin, ZEC, Zcash, DASH, ETH, Ethereum, Classic, SHA256Compress'
@@ -73,6 +73,7 @@ class g(object):
 	force_256_color      = False
 	testnet              = False
 	regtest              = False
+	accept_defaults      = False
 	chain                = None # set by first call to rpc_init()
 	chains               = 'mainnet','testnet','regtest'
 	daemon_version       = None # set by first call to rpc_init()
@@ -108,7 +109,8 @@ class g(object):
 	# User opt sets global var:
 	common_opts = (
 		'color','no_license','rpc_host','rpc_port','testnet','rpc_user','rpc_password',
-		'daemon_data_dir','force_256_color','regtest','coin','bob','alice'
+		'daemon_data_dir','force_256_color','regtest','coin','bob','alice',
+		'accept_defaults'
 	)
 	required_opts = (
 		'quiet','verbose','debug','outdir','echo_passphrase','passwd_file','stdout',
@@ -116,6 +118,7 @@ class g(object):
 		'brain_params','b16','usr_randchars','coin','bob','alice','key_generator'
 	)
 	incompatible_opts = (
+		('base32','hex'), # mmgen-passgen
 		('bob','alice'),
 		('quiet','verbose'),
 		('label','keep_label'),

+ 1 - 1
mmgen/main.py

@@ -57,5 +57,5 @@ def launch(what):
 			else:
 				try:    m = u'{}\n'.format(e[0])
 				except: m = u'{!r}\n'.format(e[0])
-				sys.stderr.write(m)
+				sys.stderr.write(u'ERROR: ' + m)
 				sys.exit(2)

+ 8 - 6
mmgen/main_passgen.py

@@ -29,7 +29,8 @@ from mmgen.obj import MMGenPWIDString
 
 dfl_len = {
 	'b58': PasswordList.pw_info['b58']['dfl_len'],
-	'b32': PasswordList.pw_info['b32']['dfl_len']
+	'b32': PasswordList.pw_info['b32']['dfl_len'],
+	'hex': PasswordList.pw_info['hex']['dfl_len']
 }
 
 opts_data = lambda: {
@@ -41,6 +42,7 @@ opts_data = lambda: {
 -h, --help            Print this help message
 --, --longhelp        Print help message for long options (common options)
 -b, --base32          Generate passwords in Base32 format instead of Base58
+-x, --hex             Generate passwords in raw hex format instead of Base58
 -d, --outdir=      d  Output files to directory 'd' instead of working dir
 -e, --echo-passphrase Echo passphrase or mnemonic to screen upon entry
 -i, --in-fmt=      f  Input is from wallet format 'f' (see FMT CODES below)
@@ -48,9 +50,9 @@ opts_data = lambda: {
                       'f' at offset 'o' (comma-separated)
 -O, --old-incog-fmt   Specify old-format incognito input
 -L, --passwd-len=  l  Specify length of generated passwords
-                      (default: {d58} chars [base58], {d32} chars [base32]).
-                      An argument of 'h' will generate passwords of half
-                      the default length.
+                      (default: {d58} chars [base58], {d32} chars [base32],
+                      {dhex} chars [hex]).  An argument of 'h' will generate
+                      passwords of half the default length.
 -l, --seed-len=    l  Specify wallet seed length of 'l' bits.  This option
                       is required only for brainwallet and incognito inputs
                       with non-standard (< {g.seed_len}-bit) seed lengths
@@ -65,7 +67,7 @@ opts_data = lambda: {
 -v, --verbose         Produce more verbose output
 """.format(
 	seed_lens=', '.join([str(i) for i in g.seed_lens]),
-	g=g,pnm=g.proj_name,d58=dfl_len['b58'],d32=dfl_len['b32'],
+	g=g,pnm=g.proj_name,d58=dfl_len['b58'],d32=dfl_len['b32'],dhex=dfl_len['hex'],
 	kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)])
 ),
 	'notes': """
@@ -124,7 +126,7 @@ pw_id_str = cmd_args.pop()
 
 sf = get_seed_file(cmd_args,1)
 
-pw_fmt = ('b58','b32')[bool(opt.base32)]
+pw_fmt = ('b58','b32','hex')[bool(opt.base32)+2*bool(opt.hex)]
 
 pw_len = (opt.passwd_len,dfl_len[pw_fmt]/2)[opt.passwd_len in ('h','H')]
 

+ 38 - 15
mmgen/obj.py

@@ -20,7 +20,7 @@
 obj.py: MMGen native classes
 """
 
-import sys
+import sys,os
 from decimal import *
 from mmgen.color import *
 from string import hexdigits,ascii_letters,digits
@@ -32,7 +32,7 @@ def is_coin_addr(s):     return CoinAddr(s,on_fail='silent')
 def is_addrlist_id(s):   return AddrListID(s,on_fail='silent')
 def is_tw_label(s):      return TwLabel(s,on_fail='silent')
 def is_wif(s):           return WifKey(s,on_fail='silent')
-def is_viewkey(s):       return ZcashViewKey(s,on_fail='silent')
+def is_viewkey(s):       return ViewKey(s,on_fail='silent')
 
 class MMGenObject(object):
 
@@ -111,9 +111,11 @@ class InitErrors(object):
 
 	@staticmethod
 	def init_fail(m,on_fail,silent=False):
-		if silent: m = ''
 		from mmgen.util import die,msg
-		if on_fail == 'die': die(1,m)
+		if silent: m = ''
+		if os.getenv('MMGEN_TRACEBACK'):
+			raise ValueError,m
+		elif on_fail == 'die': die(1,m)
 		elif on_fail == 'return':
 			if m: msg(m)
 			return None # TODO: change to False
@@ -404,6 +406,16 @@ class CoinAddr(str,Hilite,InitErrors,MMGenObject):
 		d = rpc_init().validateaddress(self)
 		return d['iswatchonly'] and 'account' in d
 
+class ViewKey(object):
+	def __new__(cls,s,on_fail='die'):
+		from mmgen.globalvars import g
+		if g.proto.name == 'zcash':
+			return ZcashViewKey.__new__(ZcashViewKey,s,on_fail)
+		elif g.proto.name == 'monero':
+			return MoneroViewKey.__new__(MoneroViewKey,s,on_fail)
+		else:
+			raise ValueError,'{}: protocol does not support view keys'.format(g.proto.name.capitalize())
+
 class ZcashViewKey(CoinAddr): hex_width = 128
 
 class SeedID(str,Hilite,InitErrors):
@@ -513,11 +525,11 @@ class HexStr(str,Hilite,InitErrors):
 			m = "{!r}: value cannot be converted to {} (value is {})"
 			return cls.init_fail(m.format(s,cls.__name__,e[0]),on_fail)
 
-class MMGenTxID(HexStr,Hilite,InitErrors):
-	color = 'red'
-	width = 6
+class HexStrWithWidth(HexStr):
+	color = 'nocolor'
 	trunc_ok = False
-	hexcase = 'upper'
+	hexcase = 'lower'
+	width = None
 	def __new__(cls,s,on_fail='die'):
 		cls.arg_chk(cls,on_fail)
 		try:
@@ -528,10 +540,10 @@ class MMGenTxID(HexStr,Hilite,InitErrors):
 			m = "{}\n{!r}: value cannot be converted to {}"
 			return cls.init_fail(m.format(e[0],s,cls.__name__),on_fail)
 
-class CoinTxID(MMGenTxID):
-	color = 'purple'
-	width = 64
-	hexcase = 'lower'
+class MMGenTxID(HexStrWithWidth):      color,width,hexcase = 'red',6,'upper'
+class MoneroViewKey(HexStrWithWidth):  color,width,hexcase = 'cyan',64,'lower'
+class WalletPassword(HexStrWithWidth): color,width,hexcase = 'blue',32,'lower'
+class CoinTxID(HexStrWithWidth):       color,width,hexcase = 'purple',64,'lower'
 
 class WifKey(str,Hilite,InitErrors):
 	width = 53
@@ -582,8 +594,9 @@ class PrivKey(str,Hilite,InitErrors,MMGenObject):
 				w2h = g.proto.wif2hex(wif) # raises exception on error
 				me = str.__new__(cls,w2h['hex'])
 				me.compressed = w2h['compressed']
-				me.pubkey_type   = w2h['pubkey_type']
-				me.wif        = str.__new__(WifKey,wif) # check has been done
+				me.pubkey_type = w2h['pubkey_type']
+				me.wif = str.__new__(WifKey,wif) # check has been done
+				me.orig_hex = None
 				return me
 			except Exception as e:
 				fs = "Value {!r} cannot be converted to {} WIF key ({})"
@@ -593,6 +606,7 @@ class PrivKey(str,Hilite,InitErrors,MMGenObject):
 			assert s and type(compressed) == bool and pubkey_type,'Incorrect args for PrivKey()'
 			assert len(s) == cls.width / 2,'Key length must be {}'.format(cls.width/2)
 			me = str.__new__(cls,g.proto.preprocess_key(s.encode('hex'),pubkey_type))
+			me.orig_hex = s.encode('hex') # save the non-preprocessed key
 			me.compressed = compressed
 			me.pubkey_type = pubkey_type
 			if pubkey_type != 'password': # skip WIF creation for passwds
@@ -705,6 +719,12 @@ class MMGenAddrType(str,Hilite,InitErrors,MMGenObject):
 				'gen_method':'zcash_z',
 				'addr_fmt':'zcash_z',
 				'desc':'Zcash z-address' },
+		'M': {  'name':'monero',
+				'pubkey_type':'monero',
+				'compressed':False,
+				'gen_method':'monero',
+				'addr_fmt':'monero',
+				'desc':'Monero address'}
 	}
 	def __new__(cls,s,on_fail='die',errmsg=None):
 		if type(s) == cls: return s
@@ -719,7 +739,10 @@ class MMGenAddrType(str,Hilite,InitErrors,MMGenObject):
 						setattr(me,k,v[k])
 					assert me in g.proto.mmtypes + ('P',), (
 						"'{}': invalid address type for {}".format(me.name,g.proto.__name__))
-					me.has_viewkey = me.name == 'zcash_z'
+					me.extra_attrs = []
+					if me.name in ('monero','zcash_z'): me.extra_attrs += ['viewkey']
+					if me.name == 'monero': me.extra_attrs += ['wallet_passwd']
+					me.wif_label = ('wif:','spendkey:')[me.name=='monero']
 					return me
 			raise ValueError,'not found'
 		except Exception as e:

+ 2 - 1
mmgen/opts.py

@@ -181,7 +181,7 @@ Are you sure you want to continue?
 """.strip().format(g.coin,tl[trust_level],pn=g.proj_name)
 	if os.getenv('MMGEN_TEST_SUITE'):
 		msg(m); return
-	if not keypress_confirm(m):
+	if not keypress_confirm(m,default_yes=True):
 		sys.exit(0)
 
 def init(opts_f,add_opts=[],opt_filter=None):
@@ -191,6 +191,7 @@ def init(opts_f,add_opts=[],opt_filter=None):
 
 	# most, but not all, of these set the corresponding global var
 	common_opts_data = """
+--, --accept-defaults     Accept defaults at all prompts
 --, --coin=c              Choose coin unit. Default: {cu_dfl}. Options: {cu_all}
 --, --color=0|1           Disable or enable color output
 --, --force-256-color     Force 256-color output when color is enabled

+ 61 - 10
mmgen/protocol.py

@@ -251,27 +251,32 @@ class LitecoinTestnetProtocol(LitecoinProtocol):
 class BitcoinProtocolAddrgen(BitcoinProtocol): mmcaps = ('key','addr')
 class BitcoinTestnetProtocolAddrgen(BitcoinTestnetProtocol): mmcaps = ('key','addr')
 
-class EthereumProtocol(BitcoinProtocolAddrgen):
-
-	addr_width = 40
-	mmtypes    = ('E',)
-	dfl_mmtype = 'E'
-	name = 'ethereum'
-	base_coin = 'ETH'
+class DummyWIF(object):
 
 	@classmethod
 	def hex2wif(cls,hexpriv,pubkey_type,compressed):
-		assert compressed == False,'Ethereum does not support compressed pubkeys!'
+		n = cls.name.capitalize()
+		assert pubkey_type == cls.pubkey_type,'{}: invalid pubkey_type for {}!'.format(pubkey_type,n)
+		assert compressed == False,'{} does not support compressed pubkeys!'.format(n)
 		return str(hexpriv)
 
 	@classmethod
 	def wif2hex(cls,wif):
-		return { 'hex':str(wif), 'pubkey_type':'std', 'compressed':False }
+		return { 'hex':str(wif), 'pubkey_type':cls.pubkey_type, 'compressed':False }
+
+class EthereumProtocol(DummyWIF,BitcoinProtocolAddrgen):
+
+	addr_width = 40
+	mmtypes    = ('E',)
+	dfl_mmtype = 'E'
+	name = 'ethereum'
+	base_coin = 'ETH'
+	pubkey_type = 'std' # required by DummyWIF
 
 	@classmethod
 	def verify_addr(cls,addr,hex_width,return_dict=False):
 		from mmgen.util import is_hex_str_lc
-		if is_hex_str_lc(addr) and len(addr) == 40:
+		if is_hex_str_lc(addr) and len(addr) == cls.addr_width:
 			return { 'hex': addr, 'format': 'ethereum', 'width': cls.addr_width } if return_dict else True
 		if g.debug: Msg("Invalid address '{}'".format(addr))
 		return False
@@ -324,6 +329,50 @@ class ZcashTestnetProtocol(ZcashProtocol):
 		'zcash_z': ('16b6','??'),
 		'viewkey': ('0b2a','??') }
 
+# https://github.com/monero-project/monero/blob/master/src/cryptonote_config.h
+class MoneroProtocol(DummyWIF,BitcoinProtocolAddrgen):
+	name         = 'monero'
+	base_coin    = 'XMR'
+	addr_ver_num = { 'monero': ('12','4'), 'monero_sub': ('2a','8') } # 18,42
+	wif_ver_num  = {}
+	mmtypes      = ('M',)
+	dfl_mmtype   = 'M'
+	addr_width   = 95
+	pubkey_type = 'monero' # required by DummyWIF
+
+	@classmethod
+	def preprocess_key(cls,hexpriv,pubkey_type): # reduce key
+		try:
+			from ed25519ll.djbec import l
+		except:
+			from mmgen.ed25519 import l
+		n = int(hexpriv.decode('hex')[::-1].encode('hex'),16) % l
+		return '{:064x}'.format(n).decode('hex')[::-1].encode('hex')
+
+	@classmethod
+	def verify_addr(cls,addr,hex_width,return_dict=False):
+
+		def b58dec(addr_str):
+			from mmgen.util import baseconv
+			dec,l = baseconv.tohex,len(addr_str)
+			a = ''.join([dec(addr_str[i*11:i*11+11],'b58',pad=16) for i in range(l/11)])
+			b = dec(addr_str[-(l%11):],'b58',pad=10)
+			return a + b
+
+		from mmgen.util import is_b58_str
+		assert is_b58_str(addr),'Not valid base-58 string'
+		assert len(addr) == cls.addr_width,'Incorrect width'
+
+		ret = b58dec(addr)
+		import sha3
+		chk = sha3.keccak_256(ret.decode('hex')[:-4]).hexdigest()[:8]
+		assert chk == ret[-8:],'Incorrect checksum'
+
+		return { 'hex': ret, 'format': 'monero', 'width': cls.addr_width } if return_dict else True
+
+class MoneroTestnetProtocol(MoneroProtocol):
+	addr_ver_num = { 'monero': ('35','4'), 'monero_sub': ('3f','8') } # 53,63
+
 class CoinProtocol(MMGenObject):
 	coins = {
 		#      mainnet testnet trustlevel (None == skip)
@@ -333,6 +382,7 @@ class CoinProtocol(MMGenObject):
 		'eth': (EthereumProtocol,EthereumTestnetProtocol,2),
 		'etc': (EthereumClassicProtocol,EthereumClassicTestnetProtocol,2),
 		'zec': (ZcashProtocol,ZcashTestnetProtocol,2),
+		'xmr': (MoneroProtocol,MoneroTestnetProtocol,2)
 	}
 	def __new__(cls,coin,testnet):
 		coin = coin.lower()
@@ -412,5 +462,6 @@ def make_init_genonly_altcoins_str(data):
 	return out
 
 def init_coin(coin):
+	coin = coin.upper()
 	g.coin = coin
 	g.proto = CoinProtocol(coin,g.testnet)

+ 101 - 10
mmgen/tool.py

@@ -93,7 +93,8 @@ cmd_data = OrderedDict([
 	('Encrypt',      ['<infile> [str]',"outfile [str='']","hash_preset [str='']"]),
 	('Decrypt',      ['<infile> [str]',"outfile [str='']","hash_preset [str='']"]),
 	('Bytespec',     ['<bytespec> [str]']),
-	('Regtest_setup',[]),
+
+	('Keyaddrlist2monerowallet',['<{} XMR key-address file> [str]'.format(pnm),'blockheight [int=(current height)]']),
 ])
 
 def usage(command):
@@ -315,9 +316,9 @@ def Mn_rand256(wordlist=dfl_wl_id): do_random_mn(32,wordlist)
 def Hex2mn(s,wordlist=dfl_wl_id): Msg(' '.join(baseconv.fromhex(s,wordlist)))
 def Mn2hex(s,wordlist=dfl_wl_id): Msg(baseconv.tohex(s.split(),wordlist))
 
-def Strtob58(s,pad=None): Msg(''.join(baseconv.fromhex(binascii.hexlify(s),'b58',pad)))
-def Hextob58(s,pad=None): Msg(''.join(baseconv.fromhex(s,'b58',pad)))
-def Hextob32(s,pad=None): Msg(''.join(baseconv.fromhex(s,'b32',pad)))
+def Strtob58(s,pad=None): Msg(baseconv.fromhex(binascii.hexlify(s),'b58',pad,tostr=True))
+def Hextob58(s,pad=None): Msg(baseconv.fromhex(s,'b58',pad,tostr=True))
+def Hextob32(s,pad=None): Msg(baseconv.fromhex(s,'b32',pad,tostr=True))
 def B58tostr(s):          Msg(binascii.unhexlify(baseconv.tohex(s,'b58')))
 def B58tohex(s,pad=None): Msg(baseconv.tohex(s,'b58',pad))
 def B32tohex(s,pad=None): Msg(baseconv.tohex(s.upper(),'b32',pad))
@@ -474,12 +475,102 @@ def Rand2file(outfile,nbytes,threads=4,silent=False):
 
 def Bytespec(s): Msg(str(parse_nbytes(s)))
 
-def Regtest_setup():
-	print 'ok'
-	return
-	import subprocess as sp
-	sp.check_output()
-	pass
+def Keyaddrlist2monerowallet(infile,blockheight=None):
+	import pexpect
+
+	if blockheight != None and int(blockheight) < 0: blockheight = 0
+
+	def run_cmd(cmd):
+		import subprocess as sp
+		p = sp.Popen(cmd,stdin=sp.PIPE,stdout=sp.PIPE,stderr=sp.PIPE)
+		return p
+
+	def test_rpc():
+		p = run_cmd(['monero-wallet-cli','--version'])
+		if p.stdout.read()[:6] != 'Monero':
+			die(1,"Unable to run 'monero-wallet-cli'!")
+		p = run_cmd(['monerod','status'])
+		ret = p.stdout.read()
+		if ret[:7] != 'Height:':
+			die(1,'Unable to connect to monerod!')
+		return int(ret[8:].split('/')[0])
+
+	cur_height = test_rpc()
+
+	from mmgen.protocol import init_coin
+	init_coin('xmr')
+	from mmgen.addr import AddrList
+	al = KeyAddrList(infile)
+	sid = al.al_id.sid
+	os.environ['LANG'] = 'C'
+
+	def my_expect(p,m,s,regex=False):
+		if m: msg_r('  {}...'.format(m))
+		ret = (p.expect_exact,p.expect)[regex](s)
+		if not (ret == 0 or (type(s) == list and ret in (0,1))):
+			die(2,"Expect failed: '{}' (return value: {})".format(s,ret))
+		if m: msg('OK')
+		return ret
+
+	def my_sendline(p,m,s,usr_ret):
+		if m: msg_r('  {}...'.format(m))
+		ret = p.sendline(s)
+		if ret != usr_ret:
+			die(2,"Unable to send line '{}' (return value {})".format(s,ret))
+		if m: msg('OK')
+		vmsg("sendline: '{}' => {}".format(s,ret))
+
+	def create():
+		gmsg('\nCreating {} wallet{}'.format(dl,suf(dl)))
+		for n,d in enumerate(al.data):
+			fn = '{}{}-{}-MoneroWallet'.format(('',opt.outdir+'/')[bool(opt.outdir)],sid,d.idx)
+			gmsg("\nGenerating wallet {}/{} ({})".format(n+1,dl,fn))
+			try: os.stat(fn)
+			except: pass
+			else: die(1,"Wallet '{}' already exists!".format(fn))
+#			pmsg([d.sec,d.wallet_passwd,d.viewkey,d.addr,fn])
+			p = pexpect.spawn('monero-wallet-cli --generate-from-spend-key {}'.format(fn))
+			my_expect(p,'Awaiting initial prompt','Secret spend key: ')
+			my_sendline(p,'',d.sec,65)
+			my_expect(p,'','Enter new wallet password: ')
+			my_sendline(p,'Sending password',d.wallet_passwd,33)
+			my_expect(p,'','Confirm password: ')
+			my_sendline(p,'Sending password again',d.wallet_passwd,33)
+			my_expect(p,'','of your choice: ')
+			my_sendline(p,'','1',2)
+			my_expect(p,'monerod generating wallet','Generated new wallet: ')
+			my_expect(p,'','\n')
+			if d.addr not in p.before:
+				die(3,'Addresses do not match!\n  MMGen: {}\n Monero: {}'.format(d.addr,p.before))
+			my_expect(p,'','View key: ')
+			my_expect(p,'','\n')
+			if d.viewkey not in p.before:
+				die(3,'View keys do not match!\n  MMGen: {}\n Monero: {}'.format(d.viewkey,p.before))
+			my_expect(p,'','(YYYY-MM-DD): ')
+			h = str(blockheight or cur_height-1)
+			my_sendline(p,'',h,len(h)+1)
+			ret = my_expect(p,'',['Starting refresh','Still apply restore height?  (Y/Yes/N/No): '])
+			if ret == 1:
+				my_sendline(p,'','Y',2)
+				m = '  Warning: {}: blockheight argument is higher than current blockheight'
+				ymsg(m.format(blockheight))
+			elif blockheight != None:
+				p.logfile = sys.stderr
+			my_expect(p,'Syncing wallet','\[wallet.*$',regex=True)
+			p.logfile = None
+			my_sendline(p,'Exiting','exit',5)
+			p.read()
+
+	dl = len(al.data)
+	try:
+		create()
+		gmsg('\n{} wallet{} created'.format(dl,suf(dl)))
+	except KeyboardInterrupt:
+		rdie(1,'\nUser interrupt\n')
+	except EOFError:
+		rdie(2,'\nEnd of file\n')
+	except Exception as e:
+		rdie(1,'Program died: {!r}'.format(e))
 
 # ================ RPC commands ================== #
 

+ 2 - 1
mmgen/tx.py

@@ -736,7 +736,8 @@ class MMGenTX(MMGenObject):
 		if self.is_in_mempool():
 			if status:
 				d = g.rpch.gettransaction(self.coin_txid,on_fail='silent')
-				r = '{}replaceable'.format(('NOT ','')[d['bip125-replaceable']=='yes'])
+				brs = 'bip125-replaceable'
+				r = '{}replaceable'.format(('NOT ','')[brs in d and d[brs]=='yes'])
 				t = d['timereceived']
 				m = 'Sent {} ({} h/m/s ago)'
 				b = m.format(time.strftime('%c',time.gmtime(t)),secs_to_dhms(int(time.time()-t)))

+ 8 - 3
mmgen/util.py

@@ -269,7 +269,7 @@ class baseconv(object):
 	@classmethod
 	def b58encode(cls,s,pad=None):
 		pad = cls.get_pad(s,pad,'en',cls.b58pad_lens,[bytes])
-		return ''.join(cls.fromhex(hexlify(s),'b58',pad=pad))
+		return cls.fromhex(hexlify(s),'b58',pad=pad,tostr=True)
 
 	@classmethod
 	def b58decode(cls,s,pad=None):
@@ -326,7 +326,7 @@ class baseconv(object):
 		return ('','0')[len(ret) % 2] + ret
 
 	@classmethod
-	def fromhex(cls,hexnum,wl_id,pad=None):
+	def fromhex(cls,hexnum,wl_id,pad=None,tostr=False):
 
 		hexnum = hexnum.strip()
 		if not is_hex_str(hexnum):
@@ -338,7 +338,8 @@ class baseconv(object):
 		while num:
 			ret.append(num % base)
 			num /= base
-		return [wl[n] for n in [0] * ((pad or 0)-len(ret)) + ret[::-1]]
+		o = [wl[n] for n in [0] * ((pad or 0)-len(ret)) + ret[::-1]]
+		return ''.join(o) if tostr else o
 
 baseconv.check_wordlists()
 
@@ -715,6 +716,10 @@ def keypress_confirm(prompt,default_yes=False,verbose=False,no_nl=False):
 	p = '{} {}: '.format(prompt,q)
 	nl = ('\n','\r{}\r'.format(' '*len(p)))[no_nl]
 
+	if opt.accept_defaults:
+		msg(p)
+		return (False,True)[default_yes]
+
 	while True:
 		reply = get_char(p).strip('\n\r')
 		if not reply:

+ 1 - 0
setup.py

@@ -114,6 +114,7 @@ setup(
 			'mmgen.color',
 			'mmgen.common',
 			'mmgen.crypto',
+			'mmgen.ed25519',
 			'mmgen.filename',
 			'mmgen.globalvars',
 			'mmgen.license',

+ 19 - 0
test/ref/monero/98831F3A-XMR-M[1,31-33,500-501,1010-1011].addrs

@@ -0,0 +1,19 @@
+# MMGen address file
+#
+# This file is editable.
+# Everything following a hash symbol '#' is a comment and ignored by MMGen.
+# A text label of 32 characters or less may be added to the right of each
+# address, and it will be appended to the tracking wallet label upon import.
+# The label may contain any printable ASCII symbol.
+# Address data checksum for 98831F3A-XMR-M[1,31-33,500-501,1010-1011]: 4369 0253 AC2C 0E38
+# Record this value to a secure location.
+98831F3A XMR:MONERO {
+  1     41tmwZd2CdXEGtWqGY9fH9FVtQM8VxZASYPQ3VJQhFjtGWYzQFuidD21vJYTi2yy3tXRYXTNXBTaYVLav62rwUUpFFyicZU
+  31    42R44dGVXu6gspyuWKNieJHmxzpnLxpkVbncKrFs5XniBEifRVYxPpeKJNgGPQkikX8AqjWL1Ysnf5Yc8DALiD6939PJAsb
+  32    44mhAiVtVuD5yGbT15BG3M1WmFD5R5dZfbwTfmfVDfneRT9keAcUwgGRvtx32WifQiECKVD1zESYaWqwJf5yh5w71WM6BEE
+  33    47UrLDNHrbiF5Ea6jvpPxcRYfW9ZkPRevPJXfWUmtEApb8p5xvp49d5iWQifspQzDzcBSbroBbbMFFJ6TYNcxmrtFviJ84L
+  500   428YDXihJ8d8AMSnqtiHbfGt7fA6FsMUEJUCidSB7wN4RjGsMGzC5dBgaKQojevhaxKTZacSmF3xe7wubu9zyZ5b8S79pHG
+  501   46nYucqe3BCMoASekNxvVT3BMnKuHsr9A8CwPZ6eAwVSJ9v8am5c6zR8St1Wcg2PPdJ6oL6zDM8zVChF15dZhGcgNEwLg2T
+  1010  43tWyaYaMAt5ZaK5n3SVbSJJigpBnWrVXV3iBPJ5PX7UgvPRnzMw6dLXnrogRu1LzAe1aZrYJFHPBWVw1c7CBYMvLMVHf99
+  1011  478e1Nnt14zYhinwFCrr9bFjTc7uLNfudCcvGHsYrBnN1Q4xuGUdwubCEScTYzRzs4113ndcFBJSK1xfkPvUNYFB55i5kqV
+}

BIN
test/ref/monero/98831F3A-XMR-M[1,31-33,500-501,1010-1011].akeys.mmenc


BIN
test/ref/zcash/98831F3A-ZEC-Z[1,31-33,500-501,1010-1011].akeys.mmenc


+ 7 - 6
test/scrambletest.py

@@ -63,12 +63,13 @@ test_data = OrderedDict([
 ('ltc',           ('b11f16632e63ba92','ltc:legacy',     '-LTC','LTC',       'LMxB474SVfxeYdqxNrM1WZDZMnifteSMv1')),
 ('ltc_compressed',('7ccf465d466ee7d3','ltc:compressed', '-LTC-C','LTC:COMPRESSED', 'LdkebBKVXSs6NNoPJWGM8KciDnL8LhXXjb')),
 ('ltc_segwit',    ('9460f5ba15e82768','ltc:segwit',     '-LTC-S','LTC:SEGWIT',     'MQrY3vEbqKMBgegXrSaR93R2HoTDE5bKrY')),
-('eth',           ('213ed116869b19f2','eth',            '-ETH', 'ETH','e704b6cfd9f0edb2e6cfbd0c913438d37ede7b35')),
-('etc',           ('909def37096f5ab8','etc',            '-ETC', 'ETC','1a6acbef8c38f52f20d04ecded2992b04d8608d7')),
-('dash',          ('1319d347b021f952','dash:legacy',    '-DASH','DASH','XoK491fppGNZQUUS9uEFkT6L9u8xxVFJNJ')),
-('zec',           ('0bf9b5b20af7b5a0','zec:legacy',     '-ZEC', 'ZEC', 't1URz8BHxV38v3gsaN6oHQNKC16s35R9WkY')),
-('zec_zcash_z',   ('b15570d033df9b1a','zec:zcash_z',    '-ZEC-Z','ZEC:ZCASH_Z','zcLMMsnzfFYZWU4avRBnuc83yh4jTtJXbtP32uWrs3ickzu1krMU4ppZCQPTwwfE9hLnRuFDSYF8VFW13aT9eeQK8aov3Ge')),
-('emc',           ('7e1a29853d2db875','emc:legacy',      '-EMC', 'EMC','EU4L6x2b5QUb2gRQsBAAuB8TuPEwUxCNZU')),
+('eth',           ('213ed116869b19f2','eth',          '-ETH',  'ETH', 'e704b6cfd9f0edb2e6cfbd0c913438d37ede7b35')),
+('etc',           ('909def37096f5ab8','etc',          '-ETC',  'ETC', '1a6acbef8c38f52f20d04ecded2992b04d8608d7')),
+('dash',          ('1319d347b021f952','dash:legacy',  '-DASH', 'DASH','XoK491fppGNZQUUS9uEFkT6L9u8xxVFJNJ')),
+('emc',           ('7e1a29853d2db875','emc:legacy',   '-EMC',  'EMC', 'EU4L6x2b5QUb2gRQsBAAuB8TuPEwUxCNZU')),
+('zec',           ('0bf9b5b20af7b5a0','zec:legacy',   '-ZEC',  'ZEC', 't1URz8BHxV38v3gsaN6oHQNKC16s35R9WkY')),
+('zec_zcash_z',   ('b15570d033df9b1a','zec:zcash_z',  '-ZEC-Z','ZEC:ZCASH_Z','zcLMMsnzfFYZWU4avRBnuc83yh4jTtJXbtP32uWrs3ickzu1krMU4ppZCQPTwwfE9hLnRuFDSYF8VFW13aT9eeQK8aov3Ge')),
+('xmr',           ('c76af3b088da3364','xmr:monero',   '-XMR-M','XMR:MONERO','41tmwZd2CdXEGtWqGY9fH9FVtQM8VxZASYPQ3VJQhFjtGWYzQFuidD21vJYTi2yy3tXRYXTNXBTaYVLav62rwUUpFFyicZU')),
 ])
 
 def run_tests():

+ 25 - 5
test/test.py

@@ -327,6 +327,7 @@ cfgs = {
 		},
 		'passfile_chk':    'EB29 DC4F 924B 289F',
 		'passfile32_chk':  '37B6 C218 2ABC 7508',
+		'passfilehex_chk': '523A F547 0E69 8323',
 		'wpasswd':         'reference password',
 		'ref_wallet':      'FE3C6545-D782B529[128,1].mmdat',
 		'ic_wallet':       'FE3C6545-E29303EA-5E229E30[128,1].mmincog',
@@ -378,6 +379,7 @@ cfgs = {
 		},
 		'passfile_chk':    'ADEA 0083 094D 489A',
 		'passfile32_chk':  '2A28 C5C7 36EC 217A',
+		'passfilehex_chk': 'B11C AC6A 1464 608D',
 		'wpasswd':         'reference password',
 		'ref_wallet':      '1378FC64-6F0F9BB4[192,1].mmdat',
 		'ic_wallet':       '1378FC64-2907DE97-F980D21F[192,1].mmincog',
@@ -429,6 +431,7 @@ cfgs = {
 		},
 		'passfile_chk':    '2D6D 8FBA 422E 1315',
 		'passfile32_chk':  'F6C1 CDFB 97D9 FCAE',
+		'passfilehex_chk': 'BD4F A0AC 8628 4BE4',
 		'wpasswd':         'reference password',
 		'ref_wallet':      '98831F3A-{}[256,1].mmdat'.format(('27F2BF93','E2687906')[g.testnet]),
 		'ref_addrfile':    '98831F3A{}[1,31-33,500-501,1010-1011]{}.addrs',
@@ -449,11 +452,13 @@ cfgs = {
 		},
 		'ref_addrfile_chksum_zec': '903E 7225 DD86 6E01',
 		'ref_addrfile_chksum_zec_z': '9C7A 72DC 3D4A B3AF',
+		'ref_addrfile_chksum_xmr': '4369 0253 AC2C 0E38',
 		'ref_addrfile_chksum_dash':'FBC1 6B6A 0988 4403',
 		'ref_addrfile_chksum_eth': 'E554 076E 7AF6 66A3',
 		'ref_addrfile_chksum_etc': 'E97A D796 B495 E8BC',
 		'ref_keyaddrfile_chksum_zec': 'F05A 5A5C 0C8E 2617',
-		'ref_keyaddrfile_chksum_zec_z': '220F 5F23 CC9B EC1F',
+		'ref_keyaddrfile_chksum_zec_z': '4ADB 5AA4 4590 B60A',
+		'ref_keyaddrfile_chksum_xmr': 'E0D7 9612 3D67 404A',
 		'ref_keyaddrfile_chksum_dash': 'E83D 2C63 FEA2 4142',
 		'ref_keyaddrfile_chksum_eth': '3635 4DCF B752 8772',
 		'ref_keyaddrfile_chksum_etc': '9BAC 38E7 5C8E 42E0',
@@ -602,6 +607,7 @@ cmd_group['ref'] = (
 	('refkeyaddrgen_compressed', (['mmdat',pwfile],'new refwallet key-addr chksum (compressed)')),
 	('refpasswdgen',   (['mmdat',pwfile],'new refwallet passwd file chksum')),
 	('ref_b32passwdgen',(['mmdat',pwfile],'new refwallet passwd file chksum (base32)')),
+	('ref_hexpasswdgen',(['mmdat',pwfile],'new refwallet passwd file chksum (base32)')),
 )
 
 # misc. saved reference data
@@ -715,15 +721,17 @@ cmd_group['altcoin_ref'] = (
 	('ref_addrfile_chk_etc', 'reference address file (ETC)'),
 	('ref_addrfile_chk_dash','reference address file (DASH)'),
 	('ref_addrfile_chk_zec', 'reference address file (ZEC-T)'),
+	('ref_addrfile_chk_xmr', 'reference address file (XMR)'),
 	('ref_addrfile_chk_zec_z','reference address file (ZEC-Z)'),
 	('ref_keyaddrfile_chk_eth', 'reference key-address file (ETH)'),
 	('ref_keyaddrfile_chk_etc', 'reference key-address file (ETC)'),
 	('ref_keyaddrfile_chk_dash','reference key-address file (DASH)'),
 	('ref_keyaddrfile_chk_zec', 'reference key-address file (ZEC-T)'),
 	('ref_keyaddrfile_chk_zec_z','reference key-address file (ZEC-Z)'),
+	('ref_keyaddrfile_chk_xmr', 'reference key-address file (XMR)'),
 )
 
-# undocumented admin cmds
+# undocumented admin cmds - precede with 'admin'
 cmd_group_admin = OrderedDict()
 cmd_group_admin['create_ref_tx'] = (
 	('ref_tx_setup',                     'regtest (Bob and Alice) mode setup'),
@@ -1428,8 +1436,9 @@ class MMGenTestSuite(object):
 		chk = t.expect_getend(r'Checksum for {} data .*?: '.format(desc),regex=True)
 		if check_ref:
 			k = 'passfile32_chk' if ftype == 'pass32' \
-					else 'passfile_chk' if ftype == 'pass' \
-						else '{}file{}_chk'.format(ftype,'_'+mmtype if mmtype else '')
+					else 'passfilehex_chk' if ftype == 'passhex' \
+						else 'passfile_chk' if ftype == 'pass' \
+							else '{}file{}_chk'.format(ftype,'_'+mmtype if mmtype else '')
 			chk_ref = cfg[k] if ftype[:4] == 'pass' else cfg[k][fork][g.testnet]
 			refcheck('address data checksum',chk,chk_ref)
 			return
@@ -1739,6 +1748,10 @@ class MMGenTestSuite(object):
 		ea = ['--base32','--passwd-len','17']
 		self.addrgen(name,wf,pf,check_ref=True,ftype='pass32',id_str='фубар@crypto.org',extra_args=ea)
 
+	def ref_hexpasswdgen(self,name,wf,pf):
+		ea = ['--hex']
+		self.addrgen(name,wf,pf,check_ref=True,ftype='passhex',id_str='фубар@crypto.org',extra_args=ea)
+
 	def txsign_keyaddr(self,name,keyaddr_file,txfile):
 		t = MMGenExpect(name,'mmgen-txsign', ['-d',cfg['tmpdir'],'-M',keyaddr_file,txfile])
 		t.license()
@@ -2060,6 +2073,9 @@ class MMGenTestSuite(object):
 		self.ref_addrfile_chk(name,ftype='addr',coin='ZEC',subdir='zcash',pfx='-ZEC-Z',
 								mmtype='z',add_args=['mmtype=zcash_z'])
 
+	def ref_addrfile_chk_xmr(self,name):
+		self.ref_addrfile_chk(name,ftype='addr',coin='XMR',subdir='monero',pfx='-XMR-M')
+
 	def ref_addrfile_chk_dash(self,name):
 		self.ref_addrfile_chk(name,ftype='addr',coin='DASH',subdir='dash',pfx='-DASH-C')
 
@@ -2076,6 +2092,9 @@ class MMGenTestSuite(object):
 		self.ref_addrfile_chk(name,ftype='keyaddr',coin='ZEC',subdir='zcash',pfx='-ZEC-Z',
 								mmtype='z',add_args=['mmtype=zcash_z'])
 
+	def ref_keyaddrfile_chk_xmr(self,name):
+		self.ref_addrfile_chk(name,ftype='keyaddr',coin='XMR',subdir='monero',pfx='-XMR-M')
+
 	def ref_keyaddrfile_chk_dash(self,name):
 		self.ref_addrfile_chk(name,ftype='keyaddr',coin='DASH',subdir='dash',pfx='-DASH-C')
 
@@ -2715,7 +2734,8 @@ class MMGenTestSuite(object):
 			'refaddrgen_compressed',
 			'refkeyaddrgen_compressed',
 			'refpasswdgen',
-			'ref_b32passwdgen'
+			'ref_b32passwdgen',
+			'ref_hexpasswdgen'
 		):
 		for i in ('1','2','3'):
 			locals()[k+i] = locals()[k]