Standardizing message format and implementing stderr for errors
[electrum-nvc.git] / lib / wallet.py
index dd4ec66..8c0c8bc 100644 (file)
@@ -17,7 +17,7 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-import sys, base64, os, re, hashlib, copy, operator, ast, threading, random
+import sys, base64, os, re, hashlib, copy, operator, ast, threading, random, getpass
 import aes, ecdsa
 from ecdsa.util import string_to_number, number_to_string
 
@@ -50,6 +50,16 @@ def bc_address_to_hash_160(addr):
     bytes = b58decode(addr, 25)
     return bytes[1:21]
 
+def encode_point(pubkey, compressed=False):
+    order = generator_secp256k1.order()
+    p = pubkey.pubkey.point
+    x_str = ecdsa.util.number_to_string(p.x(), order)
+    y_str = ecdsa.util.number_to_string(p.y(), order)
+    if compressed:
+        return chr(2 + (p.y() & 1)) + x_str
+    else:
+        return chr(4) + pubkey.to_string() #x_str + y_str
+
 __b58chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
 __b58base = len(__b58chars)
 
@@ -137,6 +147,27 @@ def ASecretToSecret(key):
 
 ########### end pywallet functions #######################
 
+# get password routine
+def prompt_password(prompt, confirm=True):
+    if sys.stdin.isatty():
+        password = getpass.getpass(prompt)
+
+        if password and confirm:
+            password2 = getpass.getpass("Confirm: ")
+
+            if password != password2:
+                sys.stderr.write("Error: Passwords do not match.\n")
+                sys.stderr.flush()
+                sys.exit(1)
+
+    else:
+        password = raw_input(prompt)
+
+    if not password:
+        password = None
+
+    return password
+
 # URL decode
 _ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE)
 urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x)
@@ -239,13 +270,14 @@ from interface import DEFAULT_SERVERS
 
 
 class Wallet:
-    def __init__(self, gui_callback = lambda: None):
+    def __init__(self):
 
         self.electrum_version = ELECTRUM_VERSION
         self.seed_version = SEED_VERSION
-        self.gui_callback = gui_callback
+        self.update_callbacks = []
 
         self.gap_limit = 5           # configuration
+        self.use_change = True
         self.fee = 100000
         self.num_zeros = 0
         self.master_public_key = ''
@@ -266,6 +298,7 @@ class Wallet:
         self.receipts = {}           # signed URIs
         self.receipt = None          # next receipt
         self.addressbook = []        # outgoing addresses, for payments
+        self.debug_server = False    # write server communication debug info to stdout
 
         # not saved
         self.tx_history = {}
@@ -288,7 +321,14 @@ class Wallet:
 
         self.pick_random_server()
 
+    def register_callback(self, update_callback):
+        with self.lock:
+            self.update_callbacks.append(update_callback)
 
+    def trigger_callbacks(self):
+        with self.lock:
+            callbacks = self.update_callbacks[:]
+        [update() for update in callbacks]
 
     def pick_random_server(self):
         self.server = random.choice( DEFAULT_SERVERS )         # random choice when the wallet is created
@@ -435,8 +475,8 @@ class Wallet:
                 continue
         else:
             raise BaseException("error: cannot sign message")
-        
-            
+
+
     def verify_message(self, address, signature, message):
         """ See http://www.secg.org/download/aid-780/sec1-v2.pdf for the math """
         from ecdsa import numbertheory, ellipticcurve, util
@@ -448,7 +488,16 @@ class Wallet:
         sig = base64.b64decode(signature)
         if len(sig) != 65: raise BaseException("Wrong encoding")
         r,s = util.sigdecode_string(sig[1:], order)
-        recid = ord(sig[0]) - 27
+        nV = ord(sig[0])
+        if nV < 27 or nV >= 35:
+            raise BaseException("Bad encoding")
+        if nV >= 31:
+            compressed = True
+            nV -= 4
+        else:
+            compressed = False
+
+        recid = nV - 27
         # 1.1
         x = r + (recid/2) * order
         # 1.3
