commit 128d39d52e7ce429257f447c1faea1218be55108
parent 901d8db9c249ea63247c1cf3daf47ffa093230f4
Author: thomasv <thomasv@gitorious>
Date: Thu, 19 Jan 2012 17:11:36 +0100
separate files for wallet and interface
Diffstat:
M | client/electrum.py | | | 736 | +------------------------------------------------------------------------------ |
A | client/interface.py | | | 112 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | client/wallet.py | | | 658 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
3 files changed, 774 insertions(+), 732 deletions(-)
diff --git a/client/electrum.py b/client/electrum.py
@@ -16,741 +16,13 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-import sys, base64, os, re, hashlib, socket, getpass, copy, operator, ast, random
-from decimal import Decimal
-
-try:
- import ecdsa
- from ecdsa.util import string_to_number, number_to_string
-except:
- print "python-ecdsa does not seem to be installed. Try 'sudo easy_install ecdsa'"
- sys.exit(1)
-
-try:
- import aes
-except:
- print "AES does not seem to be installed. Try 'sudo easy_install slowaes'"
- sys.exit(1)
-
-
-############ functions from pywallet #####################
-
-addrtype = 0
-
-def hash_160(public_key):
- md = hashlib.new('ripemd160')
- md.update(hashlib.sha256(public_key).digest())
- return md.digest()
-
-def public_key_to_bc_address(public_key):
- h160 = hash_160(public_key)
- return hash_160_to_bc_address(h160)
-
-def hash_160_to_bc_address(h160):
- vh160 = chr(addrtype) + h160
- h = Hash(vh160)
- addr = vh160 + h[0:4]
- return b58encode(addr)
-
-def bc_address_to_hash_160(addr):
- bytes = b58decode(addr, 25)
- return bytes[1:21]
-
-__b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
-__b58base = len(__b58chars)
-
-def b58encode(v):
- """ encode v, which is a string of bytes, to base58.
- """
-
- long_value = 0L
- for (i, c) in enumerate(v[::-1]):
- long_value += (256**i) * ord(c)
-
- result = ''
- while long_value >= __b58base:
- div, mod = divmod(long_value, __b58base)
- result = __b58chars[mod] + result
- long_value = div
- result = __b58chars[long_value] + result
-
- # Bitcoin does a little leading-zero-compression:
- # leading 0-bytes in the input become leading-1s
- nPad = 0
- for c in v:
- if c == '\0': nPad += 1
- else: break
-
- return (__b58chars[0]*nPad) + result
-
-def b58decode(v, length):
- """ decode v into a string of len bytes
- """
- long_value = 0L
- for (i, c) in enumerate(v[::-1]):
- long_value += __b58chars.find(c) * (__b58base**i)
-
- result = ''
- while long_value >= 256:
- div, mod = divmod(long_value, 256)
- result = chr(mod) + result
- long_value = div
- result = chr(long_value) + result
-
- nPad = 0
- for c in v:
- if c == __b58chars[0]: nPad += 1
- else: break
-
- result = chr(0)*nPad + result
- if length is not None and len(result) != length:
- return None
-
- return result
-
-
-def Hash(data):
- return hashlib.sha256(hashlib.sha256(data).digest()).digest()
-
-def EncodeBase58Check(vchIn):
- hash = Hash(vchIn)
- return b58encode(vchIn + hash[0:4])
-
-def DecodeBase58Check(psz):
- vchRet = b58decode(psz, None)
- key = vchRet[0:-4]
- csum = vchRet[-4:]
- hash = Hash(key)
- cs32 = hash[0:4]
- if cs32 != csum:
- return None
- else:
- return key
-
-def PrivKeyToSecret(privkey):
- return privkey[9:9+32]
-
-def SecretToASecret(secret):
- vchIn = chr(addrtype+128) + secret
- return EncodeBase58Check(vchIn)
-
-def ASecretToSecret(key):
- vch = DecodeBase58Check(key)
- if vch and vch[0] == chr(addrtype+128):
- return vch[1:]
- else:
- return False
-
-########### end pywallet functions #######################
-
-
-def int_to_hex(i, length=1):
- s = hex(i)[2:].rstrip('L')
- s = "0"*(2*length - len(s)) + s
- return s.decode('hex')[::-1].encode('hex')
-
-
-# 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
-EncodeAES = lambda secret, s: base64.b64encode(aes.encryptData(secret,s))
-DecodeAES = lambda secret, e: aes.decryptData(secret, base64.b64decode(e))
-
-
-
-# secp256k1, http://www.oid-info.com/get/1.3.132.0.10
-_p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2FL
-_r = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141L
-_b = 0x0000000000000000000000000000000000000000000000000000000000000007L
-_a = 0x0000000000000000000000000000000000000000000000000000000000000000L
-_Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798L
-_Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8L
-curve_secp256k1 = ecdsa.ellipticcurve.CurveFp( _p, _a, _b )
-generator_secp256k1 = ecdsa.ellipticcurve.Point( curve_secp256k1, _Gx, _Gy, _r )
-oid_secp256k1 = (1,3,132,0,10)
-SECP256k1 = ecdsa.curves.Curve("SECP256k1", curve_secp256k1, generator_secp256k1, oid_secp256k1 )
-
-
-def filter(s):
- out = re.sub('( [^\n]*|)\n','',s)
- out = out.replace(' ','')
- out = out.replace('\n','')
- return out
-
-def raw_tx( inputs, outputs, for_sig = None ):
- s = int_to_hex(1,4) + ' version\n'
- s += int_to_hex( len(inputs) ) + ' number of inputs\n'
- for i in range(len(inputs)):
- _, _, p_hash, p_index, p_script, pubkey, sig = inputs[i]
- s += p_hash.decode('hex')[::-1].encode('hex') + ' prev hash\n'
- s += int_to_hex(p_index,4) + ' prev index\n'
- if for_sig is None:
- sig = sig + chr(1) # hashtype
- script = int_to_hex( len(sig)) + ' push %d bytes\n'%len(sig)
- script += sig.encode('hex') + ' sig\n'
- pubkey = chr(4) + pubkey
- script += int_to_hex( len(pubkey)) + ' push %d bytes\n'%len(pubkey)
- script += pubkey.encode('hex') + ' pubkey\n'
- elif for_sig==i:
- script = p_script + ' scriptsig \n'
- else:
- script=''
- s += int_to_hex( len(filter(script))/2 ) + ' script length \n'
- s += script
- s += "ffffffff" + ' sequence\n'
- s += int_to_hex( len(outputs) ) + ' number of outputs\n'
- for output in outputs:
- addr, amount = output
- s += int_to_hex( amount, 8) + ' amount: %d\n'%amount
- script = '76a9' # op_dup, op_hash_160
- script += '14' # push 0x14 bytes
- script += bc_address_to_hash_160(addr).encode('hex')
- script += '88ac' # op_equalverify, op_checksig
- s += int_to_hex( len(filter(script))/2 ) + ' script length \n'
- s += script + ' script \n'
- s += int_to_hex(0,4) # lock time
- if for_sig is not None: s += int_to_hex(1, 4) # hash type
- return s
-
-
-
-
-from version import ELECTRUM_VERSION, SEED_VERSION
-
-class Interface:
- def __init__(self):
- self.servers = ['ecdsa.org','electrum.novit.ro'] # list of default servers
- self.host = random.choice( self.servers ) # random choice when the wallet is created
- self.rtime = 0
- self.blocks = 0
- self.message = ''
- self.set_port(50000)
- self.is_connected = False
-
- def set_port(self, port_number):
- self.port = port_number
- if self.use_http():
- self.handler = self.http_json_handler
- else:
- self.handler = self.native_handler
-
- def use_http(self):
- return self.port in [80,81,8080,8081]
-
- def native_handler(self, method, params = ''):
- import time
- cmds = {'session.new':'new_session',
- 'peers':'peers',
- 'session.poll':'poll',
- 'session.update':'update_session',
- 'blockchain.transaction.broadcast':'tx',
- 'blockchain.address.get_history':'h'
- }
- cmd = cmds[method]
- if type(params) != type(''): params = repr( params )
- t1 = time.time()
- request = repr ( (cmd, params) ) + "#"
- s = socket.socket( socket.AF_INET, socket.SOCK_STREAM)
- s.connect(( self.host, self.port))
- s.send( request )
- out = ''
- while 1:
- msg = s.recv(1024)
- if msg: out += msg
- else: break
- s.close()
- self.rtime = time.time() - t1
- self.is_connected = True
- if cmd in[ 'peers','h']:
- out = ast.literal_eval( out )
- return out
-
- def http_json_handler(self, method, params = []):
- import urllib2, json, time
- if type(params) != type([]): params = [ params ]
- t1 = time.time()
- data = { 'method':method, 'id':'jsonrpc', 'params':params }
- data_json = json.dumps(data)
- host = 'http://%s:%d'%(self.host,self.port)
- req = urllib2.Request(host, data_json, {'content-type': 'application/json'})
- response_stream = urllib2.urlopen(req)
- response = json.loads( response_stream.read() )
- out = response.get('result')
- if not out:
- print response
- self.rtime = time.time() - t1
- self.is_connected = True
- return out
-
- def send_tx(self, data):
- out = self.handler('blockchain.transaction.broadcast', data )
- return out
-
- def retrieve_history(self, address):
- out = self.handler('blockchain.address.get_history', address )
- return out
-
- def poll(self):
- out = self.handler('session.poll', self.session_id )
- blocks, changed_addr = ast.literal_eval( out )
- if blocks == -1: raise BaseException("session not found")
- self.blocks = int(blocks)
- return changed_addr
-
- def new_session(self, addresses, version):
- out = self.handler('session.new', [ version, addresses ] )
- self.session_id, self.message = ast.literal_eval( out )
-
- def update_session(self, addresses):
- out = self.handler('session.update', [ self.session_id, addresses ] )
- return out
-
- def get_servers(self):
- out = self.handler('peers')
- self.servers = map( lambda x:x[1], out )
-
-
-
-
-class Wallet:
- def __init__(self, interface):
-
- self.electrum_version = ELECTRUM_VERSION
- self.seed_version = SEED_VERSION
-
- self.gap_limit = 5 # configuration
- self.fee = 100000
- self.master_public_key = ''
-
- # saved fields
- self.use_encryption = False
- self.addresses = [] # receiving addresses visible for user
- self.change_addresses = [] # addresses used as change
- self.seed = '' # encrypted
- self.status = {} # current status of addresses
- self.history = {}
- self.labels = {} # labels for addresses and transactions
- self.addressbook = [] # outgoing addresses, for payments
-
- # not saved
- self.tx_history = {}
-
- self.imported_keys = {}
-
- self.interface = interface
-
-
- def set_path(self, wallet_path):
-
- if wallet_path is not None:
- self.path = wallet_path
- else:
- # backward compatibility: look for wallet file in the default data directory
- if "HOME" in os.environ:
- wallet_dir = os.path.join( os.environ["HOME"], '.electrum')
- elif "LOCALAPPDATA" in os.environ:
- wallet_dir = os.path.join( os.environ["LOCALAPPDATA"], 'Electrum' )
- elif "APPDATA" in os.environ:
- wallet_dir = os.path.join( os.environ["APPDATA"], 'Electrum' )
- else:
- raise BaseException("No home directory found in environment variables.")
-
- if not os.path.exists( wallet_dir ): os.mkdir( wallet_dir )
- self.path = os.path.join( wallet_dir, 'electrum.dat' )
-
- def import_key(self, keypair, password):
- address, key = keypair.split(':')
- if not self.is_valid(address): return False
- b = ASecretToSecret( key )
- if not b: return False
- 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
- self.imported_keys[address] = self.pw_encode( key, password )
- return True
-
- def new_seed(self, password):
- seed = "%032x"%ecdsa.util.randrange( pow(2,128) )
- self.init_mpk(seed)
- # encrypt
- self.seed = wallet.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()
-
- 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 ) )
-
- def get_private_key2(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 )
- b = ASecretToSecret( b )
- secexp = int( b.encode('hex'), 16)
- 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")
- seed = self.pw_decode( self.seed, password)
- secexp = self.stretch_key(seed)
- secexp = ( secexp + self.get_sequence(n,for_change) ) % order
-
- pk = number_to_string(secexp,order)
- return pk
-
-
-
- def create_new_address2(self, for_change):
- """ Publickey(type,n) = Master_public_key + H(n|S|type)*point """
- curve = SECP256k1
- n = len(self.change_addresses) if for_change else len(self.addresses)
- z = self.get_sequence(n,for_change)
- master_public_key = ecdsa.VerifyingKey.from_string( self.master_public_key, 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() )
- if for_change:
- self.change_addresses.append(address)
- else:
- self.addresses.append(address)
-
- # updates
- print address
- self.history[address] = h = self.interface.retrieve_history(address)
- self.status[address] = h[-1]['blk_hash'] if h else None
- return address
-
-
- def synchronize(self):
- is_new = False
- while True:
- if self.change_addresses == []:
- self.create_new_address2(True)
- is_new = True
- continue
- a = self.change_addresses[-1]
- if self.history.get(a):
- self.create_new_address2(True)
- is_new = True
- else:
- break
-
- n = self.gap_limit
- while True:
- if len(self.addresses) < n:
- self.create_new_address2(False)
- is_new = True
- continue
- if map( lambda a: self.history.get(a), self.addresses[-n:] ) == n*[[]]:
- break
- else:
- self.create_new_address2(False)
- is_new = True
-
-
- def is_found(self):
- return (len(self.change_addresses) > 1 ) or ( len(self.addresses) > self.gap_limit )
-
- def fill_addressbook(self):
- for tx in self.tx_history.values():
- if tx['value']<0:
- for i in tx['outputs']:
- if not self.is_mine(i) and i not in self.addressbook:
- self.addressbook.append(i)
- # redo labels
- self.update_tx_labels()
-
-
- def save(self):
- s = {
- 'seed_version':self.seed_version,
- 'use_encryption':self.use_encryption,
- 'master_public_key': self.master_public_key.encode('hex'),
- 'fee':self.fee,
- 'host':self.interface.host,
- 'port':self.interface.port,
- 'blocks':self.interface.blocks,
- 'seed':self.seed,
- 'addresses':self.addresses,
- 'change_addresses':self.change_addresses,
- 'status':self.status,
- 'history':self.history,
- 'labels':self.labels,
- 'contacts':self.addressbook,
- 'imported_keys':self.imported_keys,
- }
- f = open(self.path,"w")
- f.write( repr(s) )
- f.close()
-
- def read(self):
- upgrade_msg = """This wallet seed is deprecated. Please run upgrade.py for a diagnostic."""
- try:
- f = open(self.path,"r")
- data = f.read()
- f.close()
- except:
- return False
- try:
- d = ast.literal_eval( data )
- 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.fee = int( d.get('fee') )
- self.interface.host = d.get('host')
- self.interface.set_port( d.get('port') )
- self.interface.blocks = d.get('blocks')
- self.seed = d.get('seed')
- self.addresses = d.get('addresses')
- self.change_addresses = d.get('change_addresses')
- self.status = d.get('status')
- self.history = d.get('history')
- self.labels = d.get('labels')
- self.addressbook = d.get('contacts')
- self.imported_keys = d.get('imported_keys',{})
- except:
- raise BaseException(upgrade_msg)
-
- self.update_tx_history()
-
- if self.seed_version != SEED_VERSION:
- raise BaseException(upgrade_msg)
-
- return True
-
- def get_new_address(self):
- n = 0
- for addr in self.addresses[-self.gap_limit:]:
- if not self.history.get(addr):
- n = n + 1
- if n < self.gap_limit:
- new_address = self.create_new_address2(False)
- self.history[new_address] = [] #get from server
- return True, new_address
- else:
- return False, "The last %d addresses in your list have never been used. You should use them first, or increase the allowed gap size in your preferences. "%self.gap_limit
-
- def get_addr_balance(self, addr):
- if self.is_mine(addr):
- h = self.history.get(addr)
- else:
- h = self.interface.retrieve_history(addr)
- if not h: return 0,0
- c = u = 0
- for item in h:
- v = item['value']
- if item['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 update(self):
- is_new = False
- changed_addresses = self.interface.poll()
- for addr, blk_hash in changed_addresses.items():
- if self.status.get(addr) != blk_hash:
- print "updating history for", addr
- self.history[addr] = self.interface.retrieve_history(addr)
- self.status[addr] = blk_hash
- is_new = True
-
- if is_new:
- self.synchronize()
- self.update_tx_history()
- self.save()
- return True
- else:
- return False
-
- def choose_tx_inputs( self, amount, fixed_fee ):
- """ todo: minimize tx size """
- total = 0
- fee = self.fee if fixed_fee is None else fixed_fee
-
- coins = []
- for addr in self.all_addresses():
- h = self.history.get(addr)
- if h is None: continue
- for item in h:
- if item.get('raw_scriptPubKey'):
- coins.append( (addr,item))
-
- coins = sorted( coins, key = lambda x: x[1]['nTime'] )
- inputs = []
- for c in coins:
- addr, item = c
- v = item.get('value')
- total += v
- inputs.append((addr, v, item['tx_hash'], item['pos'], item['raw_scriptPubKey'], None, None) )
- 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)
- inputs = []
- return inputs, total, fee
-
- def choose_tx_outputs( self, to_addr, amount, fee, total ):
- outputs = [ (to_addr, amount) ]
- change_amount = total - ( amount + fee )
- if change_amount != 0:
- # normally, the update thread should ensure that the last change address is unused
- outputs.append( ( self.change_addresses[-1], 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]
- private_key = ecdsa.SigningKey.from_string( self.get_private_key2(addr, password), curve = SECP256k1 )
- public_key = private_key.get_verifying_key()
- pubkey = public_key.to_string()
- 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:
- raise BaseException("Invalid password")
- return d
- else:
- return s
-
- def get_tx_history(self):
- lines = self.tx_history.values()
- lines = sorted(lines, key=operator.itemgetter("nTime"))
- return lines
-
- def update_tx_history(self):
- self.tx_history= {}
- for addr in self.all_addresses():
- h = self.history.get(addr)
- if h is None: continue
- for tx in h:
- tx_hash = tx['tx_hash']
- line = self.tx_history.get(tx_hash)
- if not line:
- self.tx_history[tx_hash] = copy.copy(tx)
- line = self.tx_history.get(tx_hash)
- else:
- line['value'] += tx['value']
- if line['height'] == 0:
- line['nTime'] = 1e12
- self.update_tx_labels()
-
- def update_tx_labels(self):
- for tx in self.tx_history.values():
- default_label = ''
- if tx['value']<0:
- for o_addr in tx['outputs']:
- if not self.is_change(o_addr):
- dest_label = self.labels.get(o_addr)
- if dest_label:
- default_label = 'to: ' + dest_label
- else:
- default_label = 'to: ' + o_addr
- 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
- tx['default_label'] = default_label
-
- def mktx(self, to_address, amount, label, password, fee=None):
- if not self.is_valid(to_address):
- raise BaseException("Invalid address")
- inputs, total, fee = wallet.choose_tx_inputs( amount, fee )
- if not inputs:
- raise BaseException("Not enough funds")
- outputs = wallet.choose_tx_outputs( to_address, amount, fee, total )
- s_inputs = wallet.sign_inputs( inputs, outputs, password )
-
- tx = filter( raw_tx( s_inputs, outputs ) )
- if to_address not in self.addressbook:
- self.addressbook.append(to_address)
- if label:
- tx_hash = Hash(tx.decode('hex') )[::-1].encode('hex')
- wallet.labels[tx_hash] = label
- wallet.save()
- return tx
-
- def sendtx(self, tx):
- tx_hash = Hash(tx.decode('hex') )[::-1].encode('hex')
- out = self.interface.send_tx(tx)
- if out != tx_hash:
- return False, "error: " + out
- return True, out
-
-
+import re,sys
from optparse import OptionParser
+from wallet import Wallet
+from interface import Interface
+
if __name__ == '__main__':
known_commands = ['help', 'validateaddress', 'balance', 'contacts', 'create', 'payto', 'sendtx', 'password', 'newaddress', 'addresses', 'history', 'label', 'gui', 'mktx','seed','import']
diff --git a/client/interface.py b/client/interface.py
@@ -0,0 +1,112 @@
+#!/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/>.
+
+
+import random, socket, ast
+
+class Interface:
+ def __init__(self):
+ self.servers = ['ecdsa.org','electrum.novit.ro'] # list of default servers
+ self.host = random.choice( self.servers ) # random choice when the wallet is created
+ self.rtime = 0
+ self.blocks = 0
+ self.message = ''
+ self.set_port(50000)
+ self.is_connected = False
+
+ def set_port(self, port_number):
+ self.port = port_number
+ if self.use_http():
+ self.handler = self.http_json_handler
+ else:
+ self.handler = self.native_handler
+
+ def use_http(self):
+ return self.port in [80,81,8080,8081]
+
+ def native_handler(self, method, params = ''):
+ import time
+ cmds = {'session.new':'new_session',
+ 'peers':'peers',
+ 'session.poll':'poll',
+ 'session.update':'update_session',
+ 'blockchain.transaction.broadcast':'tx',
+ 'blockchain.address.get_history':'h'
+ }
+ cmd = cmds[method]
+ if type(params) != type(''): params = repr( params )
+ t1 = time.time()
+ request = repr ( (cmd, params) ) + "#"
+ s = socket.socket( socket.AF_INET, socket.SOCK_STREAM)
+ s.connect(( self.host, self.port))
+ s.send( request )
+ out = ''
+ while 1:
+ msg = s.recv(1024)
+ if msg: out += msg
+ else: break
+ s.close()
+ self.rtime = time.time() - t1
+ self.is_connected = True
+ if cmd in[ 'peers','h']:
+ out = ast.literal_eval( out )
+ return out
+
+ def http_json_handler(self, method, params = []):
+ import urllib2, json, time
+ if type(params) != type([]): params = [ params ]
+ t1 = time.time()
+ data = { 'method':method, 'id':'jsonrpc', 'params':params }
+ data_json = json.dumps(data)
+ host = 'http://%s:%d'%(self.host,self.port)
+ req = urllib2.Request(host, data_json, {'content-type': 'application/json'})
+ response_stream = urllib2.urlopen(req)
+ response = json.loads( response_stream.read() )
+ out = response.get('result')
+ if not out:
+ print response
+ self.rtime = time.time() - t1
+ self.is_connected = True
+ return out
+
+ def send_tx(self, data):
+ out = self.handler('blockchain.transaction.broadcast', data )
+ return out
+
+ def retrieve_history(self, address):
+ out = self.handler('blockchain.address.get_history', address )
+ return out
+
+ def poll(self):
+ out = self.handler('session.poll', self.session_id )
+ blocks, changed_addr = ast.literal_eval( out )
+ if blocks == -1: raise BaseException("session not found")
+ self.blocks = int(blocks)
+ return changed_addr
+
+ def new_session(self, addresses, version):
+ out = self.handler('session.new', [ version, addresses ] )
+ self.session_id, self.message = ast.literal_eval( out )
+
+ def update_session(self, addresses):
+ out = self.handler('session.update', [ self.session_id, addresses ] )
+ return out
+
+ def get_servers(self):
+ out = self.handler('peers')
+ self.servers = map( lambda x:x[1], out )
diff --git a/client/wallet.py b/client/wallet.py
@@ -0,0 +1,658 @@
+#!/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/>.
+
+
+import sys, base64, os, re, hashlib, socket, getpass, copy, operator, ast, random
+from decimal import Decimal
+
+try:
+ import ecdsa
+ from ecdsa.util import string_to_number, number_to_string
+except:
+ print "python-ecdsa does not seem to be installed. Try 'sudo easy_install ecdsa'"
+ sys.exit(1)
+
+try:
+ import aes
+except:
+ print "AES does not seem to be installed. Try 'sudo easy_install slowaes'"
+ sys.exit(1)
+
+
+############ functions from pywallet #####################
+
+addrtype = 0
+
+def hash_160(public_key):
+ md = hashlib.new('ripemd160')
+ md.update(hashlib.sha256(public_key).digest())
+ return md.digest()
+
+def public_key_to_bc_address(public_key):
+ h160 = hash_160(public_key)
+ return hash_160_to_bc_address(h160)
+
+def hash_160_to_bc_address(h160):
+ vh160 = chr(addrtype) + h160
+ h = Hash(vh160)
+ addr = vh160 + h[0:4]
+ return b58encode(addr)
+
+def bc_address_to_hash_160(addr):
+ bytes = b58decode(addr, 25)
+ return bytes[1:21]
+
+__b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
+__b58base = len(__b58chars)
+
+def b58encode(v):
+ """ encode v, which is a string of bytes, to base58.
+ """
+
+ long_value = 0L
+ for (i, c) in enumerate(v[::-1]):
+ long_value += (256**i) * ord(c)
+
+ result = ''
+ while long_value >= __b58base:
+ div, mod = divmod(long_value, __b58base)
+ result = __b58chars[mod] + result
+ long_value = div
+ result = __b58chars[long_value] + result
+
+ # Bitcoin does a little leading-zero-compression:
+ # leading 0-bytes in the input become leading-1s
+ nPad = 0
+ for c in v:
+ if c == '\0': nPad += 1
+ else: break
+
+ return (__b58chars[0]*nPad) + result
+
+def b58decode(v, length):
+ """ decode v into a string of len bytes
+ """
+ long_value = 0L
+ for (i, c) in enumerate(v[::-1]):
+ long_value += __b58chars.find(c) * (__b58base**i)
+
+ result = ''
+ while long_value >= 256:
+ div, mod = divmod(long_value, 256)
+ result = chr(mod) + result
+ long_value = div
+ result = chr(long_value) + result
+
+ nPad = 0
+ for c in v:
+ if c == __b58chars[0]: nPad += 1
+ else: break
+
+ result = chr(0)*nPad + result
+ if length is not None and len(result) != length:
+ return None
+
+ return result
+
+
+def Hash(data):
+ return hashlib.sha256(hashlib.sha256(data).digest()).digest()
+
+def EncodeBase58Check(vchIn):
+ hash = Hash(vchIn)
+ return b58encode(vchIn + hash[0:4])
+
+def DecodeBase58Check(psz):
+ vchRet = b58decode(psz, None)
+ key = vchRet[0:-4]
+ csum = vchRet[-4:]
+ hash = Hash(key)
+ cs32 = hash[0:4]
+ if cs32 != csum:
+ return None
+ else:
+ return key
+
+def PrivKeyToSecret(privkey):
+ return privkey[9:9+32]
+
+def SecretToASecret(secret):
+ vchIn = chr(addrtype+128) + secret
+ return EncodeBase58Check(vchIn)
+
+def ASecretToSecret(key):
+ vch = DecodeBase58Check(key)
+ if vch and vch[0] == chr(addrtype+128):
+ return vch[1:]
+ else:
+ return False
+
+########### end pywallet functions #######################
+
+
+def int_to_hex(i, length=1):
+ s = hex(i)[2:].rstrip('L')
+ s = "0"*(2*length - len(s)) + s
+ return s.decode('hex')[::-1].encode('hex')
+
+
+# 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
+EncodeAES = lambda secret, s: base64.b64encode(aes.encryptData(secret,s))
+DecodeAES = lambda secret, e: aes.decryptData(secret, base64.b64decode(e))
+
+
+
+# secp256k1, http://www.oid-info.com/get/1.3.132.0.10
+_p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2FL
+_r = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141L
+_b = 0x0000000000000000000000000000000000000000000000000000000000000007L
+_a = 0x0000000000000000000000000000000000000000000000000000000000000000L
+_Gx = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798L
+_Gy = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8L
+curve_secp256k1 = ecdsa.ellipticcurve.CurveFp( _p, _a, _b )
+generator_secp256k1 = ecdsa.ellipticcurve.Point( curve_secp256k1, _Gx, _Gy, _r )
+oid_secp256k1 = (1,3,132,0,10)
+SECP256k1 = ecdsa.curves.Curve("SECP256k1", curve_secp256k1, generator_secp256k1, oid_secp256k1 )
+
+
+def filter(s):
+ out = re.sub('( [^\n]*|)\n','',s)
+ out = out.replace(' ','')
+ out = out.replace('\n','')
+ return out
+
+def raw_tx( inputs, outputs, for_sig = None ):
+ s = int_to_hex(1,4) + ' version\n'
+ s += int_to_hex( len(inputs) ) + ' number of inputs\n'
+ for i in range(len(inputs)):
+ _, _, p_hash, p_index, p_script, pubkey, sig = inputs[i]
+ s += p_hash.decode('hex')[::-1].encode('hex') + ' prev hash\n'
+ s += int_to_hex(p_index,4) + ' prev index\n'
+ if for_sig is None:
+ sig = sig + chr(1) # hashtype
+ script = int_to_hex( len(sig)) + ' push %d bytes\n'%len(sig)
+ script += sig.encode('hex') + ' sig\n'
+ pubkey = chr(4) + pubkey
+ script += int_to_hex( len(pubkey)) + ' push %d bytes\n'%len(pubkey)
+ script += pubkey.encode('hex') + ' pubkey\n'
+ elif for_sig==i:
+ script = p_script + ' scriptsig \n'
+ else:
+ script=''
+ s += int_to_hex( len(filter(script))/2 ) + ' script length \n'
+ s += script
+ s += "ffffffff" + ' sequence\n'
+ s += int_to_hex( len(outputs) ) + ' number of outputs\n'
+ for output in outputs:
+ addr, amount = output
+ s += int_to_hex( amount, 8) + ' amount: %d\n'%amount
+ script = '76a9' # op_dup, op_hash_160
+ script += '14' # push 0x14 bytes
+ script += bc_address_to_hash_160(addr).encode('hex')
+ script += '88ac' # op_equalverify, op_checksig
+ s += int_to_hex( len(filter(script))/2 ) + ' script length \n'
+ s += script + ' script \n'
+ s += int_to_hex(0,4) # lock time
+ if for_sig is not None: s += int_to_hex(1, 4) # hash type
+ return s
+
+
+
+
+from version import ELECTRUM_VERSION, SEED_VERSION
+
+
+
+
+
+class Wallet:
+ def __init__(self, interface):
+
+ self.electrum_version = ELECTRUM_VERSION
+ self.seed_version = SEED_VERSION
+
+ self.gap_limit = 5 # configuration
+ self.fee = 100000
+ self.master_public_key = ''
+
+ # saved fields
+ self.use_encryption = False
+ self.addresses = [] # receiving addresses visible for user
+ self.change_addresses = [] # addresses used as change
+ self.seed = '' # encrypted
+ self.status = {} # current status of addresses
+ self.history = {}
+ self.labels = {} # labels for addresses and transactions
+ self.addressbook = [] # outgoing addresses, for payments
+
+ # not saved
+ self.tx_history = {}
+
+ self.imported_keys = {}
+
+ self.interface = interface
+
+
+ def set_path(self, wallet_path):
+
+ if wallet_path is not None:
+ self.path = wallet_path
+ else:
+ # backward compatibility: look for wallet file in the default data directory
+ if "HOME" in os.environ:
+ wallet_dir = os.path.join( os.environ["HOME"], '.electrum')
+ elif "LOCALAPPDATA" in os.environ:
+ wallet_dir = os.path.join( os.environ["LOCALAPPDATA"], 'Electrum' )
+ elif "APPDATA" in os.environ:
+ wallet_dir = os.path.join( os.environ["APPDATA"], 'Electrum' )
+ else:
+ raise BaseException("No home directory found in environment variables.")
+
+ if not os.path.exists( wallet_dir ): os.mkdir( wallet_dir )
+ self.path = os.path.join( wallet_dir, 'electrum.dat' )
+
+ def import_key(self, keypair, password):
+ address, key = keypair.split(':')
+ if not self.is_valid(address): return False
+ b = ASecretToSecret( key )
+ if not b: return False
+ 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
+ self.imported_keys[address] = self.pw_encode( key, password )
+ return True
+
+ def new_seed(self, password):
+ seed = "%032x"%ecdsa.util.randrange( pow(2,128) )
+ self.init_mpk(seed)
+ # encrypt
+ self.seed = wallet.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()
+
+ 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 ) )
+
+ def get_private_key2(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 )
+ b = ASecretToSecret( b )
+ secexp = int( b.encode('hex'), 16)
+ 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")
+ seed = self.pw_decode( self.seed, password)
+ secexp = self.stretch_key(seed)
+ secexp = ( secexp + self.get_sequence(n,for_change) ) % order
+
+ pk = number_to_string(secexp,order)
+ return pk
+
+
+
+ def create_new_address2(self, for_change):
+ """ Publickey(type,n) = Master_public_key + H(n|S|type)*point """
+ curve = SECP256k1
+ n = len(self.change_addresses) if for_change else len(self.addresses)
+ z = self.get_sequence(n,for_change)
+ master_public_key = ecdsa.VerifyingKey.from_string( self.master_public_key, 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() )
+ if for_change:
+ self.change_addresses.append(address)
+ else:
+ self.addresses.append(address)
+
+ # updates
+ print address
+ self.history[address] = h = self.interface.retrieve_history(address)
+ self.status[address] = h[-1]['blk_hash'] if h else None
+ return address
+
+
+ def synchronize(self):
+ is_new = False
+ while True:
+ if self.change_addresses == []:
+ self.create_new_address2(True)
+ is_new = True
+ continue
+ a = self.change_addresses[-1]
+ if self.history.get(a):
+ self.create_new_address2(True)
+ is_new = True
+ else:
+ break
+
+ n = self.gap_limit
+ while True:
+ if len(self.addresses) < n:
+ self.create_new_address2(False)
+ is_new = True
+ continue
+ if map( lambda a: self.history.get(a), self.addresses[-n:] ) == n*[[]]:
+ break
+ else:
+ self.create_new_address2(False)
+ is_new = True
+
+
+ def is_found(self):
+ return (len(self.change_addresses) > 1 ) or ( len(self.addresses) > self.gap_limit )
+
+ def fill_addressbook(self):
+ for tx in self.tx_history.values():
+ if tx['value']<0:
+ for i in tx['outputs']:
+ if not self.is_mine(i) and i not in self.addressbook:
+ self.addressbook.append(i)
+ # redo labels
+ self.update_tx_labels()
+
+
+ def save(self):
+ s = {
+ 'seed_version':self.seed_version,
+ 'use_encryption':self.use_encryption,
+ 'master_public_key': self.master_public_key.encode('hex'),
+ 'fee':self.fee,
+ 'host':self.interface.host,
+ 'port':self.interface.port,
+ 'blocks':self.interface.blocks,
+ 'seed':self.seed,
+ 'addresses':self.addresses,
+ 'change_addresses':self.change_addresses,
+ 'status':self.status,
+ 'history':self.history,
+ 'labels':self.labels,
+ 'contacts':self.addressbook,
+ 'imported_keys':self.imported_keys,
+ }
+ f = open(self.path,"w")
+ f.write( repr(s) )
+ f.close()
+
+ def read(self):
+ upgrade_msg = """This wallet seed is deprecated. Please run upgrade.py for a diagnostic."""
+ try:
+ f = open(self.path,"r")
+ data = f.read()
+ f.close()
+ except:
+ return False
+ try:
+ d = ast.literal_eval( data )
+ 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.fee = int( d.get('fee') )
+ self.interface.host = d.get('host')
+ self.interface.set_port( d.get('port') )
+ self.interface.blocks = d.get('blocks')
+ self.seed = d.get('seed')
+ self.addresses = d.get('addresses')
+ self.change_addresses = d.get('change_addresses')
+ self.status = d.get('status')
+ self.history = d.get('history')
+ self.labels = d.get('labels')
+ self.addressbook = d.get('contacts')
+ self.imported_keys = d.get('imported_keys',{})
+ except:
+ raise BaseException(upgrade_msg)
+
+ self.update_tx_history()
+
+ if self.seed_version != SEED_VERSION:
+ raise BaseException(upgrade_msg)
+
+ return True
+
+ def get_new_address(self):
+ n = 0
+ for addr in self.addresses[-self.gap_limit:]:
+ if not self.history.get(addr):
+ n = n + 1
+ if n < self.gap_limit:
+ new_address = self.create_new_address2(False)
+ self.history[new_address] = [] #get from server
+ return True, new_address
+ else:
+ return False, "The last %d addresses in your list have never been used. You should use them first, or increase the allowed gap size in your preferences. "%self.gap_limit
+
+ def get_addr_balance(self, addr):
+ if self.is_mine(addr):
+ h = self.history.get(addr)
+ else:
+ h = self.interface.retrieve_history(addr)
+ if not h: return 0,0
+ c = u = 0
+ for item in h:
+ v = item['value']
+ if item['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 update(self):
+ is_new = False
+ changed_addresses = self.interface.poll()
+ for addr, blk_hash in changed_addresses.items():
+ if self.status.get(addr) != blk_hash:
+ print "updating history for", addr
+ self.history[addr] = self.interface.retrieve_history(addr)
+ self.status[addr] = blk_hash
+ is_new = True
+
+ if is_new:
+ self.synchronize()
+ self.update_tx_history()
+ self.save()
+ return True
+ else:
+ return False
+
+ def choose_tx_inputs( self, amount, fixed_fee ):
+ """ todo: minimize tx size """
+ total = 0
+ fee = self.fee if fixed_fee is None else fixed_fee
+
+ coins = []
+ for addr in self.all_addresses():
+ h = self.history.get(addr)
+ if h is None: continue
+ for item in h:
+ if item.get('raw_scriptPubKey'):
+ coins.append( (addr,item))
+
+ coins = sorted( coins, key = lambda x: x[1]['nTime'] )
+ inputs = []
+ for c in coins:
+ addr, item = c
+ v = item.get('value')
+ total += v
+ inputs.append((addr, v, item['tx_hash'], item['pos'], item['raw_scriptPubKey'], None, None) )
+ 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)
+ inputs = []
+ return inputs, total, fee
+
+ def choose_tx_outputs( self, to_addr, amount, fee, total ):
+ outputs = [ (to_addr, amount) ]
+ change_amount = total - ( amount + fee )
+ if change_amount != 0:
+ # normally, the update thread should ensure that the last change address is unused
+ outputs.append( ( self.change_addresses[-1], 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]
+ private_key = ecdsa.SigningKey.from_string( self.get_private_key2(addr, password), curve = SECP256k1 )
+ public_key = private_key.get_verifying_key()
+ pubkey = public_key.to_string()
+ 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:
+ raise BaseException("Invalid password")
+ return d
+ else:
+ return s
+
+ def get_tx_history(self):
+ lines = self.tx_history.values()
+ lines = sorted(lines, key=operator.itemgetter("nTime"))
+ return lines
+
+ def update_tx_history(self):
+ self.tx_history= {}
+ for addr in self.all_addresses():
+ h = self.history.get(addr)
+ if h is None: continue
+ for tx in h:
+ tx_hash = tx['tx_hash']
+ line = self.tx_history.get(tx_hash)
+ if not line:
+ self.tx_history[tx_hash] = copy.copy(tx)
+ line = self.tx_history.get(tx_hash)
+ else:
+ line['value'] += tx['value']
+ if line['height'] == 0:
+ line['nTime'] = 1e12
+ self.update_tx_labels()
+
+ def update_tx_labels(self):
+ for tx in self.tx_history.values():
+ default_label = ''
+ if tx['value']<0:
+ for o_addr in tx['outputs']:
+ if not self.is_change(o_addr):
+ dest_label = self.labels.get(o_addr)
+ if dest_label:
+ default_label = 'to: ' + dest_label
+ else:
+ default_label = 'to: ' + o_addr
+ 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
+ tx['default_label'] = default_label
+
+ def mktx(self, to_address, amount, label, password, fee=None):
+ if not self.is_valid(to_address):
+ raise BaseException("Invalid address")
+ inputs, total, fee = wallet.choose_tx_inputs( amount, fee )
+ if not inputs:
+ raise BaseException("Not enough funds")
+ outputs = wallet.choose_tx_outputs( to_address, amount, fee, total )
+ s_inputs = wallet.sign_inputs( inputs, outputs, password )
+
+ tx = filter( raw_tx( s_inputs, outputs ) )
+ if to_address not in self.addressbook:
+ self.addressbook.append(to_address)
+ if label:
+ tx_hash = Hash(tx.decode('hex') )[::-1].encode('hex')
+ wallet.labels[tx_hash] = label
+ wallet.save()
+ return tx
+
+ def sendtx(self, tx):
+ tx_hash = Hash(tx.decode('hex') )[::-1].encode('hex')
+ out = self.interface.send_tx(tx)
+ if out != tx_hash:
+ return False, "error: " + out
+ return True, out
+