From a6547be82c568b028b51cd2de7d42f44ccadce83 Mon Sep 17 00:00:00 2001 From: philemon Date: Sat, 30 Jul 2016 14:42:22 +0300 Subject: [PATCH] Label editing in mmgen-txcreate --- mmgen/crypto.py | 24 ++++--- mmgen/main_tool.py | 4 +- mmgen/main_txcreate.py | 20 ++---- mmgen/main_txsign.py | 14 ++-- mmgen/obj.py | 44 +++++++------ mmgen/rpc.py | 16 +++-- mmgen/seed.py | 11 ---- mmgen/term.py | 3 + mmgen/tool.py | 104 +++++++++++------------------- mmgen/tw.py | 142 +++++++++++++++++++++++++++++++---------- mmgen/util.py | 78 ++-------------------- test/test.py | 8 +-- 12 files changed, 222 insertions(+), 246 deletions(-) diff --git a/mmgen/crypto.py b/mmgen/crypto.py index e061b50c..773e917a 100755 --- a/mmgen/crypto.py +++ b/mmgen/crypto.py @@ -28,8 +28,8 @@ from mmgen.term import get_char crmsg = { 'usr_rand_notice': """ -You've chosen to not fully trust your OS's random number generator and provide -some additional entropy of your own. Please type %s symbols on your keyboard. +Since we don't fully trust our OS's random number generator, we'll provide +some additional entropy of our own. Please type %s symbols on your keyboard. Type slowly and choose your symbols carefully for maximum randomness. Try to use both upper and lowercase as well as punctuation and numerals. What you type will not be displayed on the screen. Note that the timings between your @@ -57,7 +57,6 @@ keystrokes will also be used as a source of randomness. def encrypt_seed(seed, key): return encrypt_data(seed, key, iv=1, desc='seed') - def decrypt_seed(enc_seed, key, seed_id, key_id): vmsg_r('Checking key...') @@ -91,7 +90,6 @@ def decrypt_seed(enc_seed, key, seed_id, key_id): return dec_seed - def encrypt_data(data, key, iv=1, desc='data', verify=True): # 192-bit seed is 24 bytes -> not multiple of 16. Must use MODE_CTR @@ -117,7 +115,6 @@ def encrypt_data(data, key, iv=1, desc='data', verify=True): return enc_data - def decrypt_data(enc_data, key, iv=1, desc='data'): vmsg_r('Decrypting %s with key...' % desc) @@ -130,7 +127,6 @@ def decrypt_data(enc_data, key, iv=1, desc='data'): return c.decrypt(enc_data) - def scrypt_hash_passphrase(passwd, salt, hash_preset, buflen=32): # Buflen arg is for brainwallets only, which use this function to generate @@ -141,7 +137,6 @@ def scrypt_hash_passphrase(passwd, salt, hash_preset, buflen=32): import scrypt return scrypt.hash(passwd, salt, 2**N, r, p, buflen=buflen) - def make_key(passwd,salt,hash_preset, desc='encryption key',from_what='passphrase',verbose=False): @@ -153,7 +148,6 @@ def make_key(passwd,salt,hash_preset, dmsg('Key: %s' % hexlify(key)) return key - def _get_random_data_from_user(uchars): if opt.quiet: msg('Enter %s random symbols' % uchars) @@ -189,7 +183,6 @@ def _get_random_data_from_user(uchars): return key_data+''.join(fmt_time_data) - def get_random(length): from Crypto import Random os_rand = Random.new().read(length) @@ -206,6 +199,18 @@ def get_random(length): else: return os_rand +def get_hash_preset_from_user(hp=g.hash_preset,desc='data'): + p = """Enter hash preset for %s, + or hit ENTER to accept the default value ('%s'): """ % (desc,hp) + while True: + ret = my_raw_input(p) + if ret: + if ret in g.hash_presets.keys(): return ret + else: + msg('Invalid input. Valid choices are %s' % + ', '.join(sorted(g.hash_presets.keys()))) + continue + else: return hp # Vars for mmgen_*crypt functions only salt_len,sha256_len,nonce_len = 32,32,32 @@ -224,7 +229,6 @@ def mmgen_encrypt(data,desc='data',hash_preset=''): int(hexlify(iv),16), desc=desc) return salt+iv+enc_d - def mmgen_decrypt(data,desc='data',hash_preset=''): dstart = salt_len + g.aesctr_iv_len salt,iv,enc_d = data[:salt_len],data[salt_len:dstart],data[dstart:] diff --git a/mmgen/main_tool.py b/mmgen/main_tool.py index 3c9e53ee..3f6184d9 100755 --- a/mmgen/main_tool.py +++ b/mmgen/main_tool.py @@ -64,5 +64,5 @@ if cmd_args and cmd_args[0] == '--help': sys.exit() args,kwargs = tool.process_args(g.prog_name, command, cmd_args) - -tool.__dict__[command](*args,**kwargs) +ret = tool.__dict__[command](*args,**kwargs) +sys.exit(0 if ret in (None,True) else 1) # some commands die, some return False on failure diff --git a/mmgen/main_txcreate.py b/mmgen/main_txcreate.py index 5a2c6e8f..376a3865 100755 --- a/mmgen/main_txcreate.py +++ b/mmgen/main_txcreate.py @@ -98,22 +98,14 @@ was specified. } def select_unspent(unspent,prompt): - while True: reply = my_raw_input(prompt).strip() - - if not reply: continue - - selected = parse_addr_idxs(reply,sep=None) - - if not selected: continue - - if selected[-1] > len(unspent): - msg('Inputs must be less than %s' % len(unspent)) - continue - - return selected - + if reply: + selected = AddrIdxList(fmt_str=','.join(reply.split()),on_fail='return') + if selected: + if selected[-1] <= len(unspent): + return selected + msg('Unspent output number must be <= %s' % len(unspent)) def mmaddr2baddr(c,mmaddr,ad_w,ad_f): diff --git a/mmgen/main_txsign.py b/mmgen/main_txsign.py index 3bb197b0..0c7b8ae4 100755 --- a/mmgen/main_txsign.py +++ b/mmgen/main_txsign.py @@ -164,7 +164,14 @@ def add_keys(tx,src,infiles=None,saved_seeds=None,keyaddr_list=None): 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 +# # functions unneeded - use bitcoin-cli walletdump instead +# def get_bitcoind_passphrase(prompt): +# if opt.passwd_file: +# pwfile_reuse_warning() +# return get_data_from_file(opt.passwd_file,'passphrase').strip('\r\n') +# else: +# return my_raw_input(prompt, echo=opt.echo_passphrase) +# # def sign_tx_with_bitcoind_wallet(c,tx,tx_num_str,keys): # ok = tx.sign(c,tx_num_str,keys) # returns false on failure # if ok: @@ -174,7 +181,7 @@ def add_keys(tx,src,infiles=None,saved_seeds=None,keyaddr_list=None): # prompt = 'Enter passphrase for bitcoind wallet: ' # while True: # passwd = get_bitcoind_passphrase(prompt) -# ret = c.walletpassphrase(passwd, 9999,ret_on_error=True) +# ret = c.walletpassphrase(passwd, 9999,on_fail='return') # if rpc_error(ret): # if rpc_errmsg(ret,'unencrypted wallet, but walletpassphrase was called'): # msg('Wallet is unencrypted'); break @@ -184,12 +191,11 @@ def add_keys(tx,src,infiles=None,saved_seeds=None,keyaddr_list=None): # ok = tx.sign(c,tx_num_str,keys) # # msg('Locking wallet') -# ret = c.walletlock(ret_on_error=True) +# ret = c.walletlock(on_fail='return') # if rpc_error(ret): # msg('Failed to lock wallet') # # return ok -# # main(): execution begins here diff --git a/mmgen/obj.py b/mmgen/obj.py index fe7935fa..e17e84ed 100755 --- a/mmgen/obj.py +++ b/mmgen/obj.py @@ -187,7 +187,7 @@ class AddrIdx(int,InitErrors): assert type(num) is not float me = int.__new__(cls,num) except: - m = "'%s': value cannot be converted to addr idx" % num + m = "'%s': value cannot be converted to address index" % num else: if len(str(me)) > cls.max_digits: m = "'%s': too many digits in addr idx" % num @@ -205,9 +205,11 @@ class AddrIdxList(list,InitErrors): self.arg_chk(type(self),on_fail) assert fmt_str or idx_list if idx_list: - return list.__init__(self,sorted(set(idx_list))) + # dies on failure + return list.__init__(self,sorted(set([AddrIdx(i) for i in idx_list]))) elif fmt_str: - ret,fs = [],"'%s': value cannot be converted to addr idx" + desc = fmt_str + ret,fs = [],"'%s': value cannot be converted to address index" from mmgen.util import msg for i in (fmt_str.split(sep)): j = i.split('-') @@ -228,7 +230,7 @@ class AddrIdxList(list,InitErrors): else: return list.__init__(self,sorted(set(ret))) # fell off end of loop - success - return self.init_fail(fs % err,on_fail,silent=True) + return self.init_fail((fs + ' list') % desc,on_fail) class Hilite(object): @@ -238,19 +240,21 @@ class Hilite(object): trunc_ok = True @classmethod - def fmtc(cls,s,width=None,color=False,encl='',trunc_ok=None): + def fmtc(cls,s,width=None,color=False,encl='',trunc_ok=None,center=False,nullrepl=''): if width == None: width = cls.width if trunc_ok == None: trunc_ok = cls.trunc_ok assert width > 0 + if s == '' and nullrepl: + s,center = nullrepl,True + if center: s = s.center(width) 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) + def fmt(self,*args,**kwargs): + assert args == () # forbid invocation w/o keywords + return self.fmtc(self,*args,**kwargs) @classmethod def hlc(cls,s,color=True): @@ -266,7 +270,8 @@ class Hilite(object): 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 + k = color if type(color) is str else cls.color # hack: override color with str value + return locals()[k](s) if (color or cls.color_always) and g.color else s class BTCAmt(Decimal,Hilite,InitErrors): color = 'yellow' @@ -337,7 +342,6 @@ class BTCAmt(Decimal,Hilite,InitErrors): def __neg__(self,other,context=None): return type(self)(Decimal.__neg__(self,other,context)) - class BTCAddr(str,Hilite,InitErrors): color = 'cyan' width = 34 @@ -351,16 +355,15 @@ class BTCAddr(str,Hilite,InitErrors): 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) + def fmtc(cls,s,**kwargs): + # True -> 'cyan': use the str value override hack + if 'color' in kwargs and kwargs['color'] == True: + kwargs['color'] = cls.color + if not 'width' in kwargs: kwargs['width'] = cls.width + if kwargs['width'] < len(s): + s = s[:kwargs['width']-2] + '..' + return Hilite.fmtc(s,**kwargs) class SeedID(str,Hilite,InitErrors): color = 'blue' @@ -431,6 +434,7 @@ class MMGenWalletLabel(MMGenLabel): class MMGenAddrLabel(MMGenLabel): max_len = 32 + allowed = [chr(i+32) for i in range(95)] desc = 'address label' class MMGenTXLabel(MMGenLabel): diff --git a/mmgen/rpc.py b/mmgen/rpc.py index 8fadfb34..af100d7d 100755 --- a/mmgen/rpc.py +++ b/mmgen/rpc.py @@ -56,10 +56,10 @@ class BitcoinRPCConnection(object): # kwargs are for local use and are not passed to server # By default, dies with an error msg on all errors and exceptions - # With ret_on_error=True, returns 'rpcfail',(resp_object,(die_args)) + # With on_fail='return', returns 'rpcfail',(resp_object,(die_args)) def request(self,cmd,*args,**kwargs): - cf = { 'timeout':g.http_timeout, 'batch':False, 'ret_on_error':False } + cf = { 'timeout':g.http_timeout, 'batch':False, 'on_fail':'die' } for k in cf: if k in kwargs and kwargs[k]: cf[k] = kwargs[k] @@ -72,7 +72,7 @@ class BitcoinRPCConnection(object): p = {'method':cmd,'params':args,'id':1} def die_maybe(*args): - if cf['ret_on_error']: + if cf['on_fail'] == 'return': return 'rpcfail',args else: die(*args[1:]) @@ -88,7 +88,10 @@ class BitcoinRPCConnection(object): return (float,str)[caller.client_version>=120000](obj) return json.JSONEncoder.default(self, obj) -# pp_msg(json.dumps(p,cls=MyJSONEncoder)) +# Can't do UTF-8 labels yet: httplib only ascii? +# if type(p) != list and p['method'] == 'importaddress': +# dump = json.dumps(p,cls=MyJSONEncoder,ensure_ascii=False) +# print(dump) try: c.request('POST', '/', json.dumps(p,cls=MyJSONEncoder), { @@ -153,7 +156,6 @@ class BitcoinRPCConnection(object): exec "def {n}(self,*a,**k):return self.request('{n}',*a,**k)\n".format(n=name) def rpc_error(ret): - return ret is list and ret and ret[0] == 'rpcfail' + return type(ret) is tuple and ret and ret[0] == 'rpcfail' -def rpc_errmsg(ret,e): - return (False,True)[ret[1][2].find(e) == -1] +def rpc_errmsg(ret): return ret[1][2] diff --git a/mmgen/seed.py b/mmgen/seed.py index 8f541a21..7282fe6e 100755 --- a/mmgen/seed.py +++ b/mmgen/seed.py @@ -53,7 +53,6 @@ class Seed(MMGenObject): def get_data(self): return self.data - class SeedSource(MMGenObject): desc = g.proj_name + ' seed source' @@ -216,13 +215,11 @@ class SeedSource(MMGenObject): } write_data_to_file(self._filename(),self.fmt_data,**kwargs) - class SeedSourceUnenc(SeedSource): def _decrypt_retry(self): pass def _encrypt(self): pass - class SeedSourceEnc(SeedSource): _msg = { @@ -346,7 +343,6 @@ an empty passphrase, just hit ENTER twice. d.key_id = make_chksum_8(key) d.enc_seed = encrypt_seed(self.seed.data,key) - class Mnemonic (SeedSourceUnenc): stdin_ok = True @@ -466,7 +462,6 @@ class Mnemonic (SeedSourceUnenc): def _filename(self): return '%s[%s].%s' % (self.seed.sid,self.seed.length,self.ext) - class SeedFile (SeedSourceUnenc): stdin_ok = True @@ -524,7 +519,6 @@ class SeedFile (SeedSourceUnenc): def _filename(self): return '%s[%s].%s' % (self.seed.sid,self.seed.length,self.ext) - class Wallet (SeedSourceEnc): fmt_codes = 'wallet','w' @@ -687,7 +681,6 @@ class Wallet (SeedSourceEnc): self.ext ) - class Brainwallet (SeedSourceEnc): stdin_ok = True @@ -727,7 +720,6 @@ class Brainwallet (SeedSourceEnc): qmsg('Check this value against your records') return True - class IncogWallet (SeedSourceEnc): file_mode = 'binary' @@ -883,7 +875,6 @@ to exit and re-run the program with the '--old-incog-fmt' option. else: return False - class IncogWalletHex (IncogWallet): file_mode = 'text' @@ -904,7 +895,6 @@ class IncogWalletHex (IncogWallet): IncogWallet._format(self) self.fmt_data = pretty_hexdump(self.fmt_data) - class IncogWalletHidden (IncogWallet): desc = 'hidden incognito data' @@ -931,7 +921,6 @@ harder to find, you're advised to choose a much larger file size than this. 'dec_chk': ', hash preset, offset %s seed length' } - def _get_hincog_params(self,wtype): p = getattr(opt,'hidden_incog_'+ wtype +'_params') a,b = p.split(',') diff --git a/mmgen/term.py b/mmgen/term.py index 5bf7a9dd..d6215588 100755 --- a/mmgen/term.py +++ b/mmgen/term.py @@ -23,6 +23,9 @@ term.py: Terminal-handling routines for the MMGen suite import os,struct from mmgen.common import * +CUR_SHOW = '\033[?25h' +CUR_HIDE = '\033[?25l' + def _kb_hold_protect_unix(): fd = sys.stdin.fileno() diff --git a/mmgen/tool.py b/mmgen/tool.py index c7794839..f3c1f159 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -71,7 +71,6 @@ cmd_data = OrderedDict([ ('mn_stats', ["wordlist [str='electrum']"]), ('mn_printlist', ["wordlist [str='electrum']"]), - ('listaddresses',["addrs [str='']",'minconf [int=1]','showempty [bool=False]','pager [bool=False]','showbtcaddrs [bool=False]']), ('getbalance', ['minconf [int=1]']), ('txview', ['<{} TX file> [str]'.format(pnm),'pager [bool=False]','terse [bool=False]']), @@ -102,7 +101,7 @@ cmd_help = """ wif2hex - convert a private key from WIF to hex format Wallet/TX operations (bitcoind must be running): - getbalance - like 'bitcoind getbalance' but shows confirmed/unconfirmed, + getbalance - like 'bitcoin-cli getbalance' but shows confirmed/unconfirmed, spendable/unspendable balances for individual {pnm} wallets listaddresses - list {pnm} addresses and their balances txview - show raw/signed {pnm} transaction in human-readable form @@ -229,15 +228,12 @@ def are_equal(a,b,dtype=''): else: return a == b def print_convert_results(indata,enc,dec,dtype): - error = (True,False)[are_equal(indata,dec,dtype)] - if error or opt.verbose: Msg('Input: %s' % repr(indata)) Msg('Encoded data: %s' % repr(enc)) Msg('Recoded data: %s' % repr(dec)) else: Msg(enc) - if error: die(3,"Error! Recoded data doesn't match input!") @@ -364,23 +360,15 @@ def str2id6(s): Msg(make_chksum_6(''.join(s.split()))) # List MMGen addresses and their balances: def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=False): + # TODO - move some or all of this code to AddrList usr_addr_list = [] if addrs: - sid,idxs = split2(addrs,':') - if not idxs: - s1 = "'%s': invalid address argument\n" - s2 = "Address argument format: <%s Seed ID>':'" - die(1, (s1+s2) % (addrs,g.proj_name)) - if not is_mmgen_seed_id(sid): - die(1,'%s: invalid %s Seed ID' % (g.proj_name,sid)) - tmp = parse_addr_idxs(idxs) - if not tmp: return False - usr_addr_list = ['%s:%s' % (sid,i) for i in tmp] + sid_s,idxs = split2(addrs,':') + sid = SeedID(sid=sid_s) + usr_addr_list = ['{}:{}'.format(sid,a) for a in AddrIdxList(idxs)] c = bitcoin_connection() - addrs = {} # reusing variable name! - from mmgen.obj import BTCAmt total = BTCAmt('0') for d in c.listunspent(0): mmaddr,comment = split2(d['account']) @@ -392,7 +380,7 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa die(2,'duplicate BTC address ({}) for this MMGen address! ({})'.format( (d['address'], addrs[key][2]))) else: - addrs[key] = [BTCAmt('0'),comment,d['address']] + addrs[key] = [BTCAmt('0'),MMGenAddrLabel(comment),BTCAddr(d['address'])] addrs[key][0] += d['amount'] total += d['amount'] @@ -407,45 +395,47 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa key = mmaddr.replace(':','_') if key not in addrs: if showbtcaddrs: save_a.append([acct]) - addrs[key] = [BTCAmt('0'),comment,''] + addrs[key] = [BTCAmt('0'),MMGenAddrLabel(comment),''] for acct,addr in zip(save_a,c.getaddressesbyaccount(save_a,batch=True)): if len(addr) != 1: die(2,"Account '%s' has more or less than one BTC address!" % addr) key = split2(acct[0])[0].replace(':','_') - addrs[key][2] = addr[0] + addrs[key][2] = BTCAddr(addr[0]) if not addrs: die(0,('No addresses with balances!','No tracked addresses!')[showempty]) - fs = '%-{}s %-{}s %-{}s %s'.format( - max(len(k) for k in addrs), - (0,36)[showbtcaddrs], - max(max(len(addrs[k][1]) for k in addrs) + 1,8) # pad 8 if no comments - ) + fs = ('{mid} {lbl} {amt}','{mid} {addr} {lbl} {amt}')[showbtcaddrs] + max_mmid_len = max(len(k) for k in addrs) or 10 + max_lbl_len = max(len(addrs[k][1]) for k in addrs) or 7 + out = [fs.format( + mid=MMGenID.fmtc('MMGenID',width=max_mmid_len), + addr=BTCAddr.fmtc('ADDRESS'), + lbl=MMGenAddrLabel.fmtc('COMMENT',width=max_lbl_len), + amt='BALANCE' + )] - def s_mmgen(key): # TODO - return '{}:{:>0{w}}'.format(w=AddrIdx.max_digits, *key.split('_')) - - out = [] + old_sid = '' + def s_mmgen(k): return '{:>0{w}}'.format(k,w=AddrIdx.max_digits+9) # TODO 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], addrs[k][0].fmt('3.0',color=1))) + if old_sid and old_sid != k[:8]: out.append('') + old_sid = k[:8] + out.append(fs.format( + mid=MMGenID(k.replace('_',':')).fmt(width=max_mmid_len,color=True), + addr=(addrs[k][2].fmt(color=True) if showbtcaddrs else None), + lbl=addrs[k][1].fmt(width=max_lbl_len,color=True,nullrepl='-'), + amt=addrs[k][0].fmt('3.0',color=True))) - o = (fs + '\n%s\nTOTAL: %s BTC') % ( - 'ADDRESS','','COMMENT',' BALANCE', '\n'.join(out), total.hl() - ) + out.append('\nTOTAL: %s BTC' % total.hl(color=True)) + o = '\n'.join(out) if pager: do_pager(o) else: Msg(o) - def getbalance(minconf=1): accts = {} - us = bitcoin_connection().listunspent(0) -# pp_die(us) - for d in us: + for d in bitcoin_connection().listunspent(0): ma = split2(d['account'])[0] keys = ['TOTAL'] if d['spendable']: keys += ['SPENDABLE'] @@ -477,30 +467,16 @@ def twview(pager=False,reverse=False,wide=False,sort='age'): 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_id(mmaddr): - die(1,'{a}: not a valid {pnm} address'.format(pnm=pnm,a=mmaddr)) - MMGenAddrLabel(label) # Exits on failure +def add_label(mmaddr,label): + from mmgen.tw import MMGenTrackingWallet + if MMGenTrackingWallet.add_label(mmaddr,label): # returns on failure + s = '{pnm} address {a} in tracking wallet'.format(a=mmaddr,pnm=pnm) + if label: msg("Added label '{}' for {}".format(label,s)) + else: msg('Removed label for {}'.format(s)) + else: + die(1,'Label could not be %s' % ('removed','added')[bool(label)]) - 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 - except: - die(1,'Unable to add label') - - s = '{pnm} address {a} in tracking wallet'.format(a=mmaddr,pnm=pnm) - if remove: msg('Removed label from {}'.format(s)) - else: msg("Added label '{}' for {}".format(label,s)) - -def remove_label(mmaddr): add_label(mmaddr,'',remove=True) +def remove_label(mmaddr): add_label(mmaddr,'') def addrfile_chksum(infile): from mmgen.addr import AddrList @@ -544,7 +520,6 @@ def wif2hex(wif,compressed=False): def hex2wif(hexpriv,compressed=False): Msg(bitcoin.hextowif(hexpriv,compressed)) - def encrypt(infile,outfile='',hash_preset=''): data = get_data_from_file(infile,'data for encryption',binary=True) enc_d = mmgen_encrypt(data,'user data',hash_preset) @@ -553,7 +528,6 @@ def encrypt(infile,outfile='',hash_preset=''): write_data_to_file(outfile,enc_d,'encrypted data',binary=True) - def decrypt(infile,outfile='',hash_preset=''): enc_d = get_data_from_file(infile,'encrypted data',binary=True) while True: @@ -568,7 +542,6 @@ def decrypt(infile,outfile='',hash_preset=''): write_data_to_file(outfile,dec_d,'decrypted data',binary=True) - def find_incog_data(filename,iv_id,keep_searching=False): ivsize,bsize,mod = g.aesctr_iv_len,4096,4096*8 n,carry = 0,' '*ivsize @@ -594,7 +567,6 @@ def find_incog_data(filename,iv_id,keep_searching=False): msg('') os.close(f) - def rand2file(outfile, nbytes, threads=4, silent=False): nbytes = parse_nbytes(nbytes) from Crypto import Random diff --git a/mmgen/tw.py b/mmgen/tw.py index 9982a082..57e68262 100755 --- a/mmgen/tw.py +++ b/mmgen/tw.py @@ -46,7 +46,26 @@ 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) } + sort_keys = 'addr','age','amt','txid','mmid' + def __init__(self): + self.unspent = [] + self.fmt_display = '' + self.fmt_print = '' + self.cols = None + self.reverse = False + self.group = False + self.show_days = True + self.show_mmid = True + self.get_data() + self.sort_key = 'age' + self.do_sort() + self.total = self.get_total_btc() + + def get_total_btc(self): + return sum([i.amt for i in self.unspent]) + + def get_data(self): if g.bogus_wallet_data: # for debugging purposes only us_rpc = eval(get_data_from_file(g.bogus_wallet_data)) else: @@ -56,27 +75,15 @@ watch-only wallet using '{}-addrimport' and then re-run this program. 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['mmid'],o['label'] = parse_tw_acct_label(o['account']) if 'account' in o else ('','') 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])) + self.unspent = [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([pp_format(i)+'\n' for i in us_rpc])) +# die(1,''.join([str(i)+'\n' for i in self.unspent])) - 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 @@ -87,18 +94,20 @@ watch-only wallet using '{}-addrimport' and then re-run this program. *i.mmid.split(':'), w=AddrIdx.max_digits) else: return 'G' + (i.label or '') - def do_sort(self,key,reverse=None): + def do_sort(self,key=None,reverse=None): + if not key: key = self.sort_key + assert key + self.sort_key = key if key not in self.sort_keys: fs = "'{}': invalid sort key. Valid keys: [{}]" die(2,fs.format(key,' '.join(self.sort_keys))) if reverse == None: reverse = self.reverse - self.sort = key 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.capitalize().replace('Mmid','MMGenID')) - if include_group and self.group and (self.sort in ('addr','txid','mmid')): + ret.append(self.sort_key.capitalize().replace('Mmid','MMGenID')) + if include_group and self.group and (self.sort_key in ('addr','txid','mmid')): ret.append('Grouped') return ret @@ -122,7 +131,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program. if i.label == None: i.label = '' i.skip = '' - mmid_w = max(len(i.mmid or '') for i in unsp) + mmid_w = max(len(i.mmid or '') for i in unsp) or 10 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))) @@ -138,10 +147,10 @@ watch-only wallet using '{}-addrimport' and then re-run this program. 'Amt(BTC) ', ('Conf.','Age(d)')[self.show_days]) - if self.group and (self.sort in ('addr','txid','mmid')): + if self.group and (self.sort_key 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): + if self.sort_key == k and getattr(a,k) == getattr(b,k): b.skip = (k,'addr')[k=='mmid'] hdr_fmt = 'UNSPENT OUTPUTS (sort order: %s) Total BTC: %s' @@ -156,7 +165,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program. 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)) + '{} {}'.format(mmid_disp,i.label.fmt(width=label_w,color=True) if label_w > 0 else '') ) else: addr_out = type(i.addr).fmtc(addr_dots,width=addr_w,color=True) if i.skip=='addr' \ @@ -177,12 +186,16 @@ watch-only wallet using '{}-addrimport' and then re-run this program. out = [fs % ('Num','Tx ID,Vout','Address'.ljust(34),'MMGen ID'.ljust(15), 'Amount(BTC)','Conf.','Age(d)', 'Label')] + max_lbl_len = max(len(i.label) for i in self.unspent if i.label) or 1 for n,i in enumerate(self.unspent): 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 '') + (i.mmid.fmt(width=14,color=color) if i.mmid else + MMGenID.fmtc('',width=14,nullrepl='-',color=color)), + i.amt.fmt(color=color),i.confs,i.days, + i.label.hl(color=color) if i.label else + MMGenAddrLabel.fmtc('',color=color,nullrepl='-',width=max_lbl_len)) out.append(s.rstrip()) fs = 'Unspent outputs ({} UTC)\nSort order: {}\n\n{}\n\nTotal BTC: {}\n' @@ -197,6 +210,30 @@ watch-only wallet using '{}-addrimport' and then re-run this program. fs = '\nTotal unspent: %s BTC (%s outputs)' msg(fs % (self.total.hl(),len(self.unspent))) + def get_idx_and_label_from_user(self): + msg('') + while True: + ret = my_raw_input("Enter unspent output number (or 'q' to return to main menu): ") + if ret == 'q': return None,None + n = AddrIdx(ret,on_fail='silent') # hacky way to test and convert to integer + if not n or n < 1 or n > len(self.unspent): + msg('Choice must be a single number between 1 and %s' % len(self.unspent)) + elif not self.unspent[n-1].mmid: + msg('Address #%s is not an %s address. No label can be added to it' % + (n,g.proj_name)) + else: + while True: + s = my_raw_input("Enter label text (or 'q' to return to main menu): ") + if s == 'q': + return None,None + elif s == '': + if keypress_confirm( + "Removing label for address #%s. Is this what you want?" % n): + return n,s + elif s: + if MMGenAddrLabel(s,on_fail='return'): + return n,s + def view_and_sort(self): from mmgen.term import do_pager prompt = """ @@ -206,27 +243,37 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen self.display() msg(prompt) - p = "('q' = quit sorting, 'p' = print to file, 'v' = pager view, 'w' = wide view): " + p = "'q'=quit view, 'p'=print to file, 'v'=pager view, 'w'=wide view, 'l'=add label:\b" while True: reply = get_char(p, immed_chars='atDdAMrgmeqpvw') 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('addr') elif reply == 'A': self.do_sort('age') - elif reply == 'M': self.do_sort('mmid'); self.show_mmid = True - elif reply == 'r': self.unspent.reverse(); self.reverse = not self.reverse + elif reply == 'd': self.do_sort('addr') + elif reply == 'D': self.show_days = not self.show_days + elif reply == 'e': msg('\n%s\n%s\n%s' % (self.fmt_display,prompt,p)) elif reply == 'g': self.group = not self.group + elif reply == 'l': + idx,lbl = self.get_idx_and_label_from_user() + if idx: + e = self.unspent[idx-1] + if type(self).add_label(e.mmid,lbl,addr=e.addr): + self.get_data() + self.do_sort() + msg('%s\n%s\n%s' % (self.fmt_display,prompt,p)) + else: + msg('Label could not be added\n%s\n%s' % (prompt,p)) + elif reply == 'M': self.do_sort('mmid'); self.show_mmid = True 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('') + of = 'listunspent[%s].out' % ','.join(self.sort_info(include_group=False)).lower() write_data_to_file(of,self.format_for_printing(),'unspent outputs listing') m = yellow("Data written to '%s'" % of) msg('\n%s\n%s\n\n%s' % (self.fmt_display,m,prompt)) continue + elif reply == 'q': return self.unspent + elif reply == 'r': self.unspent.reverse(); self.reverse = not self.reverse + elif reply == 't': self.do_sort('txid') elif reply == 'v': do_pager(self.fmt_display) continue @@ -240,3 +287,30 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen msg('\n') self.display() msg(prompt) + + # returns on failure + @classmethod + def add_label(cls,mmaddr,label='',addr=None): + mmaddr = MMGenID(mmaddr) + + if addr: # called from view_and_sort() + if not BTCAddr(addr,on_fail='return'): return False + else: + from mmgen.addr import AddrData + addr = AddrData(source='tw').mmaddr2btcaddr(mmaddr) + if not addr: + msg('{} address {} not found in tracking wallet'.format(g.proj_name,mmaddr)) + return False + + label = MMGenAddrLabel(label,on_fail='return') + if not label and label != '': return False + + acct = mmaddr + (' ' + label if label else '') # label is ASCII for now + # return on failure - args: addr,label,rescan,p2sh + ret = bitcoin_connection().importaddress(addr,acct,False,on_fail='return') + from mmgen.rpc import rpc_error,rpc_errmsg + if rpc_error(ret): msg('From bitcoind: ' + rpc_errmsg(ret)) + return not rpc_error(ret) + + @classmethod + def remove_label(cls,mmaddr): cls.add_label(mmaddr,'') diff --git a/mmgen/util.py b/mmgen/util.py index 013e16ae..f38e9fd4 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -17,7 +17,7 @@ # along with this program. If not, see . """ -util.py: Low-level routines imported by other modules for the MMGen suite +util.py: Low-level routines imported by other modules in the MMGen suite """ import sys,os,time,stat,re @@ -136,12 +136,10 @@ def vmsg(s): if opt.verbose: sys.stderr.write(s + '\n') def vmsg_r(s): if opt.verbose: sys.stderr.write(s) - def Vmsg(s): if opt.verbose: sys.stdout.write(s + '\n') def Vmsg_r(s): if opt.verbose: sys.stdout.write(s) - def dmsg(s): if opt.debug: sys.stdout.write(s + '\n') @@ -190,7 +188,7 @@ def split_into_cols(col_wid,s): return ' '.join([s[col_wid*i:col_wid*(i+1)] for i in range(len(s)/col_wid+1)]).rstrip() -def capfirst(s): +def capfirst(s): # different from str.capitalize() - doesn't downcase any uc in string return s if len(s) == 0 else s[0].upper() + s[1:] def decode_timestamp(s): @@ -338,7 +336,6 @@ def open_file_or_exit(filename,mode): die(2,"Unable to open file '%s' for %s" % (filename,op)) return f - def check_file_type_and_access(fname,ftype,blkdev_ok=False): a = ((os.R_OK,'read'),(os.W_OK,'writ')) @@ -372,46 +369,9 @@ def check_outfile(f,blkdev_ok=False): return check_file_type_and_access(f,'output file',blkdev_ok=blkdev_ok) def check_outdir(f): return check_file_type_and_access(f,'output directory') - def make_full_path(outdir,outfile): return os.path.normpath(os.path.join(outdir, os.path.basename(outfile))) -def _validate_addr_num(n): - from mmgen.tx import is_mmgen_idx - if is_mmgen_idx(n): - return int(n) - else: - msg("'%s': invalid %s address index" % (n,g.proj_name)) - return False - -def parse_addr_idxs(arg,sep=','): # TODO - delete - - ret = [] - - for i in (arg.split(sep)): - - j = i.split('-') - - if len(j) == 1: - i = _validate_addr_num(i) - if not i: return False - ret.append(i) - elif len(j) == 2: - beg = _validate_addr_num(j[0]) - if not beg: return False - end = _validate_addr_num(j[1]) - if not end: return False - if end < beg: - msg("'%s-%s': invalid range (end is less than beginning)" % (beg,end)) - return False - ret.extend(range(beg,end+1)) - else: - msg("'%s': invalid address range argument" % i) - return False - - return sorted(set(ret)) - - def get_new_passphrase(desc,passchg=False): w = '{}passphrase for {}'.format(('','new ')[bool(passchg)], desc) @@ -586,17 +546,13 @@ def get_data_from_file(infile,desc='data',dash=False,silent=False,binary=False): f.close() return data -passwd_file_used = False - def pwfile_reuse_warning(): - global passwd_file_used - if passwd_file_used: + if 'passwd_file_used' in globals(): qmsg("Reusing passphrase from file '%s' at user request" % opt.passwd_file) return True - passwd_file_used = True + globals()['passwd_file_used'] = True return False - def get_mmgen_passphrase(desc,passchg=False): prompt ='Enter {}passphrase for {}: '.format(('','old ')[bool(passchg)],desc) if opt.passwd_file: @@ -605,28 +561,6 @@ def get_mmgen_passphrase(desc,passchg=False): else: return ' '.join(get_words_from_user(prompt)) -def get_bitcoind_passphrase(prompt): - if opt.passwd_file: - pwfile_reuse_warning() - return get_data_from_file(opt.passwd_file,'passphrase').strip('\r\n') - else: - return my_raw_input(prompt, echo=opt.echo_passphrase) - - -def get_hash_preset_from_user(hp=g.hash_preset,desc='data'): - p = """Enter hash preset for %s, - or hit ENTER to accept the default value ('%s'): """ % (desc,hp) - while True: - ret = my_raw_input(p) - if ret: - if ret in g.hash_presets.keys(): return ret - else: - msg('Invalid input. Valid choices are %s' % - ', '.join(sorted(g.hash_presets.keys()))) - continue - else: return hp - - def my_raw_input(prompt,echo=True,insert_txt='',use_readline=True): try: import readline @@ -651,7 +585,6 @@ def my_raw_input(prompt,echo=True,insert_txt='',use_readline=True): return reply.strip() - def keypress_confirm(prompt,default_yes=False,verbose=False): from mmgen.term import get_char @@ -670,7 +603,6 @@ def keypress_confirm(prompt,default_yes=False,verbose=False): if verbose: msg('\nInvalid reply') else: msg_r('\r') - def prompt_and_get_char(prompt,chars,enter_ok=False,verbose=False): from mmgen.term import get_char @@ -685,7 +617,6 @@ def prompt_and_get_char(prompt,chars,enter_ok=False,verbose=False): if verbose: msg('\nInvalid reply') else: msg_r('\r') - def do_license_msg(immed=False): if opt.quiet or g.no_license: return @@ -708,7 +639,6 @@ def do_license_msg(immed=False): msg_r('\r') msg('') - def get_bitcoind_cfg_options(cfg_keys): cfg_file = os.path.join(get_homedir(), get_datadir(), 'bitcoin.conf') diff --git a/test/test.py b/test/test.py index 4f333ca7..8df9132b 100755 --- a/test/test.py +++ b/test/test.py @@ -1176,13 +1176,13 @@ class MMGenTestSuite(object): if opt.verbose or opt.exact_output: sys.stderr.write(green('Generating fake tracking wallet info\n')) silence() - from mmgen.addr import AddrList,AddrData + from mmgen.addr import AddrList,AddrData,AddrIdxList tx_data,ad = {},AddrData() for s in sources: afile = get_file_with_ext('addrs',cfgs[s]['tmpdir']) ai = AddrList(afile) ad.add(ai) - aix = parse_addr_idxs(cfgs[s]['addr_idx_list']) + aix = AddrIdxList(fmt_str=cfgs[s]['addr_idx_list']) if len(aix) != addrs_per_wallet: errmsg(red('Address index list length != %s: %s' % (addrs_per_wallet,repr(aix)))) @@ -1244,8 +1244,8 @@ class MMGenTestSuite(object): for num in tx_data: 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) + t.expect(r"'q'=quit view, .*?:.",'M', regex=True) + t.expect(r"'q'=quit view, .*?:.",'q', regex=True) 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: ',