Files
bitcoin/src/wallet/wallettool.cpp
Ava Chow 4f348c2d73 Merge bitcoin/bitcoin#28802: ArgsManager: support command-specific options
89af67d79f tests: Add some fuzz test coverage for command-specific args (Anthony Towns)
92df785859 tests: Add some test coverage for ArgsManager::AddCommand (Anthony Towns)
33c8090be9 ArgsManager: automate checking for correct command options (Anthony Towns)
186354a0d8 bitcoin-wallet: use command-specific options (Anthony Towns)
d21e82b7d6 ArgsManager: support command-specific options (Anthony Towns)

Pull request description:

  Adds the ability to link particular options to one or more (`OptionsCategory::COMMANDS`) commands, and uses this feature
  in `bitcoin-wallet`. Separates out the help information for these command-specific options (duplicating it if an option applies to multiple commands), and provides a function for checking at runtime if some options have been specified by the user that only apply to other commands.

  #### Motivation

  Currently, `ArgsManager` supports commands like `bitcoin-wallet dump` but while some of the options are command-specific (like `-dumpfile`), `ArgsManager` itself doesn't know that.  As a result, `-dumpfile` is listed in the global help rather than under the relevant commands, and if you use `-dumpfile` with a different command that doesn't support it, `ArgsManager` cannot automatically report that as an error, resulting in the commands that don't support the option having to have error-handling specific to all the options they don't support.

  #### Changes

  Help output moves command-specific options under their associated commands:

  Before:

  ```
  Options:
    -dumpfile=<file name>
         When used with 'dump', writes out the records to this file. When used
         with 'createfromdump', loads the records into a new wallet.
    ...

  Commands:
    createfromdump
         Create new wallet file from dumped records

    dump
         Print out all of the wallet key-value records
  ```

  After:

  ```
  Commands:
    createfromdump
         Create new wallet file from dumped records

         -dumpfile=<file name>
              When used with 'dump', writes out the records to this file. When
              used with 'createfromdump', loads the records into a new wallet.

    dump
         Print out all of the wallet key-value records

         -dumpfile=<file name>
              When used with 'dump', writes out the records to this file. When
              used with 'createfromdump', loads the records into a new wallet.
  ```

  Error messages are now generated automatically by `ArgsManager` rather than ad-hoc wallet code for each option:

  Before:
  ```c++
      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");
          return false;
      }
  ```

  After:
  ```c++
      std::vector<std::string> details;
      if (!args.CheckCommandOptions(command, &details)) {
          tfm::format(std::cerr, "Error: Invalid arguments provided:\n%s\n", util::MakeUnorderedList(details));
          return false;
      }
  ```

  #### Limitations

  - If an option applies to multiple commands, it shares the same help text. There's no way to provide per-command descriptions.
  - Option parsing rules are unchanged — options still cannot appear after the command.

ACKs for top commit:
  achow101:
    ACK 89af67d79f
  sedited:
    Re-ACK 89af67d79f
  ryanofsky:
    Code review ACK 89af67d79f. Since last review: rebase, integration with ClearArgs and fuzz test, and Assume -> Assert switch

Tree-SHA512: 7ae7c3b74d0c8c4db8459e9f0b9c7498b2fa4758954ec49983decbba177877b039779f0f7b55e60c3a0ed74c5e9e4ac4734ba9e049bf3a7743280ef8300869fa
2026-05-18 13:37:09 -07:00

183 lines
6.6 KiB
C++

