Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Replace BResult with util::Result #25721

Merged
merged 1 commit into from Aug 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Makefile.test.include
Expand Up @@ -119,6 +119,7 @@ BITCOIN_TESTS =\
test/random_tests.cpp \
test/rbf_tests.cpp \
test/rest_tests.cpp \
test/result_tests.cpp \
test/reverselock_tests.cpp \
test/rpc_tests.cpp \
test/sanity_tests.cpp \
Expand Down
5 changes: 1 addition & 4 deletions src/bench/wallet_loading.cpp
Expand Up @@ -45,11 +45,8 @@ static void BenchUnloadWallet(std::shared_ptr<CWallet>&& wallet)

static void AddTx(CWallet& wallet)
{
const auto& dest = wallet.GetNewDestination(OutputType::BECH32, "");
assert(dest.HasRes());

CMutableTransaction mtx;
mtx.vout.push_back({COIN, GetScriptForDestination(dest.GetObj())});
mtx.vout.push_back({COIN, GetScriptForDestination(*Assert(wallet.GetNewDestination(OutputType::BECH32, "")))});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add util/check.h include header for Assert

mtx.vin.push_back(CTxIn());

wallet.AddToWallet(MakeTransactionRef(mtx), TxStateInactive{});
Expand Down
6 changes: 3 additions & 3 deletions src/interfaces/wallet.h
Expand Up @@ -88,7 +88,7 @@ class Wallet
virtual std::string getWalletName() = 0;

// Get a new address.
virtual BResult<CTxDestination> getNewDestination(const OutputType type, const std::string label) = 0;
virtual util::Result<CTxDestination> getNewDestination(const OutputType type, const std::string label) = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While touching this line here and in the override function in src/wallet/interfaces.cpp

Suggested change
virtual util::Result<CTxDestination> getNewDestination(const OutputType type, const std::string label) = 0;
virtual util::Result<CTxDestination> getNewDestination(const OutputType type, const std::string& label) = 0;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Picked up in #25616.


//! Get public key.
virtual bool getPubKey(const CScript& script, const CKeyID& address, CPubKey& pub_key) = 0;
Expand Down Expand Up @@ -139,7 +139,7 @@ class Wallet
virtual void listLockedCoins(std::vector<COutPoint>& outputs) = 0;

//! Create transaction.
virtual BResult<CTransactionRef> createTransaction(const std::vector<wallet::CRecipient>& recipients,
virtual util::Result<CTransactionRef> createTransaction(const std::vector<wallet::CRecipient>& recipients,
const wallet::CCoinControl& coin_control,
bool sign,
int& change_pos,
Expand Down Expand Up @@ -329,7 +329,7 @@ class WalletLoader : public ChainClient
virtual std::string getWalletDir() = 0;

//! Restore backup wallet
virtual BResult<std::unique_ptr<Wallet>> restoreWallet(const fs::path& backup_file, const std::string& wallet_name, std::vector<bilingual_str>& warnings) = 0;
virtual util::Result<std::unique_ptr<Wallet>> restoreWallet(const fs::path& backup_file, const std::string& wallet_name, std::vector<bilingual_str>& warnings) = 0;

//! Return available wallets in wallet directory.
virtual std::vector<std::string> listWalletDir() = 0;
Expand Down
2 changes: 1 addition & 1 deletion src/qt/addresstablemodel.cpp
Expand Up @@ -384,7 +384,7 @@ QString AddressTableModel::addRow(const QString &type, const QString &label, con
return QString();
}
}
strAddress = EncodeDestination(op_dest.GetObj());
strAddress = EncodeDestination(*op_dest);
}
else
{
Expand Down
4 changes: 2 additions & 2 deletions src/qt/walletcontroller.cpp
Expand Up @@ -393,8 +393,8 @@ void RestoreWalletActivity::restore(const fs::path& backup_file, const std::stri
QTimer::singleShot(0, worker(), [this, backup_file, wallet_name] {
auto wallet{node().walletLoader().restoreWallet(backup_file, wallet_name, m_warning_message)};

m_error_message = wallet ? bilingual_str{} : wallet.GetError();
if (wallet) m_wallet_model = m_wallet_controller->getOrCreateWallet(wallet.ReleaseObj());
m_error_message = util::ErrorString(wallet);
if (wallet) m_wallet_model = m_wallet_controller->getOrCreateWallet(std::move(*wallet));

QTimer::singleShot(0, this, &RestoreWalletActivity::finish);
});
Expand Down
4 changes: 2 additions & 2 deletions src/qt/walletmodel.cpp
Expand Up @@ -207,7 +207,7 @@ WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransact

auto& newTx = transaction.getWtx();
const auto& res = m_wallet->createTransaction(vecSend, coinControl, !wallet().privateKeysDisabled() /* sign */, nChangePosRet, nFeeRequired);
newTx = res ? res.GetObj() : nullptr;
newTx = res ? *res : nullptr;
transaction.setTransactionFee(nFeeRequired);
if (fSubtractFeeFromAmount && newTx)
transaction.reassignAmounts(nChangePosRet);
Expand All @@ -218,7 +218,7 @@ WalletModel::SendCoinsReturn WalletModel::prepareTransaction(WalletModelTransact
{
return SendCoinsReturn(AmountWithFeeExceedsBalance);
}
Q_EMIT message(tr("Send Coins"), QString::fromStdString(res.GetError().translated),
Q_EMIT message(tr("Send Coins"), QString::fromStdString(util::ErrorString(res).translated),
CClientUIInterface::MSG_ERROR);
return TransactionCreationFailed;
}
Expand Down
96 changes: 96 additions & 0 deletions src/test/result_tests.cpp
@@ -0,0 +1,96 @@
// Copyright (c) 2022 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 <util/result.h>

#include <boost/test/unit_test.hpp>

inline bool operator==(const bilingual_str& a, const bilingual_str& b)
{
return a.original == b.original && a.translated == b.translated;
}

inline std::ostream& operator<<(std::ostream& os, const bilingual_str& s)
{
return os << "bilingual_str('" << s.original << "' , '" << s.translated << "')";
}

BOOST_AUTO_TEST_SUITE(result_tests)

struct NoCopy {
NoCopy(int n) : m_n{std::make_unique<int>(n)} {}
std::unique_ptr<int> m_n;
};

bool operator==(const NoCopy& a, const NoCopy& b)
{
return *a.m_n == *b.m_n;
}

std::ostream& operator<<(std::ostream& os, const NoCopy& o)
{
return os << "NoCopy(" << *o.m_n << ")";
}

util::Result<int> IntFn(int i, bool success)
{
if (success) return i;
return util::Error{Untranslated(strprintf("int %i error.", i))};
}

util::Result<bilingual_str> StrFn(bilingual_str s, bool success)
{
if (success) return s;
return util::Error{strprintf(Untranslated("str %s error."), s.original)};
}

util::Result<NoCopy> NoCopyFn(int i, bool success)
{
if (success) return {i};
return util::Error{Untranslated(strprintf("nocopy %i error.", i))};
}

template <typename T>
void ExpectResult(const util::Result<T>& result, bool success, const bilingual_str& str)
{
BOOST_CHECK_EQUAL(bool(result), success);
BOOST_CHECK_EQUAL(util::ErrorString(result), str);
}

template <typename T, typename... Args>
void ExpectSuccess(const util::Result<T>& result, const bilingual_str& str, Args&&... args)
{
ExpectResult(result, true, str);
BOOST_CHECK_EQUAL(result.has_value(), true);
BOOST_CHECK_EQUAL(result.value(), T{std::forward<Args>(args)...});
BOOST_CHECK_EQUAL(&result.value(), &*result);
}

template <typename T, typename... Args>
void ExpectFail(const util::Result<T>& result, const bilingual_str& str)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add static for the non-template functions in this test file?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

template should imply inline, which again implies that this can't be used in another translation unit, but correct me if I am wrong.

{
ExpectResult(result, false, str);
}

BOOST_AUTO_TEST_CASE(check_returned)
{
ExpectSuccess(IntFn(5, true), {}, 5);
ExpectFail(IntFn(5, false), Untranslated("int 5 error."));
ExpectSuccess(NoCopyFn(5, true), {}, 5);
ExpectFail(NoCopyFn(5, false), Untranslated("nocopy 5 error."));
ExpectSuccess(StrFn(Untranslated("S"), true), {}, Untranslated("S"));
ExpectFail(StrFn(Untranslated("S"), false), Untranslated("str S error."));
}

BOOST_AUTO_TEST_CASE(check_value_or)
{
BOOST_CHECK_EQUAL(IntFn(10, true).value_or(20), 10);
BOOST_CHECK_EQUAL(IntFn(10, false).value_or(20), 20);
BOOST_CHECK_EQUAL(NoCopyFn(10, true).value_or(20), 10);
BOOST_CHECK_EQUAL(NoCopyFn(10, false).value_or(20), 20);
BOOST_CHECK_EQUAL(StrFn(Untranslated("A"), true).value_or(Untranslated("B")), Untranslated("A"));
BOOST_CHECK_EQUAL(StrFn(Untranslated("A"), false).value_or(Untranslated("B")), Untranslated("B"));
}

BOOST_AUTO_TEST_SUITE_END()
6 changes: 2 additions & 4 deletions src/test/util/wallet.cpp
Expand Up @@ -8,6 +8,7 @@
#include <outputtype.h>
#include <script/standard.h>
#ifdef ENABLE_WALLET
#include <util/check.h>
#include <util/translation.h>
#include <wallet/wallet.h>
#endif
Expand All @@ -20,10 +21,7 @@ const std::string ADDRESS_BCRT1_UNSPENDABLE = "bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqq
std::string getnewaddress(CWallet& w)
{
constexpr auto output_type = OutputType::BECH32;
auto op_dest = w.GetNewDestination(output_type, "");
assert(op_dest.HasRes());

return EncodeDestination(op_dest.GetObj());
return EncodeDestination(*Assert(w.GetNewDestination(output_type, "")));
}

#endif // ENABLE_WALLET
87 changes: 61 additions & 26 deletions src/util/result.h
Expand Up @@ -5,45 +5,80 @@
#ifndef BITCOIN_UTIL_RESULT_H
#define BITCOIN_UTIL_RESULT_H

#include <attributes.h>
#include <util/translation.h>

#include <variant>

/*
* 'BResult' is a generic class useful for wrapping a return object
* (in case of success) or propagating the error cause.
*/
template<class T>
class BResult {
namespace util {

struct Error {
bilingual_str message;
};

//! The util::Result class provides a standard way for functions to return
//! either error messages or result values.
//!
//! It is intended for high-level functions that need to report error strings to
//! end users. Lower-level functions that don't need this error-reporting and
//! only need error-handling should avoid util::Result and instead use standard
//! classes like std::optional, std::variant, and std::tuple, or custom structs
//! and enum types to return function results.
//!
//! Usage examples can be found in \example ../test/result_tests.cpp, but in
//! general code returning `util::Result<T>` values is very similar to code
//! returning `std::optional<T>` values. Existing functions returning
//! `std::optional<T>` can be updated to return `util::Result<T>` and return
//! error strings usually just replacing `return std::nullopt;` with `return
//! util::Error{error_string};`.
template <class T>
class Result
{
private:
std::variant<bilingual_str, T> m_variant;

public:
BResult() : m_variant{Untranslated("")} {}
BResult(T obj) : m_variant{std::move(obj)} {}
BResult(bilingual_str error) : m_variant{std::move(error)} {}
template <typename FT>
ryanofsky marked this conversation as resolved.
Show resolved Hide resolved
friend bilingual_str ErrorString(const Result<FT>& result);

/* Whether the function succeeded or not */
bool HasRes() const { return std::holds_alternative<T>(m_variant); }
public:
Result(T obj) : m_variant{std::in_place_index_t<1>{}, std::move(obj)} {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

styling nit: what about creating an enum for the indexes?

enum { ERR=0, VAL=1 };
// then
std::in_place_index_t<VAL>{} 

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #25721 (comment)

enum { ERR=0, VAL=1 };
// then
std::in_place_index_t<VAL>{} 

#25665 should drop the std::variant entirely making this moot, but using enum for this seems more indirect and fragile since nothing keeps enum and variant declarations lined up. I'm not against adding this, but would prefer not to unless there is more demand

Result(Error error) : m_variant{std::in_place_index_t<0>{}, std::move(error.message)} {}

/* In case of success, the result object */
const T& GetObj() const {
assert(HasRes());
return std::get<T>(m_variant);
//! std::optional methods, so functions returning optional<T> can change to
//! return Result<T> with minimal changes to existing code, and vice versa.
bool has_value() const noexcept { return m_variant.index() == 1; }
const T& value() const LIFETIMEBOUND
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #25721 (comment)

same?

Can throw bad_variant_access according to https://en.cppreference.com/w/cpp/utility/variant/get. The equivalent std::optional method can also throw https://en.cppreference.com/w/cpp/utility/optional/value

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it can throw after our assert, can it?

{
assert(has_value());
return std::get<1>(m_variant);
}
T ReleaseObj()
T& value() LIFETIMEBOUND
ryanofsky marked this conversation as resolved.
Show resolved Hide resolved
{
assert(HasRes());
return std::move(std::get<T>(m_variant));
assert(has_value());
return std::get<1>(m_variant);
}

/* In case of failure, the error cause */
const bilingual_str& GetError() const {
assert(!HasRes());
return std::get<bilingual_str>(m_variant);
template <class U>
T value_or(U&& default_value) const&
{
return has_value() ? value() : std::forward<U>(default_value);
}

explicit operator bool() const { return HasRes(); }
template <class U>
T value_or(U&& default_value) &&
{
return has_value() ? std::move(value()) : std::forward<U>(default_value);
}
explicit operator bool() const noexcept { return has_value(); }
const T* operator->() const LIFETIMEBOUND { return &value(); }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: #25721 (comment)

Same, per https://en.cppreference.com/w/cpp/utility/optional/operator*

I guess this is a difference between std::optional and std::variant. It doesn't seem like there is a way to get the address of an object inside a variant that is noexcept. it would be possible to make this noexcept in followup #25665 which removes the std::variant.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be possible, given our assert, no?

const T& operator*() const LIFETIMEBOUND { return value(); }
T* operator->() LIFETIMEBOUND { return &value(); }
T& operator*() LIFETIMEBOUND { return value(); }
};

template <typename T>
bilingual_str ErrorString(const Result<T>& result)
{
return result ? bilingual_str{} : std::get<0>(result.m_variant);
}
} // namespace util

#endif // BITCOIN_UTIL_RESULT_H
4 changes: 2 additions & 2 deletions src/wallet/feebumper.cpp
Expand Up @@ -221,11 +221,11 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo
constexpr int RANDOM_CHANGE_POSITION = -1;
auto res = CreateTransaction(wallet, recipients, RANDOM_CHANGE_POSITION, new_coin_control, false);
if (!res) {
errors.push_back(Untranslated("Unable to create transaction.") + Untranslated(" ") + res.GetError());
errors.push_back(Untranslated("Unable to create transaction.") + Untranslated(" ") + util::ErrorString(res));
return Result::WALLET_ERROR;
}

const auto& txr = res.GetObj();
const auto& txr = *res;
// Write back new fee if successful
new_fee = txr.fee;

Expand Down
16 changes: 8 additions & 8 deletions src/wallet/interfaces.cpp
Expand Up @@ -148,7 +148,7 @@ class WalletImpl : public Wallet
void abortRescan() override { m_wallet->AbortRescan(); }
bool backupWallet(const std::string& filename) override { return m_wallet->BackupWallet(filename); }
std::string getWalletName() override { return m_wallet->GetName(); }
BResult<CTxDestination> getNewDestination(const OutputType type, const std::string label) override
util::Result<CTxDestination> getNewDestination(const OutputType type, const std::string label) override
{
LOCK(m_wallet->cs_wallet);
return m_wallet->GetNewDestination(type, label);
Expand Down Expand Up @@ -251,17 +251,17 @@ class WalletImpl : public Wallet
LOCK(m_wallet->cs_wallet);
return m_wallet->ListLockedCoins(outputs);
}
BResult<CTransactionRef> createTransaction(const std::vector<CRecipient>& recipients,
util::Result<CTransactionRef> createTransaction(const std::vector<CRecipient>& recipients,
const CCoinControl& coin_control,
bool sign,
int& change_pos,
CAmount& fee) override
{
LOCK(m_wallet->cs_wallet);
const auto& res = CreateTransaction(*m_wallet, recipients, change_pos,
auto res = CreateTransaction(*m_wallet, recipients, change_pos,
coin_control, sign);
if (!res) return res.GetError();
const auto& txr = res.GetObj();
if (!res) return util::Error{util::ErrorString(res)};
const auto& txr = *res;
fee = txr.fee;
change_pos = txr.change_pos;

Expand Down Expand Up @@ -571,12 +571,12 @@ class WalletLoaderImpl : public WalletLoader
options.require_existing = true;
return MakeWallet(m_context, LoadWallet(m_context, name, true /* load_on_start */, options, status, error, warnings));
}
BResult<std::unique_ptr<Wallet>> restoreWallet(const fs::path& backup_file, const std::string& wallet_name, std::vector<bilingual_str>& warnings) override
util::Result<std::unique_ptr<Wallet>> restoreWallet(const fs::path& backup_file, const std::string& wallet_name, std::vector<bilingual_str>& warnings) override
{
DatabaseStatus status;
bilingual_str error;
BResult<std::unique_ptr<Wallet>> wallet{MakeWallet(m_context, RestoreWallet(m_context, backup_file, wallet_name, /*load_on_start=*/true, status, error, warnings))};
if (!wallet) return error;
util::Result<std::unique_ptr<Wallet>> wallet{MakeWallet(m_context, RestoreWallet(m_context, backup_file, wallet_name, /*load_on_start=*/true, status, error, warnings))};
if (!wallet) return util::Error{error};
return wallet;
}
std::string getWalletDir() override
Expand Down
8 changes: 4 additions & 4 deletions src/wallet/rpc/addresses.cpp
Expand Up @@ -60,10 +60,10 @@ RPCHelpMan getnewaddress()

auto op_dest = pwallet->GetNewDestination(output_type, label);
if (!op_dest) {
throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, op_dest.GetError().original);
throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, util::ErrorString(op_dest).original);
}

return EncodeDestination(op_dest.GetObj());
return EncodeDestination(*op_dest);
},
};
}
Expand Down Expand Up @@ -107,9 +107,9 @@ RPCHelpMan getrawchangeaddress()

auto op_dest = pwallet->GetNewChangeDestination(output_type);
if (!op_dest) {
throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, op_dest.GetError().original);
throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, util::ErrorString(op_dest).original);
}
return EncodeDestination(op_dest.GetObj());
return EncodeDestination(*op_dest);
},
};
}
Expand Down