wallet: Add importdescriptors interface #34861

pull polespinasa wants to merge 3 commits into bitcoin:master from polespinasa:2026-03-19-importdescriptors changing 5 files +332 −135
  1. polespinasa commented at 9:24 am on March 19, 2026: member

    This PR adds an interface for importing descriptors.

    The motivation behind this is that currently, importing descriptors is only possible via RPC. Bitcoin Core GUI doesn’t use the RPC interface so it cannot offer descriptor import functionality, which is needed to support more complex wallet setups such as multisig.

    This PR also adds a refactor by moving the importdescriptors logic from the RPC layer into CWallet::ImportDescriptor, making it reusable by both the RPC and this new interface.

    The main changes are:

    • Introduces CWallet::ImportDescriptor() containing the core import logic, previously embedded in the RPC ProcessDescriptorImport function.
    • Introduces wallet::ImportDescriptorResult, a new result struct that carries success status, error message, warnings, and a FailureReason enum. The RPC layer uses FailureReason to map results back to the appropriate JSON-RPC error codes, keeping RPC concerns out of CWallet.
    • Updates ProcessDescriptorImport in rpc/backup.cpp to delegate to CWallet::ImportDescriptor.
    • Adds interfaces::Wallet::importDescriptors() as a new interface method, allowing the GUI to import descriptors without going through RPC.

    I have a vibe coded PoC commit for how the GUI could consume it here: https://github.com/polespinasa/bitcoin/commit/d1cb120a1106103234a91e693f6e2d3ba653f5eb It might be useful to cherry-pick the commit to test the interface.

  2. DrahtBot commented at 9:24 am on March 19, 2026: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK Sjors, sedited, stickies-v, rkrux
    Approach ACK w0xlt

    If your review is incorrectly listed, please copy-paste <!–meta-tag:bot-skip–> into the comment that the bot should ignore.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #34681 (wallet: refactor ScanForWalletTransactions by Eunovo)
    • #34400 (wallet: parallel fast rescan (approx 5x speed up with 16 threads) by Eunovo)
    • #33008 (wallet: support bip388 policy with external signer by Sjors)
    • #32861 (Have createwalletdescriptor auto-detect an unused(KEY) by Sjors)
    • #32784 (wallet: derivehdkey RPC to get xpub at arbitrary path by Sjors)
    • #32489 (wallet: Add exportwatchonlywallet RPC to export a watchonly version of a wallet by achow101)
    • #29136 (wallet: addhdkey RPC to add just keys to wallets via new unused(KEY) descriptor by achow101)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

  3. polespinasa renamed this:
    wallet, refactor, ipc: Add importdescriptors IPC interface
    wallet, ipc: Add importdescriptors IPC interface
    on Mar 19, 2026
  4. refactor: Add ImportDescriptor to CWallet
    Adds ImportDescriptor to CWallet. ImportDescriptor does the same as ProcessDescriptorImport from rpc/backup.cpp,
    but removes the UniValue dependency so it can be used for other interfaces apart from RPC.
    
    RPC importdescriptors is refactored in the next commit to use this new method.
    1571cdb005
  5. refactor: Make ProcessDescriptorImport use CWallet::ImportDescriptor b39d09b0c3
  6. polespinasa force-pushed on Mar 19, 2026
  7. DrahtBot added the label CI failed on Mar 19, 2026
  8. DrahtBot removed the label CI failed on Mar 19, 2026
  9. stickies-v commented at 1:57 pm on March 19, 2026: contributor

    currently, importing descriptors is only possible via RPC. This makes it impossible for the GUI to offer descriptor import functionality,

    Why can a GUI not use the RPC interface?

  10. polespinasa commented at 3:03 pm on March 19, 2026: member

    Why can a GUI not use the RPC interface?

    Sorry, a GUI can. I ment Bitcoin Core GUI needs it because it doesn’t use RPC. Will rephrase to make it more clear.

  11. Sjors commented at 3:41 pm on March 19, 2026: member

    Concept ACK

    We’ll eventually want a way to import descriptors in the GUI, and moving this code out of src/wallet/rpc is a sensible refactor in any case.

    Let’s update the PR description to make it clear this isn’t an IPC change. Until #10102 lands it’s just an interface change, used internally.

  12. sedited commented at 3:49 pm on March 19, 2026: contributor

    Concept ACK

    Makes sense to move this into the wallet interface, but this does indeed have nothing to do with ipc for now.

  13. polespinasa renamed this:
    wallet, ipc: Add importdescriptors IPC interface
    wallet, ipc: Add importdescriptors interface
    on Mar 19, 2026
  14. polespinasa commented at 4:56 pm on March 19, 2026: member
    Updated the title and description to avoid confusion
  15. polespinasa renamed this:
    wallet, ipc: Add importdescriptors interface
    wallet: Add importdescriptors interface
    on Mar 19, 2026
  16. DrahtBot added the label Wallet on Mar 19, 2026
  17. stickies-v commented at 9:52 am on March 20, 2026: contributor
    Thanks for the clarification, Concept ACK.
  18. w0xlt commented at 8:08 am on March 22, 2026: contributor
    Approach ACK
  19. wallet: Add importdescriptors interface b88462d758
  20. in src/wallet/interfaces.cpp:452 in 5e9dab43fd outdated
    447+                        r.error = "Rescan aborted by user.";
    448+                        r.reason = wallet::ImportDescriptorResult::FailureReason::MISC_ERROR;
    449+                    }
    450+                }
    451+                return response;
    452+            }
    


    rxbryan commented at 9:41 pm on March 22, 2026:
    • Adds interfaces::Wallet::importDescriptors() as a new interface method, allowing the GUI to import descriptors without going through RPC.

    Looking at importDescriptors() , it uses the same scaffolding as the RPC path in src/wallet/rpc/backup.cpp, but RescanFromTime is never called when rescan == true. The RPC calls RescanFromTime before ResubmitWalletTransactions, but the interface only calls ResubmitWalletTransactions. Would importing a descriptor with a historical timestamp via this interface scan for past transactions? The IsAbortingRescan() check also appears unreachable as written, no rescan was started.


    polespinasa commented at 10:03 pm on March 22, 2026:

    Yep you are right, good catch thanks! I tried to keep the same structure to make sure the behavior is as close as possible to the RPC one and for some reason I missed that function call.

    Force pushed to add it.


    rxbryan commented at 11:07 pm on March 22, 2026:
    Looks correct.
  21. polespinasa force-pushed on Mar 22, 2026
  22. in src/wallet/wallet.cpp:578 in 1571cdb005
    574@@ -575,6 +575,183 @@ void CWallet::UpgradeDescriptorCache()
    575     SetWalletFlag(WALLET_FLAG_LAST_HARDENED_XPUB_CACHED);
    576 }
    577 
    578+wallet::ImportDescriptorResult CWallet::ImportDescriptor(const std::string& descriptor,
    


    rkrux commented at 7:42 am on March 23, 2026:

    In 1571cdb00517a62769f635e077581c2095289646 “refactor: Add ImportDescriptor to CWallet”

    This is close to a 200 line method being added in a file that’s already more than 4500 lines long and hard enough to follow - I don’t believe we should add more lengthy code in this file anymore. An alternative can be to add this implementation in a separate file while being in the same wallet namespace.

    Something like below in a src/wallet/descriptor.cpp file, this is a starting point and not a full solution. PR #34681 does a simlar refactoring and can be referred to.

      0#include <wallet/wallet.h>
      1
      2namespace wallet {
      3  ImportDescriptorResult CWallet::ImportDescriptor(const std::string& descriptor,
      4    bool active,
      5    const std::optional<bool>& internal,
      6    const std::optional<std::string>& label,
      7    int64_t timestamp,
      8    const std::optional<std::pair<int64_t, int64_t>>& range,
      9    const std::optional<int64_t>& next_idx)
     10  {
     11    AssertLockHeld(cs_wallet);
     12
     13    ImportDescriptorResult result;
     14
     15    // Parse descriptor string
     16    FlatSigningProvider keys;
     17    std::string error;
     18    auto parsed_descs = Parse(descriptor, keys, error, /*require_checksum=*/true);
     19    if (parsed_descs.empty()) {
     20        result.error = error;
     21        result.reason = ImportDescriptorResult::FailureReason::INVALID_DESCRIPTOR;
     22        return result;
     23    }
     24    if (internal.has_value() && parsed_descs.size() > 1) {
     25        result.error = "Cannot have multipath descriptor while also specifying 'internal'";
     26        result.reason = ImportDescriptorResult::FailureReason::INVALID_DESCRIPTOR;
     27        return result;
     28    }
     29
     30    // Range check
     31    std::optional<bool> is_ranged;
     32    int64_t range_start = 0, range_end = 1, next_index = 0;
     33    if (!parsed_descs.at(0)->IsRange() && range.has_value()) {
     34        result.error = "Range should not be specified for an un-ranged descriptor";
     35        result.reason = ImportDescriptorResult::FailureReason::INVALID_PARAMETER;
     36        return result;
     37    } else if (parsed_descs.at(0)->IsRange()) {
     38        if (range.has_value()) {
     39            range_start = range->first;
     40            range_end   = range->second + 1;
     41        } else {
     42            result.warnings.emplace_back("Range not given, using default keypool range");
     43            result.used_default_range = true;
     44            range_start = 0;
     45            range_end = m_keypool_size;
     46        }
     47        next_index  = range_start;
     48        is_ranged = true;
     49
     50        if (next_idx.has_value()) {
     51            next_index = next_idx.value();
     52            if (next_index < range_start || next_index >= range_end) {
     53                result.error = "next_index is out of range";
     54                result.reason = ImportDescriptorResult::FailureReason::INVALID_PARAMETER;
     55                return result;
     56            }
     57        }
     58    }
     59
     60    // Active descriptors must be ranged
     61    if (active && !parsed_descs.at(0)->IsRange()) {
     62        result.error = "Active descriptors must be ranged";
     63        result.reason = ImportDescriptorResult::FailureReason::INVALID_PARAMETER;
     64        return result;
     65    }
     66
     67    // Multipath descriptors should not have a label
     68    if (parsed_descs.size() > 1 && label.has_value()) {
     69        result.error = "Multipath descriptors should not have a label";
     70        result.reason = ImportDescriptorResult::FailureReason::INVALID_PARAMETER;
     71        return result;
     72    }
     73
     74    // Ranged descrptors should not have a label
     75    if (is_ranged.has_value() && is_ranged.value() && label.has_value()) {
     76        result.error = "Ranged descriptors should not have a label";
     77        result.reason = ImportDescriptorResult::FailureReason::INVALID_PARAMETER;
     78        return result;
     79    }
     80
     81    // Internal descriptors should not have a label
     82    bool desc_internal = internal.value_or(false);
     83    if (desc_internal && label.has_value()) {
     84        result.error = "Internal addresses should not have a label";
     85        result.reason = ImportDescriptorResult::FailureReason::INVALID_PARAMETER;
     86        return result;
     87    }
     88
     89    //Combo descriptor check
     90    if (active && !parsed_descs.at(0)->IsSingleType()) {
     91        result.error = "Combo descriptors cannot be set to active";
     92        result.reason = ImportDescriptorResult::FailureReason::WALLET_ERROR;
     93        return result;
     94    }
     95
     96    // If the wallet disabled private keys, abort if private keys exist
     97    if (IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && !keys.keys.empty()) {
     98        result.error = "Cannot import private keys to a wallet with private keys disabled";
     99        result.reason = ImportDescriptorResult::FailureReason::WALLET_ERROR;
    100        return result;
    101    }
    102
    103    const std::string label_str = label.value_or(std::string{});
    104
    105    for (size_t j = 0; j < parsed_descs.size(); ++j) {
    106        auto parsed_desc = std::move(parsed_descs[j]);
    107        if (parsed_descs.size() == 2) {
    108            desc_internal = j == 1;
    109        } else if (parsed_descs.size() > 2) {
    110            CHECK_NONFATAL(!desc_internal);
    111        }
    112        // Need to ExpandPrivate to check if private keys are available for all pubkeys
    113        FlatSigningProvider expand_keys;
    114        std::vector<CScript> scripts;
    115        if (!parsed_desc->Expand(0, keys, scripts, expand_keys)) {
    116            result.error = "Cannot expand descriptor. Probably because of hardened derivations without private keys provided";
    117            result.reason = ImportDescriptorResult::FailureReason::WALLET_ERROR;
    118            return result;
    119        }
    120        parsed_desc->ExpandPrivate(0, keys, expand_keys);
    121
    122        for (const auto& w : parsed_desc->Warnings()) {
    123            result.warnings.push_back(w);
    124        }
    125
    126        // Check if all private keys are provided
    127        bool have_all_privkeys = !expand_keys.keys.empty();
    128        for (const auto& entry : expand_keys.origins) {
    129            const CKeyID& key_id = entry.first;
    130            CKey key;
    131            if (!expand_keys.GetKey(key_id, key)) {
    132                have_all_privkeys = false;
    133                break;
    134            }
    135        }
    136
    137        // If private keys are enabled, check some things.
    138        if (!IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
    139            if (keys.keys.empty()) {
    140                result.error = "Cannot import descriptor without private keys to a wallet with private keys enabled";
    141                result.reason = ImportDescriptorResult::FailureReason::WALLET_ERROR;
    142                return result;
    143            }
    144            if (!have_all_privkeys) {
    145                result.warnings.emplace_back("Not all private keys provided. Some wallet functionality may return unexpected errors");
    146            }
    147        }
    148
    149        WalletDescriptor w_desc(std::move(parsed_desc), timestamp, range_start, range_end, next_index);
    150
    151        // Add descriptor to wallet
    152        auto spk_manager_res = AddWalletDescriptor(w_desc, keys, label_str, desc_internal);
    153
    154        if (!spk_manager_res) {
    155            result.error = strprintf("Could not add descriptor '%s': %s", descriptor, util::ErrorString(spk_manager_res).original);
    156            result.reason = ImportDescriptorResult::FailureReason::WALLET_ERROR;
    157            return result;
    158        }
    159
    160        auto& spk_manager = spk_manager_res.value().get();
    161
    162        // Set descriptor as active if necessary
    163        if (active) {
    164            if (!w_desc.descriptor->GetOutputType()) {
    165                result.warnings.emplace_back("Unknown output type, cannot set descriptor to active.");
    166            } else {
    167                AddActiveScriptPubKeyMan(spk_manager.GetID(), *w_desc.descriptor->GetOutputType(), desc_internal);
    168            }
    169        } else {
    170            if (w_desc.descriptor->GetOutputType()) {
    171                DeactivateScriptPubKeyMan(spk_manager.GetID(), *w_desc.descriptor->GetOutputType(), desc_internal);
    172            }
    173        }
    174    }
    175
    176    result.success = true;
    177    return result;
    178  }
    179}
    
  23. rkrux changes_requested
  24. rkrux commented at 7:44 am on March 23, 2026: contributor

    Concept ACK b88462d758e2d24f278607dc5fbd9dcee6c1a076

    I have not reviewed the PR fully but only looked at one portion that was conspicuous in the diff.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-03-24 03:12 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me