Fix max outputs in tx construction, and fix static asserts

This commit is contained in:
Lee Clagett
2025-05-27 02:28:18 -04:00
parent c1c2c64320
commit ec98d76b69
12 changed files with 350 additions and 217 deletions
+2 -1
View File
@@ -29,6 +29,7 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <optional>
#include <string> #include <string>
#include <vector> #include <vector>
#include "wallet/api/wallet2_api.h" // monero/src #include "wallet/api/wallet2_api.h" // monero/src
@@ -36,7 +37,7 @@
namespace lwsf namespace lwsf
{ {
std::string displayAmount(std::uint64_t amount); std::string displayAmount(std::uint64_t amount);
std::uint64_t amountFromString(const std::string &amount); std::optional<std::uint64_t> amountFromString(const std::string &amount);
bool addressValid(const std::string &str, Monero::NetworkType nettype); bool addressValid(const std::string &str, Monero::NetworkType nettype);
std::vector<std::vector<std::uint8_t>> qrcode(Monero::Wallet const* wal, std::uint32_t major, std::uint32_t minor); std::vector<std::vector<std::uint8_t>> qrcode(Monero::Wallet const* wal, std::uint32_t major, std::uint32_t minor);
+10 -3
View File
@@ -576,8 +576,15 @@ namespace lwsf { namespace internal { namespace backend
self.primary.restore_height = source.start_height; self.primary.restore_height = source.start_height;
self.primary.scan_height = source.scanned_block_height; self.primary.scan_height = source.scanned_block_height;
self.per_byte_fee = unspents.per_byte_fee;
self.fee_mask = unspents.fee_mask; self.fee_mask = unspents.fee_mask;
if (unspents.fees.empty())
{
self.per_byte_fee.resize(1);
self.per_byte_fee[0] = unspents.per_byte_fee;
}
else
self.per_byte_fee = unspents.fees;
return out; return out;
} }
@@ -787,12 +794,12 @@ namespace lwsf { namespace internal { namespace backend
: listener(nullptr), : listener(nullptr),
client(), client(),
primary{}, primary{},
per_byte_fee(),
refresh_error(), refresh_error(),
lookahead_error(default_subaddr_state), lookahead_error(default_subaddr_state),
import_error(), import_error(),
last_sync(), last_sync(),
blockchain_height(0), blockchain_height(0),
per_byte_fee(0),
fee_mask(0), fee_mask(0),
server_lookahead(0), server_lookahead(0),
sync(), sync(),
@@ -879,7 +886,7 @@ namespace lwsf { namespace internal { namespace backend
{ {
passed_login = false; passed_login = false;
blockchain_height = 0; blockchain_height = 0;
per_byte_fee = 0; per_byte_fee.clear();
fee_mask = 0; fee_mask = 0;
server_lookahead = 0; server_lookahead = 0;
import_error = {}; import_error = {};
+1 -1
View File
@@ -227,12 +227,12 @@ namespace lwsf { namespace internal { namespace backend
Monero::WalletListener* listener; Monero::WalletListener* listener;
rpc::http_client client; rpc::http_client client;
account primary; account primary;
std::vector<std::uint64_t> per_byte_fee; //!< by priority level
std::error_code refresh_error; //!< Cached because `refresh(...)` is rate limited std::error_code refresh_error; //!< Cached because `refresh(...)` is rate limited
std::error_code lookahead_error; //!< warnings/errors of `server_lookahead` value std::error_code lookahead_error; //!< warnings/errors of `server_lookahead` value
std::error_code import_error; //!< Error from `import_wallet_request` std::error_code import_error; //!< Error from `import_wallet_request`
std::chrono::steady_clock::time_point last_sync; std::chrono::steady_clock::time_point last_sync;
std::uint64_t blockchain_height; std::uint64_t blockchain_height;
std::uint64_t per_byte_fee;
std::uint64_t fee_mask; std::uint64_t fee_mask;
std::uint32_t server_lookahead; //!< Status of major lookahead server-side. Inclusive std::uint32_t server_lookahead; //!< Status of major lookahead server-side. Inclusive
mutable boost::mutex sync; mutable boost::mutex sync;
-20
View File
@@ -55,26 +55,6 @@ namespace lwsf
return "server has subaddresses disabled"; return "server has subaddresses disabled";
case error::subaddr_local: case error::subaddr_local:
return "lwsf (local) limits on subaddresses too small"; return "lwsf (local) limits on subaddresses too small";
case error::tx_critical:
return "Tx construction had critical error";
case error::tx_decoys:
return "Tx construction failed due to invalid decoy response";
case error::tx_extra:
return "Tx construction failed to generate tx extra";
case error::tx_failed:
return "Tx construction failed (construct_tx failure)";
case error::tx_invalid_address:
return "Tx construction was given invalid address";
case error::tx_long_pid:
return "Tx construction requested with deprecated long payment id";
case error::tx_low_funds:
return "Tx construction failed due to insufficient funds";
case error::tx_size_mismatch:
return "Tx construction requested with amount and destination mismatch";
case error::tx_sweep:
return "Tx construction sweep with multiple destinations";
case error::tx_two_pid:
return "Tx construction given two payment ids";
case error::unexpected_userinfo: case error::unexpected_userinfo:
return "Unexpected user+pass field in URL"; return "Unexpected user+pass field in URL";
-10
View File
@@ -45,16 +45,6 @@ namespace lwsf
subaddr_ahead, //!< Server limits on subaddresses affects lookahead subaddr_ahead, //!< Server limits on subaddresses affects lookahead
subaddr_disabled, //!< Server has subaddresses disabled subaddr_disabled, //!< Server has subaddresses disabled
subaddr_local, //!< Local limits on subaddresses too small subaddr_local, //!< Local limits on subaddresses too small
tx_critical, //!< Tx creation had critical error
tx_decoys, //!< Tx creation failed due to invalid decoy response
tx_extra, //!< Tx creation failed to generate tx extra
tx_failed, //!< Tx creation failed (contruct_tx failure)
tx_invalid_address, //!< Tx creation was given invalid address
tx_long_pid, //!< Tx creation with long payment_id (deprecated)
tx_low_funds, //!< Tx creation failed due to insufficient funds
tx_size_mismatch, //!< Tx creation mismatch on amounts and destinations
tx_two_pid, //!< Tx creation given two payment ids
tx_sweep, //!< Tx creation requested sweep to multiple dests
unexpected_userinfo,//!< Unexpected user+pass provided unexpected_userinfo,//!< Unexpected user+pass provided
}; };
+4 -3
View File
@@ -48,10 +48,11 @@ namespace lwsf { namespace config
constexpr const lookahead default_minimal_lookahead{5, 15}; constexpr const lookahead default_minimal_lookahead{5, 15};
constexpr const std::string_view default_primary_name{"Primary account"}; constexpr const std::string_view default_primary_name{"Primary account"};
constexpr const std::size_t initial_buffer_size = 1024 * 64; // 64 KiB constexpr const std::size_t initial_buffer_size = 1024 * 64; // 64 KiB
constexpr const std::size_t max_inputs = 512; constexpr const std::size_t max_inputs_in_rpc = 512;
constexpr const std::size_t max_ring_size = 128; constexpr const std::size_t max_outputs_in_construction = 16;
constexpr const std::size_t max_ring_size_in_rpc = 128;
constexpr const std::size_t max_txes_in_rpc = 2048; constexpr const std::size_t max_txes_in_rpc = 2048;
constexpr const std::size_t minimum_outputs = 2; constexpr const std::size_t min_outputs = 2;
constexpr const std::uint32_t mixin_default = 15; constexpr const std::uint32_t mixin_default = 15;
constexpr const std::chrono::seconds refresh_interval{30}; constexpr const std::chrono::seconds refresh_interval{30};
constexpr const std::chrono::seconds refresh_interval_min{5}; constexpr const std::chrono::seconds refresh_interval_min{5};
+33 -30
View File
@@ -47,11 +47,17 @@ namespace lwsf { namespace internal
return {}; return {};
} }
} }
pending_transaction::pending_transaction(std::shared_ptr<backend::wallet> wallet, expect<cryptonote::transaction> source, std::shared_ptr<backend::transaction> local) pending_transaction::pending_transaction(std::shared_ptr<backend::wallet> wallet, std::string error, std::vector<std::shared_ptr<backend::transaction>> local)
: wallet_(std::move(wallet)), error_(source.error()), local_(std::move(local)) : wallet_(std::move(wallet)), local_(std::move(local)), error_(std::move(error))
{ {
if (!wallet_) if (!wallet_)
throw std::invalid_argument{"lwsf::internal::pending_transction cannot be given nullptr backend"}; throw std::invalid_argument{"lwsf::internal::pending_transaction cannot be given nullptr backend"};
for (const auto& e : local)
{
if (!e)
throw std::invalid_argument{"lwsf::internal::pending_transaction cannot be given nullptr backend::transaction"};
}
} }
pending_transaction::~pending_transaction() pending_transaction::~pending_transaction()
@@ -59,56 +65,53 @@ namespace lwsf { namespace internal
int pending_transaction::status() const int pending_transaction::status() const
{ {
return error_ ? Status_Error : Status_Ok; return error_.empty() ? Status_Ok : Status_Error;
}
std::string pending_transaction::errorString() const
{
if (error_)
return error_.message();
return {};
} }
bool pending_transaction::commit(const std::string &filename, bool) bool pending_transaction::commit(const std::string &filename, bool)
{ {
// saving to file not yet implemented // saving to file not yet implemented
if (error_ || !filename.empty()) if (!error_.empty() || !filename.empty())
return false; return false;
if (local_) for (auto e : local_)
error_ = wallet_->send_tx(local_->raw_bytes.clone());
else if (!error_)
error_ = error::tx_failed;
if (!error_ && local_)
{ {
const std::error_code error = wallet_->send_tx(e->raw_bytes.clone());
if (error)
{
error_ = error.message();
return false;
}
const boost::lock_guard<boost::mutex> lock{wallet_->sync}; const boost::lock_guard<boost::mutex> lock{wallet_->sync};
auto entry = wallet_->primary.txes.try_emplace(local_->id).first; auto entry = wallet_->primary.txes.try_emplace(e->id).first;
if (!entry->second) if (!entry->second)
entry->second = local_; entry->second = std::move(e);
} }
return !error_; return true;
} }
std::uint64_t pending_transaction::amount() const std::uint64_t pending_transaction::amount() const
{ {
if (!local_) std::uint64_t out = 0;
return 0; for (const auto& e : local_)
return local_->amount; out += e->amount;
return out;
} }
std::uint64_t pending_transaction::fee() const std::uint64_t pending_transaction::fee() const
{ {
if (!local_) std::uint64_t out = 0;
return 0; for (const auto& e : local_)
return local_->fee; out += e->fee;
return out;
} }
std::vector<std::string> pending_transaction::txid() const std::vector<std::string> pending_transaction::txid() const
{ {
if (!local_) std::vector<std::string> out;
return {}; for (const auto& e : local_)
return {epee::to_hex::string(epee::as_byte_span(local_->id))}; out.push_back(epee::to_hex::string(epee::as_byte_span(e->id)));
return out;
} }
}} // lwsf // internal }} // lwsf // internal
+5 -5
View File
@@ -49,11 +49,11 @@ namespace lwsf { namespace internal
class pending_transaction final : public Monero::PendingTransaction class pending_transaction final : public Monero::PendingTransaction
{ {
const std::shared_ptr<backend::wallet> wallet_; const std::shared_ptr<backend::wallet> wallet_;
std::error_code error_; const std::vector<std::shared_ptr<backend::transaction>> local_;
const std::shared_ptr<backend::transaction> local_; std::string error_;
public: public:
pending_transaction(std::shared_ptr<backend::wallet> wallet, expect<cryptonote::transaction> source, std::shared_ptr<backend::transaction> local = {}); pending_transaction(std::shared_ptr<backend::wallet> wallet, std::string error, std::vector<std::shared_ptr<backend::transaction>> local = {});
pending_transaction(pending_transaction&&) = delete; pending_transaction(pending_transaction&&) = delete;
pending_transaction(const pending_transaction&) = delete; pending_transaction(const pending_transaction&) = delete;
@@ -64,13 +64,13 @@ namespace lwsf { namespace internal
pending_transaction& operator=(const pending_transaction&) = delete; pending_transaction& operator=(const pending_transaction&) = delete;
virtual int status() const override final; virtual int status() const override final;
virtual std::string errorString() const override final; virtual std::string errorString() const override final { return error_; }
virtual bool commit(const std::string &filename = "", bool overwrite = false) override final; virtual bool commit(const std::string &filename = "", bool overwrite = false) override final;
virtual std::uint64_t amount() const override final; virtual std::uint64_t amount() const override final;
virtual std::uint64_t dust() const override final { return 0; } virtual std::uint64_t dust() const override final { return 0; }
virtual std::uint64_t fee() const override final; virtual std::uint64_t fee() const override final;
virtual std::vector<std::string> txid() const override final; virtual std::vector<std::string> txid() const override final;
virtual std::uint64_t txCount() const override final { return 1; } virtual std::uint64_t txCount() const override final { return local_.size(); }
virtual std::vector<uint32_t> subaddrAccount() const override final { return {}; } virtual std::vector<uint32_t> subaddrAccount() const override final { return {}; }
virtual std::vector<std::set<uint32_t>> subaddrIndices() const override final { return {}; } virtual std::vector<std::set<uint32_t>> subaddrIndices() const override final { return {}; }
+4 -2
View File
@@ -262,7 +262,7 @@ namespace lwsf { namespace internal { namespace rpc
void read_bytes(wire::json_reader& source, random_outputs& self) void read_bytes(wire::json_reader& source, random_outputs& self)
{ {
using max_ring = wire::max_element_count<config::max_ring_size>; using max_ring = wire::max_element_count<config::max_ring_size_in_rpc>;
wire::object(source, WIRE_FIELD_ARRAY(outputs, max_ring), WIRE_FIELD(amount)); wire::object(source, WIRE_FIELD_ARRAY(outputs, max_ring), WIRE_FIELD(amount));
} }
@@ -274,7 +274,7 @@ namespace lwsf { namespace internal { namespace rpc
void read_bytes(wire::json_reader& source, get_random_outs_response& self) void read_bytes(wire::json_reader& source, get_random_outs_response& self)
{ {
using max_inputs = wire::max_element_count<config::max_inputs>; using max_inputs = wire::max_element_count<config::max_inputs_in_rpc>;
wire::object(source, WIRE_FIELD_ARRAY(amount_outs, max_inputs)); wire::object(source, WIRE_FIELD_ARRAY(amount_outs, max_inputs));
} }
@@ -421,8 +421,10 @@ namespace lwsf { namespace internal { namespace rpc
using min_output_size = wire::min_element_sizeof< using min_output_size = wire::min_element_sizeof<
ringct, crypto::hash, crypto::hash, crypto::public_key, crypto::public_key ringct, crypto::hash, crypto::hash, crypto::public_key, crypto::public_key
>; >;
using max_fees = wire::max_element_count<8>;
wire::object(source, wire::object(source,
WIRE_FIELD_ARRAY(outputs, min_output_size), WIRE_FIELD_ARRAY(outputs, min_output_size),
WIRE_FIELD_ARRAY(fees, max_fees),
WIRE_FIELD(per_byte_fee), WIRE_FIELD(per_byte_fee),
WIRE_FIELD(fee_mask) WIRE_FIELD(fee_mask)
); );
+1
View File
@@ -370,6 +370,7 @@ namespace lwsf { namespace internal { namespace rpc
static constexpr const char* endpoint() noexcept { return "/get_unspent_outs"; } static constexpr const char* endpoint() noexcept { return "/get_unspent_outs"; }
std::vector<output> outputs; std::vector<output> outputs;
std::vector<std::uint64_t> fees;
std::uint64_t per_byte_fee; std::uint64_t per_byte_fee;
std::uint64_t fee_mask; std::uint64_t fee_mask;
}; };
+284 -137
View File
@@ -33,6 +33,7 @@
#endif #endif
#include <algorithm> #include <algorithm>
#include <boost/range/combine.hpp>
#include <exception> #include <exception>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
@@ -63,11 +64,11 @@
#include "wire/msgpack.h" #include "wire/msgpack.h"
//! Runtime-check (assertion) for tx construction //! Runtime-check (assertion) for tx construction
#define LWSF_TX_ASSERT(x) \ #define LWSF_TX_VERIFY(x) \
do \ do \
{ \ { \
if (!(x)) \ if (!(x)) \
throw std::logic_error{"Tx construction verify (line " + std::to_string(__LINE__) + "): " + #x}; \ throw std::logic_error{"Tx construction assertion failed (line " + std::to_string(__LINE__) + "): " + #x}; \
} while (0) } while (0)
namespace lwsf { namespace internal namespace lwsf { namespace internal
@@ -88,14 +89,14 @@ namespace lwsf { namespace internal
} }
}; };
std::uint64_t calculate_fee_from_weight(const std::uint64_t base_fee, const std::uint64_t weight, const std::uint64_t fee_quantization_mask) std::uint64_t calculate_fee_from_weight(const std::uint64_t base_fee, const std::uint64_t weight, const std::uint64_t fee_quantization_mask) noexcept
{ {
uint64_t fee = weight * base_fee; uint64_t fee = weight * base_fee;
fee = (fee + fee_quantization_mask - 1) / fee_quantization_mask * fee_quantization_mask; fee = (fee + fee_quantization_mask - 1) / fee_quantization_mask * fee_quantization_mask;
return fee; return fee;
} }
std::size_t estimate_rct_tx_size(const std::size_t n_inputs, const std::size_t n_outputs, const std::uint32_t mixin, const std::size_t extra_size) std::size_t estimate_rct_tx_size(const std::size_t n_inputs, const std::size_t n_outputs, const std::uint32_t mixin, const std::size_t extra_size) noexcept
{ {
std::size_t size = 0; std::size_t size = 0;
@@ -155,7 +156,7 @@ namespace lwsf { namespace internal
return size; return size;
} }
std::uint64_t estimate_tx_weight(const std::size_t n_inputs, const std::size_t n_outputs, const std::uint32_t mixin, const std::size_t extra_size) std::uint64_t estimate_tx_weight(const std::size_t n_inputs, const std::size_t n_outputs, const std::uint32_t mixin, const std::size_t extra_size) noexcept
{ {
size_t size = estimate_rct_tx_size(n_inputs, n_outputs, mixin, extra_size); size_t size = estimate_rct_tx_size(n_inputs, n_outputs, mixin, extra_size);
if (/*use_rct && (bulletproof || bulletproof_plus) && */n_outputs > 2) if (/*use_rct && (bulletproof || bulletproof_plus) && */n_outputs > 2)
@@ -173,23 +174,11 @@ namespace lwsf { namespace internal
return size; return size;
} }
std::uint64_t estimate_fee(const std::uint64_t base_fee, const std::size_t n_inputs, const std::size_t n_outputs, const std::uint32_t mixin, const std::size_t extra_size, const std::uint64_t fee_mask) std::uint64_t estimate_fee(const std::uint64_t base_fee, const std::size_t n_inputs, const std::size_t n_outputs, const std::uint32_t mixin, const std::size_t extra_size, const std::uint64_t fee_mask) noexcept
{ {
return calculate_fee_from_weight(base_fee, estimate_tx_weight(n_inputs, n_outputs, mixin, extra_size), fee_mask); return calculate_fee_from_weight(base_fee, estimate_tx_weight(n_inputs, n_outputs, mixin, extra_size), fee_mask);
} }
class need_more_sources final : public std::exception
{
std::uint64_t fee_;
public:
explicit need_more_sources(const std::uint64_t fee) noexcept
: std::exception(), fee_(fee)
{}
std::uint64_t fee() const noexcept { return fee_; }
const char* what() const noexcept override { return "need_more_sources"; }
};
struct encrypted_file struct encrypted_file
{ {
std::string cipher; //!< Name of cipher+authentication used std::string cipher; //!< Name of cipher+authentication used
@@ -423,6 +412,18 @@ namespace lwsf { namespace internal
#error atomic_file_write not implemented for this platform #error atomic_file_write not implemented for this platform
#endif #endif
class need_more_sources final : public std::exception
{
std::uint64_t fee_;
public:
explicit need_more_sources(const std::uint64_t fee) noexcept
: std::exception(), fee_(fee)
{}
std::uint64_t fee() const noexcept { return fee_; }
const char* what() const noexcept override { return "need_more_sources"; }
};
struct unknown_exception : std::exception struct unknown_exception : std::exception
{ {
unknown_exception() noexcept unknown_exception() noexcept
@@ -434,9 +435,15 @@ namespace lwsf { namespace internal
constexpr boost::chrono::nanoseconds to_boost(const std::chrono::nanoseconds source) noexcept constexpr boost::chrono::nanoseconds to_boost(const std::chrono::nanoseconds source) noexcept
{ {
static_assert( // types could differ - verify they can represent all the same values
std::is_same<boost::chrono::nanoseconds::rep, std::chrono::nanoseconds::rep>{} using boost_rep = boost::chrono::nanoseconds::rep;
); using std_rep = std::chrono::nanoseconds::rep;
static_assert(std::is_integral<boost_rep>());
static_assert(std::is_integral<std_rep>());
static_assert(std::is_signed<boost_rep>());
static_assert(std::is_signed<std_rep>());
static_assert(std::numeric_limits<boost_rep>::min() == std::numeric_limits<std_rep>::min());
static_assert(std::numeric_limits<boost_rep>::max() == std::numeric_limits<std_rep>::max());
return boost::chrono::nanoseconds{source.count()}; return boost::chrono::nanoseconds{source.count()};
} }
@@ -578,7 +585,7 @@ namespace lwsf { namespace internal
return false; return false;
encrypted_file contents{}; encrypted_file contents{};
if (!wire::msgpack::from_bytes(std::move(file), contents)) if (wire::msgpack::from_bytes(std::move(file), contents))
return false; return false;
return !contents.get_payload(password).empty(); return !contents.get_payload(password).empty();
} }
@@ -1261,17 +1268,30 @@ namespace lwsf { namespace internal
Monero::PendingTransaction* wallet::createTransactionMultDest(const std::vector<std::string> &dst_addr, const std::string &payment_id, Monero::PendingTransaction* wallet::createTransactionMultDest(const std::vector<std::string> &dst_addr, const std::string &payment_id,
Monero::optional<std::vector<uint64_t>> amount, uint32_t mixin_count, Monero::optional<std::vector<uint64_t>> amount, uint32_t mixin_count,
Monero::PendingTransaction::Priority priority, const Monero::PendingTransaction::Priority priority,
uint32_t subaddr_account, const uint32_t subaddr_account,
std::set<uint32_t> subaddr_indices) std::set<uint32_t> subaddr_indices)
{ {
using destinations_vector = std::vector<cryptonote::tx_destination_entry>;
using transfers_map = std::multiset<std::pair<std::uint64_t, std::string>>;
using unspent_map =
std::unordered_map<crypto::public_key, std::pair<std::pair<std::uint64_t, std::uint64_t>, std::shared_ptr<const backend::transaction>>>;
using unspent_by_amount_map =
std::map<std::pair<std::uint64_t, std::uint64_t>, std::pair<crypto::public_key, std::shared_ptr<const backend::transaction>>>;
// default priority not handled like standard wallet
static_assert(Monero::PendingTransaction::Priority_Default == 0);
static_assert(Monero::PendingTransaction::Priority_Medium == 2);
const bool subtract_from_dest = !amount;
const unsigned priority_int = priority <= 0 ? 2 : unsigned(priority) - 1;
if (!mixin_count) if (!mixin_count)
mixin_count = mixin_; mixin_count = mixin_;
std::unique_ptr<internal::pending_transaction> out; std::unique_ptr<internal::pending_transaction> out;
const auto tx_error = [this] (std::error_code error) const auto throw_low_funds = [] ()
{ {
return std::make_unique<internal::pending_transaction>(data_, std::move(error)); throw std::runtime_error{"Unlocked funds too low"};
}; };
try try
@@ -1282,29 +1302,35 @@ namespace lwsf { namespace internal
out = [&, this] () out = [&, this] ()
{ {
if (!amount && dst_addr.size() > 1) if (!amount && dst_addr.size() > 1)
return tx_error(error::tx_sweep); throw std::invalid_argument{"Sweep requested with multiple destinations"};
if (amount && amount->size() != dst_addr.size()) if (amount && amount->size() != dst_addr.size())
return tx_error(error::tx_size_mismatch); throw std::invalid_argument{"Must have equal amounts and destinations"};
if (amount && amount->empty())
throw std::invalid_argument{"Zero destinations only valid with empty amounts"};
if (!payment_id.empty()) if (!payment_id.empty())
return tx_error(error::tx_long_pid); throw std::invalid_argument{"Long payment id was provided - deprecated"};
cryptonote::network_type ctype{}; cryptonote::network_type ctype{};
Monero::NetworkType mtype{}; Monero::NetworkType mtype{};
std::uint64_t per_byte_fee = 0; std::uint64_t per_byte_fee = 0;
std::uint64_t fee_mask = 0; std::uint64_t fee_mask = 0;
std::string change_address; std::string change_address{};
cryptonote::account_keys keys{}; cryptonote::account_keys keys{};
cryptonote::account_public_address change_account{}; cryptonote::account_public_address change_account{};
std::unordered_map<crypto::public_key, std::pair<std::pair<std::uint64_t, std::uint64_t>, std::shared_ptr<const backend::transaction>>> unspent; unspent_map unspent{};
{ {
data_->refresh(); // get latest outputs, block height, and fee info data_->refresh(); // get latest outputs, block height, and fee info
const boost::lock_guard<boost::mutex> lock{data_->sync}; const boost::lock_guard<boost::mutex> lock{data_->sync};
per_byte_fee = data_->per_byte_fee;
fee_mask = data_->fee_mask; fee_mask = data_->fee_mask;
if (!per_byte_fee || !fee_mask || !data_->passed_login) if (data_->per_byte_fee.empty() || !fee_mask || !data_->passed_login)
return tx_error(data_->refresh_error); throw std::system_error{data_->refresh_error, "Tx construction"};
if (data_->per_byte_fee.size() <= priority_int)
per_byte_fee = data_->per_byte_fee.back();
else
per_byte_fee = data_->per_byte_fee[priority_int];
mtype = data_->primary.type; mtype = data_->primary.type;
ctype = data_->get_net_type(); ctype = data_->get_net_type();
@@ -1346,86 +1372,36 @@ namespace lwsf { namespace internal
} }
} }
const bool subtract_from_dest = !amount;
if (subtract_from_dest)
{
std::uint64_t total_unspent = 0;
for (const auto& tx : unspent)
{
const auto output = tx.second.second->receives.find(tx.first);
LWSF_TX_ASSERT(output != tx.second.second->receives.end());
total_unspent += output->second.amount;
}
amount = std::vector<std::uint64_t>{total_unspent};
}
LWSF_TX_ASSERT(dst_addr.size() <= amount->size());
// By copy to catch mutations (want each invoke to be consistent) // By copy to catch mutations (want each invoke to be consistent)
const auto gather_sources_and_construct_tx = [=] (const std::uint64_t min_fee) const auto gather_sources_and_construct_tx =
-> std::unique_ptr<internal::pending_transaction> [=] (unspent_by_amount_map unspent_by_amount, destinations_vector dests, const transfers_map& transfers, const std::string& extra_nonce, const std::uint64_t min_fee)
-> std::shared_ptr<backend::transaction>
{ {
std::string extra_nonce; // leave 1 spot for change
std::vector<cryptonote::tx_destination_entry> dests; LWSF_TX_VERIFY(!dests.empty());
std::multiset<std::pair<std::uint64_t, std::string>> transfers; LWSF_TX_VERIFY(dests.size() < config::max_outputs_in_construction);
for (std::size_t i = 0; i < dst_addr.size(); ++i) LWSF_TX_VERIFY(dests.size() == transfers.size());
{
cryptonote::address_parse_info info{};
if (!cryptonote::get_account_address_from_str(info, ctype, dst_addr.at(i)))
return tx_error(error::tx_invalid_address);
if (info.has_payment_id) std::uint64_t transfer_total = 0;
{ for (const auto & e : dests)
if (!extra_nonce.empty()) transfer_total += e.amount;
return tx_error(error::tx_two_pid);
cryptonote::set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, info.payment_id);
}
transfers.insert({amount->at(i), dst_addr.at(i)});
auto& dest = dests.emplace_back();
dest.original = dst_addr.at(i);
dest.addr = info.address;
dest.amount = amount->at(i);
dest.is_subaddress = info.is_subaddress;
dest.is_integrated = info.has_payment_id;
}
for (std::size_t i = dst_addr.size(); i < amount->size(); ++i)
{
// this is a "self-sweep" (consolidation)
auto& dest = dests.emplace_back();
dest.original = change_address;
dest.addr = change_account;
dest.amount = amount->at(i);
dest.is_subaddress = subaddr_account;
dest.is_integrated = false;
}
std::vector<std::uint8_t> extra; std::vector<std::uint8_t> extra;
if (!extra_nonce.empty() && !cryptonote::add_extra_nonce_to_tx_extra(extra, extra_nonce)) if (!extra_nonce.empty() && !cryptonote::add_extra_nonce_to_tx_extra(extra, extra_nonce))
return tx_error(error::tx_extra); throw std::runtime_error{"cryptonote:add_extra_nonce_to_tx_extra failed"};
// actually sorted by amount then reverse index (see comment above)
std::map<std::pair<std::uint64_t, std::uint64_t>, std::pair<crypto::public_key, std::shared_ptr<const backend::transaction>>> unspent_by_amount_then_index;
for (const auto& tx : unspent)
unspent_by_amount_then_index.try_emplace(tx.second.first).first->second = {tx.first, tx.second.second};
bool has_change = false; bool has_change = false;
std::vector<std::pair<crypto::public_key, std::shared_ptr<const backend::transaction>>> spending; std::vector<std::pair<crypto::public_key, std::shared_ptr<const backend::transaction>>> spending;
{ {
std::uint64_t fee = 0; std::uint64_t fee = 1;
std::uint64_t remaining = std::uint64_t remaining = transfer_total;
std::accumulate(amount->begin(), amount->end(), std::uint64_t(0));
while (bool(remaining + fee)) while (bool(remaining + fee))
{ {
if (unspent_by_amount_then_index.empty()) if (unspent_by_amount.empty())
{ {
if (subtract_from_dest && !remaining) if (subtract_from_dest && !remaining)
break; break;
return tx_error(error::tx_low_funds); throw_low_funds();
} }
/* Check if we can omit change address for more efficiency. We /* Check if we can omit change address for more efficiency. We
@@ -1435,12 +1411,12 @@ namespace lwsf { namespace internal
fee = estimate_fee(per_byte_fee, spending.size() + 1, dests.size(), mixin_count, extra.size(), fee_mask); fee = estimate_fee(per_byte_fee, spending.size() + 1, dests.size(), mixin_count, extra.size(), fee_mask);
fee = std::max(min_fee, fee); fee = std::max(min_fee, fee);
const std::uint64_t needed = remaining + fee; const std::uint64_t needed = remaining + fee;
const auto output = unspent_by_amount_then_index.lower_bound({needed, 0}); const auto output = unspent_by_amount.lower_bound({needed, 0});
if (output != unspent_by_amount_then_index.end() && needed == output->first.first) if (output != unspent_by_amount.end() && needed == output->first.first)
{ {
remaining = 0; remaining = 0;
spending.emplace_back(output->second.first, output->second.second); spending.emplace_back(output->second.first, output->second.second);
unspent_by_amount_then_index.erase(output); unspent_by_amount.erase(output);
break; break;
} }
} }
@@ -1450,14 +1426,14 @@ namespace lwsf { namespace internal
fee = std::max(min_fee, fee); fee = std::max(min_fee, fee);
const std::uint64_t needed = remaining + fee; const std::uint64_t needed = remaining + fee;
auto output = unspent_by_amount_then_index.lower_bound({needed, 0}); auto output = unspent_by_amount.lower_bound({needed, 0});
if (output == unspent_by_amount_then_index.end()) if (output == unspent_by_amount.end())
--output; --output;
const std::uint64_t this_amount = output->first.first; const std::uint64_t this_amount = output->first.first;
const bool complete = needed <= this_amount; const bool complete = needed <= this_amount;
spending.emplace_back(output->second.first, output->second.second); spending.emplace_back(output->second.first, output->second.second);
unspent_by_amount_then_index.erase(output); unspent_by_amount.erase(output);
remaining -= this_amount; remaining -= this_amount;
if (complete) if (complete)
@@ -1466,9 +1442,9 @@ namespace lwsf { namespace internal
if (subtract_from_dest) if (subtract_from_dest)
{ {
LWSF_TX_ASSERT(dests.size() == 1); LWSF_TX_VERIFY(dests.size() == 1);
if (dests.back().amount < fee) if (dests.back().amount < fee)
return tx_error(error::tx_low_funds); throw_low_funds();
dests.back().amount -= fee; dests.back().amount -= fee;
fee = 0; fee = 0;
} }
@@ -1485,7 +1461,7 @@ namespace lwsf { namespace internal
change_dest.is_integrated = false; change_dest.is_integrated = false;
} }
LWSF_TX_ASSERT(config::minimum_outputs <= dests.size()); LWSF_TX_VERIFY(config::min_outputs <= dests.size());
// verify SUM(input) = SUM(output) - fee // verify SUM(input) = SUM(output) - fee
@@ -1493,14 +1469,14 @@ namespace lwsf { namespace internal
for (const auto& source : spending) for (const auto& source : spending)
{ {
const auto output = source.second->receives.find(source.first); const auto output = source.second->receives.find(source.first);
LWSF_TX_ASSERT(output != source.second->receives.end()); LWSF_TX_VERIFY(output != source.second->receives.end());
remaining += output->second.amount; remaining += output->second.amount;
} }
for (const auto& dest : dests) for (const auto& dest : dests)
remaining -= dest.amount; remaining -= dest.amount;
LWSF_TX_ASSERT(fee == remaining); LWSF_TX_VERIFY(fee == remaining);
} }
const auto rct_amount = [] (const backend::transfer_in& source) const auto rct_amount = [] (const backend::transfer_in& source)
@@ -1517,8 +1493,7 @@ namespace lwsf { namespace internal
auto resp = data_->get_decoys(req); auto resp = data_->get_decoys(req);
if (!resp) if (!resp)
return tx_error(resp.error()); throw std::system_error{resp.error()};
decoys = std::move(*resp); decoys = std::move(*resp);
} }
@@ -1555,7 +1530,7 @@ namespace lwsf { namespace internal
const std::uint64_t amount = rct_amount(source); const std::uint64_t amount = rct_amount(source);
const auto ring = std::lower_bound(decoys.begin(), decoys.end(), amount, by_amount{}); const auto ring = std::lower_bound(decoys.begin(), decoys.end(), amount, by_amount{});
if (ring == decoys.end() || ring->amount != rpc::uint64_string(amount)) if (ring == decoys.end() || ring->amount != rpc::uint64_string(amount))
return tx_error(error::tx_decoys); throw std::runtime_error{"Missing requested decoys"};
for (const auto& decoy : ring->outputs) for (const auto& decoy : ring->outputs)
entry.outputs.emplace_back(std::uint64_t(decoy.global_index), rct::ctkey{decoy.public_key, decoy.rct}); entry.outputs.emplace_back(std::uint64_t(decoy.global_index), rct::ctkey{decoy.public_key, decoy.rct});
@@ -1583,11 +1558,11 @@ namespace lwsf { namespace internal
std::shuffle(sources.begin(), sources.end(), crypto::random_device{}); std::shuffle(sources.begin(), sources.end(), crypto::random_device{});
for (auto& source : sources) for (auto& source : sources)
{ {
LWSF_TX_ASSERT(!source.outputs.empty()); LWSF_TX_VERIFY(!source.outputs.empty());
const std::uint64_t real_source = source.outputs.front().first; const std::uint64_t real_source = source.outputs.front().first;
std::sort(source.outputs.begin(), source.outputs.end(), ring_sort); std::sort(source.outputs.begin(), source.outputs.end(), ring_sort);
LWSF_TX_ASSERT(std::unique(source.outputs.begin(), source.outputs.end(), ring_compare) == source.outputs.end()); LWSF_TX_VERIFY(std::unique(source.outputs.begin(), source.outputs.end(), ring_compare) == source.outputs.end());
for (std::size_t i = 0; i < source.outputs.size(); ++i) for (std::size_t i = 0; i < source.outputs.size(); ++i)
{ {
@@ -1601,7 +1576,7 @@ namespace lwsf { namespace internal
// By copy to catch mutations (want each invoke to be consistent) // By copy to catch mutations (want each invoke to be consistent)
const auto construct_tx = [=, &dests, &sources = std::as_const(sources)] () const auto construct_tx = [=, &dests, &sources = std::as_const(sources)] ()
-> std::unique_ptr<internal::pending_transaction> -> std::shared_ptr<backend::transaction>
{ {
auto rsources = sources; // already shuffled above auto rsources = sources; // already shuffled above
auto rdests = dests; // in case we need to retry fee auto rdests = dests; // in case we need to retry fee
@@ -1614,13 +1589,15 @@ namespace lwsf { namespace internal
crypto::secret_key tx_key; crypto::secret_key tx_key;
std::vector<crypto::secret_key> tx_keys; std::vector<crypto::secret_key> tx_keys;
cryptonote::transaction tx; cryptonote::transaction tx;
LWSF_TX_VERIFY(config::min_outputs <= rdests.size());
LWSF_TX_VERIFY(rdests.size() <= config::max_outputs_in_construction);
if (!cryptonote::construct_tx_and_get_tx_key(keys, subs, rsources, rdests, change_account, extra, tx, tx_key, tx_keys, true /* rct */, config, true /* view_tags */)) if (!cryptonote::construct_tx_and_get_tx_key(keys, subs, rsources, rdests, change_account, extra, tx, tx_key, tx_keys, true /* rct */, config, true /* view_tags */))
return tx_error(error::tx_failed); throw std::runtime_error{"cryptonote::construct_tx_and_get_tx_key failed"};
auto details = std::make_shared<backend::transaction>(); auto details = std::make_shared<backend::transaction>();
details->raw_bytes = epee::byte_slice{cryptonote::t_serializable_object_to_blob(tx)}; details->raw_bytes = epee::byte_slice{cryptonote::t_serializable_object_to_blob(tx)};
details->timestamp = std::chrono::system_clock::now(); details->timestamp = std::chrono::system_clock::now();
details->amount = std::accumulate(amount->begin(), amount->end(), std::uint64_t(0)); details->amount = transfer_total;
details->fee = get_tx_fee(tx); details->fee = get_tx_fee(tx);
const std::uint64_t weight = get_transaction_weight(tx, details->raw_bytes.size()); const std::uint64_t weight = get_transaction_weight(tx, details->raw_bytes.size());
@@ -1628,7 +1605,7 @@ namespace lwsf { namespace internal
const auto get_change_index = [&] () -> std::size_t const auto get_change_index = [&] () -> std::size_t
{ {
LWSF_TX_ASSERT(!dests.empty()); LWSF_TX_VERIFY(!dests.empty());
if (subtract_from_dest) if (subtract_from_dest)
return 0; return 0;
if (has_change && dests.back().original == change_address) if (has_change && dests.back().original == change_address)
@@ -1663,7 +1640,7 @@ namespace lwsf { namespace internal
get_transaction_hash(tx, details->id); get_transaction_hash(tx, details->id);
get_transaction_prefix_hash(tx, details->prefix); get_transaction_prefix_hash(tx, details->prefix);
LWSF_TX_ASSERT(rsources.size() == tx.vin.size()); LWSF_TX_VERIFY(rsources.size() == tx.vin.size());
for (std::size_t i = 0; i < rsources.size(); ++i) for (std::size_t i = 0; i < rsources.size(); ++i)
{ {
const auto& source = rsources.at(i); const auto& source = rsources.at(i);
@@ -1674,16 +1651,16 @@ namespace lwsf { namespace internal
const auto elem = std::find_if(spending.begin(), spending.end(), [&] (const auto& e) { const auto elem = std::find_if(spending.begin(), spending.end(), [&] (const auto& e) {
return e.first == spend->second.output_pub; return e.first == spend->second.output_pub;
}); });
LWSF_TX_ASSERT(elem != spending.end()); LWSF_TX_VERIFY(elem != spending.end());
const auto& base = elem->second->receives.at(spend->second.output_pub); const auto& base = elem->second->receives.at(spend->second.output_pub);
spend->second.sender = base.recipient; spend->second.sender = base.recipient;
spend->second.tx_pub = base.tx_pub; spend->second.tx_pub = base.tx_pub;
} }
LWSF_TX_ASSERT(rdests.size() == tx.vout.size()); LWSF_TX_VERIFY(rdests.size() == tx.vout.size());
LWSF_TX_ASSERT(rdests.size() == tx.rct_signatures.ecdhInfo.size()); LWSF_TX_VERIFY(rdests.size() == tx.rct_signatures.ecdhInfo.size());
LWSF_TX_ASSERT(tx_keys.empty() || tx_keys.size() == rdests.size()); LWSF_TX_VERIFY(tx_keys.empty() || tx_keys.size() == rdests.size());
struct get_output_pub struct get_output_pub
{ {
@@ -1728,7 +1705,7 @@ namespace lwsf { namespace internal
details->transfers.emplace_back(dest.original, dest.amount).secret = tx_keys.at(i); details->transfers.emplace_back(dest.original, dest.amount).secret = tx_keys.at(i);
rtransfers.erase(is_transfer); rtransfers.erase(is_transfer);
} }
LWSF_TX_ASSERT(rtransfers.empty()); LWSF_TX_VERIFY(rtransfers.empty());
} }
else else
{ {
@@ -1736,7 +1713,7 @@ namespace lwsf { namespace internal
details->transfers.emplace_back(transfer.second, transfer.first).secret = tx_key; details->transfers.emplace_back(transfer.second, transfer.first).secret = tx_key;
} }
return std::make_unique<internal::pending_transaction>(data_, std::move(tx), std::move(details)); return details;
}; // end `construct_tx` lambda }; // end `construct_tx` lambda
// only one retry should be needed // only one retry should be needed
@@ -1747,28 +1724,198 @@ namespace lwsf { namespace internal
return pending; return pending;
// else `dests` should've been modified for retry // else `dests` should've been modified for retry
} }
LWSF_TX_ASSERT(false); // this should be unreachable LWSF_TX_VERIFY(false); // this should be unreachable
}; // end `gather_sources_and_construct_tx` lamda }; // end `gather_sources_and_construct_tx` lamda
std::uint64_t min_fee = 0; const auto get_unspent_by_amount = [] (const unspent_map& unspent)
for (unsigned attempt = 0; attempt < unspent.size(); ++attempt)
{ {
try { return gather_sources_and_construct_tx(min_fee); } // actually sorted by amount then reverse index (see comment above)
catch (const need_more_sources& e) unspent_by_amount_map out;
for (const auto& tx : unspent)
out.try_emplace(tx.second.first).first->second = {tx.first, tx.second.second};
return out;
};
if (subtract_from_dest)
{
std::uint64_t total_unspent = 0;
for (const auto& tx : unspent)
{ {
min_fee = e.fee(); const auto output = tx.second.second->receives.find(tx.first);
LWSF_TX_VERIFY(output != tx.second.second->receives.end());
total_unspent += output->second.amount;
} }
amount = std::vector<std::uint64_t>{total_unspent};
} }
return tx_error(error::tx_low_funds); std::string extra_nonce{};
destinations_vector dests_flat{};
transfers_map transfers_flat{};
for (std::size_t i = 0; i < dst_addr.size(); ++i)
{
cryptonote::address_parse_info info{};
if (!cryptonote::get_account_address_from_str(info, ctype, dst_addr.at(i)))
throw std::invalid_argument{"Invalid destination address"};
if (info.has_payment_id)
{
if (!extra_nonce.empty())
throw std::invalid_argument{"Two payment ids provided"};
cryptonote::set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, info.payment_id);
}
transfers_flat.insert({amount->at(i), dst_addr.at(i)});
auto& dest = dests_flat.emplace_back();
dest.original = dst_addr.at(i);
dest.addr = info.address;
dest.amount = amount->at(i);
dest.is_subaddress = info.is_subaddress;
dest.is_integrated = info.has_payment_id;
}
LWSF_TX_VERIFY(bool(amount));
LWSF_TX_VERIFY(dst_addr.size() <= amount->size());
for (std::size_t i = dst_addr.size(); i < amount->size(); ++i)
{
// this is a "self-sweep" (consolidation)
auto& dest = dests_flat.emplace_back();
dest.original = change_address;
dest.addr = change_account;
dest.amount = amount->at(i);
dest.is_subaddress = subaddr_account;
dest.is_integrated = false;
}
std::sort(dests_flat.begin(), dests_flat.end(), [] (const auto& lhs, const auto& rhs) {
return lhs.amount < rhs.amount;
});
/* This function makes a crude attempt at breaking up destinations
into multiple transactions IFF exceeding output maximum. The
algorithm is crude, and does a best effort to minimize source/input
usage. Unfortunately capture by reference, just much easier. */
const auto get_and_update_destinations = [&] ()
{
// leave a spot for change
LWSF_TX_VERIFY(dests_flat.size() == transfers_flat.size());
if (dests_flat.size() < config::max_outputs_in_construction)
{
auto out = std::make_pair(std::move(dests_flat), std::move(transfers_flat));
dests_flat.clear();
transfers_flat.clear();
return out;
}
// split //
const std::size_t extra_size = extra_nonce.empty() ? 0 : extra_nonce.size() + 2;
auto unspent_by_amount = get_unspent_by_amount(unspent);
const std::size_t unspent_initial = unspent_by_amount.size();
std::pair<destinations_vector, transfers_map> out{};
const auto stop_condition = [&] ()
{
// leave one spot for change
static_assert(1 <= config::max_outputs_in_construction);
return dests_flat.empty() || std::get<0>(out).size() == config::max_outputs_in_construction - 1;
};
const auto update_transfers = [&] (const auto& dest)
{
auto transfer = transfers_flat.find({dest.amount, dest.original});
LWSF_TX_VERIFY(transfer != transfers_flat.end());
std::get<1>(out).insert(std::move(*transfer));
transfers_flat.erase(transfer);
};
const auto by_amount = [] (const auto& lhs, const std::uint64_t rhs)
{
return lhs.amount < rhs;
};
std::uint64_t remaining = 0;
while (!stop_condition())
{
// add largest amount first
std::get<0>(out).push_back(std::move(dests_flat.back()));
dests_flat.pop_back();
update_transfers(std::get<0>(out).back());
{
const std::uint64_t amount = std::get<0>(out).back().amount;
const std::size_t spend_size = unspent_initial - unspent_by_amount.size() + 1;
const std::uint64_t fee =
estimate_fee(per_byte_fee, spend_size, std::get<0>(out).size() + 1, mixin_count, extra_size, fee_mask);
const auto source = unspent_by_amount.lower_bound({amount + fee, 0});
if (source == unspent_by_amount.end())
return out; // the "real" tx construction mechanism needs to run
remaining += std::get<0>(source->first) - amount;
unspent_by_amount.erase(source);
}
const std::size_t spend_size = unspent_initial - unspent_by_amount.size();
while (!stop_condition())
{
const std::uint64_t fee =
estimate_fee(per_byte_fee, spend_size, std::get<0>(out).size() + 1, mixin_count, extra_size, fee_mask);
if (remaining <= fee)
break;
auto dest = std::lower_bound(dests_flat.begin(), dests_flat.end(), remaining - fee, by_amount);
if (dest == dests_flat.end())
--dest;
std::get<0>(out).push_back(std::move(*dest));
dests_flat.erase(dest);
update_transfers(std::get<0>(out).back());
remaining -= std::get<0>(out).back().amount;
}
}
return out;
};
std::vector<std::shared_ptr<backend::transaction>> pending;
while (!dests_flat.empty())
{
std::uint64_t min_fee = 0;
const auto dests = get_and_update_destinations(); // `dests_flat` pruned
const auto unspent_by_amount = get_unspent_by_amount(unspent);
unsigned attempt = 0;
for (; attempt < unspent.size(); ++attempt)
{
try
{
auto tx = gather_sources_and_construct_tx(unspent_by_amount, std::get<0>(dests), std::get<1>(dests), extra_nonce, min_fee);
LWSF_TX_VERIFY(tx != nullptr);
for (const auto& spend : tx->spends)
unspent.erase(spend.second.output_pub);
pending.push_back(std::move(tx));
break; // goto next tx
}
catch (const need_more_sources& e)
{
min_fee = std::max(min_fee, e.fee());
}
}
if (attempt == unspent.size())
break;
}
if (!dests_flat.empty())
throw_low_funds();
return std::make_unique<internal::pending_transaction>(data_, std::string{}, std::move(pending));
}(); // end unnamed lambda encapsulating all tx construction }(); // end unnamed lambda encapsulating all tx construction
LWSF_TX_ASSERT(out != nullptr); LWSF_TX_VERIFY(out != nullptr);
} }
catch (const std::exception & e) catch (const std::exception& e)
{ {
set_critical(e); out = std::make_unique<internal::pending_transaction>(data_, e.what());
out = tx_error(error::tx_critical);
} }
return out.release(); return out.release();
+4 -3
View File
@@ -393,11 +393,12 @@ namespace lwsf
return cryptonote::print_money(amount); return cryptonote::print_money(amount);
} }
std::uint64_t amountFromString(const std::string &amount) std::optional<std::uint64_t> amountFromString(const std::string &amount)
{ {
std::uint64_t result = 0; std::uint64_t result = 0;
cryptonote::parse_amount(result, amount); if (cryptonote::parse_amount(result, amount))
return result; return result;
return std::nullopt;
} }
bool addressValid(const std::string &str, Monero::NetworkType nettype) bool addressValid(const std::string &str, Monero::NetworkType nettype)