Coverage Report

Created: 2026-07-14 18:13

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