diff --git a/data_files/mmgen.cfg b/data_files/mmgen.cfg index 171a80f5..31e99560 100644 --- a/data_files/mmgen.cfg +++ b/data_files/mmgen.cfg @@ -17,6 +17,9 @@ # Uncomment to force 256-color output when 'color' is true: # force_256_color true +# Uncomment to use regtest mode (this also sets testnet to true): +# regtest true + # Uncomment to use testnet instead of mainnet: # testnet true diff --git a/mmgen/addr.py b/mmgen/addr.py index c857f56a..fd3b9423 100755 --- a/mmgen/addr.py +++ b/mmgen/addr.py @@ -23,31 +23,17 @@ addr.py: Address generation/display routines for the MMGen suite from hashlib import sha256,sha512 from binascii import hexlify,unhexlify from mmgen.common import * -from mmgen.bitcoin import privnum2addr,hex2wif,wif2hex +from mmgen.bitcoin import hex2wif,wif2hex,wif_is_compressed from mmgen.obj import * from mmgen.tx import * from mmgen.tw import * pnm = g.proj_name -def _test_for_keyconv(silent=False): - no_keyconv_errmsg = """ -Executable '{kconv}' unavailable. Please install '{kconv}' from the {vgen} -package on your system or specify the secp256k1 library. -""".format(kconv=g.keyconv_exec, vgen='vanitygen') - from subprocess import check_output,STDOUT - try: - check_output([g.keyconv_exec, '-G'],stderr=STDOUT) - except: - if not silent: msg(no_keyconv_errmsg.strip()) - return False - return True - def _test_for_secp256k1(silent=False): no_secp256k1_errmsg = """ -secp256k1 library unavailable. Will use '{kconv}', or failing that, the (slow) -native Python ECDSA library for address generation. -""".format(kconv=g.keyconv_exec) +secp256k1 library unavailable. Using (slow) native Python ECDSA library for address generation. +""" try: from mmgen.secp256k1 import priv2pub assert priv2pub(os.urandom(32),1) @@ -56,58 +42,68 @@ native Python ECDSA library for address generation. return False return True -def _wif2addr_python(wif): +def _pubhex2addr(pubhex,mmtype): + if mmtype == 'L': + from mmgen.bitcoin import hexaddr2addr,hash160 + return hexaddr2addr(hash160(pubhex)) + elif mmtype == 'S': + from mmgen.bitcoin import pubhex2segwitaddr + return pubhex2segwitaddr(pubhex) + else: + die(2,"'{}': mmtype unrecognized".format(mmtype)) + +def _privhex2addr_python(privhex,compressed,mmtype): + assert compressed or mmtype != 'S' + from mmgen.bitcoin import privnum2pubhex + pubhex = privnum2pubhex(int(privhex,16),compressed=compressed) + return _pubhex2addr(pubhex,mmtype=mmtype) + +def _privhex2addr_secp256k1(privhex,compressed,mmtype): + assert compressed or mmtype != 'S' + from mmgen.secp256k1 import priv2pub + pubhex = hexlify(priv2pub(unhexlify(privhex),int(compressed))) + return _pubhex2addr(pubhex,mmtype=mmtype) + +def _wif2addr_python(wif,mmtype): privhex = wif2hex(wif) if not privhex: return False - return privnum2addr(int(privhex,16),wif[0] != ('5','9')[g.testnet]) + return _privhex2addr_python(privhex,wif_is_compressed(wif),mmtype=mmtype) -def _wif2addr_keyconv(wif): - if wif[0] == ('5','9')[g.testnet]: - from subprocess import check_output - return check_output(['keyconv', wif]).split()[1] - else: - return _wif2addr_python(wif) +def _wif2addr_secp256k1(wif,mmtype): + privhex = wif2hex(wif) + if not privhex: return False + return _privhex2addr_secp256k1(privhex,wif_is_compressed(wif),mmtype=mmtype) -def _wif2addr_secp256k1(wif): - return _privhex2addr_secp256k1(wif2hex(wif),wif[0] != ('5','9')[g.testnet]) +def keygen_wif2pubhex(wif,selector): + privhex = wif2hex(wif) + if not privhex: return False + if selector == 1: + from mmgen.secp256k1 import priv2pub + return hexlify(priv2pub(unhexlify(privhex),int(wif_is_compressed(wif)))) + elif selector == 0: + from mmgen.bitcoin import privnum2pubhex + return privnum2pubhex(int(privhex,16),compressed=wif_is_compressed(wif)) -def _privhex2addr_python(privhex,compressed=False): - return privnum2addr(int(privhex,16),compressed) - -def _privhex2addr_keyconv(privhex,compressed=False): - if compressed: - return privnum2addr(int(privhex,16),compressed) - else: - from subprocess import check_output - return check_output(['keyconv', hex2wif(privhex,compressed=False)]).split()[1] - -def _privhex2addr_secp256k1(privhex,compressed=False): - from mmgen.secp256k1 import priv2pub - from mmgen.bitcoin import hexaddr2addr,pubhex2hexaddr - pubkey = priv2pub(unhexlify(privhex),int(compressed)) - return hexaddr2addr(pubhex2hexaddr(hexlify(pubkey))) - -def _keygen_selector(generator=None): - if generator: - if generator == 3 and _test_for_secp256k1(): return 2 - elif generator in (2,3) and _test_for_keyconv(): return 1 - else: - if opt.key_generator == 3 and _test_for_secp256k1(): return 2 - elif opt.key_generator in (2,3) and _test_for_keyconv(): return 1 +def keygen_selector(generator=None): + if _test_for_secp256k1() and generator != 1: + if opt.key_generator != 1: + return 1 msg('Using (slow) native Python ECDSA library for address generation') return 0 def get_wif2addr_f(generator=None): - gen = _keygen_selector(generator=generator) - return (_wif2addr_python,_wif2addr_keyconv,_wif2addr_secp256k1)[gen] + gen = keygen_selector(generator=generator) + return (_wif2addr_python,_wif2addr_secp256k1)[gen] def get_privhex2addr_f(generator=None): - gen = _keygen_selector(generator=generator) - return (_privhex2addr_python,_privhex2addr_keyconv,_privhex2addr_secp256k1)[gen] + gen = keygen_selector(generator=generator) + return (_privhex2addr_python,_privhex2addr_secp256k1)[gen] + class AddrListEntry(MMGenListItem): attrs = 'idx','addr','label','wif','sec' idx = MMGenListItemAttr('idx','AddrIdx') + wif = MMGenListItemAttr('wif','WifKey') class AddrListChksum(str,Hilite): color = 'pink' @@ -119,7 +115,7 @@ class AddrListChksum(str,Hilite): # print '[{}]'.format(' '.join(lines)) return str.__new__(cls,make_chksum_N(' '.join(lines), nchars=16, sep=True)) -class AddrListID(unicode,Hilite): +class AddrListIDStr(unicode,Hilite): color = 'green' trunc_ok = False def __new__(cls,addrlist,fmt_str=None): @@ -138,7 +134,15 @@ class AddrListID(unicode,Hilite): ret += ',', i prev = i s = ''.join([unicode(i) for i in ret]) - return unicode.__new__(cls,fmt_str.format(s) if fmt_str else '{}[{}]'.format(addrlist.seed_id,s)) + + if fmt_str: + ret = fmt_str.format(s) + elif addrlist.al_id.mmtype == 'L': + ret = '{}[{}]'.format(addrlist.al_id.sid,s) + else: + ret = '{}-{}[{}]'.format(addrlist.al_id.sid,addrlist.al_id.mmtype,s) + + return unicode.__new__(cls,ret) class AddrList(MMGenObject): # Address info for a single seed ID msgs = { @@ -150,7 +154,7 @@ class AddrList(MMGenObject): # Address info for a single seed ID # A text label of {n} characters or less may be added to the right of each # address, and it will be appended to the bitcoind wallet label upon import. # The label may contain any printable ASCII symbol. -""".strip().format(n=MMGenAddrLabel.max_len,pnm=pnm), +""".strip().format(n=TwComment.max_len,pnm=pnm), 'record_chksum': """ Record this checksum: it will be used to verify the address file in the future """.strip(), @@ -169,40 +173,46 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file gen_keys = False has_keys = False ext = 'addrs' + dfl_mmtype = MMGenAddrType('L') + cook_hash_rounds = 10 # not too many rounds, so hand decoding can still be feasible - def __init__(self,addrfile='',sid='',adata=[],seed='',addr_idxs='',src='', - addrlist='',keylist='',do_chksum=True,chksum_only=False): + def __init__(self,addrfile='',al_id='',adata=[],seed='',addr_idxs='',src='', + addrlist='',keylist='',mmtype=None,do_chksum=True,chksum_only=False): self.update_msgs() + mmtype = mmtype or self.dfl_mmtype + assert mmtype in MMGenAddrType.mmtypes - if addrfile: # data from MMGen address file - (sid,adata) = self.parse_file(addrfile) - elif sid and adata: # data from tracking wallet + if seed and addr_idxs: # data from seed + idxs + self.al_id,src = AddrListID(seed.sid,mmtype),'gen' + adata = self.generate(seed,addr_idxs,compressed=(mmtype=='S')) + elif addrfile: # data from MMGen address file + adata = self.parse_file(addrfile) # sets self.al_id + elif al_id and adata: # data from tracking wallet + self.al_id = al_id do_chksum = False - elif seed and addr_idxs: # data from seed + idxs - sid,src = seed.sid,'gen' - adata = self.generate(seed,addr_idxs) elif addrlist: # data from flat address list - sid = None - adata = [AddrListEntry(addr=a) for a in set(addrlist)] + self.al_id = None + adata = AddrListList([AddrListEntry(addr=a) for a in set(addrlist)]) elif keylist: # data from flat key list - sid,do_chksum = None,False - adata = [AddrListEntry(wif=k) for k in set(keylist)] + self.al_id = None + adata = AddrListList([AddrListEntry(wif=k) for k in set(keylist)]) elif seed or addr_idxs: die(3,'Must specify both seed and addr indexes') - elif sid or adata: - die(3,'Must specify both seed_id and adata') + elif al_id or adata: + die(3,'Must specify both al_id and adata') else: die(3,'Incorrect arguments for %s' % type(self).__name__) - # sid,adata now set - self.seed_id = sid + # al_id,adata now set self.data = adata self.num_addrs = len(adata) self.fmt_data = '' - self.id_str = None self.chksum = None - self.id_str = AddrListID(self) + + if self.al_id == None: return + + self.id_str = AddrListIDStr(self) if type(self) == KeyList: return @@ -221,17 +231,17 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file if k not in self.msgs: self.msgs[k] = AddrList.msgs[k] - def generate(self,seed,addrnums): + def generate(self,seed,addrnums,compressed): assert type(addrnums) is AddrIdxList - self.seed_id = SeedID(seed=seed) - seed = seed.get_data() + assert compressed in (True,False,None) + seed = seed.get_data() seed = self.cook_seed(seed) if self.gen_addrs: - privhex2addr_f = get_privhex2addr_f() + privhex2addr_f = get_privhex2addr_f() # choose internal ECDSA or secp256k1 generator - t_addrs,num,pos,out = len(addrnums),0,0,[] + t_addrs,num,pos,out = len(addrnums),0,0,AddrListList() while pos != t_addrs: seed = sha512(seed).digest() @@ -250,10 +260,10 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file sec = sha256(sha256(seed).digest()).hexdigest() if self.gen_addrs: - e.addr = privhex2addr_f(sec,compressed=False) + e.addr = privhex2addr_f(sec,compressed=compressed,mmtype=self.al_id.mmtype) if self.gen_keys: - e.wif = hex2wif(sec,compressed=False) + e.wif = hex2wif(sec,compressed=compressed) if opt.b16: e.sec = sec if self.gen_passwds: @@ -261,14 +271,31 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file dmsg('Key {:>03}: {}'.format(pos,sec)) out.append(e) + if g.debug: print 'generate():\n', e.pformat() qmsg('\r%s: %s %s%s generated%s' % ( - self.seed_id.hl(),t_addrs,self.gen_desc,suf(t_addrs,self.gen_desc_pl),' '*15)) + self.al_id.hl(),t_addrs,self.gen_desc,suf(t_addrs,self.gen_desc_pl),' '*15)) return out - def chk_addr_or_pw(self,addr): return is_btc_addr(addr) + def is_mainnet(self): + return self.data[0].addr.is_mainnet() - def cook_seed(self,seed): return seed + def is_for_current_chain(self): + return self.data[0].addr.is_for_current_chain() + + def chk_addr_or_pw(self,addr): + return {'L':'p2pkh','S':'p2sh'}[self.al_id.mmtype] == is_btc_addr(addr).addr_fmt + + def cook_seed(self,seed): + if self.al_id.mmtype == 'L': + return seed + else: + from mmgen.crypto import sha256_rounds + import hmac + key = self.al_id.mmtype.name + cseed = hmac.new(seed,key,sha256).digest() + dmsg('Seed: {}\nKey: {}\nCseed: {}\nCseed len: {}'.format(hexlify(seed),key,hexlify(cseed),len(cseed))) + return sha256_rounds(cseed,self.cook_hash_rounds) def encrypt(self,desc='new key list'): from mmgen.crypto import mmgen_encrypt @@ -284,7 +311,7 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file return [e.idx for e in self.data] def addrs(self): - return ['%s:%s'%(self.seed_id,e.idx) for e in self.data] + return ['%s:%s'%(self.al_id.sid,e.idx) for e in self.data] def addrpairs(self): return [(e.idx,e.addr) for e in self.data] @@ -313,21 +340,18 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file e.label = comment def make_reverse_dict(self,btcaddrs): - d,b = {},btcaddrs + d,b = MMGenDict(),btcaddrs for e in self.data: try: - d[b[b.index(e.addr)]] = ('%s:%s'%(self.seed_id,e.idx),e.label) + d[b[b.index(e.addr)]] = MMGenID('{}:{}'.format(self.al_id,e.idx)),e.label except: pass return d def flat_list(self): class AddrListFlatEntry(AddrListEntry): attrs = 'mmid','addr','wif' - return [AddrListFlatEntry( - mmid='{}:{}'.format(self.seed_id,e.idx), - addr=e.addr, - wif=e.wif) - for e in self.data] + return [AddrListFlatEntry(mmid='{}:{}'.format(self.al_id,e.idx),addr=e.addr,wif=e.wif) + for e in self.data] def remove_dups(self,cmplist,key='wif'): pop_list = [] @@ -338,7 +362,7 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file pop_list.append(n) for n in reversed(pop_list): self.data.pop(n) if pop_list: - vmsg(self.msgs['removed_dups'] % (len(pop_list),suf(removed,'k'))) + vmsg(self.msgs['removed_dups'] % (len(pop_list),suf(removed,'s'))) def add_wifs(self,al_key): if not al_key: return @@ -355,13 +379,15 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file def get_addrs(self): return self.get('addr') def get_wifs(self): return self.get('wif') + def get_addr_wif_pairs(self): + return [(d.addr,d.wif) for d in self.data if hasattr(d,'wif')] - def generate_addrs(self): + def generate_addrs_from_keylist(self): wif2addr_f = get_wif2addr_f() d = self.data for n,e in enumerate(d,1): qmsg_r('\rGenerating addresses from keylist: %s/%s' % (n,len(d))) - e.addr = wif2addr_f(e.wif) + e.addr = wif2addr_f(e.wif,mmtype='L') # 'L' == p2pkh qmsg('\rGenerated addresses from keylist: %s/%s ' % (n,len(d))) def format(self,enable_comments=False): @@ -385,9 +411,11 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file if type(self) == PasswordList: out.append(u'{} {} {}:{} {{'.format( - self.seed_id,self.pw_id_str,self.pw_fmt,self.pw_len)) + self.al_id.sid,self.pw_id_str,self.pw_fmt,self.pw_len)) + elif self.al_id.mmtype == 'L': + out.append('{} {{'.format(self.al_id.sid)) else: - out.append('{} {{'.format(self.seed_id)) + out.append('{} {} {{'.format(self.al_id.sid,MMGenAddrType.mmtypes[self.al_id.mmtype].upper())) fs = ' {:<%s} {:<34}{}' % len(str(self.data[-1].idx)) for e in self.data: @@ -410,7 +438,7 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file if self.has_keys and len(lines) % 2: return 'Key-address file has odd number of lines' - ret = [] + ret = AddrListList() while lines: l = lines.pop(0) @@ -444,7 +472,7 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file llen = len(ret) for n,e in enumerate(ret): msg_r('\rVerifying keys %s/%s' % (n+1,llen)) - if e.addr != wif2addr_f(e.wif): + if e.addr != wif2addr_f(e.wif,mmtype=self.al_id.mmtype): return "Key doesn't match address!\n %s\n %s" % (e.wif,e.addr) msg(' - done') @@ -463,29 +491,44 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file return do_error("Too few lines in address file (%s)" % len(lines)) ls = lines[0].split() - ls_len = (2,4)[type(self)==PasswordList] - if len(ls) != ls_len: + if not 1 < len(ls) < 5: return do_error("Invalid first line for {} file: '{}'".format(self.gen_desc,lines[0])) - if ls[-1] != '{': + if ls.pop() != '{': return do_error("'%s': invalid first line" % ls) if lines[-1] != '}': return do_error("'%s': invalid last line" % lines[-1]) - if not is_mmgen_seed_id(ls[0]): + + sid = ls.pop(0) + if not is_mmgen_seed_id(sid): return do_error("'%s': invalid Seed ID" % ls[0]) - if type(self) == PasswordList: - self.pw_id_str = MMGenPWIDString(ls[1]) - ss = ls[2].split(':') + if type(self) == PasswordList and len(ls) == 2: + ss = ls.pop().split(':') if len(ss) != 2: return do_error("'%s': invalid password length specifier (must contain colon)" % ls[2]) self.set_pw_fmt(ss[0]) self.set_pw_len(ss[1]) + self.pw_id_str = MMGenPWIDString(ls.pop()) + mmtype = MMGenPasswordType('P') + elif len(ls) == 1: + mmtype = ls.pop().lower() + try: + mmtype = MMGenAddrType(mmtype) + except: + return do_error(u"'{}': invalid address type in address file. Must be one of: {}".format( + mmtype,' '.join(MMGenAddrType.mmtypes.values()).upper())) + elif len(ls) == 0: + mmtype = MMGenAddrType('L') + else: + return do_error(u"Invalid first line for {} file: '{}'".format(self.gen_desc,lines[0])) - ret = self.parse_file_body(lines[1:-1]) - if type(ret) != list: - return do_error(ret) + self.al_id = AddrListID(SeedID(sid=sid),mmtype) - return ls[0],ret + data = self.parse_file_body(lines[1:-1]) + if not issubclass(type(data),list): + return do_error(data) + + return data class KeyAddrList(AddrList): data_desc = 'key-address' @@ -525,7 +568,7 @@ class PasswordList(AddrList): # A text label of {n} characters or less may be added to the right of each # password. The label may contain any printable ASCII symbol. # -""".strip().format(n=MMGenAddrLabel.max_len,pnm=pnm), +""".strip().format(n=TwComment.max_len,pnm=pnm), 'record_chksum': """ Record this checksum: it will be used to verify the password file in the future """.strip() @@ -546,7 +589,6 @@ Record this checksum: it will be used to verify the password file in the future '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' } } - cook_hash_rounds = 10 # not too many rounds, so hand decoding can still be feasible def __init__(self,infile=None,seed=None,pw_idxs=None,pw_id_str=None,pw_len=None,pw_fmt=None, chksum_only=False,chk_params_only=False): @@ -554,7 +596,7 @@ Record this checksum: it will be used to verify the password file in the future self.update_msgs() if infile: - (self.seed_id,self.data) = self.parse_file(infile) # sets self.pw_id_str,self.pw_fmt,self.pw_len + self.data = self.parse_file(infile) # sets self.pw_id_str,self.pw_fmt,self.pw_len else: for k in seed,pw_idxs: assert chk_params_only or k for k in pw_id_str,pw_fmt: assert k @@ -562,8 +604,8 @@ Record this checksum: it will be used to verify the password file in the future self.set_pw_fmt(pw_fmt) self.set_pw_len(pw_len) if chk_params_only: return - self.seed_id = seed.sid - self.data = self.generate(seed,pw_idxs) + self.al_id = AddrListID(seed.sid,MMGenPasswordType('P')) + self.data = self.generate(seed,pw_idxs,compressed=None) self.num_addrs = len(self.data) self.fmt_data = '' @@ -572,8 +614,8 @@ Record this checksum: it will be used to verify the password file in the future if chksum_only: Msg(self.chksum) else: - self.id_str = AddrListID(self,fmt_str=u'{}-{}-{}-{}[{{}}]'.format( - self.seed_id,self.pw_id_str,self.pw_fmt,self.pw_len)) + fs = u'{}-{}-{}-{}[{{}}]'.format(self.al_id.sid,self.pw_id_str,self.pw_fmt,self.pw_len) + self.id_str = AddrListIDStr(self,fs) qmsg(u'Checksum for {} data {}: {}'.format(self.data_desc,self.id_str.hl(),self.chksum.hl())) qmsg(self.msgs[('record_chksum','check_chksum')[bool(infile)]]) @@ -626,6 +668,7 @@ Record this checksum: it will be used to verify the password file in the future from mmgen.crypto import sha256_rounds # Changing either pw_fmt, pw_len or id_str will cause a different, unrelated set of # passwords to be generated: this is what we want + # NB: In original implementation, pw_id_str was 'baseN', not 'bN' fid_str = '{}:{}:{}'.format(self.pw_fmt,self.pw_len,self.pw_id_str.encode('utf8')) dmsg(u'Full ID string: {}'.format(fid_str.decode('utf8'))) # Original implementation was 'cseed = seed + fid_str'; hmac was not used @@ -646,54 +689,58 @@ re-import your addresses. } def __init__(self,source=None): - self.sids = {} + self.al_ids = {} if source == 'tw': self.add_tw_data() def seed_ids(self): - return self.sids.keys() + return self.al_ids.keys() - def addrlist(self,sid): - # TODO: Validate sid - if sid in self.sids: - return self.sids[sid] + def addrlist(self,al_id): + # TODO: Validate al_id + if al_id in self.al_ids: + return self.al_ids[al_id] def mmaddr2btcaddr(self,mmaddr): + al_id,idx = MMGenID(mmaddr).rsplit(':',1) btcaddr = '' - sid,idx = mmaddr.split(':') - if sid in self.seed_ids(): - btcaddr = self.addrlist(sid).btcaddr(int(idx)) - return btcaddr + if al_id in self.al_ids: + btcaddr = self.addrlist(al_id).btcaddr(int(idx)) + return btcaddr or None + + def btcaddr2mmaddr(self,btcaddr): + d = self.make_reverse_dict([btcaddr]) + return (d.values()[0][0]) if d else None def add_tw_data(self): - vmsg_r('Getting address data from tracking wallet...') + vmsg('Getting address data from tracking wallet') c = bitcoin_connection() accts = c.listaccounts(0,True) data,i = {},0 alists = c.getaddressesbyaccount([[k] for k in accts],batch=True) for acct,addrlist in zip(accts,alists): - maddr,label = parse_tw_acct_label(acct) - if maddr: + l = TwLabel(acct,on_fail='silent') + if l and l.mmid.type == 'mmgen': + obj = l.mmid.obj i += 1 if len(addrlist) != 1: die(2,self.msgs['too_many_acct_addresses'] % acct) - seed_id,idx = maddr.split(':') - if seed_id not in data: - data[seed_id] = [] - data[seed_id].append(AddrListEntry(idx=idx,addr=addrlist[0],label=label)) - vmsg('{n} {pnm} addresses found, {m} accounts total'.format( - n=i,pnm=pnm,m=len(accts))) - for sid in data: - self.add(AddrList(sid=sid,adata=data[sid])) + al_id = AddrListID(SeedID(sid=obj.sid),MMGenAddrType(obj.mmtype)) + if al_id not in data: + data[al_id] = [] + data[al_id].append(AddrListEntry(idx=obj.idx,addr=addrlist[0],label=l.comment)) + vmsg('{n} {pnm} addresses found, {m} accounts total'.format(n=i,pnm=pnm,m=len(accts))) + for al_id in data: + self.add(AddrList(al_id=al_id,adata=AddrListList(sorted(data[al_id],key=lambda a: a.idx)))) def add(self,addrlist): if type(addrlist) == AddrList: - self.sids[addrlist.seed_id] = addrlist + self.al_ids[addrlist.al_id] = addrlist return True else: raise TypeError, 'Error: object %s is not of type AddrList' % repr(addrlist) def make_reverse_dict(self,btcaddrs): - d = {} - for sid in self.sids: - d.update(self.sids[sid].make_reverse_dict(btcaddrs)) + d = MMGenDict() + for al_id in self.al_ids: + d.update(self.al_ids[al_id].make_reverse_dict(btcaddrs)) return d diff --git a/mmgen/bitcoin.py b/mmgen/bitcoin.py index 8517f790..65235461 100755 --- a/mmgen/bitcoin.py +++ b/mmgen/bitcoin.py @@ -53,33 +53,40 @@ b58a='123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' from mmgen.globalvars import g -def hash256(hexnum): # take hex, return hex - OP_HASH256 - return sha256(sha256(unhexlify(hexnum)).digest()).hexdigest() - def hash160(hexnum): # take hex, return hex - OP_HASH160 return hashlib_new('ripemd160',sha256(unhexlify(hexnum)).digest()).hexdigest() -pubhex2hexaddr = hash160 +def hash256(hexnum): # take hex, return hex - OP_HASH256 + return sha256(sha256(unhexlify(hexnum)).digest()).hexdigest() + +# devdoc/ref_transactions.md: +btc_ver_nums = { + 'p2pkh': (('00','1'),('6f','mn')), + 'p2sh': (('05','3'),('c4','2')) +} +addr_pfxs = { 'mainnet': '13', 'testnet': 'mn2', 'regtest': 'mn2' } +vnum_all = tuple([k for k,v in btc_ver_nums['p2pkh'] + btc_ver_nums['p2sh']]) def hexaddr2addr(hexaddr,p2sh=False): - # devdoc/ref_transactions.md: - s = ('00','6f','05','c4')[g.testnet+(2*p2sh)] + hexaddr.strip() + s = vnum_all[g.testnet+(2*p2sh)] + hexaddr.strip() lzeroes = (len(s) - len(s.lstrip('0'))) / 2 return ('1' * lzeroes) + _numtob58(int(s+hash256(s)[:8],16)) -def verify_addr(addr,verbose=False,return_hex=False): +def verify_addr(addr,verbose=False,return_hex=False,return_type=False): addr = addr.strip() - for vers_num,ldigit in ('00','1'),('05','3'),('6f','mn'),('c4','2'): - if addr[0] not in ldigit: continue - num = _b58tonum(addr) - if num == False: break - addr_hex = '{:050x}'.format(num) - if addr_hex[:2] != vers_num: continue - if hash256(addr_hex[:42])[:8] == addr_hex[42:]: - return addr_hex[2:42] if return_hex else True - else: - if verbose: Msg("Invalid checksum in address '%s'" % addr) - break + + for k in ('p2pkh','p2sh'): + for ver_num,ldigit in btc_ver_nums[k]: + if addr[0] not in ldigit: continue + num = _b58tonum(addr) + if num == False: break + addr_hex = '{:050x}'.format(num) + if addr_hex[:2] != ver_num: continue + if hash256(addr_hex[:42])[:8] == addr_hex[42:]: + return addr_hex[2:42] if return_hex else k if return_type else True + else: + if verbose: Msg("Invalid checksum in address '%s'" % addr) + break if verbose: Msg("Invalid address '%s'" % addr) return False @@ -97,7 +104,7 @@ def _b58tonum(b58num): b58num = b58num.strip() for i in b58num: if not i in b58a: return False - return sum([b58a.index(n) * (58**i) for i,n in enumerate(list(b58num[::-1]))]) + return sum(b58a.index(n) * (58**i) for i,n in enumerate(list(b58num[::-1]))) # The following are MMGen internal (non-Bitcoin) b58 functions @@ -146,9 +153,11 @@ def b58decode_pad(s): # Compressed address support: +def wif_is_compressed(wif): return wif[0] != ('5','9')[g.testnet] + def wif2hex(wif): wif = wif.strip() - compressed = wif[0] != ('5','9')[g.testnet] + compressed = wif_is_compressed(wif) num = _b58tonum(wif) if num == False: return False key = '{:x}'.format(num) @@ -178,5 +187,16 @@ def privnum2pubhex(numpriv,compressed=False): else: return '04'+pubkey -def privnum2addr(numpriv,compressed=False): - return hexaddr2addr(pubhex2hexaddr(privnum2pubhex(numpriv,compressed))) +def privnum2addr(numpriv,compressed=False,segwit=False): # used only by tool and testsuite + pubhex = privnum2pubhex(numpriv,compressed) + return pubhex2segwitaddr(pubhex) if segwit else hexaddr2addr(hash160(pubhex)) + +# Segwit: +def pubhex2redeem_script(pubhex): + # https://bitcoincore.org/en/segwit_wallet_dev/ + # The P2SH redeemScript is always 22 bytes. It starts with a OP_0, followed + # by a canonical push of the keyhash (i.e. 0x0014{20-byte keyhash}) + return '0014' + hash160(pubhex) + +def pubhex2segwitaddr(pubhex): + return hexaddr2addr(hash160(pubhex2redeem_script(pubhex)),p2sh=True) diff --git a/mmgen/common.py b/mmgen/common.py index a1ac332b..be39fb4b 100755 --- a/mmgen/common.py +++ b/mmgen/common.py @@ -25,3 +25,17 @@ from mmgen.globalvars import g import mmgen.opts as opts from mmgen.opts import opt from mmgen.util import * + +pw_note = """ +For passphrases all combinations of whitespace are equal and leading and +trailing space is ignored. This permits reading passphrase or brainwallet +data from a multi-line file with free spacing and indentation. +""".strip() + +bw_note = """ +BRAINWALLET NOTE: + +To thwart dictionary attacks, it's recommended to use a strong hash preset +with brainwallets. For a brainwallet passphrase to generate the correct +seed, the same seed length and hash preset parameters must always be used. +""".strip() diff --git a/mmgen/filename.py b/mmgen/filename.py index edb94638..5cf53444 100755 --- a/mmgen/filename.py +++ b/mmgen/filename.py @@ -32,11 +32,15 @@ class Filename(MMGenObject): self.basename = os.path.basename(fn) self.ext = get_extension(fn) self.ftype = None # the file's associated class + self.mtime = None + self.ctime = None + self.atime = None from mmgen.seed import SeedSource + from mmgen.tx import MMGenTX if ftype: if type(ftype) == type: - if issubclass(ftype,SeedSource): + if issubclass(ftype,SeedSource) or issubclass(ftype,MMGenTX): self.ftype = ftype # elif: # other MMGen file types else: @@ -44,6 +48,7 @@ class Filename(MMGenObject): else: die(3,"'%s': not a class" % ftype) else: + # TODO: other file types self.ftype = SeedSource.ext_to_type(self.ext) if not self.ftype: die(3,"'%s': not a recognized extension for SeedSource" % self.ext) @@ -64,6 +69,23 @@ class Filename(MMGenObject): os.close(fd) else: self.size = os.stat(fn).st_size + self.mtime = os.stat(fn).st_mtime + self.ctime = os.stat(fn).st_ctime + self.atime = os.stat(fn).st_atime + +class MMGenFileList(list,MMGenObject): + + def __init__(self,fns,ftype): + flist = [Filename(fn,ftype) for fn in fns] + return list.__init__(self,flist) + + def names(self): + return [f.name for f in self] + + def sort_by_age(self,key='mtime',reverse=False): + if key not in ('atime','ctime','mtime'): + die(1,"'{}': illegal sort key".format(key)) + self.sort(key=lambda a: getattr(a,key),reverse=reverse) def find_files_in_dir(ftype,fdir,no_dups=False): if type(ftype) != type: diff --git a/mmgen/globalvars.py b/mmgen/globalvars.py index bd9e0927..f1d7d1cd 100755 --- a/mmgen/globalvars.py +++ b/mmgen/globalvars.py @@ -31,13 +31,15 @@ from mmgen.obj import BTCAmt class g(object): + skip_segwit_active_check = bool(os.getenv('MMGEN_TEST_SUITE')) + def die(ev=0,s=''): if s: sys.stderr.write(s+'\n') sys.exit(ev) # Variables - these might be altered at runtime: - version = '0.9.1' - release_date = 'May 2017' + version = '0.9.199' + release_date = 'July 2017' proj_name = 'MMGen' proj_url = 'https://github.com/mmgen/mmgen' @@ -70,6 +72,10 @@ class g(object): color = (False,True)[sys.stdout.isatty()] force_256_color = False testnet = False + regtest = False + chain = None # set by first call to bitcoin_connection() + chains = 'mainnet','testnet','regtest' + bitcoind_version = None # set by first call to bitcoin_connection() rpc_host = '' rpc_port = 0 rpc_user = '' @@ -97,7 +103,7 @@ class g(object): # User opt sets global var: common_opts = ( 'color','no_license','rpc_host','rpc_port','testnet','rpc_user','rpc_password', - 'bitcoin_data_dir','force_256_color' + 'bitcoin_data_dir','force_256_color','regtest' ) required_opts = ( 'quiet','verbose','debug','outdir','echo_passphrase','passwd_file','stdout', @@ -114,7 +120,7 @@ class g(object): cfg_file_opts = ( 'color','debug','hash_preset','http_timeout','no_license','rpc_host','rpc_port', 'quiet','tx_fee_adj','usr_randchars','testnet','rpc_user','rpc_password', - 'bitcoin_data_dir','force_256_color','max_tx_fee' + 'bitcoin_data_dir','force_256_color','max_tx_fee','regtest' ) env_opts = ( 'MMGEN_BOGUS_WALLET_DATA', @@ -127,6 +133,7 @@ class g(object): 'MMGEN_NO_LICENSE', 'MMGEN_RPC_HOST', 'MMGEN_TESTNET' + 'MMGEN_REGTEST' ) min_screen_width = 80 @@ -136,8 +143,6 @@ class g(object): global_sets_opt = ['minconf','seed_len','hash_preset','usr_randchars','debug', 'quiet','tx_confs','tx_fee_adj','key_generator'] - keyconv_exec = 'keyconv' - mins_per_block = 9 passwd_max_tries = 5 @@ -151,8 +156,8 @@ class g(object): aesctr_iv_len = 16 hincog_chk_len = 8 - key_generators = 'python-ecdsa','keyconv','secp256k1' # 1,2,3 - key_generator = 3 # secp256k1 is default + key_generators = 'python-ecdsa','secp256k1' # '1','2' + key_generator = 2 # secp256k1 is default hash_presets = { # Scrypt params: diff --git a/mmgen/main_addrgen.py b/mmgen/main_addrgen.py index 3b174b8f..42a04372 100755 --- a/mmgen/main_addrgen.py +++ b/mmgen/main_addrgen.py @@ -25,18 +25,19 @@ from mmgen.common import * from mmgen.crypto import * from mmgen.addr import * from mmgen.seed import SeedSource +MAT = MMGenAddrType if sys.argv[0].split('-')[-1] == 'keygen': gen_what = 'keys' gen_desc = 'secret keys' opt_filter = None - note2 = 'By default, both addresses and secret keys are generated.\n\n' + note_addrkey = 'By default, both addresses and secret keys are generated.\n\n' else: gen_what = 'addresses' gen_desc = 'addresses' - opt_filter = 'hbcdeiHOKlpzPqrSv-' - note2 = '' -note1 = """ + opt_filter = 'hbcdeiHOKlpzPqrStv-' + note_addrkey = '' +note_secp256k1 = """ If available, the secp256k1 library will be used for address generation. """.strip() @@ -70,6 +71,8 @@ opts_data = { -r, --usr-randchars=n Get 'n' characters of additional randomness from user (min={g.min_urandchars}, max={g.max_urandchars}, default={g.usr_randchars}) -S, --stdout Print {what} to stdout +-t, --type=t Choose address type. Options: see ADDRESS TYPES below + (default: {dmat}) -v, --verbose Produce more verbose output -x, --b16 Print secret keys in hexadecimal too """.format( @@ -77,7 +80,8 @@ opts_data = { pnm=g.proj_name, kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]), kg=g.key_generator, - what=gen_what,g=g + what=gen_what,g=g, + dmat="'{}' or '{}'".format(MAT.dfl_mmtype,MAT.mmtypes[MAT.dfl_mmtype]) ), 'notes': """ @@ -87,26 +91,32 @@ opts_data = { Address indexes are given as a comma-separated list and/or hyphen-separated range(s). -{n2}{n1} +{n_addrkey}{n_secp} +ADDRESS TYPES: + {n_at} NOTES FOR ALL GENERATOR COMMANDS -{o.pw_note} +{pwn} -{o.bw_note} +{bwn} FMT CODES: {f} """.format( - n1=note1,n2=note2, + n_secp=note_secp256k1,n_addrkey=note_addrkey,pwn=pw_note,bwn=bw_note, f='\n '.join(SeedSource.format_fmt_codes().splitlines()), + n_at='\n '.join(["'{}', '{}'".format(k,v) for k,v in MAT.mmtypes.items()]), o=opts ) } cmd_args = opts.init(opts_data,add_opts=['b16'],opt_filter=opt_filter) +errmsg = "'{}': invalid parameter for --type option".format(opt.type) +addr_type = MAT(opt.type or MAT.dfl_mmtype,errmsg=errmsg) + if len(cmd_args) < 1: opts.usage() idxs = AddrIdxList(fmt_str=cmd_args.pop()) @@ -117,7 +127,7 @@ do_license_msg() ss = SeedSource(sf) i = (gen_what=='addresses') or bool(opt.no_addresses)*2 -al = (KeyAddrList,AddrList,KeyList)[i](seed=ss.seed,addr_idxs=idxs) +al = (KeyAddrList,AddrList,KeyList)[i](seed=ss.seed,addr_idxs=idxs,mmtype=addr_type) al.format() if al.gen_addrs and opt.print_checksum: diff --git a/mmgen/main_addrimport.py b/mmgen/main_addrimport.py index d134af8d..f7909b84 100755 --- a/mmgen/main_addrimport.py +++ b/mmgen/main_addrimport.py @@ -24,6 +24,7 @@ import time from mmgen.common import * from mmgen.addr import AddrList,KeyAddrList +from mmgen.obj import TwLabel # In batch mode, bitcoind just rescans each address separately anyway, so make # --batch and --rescan incompatible. @@ -35,6 +36,7 @@ opts_data = { 'options': """ -h, --help Print this help message --, --longhelp Print help message for long options (common options) +-a, --address=a Import the single Bitcoin address 'a' -b, --batch Import all addresses in one RPC call. -l, --addrlist Address source is a flat list of (non-MMGen) Bitcoin addresses -k, --keyaddr-file Address source is a key-address file @@ -53,29 +55,46 @@ The --batch and --rescan options cannot be used together. cmd_args = opts.init(opts_data) +def import_mmgen_list(infile): + al = (AddrList,KeyAddrList)[bool(opt.keyaddr_file)](infile) + if al.al_id.mmtype == 'S': + from mmgen.tx import segwit_is_active + if not segwit_is_active(): + rdie(2,'Segwit is not active on this chain. Cannot import Segwit addresses') + return al + +def import_flat_list(lines): + al = AddrList(addrlist=lines) + from mmgen.bitcoin import verify_addr + qmsg_r('Validating addresses...') + for e in al.data: + if not verify_addr(e.addr,verbose=True): + die(2,'\n%s: invalid address' % e.addr) + if e.addr.addr_fmt == 'p2sh': + fs = "\n'{}':\n Non-{} P2SH addresses may not be imported into the tracking wallet" + rdie(2,fs.format(e.addr,g.proj_name)) + return al + if len(cmd_args) == 1: infile = cmd_args[0] check_infile(infile) if opt.addrlist: lines = get_lines_from_file( infile,'non-{pnm} addresses'.format(pnm=g.proj_name),trim_comments=True) - ai = AddrList(addrlist=lines) + al = import_flat_list(lines) else: - ai = (AddrList,KeyAddrList)[bool(opt.keyaddr_file)](infile) + al = import_mmgen_list(infile) +elif len(cmd_args) == 0 and opt.address: + al = import_flat_list([opt.address]) + infile = 'command line' else: die(1,""" -You must specify an {pnm} address file (or a list of non-{pnm} addresses -with the '--addrlist' option) +You must specify an {pnm} address file, a single address, or a list of +non-{pnm} addresses with the '--addrlist' option) """.strip().format(pnm=g.proj_name)) -from mmgen.bitcoin import verify_addr -qmsg_r('Validating addresses...') -for e in ai.data: - if not verify_addr(e.addr,verbose=True): - die(2,'%s: invalid address' % e.addr) - -m = (' from Seed ID %s' % ai.seed_id) if ai.seed_id else '' -qmsg('OK. %s addresses%s' % (ai.num_addrs,m)) +m = ' from Seed ID {}'.format(al.al_id.sid) if hasattr(al.al_id,'sid') else '' +qmsg('OK. {} addresses{}'.format(al.num_addrs,m)) if not opt.test: c = bitcoin_connection() @@ -104,8 +123,8 @@ def import_address(addr,label,rescan): global err_flag err_flag = True -w_n_of_m = len(str(ai.num_addrs)) * 2 + 2 -w_mmid = '' if opt.addrlist else len(str(max(ai.idxs()))) + 12 +w_n_of_m = len(str(al.num_addrs)) * 2 + 2 +w_mmid = '' if opt.addrlist else len(str(max(al.idxs()))) + 12 if opt.rescan: import threading @@ -113,19 +132,26 @@ if opt.rescan: else: msg_fmt = '\r%-{}s %-34s %s'.format(w_n_of_m, w_mmid) -msg("Importing %s addresses from '%s'%s" % - (len(ai.data),infile,('',' (batch mode)')[bool(opt.batch)])) +msg("Importing {} address{} from {}{}".format( + len(al.data), suf(al.data,'es'), infile, + ('',' (batch mode)')[bool(opt.batch)] + )) + +if not al.is_for_current_chain(): + die(2,"Address{} not compatible with {} chain!".format((' list','')[bool(opt.address)],g.chain)) arg_list = [] -for n,e in enumerate(ai.data): +for n,e in enumerate(al.data): if e.idx: - label = '%s:%s' % (ai.seed_id,e.idx) + label = '{}:{}'.format(al.al_id,e.idx) if e.label: label += ' ' + e.label m = label else: label = 'btc:{}'.format(e.addr) m = 'non-'+g.proj_name + label = TwLabel(label) + if opt.batch: arg_list.append((e.addr,label,False)) elif opt.rescan: @@ -138,7 +164,7 @@ for n,e in enumerate(ai.data): while True: if t.is_alive(): elapsed = int(time.time() - start) - count = '%s/%s:' % (n+1, ai.num_addrs) + count = '%s/%s:' % (n+1, al.num_addrs) msg_r(msg_fmt % (secs_to_hms(elapsed),count,e.addr,'(%s)' % m)) time.sleep(1) else: @@ -147,7 +173,7 @@ for n,e in enumerate(ai.data): break else: import_address(e.addr,label,False) - count = '%s/%s:' % (n+1, ai.num_addrs) + count = '%s/%s:' % (n+1, al.num_addrs) msg_r(msg_fmt % (count, e.addr, '(%s)' % m)) if err_flag: die(2,'\nImport failed') msg(' - OK') diff --git a/mmgen/main_passgen.py b/mmgen/main_passgen.py index 6c4c324c..1776ff5b 100755 --- a/mmgen/main_passgen.py +++ b/mmgen/main_passgen.py @@ -98,9 +98,9 @@ EXAMPLE: NOTES FOR ALL GENERATOR COMMANDS -{o.pw_note} +{pwn} -{o.bw_note} +{bwn} FMT CODES: {f} @@ -108,6 +108,7 @@ FMT CODES: f='\n '.join(SeedSource.format_fmt_codes().splitlines()), o=opts,g=g,d58=dfl_len['b58'],d32=dfl_len['b32'], ml=MMGenPWIDString.max_len, + pwn=pw_note,bwn=bw_note, fs="', '".join(MMGenPWIDString.forbidden) ) } diff --git a/mmgen/main_tool.py b/mmgen/main_tool.py index 3c7f3690..c8f908fd 100755 --- a/mmgen/main_tool.py +++ b/mmgen/main_tool.py @@ -39,30 +39,23 @@ opts_data = { """.format(g=g), 'notes': """ -COMMANDS:{} + COMMANDS +{} Type '{} help for help on a particular command """.format(tool.cmd_help,g.prog_name) } -cmd_args = opts.init(opts_data, - add_opts=[ - 'hidden_incog_input_params', - 'in_fmt' - ]) +cmd_args = opts.init(opts_data,add_opts=['hidden_incog_input_params','in_fmt']) -if len(cmd_args) < 1: - opts.usage() - sys.exit(1) +if len(cmd_args) < 1: opts.usage() -command = cmd_args.pop(0) +Command = cmd_args.pop(0).capitalize() -if command not in tool.cmd_data: - die(1,"'%s': no such command" % command) +if Command == 'Help' and not cmd_args: tool.usage(None) -if cmd_args and cmd_args[0] == '--help': - tool.tool_usage(g.prog_name, command) - sys.exit() +if Command not in tool.cmd_data: + die(1,"'%s': no such command" % Command.lower()) -args,kwargs = tool.process_args(g.prog_name, command, cmd_args) -ret = tool.__dict__[command](*args,**kwargs) +args,kwargs = tool.process_args(Command,cmd_args) +ret = tool.__dict__[Command](*args,**kwargs) sys.exit(0 if ret in (None,True) else 1) # some commands die, some return False on failure diff --git a/mmgen/main_txcreate.py b/mmgen/main_txcreate.py index b8957961..80dd73e6 100755 --- a/mmgen/main_txcreate.py +++ b/mmgen/main_txcreate.py @@ -51,5 +51,5 @@ opts_data = { cmd_args = opts.init(opts_data) do_license_msg() -tx = txcreate(opt,cmd_args,do_info=opt.info) +tx = txcreate(cmd_args,do_info=opt.info) tx.write_to_file(ask_write=not opt.yes,ask_overwrite=not opt.yes,ask_write_default_yes=False) diff --git a/mmgen/main_txdo.py b/mmgen/main_txdo.py index 33fd3811..c86bf9d8 100755 --- a/mmgen/main_txdo.py +++ b/mmgen/main_txdo.py @@ -83,7 +83,7 @@ kal = get_keyaddrlist(opt) kl = get_keylist(opt) if kl and kal: kl.remove_dups(kal,key='wif') -tx = txcreate(opt,cmd_args,caller='txdo') +tx = txcreate(cmd_args,caller='txdo') txsign(opt,c,tx,seed_files,kl,kal) tx.write_to_file(ask_write=False) diff --git a/mmgen/main_txsend.py b/mmgen/main_txsend.py index bfa1e53a..726fd907 100755 --- a/mmgen/main_txsend.py +++ b/mmgen/main_txsend.py @@ -44,17 +44,13 @@ if len(cmd_args) == 1: else: opts.usage() do_license_msg() -tx = MMGenTX(infile) c = bitcoin_connection() - -if not tx.check_signed(c): - die(1,'Transaction is not signed!') - -if tx.btc_txid: - msg('Warning: transaction has already been sent!') - +tx = MMGenTX(infile) # sig check performed here qmsg("Signed transaction file '%s' is valid" % infile) +if not tx.marked_signed(c): + die(1,'Transaction is not signed!') + if not opt.yes: tx.view_with_prompt('View transaction data?') if tx.add_comment(): # edits an existing comment, returns true if changed diff --git a/mmgen/main_txsign.py b/mmgen/main_txsign.py index 46fdd64b..1d330518 100755 --- a/mmgen/main_txsign.py +++ b/mmgen/main_txsign.py @@ -90,7 +90,7 @@ for tx_num,tx_file in enumerate(tx_files,1): tx_num_str = ' #%s' % tx_num tx = MMGenTX(tx_file) - if tx.check_signed(c): + if tx.marked_signed(): die(1,'Transaction is already signed!') vmsg("Successfully opened transaction file '%s'" % tx_file) diff --git a/mmgen/main_wallet.py b/mmgen/main_wallet.py index 8c9159ee..8970e81b 100755 --- a/mmgen/main_wallet.py +++ b/mmgen/main_wallet.py @@ -30,8 +30,6 @@ usage = '[opts] [infile]' nargs = 1 iaction = 'convert' oaction = 'convert' -bw_note = opts.bw_note -pw_note = opts.pw_note invoked_as = 'passchg' if g.prog_name == 'mmgen-passchg' else g.prog_name.partition('-wallet')[2] @@ -99,14 +97,14 @@ opts_data = { ), 'notes': """ -{pw_note}{bw_note} +{pwn}{bwn} FMT CODES: {f} """.format( f='\n '.join(SeedSource.format_fmt_codes().splitlines()), - pw_note=pw_note, - bw_note=('','\n\n' + bw_note)[bool(bw_note)] + pwn=pw_note, + bwn=('','\n\n' + bw_note)[bool(bw_note)] ) } @@ -125,12 +123,11 @@ if invoked_as in ('conv','passchg'): msg(green('Processing input wallet')+dw_msg) ss_in = None if invoked_as == 'gen' else SeedSource(sf,passchg=(invoked_as=='passchg')) - if invoked_as == 'chk': lbl = ss_in.ssdata.label.hl() if hasattr(ss_in.ssdata,'label') else 'NONE' vmsg('Wallet label: {}'.format(lbl)) # TODO: display creation date - sys.exit() + sys.exit(0) if invoked_as in ('conv','passchg'): msg(green('Processing output wallet')) @@ -141,7 +138,7 @@ if invoked_as == 'gen': qmsg("This wallet's Seed ID: %s" % ss_out.seed.sid.hl()) if invoked_as == 'passchg': - if not (opt.force_update or [k for k in 'passwd','hash_preset','label' + if not (opt.force_update or [k for k in ('passwd','hash_preset','label') if getattr(ss_out.ssdata,k) != getattr(ss_in.ssdata,k)]): die(1,'Password, hash preset and label are unchanged. Taking no action') diff --git a/mmgen/obj.py b/mmgen/obj.py index bd12e1cf..ef3d6768 100755 --- a/mmgen/obj.py +++ b/mmgen/obj.py @@ -20,6 +20,7 @@ obj.py: MMGen native classes """ +import sys from decimal import * from mmgen.color import * lvl = 0 @@ -27,38 +28,73 @@ lvl = 0 class MMGenObject(object): # Pretty-print any object of type MMGenObject, recursing into sub-objects - WIP - def pprint(self): print self.pformat() +# def pmsg(self): sys.stderr.write(self.pformat()+'\n') +# def pdie(self): sys.stderr.write(self.pformat()+'\n'); sys.exit(0) + def pmsg(self): print(self.pformat()) + def pdie(self): print(self.pformat()); sys.exit(0) def pformat(self,lvl=0): - def do_list(out,e,lvl=0): - add_spc = False - if e and type(e[0]) not in (str,unicode): - out.append('\n') + from decimal import Decimal + scalars = (str,unicode,int,float,Decimal) + def do_list(out,e,lvl=0,is_dict=False): + out.append('\n') for i in e: - if hasattr(i,'pformat'): - out.append('{:>{l}}{}'.format('',i.pformat(lvl=lvl+1),l=(lvl+1)*8)) - elif type(i) in (str,unicode): - add_spc = True - out.append(u' {}'.format(repr(i))) - elif type(i) == list: - out.append(u'{:>{l}}{:16}'.format('','<'+type(i).__name__+'>',l=(lvl*8)+4)) - do_list(out,i,lvl=lvl) + el = i if not is_dict else e[i] + if is_dict: + out.append('{s}{:<{l}}'.format(i,s=' '*(4*lvl+8),l=10,l2=8*(lvl+1)+8)) + if hasattr(el,'pformat'): + out.append('{:>{l}}{}'.format('',el.pformat(lvl=lvl+1),l=(lvl+1)*8)) + elif type(el) in scalars: + if isList(e): + out.append(u'{:>{l}}{:16}\n'.format('',repr(el),l=lvl*8)) + else: + out.append(u' {}'.format(repr(el))) + elif isList(el) or isDict(el): + indent = 1 if is_dict else lvl*8+4 + out.append(u'{:>{l}}{:16}'.format('','<'+type(el).__name__+'>',l=indent)) + if isList(el) and type(el[0]) in scalars: out.append('\n') + do_list(out,el,lvl=lvl+1,is_dict=isDict(el)) else: - out.append(u'{:>{l}}{:16} {}\n'.format('','<'+type(i).__name__+'>',repr(i),l=(lvl*8)+8)) + out.append(u'{:>{l}}{:16} {}\n'.format('','<'+type(el).__name__+'>',repr(el),l=(lvl*8)+8)) + out.append('\n') if not e: out.append('{}\n'.format(repr(e))) - if add_spc: out.append('\n') - out = [] - out.append(u'<{}>\n'.format(type(self).__name__)) - d = self.__dict__ - for k in d: + + from collections import OrderedDict + def isDict(obj): + return issubclass(type(obj),dict) or issubclass(type(obj),OrderedDict) + def isList(obj): + return issubclass(type(obj),list) and type(obj) != OrderedDict + def isScalar(obj): + return any(issubclass(type(obj),t) for t in scalars) + +# print type(self) +# print dir(self) +# print self.__dict__ # *attributes* of object +# print self.__dict__.keys() # *attributes* of object +# print self.keys() + + out = [u'<{}>{}\n'.format(type(self).__name__,' '+repr(self) if isScalar(self) else '')] + if isList(self) or isDict(self): + do_list(out,self,lvl=lvl,is_dict=isDict(self)) + +# print repr(self.__dict__.keys()) + + for k in self.__dict__: + if k in ('_OrderedDict__root', '_OrderedDict__map'): continue # exclude these because of recursion e = getattr(self,k) - if type(e) == list: + if isList(e) or isDict(e): out.append(u'{:>{l}}{:<10} {:16}'.format('',k,'<'+type(e).__name__+'>',l=(lvl*8)+4)) - do_list(out,e,lvl=lvl) + do_list(out,e,lvl=lvl,is_dict=isDict(e)) elif hasattr(e,'pformat') and type(e) != type: out.append(u'{:>{l}}{:10} {}'.format('',k,e.pformat(lvl=lvl+1),l=(lvl*8)+4)) else: - out.append(u'{:>{l}}{:<10} {:16} {}\n'.format('',k,'<'+type(e).__name__+'>',repr(e),l=(lvl*8)+4)) - return ''.join(out) + out.append(u'{:>{l}}{:<10} {:16} {}\n'.format( + '',k,'<'+type(e).__name__+'>',repr(e),l=(lvl*8)+4)) + + import re + return re.sub('\n+','\n',''.join(out)) + +class MMGenList(list,MMGenObject): pass +class MMGenDict(dict,MMGenObject): pass # Descriptor: https://docs.python.org/2/howto/descriptor.html class MMGenListItemAttr(object): @@ -75,10 +111,10 @@ class MMGenListItemAttr(object): class MMGenListItem(MMGenObject): - addr = MMGenListItemAttr('addr','BTCAddr') - amt = MMGenListItemAttr('amt','BTCAmt') - mmid = MMGenListItemAttr('mmid','MMGenID') - label = MMGenListItemAttr('label','MMGenAddrLabel') + addr = MMGenListItemAttr('addr','BTCAddr') + amt = MMGenListItemAttr('amt','BTCAmt') + mmid = MMGenListItemAttr('mmid','MMGenID') + label = MMGenListItemAttr('label','TwComment') attrs = () attrs_priv = () @@ -91,7 +127,8 @@ class MMGenListItem(MMGenObject): "'{}': attribute '{}' in instance of class '{}' cannot be reassigned".format( val,attr,type(self).__name__) - attrs_base = ('attrs','attrs_priv','attrs_reassign','attrs_base','attr_error','set_error','__dict__','pformat') + attrs_base = ('attrs','attrs_priv','attrs_reassign','attrs_base','attr_error','set_error', + '__dict__','pformat','pmsg','pdie') def __init__(self,*args,**kwargs): if args: @@ -133,7 +170,7 @@ class InitErrors(object): @staticmethod def arg_chk(cls,on_fail): - assert on_fail in ('die','return','silent','raise'),"'on_fail' in class %s" % cls.__name__ + assert on_fail in ('die','return','silent','raise'),"arg_chk in class %s" % cls.__name__ @staticmethod def init_fail(m,on_fail,silent=False): @@ -142,8 +179,8 @@ class InitErrors(object): if on_fail == 'die': die(1,m) elif on_fail == 'return': if m: msg(m) - return None - elif on_fail == 'silent': return None + return None # TODO: change to False + elif on_fail == 'silent': return None # same here elif on_fail == 'raise': raise ValueError,m class AddrIdx(int,InitErrors): @@ -167,7 +204,7 @@ class AddrIdx(int,InitErrors): return cls.init_fail(m,on_fail) -class AddrIdxList(list,InitErrors): +class AddrIdxList(list,InitErrors,MMGenObject): max_len = 1000000 @@ -176,7 +213,7 @@ class AddrIdxList(list,InitErrors): assert fmt_str or idx_list if idx_list: # dies on failure - return list.__init__(self,sorted(set([AddrIdx(i) for i in idx_list]))) + return list.__init__(self,sorted(set(AddrIdx(i) for i in idx_list))) elif fmt_str: desc = fmt_str ret,fs = [],"'%s': value cannot be converted to address index" @@ -315,17 +352,19 @@ class BTCAmt(Decimal,Hilite,InitErrors): def __neg__(self,other,context=None): return type(self)(Decimal.__neg__(self,other,context)) -class BTCAddr(str,Hilite,InitErrors): +class BTCAddr(str,Hilite,InitErrors,MMGenObject): color = 'cyan' - width = 34 + width = 35 # max len of testnet p2sh addr def __new__(cls,s,on_fail='die'): cls.arg_chk(cls,on_fail) + m = "'%s': value is not a Bitcoin address" % s me = str.__new__(cls,s) - from mmgen.bitcoin import verify_addr - if type(s) in (str,unicode,BTCAddr) and verify_addr(s): - return me - else: - m = "'%s': value is not a Bitcoin address" % s + from mmgen.bitcoin import verify_addr,addr_pfxs + if type(s) in (str,unicode,BTCAddr): + me.addr_fmt = verify_addr(s,return_type=True) + me.testnet = s[0] in addr_pfxs['testnet'] + if me.addr_fmt: + return me return cls.init_fail(m,on_fail) @classmethod @@ -338,6 +377,21 @@ class BTCAddr(str,Hilite,InitErrors): s = s[:kwargs['width']-2] + '..' return Hilite.fmtc(s,**kwargs) + def is_for_current_chain(self): + from mmgen.globalvars import g + assert g.chain, 'global chain variable unset' + from bitcoin import addr_pfxs + return self[0] in addr_pfxs[g.chain] + + def is_mainnet(self): + from bitcoin import addr_pfxs + return self[0] in addr_pfxs['mainnet'] + + def is_in_tracking_wallet(self): + from mmgen.rpc import bitcoin_connection + d = bitcoin_connection().validateaddress(self) + return d['iswatchonly'] and 'account' in d + class SeedID(str,Hilite,InitErrors): color = 'blue' width = 8 @@ -351,6 +405,7 @@ class SeedID(str,Hilite,InitErrors): if type(seed) == Seed: return str.__new__(cls,make_chksum_8(seed.get_data())) elif sid: + sid = str(sid) from string import hexdigits if len(sid) == cls.width and set(sid) <= set(hexdigits.upper()): return str.__new__(cls,sid) @@ -358,7 +413,7 @@ class SeedID(str,Hilite,InitErrors): m = "'%s': value cannot be converted to SeedID" % str(seed or sid) return cls.init_fail(m,on_fail) -class MMGenID(str,Hilite,InitErrors): +class MMGenID(str,Hilite,InitErrors,MMGenObject): color = 'orange' width = 0 @@ -367,15 +422,83 @@ class MMGenID(str,Hilite,InitErrors): def __new__(cls,s,on_fail='die'): cls.arg_chk(cls,on_fail) s = str(s) - if ':' in s: - a,b = s.split(':',1) - sid = SeedID(sid=a,on_fail='silent') - if sid: - idx = AddrIdx(b,on_fail='silent') - if idx: - return str.__new__(cls,'%s:%s' % (sid,idx)) + try: + ss = s.split(':') + assert len(ss) in (2,3) + sid = SeedID(sid=ss[0],on_fail='silent') + assert sid + idx = AddrIdx(ss[-1],on_fail='silent') + assert idx + t = MMGenAddrType((MMGenAddrType.dfl_mmtype,ss[1])[len(ss) != 2],on_fail='silent') + assert t + me = str.__new__(cls,'{}:{}:{}'.format(sid,t,idx)) + me.sid = sid + me.mmtype = t + me.idx = idx + me.al_id = AddrListID(sid,me.mmtype) # key with colon! + assert me.al_id + me.sort_key = '{}:{}:{:0{w}}'.format(sid,t,idx,w=idx.max_digits) + return me + except: + m = "'%s': value cannot be converted to MMGenID" % s + return cls.init_fail(m,on_fail) - m = "'%s': value cannot be converted to MMGenID" % s +class TwMMGenID(str,Hilite,InitErrors,MMGenObject): + + color = 'orange' + width = 0 + trunc_ok = False + + def __new__(cls,s,on_fail='die'): + cls.arg_chk(cls,on_fail) + obj,sort_key = None,None + try: + obj = MMGenID(s,on_fail='silent') + sort_key,t = obj.sort_key,'mmgen' + except: + try: + assert len(s) > 4 and s[:4] == 'btc:' + obj,sort_key,t = str(s),'z_'+s,'non-mmgen' + except: + pass + + if obj and sort_key: + me = str.__new__(cls,obj) + me.obj = obj + me.sort_key = sort_key + me.type = t + return me + + m = "'{}': value cannot be converted to {}".format(s,cls.__name__) + return cls.init_fail(m,on_fail) + +# contains TwMMGenID,TwComment. Not for display +class TwLabel(str,InitErrors,MMGenObject): + + def __new__(cls,s,on_fail='die'): + cls.arg_chk(cls,on_fail) + try: + ss = s.split(None,1) + me = str.__new__(cls,s) + me.mmid = TwMMGenID(ss[0],on_fail='silent') + assert me.mmid + me.comment = TwComment(ss[1] if len(ss) == 2 else '',on_fail='silent') + assert me.comment != None + return me + except: + m = "'{}': value cannot be converted to {}".format(s,cls.__name__) + return cls.init_fail(m,on_fail) + +class HexStr(str,Hilite,InitErrors): + color = 'red' + trunc_ok = False + def __new__(cls,s,on_fail='die',case='lower'): + assert case in ('upper','lower') + cls.arg_chk(cls,on_fail) + from string import hexdigits + if set(s) <= set(getattr(hexdigits,case)()) and not len(s) % 2: + return str.__new__(cls,s) + m = "'{}': value cannot be converted to {}".format(s,cls.__name__) return cls.init_fail(m,on_fail) class MMGenTxID(str,Hilite,InitErrors): @@ -396,6 +519,65 @@ class BitcoinTxID(MMGenTxID): width = 64 hexcase = 'lower' +class WifKey(str,Hilite,InitErrors): + width = 53 + color = 'blue' + desc = 'WIF key' + def __new__(cls,s,on_fail='die',errmsg=None): + cls.arg_chk(cls,on_fail) + from mmgen.tx import is_wif + if is_wif(s): + me = str.__new__(cls,s) + return me + m = errmsg or "'{}': invalid value for {}".format(s,cls.desc) + return cls.init_fail(m,on_fail) + +class MMGenAddrType(str,Hilite,InitErrors): + width = 1 + trunc_ok = False + color = 'blue' + mmtypes = { + # TODO 'L' is ambiguous: For user, it means MMGen legacy uncompressed address. + # For generator functions, 'L' means any p2pkh address, and 'S' any ps2h address + 'L': 'legacy', + 'S': 'segwit', +# 'l': 'litecoin', +# 'e': 'ethereum', +# 'E': 'ethereum_classic', +# 'm': 'monero', +# 'z': 'zcash', + } + dfl_mmtype = 'L' + def __new__(cls,s,on_fail='die',errmsg=None): + cls.arg_chk(cls,on_fail) + for k,v in cls.mmtypes.items(): + if s in (k,v): + if s == v: s = k + me = str.__new__(cls,s) + me.name = cls.mmtypes[s] + return me + m = errmsg or "'{}': invalid value for {}".format(s,cls.__name__) + return cls.init_fail(m,on_fail) + +class MMGenPasswordType(MMGenAddrType): + mmtypes = { 'P': 'password' } + +class AddrListID(str,Hilite,InitErrors): + width = 10 + trunc_ok = False + color = 'yellow' + def __new__(cls,sid,mmtype,on_fail='die'): + cls.arg_chk(cls,on_fail) + m = "'{}': not a SeedID. Cannot create {}".format(sid,cls.__name__) + if type(sid) == SeedID: + m = "'{}': not an MMGenAddrType object. Cannot create {}".format(mmtype,cls.__name__) + if type(mmtype) in (MMGenAddrType,MMGenPasswordType): + me = str.__new__(cls,sid+':'+mmtype) # colon in key is OK + me.sid = sid + me.mmtype = mmtype + return me + return cls.init_fail(m,on_fail) + class MMGenLabel(unicode,Hilite,InitErrors): color = 'pink' @@ -425,7 +607,7 @@ class MMGenLabel(unicode,Hilite,InitErrors): elif cls.allowed and not set(list(s)).issubset(set(cls.allowed)): m = u"{} '{}' contains non-allowed symbols: {}".format(capfirst(cls.desc),s, ' '.join(set(list(s)) - set(cls.allowed))) - elif cls.forbidden and any([ch in s for ch in cls.forbidden]): + elif cls.forbidden and any(ch in s for ch in cls.forbidden): m = u"{} '{}' contains one of these forbidden symbols: '{}'".format(capfirst(cls.desc),s, "', '".join(cls.forbidden)) else: @@ -437,10 +619,10 @@ class MMGenWalletLabel(MMGenLabel): allowed = [unichr(i+32) for i in range(95)] desc = 'wallet label' -class MMGenAddrLabel(MMGenLabel): +class TwComment(MMGenLabel): max_len = 32 allowed = [unichr(i+32) for i in range(95)] - desc = 'address label' + desc = 'tracking wallet comment' class MMGenTXLabel(MMGenLabel): max_len = 72 @@ -451,3 +633,5 @@ class MMGenPWIDString(MMGenLabel): min_len = 1 desc = 'password ID string' forbidden = list(u' :/\\') + +class AddrListList(list,MMGenObject): pass diff --git a/mmgen/opts.py b/mmgen/opts.py index 48eb1045..4f434e07 100755 --- a/mmgen/opts.py +++ b/mmgen/opts.py @@ -27,26 +27,6 @@ from mmgen.globalvars import g import mmgen.share.Opts from mmgen.util import * -pw_note = """ -For passphrases all combinations of whitespace are equal and leading and -trailing space is ignored. This permits reading passphrase or brainwallet -data from a multi-line file with free spacing and indentation. -""".strip() - -bw_note = """ -BRAINWALLET NOTE: - -To thwart dictionary attacks, it's recommended to use a strong hash preset -with brainwallets. For a brainwallet passphrase to generate the correct -seed, the same seed length and hash preset parameters must always be used. -""".strip() - -version_info = """ -{pgnm_uc} version {g.version} -Part of the {pnm} suite, a Bitcoin cold-storage solution for the command line. -Copyright (C) {g.Cdates} {g.author} {g.email} -""".format(pnm=g.proj_name, g=g, pgnm_uc=g.prog_name.upper()).strip() - def usage(): Die(2,'USAGE: %s %s' % (g.prog_name, usage_txt)) def die_on_incompatible_opts(incompat_list): @@ -76,6 +56,7 @@ common_opts_data = """ --, --rpc-port=p Communicate with bitcoind listening on port 'p' --, --rpc-user=user Override 'rpcuser' in bitcoin.conf --, --rpc-password=pass Override 'rpcpassword' in bitcoin.conf +--, --regtest=0|1 Disable or enable regtest mode --, --testnet=0|1 Disable or enable testnet --, --skip-cfg-file Skip reading the configuration file --, --version Print version information and exit @@ -179,6 +160,13 @@ def override_from_env(): setattr(g,gname,set_for_type(val,getattr(g,gname),name,invert_bool)) def init(opts_data,add_opts=[],opt_filter=None): + + version_info = """ + {pgnm_uc} version {g.version} + Part of the {pnm} suite, a Bitcoin cold-storage solution for the command line. + Copyright (C) {g.Cdates} {g.author} {g.email} + """.format(pnm=g.proj_name, g=g, pgnm_uc=g.prog_name.upper()).strip() + opts_data['long_options'] = common_opts_data uopts,args,short_opts,long_opts,skipped_opts = \ @@ -218,6 +206,8 @@ def init(opts_data,add_opts=[],opt_filter=None): val = getattr(opt,k) if val != None: setattr(g,k,set_for_type(val,getattr(g,k),'--'+k)) + if g.regtest: g.testnet = True # These are equivalent for now + # Global vars are now final, including g.testnet, so we can set g.data_dir g.data_dir=os.path.normpath(os.path.join(g.data_dir_root,('',g.testnet_name)[g.testnet])) @@ -238,15 +228,16 @@ def init(opts_data,add_opts=[],opt_filter=None): if opt.show_hash_presets: _show_hash_presets() - sys.exit() + sys.exit(0) - if g.debug: opt_postproc_debug() if opt.verbose: opt.quiet = None die_on_incompatible_opts(g.incompatible_opts) opt_postproc_initializations() + if g.debug: opt_postproc_debug() + return args def check_opts(usr_opts): # Returns false if any check fails diff --git a/mmgen/rpc.py b/mmgen/rpc.py index 2b2d2461..6220c264 100755 --- a/mmgen/rpc.py +++ b/mmgen/rpc.py @@ -28,8 +28,6 @@ from mmgen.obj import BTCAmt class BitcoinRPCConnection(object): - client_version = 0 - def __init__( self, host=g.rpc_host,port=(8332,18332)[g.testnet], @@ -78,7 +76,7 @@ class BitcoinRPCConnection(object): p = {'method':cmd,'params':args,'id':1} def die_maybe(*args): - if cf['on_fail'] == 'return': + if cf['on_fail'] in ('return','silent'): return 'rpcfail',args else: die(*args[1:]) @@ -89,7 +87,7 @@ class BitcoinRPCConnection(object): class MyJSONEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, BTCAmt): - return (float,str)[caller.client_version>=120000](obj) + return (float,str)[g.bitcoind_version>=120000](obj) return json.JSONEncoder.default(self, obj) # Can't do UTF-8 labels yet: httplib only ascii? @@ -111,8 +109,9 @@ class BitcoinRPCConnection(object): dmsg(' RPC GETRESPONSE data ==> %s\n' % r.__dict__) if r.status != 200: - msg_r(yellow('Bitcoind RPC Error: ')) - msg(red('{} {}'.format(r.status,r.reason))) + if cf['on_fail'] != 'silent': + msg_r(yellow('Bitcoind RPC Error: ')) + msg(red('{} {}'.format(r.status,r.reason))) e1 = r.read() try: e3 = json.loads(e1)['error'] @@ -142,26 +141,30 @@ class BitcoinRPCConnection(object): return ret if cf['batch'] else ret[0] - rpcmethods = ( - 'createrawtransaction', 'backupwallet', + 'createrawtransaction', 'decoderawtransaction', 'disconnectnode', 'estimatefee', 'getaddressesbyaccount', 'getbalance', 'getblock', + 'getblockchaininfo', 'getblockcount', 'getblockhash', - 'getinfo', + 'getmempoolentry', + 'getnetworkinfo', 'getpeerinfo', + 'getrawmempool', + 'getrawtransaction', + 'gettransaction', 'importaddress', 'listaccounts', 'listunspent', 'sendrawtransaction', 'signrawtransaction', - 'getrawmempool', + 'validateaddress', 'walletpassphrase', ) diff --git a/mmgen/seed.py b/mmgen/seed.py index 1e123858..f0adb512 100755 --- a/mmgen/seed.py +++ b/mmgen/seed.py @@ -159,18 +159,21 @@ class SeedSource(MMGenObject): msg('Trying again...') @classmethod - def get_subclasses(cls): - if not hasattr(cls,'subclasses'): - gl = globals() - setattr(cls,'subclasses', - [gl[k] for k in gl if type(gl[k]) == type and issubclass(gl[k],cls)]) - return cls.subclasses + def get_subclasses_str(cls): # returns name of calling class too + return cls.__name__ + ' ' + ''.join([c.get_subclasses_str() for c in cls.__subclasses__()]) @classmethod - def get_subclasses_str(cls): - def GetSubclassesTree(cls): - return ''.join([c.__name__ +' '+ GetSubclassesTree(c) for c in cls.__subclasses__()]) - return GetSubclassesTree(cls) + def get_subclasses_easy(cls,acc=[]): + return [globals()[c] for c in cls.get_subclasses_str().split()] + + @classmethod + def get_subclasses(cls): # returns calling class too + def GetSubclassesTree(cls,acc): + acc += [cls] + for c in cls.__subclasses__(): GetSubclassesTree(c,acc) + acc = [] + GetSubclassesTree(cls,acc) + return acc @classmethod def get_extensions(cls): @@ -1027,8 +1030,8 @@ harder to find, you're advised to choose a much larger file size than this. msg('File size must be an integer no less than %s' % min_fsize) - from mmgen.tool import rand2file - rand2file(fn, str(fsize)) + from mmgen.tool import Rand2file # threaded routine + Rand2file(fn,str(fsize)) check_offset = False else: die(1,'Exiting at user request') diff --git a/mmgen/share/Opts.py b/mmgen/share/Opts.py index 2df52804..431d5772 100755 --- a/mmgen/share/Opts.py +++ b/mmgen/share/Opts.py @@ -21,7 +21,7 @@ Opts.py: Generic options handling """ import sys, getopt -# from mmgen.util import mdie,die,pp_die,pp_msg # DEBUG +# from mmgen.util import mdie,die,pdie,pmsg # DEBUG def usage(opts_data): print 'USAGE: %s %s' % (opts_data['prog_name'], opts_data['usage']) @@ -54,8 +54,8 @@ def process_opts(argv,opts_data,short_opts,long_opts): opts = {} for opt, arg in cl_opts: - if opt in ('-h','--help'): print_help(opts_data); sys.exit() - elif opt == '--longhelp': print_help(opts_data,longhelp=True); sys.exit() + if opt in ('-h','--help'): print_help(opts_data); sys.exit(0) + elif opt == '--longhelp': print_help(opts_data,longhelp=True); sys.exit(0) elif opt[:2] == '--' and opt[2:] in long_opts: opts[opt[2:].replace('-','_')] = True elif opt[:2] == '--' and opt[2:]+'=' in long_opts: diff --git a/mmgen/test.py b/mmgen/test.py index fc9b95be..8e5a348d 100755 --- a/mmgen/test.py +++ b/mmgen/test.py @@ -48,7 +48,8 @@ def mk_tmpdir(d): try: os.mkdir(d,0755) except OSError as e: if e.errno != 17: raise - else: msg("Created directory '%s'" % d) + else: + qmsg("Created directory '%s'" % d) def mk_tmpdir_path(path,cfg): try: diff --git a/mmgen/tool.py b/mmgen/tool.py index dfe07d3e..ddcf090a 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# -*- coding: UTF-8 -*- # # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution # Copyright (C)2013-2017 Philemon @@ -31,84 +32,99 @@ pnm = g.proj_name from collections import OrderedDict cmd_data = OrderedDict([ - ('help', [' [str]']), - ('usage', [' [str]']), - ('strtob58', [' [str-]','pad [int=0]']), - ('b58tostr', [' [str-]']), - ('hextob58', [' [str-]','pad [int=0]']), - ('b58tohex', [' [str-]','pad [int=0]']), - ('b58randenc', []), - ('b32tohex', [' [str-]','pad [int=0]']), - ('hextob32', [' [str-]','pad [int=0]']), - ('randhex', ['nbytes [int=32]']), - ('id8', [' [str]']), - ('id6', [' [str]']), - ('sha256x2', [' [str]', # TODO handle stdin + ('Help', [' [str]']), + ('Usage', [' [str]']), + ('Strtob58', [' [str-]','pad [int=0]']), + ('B58tostr', [' [str-]']), + ('Hextob58', [' [str-]','pad [int=0]']), + ('B58tohex', [' [str-]','pad [int=0]']), + ('B58randenc', []), + ('B32tohex', [' [str-]','pad [int=0]']), + ('Hextob32', [' [str-]','pad [int=0]']), + ('Randhex', ['nbytes [int=32]']), + ('Id8', [' [str]']), + ('Id6', [' [str]']), + ('Hash160', [' [str-]']), + ('Hash256', [' [str]', # TODO handle stdin 'hex_input [bool=False]','file_input [bool=False]']), - ('str2id6', [' [str-]']), - ('hexdump', [' [str]', 'cols [int=8]', 'line_nums [bool=True]']), - ('unhexdump', [' [str]']), - ('hexreverse', [' [str-]']), - ('hexlify', [' [str-]']), - ('rand2file', [' [str]',' [str]','threads [int=4]','silent [bool=False]']), + ('Str2id6', [' [str-]']), + ('Hexdump', [' [str]', 'cols [int=8]', 'line_nums [bool=True]']), + ('Unhexdump', [' [str]']), + ('Hexreverse', [' [str-]']), + ('Hexlify', [' [str-]']), + ('Rand2file', [' [str]',' [str]','threads [int=4]','silent [bool=False]']), - ('randwif', ['compressed [bool=False]']), - ('randpair', ['compressed [bool=False]']), - ('hex2wif', [' [str-]', 'compressed [bool=False]']), - ('wif2hex', [' [str-]', 'compressed [bool=False]']), - ('wif2addr', [' [str-]', 'compressed [bool=False]']), - ('hexaddr2addr', [' [str-]']), - ('addr2hexaddr', [' [str-]']), - ('pubkey2addr', [' [str-]']), - ('pubkey2hexaddr', [' [str-]']), - ('privhex2addr', [' [str-]','compressed [bool=False]']), + ('Randwif', ['compressed [bool=False]']), + ('Randpair', ['compressed [bool=False]','segwit [bool=False]']), + ('Hex2wif', [' [str-]','compressed [bool=False]']), + ('Wif2hex', [' [str-]']), + ('Wif2addr', [' [str-]','segwit [bool=False]']), + ('Wif2segwit_pair',[' [str-]']), + ('Hexaddr2addr', [' [str-]']), + ('Addr2hexaddr', [' [str-]']), + ('Privhex2addr', [' [str-]','compressed [bool=False]','segwit [bool=False]']), + ('Privhex2pubhex',[' [str-]','compressed [bool=False]']), + ('Pubhex2addr', [' [str-]','p2sh [bool=False]']), # new + ('Pubhex2redeem_script',[' [str-]']), # new + ('Wif2redeem_script', [' [str-]']), # new - ('hex2mn', [' [str-]',"wordlist [str='electrum']"]), - ('mn2hex', [' [str-]', "wordlist [str='electrum']"]), - ('mn_rand128', ["wordlist [str='electrum']"]), - ('mn_rand192', ["wordlist [str='electrum']"]), - ('mn_rand256', ["wordlist [str='electrum']"]), - ('mn_stats', ["wordlist [str='electrum']"]), - ('mn_printlist', ["wordlist [str='electrum']"]), + ('Hex2mn', [' [str-]',"wordlist [str='electrum']"]), + ('Mn2hex', [' [str-]', "wordlist [str='electrum']"]), + ('Mn_rand128', ["wordlist [str='electrum']"]), + ('Mn_rand192', ["wordlist [str='electrum']"]), + ('Mn_rand256', ["wordlist [str='electrum']"]), + ('Mn_stats', ["wordlist [str='electrum']"]), + ('Mn_printlist', ["wordlist [str='electrum']"]), - ('listaddresses',["addrs [str='']",'minconf [int=1]','showempty [bool=False]','pager [bool=False]','showbtcaddrs [bool=False]']), - ('getbalance', ['minconf [int=1]']), - ('txview', ['<{} TX file> [str]'.format(pnm),'pager [bool=False]','terse [bool=False]']), - ('twview', ["sort [str='age']",'reverse [bool=False]','minconf [int=1]','wide [bool=False]','pager [bool=False]']), + ('Listaddress',['<{} address> [str]'.format(pnm),'minconf [int=1]','pager [bool=False]','showempty [bool=True]''showbtcaddr [bool=True]']), + ('Listaddresses',["addrs [str='']",'minconf [int=1]','showempty [bool=False]','pager [bool=False]','showbtcaddrs [bool=False]']), + ('Getbalance', ['minconf [int=1]']), + ('Txview', ['<{} TX file(s)> [str]'.format(pnm),'pager [bool=False]','terse [bool=False]',"sort [str='mtime'] (options: 'ctime','atime')",'MARGS']), + ('Twview', ["sort [str='age']",'reverse [bool=False]','show_days [bool=True]','show_mmid [bool=True]','minconf [int=1]','wide [bool=False]','pager [bool=False]']), - ('add_label', ['<{} address> [str]'.format(pnm),'