diff --git a/mmgen/addr.py b/mmgen/addr.py index c040c24f..7c1626a1 100755 --- a/mmgen/addr.py +++ b/mmgen/addr.py @@ -20,24 +20,68 @@ addr.py: Address generation/display routines for the MMGen suite """ -from hashlib import sha256, sha512, new as hashlib_new -from binascii import hexlify, unhexlify - +from hashlib import sha256, sha512 from mmgen.common import * from mmgen.bitcoin import numtowif -from mmgen.tx import * from mmgen.obj import * +from mmgen.tx import * +from mmgen.tw import * pnm = g.proj_name -addrmsgs = { - 'too_many_acct_addresses': """ -ERROR: More than one address found for account: '%s'. -Your 'wallet.dat' file appears to have been altered by a non-{pnm} program. -Please restore your tracking wallet from a backup or create a new one and -re-import your addresses. -""".strip().format(pnm=pnm), - 'addrfile_header': """ +def test_for_keyconv(silent=False): + no_keyconv_errmsg = """ +Executable '{kconv}' unavailable. Falling back on (slow) internal ECDSA library. +Please install '{kconv}' from the {vgen} package on your system for much +faster address generation. +""".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) + return False + + return True + +class AddrListEntry(MMGenListItem): + attrs = 'idx','addr','label','wif','sec' + label = MMGenListItemAttr('label','MMGenAddrLabel') + idx = MMGenListItemAttr('idx','AddrIdx') + +class AddrListChksum(str,Hilite): + color = 'pink' + trunc_ok = False + def __new__(cls,addrlist): + lines=[' '.join([str(e.idx),e.addr]+([e.wif] if addrlist.has_keys else [])) + for e in addrlist.data] + return str.__new__(cls,make_chksum_N(' '.join(lines), nchars=16, sep=True)) + +class AddrListID(str,Hilite): + color = 'green' + trunc_ok = False + def __new__(cls,addrlist): + try: int(addrlist.data[0].idx) + except: + s = '(no idxs)' + else: + idxs = [e.idx for e in addrlist.data] + prev = idxs[0] + ret = prev, + for i in idxs[1:]: + if i == prev + 1: + if i == idxs[-1]: ret += '-', i + else: + if prev != ret[-1]: ret += '-', prev + ret += ',', i + prev = i + s = ''.join([str(i) for i in ret]) + return str.__new__(cls,'%s[%s]' % (addrlist.seed_id,s)) + +class AddrList(MMGenObject): # Address info for a single seed ID + msgs = { + 'file_header': """ # {pnm} address file # # This file is editable. @@ -46,396 +90,408 @@ re-import your addresses. # address, and it will be appended to the bitcoind wallet label upon import. # The label may contain any printable ASCII symbol. """.strip().format(n=g.max_addr_label_len,pnm=pnm), - 'keyfile_header': """ + 'record_chksum': """ +Record this checksum: it will be used to verify the address file in the future +""".strip(), + 'check_chksum': 'Check this value against your records', + 'removed_dups': """ +Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file +""".strip().format(pnm=pnm) + } + data_desc = 'address' + file_desc = 'addresses' + gen_desc = 'address' + gen_desc_pl = 'es' + gen_addrs = True + gen_keys = False + has_keys = False + ext = 'addrs' + + def __init__(self,addrfile='',sid='',adata=[],seed='',addr_idxs='',src='', + addrlist='',keylist='',do_chksum=True,chksum_only=False): + + self.update_msgs() + + if addrfile: # data from MMGen address file + (sid,adata) = self.parse_file(addrfile) + elif sid and adata: # data from tracking wallet + 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 addrlist] + elif keylist: # data from flat key list + sid,do_chksum = None,False + adata = [AddrListEntry(wif=k) for k in 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') + else: + die(3,'Incorrect arguments for %s' % type(self).__name__) + + # sid,adata now set + self.seed_id = sid + self.data = adata + self.num_addrs = len(adata) + self.fmt_data = '' + self.id_str = None + self.chksum = None + + if type(self) == KeyList: + self.id_str = AddrListID(self) + return + + if do_chksum: + self.chksum = AddrListChksum(self) + if chksum_only: + Msg(self.chksum) + else: + self.id_str = AddrListID(self) + qmsg('Checksum for %s data %s: %s' % + (self.data_desc,self.id_str.hl(),self.chksum.hl())) + qmsg(self.msgs[('check_chksum','record_chksum')[src=='gen']]) + + def update_msgs(self): + if type(self).msgs and type(self) != AddrList: + for k in AddrList.msgs: + if k not in self.msgs: + self.msgs[k] = AddrList.msgs[k] + + def generate(self,seed,addrnums): + assert type(addrnums) is AddrIdxList + self.seed_id = SeedID(seed=seed) + seed = seed.get_data() + + if self.gen_addrs: + if opt.no_keyconv or test_for_keyconv() == False: + msg('Using (slow) internal ECDSA library for address generation') + from mmgen.bitcoin import privnum2addr + keyconv = False + else: + from subprocess import check_output + keyconv = 'keyconv' + + t_addrs,num,pos,out = len(addrnums),0,0,[] + + while pos != t_addrs: + seed = sha512(seed).digest() + num += 1 # round + + if num != addrnums[pos]: continue + + pos += 1 + + qmsg_r('\rGenerating %s #%s (%s of %s)' % (self.gen_desc,num,pos,t_addrs)) + + e = AddrListEntry(idx=num) + + # Secret key is double sha256 of seed hash round /num/ + sec = sha256(sha256(seed).digest()).hexdigest() + wif = numtowif(int(sec,16)) + + if self.gen_addrs: + if keyconv: + e.addr = check_output([keyconv, wif]).split()[1] + else: + e.addr = privnum2addr(int(sec,16)) + + if self.gen_keys: + e.wif = wif + if opt.b16: e.sec = sec + + out.append(e) + + 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)) + return out + + def encrypt(self): + from mmgen.crypto import mmgen_encrypt + self.fmt_data = mmgen_encrypt(self.fmt_data,'new key list','') + self.ext += '.'+g.mmenc_ext + + def write_to_file(self,ask_tty=True,ask_write_default_yes=False): + fn = '{}.{}'.format(self.id_str,self.ext) + ask_tty = self.has_keys and not opt.quiet + write_data_to_file(fn,self.fmt_data,self.file_desc,ask_tty=ask_tty) + + def idxs(self): + return [e.idx for e in self.data] + + def addrs(self): + return ['%s:%s'%(self.seed_id,e.idx) for e in self.data] + + def addrpairs(self): + return [(e.idx,e.addr) for e in self.data] + + def btcaddrs(self): + return [e.addr for e in self.data] + + def comments(self): + return [e.label for e in self.data] + + def entry(self,idx): + for e in self.data: + if idx == e.idx: return e + + def btcaddr(self,idx): + for e in self.data: + if idx == e.idx: return e.addr + + def comment(self,idx): + for e in self.data: + if idx == e.idx: return e.label + + def set_comment(self,idx,comment): + for e in self.data: + if idx == e.idx: + e.label = comment + + def make_reverse_dict(self,btcaddrs): + d,b = {},btcaddrs + for e in self.data: + try: + d[b[b.index(e.addr)]] = ('%s:%s'%(self.seed_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] + + def remove_dups(self,cmplist,key='wif'): + pop_list = [] + for n,d in enumerate(self.data): + if getattr(d,key) == None: continue + for e in cmplist.data: + if getattr(e,key) and getattr(e,key) == getattr(d,key): + 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'))) + + def add_wifs(self,al_key): + for d in self.data: + for e in al_key.data: + if e.addr and e.wif and e.addr == d.addr: + d.wif = e.wif + + def list_missing(self,key): + return [d for d in self.data if not getattr(d,key)] + + def get(self,key): + return [getattr(d,key) for d in self.data if getattr(d,key)] + + def get_addrs(self): return self.get('addr') + def get_wifs(self): return self.get('wif') + + def generate_addrs(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) + qmsg('\rGenerated addresses from keylist: %s/%s ' % (n,len(d))) + + def format(self,enable_comments=False): + + def check_attrs(key,desc): + for e in self.data: + if not getattr(e,key): + die(3,'missing %s in addr data' % desc) + + if type(self) != KeyList: check_attrs('addr','addresses') + if self.has_keys: + if opt.b16: check_attrs('sec','hex keys') + check_attrs('wif','wif keys') + + out = [self.msgs['file_header']+'\n'] + if self.chksum: + out.append('# {} data checksum for {}: {}'.format( + self.data_desc.capitalize(),self.id_str,self.chksum)) + out.append('# Record this value to a secure location.\n') + out.append('%s {' % self.seed_id) + + fs = ' {:<%s} {:<34}{}' % len(str(self.data[-1].idx)) + 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: '+e.wif,c)) + else: # First line with idx + out.append(fs.format(e.idx, e.addr,c)) + if self.has_keys: + if opt.b16: out.append(fs.format('', 'hex: '+e.sec,c)) + out.append(fs.format('', 'wif: '+e.wif,c)) + + out.append('}') + self.fmt_data = '\n'.join([l.rstrip() for l in out]) + '\n' + + def parse_file_body(self,lines): + + if self.has_keys and len(lines) % 2: + return 'Key-address file has odd number of lines' + + ret = [] + while lines: + l = lines.pop(0) + d = l.split(None,2) + + if not is_mmgen_idx(d[0]): + return "'%s': invalid address num. in line: '%s'" % (d[0],l) + if not is_btc_addr(d[1]): + return "'%s': invalid Bitcoin address" % d[1] + + if len(d) != 3: d.append('') + + a = AddrListEntry(idx=int(d[0]),addr=d[1],label=d[2]) + + if self.has_keys: + l = lines.pop(0) + d = l.split(None,2) + + if d[0] != 'wif:': + return "Invalid key line in file: '%s'" % l + if not is_wif(d[1]): + return "'%s': invalid Bitcoin key" % d[1] + + a.wif = d[1] + + ret.append(a) + + if self.has_keys and keypress_confirm('Check key-to-address validity?'): + wif2addr_f = get_wif2addr_f() + 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): + return "Key doesn't match address!\n %s\n %s" % (e.wif,e.addr) + msg(' - done') + + return ret + + def parse_file(self,fn,buf=[],exit_on_error=True): + + lines = get_lines_from_file(fn,self.data_desc+' data',trim_comments=True) + + try: + sid,obrace = lines[0].split() + except: + errmsg = "Invalid first line: '%s'" % lines[0] + else: + cbrace = lines[-1] + if obrace != '{': + errmsg = "'%s': invalid first line" % lines[0] + elif cbrace != '}': + errmsg = "'%s': invalid last line" % cbrace + elif not is_mmgen_seed_id(sid): + errmsg = "'%s': invalid Seed ID" % sid + else: + ret = self.parse_file_body(lines[1:-1]) + if type(ret) == list: + return sid,ret + else: + errmsg = ret + + if exit_on_error: die(3,errmsg) + msg(errmsg) + return False + +class KeyAddrList(AddrList): + data_desc = 'key-address' + file_desc = 'secret keys' + gen_desc = 'key/address pair' + gen_desc_pl = 's' + gen_addrs = True + gen_keys = True + has_keys = True + ext = 'akeys' + +class KeyList(AddrList): + msgs = { + 'file_header': """ # {pnm} key file # # This file is editable. # Everything following a hash symbol '#' is a comment and ignored by {pnm}. -""".strip().format(pnm=pnm), - 'no_keyconv_msg': """ -Executable '{kconv}' unavailable. Falling back on (slow) internal ECDSA library. -Please install '{kconv}' from the {vgen} package on your system for much -faster address generation. -""".format(kconv=g.keyconv_exec, vgen='vanitygen') -} - -def test_for_keyconv(silent=False): - - from subprocess import check_output,STDOUT - try: - check_output([g.keyconv_exec, '-G'],stderr=STDOUT) - except: - if not silent: msg(addrmsgs['no_keyconv_msg']) - return False - - return True +""".strip().format(pnm=pnm) + } + data_desc = 'key' + file_desc = 'secret keys' + gen_desc = 'key' + gen_desc_pl = 's' + gen_addrs = False + gen_keys = True + has_keys = True + ext = 'keys' -def generate_addrs(seed, addrnums, source='addrgen'): +class AddrData(MMGenObject): + msgs = { + 'too_many_acct_addresses': """ +ERROR: More than one address found for account: '%s'. +Your 'wallet.dat' file appears to have been altered by a non-{pnm} program. +Please restore your tracking wallet from a backup or create a new one and +re-import your addresses. +""".strip().format(pnm=pnm) + } - from util import make_chksum_8 - seed_id = make_chksum_8(seed) # Must do this before seed gets clobbered - - if 'a' in opt.gen_what: - if opt.no_keyconv or test_for_keyconv() == False: - msg('Using (slow) internal ECDSA library for address generation') - from mmgen.bitcoin import privnum2addr - keyconv = False - else: - from subprocess import check_output - keyconv = 'keyconv' - - addrnums = sorted(set(addrnums)) # don't trust the calling function - t_addrs,num,pos,out = len(addrnums),0,0,[] - - w = { - 'ka': ('key/address pair','s'), - 'k': ('key','s'), - 'a': ('address','es') - }[opt.gen_what] - - from mmgen.addr import AddrInfoEntry,AddrInfo - - while pos != t_addrs: - seed = sha512(seed).digest() - num += 1 # round - - if num != addrnums[pos]: continue - - pos += 1 - - qmsg_r('\rGenerating %s #%s (%s of %s)' % (w[0],num,pos,t_addrs)) - - e = AddrInfoEntry() - e.idx = num - - # Secret key is double sha256 of seed hash round /num/ - sec = sha256(sha256(seed).digest()).hexdigest() - wif = numtowif(int(sec,16)) - - if 'a' in opt.gen_what: - if keyconv: - e.addr = check_output([keyconv, wif]).split()[1] - else: - e.addr = privnum2addr(int(sec,16)) - - if 'k' in opt.gen_what: e.wif = wif - if opt.b16: e.sec = sec - - out.append(e) - - m = w[0] if t_addrs == 1 else w[0]+w[1] - qmsg('\r%s: %s %s generated%s' % (seed_id,t_addrs,m,' '*15)) - a = AddrInfo(has_keys='k' in opt.gen_what, source=source) - a.initialize(seed_id,out) - return a - -def _parse_addrfile_body(lines,has_keys=False,check=False): - - if has_keys and len(lines) % 2: - return 'Key-address file has odd number of lines' - - ret = [] - while lines: - a = AddrInfoEntry() - l = lines.pop(0) - d = l.split(None,2) - - if not is_mmgen_idx(d[0]): - return "'%s': invalid address num. in line: '%s'" % (d[0],l) - if not is_btc_addr(d[1]): - return "'%s': invalid Bitcoin address" % d[1] - - if len(d) == 3: check_addr_label(d[2]) - else: d.append('') - - a.idx,a.addr,a.comment = int(d[0]),unicode(d[1]),unicode(d[2]) - - if has_keys: - l = lines.pop(0) - d = l.split(None,2) - - if d[0] != 'wif:': - return "Invalid key line in file: '%s'" % l - if not is_wif(d[1]): - return "'%s': invalid Bitcoin key" % d[1] - - a.wif = unicode(d[1]) - - ret.append(a) - - if has_keys and keypress_confirm('Check key-to-address validity?'): - wif2addr_f = get_wif2addr_f() - 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): - return "Key doesn't match address!\n %s\n %s" % (e.wif,e.addr) - msg(' - done') - - return ret - - -def _parse_addrfile(fn,buf=[],has_keys=False,exit_on_error=True): - - if buf: lines = remove_comments(buf.splitlines()) # DOS-safe - else: lines = get_lines_from_file(fn,'address data',trim_comments=True) - - try: - sid,obrace = lines[0].split() - except: - errmsg = "Invalid first line: '%s'" % lines[0] - else: - cbrace = lines[-1] - if obrace != '{': - errmsg = "'%s': invalid first line" % lines[0] - elif cbrace != '}': - errmsg = "'%s': invalid last line" % cbrace - elif not is_mmgen_seed_id(sid): - errmsg = "'%s': invalid Seed ID" % sid - else: - ret = _parse_addrfile_body(lines[1:-1],has_keys) - if type(ret) == list: return sid,ret - else: errmsg = ret - - if exit_on_error: die(3,errmsg) - else: return False - - -def _parse_keyaddr_file(fn): - from mmgen.crypto import mmgen_decrypt_file_maybe - d = mmgen_decrypt_file_maybe(fn,'key-address file') - return _parse_addrfile('',buf=d,has_keys=True,exit_on_error=False) - - -class AddrInfoList(MMGenObject): - - def __init__(self,addrinfo=None,bitcoind_connection=None): - self.data = {} - if bitcoind_connection: - self.add_wallet_data(bitcoind_connection) + def __init__(self,source=None): + self.sids = {} + if source == 'tw': self.add_tw_data() def seed_ids(self): - return self.data.keys() + return self.sids.keys() - def addrinfo(self,sid): + def addrlist(self,sid): # TODO: Validate sid - if sid in self.data: - return self.data[sid] + if sid in self.sids: + return self.sids[sid] def mmaddr2btcaddr(self,mmaddr): btcaddr = '' sid,idx = mmaddr.split(':') if sid in self.seed_ids(): - btcaddr = self.addrinfo(sid).btcaddr(int(idx)) + btcaddr = self.addrlist(sid).btcaddr(int(idx)) return btcaddr - def add_wallet_data(self,c): - vmsg_r('Getting account data from wallet...') + def add_tw_data(self): + vmsg_r('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): - ma,comment = parse_mmgen_label(acct) - if ma: + maddr,label = parse_tw_acct_label(acct) + if maddr: i += 1 -# addrlist = c.getaddressesbyaccount(acct) if len(addrlist) != 1: - die(2,addrmsgs['too_many_acct_addresses'] % acct) - seed_id,idx = ma.split(':') + die(2,self.msgs['too_many_acct_addresses'] % acct) + seed_id,idx = maddr.split(':') if seed_id not in data: data[seed_id] = [] - a = AddrInfoEntry() - a.idx,a.addr,a.comment = \ - int(idx),unicode(addrlist[0]),unicode(comment) - data[seed_id].append(a) + 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(AddrInfo(sid=sid,adata=data[sid])) + self.add(AddrList(sid=sid,adata=data[sid])) - def add(self,addrinfo): - if type(addrinfo) == AddrInfo: - self.data[addrinfo.seed_id] = addrinfo + def add(self,addrlist): + if type(addrlist) == AddrList: + self.sids[addrlist.seed_id] = addrlist return True else: - die(1,'Error: object %s is not of type AddrInfo' % repr(addrinfo)) + raise TypeError, 'Error: object %s is not of type AddrList' % repr(addrlist) def make_reverse_dict(self,btcaddrs): d = {} - for k in self.data.keys(): - d.update(self.data[k].make_reverse_dict(btcaddrs)) + for sid in self.sids: + d.update(self.sids[sid].make_reverse_dict(btcaddrs)) return d - -class AddrInfoEntry(MMGenObject): - - def __init__(self): pass - -class AddrInfo(MMGenObject): - - def __init__(self,addrfile='',has_keys=False,sid='',adata=[],source='',caller=''): - self.has_keys = has_keys - self.caller = caller - do_chksum = True - if addrfile: - f = (_parse_addrfile,_parse_keyaddr_file)[bool(has_keys)] - sid,adata = f(addrfile) - self.source = 'addrfile' - elif sid and adata: # data from wallet - self.source = 'wallet' - elif sid or adata: - die(3,'Must specify address file, or seed_id + adata') - else: - self.source = source if source else 'unknown' - return - - self.initialize(sid,adata) - - def initialize(self,seed_id,addrdata): - if seed_id in self.__dict__: - msg('Seed ID already set for object %s' % self) - return False - self.seed_id = seed_id - self.addrdata = addrdata - self.num_addrs = len(addrdata) - if self.source in ('wallet','txsign'): - self.checksum = None - self.idxs_fmt = None - elif self.source == 'addrgen' and opt.gen_what == 'k': - self.checksum = None - self.fmt_addr_idxs() - else: # self.source in addrfile, addrgen - self.make_addrdata_chksum() - if self.caller == 'tool': - Msg(self.checksum) - else: - self.fmt_addr_idxs() - w = ('address','key-address')[bool(self.has_keys)] - qmsg('Checksum for %s data %s[%s]: %s' % - (w,self.seed_id,self.idxs_fmt,self.checksum)) - if self.source == 'addrgen': - qmsg( - 'Record this checksum: it will be used to verify the address file in the future') - elif self.source == 'addrfile': - qmsg('Check this value against your records') - - def idxs(self): - return [e.idx for e in self.addrdata] - - def addrs(self): - return ['%s:%s'%(self.seed_id,e.idx) for e in self.addrdata] - - def addrpairs(self): - return [(e.idx,e.addr) for e in self.addrdata] - - def btcaddrs(self): - return [e.addr for e in self.addrdata] - - def comments(self): - return [e.comment for e in self.addrdata] - - def entry(self,idx): - for e in self.addrdata: - if idx == e.idx: return e - - def btcaddr(self,idx): - for e in self.addrdata: - if idx == e.idx: return e.addr - - def comment(self,idx): - for e in self.addrdata: - if idx == e.idx: return e.comment - - def set_comment(self,idx,comment): - for e in self.addrdata: - if idx == e.idx: - if is_valid_tx_comment(comment): - e.comment = comment - else: - sys.exit(2) - - def make_reverse_dict(self,btcaddrs): - d,b = {},btcaddrs - for e in self.addrdata: - try: - d[b[b.index(e.addr)]] = ('%s:%s'%(self.seed_id,e.idx),e.comment) - except: pass - return d - - - def make_addrdata_chksum(self): - lines=[' '.join([str(e.idx),e.addr]+([e.wif] if self.has_keys else [])) - for e in self.addrdata] - self.checksum = make_chksum_N(' '.join(lines), nchars=24, sep=True) - - - def fmt_data(self,enable_comments=False): - # Check data integrity - either all or none must exist for each attr - attrs = ['addr','wif','sec'] - status = [0,0,0] - for d in self.addrdata: - for j,attr in enumerate(attrs): - if hasattr(d,attr): - status[j] += 1 - - for i,s in enumerate(status): - if s != 0 and s != self.num_addrs: - die(3,'%s missing %s in addr data'% (self.num_addrs-s,attrs[i])) - - if status[0] == status[1] == 0: - die(3,'Addr data contains neither addresses nor keys') - - # Header - out = [] - k = ('addrfile_header','keyfile_header')[status[0]==0] - out.append(addrmsgs[k]+'\n') - if self.checksum: - w = ('Key-address','Address')[status[1]==0] - out.append('# {} data checksum for {}[{}]: {}'.format( - w, self.seed_id, self.idxs_fmt, self.checksum)) - out.append('# Record this value to a secure location.\n') - out.append('%s {' % self.seed_id) - - # Body - fs = ' {:<%s} {:<34}{}' % len(str(self.addrdata[-1].idx)) - for e in self.addrdata: - c = '' - if enable_comments: - try: c = ' '+e.comment - except: pass - if status[0]: # First line with idx - out.append(fs.format(e.idx, e.addr,c)) - else: - out.append(fs.format(e.idx, 'wif: '+e.wif,c)) - - if status[1]: # Subsequent lines - if status[2]: - out.append(fs.format('', 'hex: '+e.sec,c)) - if status[0]: - out.append(fs.format('', 'wif: '+e.wif,c)) - - out.append('}') - - return '\n'.join([l.rstrip() for l in out]) + '\n' - - - def fmt_addr_idxs(self): - - try: int(self.addrdata[0].idx) - except: - self.idxs_fmt = '(no idxs)' - return - - addr_idxs = [e.idx for e in self.addrdata] - prev = addr_idxs[0] - ret = prev, - - for i in addr_idxs[1:]: - if i == prev + 1: - if i == addr_idxs[-1]: ret += '-', i - else: - if prev != ret[-1]: ret += '-', prev - ret += ',', i - prev = i - - self.idxs_fmt = ''.join([str(i) for i in ret]) diff --git a/mmgen/bitcoin.py b/mmgen/bitcoin.py index 7e71358c..819857eb 100755 --- a/mmgen/bitcoin.py +++ b/mmgen/bitcoin.py @@ -120,10 +120,11 @@ def b58encode(s): def b58decode(b58num): if b58num == '': return '' # Zap all spaces: - num = _b58tonum(b58num.translate(None,' \t\n\r')) + # Use translate() only with str, not unicode + num = _b58tonum(str(b58num).translate(None,' \t\n\r')) if num == False: return False - out = '{:x}'.format(num) - return unhexlify('0'*(len(out)%2) + out) + out = u'{:x}'.format(num) + return unhexlify(u'0'*(len(out)%2) + out) # These yield bytewise equivalence in our special cases: diff --git a/mmgen/crypto.py b/mmgen/crypto.py index 13c1aa3b..e061b50c 100755 --- a/mmgen/crypto.py +++ b/mmgen/crypto.py @@ -242,15 +242,6 @@ def mmgen_decrypt(data,desc='data',hash_preset=''): msg('Incorrect passphrase or hash preset') return False -def mmgen_decrypt_file_maybe(fn,desc): - d = get_data_from_file(fn,'{} data'.format(desc),binary=True) - have_enc_ext = get_extension(fn) == g.mmenc_ext - if have_enc_ext or not is_ascii(d): - m = ('Attempting to decrypt','Decrypting')[have_enc_ext] - msg('%s %s %s' % (m,desc,fn)) - d = mmgen_decrypt_retry(d,desc) - return d - def mmgen_decrypt_retry(d,desc='data'): while True: d_dec = mmgen_decrypt(d,desc) diff --git a/mmgen/filename.py b/mmgen/filename.py index bd2903ce..87bb97cb 100755 --- a/mmgen/filename.py +++ b/mmgen/filename.py @@ -29,16 +29,24 @@ class Filename(MMGenObject): self.name = fn self.dirname = os.path.dirname(fn) self.basename = os.path.basename(fn) - self.ext = None - self.ftype = ftype + self.ext = get_extension(fn) + self.ftype = None # the file's associated class -# This should be done before license msg instead -# check_infile(fn) + from mmgen.seed import SeedSource + if ftype: + if type(ftype) == type: + if issubclass(ftype,SeedSource): + self.ftype = ftype + # elif: # other MMGen file types + else: + die(3,"'%s': not a recognized file type for SeedSource" % ftype) + else: + die(3,"'%s': not a class" % ftype) + else: + self.ftype = SeedSource.ext_to_type(self.ext) + if not self.ftype: + die(3,"'%s': not a recognized extension for SeedSource" % self.ext) - if not ftype: - self.ext = get_extension(fn) - if not (self.ext): - die(2,"Unrecognized extension '.%s' for file '%s'" % (self.ext,fn)) # TODO: Check for Windows mode = (os.O_RDONLY,os.O_RDWR)[bool(write)] diff --git a/mmgen/globalvars.py b/mmgen/globalvars.py index 62340b3d..18949f9c 100755 --- a/mmgen/globalvars.py +++ b/mmgen/globalvars.py @@ -34,13 +34,11 @@ debug = os.getenv('MMGEN_DEBUG') no_license = os.getenv('MMGEN_NOLICENSE') bogus_wallet_data = os.getenv('MMGEN_BOGUS_WALLET_DATA') disable_hold_protect = os.getenv('MMGEN_DISABLE_HOLD_PROTECT') +color = (False,True)[sys.stdout.isatty() and not os.getenv('MMGEN_DISABLE_COLOR')] -btc_amt_decimal_places = 8 - -from decimal import Decimal -tx_fee = Decimal('0.0003') -max_tx_fee = Decimal('0.01') -tx_fee_adj = Decimal('1.0') +from mmgen.obj import BTCAmt +tx_fee = BTCAmt('0.0003') +tx_fee_adj = 1.0 tx_confs = 3 seed_len = 256 @@ -58,7 +56,7 @@ version = '0.8.4' required_opts = [ 'quiet','verbose','debug','outdir','echo_passphrase','passwd_file', 'usr_randchars','stdout','show_hash_presets','label', - 'keep_passphrase','keep_hash_preset','brain_params' + 'keep_passphrase','keep_hash_preset','brain_params','b16' ] incompatible_opts = ( ('quiet','verbose'), @@ -66,46 +64,27 @@ incompatible_opts = ( ('tx_id', 'info'), ('tx_id', 'terse_info'), ) + min_screen_width = 80 -wallet_ext = 'mmdat' -seed_ext = 'mmseed' -mn_ext = 'mmwords' -brain_ext = 'mmbrain' -incog_ext = 'mmincog' -incog_hex_ext = 'mmincox' - -seedfile_exts = ( - wallet_ext, seed_ext, mn_ext, brain_ext, incog_ext, incog_hex_ext -) - -rawtx_ext = 'rawtx' -sigtx_ext = 'sigtx' -txid_ext = 'txid' -addrfile_ext = 'addrs' -addrfile_chksum_ext = 'chk' -keyfile_ext = 'keys' -keyaddrfile_ext = 'akeys' -mmenc_ext = 'mmenc' - -default_wordlist = 'electrum' -#default_wordlist = 'tirosh' - # Global value sets user opt -dfl_vars = 'seed_len','hash_preset','usr_randchars','debug','tx_fee','tx_confs','tx_fee_adj' - -seed_lens = 128,192,256 -mn_lens = [i / 32 * 3 for i in seed_lens] +dfl_vars = 'seed_len','hash_preset','usr_randchars','debug','tx_confs','tx_fee_adj','tx_fee' keyconv_exec = 'keyconv' mins_per_block = 9 passwd_max_tries = 5 -max_urandchars,min_urandchars = 80,10 +max_urandchars = 80 +_x = os.getenv('MMGEN_MIN_URANDCHARS') +min_urandchars = int(_x) if _x and int(_x) else 10 -salt_len = 16 -aesctr_iv_len = 16 +seed_lens = 128,192,256 +mn_lens = [i / 32 * 3 for i in seed_lens] + +mmenc_ext = 'mmenc' +salt_len = 16 +aesctr_iv_len = 16 hincog_chk_len = 8 hash_presets = { @@ -125,9 +104,8 @@ mmgen_idx_max_digits = 7 printable_nonl = [chr(i+32) for i in range(95)] printable = printable_nonl + ['\n','\t'] - addr_label_symbols = wallet_label_symbols = printable_nonl -max_addr_label_len = 32 +max_addr_label_len = 32 max_wallet_label_len = 48 -max_tx_comment_len = 72 # Comment is b58 encoded, so can permit all UTF-8 +max_tx_comment_len = 72 # Comment is b58 encoded, so can permit UTF-8 diff --git a/mmgen/main_addrgen.py b/mmgen/main_addrgen.py index dda02bdf..1e6bf4c1 100755 --- a/mmgen/main_addrgen.py +++ b/mmgen/main_addrgen.py @@ -101,35 +101,21 @@ if len(cmd_args) < nargs and not (opt.hidden_incog_input_params or opt.in_fmt): elif len(cmd_args) > nargs - int(bool(opt.hidden_incog_input_params)): opts.usage() -addrlist_arg = cmd_args.pop() -addr_idxs = parse_addr_idxs(addrlist_arg) -if not addr_idxs: - die(1,"'%s': invalid address list argument" % addrlist_arg) +addridxlist_str = cmd_args.pop() +idxs = AddrIdxList(fmt_str=addridxlist_str) do_license_msg() -opt.gen_what = 'a' if gen_what == 'addresses' \ - else 'k' if opt.no_addresses else 'ka' +ss = SeedSource(*cmd_args) # *(cmd_args[0] if cmd_args else []) -# Generate data: -ss = SeedSource(*cmd_args) +i = (gen_what=='addresses') or bool(opt.no_addresses)*2 +al = (KeyAddrList,AddrList,KeyList)[i](seed=ss.seed,addr_idxs=idxs) +al.format() -ainfo = generate_addrs(ss.seed.data,addr_idxs) +if al.gen_addrs and opt.print_checksum: + Die(0,al.checksum) -addrdata_str = ainfo.fmt_data() -outfile_base = '{}[{}]'.format(ss.seed.sid, ainfo.idxs_fmt) +if al.gen_keys and keypress_confirm('Encrypt key list?'): + al.encrypt() -if 'a' in opt.gen_what and opt.print_checksum: - Die(0,ainfo.checksum) - -if 'k' in opt.gen_what and keypress_confirm('Encrypt key list?'): - addrdata_str = mmgen_encrypt(addrdata_str,'new key list','') - enc_ext = '.' + g.mmenc_ext -else: enc_ext = '' - -ext = (g.keyfile_ext,g.keyaddrfile_ext)['ka' in opt.gen_what] -ext = (g.addrfile_ext,ext)['k' in opt.gen_what] -outfile = '%s.%s%s' % (outfile_base, ext, enc_ext) -ask_tty = 'k' in opt.gen_what and not opt.quiet -if gen_what == 'keys': gen_what = 'secret keys' -write_data_to_file(outfile,addrdata_str,gen_what,ask_tty=ask_tty) +al.write_to_file() diff --git a/mmgen/main_addrimport.py b/mmgen/main_addrimport.py index 527b063c..21d0a5be 100755 --- a/mmgen/main_addrimport.py +++ b/mmgen/main_addrimport.py @@ -23,7 +23,7 @@ mmgen-addrimport: Import addresses into a MMGen bitcoind tracking wallet import time from mmgen.common import * -from mmgen.addr import AddrInfo,AddrInfoEntry +from mmgen.addr import AddrList,KeyAddrList opts_data = { 'desc': """Import addresses (both {pnm} and non-{pnm}) into a bitcoind @@ -53,14 +53,9 @@ if len(cmd_args) == 1: if opt.addrlist: lines = get_lines_from_file( infile,'non-{pnm} addresses'.format(pnm=g.proj_name),trim_comments=True) - ai,adata = AddrInfo(),[] - for btcaddr in lines: - a = AddrInfoEntry() - a.idx,a.addr,a.comment = None,btcaddr,None - adata.append(a) - ai.initialize(None,adata) + ai = AddrList(addrlist=lines) else: - ai = AddrInfo(infile,has_keys=opt.keyaddr_file) + ai = (AddrList,KeyAddrList)[bool(opt.keyaddr_file)](infile) else: die(1,""" You must specify an {pnm} address file (or a list of non-{pnm} addresses @@ -69,7 +64,7 @@ with the '--addrlist' option) from mmgen.bitcoin import verify_addr qmsg_r('Validating addresses...') -for e in ai.addrdata: +for e in ai.data: if not verify_addr(e.addr,verbose=True): die(2,'%s: invalid address' % e.addr) @@ -114,13 +109,13 @@ else: msg_fmt = '\r%-{}s %-34s %s'.format(w_n_of_m, w_mmid) msg("Importing %s addresses from '%s'%s" % - (len(ai.addrdata),infile,('',' (batch mode)')[bool(opt.batch)])) + (len(ai.data),infile,('',' (batch mode)')[bool(opt.batch)])) arg_list = [] -for n,e in enumerate(ai.addrdata): +for n,e in enumerate(ai.data): if e.idx: label = '%s:%s' % (ai.seed_id,e.idx) - if e.comment: label += ' ' + e.comment + if e.label: label += ' ' + e.label else: label = 'non-{pnm}'.format(pnm=g.proj_name) if opt.batch: diff --git a/mmgen/main_txcreate.py b/mmgen/main_txcreate.py index 255f017a..5a2c6e8f 100755 --- a/mmgen/main_txcreate.py +++ b/mmgen/main_txcreate.py @@ -21,8 +21,6 @@ mmgen-txcreate: Create a Bitcoin transaction to and from MMGen- or non-MMGen inputs and outputs """ -from decimal import Decimal - from mmgen.common import * from mmgen.tx import * from mmgen.tw import * @@ -83,13 +81,12 @@ No data for {pnm} address {mmgenaddr} could be found in the tracking wallet. Please import this address into your tracking wallet or supply an address file for it on the command line. """.strip(), - 'mixed_inputs': """ -NOTE: This transaction uses a mixture of both {pnm} and non-{pnm} inputs, which -makes the signing process more complicated. When signing the transaction, keys -for the non-{pnm} inputs must be supplied to '{pnl}-txsign' in a file with the -'--keys-from-file' option. - -Selected mmgen inputs: %s + 'non_mmgen_inputs': """ +NOTE: This transaction includes non-{pnm} inputs, which makes the signing +process more complicated. When signing the transaction, keys for non-{pnm} +inputs must be supplied to '{pnl}-txsign' in a file with the '--keys-from-file' +option. +Selected non-{pnm} inputs: %s """.strip().format(pnm=pnm,pnl=pnm.lower()), 'not_enough_btc': """ Not enough BTC in the inputs for this transaction (%s BTC) @@ -100,7 +97,7 @@ was specified. """.strip(), } -def select_outputs(unspent,prompt): +def select_unspent(unspent,prompt): while True: reply = my_raw_input(prompt).strip() @@ -118,14 +115,14 @@ def select_outputs(unspent,prompt): return selected -def mmaddr2baddr(c,mmaddr,ail_w,ail_f): +def mmaddr2baddr(c,mmaddr,ad_w,ad_f): # assume mmaddr has already been checked - btc_addr = ail_w.mmaddr2btcaddr(mmaddr) + btc_addr = ad_w.mmaddr2btcaddr(mmaddr) if not btc_addr: - if ail_f: - btc_addr = ail_f.mmaddr2btcaddr(mmaddr) + if ad_f: + btc_addr = ad_f.mmaddr2btcaddr(mmaddr) if btc_addr: msg(wmsg['addr_in_addrfile_only'].format(mmgenaddr=mmaddr)) if not keypress_confirm('Continue anyway?'): @@ -135,16 +132,16 @@ def mmaddr2baddr(c,mmaddr,ail_w,ail_f): else: die(2,wmsg['addr_not_found_no_addrfile'].format(pnm=pnm,mmgenaddr=mmaddr)) - return btc_addr + return BTCAddr(btc_addr) def get_fee_estimate(): - if 'tx_fee' in opt.set_by_user: + if 'tx_fee' in opt.set_by_user: # TODO return None else: ret = c.estimatefee(opt.tx_confs) if ret != -1: - return ret + return BTCAmt(ret) else: m = """ Fee estimation failed! @@ -172,46 +169,41 @@ c = bitcoin_connection() if not opt.info: do_license_msg(immed=True) - addrfiles = [a for a in cmd_args if get_extension(a) == g.addrfile_ext] + from mmgen.addr import AddrList,AddrData + addrfiles = [a for a in cmd_args if get_extension(a) == AddrList.ext] cmd_args = set(cmd_args) - set(addrfiles) - from mmgen.addr import AddrInfo,AddrInfoList - ail_f = AddrInfoList() + ad_f = AddrData() for a in addrfiles: check_infile(a) - ail_f.add(AddrInfo(a)) + ad_f.add(AddrList(a)) - ail_w = AddrInfoList(bitcoind_connection=c) + ad_w = AddrData(source='tw') for a in cmd_args: if ',' in a: - a1,a2 = split2(a,',') + a1,a2 = a.split(',',1) if is_btc_addr(a1): - btc_addr = a1 - elif is_mmgen_addr(a1): - btc_addr = mmaddr2baddr(c,a1,ail_w,ail_f) + btc_addr = BTCAddr(a1) + elif is_mmgen_id(a1): + btc_addr = mmaddr2baddr(c,a1,ad_w,ad_f) else: die(2,"%s: unrecognized subargument in argument '%s'" % (a1,a)) - - btc_amt = convert_to_btc_amt(a2) - if btc_amt: - tx.add_output(btc_addr,btc_amt) - else: - die(2,"%s: invalid amount in argument '%s'" % (a2,a)) - elif is_mmgen_addr(a) or is_btc_addr(a): + tx.add_output(btc_addr,BTCAmt(a2)) + elif is_mmgen_id(a) or is_btc_addr(a): if tx.change_addr: die(2,'ERROR: More than one change address specified: %s, %s' % (change_addr, a)) - tx.change_addr = a if is_btc_addr(a) else mmaddr2baddr(c,a,ail_w,ail_f) - tx.add_output(tx.change_addr,Decimal('0')) + tx.change_addr = mmaddr2baddr(c,a,ad_w,ad_f) if is_mmgen_id(a) else BTCAddr(a) + tx.add_output(tx.change_addr,BTCAmt('0')) else: die(2,'%s: unrecognized argument' % a) if not tx.outputs: die(2,'At least one output must be specified on the command line') - if opt.tx_fee > g.max_tx_fee: - die(2,'Transaction fee too large: %s > %s' % (opt.tx_fee,g.max_tx_fee)) + if opt.tx_fee > tx.max_fee: + die(2,'Transaction fee too large: %s > %s' % (opt.tx_fee,tx.max_fee)) fee_estimate = get_fee_estimate() @@ -223,10 +215,10 @@ if opt.info: sys.exit() tx.send_amt = tx.sum_outputs() -msg('Total amount to spend: %s' % ('Unknown','%s BTC'%tx.send_amt,)[bool(tx.send_amt)]) +msg('Total amount to spend: %s' % ('Unknown','%s BTC'%tx.send_amt.hl())[bool(tx.send_amt)]) while True: - sel_nums = select_outputs(tw.unspent, + sel_nums = select_unspent(tw.unspent, 'Enter a range or space-separated list of outputs to spend: ') msg('Selected output%s: %s' % ( ('s','')[len(sel_nums)==1], @@ -234,31 +226,31 @@ while True: )) sel_unspent = [tw.unspent[i-1] for i in sel_nums] - mmaddrs = set([i['mmid'] for i in sel_unspent]) - - if '' in mmaddrs and len(mmaddrs) > 1: - mmaddrs.discard('') - msg(wmsg['mixed_inputs'] % ', '.join(sorted(mmaddrs))) + non_mmaddrs = [i for i in sel_unspent if i.mmid == None] + if non_mmaddrs: + msg(wmsg['non_mmgen_inputs'] % ', '.join(set(sorted([a.addr.hl() for a in non_mmaddrs])))) if not keypress_confirm('Accept?'): continue - tx.copy_inputs(sel_unspent) # makes tx.inputs + tx.copy_inputs_from_tw(sel_unspent) # makes tx.inputs tx.calculate_size_and_fee(fee_estimate) # sets tx.size, tx.fee change_amt = tx.sum_inputs() - tx.send_amt - tx.fee if change_amt >= 0: - prompt = 'Transaction produces %s BTC in change. OK?' % change_amt + prompt = 'Transaction produces %s BTC in change. OK?' % change_amt.hl() if keypress_confirm(prompt,default_yes=True): break else: msg(wmsg['not_enough_btc'] % change_amt) if change_amt > 0: + change_amt = BTCAmt(change_amt) if not tx.change_addr: die(2,wmsg['throwaway_change'] % change_amt) - tx.add_output(tx.change_addr,change_amt) + tx.del_output(tx.change_addr) + tx.add_output(BTCAddr(tx.change_addr),change_amt) elif tx.change_addr: msg('Warning: Change address will be unused as transaction produces no change') tx.del_output(tx.change_addr) @@ -270,7 +262,7 @@ dmsg('tx: %s' % tx) tx.add_comment() # edits an existing comment tx.create_raw(c) # creates tx.hex, tx.txid -tx.add_mmaddrs_to_outputs(ail_w,ail_f) +tx.add_mmaddrs_to_outputs(ad_w,ad_f) tx.add_timestamp() tx.add_blockcount(c) diff --git a/mmgen/main_txsign.py b/mmgen/main_txsign.py index 47cbb0f1..3bb197b0 100755 --- a/mmgen/main_txsign.py +++ b/mmgen/main_txsign.py @@ -21,8 +21,9 @@ mmgen-txsign: Sign a transaction generated by 'mmgen-txcreate' """ from mmgen.common import * +from mmgen.seed import * from mmgen.tx import * -from mmgen.seed import SeedSource +from mmgen.addr import * pnm = g.proj_name @@ -86,28 +87,29 @@ mappings are verified. Therefore, seed material or a key-address file for these addresses must be supplied on the command line. Seed data supplied in files must have the following extensions: - wallet: '.{g.wallet_ext}' - seed: '.{g.seed_ext}' - mnemonic: '.{g.mn_ext}' - brainwallet: '.{g.brain_ext}' + wallet: '.{w.ext}' + seed: '.{s.ext}' + mnemonic: '.{m.ext}' + brainwallet: '.{b.ext}' FMT CODES: {f} """.format( f='\n '.join(SeedSource.format_fmt_codes().splitlines()), - g=g,pnm=pnm,pnl=pnm.lower() + pnm=pnm,pnl=pnm.lower(), + w=Wallet,s=SeedFile,m=Mnemonic,b=Brainwallet ) } wmsg = { - 'mm2btc_mapping_error': """ + 'mapping_error': """ {pnm} -> BTC address mappings differ! -From %-18s %s -> %s -From %-18s %s -> %s -""".strip().format(pnm=pnm), - 'removed_dups': """ -Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file +%-23s %s -> %s +%-23s %s -> %s """.strip().format(pnm=pnm), + 'missing_keys_error': """ +A key file must be supplied for the following non-{pnm} address%s:\n %s +""".format(pnm=pnm).strip() } def get_seed_for_seed_id(seed_id,infiles,saved_seeds): @@ -121,30 +123,47 @@ def get_seed_for_seed_id(seed_id,infiles,saved_seeds): elif opt.in_fmt: qmsg('Need seed data for Seed ID %s' % seed_id) ss = SeedSource() - msg('User input produced Seed ID %s' % make_chksum_8(seed)) + msg('User input produced Seed ID %s' % ss.seed.sid) else: die(2,'ERROR: No seed source found for Seed ID: %s' % seed_id) - saved_seeds[ss.seed.sid] = ss.seed.data - - if ss.seed.sid == seed_id: return ss.seed.data + saved_seeds[ss.seed.sid] = ss.seed + if ss.seed.sid == seed_id: return ss.seed def generate_keys_for_mmgen_addrs(mmgen_addrs,infiles,saved_seeds): - seed_ids = set([i[:8] for i in mmgen_addrs]) vmsg('Need seed%s: %s' % (suf(seed_ids,'k'),' '.join(seed_ids))) d = [] - - from mmgen.addr import generate_addrs + from mmgen.addr import KeyAddrList for seed_id in seed_ids: # Returns only if seed is found seed = get_seed_for_seed_id(seed_id,infiles,saved_seeds) - addr_nums = [int(i[9:]) for i in mmgen_addrs if i[:8] == seed_id] - opt.gen_what = 'ka' - ai = generate_addrs(seed,addr_nums,source='txsign') - d += [('{}:{}'.format(seed_id,e.idx),e.addr,e.wif) for e in ai.addrdata] + addr_idxs = AddrIdxList(idx_list=[int(i[9:]) for i in mmgen_addrs if i[:8] == seed_id]) + d += KeyAddrList(seed=seed,addr_idxs=addr_idxs,do_chksum=False).flat_list() return d +def add_keys(tx,src,infiles=None,saved_seeds=None,keyaddr_list=None): + need_keys = [e for e in getattr(tx,src) if e.mmid and not e.have_wif] + if not need_keys: return [] + desc,m1 = ('key-address file','From key-address file:') if keyaddr_list else \ + ('seed(s)','Generated from seed:') + qmsg('Checking {} -> BTC address mappings for {} (from {})'.format(pnm,src,desc)) + d = keyaddr_list.flat_list() if keyaddr_list else \ + generate_keys_for_mmgen_addrs([e.mmid for e in need_keys],infiles,saved_seeds) + new_keys = [] + for e in need_keys: + for f in d: + if f.mmid == e.mmid: + if f.addr == e.addr: + e.have_wif = True + if src == 'inputs': + new_keys.append(f.wif) + else: + die(3,wmsg['mapping_error'] % (m1,f.mmid,f.addr,'tx file:',e.mmid,e.addr)) + if new_keys: + vmsg('Added %s wif key%s from %s' % (len(new_keys),suf(new_keys,'k'),desc)) + return new_keys + # # function unneeded - use bitcoin-cli walletdump instead # def sign_tx_with_bitcoind_wallet(c,tx,tx_num_str,keys): # ok = tx.sign(c,tx_num_str,keys) # returns false on failure @@ -171,86 +190,6 @@ def generate_keys_for_mmgen_addrs(mmgen_addrs,infiles,saved_seeds): # # return ok # -def missing_keys_errormsg(addrs): - Msg(""" -A key file must be supplied (or use the '--use-wallet-dat' option) -for the following non-{pnm} address{suf}:\n {l}""".format( - pnm=pnm, suf=suf(addrs,'a'), l='\n '.join(addrs)).strip()) - -def parse_mmgen_keyaddr_file(): - from mmgen.addr import AddrInfo - ai = AddrInfo(opt.mmgen_keys_from_file,has_keys=True) - vmsg('Found %s wif key%s for Seed ID %s' % - (ai.num_addrs, suf(ai.num_addrs,'k'), ai.seed_id)) - # idx: (0=addr, 1=comment 2=wif) -> mmaddr: (0=addr, 1=wif) - return dict( - [('%s:%s'%(ai.seed_id,e.idx), (e.addr,e.wif)) for e in ai.addrdata]) - -def parse_keylist(key_data): - fn = opt.keys_from_file - from mmgen.crypto import mmgen_decrypt_file_maybe - dec = mmgen_decrypt_file_maybe(fn,'non-{} keylist file'.format(pnm)) - # Key list could be bitcoind dump, so remove first space and everything following - keys_all = set([line.split()[0] for line in remove_comments(dec.splitlines())]) # DOS-safe - dmsg(repr(keys_all)) - ka_keys = [d[k][1] for k in key_data['kafile']] - keys = [k for k in keys_all if k not in ka_keys] - removed = len(keys_all) - len(keys) - if removed: - vmsg(wmsg['removed_dups'] % (removed,suf(removed,'k'))) - addrs = [] - wif2addr_f = get_wif2addr_f() - for n,k in enumerate(keys,1): - qmsg_r('\rGenerating addresses from keylist: %s/%s' % (n,len(keys))) - addrs.append(wif2addr_f(k)) - qmsg('\rGenerated addresses from keylist: %s/%s ' % (n,len(keys))) - - return dict(zip(addrs,keys)) - -# Check inputs and outputs maps against key-address file, deleting entries: -def check_maps_from_kafile(io_map,desc,kadata,return_keys=False): - if not kadata: return [] - qmsg('Checking {pnm} -> BTC address mappings for {w}s (from key-address file)'.format(pnm=pnm,w=desc)) - ret = [] - for k in io_map.keys(): - if k in kadata: - if kadata[k][0] == io_map[k]: - del io_map[k] - ret += [kadata[k][1]] - else: - kl,il = 'key-address file:','tx file:' - die(2,wmsg['mm2btc_mapping_error']%(kl,k,kadata[k][0],il,k,io_map[k])) - if ret: vmsg('Removed %s address%s from %ss map' % (len(ret),suf(ret,'a'),desc)) - if return_keys: - vmsg('Added %s wif key%s from %ss map' % (len(ret),suf(ret,'k'),desc)) - return ret - -# Check inputs and outputs maps against values generated from seeds -def check_maps_from_seeds(io_map,desc,infiles,saved_seeds,return_keys=False): - - if not io_map: return [] - qmsg('Checking {pnm} -> BTC address mappings for {w}s (from seed(s))'.format( - pnm=pnm,w=desc)) - d = generate_keys_for_mmgen_addrs(io_map.keys(),infiles,saved_seeds) -# 0=mmaddr 1=addr 2=wif - m = dict([(e[0],e[1]) for e in d]) - for a,b in zip(sorted(m),sorted(io_map)): - if a != b: - al,bl = 'generated seed:','tx file:' - die(3,wmsg['mm2btc_mapping_error'] % (al,a,m[a],bl,b,io_map[b])) - if return_keys: - vmsg('Added %s wif key%s from seeds' % (len(d),suf(d,'k'))) - return [e[2] for e in d] - -def get_keys_from_keylist(kldata,addrs): - ret = [] - for addr in addrs[:]: - if addr in kldata: - ret += [kldata[addr]] - addrs.remove(addr) - vmsg('Added %s wif key%s from user-supplied keylist' % - (len(ret),suf(ret,'k'))) - return ret # main(): execution begins here @@ -262,22 +201,27 @@ for i in infiles: check_infile(i) c = bitcoin_connection() saved_seeds = {} -tx_files = [i for i in infiles if get_extension(i) == g.rawtx_ext] -seed_files = [i for i in infiles if get_extension(i) in g.seedfile_exts] +tx_files = [i for i in infiles if get_extension(i) == MMGenTX.raw_ext] +seed_files = [i for i in infiles if get_extension(i) in SeedSource.get_extensions()] if not tx_files: die(1,'You must specify a raw transaction file!') -if not (seed_files or opt.mmgen_keys_from_file or opt.keys_from_file or opt.use_wallet_dat): +if not (seed_files or opt.mmgen_keys_from_file or opt.keys_from_file): # or opt.use_wallet_dat): die(1,'You must specify a seed or key source!') if not opt.info and not opt.terse_info: do_license_msg(immed=True) -key_data = { 'kafile':{}, 'klfile':{} } +kal,kl = None,None if opt.mmgen_keys_from_file: - key_data['kafile'] = parse_mmgen_keyaddr_file() or {} + kal = KeyAddrList(opt.mmgen_keys_from_file) + if opt.keys_from_file: - key_data['klfile'] = parse_keylist(key_data) or {} + l = get_lines_from_file(opt.keys_from_file,'key-address data',trim_comments=True) + kl = KeyAddrList(keylist=l) + if kal: kl.remove_dups(kal,key='wif') + kl.generate_addrs() +# pp_die(kl) tx_num_str = '' for tx_num,tx_file in enumerate(tx_files,1): @@ -301,26 +245,26 @@ for tx_num,tx_file in enumerate(tx_files,1): tx.view_with_prompt('View data for transaction%s?' % tx_num_str) # Start - other_addrs = list(set([i['address'] for i in tx.inputs if not i['mmid']])) + keys = [] + non_mm_addrs = tx.get_non_mmaddrs('inputs') + if non_mm_addrs: + tmp = KeyAddrList(addrlist=non_mm_addrs,do_chksum=False) + tmp.add_wifs(kl) + m = tmp.list_missing('wif') + if m: die(2,wmsg['missing_keys_error'] % (suf(m,'es'),'\n '.join(m))) + keys += tmp.get_wifs() - # should remove all elements from other_addrs - keys = get_keys_from_keylist(key_data['klfile'],other_addrs) + if opt.mmgen_keys_from_file: + keys += add_keys(tx,'inputs',keyaddr_list=kal) + add_keys(tx,'outputs',keyaddr_list=kal) - if other_addrs and not opt.use_wallet_dat: - missing_keys_errormsg(other_addrs) - sys.exit(2) + keys += add_keys(tx,'inputs',seed_files,saved_seeds) + add_keys(tx,'outputs',seed_files,saved_seeds) - imap = dict([(i['mmid'],i['address']) for i in tx.inputs if i['mmid']]) - omap = dict([(tx.outputs[k][1],k) for k in tx.outputs if len(tx.outputs[k]) > 1]) - sids = set([i[:8] for i in imap]) + tx.delete_attrs('inputs','have_wif') + tx.delete_attrs('outputs','have_wif') - keys += check_maps_from_kafile(imap,'input',key_data['kafile'],True) - check_maps_from_kafile(omap,'output',key_data['kafile']) - - keys += check_maps_from_seeds(imap,'input',seed_files,saved_seeds,True) - check_maps_from_seeds(omap,'output',seed_files,saved_seeds) - - extra_sids = set(saved_seeds) - sids + extra_sids = set(saved_seeds) - tx.get_input_sids() if extra_sids: msg('Unused Seed ID%s: %s' % (suf(extra_sids,'k'),' '.join(extra_sids))) diff --git a/mmgen/main_wallet.py b/mmgen/main_wallet.py index b7b96d5e..d049f819 100755 --- a/mmgen/main_wallet.py +++ b/mmgen/main_wallet.py @@ -24,6 +24,7 @@ import os,re from mmgen.common import * from mmgen.seed import SeedSource +from mmgen.obj import MMGenWalletLabel bn = os.path.basename(sys.argv[0]) invoked_as = re.sub(r'^wallet','',bn.split('-')[-1]) @@ -110,6 +111,9 @@ FMT CODES: cmd_args = opts.init(opts_data,opt_filter=opt_filter) +if opt.label: + opt.label = MMGenWalletLabel(opt.label,msg="Error in option '--label'") + if len(cmd_args) < nargs \ and not opt.hidden_incog_input_params and not opt.in_fmt: die(1,'An input file or input format must be specified') @@ -128,10 +132,12 @@ if invoked_as in ('conv','passchg'): msg(green('Processing input wallet')) ss_in = None if invoked_as == 'gen' \ else SeedSource(*cmd_args,passchg=invoked_as=='passchg') -if invoked_as == 'chk': - sys.exit() +if invoked_as == 'chk': sys.exit() if invoked_as in ('conv','passchg'): msg(green('Processing output wallet')) ss_out = SeedSource(ss=ss_in,passchg=invoked_as=='passchg') + +if invoked_as == 'gen': qmsg("This wallet's Seed ID: %s" % ss_out.seed.sid.hl()) + ss_out.write_to_file() diff --git a/mmgen/obj.py b/mmgen/obj.py index c2b29199..77a604a1 100755 --- a/mmgen/obj.py +++ b/mmgen/obj.py @@ -19,9 +19,8 @@ """ obj.py: The MMGenObject class and methods """ -import mmgen.globalvars as g -from decimal import Decimal +from decimal import * lvl = 0 class MMGenObject(object): @@ -40,11 +39,12 @@ class MMGenObject(object): def conv(v,col_w): vret = '' if type(v) in (str,unicode): + import mmgen.globalvars as g if not (set(list(v)) <= set(list(g.printable))): vret = repr(v) else: vret = fix_linebreaks(v,fixed_indent=0) - elif type(v) in (int,long,Decimal): + elif type(v) in (int,long,BTCAmt): vret = str(v) elif type(v) == dict: sep = '\n{}{}'.format(indent,' '*4) @@ -68,8 +68,8 @@ class MMGenObject(object): out = [] def f(k): return k[:2] != '__' - keys = filter(f, dir(self)) - col_w = max(len(k) for k in keys) + keys = filter(f, self.__dict__.keys()) + col_w = max(len(k) for k in keys) if keys else 1 fs = '{}%-{}s: %s'.format(indent,col_w) methods = [k for k in keys if repr(getattr(self,k))[:14] == ' cls.max_digits: + m = "'%s': too many digits in addr idx" % num + elif me < 1: + m = "'%s': addr idx cannot be less than one" % num + else: + return me + return cls.init_fail(m,on_fail) + +class AddrIdxList(list,InitErrors): + + max_len = 1000000 + + def __init__(self,fmt_str=None,idx_list=None,on_fail='die',sep=','): + self.arg_chk(type(self),on_fail) + assert fmt_str or idx_list + if idx_list: + return list.__init__(self,sorted(set(idx_list))) + elif fmt_str: + ret,fs = [],"'%s': value cannot be converted to addr idx" + from mmgen.util import msg + for i in (fmt_str.split(sep)): + j = i.split('-') + if len(j) == 1: + idx = AddrIdx(i,on_fail='return') + if not idx: break + ret.append(idx) + elif len(j) == 2: + beg = AddrIdx(j[0],on_fail='return') + if not beg: break + end = AddrIdx(j[1],on_fail='return') + if not beg: break + if end < beg: + msg(fs % "%s-%s (invalid range)" % (beg,end)); break + ret.extend([AddrIdx(x) for x in range(beg,end+1)]) + else: + msg((fs % i) + ' list'); break + else: + return list.__init__(self,sorted(set(ret))) # fell off end of loop - success + + return self.init_fail(fs % err,on_fail,silent=True) + +class Hilite(object): + + color = 'red' + color_always = False + width = 0 + trunc_ok = True + + @classmethod + def fmtc(cls,s,width=None,color=False,encl='',trunc_ok=None): + if width == None: width = cls.width + if trunc_ok == None: trunc_ok = cls.trunc_ok + assert width > 0 + assert type(encl) is str and len(encl) in (0,2) + a,b = list(encl) if encl else ('','') + if trunc_ok and len(s) > width: s = s[:width] + return cls.colorize((a+s+b).ljust(width),color=color) + + def fmt(self,width=None,color=False,encl='',trunc_ok=None): + if width == None: width = self.width + if trunc_ok == None: trunc_ok = self.trunc_ok + return self.fmtc(self,width=width,color=color,encl=encl,trunc_ok=trunc_ok) + + @classmethod + def hlc(cls,s,color=True): + return cls.colorize(s,color=color) + + def hl(self,color=True): + return self.colorize(self,color=color) + + def __str__(self): + return self.colorize(self,color=False) + + @classmethod + def colorize(cls,s,color=True): + import mmgen.globalvars as g + from mmgen.util import red,blue,green,yellow,pink,cyan,gray,orange,magenta + return locals()[cls.color](s) if (color or cls.color_always) and g.color else s + +class BTCAmt(Decimal,Hilite,InitErrors): + color = 'yellow' + max_prec = 8 + max_amt = 21000000 + + def __new__(cls,num,on_fail='die'): + cls.arg_chk(cls,on_fail) + try: + me = Decimal.__new__(cls,str(num)) + except: + m = "'%s': value cannot be converted to decimal" % num + else: + if me.normalize().as_tuple()[-1] < -cls.max_prec: + m = "'%s': too many decimal places in BTC amount" % num + elif me > cls.max_amt: + m = "'%s': BTC amount too large (>%s)" % (num,cls.max_amt) +# elif me.as_tuple()[0]: +# m = "'%s': BTC amount cannot be negative" % num + else: + return me + return cls.init_fail(m,on_fail) + + @classmethod + def fmtc(cls): + raise NotImplemented + + def fmt(self,fs='3.8',color=False,suf=''): + s = self.__str__(color=False) + if '.' in fs: + p1,p2 = [int(i) for i in fs.split('.',1)] + ss = s.split('.',1) + if len(ss) == 2: + a,b = ss + ret = a.rjust(p1) + '.' + (b+suf).ljust(p2+len(suf)) + else: + ret = s.rjust(p1) + suf + ' ' * (p2+1) + else: + ret = s.ljust(int(fs)) + return self.colorize(ret,color=color) + + def hl(self,color=True): + return self.__str__(color=color) + + def __str__(self,color=False): # format simply, no exponential notation + if int(self) == self: + ret = str(int(self)) + else: + ret = self.normalize().__format__('f') + return self.colorize(ret,color=color) + + def __repr__(self): + return "{}('{}')".format(type(self).__name__,self.__str__()) + + def __add__(self,other,context=None): + return type(self)(Decimal.__add__(self,other,context)) + __radd__ = __add__ + + def __sub__(self,other,context=None): + return type(self)(Decimal.__sub__(self,other,context)) + + def __mul__(self,other,context=None): + return type(self)('{:0.8f}'.format(Decimal.__mul__(self,Decimal(other),context))) + + def __div__(self,other,context=None): + return type(self)('{:0.8f}'.format(Decimal.__div__(self,Decimal(other),context))) + + def __neg__(self,other,context=None): + return type(self)(Decimal.__neg__(self,other,context)) + + +class BTCAddr(str,Hilite,InitErrors): + color = 'cyan' + width = 34 + def __new__(cls,s,on_fail='die'): + cls.arg_chk(cls,on_fail) + me = str.__new__(cls,s) + from mmgen.bitcoin import verify_addr + if verify_addr(s): + return me + else: + m = "'%s': value is not a Bitcoin address" % s + return cls.init_fail(m,on_fail) + + def fmt(self,width=width,color=False): + return self.fmtc(self,width=width,color=color) + + @classmethod + def fmtc(cls,s,width=width,color=False): + if width >= len(s): + s = s.ljust(width) + else: + s = s[:width-2] + '..' + return cls.colorize(s,color=color) + +class SeedID(str,Hilite,InitErrors): + color = 'blue' + width = 8 + trunc_ok = False + def __new__(cls,seed=None,sid=None,on_fail='die'): + cls.arg_chk(cls,on_fail) + assert seed or sid + if seed: + from mmgen.seed import Seed + from mmgen.util import make_chksum_8 + assert type(seed) == Seed + return str.__new__(cls,make_chksum_8(seed.get_data())) + elif sid: + from string import hexdigits + assert len(sid) == cls.width and set(sid) <= set(hexdigits.upper()) + return str.__new__(cls,sid) + m = "'%s': value cannot be converted to SeedID" % s + return cls.init_fail(m,on_fail) + +class MMGenID(str,Hilite,InitErrors): + + color = 'orange' + width = 0 + trunc_ok = False + + 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='return') + if sid: + idx = AddrIdx(b,on_fail='return') + if idx: + return str.__new__(cls,'%s:%s' % (sid,idx)) + + m = "'%s': value cannot be converted to MMGenID" % s + return cls.init_fail(m,on_fail) + +class MMGenLabel(unicode,Hilite,InitErrors): + + color = 'pink' + allowed = u'' + max_len = 0 + desc = 'label' + + def __new__(cls,s,on_fail='die',msg=None): + cls.arg_chk(cls,on_fail) + try: + s = s.decode('utf8').strip() + except: + m = "'%s: value is not a valid UTF-8 string" % s + else: + if len(s) > cls.max_len: + m = '%s too long (>%s symbols)' % (cls.desc.capitalize(),cls.max_len) + elif cls.allowed and not set(list(s)).issubset(set(list(cls.allowed))): + m = '%s contains non-permitted symbols: %s' % (cls.desc.capitalize(), + ' '.join(set(list(s)) - set(list(cls.allowed)))) + else: + return unicode.__new__(cls,s) + return cls.init_fail((msg+'\n' if msg else '') + m,on_fail) + +class MMGenWalletLabel(MMGenLabel): + max_len = 48 + allowed = [chr(i+32) for i in range(95)] + desc = 'wallet label' + +class MMGenAddrLabel(MMGenLabel): + max_len = 32 + desc = 'address label' + +class MMGenTXLabel(MMGenLabel): + max_len = 72 + desc = 'transaction label' diff --git a/mmgen/opts.py b/mmgen/opts.py index 39a43877..5109572d 100755 --- a/mmgen/opts.py +++ b/mmgen/opts.py @@ -230,14 +230,10 @@ def check_opts(usr_opts): # Returns false if any check fails if key == 'outdir': check_outdir(val) # exits on error - elif key == 'label': - if not is_mmgen_wallet_label(val): - msg("Illegal value for option '%s': '%s'" % (fmt_opt(key),val)) - return False - # NEW +# # NEW elif key in ('in_fmt','out_fmt'): from mmgen.seed import SeedSource,IncogWallet,Brainwallet,IncogWalletHidden - sstype = SeedSource.fmt_code_to_sstype(val) + sstype = SeedSource.fmt_code_to_type(val) if not sstype: return opt_unrecognized(key,val,'format code') if key == 'out_fmt': diff --git a/mmgen/rpc.py b/mmgen/rpc.py index fdd8248b..7f473103 100755 --- a/mmgen/rpc.py +++ b/mmgen/rpc.py @@ -78,17 +78,17 @@ class BitcoinRPCConnection(object): dmsg('=== rpc.py debug ===') dmsg(' RPC POST data ==> %s\n' % p) - from decimal import Decimal - class JSONDecEncoder(json.JSONEncoder): + from mmgen.obj import BTCAmt + class MyJSONEncoder(json.JSONEncoder): def default(self, obj): - if isinstance(obj, Decimal): + if isinstance(obj, BTCAmt): return str(obj) return json.JSONEncoder.default(self, obj) -# pp_msg(json.dumps(p,cls=JSONDecEncoder)) +# pp_msg(json.dumps(p,cls=MyJSONEncoder)) try: - c.request('POST', '/', json.dumps(p,cls=JSONDecEncoder), { + c.request('POST', '/', json.dumps(p,cls=MyJSONEncoder), { 'Host': self.host, 'Authorization': 'Basic ' + base64.b64encode(self.auth_str) }) @@ -112,7 +112,8 @@ class BitcoinRPCConnection(object): if not r2: return die_maybe(r,2,'Error: empty reply') - r3 = json.loads(r2.decode('utf8'), parse_float=decimal.Decimal) + from decimal import Decimal + r3 = json.loads(r2.decode('utf8'), parse_float=Decimal) ret = [] for resp in r3 if cf['batch'] else [r3]: diff --git a/mmgen/seed.py b/mmgen/seed.py index 740e9030..065b183d 100755 --- a/mmgen/seed.py +++ b/mmgen/seed.py @@ -37,7 +37,6 @@ def check_usr_seed_len(seed_len): "doesn't match seed length of source (%s)" die(1, m % (opt.seed_len,seed_len)) - class Seed(MMGenObject): def __init__(self,seed_bin=None): if not seed_bin: @@ -48,9 +47,12 @@ class Seed(MMGenObject): self.data = seed_bin self.hexdata = hexlify(seed_bin) - self.sid = make_chksum_8(seed_bin) + self.sid = SeedID(seed=self) self.length = len(seed_bin) * 8 + def get_data(self): + return self.data + class SeedSource(MMGenObject): @@ -64,21 +66,17 @@ class SeedSource(MMGenObject): class SeedSourceData(MMGenObject): pass - def __new__(cls,fn=None,ss=None,seed=None, - ignore_in_fmt=False,passchg=False): + def __new__(cls,fn=None,ss=None,seed=None,ignore_in_fmt=False,passchg=False): def die_on_opt_mismatch(opt,sstype): - opt_sstype = cls.fmt_code_to_sstype(opt) + opt_sstype = cls.fmt_code_to_type(opt) compare_or_die( opt_sstype.__name__, 'input format requested on command line', sstype.__name__, 'input file format' ) if ss: - if passchg: - sstype = ss.__class__ - else: - sstype = cls.fmt_code_to_sstype(opt.out_fmt) + sstype = ss.__class__ if passchg else cls.fmt_code_to_type(opt.out_fmt) me = super(cls,cls).__new__(sstype or Wallet) # default: Wallet me.seed = ss.seed me.ss_in = ss @@ -86,32 +84,28 @@ class SeedSource(MMGenObject): elif fn or opt.hidden_incog_input_params: if fn: f = Filename(fn) - sstype = cls.ext_to_sstype(f.ext) else: fn = opt.hidden_incog_input_params.split(',')[0] - f = Filename(fn,ftype='hincog') - sstype = cls.fmt_code_to_sstype('hincog') - + f = Filename(fn,ftype=IncogWalletHidden) if opt.in_fmt and not ignore_in_fmt: - die_on_opt_mismatch(opt.in_fmt,sstype) - - me = super(cls,cls).__new__(sstype) + die_on_opt_mismatch(opt.in_fmt,f.ftype) + me = super(cls,cls).__new__(f.ftype) me.infile = f me.op = ('old','pwchg_old')[bool(passchg)] elif opt.in_fmt: # Input format - sstype = cls.fmt_code_to_sstype(opt.in_fmt) + sstype = cls.fmt_code_to_type(opt.in_fmt) me = super(cls,cls).__new__(sstype) me.op = ('old','pwchg_old')[bool(passchg)] else: # Called with no inputs - initialize with random seed - sstype = cls.fmt_code_to_sstype(opt.out_fmt) + sstype = cls.fmt_code_to_type(opt.out_fmt) me = super(cls,cls).__new__(sstype or Wallet) # default: Wallet me.seed = Seed(seed_bin=seed or None) me.op = 'new' +# die(1,me.seed.sid.hl()) # DEBUG return me - def __init__(self,fn=None,ss=None,seed=None, - ignore_in_fmt=False,passchg=False): + def __init__(self,fn=None,ss=None,seed=None,ignore_in_fmt=False,passchg=False): self.ssdata = self.SeedSourceData() self.msg = {} @@ -135,7 +129,7 @@ class SeedSource(MMGenObject): self._decrypt_retry() m = ('',', seed length %s' % self.seed.length)[self.seed.length!=256] - qmsg('Valid %s for Seed ID %s%s' % (self.desc,self.seed.sid,m)) + qmsg('Valid %s for Seed ID %s%s' % (self.desc,self.seed.sid.hl(),m)) def _get_data(self): if hasattr(self,'infile'): @@ -162,36 +156,36 @@ class SeedSource(MMGenObject): die(2,'Passphrase from password file, so exiting') msg('Trying again...') - subclasses = [] + @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 @classmethod - def _get_subclasses(cls): - - if cls.subclasses: return cls.subclasses - - ret,gl = [],globals() - for c in [gl[k] for k in gl]: - try: - if issubclass(c,cls): - ret.append(c) - except: - pass - - cls.subclasses = ret - return ret + def get_subclasses_str(cls): + def GetSubclassesTree(cls): + return ''.join([c.__name__ +' '+ GetSubclassesTree(c) for c in cls.__subclasses__()]) + return GetSubclassesTree(cls) @classmethod - def fmt_code_to_sstype(cls,fmt_code): + def get_extensions(cls): + return [s.ext for s in cls.get_subclasses() if hasattr(s,'ext')] + + @classmethod + def fmt_code_to_type(cls,fmt_code): if not fmt_code: return None - for c in cls._get_subclasses(): + for c in cls.get_subclasses(): if hasattr(c,'fmt_codes') and fmt_code in c.fmt_codes: return c return None @classmethod - def ext_to_sstype(cls,ext): + def ext_to_type(cls,ext): if not ext: return None - for c in cls._get_subclasses(): + for c in cls.get_subclasses(): if hasattr(c,'ext') and ext == c.ext: return c return None @@ -199,7 +193,7 @@ class SeedSource(MMGenObject): @classmethod def format_fmt_codes(cls): d = [(c.__name__,('.'+c.ext if c.ext else c.ext),','.join(c.fmt_codes)) - for c in cls._get_subclasses() + for c in cls.get_subclasses() if hasattr(c,'fmt_codes')] w = max([len(a) for a,b,c in d]) ret = ['{:<{w}} {:<9} {}'.format(a,b,c,w=w) for a,b,c in [ @@ -365,6 +359,8 @@ class Mnemonic (SeedSourceUnenc): } mn_base = 1626 wordlists = sorted(wl_checksums) + dfl_wordlist = 'electrum' + # dfl_wordlist = 'tirosh' @staticmethod def _mn2hex_pad(mn): return len(mn) * 8 / 3 @@ -399,7 +395,7 @@ class Mnemonic (SeedSourceUnenc): @classmethod def get_wordlist(cls,wordlist=None): - wordlist = wordlist or g.default_wordlist + wordlist = wordlist or cls.dfl_wordlist if wordlist not in cls.wordlists: die(1,"'%s': invalid wordlist. Valid choices: '%s'" % (wordlist,"' '".join(cls.wordlists))) @@ -536,28 +532,31 @@ class Wallet (SeedSourceEnc): ext = 'mmdat' def _get_label_from_user(self,old_lbl=''): - d = ("to reuse the label '%s'" % old_lbl) if old_lbl else 'for no label' + d = ("to reuse the label '%s'" % old_lbl.hl()) if old_lbl else 'for no label' p = 'Enter a wallet label, or hit ENTER %s: ' % d while True: - ret = my_raw_input(p) + msg_r(p) + ret = my_raw_input('') if ret: - if is_mmgen_wallet_label(ret): - self.ssdata.label = ret; return ret + self.ssdata.label = MMGenWalletLabel(ret,on_fail='return') + if self.ssdata.label: + break else: msg('Invalid label. Trying again...') else: - ret = old_lbl or 'No Label' - self.ssdata.label = ret; return ret + self.ssdata.label = old_lbl or MMGenWalletLabel('No Label') + break + return self.ssdata.label # nearly identical to _get_hash_preset() - factor? def _get_label(self): if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'label'): old_lbl = self.ss_in.ssdata.label if opt.keep_label: - qmsg("Reusing label '%s' at user request" % old_lbl) + qmsg("Reusing label '%s' at user request" % old_lbl.hl()) self.ssdata.label = old_lbl elif opt.label: - qmsg("Using label '%s' requested on command line" % opt.label) + qmsg("Using label '%s' requested on command line" % opt.label.hl()) lbl = self.ssdata.label = opt.label else: # Prompt, using old value as default lbl = self._get_label_from_user(old_lbl) @@ -566,7 +565,7 @@ class Wallet (SeedSourceEnc): m = ("changed to '%s'" % lbl,'unchanged')[lbl==old_lbl] qmsg('Label %s' % m) elif opt.label: - qmsg("Using label '%s' requested on command line" % opt.label) + qmsg("Using label '%s' requested on command line" % opt.label.hl()) self.ssdata.label = opt.label else: self._get_label_from_user() @@ -619,7 +618,7 @@ class Wallet (SeedSourceEnc): if not check_master_chksum(lines,self.desc): return False d = self.ssdata - d.label = lines[1] + d.label = MMGenWalletLabel(lines[1]) d1,d2,d3,d4,d5 = lines[2].split() d.seed_id = d1.upper() @@ -994,7 +993,7 @@ harder to find, you're advised to choose a much larger file size than this. else: die(1,'Exiting at user request') - self.outfile = f = Filename(fn,ftype=self.fmt_codes[0],write=True) + f = Filename(fn,ftype=type(self),write=True) dmsg('%s data len %s, offset %s' % ( capfirst(self.desc),d.target_data_len,d.hincog_offset)) diff --git a/mmgen/tool.py b/mmgen/tool.py index fd30f632..8991178d 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -162,10 +162,11 @@ def tool_usage(prog_name, command): for line in cmd_help.split('\n'): if ' ' + command in line: c,h = line.split('-',1) - Msg('{}: {}'.format(c.strip(),h.strip())) - Msg('USAGE: %s %s %s' % (prog_name, command, ' '.join(cmd_data[command]))) + Msg('MMGEN-TOOL {}: {}'.format(c.strip().upper(),h.strip())) + msg('USAGE: %s %s %s' % (prog_name, command, ' '.join(cmd_data[command]))) else: - Msg("'%s': no such tool command" % command) + msg("'%s': no such tool command" % command) + sys.exit(1) def process_args(prog_name, command, cmd_args): c_args = [[i.split(' [')[0],i.split(' [')[1][:-1]] @@ -174,61 +175,53 @@ def process_args(prog_name, command, cmd_args): i.split(' [')[0], [i.split(' [')[1].split('=')[0], i.split(' [')[1].split('=')[1][:-1]] ] for i in cmd_data[command] if '=' in i]) - - u_args = cmd_args[:len(c_args)] - u_kwargs = cmd_args[len(c_args):] + u_args = [a for a in cmd_args[:len(c_args)]] if len(u_args) < len(c_args): - msg('%s argument%s required' % (len(c_args),suf(c_args,'k'))) - tool_usage(prog_name, command) - sys.exit(1) + msg('Command requires exactly %s non-keyword argument%s' % (len(c_args),suf(c_args,'k'))) + tool_usage(prog_name,command) - if len(u_kwargs) > len(c_kwargs): - msg('Too many arguments') - tool_usage(prog_name, command) - sys.exit(1) + extra_args = len(cmd_args) - len(c_args) + u_kwargs = {} + if extra_args > 0: + u_kwargs = dict([a.split('=') for a in cmd_args[len(c_args):] if '=' in a]) + if len(u_kwargs) != extra_args: + msg('Command requires exactly %s non-keyword argument%s' + % (len(c_args),suf(c_args,'k'))) + tool_usage(prog_name,command) + if len(u_kwargs) > len(c_kwargs): + msg('Command requires exactly %s keyword argument%s' + % (len(c_kwargs),suf(c_kwargs,'k'))) + tool_usage(prog_name,command) - u_kwargs = dict([a.split('=') for a in u_kwargs]) +# mdie(c_args,c_kwargs,u_args,u_kwargs) -# print c_args; print c_kwargs; print u_args; print u_kwargs; sys.exit() + for k in u_kwargs: + if k not in c_kwargs: + msg("'%s': invalid keyword argument" % k) + tool_usage(prog_name,command) - if set(u_kwargs) > set(c_kwargs): - die(1,'Invalid named argument') - - def convert_type(arg,arg_name,arg_type): + def conv_type(arg,arg_name,arg_type): + if arg_type == 'bool': + if arg.lower() in ('true','yes','1','on'): arg = True + elif arg.lower() in ('false','no','0','off'): arg = False + else: + msg("'%s': invalid boolean value for keyword argument" % arg) + tool_usage(prog_name,command) try: return __builtins__[arg_type](arg) except: die(1,"'%s': Invalid argument for argument %s ('%s' required)" % \ (arg, arg_name, arg_type)) - def convert_to_bool_maybe(arg, arg_type): - if arg_type == 'bool': - if arg.lower() in ('true','yes','1','on'): return True - if arg.lower() in ('false','no','0','off'): return False - return arg - - args = [] - for i in range(len(c_args)): - arg_type = c_args[i][1] - arg = convert_to_bool_maybe(u_args[i], arg_type) - args.append(convert_type(arg,c_args[i][0],arg_type)) - - kwargs = {} - for k in u_kwargs: - arg_type = c_kwargs[k][0] - arg = convert_to_bool_maybe(u_kwargs[k], arg_type) - kwargs[k] = convert_type(arg,k,arg_type) + args = [conv_type(u_args[i],c_args[i][0],c_args[i][1]) for i in range(len(c_args))] + kwargs = dict([(k,conv_type(u_kwargs[k],k,c_kwargs[k][0])) for k in u_kwargs]) +# mdie(args,kwargs) return args,kwargs # Individual cmd_data -# def help(): -# Msg('Available commands:') -# for k in sorted(cmd_data.keys()): -# Msg('%-16s %s' % (k,' '.join(cmd_data[k]))) - def are_equal(a,b,dtype=''): if dtype == 'str': return a.lstrip('\0') == b.lstrip('\0') if dtype == 'hex': return a.lstrip('0') == b.lstrip('0') @@ -387,19 +380,19 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa c = bitcoin_connection() addrs = {} # reusing variable name! - from decimal import Decimal - total = Decimal('0') + from mmgen.obj import BTCAmt + total = BTCAmt('0') for d in c.listunspent(0): mmaddr,comment = split2(d['account']) if usr_addr_list and (mmaddr not in usr_addr_list): continue - if is_mmgen_addr(mmaddr) and d['confirmations'] >= minconf: + if is_mmgen_id(mmaddr) and d['confirmations'] >= minconf: key = mmaddr.replace(':','_') if key in addrs: if addrs[key][2] != d['address']: die(2,'duplicate BTC address ({}) for this MMGen address! ({})'.format( (d['address'], addrs[key][2]))) else: - addrs[key] = [Decimal('0'),comment,d['address']] + addrs[key] = [BTCAmt('0'),comment,d['address']] addrs[key][0] += d['amount'] total += d['amount'] @@ -410,11 +403,11 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa for acct in accts: mmaddr,comment = split2(acct) if usr_addr_list and (mmaddr not in usr_addr_list): continue - if is_mmgen_addr(mmaddr): + if is_mmgen_id(mmaddr): key = mmaddr.replace(':','_') if key not in addrs: if showbtcaddrs: save_a.append([acct]) - addrs[key] = [Decimal('0'),comment,''] + addrs[key] = [BTCAmt('0'),comment,''] for acct,addr in zip(save_a,c.getaddressesbyaccount(save_a,batch=True)): if len(addr) != 1: @@ -431,17 +424,17 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa max(max(len(addrs[k][1]) for k in addrs) + 1,8) # pad 8 if no comments ) - def s_mmgen(key): + def s_mmgen(key): # TODO return '{}:{:>0{w}}'.format(w=g.mmgen_idx_max_digits, *key.split('_')) out = [] for k in sorted(addrs,key=s_mmgen): if out and k.split('_')[0] != out[-1].split(':')[0]: out.append('') baddr = ' ' + addrs[k][2] if showbtcaddrs else '' - out.append(fs % (k.replace('_',':'), baddr, addrs[k][1], normalize_btc_amt(addrs[k][0]))) + out.append(fs % (k.replace('_',':'), baddr, addrs[k][1], addrs[k][0].fmt('3.0',color=1))) o = (fs + '\n%s\nTOTAL: %s BTC') % ( - 'ADDRESS','','COMMENT','BALANCE', '\n'.join(out), normalize_btc_amt(total) + 'ADDRESS','','COMMENT',' BALANCE', '\n'.join(out), total.hl() ) if pager: do_pager(o) else: Msg(o) @@ -456,21 +449,21 @@ def getbalance(minconf=1): ma = split2(d['account'])[0] keys = ['TOTAL'] if d['spendable']: keys += ['SPENDABLE'] - if is_mmgen_addr(ma): keys += [ma.split(':')[0]] + if is_mmgen_id(ma): keys += [ma.split(':')[0]] confs = d['confirmations'] i = (1,2)[confs >= minconf] for key in keys: - if key not in accts: accts[key] = [Decimal('0')] * 3 + if key not in accts: accts[key] = [BTCAmt('0')] * 3 for j in ([],[0])[confs==0] + [i]: accts[key][j] += d['amount'] - fs = '{:12} {:<%s} {:<%s} {:<}' % (16,16) + fs = '{:13} {} {} {}' mc,lbl = str(minconf),'confirms' - Msg(fs.format('Wallet','Unconfirmed','<%s %s'%(mc,lbl),'>=%s %s'%(mc,lbl))) + Msg(fs.format('Wallet', + *[s.ljust(16) for s in ' Unconfirmed',' <%s %s'%(mc,lbl),' >=%s %s'%(mc,lbl)])) for key in sorted(accts.keys()): - line = [str(normalize_btc_amt(a))+' BTC' for a in accts[key]] - Msg(fs.format(key+':', *line)) + Msg(fs.format(key+':', *[a.fmt(color=True,suf=' BTC') for a in accts[key]])) def txview(infile,pager=False,terse=False): c = bitcoin_connection() @@ -481,23 +474,22 @@ def twview(pager=False,reverse=False,wide=False,sort='age'): from mmgen.tw import MMGenTrackingWallet tw = MMGenTrackingWallet() tw.do_sort(sort,reverse=reverse) - out = tw.format(wide=wide) + out = tw.format_for_printing(color=True) if wide else tw.format_for_display() do_pager(out) if pager else sys.stdout.write(out) def add_label(mmaddr,label,remove=False): - if not is_mmgen_addr(mmaddr): + if not is_mmgen_id(mmaddr): die(1,'{a}: not a valid {pnm} address'.format(pnm=pnm,a=mmaddr)) - check_addr_label(label) # Exits on failure + MMGenAddrLabel(label) # Exits on failure - c = bitcoin_connection() - - from mmgen.addr import AddrInfoList - btcaddr = AddrInfoList(bitcoind_connection=c).mmaddr2btcaddr(mmaddr) + from mmgen.addr import AddrData + btcaddr = AddrData(source='tw').mmaddr2btcaddr(mmaddr) if not btcaddr: die(1,'{pnm} address {a} not found in tracking wallet'.format( pnm=pnm,a=mmaddr)) + c = bitcoin_connection() try: l = ' ' + label if label else '' c.importaddress(btcaddr,mmaddr+l,False) # addr,label,rescan,p2sh @@ -511,12 +503,12 @@ def add_label(mmaddr,label,remove=False): def remove_label(mmaddr): add_label(mmaddr,'',remove=True) def addrfile_chksum(infile): - from mmgen.addr import AddrInfo - AddrInfo(infile,caller='tool') + from mmgen.addr import AddrList + AddrList(infile,chksum_only=True) def keyaddrfile_chksum(infile): - from mmgen.addr import AddrInfo - AddrInfo(infile,has_keys=True,caller='tool') + from mmgen.addr import KeyAddrList + KeyAddrList(infile,chksum_only=True) def hexreverse(hex_str): Msg(ba.hexlify(decode_pretty_hexdump(hex_str)[::-1])) diff --git a/mmgen/tw.py b/mmgen/tw.py index 63199ec3..2fd8d8b2 100755 --- a/mmgen/tw.py +++ b/mmgen/tw.py @@ -22,28 +22,70 @@ tw: Tracking wallet methods for the MMGen suite from mmgen.common import * from mmgen.obj import * -from mmgen.tx import parse_mmgen_label,normalize_btc_amt from mmgen.term import get_char -class MMGenTrackingWallet(MMGenObject): +def parse_tw_acct_label(s): + ret = s.split(None,1) + if ret and MMGenID(ret[0],on_fail='silent'): + if len(ret) == 2: + return tuple(ret) + else: + return ret[0],None + else: + return None,None +class MMGenTWOutput(MMGenListItem): + attrs_reassign = 'label','skip' + attrs = 'txid','vout','amt','label','mmid','addr','confs','scriptPubKey','days','skip' + label = MMGenListItemAttr('label','MMGenAddrLabel') + +class MMGenTrackingWallet(MMGenObject): wmsg = { 'no_spendable_outputs': """ No spendable outputs found! Import addresses with balances into your watch-only wallet using '{}-addrimport' and then re-run this program. """.strip().format(g.proj_name) } + def __init__(self): + if g.bogus_wallet_data: # for debugging purposes only + us_rpc = eval(get_data_from_file(g.bogus_wallet_data)) + else: + us_rpc = bitcoin_connection().listunspent() +# write_data_to_file('bogus_unspent.json', repr(us), 'bogus unspent data') +# sys.exit() - sort_keys = 'address','age','amount','txid','mmaddr' - def s_address(self,i): return i['address'] - def s_age(self,i): return 0 - i['confirmations'] - def s_amount(self,i): return i['amount'] - def s_txid(self,i): return '%s %03s' % (i['txid'],i['vout']) - def s_mmaddr(self,i): - if i['mmid']: + if not us_rpc: die(2,self.wmsg['no_spendable_outputs']) + for o in us_rpc: + o['mmid'],o['label'] = parse_tw_acct_label(o['account']) + o['days'] = int(o['confirmations'] * g.mins_per_block / (60*24)) + o['amt'] = o['amount'] # TODO + o['addr'] = o['address'] + o['confs'] = o['confirmations'] + us = [MMGenTWOutput(**dict([(k,v) for k,v in o.items() if k in MMGenTWOutput.attrs and o[k] not in (None,'')])) for o in us_rpc] +# die(1,''.join([str(i)+'\n' for i in us])) +# die(1,''.join([pp_format(i)+'\n' for i in us_rpc])) + + self.unspent = us + self.fmt_display = '' + self.fmt_print = '' + self.cols = None + self.reverse = False + self.group = False + self.show_days = True + self.show_mmid = True + self.do_sort('age') + self.total = sum([i.amt for i in self.unspent]) + + sort_keys = 'addr','age','amt','txid','mmid' + def s_addr(self,i): return i.addr + def s_age(self,i): return 0 - i.confs + def s_amt(self,i): return i.amt + def s_txid(self,i): return '%s %03s' % (i.txid,i.vout) + def s_mmid(self,i): + if i.mmid: return '{}:{:>0{w}}'.format( - *i['mmid'].split(':'), w=g.mmgen_idx_max_digits) - else: return 'G' + i['comment'] + *i.mmid.split(':'), w=g.mmgen_idx_max_digits) + else: return 'G' + (i.label or '') def do_sort(self,key,reverse=None): if key not in self.sort_keys: @@ -54,115 +96,93 @@ watch-only wallet using '{}-addrimport' and then re-run this program. self.unspent.sort(key=getattr(self,'s_'+key),reverse=reverse) def sort_info(self,include_group=True): - ret = ([],['reverse'])[self.reverse] - ret.append(self.sort) - if include_group and self.group and (self.sort in ('address','txid')): - ret.append('grouped') + ret = ([],['Reverse'])[self.reverse] + ret.append(self.sort.capitalize().replace('Mmid','MMGenId')) + if include_group and self.group and (self.sort in ('addr','txid','mmid')): + ret.append('Grouped') return ret - def __init__(self): - if g.bogus_wallet_data: # for debugging purposes only - us = eval(get_data_from_file(g.bogus_wallet_data)) - else: - us = bitcoin_connection().listunspent() -# write_data_to_file('bogus_unspent.json', repr(us), 'bogus unspent data') -# sys.exit() - - if not us: die(2,self.wmsg['no_spendable_outputs']) - for o in us: - o['mmid'],o['comment'] = parse_mmgen_label(o['account']) - del o['account'] - o['skip'] = '' - amt = str(normalize_btc_amt(o['amount'])) - lfill = 3 - len(amt.split('.')[0]) if '.' in amt else 3 - len(amt) - o['amt_fmt'] = ' '*lfill + amt - o['days'] = int(o['confirmations'] * g.mins_per_block / (60*24)) - - self.unspent = us - self.fmt_display = '' - self.fmt_print = '' - self.cols = None - self.reverse = False - self.group = False - self.show_days = True - self.show_mmaddr = True - self.do_sort('age') - self.total = sum([i['amount'] for i in self.unspent]) - - def set_cols(self): + def set_term_columns(self): from mmgen.term import get_terminal_size - self.cols = get_terminal_size()[0] - if self.cols < g.min_screen_width: - m = 'A screen at least {} characters wide is required to display the tracking wallet' - die(2,m.format(g.min_screen_width)) + while True: + self.cols = get_terminal_size()[0] + if self.cols >= g.min_screen_width: break + m1 = 'Screen too narrow to display the tracking wallet' + m2 = 'Please resize your screen to at least {} characters and hit ENTER ' + my_raw_input(m1+'\n'+m2.format(g.min_screen_width)) def display(self): msg(self.format_for_display()) - def format(self,wide=False): - return self.format_for_printing() if wide else self.format_for_display() - def format_for_display(self): - unspent = self.unspent - total = sum([i['amount'] for i in unspent]) - mmid_w = max(len(i['mmid']) for i in unspent) - self.set_cols() - - max_acct_len = max([len(i['mmid']+i['comment'])+1 for i in self.unspent]) - addr_w = min(34+((1+max_acct_len) if self.show_mmaddr else 0),self.cols-46) - acct_w = min(max_acct_len, max(24,int(addr_w-10))) - btaddr_w = addr_w - acct_w - 1 - tx_w = max(11,min(64, self.cols-addr_w-32)) - txdots = ('','...')[tx_w < 64] - fs = ' %-4s %-' + str(tx_w) + 's %-2s %-' + str(addr_w) + 's %-13s %-s' - table_hdr = fs % ('Num','TX id Vout','','Address','Amount (BTC)', - ('Conf.','Age(d)')[self.show_days]) - - from copy import deepcopy - unsp = deepcopy(unspent) - for i in unsp: i['skip'] = '' - if self.group and (self.sort in ('address','txid')): - for a,b in [(unsp[i],unsp[i+1]) for i in range(len(unsp)-1)]: - if self.sort == 'address' and a['address'] == b['address']: b['skip'] = 'addr' - elif self.sort == 'txid' and a['txid'] == b['txid']: b['skip'] = 'txid' + unsp = [MMGenTWOutput(**i.__dict__) for i in self.unspent] + self.set_term_columns() for i in unsp: - addr_disp = (i['address'],'|' + '.'*33)[i['skip']=='addr'] - mmid_disp = (i['mmid'],'.'*len(i['mmid']))[i['skip']=='addr'] - if self.show_mmaddr: - dots = ('','..')[btaddr_w < len(i['address'])] - i['addr'] = '%s%s %s' % ( - addr_disp[:btaddr_w-len(dots)], - dots, ( - ('{:<{w}} '.format(mmid_disp,w=mmid_w) if i['mmid'] else '') - + i['comment'])[:acct_w] - ) - else: - i['addr'] = addr_disp + if i.label == None: i.label = '' + i.skip = '' - i['tx'] = ' ' * (tx_w-4) + '|...' if i['skip'] == 'txid' \ - else i['txid'][:tx_w-len(txdots)]+txdots + mmid_w = max(len(i.mmid or '') for i in unsp) + max_acct_len = max([len((i.mmid or '')+i.label)+1 for i in unsp]) + addr_w = min(34+((1+max_acct_len) if self.show_mmid else 0),self.cols-46) + 6 + acct_w = min(max_acct_len, max(24,int(addr_w-10))) + btaddr_w = addr_w - acct_w - 1 + label_w = acct_w - mmid_w - 1 + tx_w = max(11,min(64, self.cols-addr_w-32)) + txdots = ('','...')[tx_w < 64] + fs = ' %-4s %-' + str(tx_w) + 's %-2s %s %s %s' + table_hdr = fs % ('Num', + 'TX id'.ljust(tx_w - 5) + ' Vout', + '', + BTCAddr.fmtc('Address',width=addr_w+1), + 'Amt(BTC) ', + ('Conf.','Age(d)')[self.show_days]) + + if self.group and (self.sort in ('addr','txid','mmid')): + for a,b in [(unsp[i],unsp[i+1]) for i in range(len(unsp)-1)]: + for k in ('addr','txid','mmid'): + if self.sort == k and getattr(a,k) == getattr(b,k): + b.skip = (k,'addr')[k=='mmid'] hdr_fmt = 'UNSPENT OUTPUTS (sort order: %s) Total BTC: %s' - out = [hdr_fmt % (' '.join(self.sort_info()), normalize_btc_amt(total)), table_hdr] - out += [fs % (str(n+1)+')',i['tx'],i['vout'],i['addr'],i['amt_fmt'], - i['days'] if self.show_days else i['confirmations']) - for n,i in enumerate(unsp)] - self.fmt_display = '\n'.join(out) + out = [hdr_fmt % (' '.join(self.sort_info()), self.total.hl()), table_hdr] + + for n,i in enumerate(unsp): + addr_dots = '|' + '.'*33 + mmid_disp = (MMGenID.hlc('.'*mmid_w) \ + if i.skip=='addr' else i.mmid.fmt(width=mmid_w,color=True)) \ + if i.mmid else ' ' * mmid_w + if self.show_mmid and i.mmid: + addr_out = '%s %s' % ( + type(i.addr).fmtc(addr_dots,width=btaddr_w,color=True) if i.skip == 'addr' \ + else i.addr.fmt(width=btaddr_w,color=True), + '{} {}'.format(mmid_disp,i.label.fmt(width=label_w,color=True)) + ) + else: + addr_out = type(i.addr).fmtc(addr_dots,width=addr_w,color=True) if i.skip=='addr' \ + else i.addr.fmt(width=addr_w,color=True) + + tx = ' ' * (tx_w-4) + '|...' if i.skip == 'txid' \ + else i.txid[:tx_w-len(txdots)]+txdots + + out.append(fs % (str(n+1)+')',tx,i.vout,addr_out,i.amt.fmt(color=True), + i.days if self.show_days else i.confs)) + + self.fmt_display = '\n'.join(out) + '\n' return self.fmt_display - def format_for_printing(self): + def format_for_printing(self,color=False): - total = sum([i['amount'] for i in self.unspent]) - fs = ' %-4s %-67s %-34s %-14s %-12s %-8s %-6s %s' - out = [fs % ('Num','Tx ID,Vout','Address','{} ID'.format(g.proj_name), - 'Amount(BTC)','Conf.','Age(d)', 'Comment')] + fs = ' %-4s %-67s %s %s %s %-8s %-6s %s' + out = [fs % ('Num','Tx ID,Vout','Address'.ljust(34),'MMGen ID'.ljust(15), + 'Amount(BTC)','Conf.','Age(d)', 'Label')] for n,i in enumerate(self.unspent): - addr = '=' if i['skip'] == 'addr' and self.group else i['address'] - tx = ' ' * 63 + '=' if i['skip'] == 'txid' and self.group else str(i['txid']) - s = fs % (str(n+1)+')', tx+','+str(i['vout']),addr, - i['mmid'],i['amt_fmt'].strip(),i['confirmations'],i['days'],i['comment']) + addr = '=' if i.skip == 'addr' and self.group else i.addr.fmt(color=color) + tx = ' ' * 63 + '=' if i.skip == 'txid' and self.group else str(i.txid) + s = fs % (str(n+1)+')', tx+','+str(i.vout),addr, + (i.mmid.fmt(14,color=color) if i.mmid else ''.ljust(14)), + i.amt.fmt(color=color),i.confs,i.days,i.label.hl(color=color) if i.label else '') out.append(s.rstrip()) fs = 'Unspent outputs ({} UTC)\nSort order: {}\n\n{}\n\nTotal BTC: {}\n' @@ -170,51 +190,53 @@ watch-only wallet using '{}-addrimport' and then re-run this program. make_timestr(), ' '.join(self.sort_info(include_group=False)), '\n'.join(out), - normalize_btc_amt(total)) + self.total.hl(color=color)) return self.fmt_print def display_total(self): fs = '\nTotal unspent: %s BTC (%s outputs)' - msg(fs % (normalize_btc_amt(self.total), len(self.unspent))) + msg(fs % (self.total.hl(),len(self.unspent))) def view_and_sort(self): from mmgen.term import do_pager - s = """ + prompt = """ Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen """.strip() self.display() - msg(s) + msg(prompt) p = "('q' = quit sorting, 'p' = print to file, 'v' = pager view, 'w' = wide view): " while True: reply = get_char(p, immed_chars='atDdAMrgmeqpvw') - if reply == 'a': self.do_sort('amount') + if reply == 'a': self.do_sort('amt') elif reply == 't': self.do_sort('txid') elif reply == 'D': self.show_days = not self.show_days - elif reply == 'd': self.do_sort('address') + elif reply == 'd': self.do_sort('addr') elif reply == 'A': self.do_sort('age') - elif reply == 'M': self.do_sort('mmaddr'); self.show_mmaddr = True + elif reply == 'M': self.do_sort('mmid'); self.show_mmid = True elif reply == 'r': self.unspent.reverse(); self.reverse = not self.reverse elif reply == 'g': self.group = not self.group - elif reply == 'm': self.show_mmaddr = not self.show_mmaddr - elif reply == 'e': msg("\n%s\n%s\n%s" % (self.fmt_display,s,p)) + elif reply == 'm': self.show_mmid = not self.show_mmid + elif reply == 'e': msg("\n%s\n%s\n%s" % (self.fmt_display,prompt,p)) elif reply == 'q': return self.unspent elif reply == 'p': of = 'listunspent[%s].out' % ','.join(self.sort_info(include_group=False)) + msg('') write_data_to_file(of,self.format_for_printing(),'unspent outputs listing') m = yellow("Data written to '%s'" % of) - msg('\n%s\n\n%s\n\n%s' % (self.fmt_display,m,s)) + msg('\n%s\n%s\n\n%s' % (self.fmt_display,m,prompt)) continue elif reply == 'v': do_pager(self.fmt_display) continue elif reply == 'w': - do_pager(self.format_for_printing()) + do_pager(self.format_for_printing(color=True)) continue else: msg('\nInvalid input') continue + msg('\n') self.display() - msg(s) + msg(prompt) diff --git a/mmgen/tx.py b/mmgen/tx.py index be243f1b..e9c1c3df 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -20,86 +20,17 @@ tx.py: Bitcoin transaction routines """ -import sys, os +import sys,os from stat import * -from binascii import hexlify,unhexlify -from decimal import Decimal -from collections import OrderedDict - +from binascii import unhexlify from mmgen.common import * +from mmgen.obj import * from mmgen.term import do_pager -def normalize_btc_amt(amt): - '''Remove exponent and trailing zeros. - ''' - # to_integral() needed to keep ints > 9 from being shown in exp. notation - if is_btc_amt(amt): - return amt.quantize(Decimal(1)) if amt == amt.to_integral() else amt.normalize() - else: - die(2,'%s: not a BTC amount' % amt) - -def is_btc_amt(amt): - - if type(amt) is not Decimal: - msg('%s: not a decimal number' % amt) - return False - - if amt.as_tuple()[-1] < -g.btc_amt_decimal_places: - msg('%s: Too many decimal places in amount' % amt) - return False - - return True - -def convert_to_btc_amt(amt,return_on_fail=False): - # amt must be a string! - - from decimal import Decimal - try: - ret = Decimal(amt) - except: - m = '%s: amount cannot be converted to decimal' % amt - if return_on_fail: - msg(m); return False - else: - die(2,m) - - dmsg('Decimal(amt): %s' % repr(amt)) - - if ret.as_tuple()[-1] < -g.btc_amt_decimal_places: - m = '%s: Too many decimal places in amount' % amt - if return_on_fail: - msg(m); return False - else: - die(2,m) - - if ret == 0: - msg('WARNING: BTC amount is zero') - - return ret - - -def parse_mmgen_label(s,check_label_len=False): - l = split2(s) - if not is_mmgen_addr(l[0]): return '',s - if check_label_len: check_addr_label(l[1]) - return tuple(l) - -def is_mmgen_seed_id(s): - import re - return re.match(r'^[0123456789ABCDEF]{8}$',s) is not None - -def is_mmgen_idx(s): - try: int(s) - except: return False - return len(s) <= g.mmgen_idx_max_digits - -def is_mmgen_addr(s): - seed_id,idx = split2(s,':') - return is_mmgen_seed_id(seed_id) and is_mmgen_idx(idx) - -def is_btc_addr(s): - from mmgen.bitcoin import verify_addr - return verify_addr(s) +def is_mmgen_seed_id(s): return SeedID(sid=s,on_fail='silent') +def is_mmgen_idx(s): return AddrIdx(s,on_fail='silent') +def is_mmgen_id(s): return MMGenID(s,on_fail='silent') +def is_btc_addr(s): return BTCAddr(s,on_fail='silent') def is_b58_str(s): from mmgen.bitcoin import b58a @@ -111,7 +42,7 @@ def is_wif(s): from mmgen.bitcoin import wiftohex return wiftohex(s,compressed) is not False -def wiftoaddr(s): +def _wiftoaddr(s): if s == '': return False compressed = not s[0] == '5' from mmgen.bitcoin import wiftohex,privnum2addr @@ -119,75 +50,51 @@ def wiftoaddr(s): if not hex_key: return False return privnum2addr(int(hex_key,16),compressed) - -def is_valid_tx_comment(s): - - try: s = s.decode('utf8') - except: - msg('Invalid transaction comment (not UTF-8)') - return False - - if len(s) > g.max_tx_comment_len: - msg('Invalid transaction comment (longer than %s characters)' % - g.max_tx_comment_len) - return False - - return True - - -def check_addr_label(label): - - if len(label) > g.max_addr_label_len: - msg("'%s': overlong label (length must be <=%s)" % - (label,g.max_addr_label_len)) - sys.exit(3) - - for ch in label: - if ch not in g.addr_label_symbols: - msg(""" -'%s': illegal character in label '%s'. -Only ASCII printable characters are permitted. -""".strip() % (ch,label)) - sys.exit(3) - -def wiftoaddr_keyconv(wif): +def _wiftoaddr_keyconv(wif): if wif[0] == '5': from subprocess import check_output return check_output(['keyconv', wif]).split()[1] else: - return wiftoaddr(wif) + return _wiftoaddr(wif) def get_wif2addr_f(): - if opt.no_keyconv: return wiftoaddr + if opt.no_keyconv: return _wiftoaddr from mmgen.addr import test_for_keyconv - return (wiftoaddr,wiftoaddr_keyconv)[bool(test_for_keyconv())] + return (_wiftoaddr,_wiftoaddr_keyconv)[bool(test_for_keyconv())] +class MMGenTxInputOldFmt(MMGenListItem): # for converting old tx files only + tr = {'amount':'amt', 'address':'addr', 'confirmations':'confs','comment':'label'} + attrs = 'txid','vout','amt','label','mmid','addr','confs','scriptPubKey','wif' + attrs_priv = 'tr', -def mmaddr2btcaddr_addrdata(mmaddr,addr_data,source=''): - seed_id,idx = mmaddr.split(':') - if seed_id in addr_data: - if idx in addr_data[seed_id]: - vmsg('%s -> %s%s' % (mmaddr,addr_data[seed_id][idx][0], - ' (from %s)' % source if source else '')) - return addr_data[seed_id][idx] +class MMGenTxInput(MMGenListItem): + attrs = 'txid','vout','amt','label','mmid','addr','confs','scriptPubKey','have_wif' + label = MMGenListItemAttr('label','MMGenAddrLabel') - return '','' - -from mmgen.obj import * +class MMGenTxOutput(MMGenListItem): + attrs = 'txid','vout','amt','label','mmid','addr','have_wif' + label = MMGenListItemAttr('label','MMGenAddrLabel') class MMGenTX(MMGenObject): - ext = g.rawtx_ext + ext = 'rawtx' + raw_ext = 'rawtx' + sig_ext = 'sigtx' + txid_ext = 'txid' desc = 'transaction' + max_fee = BTCAmt('0.01') + def __init__(self,filename=None): self.inputs = [] - self.outputs = {} + self.inputs_enc = [] + self.outputs = [] + self.outputs_enc = [] self.change_addr = '' self.size = 0 # size of raw serialized tx - self.fee = Decimal('0') - self.send_amt = Decimal('0') # total amt minus change + self.fee = BTCAmt('0') + self.send_amt = BTCAmt('0') # total amt minus change self.hex = '' # raw serialized hex transaction - self.comment = '' + self.label = MMGenTXLabel('') self.txid = '' self.btc_txid = '' self.timestamp = '' @@ -195,37 +102,56 @@ class MMGenTX(MMGenObject): self.fmt_data = '' self.blockcount = 0 if filename: - if get_extension(filename) == g.sigtx_ext: + if get_extension(filename) == self.sig_ext: self.mark_signed() self.parse_tx_file(filename) - def add_output(self,btcaddr,amt): - self.outputs[btcaddr] = (amt,) + def add_output(self,btcaddr,amt): # 'txid','vout','amount','label','mmid','address' + self.outputs.append(MMGenTxOutput(addr=btcaddr,amt=amt)) def del_output(self,btcaddr): - del self.outputs[btcaddr] + for i in range(len(self.outputs)): + if self.outputs[i].addr == btcaddr: + self.outputs.pop(i); return + raise ValueError def sum_outputs(self): - return sum([self.outputs[k][0] for k in self.outputs]) + return BTCAmt(sum([e.amt for e in self.outputs])) + + def add_mmaddrs_to_outputs(self,ad_w,ad_f): + a = [e.addr for e in self.outputs] + d = ad_w.make_reverse_dict(a) + d.update(ad_f.make_reverse_dict(a)) + for e in self.outputs: + if e.addr and e.addr in d: + e.mmid,f = d[e.addr] + if f: e.label = f + +# def encode_io(self,desc): +# tr = getattr((MMGenTxOutput,MMGenTxInput)[desc=='inputs'],'tr') +# tr_rev = dict([(v,k) for k,v in tr.items()]) +# return [dict([(tr_rev[e] if e in tr_rev else e,getattr(d,e)) for e in d.__dict__]) +# for d in getattr(self,desc)] +# + def create_raw(self,c): + i = [{'txid':e.txid,'vout':e.vout} for e in self.inputs] + o = dict([(e.addr,e.amt) for e in self.outputs]) + self.hex = c.createrawtransaction(i,o) + self.txid = make_chksum_6(unhexlify(self.hex)).upper() # returns true if comment added or changed def add_comment(self,infile=None): if infile: - s = get_data_from_file(infile,'transaction comment') - if is_valid_tx_comment(s): - self.comment = s.decode('utf8').strip() - return True - else: - sys.exit(2) + self.label = MMGenTXLabel(get_data_from_file(infile,'transaction comment')) else: # get comment from user, or edit existing comment - m = ('Add a comment to transaction?','Edit transaction comment?')[bool(self.comment)] + m = ('Add a comment to transaction?','Edit transaction comment?')[bool(self.label)] if keypress_confirm(m,default_yes=False): while True: - s = my_raw_input('Comment: ',insert_txt=self.comment.encode('utf8')) - if is_valid_tx_comment(s): - csave = self.comment - self.comment = s.decode('utf8').strip() - return (True,False)[csave == self.comment] + s = MMGenTXLabel(my_raw_input('Comment: ',insert_txt=self.label)) + if s: + lbl_save = self.label + self.label = s + return (True,False)[lbl_save == self.label] else: msg('Invalid comment') return False @@ -237,53 +163,57 @@ class MMGenTX(MMGenObject): def calculate_size_and_fee(self,fee_estimate): self.size = len(self.inputs)*180 + len(self.outputs)*34 + 10 if fee_estimate: - ftype,fee = 'Calculated','{:.8f}'.format(fee_estimate*opt.tx_fee_adj*self.size / 1024) + ftype,fee = 'Calculated',fee_estimate*opt.tx_fee_adj*self.size / 1024 else: ftype,fee = 'User-selected',opt.tx_fee ufee = None - if not keypress_confirm('{} TX fee: {} BTC. OK?'.format(ftype,fee),default_yes=True): + if not keypress_confirm('{} TX fee is {} BTC. OK?'.format(ftype,fee.hl()),default_yes=True): while True: ufee = my_raw_input('Enter transaction fee: ') - if convert_to_btc_amt(ufee,return_on_fail=True): - if Decimal(ufee) > g.max_tx_fee: - msg('{} BTC: fee too large (maximum fee: {} BTC)'.format(ufee,g.max_tx_fee)) + if BTCAmt(ufee,on_fail='return'): + ufee = BTCAmt(ufee) + if ufee > self.max_fee: + msg('{} BTC: fee too large (maximum fee: {} BTC)'.format(ufee,self.max_fee)) else: fee = ufee break - self.fee = convert_to_btc_amt(fee) + self.fee = fee vmsg('Inputs:{} Outputs:{} TX size:{}'.format( len(self.inputs),len(self.outputs),self.size)) vmsg('Fee estimate: {} (1024 bytes, {} confs)'.format(fee_estimate,opt.tx_confs)) m = ('',' (after %sx adjustment)' % opt.tx_fee_adj)[opt.tx_fee_adj != 1 and not ufee] vmsg('TX fee: {}{}'.format(self.fee,m)) - def copy_inputs(self,source): - copy_keys = 'txid','vout','amount','comment','mmid','address',\ - 'confirmations','scriptPubKey' - self.inputs = [dict([(k,d[k] if k in d else '') for k in copy_keys]) for d in source] + # inputs methods + def list_wifs(self,desc,mmaddrs_only=False): + return [e.wif for e in getattr(self,desc) if e.mmid] if mmaddrs_only \ + else [e.wif for e in getattr(self,desc)] + + def delete_attrs(self,desc,attr): + for e in getattr(self,desc): + if hasattr(e,attr): delattr(e,attr) + + def decode_io(self,desc,data): + io = (MMGenTxOutput,MMGenTxInput)[desc=='inputs'] + return [io(**dict([(k,d[k]) for k in io.attrs + if k in d and d[k] not in ('',None)])) for d in data] + + def decode_io_oldfmt(self,data): + io = MMGenTxInputOldFmt + tr_rev = dict([(v,k) for k,v in io.tr.items()]) + copy_keys = [tr_rev[k] if k in tr_rev else k for k in io.attrs] + return [io(**dict([(io.tr[k] if k in io.tr else k,d[k]) + for k in copy_keys if k in d and d[k] != ''])) for d in data] + + def copy_inputs_from_tw(self,data): + self.inputs = self.decode_io('inputs',[e.__dict__ for e in data]) + + def get_input_sids(self): + return set([e.mmid[:8] for e in self.inputs if e.mmid]) def sum_inputs(self): - return sum([i['amount'] for i in self.inputs]) - - def create_raw(self,c): - o = dict([(k,v[0]) for k,v in self.outputs.items()]) - self.hex = c.createrawtransaction(self.inputs,o) - self.txid = make_chksum_6(unhexlify(self.hex)).upper() - -# def make_b2m_map(self,ail_w,ail_f): -# d = dict([(d['address'], (d['mmid'],d['comment'])) -# for d in self.inputs if d['mmid']]) -# d = ail_w.make_reverse_dict(self.outputs.keys()) -# d.update(ail_f.make_reverse_dict(self.outputs.keys())) -# self.b2m_map = d - - def add_mmaddrs_to_outputs(self,ail_w,ail_f): - d = ail_w.make_reverse_dict(self.outputs.keys()) - d.update(ail_f.make_reverse_dict(self.outputs.keys())) - for k in self.outputs: - if k in d: - self.outputs[k] += d[k] + return sum([e.amt for e in self.inputs]) def add_timestamp(self): self.timestamp = make_timestamp() @@ -301,20 +231,28 @@ class MMGenTX(MMGenObject): (self.blockcount or 'None') ), self.hex, - repr(self.inputs), - repr(self.outputs) - ) + ((b58encode(self.comment.encode('utf8')),) if self.comment else ()) + repr([e.__dict__ for e in self.inputs]), + repr([e.__dict__ for e in self.outputs]) + ) + ((b58encode(self.label),) if self.label else ()) self.chksum = make_chksum_6(' '.join(lines)) self.fmt_data = '\n'.join((self.chksum,) + lines)+'\n' + + def get_non_mmaddrs(self,desc): + return list(set([i.addr for i in getattr(self,desc) if not i.mmid])) + # return true or false, don't exit - def sign(self,c,tx_num_str,keys=None): + def sign(self,c,tx_num_str,keys): - if keys: - qmsg('Passing %s key%s to bitcoind' % (len(keys),suf(keys,'k'))) - dmsg('Keys:\n %s' % '\n '.join(keys)) + if not keys: + msg('No keys. Cannot sign!') + return False - sig_data = [dict([(k,d[k]) for k in 'txid','vout','scriptPubKey']) for d in self.inputs] + qmsg('Passing %s key%s to bitcoind' % (len(keys),suf(keys,'k'))) + dmsg('Keys:\n %s' % '\n '.join(keys)) + + sig_data = [dict([(k,getattr(d,k)) for k in 'txid','vout','scriptPubKey']) + for d in self.inputs] dmsg('Sig data:\n%s' % pp_format(sig_data)) dmsg('Raw hex:\n%s' % self.hex) @@ -333,7 +271,7 @@ class MMGenTX(MMGenObject): def mark_signed(self): self.desc = 'signed transaction' - self.ext = g.sigtx_ext + self.ext = self.sig_ext def check_signed(self,c): d = c.decoderawtransaction(self.hex) @@ -351,7 +289,7 @@ class MMGenTX(MMGenObject): msg(m % self.btc_txid) def write_txid_to_file(self,ask_write=False,ask_write_default_yes=True): - fn = '%s[%s].%s' % (self.txid,self.send_amt,g.txid_ext) + fn = '%s[%s].%s' % (self.txid,self.send_amt,self.txid_ext) write_data_to_file(fn,self.btc_txid+'\n','transaction ID', ask_write=ask_write, ask_write_default_yes=ask_write_default_yes) @@ -392,49 +330,45 @@ class MMGenTX(MMGenObject): 'Transaction {} - {} BTC - {} UTC\n' )[bool(terse)] - out = fs.format(self.txid,self.send_amt,self.timestamp) + out = fs.format(self.txid,self.send_amt.hl(),self.timestamp) enl = ('\n','')[bool(terse)] - if self.comment: - out += 'Comment: %s\n%s' % (self.comment,enl) + if self.label: + out += 'Comment: %s\n%s' % (self.label.hl(),enl) out += 'Inputs:\n' + enl - nonmm_str = 'non-{pnm} address'.format(pnm=g.proj_name) - - for n,i in enumerate(self.inputs): + nonmm_str = '(non-{pnm} address)'.format(pnm=g.proj_name) +# for i in self.inputs: print i #DEBUG + for n,e in enumerate(self.inputs): if blockcount: - confirmations = i['confirmations'] + blockcount - self.blockcount - days = int(confirmations * g.mins_per_block / (60*24)) - if not i['mmid']: - i['mmid'] = nonmm_str - mmid_fmt = ' ({:>{l}})'.format(i['mmid'],l=34-len(i['address'])) + confs = e.confs + blockcount - self.blockcount + days = int(confs * g.mins_per_block / (60*24)) + mmid_fmt = e.mmid.fmt(width=len(nonmm_str),encl='()',color=True) if e.mmid \ + else MMGenID.hlc(nonmm_str) if terse: - out += ' %s: %-54s %s BTC' % (n+1,i['address'] + mmid_fmt, - normalize_btc_amt(i['amount'])) + out += '%3s: %s %s %s BTC' % (n+1, e.addr.fmt(color=True),mmid_fmt, e.amt.hl()) else: for d in ( - (n+1, 'tx,vout:', '%s,%s' % (i['txid'], i['vout'])), - ('', 'address:', i['address'] + mmid_fmt), - ('', 'comment:', i['comment']), - ('', 'amount:', '%s BTC' % normalize_btc_amt(i['amount'])), - ('', 'confirmations:', '%s (around %s days)' % (confirmations,days) if blockcount else '') + (n+1, 'tx,vout:', '%s,%s' % (e.txid, e.vout)), + ('', 'address:', e.addr.fmt(color=True) + ' ' + mmid_fmt), + ('', 'comment:', e.label.hl() if e.label else ''), + ('', 'amount:', '%s BTC' % e.amt.hl()), + ('', 'confirmations:', '%s (around %s days)' % (confs,days) if blockcount else '') ): if d[2]: out += ('%3s %-8s %s\n' % d) out += '\n' out += 'Outputs:\n' + enl - for n,k in enumerate(self.outputs): - btcaddr = k - v = self.outputs[k] - btc_amt,mmid,comment = (v[0],'Non-MMGen address','') if len(v) == 1 else v - mmid_fmt = ' ({:>{l}})'.format(mmid,l=34-len(btcaddr)) + for n,e in enumerate(self.outputs): + mmid_fmt = e.mmid.fmt(width=len(nonmm_str),encl='()',color=True) if e.mmid \ + else MMGenID.hlc(nonmm_str) if terse: - out += ' %s: %-54s %s BTC' % (n+1, btcaddr+mmid_fmt, normalize_btc_amt(btc_amt)) + out += '%3s: %s %s %s BTC' % (n+1, e.addr.fmt(color=True),mmid_fmt, e.amt.hl()) else: for d in ( - (n+1, 'address:', btcaddr + mmid_fmt), - ('', 'comment:', comment), - ('', 'amount:', '%s BTC' % normalize_btc_amt(btc_amt)) + (n+1, 'address:', e.addr.fmt(color=True) + ' ' + mmid_fmt), + ('', 'comment:', e.label.hl() if e.label else ''), + ('', 'amount:', '%s BTC' % e.amt.hl()) ): if d[2]: out += ('%3s %-8s %s\n' % d) out += '\n' @@ -447,9 +381,9 @@ class MMGenTX(MMGenObject): total_in = self.sum_inputs() total_out = self.sum_outputs() out += fs % ( - normalize_btc_amt(total_in), - normalize_btc_amt(total_out), - normalize_btc_amt(total_in-total_out) + total_in.hl(), + total_out.hl(), + (total_in-total_out).hl() ) return out @@ -477,15 +411,15 @@ class MMGenTX(MMGenObject): err_str = 'metadata' else: self.txid,send_amt,self.timestamp,blockcount = metadata.split() - self.send_amt = Decimal(send_amt) + self.send_amt = BTCAmt(send_amt) self.blockcount = int(blockcount) try: unhexlify(self.hex) except: err_str = 'hex data' else: - try: self.inputs = eval(inputs_data) + try: self.inputs = self.decode_io('inputs',eval(inputs_data)) except: err_str = 'inputs data' else: - try: self.outputs = eval(outputs_data) + try: self.outputs = self.decode_io('outputs',eval(outputs_data)) except: err_str = 'btc-to-mmgen address map data' else: if comment: @@ -494,13 +428,10 @@ class MMGenTX(MMGenObject): if comment == False: err_str = 'encoded comment (not base58)' else: - if is_valid_tx_comment(comment): - self.comment = comment.decode('utf8') - else: + self.label = MMGenTXLabel(comment,on_fail='return') + if not self.label: err_str = 'comment' if err_str: msg(err_fmt % err_str) sys.exit(2) - - diff --git a/mmgen/util.py b/mmgen/util.py index 79049470..18559254 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -29,14 +29,31 @@ import mmgen.globalvars as g pnm = g.proj_name -_red,_grn,_yel,_cya,_reset,_grnbg = \ - ['\033[%sm' % c for c in '31;1','32;1','33;1','36;1','0','30;102'] +# If 88- or 256-color support is compiled, the following apply. +# P s = 3 8 ; 5 ; P s -> Set foreground color to the second P s . +# P s = 4 8 ; 5 ; P s -> Set background color to the second P s . +if os.environ['TERM'][-8:] == '256color': + _blk,_red,_grn,_yel,_blu,_mag,_cya,_bright,_dim,_ybright,_ydim,_pnk,_orng,_gry = [ + '\033[38;5;%s;1m' % c for c in 232,210,121,229,75,90,122,231,245,187,243,218,215,246] + _redbg = '\033[38;5;232;48;5;210;1m' + _grnbg = '\033[38;5;232;48;5;121;1m' + _grybg = '\033[38;5;231;48;5;240;1m' + _reset = '\033[0m' +else: + _blk,_red,_grn,_yel,_blu,_mag,_cya,_reset,_grnbg = \ + ['\033[%sm' % c for c in '30;1','31;1','32;1','33;1','34;1','35;1','36;1','0','30;102'] + _gry = _orng = _pnk = _redbg = _ybright = _ydim = _bright = _dim = _grybg = _mag # TODO def red(s): return _red+s+_reset def green(s): return _grn+s+_reset -def grnbg(s): return _grnbg+s+_reset +def grnbg(s): return _grnbg+s+_reset def yellow(s): return _yel+s+_reset def cyan(s): return _cya+s+_reset +def blue(s): return _blu+s+_reset +def pink(s): return _pnk+s+_reset +def orange(s): return _orng+s+_reset +def gray(s): return _gry+s+_reset +def magenta(s): return _mag+s+_reset def nocolor(s): return s def start_mscolor(): @@ -65,10 +82,12 @@ def mdie(*args): sys.stdout.write(repr(d)+'\n') sys.exit() -def die(ev,s): - sys.stderr.write(s+'\n'); sys.exit(ev) -def Die(ev,s): - sys.stdout.write(s+'\n'); sys.exit(ev) +def die(ev=0,s=''): + if s: sys.stderr.write(s+'\n') + sys.exit(ev) +def Die(ev=0,s=''): + if s: sys.stdout.write(s+'\n') + sys.exit(ev) def pp_format(d): import pprint @@ -144,7 +163,7 @@ def suf(arg,suf_type): t = type(arg) if t == int: n = arg - elif t == list or t == tuple or t == set: + elif t in (list,tuple,set,dict): n = len(arg) else: msg('%s: invalid parameter' % arg) @@ -376,7 +395,7 @@ def _validate_addr_num(n): msg("'%s': invalid %s address index" % (n,g.proj_name)) return False -def parse_addr_idxs(arg,sep=','): +def parse_addr_idxs(arg,sep=','): # TODO - delete ret = [] @@ -517,52 +536,6 @@ def write_data_to_file( return True - -def _check_mmseed_format(words): - - valid = False - desc = '%s data' % g.seed_ext - try: - chklen = len(words[0]) - except: - return False - - if len(words) < 3 or len(words) > 12: - msg('Invalid data length (%s) in %s' % (len(words),desc)) - elif not is_hexstring(words[0]): - msg("Invalid format of checksum '%s' in %s"%(words[0], desc)) - elif chklen != 6: - msg('Incorrect length of checksum (%s) in %s' % (chklen,desc)) - else: valid = True - - return valid - - -def _check_wallet_format(infile, lines): - - desc = "wallet file '%s'" % infile - valid = False - chklen = len(lines[0]) - if len(lines) != 6: - vmsg('Invalid number of lines (%s) in %s' % (len(lines),desc)) - elif chklen != 6: - vmsg('Incorrect length of Master checksum (%s) in %s' % (chklen,desc)) - elif not is_hexstring(lines[0]): - vmsg("Invalid format of Master checksum '%s' in %s"%(lines[0], desc)) - else: valid = True - - if valid == False: - die(2,'Invalid %s' % desc) - - -def _check_chksum_6(chk,val,desc,infile): - comp_chk = make_chksum_6(val) - if chk != comp_chk: - msg("%s checksum incorrect in file '%s'!" % (desc,infile)) - die(2,'Checksum: %s. Computed value: %s' % (chk,comp_chk)) - dmsg('%s checksum passed: %s' % (capfirst(desc),chk)) - - def get_words_from_user(prompt): # split() also strips words = my_raw_input(prompt, echo=opt.echo_passphrase).split() @@ -591,19 +564,25 @@ def remove_comments(lines): # re.sub(pattern, repl, string, count=0, flags=0) ret = [] for i in lines: - i = re.sub('#.*','',i,1) - i = re.sub('\s+$','',i) + i = re.sub(ur'#.*',u'',i,1) + i = re.sub(ur'\s+$',u'',i) if i: ret.append(i) return ret -def get_lines_from_file(infile,desc='',trim_comments=False): - if desc != '': - qmsg("Getting %s from file '%s'" % (desc,infile)) - f = open_file_or_exit(infile,'r') - lines = f.read().splitlines() # DOS-safe - f.close() - return remove_comments(lines) if trim_comments else lines +def mmgen_decrypt_file_maybe(fn,desc=''): + d = get_data_from_file(fn,desc,binary=True) + have_enc_ext = get_extension(fn) == g.mmenc_ext + if have_enc_ext or not is_ascii(d): + m = ('Attempting to decrypt','Decrypting')[have_enc_ext] + msg("%s %s '%s'" % (m,desc,fn)) + from mmgen.crypto import mmgen_decrypt_retry + d = mmgen_decrypt_retry(d,desc) + return d +def get_lines_from_file(fn,desc='',trim_comments=False): + dec = mmgen_decrypt_file_maybe(fn,desc) + ret = dec.decode('utf8').splitlines() # DOS-safe + return remove_comments(ret) if trim_comments else ret def get_data_from_user(desc='data',silent=False): data = my_raw_input('Enter %s: ' % desc, echo=opt.echo_passphrase) @@ -612,40 +591,13 @@ def get_data_from_user(desc='data',silent=False): def get_data_from_file(infile,desc='data',dash=False,silent=False,binary=False): if dash and infile == '-': return sys.stdin.read() - if not silent: + if not silent and desc: qmsg("Getting %s from file '%s'" % (desc,infile)) f = open_file_or_exit(infile,('r','rb')[bool(binary)]) data = f.read() f.close() return data - -def get_seed_from_seed_data(words): - - if not _check_mmseed_format(words): - msg('Invalid %s data' % g.seed_ext) - return False - - stored_chk = words[0] - seed_b58 = ''.join(words[1:]) - - chk = make_chksum_6(seed_b58) - vmsg_r('Validating %s checksum...' % g.seed_ext) - - if compare_chksums(chk, 'seed', stored_chk, 'input'): - from mmgen.bitcoin import b58decode_pad - seed = b58decode_pad(seed_b58) - if seed == False: - msg('Invalid b58 number: %s' % val) - return False - - msg('Valid seed data for Seed ID %s' % make_chksum_8(seed)) - return seed - else: - msg('Invalid checksum for {pnm} seed'.format(pnm=pnm)) - return False - - passwd_file_used = False def pwfile_reuse_warning(): @@ -790,8 +742,8 @@ def get_bitcoind_cfg_options(cfg_keys): cfg_file = os.path.join(get_homedir(), get_datadir(), 'bitcoin.conf') - cfg = dict([(k,v) for k,v in [split2(line.translate(None,'\t '),'=') - for line in get_lines_from_file(cfg_file)] if k in cfg_keys]) \ + cfg = dict([(k,v) for k,v in [split2(str(line).translate(None,'\t '),'=') + for line in get_lines_from_file(cfg_file,'')] if k in cfg_keys]) \ if file_is_readable(cfg_file) else {} for k in set(cfg_keys) - set(cfg.keys()): cfg[k] = '' @@ -803,7 +755,7 @@ def get_bitcoind_auth_cookie(): f = os.path.join(get_homedir(), get_datadir(), '.cookie') if file_is_readable(f): - return get_lines_from_file(f)[0] + return get_lines_from_file(f,'')[0] else: return '' diff --git a/scripts/compute-file-chksum.py b/scripts/compute-file-chksum.py new file mode 100755 index 00000000..9c7485aa --- /dev/null +++ b/scripts/compute-file-chksum.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python + +from mmgen.common import * +from mmgen.util import * + +opts_data = { + 'desc': 'Compute checksum for a MMGen data file', + 'usage':'[opts] infile', + 'options': """ +-h, --help Print this help message. +-i, --include-first-line Include the first line of the file (you probably don't want this) +""".strip() +} + +cmd_args = opts.init(opts_data) + +lines = get_lines_from_file(cmd_args[0]) +start = (1,0)[bool(opt.include_first_line)] +a = make_chksum_6(' '.join(lines[start:])) +if start == 1: + b = lines[0] + msg(("Checksum in file (%s) doesn't match computed value!" % b,"Checksum in file OK")[a==b]) +Msg(a) diff --git a/scripts/tx-old2new.py b/scripts/tx-old2new.py index 66c4fb90..1d99b935 100755 --- a/scripts/tx-old2new.py +++ b/scripts/tx-old2new.py @@ -94,22 +94,44 @@ def find_block_by_time(c,timestamp): tx = MMGenTX() -[tx.txid,send_amt,tx.timestamp],tx.hex,inputs,b2m_map,tx.comment = parse_tx_file(cmd_args[0]) +[tx.txid,send_amt,tx.timestamp],tx.hex,inputs,b2m_map,tx.label = parse_tx_file(cmd_args[0]) tx.send_amt = Decimal(send_amt) c = bitcoin_connection() -tx.copy_inputs(inputs) +# attrs = 'txid','vout','amt','comment','mmid','addr','wif' +#pp_msg(inputs) +for i in inputs: + if not 'mmid' in i and 'account' in i: + from mmgen.tw import parse_tw_acct_label + a,b = parse_tw_acct_label(i['account']) + if a: + i['mmid'] = a.decode('utf8') + if b: i['comment'] = b.decode('utf8') + +#pp_msg(inputs) +tx.inputs = tx.decode_io_oldfmt(inputs) + if tx.check_signed(c): msg('Transaction is signed') dec_tx = c.decoderawtransaction(tx.hex) -tx.outputs = dict([(i['scriptPubKey']['addresses'][0],(i['value'],)) for i in dec_tx['vout']]) +tx.outputs = [MMGenTxOutput(addr=i['scriptPubKey']['addresses'][0],amt=i['value']) + for i in dec_tx['vout']] +for e in tx.outputs: + if e.addr in b2m_map: + f = b2m_map[e.addr] + e.mmid = f[0] + if f[1]: e.label = f[1].decode('utf8') + else: + for f in tx.inputs: + if e.addr == f.addr and f.mmid: + e.mmid = f.mmid + if f.label: e.label = f.label.decode('utf8') +#for i in tx.inputs: print i +#for i in tx.outputs: print i +#die(1,'') tx.blockcount = find_block_by_time(c,tx.timestamp) -for k in tx.outputs: - if k in b2m_map: - tx.outputs[k] += b2m_map[k] - tx.write_to_file(ask_write=False) diff --git a/test/ref/FFB367[1.234].rawtx b/test/ref/FFB367[1.234].rawtx new file mode 100644 index 00000000..9c9b080c --- /dev/null +++ b/test/ref/FFB367[1.234].rawtx @@ -0,0 +1,6 @@ +2957e0 +FFB367 1.234 20150405_102927 350828 +01000000013364630b6d290a82c822facc2f7c1db4452cea459b2ce22371135530485a5d010600000000ffffffff0205d7d600010000001976a914bba3993079ccdf40c9bbbe495473f0b3d2dc5eec88ac40ef5a07000000001976a914abe58e1e45f6176910a4c1ac1ee62328d5cc4fd588ac00000000 +[{'label': u'Test Wallet', 'mmid': u'98831F3A:500', 'vout': 6, 'txid': u'015d5a483055137123e22c9b45ea2c45b41d7c2fccfa22c8820a296d0b636433', 'amt': BTCAmt('44.32452045'), 'confs': 495L, 'addr': u'1EZNuddPnaZFah9QVbGvzvTcP4KeRrRFt8', 'scriptPubKey': '76a91494b93bbe8a32f1db80b307482e83c25fa4e99b8c88ac'}] +[{'amt': BTCAmt('43.09047045'), 'mmid': '98831F3A:3', 'addr': u'1J79LtWctedRLnMfFNRgzzSFsozQqDeoKD'}, {'amt': BTCAmt('1.23400000'), 'mmid': '98831F3A:2', 'addr': u'1GfuYaKHrhdiVybXMGCcjadSgfjvpdt2x9'}] +3SBcsGkhcKRVB2gr98BmscU8HtWJ12HTXpJa5XmvbEUateQ3bJBEgvLd5kPGAzg1rFkzjVpZJgiKGwvnq5mJpwnbJqcHpVEAopWyALDmtjrDwEvPiTY diff --git a/test/ref/tx_FFB367[1.234].raw b/test/ref/tx_FFB367[1.234].raw deleted file mode 100644 index b0f29d19..00000000 --- a/test/ref/tx_FFB367[1.234].raw +++ /dev/null @@ -1,5 +0,0 @@ -FFB367 1.234 20150405_102927 -01000000013364630b6d290a82c822facc2f7c1db4452cea459b2ce22371135530485a5d010600000000ffffffff0205d7d600010000001976a914bba3993079ccdf40c9bbbe495473f0b3d2dc5eec88ac40ef5a07000000001976a914abe58e1e45f6176910a4c1ac1ee62328d5cc4fd588ac00000000 -[{'comment': u'Test Wallet', 'mmid': u'98831F3A:500', 'vout': 6, 'txid': u'015d5a483055137123e22c9b45ea2c45b41d7c2fccfa22c8820a296d0b636433', 'amount': Decimal('44.32452045'), 'confirmations': 495L, 'address': u'1EZNuddPnaZFah9QVbGvzvTcP4KeRrRFt8', 'spendable': False, 'scriptPubKey': '76a91494b93bbe8a32f1db80b307482e83c25fa4e99b8c88ac'}] -{u'1J79LtWctedRLnMfFNRgzzSFsozQqDeoKD': ('98831F3A:3', u''), u'1EZNuddPnaZFah9QVbGvzvTcP4KeRrRFt8': (u'98831F3A:500', u'Test Wallet'), u'1GfuYaKHrhdiVybXMGCcjadSgfjvpdt2x9': ('98831F3A:2', u'')} -3SBcsGkhcKRVB2gr98BmscU8HtWJ12HTXpJa5XmvbEUateQ3bJBEgvLd5kPGAzg1rFkzjVpZJgiKGwvnq5mJpwnbJqcHpVEAopWyALDmtjrDwEvPiTY diff --git a/test/ref/wallet-enc.dat b/test/ref/wallet-enc.dat deleted file mode 100644 index 15bc741d..00000000 Binary files a/test/ref/wallet-enc.dat and /dev/null differ diff --git a/test/ref/wallet-unenc.dat b/test/ref/wallet-unenc.dat deleted file mode 100644 index 54eb57d5..00000000 Binary files a/test/ref/wallet-unenc.dat and /dev/null differ diff --git a/test/test.py b/test/test.py index f43fd11c..20df29e1 100755 --- a/test/test.py +++ b/test/test.py @@ -22,6 +22,22 @@ test/test.py: Test suite for the MMGen suite import sys,os +def run_in_tb(): + fn = sys.argv[0] + source = open(fn) + try: + exec source in {'inside_tb':1} + except SystemExit: + pass + except: + def color(s): return '\033[36;1m' + s + '\033[0m' + e = sys.exc_info() + sys.stdout.write(color('\nTest script returned: %s\n' % (e[0].__name__))) + +if not 'inside_tb' in globals() and 'MMGEN_TEST_TRACEBACK' in os.environ: + run_in_tb() + sys.exit() + pn = os.path.dirname(sys.argv[0]) os.chdir(os.path.join(pn,os.pardir)) sys.path.__setitem__(0,os.path.abspath(os.curdir)) @@ -39,7 +55,7 @@ scripts = ( 'walletchk', 'walletconv', 'walletgen' ) -tb_cmd = 'scripts/traceback.py' +tb_cmd = 'scripts/traceback.py' hincog_fn = 'rand_data' hincog_bytes = 1024*1024 hincog_offset = 98765 @@ -114,19 +130,31 @@ cfgs = { }, '4': { 'tmpdir': os.path.join('test','tmp4'), - 'wpasswd': 'Hashrate rising', + 'wpasswd': 'Hashrate good', 'addr_idx_list': '63,1004,542-544,7-9', # 8 addresses 'seed_len': 192, 'dep_generators': { 'mmdat': 'walletgen4', 'mmbrain': 'walletgen4', 'addrs': 'addrgen4', - 'rawtx': 'txcreate4', - 'sigtx': 'txsign4', + 'rawtx': 'txcreate4', + 'sigtx': 'txsign4', }, 'bw_filename': 'brainwallet.mmbrain', 'bw_params': '192,1', }, + '14': { + 'kapasswd': 'Maxwell', + 'tmpdir': os.path.join('test','tmp14'), + 'wpasswd': 'The Halving', + 'addr_idx_list': '61,998,502-504,7-9', # 8 addresses + 'seed_len': 256, + 'dep_generators': { + 'mmdat': 'walletgen14', + 'addrs': 'addrgen14', + 'akeys.mmenc': 'keyaddrgen14', + }, + }, '5': { 'tmpdir': os.path.join('test','tmp5'), 'wpasswd': 'My changed password', @@ -141,8 +169,8 @@ cfgs = { 'seed_len': 128, 'seed_id': 'FE3C6545', 'ref_bw_seed_id': '33F10310', - 'addrfile_chk': 'B230 7526 638F 38CB 8FDC 8B76', - 'keyaddrfile_chk': 'CF83 32FB 8A8B 08E2 0F00 D601', + 'addrfile_chk': 'B230 7526 638F 38CB', + 'keyaddrfile_chk': 'CF83 32FB 8A8B 08E2', 'wpasswd': 'reference password', 'ref_wallet': 'FE3C6545-D782B529[128,1].mmdat', 'ic_wallet': 'FE3C6545-E29303EA-5E229E30[128,1].mmincog', @@ -167,8 +195,8 @@ cfgs = { 'seed_len': 192, 'seed_id': '1378FC64', 'ref_bw_seed_id': 'CE918388', - 'addrfile_chk': '8C17 A5FA 0470 6E89 3A87 8182', - 'keyaddrfile_chk': '9648 5132 B98E 3AD9 6FC3 C5AD', + 'addrfile_chk': '8C17 A5FA 0470 6E89', + 'keyaddrfile_chk': '9648 5132 B98E 3AD9', 'wpasswd': 'reference password', 'ref_wallet': '1378FC64-6F0F9BB4[192,1].mmdat', 'ic_wallet': '1378FC64-2907DE97-F980D21F[192,1].mmincog', @@ -193,14 +221,14 @@ cfgs = { 'seed_len': 256, 'seed_id': '98831F3A', 'ref_bw_seed_id': 'B48CD7FC', - 'addrfile_chk': '6FEF 6FB9 7B13 5D91 854A 0BD3', - 'keyaddrfile_chk': '9F2D D781 1812 8BAD C396 9DEB', + 'addrfile_chk': '6FEF 6FB9 7B13 5D91', + 'keyaddrfile_chk': '9F2D D781 1812 8BAD', 'wpasswd': 'reference password', 'ref_wallet': '98831F3A-27F2BF93[256,1].mmdat', 'ref_addrfile': '98831F3A[1,31-33,500-501,1010-1011].addrs', 'ref_keyaddrfile': '98831F3A[1,31-33,500-501,1010-1011].akeys.mmenc', - 'ref_addrfile_chksum': '6FEF 6FB9 7B13 5D91 854A 0BD3', - 'ref_keyaddrfile_chksum': '9F2D D781 1812 8BAD C396 9DEB', + 'ref_addrfile_chksum': '6FEF 6FB9 7B13 5D91', + 'ref_keyaddrfile_chksum': '9F2D D781 1812 8BAD', # 'ref_fake_unspent_data':'98831F3A_unspent.json', 'ref_tx_file': 'FFB367[1.234].rawtx', @@ -285,10 +313,13 @@ cmd_group['main'] = OrderedDict([ ['txcreate3', (3,'tx creation with inputs and outputs from two wallets', [[['addrs'],1],[['addrs'],3]])], ['txsign3', (3,'tx signing with inputs and outputs from two wallets',[[['mmdat'],1],[['mmdat','rawtx'],3]])], + ['walletgen14', (14,'wallet generation (14)', [[[],14]],14)], + ['addrgen14', (14,'address generation (14)', [[['mmdat'],14]])], + ['keyaddrgen14',(14,'key-address file generation (14)', [[['mmdat'],14]],14)], ['walletgen4',(4,'wallet generation (4) (brainwallet)', [])], ['addrgen4', (4,'address generation (4)', [[['mmdat'],4]])], - ['txcreate4', (4,'tx creation with inputs and outputs from four seed sources, plus non-MMGen inputs and outputs', [[['addrs'],1],[['addrs'],2],[['addrs'],3],[['addrs'],4]])], - ['txsign4', (4,'tx signing with inputs and outputs from incog file, mnemonic file, wallet and brainwallet, plus non-MMGen inputs and outputs', [[['mmincog'],1],[['mmwords'],2],[['mmdat'],3],[['mmbrain','rawtx'],4]])], + ['txcreate4', (4,'tx creation with inputs and outputs from four seed sources, key-address file and non-MMGen inputs and outputs', [[['addrs'],1],[['addrs'],2],[['addrs'],3],[['addrs'],4],[['addrs','akeys.mmenc'],14]])], + ['txsign4', (4,'tx signing with inputs and outputs from incog file, mnemonic file, wallet, brainwallet, key-address file and non-MMGen inputs and outputs', [[['mmincog'],1],[['mmwords'],2],[['mmdat'],3],[['mmbrain','rawtx'],4],[['akeys.mmenc'],14]])], ]) cmd_group['tool'] = OrderedDict([ @@ -447,6 +478,7 @@ opts_data = { -I, --non-interactive Non-interactive operation (MS Windows mode) -p, --pause Pause between tests, resuming on keypress. -q, --quiet Produce minimal output. Suppress dependency info. +-r, --resume=c Resume at command 'c' after interrupted run -s, --system Test scripts and modules installed on system rather than those in the repo root. -S, --skip-deps Skip dependency checking for command @@ -460,6 +492,8 @@ If no command is given, the whole suite of tests is run. } cmd_args = opts.init(opts_data) + +if opt.resume: opt.skip_deps = True if opt.log: log_fd = open(log_file,'a') log_fd.write('\nLog started: %s\n' % make_timestr()) @@ -470,6 +504,8 @@ ni = bool(opt.non_interactive) # Disable MS color in spawned scripts due to bad interactions os.environ['MMGEN_NOMSCOLOR'] = '1' os.environ['MMGEN_NOLICENSE'] = '1' +os.environ['MMGEN_DISABLE_COLOR'] = '1' +os.environ['MMGEN_MIN_URANDCHARS'] = '3' if opt.debug_scripts: os.environ['MMGEN_DEBUG'] = '1' @@ -599,11 +635,19 @@ def get_file_with_ext(ext,mydir,delete=True,no_dot=False): else: return flist[0] +def find_generated_exts(cmd): + out = [] + for k in cfgs: + for ext,prog in cfgs[k]['dep_generators'].items(): + if prog == cmd: + out.append((ext,cfgs[k]['tmpdir'])) + return out + def get_addrfile_checksum(display=False): addrfile = get_file_with_ext('addrs',cfg['tmpdir']) silence() - from mmgen.addr import AddrInfo - chk = AddrInfo(addrfile).checksum + from mmgen.addr import AddrList + chk = AddrList(addrfile).chksum if opt.verbose and display: msg('Checksum: %s' % cyan(chk)) end_silence() return chk @@ -622,6 +666,9 @@ class MMGenExpect(object): mmgen_cmd = os.path.join(os.curdir,mmgen_cmd) desc = (cmd_data[name][1],name)[bool(opt.names)] if extra_desc: desc += ' ' + extra_desc + for i in cmd_args: + if type(i) not in (str,unicode): + die(2,'Error: missing input files in cmd line?:\n%s' % cmd_args) cmd_str = mmgen_cmd + ' ' + ' '.join(cmd_args) if opt.log: log_fd.write(cmd_str+'\n') @@ -737,30 +784,59 @@ class MMGenExpect(object): def read(self,n=None): return self.p.read(n) -from decimal import Decimal +from mmgen.obj import BTCAmt from mmgen.bitcoin import verify_addr -def add_fake_unspent_entry(out,address,comment): - out.append({ - 'account': unicode(comment), +def create_fake_unspent_entry(address,sid=None,idx=None,lbl=None,non_mmgen=None): + if lbl: lbl = ' ' + lbl + return { + 'account': (non_mmgen or ('%s:%s%s' % (sid,idx,lbl))).decode('utf8'), 'vout': int(getrandnum(4) % 8), - 'txid': unicode(hexlify(os.urandom(32))), - 'amount': Decimal('%s.%s' % (10+(getrandnum(4) % 40), getrandnum(4) % 100000000)), + 'txid': hexlify(os.urandom(32)).decode('utf8'), + 'amount': BTCAmt('%s.%s' % (10+(getrandnum(4) % 40), getrandnum(4) % 100000000)), 'address': address, 'spendable': False, 'scriptPubKey': ('76a914'+verify_addr(address,return_hex=True)+'88ac'), - 'confirmations': getrandnum(4) % 500 - }) + 'confirmations': getrandnum(4) % 50000 + } +labels = [ + "Automotive", + "Travel expenses", + "Healthcare", + "Freelancing 1", + "Freelancing 2", + "Alice's assets", + "Bob's bequest", + "House purchase", + "Real estate fund", + "Job 1", + "XYZ Corp.", + "Eddie's endowment", + "Emergency fund", + "Real estate fund", + "Ian's inheritance", + "", + "Rainy day", + "Fred's funds", + "Job 2", + "Carl's capital", +] +label_iter = None def create_fake_unspent_data(adata,unspent_data_file,tx_data,non_mmgen_input=''): out = [] for s in tx_data: sid = tx_data[s]['sid'] - a = adata.addrinfo(sid) + a = adata.addrlist(sid) for n,(idx,btcaddr) in enumerate(a.addrpairs(),1): - lbl = ('',' addr %02i' % n)[bool(n%3)] - add_fake_unspent_entry(out,btcaddr,'%s:%s%s' % (sid,idx,lbl)) + while True: + try: lbl = next(label_iter) + except: label_iter = iter(labels) + else: break + out.append(create_fake_unspent_entry(btcaddr,sid,idx,lbl)) + if n == 1: # create a duplicate address. This means addrs_per_wallet += 1 + out.append(create_fake_unspent_entry(btcaddr,sid,idx,lbl)) if non_mmgen_input: from mmgen.bitcoin import privnum2addr,hextowif @@ -770,7 +846,7 @@ def create_fake_unspent_data(adata,unspent_data_file,tx_data,non_mmgen_input='') write_data_to_file(of, hextowif('{:064x}'.format(privnum), compressed=True)+'\n','compressed bitcoin key',silent=True) - add_fake_unspent_entry(out,btcaddr,'Non-MMGen address') + out.append(create_fake_unspent_entry(btcaddr,non_mmgen='Non-MMGen address')) # msg('\n'.join([repr(o) for o in out])); sys.exit() write_data_to_file(unspent_data_file,repr(out),'Unspent outputs',silent=True) @@ -779,11 +855,12 @@ def create_fake_unspent_data(adata,unspent_data_file,tx_data,non_mmgen_input='') def add_comments_to_addr_file(addrfile,outfile): silence() msg(green("Adding comments to address file '%s'" % addrfile)) - from mmgen.addr import AddrInfo - a = AddrInfo(addrfile) + from mmgen.addr import AddrList + a = AddrList(addrfile) for n,idx in enumerate(a.idxs(),1): if n % 2: a.set_comment(idx,'Test address %s' % n) - write_data_to_file(outfile,a.fmt_data(enable_comments=True),silent=True) + a.format(enable_comments=True) + write_data_to_file(outfile,a.fmt_data,silent=True) end_silence() def make_brainwallet_file(fn): @@ -928,12 +1005,26 @@ class MMGenTestSuite(object): if ni and (len(cmd_data[cmd]) < 4 or cmd_data[cmd][3] != 1): return + # delete files produced by this cmd +# for ext,tmpdir in find_generated_exts(cmd): +# print cmd, get_file_with_ext(ext,tmpdir) + d = [(str(num),ext) for exts,num in cmd_data[cmd][2] for ext in exts] + + # delete files depended on by this cmd al = [get_file_with_ext(ext,cfgs[num]['tmpdir']) for num,ext in d] global cfg cfg = cfgs[str(cmd_data[cmd][0])] + if opt.resume: + if cmd == opt.resume: + msg(yellow("Resuming at '%s'" % cmd)) + opt.resume = False + opt.skip_deps = False + else: + return + self.__class__.__dict__[cmd](*([self,cmd] + al)) def generate_file_deps(self,cmd): @@ -951,7 +1042,7 @@ class MMGenTestSuite(object): def walletgen(self,name,seed_len=None): write_to_tmpfile(cfg,pwfile,cfg['wpasswd']+'\n') - add_args = (['-r10'], + add_args = (['-r5'], ['-q','-r0','-L','NI Wallet','-P',get_tmpfile_fn(cfg,pwfile)])[bool(ni)] args = ['-d',cfg['tmpdir'],'-p1'] if seed_len: args += ['-l',str(seed_len)] @@ -977,7 +1068,7 @@ class MMGenTestSuite(object): add_args = ['-r0', '-q', '-P%s' % get_tmpfile_fn(cfg,pwfile), get_tmpfile_fn(cfg,bf)] else: - add_args = ['-r10'] + add_args = ['-r5'] t = MMGenExpect(name,'mmgen-walletconv', args + add_args) if ni: return t.license() @@ -1038,8 +1129,8 @@ class MMGenTestSuite(object): def walletchk_newpass (self,name,wf,pf): return self.walletchk(name,wf,pf,pw=True) - def addrgen(self,name,wf,pf,check_ref=False): - add_args = ([],['-P',pf,'-q'])[ni] + def addrgen(self,name,wf,pf=None,check_ref=False): + add_args = ([],['-q'] + ([],['-P',pf])[bool(pf)])[ni] t = MMGenExpect(name,'mmgen-addrgen', add_args + ['-d',cfg['tmpdir'],wf,cfg['addr_idx_list']]) if ni: return @@ -1074,14 +1165,14 @@ class MMGenTestSuite(object): def txcreate_common(self,name,sources=['1'],non_mmgen_input=''): if opt.verbose or opt.exact_output: - sys.stderr.write(green('Generating fake transaction info\n')) + sys.stderr.write(green('Generating fake tracking wallet info\n')) silence() - from mmgen.addr import AddrInfo,AddrInfoList - tx_data,ail = {},AddrInfoList() + from mmgen.addr import AddrList,AddrData + tx_data,ad = {},AddrData() for s in sources: afile = get_file_with_ext('addrs',cfgs[s]['tmpdir']) - ai = AddrInfo(afile) - ail.add(ai) + ai = AddrList(afile) + ad.add(ai) aix = parse_addr_idxs(cfgs[s]['addr_idx_list']) if len(aix) != addrs_per_wallet: errmsg(red('Address index list length != %s: %s' % @@ -1089,13 +1180,15 @@ class MMGenTestSuite(object): sys.exit() tx_data[s] = { 'addrfile': afile, - 'chk': ai.checksum, + 'chk': ai.chksum, 'sid': ai.seed_id, 'addr_idxs': aix[-2:], } unspent_data_file = os.path.join(cfg['tmpdir'],'unspent.json') - create_fake_unspent_data(ail,unspent_data_file,tx_data,non_mmgen_input) + create_fake_unspent_data(ad,unspent_data_file,tx_data,non_mmgen_input) + if opt.verbose or opt.exact_output: + sys.stderr.write("Fake transaction wallet data written to file '%s'\n" % unspent_data_file) # make the command line from mmgen.bitcoin import privnum2addr @@ -1144,8 +1237,8 @@ class MMGenTestSuite(object): t.expect('Continue anyway? (y/N): ','y') t.expect(r"'q' = quit sorting, .*?: ",'M', regex=True) t.expect(r"'q' = quit sorting, .*?: ",'q', regex=True) - outputs_list = [addrs_per_wallet*i + 1 for i in range(len(tx_data))] - if non_mmgen_input: outputs_list.append(len(tx_data)*addrs_per_wallet + 1) + outputs_list = [(addrs_per_wallet+1)*i + 1 for i in range(len(tx_data))] + if non_mmgen_input: outputs_list.append(len(tx_data)*(addrs_per_wallet+1) + 1) t.expect('Enter a range or space-separated list of outputs to spend: ', ' '.join([str(i) for i in outputs_list])+'\n') if non_mmgen_input: t.expect('Accept? (y/N): ','y') @@ -1229,7 +1322,7 @@ class MMGenTestSuite(object): self.export_seed(name,wf,desc='mnemonic data',out_fmt='words') def export_incog(self,name,wf,desc='incognito data',out_fmt='i',add_args=[]): - uargs = ['-p1','-r10'] + add_args + uargs = ['-p1','-r5'] + add_args self.walletconv_export(name,wf,desc=desc,out_fmt=out_fmt,uargs=uargs,pw=True) ok() @@ -1282,12 +1375,12 @@ class MMGenTestSuite(object): self.addrgen_incog(name,[],'',in_fmt='hi',desc='hidden incognito data', args=['-H','%s,%s'%(rf,hincog_offset),'-l',str(hincog_seedlen)]) - def keyaddrgen(self,name,wf,pf,check_ref=False): + def keyaddrgen(self,name,wf,pf=None,check_ref=False): args = ['-d',cfg['tmpdir'],wf,cfg['addr_idx_list']] if ni: m = "\nAnswer 'n' at the interactive prompt" msg(grnbg(m)) - args = ['-q','-P',pf] + args + args = ['-q'] + ([],['-P',pf])[bool(pf)] + args t = MMGenExpect(name,'mmgen-keygen', args) if ni: return t.license() @@ -1308,8 +1401,8 @@ class MMGenTestSuite(object): def txsign_keyaddr(self,name,keyaddr_file,txfile): t = MMGenExpect(name,'mmgen-txsign', ['-d',cfg['tmpdir'],'-M',keyaddr_file,txfile]) t.license() - t.hash_preset('key-address file','1') - t.passphrase('key-address file',cfg['kapasswd']) + t.hash_preset('key-address data','1') + t.passphrase('key-address data',cfg['kapasswd']) t.expect('Check key-to-address validity? (y/N): ','y') t.tx_view() self.txsign_end(t) @@ -1359,7 +1452,7 @@ class MMGenTestSuite(object): bwf = os.path.join(cfg['tmpdir'],cfg['bw_filename']) make_brainwallet_file(bwf) seed_len = str(cfg['seed_len']) - args = ['-d',cfg['tmpdir'],'-p1','-r10','-l'+seed_len,'-ib'] + args = ['-d',cfg['tmpdir'],'-p1','-r5','-l'+seed_len,'-ib'] t = MMGenExpect(name,'mmgen-walletconv', args + [bwf]) t.license() t.passphrase_new('new MMGen wallet',cfg['wpasswd']) @@ -1371,14 +1464,19 @@ class MMGenTestSuite(object): def addrgen4(self,name,wf): self.addrgen(name,wf,pf='') - def txcreate4(self,name,f1,f2,f3,f4): - self.txcreate_common(name,sources=['1','2','3','4'],non_mmgen_input='4') + def txcreate4(self,name,f1,f2,f3,f4,f5,f6): + self.txcreate_common(name,sources=['1','2','3','4','14'],non_mmgen_input='4') - def txsign4(self,name,f1,f2,f3,f4,f5): + def txsign4(self,name,f1,f2,f3,f4,f5,f6): non_mm_fn = os.path.join(cfg['tmpdir'],non_mmgen_fn) - t = MMGenExpect(name,'mmgen-txsign', - ['-d',cfg['tmpdir'],'-i','brain','-b'+cfg['bw_params'],'-p1','-k',non_mm_fn,f1,f2,f3,f4,f5]) + a = ['-d',cfg['tmpdir'],'-i','brain','-b'+cfg['bw_params'],'-p1','-k',non_mm_fn,'-M',f6,f1,f2,f3,f4,f5] + t = MMGenExpect(name,'mmgen-txsign',a) t.license() + + t.hash_preset('key-address data','1') + t.passphrase('key-address data',cfgs['14']['kapasswd']) + t.expect('Check key-to-address validity? (y/N): ','y') + t.tx_view() for cnum,desc in ('1','incognito data'),('3','MMGen wallet'): @@ -1530,12 +1628,16 @@ class MMGenTestSuite(object): pf = None self.walletchk(name,wf,pf=pf,pw=True,sid=cfg['seed_id']) - def ref_seed_chk(self,name,ext=g.seed_ext): + from mmgen.seed import SeedFile + def ref_seed_chk(self,name,ext=SeedFile.ext): wf = os.path.join(ref_dir,'%s.%s' % (cfg['seed_id'],ext)) - desc = ('mnemonic data','seed data')[ext==g.seed_ext] + from mmgen.seed import SeedFile + desc = ('mnemonic data','seed data')[ext==SeedFile.ext] self.walletchk(name,wf,pf=None,desc=desc,sid=cfg['seed_id']) - def ref_mn_chk(self,name): self.ref_seed_chk(name,ext=g.mn_ext) + def ref_mn_chk(self,name): + from mmgen.seed import Mnemonic + self.ref_seed_chk(name,ext=Mnemonic.ext) def ref_brain_chk(self,name,bw_file=ref_bw_file): wf = os.path.join(ref_dir,bw_file) @@ -1593,7 +1695,7 @@ class MMGenTestSuite(object): msg(grnbg('%s %s' % (m,n))) return if ftype == 'keyaddr': - w = 'key-address file' + w = 'key-address data' t.hash_preset(w,ref_kafile_hash_preset) t.passphrase(w,ref_kafile_pass) t.expect('Check key-to-address validity? (y/N): ','y') @@ -1631,7 +1733,7 @@ class MMGenTestSuite(object): # wallet conversion tests def walletconv_in(self,name,infile,desc,uopts=[],pw=False,oo=False): - opts = ['-d',cfg['tmpdir'],'-o','words','-r10'] + opts = ['-d',cfg['tmpdir'],'-o','words','-r5'] if_arg = [infile] if infile else [] d = '(convert)' if ni: @@ -1685,7 +1787,7 @@ class MMGenTestSuite(object): rd = os.urandom(ref_wallet_incog_offset+128) write_to_tmpfile(cfg,hincog_fn,rd) else: - aa = ['-r10'] + aa = ['-r5'] infile = os.path.join(ref_dir,cfg['seed_id']+'.mmwords') t = MMGenExpect(name,'mmgen-walletconv',aa+opts+[infile],extra_desc='(convert)') @@ -1696,7 +1798,7 @@ class MMGenTestSuite(object): pf = get_tmpfile_fn(cfg,pfn) if desc != 'hidden incognito data': from mmgen.seed import SeedSource - ext = SeedSource.fmt_code_to_sstype(out_fmt).ext + ext = SeedSource.fmt_code_to_type(out_fmt).ext hps = ('',',1')[bool(pw)] # TODO real hp pre_ext = '[%s%s].' % (cfg['seed_len'],hps) wf = get_file_with_ext(pre_ext+ext,cfg['tmpdir'],no_dot=True) @@ -1755,6 +1857,8 @@ class MMGenTestSuite(object): for i in ('1','2','3'): locals()[k+i] = locals()[k] + for k in ('walletgen','addrgen','keyaddrgen'): locals()[k+'14'] = locals()[k] + # main() if opt.pause: @@ -1809,10 +1913,13 @@ try: clean() for cmd in cmd_data: if cmd[:5] == 'info_': - msg(green('\nTesting ' + cmd_data[cmd][0])) + msg(green('%sTesting %s' % (('\n','')[bool(opt.resume)],cmd_data[cmd][0]))) continue ts.do_cmd(cmd) if cmd is not cmd_data.keys()[-1]: do_between() +except KeyboardInterrupt: + die(1,'\nExiting at user request') + raise except: sys.stderr = stderr_save raise diff --git a/test/tooltest.py b/test/tooltest.py index a39706c3..868132ed 100755 --- a/test/tooltest.py +++ b/test/tooltest.py @@ -105,7 +105,7 @@ cfg = { 'refdir': 'test/ref', 'txfile': 'FFB367[1.234].rawtx', 'addrfile': '98831F3A[1,31-33,500-501,1010-1011].addrs', - 'addrfile_chk': '6FEF 6FB9 7B13 5D91 854A 0BD3', + 'addrfile_chk': '6FEF 6FB9 7B13 5D91', } opts_data = {