Label editing in mmgen-txcreate

This commit is contained in:
The MMGen Project 2016-07-30 14:42:22 +03:00
commit a6547be82c
12 changed files with 224 additions and 248 deletions

View file

@ -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:]

View file

@ -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

View file

@ -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):

View file

@ -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

View file

@ -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):

View file

@ -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]

View file

@ -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(',')

View file

@ -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()

View file

@ -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>':'<index or range>"
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

View file

@ -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,'')

View file

@ -17,7 +17,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
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')

View file

@ -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: ',