allow address creation beyond gap limit in GUI. fixes #512
[electrum-nvc.git] / lib / wallet.py
index 58258d7..fb1e31b 100644 (file)
@@ -123,11 +123,15 @@ class WalletStorage:
         f.write( s )
         f.close()
         if 'ANDROID_DATA' not in os.environ:
-            import stat  #XXX: IS this really android only?
+            import stat
             os.chmod(self.path,stat.S_IREAD | stat.S_IWRITE)
 
 
 class Abstract_Wallet:
+    """
+    Wallet classes are created to handle various address generation methods.
+    Completion states (watching-only, single account, no seed, etc) are handled inside classes.
+    """
 
     def __init__(self, storage):
         self.storage = storage
@@ -383,11 +387,12 @@ class Abstract_Wallet:
 
     def add_keypairs(self, tx, keypairs, password):
         # first check the provided password. This will raise if invalid.
-        self.get_seed(password)
+        self.check_password(password)
+
         addr_list, xpub_list = tx.inputs_to_sign()
         for addr in addr_list:
             if self.is_mine(addr):
-                private_keys = self.get_private_key(address, password)
+                private_keys = self.get_private_key(addr, password)
                 for sec in private_keys:
                     pubkey = public_key_from_private_key(sec)
                     keypairs[ pubkey ] = sec
@@ -542,7 +547,7 @@ class Abstract_Wallet:
 
     def get_account_addresses(self, a, include_change=True):
         if a is None:
-            o = self.addresses(True)
+            o = self.addresses(include_change)
         elif a in self.accounts:
             ac = self.accounts[a]
             o = ac.get_addresses(0)
@@ -1005,6 +1010,20 @@ class Abstract_Wallet:
         c, u = self.get_addr_balance(address)
         return len(h), len(h) > 0 and c == -u
 
+    def address_is_old(self, address, age_limit=2):
+        age = -1
+        h = self.history.get(address, [])
+        if h == ['*']:
+            return True
+        for tx_hash, tx_height in h:
+            if tx_height == 0:
+                tx_age = 0
+            else:
+                tx_age = self.network.get_local_height() - tx_height + 1
+            if tx_age > age:
+                age = tx_age
+        return age > age_limit
+
 
 class Imported_Wallet(Abstract_Wallet):
 
@@ -1033,6 +1052,11 @@ class Imported_Wallet(Abstract_Wallet):
         h = self.history.get(address,[])
         return len(h), False
 
+    def get_master_public_keys(self):
+        return {}
+
+    def is_beyond_limit(self, address, account, is_change):
+        return False
 
 class Deterministic_Wallet(Abstract_Wallet):
 
@@ -1115,39 +1139,26 @@ class Deterministic_Wallet(Abstract_Wallet):
                     if n > nmax: nmax = n
         return nmax + 1
 
-    def address_is_old(self, address):
-        age = -1
-        h = self.history.get(address, [])
-        if h == ['*']:
-            return True
-        for tx_hash, tx_height in h:
-            if tx_height == 0:
-                tx_age = 0
-            else:
-                tx_age = self.network.get_local_height() - tx_height + 1
-            if tx_age > age:
-                age = tx_age
-        return age > 2
+    def create_new_address(self, account, for_change):
+        if account is None:
+            account = self.default_account()
+        address = account.create_new_address(for_change)
+        self.history[address] = []
+        self.synchronizer.add(address)
+        self.save_accounts()
+        return address
 
     def synchronize_sequence(self, account, for_change):
         limit = self.gap_limit_for_change if for_change else self.gap_limit
-        new_addresses = []
         while True:
             addresses = account.get_addresses(for_change)
             if len(addresses) < limit:
-                address = account.create_new_address(for_change)
-                self.history[address] = []
-                new_addresses.append( address )
+                self.create_new_address(account, for_change)
                 continue
-
             if map( lambda a: self.address_is_old(a), addresses[-limit:] ) == limit*[False]:
                 break
             else:
-                address = account.create_new_address(for_change)
-                self.history[address] = []
-                new_addresses.append( address )
-
-        return new_addresses
+                self.create_new_address(account, for_change)
 
     def check_pending_accounts(self):
         for account_id, addr in self.next_addresses.items():
@@ -1159,22 +1170,15 @@ class Deterministic_Wallet(Abstract_Wallet):
                 self.next_addresses.pop(account_id)
 
     def synchronize_account(self, account):
-        new = []
-        new += self.synchronize_sequence(account, 0)
-        new += self.synchronize_sequence(account, 1)
-        return new
+        self.synchronize_sequence(account, 0)
+        self.synchronize_sequence(account, 1)
 
     def synchronize(self):
         self.check_pending_accounts()
-        new = []
         for account in self.accounts.values():
             if type(account) in [ImportedAccount, PendingAccount]:
                 continue
-            new += self.synchronize_account(account)
-        if new:
-            self.save_accounts()
-            self.storage.put('addr_history', self.history, True)
-        return new
+            self.synchronize_account(account)
 
     def restore(self, callback):
         from i18n import _
@@ -1235,14 +1239,35 @@ class Deterministic_Wallet(Abstract_Wallet):
         self.accounts[account_id] = PendingAccount({'pending':addr})
         self.save_accounts()
 
