set default fee to 200 uBTC
[electrum-nvc.git] / lib / wallet.py
index 5341f19..c64813b 100644 (file)
@@ -33,9 +33,6 @@ import time
 from util import print_msg, print_error, user_dir, format_satoshis
 from bitcoin import *
 
-# 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))
@@ -77,24 +74,21 @@ class Wallet:
         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.fee                   = int(config.get('fee_per_kb',20000))
         self.num_zeros             = int(config.get('num_zeros',0))
         self.use_encryption        = config.get('use_encryption', False)
         self.seed                  = config.get('seed', '')               # encrypted
         self.labels                = config.get('labels', {})
-        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', [])
         self.imported_keys         = config.get('imported_keys',{})
         self.history               = config.get('addr_history',{})        # address -> list(txid, height)
-        self.tx_height             = config.get('tx_height',{})
         self.accounts              = config.get('accounts', {})   # this should not include public keys
 
+        self.SequenceClass = ElectrumSequence
         self.sequences = {}
-        self.sequences[0] = ElectrumSequence(self.config.get('master_public_key'))
+        self.sequences[0] = self.SequenceClass(self.config.get('master_public_key'))
 
         if self.accounts.get(0) is None:
             self.accounts[0] = { 0:[], 1:[], 'name':'Main account' }
@@ -109,8 +103,6 @@ class Wallet:
         # not saved
         self.prevout_values = {}     # my own transaction outputs
         self.spent_outputs = []
-        self.receipt = None          # next receipt
-        self.banner = ''
 
         # spv
         self.verifier = None
@@ -121,6 +113,7 @@ class Wallet:
         
         self.up_to_date = False
         self.lock = threading.Lock()
+        self.transaction_lock = threading.Lock()
         self.tx_event = threading.Event()
 
         if self.seed_version != SEED_VERSION:
@@ -161,13 +154,13 @@ class Wallet:
         self.seed = seed 
         self.config.set_key('seed', self.seed, True)
         self.config.set_key('seed_version', self.seed_version, True)
-        mpk = ElectrumSequence.mpk_from_seed(self.seed)
+        mpk = self.SequenceClass.mpk_from_seed(self.seed)
         self.init_sequence(mpk)
 
 
     def init_sequence(self, mpk):
         self.config.set_key('master_public_key', mpk, True)
-        self.sequences[0] = ElectrumSequence(mpk)
+        self.sequences[0] = self.SequenceClass(mpk)
         self.accounts[0] = { 0:[], 1:[], 'name':'Main account' }
         self.config.set_key('accounts', self.accounts, True)
 
@@ -184,11 +177,13 @@ class Wallet:
         return address in self.addresses(True)
 
     def is_change(self, address):
-        #return address in self.change_addresses
-        return False
+        if not self.is_mine(address): return False
+        if address in self.imported_keys.keys(): return False
+        acct, s = self.get_address_index(address)
+        return s[0] == 1
 
     def get_master_public_key(self):
-        return self.sequences[0].master_public_key
+        return self.config.get("master_public_key")
 
     def get_address_index(self, address):
         if address in self.imported_keys.keys():
@@ -216,6 +211,7 @@ class Wallet:
         return self.get_private_keys([address], password).get(address)
 
     def get_private_keys(self, addresses, password):
+        if not self.seed: return {}
         # decode seed in any case, in order to test the password
         seed = self.decode_seed(password)
         out = {}
@@ -255,7 +251,7 @@ class Wallet:
                 if item.get('txid') == txin['tx_hash'] and item.get('vout') == txin['index']:
                     txin['raw_output_script'] = item['scriptPubKey']
                     txin['redeemScript'] = item.get('redeemScript')
-                    txin['electrumKeyID'] = item.get('electrumKeyID')
+                    txin['KeyID'] = item.get('KeyID')
                     break
             else:
                 for item in unspent_coins:
@@ -267,8 +263,9 @@ class Wallet:
                     raise
 
             # find the address:
-            if txin.get('electrumKeyID'):
-                account, sequence = txin.get('electrumKeyID')
+            if txin.get('KeyID'):
+                account, name, sequence = txin.get('KeyID')
+                if name != 'Electrum': continue
                 sec = self.sequences[account].get_private_key(sequence, seed)
                 addr = self.sequences[account].get_address(sequence)
                 txin['address'] = addr
