From 680ea8a5fc1b0d6ecac2341eb0935fdd0c4163a8 Mon Sep 17 00:00:00 2001 From: philemon Date: Tue, 26 Jul 2016 22:16:25 +0300 Subject: [PATCH] OO rewrite mostly done Colored output --- mmgen/addr.py | 794 ++++++++++++++++++--------------- mmgen/bitcoin.py | 7 +- mmgen/crypto.py | 9 - mmgen/filename.py | 24 +- mmgen/globalvars.py | 58 +-- mmgen/main_addrgen.py | 36 +- mmgen/main_addrimport.py | 19 +- mmgen/main_txcreate.py | 88 ++-- mmgen/main_txsign.py | 194 +++----- mmgen/main_wallet.py | 10 +- mmgen/obj.py | 355 ++++++++++++++- mmgen/opts.py | 8 +- mmgen/rpc.py | 13 +- mmgen/seed.py | 105 +++-- mmgen/tool.py | 126 +++--- mmgen/tw.py | 250 ++++++----- mmgen/tx.py | 375 +++++++--------- mmgen/util.py | 142 ++---- scripts/compute-file-chksum.py | 23 + scripts/tx-old2new.py | 36 +- test/ref/FFB367[1.234].rawtx | 6 + test/ref/tx_FFB367[1.234].raw | 5 - test/ref/wallet-enc.dat | Bin 61440 -> 0 bytes test/ref/wallet-unenc.dat | Bin 86016 -> 0 bytes test/test.py | 233 +++++++--- test/tooltest.py | 2 +- 26 files changed, 1633 insertions(+), 1285 deletions(-) create mode 100755 scripts/compute-file-chksum.py create mode 100644 test/ref/FFB367[1.234].rawtx delete mode 100644 test/ref/tx_FFB367[1.234].raw delete mode 100644 test/ref/wallet-enc.dat delete mode 100644 test/ref/wallet-unenc.dat 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 15bc741d7c35ec6dcdd7d6be932375ac953b3dac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61440 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 diff --git a/test/ref/wallet-unenc.dat b/test/ref/wallet-unenc.dat deleted file mode 100644 index 54eb57d5be298a3ddf71c7bf4488cca05da56409..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 86016 zcmeFa2{={V+dqCL9LGEqMHw??YC>o*Mwya1Vg`?)zRC3183m56Q?6uDSU?#Kc^f zV_=SfIR@q!m}6j$fjI`|7?@*Vj)6G_<`|e`fO;7qOwROi81R3Wo%8NVL7}-nw0Hlf z{eO5*AI=d!`0zLLe>@yv?*IQ82C6}rP1w2L{>;oAFaLdBAWq*d!C=fTUMn&%dRhpl z0NtMpa}3NeFvGxRS`5awf3BU(Ff#XPj)C795C;QN=v@M}mAUuH@BGZ&nPb2g=zNQT zb~4Ar|9=>O`a)>_kDRZwVWOCE_yLyd%<3%JECbA^St^;`S%@sMETt?_%pokiEGf)p zEU%e487!H_m~$EZ83_z^Br|$p`fNI1It*Erz+wnkI)VVsuf*}d_t&?xjZ?Ors10MF!zGNplByMZY2F{S zb^Zv~>EF@DZ{@uHa8)Zs>F!hOs5&_n6_EUSVZ0y)HM1Gka%)U2*!jip2e`hKJ^8xN zzth3*uXtm`V4*4lb6qWg!P0@ayBs|2{wn@&WwrIq z*JnTYy|(W-*^qU`>8X&ptVxg8%^i8mKb!iFy_aJ{lob(th61vzwimb>;F30c4DI65 zyiUx&A(E|Q_|%lGtL0iU4?{*4D|M9>T4u#@c`s`3m)blUms-QZ(V}>u)Rw2sPq=u9 zKSU{Nv;6Gd5J$jS;WKIjH1%L=dgKy}RG2jlTXUYSKT!+JjaO@iNg6w7w) zCX)8QUfG`o(uCU;9rz3dWLbT@dh%TCal4>-H`(uT@HD3uVD*d~2JbJA9rwNI{rrX< zRh1QLW>3Cr@xhENHz7PdpL_q_J0AZ*&G58K7q+*LFJ{pUOiQb-hT9b?6yTbotjOBc z-^8aZE8N<)U1gh6f(Y%)zN%Q2>a}X+2{$$Q?Vww;G{1b~njLFFns8ZxY2;);men#1 zjYzB&cL3v+*w^0H*7)=JwI1W2GyR+eKX3Q=K;ubOWrdnq))oA!{!S@d-ZV!ET8>(5 z4r^j5YOb${Xb8Tk?W0N;H&a%qP$*^fH}QWft9?rXqx@g8U)#^dk)bWv;ID7-_!M9K zmBAgsbbg-?mU4l)3tUz-e<-UJsz#9w#mn-YUS)UP#kpSLv+umEaA9HJk=+8B@hkgm zsjIBeGCR;+KzRTBYs~hhFE3WDzdMB6Qjq^Xj@9ba{hvHcKNXb`WrYTXR#txz|F^OV z+<5gvEYpI(&l1|@Cuj?43}2iX9zV{5Y2#N{bXfH6Iig(={!msp@v837W%sNtFpW0X z)a5Tp>wglLpwG%$!M{{UVd>p<)Kyk!nXTWnS?uaYqCUgL%3D&3^Ny(X1bwzT^73xt z)6I;R0>X_DWrYTXR#s^7K^vhgEBpxe1q=UO4XyJ9&VRB@Jbc>jYMRxs&xpv23EsgS zr;e>alodP%M)raIM@3b<1%|`Zuvu00e!+zgoYr1_c7KU}EWe+~8atu)R8>}}nN_>> z@Q8dOO&z-g@zI@6Svo_a-<{5Mc~|q|ETW^TTSsTg3Ka^ato|ncZ)MfG*WK!3*&8?R z%&6*y(KC#z69y!{e78R)$n}MAR=%q|YU9N2o8#xef5)@$vERg9*M*qaTE=H}P6zxnWf{wj+o zD>NvyvighoQ)RVudVckML3TubbNfd7Ju9X6A2OEK_Tc2zi)`Do?}2r}2Kw=4L|H+T z1t=iPs!z{lq;JqJJ6E1S=XH3vTxi_0pAO7KVSDq*$q0S~OAGFC{ECj5oUG|hOhC`E zPu=@9Y2ll^2h9_p@ax`a2?KZzGG&Lt^aKU3J(&(X)%^8r$z*)yUyuHZj}KCT(q@{v zH<|Ww`#n(FjAYMUqlfM>An)M}G@-OfQ}?FR#_bb;o`vU|zw)Tb38e$iYbMi~hs8qA z!Q9uNG&Ln}-S*iQp<&?(uVe^B|r-yvREF{QLvnnsov z%y;NYX|tJgnL?Pt87&!Y8CEhVkQhm<^c}=5x_H{tH0lH`Jo4VtLR-XsT?)XG1s5Ek z{uSfPaVn+kq>EJ-;ZID}Q>O7Wq5!vsuTrqGjL)^lUCS`t!0NL4_#*WL><@ z6?<)Gs`_gBQn)P>UDZ;n!4IP&1qIV^$G@IyTo5P==Q6p zJR~I{ctV5vPRpDm@)1IpHHW>;RM@x%hS5@9?4gBiAjX`Q5R3_ zyq^go#x<<=W*)e(=)zT_Av#}b4AVc2uW^%*KUg@PvwH7YE15B&VUgvm(w6kvH>#Yn z>6#7oT6F4uB&^hNK=6bHMZwcn(IY=fIEBP)@tEoyMeBQ8Y}n1%6^?dT73xd5oOe*B zE}o=!iLNqVwl|V9jqWxHf6BTwC8@{J)5M2|CZvCO!y@04freNHs|}3TgZFgO5A*s* zFUyp!h-0yxH~!SuWa6tlf+sX63Z5*rR`_)qUsM<9XklS8d2?c7{HRc8r8c+r@y>fO z$F44?E}rP-*9)ds3|7+eG1MLscE?}KUCcld56}$Ec+hz9NBN0pUukU9BdO2HCthu& zE6Z*1kJ-^={nS#nMK;rEb@}mk2%gZOD0qsoT5(ZQ+61fiR0FepJALHwjOZovj@i&2 zsVyooO{Tp-T|6zyvueHRmhUpGQOxpSSCEnV*$taK3LDSz3~?0}nd8PR-#suM9a!74 zQLIE%s*KhB@uS`4hSwjQ@@4+SCT3r90KpR)6a`NVJ09alJF>hR75T4My#GvMkL#56 z;|*(eP!#krZdL4|E}poSXY<}D;?PZXS)t{yRSnMnWvx zWnV68TUo+%`o&#lmGDvO;)$~`n3?AInB@Y=p9IF7YS9hptsT$r+-hJ?x*CW9TgUKF??S!Q|le@Ezlr$symZRdPL>o%p$y-Fn{r zmzY9)O{ylP(8kavlA=hGL|&pLNr%J>Rvk{$ zgwyz9ov@6AF?=}Q7yp{Rnm!Qcjw9d)aAVjWtS}uXT_SxHJtqx;CWa75u%y=k_>A-z z`VnFmv5g28r2b`?TKkfkUi&($l|C{QVODh3=Vqg{-@*MIkrUcU#rz>|bVb!C2Cs92 zbl|-jZonrLruO!vkn`!gB+r}uQZe5nUG#fs$Ct}3viQL2N9!Y8#5%0i-e=fFRrM0z zUn2NKsAO5?&o%jl31>%Ii)f z`^{6vJ~_E4&;5B+h{sI5M1?x?@)eRNwJMUqiI!B8mzTBlhDAT!Q3?(X`zU3Uh+B>Q zF`8y(LtQ+5X$?BD(1PvNlA@B9=YmHt^o(Cfi(_}B$gim6kR*usIA&#)M@-c0dsUIY zWqIg8Y_y-(;BCPKx0hVi*6C$Zm+b`L@dX|Y3X37DPrG@X7BmjAzV?rJ%0ql8w=)Ej z6Kxh~7AF-ZaZ6%-?*L?GiR-hZcwO8+x3h%RIcC!&Pt-cA6(NsC_gakoBotd+k?U-FMp) z#LMtK6Y*+SqmOBvqb{EM#7ZuIEFhLFPW`@1@U(oZtDJ9ONv!)5ajnQ7xBDu?)GZa_n(s53)k0A^MnQk^m>py z>1jPHz^n0CUoDj6J^ey6n?)^*Vf8ncJ&TnD8SdaxsG>e0<@JBi^Zb24CvET1p8-Al zS|c_t9aA8Ty^~2VORGK+k&{bZ<=T1iaEmG185$oSg7eO#I?)sw%)phmx7(X~o?5;$ zRjb(YF-|YAc7eH|)X!?K*q7{f7Y>_VK0N6CK6!rs4ke!pQnl0NiUtLA#gIJBy?17L zg;BhB=K6_Qx*UprV($Gs%d4K^{rvE@wxDu|?!@6QXRe>Cm5EVxIb?PEi>Urim9cgw z8MmX<)jxe&P$J-_LR#_jlTh9zH|az6o1VTiIc^!0-)%fF;UR=G^ivd%dF)829auH1cxbtR4$;cI`G($N*1&b$}W(RXDmAm!(CS-YV?Z_evI zUQ)j1$JtLin+o4^bDgeFXi#Qx*2wxarx!B2J`FTCh35Z|UIC=T0O>_6#Ef8m?f(y8 zM=z=B3HJX&G-Um-<&1LE_&r|QQ&qo=+xtyR=5c_xcacc=>&n=$*0iO?yPXck4ZA;6NEF{O zug{I=&d(>}{1?()WgcCNo{%;3ix2;_v(z&H<^6g&CWN{tW>1FwvW#~N11)A+IXrPA+@9R zY*t6d3`!W1V|8?Fwc_R<`H|;59^>H{) zT|C{!ynX)SM#DsA=Ap;sKUnpo^PFDU46muMUb5h0{HC&>HOVa39v?ftGN-^yyTNhS zyoZMzaQsCZxv?U*2D7Uw^szJj6B-oIWkS}c9(s+VC7+`^dsjJmim*JtZ_KW`aVX}v zvUJ+ZUKwkKBI@EP(YGwWX~VH<-RB0*dJfrv%f&m*AD)sDp^aewaIA{{MR0ZKK(S zm$4t+1>C$gaV4p)4KCc_R=<+Ec*?t77VH=%NB5y`WoSQ6wa!Q}nG~`A#>X$4TbT7t zY<{!`C0AyPJs+(VxT>5W~enB#Yg_@WL|7f)!L5^FA>yk6=Q)ikliVsWFcZN$-`WdR>A z^jW=?R5onA!gU8xpU|Kv>XZ2;nuHV5YkA(^b~Y0bKe0T!GF3u8991XGkx7c9?DH3h{9}#TbDTk+$-cD#SRdsvmehksUrv z`@`)FR%^^-SmqqWXi$R7UUjbAUM@1mhViVR_J~A*P*C)DI@9QF5B#u(QcmU^mhwHz zy0@0v5^WtXO;YR%c)TRuD2_I~Hp`WW4j z*TB)$S@qq*Hvec&H1CQO7X(jeP@p0rdBXMbAE(0?D-JXXY{0YXZ#k0V)bl*j`6GR2 ziLc;!Pa~@03HRoqM|)Af$K~9@&J!VvSRI>Mn6$*q`q_1(tQN`*wm*Kq?fgg}+x?W( zu#?>Te=6yQJUNm_x6u!`BbTY*XsL7F3{R*~6g+hWv#?@EZ#1X~tcy*2QyUiXW_?hW zXOMi(DXGX3=ZAUJ#nX7z8tw8G93Hz4DSaAmcQ!h*L#p0+i`@8^G8fNK6B$*8yQ2va zN4I4Bu)1a%Csn>*cKboBiz!n$kMDlzQ0es32%gZOmXNnQx%!o&59>MU_Xy{;A7A`! z@32ah#mYMyj(@4pNqe@=GL*V_YNu~tup1G*8f#AOID8}i;zT+X7op;pJ<7N2k zF&7NVXjzQC9@uOo_*Wb~>&3iObI`Kv*fP zx?vyBK8qjMz6E^$rlp%!oU4M5QQb{lJoUP4sCy~tmYBiiG?e6}btC}M^2+AX2+d%cRy#e`1vn%=iuUBRiOWZYlymd8c|{sZQi^q(mhtlLtN@c)OE?{ z8v7I@495w@Z|^_%)-`q1WqQ9x#L{l3t+LzpxKoU3_t(GLhIt{jpd-QjP>&*_KA}OS zHIE_t@j{Jw_YzLR&c>u3u8ge1r+%_(mwhL>ym_`!=k@r-!9&!=)39JRUBRUb|c#Iip3lA9+#^Hb+Qj5?`*O7dVcp7y9E{}3KX4s+}i9aEg!Ww zUBC0iwNEd`(cDC#B3Z3005ShZgQA$P&Asntd393Mr@3+FEJIcbp87)L@98_aETUQW z`Plb87aG_5(w!NpmmSKp5C&MVO_Rxs5rSIg#0k*^F73nUlGVsg5a;DT>LA zNsEb-iNI9HSjrd$JOOGkDlifl2N+5javA&?Tp1J?#25xh9i&`R3dxmZNfIM*lRD@d z=~L)q=phY#Zh9hpBe9$qLkuM95d8^Sgb{o>{yIJo?~Yf-OX5dxUAXJGRG{Z?i<89h z;<~Ueu&LO1tS#06%ZpW}lce*cwWo!w3@S-CNlr9oG~5ItA)Mq((jqC4h``>UlKv)r zIK3~u7QF&UYlK)yyh$vj%cTn^`T}o+uZh(lbyo*ZX?r`*Nd{(4H<;4wJ(bjG;u1Av2v9?K|;e zImg+3*aZ!06&`KKWIEPQ@QwZuEJ6GF@^PQVJAofX7y}HUCc{)-W-vBwY*-Q%TVuQQ zpxX^ifeFkCkzBjFft30tK3;~{E*~E;rYxD5ZygwJ zrXuN?m+0iAE`0Im$hNKCU%YExV=&VgV3_mAyr9lqfzA;uhumx!X!APYClSV2H46s* z!J)?B&87FxD{Ow2e0MNroTqy2F6CFjD|(A_FYPJRJgF6T55`zI3kD9&ZW{P%gfUi3 zVNBVlz-7xz<|hyh52yuFmIpJG}adyTJQz8i;xy*U0L6t`Nhvv%d9)L}v}Yp^@@+!Hf|(^M;<#N11N*IfOmiWaPtXMa-Rm<&gO$ z25j#G^GWV^@h|XxlL2jxx40t_mO*+-21*S4{b+GR|06L~DxB-jI{92*@@%|kw`lU< zyXNeQ!)mJr58in-UB{)!F{I&s1BYhw%Fi*~2ZHVS$uL#7U<`6D{sM0*A8>pDJenQD z`{W%k7fWZ$2byi%(6Xg4hQy!qkwVr+(C{#rK+1gJY%iL z=~XjGC#|8I8TXy_{X-obEQ9#p$bg0iFeqifUn9u4ci3#OE?3}Ul!Ib|>YTYdXK+x7aPf4B{Z&e8_(b}cp4m$Wr)mwB|xjIB6YkQmvlH{`cs6TM4ln}MR^S`214j3F`` zMrD$WqpOza(;#U-uJ)Y8Nu?xb%{I1ul@cpi&Zn~q_dW}p9+NGZ1p}vXK}TJ(BG}Wn z!7zezkI~A9W#>hVb(`sKte&`PYf<8f!HmIj2+x9ns;Z5dkwa(}44mbe`#3OGxh83q zCnl+4!?W<7AVI6@Cfo7@yhgE6H-bPsSPsEiFz{%01HfZ8j3F=^M#xeJwOctfDPXK% zf^oVQ&=URjNTIn(*p=XIPA)8ceurk{;GYdc_p}|(!M0z%p?Q2ac-Ql$G-a0SF3D`@ zc%b9p8l<)nY+J%|@R4Io+YIBuyH$z7h3Colvm5-tyL!iBo$fVzPO&>~mu#ioNZM-> zG9w4r%A5>Sb$uEG*I*s@E{yIGTW$LIx5Wukx52x1Sf)NIBvtW}WAM&~!Tdxl3%r|NNvifa ztGqae;~?Eyw7t6JokX_Ofw<8pfR8{JgJ%{D++VCQ!Wau@!x)`!j(hBUerFzoTddKe zXqOcphiO1tjd#`+<{v%p)&g9r!x#%@!$8w@0&`>-1GeSufN_yeSOmAGH4u`6Ofm7c;oHG5omS_y3{&erVqx())*O0U&*U*cTwA z2LSE=(*P(44e}-c)gIXUBDAV7>Ddta00rnFo(K%)QxxbI!ROCM4N9OP4j8TlLcc-x zkzi1Do|*0d7lBA$|9Am;o(y2~v7K}#_)PCa=VqT%p=#2iS!PYUBcqoH<(-BR6{3OH zdVq8gYlmFtH^Q_plIwaO3l=Agv*T=wikGIhz0BLjQ+ucHK+2&!>hjly53Mp>afgHD zQ?=(Yvw2D}JoIZMmRI_J(#Y8*9;r_IDdO>lOMK!lgNAkO4)2ovuwh`2C*PjS$r=5J zx-@EzYezgc)`jyA3RktP{En$lU}vIU2B7OSZ7cM<3_7xGz(gCj|*iQTUS2ek=^Lb zSTQ!9B~$fMy=vH2zim|#f4bPqX`axaz)1{9o=PqSRD9f_Qhxl5R3PyvxA(xh$f&I~ z@<~3&;?AyNA8Da3o-C^FolBpWn5^y>-26E7!1^oVfwHl=Yb@JV=0(Py*q(taB7c zUZ85ndC@-g^FG?Im=79^jZv3=Ener-+ZVBmSEznFz2NAYM>fUtFNg+re`i%#wQ8tp zDN|_iW*6D6WvW3U$1YY?e@(^hFD_k9^LFLqNR^C^o}JmMB{Tbws8HTIXOKKCT`%4E zMDz#iB3dRnmgQkRPYZ{hT`8-@+`fLXB2|R&%BNZ@PKs@8nDhGgVGi3l*AR zZz>H~sYJ{C!TfW*PFtwegFTgWDnZ9}YPNoJk&i9K7xNIW=$5uSglTU&kS05=zlKMH z0!OtVc?wC%V)a(3ynbq-klR6hiQIzHrzWe@8crW-yf(x@ zVciRQ&;qCYkfB2DjVIGjvMV_dy6yGtyFZ2H;=c&o=nQ|cKCS7?NP03)P+8f+7qrw> zpIi)_`en4gxNhJ}&rE;6V@2^=-PP_bdcrwL7kkI|N>sk!)LnwPST!W^tiC8AVM6<| z?S=fVQZXBW<|3{3ylVxBejE*oq96CWNm``Pr)KB<=(x^rO)=2$EeO>rP9^W zHY;2GtshbsPiUJvD;>+?atJ9G=m#&nXpPO;M`KJ1s#|rtZX(h@{PH`MDnxxkgQBQU zKC2?i5<3!XN^*xYO^IRaZ`z-5TkzAgMCIGFG44SY8tUQ+ZILtKT_r=RMYg9>y zhY{k+8~5ABG3nXM+Hy57D$A-y^iOC|6#bKu_mZRPNngcRy?!f?3#!_`JW+7_>tlxX zPH&92h%MX_PF*~qZ8*NpUbIu_#QR;k#sV^lZdxTOciiW{eKxq6G3nmI<#(W2;`I9t z4T@s?GS`3}D0L|F-FzbQ;|2lBmUkpni3o8*rG+ zj$saDHeg=Q%*u>muA?iZt79rCc}Ov_0tjOWC|c#tPS2veBIN$j9)q=m6z z+zvt`0m2q0a1!i^MnqvOCl*R$fY<{x0UC*5mrM8Q{Hgt_Wm9`zs>ppFRNr;rum_6x zN66x%jcG9$b&B{{WPDWHVX(q66(81HAdin`Qwz$UB0e)ReN>wPpr%3*pKQMmb*%b( z`k~15aZg5GD;Xd9a@Oe7Xyd$^H}WhNuRhdy=%3pWD_0%Kzx6OUiIF1x39|TTwzA+P zMvC~Sk@4}do9Z6Pl`6dQdU!o)UgRd#+$#Fw9;c+`{Q7K9Y>S>G zo_-DfO*AKeE3~ekBpD%w*;6SP{hZR#Ygk!0dzQ$^*^lrGl`r4QtWBW)Fjs*g6^SHkj-la(LZ{u(%jVM=~DKG~V2xWCvJ zqlkZ;EPb>+LU7nJ1%GN}@zHcPX5#-@{=e8yqe!1T|7f~rKo^-JK3V-i)k&P`>;EkO zU+gJPr4OI$MlSzfobfglA5NcKezg5+aAfpUe7ODn+p!+pe#q1R`*$@gKY9L3>0U;f z4Rl_!alYAH5oA&7a-gChQjc4`%`vjnpnn_o_x|JGj@4lK$>LybN$57%F^@xy;G*8Ow(e{tUF zKg$0XXWN1`GCM|7$cT#Spt4?L+Gi5%Y@BC-;i;jRYlugVp zlGU93SxC)dGq|AV;BtlHQwHccNZAAJIbZM$q;nb*KYNtOT6r#EbJc@t`)6T+Cy3JxScRMyb*Ou)e-a-{1X+Q&{r?&a{!2KJ9@2k9p0@uIju_~7{u}Z@{o{WF2P!9R za6xUZO*OxW2tGsCWC;BAkMbUJ|Np3)9#;Ve7X_N-O!q9iFYcd)2V%*p{0- zIk)am)qV)h@Tad$dq!7xBR6*qp)gV}`L*9y`ExR|mr6P_=!(OeIjbz~-*tQaP?O8! zOZk))s93t}z?lERBWV_PrEiQz`)2k-P@w>INS@M!B(JD`I>=F><`CfJr*|;EO)pb^ zf9{%=d-ug_oP(+BJQbXQi&xqbj{OQRgOaqPMm--?U0*z&f7tS-4@qiOpu?j2)-u6s zn+peDCoPbg@6{($TJYJer~b|=b%C1rSC7rZ_aS&fg97~}lBb?6c5iR;wXCXpeBbQ8 zL14vQrYC1lo`}{@VB{XT@c_Suy6O{}@yCkQv@BQm*-V@x?q7Oi{)=IvT&iaJ$-;-< zdd{cqW9tWf3cSCDs+UhupP2T04~4I|oS9YQouu)?q#la>E;o)Y-7l3YflyCbrzs4rVj2Hgs`vKD8?FDuSoK&~ZoBr|ku<2DqdR zA49viG_Mo$Z-`{;7(O*+>uR}{%)^k8#Y$cEY3rM>&wlWGZQpUSA?u3MQz3I%lOC^| zJMxx)HuW8QFUKa1%X?9KztrZ@xYQaJjuyoOrM5h6e!|5={2@x=w8T;bPiRmS^+{&+ zN?AUkf?S!xmr@mHPQ5+8?sU&V#zv*_O%H1;nq(@dizgFiW=W21q9O6o8S~!+U$0;} zl_-0<`s`rqM)i|oMOnrNHho;xvoJT2{wwbhpA;45yKJ@LNAnzWWcVWcqRO4z5Imtl zf#DRgKJ5)@$wsfk`B3pnpmJPI)7{>6>*Mx0at>=W|PiRmS^@$7H+rLZ)v!wgSTjwV=uVj7Y4aXXi35*4<+nqWxPS2w*o(wF{oZrU( z^4kriDh^_bRB(>>)>oKHal)!GrSoT_#0YDDHmuo@Rd~5{O!Z3om0Ld?H+g&f(BIp$ zee9!v25tT#1W#yCfF~$`t{?1K{nGu1#@#dXe`wwhtpS8Eg@e=mY#Ctve@O2iJP3uU z1qj$UlWYMFk7m%c3)tObu#hr89ADty7%)-DAV9VVtbt+>)isYqDZ{@pfTEB=1Stb5 z-pK>M8D^rsrvUe`F^Aw}N&Hq8%19Yd@xWsEZ#?`911t(2ZXsnrHRu9vE`O8ZUl?#v z$UwdUiiT(B>iU}uXo`M+FaVp90bZCSTNuTo)eU7xDJG@l97w@@4I@vZpSd?z`KG`CMnIPtaO?=0tzyw}SIaHa%u{ z2o+)*ZpzNSwDy?r3sd)abv|5q#-;B+T>1*Xueo%n{|g_T)04p6A08>qjEC@OP{7X; zlBcI_?6*bxb3`50xcSO+qvF=>?TS+Ht4p`?+Y`kZ=9o=gJUzL+{QaR5*ZrP(9=MVc za?&<3^UPD{cM&@_FeHDxu?c(DENP+FLlMyhF*%>?3*@TbZBcaeddjDvIrK`gW0z2W z-^{ricr++rLy6?cY4g{!Z`KLzG@Ebp`IAR-X2|o7rw{a(=Sj*0yvdW!HlQw^(8ikraFF)Wu6*_20e9M&>H^ER zCz#v);=8p{$#A>N=yw`BiUje7IWH znk~}8arKcY`QJI}7 zno}L;!VO=~akCzL{i#DcyY9VQS?l-B8!p68sC{l)yX-Tz5@A1y21Q{%SrzHH+rgYa zM%yCKqI8~+qd>pb#;dpaU5?6qB+2?Eo}jM!gf`xMwXr+pWnZ}FrE_Pk*=XIxNs-}? zRoe`sj`?|5yfd%^#^Uh$BWYVc%L(xhB* zbzgStCxN}duSA(=Xb+9bWGq_6_d)IqX?eRpn#nO zG;@UJ|Bzumw6_A;04QK${uliJVF=auJNR(C0-k`sjtc}H|CE8pzY**utS8nUD~nBG zjA3lVmE+9t;2c8WE6|qFfRP_q7ql_dF{A*?f=cX7tSzuEuqPN1_<@B%8>tTX9E>5^ z0t*9vU}4ZkUpMJ>&=y!1@YA!>w-M`z7y(rOF%UTtUFq{p8n7kXDCP=(sn=s#Y ziM+@YW76LH^v6OfGcLD%qko3E0m=i$fOHL@Fg2xs^FlVw#i8NJ2EkJ+IKBXmVMR^& z&~Bc-n+)_B=WN~zM@%>tluc9yZU_(|YPtl5%hD2H84P}jKqn$py6RLx|I1qwYdS~2RI)(v*ZKSmK>3fO|xL2+ule{^SN<03{?9Y znrZ)e&|Wwcrucw$%mh$vaJ(MAgZa?<1@Suun)x}12V-oQ1q0O<2Z5nE8wQ#^PUPYs zSPqR@Fi>rBz#9(6P@fG0%`QihcG`ER+AJ8TwmG$746q!kvtZ!S>~n(aJzxx#*)YDe z2Ax=F!S-s&Ea#H%GnuYq>u15hvGaZ=h#1$f+M9Xc!lDaTjfUuasWD9dG`_}7LjGXk zIEV+!v2F@us@=o&l)NAQ3p}WvQ?`8^T7Q9%1MYt)G3NS@$t@PRT;}?ZUt3Nye4yF% z&GjE&NQ+~4q{y$RYah^Sl2)$lo8N3xL!CnA2gI(_zs{ zQM&uoI;u_%cqhSvnJiuqgF{n1`S0qmAlLfFw;t8>_u$f4sPVP%M|!B$t=ridm#a%Y zui$m5l$ocVPE~7tIDJDV>ra~)E*Gz_U&Ea-CUI+HB5#61lxjFwnS0!8I2f{B-%mM= zuj#c~v2mOG`P=Pdy+^s))GRmN)*s#U(DlO1S|2JDXxB)dmT722Vy(CX7_Y>>_O`ah zpUX-7v6B~b4m30Kyus#iW$riIHBEz%W za^;g1We$GR>-=a?7{C2Uo;I39?BV&@v~}46lhp6yRrO-;s(b6b!_5hptnY_!t)nu( zn^$h7=JQK8Uphoc8P^zIurlm(HQlz`pC@e9b7+Tts+f8krXYg(!|r~^A+BghFfpn<&9Wm!V=~p{po-Smcsxn8cp+r z1{F)*M}9LH8E85gW4l3f@Oi@Pcm6V_N3IKLdAGh54GQzw2HEat4lQ7c;2SL~ z7Kl3GWqJ4B(Nk5ptl`xfYfq9Cbo;eysf#BHM=!q}sw8zf@2-z`yvU|XLznvH_HWe| zy7F^;GJert_1NF?*wIdeo&*{cg`UKmKkivvdsFC1+>)@nF}Q4O*u3G%>a3y4vp1A0 zO#R+3GwpFqaY#66^p3jf6Pn&_Ll`)KfY0UPh43|D-rU5nZI_Go4^*f!=T@9I4a=^& zgy^5ppum6{+3s#Aov9dbA2hph=T^Y;R{waPUP+>xd0VOP3o*N}BXsf9#S@x?tL1)r zS`t_NcYcYS7r!?=)NM?s#kSOJxm3@`yBJ~Nik%335;UkO%Ux*x&jK#U_8+pK{qy`E z_5}dV{~`T#mUl6@Anfbzu#{y^#elN~plt`y%j$Fvy6~p1@zoMr3)UJ zv>%6X!%;Ydo}DddfghkeIDzy$F*TxZvfOj6ZxpQFxPV(Yn)i+1#m%uj9^DxQrGN|Q zIaVP1-`I~sy!CD%&*!9tHDq25nPdZdq{(aA{u;UoeSw&VDhKrcm(BQO#e(8Z8f-%m zfBpFDc_h!N<<#?mOB2JB5jbkh-yBS#8D-P4-36{0iCHQ>{2rLnr zdC#Ll0q&7J4Mmr95*D}RH}#A6jXWwT_v0ogHx(T-d7~R4Qgt>mmb!RCb8e`TyO;l} z+Z%=6Dj^Rq(Rt>}WWPSR{?=^ugo5PI0+AY^90JcnaA;5zJPG9nsBPcJUbKQfZbA9m zPp1#J<#Ahn_^dnr;Zh!pyS)@(d#`b8;Dy1OlHLqJ(( z(5Yk1hh!`7PxFKZWjr8HSEYTe#r&sjBN)*W!M7nLrnSW>Y5qO3a$ za*p6{UhY`Gr!n@{T@K9|I|)?tH3~Zkg%e5&*s8@z=eI_j3hKVMvD8rO8ne$pm-xYp zs?5PrV1fHE&_)ECD6&A=NFo$?GR9(mUwK6ia>3|#Oa^EpVlf0P9YFx+SK@f!`|I1; z#wpuQ0A(XOV1M;Ws`g*4JNlS(;lN_4k&(!#^_bxY2p)Q`#oCKyN zl$>hobv$5&KYlk#{veC@VoWC5IS?yzJwlE}qbw zKHmF!4d0c^?NZ-YepR*%w-wLr-I3}hmi)4+K{s4Z!2)AH{k}tkn*R;ds9zH*$Uh%+ z1^^U{FkN8(zi!I^zX9<55BbVxo$~*0PiI7jJOdz=HlDVQ;tT*`I!?Me#!{fMAIO+W z7f)v}bq+usI0FFk%^!m}2cUzlk*<#7EC9%NKkTy~^4AZ%4NRT`AW6$ld!3j~WTow* z38A&5O$7e_F{QwB8Z={bdxM>v8$xEK6-qPE!7*p_DVVmXI`I-J5hy_4C(lk?H~ICKAhE4jj2!^m z4epT^Kgxx{phThjNH897kMiuqY27zeJx>@h2cUxY8D_7s{Z6Q&K%zsxLHD8m$ZYbq zV=*q^-u}jOc93NE{*X-w19H|V3oa-~fD4?R04%;e?F3N0%tI-b19G)MN|4I;vV<^N zI3?&e=swH=l#)5PU_v1k7g%EO@mE2C=dXba%3-*i-xDYfBsKILbRT{mu20ZlC-Oelze3lDPmce&HKhs8*|!(!w?(r{!A$A_V%^%KNQHg*d&|-cg%Y@R}XwR~r)Rsr(-kMYrT62OaxB0d3W|AI$5BNH${5&u0|e6+oOP}3>mljk4J z?Ep9mkRpB%S^8);^k7Od6(6>zawLn7c8uo4#5jT6#e=uQUJqwCP?5oX$W>4wR41oy z!v6dJ=sy`h!hONQe^*26e1Y?yEE5l(w!4~UHS9AY@?wH_aL1`*D`xyR!?OO{vwmRr zMb>0_?Gr1x{IP&ovN-koE^yXCtE-%Ek-VWpT5ptX%o6*P9G`ebrnrLN$5CYQ(d;;8 z&RT)f|Bv16P^3>@SN{I26gd6=*zM6&`hPCJ*1hgl7t7wbac4$VH;kTPT%9l=@#VYy zF+r}kEc<*%yk^P|c9TQqKONPN!AyKue)9aI+4qC7%9Q-D4lWZ}`Jvgp&!i8>C+{oJ z?7_iOz*PEh{>kM>Gr0yXex~BX`OhSiAI*L^@{_?orjMpa0$l4*#3wI5H2sPh{^0iW zZ_jFi`6uTOO>bf5UGeAi(QL|Q;=}y^$F8cT_=D3Ymmhuq7k*d#IX;>#+RRx|u>3S+ z^#|=7QQ+QgN`AQi{kLbO{aOCMuyLfQfB&(YuBrHcu0Q|zSxw~iAMHHInfm|d^#9_l zDNqF`$BS&>KeUGmO(o`IwD7<5|Dip9r0;*|ECA&GzdAt+?@HrN=)g7NQnB&aI&eN< z2euIl&Hq#J@px#RK$wP;CKVTtGl0(w1ZxHMv_`bhxUY2s<1bp(5y5zZcKgoV};I`ED7R6;z#VD4N%uJC@bvfBN{ zFY>}^m+VNh!LDaveDI8Q)9+_(kkxe@s$KH*xEOA0|MsjTI6iq__4jQRjvtEbYfx>G z|A)^7M6O>SP|uj>l2lLdcQd@${3Ag4+u;YMO|NA?8#g|$p|NvcKvnCFsE4HO{)|1& z{ielN8PEnKM4@}#aZQZH4V;eowRrV3f z0@X_D;tB20yJsS&=(TR~f8Oe^!S5xv*~o7l7Y!!5km;<;I{wa-j<1EC)Ab1r3d{qM zJQXTFFnn@@hPzk8)HiCQ0?se#>8_=xkG{EOl$Eqg{V0~YctShe&r4`=c0AYPG`%S8 z944VE&8Ed=7tel44b<0LQn;@|CIM4D%@Z1w41Yb6Ct>_c8m6mz7H_N-cs}^%(Xe9c zgrY&C=<8L7bNkwFUcE+DJmJusm^9T17Dl%xd`x>!r^zkGyv5r^#X;?7miQO5RjX*% zNrp(L!#trvQPih&zqBL#cJBs-hYYq}&m|J-df5xIL!B3@c>El7%94(wDxUCYhaLJp z`jWWwap5?VmX}7DLZO%F_SOTm3Ds$rZY(wMdi4x>u00wQ7$YL7~yOvn^dAK5nNNBXAbS%Wk=7fD%`A4*RiDs&(%POK^zHaCpJRaalEx^nB`H7Y zI3922T6ZNIC;r90I|9KI8WcFA2w9&bT?@MJCCMGTY_;}&iA++Tj05kgx2Y4NkFJf< zC4N7!mb!RCJ5X1(=4|dQpPg%azw>H+jPA&5;OOeC`fg#He>5kWcSVW|f+sX6igwo( z%)*Ktz0sf|ur4<7O>J1joAp6io<6;G&#kuuyJO^7(UCF6(HHPbk$ z^8K>g4`N+RnZkK|_e+OLr=OnT2^ET>J{7F_ae44U`e6@=Ep+kUxA#=-ku})7y+^M82m|8JFwP`yvAbS#U@A*5WOAH49QH8y7-jWH>x zZq@C&iAewO%kNaGWT%xLQK7&ND6&(TGrqD83^!Ae{LPMw!uV>==HRT82fM}wm1$4^%UrqMorXS-&k;Air^EjzT; zx1<&5>bagi5LL3PW#0zs;tB1b5AoU`8U^t!(V6Ae46(;OA`jE<+T4_IXM9l-Yqta5 zA2}&QgQA#h_^gU3OYBInDajqqG$n>1yROz$qSOu_)e~c`%nM}qBt7rW*;ADjc!ZCI z=hcOPzJa4uMIov)+qc^Vn0fg>zuJ$*X-4ui=SyZ5w=@*5!n2Pge^_77F~@~SyJ-)`tTs;>AL}%#qP=`g(@-b%IhE=E zYoMG5wPT~PJeOa2y7K-c1>L$EZl(_TTKWvS{EKbrl)J{hgd^r5XiyY9&H3D!#eEOh z$3(U}TuTVg@HH9Q35$Y7y-X^}4qGy8xK65{>t3~geBPd~Mx}(0&D@&`5cLTSilRPk6+QB!gi}b&7LTdUQMA6d#fIIC zUEyelRiVC=%XtT7>f%Xym*^_Q<8cdJxzRgXhQmjH!Si!8EA-Q zu-d?QJ$O$i{V=b8^s-FpiZ~YAdE-xgO(wp|BX~lCqTs1mi$it4=V)*WZ| z1}*p*wJ+qt(z0?d%WcsL)Ws8@eRV0(S%YtAq$m0iK`$&Ho|4|Bxanq%XS- zV+;2Gds*(VWV86PII*l`5oYOSZez}7PGojsHe(iM=45VTG9dC28Hq3GD#1Q}2%Q1l zdOAkBG1^Mno3tUcp0w*}dui^_xY8)lh|vrXItaOh6oM;(m(YcOfrm5$Z1K5FDNM=) zNrF3*Et4!0KT|Jb8)GhG3ZpxtEu$>(?BC1K#*hm<`@1vPGRQLUGxU<$NV&kHzdP{g zFAI6}r^V1l(fHH!(zns)0?+>L^tSY}z_WiZv5lBZOd+}xZHbhA>GE!u1v`ex(S7J! z8QPCitusus=ZdY37$`5>803<#~VG*-SwE#PaOY+V-FLdFRnVSKo%N@N`mC%K{7Z!t zmfl@QT|5PDy!s)QX+hv;3GMO|w1qT=FU|~)ALqfe@vAF3EPD66e$$kXPFR;&09Ah( zK*2t3foa%epu|A6i(c7h3uD;NhCw&KUNF64u#%RKq4tolJN{bkVg{0UfM#IEgT{+L z%1;1WLKtKF6vmXU({x_Q^e@oxqWoX}mJc+$Wvf&7VHviO=L6R57C_ZGxKmdNW7y4x zfwr{|^ekZv+gUJB&tOMj*i2ze@d@Wekc>~X=@;|ri3mCm1|4(G%A&oYqd zI2t^ zupAb%VW92JKRhJ`V?ahqQ20#_G@bvaGrssClPW0uhJmK{4=mtdIUs{MC{SYH_I}fn zc^u&FT_h6zx-vGbHEn6}Zl{BB!|u-%62*7S>jQCM43kNOX`L~+j*{1}zrdSnBY)Hn z$L$HATb&Sa5j#pU1FFnUk0c(n#eq8hZ2^1KqiS5e-i&@Ete=`f*2ZjkBjSQCS=;qR z-RW`r4o=y@4`h1P4AMz!=w`-!XMGRyFqJ+W|35xbmqLE>{G*+VJ|q91^N)63I9mL_ zI7gd8elq=3R5!n9>7%*7orw?gFF;m)X!CDywDUj8{};Ysr{cr<$^Y?LHdFCo{aju$ z`O)SvCdVx)(*KXod7?<4y#Arh4~V(2eZimW-=aLL)|+nmF2fqdEDv@C8L6M$u*sva z@hr~}S7DJkZtVB+BTt`ec{cBjA`abDmo+*aZpj=t75(kS;wGGilKlS$9U=ec8Py*Z1r=wYpM$qw?inLJGn#{jfEosQQVx zlLRgNNxs(!O+TvrMBGh<=KlpK>4&+@!wPsG%!L(Q&w!?S~D1y2c23b@DqhWifp3ho)) zDcm8z{<;A79j-H6Gq^gqLbzOj_wO-qodIqxFqDOyi+5lN;>@fEKM-t?d(Z*MeW1;- z0=8T5o~M@pR0BGXtH5;2P9TL70EckF>J+}=xA)i%CeSu_h6I}h04HWTE&nNW4a zocAZOVe?Lh|M=|&8ix~j0A)b_TmW=e{G^8|8lvI$k6aSZsL9!{zVZ8^__s@E9n#c4 z)D{3822%F|=yrVP{VB*QY(EK%d;#D&W}q_*5f;M^(gTVxgGE6}1e{(#XXinL5C`hP z58{I#_6JT4srp=Ah0&@ zA%hSW^t?fka*$zQeIRvU#UKyCj|BvAKo}$oPWwRhps=MB0B?$2XshkjU zX`3eS=n;^4`0NF#WrV7`0CWS0j||tg^kM@rd-16O>8l98w5?R)lW1M@Szw(OMCQ+m#|+JEtbh1(2ZeZ&*H?78J6r>O~g${c}e z`H#L9uv=W`*dg}%YruI>cmx8?1J%0|fNg`92e@L-R(dg;>=o_zfB1h-bBNH4&95IG zftjZOj5q77Hzi+UW10TV*#2os%a+-m9}17vd@~et?Qfe>!2t;uppgu?&kO#a4oq?k z6M+q(0${v>a0f6PK^Q2kNb9>Dq3uD~K5j)2A9O^B25_t_FEN*x12@i~o54irJTYMZ z3^rZ^YX5-%h8cwBIgrc%%^`sRx*5dm!-u&8v85o1{}cW^?Cz%+xrV0Js|^e5^!&!jmr`m$%g`;{wPQ~LbI>R@gj2UqnSlTWLV%mBAafqZl`n1~pc zg1LhY;tWhPs#F_*Q!YZAss)9Ydn#S7@sBz+U4F{iuj$R~m2c}0hy&{ym>J+kJvMg` zn%+W+7tq>q5J2|}F=w#A+yQC#V=;q>b3kBb0GmHxfIO%8sZZUpO!0fiQpQ!MFIF~h zJDOp){_0^>|A`8fYPN~WPY2bUBEbN2-gUgwd=BdcAuW!RdtR^d`KJCeW9EypGcrt3 Sy*+2{{X(4MO3)}^`~d(@$uf@s 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 = {