Coverage Report

Created: 2026-07-08 14:14

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