Repository: cryptoadvance/specter-desktop

Issue:
Specter-v2.0.1-pre3: Not all wallets loaded
- On Spectrum instance during 1st sync the MultiSig wallet stopped loading.
- Upon relaunch the Specter did not find the MultiSig wallet: "Not all Wallets loaded"
- Log File: [specterApp.log](https://github.com/cryptoadvance/specter-desktop/files/11035560/specterApp.log)


![image](https://user-images.githubusercontent.com/47259243/226784164-82f5c5a2-9917-4e21-b5d1-0411ed8b8ec4.png)



Discussion:
netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *specter-desktop-docs* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 80d78f0ed7970146a02ff6370612f8f65c22c52f |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/specter-desktop-docs/deploys/641b320b03fef50007822740 |

Patch:
diff --git a/requirements.in b/requirements.in
index aa959dfe..e018be8c 100644
--- a/requirements.in
+++ b/requirements.in
@@ -33,5 +33,5 @@ psycopg2-binary==2.9.5
 cryptoadvance-liquidissuer==0.2.4
 specterext-exfund==0.1.7
 specterext-faucet==0.1.2
-cryptoadvance.spectrum==0.6.3
+cryptoadvance.spectrum==0.6.4
 specterext-stacktrack==0.3.0
diff --git a/requirements.txt b/requirements.txt
index 62edf1fc..c2d4f345 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,7 +2,7 @@
 # This file is autogenerated by pip-compile with Python 3.10
 # by the following command:
 #
-#    pip-compile --generate-hashes requirements.in
+#    pip-compile --generate-hashes --resolver=backtracking requirements.in
 #
 aniso8601==9.0.1 \
     --hash=sha256:1d2b7ef82963909e93c4f24ce48d4de9e66009a21bf1c1e1c85bdd0812fe412f \
@@ -119,9 +119,9 @@ cryptoadvance-liquidissuer==0.2.4 \
     --hash=sha256:5a2c531801854c5a4a46daf184877e22f731cdb42d2cfb840785bda7371ba6fb \
     --hash=sha256:9e468f3e35ecc566b3f74a2263677cf26632548abb194521dba15ad37acd1e9b
     # via -r requirements.in
-cryptoadvance-spectrum==0.6.3 \
-    --hash=sha256:706287a9e1640f1a27a1a3c6e27a147599dde9969e7dbd2df99c890514b2b1ba \
-    --hash=sha256:877006d81c5be0d5da12ca8d4b0dd7dc21b104bc826cd35c7de5019ca56cf64a
+cryptoadvance-spectrum==0.6.4 \
+    --hash=sha256:226f89dfe96dac2fcd82f462706e118f017582df5565ff1924f27e4392bc6a0e \
+    --hash=sha256:fa6c877b3a5ba683ccde991f020fce2f3b06e0b8f5219a6fdc592658cacb1d3d
     # via -r requirements.in
 cryptography==39.0.1 \
     --hash=sha256:0f8da300b5c8af9f98111ffd512910bc792b4c77392a9523624680f7956a99d4 \
diff --git a/src/cryptoadvance/specter/liquid/addresslist.py b/src/cryptoadvance/specter/liquid/addresslist.py
index 003a9d74..d3fa5ce6 100644
--- a/src/cryptoadvance/specter/liquid/addresslist.py
+++ b/src/cryptoadvance/specter/liquid/addresslist.py
@@ -1,4 +1,4 @@
-from ..addresslist import *
+from ..wallet.addresslist import *
 from embit.liquid.addresses import addr_decode, to_unconfidential
 
 
diff --git a/src/cryptoadvance/specter/liquid/txlist.py b/src/cryptoadvance/specter/liquid/txlist.py
index f7efecb6..f0f19ef7 100644
--- a/src/cryptoadvance/specter/liquid/txlist.py
+++ b/src/cryptoadvance/specter/liquid/txlist.py
@@ -1,4 +1,4 @@
-from ..txlist import *
+from ..wallet.txlist import *
 from embit.liquid.transaction import LTransaction, TxOutWitness, unblind
 from embit.liquid.pset import PSET
 from embit.liquid import slip77
diff --git a/src/cryptoadvance/specter/liquid/wallet.py b/src/cryptoadvance/specter/liquid/wallet.py
index ced71a22..2c9dae97 100644
--- a/src/cryptoadvance/specter/liquid/wallet.py
+++ b/src/cryptoadvance/specter/liquid/wallet.py
@@ -8,12 +8,12 @@ from embit.liquid.descriptor import LDescriptor
 from embit.liquid.pset import PSET
 from embit.liquid.transaction import LTransaction
 
-from ..addresslist import Address
 from ..specter_error import SpecterError
 from ..wallet import *
 from .addresslist import LAddressList
 from .txlist import LTxList
 from .util.pset import SpecterPSET
+from .addresslist import Address
 
 
 class LWallet(Wallet):
diff --git a/src/cryptoadvance/specter/server_endpoints/wallets/wallets_api.py b/src/cryptoadvance/specter/server_endpoints/wallets/wallets_api.py
index 065a50a1..b2534af9 100644
--- a/src/cryptoadvance/specter/server_endpoints/wallets/wallets_api.py
+++ b/src/cryptoadvance/specter/server_endpoints/wallets/wallets_api.py
@@ -21,7 +21,7 @@ from flask_babel import lazy_gettext as _
 from flask_login import current_user, login_required
 from werkzeug.wrappers import Response
 
-from cryptoadvance.specter.txlist import WalletAwareTxItem
+from cryptoadvance.specter.wallet.txlist import WalletAwareTxItem
 
 from ...commands.psbt_creator import PsbtCreator
 from ...helpers import bcur2base64
@@ -1075,8 +1075,8 @@ def process_txlist(txlist, idx=0, limit=100, search=None, sortby=None, sortdir="
             )
             or (
                 any(search in str(amount) for amount in tx["amount"])
-                if isinstance(tx["amount"], list)
-                else search in str(tx["amount"])
+                if isinstance(tx["flow_amount"], list)
+                else search in str(tx["flow_amount"])
             )
             or search in str(tx["confirmations"])
             or search in str(tx["time"])
diff --git a/src/cryptoadvance/specter/services/service.py b/src/cryptoadvance/specter/services/service.py
index 2409642d..5d73ce24 100644
--- a/src/cryptoadvance/specter/services/service.py
+++ b/src/cryptoadvance/specter/services/service.py
@@ -13,7 +13,6 @@ from typing import List
 from .service_encrypted_storage import ServiceEncryptedStorageManager
 from .service_annotations_storage import ServiceAnnotationsStorage
 
-from cryptoadvance.specter.addresslist import Address
 from cryptoadvance.specter.services import callbacks
 
 
diff --git a/src/cryptoadvance/specter/wallet/__init__.py b/src/cryptoadvance/specter/wallet/__init__.py
new file mode 100644
index 00000000..e3f2b4d7
--- /dev/null
+++ b/src/cryptoadvance/specter/wallet/__init__.py
@@ -0,0 +1,3 @@
+from .wallet import Wallet, purposes
+from .addresslist import Address
+from .txlist import WalletAwareTxItem
diff --git a/src/cryptoadvance/specter/wallet/abstract_wallet.py b/src/cryptoadvance/specter/wallet/abstract_wallet.py
new file mode 100644
index 00000000..595961bd
--- /dev/null
+++ b/src/cryptoadvance/specter/wallet/abstract_wallet.py
@@ -0,0 +1,25 @@
+from .txlist import TxList
+
+
+class AbstractWallet:
+    @property
+    def transactions(self) -> TxList:
+        if hasattr(self, "_transactions"):
+            return self._transactions
+        else:
+            return None
+
+    @property
+    def addresses(self) -> TxList:
+        if hasattr(self, "_addresses"):
+            return self._addresses
+        else:
+            return None
+
+    @property
+    # abstractmethod
+    def rpc(self):
+        """Cache RPC instance. Reuse if manager's RPC instance hasn't changed. Create new RPC instance otherwise.
+        This RPC instance is also used by objects created by the wallet, such as TxList or TxItem
+        """
+        pass
diff --git a/src/cryptoadvance/specter/addresslist.py b/src/cryptoadvance/specter/wallet/addresslist.py
similarity index 99%
rename from src/cryptoadvance/specter/addresslist.py
rename to src/cryptoadvance/specter/wallet/addresslist.py
index 4f0f90f3..b24d682e 100644
--- a/src/cryptoadvance/specter/addresslist.py
+++ b/src/cryptoadvance/specter/wallet/addresslist.py
@@ -2,7 +2,7 @@
 Manages the list of addresses for the wallet, including labels and derivation paths
 """
 import os
-from .persistence import write_csv, read_csv
+from ..persistence import write_csv, read_csv
 import logging
 
 logger = logging.getLogger(__name__)
diff --git a/src/cryptoadvance/specter/wallet/tx_fetcher.py b/src/cryptoadvance/specter/wallet/tx_fetcher.py
new file mode 100644
index 00000000..6ac4bfc1
--- /dev/null
+++ b/src/cryptoadvance/specter/wallet/tx_fetcher.py
@@ -0,0 +1,275 @@
+import logging
+from .abstract_wallet import AbstractWallet
+
+logger = logging.getLogger(__name__)
+
+
+class TxFetcher:
+    """A class to refactor the fetch_transaction_method which no one understands"""
+
+    LISTTRANSACTIONS_BATCH_SIZE = 1000
+
+    def __init__(self, wallet: AbstractWallet):
+        self.wallet = wallet
+
+    def _fetch_transactions(self):
+
+        # unconfirmed_selftransfers needed since Bitcoin Core does not properly list `selftransfer` txs in `listtransactions` command
+        # Until v0.21, it listed there consolidations to a receive address, but not change address
+        # Since v0.21, it does not list there consolidations at all
+        # Therefore we need to check here if a transaction might got confirmed
+        # NOTE: This might be a problem in case of re-org...
+        # More details: https://github.com/cryptoadvance/specter-desktop/issues/996
+
+        arr = [
+            tx["result"]
+            for tx in self.unconfirmed_selftransfers_txs
+            if tx.get("result")
+        ]
+        arr.extend(self.interesting_txs())
+        txs = self.transform_to_dict_with_txid_as_key(
+            arr
+        )  # and gettransaction as value
+
+        # fix for core versions < v0.20 (add blockheight if not there)
+        self.fill_blockheight_if_necessary(txs)
+
+        if self.wallet.use_descriptors:
+            # Get all used addresses that belong to the wallet
+
+            addresses_info = self.extract_addresses(txs)
+
+            # representing the highest index of the addresses from the wallet and
+            # the passed addresses
+            (
+                max_used_receiving,
+                max_used_change,
+            ) = self.calculate_max_used_from_addresses(addresses_info)
+
+            # If max receiving address bigger than current max receiving index minus the gap limit - self._addresses.max_index(change=False)
+            if (
+                max_used_receiving + self.wallet.GAP_LIMIT
+                > self.wallet._addresses.max_index(change=False)
+            ):
+                addresses = [
+                    dict(
+                        address=self.wallet.get_address(
+                            idx, change=False, check_keypool=False
+                        ),
+                        index=idx,
+                        change=False,
+                    )
+                    for idx in range(
+                        self.wallet.addresses.max_index(change=False),
+                        max_used_receiving + self.wallet.GAP_LIMIT,
+                    )
+                ]
+                self.wallet.addresses.add(addresses, check_rpc=False)
+
+            # If max change address bigger than current max change index minus the gap limit  - wallet.addresses.max_index(change=True)
+            if (
+                max_used_change + self.wallet.GAP_LIMIT
+                > self.wallet.addresses.max_index(change=True)
+            ):
+                # Add change addresses until the new max address plus the GAP_LIMIT
+                change_addresses = [
+                    dict(
+                        address=self.wallet.get_address(
+                            idx, change=True, check_keypool=False
+                        ),
+                        index=idx,
+                        change=True,
+                    )
+                    for idx in range(
+                        self.wallet.addresses.max_index(change=True),
+                        max_used_change + self.wallet.GAP_LIMIT,
+                    )
+                ]
+                self.wallet.addresses.add(change_addresses, check_rpc=False)
+
+        # only delete with confirmed txs
+        self.wallet.delete_spent_pending_psbts(
+            [
+                tx["hex"]
+                for tx in txs.values()
+                if tx.get("confirmations", 0) > 0 or tx.get("blockheight")
+            ]
+        )
+        self.wallet.transactions.add(txs)
+
+    def is_interesting_tx(self, tx: dict):
+        """transactions that we don't know about,
+        # or that it has a different blockhash (reorg / confirmed)
+        # or doesn't have an address(?)
+        # or has wallet conflicts
+        """
+        if (
+            # we don't know about tx
+            tx["txid"] not in self.wallet._transactions
+            # we don't know addresses
+            or not self.wallet._transactions[tx["txid"]].get("address", None)
+            # blockhash is different (reorg / unconfirmed)
+            or self.wallet._transactions[tx["txid"]].get("blockhash", None)
+            != tx.get("blockhash", None)
+            # we have conflicts
+            or self.wallet._transactions[tx["txid"]].get("conflicts", [])
+            != tx.get("walletconflicts", [])
+        ):
+            return True
+        return False
+
+    def interesting_txs(self):
+        """returns an array of interesting transactions (see is_interesting_tx() ) where txid is
+        the key and the result is whatever listtransactions is retuirning as values
+        """
+        idx = 0
+        arr = []
+        while True:
+            txlist = self.wallet.rpc.listtransactions(
+                "*",
+                self.LISTTRANSACTIONS_BATCH_SIZE,  # count
+                self.LISTTRANSACTIONS_BATCH_SIZE * idx,  # skip
+                True,
+            )
+            # filter interesting TXs
+            res = [tx for tx in txlist if self.is_interesting_tx(tx)]
+            arr.extend(res)
+            idx += 1
+            # stop if we reached known transactions
+            # not sure if Core <20 returns last batch or empty array at the end
+            if (
+                len(res) < self.LISTTRANSACTIONS_BATCH_SIZE
+                or len(arr) < self.LISTTRANSACTIONS_BATCH_SIZE * idx
+            ):
+                break
+
+        return arr
+
+    def transform_to_dict_with_txid_as_key(self, arr):
+        """gets an array of tx-dicts where and transoforms it
+        to a dict with txid as keys and the corresponding result
+        of gettransaction as value.
+        """
+        # Start with an dict with txids as keys and None as values:
+        txs = dict.fromkeys([a["txid"] for a in arr])
+        txids = list(txs.keys())
+        # get all raw transactions
+        res = self.wallet.rpc.multi([("gettransaction", txid) for txid in txids])
+        for i, r in enumerate(res):
+            txid = txids[i]
+            # check if we already added it
+            if txs.get(txid, None) is not None:
+                continue
+            txs[txid] = r["result"]
+        return txs
+
+    def fill_blockheight_if_necessary(self, txs):
+        """
+        This is a fix for Bitcoin Core versions < v0.20
+        These do not return the blockheight as part of the `gettransaction` command
+        So here we check if this property is lacking and if so
+        query the current block height and manually calculate it.
+
+        Remove after dropping Core v0.19 support
+        """
+        check_blockheight = False
+        for tx in txs.values():
+            if tx and tx.get("confirmations", 0) > 0 and "blockheight" not in tx:
+                check_blockheight = True
+                break
+        if check_blockheight:
+            current_blockheight = self.wallet.rpc.getblockcount()
+            for tx in txs.values():
+                if tx.get("confirmations", 0) > 0:
+                    tx["blockheight"] = current_blockheight - tx["confirmations"] + 1
+
+    @property
+    def unconfirmed_selftransfers_txs(self):
+        if not hasattr(self, "_unconfirmed_selftransfers_txs"):
+            unconfirmed_selftransfers = [
+                txid
+                for txid in self.wallet._transactions
+                if self.wallet._transactions[txid].category == "selftransfer"
+                and not self.wallet._transactions[txid].get("blockhash", None)
+            ]
+            unconfirmed_selftransfers_txs = []
+            if unconfirmed_selftransfers:
+                self._unconfirmed_selftransfers_txs = self.wallet.rpc.multi(
+                    [("gettransaction", txid) for txid in unconfirmed_selftransfers]
+                )
+            else:
+                self._unconfirmed_selftransfers_txs = []
+        return self._unconfirmed_selftransfers_txs
+
+    def extract_addresses(self, txs):
+        """Takes txs (dict with txid as key and the result of gettransaction as value )
+        and extracts all the addresses which
+        * belongs to the wallet
+        * are not yet in self.addresses
+        """
+        potential_relevant_txs = [
+            tx
+            for tx in txs.values()
+            if tx
+            and tx.get("details")
+            and (
+                tx.get("details")[0].get("category") != "send"
+                and tx["details"][0].get("address") not in self.wallet.addresses
+            )
+        ]
+
+        addresses_info_multi = self.wallet.rpc.multi(
+            [
+                ("getaddressinfo", address)
+                for address in [
+                    tx["details"][0].get("address") for tx in potential_relevant_txs
+                ]
+                if address
+            ]
+        )
+
+        addresses_info = [
+            r["result"]
+            for r in addresses_info_multi
+            if r["result"].get("ismine", False)
+        ]
+        logger.info(f"Those addresses got used recently: {addresses_info}")
+        return addresses_info
+
+    def calculate_max_used_from_addresses(self, addresses_info):
+        """Return a tuple of max_used_receiving and max_used_change
+        representing the highest index of the addresses from the wallet and
+        the passed addresses
+        """
+        # Gets max index used receiving and change addresses
+        max_used_receiving = self.wallet.addresses.max_used_index(change=False)
+        max_used_change = self.wallet.addresses.max_used_index(change=True)
+
+        for address in addresses_info:
+            desc = self.wallet.DescriptorCls.from_string(address["desc"])
+            indexes = [
+                {
+                    "idx": k.origin.derivation[-1],
+                    "change": k.origin.derivation[-2],
+                }
+                for k in desc.keys
+            ]
+            for idx in indexes:
+                if int(idx["change"]) == 0:
+                    max_used_receiving = max(max_used_receiving, int(idx["idx"]))
+                elif int(idx["change"]) == 1:
+                    max_used_change = max(max_used_change, int(idx["idx"]))
+        return max_used_receiving, max_used_change
+
+    @classmethod
+    def fetch_transactions(cls, wallet: AbstractWallet):
+        """Loads new transactions from Bitcoin Core. A quite confusing method which mainly tries to figure out which transactions are new
+        and need to be added to the local TxList wallet._transactions and adding them.
+        So the method doesn't return anything but has these side_effects:
+        1. Adding the new interesting transactions to wallet._transactions
+        2. for wallet.use_descriptors create new addresses and add them to wallet._addresses
+        3. calls wallet.delete_spent_pending_psbts
+        Most of that code could probably encapsulated in the TxList class.
+        """
+        tx_fetcher = TxFetcher(wallet)
+        tx_fetcher._fetch_transactions()
diff --git a/src/cryptoadvance/specter/txlist.py b/src/cryptoadvance/specter/wallet/txlist.py
similarity index 98%
rename from src/cryptoadvance/specter/txlist.py
rename to src/cryptoadvance/specter/wallet/txlist.py
index 82197b1a..c9c604b8 100644
--- a/src/cryptoadvance/specter/txlist.py
+++ b/src/cryptoadvance/specter/wallet/txlist.py
@@ -11,20 +11,20 @@ from embit import bip32
 from embit.liquid.networks import get_network
 from embit.transaction import Transaction
 
-from .helpers import get_address_from_dict
-from .persistence import delete_file, read_csv, write_csv
-from .specter_error import SpecterError, SpecterInternalException
+from ..helpers import get_address_from_dict
+from ..persistence import delete_file, read_csv, write_csv
+from ..specter_error import SpecterError, SpecterInternalException
 from embit.descriptor import Descriptor
 from embit.liquid.descriptor import LDescriptor
-from .util.common import str2bool
-from .util.psbt import (
+from ..util.common import str2bool
+from ..util.psbt import (
     AbstractTxContext,
     SpecterInputScope,
     SpecterOutputScope,
     SpecterPSBT,
     SpecterTx,
 )
-from .util.tx import decoderawtransaction
+from ..util.tx import decoderawtransaction
 from threading import RLock
 
 logger = logging.getLogger(__name__)
@@ -429,6 +429,7 @@ class WalletAwareTxItem(TxItem):
         all_outs = [out.to_dict() for out in self.psbt.outputs]
         my_outs = [out for out in all_outs if out["is_mine"]]
         my_receiving = [out for out in my_outs if not out["change"]]
+        my_change = [out for out in my_outs if out["change"]]
         external = [out for out in all_outs if out not in my_outs]
         # decide what addresses to show
         if self.category in ["generate", "receive", "selftransfer"]:
@@ -439,7 +440,7 @@ class WalletAwareTxItem(TxItem):
             outs = external or all_outs
         else:
             # not sure what's the best here
-            outs = my_receiving or my_outs or external or all_outs
+            outs = my_receiving or my_change or my_outs or external or all_outs
         addresses = [out.get("address", "Unknown") for out in outs]
         self["address"] = addresses[0]
         return self["address"]
diff --git a/src/cryptoadvance/specter/wallet.py b/src/cryptoadvance/specter/wallet/wallet.py
similarity index 89%
rename from src/cryptoadvance/specter/wallet.py
rename to src/cryptoadvance/specter/wallet/wallet.py
index 679dd12f..e497031c 100644
--- a/src/cryptoadvance/specter/wallet.py
+++ b/src/cryptoadvance/specter/wallet/wallet.py
@@ -1,10 +1,16 @@
-import json, logging, os, re, csv
-import requests
+import csv
+import json
+import logging
+import os
+import re
 import threading
 import time
-
 from collections import OrderedDict
 from csv import Error
+from io import StringIO
+from typing import Dict, List
+
+import requests
 from embit import bip32
 from embit.descriptor import Descriptor
 from embit.descriptor.checksum import add_checksum
@@ -12,23 +18,23 @@ from embit.ec import PublicKey
 from embit.liquid.networks import get_network
 from embit.psbt import DerivationPath
 from embit.transaction import Transaction
-from io import StringIO
-from typing import Dict, List
 
 from cryptoadvance.specter.commands.utxo_scanner import UtxoScanner
 from cryptoadvance.specter.rpc import RpcError
 
-from .addresslist import Address, AddressList
-from .device import Device
-from .key import Key
-from .util.merkleblock import is_valid_merkle_proof
-from .helpers import get_address_from_dict
-from .persistence import write_json_file, delete_file, delete_folder
-from .specter_error import SpecterError, handle_exception
+from ..device import Device
+from ..helpers import get_address_from_dict
+from ..key import Key
+from ..persistence import delete_file, delete_folder, write_json_file
+from ..specter_error import SpecterError, handle_exception
+from ..util.merkleblock import is_valid_merkle_proof
+from ..util.psbt import SpecterPSBT
+from ..util.tx import decoderawtransaction
+from ..util.xpub import get_xpub_fingerprint
+from .tx_fetcher import TxFetcher
 from .txlist import TxItem, TxList, WalletAwareTxItem
-from .util.psbt import SpecterPSBT
-from .util.tx import decoderawtransaction
-from .util.xpub import get_xpub_fingerprint
+from .abstract_wallet import AbstractWallet
+from .addresslist import AddressList, Address
 
 logger = logging.getLogger(__name__)
 LISTTRANSACTIONS_BATCH_SIZE = 1000
@@ -57,7 +63,7 @@ addrtypes = {
 }
 
 
-class Wallet:
+class Wallet(AbstractWallet):
     # if the wallet is old we import 300 addresses
     IMPORT_KEYPOOL = 300
     # a gap of 20 addresses is what many wallets do (not used with descriptor wallets)
@@ -404,176 +410,7 @@ class Wallet:
         3. calls self.delete_spent_pending_psbts
         Most of that code could probably encapsulated in the TxList class.
         """
-        arr = []
-        idx = 0
-        # unconfirmed_selftransfers needed since Bitcoin Core does not properly list `selftransfer` txs in `listtransactions` command
-        # Until v0.21, it listed there consolidations to a receive address, but not change address
-        # Since v0.21, it does not list there consolidations at all
-        # Therefore we need to check here if a transaction might got confirmed
-        # NOTE: This might be a problem in case of re-org...
-        # More details: https://github.com/cryptoadvance/specter-desktop/issues/996
-        unconfirmed_selftransfers = [
-            txid
-            for txid in self._transactions
-            if self._transactions[txid].category == "selftransfer"
-            and not self._transactions[txid].get("blockhash", None)
-        ]
-        unconfirmed_selftransfers_txs = []
-        if unconfirmed_selftransfers:
-            unconfirmed_selftransfers_txs = self.rpc.multi(
-                [("gettransaction", txid) for txid in unconfirmed_selftransfers]
-            )
-        arr = [tx["result"] for tx in unconfirmed_selftransfers_txs if tx.get("result")]
-        while True:
-            txlist = self.rpc.listtransactions(
-                "*",
-                LISTTRANSACTIONS_BATCH_SIZE,  # count
-                LISTTRANSACTIONS_BATCH_SIZE * idx,  # skip
-                True,
-            )
-            # list of transactions that we don't know about,
-            # or that it has a different blockhash (reorg / confirmed)
-            # or doesn't have an address(?)
-            # or has wallet conflicts
-            res = [
-                tx
-                for tx in txlist
-                # we don't know about tx
-                if tx["txid"] not in self._transactions
-                # we don't know addresses
-                or not self._transactions[tx["txid"]].get("address", None)
-                # blockhash is different (reorg / unconfirmed)
-                or self._transactions[tx["txid"]].get("blockhash", None)
-                != tx.get("blockhash", None)
-                # we have conflicts
-                or self._transactions[tx["txid"]].get("conflicts", [])
-                != tx.get("walletconflicts", [])
-            ]
-            arr.extend(res)
-            idx += 1
-            # stop if we reached known transactions
-            # not sure if Core <20 returns last batch or empty array at the end
-            if (
-                len(res) < LISTTRANSACTIONS_BATCH_SIZE
-                or len(arr) < LISTTRANSACTIONS_BATCH_SIZE * idx
-            ):
-                break
-        txs = dict.fromkeys([a["txid"] for a in arr])
-        txids = list(txs.keys())
-        # get all raw transactions
-        res = self.rpc.multi([("gettransaction", txid) for txid in txids])
-        for i, r in enumerate(res):
-            txid = txids[i]
-            # check if we already added it
-            if txs.get(txid, None) is not None:
-                continue
-            txs[txid] = r["result"]
-        # This is a fix for Bitcoin Core versions < v0.20
-        # These do not return the blockheight as part of the `gettransaction` command
-        # So here we check if this property is lacking and if so
-        # query the current block height and manually calculate it.
-        ##################### Remove from here after dropping Core v0.19 support #####################
-        check_blockheight = False
-        for tx in txs.values():
-            if tx and tx.get("confirmations", 0) > 0 and "blockheight" not in tx:
-                check_blockheight = True
-                break
-        if check_blockheight:
-            current_blockheight = self.rpc.getblockcount()
-            for tx in txs.values():
-                if tx.get("confirmations", 0) > 0:
-                    tx["blockheight"] = current_blockheight - tx["confirmations"] + 1
-        ##################### Remove until here after dropping Core v0.19 support #####################
-        if self.use_descriptors:
-            # Get all used addresses that belong to the wallet
-            addresses_info_multi = self.rpc.multi(
-                [
-                    ("getaddressinfo", address)
-                    for address in [
-                        tx["details"][0].get("address")
-                        for tx in txs.values()
-                        if tx
-                        and tx.get("details")
-                        and (
-                            tx.get("details")[0].get("category") != "send"
-                            and tx["details"][0].get("address") not in self._addresses
-                        )
-                    ]
-                    if address
-                ]
-            )
-            addresses_info = [
-                r["result"]
-                for r in addresses_info_multi
-                if r["result"].get("ismine", False)
-            ]
-
-            # Gets max index used receiving and change addresses
-            max_used_receiving = self._addresses.max_used_index(change=False)
-            max_used_change = self._addresses.max_used_index(change=True)
-
-            for address in addresses_info:
-                desc = self.DescriptorCls.from_string(address["desc"])
-                indexes = [
-                    {
-                        "idx": k.origin.derivation[-1],
-                        "change": k.origin.derivation[-2],
-                    }
-                    for k in desc.keys
-                ]
-                for idx in indexes:
-                    if int(idx["change"]) == 0:
-                        max_used_receiving = max(max_used_receiving, int(idx["idx"]))
-                    elif int(idx["change"]) == 1:
-                        max_used_change = max(max_used_change, int(idx["idx"]))
-
-            # If max receiving address bigger than current max receiving index minus the gap limit - self._addresses.max_index(change=False)
-            if max_used_receiving + self.GAP_LIMIT > self._addresses.max_index(
-                change=False
-            ):
-                # Add receiving addresses until the new max address plus the GAP_LIMIT
-                addresses = [
-                    dict(
-                        address=self.get_address(
-                            idx, change=False, check_keypool=False
-                        ),
-                        index=idx,
-                        change=False,
-                    )
-                    for idx in range(
-                        self._addresses.max_index(change=False),
-                        max_used_receiving + self.GAP_LIMIT,
-                    )
-                ]
-                self._addresses.add(addresses, check_rpc=False)
-
-            # If max change address bigger than current max change index minus the gap limit  - self._addresses.max_index(change=True)
-            if max_used_change + self.GAP_LIMIT > self._addresses.max_index(
-                change=True
-            ):
-                # Add change addresses until the new max address plus the GAP_LIMIT
-                change_addresses = [
-                    dict(
-                        address=self.get_address(idx, change=True, check_keypool=False),
-                        index=idx,
-                        change=True,
-                    )
-                    for idx in range(
-                        self._addresses.max_index(change=True),
-                        max_used_change + self.GAP_LIMIT,
-                    )
-                ]
-                self._addresses.add(change_addresses, check_rpc=False)
-
-        # only delete with confirmed txs
-        self.delete_spent_pending_psbts(
-            [
-                tx["hex"]
-                for tx in txs.values()
-                if tx.get("confirmations", 0) > 0 or tx.get("blockheight")
-            ]
-        )
-        self._transactions.add(txs)
+        TxFetcher.fetch_transactions(self)
 
     def import_address_labels(self, address_labels):
         """
@@ -664,7 +501,7 @@ class Wallet:
             while self.rpc.getreceivedbyaddress(addr, 0) != 0:
                 addr = self.getnewaddress()
         except Exception as e:
-            logger.exception(f"Failed to check for address reuse: {e}", e)
+            logger.exception(f"Failed to check for address reuse: {e}")
 
     def check_addresses(self):
         """Checking the gap limit is still ok"""
@@ -1711,11 +1548,17 @@ class Wallet:
         return round(self.balance["trusted"] + self.balance["untrusted_pending"], 8)
 
     @property
-    def addresses(self):
+    def relevant_addresses(self):
+        """Not used much (only in tests?!)
+        Returns a list of addresses from index 0 until self.address_index + 1
+        """
         return [self.get_address(idx) for idx in range(0, self.address_index + 1)]
 
     @property
-    def change_addresses(self):
+    def relevant_change_addresses(self):
+        """Not used much (only in tests?!)
+        Returns a list of change-addresses from index 0 until self.change_index + 1
+        """
         return [
             self.get_address(idx, change=True)
             for idx in range(0, self.change_index + 1)
@@ -2008,8 +1851,7 @@ class Wallet:
                     inp.non_witness_utxo = self.TxCls.from_string(res["hex"])
                 except Exception as e:
                     logger.exception(
-                        f"Can't find previous transaction in the wallet. Signing might not be possible for certain devices... Txid: {txid}, Exception: {e}",
-                        e,
+                        f"Can't find previous transaction in the wallet. Signing might not be possible for certain devices... Txid: {txid}, Exception: {e}"
                     )
         else:
             # remove non_witness_utxo if we don't want them
diff --git a/src/cryptoadvance/specterext/swan/service.py b/src/cryptoadvance/specterext/swan/service.py
index 0969ef81..92190f42 100644
--- a/src/cryptoadvance/specterext/swan/service.py
+++ b/src/cryptoadvance/specterext/swan/service.py
@@ -18,7 +18,7 @@ from cryptoadvance.specter.services.service import (
     devstatus_beta,
     devstatus_prod,
 )
-from cryptoadvance.specter.addresslist import Address
+from cryptoadvance.specter.wallet import Address
 from cryptoadvance.specter.wallet import Wallet
 from urllib.parse import urlparse
 


Test patch:
diff --git a/tests/fix_devices_and_wallets.py b/tests/fix_devices_and_wallets.py
index 2fff51fc..af21c577 100644
--- a/tests/fix_devices_and_wallets.py
+++ b/tests/fix_devices_and_wallets.py
@@ -6,7 +6,8 @@ from cryptoadvance.specter.util.mnemonic import generate_mnemonic
 from cryptoadvance.specter.process_controller.node_controller import NodeController
 from cryptoadvance.specter.managers.wallet_manager import WalletManager
 from cryptoadvance.specter.specter import Specter
-from cryptoadvance.specter.wallet import Wallet, Device
+from cryptoadvance.specter.wallet import Wallet
+from cryptoadvance.specter.device import Device
 
 
 def create_hot_wallet_device(
diff --git a/tests/test_labels_import.py b/tests/test_labels_import.py
index 1a073b0a..2e7681d8 100644
--- a/tests/test_labels_import.py
+++ b/tests/test_labels_import.py
@@ -19,7 +19,7 @@ def test_import_address_labels(
     txid = utxos[0]["txid"]
     logger.debug(f"this is the txid: {txid}.")
     test_address = utxos[0]["address"]
-    logger.debug(f"these are the addresses: {wallet.addresses}.")
+    logger.debug(f"these are the relevant_addresses: {wallet.relevant_addresses}.")
     logger.debug(f"these are the _addresses: {wallet._addresses}.")
     assert wallet._addresses[test_address]["label"] is None
     number_of_addresses = len(wallet._addresses)
diff --git a/tests/test_managers_wallet.py b/tests/test_managers_wallet.py
index bbe33191..3a0f62de 100644
--- a/tests/test_managers_wallet.py
+++ b/tests/test_managers_wallet.py
@@ -329,7 +329,7 @@ def test_wallet_labeling(bitcoin_regtest, devices_filled_data_folder, device_man
     wallet.setlabel(third_address, "")
     bitcoin_regtest.testcoin_faucet(third_address, amount=0.4)
 
-    assert sorted(wallet.addresses) == sorted(
+    assert sorted(wallet.relevant_addresses) == sorted(
         [first_address, second_address, third_address]
     )
 
@@ -376,8 +376,8 @@ def test_wallet_change_addresses(
 
     address = wallet.address
     change_address = wallet.change_address
-    assert wallet.addresses == [address]
-    assert wallet.change_addresses == [change_address]
+    assert wallet.relevant_addresses == [address]
+    assert wallet.relevant_change_addresses == [change_address]
     bitcoin_regtest.testcoin_faucet(change_address, amount=0.1)
     wallet.update_balance()
     assert wallet.amount_total == 0.1
diff --git a/tests/test_wallet.py b/tests/test_wallet.py
index 09786444..8f7a4c28 100644
--- a/tests/test_wallet.py
+++ b/tests/test_wallet.py
@@ -5,7 +5,7 @@ from typing import List
 import pytest, logging
 from cryptoadvance.specter.util.psbt import SpecterPSBT
 from cryptoadvance.specter.commands.psbt_creator import PsbtCreator
-from cryptoadvance.specter.txlist import WalletAwareTxItem
+from cryptoadvance.specter.wallet.txlist import WalletAwareTxItem
 from cryptoadvance.specter.device import Device
 
 from cryptoadvance.specter.specter import Specter
@@ -53,8 +53,6 @@ def send_helper(specter_regtest_configured: Specter, wallet: Wallet, device: Dev
     signed_psbt = specter_regtest_configured.device_manager.get_by_alias(
         device.alias
     ).sign_psbt(b64psbt, wallet, "")
-    print()
-    print(f"signed_psbt: {signed_psbt}")
 
     if signed_psbt["complete"]:
         raw = wallet.rpc.finalizepsbt(signed_psbt["psbt"])
@@ -73,7 +71,7 @@ def test_txlist(
     (Always search for failures first before checking errors)
         so this should speed up the fixing process although it's a duplication.
     """
-    caplog.set_level(logging.ERROR)
+    caplog.set_level(logging.DEBUG)
     logging.getLogger("urllib3.connectionpool").setLevel(logging.INFO)
     wallet = create_hot_segwit_wallet(
         specter_regtest_configured,
@@ -129,6 +127,27 @@ def test_txlist(
     # two of them are unconfirmed
     assert len([tx for tx in wallet.txlist() if tx["confirmations"] == 0]) == 2
 
+    assert wallet.address_index == 15
+    assert wallet._addresses.max_used_index(change=False) == 14
+    # -1 is a magic value which is returned if no change addresses has been used
+    assert wallet._addresses.max_used_index(change=True) == -1
+
+    number_of_additional_txs = 200
+    for i in range(0, number_of_additional_txs):
+        bitcoin_regtest.testcoin_faucet(
+            wallet.getnewaddress(),
+            amount=2.5,
+            confirm_payment=False,
+        )
+    wallet.fetch_transactions()
+
+    assert wallet.address_index == 15 + number_of_additional_txs
+    assert (
+        wallet._addresses.max_used_index(change=False)
+        == 14 + number_of_additional_txs + 1
+    )
+    assert wallet._addresses.max_used_index(change=True) == -1
+
 
 @pytest.mark.slow
 def test_createpsbt(
diff --git a/tests/test_txlist.py b/tests/test_wallet_txlist.py
similarity index 99%
rename from tests/test_txlist.py
rename to tests/test_wallet_txlist.py
index 884b963c..cbf2b4fa 100644
--- a/tests/test_txlist.py
+++ b/tests/test_wallet_txlist.py
@@ -13,7 +13,7 @@ from cryptoadvance.specter.process_controller.bitcoind_controller import (
     BitcoindPlainController,
 )
 from cryptoadvance.specter.rpc import BitcoinRPC
-from cryptoadvance.specter.txlist import TxItem, TxList, WalletAwareTxItem
+from cryptoadvance.specter.wallet.txlist import TxItem, TxList, WalletAwareTxItem
 from cryptoadvance.specter.util.psbt import (
     SpecterInputScope,
     SpecterOutputScope,