From 1c3370cc6f6e9d86a43d80285d5dcf874ad1d493 Mon Sep 17 00:00:00 2001 From: Lee Clagett Date: Fri, 4 Oct 2024 17:36:05 -0400 Subject: [PATCH] Various updates to a still WIP frontend --- include/lws_frontend.h | 6 +- src/backend.cpp | 143 +++++++++++++--------- src/backend.h | 15 ++- src/rpc.cpp | 14 +++ src/rpc.h | 14 ++- src/wallet.cpp | 261 +++++++++++++++++++++++++++++++++-------- src/wallet.h | 29 +++-- src/wallet_manager.cpp | 15 ++- 8 files changed, 372 insertions(+), 125 deletions(-) diff --git a/include/lws_frontend.h b/include/lws_frontend.h index a933f54..4e1b1cc 100644 --- a/include/lws_frontend.h +++ b/include/lws_frontend.h @@ -416,7 +416,7 @@ struct WalletListener /** * @brief If the listener is created before the wallet this enables to set created wallet object */ - virtual void onSetWallet(Wallet * wallet) { (void)wallet; }; + virtual void onSetWallet(std::shared_ptr wallet) { (void)wallet; }; }; @@ -1129,8 +1129,8 @@ struct WalletManager * \param listener Wallet listener to set to the wallet after creation * \return Wallet instance (Wallet::status() needs to be called to check if opened successfully) */ - virtual std::unique_ptr openWallet(const std::string &path, const std::string &password, NetworkType nettype, uint64_t kdf_rounds = 1, std::shared_ptr listener = nullptr) = 0; - std::unique_ptr openWallet(const std::string &path, const std::string &password, bool testnet = false) // deprecated + virtual std::shared_ptr openWallet(const std::string &path, const std::string &password, NetworkType nettype, uint64_t kdf_rounds = 1, std::shared_ptr listener = nullptr) = 0; + std::shared_ptr openWallet(const std::string &path, const std::string &password, bool testnet = false) // deprecated { return openWallet(path, password, testnet ? TESTNET : MAINNET); } diff --git a/src/backend.cpp b/src/backend.cpp index e52185a..b32b0c5 100644 --- a/src/backend.cpp +++ b/src/backend.cpp @@ -71,7 +71,8 @@ namespace lwsf { namespace internal { namespace backend WIRE_FIELD(amount), WIRE_FIELD(sender), WIRE_OPTIONAL_FIELD(secret), - WIRE_FIELD(tx_pub) + WIRE_FIELD(tx_pub), + WIRE_FIELD(output_pub) ); } @@ -138,8 +139,9 @@ namespace lwsf { namespace internal { namespace backend void map_account(F& format, T& self) { wire::object(format, - WIRE_FIELD(restore_height), wire::field("txes", wire::trusted_array(std::ref(self.txes))), + WIRE_FIELD(scan_height), + WIRE_FIELD(restore_height), WIRE_FIELD(type), WIRE_FIELD(view), WIRE_FIELD(spend), @@ -252,24 +254,24 @@ namespace lwsf { namespace internal { namespace backend update_output(out->receives.try_emplace(source.public_key).first->second, source, view_key); } - std::vector> merge_response(wallet& self, const rpc::get_address_txs& source) + std::vector> + merge_response(wallet& self, const rpc::get_address_txs& source, const rpc::get_unspent_outs& unspents) { std::vector> out; - self.scan_height = source.scanned_block_height; - self.blockchain_height = source.blockchain_height; - self.primary.restore_height = source.start_height; - /* 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. */ + 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()) + ADDITIONALLY, the strong exception guarantee is provided by the + `refresh()` method; the wallet is never in a partial-state. Swapping + the transactions at the end helps with this guarantee. */ + + boost::container::flat_map, memory> updated_txes; + updated_txes.reserve( + std::max(self.primary.txes.size(), source.transactions.size()) ); /* The frontend will know about the spend first, iff the frontend was @@ -277,17 +279,16 @@ namespace lwsf { namespace internal { namespace backend 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) + for (const auto& tx : self.primary.txes) { if (tx.second) { - 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) + updated_txes.emplace_hint( + updated_txes.end(), tx.first, std::make_shared(*tx.second) ); break; // inner loop } @@ -297,42 +298,39 @@ namespace lwsf { namespace internal { namespace backend for (const auto& tx : source.transactions) { - auto inserted = self.primary.txes.try_emplace(tx.tx_hash, nullptr); + auto inserted = updated_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; - } + const auto existing = self.primary.txes.find(tx.tx_hash); + if (existing != self.primary.txes.end() && existing->second) + inserted.first->second = std::make_shared(*existing->second); + else + inserted.first->second = std::make_shared(); - if (existing != existing_txes.end()) + if (existing != self.primary.txes.end()) out.push_back(inserted.first->second); } update_tx(self.primary, *inserted.first->second, tx); } - return out; - } - - 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) + for (const auto& output : unspents.outputs) { - auto iter = self.primary.txes.find(output.tx_hash); - if (iter != self.primary.txes.end()) + auto iter = updated_txes.find(output.tx_hash); + if (iter != updated_txes.end()) merge_output(iter->second, output, self.primary.view.sec); } + + // don't touch `self` until end to provide strong exception guarantee + self.primary.txes.swap(updated_txes); + + self.blockchain_height = source.blockchain_height; + self.primary.restore_height = source.start_height; + self.primary.scan_height = source.scanned_block_height; + + self.per_byte_fee = unspents.per_byte_fee; + self.fee_mask = unspents.fee_mask; + + return out; } } // anonymous @@ -355,8 +353,8 @@ namespace lwsf { namespace internal { namespace backend client(), primary{}, last_sync(0), - scan_height(0), blockchain_height(0), + fee_mask(0), sync() {} @@ -373,13 +371,44 @@ namespace lwsf { namespace internal { namespace backend } std::error_code wallet::from_bytes(epee::byte_slice source) + { + /* The move call shouldn't throw an exception. So either the entire + contents of the file get loaded, or the wallet remains unchanged. */ + + account reload{}; + std::error_code status = wire::msgpack::from_bytes(std::move(source), reload); + if (!status) + { + const boost::lock_guard lock{sync}; + primary = std::move(reload); + } + return status; + } + + std::error_code wallet::login() { const boost::lock_guard lock{sync}; - return wire::msgpack::from_bytes(std::move(source), primary); + + const rpc::login_request login{ + primary.address, primary.view.sec, true, primary.generated_locally + }; + + const auto response = rpc::invoke(client, login); + if (!response) + return response.error(); + + if (response->start_height) + primary.restore_height = *response->start_height; + + return {}; } std::error_code wallet::refresh(const bool mandatory) { + /* Remember that this function provides the strong exception guarantee. + The state of the wallet should always be valid and never in a partially + updated state. */ + if (!mandatory) { const auto now = std::chrono::steady_clock::now(); @@ -391,21 +420,23 @@ namespace lwsf { namespace internal { namespace backend } expect outs_response{common_error::kInvalidArgument}; + + boost::unique_lock lock{sync}; const rpc::login login{primary.address, primary.view.sec}; + lock.unlock(); + const auto txs_response = rpc::invoke(client, login); if (txs_response) outs_response = rpc::invoke(client, login); - boost::unique_lock lock{sync}; - if (!txs_response) 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); + lock.lock(); + const std::uint64_t orig_scan_height = primary.scan_height; + const auto new_txes = merge_response(*this, *txs_response, *outs_response); last_sync = std::chrono::steady_clock::now().time_since_epoch().count(); if (!listener) @@ -413,15 +444,17 @@ namespace lwsf { namespace internal { namespace backend // 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); + const std::uint64_t new_scan_height = + std::max(orig_scan_height, primary.scan_height); + const std::shared_ptr listener_copy = listener; lock.unlock(); - listener->refreshed(); + listener_copy->refreshed(); if (!new_txes.empty()) - listener->updated(); + listener_copy->updated(); for (std::uint64_t i = orig_scan_height; i < new_scan_height; ++i) - listener->newBlock(i); + listener_copy->newBlock(i); for (const auto& tx : new_txes) { @@ -429,12 +462,12 @@ namespace lwsf { namespace internal { namespace backend if (tx->direction == TransactionInfo::Direction::In) { if (tx->height) - listener->moneyReceived(txid, tx->amount); + listener_copy->moneyReceived(txid, tx->amount); else - listener->unconfirmedMoneyReceived(txid, tx->amount); + listener_copy->unconfirmedMoneyReceived(txid, tx->amount); } else - listener->moneySpent(txid, tx->amount); + listener_copy->moneySpent(txid, tx->amount); } return {}; diff --git a/src/backend.h b/src/backend.h index 7d3b906..2df5b54 100644 --- a/src/backend.h +++ b/src/backend.h @@ -70,9 +70,10 @@ namespace lwsf { namespace internal { namespace backend rpc::address_meta sender; std::optional secret; crypto::public_key tx_pub; + crypto::public_key output_pub; transfer_out() - : address(), amount(0), sender{}, secret(), tx_pub{} + : address(), amount(0), sender{}, secret(), tx_pub{}, outpub_pub{} {} }; WIRE_DECLARE_OBJECT(transfer_out); @@ -154,6 +155,7 @@ namespace lwsf { namespace internal { namespace backend account() = delete; std::string address; //!> not serialized, recovered on read_bytes boost::container::flat_map, memory> txes; + std::uint64_t scan_height; std::uint64_t restore_height; NetworkType type; keypair view; @@ -168,7 +170,6 @@ namespace lwsf { namespace internal { namespace backend rpc::http_client client; account primary; 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; @@ -176,12 +177,20 @@ namespace lwsf { namespace internal { namespace backend wallet(); + //! Serializate `this` wallet to msgpack. Locks contents. expect to_bytes() const; + + //! De-serialize `this` from msgpack. Locks+replaces contents. std::error_code from_bytes(epee::byte_slice source); - bool login() const; + //! Attempt + std::error_code login(); + + //! Refreshes txes information. Strong exception guarantee. Locks contents. std::error_code refresh(bool mandatory = false); + std::error_code restore_height(const std::uint64_t height); + std::error_code send_tx(epee::byte_slice tx_bytes); }; }}} // lwsf // internal // backend diff --git a/src/rpc.cpp b/src/rpc.cpp index fdd3367..c6da6b4 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp @@ -119,6 +119,11 @@ namespace lwsf { namespace internal { namespace rpc ); } + void read_bytes(wire::json_reader& source, login_response& self) + { + wire::object(source, WIRE_OPTIONAL_FIELD(start_height)); + } + void read_bytes(wire::json_reader& source, uint64_string& dest) { dest = uint64_string(wire::integer::cast_unsigned(source.safe_unsigned_integer())); @@ -285,6 +290,15 @@ namespace lwsf { namespace internal { namespace rpc } + void read_bytes(wire::json_reader& source, import_response& self) + { + wire::object(source, + WIRE_OPTIONAL_FIELD(import_fee), + WIRE_FIELD(request_fulfilled) + ); + } + + void write_bytes(wire::json_writer& dest, const submit_raw_tx_request& self) { wire::object(dest, WIRE_FIELD(tx)); diff --git a/src/rpc.h b/src/rpc.h index 1d60262..fcd9367 100644 --- a/src/rpc.h +++ b/src/rpc.h @@ -121,10 +121,9 @@ namespace lwsf { namespace internal { namespace rpc login_response() = delete; static constexpr const char* endpoint() noexcept { return "/login"; } - std::optional generated_locally; + std::optional start_height; }; - inline void read_bytes(const wire::json_reader&, const login_response&) noexcept - {} + void read_bytes(wire::json_reader&, login_response&); struct daemon_status @@ -266,6 +265,15 @@ namespace lwsf { namespace internal { namespace rpc void read_bytes(wire::json_reader&, get_unspent_outs&); + struct import_response + { + import_response() = delete; + std::optional import_fee; + bool request_fulfilled; + }; + void read_bytes(wire::json_reader&, import_response&); + + struct submit_raw_tx_request { submit_raw_tx_request() = delete; diff --git a/src/wallet.cpp b/src/wallet.cpp index 905d7a3..6428169 100644 --- a/src/wallet.cpp +++ b/src/wallet.cpp @@ -39,11 +39,14 @@ #include #include #include +#include #include "backend.h" #include "error.h" #include "hex.h" // monero/contrib/epee/include #include "lwsf_config.h" +#include "net/net_parse_helpers.h" // monero/contrib/epee/include #include "net/parse.h" // monero/src +#include "net/socks.h" // monero/src #include "net/socks_connect.h" // monero/src #include "transaction_history.h" #include "wire.h" @@ -281,46 +284,114 @@ namespace lwsf { namespace internal #else #error atomic_file_write not implemented for this platform #endif + + struct unknown_exception : std::exception + { + unknown_exception() noexcept + : std::exception() + {} + + const char* what() const noexcept override { return "unknown error"; } + }; + + constexpr boost::chrono::nanoseconds to_boost(const std::chrono::nanoseconds source) noexcept + { + static_assert( + std::is_same{} + ); + return boost::chrono::nanoseconds{source.count()}; + } } // anonymous - void wallet::set_error(const std::error_code status) + bool wallet::set_error(const std::error_code status) { - const boost::lock_guard lock{sync_}; + const boost::lock_guard lock{error_sync_}; status_ = status; exception_error_.clear(); + return !status_; } void wallet::set_critical(const std::exception& e) { - const boost::lock_guard lock{sync_}; + const boost::lock_guard lock{error_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)), + void wallet::stop_refresh_loop() + { + { + const boost::lock_guard lock{refresh_sync_}; + thread_state_ = state::stop; + } + refresh_notify_.notify_all(); + if (thread_.joinable()) + thread_.join(); + } + + void wallet::refresh_loop() + { + try + { + boost::unique_lock lock{refresh_sync_}; + while (thread_state_ != state::stop) + { + const bool mandatory_refresh = mandatory_refresh_; + mandatory_refresh_ = false; + + if (thread_state_ == state::run || mandatory_refresh) + { + // refresh has strong exception guarantee - never in partial state. + lock.unlock(); + set_error(data_->refresh(mandatory_refresh)); + lock.lock(); + } + else if (thread_state_ == state::skip_once) + thread_state_ = state::run; + + // check while holding lock and before a wait call + if (thread_state_ == state::stop) + return; + + refresh_notify_.wait_for(lock, to_boost(config::refresh_interval)); + } + } + catch (const std::exception& e) + { + set_critical(e); + } + catch (...) + { + set_critical(unknown_exception{}); + } + } + + wallet::wallet(create_tag, NetworkType nettype, std::string filename, std::string password, const std::uint64_t kdf_rounds) + : data_(std::make_shared()), filename_(std::move(filename)), password_(std::move(password)), exception_error_(), status_(), thread_(), iterations_(kdf_rounds), - sync_() + refresh_notify_(), + error_sync_(), + refresh_sync_(), + thread_state_(state::stop), + mandatory_refresh_(false) { - if (!data_) - 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; @@ -331,7 +402,7 @@ namespace lwsf { namespace internal data_->primary.generated_locally = true; data_->primary.type = nettype; - store(""); + store(std::string{}); } wallet::wallet(open_tag, NetworkType nettype, std::string filename, std::string password, const std::uint64_t kdf_rounds, std::shared_ptr data) @@ -342,16 +413,20 @@ namespace lwsf { namespace internal status_(), thread_(), iterations_(kdf_rounds), - sync_() + refresh_notify_(), + error_sync_(), + refresh_sync_(), + thread_state_(state::stop), + mandatory_refresh_(false) { if (!data_) - throw std::logic_error{"Unexpected nullptr in internal::wallet"}; + throw std::logic_error{"backend::wallet cannot be null"}; try { - epee::byte_slice file = try_load(filename + ".new"); + epee::byte_slice file = try_load(filename_ + ".new"); if (file.empty()) - file = try_load(filename); + file = try_load(filename_); if (!file.empty()) { @@ -360,18 +435,21 @@ namespace lwsf { namespace internal { epee::byte_slice payload = contents.get_payload(password_); if (!payload.empty()) - status_ = data_->from_bytes(std::move(payload)); + { + if (!(status_ = data_->from_bytes(std::move(payload)))) + { + // lock not needed; data_ was created and unique to us + if (nettype != data_->primary.type) + throw std::runtime_error{"Wallet file NetworkType does not match requested"}; + } + } else - status_ = error::crypto_failure; + status_ = error::unsupported_format; } } else status_ = error::read_failure; } - catch (const std::system_error& e) - { - status_ = e.code(); - } catch (const std::exception& e) { status_.clear(); @@ -379,8 +457,7 @@ namespace lwsf { namespace internal } } - wallet::~wallet() - {} + wallet::~wallet() { stop_refresh_loop(); } int wallet::status() const { @@ -400,7 +477,7 @@ namespace lwsf { namespace internal void wallet::statusWithErrorString(int& status, std::string& errorString) const { - const boost::lock_guard lock{sync_}; + const boost::lock_guard lock{error_sync_}; if (!exception_error_.empty()) { status = Status_Critical; @@ -446,6 +523,12 @@ namespace lwsf { namespace internal return epee::to_hex::string(epee::as_byte_span(data_->primary.spend.pub)); } + void wallet::stop() + { + const boost::lock_guard lock{refresh_sync_}; + thread_state_ = state::skip_once; + } + bool wallet::store(const std::string& path) { const std::string& real_path = path.empty() ? filename_ : path; @@ -487,17 +570,14 @@ namespace lwsf { namespace internal else status = payload.error(); } - catch (const std::system_error& e) - { - status = e.code(); - } catch (const std::exception& e) { set_critical(e); + return false; } // this will clear any existing errors - set_error(status); + return 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) @@ -505,26 +585,67 @@ namespace lwsf { namespace internal if (!light_wallet) throw std::logic_error{"Only light_wallets are supported with this instance"}; - const auto split = daemon_address.rfind(':'); - if (!setProxy(proxy_address)) - return false; // set_status already set - - std::string host = daemon_address.substr(0, split); - std::string port = use_ssl ? "443" : "80"; - if (split != std::string::npos) - port = daemon_address.substr(split + 1); - - epee::net_utils::http::login login{daemon_username, daemon_password}; - const epee::net_utils::ssl_options_t options{ - use_ssl ? epee::net_utils::ssl_support_t::e_ssl_support_disabled : epee::net_utils::ssl_support_t::e_ssl_support_enabled + // on exceptions, reset state + struct on_throw + { + void operator()(wallet* self) const + { + if (self) + { + self->stop_refresh_loop(); + self->data_->client.disconnect(); + } + } }; - data_->client.set_server(std::move(host), std::move(port), std::move(login), std::move(options)); + try + { + std::unique_ptr rollback{this}; + stop_refresh_loop(); + data_->client.disconnect(); + + if (!setProxy(proxy_address)) + return false; + + epee::net_utils::http::url_content url{}; + if (!epee::net_utils::parse_url(daemon_address, url)) + throw std::runtime_error{"Invalid LWS URL: " + daemon_address}; + if (!url.m_uri_content.m_path.empty()) + throw std::runtime_error{"LWS URL contains path (unsupported)"}; + + if (url.schema == "https") + use_ssl = true; + + epee::net_utils::http::login login{daemon_username, daemon_password}; + const epee::net_utils::ssl_options_t options{ + use_ssl ? epee::net_utils::ssl_support_t::e_ssl_support_disabled : epee::net_utils::ssl_support_t::e_ssl_support_enabled + }; + + data_->client.set_server(std::move(url.host), std::to_string(url.port), std::move(login), std::move(options)); + if (!connectToDaemon()) + return false; + + thread_state_ = state::run; + thread_ = boost::thread{[this] () { this->refresh_loop(); }}; + rollback.release(); + } + catch (const std::exception& e) + { + set_critical(e); + return false; + } - //! \TODO Login! return true; } + void wallet::setRefreshFromBlockHeight(uint64_t refresh_from_block_height) override + { + try { set_error(data_->restore_height(refresh_from_block_height)); } + catch (const std::exception& e) + { + set_critical(e); + } + } uint64_t wallet::getRefreshFromBlockHeight() const { @@ -534,9 +655,28 @@ namespace lwsf { namespace internal bool wallet::connectToDaemon() { + struct on_throw + { + void operator()(wallet* self) const + { + if (self) + self->data_->client.disconnect(); + } + }; + if (data_->client.is_connected()) return true; - return data_->client.connect(config::connect_timeout); + + std::unique_ptr disconnect{this}; + if (data_->client.connect(config::connect_timeout)) + { + const std::error_code status = data_->login(); + if (!status) + disconnect.reset(); + return set_error(status); + } + set_error(error::connect_failure); + return false; } Wallet::ConnectionStatus wallet::connected() const @@ -551,8 +691,7 @@ namespace lwsf { namespace internal if (!endpoint) { data_->client.set_connector(null_connector{}); - set_error(endpoint.error()); - return false; + return set_error(endpoint.error()); } data_->client.set_connector(net::socks::connector{std::move(*endpoint)}); return true; @@ -570,6 +709,35 @@ namespace lwsf { namespace internal uint64_t wallet::daemonBlockChainTargetHeight() const { return blockChainHeight(); } + void wallet::startRefresh() + { + { + const boost::lock_guard lock{refresh_sync_}; + thread_state_ = state::run; + } + refresh_notify_.notify_all(); + } + + void wallet::pauseRefresh() + { + const boost::lock_guard lock{refresh_sync_}; + thread_state_ = state::paused; + } + + bool wallet::refresh() + { + return set_error(data_->refresh(true)); + } + + void wallet::refreshAsync() + { + { + const boost::lock_guard lock{refresh_sync_}; + mandatory_refresh_ = true; + } + refresh_notify_.notify_all(); + } + uint64_t wallet::estimateTransactionFee(const std::vector> &destinations, PendingTransaction::Priority priority) const { @@ -581,7 +749,6 @@ namespace lwsf { namespace internal return std::make_shared(data_); } - void wallet::setListener(std::shared_ptr listener) { const boost::lock_guard lock{data_->sync}; diff --git a/src/wallet.h b/src/wallet.h index 5062d4d..70e2214 100644 --- a/src/wallet.h +++ b/src/wallet.h @@ -30,6 +30,8 @@ #include "lws_frontend.h" +#include +#include #include #include #include @@ -47,6 +49,8 @@ namespace internal //! \TODO Mark final when completely implemented class wallet : public lwsf::Wallet { + enum class state : std::uint8_t { stop = 0, paused, skip_once, run }; + const std::shared_ptr data_; const std::string filename_; std::string password_; @@ -54,16 +58,23 @@ namespace internal std::error_code status_; boost::thread thread_; const std::uint64_t iterations_; - mutable boost::mutex sync_; + boost::condition_variable refresh_notify_; + mutable boost::mutex error_sync_; + boost::mutex refresh_sync_; + state thread_state_; + bool mandatory_refresh_; - void set_error(const std::error_code status); + bool set_error(std::error_code status); void set_critical(const std::exception& e); + void stop_refresh_loop(); + void refresh_loop(); + 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(create_tag, NetworkType nettype, std::string filename, std::string password, std::uint64_t kdf_rounds); wallet(open_tag, NetworkType nettpe, std::string filename, std::string password, std::uint64_t kdf_rounds, std::shared_ptr data); wallet(const wallet&) = delete; @@ -142,7 +153,7 @@ namespace internal /*! * \brief stop - interrupts wallet refresh() loop once (doesn't stop background refresh thread) */ - virtual void stop() = 0; + virtual void stop() override; /*! * \brief store - stores wallet to file. @@ -188,7 +199,7 @@ namespace internal * * \param refresh_from_block_height - blockchain start height */ - virtual void setRefreshFromBlockHeight(uint64_t refresh_from_block_height) = 0; + virtual void setRefreshFromBlockHeight(uint64_t refresh_from_block_height) override; /*! * \brief getRestoreHeight - get wallet creation height @@ -329,22 +340,22 @@ namespace internal /** * @brief StartRefresh - Start/resume refresh thread (refresh every 10 seconds) */ - virtual void startRefresh() = 0; + virtual void startRefresh() override; /** * @brief pauseRefresh - pause refresh thread */ - virtual void pauseRefresh() = 0; + virtual void pauseRefresh() override; /** * @brief refresh - refreshes the wallet, updating transactions from daemon * @return - true if refreshed successfully; */ - virtual bool refresh() = 0; + virtual bool refresh() override; /** * @brief refreshAsync - refreshes wallet asynchronously. */ - virtual void refreshAsync() = 0; + virtual void refreshAsync() override; /** * @brief rescanBlockchain - rescans the wallet, updating transactions from daemon diff --git a/src/wallet_manager.cpp b/src/wallet_manager.cpp index bc7beaa..503628d 100644 --- a/src/wallet_manager.cpp +++ b/src/wallet_manager.cpp @@ -109,13 +109,18 @@ namespace lwsf * \return Wallet instance (Wallet::status() needs to be called to check if opened successfully) */ - std::unique_ptr openWallet(const std::string &path, const std::string &password, NetworkType nettype, uint64_t kdf_rounds, std::shared_ptr listener) override + std::shared_ptr openWallet(const std::string &path, const std::string &password, NetworkType nettype, uint64_t kdf_rounds, std::shared_ptr listener) override { - 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) + auto data = std::make_shared(); + data->listener = std::move(listener); + auto out = std::make_shared( + wallet::open_tag{}, nettype, path, password, kdf_rounds, data ); + + // wallet file should be fully loaded in construction of `wallet`. + if (data->listener) + data->listener->onSetWallet(out); + return out; } /*!