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