@@ -468,10 +517,8 @@ class Wallet:
         # check that Q is the public key
         public_key.verify_digest( sig[1:], h, sigdecode = ecdsa.util.sigdecode_string)
         # check that we get the original signing address
-        addr = public_key_to_bc_address( '04'.decode('hex') + public_key.to_string() )
-        # print addr
+        addr = public_key_to_bc_address( encode_point(public_key, compressed) )
         if address != addr:
-            print "bad signature"
             raise BaseException("Bad signature")
     
 
@@ -498,6 +545,7 @@ class Wallet:
         if value >= self.gap_limit:
             self.gap_limit = value
             self.save()
+            self.interface.poke()
             return True
 
         elif value >= self.min_acceptable_gap():
@@ -593,6 +641,7 @@ class Wallet:
         s = {
             'seed_version':self.seed_version,
             'use_encryption':self.use_encryption,
+            'use_change':self.use_change,
             'master_public_key': self.master_public_key.encode('hex'),
             'fee':self.fee,
             'server':self.server,
@@ -611,10 +660,13 @@ class Wallet:
             'prioritized_addresses':self.prioritized_addresses,
             'expert_mode':self.expert_mode,
             'gap_limit':self.gap_limit,
+            'debug_server':self.debug_server,
             }
         f = open(self.path,"w")
         f.write( repr(s) )
         f.close()
+        import stat
+        os.chmod(self.path,stat.S_IREAD | stat.S_IWRITE)
 
     def read(self):
         import interface
@@ -627,12 +679,13 @@ class Wallet:
             f.close()
         except:
             return
-        data = interface.old_to_new(data)
         try:
             d = ast.literal_eval( data )
+            interface.old_to_new(d)
             self.seed_version = d.get('seed_version')
             self.master_public_key = d.get('master_public_key').decode('hex')
             self.use_encryption = d.get('use_encryption')
+            self.use_change = bool(d.get('use_change',True))
             self.fee = int( d.get('fee') )
             self.seed = d.get('seed')
             self.server = d.get('server')
@@ -651,6 +704,7 @@ class Wallet:
             self.prioritized_addresses = d.get('prioritized_addresses',[])
             self.expert_mode = d.get('expert_mode',False)
             self.gap_limit = d.get('gap_limit',5)
+            self.debug_server = d.get('debug_server',False)
         except:
             raise BaseException("cannot read wallet file")
 
@@ -734,7 +788,7 @@ class Wallet:
             fee = self.fee*len(inputs) if fixed_fee is None else fixed_fee
             if total >= amount + fee: break
         else:
-            #print "not enough funds: %d %d"%(total, fee)
+            #print "not enough funds: %s %s"%(format_satoshis(total), format_satoshis(fee))
             inputs = []
         return inputs, total, fee
 
@@ -832,7 +886,7 @@ class Wallet:
             default_label = ''
             if tx['value']<0:
                 for o_addr in tx['outputs']:
-                    if not self.is_change(o_addr):
+                    if not self.is_mine(o_addr):
                         dest_label = self.labels.get(o_addr)
                         if dest_label:
                             default_label = 'to: ' + dest_label
@@ -864,6 +918,11 @@ class Wallet:
         inputs, total, fee = self.choose_tx_inputs( amount, fee, from_addr )
         if not inputs:
             raise BaseException("Not enough funds")
+
+        if not self.use_change and not change_addr:
+            change_addr = inputs[0][0]
+            print "Sending change to", change_addr
+
         outputs = self.choose_tx_outputs( to_address, amount, fee, total, change_addr )
         s_inputs = self.sign_inputs( inputs, outputs, password )
 
@@ -1005,6 +1064,12 @@ class Wallet:
             else: 
                 print k,v
 
+        if label and self.labels.get(address) != label:
+            if question('Give label "%s" to address %s ?'%(label,address)):
+                if address not in self.addressbook and address not in self.all_addresses(): 
+                    self.addressbook.append(address)
+                self.labels[address] = label
+
         if signature:
             if re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', identity):
                 signing_address = self.get_alias(identity, True, show_message, question)