@@ -300,6 +297,7 @@ class Wallet:
         address = self.get_new_address( account, for_change, n)
         self.accounts[account][for_change].append(address)
         self.history[address] = []
+        print_msg(address)
         return address
         
 
@@ -410,6 +408,12 @@ class Wallet:
         # redo labels
         # self.update_tx_labels()
 
+    def get_num_tx(self, address):
+        n = 0 
+        for tx in self.transactions.values():
+            if address in map(lambda x:x[0], tx.outputs): n += 1
+        return n
+
 
     def get_address_flags(self, addr):
         flags = "C" if self.is_change(addr) else "I" if addr in self.imported_keys.keys() else "-" 
@@ -422,46 +426,6 @@ class Wallet:
         return tx.get_value(addresses, self.prevout_values)
 
 
-    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)
-        conf, timestamp = self.verifier.get_confirmations(tx_hash)
-
-        if timestamp:
-            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.d['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)
@@ -469,8 +433,7 @@ class Wallet:
         for item in tx.outputs:
             addr, value = item
             key = tx_hash+ ':%d'%i
-            with self.lock:
-                self.prevout_values[key] = value
+            self.prevout_values[key] = value
             i += 1
 
         for item in tx.inputs:
@@ -564,6 +527,7 @@ class Wallet:
             if h == ['*']: continue
             for tx_hash, tx_height in h:
                 tx = self.transactions.get(tx_hash)
+                if tx is None: raise BaseException("Wallet not synchronized")
                 for output in tx.d.get('outputs'):
                     if output.get('address') != addr: continue
                     key = tx_hash + ":%d" % output.get('index')
@@ -600,14 +564,20 @@ class Wallet:
             total += v
 
             inputs.append( item )
-            fee = self.fee*len(inputs) if fixed_fee is None else fixed_fee
+            if fixed_fee is None:
+                estimated_size =  len(inputs) * 180 + 80     # this assumes non-compressed keys
+                fee = self.fee * round(estimated_size/1024.)
+                if fee == 0: fee = self.fee
+            else:
+                fee = fixed_fee
             if total >= amount + fee: break
         else:
-            #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:
@@ -637,18 +607,17 @@ class Wallet:
 
     def receive_tx_callback(self, tx_hash, tx, tx_height):
 
+
         if not self.check_new_tx(tx_hash, tx):
-            raise BaseException("error: received transaction is not consistent with history"%tx_hash)
+            # may happen due to pruning
+            print_error("received transaction that is no longer referenced in history", tx_hash)
+            return
 
-        with self.lock:
+        with self.transaction_lock:
             self.transactions[tx_hash] = tx
-            self.tx_height[tx_hash] = tx_height
-
-        #tx_height = tx.get('height')
-        if self.verifier and tx_height>0: 
-            self.verifier.add(tx_hash, tx_height)
-
-        self.update_tx_outputs(tx_hash)
+            if self.verifier and tx_height>0: 
+                self.verifier.add(tx_hash, tx_height)
+            self.update_tx_outputs(tx_hash)
 
         self.save()
 
@@ -667,37 +636,32 @@ class Wallet:
                 if tx_height>0:
                     # add it in case it was previously unconfirmed
                     if self.verifier: self.verifier.add(tx_hash, tx_height)
-                    # set the height in case it changed
-                    txh = self.tx_height.get(tx_hash)
-                    if txh is not None and txh != tx_height:
-                        print_error( "changing height for tx", tx_hash )
-                        self.tx_height[tx_hash] = tx_height
 
 
     def get_tx_history(self):
-        with self.lock:
+        with self.transaction_lock:
             history = self.transactions.items()
-        history.sort(key = lambda x: self.tx_height.get(x[0]) if self.tx_height.get(x[0]) else 1e12)
-        result = []
+            history.sort(key = lambda x: self.verifier.verified_tx.get(x[0]) if self.verifier.verified_tx.get(x[0]) else (1e12,0,0))
+            result = []
     
-        balance = 0
-        for tx_hash, tx in history:
-            is_mine, v, fee = self.get_tx_value(tx)
-            if v is not None: balance += v
-        c, u = self.get_balance()
+            balance = 0
+            for tx_hash, tx in history:
+                is_mine, v, fee = self.get_tx_value(tx)
+                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 ) )
+            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_hash, tx in history:
-            conf, timestamp = self.verifier.get_confirmations(tx_hash) if self.verifier else (None, None)
-            is_mine, value, fee = self.get_tx_value(tx)
-            if value is not None:
-                balance += value
+            balance = c + u - balance
+            for tx_hash, tx in history:
+                conf, timestamp = self.verifier.get_confirmations(tx_hash) if self.verifier else (None, None)
+                is_mine, value, fee = self.get_tx_value(tx)
+                if value is not None:
+                    balance += value
 
