/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 |