diff --git a/src/bitcoin-wallet.cpp b/src/bitcoin-wallet.cpp index d5dfbbec27..fc75c0fb53 100644 --- a/src/bitcoin-wallet.cpp +++ b/src/bitcoin-wallet.cpp @@ -50,6 +50,7 @@ static void SetupWalletToolArgs(ArgsManager& argsman) argsman.AddCommand("salvage", "Attempt to recover private keys from a corrupt wallet. Warning: 'salvage' is experimental."); argsman.AddCommand("dump", "Print out all of the wallet key-value records"); argsman.AddCommand("createfromdump", "Create new wallet file from dumped records"); + argsman.AddCommand("importfromcoldcard", "Create new wallet file and import descriptors from Coldcard wallet"); } static std::optional WalletAppInit(ArgsManager& args, int argc, char* argv[]) diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp index 0e7787586e..3e2a3e381b 100644 --- a/src/wallet/rpc/backup.cpp +++ b/src/wallet/rpc/backup.cpp @@ -1486,7 +1486,7 @@ RPCHelpMan importmulti() }; } -static UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet) +UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet) { UniValue warnings(UniValue::VARR); UniValue result(UniValue::VOBJ); diff --git a/src/wallet/wallettool.cpp b/src/wallet/wallettool.cpp index cda344ab19..8afb6a1ff1 100644 --- a/src/wallet/wallettool.cpp +++ b/src/wallet/wallettool.cpp @@ -9,6 +9,8 @@ #include #include +#include +#include #include #include #include @@ -16,7 +18,15 @@ #include #include +#include +#include +#include + namespace wallet { + +UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, const int64_t timestamp) + EXCLUSIVE_LOCKS_REQUIRED(wallet.cs_wallet); + namespace WalletTool { // The standard wallet deleter function blocks on the validation interface @@ -36,6 +46,10 @@ static void WalletCreate(CWallet* wallet_instance, uint64_t wallet_creation_flag wallet_instance->SetMinVersion(FEATURE_LATEST); wallet_instance->InitWalletFlags(wallet_creation_flags); + if (wallet_instance->IsWalletFlagSet(WALLET_FLAG_BLANK_WALLET)) { + return; + } + if (!wallet_instance->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS)) { auto spk_man = wallet_instance->GetOrCreateLegacyScriptPubKeyMan(); spk_man->SetupGeneration(false); @@ -112,14 +126,40 @@ static void WalletShowInfo(CWallet* wallet_instance) tfm::format(std::cout, "Address Book: %zu\n", wallet_instance->m_address_book.size()); } +static bool ReadAndParseColdcardFile(const fs::path& path, UniValue& decriptors) +{ + std::ifstream file; + file.open(path); + if (!file.is_open()) { + tfm::format(std::cerr, "%s. Please check permissions.\n", fs::PathToString(path)); + return false; + } + + std::string line; + while (std::getline(file, line)) { + if (line.substr(0, 22) == "importdescriptors \'[{\"") break; + } + + file.close(); + + decriptors.clear(); + if (!decriptors.read(line.substr(19, line.size() - 20))) { + tfm::format(std::cerr, "Unable to parse %s\n", fs::PathToString(path)); + return false; + } + + assert(decriptors.isArray()); + return true; +} + bool ExecuteWalletToolFunc(const ArgsManager& args, const std::string& command) { if (args.IsArgSet("-format") && command != "createfromdump") { tfm::format(std::cerr, "The -format option can only be used with the \"createfromdump\" command.\n"); return false; } - if (args.IsArgSet("-dumpfile") && command != "dump" && command != "createfromdump") { - tfm::format(std::cerr, "The -dumpfile option can only be used with the \"dump\" and \"createfromdump\" commands.\n"); + if (args.IsArgSet("-dumpfile") && command != "dump" && command != "createfromdump" && command != "importfromcoldcard") { + tfm::format(std::cerr, "The -dumpfile option can only be used with the \"dump\", \"createfromdump\" and \"importfromcoldcard\" commands.\n"); return false; } if (args.IsArgSet("-descriptors") && command != "create") { @@ -219,6 +259,45 @@ bool ExecuteWalletToolFunc(const ArgsManager& args, const std::string& command) tfm::format(std::cerr, "%s\n", error.original); } return ret; + } else if (command == "importfromcoldcard") { + tfm::format(std::cerr, "WARNING: The \"importfromcoldcard\" command is experimental and will likely be removed or changed incompatibly in a future version.\n"); + + std::string filename = gArgs.GetArg("-dumpfile", ""); + if (filename.empty()) { + tfm::format(std::cerr, "To use importfromcoldcard, -dumpfile= must be provided.\n"); + return false; + } + + const fs::path import_file_path{fs::absolute(fs::PathFromString(filename))}; + if (!fs::exists(import_file_path)) { + tfm::format(std::cerr, "File %s does not exist.\n", fs::PathToString(import_file_path)); + return false; + } + + UniValue descriptors; + if (!ReadAndParseColdcardFile(import_file_path, descriptors)) { + return false; + } + + DatabaseOptions options; + options.require_create = true; + options.create_flags |= WALLET_FLAG_DESCRIPTORS; + options.create_flags |= WALLET_FLAG_DISABLE_PRIVATE_KEYS; + options.create_flags |= WALLET_FLAG_BLANK_WALLET; + options.require_format = DatabaseFormat::SQLITE; + std::shared_ptr wallet_instance = MakeWallet(name, path, options); + if (!wallet_instance) { + return false; + } + + LOCK(wallet_instance->cs_wallet); + for (const UniValue& descriptor : descriptors.getValues()) { + const UniValue result = ProcessDescriptorImport(*wallet_instance, descriptor, 0); + tfm::format(std::cerr, "%s\n", result.write(2)); + } + + WalletShowInfo(wallet_instance.get()); + wallet_instance->Close(); } else { tfm::format(std::cerr, "Invalid command: %s\n", command); return false;