-            result.append( (tx_hash, conf, is_mine, value, fee, balance, timestamp) )
+                result.append( (tx_hash, conf, is_mine, value, fee, balance, timestamp) )
 
         return result
 
@@ -722,6 +686,9 @@ class Wallet:
                             default_label = self.labels[o_addr]
                         except KeyError:
                             default_label = o_addr
+                        break
+                else:
+                    default_label = '(internal)'
             else:
                 for o in tx.outputs:
                     o_addr, _ = o
@@ -770,14 +737,15 @@ class Wallet:
                 pk_addresses.append(address)
                 continue
             account, sequence = self.get_address_index(address)
-            txin['electrumKeyID'] = (account, sequence) # used by the server to find the key
+            txin['KeyID'] = (account, 'Electrum', sequence) # used by the server to find the key
             pk_addr, redeemScript = self.sequences[account].get_input_info(sequence)
             if redeemScript: txin['redeemScript'] = redeemScript
             pk_addresses.append(pk_addr)
 
         # get all private keys at once.
-        private_keys = self.get_private_keys(pk_addresses, password)
-        tx.sign(private_keys)
+        if self.seed:
+            private_keys = self.get_private_keys(pk_addresses, password)
+            tx.sign(private_keys)
 
         for address, x in outputs:
             if address not in self.addressbook and not self.is_mine(address):
@@ -803,54 +771,9 @@ class Wallet:
         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
 
 
-    def read_alias(self, alias):
-        # this might not be the right place for this function.
-        import urllib
-
-        m1 = re.match('([\w\-\.]+)@((\w[\w\-]+\.)+[\w\-]+)', alias)
-        m2 = re.match('((\w[\w\-]+\.)+[\w\-]+)', alias)
-        if m1:
-            url = 'https://' + m1.group(2) + '/bitcoin.id/' + m1.group(1) 
-        elif m2:
-            url = 'https://' + alias + '/bitcoin.id'
-        else:
-            return ''
-        try:
-            lines = urllib.urlopen(url).readlines()
-        except:
-            return ''
-
-        # 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
-            EC_KEY.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
-            EC_KEY.verify_message(previous, signature, "alias:%s:%s"%(alias,target))
-
-        if not is_valid(target):
-            raise ValueError("Invalid bitcoin address")
-
-        return target, signing_addr, auth_name
 
     def update_password(self, seed, old_password, new_password):
         if new_password == '': new_password = None
@@ -864,96 +787,6 @@ class Wallet:
             self.imported_keys[k] = c
         self.save()
 
-    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))
-            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
-
-
-    def parse_url(self, url, show_message, question):
-        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
-
-        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 not self.is_mine(address):
-                    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)
-            elif is_valid(identity):
-                signing_address = identity
-            else:
-                signing_address = None
-            if not signing_address:
-                return
-            try:
-                EC_KEY.verify_message(signing_address, signature, url )
-                self.receipt = (signing_address, signature, url)
-            except:
-                show_message('Warning: the URI contains a bad signature.\nThe identity of the recipient cannot be verified.')
-                address = amount = label = identity = message = ''
-
-        if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', address):
-            payto_address = self.get_alias(address, True, show_message, question)
-            if payto_address:
-                address = address + ' <' + payto_address + '>'
-
-        return address, amount, label, message, signature, identity, url
-
 
 
     def freeze(self,addr):
