Skip to content

Use regex for more robust multipath descriptor conversion during import

0a969d1
Select commit
Loading
Failed to load commit list.
Draft

Export wallets with BIP 389 multipath descriptors for both receive and change branches #2534

Use regex for more robust multipath descriptor conversion during import
0a969d1
Select commit
Loading
Failed to load commit list.
Cirrus CI / test failed Feb 5, 2026 in 5m 17s

Task Summary

Instruction test failed in 03:35

Details

✅ 00:09 clone
✅ 00:05 bitcoind_installation
✅ 00:02 elementsd_installation
✅ 00:01 verify
✅ 01:06 pip
✅ 00:14 install
❌ 03:35 test

src/cryptoadvance/specter/util/requests_tools.py                                               37     12    68%
src/cryptoadvance/specter/util/rpcauth.py                                                      29     18    38%
src/cryptoadvance/specter/util/setup_states.py                                                  1      0   100%
src/cryptoadvance/specter/util/sha256sum.py                                                     9      7    22%
src/cryptoadvance/specter/util/shell.py                                                        41     10    76%
src/cryptoadvance/specter/util/specter_migrator.py                                            155     14    91%
src/cryptoadvance/specter/util/tor.py                                                          50     33    34%
src/cryptoadvance/specter/util/tor_setup_tasks.py                                              42      9    79%
src/cryptoadvance/specter/util/tx.py                                                           52      5    90%
src/cryptoadvance/specter/util/version.py                                                     148     24    84%
src/cryptoadvance/specter/util/wallet_importer.py                                             225     66    71%
src/cryptoadvance/specter/util/xpub.py                                                          9      0   100%
src/cryptoadvance/specter/wallet/__init__.py                                                    3      0   100%
src/cryptoadvance/specter/wallet/abstract_wallet.py                                            15      3    80%
src/cryptoadvance/specter/wallet/addresslist.py                                               170     31    82%
src/cryptoadvance/specter/wallet/tx_fetcher.py                                                 90     15    83%
src/cryptoadvance/specter/wallet/txlist.py                                                    364     50    86%
src/cryptoadvance/specter/wallet/wallet.py                                                    930    308    67%
src/cryptoadvance/specterext/devhelp/__init__.py                                                0      0   100%
src/cryptoadvance/specterext/devhelp/config.py                                                  4      0   100%
src/cryptoadvance/specterext/devhelp/console.py                                                30     18    40%
src/cryptoadvance/specterext/devhelp/controller.py                                             62     52    16%
src/cryptoadvance/specterext/devhelp/devices/devhelpdevice.py                                   6      0   100%
src/cryptoadvance/specterext/devhelp/service.py                                                45     10    78%
src/cryptoadvance/specterext/electrum/__init__.py                                               0      0   100%
src/cryptoadvance/specterext/electrum/controller.py                                            19      3    84%
src/cryptoadvance/specterext/electrum/devices/__init__.py                                       0      0   100%
src/cryptoadvance/specterext/electrum/devices/electrum.py                                      16      3    81%
src/cryptoadvance/specterext/electrum/service.py                                               19      0   100%
src/cryptoadvance/specterext/notifications/__init__.py                                          0      0   100%
src/cryptoadvance/specterext/notifications/config.py                                            8      0   100%
src/cryptoadvance/specterext/notifications/controller.py                                       45     33    27%
src/cryptoadvance/specterext/notifications/notification_manager.py                            171     90    47%
src/cryptoadvance/specterext/notifications/notifications.py                                    72     12    83%
src/cryptoadvance/specterext/notifications/service.py                                          42     14    67%
src/cryptoadvance/specterext/notifications/ui_notifications.py                                103     46    55%
src/cryptoadvance/specterext/notifications/websockets_server_client.py                        217    180    17%
src/cryptoadvance/specterext/spectrum/__init__.py                                               0      0   100%
src/cryptoadvance/specterext/spectrum/bridge_rpc.py                                            62     19    69%
src/cryptoadvance/specterext/spectrum/config.py                                                38      0   100%
src/cryptoadvance/specterext/spectrum/controller.py                                           106     78    26%
src/cryptoadvance/specterext/spectrum/controller_helpers.py                                    39      6    85%
src/cryptoadvance/specterext/spectrum/service.py                                               99     41    59%
src/cryptoadvance/specterext/spectrum/spectrum_node.py                                        141     57    60%
src/cryptoadvance/specterext/swan/__init__.py                                                   0      0   100%
src/cryptoadvance/specterext/swan/client.py                                                   135     39    71%
src/cryptoadvance/specterext/swan/config.py                                                    11      0   100%
src/cryptoadvance/specterext/swan/controller.py                                               130     74    43%
src/cryptoadvance/specterext/swan/service.py                                                  193    100    48%
---------------------------------------------------------------------------------------------------------------
TOTAL                                                                                       20820  10038    52%