// Copyright (c) 2016-present The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <wallet/wallettool.h>
#include <common/args.h>
#include <util/check.h>
#include <util/fs.h>
#include <util/translation.h>
#include <wallet/dump.h>
#include <wallet/wallet.h>
#include <wallet/walletutil.h>
namespace wallet {
namespace WalletTool {
// The standard wallet deleter function blocks on the validation interface
// queue, which doesn't exist for the bitcoin-wallet. Define our own
// deleter here.
static void WalletToolReleaseWallet(CWallet* wallet)
{
wallet->WalletLogPrintf("Releasing wallet\n");
wallet->Close();
delete wallet;
}
static void WalletCreate(CWallet* wallet_instance, uint64_t wallet_creation_flags)
{
LOCK(wallet_instance->cs_wallet);
wallet_instance->InitWalletFlags(wallet_creation_flags);
Assert(wallet_instance->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS));
wallet_instance->SetupDescriptorScriptPubKeyMans();
tfm::format(std::cout, "Topping up keypool...\n");
wallet_instance->TopUpKeyPool();
}
static std::shared_ptr<CWallet> MakeWallet(const std::string& name, const fs::path& path, DatabaseOptions options)
{
DatabaseStatus status;
bilingual_str error;
std::vector<bilingual_str> warnings;
std::unique_ptr<WalletDatabase> database = MakeDatabase(path, options, status, error);
if (!database) {
tfm::format(std::cerr, "%s\n", error.original);
return nullptr;
}
// dummy chain interface
std::shared_ptr<CWallet> wallet_instance{new CWallet(/*chain=*/nullptr, name, std::move(database)), WalletToolReleaseWallet};
DBErrors load_wallet_ret;
try {
load_wallet_ret = wallet_instance->PopulateWalletFromDB(error, warnings);
} catch (const std::runtime_error&) {
tfm::format(std::cerr, "Error loading %s. Is wallet being used by another process?\n", name);
return nullptr;
}
if (!error.empty()) {
tfm::format(std::cerr, "%s", error.original);
}
for (const auto &warning : warnings) {
tfm::format(std::cerr, "%s", warning.original);
}
if (load_wallet_ret != DBErrors::LOAD_OK && load_wallet_ret != DBErrors::NONCRITICAL_ERROR && load_wallet_ret != DBErrors::NEED_RESCAN) {
return nullptr;
}
if (options.require_create) WalletCreate(wallet_instance.get(), options.create_flags);
return wallet_instance;
}
static void WalletShowInfo(CWallet* wallet_instance)
{
LOCK(wallet_instance->cs_wallet);
tfm::format(std::cout, "Wallet info\n===========\n");
tfm::format(std::cout, "Name: %s\n", wallet_instance->GetName());
tfm::format(std::cout, "Format: %s\n", wallet_instance->GetDatabase().Format());
tfm::format(std::cout, "Descriptors: %s\n", wallet_instance->IsWalletFlagSet(WALLET_FLAG_DESCRIPTORS) ? "yes" : "no");
tfm::format(std::cout, "Encrypted: %s\n", wallet_instance->HasEncryptionKeys() ? "yes" : "no");
tfm::format(std::cout, "HD (hd seed available): %s\n", wallet_instance->IsHDEnabled() ? "yes" : "no");
tfm::format(std::cout, "Keypool Size: %u\n", wallet_instance->GetKeyPoolSize());
tfm::format(std::cout, "Transactions: %zu\n", wallet_instance->mapWallet.size());
tfm::format(std::cout, "Address Book: %zu\n", wallet_instance->m_address_book.size());
}
bool ExecuteWalletToolFunc(const ArgsManager& args, const std::string& command)
{
{
std::vector<std::string> details;
if (!args.CheckCommandOptions(command, &details)) {
tfm::format(std::cerr, "Error: Invalid arguments provided:\n%s\n", util::MakeUnorderedList(details));
return false;
}
}
if ((command == "create" || command == "createfromdump") && !args.IsArgSet("-wallet")) {
tfm::format(std::cerr, "Wallet name must be provided when creating a new wallet.\n");
return false;
}
const std::string name = args.GetArg("-wallet", "");
util::Result<fs::path> path_res = GetWalletPath(name);
if (!path_res) {
tfm::format(std::cerr, "%s\n", util::ErrorString(path_res).original);
return false;
}
const fs::path& path = *path_res;
if (command == "create") {
if (name.empty()) {
tfm::format(std::cerr, "Wallet name cannot be empty\n");
return false;
}
DatabaseOptions options;
ReadDatabaseArgs(args, options);
options.require_create = true;
options.create_flags |= WALLET_FLAG_DESCRIPTORS;
options.require_format = DatabaseFormat::SQLITE;
const std::shared_ptr<CWallet> wallet_instance = MakeWallet(name, path, options);
if (wallet_instance) {
WalletShowInfo(wallet_instance.get());
wallet_instance->Close();
}
} else if (command == "info") {
DatabaseOptions options;
ReadDatabaseArgs(args, options);
options.require_existing = true;
const std::shared_ptr<CWallet> wallet_instance = MakeWallet(name, path, options);
if (!wallet_instance) return false;
WalletShowInfo(wallet_instance.get());
wallet_instance->Close();
} else if (command == "dump") {
DatabaseOptions options;
ReadDatabaseArgs(args, options);
options.require_existing = true;
DatabaseStatus status;
if (IsBDBFile(BDBDataFile(path))) {
options.require_format = DatabaseFormat::BERKELEY_RO;
}
bilingual_str error;
std::unique_ptr<WalletDatabase> database = MakeDatabase(path, options, status, error);
if (!database) {
tfm::format(std::cerr, "%s\n", error.original);
return false;
}
bool ret = DumpWallet(args, *database, error);
if (!ret && !error.empty()) {
tfm::format(std::cerr, "%s\n", error.original);
return ret;
}
tfm::format(std::cout, "The dumpfile may contain private keys. To ensure the safety of your Bitcoin, do not share the dumpfile.\n");
return ret;
} else if (command == "createfromdump") {
bilingual_str error;
std::vector<bilingual_str> warnings;
bool ret = CreateFromDump(args, name, path, error, warnings);
for (const auto& warning : warnings) {
tfm::format(std::cout, "%s\n", warning.original);
}
if (!ret && !error.empty()) {
tfm::format(std::cerr, "%s\n", error.original);
}
return ret;
} else {
tfm::format(std::cerr, "Invalid command: %s\n", command);
return false;
}
return true;
}
} // namespace WalletTool
} // namespace wallet