@@ -998,21 +831,17 @@ class Wallet:
         s = {
             'use_encryption': self.use_encryption,
             'use_change': self.use_change,
-            'fee': self.fee,
+            'fee_per_kb': self.fee,
             'accounts': self.accounts,
             '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': tx,
-            'tx_height': self.tx_height,
         }
         for k, v in s.items():
             self.config.set_key(k,v)
@@ -1021,17 +850,6 @@ class Wallet:
     def set_verifier(self, verifier):
         self.verifier = verifier
 
-        # review stored transactions and send them to the verifier
-        # (they are not necessarily in the history, because history items might have have been pruned)
-        for tx_hash, tx in self.transactions.items():
-            tx_height = self.tx_height[tx_hash]
-            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
         for addr, hist in self.history.items():
             if hist == ['*']: continue
@@ -1039,13 +857,14 @@ class Wallet:
                 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
-                    txh = self.tx_height.get(tx_hash)
-                    if txh is not None and txh != tx_height:
-                        print_error( "changing height for tx", tx_hash )
-                        self.tx_height[tx_hash] = tx_height
 
 
+        # if we are on a pruning server, remove unverified transactions
+        vr = self.verifier.transactions.keys() + self.verifier.verified_tx.keys()
+        for tx_hash in self.transactions.keys():
+            if tx_hash not in vr:
+                self.transactions.pop(tx_hash)
+
 
 
     def check_new_history(self, addr, hist):
@@ -1079,7 +898,7 @@ class Wallet:
                 if not tx: continue
                 
                 # already verified?
-                if self.tx_height.get(tx_hash):
+                if self.verifier.get_height(tx_hash):
                     continue
                 # unconfirmed tx
                 print_error("new history is orphaning transaction:", tx_hash)
@@ -1093,10 +912,10 @@ class Wallet:
                 height = None
                 for h in ext_h:
                     if h == ['*']: continue
+                    print_error(h)
                     for item in h:
                         if item.get('tx_hash') == tx_hash:
                             height = item.get('height')
-                            self.tx_height[tx_hash] = height
                 if height:
                     print_error("found height for", tx_hash, height)
                     self.verifier.add(tx_hash, height)
@@ -1140,7 +959,6 @@ class WalletSynchronizer(threading.Thread):
         self.interface = self.wallet.interface
         self.interface.register_channel('synchronizer')
         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
         self.running = False
         self.lock = threading.Lock()
@@ -1152,22 +970,6 @@ class WalletSynchronizer(threading.Thread):
     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'):
-            if self.wallet.is_up_to_date():
-                self.wallet.set_up_to_date(False)
-                self.was_updated = True
-            return
-
-        self.wallet.set_up_to_date(True)
-        self.was_updated = True
-
     
     def subscribe_to_addresses(self, addresses):
         messages = []
@@ -1195,22 +997,34 @@ class WalletSynchronizer(threading.Thread):
         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.addresses(True))
 
         while self.is_running():
-            # 1. send new requests
-            self.synchronize_wallet()
+            # 1. create new addresses
+            new_addresses = self.wallet.synchronize()
+
+            # request missing addresses
+            if new_addresses:
+                self.subscribe_to_addresses(new_addresses)
 
+            # request missing transactions
             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 = []
 
+            # detect if situation has changed
+            if not self.interface.is_up_to_date('synchronizer'):
+                if self.wallet.is_up_to_date():
+                    self.wallet.set_up_to_date(False)
+                    self.was_updated = True
+            else:
+                if not self.wallet.is_up_to_date():
+                    self.wallet.set_up_to_date(True)
+                    self.was_updated = True
+
             if self.was_updated:
                 self.interface.trigger_callback('updated')
                 self.was_updated = False
@@ -1284,9 +1098,6 @@ class WalletSynchronizer(threading.Thread):
                 self.wallet.tx_result = result
                 self.wallet.tx_event.set()
 
-            elif method == 'server.banner':
-                self.wallet.banner = result
-                self.interface.trigger_callback('banner')
             else:
                 print_error("Error: Unknown message:" + method + ", " + repr(params) + ", " + repr(result) )