From dff93153eb53cbaa5187336780773f50dec0d884 Mon Sep 17 00:00:00 2001 From: Lee *!* Clagett Date: Mon, 26 Aug 2024 15:29:06 -0400 Subject: [PATCH] Added some more components, but still incomplete and only partially builds --- LICENSE | 2 +- include/lws_frontend.h | 32 +-- src/CMakeLists.txt | 22 +- src/backend.cpp | 303 ++++++++++++++++-------- src/backend.h | 72 ++++-- src/rpc.cpp | 85 ++++++- src/rpc.h | 46 +++- src/transaction_history.cpp | 10 +- src/transaction_info.cpp | 13 +- src/transaction_info.h | 4 +- src/wallet.cpp | 453 ++++++++++++++++++++++++++++++++++-- src/wallet.h | 40 +++- src/wallet_manager.cpp | 38 ++- src/wire/CMakeLists.txt | 4 +- src/wire/fwd.h | 1 + src/wire/json/read.cpp | 179 ++++++++------ src/wire/json/read.h | 8 +- src/wire/msgpack/read.cpp | 5 +- src/wire/msgpack/read.h | 2 +- src/wire/read.h | 6 +- 20 files changed, 1033 insertions(+), 292 deletions(-) diff --git a/LICENSE b/LICENSE index c5f3871..dbf279e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ BSD 3-Clause License -Copyright (c) 2024, Lee *!* Clagett +Copyright (c) 2024, The Monero Project Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/include/lws_frontend.h b/include/lws_frontend.h index f88d07f..a933f54 100644 --- a/include/lws_frontend.h +++ b/include/lws_frontend.h @@ -425,22 +425,28 @@ struct WalletListener */ struct Wallet { - enum class Device { - Software = 0, - Ledger = 1, - Trezor = 2 + enum Device { + Device_Software = 0, + Device_Ledger = 1, + Device_Trezor = 2 }; - enum class Status { - Ok, - Error, - Critical + enum Status { + Status_Ok, + Status_Error, + Status_Critical }; - enum class ConnectionStatus { - Disconnected, - Connected, - WrongVersion + enum ConnectionStatus { + ConnectionStatus_Disconnected, + ConnectionStatus_Connected, + ConnectionStatus_WrongVersion + }; + + enum BackgroundSyncType { + BackgroundSync_Off = 0, + BackgroundSync_ReusePassword = 1, + BackgroundSync_CustomPassword = 2 }; virtual ~Wallet() = 0; @@ -938,7 +944,7 @@ struct Wallet virtual std::shared_ptr addressBook() = 0; virtual std::shared_ptr subaddress() = 0; virtual std::shared_ptr subaddressAccount() = 0; - virtual void setListener(WalletListener *) = 0; + virtual void setListener(std::shared_ptr) = 0; /*! * \brief defaultMixin - returns number of mixins used in transactions * \return diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e948128..85f3fcf 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -29,8 +29,26 @@ include_directories(.) # For both the server and admin utility. -set(monero-lwsf_sources backend.cpp rpc.cpp transaction_history.cpp transaction_info.cpp wallet.cpp wallet_manager.cpp) -set(monero-lwsf_headers backend.h lwsf_config.h rpc.h transaction_history.h transaction_info.h wallet.h) +set(monero-lwsf_sources + backend.cpp + error.cpp + pending_transaction.cpp + rpc.cpp + transaction_history.cpp + transaction_info.cpp + wallet.cpp + wallet_manager.cpp +) +set(monero-lwsf_headers + backend.h + error.h + lwsf_config.h + pending_transaction.h + rpc.h + transaction_history.h + transaction_info.h + wallet.h +) add_executable(monero-lwsf dummy_main.cpp ${monero-lwsf_sources} ${monero-lwsf_headers}) target_link_libraries(monero-lwsf diff --git a/src/backend.cpp b/src/backend.cpp index 2dc5119..e52185a 100644 --- a/src/backend.cpp +++ b/src/backend.cpp @@ -31,8 +31,10 @@ #include #include "cryptonote_basic/cryptonote_basic_impl.h" // monero/src #include "lwsf_config.h" +#include "ringct/rctOps.h" #include "wire.h" #include "wire/adapted/crypto.h" +#include "wire/adapted/pair.h" #include "wire/msgpack.h" #include "wire/wrapper/trusted_array.h" @@ -81,24 +83,14 @@ namespace lwsf { namespace internal { namespace backend WIRE_FIELD(amount), WIRE_FIELD(recipient), WIRE_FIELD(index), - WIRE_FIELD(output_pub), - WIRE_FIELD(rct_mask) + WIRE_OPTIONAL_FIELD(rct_mask), + WIRE_FIELD(tx_pub) ); } - } - static void read_bytes(wire::reader& source, std::pair& dest) - { - read_bytes(source, dest.second); - dest.first = dest.second.output_pub; - } - static void write_bytes(wire::writer& dest, const std::pair& source) - { - write_bytes(dest, source.second); - } - -namespace -{ + } // anonymous + namespace + { template void map_transaction(F& format, T& self) { @@ -113,27 +105,29 @@ namespace WIRE_OPTIONAL_FIELD(height), WIRE_FIELD(unlock_time), WIRE_FIELD(direction), - WIRE_OPTIONAL_FIELD(tx_secret), WIRE_FIELD(id), + WIRE_FIELD(prefix), WIRE_FIELD(coinbase) ); } -} + } // anonymous - static void read_bytes(wire::reader& source, std::pair>& dest) - { - if (!dest.second) - dest.second = std::make_shared(transaction{}); - read_bytes(source, *dest.second); - dest.first = dest.second->id; - } - static void write_bytes(wire::writer& dest, const std::pair>& source) - { - if (!source.second) - WIRE_DLOG_THROW(wire::error::schema::object, "Unexpected nullptr"); - write_bytes(dest, *source.second); - } -namespace { + static void read_bytes(wire::reader& source, std::pair>& dest) + { + if (!dest.second) + dest.second = std::make_shared(); + read_bytes(source, *dest.second); + dest.first = dest.second->id; + } + static void write_bytes(wire::writer& dest, const std::pair>& source) + { + if (!source.second) + WIRE_DLOG_THROW(wire::error::schema::object, "Unexpected nullptr"); + write_bytes(dest, *source.second); + } + + namespace + { template void map_keypair(F& format, T& self) { @@ -148,12 +142,64 @@ namespace { wire::field("txes", wire::trusted_array(std::ref(self.txes))), WIRE_FIELD(type), WIRE_FIELD(view), - WIRE_FIELD(spend) + WIRE_FIELD(spend), + WIRE_FIELD(generated_locally) ); } - - void new_tx(const account& self, transaction& out, const rpc::transaction& source) + + rct::key get_mask(const crypto::secret_key& view_key, const rpc::output& source) { + crypto::key_derivation derived{}; + if (!crypto::generate_key_derivation(source.tx_pub_key, view_key, derived)) + throw std::runtime_error{"generate_key_derivation failure"}; + + crypto::secret_key scalar{}; + crypto::derivation_to_scalar(derived, source.index, scalar); + + rct::ecdhTuple commitment{source.rct.mask}; + rct::ecdhDecode(commitment, rct::sk2rct(scalar), source.rct.type == rpc::ringct::format::recompute); + + return commitment.mask; + } + + void update_output(transfer_in& out, const rpc::output& source, const crypto::secret_key& view_key) + { + out.global_index = std::uint64_t(source.global_index); + out.amount = std::uint64_t(source.amount); + out.recipient = source.recipient.value_or(rpc::address_meta{}); + out.index = source.index; + out.tx_pub = source.tx_pub_key; + + switch (source.rct.type) + { + default: + throw std::runtime_error{"Unexpected ringct mask type"}; + case rpc::ringct::format::none: + out.rct_mask = std::nullopt; + break; + case rpc::ringct::format::encrypted: + case rpc::ringct::format::recompute: + out.rct_mask = get_mask(view_key, source); + break; + case rpc::ringct::format::unencrypted: + out.rct_mask = source.rct.mask; + break; + } + } + + void update_spend(transfer_out& out, const rpc::transaction_spend& source) + { + out.amount = std::uint64_t(source.amount); + out.sender = source.sender.value_or(rpc::address_meta{}); + out.tx_pub = source.tx_pub_key; + } + + void update_tx(const account& self, transaction& out, const rpc::transaction& source) + { + /* Let `receives` re-populate in `merge_output`. This works because the + server supplies all info - there is no local info to keep. */ + out.receives.clear(); + out.id = source.tx_hash; out.timestamp = source.timestamp; out.fee = std::uint64_t(source.fee.value_or(rpc::uint64_string(0))); @@ -178,7 +224,9 @@ namespace { crypto::generate_key_image(output_pub, output_secret, image); if (image == spend.key_image) { - out.spends.emplace_back(std::uint64_t(spend.amount), spend.tx_pub_key); + /* Frontend will typically know about spend before backend. So only + merge and never erase spends. */ + update_spend(out.spends.try_emplace(image).first->second, spend); total_spent += std::uint64_t(spend.amount); } } @@ -195,46 +243,95 @@ namespace { } } - void merge_output(std::shared_ptr& out, const rpc::output& output) + void merge_output(const std::shared_ptr& out, const rpc::output& source, const crypto::secret_key& view_key) { if (!out) throw std::logic_error{"nullptr transaction in merge_output"}; - out->prefix = output.tx_prefix_hash; - const auto receive = out->receives.lower_bound(output.public_key); - if (receive == out->receives.end() || receive->first != output.public_key) - { - out->receives.emplace_hint(receive, output.public_key, transfer_in{output}); - } + out->prefix = source.tx_prefix_hash; + update_output(out->receives.try_emplace(source.public_key).first->second, source, view_key); } - bool merge_response(wallet& self, const rpc::get_address_txs& source) + std::vector> merge_response(wallet& self, const rpc::get_address_txs& source) { - bool updated_wallet = false; + std::vector> out; + self.scan_height = source.scanned_block_height; self.blockchain_height = source.blockchain_height; self.primary.restore_height = source.start_height; - self.primary.txes.reserve(source.transactions.size()); - for (const auto& tx : source.transactions) + /* Backend server could remove or modify txes (rescan or bug fix); the + easiest way to handle this is to start a new copy of the txes. This is + what the existing (JS) MyMonero frontend does. This has the benefit + of allowing `shared_ptr` objects to be "given away" to + other parts of the frontend without a mutex. */ + + const auto existing_txes = std::move(self.primary.txes); + self.primary.txes.clear(); + self.primary.txes.reserve( + std::max(existing_txes.size(), source.transactions.size()) + ); + + /* The frontend will know about the spend first, iff the frontend was + used to perform the spend. We copy _all_ transactions that have a + spend secret, even if the backend doesn't acknowledge it, otherwise the + secret information will be lost in many situations. If the spend + never gets confirmed, this will just sit in the transaction list. */ + for (const auto& tx : existing_txes) { - auto iter = self.primary.txes.lower_bound(tx.tx_hash); - if (iter == self.primary.txes.end() || iter->first != tx.tx_hash) + if (tx.second) { - updated_wallet = true; - new_tx(self.primary, *self.primary.txes.emplace_hint(iter, tx.tx_hash, std::make_shared(transaction{}))->second, tx); + bool copy = false; + for (const auto& spend : tx.second->spends) + { + if (spend.second.secret) + { + self.primary.txes.emplace_hint( + self.primary.txes.end(), tx.first, std::make_shared(*tx.second) + ); + break; // inner loop + } + } } } - return updated_wallet; + + for (const auto& tx : source.transactions) + { + auto inserted = self.primary.txes.try_emplace(tx.tx_hash, nullptr); + if (inserted.second) + { + const auto existing = existing_txes.find(tx.tx_hash); + try + { + if (existing != existing_txes.end() && existing->second) + inserted.first->second = std::make_shared(*existing->second); + else + inserted.first->second = std::make_shared(); + } + catch (...) + { + self.primary.txes.erase(inserted.first); + throw; + } + + if (existing != existing_txes.end()) + out.push_back(inserted.first->second); + } + update_tx(self.primary, *inserted.first->second, tx); + } + + return out; } - void merge_response(account& self, const rpc::get_unspent_outs& source) + void merge_response(wallet& self, const rpc::get_unspent_outs& source) { + self.per_byte_fee = source.per_byte_fee; + self.fee_mask = source.fee_mask; for (const auto& output : source.outputs) { - auto iter = self.txes.find(output.tx_hash); - if (iter != self.txes.end()) - merge_output(iter->second, output); + auto iter = self.primary.txes.find(output.tx_hash); + if (iter != self.primary.txes.end()) + merge_output(iter->second, output, self.primary.view.sec); } } } // anonymous @@ -253,39 +350,21 @@ namespace { void write_bytes(wire::writer& dest, const account& source) { map_account(dest, source); } - transfer_in::transfer_in() - : global_index(0), - amount(0), - recipient(), - index(0), - output_pub{}, - rct_mask{} - {} - - transfer_in::transfer_in(const rpc::output& source) - : global_index(std::uint64_t(source.global_index)), - amount(std::uint64_t(source.amount)), - recipient(source.recipient.value_or(rpc::address_meta{})), - index(source.index), - output_pub(source.public_key), - rct_mask(source.rct.mask) - {} - - wallet::wallet(const bool generated_locally) - : client(), + wallet::wallet() + : listener(nullptr), + client(), primary{}, - status(), last_sync(0), scan_height(0), blockchain_height(0), - sync(), - generated_locally(generated_locally) + sync() {} expect wallet::to_bytes() const { epee::byte_stream dest{}; dest.reserve(config::initial_buffer_size); + const boost::lock_guard lock{sync}; const std::error_code error = wire::msgpack::to_bytes(dest, primary); if (error) @@ -293,16 +372,13 @@ namespace { return epee::byte_slice{std::move(dest)}; } - expect wallet::from_bytes(epee::byte_slice source) + std::error_code wallet::from_bytes(epee::byte_slice source) { const boost::lock_guard lock{sync}; - const std::error_code error = wire::msgpack::from_bytes(std::move(source), primary); - if (error) - return error; - return success(); + return wire::msgpack::from_bytes(std::move(source), primary); } - void wallet::refresh(const bool mandatory) + std::error_code wallet::refresh(const bool mandatory) { if (!mandatory) { @@ -311,7 +387,7 @@ namespace { std::chrono::steady_clock::time_point::duration{last_sync.load()} }; if (now - start < config::refresh_interval) - return; + return {}; } expect outs_response{common_error::kInvalidArgument}; @@ -320,21 +396,56 @@ namespace { if (txs_response) outs_response = rpc::invoke(client, login); - const boost::lock_guard lock{sync}; + boost::unique_lock lock{sync}; + if (!txs_response) - { - status = txs_response.error(); - return; - } + return txs_response.error(); if (!outs_response) + return outs_response.error(); + + const std::uint64_t orig_scan_height = scan_height; + const auto new_txes = merge_response(*this, *txs_response); + merge_response(*this, *outs_response); + last_sync = std::chrono::steady_clock::now().time_since_epoch().count(); + + if (!listener) + return {}; + + // Call listener functions without holding lock, in case a call is made + // back into the library. + const std::uint64_t new_scan_height = std::max(orig_scan_height, scan_height); + lock.unlock(); + + listener->refreshed(); + if (!new_txes.empty()) + listener->updated(); + + for (std::uint64_t i = orig_scan_height; i < new_scan_height; ++i) + listener->newBlock(i); + + for (const auto& tx : new_txes) { - status = outs_response.error(); - return; + const auto txid = epee::string_tools::pod_to_hex(tx->id); + if (tx->direction == TransactionInfo::Direction::In) + { + if (tx->height) + listener->moneyReceived(txid, tx->amount); + else + listener->unconfirmedMoneyReceived(txid, tx->amount); + } + else + listener->moneySpent(txid, tx->amount); } - status = std::error_code{}; - merge_response(*this, *txs_response); - merge_response(primary, *outs_response); - last_sync = std::chrono::steady_clock::now().time_since_epoch().count(); + return {}; + } + + std::error_code wallet::send_tx(epee::byte_slice tx_bytes) + { + const rpc::submit_raw_tx_request request{std::move(tx_bytes)}; + auto response = rpc::invoke(client, request); + if (!response) + return response.error(); + return {}; } }}} // lwsf // internal // backend diff --git a/src/backend.h b/src/backend.h index 1976fe7..7d3b906 100644 --- a/src/backend.h +++ b/src/backend.h @@ -35,11 +35,13 @@ #include #include #include +#include #include #include #include #include +#include "byte_slice.h" // monero/contrib/epee/include #include "common/expect.h" // moneor/src #include "crypto/crypto.h" // monero/src #include "crypto/hash.h" // monero/src @@ -70,11 +72,7 @@ namespace lwsf { namespace internal { namespace backend crypto::public_key tx_pub; transfer_out() - : address(), amount(0), sender(), secret(), tx_pub{} - {} - - explicit transfer_out(const std::uint64_t amount, const crypto::public_key& tx_pub) - : address(), amount(amount), sender(), secret(), tx_pub(tx_pub) + : address(), amount(0), sender{}, secret(), tx_pub{} {} }; WIRE_DECLARE_OBJECT(transfer_out); @@ -85,19 +83,49 @@ namespace lwsf { namespace internal { namespace backend std::uint64_t amount; rpc::address_meta recipient; std::uint16_t index; - crypto::public_key output_pub; - rct::key rct_mask; + std::optional rct_mask; + crypto::public_key tx_pub; - transfer_in(); - explicit transfer_in(const rpc::output& source); + transfer_in() + : global_index(0), + amount(0), + recipient{}, + index(0), + rct_mask(), + tx_pub{} + {} }; WIRE_DECLARE_OBJECT(transfer_in); - + struct transaction { - transaction() = delete; - std::vector spends; - std::map receives; + transaction() + : spends(), + receives(), + description(), + label(), + timestamp(), + amount(0), + fee(0), + height(), + unlock_time(0), + direction(TransactionInfo::Direction::Out), + payment_id(), + id{}, + prefix{}, + coinbase(false) + {} + + transaction(transaction&&) = default; + transaction(const transaction&) = default; + transaction& operator=(transaction&&) = default; + transaction& operator=(const transaction&) = default; + + /*! flat_map is used here for faster copies/merging. A single allocation + is needed in the copy (done every refresh interval), instead of an + allocation per key. */ + boost::container::flat_map spends; + boost::container::flat_map receives; //!< Key is output pub std::string description; std::string label; std::optional timestamp; @@ -107,8 +135,6 @@ namespace lwsf { namespace internal { namespace backend std::uint64_t unlock_time; TransactionInfo::Direction direction; std::variant payment_id; - std::optional tx_secret; - crypto::public_key pub; crypto::hash id; crypto::hash prefix; bool coinbase; @@ -132,26 +158,30 @@ namespace lwsf { namespace internal { namespace backend NetworkType type; keypair view; keypair spend; + bool generated_locally; }; WIRE_DECLARE_OBJECT(account); struct wallet { + std::shared_ptr listener; rpc::http_client client; account primary; - std::error_code status; - std::atomic last_sync; + std::atomic last_sync; std::uint64_t scan_height; std::uint64_t blockchain_height; + std::uint64_t per_byte_fee; + std::uint64_t fee_mask; mutable boost::mutex sync; - const bool generated_locally; - explicit wallet(bool generated_locally); + wallet(); expect to_bytes() const; - expect from_bytes(epee::byte_slice source); + std::error_code from_bytes(epee::byte_slice source); bool login() const; - void refresh(bool mandatory = false); + std::error_code refresh(bool mandatory = false); + + std::error_code send_tx(epee::byte_slice tx_bytes); }; }}} // lwsf // internal // backend diff --git a/src/rpc.cpp b/src/rpc.cpp index 3312877..fdd3367 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp @@ -33,8 +33,10 @@ #include #include #include +#include "hex.h" // monero/contrib/epee/include #include "lwsf_config.h" #include "net/http_client.h" // monero/contrib/epee/include +#include "ringct/rctOps.h" // monero/contrib/epee/include #include "wire.h" #include "wire/adapted/crypto.h" #include "wire/field.h" @@ -71,14 +73,14 @@ namespace lwsf { namespace internal { namespace rpc struct category final : std::error_category { virtual const char* name() const noexcept override final - { - return "lwsf::internal::rpc::error_category()"; - } + { + return "lwsf::internal::rpc::error_category()"; + } - virtual std::string message(int value) const override final - { - return get_string(error(value)); - } + virtual std::string message(int value) const override final + { + return get_string(error(value)); + } }; static const category instance{}; return instance; @@ -95,7 +97,7 @@ namespace lwsf { namespace internal { namespace rpc return {error::invoke_failure}; if (!response) return {error::invoke_failure}; - if(response->m_response_code != 200) + if(response->m_response_code != 200 && response->m_response_code != 201) return {error::wrong_response_code}; return response->m_body; @@ -149,7 +151,7 @@ namespace lwsf { namespace internal { namespace rpc using min_spent = wire::min_element_sizeof; std::optional timestamp; - std::optional> payment_id; + std::optional payment_id; wire::object(source, WIRE_FIELD_ARRAY(spent_outputs, min_spent), wire::optional_field("payment_id", std::ref(payment_id)), @@ -159,10 +161,10 @@ namespace lwsf { namespace internal { namespace rpc WIRE_FIELD(unlock_time), WIRE_OPTIONAL_FIELD(height), WIRE_FIELD(tx_hash), - WIRE_FIELD(is_coinbase) + WIRE_FIELD(is_coinbase), + WIRE_FIELD(mempool) ); - if (payment_id && !payment_id->empty()) { if (payment_id->size() == sizeof(crypto::hash8)) @@ -207,19 +209,67 @@ namespace lwsf { namespace internal { namespace rpc ); } + namespace + { + // This field is a mess between implementations, this cleans it up a bit + struct convert_rct + { + ringct operator()(const std::nullptr_t&) const noexcept + { + return ringct{{}, ringct::format::none}; + } + + ringct operator()(const std::string& source) const + { + struct ringct_triplet + { + ringct_triplet() = delete; + rct::key commitment; + rct::key mask; + rct::key amount; + }; + static_assert(sizeof(ringct_triplet) == 96); + + if (source.empty()) + return ringct{{}, ringct::format::none}; + + rct::key out{}; + if (source == "coinbase") // non-standard, openmonero + return ringct{rct::identity(), ringct::format::unencrypted}; + else if (source.size() == sizeof(ringct_triplet) * 2) + { + ringct_triplet rct{}; + if (!epee::from_hex::to_buffer(epee::as_mut_byte_span(rct), source)) + WIRE_DLOG_THROW(wire::error::schema::binary, "Invalid hex for ringct"); + return ringct{rct.mask, ringct::format::encrypted}; + } + + return ringct{{}, ringct::format::recompute}; + } + + template + ringct operator()(const T&) const + { + WIRE_DLOG_THROW(wire::error::schema::string, "Expected string or null"); + } + }; + } + void read_bytes(wire::json_reader& source, output& self) { + wire::basic_value raw_rct{}; wire::object(source, WIRE_FIELD(amount), WIRE_FIELD(index), WIRE_FIELD(global_index), WIRE_OPTIONAL_FIELD(recipient), - WIRE_FIELD(rct), + wire::optional_field("rct", std::ref(raw_rct)), WIRE_FIELD(tx_hash), WIRE_FIELD(tx_prefix_hash), WIRE_FIELD(public_key), WIRE_FIELD(tx_pub_key) ); + self.rct = std::visit(convert_rct{}, raw_rct.value); } void read_bytes(wire::json_reader& source, get_unspent_outs& self) @@ -233,5 +283,16 @@ namespace lwsf { namespace internal { namespace rpc WIRE_FIELD(fee_mask) ); } + + + void write_bytes(wire::json_writer& dest, const submit_raw_tx_request& self) + { + wire::object(dest, WIRE_FIELD(tx)); + } + + void read_bytes(wire::json_reader& source, submit_raw_tx_response& self) + { + wire::object(source, WIRE_FIELD(status)); + } }}} // lwsf // internal // rpc diff --git a/src/rpc.h b/src/rpc.h index 340ee5a..1d60262 100644 --- a/src/rpc.h +++ b/src/rpc.h @@ -30,17 +30,17 @@ #include #include +#include #include #include "byte_slice.h" // monero/contrib/epee/include #include "byte_stream.h" // moneor/contrib/epee/include #include "common/expect.h" // monero/src #include "crypto/crypto.h" // monero/src #include "ringct/rctTypes.h" // monero/src +#include "wire/basic_value.h" #include "wire/fwd.h" #include "wire/json.h" - // Contains code for interact - namespace epee { namespace net_utils { class blocked_mode_client; @@ -63,7 +63,7 @@ namespace lwsf { namespace internal { namespace rpc inline std::error_code make_error_code(const error value) noexcept { return std::error_code{int(value), error_category()}; - } + } //! Send `payload` to `client` at uri `endpoint`, and \return payload response @@ -84,7 +84,7 @@ namespace lwsf { namespace internal { namespace rpc F out{}; error = wire::json::from_bytes(std::move(*result), out); if (error) - return out; + return error; return out; } @@ -123,9 +123,10 @@ namespace lwsf { namespace internal { namespace rpc std::optional generated_locally; }; - inline void read_bytes(const wire::json_reader&, const login_response&) + inline void read_bytes(const wire::json_reader&, const login_response&) noexcept {} + struct daemon_status { daemon_status() = delete; @@ -181,6 +182,7 @@ namespace lwsf { namespace internal { namespace rpc std::optional height; crypto::hash tx_hash; bool is_coinbase; + bool mempool; transaction() : spent_outputs(), @@ -191,7 +193,8 @@ namespace lwsf { namespace internal { namespace rpc unlock_time(0), height(), tx_hash{}, - is_coinbase(false) + is_coinbase(false), + mempool(false) {} }; void read_bytes(wire::json_reader&, transaction&); @@ -212,11 +215,18 @@ namespace lwsf { namespace internal { namespace rpc struct ringct { ringct() = delete; - rct::key commitment; + + enum class format : std::uint8_t + { + none = 0, //!< Not ringct + encrypted, + recompute, + unencrypted + }; + rct::key mask; - rct::key amount; + format type; }; - static_assert(sizeof(ringct) == 96); struct output { @@ -254,6 +264,24 @@ namespace lwsf { namespace internal { namespace rpc std::uint64_t fee_mask; }; void read_bytes(wire::json_reader&, get_unspent_outs&); + + + struct submit_raw_tx_request + { + submit_raw_tx_request() = delete; + epee::byte_slice tx; + }; + void write_bytes(wire::json_writer&, const submit_raw_tx_request&); + + struct submit_raw_tx_response + { + submit_raw_tx_response() = delete; + static constexpr const char* endpoint() noexcept { return "/submit_raw_tx"; } + + std::string status; + }; + void read_bytes(wire::json_reader&, submit_raw_tx_response&); + }}} // lwsf // internal // rpc WIRE_DECLARE_BLOB_NS(lwsf::internal::rpc::ringct); diff --git a/src/transaction_history.cpp b/src/transaction_history.cpp index 0ab0e5b..a961837 100644 --- a/src/transaction_history.cpp +++ b/src/transaction_history.cpp @@ -34,7 +34,7 @@ #include "transaction_info.h" -#include "string_tools.h" // monero/contrib/epee/include +#include "hex.h" // monero/contrib/epee/include namespace lwsf { namespace internal { @@ -59,17 +59,15 @@ namespace lwsf { namespace internal std::shared_ptr transaction_history::transaction(int index) const { const boost::lock_guard lock{data_->sync}; - if (std::numeric_limits::max() < data_->primary.txes.size()) - throw std::runtime_error{"Exceeded max int size in transaction_history:transaction"}; - if (data_->primary.txes.size() < index || index < 0) + if (index < 0 || data_->primary.txes.size() < unsigned(index)) throw std::runtime_error{"index provided to transaction invalid"}; - return std::make_shared(data_, data_->primary.txes.nth(index)->second); + return std::make_shared(data_, data_->primary.txes.nth(unsigned(index))->second); } std::shared_ptr transaction_history::transaction(const std::string &id) const { crypto::hash binary_id{}; - if (!epee::string_tools::hex_to_pod(id, binary_id)) + if (!epee::from_hex::to_buffer(epee::as_mut_byte_span(binary_id), id)) throw std::runtime_error{"transaction_history given invalid hex id"}; const boost::lock_guard lock{data_->sync}; diff --git a/src/transaction_info.cpp b/src/transaction_info.cpp index 2efc055..48c6586 100644 --- a/src/transaction_info.cpp +++ b/src/transaction_info.cpp @@ -54,17 +54,24 @@ namespace lwsf { namespace internal transfers_.reserve(data_->spends.size()); for (const auto& spend : data_->spends) - transfers_.emplace_back(spend.amount, spend.address); + transfers_.emplace_back(spend.second.amount, spend.second.address); } transaction_info::~transaction_info() {} + std::string transaction_info::description() const + { + const boost::lock_guard lock{wallet_->sync}; + return data_->description; + } + uint64_t transaction_info::confirmations() const { - const auto block_height = blockHeight(); + if (!data_->height) + return 0; const boost::lock_guard lock{wallet_->sync}; - return std::max(wallet_->blockchain_height, block_height) - block_height; + return std::max(wallet_->blockchain_height, *data_->height) - *data_->height + 1; } std::string transaction_info::hash() const diff --git a/src/transaction_info.h b/src/transaction_info.h index 576a4fe..f63dee2 100644 --- a/src/transaction_info.h +++ b/src/transaction_info.h @@ -55,13 +55,13 @@ namespace lwsf { namespace internal transaction_info& operator=(transaction_info&&) = delete; virtual Direction direction() const override { return data_->direction; } - virtual bool isPending() const override { return confirmations() == 0; } + virtual bool isPending() const override { return !data_->height; } virtual bool isFailed() const override { return false; } virtual bool isCoinbase() const override { return data_->coinbase; } virtual uint64_t amount() const override { return data_->amount; } virtual uint64_t fee() const override { return data_->fee; } virtual uint64_t blockHeight() const override { return data_->height.value_or(0); } - virtual std::string description() const override { return data_->description; } + virtual std::string description() const override; virtual std::set subaddrIndex() const override { return {}; }; virtual uint32_t subaddrAccount() const override { return 0; } virtual std::string label() const override { return data_->label; } diff --git a/src/wallet.cpp b/src/wallet.cpp index 56d9c2d..905d7a3 100644 --- a/src/wallet.cpp +++ b/src/wallet.cpp @@ -28,16 +28,30 @@ #include "wallet.h" +#if defined (__unix__) || (defined (__APPLE__) && defined (__MACH__)) + #include +#endif + +#include +#include #include +#include +#include +#include +#include #include "backend.h" +#include "error.h" #include "hex.h" // monero/contrib/epee/include #include "lwsf_config.h" #include "net/parse.h" // monero/src #include "net/socks_connect.h" // monero/src #include "transaction_history.h" +#include "wire.h" +#include "wire/msgpack.h" namespace lwsf { namespace internal { + namespace { struct null_connector @@ -48,29 +62,369 @@ namespace lwsf { namespace internal throw std::runtime_error{"Unable to connect"}; } }; + + struct encrypted_file + { + std::string cipher; //!< Name of cipher+authentication used + std::string pwhasher; //!< Name of pwhasher used + epee::byte_slice salt; //!< Pwhashing salt + epee::byte_slice nonce; //!< Encryption iv/nonce + epee::byte_slice epayload; //!< Encrypted contents + std::uint64_t iterations; //!< Pwhashing iterations + std::uint32_t memory; //!< Pwhashing memory + + encrypted_file() = delete; + + //! \pre `sodium_init()` has been called + static epee::byte_slice get_random(const std::size_t length) + { + epee::byte_stream out; + out.put_n(0, length); + randombytes_buf(out.data(), length); + return epee::byte_slice{std::move(out)}; + } + + static constexpr const char* pwhasher_name() noexcept + { + return "argon2id"; + } + static constexpr int pwhash_algorithm() noexcept + { + return crypto_pwhash_argon2id_ALG_ARGON2ID13; + } + static constexpr std::size_t salt_size() noexcept + { + return crypto_pwhash_argon2id_SALTBYTES; + } + static constexpr std::uint64_t ops_min() noexcept { return 5; } + static constexpr std::size_t memory_limit() noexcept { return 7 * 1024 * 1024; } + static epee::byte_slice get_salt() { return get_random(salt_size()); } + + static constexpr const char* cipher_name() noexcept + { + return "chacha20-poly1305_ietf"; + } + static constexpr unsigned long long max_size() noexcept + { + return crypto_aead_chacha20poly1305_ietf_MESSAGEBYTES_MAX; + } + static constexpr std::size_t key_size() noexcept + { + return crypto_aead_chacha20poly1305_ietf_KEYBYTES; + } + static constexpr std::size_t tag_size() noexcept + { + return crypto_aead_chacha20poly1305_ietf_ABYTES; + } + static constexpr std::size_t nonce_size() noexcept + { + return crypto_aead_chacha20poly1305_ietf_NPUBBYTES; + } + static epee::byte_slice get_nonce() { return get_random(nonce_size()); } + + //! \pre `sodium_init()` has been called + std::array get_key(const std::string& password) const + { + std::array key{{}}; + if (crypto_pwhash_argon2id( + key.data(), key.size(), + password.data(), password.size(), + salt.data(), + iterations, memory, pwhash_algorithm()) != 0) + { + throw std::runtime_error{std::string{pwhasher_name()} + " failed"}; + } + return key; + } + + static encrypted_file + make(const epee::byte_slice& upayload, const std::uint64_t iterations, const std::string& password) + { + if (sodium_init() < 0) + throw std::runtime_error{"Failed to initialize libsodium"}; + + if (max_size() < upayload.size()) + throw std::runtime_error{std::string{"Exceeded max size for "} + cipher_name()}; + if (tag_size() < std::numeric_limits::max() - upayload.size()) + throw std::runtime_error{"Exceeded max size_t after authentication tag"}; + + encrypted_file out{ + cipher_name(), + pwhasher_name(), + get_salt(), + get_nonce(), + nullptr, + std::max(iterations, ops_min()), + memory_limit() + }; + + const auto key = out.get_key(password); + + epee::byte_stream buffer; + buffer.put_n(0, upayload.size() + tag_size()); + + unsigned long long out_bytes = buffer.size(); + if (crypto_aead_chacha20poly1305_ietf_encrypt( + buffer.data(), std::addressof(out_bytes), + upayload.data(), upayload.size(), + nullptr, 0, + nullptr, + out.nonce.data(), key.data()) != 0) + { + throw std::runtime_error{std::string{cipher_name()} + " encryption failed"}; + } + + out.epayload = epee::byte_slice{std::move(buffer)}.take_slice(out_bytes); + return out; + } + + //! \return Unencrypted payload, or `nullptr`. + epee::byte_slice get_payload(const std::string& password) const + { + if (nonce.size() != nonce_size()) + return nullptr; + if (salt.size() != salt_size()) + return nullptr; + if (cipher != cipher_name()) + return nullptr; + if (pwhasher != pwhasher_name()) + return nullptr; + if (max_size() < epayload.size()) + return nullptr; + + if (sodium_init() < 0) + throw std::runtime_error{"Failed to initialize libsodium"}; + + const auto key = get_key(password); + + epee::byte_stream buffer; + buffer.put_n(0, epayload.size()); + + static_assert( + std::numeric_limits::max() <= std::numeric_limits::max() + ); + unsigned long long out_bytes = buffer.size(); + if (crypto_aead_chacha20poly1305_ietf_decrypt( + buffer.data(), std::addressof(out_bytes), + nullptr, + epayload.data(), epayload.size(), + nullptr, 0, + nonce.data(), key.data()) == 0) + { + throw std::runtime_error{std::string{cipher_name()} + " decryption failed"}; + } + + return epee::byte_slice{std::move(buffer)}.take_slice(out_bytes); + } + }; + + template + void map_encrypted_file(F& format, T& self) + { + wire::object(format, + WIRE_FIELD(cipher), + WIRE_FIELD(pwhasher), + WIRE_FIELD(salt), + WIRE_FIELD(nonce), + WIRE_FIELD(epayload), + WIRE_FIELD(iterations), + WIRE_FIELD(memory) + ); + } + + WIRE_DEFINE_OBJECT(encrypted_file, map_encrypted_file); + + epee::byte_slice try_load(const std::string& filename) + { + std::ifstream file{filename, std::ios::binary | std::ios::ate}; + if (!file.is_open()) + return nullptr; + + const auto size = file.tellg(); + if (size < 0) + return nullptr; + + epee::byte_stream buffer; + buffer.put_n(0, std::size_t(size)); + + file.seekg(0); + file.read(reinterpret_cast(buffer.data()), buffer.size()); + + if (file.good()) + return epee::byte_slice{std::move(buffer)}; + return nullptr; + } + +#if defined (__unix__) || (defined (__APPLE__) && defined (__MACH__)) + bool atomic_file_write(const std::filesystem::path& filename, const std::filesystem::path& directory, epee::byte_slice contents) noexcept + { + const int fd = + open(filename.c_str(), O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR); + if (fd < 0) + return false; + + bool rc = false; + if (0 <= write(fd, contents.data(), contents.size()) && 0 <= fsync(fd)) + { + const int dir = open(directory.c_str(), O_RDONLY); + if (0 <= dir) + { + if (0 <= fsync(dir)) + rc = true; + close(dir); + } + } + + close(fd); + return rc; + } +#else + #error atomic_file_write not implemented for this platform +#endif + } // anonymous + + void wallet::set_error(const std::error_code status) + { + const boost::lock_guard lock{sync_}; + status_ = status; + exception_error_.clear(); } - wallet::wallet(std::string filename, std::shared_ptr data) - : data_(std::move(data)), filename_(std::move(filename)), thread_() + void wallet::set_critical(const std::exception& e) + { + const boost::lock_guard lock{sync_}; + exception_error_ = e.what(); + status_.clear(); + } + + wallet::wallet(create_tag, NetworkType nettype, std::string filename, std::string password, const std::uint64_t kdf_rounds, std::shared_ptr data) + : data_(std::move(data)), + filename_(std::move(filename)), + password_(std::move(password)), + exception_error_(), + status_(), + thread_(), + iterations_(kdf_rounds), + sync_() { if (!data_) - throw std::logic_error{"Unepected nullptr in internal::wallet"}; + throw std::logic_error{"Unexpected nullptr in internal::wallet"}; + if (sodium_init() < 0) + throw std::runtime_error{"Failed to initialize libsodium"}; + + // use libsodium random, more portable + crypto::secret_key recovery; + randombytes_buf(std::addressof(unwrap(unwrap(recovery))), sizeof(recovery)); + + cryptonote::account_base base{}; + base.generate(recovery, true); + + const auto& keys = base.get_keys(); + + const boost::lock_guard lock{data_->sync}; + data_->primary.view.sec = keys.m_view_secret_key; + data_->primary.view.pub = keys.m_account_address.m_view_public_key; + + data_->primary.spend.sec = keys.m_spend_secret_key; + data_->primary.spend.pub = keys.m_account_address.m_spend_public_key; + + data_->primary.generated_locally = true; + data_->primary.type = nettype; + + store(""); + } + + wallet::wallet(open_tag, NetworkType nettype, std::string filename, std::string password, const std::uint64_t kdf_rounds, std::shared_ptr data) + : data_(std::move(data)), + filename_(std::move(filename)), + password_(std::move(password)), + exception_error_(), + status_(), + thread_(), + iterations_(kdf_rounds), + sync_() + { + if (!data_) + throw std::logic_error{"Unexpected nullptr in internal::wallet"}; + + try + { + epee::byte_slice file = try_load(filename + ".new"); + if (file.empty()) + file = try_load(filename); + + if (!file.empty()) + { + encrypted_file contents{}; + if (!(status_ = wire::msgpack::from_bytes(std::move(file), contents))) + { + epee::byte_slice payload = contents.get_payload(password_); + if (!payload.empty()) + status_ = data_->from_bytes(std::move(payload)); + else + status_ = error::crypto_failure; + } + } + else + status_ = error::read_failure; + } + catch (const std::system_error& e) + { + status_ = e.code(); + } + catch (const std::exception& e) + { + status_.clear(); + exception_error_ = e.what(); + } } wallet::~wallet() {} - void wallet::statusWithErrorString(int& status, std::string& errorString) const + int wallet::status() const { - const boost::unique_lock lock{data_->sync}; - status = data_->status.value(); - if (status) - errorString = data_->status.message(); - else - errorString.clear(); + int out = 0; + std::string unused; + statusWithErrorString(out, unused); + return out; } - NetworkType wallet::nettype() const { data_->primary.type; } + std::string wallet::errorString() const + { + int unused = 0; + std::string out; + statusWithErrorString(unused, out); + return out; + } + + void wallet::statusWithErrorString(int& status, std::string& errorString) const + { + const boost::lock_guard lock{sync_}; + if (!exception_error_.empty()) + { + status = Status_Critical; + errorString = exception_error_; + } + else if (status_) + { + status = Status_Error; + errorString = status_.message(); + } + else + { + status = Status_Ok; + errorString.clear(); + } + } + + bool wallet::setPassword(const std::string &password) + { + password_ = password; + return true; + } + + NetworkType wallet::nettype() const { return data_->primary.type; } std::string wallet::secretViewKey() const { @@ -94,22 +448,62 @@ namespace lwsf { namespace internal bool wallet::store(const std::string& path) { - expect bytes = data_->to_bytes(); - if (!bytes) - return false; + const std::string& real_path = path.empty() ? filename_ : path; - //! \TODO Write, fsync, then atomic swap filenames + std::error_code status{}; + try + { + expect payload = data_->to_bytes(); + if (payload) + { + const auto contents = encrypted_file::make(*payload, iterations_, password_); + const auto payload_size = payload->size(); + *payload = nullptr; // free up some memory that is no longer needed - std::ofstream file{path, std::ios::out | std::ios::binary}; - file.write(reinterpret_cast(bytes->data()), bytes->size()); - file.flush(); - return file.good(); + epee::byte_stream buffer; + buffer.reserve(payload_size + 2048); + + if (!(status = wire::msgpack::to_bytes(buffer, contents))) + { + const std::filesystem::path file = real_path; + const std::filesystem::path new_file = real_path + ".new"; + const std::filesystem::path directory = + std::filesystem::path{real_path}.remove_filename(); + + if (std::filesystem::exists(new_file, status)) + std::filesystem::remove(file, status); + std::filesystem::rename(new_file, file, status); + + // blocks until file and directory contents are synced + if (atomic_file_write(new_file, directory, epee::byte_slice{std::move(buffer)})) + { + std::filesystem::remove(file, status); + std::filesystem::rename(new_file, file, status); + } + else + status = error::write_failure; + } + } + else + status = payload.error(); + } + catch (const std::system_error& e) + { + status = e.code(); + } + catch (const std::exception& e) + { + set_critical(e); + } + + // this will clear any existing errors + set_error(status); } bool wallet::init(const std::string &daemon_address, uint64_t, const std::string &daemon_username, const std::string &daemon_password, bool use_ssl, bool light_wallet, const std::string &proxy_address) { if (!light_wallet) - throw std::runtime_error{"Only light_wallets are supported with this instance"}; + throw std::logic_error{"Only light_wallets are supported with this instance"}; const auto split = daemon_address.rfind(':'); if (!setProxy(proxy_address)) @@ -148,7 +542,7 @@ namespace lwsf { namespace internal Wallet::ConnectionStatus wallet::connected() const { return data_->client.is_connected() ? - ConnectionStatus::Connected : ConnectionStatus::Disconnected; + ConnectionStatus_Connected : ConnectionStatus_Disconnected; } bool wallet::setProxy(const std::string &address) @@ -157,8 +551,7 @@ namespace lwsf { namespace internal if (!endpoint) { data_->client.set_connector(null_connector{}); - const boost::lock_guard lock{data_->sync}; - data_->status = endpoint.error(); + set_error(endpoint.error()); return false; } data_->client.set_connector(net::socks::connector{std::move(*endpoint)}); @@ -177,9 +570,21 @@ namespace lwsf { namespace internal uint64_t wallet::daemonBlockChainTargetHeight() const { return blockChainHeight(); } - + uint64_t wallet::estimateTransactionFee(const std::vector> &destinations, + PendingTransaction::Priority priority) const + { + throw std::runtime_error{"Not Implemented yet"}; + } + std::shared_ptr wallet::history() { return std::make_shared(data_); } + + + void wallet::setListener(std::shared_ptr listener) + { + const boost::lock_guard lock{data_->sync}; + data_->listener = std::move(listener); + } }} // lwsf // internal diff --git a/src/wallet.h b/src/wallet.h index fe134e5..5062d4d 100644 --- a/src/wallet.h +++ b/src/wallet.h @@ -30,7 +30,9 @@ #include "lws_frontend.h" +#include #include +#include #include #include "cryptonote_basic/account.h" // monero/src #include "net/http_client.h" // monero/contrib/epee/include @@ -41,15 +43,28 @@ namespace internal { namespace backend { struct wallet; } + //! \TODO Mark final when completely implemented class wallet : public lwsf::Wallet { const std::shared_ptr data_; - std::string filename_; + const std::string filename_; + std::string password_; + std::string exception_error_; + std::error_code status_; boost::thread thread_; - -public: - explicit wallet(std::string filename, std::shared_ptr data); + const std::uint64_t iterations_; + mutable boost::mutex sync_; + + void set_error(const std::error_code status); + void set_critical(const std::exception& e); + + public: + struct create_tag{}; + struct open_tag{}; + + wallet(create_tag, NetworkType nettype, std::string filename, std::string password, std::uint64_t kdf_rounds, std::shared_ptr data); + wallet(open_tag, NetworkType nettpe, std::string filename, std::string password, std::uint64_t kdf_rounds, std::shared_ptr data); wallet(const wallet&) = delete; wallet(wallet&&) = delete; @@ -62,10 +77,12 @@ public: virtual std::string getSeedLanguage() const = 0; virtual void setSeedLanguage(const std::string &arg) = 0; + virtual int status() const override; + virtual std::string errorString() const override; virtual void statusWithErrorString(int& status, std::string& errorString) const override; - virtual bool setPassword(const std::string &password) = 0; - virtual const std::string& getPassword() const = 0; + virtual bool setPassword(const std::string &password) override; + virtual const std::string& getPassword() const override { return password_; } virtual bool setDevicePin(const std::string &) override { return false; }; virtual bool setDevicePassphrase(const std::string &) override { return false; }; @@ -212,8 +229,8 @@ public: * @return - true if connected */ virtual ConnectionStatus connected() const override; - virtual void setTrustedDaemon(bool arg) = 0; - virtual bool trustedDaemon() const = 0; + virtual void setTrustedDaemon(bool) override {} + virtual bool trustedDaemon() const override { return true; } virtual bool setProxy(const std::string &address) override; virtual uint64_t balance(uint32_t accountIndex = 0) const = 0; uint64_t balanceAll() const { @@ -499,7 +516,8 @@ public: * \brief disposeTransaction - destroys transaction object * \param t - pointer to the "PendingTransaction" object. Pointer is not valid after function returned; */ - virtual void disposeTransaction(PendingTransaction * t) = 0; + virtual void disposeTransaction(PendingTransaction * t) override final + {} /*! * \brief Estimates transaction fee. @@ -507,7 +525,7 @@ public: * \return Estimated fee. */ virtual uint64_t estimateTransactionFee(const std::vector> &destinations, - PendingTransaction::Priority priority) const = 0; + PendingTransaction::Priority priority) const override final; /*! * \brief exportKeyImages - exports key images to file @@ -549,7 +567,7 @@ public: virtual std::shared_ptr addressBook() = 0; virtual std::shared_ptr subaddress() = 0; virtual std::shared_ptr subaddressAccount() = 0; - virtual void setListener(WalletListener *) = 0; + virtual void setListener(std::shared_ptr) override; /*! * \brief defaultMixin - returns number of mixins used in transactions * \return diff --git a/src/wallet_manager.cpp b/src/wallet_manager.cpp index ebad466..bc7beaa 100644 --- a/src/wallet_manager.cpp +++ b/src/wallet_manager.cpp @@ -28,11 +28,14 @@ #include "lws_frontend.h" +#include #include +#include #include #include #include #include "backend.h" +#include "common/dns_utils.h" // monero/src #include "lwsf_config.h" #include "net/http_client.h" // monero/contrib/epee/include #include "net/parse.h" // monero/src @@ -91,8 +94,9 @@ namespace lwsf */ std::unique_ptr createWallet(const std::string &path, const std::string &password, const std::string &language, NetworkType nettype, uint64_t kdf_rounds) override { - //! \TODO Complete - return std::make_unique(path, std::make_shared(true)); + return std::make_unique( + wallet::create_tag{}, nettype, path, password, kdf_rounds, std::make_shared() + ); } /*! @@ -107,7 +111,11 @@ namespace lwsf std::unique_ptr openWallet(const std::string &path, const std::string &password, NetworkType nettype, uint64_t kdf_rounds, std::shared_ptr listener) override { - //! \TODO Complete + auto back = std::make_shared(); + back->listener = std::move(listener); + return std::make_unique( + wallet::open_tag{}, nettype, path, password, kdf_rounds, std::move(back) + ); } /*! @@ -219,7 +227,7 @@ namespace lwsf * \param wallet previously opened / created wallet instance * \return None */ - bool closeWallet(std::unique_ptr wallet, bool store) override + bool closeWallet(std::unique_ptr wallet, const bool store) override { if (wallet && store) wallet->store(wallet->filename()); @@ -236,7 +244,7 @@ namespace lwsf */ bool walletExists(const std::string &path) override { - //! \TODO Complete + return std::filesystem::exists(std::filesystem::path{path}); } /*! @@ -324,11 +332,25 @@ namespace lwsf std::string resolveOpenAlias(const std::string &address, bool &dnssec_valid) const override { - //! \TODO Complete + const boost::string_ref prefix{"oa1:xmr "}; + std::string real_address = + tools::DNSResolver::instance().get_dns_format_from_oa_address(address); + + bool dnssec_available = false; + std::vector records = + tools::DNSResolver::instance().get_txt_record(real_address, dnssec_available, dnssec_valid); + for (boost::string_ref record : records) + { + if (record.starts_with(prefix)) + { + record = record.substr(prefix.size()); + return std::string{record.data(), record.size()}; + } + } } - //! checks for an update and returns version, hash and url - static std::tuple checkUpdates( + //! checks for an update and returns version, hash and url + static std::tuple checkUpdates( const std::string &software, std::string subdir, const char *buildtag = nullptr, diff --git a/src/wire/CMakeLists.txt b/src/wire/CMakeLists.txt index e0dee99..68c2a79 100644 --- a/src/wire/CMakeLists.txt +++ b/src/wire/CMakeLists.txt @@ -26,8 +26,8 @@ # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -set(monero-lws-wire_sources error.cpp read.cpp write.cpp) -set(monero-lws-wire_headers error.h field.h filters.h fwd.h json.h read.h traits.h vector.h write.h) +set(monero-lws-wire_sources basic_value.cpp error.cpp read.cpp write.cpp) +set(monero-lws-wire_headers basic_value.h error.h field.h filters.h fwd.h json.h read.h traits.h vector.h write.h) add_library(monero-lws-wire ${monero-lws-wire_sources} ${monero-lws-wire_headers}) target_include_directories(monero-lws-wire PUBLIC "${LMDB_INCLUDE}") diff --git a/src/wire/fwd.h b/src/wire/fwd.h index 01f4f29..d077a40 100644 --- a/src/wire/fwd.h +++ b/src/wire/fwd.h @@ -62,6 +62,7 @@ namespace wire { + struct basic_value; class reader; struct writer; } diff --git a/src/wire/json/read.cpp b/src/wire/json/read.cpp index 0043e09..59b419c 100644 --- a/src/wire/json/read.cpp +++ b/src/wire/json/read.cpp @@ -25,17 +25,18 @@ // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -#include "read.h" +#include "serialization/wire/json/read.h" #include #include #include #include -#include "common/expect.h" // monero/src -#include "hex.h" // monero/contrib/epee/include -#include "wire/error.h" -#include "wire/json/error.h" +#include "byte_stream.h" +#include "hex.h" +#include "serialization/wire/basic_value.h" +#include "serialization/wire/error.h" +#include "serialization/wire/json/error.h" namespace { @@ -50,8 +51,7 @@ namespace //! \throw std::system_error by converting `code` into a std::error_code [[noreturn]] void throw_json_error(const epee::span source, const rapidjson::Reader& reader, const wire::error::schema expected) { - const std::size_t offset = std::min(source.size(), reader.GetErrorOffset()); - const std::size_t start = offset;//std::max(snippet_size / 2, offset) - (snippet_size / 2); + const std::size_t start = std::min(source.size(), reader.GetErrorOffset()); const std::size_t end = start + std::min(snippet_size, source.size() - start); const boost::string_ref text{reinterpret_cast(source.data()) + start, end - start}; @@ -64,7 +64,7 @@ namespace case rapidjson::kParseErrorTermination: // the handler returned false break; } - WIRE_DLOG_THROW(expected, "near '" << text << '\''); + WIRE_DLOG_THROW(expected, "near \"" << text << '"'); } } @@ -72,36 +72,37 @@ namespace wire { struct json_reader::rapidjson_sax { - struct string_contents - { - const char* ptr; - std::size_t length; - }; - union { bool boolean; std::intmax_t integer; std::uintmax_t unsigned_integer; double number; - string_contents string; } value; - error::schema expected_; + std::string* const string_; + const error::schema expected_; + error::schema actual_; bool negative; - explicit rapidjson_sax(error::schema expected) noexcept - : expected_(expected), negative(false) + rapidjson_sax(error::schema expected) noexcept + : string_(nullptr), expected_(expected), actual_(error::schema::maximum_depth), negative(false) {} - bool Null() const noexcept + explicit rapidjson_sax(std::string& string, error::schema expected) noexcept + : string_(std::addressof(string)), expected_(expected), actual_(error::schema::maximum_depth), negative(false) + {} + + bool Null() noexcept { + actual_ = error::schema::none; return expected_ == error::schema::none; } bool Bool(bool i) noexcept { value.boolean = i; + actual_ = error::schema::boolean; return expected_ == error::schema::boolean || expected_ == error::schema::none; } @@ -120,13 +121,14 @@ namespace wire { default: return false; + case error::schema::none: case error::schema::integer: value.integer = i; + actual_ = error::schema::integer; break; case error::schema::number: value.number = i; - break; - case error::schema::none: + actual_ = error::schema::number; break; } return true; @@ -137,13 +139,14 @@ namespace wire { default: return false; + case error::schema::none: case error::schema::integer: value.unsigned_integer = i; + actual_ = error::schema::integer; break; case error::schema::number: value.number = i; - break; - case error::schema::none: + actual_ = error::schema::number; break; } return true; @@ -152,17 +155,20 @@ namespace wire bool Double(double i) noexcept { value.number = i; + actual_ = error::schema::number; return expected_ == error::schema::number || expected_ == error::schema::none; } - bool RawNumber(const char*, std::size_t, bool) const noexcept + static constexpr bool RawNumber(const char*, std::size_t, bool) noexcept { return false; } bool String(const char* str, std::size_t length, bool) noexcept { - value.string = {str, length}; + actual_ = error::schema::string; + if (string_) + string_->assign(str, length); return expected_ == error::schema::string || expected_ == error::schema::none; } bool Key(const char* str, std::size_t length, bool) @@ -215,20 +221,59 @@ namespace wire read_next_value(accept_all); } - json_reader::json_reader(std::string&& source) - : reader(nullptr), - source_(std::move(source)), - reader_() + std::size_t json_reader::do_start_array(std::size_t) { - remaining_ = {reinterpret_cast(source_.data()), source_.size()}; + if (get_next_token() != '[') + WIRE_DLOG_THROW_(error::schema::array); + remaining_.remove_prefix(1); + return 0; } + std::size_t json_reader::do_start_object() + { + if (get_next_token() != '{') + WIRE_DLOG_THROW_(error::schema::object); + remaining_.remove_prefix(1); + return 0; + } + + json_reader::json_reader(epee::span source) + : reader({reinterpret_cast(source.data()), source.size()}), + str_buffer_(), + reader_() + {} + void json_reader::check_complete() const { if (depth()) WIRE_DLOG_THROW(error::rapidjson_e(rapidjson::kParseErrorUnspecificSyntaxError), "Unexpected end"); } + basic_value json_reader::basic() + { + rapidjson_sax json_any{str_buffer_, error::schema::none}; + read_next_value(json_any); + switch(json_any.actual_) + { + default: + break; + case error::schema::none: + return {nullptr}; + case error::schema::boolean: + return {json_any.value.boolean}; + case error::schema::integer: + if (json_any.negative) + return {json_any.value.integer}; + else + return {json_any.value.unsigned_integer}; + case error::schema::number: + return {json_any.value.number}; + case error::schema::string: + return {std::move(str_buffer_)}; + }; + WIRE_DLOG_THROW(error::schema::number, "expected a boolean, integer, float or string"); + } + bool json_reader::boolean() { rapidjson_sax json_bool{error::schema::boolean}; @@ -264,24 +309,24 @@ namespace wire WIRE_DLOG_THROW_(error::schema::larger_integer); return static_cast(json_uint.value.integer); } - /* - const std::vector& json_reader::unsigned_integer_array() - { - read_next_unsigned_array( - }*/ std::uintmax_t json_reader::safe_unsigned_integer() { - if (get_next_token() != '"') - WIRE_DLOG_THROW_(error::schema::string); - remaining_.remove_prefix(1); + bool is_string = false; + if (get_next_token() == '"') + { + remaining_.remove_prefix(1); + is_string = true; + } const std::uintmax_t out = unsigned_integer(); - if (get_next_token() != '"') - WIRE_DLOG_THROW_(error::rapidjson_e(rapidjson::kParseErrorStringMissQuotationMark)); - remaining_.remove_prefix(1); - + if (is_string) + { + if (get_next_token() != '"') + WIRE_DLOG_THROW_(error::rapidjson_e(rapidjson::kParseErrorStringMissQuotationMark)); + remaining_.remove_prefix(1); + } return out; } @@ -294,38 +339,34 @@ namespace wire std::string json_reader::string() { - rapidjson_sax json_string{error::schema::string}; - read_next_value(json_string); - return std::string{json_string.value.string.ptr, json_string.value.string.length}; + { + rapidjson_sax json_string{str_buffer_, error::schema::string}; + read_next_value(json_string); + } + return std::string{std::move(str_buffer_)}; } - std::vector json_reader::binary() + epee::byte_slice json_reader::binary() { const boost::string_ref value = get_next_string(); - std::vector out; - out.resize(value.size() / 2); + epee::byte_stream out{}; + out.put_n(0, value.size() / 2); if (!epee::from_hex::to_buffer(epee::to_mut_span(out), value)) - WIRE_DLOG_THROW_(error::schema::binary); + WIRE_DLOG_THROW(error::schema::binary, "invalid hex"); - return out; + return epee::byte_slice{std::move(out)}; } - void json_reader::binary(epee::span dest) + std::size_t json_reader::binary(epee::span dest, const bool exact) { const boost::string_ref value = get_next_string(); + if (!exact && value.size() / 2 <= dest.size()) + dest = {dest.data(), value.size() / 2}; // `from_hex` will ensure exact size if (!epee::from_hex::to_buffer(dest, value)) - WIRE_DLOG_THROW(error::schema::fixed_binary, "of size" << dest.size() * 2 << " but got " << value.size()); - } - - std::size_t json_reader::start_array(std::size_t) - { - if (get_next_token() != '[') - WIRE_DLOG_THROW_(error::schema::array); - remaining_.remove_prefix(1); - increment_depth(); - return 0; + WIRE_DLOG_THROW(exact ? error::schema::fixed_binary : error::schema::binary, "invalid hex and/or expected size " << dest.size() * 2 << " but got " << value.size()); + return dest.size(); } bool json_reader::is_array_end(const std::size_t count) @@ -348,21 +389,11 @@ namespace wire return false; } - std::size_t json_reader::start_object() - { - if (get_next_token() != '{') - WIRE_DLOG_THROW_(error::schema::object); - remaining_.remove_prefix(1); - increment_depth(); - return 0; - } - bool json_reader::key(const epee::span map, std::size_t& state, std::size_t& index) { - rapidjson_sax json_key{error::schema::string}; - const auto process_key = [map] (const rapidjson_sax::string_contents value) + rapidjson_sax json_key{str_buffer_, error::schema::string}; + const auto process_key = [map] (const boost::string_ref key) { - const boost::string_ref key{value.ptr, value.length}; for (std::size_t i = 0; i < map.size(); ++i) { if (map[i].name == key) @@ -395,7 +426,7 @@ namespace wire // parse key read_next_value(json_key); - index = process_key(json_key.value.string); + index = process_key(str_buffer_); if (get_next_token() != ':') WIRE_DLOG_THROW_(error::rapidjson_e(rapidjson::kParseErrorObjectMissColon)); remaining_.remove_prefix(1); diff --git a/src/wire/json/read.h b/src/wire/json/read.h index 6bbd23e..13151b2 100644 --- a/src/wire/json/read.h +++ b/src/wire/json/read.h @@ -36,6 +36,7 @@ #include #include "wire/field.h" +#include "wire/fwd.h" #include "wire/json/base.h" #include "wire/read.h" #include "wire/traits.h" @@ -63,6 +64,9 @@ namespace wire //! \throw wire::exception if JSON parsing is incomplete. void check_complete() const override final; + //! \throw wire::exception if array, object, or end of stream. + basic_value basic(); + //! \throw wire::exception if next token not a boolean. bool boolean() override final; @@ -72,7 +76,7 @@ namespace wire //! \throw wire::exception if next token not an unsigned integer. std::uintmax_t unsigned_integer() override final; - //! \throw wire::exception if next token is not an integer encoded as string + //! \throw wire::exception if next token is not an integer encoded as string OR unsigned integer std::uintmax_t safe_unsigned_integer(); //! \throw wire::exception if next token not a valid real number @@ -82,7 +86,7 @@ namespace wire std::string string() override final; //! \throw wire::exception if next token cannot be read as hex - std::vector binary() override final; + epee::byte_slice binary() override final; //! \throw wire::exception if next token cannot be read as hex into `dest`. void binary(epee::span dest) override final; diff --git a/src/wire/msgpack/read.cpp b/src/wire/msgpack/read.cpp index cf532a6..b66a816 100644 --- a/src/wire/msgpack/read.cpp +++ b/src/wire/msgpack/read.cpp @@ -413,11 +413,12 @@ namespace wire return std::string{reinterpret_cast(bytes.data()), bytes.size()}; } - std::vector msgpack_reader::binary() + epee::byte_slice msgpack_reader::binary() { update_tags_remaining(); const epee::span bytes = read_binary(remaining_, get_tag()); - return std::vector{bytes.begin(), bytes.end()}; + const std::size_t begin = bytes.data() - source_.data(); + return source_.get_slice(begin, begin + bytes.size()); } void msgpack_reader::binary(epee::span dest) diff --git a/src/wire/msgpack/read.h b/src/wire/msgpack/read.h index 6ad03a2..4712fc2 100644 --- a/src/wire/msgpack/read.h +++ b/src/wire/msgpack/read.h @@ -115,7 +115,7 @@ namespace wire std::string string() override final; //! \throw wire::exception if next token cannot be read as hex - std::vector binary() override final; + epee::byte_slice binary() override final; //! \throw wire::exception if next token cannot be read as hex into `dest`. void binary(epee::span dest) override final; diff --git a/src/wire/read.h b/src/wire/read.h index 14f896c..7425191 100644 --- a/src/wire/read.h +++ b/src/wire/read.h @@ -32,8 +32,8 @@ #include #include #include -#include +#include "byte_slice.h" // monero/contrib/epee/include #include "common/expect.h" // monero/src #include "span.h" // monero/contrib/epee/include #include "wire/error.h" @@ -106,7 +106,7 @@ namespace wire virtual std::string string() = 0; // ! \throw wire::exception if next value cannot be read as binary - virtual std::vector binary() = 0; + virtual epee::byte_slice binary() = 0; //! \throw wire::exception if next value cannot be read as binary into `dest`. virtual void binary(epee::span dest) = 0; @@ -163,7 +163,7 @@ namespace wire { dest = source.string(); } template - inline void read_bytes(R& source, std::vector& dest) + inline void read_bytes(R& source, epee::byte_slice& dest) { dest = source.binary(); } template