=========================== short test summary info ============================
FAILED tests/test_managers_wallet.py::test_multisig_wallet_backup_and_restore
FAILED tests/test_wallet.py::test_account_map_combined_descriptor - Assertion...
====== 2 failed, 224 passed, 6 skipped, 291 warnings in 213.08s (0:03:33) ======

Annotations

Check failure on line 628 in tests/test_managers_wallet.py

See this annotation in the file changed.

@cirrus-ci cirrus-ci / test

tests/test_managers_wallet.py#L628

tests.test_managers_wallet.test_multisig_wallet_backup_and_restore
Raw output
bitcoin_regtest = <cryptoadvance.specter.process_controller.bitcoind_controller.BitcoindPlainController object at 0x7f163e5a7010>
caplog = <_pytest.logging.LogCaptureFixture object at 0x7f15c4dcbd60>
specter_regtest_configured = <cryptoadvance.specter.specter.Specter object at 0x7f15c4d3beb0>
node = <Node name=Standard node fullpath=/tmp/specter_home_tmp_jqgz8tm_/nodes/standard_node.json>

    def test_multisig_wallet_backup_and_restore(
        bitcoin_regtest, caplog, specter_regtest_configured, node
    ):
        """
        Multisig wallets should be able to be backed up and re-imported
        with or without the "devices" attr in the json backup.
        """
        caplog.set_level(logging.INFO)
    
        device_manager = specter_regtest_configured.device_manager
        wallet_manager = specter_regtest_configured.wallet_manager
    
        device = device_manager.get_by_alias("trezor")
        device_type = device.device_type
    
        # Get the multisig 'wsh' testnet key
        for key in device.keys:
            if key.key_type == "wsh" and key.xpub.startswith("tpub"):
                break
    
        # Create a pair of hot wallet signers
        hot_wallet_1_device = device_manager.add_device(
            name="hot_key_1", device_type=BitcoinCore.device_type, keys=[]
        )
        hot_wallet_1_device.setup_device(file_password=None, wallet_manager=wallet_manager)
        hot_wallet_1_device.add_hot_wallet_keys(
            mnemonic=generate_mnemonic(strength=128),
            passphrase="",
            paths=["m/48h/1h/0h/2h"],
            file_password=None,
            wallet_manager=wallet_manager,
            testnet=True,
            keys_range=[0, 1000],
            keys_purposes=[],
        )
        hot_wallet_2_device = device_manager.add_device(
            name="hot_key_2", device_type=BitcoinCore.device_type, keys=[]
        )
        hot_wallet_2_device.setup_device(file_password=None, wallet_manager=wallet_manager)
        hot_wallet_2_device.add_hot_wallet_keys(
            mnemonic=generate_mnemonic(strength=128),
            passphrase="",
            paths=["m/48h/1h/0h/2h"],
            file_password=None,
            wallet_manager=wallet_manager,
            testnet=True,
            keys_range=[0, 1000],
            keys_purposes=[],
        )
    
        # create the multisig wallet
        wallet = wallet_manager.create_wallet(
            name="my_multisig_test_wallet",
            sigs_required=2,
            key_type=key.key_type,
            keys=[key, hot_wallet_1_device.keys[0], hot_wallet_2_device.keys[0]],
            devices=[device, hot_wallet_1_device, hot_wallet_2_device],
        )
    
        # Wallet is unfunded
        address = wallet.getnewaddress()
        bitcoin_regtest.testcoin_faucet(address, amount=3.3)
        wallet.update_balance()
        assert wallet.amount_total == 3.3
    
        # Save the json backup
        wallet_backup = json.loads(wallet.account_map.replace("\\\\", "").replace("'", "h"))
        assert "devices" in wallet_backup
    
        # Clear everything out as if we've never seen this wallet or device before
        wallet_manager.delete_wallet(wallet, node)
        device_manager.remove_device(device, wallet_manager=wallet_manager)
        assert wallet.name not in wallet_manager.wallets_names
        assert device.name not in device_manager.devices_names
    
        # Parse the backed up wallet (code adapted from the new_wallet endpoint)
        (
            wallet_name,
            recv_descriptor,
            cosigners_types,
        ) = WalletImporter.parse_wallet_data_import(wallet_backup)
    
        descriptor = Descriptor.parse(
            AddChecksum(recv_descriptor.split("#")[0]),
            testnet=is_testnet(specter_regtest_configured.chain),
        )
    
        (
            keys,
            cosigners,
            unknown_cosigners,
            unknown_cosigners_types,
>       ) = descriptor.parse_signers(device_manager.devices, cosigners_types)

