electrum-bitcoinprivate/lib/wallet.py

1349 lines
49 KiB
Python
Raw Normal View History

#!/usr/bin/env python
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2011 thomasv@gitorious
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
2012-08-23 18:21:17 -07:00
import sys
import base64
import os
import re
import hashlib
import copy
import operator
import ast
import threading
import random
import aes
import ecdsa
import Queue
import time
from ecdsa.util import string_to_number, number_to_string
from util import print_error, user_dir, format_satoshis
from bitcoin import *
2012-02-14 03:45:39 -08:00
# 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)
# AES encryption
EncodeAES = lambda secret, s: base64.b64encode(aes.encryptData(secret,s))
DecodeAES = lambda secret, e: aes.decryptData(secret, base64.b64decode(e))
2012-02-05 22:48:52 -08:00
from version import ELECTRUM_VERSION, SEED_VERSION
class Wallet:
def __init__(self, config={}):
self.config = config
self.electrum_version = ELECTRUM_VERSION
# saved fields
self.seed_version = config.get('seed_version', SEED_VERSION)
self.gap_limit = config.get('gap_limit', 5)
self.use_change = config.get('use_change',True)
self.fee = int(config.get('fee',100000))
self.num_zeros = int(config.get('num_zeros',0))
self.master_public_key = config.get('master_public_key','')
self.use_encryption = config.get('use_encryption', False)
self.addresses = config.get('addresses', []) # receiving addresses visible for user
self.change_addresses = config.get('change_addresses', []) # addresses used as change
self.seed = config.get('seed', '') # encrypted
self.labels = config.get('labels',{'1NmduGNyC5XejoysbuioodCN3jR3yf64xM':'Electrum donation address'})
self.aliases = config.get('aliases', {}) # aliases for addresses
self.authorities = config.get('authorities', {}) # trusted addresses
self.frozen_addresses = config.get('frozen_addresses',[])
self.prioritized_addresses = config.get('prioritized_addresses',[])
self.receipts = config.get('receipts',{}) # signed URIs
self.addressbook = config.get('contacts', ['1NmduGNyC5XejoysbuioodCN3jR3yf64xM'])
self.imported_keys = config.get('imported_keys',{})
2012-11-04 06:47:19 -08:00
self.history = config.get('addr_history',{}) # address -> list(txid, height)
self.transactions = config.get('transactions',{}) # txid -> deserialised
self.requested_amounts = config.get('requested_amounts',{}) # txid -> deserialised
# not saved
self.prevout_values = {} # my own transaction outputs
self.spent_outputs = []
self.receipt = None # next receipt
self.banner = ''
# spv
self.verifier = None
2012-10-14 22:43:00 -07:00
# there is a difference between wallet.up_to_date and interface.is_up_to_date()
# interface.is_up_to_date() returns true when all requests have been answered and processed
# wallet.up_to_date is true when the wallet is synchronized (stronger requirement)
2012-11-20 12:36:06 -08:00
2012-03-28 05:22:46 -07:00
self.up_to_date = False
2012-03-31 02:47:16 -07:00
self.lock = threading.Lock()
2012-03-24 05:15:23 -07:00
self.tx_event = threading.Event()
if self.seed_version != SEED_VERSION:
raise ValueError("This wallet seed is deprecated. Please run upgrade.py for a diagnostic.")
for tx_hash in self.transactions.keys():
self.update_tx_outputs(tx_hash)
2012-11-20 12:36:06 -08:00
def set_up_to_date(self,b):
with self.lock: self.up_to_date = b
def is_up_to_date(self):
with self.lock: return self.up_to_date
2012-04-01 08:50:12 -07:00
def update(self):
self.up_to_date = False
self.interface.poke('synchronizer')
2012-11-22 11:14:42 -08:00
while not self.is_up_to_date(): time.sleep(0.1)
def import_key(self, keypair, password):
2012-12-31 01:41:02 -08:00
address, sec = keypair.split(':')
2012-05-30 05:42:30 -07:00
if not self.is_valid(address):
raise BaseException('Invalid Bitcoin address')
if address in self.all_addresses():
raise BaseException('Address already in wallet')
2012-12-31 01:41:02 -08:00
# rebuild public key from private key, compressed or uncompressed
pkey = regenerate_key(sec)
if not pkey:
return False
# figure out if private key is compressed
compressed = is_compressed(sec)
# rebuild private and public key from regenerated secret
private_key = GetPrivKey(pkey, compressed)
public_key = GetPubKey(pkey, compressed)
addr = public_key_to_bc_address(public_key)
# sanity check
2012-12-31 01:41:02 -08:00
if not address == addr :
2012-05-30 05:42:30 -07:00
raise BaseException('Address does not match private key')
2012-12-31 01:41:02 -08:00
# store the originally requested keypair into the imported keys table
self.imported_keys[address] = self.pw_encode(sec, password )
def new_seed(self, password):
seed = "%032x"%ecdsa.util.randrange( pow(2,128) )
#self.init_mpk(seed)
# encrypt
2012-01-19 08:39:24 -08:00
self.seed = self.pw_encode( seed, password )
def init_mpk(self,seed):
# public key
curve = SECP256k1
secexp = self.stretch_key(seed)
master_private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
self.master_public_key = master_private_key.get_verifying_key().to_string().encode('hex')
def all_addresses(self):
return self.addresses + self.change_addresses + self.imported_keys.keys()
def is_mine(self, address):
return address in self.all_addresses()
def is_change(self, address):
return address in self.change_addresses
def is_valid(self,addr):
ADDRESS_RE = re.compile('[1-9A-HJ-NP-Za-km-z]{26,}\\Z')
if not ADDRESS_RE.match(addr): return False
try:
h = bc_address_to_hash_160(addr)
except:
return False
return addr == hash_160_to_bc_address(h)
def stretch_key(self,seed):
oldseed = seed
for i in range(100000):
seed = hashlib.sha256(seed + oldseed).digest()
return string_to_number( seed )
def get_sequence(self,n,for_change):
return string_to_number( Hash( "%d:%d:"%(n,for_change) + self.master_public_key.decode('hex') ) )
def get_private_key_base58(self, address, password):
2012-12-31 01:41:02 -08:00
secexp, compressed = self.get_private_key(address, password)
if secexp is None: return None
pk = number_to_string( secexp, generator_secp256k1.order() )
return SecretToASecret( pk, compressed )
2012-02-06 09:10:30 -08:00
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():
2012-12-31 01:41:02 -08:00
sec = self.pw_decode( self.imported_keys[address], password )
if not sec: return None, None
pkey = regenerate_key(sec)
compressed = is_compressed(sec)
secexp = pkey.secret
else:
if address in self.addresses:
n = self.addresses.index(address)
for_change = False
elif address in self.change_addresses:
n = self.change_addresses.index(address)
for_change = True
else:
raise BaseException("unknown address")
2012-02-21 14:27:21 -08:00
try:
seed = self.pw_decode( self.seed, password)
except:
raise BaseException("Invalid password")
2012-05-16 23:42:40 -07:00
if not seed: return None
secexp = self.stretch_key(seed)
secexp = ( secexp + self.get_sequence(n,for_change) ) % order
2012-12-31 01:41:02 -08:00
compressed = False
2012-12-31 01:41:02 -08:00
return secexp, compressed
def msg_magic(self, message):
return "\x18Bitcoin Signed Message:\n" + chr( len(message) ) + message
def sign_message(self, address, message, password):
2012-12-31 01:41:02 -08:00
secexp, compressed = self.get_private_key(address, password)
private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
public_key = private_key.get_verifying_key()
signature = private_key.sign_digest( Hash( self.msg_magic( message ) ), sigencode = ecdsa.util.sigencode_string )
assert public_key.verify_digest( signature, Hash( self.msg_magic( message ) ), sigdecode = ecdsa.util.sigdecode_string)
for i in range(4):
2012-12-31 01:41:02 -08:00
sig = base64.b64encode( chr(27 + i + (4 if compressed else 0)) + signature )
2012-02-03 02:48:09 -08:00
try:
self.verify_message( address, sig, message)
return sig
2012-02-03 02:48:09 -08:00
except:
continue
else:
raise BaseException("error: cannot sign message")
2012-06-12 01:47:00 -07:00
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
import msqr
curve = curve_secp256k1
G = generator_secp256k1
order = G.order()
# extract r,s from signature
sig = base64.b64decode(signature)
2012-02-06 08:59:31 -08:00
if len(sig) != 65: raise BaseException("Wrong encoding")
r,s = util.sigdecode_string(sig[1:], order)
2012-06-12 01:47:00 -07:00
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
alpha = ( x * x * x + curve.a() * x + curve.b() ) % curve.p()
beta = msqr.modular_sqrt(alpha, curve.p())
y = beta if (beta - recid) % 2 == 0 else curve.p() - beta
# 1.4 the constructor checks that nR is at infinity
2012-02-03 02:48:09 -08:00
R = ellipticcurve.Point(curve, x, y, order)
# 1.5 compute e from message:
h = Hash( self.msg_magic( message ) )
e = string_to_number(h)
minus_e = -e % order
# 1.6 compute Q = r^-1 (sR - eG)
inv_r = numbertheory.inverse_mod(r,order)
Q = inv_r * ( s * R + minus_e * G )
public_key = ecdsa.VerifyingKey.from_public_point( Q, curve = SECP256k1 )
# check that Q is the public key
2012-02-03 02:48:09 -08:00
public_key.verify_digest( sig[1:], h, sigdecode = ecdsa.util.sigdecode_string)
# check that we get the original signing address
2012-06-12 01:47:00 -07:00
addr = public_key_to_bc_address( encode_point(public_key, compressed) )
2012-02-06 09:07:01 -08:00
if address != addr:
2012-02-06 08:59:31 -08:00
raise BaseException("Bad signature")
2012-01-20 08:21:29 -08:00
def create_new_address(self, for_change):
n = len(self.change_addresses) if for_change else len(self.addresses)
address = self.get_new_address(n, for_change)
if for_change:
self.change_addresses.append(address)
else:
self.addresses.append(address)
self.history[address] = []
return address
def get_new_address(self, n, for_change):
""" Publickey(type,n) = Master_public_key + H(n|S|type)*point """
curve = SECP256k1
z = self.get_sequence(n, for_change)
master_public_key = ecdsa.VerifyingKey.from_string( self.master_public_key.decode('hex'), curve = SECP256k1 )
pubkey_point = master_public_key.pubkey.point + z*curve.generator
public_key2 = ecdsa.VerifyingKey.from_public_point( pubkey_point, curve = SECP256k1 )
address = public_key_to_bc_address( '04'.decode('hex') + public_key2.to_string() )
print address
return address
2012-06-07 09:52:29 -07:00
def change_gap_limit(self, value):
if value >= self.gap_limit:
self.gap_limit = value
self.save()
2012-10-26 15:02:52 -07:00
self.interface.poke('synchronizer')
2012-06-07 09:52:29 -07:00
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
2012-06-07 09:52:29 -07:00
def synchronize(self):
if not self.master_public_key:
return []
2012-02-22 06:39:06 -08:00
new_addresses = []
while True:
if self.change_addresses == []:
new_addresses.append( self.create_new_address(True) )
continue
a = self.change_addresses[-1]
if self.history.get(a):
new_addresses.append( self.create_new_address(True) )
else:
break
n = self.gap_limit
while True:
if len(self.addresses) < n:
new_addresses.append( self.create_new_address(False) )
continue
if map( lambda a: self.history.get(a, []), self.addresses[-n:] ) == n*[[]]:
break
else:
new_addresses.append( self.create_new_address(False) )
return new_addresses
2012-02-22 06:39:06 -08:00
def is_found(self):
return (len(self.change_addresses) > 1 ) or ( len(self.addresses) > self.gap_limit )
def fill_addressbook(self):
for tx_hash, tx in self.transactions.items():
is_send, _, _ = self.get_tx_value(tx_hash)
if is_send:
2012-11-04 11:53:27 -08:00
for o in tx['outputs']:
addr = o.get('address')
if not self.is_mine(addr) and addr not in self.addressbook:
self.addressbook.append(addr)
# redo labels
2012-11-04 11:53:27 -08:00
# self.update_tx_labels()
2012-06-07 07:14:08 -07:00
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_tx_value(self, tx_hash, addresses = None):
# return the balance for that tx
if addresses is None: addresses = self.all_addresses()
2012-11-04 08:17:40 -08:00
with self.lock:
is_send = False
is_pruned = False
v_in = v_out = v_out_mine = 0
2012-11-04 08:17:40 -08:00
d = self.transactions.get(tx_hash)
2012-11-18 02:34:52 -08:00
if not d:
return 0, 0, 0
2012-11-04 08:17:40 -08:00
for item in d.get('inputs'):
addr = item.get('address')
if addr in addresses:
is_send = True
2012-11-04 08:17:40 -08:00
key = item['prevout_hash'] + ':%d'%item['prevout_n']
2012-11-14 06:33:44 -08:00
value = self.prevout_values.get( key )
if value is None:
is_pruned = True
else:
v_in += value
else:
is_pruned = True
2012-11-04 08:17:40 -08:00
for item in d.get('outputs'):
addr = item.get('address')
value = item.get('value')
v_out += value
if addr in addresses:
v_out_mine += value
if not is_pruned:
# all inputs are mine:
fee = v_out - v_in
v = v_out_mine - v_in
else:
# some inputs are mine:
fee = None
if is_send:
v = v_out_mine - v_out
else:
# no input is mine
v = v_out_mine
return is_send, v, fee
def get_tx_details(self, tx_hash):
import datetime
if not tx_hash: return ''
tx = self.transactions.get(tx_hash)
is_mine, v, fee = self.get_tx_value(tx_hash)
2012-12-05 06:16:52 -08:00
conf, timestamp = self.verifier.get_confirmations(tx_hash)
if conf:
time_str = datetime.datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
else:
time_str = 'pending'
inputs = map(lambda x: x.get('address'), tx['inputs'])
outputs = map(lambda x: x.get('address'), tx['outputs'])
tx_details = "Transaction Details" +"\n\n" \
+ "Transaction ID:\n" + tx_hash + "\n\n" \
+ "Status: %d confirmations\n"%conf
if is_mine:
if fee:
tx_details += "Amount sent: %s\n"% format_satoshis(v-fee, False) \
+ "Transaction fee: %s\n"% format_satoshis(fee, False)
else:
tx_details += "Amount sent: %s\n"% format_satoshis(v, False) \
+ "Transaction fee: unknown\n"
else:
tx_details += "Amount received: %s\n"% format_satoshis(v, False) \
tx_details += "Date: %s\n\n"%time_str \
+ "Inputs:\n-"+ '\n-'.join(inputs) + "\n\n" \
+ "Outputs:\n-"+ '\n-'.join(outputs)
r = self.receipts.get(tx_hash)
if r:
tx_details += "\n_______________________________________" \
+ '\n\nSigned URI: ' + r[2] \
+ "\n\nSigned by: " + r[0] \
+ '\n\nSignature: ' + r[1]
return tx_details
def update_tx_outputs(self, tx_hash):
tx = self.transactions.get(tx_hash)
for item in tx.get('outputs'):
value = item.get('value')
key = tx_hash+ ':%d'%item.get('index')
with self.lock:
self.prevout_values[key] = value
for item in tx.get('inputs'):
if self.is_mine(item.get('address')):
key = item['prevout_hash'] + ':%d'%item['prevout_n']
self.spent_outputs.append(key)
def get_addr_balance(self, address):
assert self.is_mine(address)
h = self.history.get(address,[])
2012-11-14 06:33:44 -08:00
if h == ['*']: return 0,0
c = u = 0
2012-12-28 08:57:33 -08:00
received_coins = [] # list of coins received at address
2012-11-22 04:24:44 -08:00
for tx_hash, tx_height in h:
d = self.transactions.get(tx_hash)
if not d: continue
for item in d.get('outputs'):
addr = item.get('address')
2012-12-28 08:57:33 -08:00
if addr == address:
key = tx_hash + ':%d'%item['index']
received_coins.append(key)
2012-11-22 04:24:44 -08:00
for tx_hash, tx_height in h:
d = self.transactions.get(tx_hash)
if not d: continue
v = 0
for item in d.get('inputs'):
addr = item.get('address')
if addr == address:
key = item['prevout_hash'] + ':%d'%item['prevout_n']
value = self.prevout_values.get( key )
if key in received_coins:
v -= value
for item in d.get('outputs'):
addr = item.get('address')
key = tx_hash + ':%d'%item['index']
if addr == address:
v += item.get('value')
if tx_height:
c += v
else:
u += v
return c, u
def get_balance(self):
conf = unconf = 0
for addr in self.all_addresses():
c, u = self.get_addr_balance(addr)
conf += c
unconf += u
return conf, unconf
def choose_tx_inputs( self, amount, fixed_fee, from_addr = None ):
""" todo: minimize tx size """
total = 0
fee = self.fee if fixed_fee is None else fixed_fee
coins = []
2012-06-06 06:40:57 -07:00
prioritized_coins = []
domain = [from_addr] if from_addr else self.all_addresses()
2012-05-05 02:33:53 -07:00
for i in self.frozen_addresses:
if i in domain: domain.remove(i)
2012-06-06 06:40:57 -07:00
for i in self.prioritized_addresses:
if i in domain: domain.remove(i)
for addr in domain:
h = self.history.get(addr, [])
2012-11-15 00:14:24 -08:00
if h == ['*']: continue
2012-11-15 03:14:29 -08:00
for tx_hash, tx_height in h:
tx = self.transactions.get(tx_hash)
for output in tx.get('outputs'):
if output.get('address') != addr: continue
key = tx_hash + ":%d" % output.get('index')
if key in self.spent_outputs: continue
output['tx_hash'] = tx_hash
coins.append(output)
2012-06-06 06:40:57 -07:00
for addr in self.prioritized_addresses:
h = self.history.get(addr, [])
2012-11-15 03:14:29 -08:00
if h == ['*']: continue
for tx_hash, tx_height in h:
2012-11-23 01:16:04 -08:00
tx = self.transactions.get(tx_hash)
for output in tx.get('outputs'):
if output.get('address') != addr: continue
key = tx_hash + ":%d" % output.get('index')
if key in self.spent_outputs: continue
output['tx_hash'] = tx_hash
prioritized_coins.append(output)
2012-06-06 06:40:57 -07:00
inputs = []
2012-06-06 06:40:57 -07:00
coins = prioritized_coins + coins
for item in coins:
addr = item.get('address')
v = item.get('value')
total += v
2012-03-30 01:48:32 -07:00
inputs.append((addr, v, item['tx_hash'], item['index'], item['raw_output_script'], None, None) )
fee = self.fee*len(inputs) if fixed_fee is None else fixed_fee
if total >= amount + fee: break
else:
2012-06-08 04:14:25 -07:00
#print "not enough funds: %s %s"%(format_satoshis(total), format_satoshis(fee))
inputs = []
return inputs, total, fee
def add_tx_change( self, outputs, amount, fee, total, change_addr=None ):
change_amount = total - ( amount + fee )
if change_amount != 0:
# normally, the update thread should ensure that the last change address is unused
if not change_addr:
change_addr = self.change_addresses[-1]
outputs.append( ( change_addr, change_amount) )
return outputs
def sign_inputs( self, inputs, outputs, password ):
s_inputs = []
for i in range(len(inputs)):
addr, v, p_hash, p_pos, p_scriptPubKey, _, _ = inputs[i]
2012-12-31 01:41:02 -08:00
secexp, compressed = self.get_private_key(addr, password)
private_key = ecdsa.SigningKey.from_secret_exponent( secexp, curve = SECP256k1 )
public_key = private_key.get_verifying_key()
2012-12-31 01:41:02 -08:00
pkey = EC_KEY(secexp)
pubkey = GetPubKey(pkey, compressed)
tx = filter( raw_tx( inputs, outputs, for_sig = i ) )
sig = private_key.sign_digest( Hash( tx.decode('hex') ), sigencode = ecdsa.util.sigencode_der )
assert public_key.verify_digest( sig, Hash( tx.decode('hex') ), sigdecode = ecdsa.util.sigdecode_der)
s_inputs.append( (addr, v, p_hash, p_pos, p_scriptPubKey, pubkey, sig) )
return s_inputs
def pw_encode(self, s, password):
if password:
secret = Hash(password)
return EncodeAES(secret, s)
else:
return s
def pw_decode(self, s, password):
if password is not None:
secret = Hash(password)
d = DecodeAES(secret, s)
if s == self.seed:
try:
d.decode('hex')
except:
2012-08-23 18:11:57 -07:00
raise ValueError("Invalid password")
return d
else:
return s
def get_history(self, address):
with self.lock:
return self.history.get(address)
def get_status(self, h):
if not h: return None
2012-11-14 06:33:44 -08:00
if h == ['*']: return '*'
status = ''
for tx_hash, height in h:
status += tx_hash + ':%d:' % height
return hashlib.sha256( status ).digest().encode('hex')
2012-03-30 05:15:05 -07:00
2012-03-31 02:47:16 -07:00
def receive_tx_callback(self, tx_hash, tx):
if not self.check_new_tx(tx_hash, tx):
raise BaseException("error: received transaction is not consistent with history"%tx_hash)
with self.lock:
self.transactions[tx_hash] = tx
2012-11-15 01:34:56 -08:00
tx_height = tx.get('height')
2012-11-18 02:34:52 -08:00
if self.verifier and tx_height>0:
self.verifier.add(tx_hash, tx_height)
2012-11-15 01:34:56 -08:00
2012-11-04 11:53:27 -08:00
self.update_tx_outputs(tx_hash)
self.save()
def receive_history_callback(self, addr, hist):
if not self.check_new_history(addr, hist):
raise BaseException("error: received history for %s is not consistent with known transactions"%addr)
with self.lock:
self.history[addr] = hist
2012-03-31 02:47:16 -07:00
self.save()
2012-11-14 06:33:44 -08:00
if hist != ['*']:
for tx_hash, tx_height in hist:
if tx_height>0:
2012-11-15 03:14:29 -08:00
# add it in case it was previously unconfirmed
2012-11-18 02:34:52 -08:00
if self.verifier: self.verifier.add(tx_hash, tx_height)
2012-11-15 03:14:29 -08:00
# set the height in case it changed
tx = self.transactions.get(tx_hash)
if tx:
if tx.get('height') != tx_height:
print_error( "changing height for tx", tx_hash )
tx['height'] = tx_height
def get_tx_history(self):
with self.lock:
history = self.transactions.values()
2012-12-05 06:16:52 -08:00
history.sort(key = lambda x: x.get('height') if x.get('height') else 1e12)
result = []
balance = 0
for tx in history:
is_mine, v, fee = self.get_tx_value(tx['tx_hash'])
if v is not None: balance += v
c, u = self.get_balance()
if balance != c+u:
v_str = format_satoshis( c+u - balance, True, self.num_zeros)
result.append( ('', 1000, 0, c+u-balance, None, c+u-balance, None ) )
balance = c + u - balance
for tx in history:
tx_hash = tx['tx_hash']
2013-01-02 02:39:44 -08:00
conf, timestamp = self.verifier.get_confirmations(tx_hash) if self.verifier else (None, None)
is_mine, value, fee = self.get_tx_value(tx_hash)
if value is not None:
balance += value
result.append( (tx_hash, conf, is_mine, value, fee, balance, timestamp) )
return result
def get_transactions_at_height(self, height):
with self.lock:
values = self.transactions.values()[:]
out = []
for tx in values:
if tx['height'] == height:
out.append(tx['tx_hash'])
return out
2012-11-05 02:08:16 -08:00
def get_label(self, tx_hash):
label = self.labels.get(tx_hash)
is_default = (label == '') or (label is None)
if is_default: label = self.get_default_label(tx_hash)
return label, is_default
def get_default_label(self, tx_hash):
tx = self.transactions.get(tx_hash)
2012-11-18 02:34:52 -08:00
default_label = ''
if tx:
is_mine, _, _ = self.get_tx_value(tx_hash)
if is_mine:
for o in tx['outputs']:
o_addr = o.get('address')
if not self.is_mine(o_addr):
try:
default_label = self.labels[o_addr]
except KeyError:
default_label = o_addr
else:
for o in tx['outputs']:
o_addr = o.get('address')
if self.is_mine(o_addr) and not self.is_change(o_addr):
break
else:
for o in tx['outputs']:
o_addr = o.get('address')
if self.is_mine(o_addr):
break
else:
o_addr = None
if o_addr:
dest_label = self.labels.get(o_addr)
try:
default_label = self.labels[o_addr]
except KeyError:
default_label = o_addr
return default_label
def mktx(self, outputs, label, password, fee=None, change_addr=None, from_addr= None):
for address, x in outputs:
assert self.is_valid(address)
amount = sum( map(lambda x:x[1], outputs) )
inputs, total, fee = self.choose_tx_inputs( amount, fee, from_addr )
if not inputs:
2012-08-23 18:16:27 -07:00
raise ValueError("Not enough funds")
2012-06-16 09:45:17 -07:00
2012-06-17 00:51:15 -07:00
if not self.use_change and not change_addr:
change_addr = inputs[-1][0]
print_error( "Sending change to", change_addr )
outputs = self.add_tx_change(outputs, amount, fee, total, change_addr)
if not self.seed:
2012-12-07 05:52:55 -08:00
return repr({'inputs':inputs, 'outputs':outputs})
2012-12-07 05:52:55 -08:00
tx = self.signed_tx(inputs, outputs, password)
for address, x in outputs:
if address not in self.addressbook and not self.is_mine(address):
2012-12-06 01:36:05 -08:00
self.addressbook.append(address)
if label:
tx_hash = Hash(tx.decode('hex') )[::-1].encode('hex')
2012-01-20 08:21:29 -08:00
self.labels[tx_hash] = label
2012-02-10 04:44:10 -08:00
return tx
def signed_tx(self, inputs, outputs, password):
s_inputs = self.sign_inputs( inputs, outputs, password )
tx = filter( raw_tx( s_inputs, outputs ) )
return tx
def sendtx(self, tx):
# synchronous
h = self.send_tx(tx)
self.tx_event.wait()
2012-11-22 04:12:17 -08:00
return self.receive_tx(h)
def send_tx(self, tx):
# asynchronous
2012-03-24 05:15:23 -07:00
self.tx_event.clear()
tx_hash = Hash(tx.decode('hex') )[::-1].encode('hex')
2012-10-26 07:06:54 -07:00
self.interface.send([('blockchain.transaction.broadcast', [tx])], 'synchronizer')
return tx_hash
def receive_tx(self,tx_hash):
2012-03-24 05:15:23 -07:00
out = self.tx_result
if out != tx_hash:
return False, "error: " + out
if self.receipt:
self.receipts[tx_hash] = self.receipt
self.receipt = None
return True, out
2012-02-06 08:59:31 -08:00
def read_alias(self, alias):
2012-02-03 06:45:41 -08:00
# this might not be the right place for this function.
import urllib
2012-02-06 14:45:21 -08:00
m1 = re.match('([\w\-\.]+)@((\w[\w\-]+\.)+[\w\-]+)', alias)
m2 = re.match('((\w[\w\-]+\.)+[\w\-]+)', alias)
if m1:
2012-10-01 06:10:51 -07:00
url = 'https://' + m1.group(2) + '/bitcoin.id/' + m1.group(1)
2012-02-06 14:45:21 -08:00
elif m2:
2012-10-01 06:10:51 -07:00
url = 'https://' + alias + '/bitcoin.id'
2012-02-03 06:45:41 -08:00
else:
2012-02-06 14:45:21 -08:00
return ''
try:
lines = urllib.urlopen(url).readlines()
except:
return ''
2012-02-03 07:39:27 -08:00
2012-02-06 14:45:21 -08:00
# line 0
line = lines[0].strip().split(':')
if len(line) == 1:
auth_name = None
target = signing_addr = line[0]
else:
target, auth_name, signing_addr, signature = line
msg = "alias:%s:%s:%s"%(alias,target,auth_name)
print msg, signature
self.verify_message(signing_addr, signature, msg)
# other lines are signed updates
for line in lines[1:]:
line = line.strip()
if not line: continue
line = line.split(':')
previous = target
print repr(line)
target, signature = line
self.verify_message(previous, signature, "alias:%s:%s"%(alias,target))
if not self.is_valid(target):
2012-08-23 18:17:30 -07:00
raise ValueError("Invalid bitcoin address")
2012-02-06 14:45:21 -08:00
return target, signing_addr, auth_name
2012-02-13 05:52:59 -08:00
2012-05-16 23:32:49 -07:00
def update_password(self, seed, old_password, new_password):
if new_password == '': new_password = None
self.use_encryption = (new_password != None)
2012-02-13 05:52:59 -08:00
self.seed = self.pw_encode( seed, new_password)
for k in self.imported_keys.keys():
a = self.imported_keys[k]
2012-05-16 23:32:49 -07:00
b = self.pw_decode(a, old_password)
2012-02-13 05:52:59 -08:00
c = self.pw_encode(b, new_password)
2012-02-13 05:56:31 -08:00
self.imported_keys[k] = c
self.save()
2012-02-14 00:52:03 -08:00
def get_alias(self, alias, interactive = False, show_message=None, question = None):
try:
target, signing_address, auth_name = self.read_alias(alias)
except BaseException, e:
# raise exception if verify fails (verify the chain)
if interactive:
show_message("Alias error: " + str(e))
2012-02-14 00:52:03 -08:00
return
print target, signing_address, auth_name
if auth_name is None:
a = self.aliases.get(alias)
if not a:
msg = "Warning: the alias '%s' is self-signed.\nThe signing address is %s.\n\nDo you want to add this alias to your list of contacts?"%(alias,signing_address)
if interactive and question( msg ):
self.aliases[alias] = (signing_address, target)
else:
target = None
else:
if signing_address != a[0]:
msg = "Warning: the key of alias '%s' has changed since your last visit! It is possible that someone is trying to do something nasty!!!\nDo you accept to change your trusted key?"%alias
if interactive and question( msg ):
self.aliases[alias] = (signing_address, target)
else:
target = None
else:
if signing_address not in self.authorities.keys():
msg = "The alias: '%s' links to %s\n\nWarning: this alias was signed by an unknown key.\nSigning authority: %s\nSigning address: %s\n\nDo you want to add this key to your list of trusted keys?"%(alias,target,auth_name,signing_address)
if interactive and question( msg ):
self.authorities[signing_address] = auth_name
else:
target = None
if target:
self.aliases[alias] = (signing_address, target)
return target
2012-02-14 03:45:39 -08:00
2012-02-14 04:49:05 -08:00
def parse_url(self, url, show_message, question):
2012-02-14 03:45:39 -08:00
o = url[8:].split('?')
address = o[0]
if len(o)>1:
params = o[1].split('&')
else:
params = []
amount = label = message = signature = identity = ''
for p in params:
k,v = p.split('=')
uv = urldecode(v)
if k == 'amount': amount = uv
elif k == 'message': message = uv
elif k == 'label': label = uv
elif k == 'signature':
identity, signature = uv.split(':')
url = url.replace('&%s=%s'%(k,v),'')
else:
print k,v
2012-06-08 14:15:08 -07:00
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
2012-02-14 04:49:05 -08:00
if signature:
if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', identity):
signing_address = self.get_alias(identity, True, show_message, question)
elif self.is_valid(identity):
signing_address = identity
else:
signing_address = None
if not signing_address:
return
try:
self.verify_message(signing_address, signature, url )
self.receipt = (signing_address, signature, url)
except:
2012-02-14 08:17:59 -08:00
show_message('Warning: the URI contains a bad signature.\nThe identity of the recipient cannot be verified.')
2012-02-14 04:49:05 -08:00
address = amount = label = identity = message = ''
if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', address):
2012-02-14 08:16:28 -08:00
payto_address = self.get_alias(address, True, show_message, question)
2012-02-14 04:49:05 -08:00
if payto_address:
address = address + ' <' + payto_address + '>'
2012-02-14 04:38:47 -08:00
return address, amount, label, message, signature, identity, url
2012-03-23 09:46:48 -07:00
def freeze(self,addr):
if addr in self.all_addresses() and addr not in self.frozen_addresses:
2012-06-07 02:21:53 -07:00
self.unprioritize(addr)
self.frozen_addresses.append(addr)
self.config.set_key('frozen_addresses', self.frozen_addresses, True)
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.config.set_key('frozen_addresses', self.frozen_addresses, True)
return True
else:
return False
2012-03-30 05:15:05 -07:00
def prioritize(self,addr):
2012-06-07 02:25:23 -07:00
if addr in self.all_addresses() and addr not in self.prioritized_addresses:
self.unfreeze(addr)
self.prioritized_addresses.append(addr)
self.config.set_key('prioritized_addresses', self.prioritized_addresses, True)
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.config.set_key('prioritized_addresses', self.prioritized_addresses, True)
return True
else:
return False
def save(self):
s = {
'seed_version': self.seed_version,
'use_encryption': self.use_encryption,
'use_change': self.use_change,
'master_public_key': self.master_public_key,
'fee': self.fee,
'seed': self.seed,
'addresses': self.addresses,
'change_addresses': self.change_addresses,
2012-11-04 06:47:19 -08:00
'addr_history': self.history,
'labels': self.labels,
'contacts': self.addressbook,
'imported_keys': self.imported_keys,
'aliases': self.aliases,
'authorities': self.authorities,
'receipts': self.receipts,
'num_zeros': self.num_zeros,
'frozen_addresses': self.frozen_addresses,
'prioritized_addresses': self.prioritized_addresses,
'gap_limit': self.gap_limit,
'transactions': self.transactions,
'requested_amounts': self.requested_amounts,
}
for k, v in s.items():
self.config.set_key(k,v)
self.config.save()
def set_verifier(self, verifier):
self.verifier = verifier
2012-11-15 03:14:29 -08:00
# review stored transactions and send them to the verifier
# (they are not necessarily in the history, because history items might have have been pruned)
2012-11-15 01:34:56 -08:00
for tx_hash, tx in self.transactions.items():
tx_height = tx.get('height')
if tx_height <1:
print_error( "skipping", tx_hash, tx_height )
continue
if tx_height>0:
self.verifier.add(tx_hash, tx_height)
# review transactions that are in the history
2012-11-15 03:14:29 -08:00
for addr, hist in self.history.items():
if hist == ['*']: continue
for tx_hash, tx_height in hist:
if tx_height>0:
# add it in case it was previously unconfirmed
self.verifier.add(tx_hash, tx_height)
# set the height in case it changed
tx = self.transactions.get(tx_hash)
if tx:
if tx.get('height') != tx_height:
print_error( "changing height for tx", tx_hash )
tx['height'] = tx_height
def is_addr_in_tx(self, addr, tx):
found = False
for txin in tx.get('inputs'):
if addr == txin.get('address'):
found = True
break
for txout in tx.get('outputs'):
if addr == txout.get('address'):
found = True
break
return found
def check_new_history(self, addr, hist):
# check that all tx in hist are relevant
if hist != ['*']:
for tx_hash, height in hist:
tx = self.transactions.get(tx_hash)
if not tx: continue
if not self.is_addr_in_tx(addr,tx):
return False
# check that we are not "orphaning" a transaction
2012-11-23 08:11:32 -08:00
old_hist = self.history.get(addr,[])
if old_hist == ['*']: return True
for tx_hash, height in old_hist:
if tx_hash in map(lambda x:x[0], hist): continue
found = False
for _addr, _hist in self.history.items():
if _addr == addr: continue
if _hist == ['*']: continue
_tx_hist = map(lambda x:x[0], _hist)
if tx_hash in _tx_hist:
found = True
break
if not found:
tx = self.transactions.get(tx_hash)
# already verified?
if tx.get('height'):
continue
# unconfirmed tx
print_error("new history is orphaning transaction:", tx_hash)
# check that all outputs are not mine, request histories
ext_requests = []
for o in tx.get('outputs'):
_addr = o.get('address')
# assert not self.is_mine(_addr)
ext_requests.append( ('blockchain.address.get_history', [_addr]) )
ext_h = self.interface.synchronous_get(ext_requests)
height = None
for h in ext_h:
if h == ['*']: continue
for item in h:
if item.get('tx_hash') == tx_hash:
height = item.get('height')
2012-11-23 07:11:52 -08:00
tx['height'] = height
if height:
print_error("found height for", tx_hash, height)
self.verifier.add(tx_hash, height)
else:
print_error("removing orphaned tx from history", tx_hash)
self.transactions.pop(tx_hash)
return True
def check_new_tx(self, tx_hash, tx):
# 1 check that tx is referenced in addr_history.
addresses = []
for addr, hist in self.history.items():
2012-11-14 06:33:44 -08:00
if hist == ['*']:continue
for txh, height in hist:
if txh == tx_hash:
addresses.append(addr)
if not addresses:
return False
# 2 check that referencing addresses are in the tx
for addr in addresses:
if not self.is_addr_in_tx(addr, tx):
return False
return True
class WalletSynchronizer(threading.Thread):
def __init__(self, wallet, config):
threading.Thread.__init__(self)
self.daemon = True
self.wallet = wallet
self.interface = self.wallet.interface
self.interface.register_channel('synchronizer')
2012-11-20 12:36:06 -08:00
self.wallet.interface.register_callback('connected', lambda: self.wallet.set_up_to_date(False))
self.wallet.interface.register_callback('connected', lambda: self.interface.send([('server.banner',[])],'synchronizer') )
self.was_updated = True
2012-11-05 14:10:38 -08:00
self.running = False
self.lock = threading.Lock()
def stop(self):
with self.lock: self.running = False
2012-11-24 11:31:07 -08:00
self.interface.poke('synchronizer')
2012-11-05 14:10:38 -08:00
def is_running(self):
with self.lock: return self.running
def synchronize_wallet(self):
new_addresses = self.wallet.synchronize()
if new_addresses:
self.subscribe_to_addresses(new_addresses)
self.wallet.up_to_date = False
return
if not self.interface.is_up_to_date('synchronizer'):
2012-11-20 12:36:06 -08:00
if self.wallet.is_up_to_date():
self.wallet.set_up_to_date(False)
self.was_updated = True
return
2012-11-20 12:36:06 -08:00
self.wallet.set_up_to_date(True)
self.was_updated = True
def subscribe_to_addresses(self, addresses):
messages = []
for addr in addresses:
messages.append(('blockchain.address.subscribe', [addr]))
self.interface.send( messages, 'synchronizer')
def run(self):
2012-11-05 14:10:38 -08:00
with self.lock: self.running = True
requested_tx = []
missing_tx = []
2012-11-06 23:45:53 -08:00
requested_histories = {}
# request any missing transactions
for history in self.wallet.history.values():
2012-11-14 06:33:44 -08:00
if history == ['*']: continue
for tx_hash, tx_height in history:
if self.wallet.transactions.get(tx_hash) is None and (tx_hash, tx_height) not in missing_tx:
missing_tx.append( (tx_hash, tx_height) )
print_error("missing tx", missing_tx)
# wait until we are connected, in case the user is not connected
while not self.interface.is_connected:
time.sleep(1)
# request banner, because 'connected' event happens before this thread is started
self.interface.send([('server.banner',[])],'synchronizer')
# subscriptions
self.subscribe_to_addresses(self.wallet.all_addresses())
2012-11-05 14:10:38 -08:00
while self.is_running():
# 1. send new requests
self.synchronize_wallet()
for tx_hash, tx_height in missing_tx:
if (tx_hash, tx_height) not in requested_tx:
self.interface.send([ ('blockchain.transaction.get',[tx_hash, tx_height]) ], 'synchronizer')
requested_tx.append( (tx_hash, tx_height) )
missing_tx = []
if self.was_updated:
self.interface.trigger_callback('updated')
self.was_updated = False
# 2. get a response
2012-11-24 11:31:07 -08:00
r = self.interface.get_response('synchronizer')
2012-11-05 14:10:38 -08:00
2012-11-24 11:31:07 -08:00
# poke sends None. (needed during stop)
if not r: continue
# 3. handle response
method = r['method']
params = r['params']
2012-11-14 06:33:44 -08:00
result = r.get('result')
error = r.get('error')
if error:
print "error", r
continue
if method == 'blockchain.address.subscribe':
addr = params[0]
if self.wallet.get_status(self.wallet.get_history(addr)) != result:
if requested_histories.get(addr) is None:
self.interface.send([('blockchain.address.get_history', [addr])], 'synchronizer')
requested_histories[addr] = result
2012-11-06 23:45:53 -08:00
elif method == 'blockchain.address.get_history':
addr = params[0]
2012-11-15 03:14:29 -08:00
print_error("receiving history", addr, result)
2012-11-14 06:33:44 -08:00
if result == ['*']:
assert requested_histories.pop(addr) == '*'
self.wallet.receive_history_callback(addr, result)
else:
hist = []
# check that txids are unique
txids = []
for item in result:
tx_hash = item['tx_hash']
if tx_hash not in txids:
txids.append(tx_hash)
hist.append( (tx_hash, item['height']) )
if len(hist) != len(result):
2012-11-15 00:14:24 -08:00
raise BaseException("error: server sent history with non-unique txid", result)
2012-11-14 06:33:44 -08:00
# check that the status corresponds to what was announced
rs = requested_histories.pop(addr)
if self.wallet.get_status(hist) != rs:
raise BaseException("error: status mismatch: %s"%addr)
2012-11-14 06:33:44 -08:00
# store received history
self.wallet.receive_history_callback(addr, hist)
# request transactions that we don't have
for tx_hash, tx_height in hist:
if self.wallet.transactions.get(tx_hash) is None:
if (tx_hash, tx_height) not in requested_tx and (tx_hash, tx_height) not in missing_tx:
missing_tx.append( (tx_hash, tx_height) )
elif method == 'blockchain.transaction.get':
tx_hash = params[0]
tx_height = params[1]
d = self.deserialize_tx(tx_hash, tx_height, result)
self.wallet.receive_tx_callback(tx_hash, d)
self.was_updated = True
requested_tx.remove( (tx_hash, tx_height) )
print_error("received tx:", d)
elif method == 'blockchain.transaction.broadcast':
self.wallet.tx_result = result
self.wallet.tx_event.set()
2012-10-22 02:34:21 -07:00
elif method == 'server.banner':
self.wallet.banner = result
self.was_updated = True
else:
2012-10-22 02:34:21 -07:00
print_error("Error: Unknown message:" + method + ", " + repr(params) + ", " + repr(result) )
if self.was_updated and not requested_tx:
self.interface.trigger_callback('updated')
self.was_updated = False
def deserialize_tx(self, tx_hash, tx_height, raw_tx):
assert tx_hash == hash_encode(Hash(raw_tx.decode('hex')))
2012-11-04 06:38:34 -08:00
import deserialize
vds = deserialize.BCDataStream()
vds.write(raw_tx.decode('hex'))
d = deserialize.parse_Transaction(vds)
2012-11-15 01:34:56 -08:00
d['height'] = tx_height
d['tx_hash'] = tx_hash
return d