# along with this program. If not, see <http://www.gnu.org/licenses/>.
-import sys, base64, os, re, hashlib, copy, operator, ast, threading, random
-import slowaes, ecdsa
+import sys, base64, os, re, hashlib, copy, operator, ast, threading, random, getpass
+import aes, ecdsa
from ecdsa.util import string_to_number, number_to_string
############ functions from pywallet #####################
bytes = b58decode(addr, 25)
return bytes[1:21]
+def encode_point(pubkey, compressed=False):
+ order = generator_secp256k1.order()
+ p = pubkey.pubkey.point
+ x_str = ecdsa.util.number_to_string(p.x(), order)
+ y_str = ecdsa.util.number_to_string(p.y(), order)
+ if compressed:
+ return chr(2 + (p.y() & 1)) + x_str
+ else:
+ return chr(4) + pubkey.to_string() #x_str + y_str
+
__b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
__b58base = len(__b58chars)
########### end pywallet functions #######################
+# get password routine
+def prompt_password(prompt, confirm=True):
+ if sys.stdin.isatty():
+ password = getpass.getpass(prompt)
+
+ if password and confirm:
+ password2 = getpass.getpass("Confirm: ")
+
+ if password != password2:
+ sys.stderr.write("Error: Passwords do not match.\n")
+ sys.stderr.flush()
+ sys.exit(1)
+
+ else:
+ password = raw_input(prompt)
+
+ if not password:
+ password = None
+
+ return password
+
# URL decode
_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE)
urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x)
def format_satoshis(x, is_diff=False, num_zeros = 0):
from decimal import Decimal
- s = str( Decimal(x) /100000000 )
- if is_diff and x>0:
+ s = Decimal(x)
+ sign, digits, exp = s.as_tuple()
+ digits = map(str, digits)
+ while len(digits) < 9:
+ digits.insert(0,'0')
+ digits.insert(-8,'.')
+ s = ''.join(digits).rstrip('0')
+ if sign:
+ s = '-' + s
+ elif is_diff:
s = "+" + s
- if not '.' in s: s += '.'
+
p = s.find('.')
s += "0"*( 1 + num_zeros - ( len(s) - p ))
s += " "*( 9 - ( len(s) - p ))
class Wallet:
- def __init__(self, gui_callback = lambda: None):
+ def __init__(self):
self.electrum_version = ELECTRUM_VERSION
self.seed_version = SEED_VERSION
- self.gui_callback = gui_callback
+ self.update_callbacks = []
self.gap_limit = 5 # configuration
+ self.use_change = True
self.fee = 100000
self.num_zeros = 0
self.master_public_key = ''
self.aliases = {} # aliases for addresses
self.authorities = {} # trusted addresses
self.frozen_addresses = []
+ self.prioritized_addresses = []
+ self.expert_mode = False
self.receipts = {} # signed URIs
self.receipt = None # next receipt
self.addressbook = [] # outgoing addresses, for payments
+ self.debug_server = False # write server communication debug info to stdout
# not saved
self.tx_history = {}
self.pick_random_server()
+ def register_callback(self, update_callback):
+ with self.lock:
+ self.update_callbacks.append(update_callback)
+ def trigger_callbacks(self):
+ with self.lock:
+ callbacks = self.update_callbacks[:]
+ [update() for update in callbacks]
def pick_random_server(self):
self.server = random.choice( DEFAULT_SERVERS ) # random choice when the wallet is created
def import_key(self, keypair, password):
address, key = keypair.split(':')
- if not self.is_valid(address): return False
- if address in self.all_addresses(): return False
+ if not self.is_valid(address):
+ raise BaseException('Invalid Bitcoin address')
+ if address in self.all_addresses():
+ raise BaseException('Address already in wallet')
b = ASecretToSecret( key )
- if not b: return False
+ if not b:
+ raise BaseException('Unsupported key format')
secexp = int( b.encode('hex'), 16)
private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve=SECP256k1 )
# sanity check
public_key = private_key.get_verifying_key()
- if not address == public_key_to_bc_address( '04'.decode('hex') + public_key.to_string() ): return False
+ if not address == public_key_to_bc_address( '04'.decode('hex') + public_key.to_string() ):
+ raise BaseException('Address does not match private key')
self.imported_keys[address] = self.pw_encode( key, password )
- return True
def new_seed(self, password):
seed = "%032x"%ecdsa.util.randrange( pow(2,128) )
def get_sequence(self,n,for_change):
return string_to_number( Hash( "%d:%d:"%(n,for_change) + self.master_public_key ) )
+ def get_private_key_base58(self, address, password):
+ pk = self.get_private_key(address, password)
+ if pk is None: return None
+ return SecretToASecret( pk )
+
def get_private_key(self, address, password):
""" Privatekey(type,n) = Master_private_key + H(n|S|type) """
order = generator_secp256k1.order()
if address in self.imported_keys.keys():
b = self.pw_decode( self.imported_keys[address], password )
+ if not b: return None
b = ASecretToSecret( b )
secexp = int( b.encode('hex'), 16)
else:
seed = self.pw_decode( self.seed, password)
except:
raise BaseException("Invalid password")
+ if not seed: return None
secexp = self.stretch_key(seed)
secexp = ( secexp + self.get_sequence(n,for_change) ) % order
continue
else:
raise BaseException("error: cannot sign message")
-
-
+
+
def verify_message(self, address, signature, message):
""" See http://www.secg.org/download/aid-780/sec1-v2.pdf for the math """
from ecdsa import numbertheory, ellipticcurve, util
sig = base64.b64decode(signature)
if len(sig) != 65: raise BaseException("Wrong encoding")
r,s = util.sigdecode_string(sig[1:], order)
- recid = ord(sig[0]) - 27
+ nV = ord(sig[0])
+ if nV < 27 or nV >= 35:
+ raise BaseException("Bad encoding")
+ if nV >= 31:
+ compressed = True
+ nV -= 4
+ else:
+ compressed = False
+
+ recid = nV - 27
# 1.1
x = r + (recid/2) * order
# 1.3
# check that Q is the public key
public_key.verify_digest( sig[1:], h, sigdecode = ecdsa.util.sigdecode_string)
# check that we get the original signing address
- addr = public_key_to_bc_address( '04'.decode('hex') + public_key.to_string() )
- # print addr
+ addr = public_key_to_bc_address( encode_point(public_key, compressed) )
if address != addr:
- print "bad signature"
raise BaseException("Bad signature")
return address
+ def change_gap_limit(self, value):
+ if value >= self.gap_limit:
+ self.gap_limit = value
+ self.save()
+ self.interface.poke()
+ return True
+
+ elif value >= self.min_acceptable_gap():
+ k = self.num_unused_trailing_addresses()
+ n = len(self.addresses) - k + value
+ self.addresses = self.addresses[0:n]
+ self.gap_limit = value
+ self.save()
+ return True
+ else:
+ return False
+
+ def num_unused_trailing_addresses(self):
+ k = 0
+ for a in self.addresses[::-1]:
+ if self.history.get(a):break
+ k = k + 1
+ return k
+
+ def min_acceptable_gap(self):
+ # fixme: this assumes wallet is synchronized
+ n = 0
+ nmax = 0
+ k = self.num_unused_trailing_addresses()
+ for a in self.addresses[0:-k]:
+ if self.history.get(a):
+ n = 0
+ else:
+ n += 1
+ if n > nmax: nmax = n
+ return nmax + 1
+
def synchronize(self):
if not self.master_public_key:
s = {
'seed_version':self.seed_version,
'use_encryption':self.use_encryption,
+ 'use_change':self.use_change,
'master_public_key': self.master_public_key.encode('hex'),
'fee':self.fee,
'server':self.server,
'receipts':self.receipts,
'num_zeros':self.num_zeros,
'frozen_addresses':self.frozen_addresses,
+ 'prioritized_addresses':self.prioritized_addresses,
+ 'expert_mode':self.expert_mode,
+ 'gap_limit':self.gap_limit,
+ 'debug_server':self.debug_server,
}
f = open(self.path,"w")
f.write( repr(s) )
f.close()
+ import stat
+ os.chmod(self.path,stat.S_IREAD | stat.S_IWRITE)
def read(self):
import interface
f.close()
except:
return
- data = interface.old_to_new(data)
try:
d = ast.literal_eval( data )
+ interface.old_to_new(d)
self.seed_version = d.get('seed_version')
self.master_public_key = d.get('master_public_key').decode('hex')
self.use_encryption = d.get('use_encryption')
+ self.use_change = bool(d.get('use_change',True))
self.fee = int( d.get('fee') )
self.seed = d.get('seed')
self.server = d.get('server')
self.receipts = d.get('receipts',{})
self.num_zeros = d.get('num_zeros',0)
self.frozen_addresses = d.get('frozen_addresses',[])
+ self.prioritized_addresses = d.get('prioritized_addresses',[])
+ self.expert_mode = d.get('expert_mode',False)
+ self.gap_limit = d.get('gap_limit',5)
+ self.debug_server = d.get('debug_server',False)
except:
raise BaseException("cannot read wallet file")
self.file_exists = True
+ def get_address_flags(self, addr):
+ flags = "C" if self.is_change(addr) else "I" if addr in self.imported_keys.keys() else "-"
+ flags += "F" if addr in self.frozen_addresses else "P" if addr in self.prioritized_addresses else "-"
+ return flags
def get_addr_balance(self, addr):
fee = self.fee if fixed_fee is None else fixed_fee
coins = []
+ prioritized_coins = []
domain = [from_addr] if from_addr else self.all_addresses()
for i in self.frozen_addresses:
if i in domain: domain.remove(i)
+ for i in self.prioritized_addresses:
+ if i in domain: domain.remove(i)
+
for addr in domain:
h = self.history.get(addr)
if h is None: continue
coins.append( (addr,item))
coins = sorted( coins, key = lambda x: x[1]['timestamp'] )
+
+ for addr in self.prioritized_addresses:
+ h = self.history.get(addr)
+ if h is None: continue
+ for item in h:
+ if item.get('raw_output_script'):
+ prioritized_coins.append( (addr,item))
+
+ prioritized_coins = sorted( prioritized_coins, key = lambda x: x[1]['timestamp'] )
+
inputs = []
+ coins = prioritized_coins + coins
+
for c in coins:
addr, item = c
v = item.get('value')
fee = self.fee*len(inputs) if fixed_fee is None else fixed_fee
if total >= amount + fee: break
else:
- #print "not enough funds: %d %d"%(total, fee)
+ #print "not enough funds: %s %s"%(format_satoshis(total), format_satoshis(fee))
inputs = []
return inputs, total, fee
default_label = ''
if tx['value']<0:
for o_addr in tx['outputs']:
- if not self.is_change(o_addr):
+ if not self.is_mine(o_addr):
dest_label = self.labels.get(o_addr)
if dest_label:
default_label = 'to: ' + dest_label
else:
for o_addr in tx['outputs']:
if self.is_mine(o_addr) and not self.is_change(o_addr):
- dest_label = self.labels.get(o_addr)
- if dest_label:
- default_label = 'at: ' + dest_label
- else:
- default_label = 'at: ' + o_addr
+ break
+ else:
+ for o_addr in tx['outputs']:
+ if self.is_mine(o_addr):
+ break
+ else:
+ o_addr = None
+
+ if o_addr:
+ dest_label = self.labels.get(o_addr)
+ if dest_label:
+ default_label = 'at: ' + dest_label
+ else:
+ default_label = 'at: ' + o_addr
+
tx['default_label'] = default_label
def mktx(self, to_address, amount, label, password, fee=None, change_addr=None, from_addr= None):
inputs, total, fee = self.choose_tx_inputs( amount, fee, from_addr )
if not inputs:
raise BaseException("Not enough funds")
+
+ if not self.use_change and not change_addr:
+ change_addr = inputs[0][0]
+ print "Sending change to", change_addr
+
outputs = self.choose_tx_outputs( to_address, amount, fee, total, change_addr )
s_inputs = self.sign_inputs( inputs, outputs, password )
return target, signing_addr, auth_name
- def update_password(self, seed, new_password):
+ def update_password(self, seed, old_password, new_password):
if new_password == '': new_password = None
self.use_encryption = (new_password != None)
self.seed = self.pw_encode( seed, new_password)
for k in self.imported_keys.keys():
a = self.imported_keys[k]
- b = self.pw_decode(a, password)
+ b = self.pw_decode(a, old_password)
c = self.pw_encode(b, new_password)
self.imported_keys[k] = c
self.save()
except BaseException, e:
# raise exception if verify fails (verify the chain)
if interactive:
- show_message("Alias error: " + e.message)
+ show_message("Alias error: " + str(e))
return
print target, signing_address, auth_name
else:
print k,v
+ if label and self.labels.get(address) != label:
+ if question('Give label "%s" to address %s ?'%(label,address)):
+ if address not in self.addressbook and address not in self.all_addresses():
+ self.addressbook.append(address)
+ self.labels[address] = label
+
if signature:
if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', identity):
signing_address = self.get_alias(identity, True, show_message, question)
def update(self):
self.interface.poke()
- self.up_to_date_event.wait()
+ self.up_to_date_event.wait(10000000000)
def start_session(self, interface):
self.interface.subscribe(self.all_addresses())
+ def freeze(self,addr):
+ if addr in self.all_addresses() and addr not in self.frozen_addresses:
+ self.unprioritize(addr)
+ self.frozen_addresses.append(addr)
+ self.save()
+ return True
+ else:
+ return False
+ def unfreeze(self,addr):
+ if addr in self.all_addresses() and addr in self.frozen_addresses:
+ self.frozen_addresses.remove(addr)
+ self.save()
+ return True
+ else:
+ return False
+ def prioritize(self,addr):
+ if addr in self.all_addresses() and addr not in self.prioritized_addresses:
+ self.unfreeze(addr)
+ self.prioritized_addresses.append(addr)
+ self.save()
+ return True
+ else:
+ return False
+
+ def unprioritize(self,addr):
+ if addr in self.all_addresses() and addr in self.prioritized_addresses:
+ self.prioritized_addresses.remove(addr)
+ self.save()
+ return True
+ else:
+ return False