Coverage Report

Created: 2026-07-23 20:35

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/tmp/bitcoin/src/wallet/export.cpp
Line
Count
Source
1
// Copyright (c) 2026-present The Bitcoin Core developers
2
// Distributed under the MIT software license, see the accompanying
3
// file COPYING or https://www.opensource.org/licenses/mit-license.php.
4
5
#include <wallet/export.h>
6
7
#include <key_io.h>
8
#include <util/fs.h>
9
#include <util/expected.h>
10
#include <wallet/scriptpubkeyman.h>
11
#include <wallet/context.h>
12
#include <wallet/sqlite.h>
13
#include <wallet/wallet.h>
14
15
#include <fstream>
16
17
namespace wallet {
18
util::Expected<std::vector<WalletDescInfo>, std::string> ExportDescriptors(const CWallet& wallet, bool export_private)
19
208
{
20
208
    AssertLockHeld(wallet.cs_wallet);
21
208
    std::vector<WalletDescInfo> wallet_descriptors;
22
1.55k
    for (const auto& spk_man : wallet.GetAllScriptPubKeyMans()) {
23
1.55k
        const auto desc_spk_man = dynamic_cast<DescriptorScriptPubKeyMan*>(spk_man);
24
1.55k
        if (!desc_spk_man) {
25
0
            return util::Unexpected{"Unexpected ScriptPubKey manager type."};
26
0
        }
27
1.55k
        LOCK(desc_spk_man->cs_desc_man);
28
1.55k
        const auto& wallet_descriptor = desc_spk_man->GetWalletDescriptor();
29
1.55k
        std::string descriptor;
30
1.55k
        if (!Assume(desc_spk_man->GetDescriptorString(descriptor, export_private))) {
31
0
            return util::Unexpected{"Can't get descriptor string."};
32
0
        }
33
1.55k
        const bool is_range = wallet_descriptor.descriptor->IsRange();
34
1.55k
        wallet_descriptors.emplace_back(
35
1.55k
            descriptor,
36
1.55k
            wallet_descriptor.creation_time,
37
1.55k
            wallet.IsActiveScriptPubKeyMan(*desc_spk_man),
38
1.55k
            wallet.IsInternalScriptPubKeyMan(desc_spk_man),
39
1.55k
            is_range ? std::optional(std::make_pair(wallet_descriptor.range_start, wallet_descriptor.range_end)) : std::nullopt,
40
1.55k
            wallet_descriptor.next_index
41
1.55k
        );
42
1.55k
    }
43
208
    return wallet_descriptors;
44
208
}
45
46
util::Result<std::string> ExportWatchOnlyWallet(const CWallet& wallet, const fs::path& destination, WalletContext& context)
47
12
{
48
12
    AssertLockHeld(wallet.cs_wallet);
49
50
12
    if (destination.empty()) {
51
1
        return util::Error{_("Error: Export destination cannot be empty")};
52
1
    }
53
11
    if (fs::exists(destination)) {
54
2
        return util::Error{strprintf(_("Error: Export destination '%s' already exists"), fs::PathToString(destination))};
55
2
    }
56
9
    if (!std::ofstream{fs::PathToString(destination)}) {
57
0
        return util::Error{strprintf(_("Error: Could not create file '%s'"), fs::PathToString(destination))};
58
0
    }
59
9
    bool success = false;
60
9
    auto cleanup_destination = interfaces::MakeCleanupHandler([&success, &destination] {
61
9
        if (!success) fs::remove(destination);
62
9
    });
63
64
    // Get the descriptors from this wallet
65
9
    util::Expected<std::vector<WalletDescInfo>, std::string> exported = ExportDescriptors(wallet, /*export_private=*/false);
66
9
    if (!exported) {
67
0
        return util::Error{Untranslated(exported.error())};
68
0
    }
69
9
    if (exported->empty()) {
70
2
        return util::Error{_("Error: Wallet has no descriptors to export")};
71
2
    }
72
73
    // Make the wallet with the same flags as this wallet, but without private keys
74
7
    const uint64_t create_flags = wallet.GetWalletFlags() | WALLET_FLAG_DISABLE_PRIVATE_KEYS;
75
76
    // Create the temporary watchonly wallet in memory to avoid leaving files on disk
77
7
    std::vector<bilingual_str> warnings;
78
7
    bilingual_str error;
79
7
    WalletContext empty_context;
80
7
    empty_context.args = context.args;
81
7
    std::shared_ptr<CWallet> watchonly_wallet = CWallet::CreateNew(empty_context, /*name=*/wallet.GetName() + "_watchonly_temp", MakeInMemoryWalletDatabase(), create_flags, /*born_encrypted=*/false, error, warnings);
82
7
    if (!watchonly_wallet) {
83
0
        return util::Error{strprintf(_("Error: Failed to create new watchonly wallet. %s"), error)};
84
0
    }
85
86
7
    {
87
7
        LOCK(watchonly_wallet->cs_wallet);
88
89
        // Parse the descriptors and add them to the new wallet
90
61
        for (const WalletDescInfo& desc_info : *Assert(exported)) {
91
            // Parse the descriptor
92
61
            FlatSigningProvider dummy_keys;
93
61
            std::string dummy_err;
94
61
            std::vector<std::unique_ptr<Descriptor>> descs = Parse(desc_info.descriptor, dummy_keys, dummy_err, /*require_checksum=*/true);
95
61
            CHECK_NONFATAL(descs.size() == 1); // All of our descriptors should be valid, and not multipath
96
61
            CHECK_NONFATAL(dummy_keys.keys.size() == 0); // No private keys should be present in our exported descriptors
97
98
            // Get the range if there is one
99
61
            int32_t range_start = 0;
100
61
            int32_t range_end = 0;
101
61
            if (desc_info.range) {
102
60
                range_start = desc_info.range->first;
103
60
                range_end = desc_info.range->second;
104
60
            }
105
106
61
            WalletDescriptor w_desc(std::move(descs.at(0)), desc_info.creation_time, range_start, range_end, desc_info.next_index);
107
108
            // For descriptors that cannot self expand (i.e. needs private keys or cache), retrieve the cache
109
61
            uint256 desc_id = w_desc.id;
110
61
            if (!w_desc.descriptor->CanSelfExpand()) {
111
1
                DescriptorScriptPubKeyMan* desc_spkm = dynamic_cast<DescriptorScriptPubKeyMan*>(wallet.GetScriptPubKeyMan(desc_id));
112
1
                w_desc.cache = WITH_LOCK(desc_spkm->cs_desc_man, return desc_spkm->GetWalletDescriptor().cache);
113
1
            }
114
115
            // Add to the watchonly wallet
116
61
            if (auto spkm_res = watchonly_wallet->AddWalletDescriptor(w_desc, dummy_keys, /*label=*/"", /*internal=*/false); !spkm_res) {
117
0
                return util::Error{util::ErrorString(spkm_res)};
118
0
            }
119
120
            // Set active spkms as active
121
61
            if (desc_info.active) {
122
                // Determine whether this descriptor is internal
123
                // This is only set for active spkms
124
56
                bool internal = false;
125
56
                if (desc_info.internal) {
126
56
                    internal = *desc_info.internal;
127
56
                }
128
56
                watchonly_wallet->AddActiveScriptPubKeyMan(desc_id, *Assert(w_desc.descriptor->GetOutputType()), internal);
129
56
            }
130
61
        }
131
132
        // Copy locked coins that are persisted
133
7
        for (const auto& [coin, persisted] : wallet.m_locked_coins) {
134
2
            if (!persisted) continue;
135
1
            watchonly_wallet->LockCoin(coin, persisted);
136
1
        }
137
138
7
        {
139
            // Make a WalletBatch for the watchonly wallet so that everything else can be written atomically
140
7
            WalletBatch watchonly_batch(watchonly_wallet->GetDatabase());
141
7
            if (!watchonly_batch.TxnBegin()) {
142
0
                return util::Error{strprintf(_("Error: database transaction cannot be executed for new watchonly wallet %s"), watchonly_wallet->GetName())};
143
0
            }
144
145
            // Copy orderPosNext
146
7
            watchonly_batch.WriteOrderPosNext(wallet.nOrderPosNext);
147
148
            // Write the best block locator to avoid rescanning on reload
149
7
            CBlockLocator best_block_locator;
150
7
            {
151
7
                WalletBatch local_wallet_batch(wallet.GetDatabase());
152
7
                if (!local_wallet_batch.ReadBestBlock(best_block_locator)) {
153
0
                    return util::Error{_("Error: Unable to read wallet's best block locator record")};
154
0
                }
155
7
            }
156
7
            if (!watchonly_batch.WriteBestBlock(best_block_locator)) {
157
0
                return util::Error{_("Error: Unable to write watchonly wallet best block locator record")};
158
0
            }
159
160
            // Copy the transactions
161
7
            for (const auto& [txid, wtx] : wallet.mapWallet) {
162
6
                if (!watchonly_wallet->LoadToWallet(txid, [&](CWalletTx& ins_wtx, bool new_tx) EXCLUSIVE_LOCKS_REQUIRED(watchonly_wallet->cs_wallet) {
163
6
                    if (!new_tx) return false;
164
6
                    ins_wtx.SetTx(wtx.tx);
165
6
                    ins_wtx.CopyFrom(wtx);
166
6
                    return true;
167
6
                })) {
168
0
                    return util::Error{strprintf(_("Error: Could not add tx %s to watchonly wallet"), txid.GetHex())};
169
0
                }
170
6
                watchonly_batch.WriteTx(watchonly_wallet->mapWallet.at(txid));
171
6
            }
172
173
            // Copy address book
174
7
            for (const auto& [dest, entry] : wallet.m_address_book) {
175
7
                auto address{EncodeDestination(dest)};
176
7
                if (entry.purpose) watchonly_batch.WritePurpose(address, PurposeToString(*entry.purpose));
177
7
                if (entry.label) watchonly_batch.WriteName(address, *entry.label);
178
7
                for (const auto& [id, request] : entry.receive_requests) {
179
0
                    watchonly_batch.WriteAddressReceiveRequest(dest, id, request);
180
0
                }
181
7
                if (entry.previously_spent) watchonly_batch.WriteAddressPreviouslySpent(dest, true);
182
7
            }
183
184
7
            if (!watchonly_batch.TxnCommit()) {
185
0
                return util::Error{_("Error: cannot commit db transaction for watchonly wallet export")};
186
0
            }
187
7
        }
188
189
        // Make a backup of this wallet at the specified destination directory
190
7
        if (!watchonly_wallet->BackupWallet(fs::PathToString(destination))) {
191
0
            return util::Error{_("Error: Unable to write the exported wallet")};
192
0
        }
193
7
        success = true;
194
7
    }
195
196
0
    return fs::PathToString(destination);
197
7
}
198
} // namespace wallet