tests/test_managers_wallet.py:628: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
src/cryptoadvance/specter/util/descriptor.py:468: in parse_signers
    desc_key = Key.parse_xpub(
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

cls = <class 'cryptoadvance.specter.key.Key'>, xpub = '1', purpose = ''

    @classmethod
    def parse_xpub(cls, xpub, purpose=""):
        derivation = ""
        arr = xpub.strip().split("]")
        original = arr[-1]
        if len(arr) > 1:
            derivation = arr[0].replace("'", "h").lower()
            xpub = arr[1]
    
        fingerprint = ""
        # just to be sure fgp/1h/2/3/ is also parsed correctly
        # because we have free-form inputs
        derivation = derivation.rstrip("/")
        if derivation != "":
            if derivation[0] != "[":
                raise Exception("Missing leading [")
            derivation_path = derivation[1:].split("/")
            try:
                fng = bytes.fromhex(
                    derivation_path[0].replace("-", "")
                )  # coldcard has hexstrings like 7c-2c-8e-1b
            except Exception:
                raise Exception("Fingerprint is not hex")
            if len(fng) != 4:
>               raise Exception("Incorrect fingerprint length")
E               Exception: Incorrect fingerprint length

src/cryptoadvance/specter/key.py:89: Exception

Check failure on line 394 in tests/test_wallet.py

See this annotation in the file changed.

@cirrus-ci cirrus-ci / test

tests/test_wallet.py#L394

tests.test_wallet.test_account_map_combined_descriptor
Raw output
funded_hot_wallet_1 = <Wallet name=a_hotwallet_948909 alias=a_hotwallet_948909>

    @pytest.mark.slow
    def test_account_map_combined_descriptor(funded_hot_wallet_1: Wallet):
        """
        Test that account_map property returns a combined descriptor with <0;1> syntax
        instead of just the receive descriptor with /0/*
    
        When exporting wallets, only the receive descriptor /0/* was included,
        but it should include both receive and change addresses using multipath descriptors.
        """
        wallet = funded_hot_wallet_1
    
        # Parse the account_map JSON
        account_map_dict = json.loads(wallet.account_map)
    
        # Check that descriptor key exists
        assert "descriptor" in account_map_dict
        descriptor_str = account_map_dict["descriptor"]
    
        # The descriptor should use BIP 389 multipath syntax <0;1>
        # not just the receive path /0/*
>       assert "<0;1>" in descriptor_str, (
            f"Expected descriptor with <0;1> multipath syntax, "
            f"but got: {descriptor_str}"
        )
E       AssertionError: Expected descriptor with <0;1> multipath syntax, but got: wpkh([915227ed/84h/1h/0h]tpubDCFZjEWPiCL57asbtmeSTMq2GW9GnBfK7f4MjKtVsRux3nC66uefXd3SPqaQYtRmPg6Aacqj2R2qQ1BwCtm6xmr9zqJSLnyhpZQgpfMCbgx/{0,1}/*)#yxak3f5f
E       assert '<0;1>' in 'wpkh([915227ed/84h/1h/0h]tpubDCFZjEWPiCL57asbtmeSTMq2GW9GnBfK7f4MjKtVsRux3nC66uefXd3SPqaQYtRmPg6Aacqj2R2qQ1BwCtm6xmr9zqJSLnyhpZQgpfMCbgx/{0,1}/*)#yxak3f5f'

tests/test_wallet.py:394: AssertionError