+    def is_beyond_limit(self, address, account, is_change):
+        if type(account) == ImportedAccount:
+            return False
+        addr_list = account.get_addresses(is_change)
+        i = addr_list.index(address)
+        prev_addresses = addr_list[:max(0, i)]
+        limit = self.gap_limit_for_change if is_change else self.gap_limit
+        if len(prev_addresses) < limit:
+            return False
+        prev_addresses = prev_addresses[max(0, i - limit):]
+        for addr in prev_addresses:
+            if self.address_is_old(addr):
+                return False
+        return True
+
 
 class NewWallet(Deterministic_Wallet):
 
     def __init__(self, storage):
         Deterministic_Wallet.__init__(self, storage)
 
+    def default_account(self):
+        return self.accounts["m/0'"]
+
+    def is_watching_only(self):
+        return self.master_private_keys is {}
+
     def can_create_accounts(self):
-        return not self.is_watching_only()
+        return 'm/' in self.master_private_keys.keys()
 
     def get_master_public_key(self):
         return self.master_public_keys["m/"]
@@ -1266,19 +1291,29 @@ class NewWallet(Deterministic_Wallet):
         xpub = self.master_public_keys["m/"]
         assert deserialize_xkey(xpriv)[3] == deserialize_xkey(xpub)[3]
 
-    def create_watching_only_wallet(self, xpub):
+    def create_xprv_wallet(self, xprv, password):
+        xpub = bitcoin.xpub_from_xprv(xprv)
+        account = BIP32_Account({'xpub':xpub})
+        account_id = 'm/' + bitcoin.get_xkey_name(xpub)
         self.storage.put('seed_version', self.seed_version, True)
-        self.add_master_public_key("m/", xpub)
+        self.add_master_private_key(account_id, xprv, password)
+        self.add_master_public_key(account_id, xpub)
+        self.add_account(account_id, account)
+
+    def create_watching_only_wallet(self, xpub):
         account = BIP32_Account({'xpub':xpub})
-        self.add_account("m/", account)
+        account_id = 'm/' + bitcoin.get_xkey_name(xpub)
+        self.storage.put('seed_version', self.seed_version, True)
+        self.add_master_public_key(account_id, xpub)
+        self.add_account(account_id, account)
 
     def create_accounts(self, password):
         # First check the password is valid (this raises if it isn't).
-        pw_decode(self.seed, password)
+        self.check_password(password)
         self.create_account('Main account', password)
 
-    def add_master_public_key(self, name, mpk):
-        self.master_public_keys[name] = mpk
+    def add_master_public_key(self, name, xpub):
+        self.master_public_keys[name] = xpub
         self.storage.put('master_public_keys', self.master_public_keys, True)
 
     def add_master_private_key(self, name, xpriv, password):
@@ -1372,6 +1407,9 @@ class Wallet_2of2(NewWallet):
         NewWallet.__init__(self, storage)
         self.storage.put('wallet_type', '2of2', True)
 
+    def default_account(self):
+        return self.accounts['m/']
+
     def can_create_accounts(self):
         return False
 
@@ -1433,6 +1471,9 @@ class Wallet_2of3(Wallet_2of2):
 
 class OldWallet(Deterministic_Wallet):
 
+    def default_account(self):
+        return self.accounts[0]
+
     def make_seed(self):
         import mnemonic
         seed = random_seed(128)
@@ -1551,21 +1592,31 @@ class Wallet(object):
             return False
 
     @classmethod
-    def is_mpk(self, mpk):
+    def is_old_mpk(self, mpk):
         try:
             int(mpk, 16)
-            old = True
+            assert len(mpk) == 128
+            return True
         except:
-            old = False
+            return False
 
-        if old:
-            return len(mpk) == 128
-        else:
-            try:
-                deserialize_xkey(mpk)
-                return True
-            except:
-                return False
+    @classmethod
+    def is_xpub(self, text):
+        try:
+            assert text[0:4] == 'xpub'
+            deserialize_xkey(text)
+            return True
+        except:
+            return False
+
+    @classmethod
+    def is_xprv(self, text):
+        try:
+            assert text[0:4] == 'xprv'
+            deserialize_xkey(text)
+            return True
+        except:
+            return False
 
     @classmethod
     def is_address(self, text):
@@ -1610,19 +1661,20 @@ class Wallet(object):
         return w
 
     @classmethod
-    def from_mpk(self, mpk, storage):
-        try:
-            int(mpk, 16)
-            old = True
-        except:
-            old = False
+    def from_old_mpk(self, mpk, storage):
+        w = OldWallet(storage)
+        w.seed = ''
+        w.create_watching_only_wallet(mpk)
+        return w
 
-        if old:
-            w = OldWallet(storage)
-            w.seed = ''
-            w.create_watching_only_wallet(mpk)
-        else:
-            w = NewWallet(storage)
-            w.create_watching_only_wallet(mpk)
+    @classmethod
+    def from_xpub(self, xpub, storage):
+        w = NewWallet(storage)
+        w.create_watching_only_wallet(xpub)
+        return w
 
+    @classmethod
+    def from_xprv(self, xprv, password, storage):
+        w = NewWallet(storage)
+        w.create_xprv_wallet(xprv, password)
         return w