wallet: Add importdescriptors interface #34861

pull polespinasa wants to merge 4 commits into bitcoin:master from polespinasa:2026-03-19-importdescriptors changing 6 files +444 −264
  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

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/34861.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process.

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

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #bitcoin-core/gui/872 (Menu action to export a watchonly wallet by achow101)
    • #35493 (wallet, descriptor: Fix MuSig private key completeness checks on importdescriptors by w0xlt)
    • #35436 (wallet: Add addHDkey interface by pseudoramdom)
    • #35377 (wallet: Allow importing of descriptors without private keys when the wallet has the private keys by achow101)
    • #33392 (wallet, rpc: add UTXO set check and incremental rescan to importdescriptors by musaHaruna)
    • #32489 (wallet: Add exportwatchonlywallet RPC to export a watchonly version of a wallet by achow101)
    • #31668 (Added rescan option for import descriptors by saikiran57)

    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.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  3. polespinasa renamed this:
    wallet, refactor, ipc: Add importdescriptors IPC interface
    wallet, ipc: Add importdescriptors IPC interface
    on Mar 19, 2026
  4. polespinasa force-pushed on Mar 19, 2026
  5. DrahtBot added the label CI failed on Mar 19, 2026
  6. DrahtBot removed the label CI failed on Mar 19, 2026
  7. 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?

  8. 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.

  9. 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.

  10. 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.

  11. polespinasa renamed this:
    wallet, ipc: Add importdescriptors IPC interface
    wallet, ipc: Add importdescriptors interface
    on Mar 19, 2026
  12. polespinasa commented at 4:56 PM on March 19, 2026: member

    Updated the title and description to avoid confusion

  13. polespinasa renamed this:
    wallet, ipc: Add importdescriptors interface
    wallet: Add importdescriptors interface
    on Mar 19, 2026
  14. DrahtBot added the label Wallet on Mar 19, 2026
  15. stickies-v commented at 9:52 AM on March 20, 2026: contributor

    Thanks for the clarification, Concept ACK.

  16. w0xlt commented at 8:08 AM on March 22, 2026: contributor

    Approach ACK

  17. 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.

  18. polespinasa force-pushed on Mar 22, 2026
  19. 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.

    <details open> <summary>Same method impementation but in a different fle</summary>

    #include <wallet/wallet.h>
    
    namespace wallet {
      ImportDescriptorResult CWallet::ImportDescriptor(const std::string& descriptor,
        bool active,
        const std::optional<bool>& internal,
        const std::optional<std::string>& label,
        int64_t timestamp,
        const std::optional<std::pair<int64_t, int64_t>>& range,
        const std::optional<int64_t>& next_idx)
      {
        AssertLockHeld(cs_wallet);
    
        ImportDescriptorResult result;
    
        // Parse descriptor string
        FlatSigningProvider keys;
        std::string error;
        auto parsed_descs = Parse(descriptor, keys, error, /*require_checksum=*/true);
        if (parsed_descs.empty()) {
            result.error = error;
            result.reason = ImportDescriptorResult::FailureReason::INVALID_DESCRIPTOR;
            return result;
        }
        if (internal.has_value() && parsed_descs.size() > 1) {
            result.error = "Cannot have multipath descriptor while also specifying 'internal'";
            result.reason = ImportDescriptorResult::FailureReason::INVALID_DESCRIPTOR;
            return result;
        }
    
        // Range check
        std::optional<bool> is_ranged;
        int64_t range_start = 0, range_end = 1, next_index = 0;
        if (!parsed_descs.at(0)->IsRange() && range.has_value()) {
            result.error = "Range should not be specified for an un-ranged descriptor";
            result.reason = ImportDescriptorResult::FailureReason::INVALID_PARAMETER;
            return result;
        } else if (parsed_descs.at(0)->IsRange()) {
            if (range.has_value()) {
                range_start = range->first;
                range_end   = range->second + 1;
            } else {
                result.warnings.emplace_back("Range not given, using default keypool range");
                result.used_default_range = true;
                range_start = 0;
                range_end = m_keypool_size;
            }
            next_index  = range_start;
            is_ranged = true;
    
            if (next_idx.has_value()) {
                next_index = next_idx.value();
                if (next_index < range_start || next_index >= range_end) {
                    result.error = "next_index is out of range";
                    result.reason = ImportDescriptorResult::FailureReason::INVALID_PARAMETER;
                    return result;
                }
            }
        }
    
        // Active descriptors must be ranged
        if (active && !parsed_descs.at(0)->IsRange()) {
            result.error = "Active descriptors must be ranged";
            result.reason = ImportDescriptorResult::FailureReason::INVALID_PARAMETER;
            return result;
        }
    
        // Multipath descriptors should not have a label
        if (parsed_descs.size() > 1 && label.has_value()) {
            result.error = "Multipath descriptors should not have a label";
            result.reason = ImportDescriptorResult::FailureReason::INVALID_PARAMETER;
            return result;
        }
    
        // Ranged descrptors should not have a label
        if (is_ranged.has_value() && is_ranged.value() && label.has_value()) {
            result.error = "Ranged descriptors should not have a label";
            result.reason = ImportDescriptorResult::FailureReason::INVALID_PARAMETER;
            return result;
        }
    
        // Internal descriptors should not have a label
        bool desc_internal = internal.value_or(false);
        if (desc_internal && label.has_value()) {
            result.error = "Internal addresses should not have a label";
            result.reason = ImportDescriptorResult::FailureReason::INVALID_PARAMETER;
            return result;
        }
    
        //Combo descriptor check
        if (active && !parsed_descs.at(0)->IsSingleType()) {
            result.error = "Combo descriptors cannot be set to active";
            result.reason = ImportDescriptorResult::FailureReason::WALLET_ERROR;
            return result;
        }
    
        // If the wallet disabled private keys, abort if private keys exist
        if (IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && !keys.keys.empty()) {
            result.error = "Cannot import private keys to a wallet with private keys disabled";
            result.reason = ImportDescriptorResult::FailureReason::WALLET_ERROR;
            return result;
        }
    
        const std::string label_str = label.value_or(std::string{});
    
        for (size_t j = 0; j < parsed_descs.size(); ++j) {
            auto parsed_desc = std::move(parsed_descs[j]);
            if (parsed_descs.size() == 2) {
                desc_internal = j == 1;
            } else if (parsed_descs.size() > 2) {
                CHECK_NONFATAL(!desc_internal);
            }
            // Need to ExpandPrivate to check if private keys are available for all pubkeys
            FlatSigningProvider expand_keys;
            std::vector<CScript> scripts;
            if (!parsed_desc->Expand(0, keys, scripts, expand_keys)) {
                result.error = "Cannot expand descriptor. Probably because of hardened derivations without private keys provided";
                result.reason = ImportDescriptorResult::FailureReason::WALLET_ERROR;
                return result;
            }
            parsed_desc->ExpandPrivate(0, keys, expand_keys);
    
            for (const auto& w : parsed_desc->Warnings()) {
                result.warnings.push_back(w);
            }
    
            // Check if all private keys are provided
            bool have_all_privkeys = !expand_keys.keys.empty();
            for (const auto& entry : expand_keys.origins) {
                const CKeyID& key_id = entry.first;
                CKey key;
                if (!expand_keys.GetKey(key_id, key)) {
                    have_all_privkeys = false;
                    break;
                }
            }
    
            // If private keys are enabled, check some things.
            if (!IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) {
                if (keys.keys.empty()) {
                    result.error = "Cannot import descriptor without private keys to a wallet with private keys enabled";
                    result.reason = ImportDescriptorResult::FailureReason::WALLET_ERROR;
                    return result;
                }
                if (!have_all_privkeys) {
                    result.warnings.emplace_back("Not all private keys provided. Some wallet functionality may return unexpected errors");
                }
            }
    
            WalletDescriptor w_desc(std::move(parsed_desc), timestamp, range_start, range_end, next_index);
    
            // Add descriptor to wallet
            auto spk_manager_res = AddWalletDescriptor(w_desc, keys, label_str, desc_internal);
    
            if (!spk_manager_res) {
                result.error = strprintf("Could not add descriptor '%s': %s", descriptor, util::ErrorString(spk_manager_res).original);
                result.reason = ImportDescriptorResult::FailureReason::WALLET_ERROR;
                return result;
            }
    
            auto& spk_manager = spk_manager_res.value().get();
    
            // Set descriptor as active if necessary
            if (active) {
                if (!w_desc.descriptor->GetOutputType()) {
                    result.warnings.emplace_back("Unknown output type, cannot set descriptor to active.");
                } else {
                    AddActiveScriptPubKeyMan(spk_manager.GetID(), *w_desc.descriptor->GetOutputType(), desc_internal);
                }
            } else {
                if (w_desc.descriptor->GetOutputType()) {
                    DeactivateScriptPubKeyMan(spk_manager.GetID(), *w_desc.descriptor->GetOutputType(), desc_internal);
                }
            }
        }
    
        result.success = true;
        return result;
      }
    }
    

    </details>


    rkrux commented at 1:56 PM on March 24, 2026:

    I've raised an extraction PR #34909 to support this point of mine. It's an attempt to reduce the size of this file.


    achow101 commented at 11:24 PM on March 26, 2026:

    This function does not need to be a member of CWallet. In the interest of not making wallet.cpp any bigger than it already is, I would suggest moving this to a new file, maybe imports.cpp and having it take a CWallet& as an argument.


    davidgumberg commented at 12:55 AM on March 27, 2026:

    One other alternative is to split the stateful part of the function that modifies / queries CWallet from the stateless error parsing stuff


    polespinasa commented at 4:41 PM on April 9, 2026:

    Done :)


    polespinasa commented at 5:08 PM on April 9, 2026:

    Done :)

  20. rkrux changes_requested
  21. 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.

  22. davidgumberg commented at 10:39 PM on March 26, 2026: contributor

    Concept ACK, this refactor is a nice win for maintainability, and makes adding importdescriptors to the GUI and/or to an IPC interface or any other interface trivial.

    I could be mistaken as I haven't tried yet to make this change myself, but I think that the RPC code should consume the interface you are adding, because it deduplicates code and increases confidence that the interface added in this PR (which in the current branch is untested) works correctly since we reuse all of our functional tests on it.

    If that's not feasible, the interface commit should probably be held off until the PR that introduces its consumer in the GUI.

  23. in src/wallet/interfaces.cpp:456 in b88462d758 outdated
     451 | +                }
     452 | +                return response;
     453 | +            }
     454 | +        }
     455 | +        return response;
     456 | +    }
    


    davidgumberg commented at 10:56 PM on March 26, 2026:

    Alot of this business logic should be moved out of the interface and into the wallet, the interface functions should probably be as thin as possible.


    rkrux commented at 10:12 AM on June 8, 2026:

    I assume this review comment is resolved now?

  24. achow101 commented at 11:22 PM on March 26, 2026: member

    but I think that the RPC code should consume the interface you are adding

    That is not how RPCs access the wallet, and making RPCs access the wallet through this interface is a pretty significant change that is massively outside of the scope of this PR. It would also mean that a bunch of things use the interface, while other things access CWallet directly, which is very weird.


    I think the layout of the commits could be changed to make this easier to review. In general, making a new function that is mostly a copy of another function that will end up replacing the other function makes review more difficult as it does not allow git to show reviewers where things are moved. It also runs the risk of things that are added to the original function not making it into the copy when the PR is rebased, although this is less likely to be an issue for this PR. Since the difference between the 2 functions is primarily in the data types, I would suggest having a first commit that separates ImportDescriptor from ProcessDescriptorImport so that the type changes can be reviewed in that commit. Then having a second commit that moves ImportDescriptor to its new location so that the move diff is minimal.

  25. in src/wallet/interfaces.cpp:388 in b88462d758 outdated
     384 | @@ -373,6 +385,75 @@ class WalletImpl : public Wallet
     385 |      {
     386 |          return m_wallet->FillPSBT(psbtx, complete, sighash_type, sign, bip32derivs, n_signed);
     387 |      }
     388 | +    std::vector<wallet::ImportDescriptorResult> importDescriptors(const std::vector<interfaces::ImportDescriptorRequest>& requests) override
    


    achow101 commented at 11:25 PM on March 26, 2026:

    I don't think most of the logic that this interface function is doing needs to be in the interface. It seems like it could probably be pushed into the underlying ImportDescriptors and the RPC refactored so that it can also use this logic rather than duplicating code.

  26. in src/wallet/wallet.cpp:597 in b88462d758
     592 | +    std::string error;
     593 | +    auto parsed_descs = Parse(descriptor, keys, error, /*require_checksum=*/true);
     594 | +    if (parsed_descs.empty()) {
     595 | +        result.error = error;
     596 | +        result.reason = ImportDescriptorResult::FailureReason::INVALID_DESCRIPTOR;
     597 | +        return result;
    


    achow101 commented at 11:28 PM on March 26, 2026:

    Instead of having errors take up 3 lines, this could be consolidated into a return result.Error() function in a similar way to how ValidationState works.


    polespinasa commented at 5:08 PM on April 9, 2026:

    Nice idea, done! :)

  27. in src/wallet/wallet.cpp:615 in 1571cdb005
     610 | +        result.reason = ImportDescriptorResult::FailureReason::INVALID_PARAMETER;
     611 | +        return result;
     612 | +    } else if (parsed_descs.at(0)->IsRange()) {
     613 | +        if (range.has_value()) {
     614 | +            range_start = range->first;
     615 | +            range_end   = range->second + 1;
    


    davidgumberg commented at 12:01 AM on March 27, 2026:

    dropped a comment:

                range_end = range->second + 1; // Specified range end is inclusive, but we need range end as exclusive
    

    polespinasa commented at 5:13 PM on April 9, 2026:

    Done

  28. in src/wallet/wallet.cpp:633 in 1571cdb005
     628 | +                result.error = "next_index is out of range";
     629 | +                result.reason = ImportDescriptorResult::FailureReason::INVALID_PARAMETER;
     630 | +                return result;
     631 | +            }
     632 | +        }
     633 | +    }
    


    davidgumberg commented at 12:18 AM on March 27, 2026:

    can be simplified (the arg also has to be renamed above)

            next_index = next_index_arg.value_or(range_start);
            is_ranged = true;
            if (next_index < range_start || next_index >= range_end) {
                result.error = "next_index is out of range";
                result.reason = ImportDescriptorResult::FailureReason::INVALID_PARAMETER;
                return result;
            }
    

    polespinasa commented at 5:19 PM on April 9, 2026:

    nice catch, done :)

  29. davidgumberg commented at 12:54 AM on March 27, 2026: contributor

    One approach to simplifying review here is with a prologue commit to make ProcessDescriptorImport and ImportDescriptor more similar, and squashing the commits where ImportDescriptor is added and where it ProcessDescriptorImport is simplified....something like: https://github.com/bitcoin/bitcoin/compare/master...davidgumberg:bitcoin:2026-03-26-34861-squashed-move (https://github.com/bitcoin/bitcoin/commit/e8f2604372df9bba9f5bf22bfcf7243ebb6546b4)

    This makes most lines more obviously moves if you do:

    git show 7eb1e638ae1e7510bdc0e895ca7cc44e20e5deea --color-moved=dimmed-zebra --color-moved-ws=ignore-all-space
    
  30. davidgumberg commented at 2:39 AM on March 27, 2026: contributor

    That is not how RPCs access the wallet, and making RPCs access the wallet through this interface is a pretty significant change that is massively outside of the scope of this PR.

    I don't think the change would be that significant, here is a rough draft as PoC (like the current pr branch some of the business logic of importDescriptors should probably be move out of the interface code)

    https://github.com/davidgumberg/bitcoin/commit/72b781d066cae3734ef30e4bbbbe66f20b500750 (https://github.com/bitcoin/bitcoin/compare/master...davidgumberg:bitcoin:2026-03-26-34861-rpc-use-interface)

    It seems like the best way to be reasonably confident that the importdescriptors of the wallet's RPC interface and the wallet's GUI interface are equivalent, and moves wallet business logic out of the RPC code.

  31. achow101 commented at 1:29 AM on March 28, 2026: member

    I don't think the change would be that significant

    It makes the RPC mix both of the public interfaces for CWallet, which I think is generally bad. Either all of the RPCs should use the CWallet, or they should all use interfaces::Wallet. Not mixing both, and definitely not mixing them within a single RPC. This constraint is less of a technical one and more on principle to not be mixing up how things are being accessed.

  32. in src/wallet/wallet.cpp:606 in b88462d758
     601 | +        result.reason = ImportDescriptorResult::FailureReason::INVALID_DESCRIPTOR;
     602 | +        return result;
     603 | +    }
     604 | +
     605 | +    // Range check
     606 | +    std::optional<bool> is_ranged;
    


    xyzconstant commented at 12:37 PM on April 9, 2026:

    In 1571cdb00517a62769f635e077581c2095289646 "refactor: Add ImportDescriptor to CWallet":

    is_ranged has only 2 possible values: true or nullopt (never false), so std::optional<bool> is a bit misleading.

    A plain bool is_ranged{false} would be clearer and simplifies the check at line 650 to if (is_ranged && label.has_value())


    polespinasa commented at 5:25 PM on April 9, 2026:

    You are right, done! This comes from before this PR. It was done this way in backup.cpp.

  33. in src/wallet/wallet.cpp:607 in b88462d758
     602 | +        return result;
     603 | +    }
     604 | +
     605 | +    // Range check
     606 | +    std::optional<bool> is_ranged;
     607 | +    int64_t range_start = 0, range_end = 1, next_index = 0;
    


    xyzconstant commented at 12:40 PM on April 9, 2026:

    nit: next_idx and next_index in the same scope can be confusing. Either renaming the parameter to requested_next_index or the local to resolved_next_index would make their meanings clearer.


    polespinasa commented at 5:30 PM on April 9, 2026:

    Done! Used next_index_arg I liked it more and saw it in @davidgumberg 's comment: #34861 (review)

  34. polespinasa force-pushed on Apr 9, 2026
  35. polespinasa force-pushed on Apr 9, 2026
  36. DrahtBot added the label CI failed on Apr 9, 2026
  37. polespinasa force-pushed on Apr 9, 2026
  38. polespinasa commented at 4:40 PM on April 9, 2026: member

    Force pushed to move the ImportDescriptor(...) function out of CWallet and into a new file imports.h/cpp as suggested by @achow101 @rkrux and @davidgumberg

    https://github.com/bitcoin/bitcoin/compare/b88462d758e2d24f278607dc5fbd9dcee6c1a076..f6671d9ac5cec296fe20b311369b5a134a52d0fa

  39. polespinasa force-pushed on Apr 9, 2026
  40. polespinasa force-pushed on Apr 9, 2026
  41. polespinasa force-pushed on Apr 9, 2026
  42. polespinasa force-pushed on Apr 9, 2026
  43. in src/interfaces/wallet.h:55 in 7bff033c56 outdated
      51 | @@ -51,6 +52,7 @@ struct WalletContext;
      52 |  namespace interfaces {
      53 |  
      54 |  class Handler;
      55 | +struct ImportDescriptorRequest;
    


    w0xlt commented at 6:16 PM on April 9, 2026:

    ImportDescriptorResult can be defined in the public interface. That makes interfaces::Wallet::importDescriptors() usable from interfaces/wallet.h.

    diff --git a/src/interfaces/wallet.h b/src/interfaces/wallet.h
    index eaf12e620a..83a3b5790b 100644
    --- a/src/interfaces/wallet.h
    +++ b/src/interfaces/wallet.h
    @@ -43,10 +43,32 @@ namespace wallet {
     struct CreatedTransactionResult;
     class CCoinControl;
     class CWallet;
    -struct ImportDescriptorResult;
     enum class AddressPurpose;
     struct CRecipient;
     struct WalletContext;
    +
    +//! Result of importing one descriptor.
    +struct ImportDescriptorResult {
    +    enum class FailureReason {
    +        NONE,
    +        INVALID_DESCRIPTOR,
    +        INVALID_PARAMETER,
    +        WALLET_ERROR,
    +        MISC_ERROR,
    +    };
    +    bool success{false};
    +    bool used_default_range{false};
    +    FailureReason reason{FailureReason::NONE};
    +    std::string error;
    +    std::vector<std::string> warnings;
    +
    +    ImportDescriptorResult& Error(FailureReason r, std::string e)
    +    {
    +        reason = r;
    +        error = std::move(e);
    +        return *this;
    +    }
    +};
     } // namespace wallet
     
     namespace interfaces {
    diff --git a/src/wallet/imports.h b/src/wallet/imports.h
    index fb2e296d5c..7f65afdc6f 100644
    --- a/src/wallet/imports.h
    +++ b/src/wallet/imports.h
    @@ -5,31 +5,10 @@
     #ifndef BITCOIN_WALLET_IMPORTS_H
     #define BITCOIN_WALLET_IMPORTS_H
     
    +#include <interfaces/wallet.h>
     #include <wallet/wallet.h>
     
     namespace wallet{
    -
    -    struct ImportDescriptorResult {
    -        enum class FailureReason {
    -            NONE,
    -            INVALID_DESCRIPTOR,
    -            INVALID_PARAMETER,
    -            WALLET_ERROR,
    -            MISC_ERROR
    -        };
    -        bool success{false};
    -        bool used_default_range{false};
    -        FailureReason reason{FailureReason::NONE};
    -        std::string error;
    -        std::vector<std::string> warnings;
    -
    -        ImportDescriptorResult& Error(FailureReason r, std::string e) {
    -            reason = r;
    -            error = std::move(e);
    -            return *this;
    -        }
    -    };
    -
         ImportDescriptorResult ImportDescriptor(CWallet& wallet, const std::string& descriptor,
             bool active,
             const std::optional<bool>& internal,
    

    polespinasa commented at 6:23 PM on April 9, 2026:

    But then I would need to be accessed by the RPC code. Which I don't have a strong opinion about if that's good or not, but @achow101 and @davidgumberg have been discussing it in this PR (see prior comments).

    In any case I think that rebase is out of the scope for this PR.

  44. DrahtBot removed the label CI failed on Apr 9, 2026
  45. in src/wallet/interfaces.cpp:418 in 7bff033c56 outdated
     413 | +        LOCK(m_wallet->m_relock_mutex);
     414 | +
     415 | +        const int64_t minimum_timestamp = 1;
     416 | +        int64_t now = 0;
     417 | +        int64_t lowest_timestamp = 0;
     418 | +        bool rescan = false;
    


    w0xlt commented at 6:35 PM on April 9, 2026:

    This patch below makes interfaces::Wallet::importDescriptors() consistent with the RPC importdescriptors behavior for locked-wallet imports and incomplete rescans.

    diff --git a/src/wallet/interfaces.cpp b/src/wallet/interfaces.cpp
    index 447a098a95..f468f107b3 100644
    --- a/src/wallet/interfaces.cpp
    +++ b/src/wallet/interfaces.cpp
    @@ -4,6 +4,7 @@
     
     #include <interfaces/wallet.h>
     
    +#include <chain.h>
     #include <common/args.h>
     #include <consensus/amount.h>
     #include <interfaces/chain.h>
    @@ -419,6 +420,17 @@ public:
             {
                 LOCK(m_wallet->cs_wallet);
     
    +            if (m_wallet->IsLocked()) {
    +                wallet::ImportDescriptorResult result;
    +                result.success = false;
    +                result.error = "Error: Please enter the wallet passphrase with walletpassphrase first.";
    +                result.reason = wallet::ImportDescriptorResult::FailureReason::WALLET_ERROR;
    +                for (size_t i = 0; i < requests.size(); ++i) {
    +                    response.push_back(result);
    +                }
    +                return response;
    +            }
    +
                 CHECK_NONFATAL(m_wallet->chain().findBlock(m_wallet->GetLastBlockHash(), FoundBlock().time(lowest_timestamp).mtpTime(now)));
     
                 for (const interfaces::ImportDescriptorRequest& request : requests) {
    @@ -439,7 +451,7 @@ public:
             }
     
             if (rescan) {
    -            m_wallet->RescanFromTime(lowest_timestamp, reserver, /*update=*/true);
    +            const int64_t scanned_time = m_wallet->RescanFromTime(lowest_timestamp, reserver, /*update=*/true);
                 m_wallet->ResubmitWalletTransactions(node::TxBroadcast::MEMPOOL_NO_BROADCAST, /*force=*/true);
     
                 if (m_wallet->IsAbortingRescan()) {
    @@ -453,6 +465,33 @@ public:
                     }
                     return response;
                 }
    +
    +            if (scanned_time > lowest_timestamp) {
    +                for (size_t i = 0; i < requests.size(); ++i) {
    +                    wallet::ImportDescriptorResult& result = response.at(i);
    +                    if (scanned_time <= requests.at(i).timestamp || !result.success) continue;
    +
    +                    result.success = false;
    +                    result.reason = wallet::ImportDescriptorResult::FailureReason::MISC_ERROR;
    +                    result.error = strprintf("Rescan failed for descriptor with timestamp %d. There "
    +                            "was an error reading a block from time %d, which is after or within %d seconds "
    +                            "of key creation, and could contain transactions pertaining to the descriptor. As a "
    +                            "result, transactions and coins using this descriptor may not appear in the wallet.",
    +                            requests.at(i).timestamp, scanned_time - TIMESTAMP_WINDOW - 1, TIMESTAMP_WINDOW);
    +                    if (m_wallet->chain().havePruned()) {
    +                        result.error += " This error could be caused by pruning or data corruption "
    +                                "(see bitcoind log for details) and could be dealt with by downloading and "
    +                                "rescanning the relevant blocks (see -reindex option and rescanblockchain RPC).";
    +                    } else if (m_wallet->chain().hasAssumedValidChain()) {
    +                        result.error += " This error is likely caused by an in-progress assumeutxo "
    +                                "background sync. Check logs or getchainstates RPC for assumeutxo background "
    +                                "sync progress and try again later.";
    +                    } else {
    +                        result.error += " This error could potentially caused by data corruption. If "
    +                                "the issue persists you may want to reindex (see -reindex option).";
    +                    }
    +                }
    +            }
             }
             return response;
         }
    

    polespinasa commented at 8:57 PM on April 9, 2026:

    You are right, I forgot to include that last part. :)

    I have modified it a bit to make it consistent with the current PR implementation.

  46. polespinasa force-pushed on Apr 9, 2026
  47. polespinasa commented at 9:04 PM on April 9, 2026: member

    Made interfaces::Wallet::importDescriptors() consistent with the RPC importdescriptors behavior for locked-wallet imports and incomplete rescans as suggested in #34861 (review)

    https://github.com/bitcoin/bitcoin/compare/7bff033c566e8293dc8d22ac05402e9cd3fa3a6a..3be7a9a06d5608719ccb3f523cc9ac63b13a1466

  48. polespinasa force-pushed on Apr 10, 2026
  49. polespinasa commented at 1:04 PM on April 10, 2026: member

    @achow101 @davidgumberg as suggested I have extracted all the functionality from the wallet interface and moved it into imports.

    It included huge changes in the RPC code in order to use that code too. Changes can be seen in the diff.

    https://github.com/bitcoin/bitcoin/compare/3be7a9a06d5608719ccb3f523cc9ac63b13a1466..2672bd569a34d3abb386912465e4ad4e47d36661

    I will try to work on a new commit structure soon to make it easier to review..

  50. polespinasa force-pushed on Apr 14, 2026
  51. polespinasa force-pushed on Apr 14, 2026
  52. DrahtBot added the label CI failed on Apr 14, 2026
  53. polespinasa commented at 10:37 PM on April 14, 2026: member

    Force pushed to improve the commit order to make reviewing it easier :)

  54. in src/wallet/imports.cpp:70 in eec8483ae9 outdated
      65 | +        if (parsed_descs.size() > 1 && label.has_value()) {
      66 | +            return result.Error(ImportDescriptorResult::FailureReason::INVALID_PARAMETER,
      67 | +                "Multipath descriptors should not have a label");
      68 | +        }
      69 | +
      70 | +        // Ranged descrptors should not have a label
    


    DrahtBot commented at 9:29 AM on April 15, 2026:

    Possible typos and grammar issues:

    Range descrptors should not have a label -> Range descriptors should not have a label [typo: “descrptors” missing an “i”, which harms comprehension]
    as we didnt import the descriptor in the request. -> as we didn't import the descriptor in the request. [typo/grammar: “dident” should be “didn’t”; missing apostrophe makes the sentence harder to read]

    2026-04-14 22:34:36

  55. polespinasa force-pushed on Apr 15, 2026
  56. polespinasa force-pushed on Apr 15, 2026
  57. polespinasa commented at 1:10 PM on April 15, 2026: member

    Force pushed to fix a merge conflict with master

  58. DrahtBot removed the label CI failed on Apr 15, 2026
  59. xyzconstant commented at 3:02 PM on April 27, 2026: contributor

    ACK e2ed4a6929

  60. DrahtBot requested review from Sjors on Apr 27, 2026
  61. DrahtBot requested review from stickies-v on Apr 27, 2026
  62. DrahtBot requested review from davidgumberg on Apr 27, 2026
  63. DrahtBot requested review from rkrux on Apr 27, 2026
  64. DrahtBot requested review from sedited on Apr 27, 2026
  65. DrahtBot requested review from w0xlt on Apr 27, 2026
  66. in src/wallet/rpc/backup.cpp:340 in 03f892e6b3
     440 | +            "Wallet is currently rescanning. Abort existing rescan or wait.");
     441 | +        // Fill response with one error for each request
     442 | +        for (size_t i = 0; i < requests.size(); ++i) {
     443 | +            response.push_back(result);
     444 | +        }
     445 | +        return response;
    


    achow101 commented at 11:07 PM on April 28, 2026:

    In 03f892e6b34f7702e4974713317d1619bfee89a1 "wallet: refactor importdescriptors RPC"

    This changes the error that the user gets if there is an existing rescan. Instead of a single RPC failure error, they get an RPC success with the error existing inside the result object which may be surprising/unexpected. Ideally we would preserve the existing behavior for these errors that are outside of the imports.


    polespinasa commented at 8:04 AM on April 29, 2026:

    fixed

  67. in src/wallet/rpc/backup.cpp:362 in 03f892e6b3
     466 | +                "Error: Please enter the wallet passphrase with walletpassphrase first.");
     467 | +            // Fill response with one error for each request
     468 | +            for (size_t i = 0; i < requests.size(); ++i) {
     469 | +                response.push_back(result);
     470 |              }
     471 | +            return response;
    


    achow101 commented at 11:07 PM on April 28, 2026:

    In 03f892e6b34f7702e4974713317d1619bfee89a1 "wallet: refactor importdescriptors RPC"

    Same comment as the rescan error about preserving errors.


    polespinasa commented at 8:04 AM on April 29, 2026:

    fixed

  68. in src/wallet/imports.h:10 in 46bb20e1b5
       5 | +#ifndef BITCOIN_WALLET_IMPORTS_H
       6 | +#define BITCOIN_WALLET_IMPORTS_H
       7 | +
       8 | +#include <wallet/wallet.h>
       9 | +
      10 | +namespace wallet{
    


    achow101 commented at 11:12 PM on April 28, 2026:

    In 46bb20e1b524acdf4277834f549b02f2d7c5f400 "wallet: Add ImportDescriptorRequest and ImportDescriptorResult structs"

    nit: space before {

    namespace wallet {
    

    Same in the .cpp file.


    polespinasa commented at 7:25 AM on April 29, 2026:

    done

  69. in src/wallet/imports.h:12 in 46bb20e1b5
       7 | +
       8 | +#include <wallet/wallet.h>
       9 | +
      10 | +namespace wallet{
      11 | +
      12 | +    struct ImportDescriptorResult {
    


    achow101 commented at 11:12 PM on April 28, 2026:

    In 46bb20e1b524acdf4277834f549b02f2d7c5f400 "wallet: Add ImportDescriptorRequest and ImportDescriptorResult structs"

    nit: Things within a namespace do not need to be tabbed in one level.

    Same in the .cpp file.


    polespinasa commented at 7:25 AM on April 29, 2026:

    done

  70. polespinasa force-pushed on Apr 29, 2026
  71. polespinasa commented at 8:08 AM on April 29, 2026: member

    Note: This comment is hidden because the solution was wrong.

    ~Added a new global_error optional attribute to ImportDescriptorResult this is different from error as it's a global error in the import descriptor process and not a per-descriptor error. This helps maintaining the old behavior in the RPC call and at the same time is cleaner for the future interfaces as it doesn't make sense to report that error on each descriptor when it affects all of them.~

    ~Also addressed some small nits.~

    $ git diff e2ed4a69291bf3b2ae6f1279bbc45074738b53ea..021236429c8fbb08cfbd619018c4780c884e2e2c
    diff --git a/src/wallet/imports.cpp b/src/wallet/imports.cpp
    index 92213e034b..1e09fa6b5d 100644
    --- a/src/wallet/imports.cpp
    +++ b/src/wallet/imports.cpp
    @@ -178,12 +178,8 @@ namespace wallet{
             WalletRescanReserver reserver(wallet);
             if (!reserver.reserve(/*with_passphrase=*/true)) {
                 wallet::ImportDescriptorResult result;
    -            result.Error(wallet::ImportDescriptorResult::FailureReason::WALLET_ERROR,
    -                "Wallet is currently rescanning. Abort existing rescan or wait.");
    -            // Fill response with one error for each request
    -            for (size_t i = 0; i < requests.size(); ++i) {
    -                response.push_back(result);
    -            }
    +            result.global_error = "Wallet is currently rescanning. Abort existing rescan or wait.";
    +            response.push_back(result);
                 return response;
             }
     
    @@ -200,12 +196,8 @@ namespace wallet{
     
                 if (wallet.IsLocked()) {
                     wallet::ImportDescriptorResult result;
    -                result.Error(wallet::ImportDescriptorResult::FailureReason::WALLET_ERROR,
    -                    "Error: Please enter the wallet passphrase with walletpassphrase first.");
    -                // Fill response with one error for each request
    -                for (size_t i = 0; i < requests.size(); ++i) {
    -                    response.push_back(result);
    -                }
    +                result.global_error = "Error: Please enter the wallet passphrase with walletpassphrase first.";
    +                response.push_back(result);
                     return response;
                 }
    
    diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp
    index 7144d9c604..8a9ca5d56a 100644
    --- a/src/wallet/rpc/backup.cpp
    +++ b/src/wallet/rpc/backup.cpp
    @@ -240,6 +240,12 @@ RPCMethod importdescriptors()
             import_results = wallet::ProcessDescriptorsImport(wallet, requests);
         }
     
    +    // Wallet-wide precondition failure (e.g. already rescanning, or locked):
    +    // surface as a top-level RPC error, matching the pre-refactor behaviour.
    +    if (!import_results.empty() && import_results[0].global_error.has_value()) {
    +        throw JSONRPCError(RPC_WALLET_ERROR, import_results[0].global_error.value());
    +    }
    +
         size_t result_idx = 0;
         for (size_t i = 0; i < univalue_requests.size(); ++i)
         {
    
    
  72. polespinasa commented at 11:57 AM on April 29, 2026: member

    @achow101 as the functional tests didn't detect that those error where silently being broken I added test coverage here #35179

    Would be nice to have review there and rebase this PR on top of it to make sure it's not being broken in other places.

  73. polespinasa force-pushed on Apr 29, 2026
  74. polespinasa commented at 2:49 PM on April 29, 2026: member

    Added a new global_error optional attribute to ImportDescriptorResult this is different from error as it's a global error in the import descriptor process and not a per-descriptor error. This helps maintaining the old behavior in the RPC call and at the same time is cleaner for the future interfaces as it doesn't make sense to report that error on each descriptor when it affects all of them.

    Also addressed some small nits.

    <details> <summary>git diff</summary>

    $ git diff e2ed4a69291bf3b2ae6f1279bbc45074738b53ea..02c061737cadf6b02f0ed53ed8053dcbf30f6b8b
    diff --git a/src/wallet/imports.cpp b/src/wallet/imports.cpp
    index 92213e034b..1dff620298 100644
    --- a/src/wallet/imports.cpp
    +++ b/src/wallet/imports.cpp
    @@ -177,13 +177,11 @@ namespace wallet{
     
             WalletRescanReserver reserver(wallet);
             if (!reserver.reserve(/*with_passphrase=*/true)) {
    -            wallet::ImportDescriptorResult result;
    -            result.Error(wallet::ImportDescriptorResult::FailureReason::WALLET_ERROR,
    -                "Wallet is currently rescanning. Abort existing rescan or wait.");
    -            // Fill response with one error for each request
    -            for (size_t i = 0; i < requests.size(); ++i) {
    -                response.push_back(result);
    -            }
    +            ImportDescriptorResult result;
    +            result.Error(ImportDescriptorResult::FailureReason::WALLET_ERROR,
    +                "Wallet is currently rescanning. Abort existing rescan or wait.",
    +                /*g_e*/true);
    +            response.push_back(result);
                 return response;
             }
     
    @@ -199,13 +197,12 @@ namespace wallet{
                 LOCK(wallet.cs_wallet);
     
                 if (wallet.IsLocked()) {
    -                wallet::ImportDescriptorResult result;
    -                result.Error(wallet::ImportDescriptorResult::FailureReason::WALLET_ERROR,
    -                    "Error: Please enter the wallet passphrase with walletpassphrase first.");
    -                // Fill response with one error for each request
    -                for (size_t i = 0; i < requests.size(); ++i) {
    -                    response.push_back(result);
    -                }
    +                ImportDescriptorResult result;
    +                result.Error(
    +                    ImportDescriptorResult::FailureReason::WALLET_UNLOCK_NEEDED,
    +                    "Error: Please enter the wallet passphrase with walletpassphrase first.",
    +                    /*g_e=*/true);
    +                response.push_back(result);
                     return response;
                 }
     
    diff --git a/src/wallet/imports.h b/src/wallet/imports.h
    index 662a33e8aa..b47072e1a6 100644
    --- a/src/wallet/imports.h
    +++ b/src/wallet/imports.h
    @@ -7,51 +7,58 @@
     
     #include <wallet/wallet.h>
     
    -namespace wallet{
    -
    -    struct ImportDescriptorResult {
    -        enum class FailureReason {
    -            NONE,
    -            INVALID_DESCRIPTOR,
    -            INVALID_PARAMETER,
    -            WALLET_ERROR,
    -            MISC_ERROR
    -        };
    -        bool success{false};
    -        bool used_default_range{false};
    -        FailureReason reason{FailureReason::NONE};
    -        std::string error;
    -        std::vector<std::string> warnings;
    -
    -        ImportDescriptorResult& Error(FailureReason r, std::string e) {
    -            reason = r;
    -            error = std::move(e);
    -            return *this;
    -        }
    -    };
    +namespace wallet {
     
    -    //! Information about a descriptor to be imported.
    -    struct ImportDescriptorRequest {
    -        std::string descriptor;
    -        std::optional<std::string> label;
    -        std::optional<int64_t> timestamp;
    -        std::optional<bool> active;
    -        std::optional<bool> internal;
    -        std::optional<std::pair<int64_t,int64_t>> range;
    -        std::optional<int64_t> next_index;
    +struct ImportDescriptorResult {
    +    enum class FailureReason {
    +        NONE,
    +        INVALID_DESCRIPTOR,
    +        INVALID_PARAMETER,
    +        WALLET_ERROR,
    +        WALLET_UNLOCK_NEEDED,
    +        MISC_ERROR
         };
     
    -    ImportDescriptorResult ImportDescriptor(CWallet& wallet,
    -        const std::string& descriptor,
    -        bool active,
    -        const std::optional<bool>& internal,
    -        const std::optional<std::string>& label,
    -        int64_t timestamp,
    -        const std::optional<std::pair<int64_t, int64_t>>& range,
    -        const std::optional<int64_t>& next_idx) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
    -
    -    std::vector<ImportDescriptorResult> ProcessDescriptorsImport(CWallet& wallet,
    -        std::vector<ImportDescriptorRequest> requests);
    +    bool success{false};
    +    bool used_default_range{false};
    +    FailureReason reason{FailureReason::NONE};
    +    std::string error;
    +    std::vector<std::string> warnings;
    +    //! Set to true when a wallet-wide precondition failed before any descriptor was
    +    //! processed (e.g. wallet is already rescanning, or wallet is locked).
    +    //! Callers that support top-level errors should surface this as a
    +    //! top-level / call-wide error rather than a per-descriptor failure.
    +    bool global_error;
    +    ImportDescriptorResult& Error(FailureReason r, std::string e, bool g_e = false) {
    +        reason = r;
    +        error = std::move(e);
    +        global_error = g_e;
    +        return *this;
    +    }
    +};
    +//! Information about a descriptor to be imported.
    +struct ImportDescriptorRequest {
    +    std::string descriptor;
    +    std::optional<std::string> label;
    +    std::optional<int64_t> timestamp;
    +    std::optional<bool> active;
    +    std::optional<bool> internal;
    +    std::optional<std::pair<int64_t,int64_t>> range;
    +    std::optional<int64_t> next_index;
    +};
    +
    +ImportDescriptorResult ImportDescriptor(CWallet& wallet,
    +    const std::string& descriptor,
    +    bool active,
    +    const std::optional<bool>& internal,
    +    const std::optional<std::string>& label,
    +    int64_t timestamp,
    +    const std::optional<std::pair<int64_t, int64_t>>& range,
    +    const std::optional<int64_t>& next_idx) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet);
    +
    +std::vector<ImportDescriptorResult> ProcessDescriptorsImport(CWallet& wallet,
    +    std::vector<ImportDescriptorRequest> requests);
    +
     }
     
     #endif // BITCOIN_WALLET_IMPORTS_H
    diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp
    index 7144d9c604..af8e45865c 100644
    --- a/src/wallet/rpc/backup.cpp
    +++ b/src/wallet/rpc/backup.cpp
    @@ -240,6 +240,15 @@ RPCMethod importdescriptors()
             import_results = wallet::ProcessDescriptorsImport(wallet, requests);
         }
     
    +    // Wallet-wide precondition failure (e.g. already rescanning, or locked):
    +    // surface as a top-level RPC error, matching the pre-refactor behaviour.
    +    if (!import_results.empty() && import_results[0].global_error) {
    +        RPCErrorCode rpc_error_code = (import_results[0].reason == ImportDescriptorResult::FailureReason::WALLET_UNLOCK_NEEDED)
    +         ? RPC_WALLET_UNLOCK_NEEDED
    +         : RPC_WALLET_ERROR;
    +        throw JSONRPCError(rpc_error_code, import_results[0].error);
    +    }
    +
         size_t result_idx = 0;
         for (size_t i = 0; i < univalue_requests.size(); ++i)
         {
    
    

    </details>

  75. polespinasa force-pushed on Apr 29, 2026
  76. DrahtBot added the label CI failed on Apr 29, 2026
  77. in src/wallet/imports.h:31 in 635b00bc76
      26 | +    std::vector<std::string> warnings;
      27 | +    //! Set to true when a wallet-wide precondition failed before any descriptor was
      28 | +    //! processed (e.g. wallet is already rescanning, or wallet is locked).
      29 | +    //! Callers that support top-level errors should surface this as a
      30 | +    //! top-level / call-wide error rather than a per-descriptor failure.
      31 | +    bool global_error;
    


    w0xlt commented at 6:01 PM on April 29, 2026:
        bool global_error{false};
    

    This will fix the CI error: runtime error: load of value 170, which is not a valid value for type 'bool' SUMMARY: UndefinedBehaviorSanitizer: invalid-bool-load.

    ImportDescriptorResult result; default-initializes the object. Members with default member initializers are initialized, but bool global_error; has none, so it is default-initialized as a scalar, which performs no initialization. Reading it is undefined behavior.


    polespinasa commented at 6:07 PM on April 29, 2026:

    Done, thanks!

  78. polespinasa force-pushed on Apr 29, 2026
  79. in src/wallet/imports.cpp:260 in 525c5592bc
     255 | +
     256 | +                    // If the descriptor timestamp is within the successfully scanned
     257 | +                    // range, or if the import result already has an error set, let
     258 | +                    // the result stand unmodified. Otherwise replace the result
     259 | +                    // with an error message.
     260 | +                    if (scanned_time <= requests.at(i).timestamp.value() || !result.success) continue;
    


    w0xlt commented at 6:54 PM on April 29, 2026:

    In this PR, "timestamp": "now" is represented as std::nullopt in ImportDescriptorRequest::timestamp (master resolves "now" immediately to an int64_t). So this branch can’t call requests.at(i).timestamp.value(), because there is no value for "now".

    The code needs to use the resolved timestamp computed earlier from std::max(request.timestamp.value_or(now), minimum_timestamp).

    Suggested patch (with a new test to cover this case):

    <details> <summary>diff</summary>

    diff --git a/src/wallet/imports.cpp b/src/wallet/imports.cpp
    index 5f9c4ca99b..4a268b3f3d 100644
    --- a/src/wallet/imports.cpp
    +++ b/src/wallet/imports.cpp
    @@ -170,6 +170,8 @@ namespace wallet{
             std::vector<wallet::ImportDescriptorRequest> requests)
         {
             std::vector<wallet::ImportDescriptorResult> response;
    +        std::vector<int64_t> resolved_timestamps;
    +        resolved_timestamps.reserve(requests.size());
     
             // Make sure the results are valid at least up to the most recent block
             // the user could have gotten from another RPC command prior to now
    @@ -210,6 +212,7 @@ namespace wallet{
     
                 for (wallet::ImportDescriptorRequest& request : requests) {
                     const int64_t timestamp = std::max(request.timestamp.value_or(now), minimum_timestamp);
    +                resolved_timestamps.push_back(timestamp);
                     wallet::ImportDescriptorResult result = wallet::ImportDescriptor(
                         wallet,
                         request.descriptor,
    @@ -257,14 +260,16 @@ namespace wallet{
                         // range, or if the import result already has an error set, let
                         // the result stand unmodified. Otherwise replace the result
                         // with an error message.
    -                    if (scanned_time <= requests.at(i).timestamp.value() || !result.success) continue;
    +                    const int64_t timestamp{resolved_timestamps.at(i)};
    +                    if (scanned_time <= timestamp || !result.success) continue;
    +                    const int64_t error_timestamp{requests.at(i).timestamp.value_or(timestamp)};
     
                         result.success = false;
                         std::string error_msg = strprintf("Rescan failed for descriptor with timestamp %d. There "
                             "was an error reading a block from time %d, which is after or within %d seconds "
                             "of key creation, and could contain transactions pertaining to the desc. As a "
                             "result, transactions and coins using this desc may not appear in the wallet.",
    -                        requests.at(i).timestamp.value(), scanned_time - TIMESTAMP_WINDOW - 1, TIMESTAMP_WINDOW);
    +                        error_timestamp, scanned_time - TIMESTAMP_WINDOW - 1, TIMESTAMP_WINDOW);
                         if (wallet.chain().havePruned()) {
                             error_msg += strprintf(" This error could be caused by pruning or data corruption "
                                 "(see bitcoind log for details) and could be dealt with by downloading and "
    diff --git a/test/functional/wallet_assumeutxo.py b/test/functional/wallet_assumeutxo.py
    index a8c97be778..9e05028316 100755
    --- a/test/functional/wallet_assumeutxo.py
    +++ b/test/functional/wallet_assumeutxo.py
    @@ -215,12 +215,18 @@ class AssumeutxoTest(BitcoinTestFramework):
             wallet_name = "w1"
             n1.createwallet(wallet_name, disable_private_keys=True)
             key = get_generate_key()
    -        time = n1.getblockchaininfo()['time']
    +        block_info = n1.getblockchaininfo()
    +        time = block_info['time']
    +        def expected_rescan_error(timestamp):
    +            return f"Rescan failed for descriptor with timestamp {timestamp}. There was an error reading a block from time {time}, which is after or within 7200 seconds of key creation, and could contain transactions pertaining to the desc. As a result, transactions and coins using this desc may not appear in the wallet. This error is likely caused by an in-progress assumeutxo background sync. Check logs or getchainstates RPC for assumeutxo background sync progress and try again later."
             timestamp = 0
    -        expected_error_message = f"Rescan failed for descriptor with timestamp {timestamp}. There was an error reading a block from time {time}, which is after or within 7200 seconds of key creation, and could contain transactions pertaining to the desc. As a result, transactions and coins using this desc may not appear in the wallet. This error is likely caused by an in-progress assumeutxo background sync. Check logs or getchainstates RPC for assumeutxo background sync progress and try again later."
             result = self.import_descriptor(n1, wallet_name, key, timestamp)
             assert_equal(result[0]['error']['code'], -1)
    -        assert_equal(result[0]['error']['message'], expected_error_message)
    +        assert_equal(result[0]['error']['message'], expected_rescan_error(timestamp))
    +        now_timestamp = block_info['mediantime']
    +        result = self.import_descriptor(n1, wallet_name, get_generate_key(), "now")
    +        assert_equal(result[0]['error']['code'], -1)
    +        assert_equal(result[0]['error']['message'], expected_rescan_error(now_timestamp))
     
             self.log.info("Test that rescanning blocks from before the snapshot fails when blocks are not available from the background sync yet")
             w1 = n1.get_wallet_rpc(wallet_name)
    

    </details>


    polespinasa commented at 8:05 PM on April 29, 2026:

    I think there is an easier patch that is just giving request.timestamp the value that std::max(request.timestamp.value_or(now), minimum_timestamp) gets.


    polespinasa commented at 8:26 PM on April 29, 2026:

    I've cherry-picked your test in #35179 with a small modification because it was failing even in master.


    polespinasa commented at 9:14 PM on April 29, 2026:

    I think there is an easier patch that is just giving request.timestamp the value that std::max(request.timestamp.value_or(now), minimum_timestamp) gets.

    Ok, you cannot hehe :)

    I picked you solution patch.

  80. DrahtBot removed the label CI failed on Apr 29, 2026
  81. in src/wallet/imports.h:42 in 26fc2eacdc outdated
      37 | +};
      38 | +//! Information about a descriptor to be imported.
      39 | +struct ImportDescriptorRequest {
      40 | +    std::string descriptor;
      41 | +    std::optional<std::string> label;
      42 | +    std::optional<int64_t> timestamp;
    


    pseudoramdom commented at 8:35 PM on April 29, 2026:

    26fc2eacdcc14aa497610b6a4c3437837c680e8d

    nit: Do we need the timestamp to be optional? It looks like we're overloading nullability - where nullopt means "now" and not absence of timestamp. Since timestamp is required input, can the RPC layer resolve "now" before constructing a ImportDescriptorRequest


    polespinasa commented at 8:27 AM on May 1, 2026:

    now is resolved in ProcessDescriptorsImport this way that logic is shared for all interfaces not only RPC.

    We could make it just a int64_t and make GetImportTimestamp return -1 (can't use 0 because it is a valid timestamp that resolves to 1) if timestamp = "now". But if we do that we are just changing:

    const int64_t timestamp = std::max(request.timestamp.value_or(now), minimum_timestamp);
    

    for:

    const int64_t timestamp;
    if (request.timestamp >= 0) {
        timestamp = std::max(request.timestamp, minimum_timestamp);
    } else {
        timestamp = std::max(now, minimum_timestamp);
    }
    

    Given that now is resolved in:

    CHECK_NONFATAL(wallet.chain().findBlock(wallet.GetLastBlockHash(), interfaces::FoundBlock().time(lowest_timestamp).mtpTime(now)));
    

    This populates the variable now with the mtptime of the current best block.

    Also I think using optional is fine, because then interfaces could implement logic to avoid setting a timestamp. "If timestamp is not set, then there's no re-scanning the blockchain".


    ViniciusCestarii commented at 1:21 PM on May 15, 2026:

    I believe it would help to add a short comment documenting that nullopt timestamp means "use the current best block's MTP time" rather than "no timestamp".

  82. in src/wallet/imports.cpp:216 in 923dc043c5 outdated
     211 | +            for (wallet::ImportDescriptorRequest& request : requests) {
     212 | +                const int64_t timestamp = std::max(request.timestamp.value_or(now), minimum_timestamp);
     213 | +                wallet::ImportDescriptorResult result = wallet::ImportDescriptor(
     214 | +                    wallet,
     215 | +                    request.descriptor,
     216 | +                    request.active.value_or(false),
    


    pseudoramdom commented at 9:07 PM on April 29, 2026:

    923dc043c5c8bb2e47b69c664782d7f3878c84bf

    If we're falling back to false, can we drop the optionality and default it to false in ImportDescriptorRequest?


    polespinasa commented at 8:58 AM on May 1, 2026:

    done

  83. polespinasa force-pushed on Apr 29, 2026
  84. in src/wallet/imports.h:37 in d5811b43fd outdated
      32 | +    ImportDescriptorResult& Error(FailureReason r, std::string e, bool g_e = false) {
      33 | +        reason = r;
      34 | +        error = std::move(e);
      35 | +        global_error = g_e;
      36 | +        return *this;
      37 | +    }
    


    pseudoramdom commented at 10:28 PM on April 29, 2026:

    d5811b43fdc97e950a8aa83a4a069e107b257d5e

    This result type looks quite inconvenient to consume. It's a flat dump of three-mutually exclusive states - per-descriptor success, per-descriptor failure and a call-wide failure (global_error).

    Checking for import_results[0].global_error) is not intuitive looking at the API.

    Could we make the API more explicit by separating call-wide errors from per-descriptor results ? For call-wide error, can we use util::Result

    util::Result<std::vector<ImportDescriptorResult>> ProcessDescriptorsImport(...)
    

    and remove global_error.

    For per-descriptor result, we could do something like

    using ImportDescriptorResult = std::variant<ImportDescriptorSuccess, ImportDescriptorError>
    
    struct ImportDescriptorSuccess {
          bool used_default_range{false};
          std::vector<std::string> warnings;
    };
    
    struct ImportDescriptorError {
          FailureReason reason;
          std::string message;
    };
    

    polespinasa commented at 9:33 AM on May 1, 2026:

    Not really familiar with util::Result but I think the problem with it is that it only takes error messages and not error codes. That's what FailureReason takes care of.

    I think there's no other way to distinguish them than this, which is not clean. I don't like relying on strings to identify errors. (in backup.cpp)

        std::optional<util::Result<std::vector<wallet::ImportDescriptorResult>>> import_results;
        if (!requests.empty()) {
            import_results.emplace(wallet::ProcessDescriptorsImport(wallet, requests));
        }
    
        // Wallet-wide precondition failure (wallet locked or already rescanning):
        // surface as a top-level RPC error, matching the pre-refactor behaviour.
        if (import_results && !*import_results) {
            const std::string error_msg = util::ErrorString(*import_results).original;
    
            const RPCErrorCode rpc_code = error_msg.starts_with("Error: Please enter")
                ? RPC_WALLET_UNLOCK_NEEDED
                : RPC_WALLET_ERROR;
            throw JSONRPCError(rpc_code, error_msg);
        }
    

    polespinasa commented at 10:26 AM on May 1, 2026:

    Maybe I can pick the std::variant idea and do something like:

    using ImportDescriptorResult = std::variant<ImportDescriptorSuccess, ImportDescriptorError>
    using ImportDescriptorsResult = std::variant<std::vector<ImportDescriptorResult>, GeneralImportError>;
    
    struct ImportDescriptorSuccess {
          bool used_default_range{false};
          std::vector<std::string> warnings;
    };
    
    struct ImportDescriptorError {
          FailureReason reason;
          std::string message;
    };
    
    struct GeneralImportError {
        FailureReason reason;
        std::string message;
    }
    
    importDescriptorsResult ProcessDescriptorsImport(...)
    

    Or just add global_error{false} into ImportDescriptorError. It will not fix what you are commenting, but will reduce the three mutual exclusive states to two in one case.


    pseudoramdom commented at 8:51 PM on May 20, 2026:

    Sorry for the late reply. I've been meaning to get to this. You're right util::Result may not work here. But.. looks like there's a util::Expected. Internally it uses std::variant. So it takes the same shape as ImportDescriptorsResult idea, but it has useful API for has_value(), error().

    struct ImportDescriptorSuccess {
          bool used_default_range{false};
          std::vector<std::string> warnings;
    };
    
    struct ImportDescriptorError {
          FailureReason reason;
          std::string message;
    };
    
    using ImportDescriptorResult = std::variant<ImportDescriptorSuccess, ImportDescriptorError>;
    util::Expected<std::vector<ImportDescriptorResult>, ImportDescriptorError>
    

    The change in backup.cpp might read slightly cleaner?

    if (!import_results) {
        const auto& err = import_results.error();
        const RPCErrorCode rpc_code = err.reason == FailureReason::WALLET_UNLOCK_NEEDED
              ? RPC_WALLET_UNLOCK_NEEDED
              : RPC_WALLET_ERROR;
        throw JSONRPCError(rpc_code, err.message);
    }
    
  85. polespinasa force-pushed on May 1, 2026
  86. polespinasa force-pushed on May 1, 2026
  87. polespinasa commented at 9:09 AM on May 1, 2026: member

    Force pushed to address some nit comments and drop result.used_default_range as it was not used.

  88. DrahtBot added the label CI failed on May 1, 2026
  89. DrahtBot removed the label CI failed on May 1, 2026
  90. rxbryan commented at 4:39 PM on May 9, 2026: none

    Looking at extending this pattern to getdescriptorinfo. This would let the GUI and anything downstream consume structured descriptor objects directly, especially Sjors' BIP-388 work.

  91. DrahtBot added the label Needs rebase on May 13, 2026
  92. ViniciusCestarii commented at 8:38 PM on May 14, 2026: contributor

    Bringing the discussion from #35181

    "wallet: refactor importdescriptors RPC (346fc3d6bd508370b5e299d1044d8de4a03f6d93)" applies the breaking change behavior discussed: a missing or invalid timestamp field now returns a per-item error instead of throwing the whole RPC, because ProcessUniValueDescriptor timestamps exceptions are catched and added as a per-item error instead of surfacing as a top-level RPC error, matching the pre-refactor behaviour.

    As @polespinasa raised in #35181 what's the correct approach: Is it worth fixing the timestamp error per query, instead of throwing if a single timestamp is wrong? It's a breaking change.

  93. in src/wallet/imports.h:22 in 677b73fb7e outdated
      17 | +        WALLET_ERROR,
      18 | +        WALLET_UNLOCK_NEEDED,
      19 | +        MISC_ERROR
      20 | +    };
      21 | +    bool success{false};
      22 | +    FailureReason reason{FailureReason::NONE};
    


    ViniciusCestarii commented at 1:41 PM on May 15, 2026:

    677b73fb7e0eda69d4b897dbedaec7c4f2258aed

    bool success is redundant if we already have reason. This also remove the risk of desyncing success and reason (the default constructor already start these member variables out of sync: success = false but reason = NONE).


    polespinasa commented at 4:21 PM on May 18, 2026:

    I am not sure. FailureReason tells you the reason why it failed, but not if it failed. FailureReason::NONE should not be used, it's just an initializer, but it's does not mean that it succeed.

    But after your comment I am thinking on making FailureReason an std::optional and drop FailureReason::NONE. I don't know if it is worth the handling of optional tho.


    ViniciusCestarii commented at 4:55 PM on May 18, 2026:

    I mean FailureReason::NONE already encodes success: if there's no failure reason, the operation succeeded. You could drop the boolean success and add an IsSuccess() function to the struct that just checks reason == NONE.


    polespinasa commented at 11:07 AM on May 28, 2026:

    Solved

  94. polespinasa force-pushed on May 18, 2026
  95. polespinasa commented at 4:09 PM on May 18, 2026: member

    Rebased on top of master

  96. polespinasa force-pushed on May 18, 2026
  97. DrahtBot added the label CI failed on May 18, 2026
  98. DrahtBot removed the label CI failed on May 18, 2026
  99. DrahtBot removed the label Needs rebase on May 18, 2026
  100. in src/wallet/imports.cpp:43 in 6ed501fe7e
      38 | +            return result.Error(ImportDescriptorResult::FailureReason::INVALID_PARAMETER,
      39 | +                "Range should not be specified for an un-ranged descriptor");
      40 | +        } else if (parsed_descs.at(0)->IsRange()) {
      41 | +            if (range.has_value()) {
      42 | +                range_start = range->first;
      43 | +                range_end = range->second + 1; // Specified range end is inclusive, but we need range end as exclusive
    


    pseudoramdom commented at 9:08 PM on May 20, 2026:

    nit: This is probably minor if this is the only place we do this. A safer way would be to define a type that has this behavior baked in.

    struct DescriptorRange {
      int64_t begin;  // inclusive
      int64_t end;    // exclusive
    
      static DescriptorRange FromInclusive(int64_t first, int64_t last) {
        return {first, last + 1};
      }
    };
    

    We can call DescriptorRange::FromInclusive(r.first, r.second) in backup.cpp


    polespinasa commented at 8:31 AM on May 28, 2026:

    Leaving at is for know, it can be done in a follow up. I rather keep this PR focused on only refactoring to allow the new interface.

  101. in src/wallet/imports.h:14 in 6ed501fe7e
       9 | +
      10 | +namespace wallet {
      11 | +
      12 | +struct ImportDescriptorResult {
      13 | +    enum class FailureReason {
      14 | +        NONE,
    


    pseudoramdom commented at 9:09 PM on May 20, 2026:

    This case will go away if you go with the util::expected suggestion.


  102. in src/wallet/imports.h:21 in bd29190bdd
      16 | +        INVALID_PARAMETER,
      17 | +        WALLET_ERROR,
      18 | +        WALLET_UNLOCK_NEEDED,
      19 | +        MISC_ERROR
      20 | +    };
      21 | +    bool success{false};
    


    achow101 commented at 7:16 PM on May 25, 2026:

    In bd29190bddfe159755e380707c7a08c1539cf90a "wallet: Add ImportDescriptorRequest and ImportDescriptorResult structs"

    success should be synonmous with FailureReason::None. FailureReason could be renamed to ImportResultCode (or something like that) and used as a result code rather than just indicating failure types.


    polespinasa commented at 11:07 AM on May 28, 2026:

    Done

  103. in src/wallet/rpc/backup.cpp:168 in da866a3eae
     172 | +    const std::optional<bool>& internal,
     173 | +    const std::optional<std::string>& label,
     174 | +    int64_t timestamp,
     175 | +    const std::optional<std::pair<int64_t, int64_t>>& range,
     176 | +    const std::optional<int64_t>& next_index_arg) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
     177 | +{
    


    achow101 commented at 7:35 PM on May 25, 2026:

    In da866a3eaeb6bb8c3bb4c2d23e1ae13c45a7b200 "wallet: refactor importdescriptors RPC"

    Why doesn't this take a ImportDescriptorRequest? Most of the arguments are the members of the struct.


    polespinasa commented at 8:50 AM on May 28, 2026:

    Done

  104. in src/wallet/rpc/backup.cpp:433 in da866a3eae
     448 | +                // range, or if the import result already has an error set, let
     449 | +                // the result stand unmodified. Otherwise replace the result
     450 | +                // with an error message.
     451 | +                const int64_t timestamp{resolved_timestamps.at(i)};
     452 | +                if (scanned_time <= timestamp || !result.success) continue;
     453 | +                const int64_t error_timestamp{requests.at(i).timestamp.value_or(timestamp)};
    


    achow101 commented at 7:40 PM on May 25, 2026:

    In da866a3eaeb6bb8c3bb4c2d23e1ae13c45a7b200 "wallet: refactor importdescriptors RPC"

    I think this is unnecessary and timestamp could be used wherever error_timestamp is. timestamp is already the result of requests.at(i).timestamp.value_or().


    polespinasa commented at 11:21 AM on May 28, 2026:

    error_timestamp does not have the same value as timestamp. This is because resolved_timestamps stores the "corrected" timestamp while error_timestamp stores the original timestamp request.

    The edge case here that makes this two variables needed is if the user enters a descriptor with timestamp=0 which is below our minimum_timestamp=1. So error_timestamp=0 while timestamp=1 because of std::max(request.timestamp.value_or(now), minimum_timestamp);.

    Our test coverage already tests that in the wallet_assumeutxo.py.

    <details> <summary>See error logs after change</summary>

     build/test/functional/wallet_assumeutxo.py 
    2026-05-28T11:13:49.371236Z TestFramework (INFO): PRNG seed is: 8610115919761428835
    2026-05-28T11:13:49.422134Z TestFramework (INFO): Initializing test directory /tmp/bitcoin_func_test_iedpmzw6
    2026-05-28T11:13:51.223312Z TestFramework (INFO): -- Testing assumeutxo
    2026-05-28T11:13:51.226248Z TestFramework (INFO): Creating a UTXO snapshot at height 299
    2026-05-28T11:13:51.741927Z TestFramework (INFO): Loading snapshot into second node from /tmp/bitcoin_func_test_iedpmzw6/node0/regtest/utxos.dat
    2026-05-28T11:13:51.894492Z TestFramework (INFO): Backup from the snapshot height can be loaded during background sync
    2026-05-28T11:13:51.903778Z TestFramework (INFO): Backup from before the snapshot height can't be loaded during background sync
    2026-05-28T11:13:51.911199Z TestFramework (INFO): Backup from the snapshot height can be loaded during background sync (pruned node)
    2026-05-28T11:13:52.068538Z TestFramework (INFO): Backup from before the snapshot height can't be loaded during background sync (pruned node)
    2026-05-28T11:13:52.078886Z TestFramework (INFO): Test loading descriptors during background sync
    2026-05-28T11:13:52.113878Z TestFramework (ERROR): Unexpected exception:
    Traceback (most recent call last):
      File "/home/sliv3r/Documentos/Projectes/BitcoinCore/bitcoin/test/functional/test_framework/test_framework.py", line 143, in main
        self.run_test()
      File "/home/sliv3r/Documentos/Projectes/BitcoinCore/bitcoin/build/test/functional/wallet_assumeutxo.py", line 223, in run_test
        assert_equal(result[0]['error']['message'], expected_error_message)
      File "/home/sliv3r/Documentos/Projectes/BitcoinCore/bitcoin/test/functional/test_framework/util.py", line 83, in assert_equal
        raise AssertionError("not(%s)" % " == ".join(str(arg) for arg in (thing1, thing2) + args))
    AssertionError: not(Rescan failed for descriptor with timestamp 1. There was an error reading a block from time 1296688654, which is after or within 7200 seconds of key creation, and could contain transactions pertaining to the desc. As a result, transactions and coins using this desc may not appear in the wallet. This error is likely caused by an in-progress assumeutxo background sync. Check logs or getchainstates RPC for assumeutxo background sync progress and try again later. == Rescan failed for descriptor with timestamp 0. There was an error reading a block from time 1296688654, which is after or within 7200 seconds of key creation, and could contain transactions pertaining to the desc. As a result, transactions and coins using this desc may not appear in the wallet. This error is likely caused by an in-progress assumeutxo background sync. Check logs or getchainstates RPC for assumeutxo background sync progress and try again later.)
    2026-05-28T11:13:52.165566Z TestFramework (INFO): Not stopping nodes as test failed. The dangling processes will be cleaned up later.
    2026-05-28T11:13:52.165864Z TestFramework (WARNING): Not cleaning up dir /tmp/bitcoin_func_test_iedpmzw6
    2026-05-28T11:13:52.165975Z TestFramework (ERROR): Test failed. Test logging available at /tmp/bitcoin_func_test_iedpmzw6/test_framework.log
    2026-05-28T11:13:52.166217Z TestFramework (ERROR): 
    2026-05-28T11:13:52.166485Z TestFramework (ERROR): Hint: Call /home/sliv3r/Documentos/Projectes/BitcoinCore/bitcoin/test/functional/combine_logs.py '/tmp/bitcoin_func_test_iedpmzw6' to consolidate all logs
    2026-05-28T11:13:52.166590Z TestFramework (ERROR): 
    2026-05-28T11:13:52.166676Z TestFramework (ERROR): If this failure happened unexpectedly or intermittently, please file a bug and provide a link or upload of the combined log.
    2026-05-28T11:13:52.166817Z TestFramework (ERROR): https://github.com/bitcoin/bitcoin/issues
    2026-05-28T11:13:52.166994Z TestFramework (ERROR): 
    [node 3] Cleaning up leftover process
    [node 2] Cleaning up leftover process
    [node 1] Cleaning up leftover process
    [node 0] Cleaning up leftover process
    
    

    </details>


    achow101 commented at 6:29 PM on May 28, 2026:

    I'm pretty sure allowing a timestamp of 0 doesn't have a negative effect. The default value for the field that the timestamp ends up in is 0 already. We should disallow invalid timestamps (e.g. negatives), not rewriting them into a valid value.


    polespinasa commented at 2:27 PM on May 31, 2026:

    Maybe I am not following what you say, but I don't think it can ever be 0 as we pick the max between 1 and the mtp time. So 1 is the minimum timestamp.

    The only reason we keep that 0 value is because error_timestamp is the "user input" timestamp and it is only used for error message purpose. resolved_timestamps stores the "real" value of timestamps used in each import. So in case the user did not specify a timestamp we can use it on error logging.

    In case what you mean is that we should be able to lower the minimum timestamp to 0 instead of 1. I would say that is out of scope for this. I would rather keep this PR in the scope of refactoring and allowing a new interface. Behavior changes can be done in a separate PR.


    achow101 commented at 11:08 PM on June 2, 2026:

    I mean that we can lower the minimum to 0, and I think that doing so would be in scope for this PR as it makes many things simpler, including the interface.

    If you disagree, I think the interface should at least not have to take the timestamp as a separate parameter and a different solution is necessary to preserve the current behavior but also pass in the resolved timestamp in the request.


    polespinasa commented at 8:05 PM on June 3, 2026:

    Lowered to 0

  105. in src/wallet/imports.cpp:311 in 41dda70864 outdated
     306 | +                }
     307 | +            }
     308 | +        }
     309 | +        return response;
     310 | +    }
     311 | +}
    


    achow101 commented at 7:44 PM on May 25, 2026:

    In 41dda7086420b37c58cf1d63492487e790b37de9 "wallet: Move ImportDescriptor and ProcessDescriptorsImport to imports.cpp"

    The closing bracket of a namespace needs to have a comment telling us what namespace that bracket is closing.

    } // namespace wallet
    

    polespinasa commented at 8:51 AM on May 28, 2026:

    done

  106. in src/wallet/imports.cpp:9 in 41dda70864
       0 | @@ -0,0 +1,311 @@
       1 | +// Copyright (c) 2026-present The Bitcoin Core developers
       2 | +// Distributed under the MIT software license, see the accompanying
       3 | +// file COPYING or http://www.opensource.org/licenses/mit-license.php.
       4 | +
       5 | +#include <chain.h>
       6 | +#include <wallet/imports.h>
       7 | +
       8 | +namespace wallet{
       9 | +    ImportDescriptorResult ImportDescriptor(CWallet& wallet, const std::string& descriptor,
    


    achow101 commented at 7:45 PM on May 25, 2026:

    In 41dda7086420b37c58cf1d63492487e790b37de9 "wallet: Move ImportDescriptor and ProcessDescriptorsImport to imports.cpp"

    Generally things inside of a namespace do not need to be indented. Unindenting all of the moved lines in this file also lets git detect that these lines were moved without using --color-moved-ws.


    polespinasa commented at 8:52 AM on May 28, 2026:

    done

  107. in src/wallet/imports.cpp:219 in 41dda70864
     214 | +        int64_t now = 0;
     215 | +        int64_t lowest_timestamp = 0;
     216 | +        bool rescan = false;
     217 | +        {
     218 | +            LOCK(wallet.cs_wallet);
     219 | +
    


    achow101 commented at 7:52 PM on May 25, 2026:

    In 41dda7086420b37c58cf1d63492487e790b37de9 "wallet: Move ImportDescriptor and ProcessDescriptorsImport to imports.cpp"

    nit: whitespace introduced in move commit


    polespinasa commented at 8:53 AM on May 28, 2026:

    done

  108. in src/wallet/imports.cpp:212 in 41dda70864
     207 | +        }
     208 | +
     209 | +        // Ensure that the wallet is not locked for the remainder of this call,
     210 | +        // as the passphrase is used to top up the keypool.
     211 | +        LOCK(wallet.m_relock_mutex);
     212 | +
    


    achow101 commented at 7:53 PM on May 25, 2026:

    In 41dda7086420b37c58cf1d63492487e790b37de9 "wallet: Move ImportDescriptor and ProcessDescriptorsImport to imports.cpp"

    nit: whitespace introduced in move


    polespinasa commented at 8:53 AM on May 28, 2026:

    done

  109. in src/wallet/rpc/backup.cpp:383 in da866a3eae
     391 | +
     392 | +        CHECK_NONFATAL(wallet.chain().findBlock(wallet.GetLastBlockHash(), interfaces::FoundBlock().time(lowest_timestamp).mtpTime(now)));
     393 | +
     394 | +        for (ImportDescriptorRequest& request : requests) {
     395 | +            const int64_t timestamp = std::max(request.timestamp.value_or(now), minimum_timestamp);
     396 | +            resolved_timestamps.push_back(timestamp);
    


    achow101 commented at 7:56 PM on May 25, 2026:

    In da866a3eaeb6bb8c3bb4c2d23e1ae13c45a7b200 "wallet: refactor importdescriptors RPC"

    Instead of storing the resolved timestamp separately, it seems like it would be easier to overwrite the timestamp in the struct, and then pass the whole struct into ImportDescriptor. This would also resolve some of my comments below about timestamp handling during rescan.


    polespinasa commented at 11:22 AM on May 28, 2026:

    For similar reasons to #34861 (review) this cannot be done. There is also some test coverage missing for the timestamps that I added here: #35179 Would be good if that gets merged prior to this PR so the CI tests that we are not missing anything.

    See also #34861 (review) for context

  110. in src/wallet/imports.cpp:188 in 41dda70864
     183 | +
     184 | +        result.success = true;
     185 | +        return result;
     186 | +    }
     187 | +
     188 | +    std::vector<wallet::ImportDescriptorResult> ProcessDescriptorsImport(CWallet& wallet,
    


    achow101 commented at 7:58 PM on May 25, 2026:

    In 41dda7086420b37c58cf1d63492487e790b37de9 "wallet: Move ImportDescriptor and ProcessDescriptorsImport to imports.cpp"

    wallet:: namespace specifiers are not necessary when everything in the file is already in the wallet namespace.


    polespinasa commented at 8:54 AM on May 28, 2026:

    done

  111. polespinasa force-pushed on May 28, 2026
  112. polespinasa force-pushed on May 28, 2026
  113. DrahtBot added the label CI failed on May 28, 2026
  114. DrahtBot commented at 11:29 AM on May 28, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task tidy: https://github.com/bitcoin/bitcoin/actions/runs/26570718078/job/78277655091</sub> <sub>LLM reason (✨ experimental): CI failed because clang-tidy reported a readability-avoid-const-params-in-decls error (const-qualified parameter in declaration, e.g. wallet/imports.h:48) and treated it as a failure.</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  115. DrahtBot removed the label CI failed on May 28, 2026
  116. in src/wallet/imports.cpp:246 in 53a0e162e5 outdated
     241 | +    if (rescan) {
     242 | +        const int64_t scanned_time = wallet.RescanFromTime(lowest_timestamp, reserver);
     243 | +        wallet.ResubmitWalletTransactions(node::TxBroadcast::MEMPOOL_NO_BROADCAST, /*force=*/true);
     244 | +
     245 | +        if (wallet.IsAbortingRescan()) {
     246 | +            // Mark all previously successful imports as aborted
    


    w0xlt commented at 8:15 PM on June 3, 2026:

    The current code as of 53a0e162e531f2393b63517515d3522ff90afac1 changes importdescriptors behavior when abortrescan is called during the rescan.

    On master, the RPC fails with a top-level RPC_MISC_ERROR: Rescan aborted by user.

    On this branch, the RPC returns a normal response array with per-descriptor failures instead.

    Is this intentional?

    If not, here is a suggestion to preserve the existing behavior. If it is intentional, it should probably be documented in the release notes, since some clients may rely on the previous behavior.

    <details> <summary>diff</summary>

    diff --git a/src/wallet/imports.cpp b/src/wallet/imports.cpp
    index a46a6b7094..4ef81adc91 100644
    --- a/src/wallet/imports.cpp
    +++ b/src/wallet/imports.cpp
    @@ -243,13 +243,12 @@ std::vector<ImportDescriptorResult> ProcessDescriptorsImport(CWallet& wallet,
             wallet.ResubmitWalletTransactions(node::TxBroadcast::MEMPOOL_NO_BROADCAST, /*force=*/true);
     
             if (wallet.IsAbortingRescan()) {
    -            // Mark all previously successful imports as aborted
    -            for (ImportDescriptorResult& r : response) {
    -                if (r.result_code == ImportDescriptorResult::ImportResultCode::OK) {
    -                    r.Error(ImportDescriptorResult::ImportResultCode::MISC_ERROR,
    -                        "Rescan aborted by user.");
    -                }
    -            }
    +            ImportDescriptorResult result;
    +            result.Error(ImportDescriptorResult::ImportResultCode::MISC_ERROR,
    +                "Rescan aborted by user.",
    +                /*g_e=*/true);
    +            response.clear();
    +            response.push_back(result);
                 return response;
             }
     
    diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp
    index cf08e52cf7..be83d55b3e 100644
    --- a/src/wallet/rpc/backup.cpp
    +++ b/src/wallet/rpc/backup.cpp
    @@ -242,9 +242,17 @@ RPCMethod importdescriptors()
         // Wallet-wide precondition failure (e.g. already rescanning, or locked):
         // surface as a top-level RPC error, matching the pre-refactor behaviour.
         if (!import_results.empty() && import_results[0].global_error) {
    -        RPCErrorCode rpc_error_code = (import_results[0].result_code == ImportDescriptorResult::ImportResultCode::WALLET_UNLOCK_NEEDED)
    -         ? RPC_WALLET_UNLOCK_NEEDED
    -         : RPC_WALLET_ERROR;
    +        RPCErrorCode rpc_error_code{RPC_WALLET_ERROR};
    +        switch (import_results[0].result_code) {
    +            case ImportDescriptorResult::ImportResultCode::WALLET_UNLOCK_NEEDED:
    +                rpc_error_code = RPC_WALLET_UNLOCK_NEEDED;
    +                break;
    +            case ImportDescriptorResult::ImportResultCode::MISC_ERROR:
    +                rpc_error_code = RPC_MISC_ERROR;
    +                break;
    +            default:
    +                break;
    +        }
             throw JSONRPCError(rpc_error_code, import_results[0].error);
         }
     
    diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py
    index 3e8b05a28d..9af9c4f809 100755
    --- a/test/functional/wallet_importdescriptors.py
    +++ b/test/functional/wallet_importdescriptors.py
    @@ -789,6 +789,20 @@ class ImportDescriptorsTest(BitcoinTestFramework):
     
             assert_equal(temp_wallet.getbalance(), encrypted_wallet.getbalance())
     
    +        self.log.info("Aborting an importdescriptors rescan should fail the RPC call")
    +        self.nodes[0].createwallet("abort_import_wallet", blank=True)
    +        abort_import_wallet = self.nodes[0].get_wallet_rpc("abort_import_wallet")
    +        with concurrent.futures.ThreadPoolExecutor(max_workers=1) as thread:
    +            with self.nodes[0].assert_debug_log(expected_msgs=["Rescan started from block"], timeout=10):
    +                importing = thread.submit(abort_import_wallet.importdescriptors, requests=[descriptor])
    +            self.nodes[0].cli("-rpcwallet=abort_import_wallet").abortrescan()
    +            try:
    +                importing.result(timeout=30)
    +                raise AssertionError("importdescriptors unexpectedly succeeded")
    +            except JSONRPCException as e:
    +                assert_equal(e.error["code"], -1)
    +                assert_equal(e.error["message"], "Rescan aborted by user.")
    +
             self.log.info("Multipath descriptors")
             self.nodes[1].createwallet(wallet_name="multipath", blank=True)
             w_multipath = self.nodes[1].get_wallet_rpc("multipath")
    

    </details>


    polespinasa commented at 8:48 AM on June 8, 2026:

    Nice catch! Fixed in the last push. Will add the test in #35179

  117. polespinasa force-pushed on Jun 3, 2026
  118. in src/wallet/rpc/backup.cpp:154 in 53a0e162e5 outdated
     328 | +    if (data.exists("label")) request.label = LabelFromValue(data["label"]);
     329 | +    request.timestamp = GetImportTimestamp(data);
     330 | +    if (data.exists("active")) request.active = data["active"].get_bool();
     331 | +    if (data.exists("internal")) request.internal = data["internal"].get_bool();
     332 | +    if (data.exists("range")) {
     333 | +        auto r = ParseDescriptorRange(data["range"]);
    


    w0xlt commented at 8:59 PM on June 3, 2026:

    If the importdescriptors request has more than one problem, for example a missing descriptor checksum and an invalid range, the PR code reports the range problem first. On master, it reports the descriptor problem (Missing checksum) first.


    polespinasa commented at 7:53 AM on June 8, 2026:

    I don't think there is a clean solution for that. The invalid range check is a RPC check, while the checksum check is a importdescriptor check, as we are separating them it is hard to mix the order.

    In any case I don't think this is a problem, if both problems are present in the request, it will fail anyway, the request will never be successful without both fixed. @achow101 thoughts?


    w0xlt commented at 8:34 AM on June 8, 2026:

    "In any case I don't think this is a problem"

    Yes, I agree that it does not need to be addressed. I also tried a few solutions, but none of them were simpler or cleaner.

    The comment was only meant to confirm the behavioral difference.

  119. w0xlt commented at 8:59 PM on June 3, 2026: contributor

    A few review comments:

  120. polespinasa force-pushed on Jun 8, 2026
  121. in src/wallet/rpc/backup.cpp:142 in 45fb860ae3
     140 |      }
     141 |      throw JSONRPCError(RPC_TYPE_ERROR, "Missing required timestamp field for key");
     142 |  }
     143 |  
     144 | -static UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet)
     145 | +static wallet::ImportDescriptorRequest ProcessUniValueDescriptor(const UniValue& data)
    


    rkrux commented at 9:14 AM on June 8, 2026:

    In 45fb860ae3299929831d1c3cd067811cd37c7842 "wallet: refactor importdescriptors RPC"

    All of these are within the wallet namepsace, scope resolution is not required that should make the code succinct.

    diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp
    index be83d55b3e..c452497a1f 100644
    --- a/src/wallet/rpc/backup.cpp
    +++ b/src/wallet/rpc/backup.cpp
    @@ -139,9 +139,9 @@ static std::optional<int64_t> GetImportTimestamp(const UniValue& data)
         throw JSONRPCError(RPC_TYPE_ERROR, "Missing required timestamp field for key");
     }
     
    -static wallet::ImportDescriptorRequest ProcessUniValueDescriptor(const UniValue& data)
    +static ImportDescriptorRequest ProcessUniValueDescriptor(const UniValue& data)
     {
    -    wallet::ImportDescriptorRequest request;
    +    ImportDescriptorRequest request;
         if (!data.exists("desc")) {
             throw JSONRPCError(RPC_INVALID_PARAMETER, "Descriptor not found.");
         }
    @@ -221,7 +221,7 @@ RPCMethod importdescriptors()
     
         UniValue response(UniValue::VARR);
         const UniValue& univalue_requests = main_request.params[0];
    -    std::vector<wallet::ImportDescriptorRequest> requests;
    +    std::vector<ImportDescriptorRequest> requests;
     
         // Collect per-item parse errors so that malformed inputs (e.g. invalid
         // label) are returned as per-item failures.
    @@ -234,9 +234,9 @@ RPCMethod importdescriptors()
             }
         }
     
    -    std::vector<wallet::ImportDescriptorResult> import_results;
    +    std::vector<ImportDescriptorResult> import_results;
         if (!requests.empty()) {
    -        import_results = wallet::ProcessDescriptorsImport(wallet, requests);
    +        import_results = ProcessDescriptorsImport(wallet, requests);
         }
     
         // Wallet-wide precondition failure (e.g. already rescanning, or locked):
    @@ -271,28 +271,28 @@ RPCMethod importdescriptors()
                 continue;
             }
     
    -        wallet::ImportDescriptorResult import_result = import_results[result_idx++];
    +        ImportDescriptorResult import_result = import_results[result_idx++];
             auto writeError = [&result, error = import_result.error](int code) {
                 result.pushKV("success", false);
                 result.pushKV("error", JSONRPCError(code, error));
             };
             switch (import_result.result_code) {
    -            case wallet::ImportDescriptorResult::ImportResultCode::OK:
    +            case ImportDescriptorResult::ImportResultCode::OK:
                     result.pushKV("success", true);
                     break;
    -            case wallet::ImportDescriptorResult::ImportResultCode::INVALID_DESCRIPTOR:
    +            case ImportDescriptorResult::ImportResultCode::INVALID_DESCRIPTOR:
                     writeError(RPC_INVALID_ADDRESS_OR_KEY);
                     break;
    -            case wallet::ImportDescriptorResult::ImportResultCode::INVALID_PARAMETER:
    +            case ImportDescriptorResult::ImportResultCode::INVALID_PARAMETER:
                     writeError(RPC_INVALID_PARAMETER);
                     break;
    -            case wallet::ImportDescriptorResult::ImportResultCode::MISC_ERROR:
    +            case ImportDescriptorResult::ImportResultCode::MISC_ERROR:
                     writeError(RPC_MISC_ERROR);
                     break;
    -            case wallet::ImportDescriptorResult::ImportResultCode::WALLET_UNLOCK_NEEDED:
    +            case ImportDescriptorResult::ImportResultCode::WALLET_UNLOCK_NEEDED:
                     NONFATAL_UNREACHABLE();
                     break;
    -            case wallet::ImportDescriptorResult::ImportResultCode::WALLET_ERROR:
    +            case ImportDescriptorResult::ImportResultCode::WALLET_ERROR:
                     writeError(RPC_WALLET_ERROR);
                     break;
             }
    
    

    polespinasa commented at 10:43 AM on June 8, 2026:

    done

  122. in src/wallet/rpc/backup.cpp:1 in f8f55b3405 outdated


    rkrux commented at 9:20 AM on June 8, 2026:

    In f8f55b3405544906563101c791cc14930e64c48b "wallet: Move ImportDescriptor and ProcessDescriptorsImport to imports.cpp"

    Nit: It can (should) be mentioned in the commit message that this commit should be reviewed with the --color-moved=dimmed-zebra git option, it greys out almost all the diff in this commit.


    polespinasa commented at 10:43 AM on June 8, 2026:

    taken

  123. in src/wallet/imports.h:28 in 934939a3b3
      23 | +    std::vector<std::string> warnings;
      24 | +    //! Set to true when a wallet-wide precondition failed before any descriptor was
      25 | +    //! processed (e.g. wallet is already rescanning, or wallet is locked).
      26 | +    //! Callers that support top-level errors should surface this as a
      27 | +    //! top-level / call-wide error rather than a per-descriptor failure.
      28 | +    bool global_error{false};
    


    rkrux commented at 9:45 AM on June 8, 2026:

    In 934939a3b30cccf21e26d6044d1dada6044c6dff "wallet: Add ImportDescriptorRequest and ImportDescriptorResult structs"

    s/global_error/is_wallet_error

    Global doesn't necessarily mean limited to wallet, instead of relying on the documentation comment, the variable name can signify it itself.

    The "is_" portion is added to the reflect that argument is bool.


    polespinasa commented at 10:44 AM on June 8, 2026:

    good idea, taken

  124. in src/wallet/imports.cpp:245 in f8f55b3405
     240 | +        const int64_t scanned_time = wallet.RescanFromTime(lowest_timestamp, reserver);
     241 | +        wallet.ResubmitWalletTransactions(node::TxBroadcast::MEMPOOL_NO_BROADCAST, /*force=*/true);
     242 | +
     243 | +        if (wallet.IsAbortingRescan()) {
     244 | +            ImportDescriptorResult result;
     245 | +            result.Error(ImportDescriptorResult::ImportResultCode::MISC_ERROR, "Rescan aborted by user.", /*g_e=*/ true);
    


    rkrux commented at 9:45 AM on June 8, 2026:

    In f8f55b3405544906563101c791cc14930e64c48b "wallet: Move ImportDescriptor and ProcessDescriptorsImport to imports.cpp"

    s//*g_e=*/ true//*g_e=*/true
    

    Nit: There is an extra space in between unlike other such calls.


    polespinasa commented at 10:46 AM on June 8, 2026:

    fixed

  125. in src/wallet/rpc/backup.cpp:521 in 45fb860ae3
     563 | +    }
     564 |  
     565 | -        if (pwallet->IsAbortingRescan()) {
     566 | -            throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted by user.");
     567 | +    // Wallet-wide precondition failure (e.g. already rescanning, or locked):
     568 | +    // surface as a top-level RPC error, matching the pre-refactor behaviour.
    


    rkrux commented at 9:52 AM on June 8, 2026:

    In 45fb860ae3299929831d1c3cd067811cd37c7842 "wallet: refactor importdescriptors RPC"

    , matching the pre-refactor behaviour.

    I don't think this should be a part of the code comment, it's more of a part of commit message or a review comment.


    polespinasa commented at 10:49 AM on June 8, 2026:

    fixed

  126. in src/wallet/imports.h:21 in 934939a3b3 outdated
      16 | +        INVALID_PARAMETER,
      17 | +        WALLET_ERROR,
      18 | +        WALLET_UNLOCK_NEEDED,
      19 | +        MISC_ERROR
      20 | +    };
      21 | +    ImportResultCode result_code{ImportResultCode::OK};
    


    rkrux commented at 10:03 AM on June 8, 2026:

    In 934939a "wallet: Add ImportDescriptorRequest and ImportDescriptorResult structs"

    I think we are moving away from this style where the "success" and "error" portion are both clubbed within the same struct. Refer PR #32958 that replaces this style with util::Expected<void, *Error> (and the corresponding util::Unexpected).


    pseudoramdom commented at 6:10 PM on June 11, 2026:

    Agree, this is the same direction discussed in this thread. Clubbing success and error in the same enum looks like an anti-pattern we should avoid IMO. FWIW, I used util::Expected in #35436 (reviews welcome :) )


    polespinasa commented at 6:20 PM on June 11, 2026:

    I'll take a look on how to use it, didn't have time yet

    I used util::Expected in #35436 (reviews welcome :) )

    Will do :)

  127. in src/wallet/imports.h:13 in 934939a3b3 outdated
       8 | +#include <wallet/wallet.h>
       9 | +
      10 | +namespace wallet {
      11 | +
      12 | +struct ImportDescriptorResult {
      13 | +    enum class ImportResultCode {
    


    rkrux commented at 10:05 AM on June 8, 2026:

    In 934939a "wallet: Add ImportDescriptorRequest and ImportDescriptorResult structs"

    s/ImportResultCode/ResultCode

    "import" within the second struct ImportResultCode that's nested inside ImportDescriptorResult seems redundant and reading its usages in backup.cpp is quite verbose such as the one below.

    case ImportDescriptorResult::ImportResultCode::INVALID_DESCRIPTOR:
    

    rkrux commented at 10:06 AM on June 8, 2026:

    Also, using the util::Expected approach allows us to rename it to ErrorCode that seems more appropriate as well because all the codes are errors except the first one that seems to have forced its way into the struct. So the case mentioned in the above comment would read like below that seems short and to the point imo.

    case ImportDescriptorResult::ErrorCode::INVALID_DESCRIPTOR:
    
  128. in src/wallet/imports.h:22 in 934939a3b3
      17 | +        WALLET_ERROR,
      18 | +        WALLET_UNLOCK_NEEDED,
      19 | +        MISC_ERROR
      20 | +    };
      21 | +    ImportResultCode result_code{ImportResultCode::OK};
      22 | +    std::string error;
    


    rkrux commented at 10:16 AM on June 8, 2026:

    In 934939a "wallet: Add ImportDescriptorRequest and ImportDescriptorResult structs"

    s/error/error_message

    To be explicit in the variable name itself.


    polespinasa commented at 11:07 AM on June 8, 2026:

    done

  129. rkrux commented at 10:16 AM on June 8, 2026: contributor

    Code review 4fd7d5ed819c5fa5285948c8cb5c740f46bf7738

  130. polespinasa force-pushed on Jun 8, 2026
  131. DrahtBot added the label CI failed on Jun 8, 2026
  132. DrahtBot commented at 12:36 PM on June 8, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task tidy: https://github.com/bitcoin/bitcoin/actions/runs/27133600610/job/80080414973</sub> <sub>LLM reason (✨ experimental): CI failed because clang-tidy reported bugprone-argument-comment errors (argument comment name mismatch is_wallet_error vs parameter w_e in wallet/imports.cpp), treated as warnings-as-errors.</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  133. wallet: Add ImportDescriptorRequest and ImportDescriptorResult structs
    Add two new structs used for creating requests and getting response when importing descriptors.
    
    These structs replace the UniValue-based interface of ProcessDescriptorImport()
    in a subsequent commit.
    9dd199e668
  134. polespinasa force-pushed on Jun 8, 2026
  135. DrahtBot removed the label CI failed on Jun 8, 2026
  136. wallet: refactor importdescriptors RPC
    Rename ProcessDescriptorImport() to ImportDescriptor() and replace its
    UniValue-based arguments and return value with the ImportDescriptorRequest
    and ImportDescriptorResult structs introduced in the previous commit.
    
    Add ProcessDescriptorsImport() to handle wallet locking and rescanning
    over a vector of ImportDescriptorRequest items.
    
    Add ProcessUniValueDescriptor() to translat from the UniValue arguments
    used by the importdescriptors RPC into ImportDescriptorRequest.
    
    This allows a next commit to extract ImportDescriptor() and
    ProcessDescriptorsImport() out of the RPC code so they can be used by
    other future interfaces.
    
    Lowers the minimum timestamps to 0 instead of 1.
    7e99dede80
  137. wallet: Move ImportDescriptor and ProcessDescriptorsImport to imports.cpp
    Extract ImportDescriptor() and ProcessDescriptorsImport() out of backup.cpp and move it into imports.cpp so other future interfaces
    can use them without needing to know about RPC code.
    
    The commit can be reviewed with the --color-moved=dimmed-zebra option for an easier review.
    3c2b8e1784
  138. wallet: Add an importDescriptors() interface for the wallet 389c3059da
  139. in src/wallet/imports.cpp:64 in e446f9bc81
      59 | +    if (parsed_descs.size() > 1 && request.label.has_value()) {
      60 | +        return result.Error(ImportDescriptorResult::ImportResultCode::INVALID_PARAMETER,
      61 | +            "Multipath descriptors should not have a label");
      62 | +    }
      63 | +
      64 | +    // Ranged descrptors should not have a label
    


    maflcko commented at 12:25 PM on June 9, 2026:

    typos (from the llm):

    • // Ranged descrptors should not have a label -> descriptors [misspelled word]
    • // ... as we didnt import the descriptor in the request. -> didn't [missing apostrophe in contraction]
  140. polespinasa force-pushed on Jun 9, 2026

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-06-13 21:30 UTC

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