diff --git a/src/backend.cpp b/src/backend.cpp index 6f4c10f..b936c8e 100644 --- a/src/backend.cpp +++ b/src/backend.cpp @@ -39,6 +39,7 @@ #include "wire/adapted/crypto.h" #include "wire/adapted/pair.h" #include "wire/msgpack.h" +#include "wire/wrapper/defaulted.h" #include "wire/wrapper/trusted_array.h" #include "wire/wrapper/variant.h" @@ -52,6 +53,22 @@ namespace lwsf { namespace internal { namespace backend { namespace { + constexpr const error default_subaddr_state = error::subaddr_disabled; + constexpr const auto rpc_unapproved = rpc::error(403); + constexpr const auto rpc_max_subaddresses = rpc::error(409); + constexpr const auto rpc_internal_error = rpc::error(500); + constexpr const auto rpc_not_implemented = rpc::error(501); + + template + std::uint32_t add_uint32_clamp(const T index, const U lookahead) + { + static_assert(std::is_unsigned()); + static_assert(std::is_same()); + if (std::numeric_limits::max() - lookahead <= index) + return std::numeric_limits::max(); + return index + lookahead; + } + cryptonote::network_type convert_net_type(const Monero::NetworkType in) { switch(in) @@ -127,19 +144,19 @@ namespace lwsf { namespace internal { namespace backend template void map_sub_account(F& format, T& self) { + // do not store server lookahead, reset on each server connection wire::object(format, - wire::field("minor", wire::trusted_array(std::ref(self.minor))) + wire::optional_field("detail", wire::trusted_array(std::ref(self.detail))), + WIRE_FIELD_DEFAULTED(used, 0) ); } template - void map_transfer_out(F& format, T& self) + void map_transfer_spend(F& format, T& self) { wire::object(format, - WIRE_FIELD(address), WIRE_FIELD(amount), WIRE_FIELD(sender), - WIRE_OPTIONAL_FIELD(secret), WIRE_FIELD(tx_pub), WIRE_FIELD(output_pub) ); @@ -158,14 +175,21 @@ namespace lwsf { namespace internal { namespace backend ); } + template + void map_transfer_out(F& format, T& self) + { + wire::object(format, WIRE_FIELD(address), WIRE_FIELD(amount), WIRE_FIELD(secret)); + } + template void map_transaction(F& format, T& self) { //! TODO complete payment_id storage auto payment_id = wire::variant(std::ref(self.payment_id)); wire::object(format, - wire::field("spends", wire::trusted_array(std::ref(self.spends))), - wire::field("receives", wire::trusted_array(std::ref(self.receives))), + wire::optional_field("spends", wire::trusted_array(std::ref(self.spends))), + wire::optional_field("receives", wire::trusted_array(std::ref(self.receives))), + wire::optional_field("transfers", wire::trusted_array(std::ref(self.transfers))), WIRE_FIELD(description), WIRE_OPTIONAL_FIELD(timestamp), WIRE_FIELD(amount), @@ -210,12 +234,14 @@ namespace lwsf { namespace internal { namespace backend { wire::object(format, WIRE_FIELD(language), - wire::field("addressbook", wire::trusted_array(std::ref(self.addressbook))), - wire::field("subaccounts", wire::trusted_array(std::ref(self.subaccounts))), - wire::field("txes", wire::trusted_array(std::ref(self.txes))), - wire::field("attributes", wire::trusted_array(std::ref(self.attributes))), + wire::optional_field("addressbook", wire::trusted_array(std::ref(self.addressbook))), + wire::optional_field("subaccounts", wire::trusted_array(std::ref(self.subaccounts))), + wire::optional_field("txes", wire::trusted_array(std::ref(self.txes))), + wire::optional_field("attributes", wire::trusted_array(std::ref(self.attributes))), WIRE_FIELD(scan_height), WIRE_FIELD(restore_height), + WIRE_FIELD_DEFAULTED(maj_lookahead, config::subaddr_major_lookahead), + WIRE_FIELD_DEFAULTED(min_lookahead, config::subaddr_minor_lookahead), WIRE_FIELD(type), WIRE_FIELD(view), WIRE_FIELD(spend), @@ -238,7 +264,7 @@ namespace lwsf { namespace internal { namespace backend return commitment.mask; } - void update_output(transfer_in& out, const rpc::output& source, const crypto::secret_key& view_key) + rpc::address_meta 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); @@ -261,18 +287,21 @@ namespace lwsf { namespace internal { namespace backend out.rct_mask = source.rct.mask; break; } + + return out.recipient; } - void update_spend(transfer_out& out, const rpc::transaction_spend& source) + rpc::address_meta update_spend(transfer_spend& 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; + return out.sender; } crypto::secret_key get_spend_secret(const account& self, const std::optional& index) { - if (!index || (index->maj_i == 0 && index->min_i == 0)) + if (!index || index->is_default()) return self.spend.sec; // m = Hs(a || index_major || index_minor) @@ -284,12 +313,22 @@ namespace lwsf { namespace internal { namespace backend return out; } - bool update_tx(const account& self, transaction& out, const rpc::transaction& source) + //! \return True if used subaddresses has been increased + bool need_expansion(const account& self, const rpc::address_meta& sub) + { + if (self.subaccounts.size() <= sub.maj_i) + return true; + return self.subaccounts[sub.maj_i].used <= sub.min_i; + } + + std::optional> 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(); + std::vector meta; + out.id = source.hash; out.timestamp = source.timestamp; out.fee = std::uint64_t(source.fee.value_or(rpc::uint64_string(0))); @@ -316,13 +355,15 @@ namespace lwsf { namespace internal { namespace backend { /* 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); + const rpc::address_meta sub = update_spend(out.spends.try_emplace(image).first->second, spend); + if (need_expansion(self, sub)) + meta.push_back(sub); total_spent += std::uint64_t(spend.amount); } } if (!std::uint64_t(source.total_received) && !total_spent) - return false; // used as decoy + return std::nullopt; // used as decoy if (out.fee <= total_spent) total_spent -= out.fee; @@ -338,22 +379,40 @@ namespace lwsf { namespace internal { namespace backend out.amount = total_spent - std::uint64_t(source.total_received); } - return true; + return meta; } - void merge_output(const std::shared_ptr& out, const rpc::output& source, const crypto::secret_key& view_key) + rpc::address_meta 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 = source.tx_prefix_hash; - update_output(out->receives.try_emplace(source.public_key).first->second, source, view_key); + return update_output(out->receives[source.public_key], source, view_key); } - std::vector> - merge_response(wallet& self, const rpc::get_address_txs& source, const rpc::get_unspent_outs_response& unspents) + struct merge_results { - std::vector> out; + std::vector> new_transactions; + boost::container::flat_set> new_subaddrs; + + void merge_subaddr(const rpc::address_meta& meta) + { + auto elem = new_subaddrs.find(meta.maj_i); + if (elem == new_subaddrs.end()) + elem = new_subaddrs.insert(rpc::subaddrs{meta.maj_i}).first; + + const auto existing = elem->head; + std::get<0>(elem->head) = std::min(std::get<0>(existing), meta.min_i); + std::get<1>(elem->head) = std::max(std::get<1>(existing), meta.min_i); + } + }; + + merge_results merge_response(wallet& self, const rpc::get_address_txs& source, const rpc::get_unspent_outs_response& unspents) + { + // Remember that this function provides the strong exception guarantee. + + merge_results out; /* 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 @@ -381,22 +440,20 @@ namespace lwsf { namespace internal { namespace backend { if (tx.second) { - for (const auto& spend : tx.second->spends) + if (!tx.second->transfers.empty()) { - if (spend.second.secret) + const auto iter = updated_txes.emplace_hint( + updated_txes.end(), tx.first, nullptr + ); + if (!iter->second) { - const auto iter = updated_txes.emplace_hint( - updated_txes.end(), tx.first, nullptr - ); - if (!iter->second) - { - iter->second = std::make_shared(*tx.second); - iter->second->height = std::nullopt; - iter->second->timestamp = std::nullopt; - iter->second->failed = false; - } - images.emplace(spend.first, iter->second); + iter->second = std::make_shared(*tx.second); + iter->second->height = std::nullopt; + iter->second->timestamp = std::nullopt; + iter->second->failed = false; } + for (const auto& spend : tx.second->spends) + images.emplace(spend.first, iter->second); } } } @@ -413,10 +470,13 @@ namespace lwsf { namespace internal { namespace backend inserted.first->second = std::make_shared(); if (existing == self.primary.txes.end()) - out.push_back(inserted.first->second); + out.new_transactions.push_back(inserted.first->second); } - if (update_tx(self.primary, *inserted.first->second, tx)) + if (const auto subs = update_tx(self.primary, *inserted.first->second, tx)) { + for (const auto& sub : *subs) + out.merge_subaddr(sub); + for (const auto& spend : inserted.first->second->spends) { for (auto range = images.equal_range(spend.first); range.first != range.second; ++range.first) @@ -431,10 +491,29 @@ namespace lwsf { namespace internal { namespace backend { auto iter = updated_txes.find(output.tx_hash); if (iter != updated_txes.end()) - merge_output(iter->second, output, self.primary.view.sec); + { + const auto sub = merge_output(iter->second, output, self.primary.view.sec); + if (need_expansion(self.primary, sub)) + out.merge_subaddr(sub); + } } // don't touch `self` until end to provide strong exception guarantee + + // update our "used" records immediately, server already knows about them. + // udpate "serer_lookahead" values later, we force lookahead from zero + for (const auto& sub : out.new_subaddrs) + { + if (std::numeric_limits::max() <= sub.key) + throw std::runtime_error{"merge_response exceeded max size_t value"}; + if (self.primary.subaccounts.size() <= sub.key) + self.primary.subaccounts.resize(std::size_t(sub.key) + 1); + + auto& acct = self.primary.subaccounts.at(sub.key); + acct.used = std::max(acct.used, add_uint32_clamp(unsigned(1), std::get<1>(sub.head))); + } + + // Update txes _after_ subaccounts table self.primary.txes.swap(updated_txes); self.blockchain_height = source.blockchain_height; @@ -446,46 +525,97 @@ namespace lwsf { namespace internal { namespace backend return out; } - void merge_subaddrs(account& self, const epee::span subaddrs) + /*! Update `server_lookahead` minor fields based on `all`. The top-level + major lookahead is set to error state if `false` is returned, and set to + the server lookahead iff true is returned. + \return True iff the server has enough lookahead for all fields. */ + bool update_lookaheads(wallet& self, const boost::container::flat_set>& all) { - /* Purge subaccounts that do not have info (label) and are not mentioned - by the server. */ + const auto maj_lookahead = self.primary.maj_lookahead; + const auto min_lookahead = self.primary.min_lookahead; + if (!maj_lookahead && !min_lookahead) + throw std::logic_error{"update_lookaheads does not work with lookahead {0, 0}"}; - std::map subaccounts; - subaccounts.swap(self.subaccounts); + self.server_lookahead = {error::subaddr_server}; + const std::uint32_t server_lookahead = all.empty() ? + std::uint32_t(0) : all.rbegin()->key; - // Never remove special {0,0} account - self.subaccounts[0].minor[0] = std::move(subaccounts[0].minor[0]); + // Ensure major lookahead starts at zero, has no gaps, and "far" enough. + bool min_met = add_uint32_clamp(self.primary.subaccounts.size() - 1, maj_lookahead) <= server_lookahead; + min_met &= all.size() == std::uint64_t(server_lookahead) + 1; - // Keep all subaccounts with a info - for (auto& account : subaccounts) + for (std::size_t i = 0; i < self.primary.subaccounts.size(); ++i) { - for (auto& subaccount : account.second.minor) - { - if (subaccount.second.preserve()) - self.subaccounts[account.first].minor[subaccount.first] = std::move(subaccount.second); - } - } + auto& acct = self.primary.subaccounts[i]; + const auto elem = all.find(i); + const bool has_range = elem != all.end(); - // Keep all subaccounts mentioned by server - for (const auto& subaddr : subaddrs) - { - auto& account = self.subaccounts[subaddr.key]; - auto& old = subaccounts[subaddr.key]; - for (const auto& minors : subaddr.value) - { - for (std::uint32_t minor = std::get<0>(minors); minor < std::uint64_t(std::get<1>(minors)) + 1; ++minor) - account.minor.try_emplace(minor); - } + const std::uint32_t lookahead = has_range ? std::get<1>(elem->head) : 0; + min_met &= add_uint32_clamp(acct.used, min_lookahead) <= lookahead; + min_met &= has_range && std::get<0>(elem->head) == 0; + + // Only increase lookahead, server has no delete subaddr command + acct.server_lookahead = std::max(acct.server_lookahead, lookahead); } + + // Ensure that unused minor lookahead is far enough + for (std::size_t i = self.primary.subaccounts.size(); i < all.size(); ++i) + { + if (!min_met) + break; + + const auto& range = all.nth(i)->head; + min_met &= min_lookahead <= std::get<1>(range); + min_met &= std::get<0>(range) == 0; + } + + if (min_met) + self.server_lookahead = server_lookahead; + return min_met; + } + + void fill_upsert(const wallet& self, boost::container::flat_set>& out) + { + static constexpr const std::uint32_t max_index = + std::numeric_limits::max(); + + const auto& accts = self.primary.subaccounts; + const std::uint32_t minor = self.primary.min_lookahead; + for (std::size_t i = 0; i < accts.size() && i <= max_index; ++i) + out.insert(out.end(), rpc::subaddrs{std::uint32_t(i)})->head = {0, add_uint32_clamp(accts[i].used, minor)}; + + const std::uint32_t major = add_uint32_clamp(accts.size(), self.primary.maj_lookahead); + for (std::size_t i = accts.size(); i < major; ++i) + out.insert(out.end(), rpc::subaddrs{std::uint32_t(i)})->head = {0, minor}; + } + + std::error_code handle_lookahead_error(std::error_code error) noexcept + { + if (error == rpc_max_subaddresses) + error = {error::subaddr_server}; + else if (error == rpc_not_implemented) + error = {error::subaddr_disabled}; + else if (error == wire::error::schema::array_max_element) + error = {error::subaddr_local}; + + return error; + } + + void prep_primary_account(sub_account& self) + { + // Enforce special account {0,0} exists and is labeled + self.detail.try_emplace(0).first->second.label = std::string{config::default_primary_name}; + self.used = 1; + self.server_lookahead = 0; } } // anonymous WIRE_DEFINE_OBJECT(address_book_entry, map_address_book_entry); WIRE_DEFINE_OBJECT(subaddress, map_subaddress); WIRE_DEFINE_OBJECT(sub_account, map_sub_account); + WIRE_DEFINE_OBJECT(transfer_spend, map_transfer_spend); + WIRE_DEFINE_OBJECT(transfer_in, map_transfer_in); WIRE_DEFINE_OBJECT(transfer_out, map_transfer_out); - WIRE_DEFINE_OBJECT(transfer_in, map_transfer_in); WIRE_DEFINE_OBJECT(transaction, map_transaction); WIRE_DEFINE_OBJECT(keypair, map_keypair); void read_bytes(wire::reader& source, account& dest) @@ -494,10 +624,50 @@ namespace lwsf { namespace internal { namespace backend dest.address = cryptonote::get_account_address_as_str( convert_net_type(dest.type), false, cryptonote::account_public_address{dest.spend.pub, dest.view.pub} ); + if (dest.subaccounts.empty()) + prep_primary_account(dest.subaccounts.emplace_back()); } void write_bytes(wire::writer& dest, const account& source) { map_account(dest, source); } + sub_account::sub_account() + : detail(), used(1), server_lookahead(0) + {} + + std::string_view sub_account::sub_label(const std::uint32_t minor) const noexcept + { + const auto elem = detail.find(minor); + if (elem != detail.end()) + return elem->second.label; + return {}; + } + + std::string_view sub_account::primary_label() const noexcept + { + const auto elem = detail.find(0); + if (elem == detail.end()) + return config::default_account_name; + return elem->second.label; + } + + transaction::transaction() + : spends(), + receives(), + transfers(), + description(), + timestamp(), + height(), + amount(0), + fee(0), + unlock_time(0), + direction(Monero::TransactionInfo::Direction_Out), + payment_id(), + id{}, + prefix{}, + coinbase(false), + failed(false) + {} + bool transaction::is_unlocked(const std::uint64_t chain_height, const Monero::NetworkType type) const { const std::uint64_t the_height = height.value_or(std::numeric_limits::max()); @@ -514,6 +684,7 @@ namespace lwsf { namespace internal { namespace backend : listener(nullptr), client(), primary{}, + server_lookahead(default_subaddr_state), last_sync(0), blockchain_height(0), per_byte_fee(0), @@ -523,7 +694,7 @@ namespace lwsf { namespace internal { namespace backend sync_refresh(), passed_login(false) { - primary.subaccounts[0].minor[0]; + prep_primary_account(primary.subaccounts.emplace_back()); } cryptonote::network_type wallet::get_net_type() const @@ -531,7 +702,7 @@ namespace lwsf { namespace internal { namespace backend crypto::public_key wallet::get_spend_public(const rpc::address_meta& index) const { - if (index.maj_i == 0 && index.min_i == 0) + if (index.is_default()) return primary.spend.pub; // m = Hs(a || index_major || index_minor) @@ -547,7 +718,7 @@ namespace lwsf { namespace internal { namespace backend std::string wallet::get_spend_address(const rpc::address_meta& index) const { - const bool is_subaddress = (index.maj_i != 0 || index.min_i != 0); + const bool is_subaddress = !index.is_default(); const auto spend_public = get_spend_public(index); const crypto::public_key view_public = is_subaddress ? rct::rct2pk(rct::scalarmultKey(rct::pk2rct(spend_public), rct::sk2rct(primary.view.sec))) : primary.view.pub; @@ -588,27 +759,72 @@ namespace lwsf { namespace internal { namespace backend std::error_code wallet::login() { + // Remember that this function provides the strong exception guarantee. + boost::unique_lock lock{sync}; { + server_lookahead = {default_subaddr_state}; const rpc::login_request login{ primary.address, primary.view.sec, true, primary.generated_locally }; + lock.unlock(); const auto response = rpc::invoke(client, login); if (!response) + { + if (response == rpc_unapproved) + return {error::approval}; + else if (response == rpc_internal_error) + return {error::network_type}; // almost always this + else if (response == rpc_not_implemented) + return {error::create}; return response.error(); + } + lock.lock(); - const boost::lock_guard lock{sync}; passed_login = true; if (response->start_height) primary.restore_height = *response->start_height; + + if (!primary.maj_lookahead && !primary.min_lookahead) + { + server_lookahead = 0; + return {}; + } } - const rpc::login login{primary.address, primary.view.sec}; - const auto response = rpc::invoke(client, login); - if (response) + // Converting `rpc::invoke` into exceptions is easier here + try { - const boost::lock_guard lock{sync}; - merge_subaddrs(primary, epee::to_span(response->all_subaddrs)); + rpc::login login{primary.address, primary.view.sec}; + { + lock.unlock(); + const auto response = rpc::invoke(client, login).value(); + lock.lock(); + if (update_lookaheads(*this, response.all_subaddrs)) + return {}; + } + + // lookaheads not far enough + rpc::upsert_subaddrs_request request{std::move(login), {}, false /* get_all */}; + fill_upsert(*this, request.subaddrs_); + + lock.unlock(); + const auto response = rpc::invoke(client, request).value(); + lock.lock(); + update_lookaheads(*this, request.subaddrs_); + } + catch (const std::system_error& e) + { + if (e.code() == rpc_max_subaddresses) + server_lookahead = {error::subaddr_server}; + else if (e.code() == rpc_not_implemented) + server_lookahead = {error::subaddr_disabled}; + else if (e.code() == rpc_unapproved) // happens on new account creation + return {error::approval}; + else if (e.code() == wire::error::schema::array_max_element) + server_lookahead = {error::subaddr_local}; + else + server_lookahead = e.code(); } return {}; @@ -616,9 +832,7 @@ namespace lwsf { namespace internal { namespace backend 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. */ + // Remember that this function provides the strong exception guarantee. if (!mandatory) { @@ -633,19 +847,40 @@ namespace lwsf { namespace internal { namespace backend boost::unique_lock lock_refresh{sync_refresh}; boost::unique_lock lock{sync}; const auto now = std::chrono::steady_clock::now(); + const auto get_rc = [this, &lock] () -> std::error_code + { + if (!lock.owns_lock()) + lock.lock(); + if (!primary.generated_locally && (primary.maj_lookahead || primary.min_lookahead)) + return server_lookahead.error(); + return {}; + }; + if (!mandatory) // double-check { const std::chrono::steady_clock::time_point start{ std::chrono::steady_clock::time_point::duration{last_sync.load()} }; if (now - start < config::refresh_interval_min) - return {}; + return get_rc(); } + struct call_refreshed + { + void operator()(wallet* ptr) const noexcept + { + if (!ptr) return; + const boost::lock_guard lock{ptr->sync_listener}; + if (!ptr->listener) return; + ptr->listener->refreshed(); + } + }; + + std::unique_ptr refresh_on_exit{this}; const bool try_login = !passed_login; const rpc::login login{primary.address, primary.view.sec}; - lock.unlock(); + lock.unlock(); if (try_login) { const std::error_code failed_login = this->login(); @@ -664,22 +899,82 @@ namespace lwsf { namespace internal { namespace backend const rpc::get_unspent_outs_request request{login, rpc::uint64_string(0), 0, true}; outs_response = rpc::invoke(client, request); } - lock.lock(); + last_sync = now.time_since_epoch().count(); passed_login = bool(txs_response) && bool(outs_response); // reset state if (!txs_response) + { + if (txs_response == rpc_unapproved) + return {error::approval}; return txs_response.error(); + } if (!outs_response) return outs_response.error(); const std::uint64_t orig_scan_height = primary.scan_height; - const auto new_txes = merge_response(*this, *txs_response, *outs_response); + auto merged = merge_response(*this, *txs_response, *outs_response); + if (!merged.new_subaddrs.empty() && server_lookahead) + { + server_lookahead = std::max(*server_lookahead, merged.new_subaddrs.rbegin()->key); + + // ensure lookahead is from zero, or our logic is busted + for (auto& sub : merged.new_subaddrs) + { + std::get<0>(sub.head) = 0; + std::get<1>(sub.head) = add_uint32_clamp(std::get<1>(sub.head), primary.min_lookahead); + } + + const rpc::upsert_subaddrs_request upsert_request{ + login, std::move(merged.new_subaddrs), false /* get_all */ + }; + + lock.unlock(); + const auto upsert_response = rpc::invoke(client, upsert_request); + lock.lock(); + + if (upsert_response) + { + for (const auto& sub : upsert_request.subaddrs_) + { + auto& acct = primary.subaccounts.at(sub.key); + acct.server_lookahead = std::max(acct.server_lookahead, std::get<1>(sub.head)); + } + + if (primary.maj_lookahead) + { + const std::uint32_t maj_i = add_uint32_clamp(unsigned(1), upsert_request.subaddrs_.rbegin()->key); + const std::uint32_t maj_count = primary.maj_lookahead; + const std::uint32_t min_count = add_uint32_clamp(unsigned(1), primary.min_lookahead); + const rpc::provision_subaddrs_request provision_request{ + login, maj_i, 0, maj_count, min_count, false /* get_all */ + }; + + lock.unlock(); + const auto provision_response = rpc::invoke(client, provision_request); + lock.lock(); + + if (provision_response) + { + if (server_lookahead) // in case another thread jumped in + server_lookahead = std::max(*server_lookahead, add_uint32_clamp(maj_i, maj_count - 1)); + } + else // if !provision_response + server_lookahead = handle_lookahead_error(provision_response.error()); + } + } + else // if !upsert_response + server_lookahead = handle_lookahead_error(upsert_response.error()); + } // if new subaddress(es) + + // return error if subaddresses enabled, and recovered wallet + const std::error_code rc = get_rc(); + refresh_on_exit.release(); // release before acquiring `sync_listener`. const boost::lock_guard lock_listener{sync_listener}; if (!listener) - return {}; + return rc; // Call listener functions without holding `sync`, in case a call is made // back into the library. @@ -689,13 +984,13 @@ namespace lwsf { namespace internal { namespace backend lock_refresh.unlock(); listener->refreshed(); - if (!new_txes.empty() || new_scan_height - orig_scan_height) + if (!merged.new_transactions.empty() || new_scan_height - orig_scan_height) listener->updated(); for (std::uint64_t i = orig_scan_height; i < new_scan_height; ++i) listener->newBlock(i); - for (const auto& tx : new_txes) + for (const auto& tx : merged.new_transactions) { const auto txid = epee::string_tools::pod_to_hex(tx->id); if (tx->direction == Monero::TransactionInfo::Direction_In) @@ -709,57 +1004,131 @@ namespace lwsf { namespace internal { namespace backend listener->moneySpent(txid, tx->amount); } - return {}; + return rc; } std::error_code wallet::add_subaccount(std::string label) { - for (unsigned i = 0; i < config::subaddr_retry; ++i) + boost::unique_lock lock{sync}; + if (std::numeric_limits::max() < primary.subaccounts.size()) + throw std::runtime_error{"wallet::add_subaccount exceeded subaddress indexes"}; + + std::uint32_t maj_lookahead = 0; + const std::uint32_t minor_count = add_uint32_clamp(unsigned(1), primary.min_lookahead); + for (;;) { - std::size_t next = -1; - { - const boost::lock_guard lock{sync}; - for (const auto& account : primary.subaccounts) - { - if (account.first != next + 1) - break; - ++next; - } - ++next; - } + if (!server_lookahead) + break; - if (std::numeric_limits::max() < next) - return {error::rpc_failure}; + maj_lookahead = *server_lookahead; + const std::uint32_t maj_i = primary.subaccounts.size(); + const std::uint32_t needed_maj_lookahead = add_uint32_clamp(maj_i, primary.maj_lookahead); + if (needed_maj_lookahead <= maj_lookahead) + break; + // Increase the lookahead by one const rpc::provision_subaddrs_request request{ - rpc::login{primary.address, primary.view.sec}, std::uint32_t(next), 0, 1, 1, true + rpc::login{primary.address, primary.view.sec}, + needed_maj_lookahead, 0, 1, minor_count, false /* get_all */ }; + + lock.unlock(); const auto response = rpc::invoke(client, request); + lock.lock(); if (!response) - return response.error(); + server_lookahead = handle_lookahead_error(response.error()); - bool matched = false; - const boost::lock_guard lock{sync}; - for (const auto& major : response->new_subaddrs) + // if NOT another call to `add_subaccount` during rpc unlock phase + if (maj_i == primary.subaccounts.size()) { - auto& account = primary.subaccounts[major.key]; - for (const auto& minors : major.value) - { - for (std::uint32_t i = std::get<0>(minors); i < std::uint64_t(std::get<1>(minors)) + 1; ++i) - { - account.minor[i].label = std::move(label); - label.clear(); - matched = true; - } - } + if (response) + server_lookahead = needed_maj_lookahead; + break; } - - if (matched) - return {}; - else - merge_subaddrs(primary, epee::to_span(response->all_subaddrs)); } - return {error::rpc_failure}; + + // Allow account to be added if server previously had enough lookahead + if (primary.subaccounts.size() <= maj_lookahead) + { + auto& maj = primary.subaccounts.emplace_back(); + maj.detail.try_emplace(0).first->second.label = std::move(label); + maj.server_lookahead = minor_count - 1; + } + + if (server_lookahead) + return {}; + return server_lookahead.error(); + } + + std::error_code wallet::add_subaddress(const std::uint32_t accountIndex, std::string label) + { + boost::unique_lock lock{sync}; + auto maj = std::addressof(primary.subaccounts.at(accountIndex)); + if (std::numeric_limits::max() <= maj->used) + throw std::runtime_error{"wallet::add_subaddress exceeded subaddress indexes"}; + + const std::uint32_t min_i = ++maj->used; // exceptions could leak an index :/ + const std::uint32_t needed_min_lookahead = add_uint32_clamp(min_i, primary.min_lookahead); + + if (needed_min_lookahead <= maj->server_lookahead) + { + maj->detail.try_emplace(min_i).first->second.label = std::move(label); + return {}; + } + + const rpc::provision_subaddrs_request request{ + rpc::login{primary.address, primary.view.sec}, + accountIndex, 0, 1, needed_min_lookahead, false /* get_all */ + }; + + lock.unlock(); + const auto response = rpc::invoke(client, request); + lock.lock(); + + // address could've changed during unlock + maj = std::addressof(primary.subaccounts.at(accountIndex)); + + if (response) + { + maj->server_lookahead = std::max(maj->server_lookahead, needed_min_lookahead); + maj->detail.try_emplace(min_i - 1).first->second.label = std::move(label); + return {}; + } + else if (server_lookahead) + server_lookahead = handle_lookahead_error(response.error()); + return server_lookahead.error(); + } + + std::error_code wallet::set_lookahead(std::uint32_t major, std::uint32_t minor) + { + boost::unique_lock lock{sync}; + const bool extending = + primary.maj_lookahead < major || + primary.min_lookahead < minor; + + primary.maj_lookahead = major; + primary.min_lookahead = minor; + + if ((!extending && server_lookahead) || (!major && !minor)) + { + if (!server_lookahead) + server_lookahead = 0; + return {}; + } + + rpc::upsert_subaddrs_request request{ + rpc::login{primary.address, primary.view.sec}, {}, false /* get_all */ + }; + fill_upsert(*this, request.subaddrs_); + + lock.unlock(); + const auto response = rpc::invoke(client, request); + lock.lock(); + if (!response) + return (server_lookahead = handle_lookahead_error(response.error())).error(); + + update_lookaheads(*this, request.subaddrs_); + return {}; } std::error_code wallet::restore_height(const std::uint64_t /*height not supported */) diff --git a/src/backend.h b/src/backend.h index 584c685..898352b 100644 --- a/src/backend.h +++ b/src/backend.h @@ -48,6 +48,7 @@ #include "crypto/crypto.h" // monero/src #include "crypto/hash.h" // monero/src #include "cryptonote_config.h" // monero/src +#include "lwsf_config.h" #include "net/http_client.h" // monero/contrib/epee/include #include "ringct/rctTypes.h" // monero/src #include "rpc.h" @@ -78,30 +79,49 @@ namespace lwsf { namespace internal { namespace backend { std::string label; - bool preserve() const noexcept { return !label.empty(); } + subaddress() + : label() + {} + + explicit subaddress(const std::string_view label) + : subaddress(std::string{label}) + {} + + explicit subaddress(std::string label) + : label(std::move(label)) + {} }; WIRE_DECLARE_OBJECT(subaddress); struct sub_account { - boost::container::flat_map minor; + boost::container::flat_map detail; //!< Minor address info + std::uint32_t used; //!< Number of minor indexes in use. + std::uint32_t server_lookahead; //!< Status of server (minor) lookahead. Inclusive + + //! Creates 1 subaddress entry (key == `0`) with default label + sub_account(); + + //! \return Label for `minor`, if one was set. + std::string_view sub_label(std::uint32_t minor) const noexcept; + + //! \return Primary label for account (`minor == 0` is "special"). + std::string_view primary_label() const noexcept; }; WIRE_DECLARE_OBJECT(sub_account); - struct transfer_out + struct transfer_spend { - std::string address; std::uint64_t amount; 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{}, output_pub{} + transfer_spend() noexcept + : amount(0), sender{}, tx_pub{}, output_pub{} {} }; - WIRE_DECLARE_OBJECT(transfer_out); + WIRE_DECLARE_OBJECT(transfer_spend); struct transfer_in { @@ -112,7 +132,7 @@ namespace lwsf { namespace internal { namespace backend std::optional rct_mask; crypto::public_key tx_pub; - transfer_in() + transfer_in() noexcept : global_index(0), amount(0), recipient{}, @@ -123,25 +143,22 @@ namespace lwsf { namespace internal { namespace backend }; WIRE_DECLARE_OBJECT(transfer_in); + struct transfer_out + { + std::string address; + std::uint64_t amount; + crypto::secret_key secret; + + transfer_out() + : address(), amount(0), secret{} + {} + }; + WIRE_DECLARE_OBJECT(transfer_out); + struct transaction { - transaction() - : spends(), - receives(), - description(), - timestamp(), - height(), - amount(0), - fee(0), - unlock_time(0), - direction(Monero::TransactionInfo::Direction_Out), - payment_id(), - id{}, - prefix{}, - coinbase(false), - failed(false) - {} - + transaction(); + transaction(transaction&&) = default; transaction(const transaction&) = default; transaction& operator=(transaction&&) = default; @@ -152,8 +169,9 @@ namespace lwsf { namespace internal { namespace backend /*! 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 spends; boost::container::flat_map receives; //!< Key is output pub + std::vector transfers; std::string description; std::optional timestamp; std::optional height; @@ -184,23 +202,27 @@ namespace lwsf { namespace internal { namespace backend std::string address; //!> not serialized, recovered on read_bytes std::string language; std::vector addressbook; - std::map subaccounts; + std::vector subaccounts; //! Enabled major subaccounts. Index indicates `sub.major` value. std::unordered_map> txes; std::unordered_map attributes; std::uint64_t scan_height; std::uint64_t restore_height; + std::uint32_t maj_lookahead = config::subaddr_major_lookahead; + std::uint32_t min_lookahead = config::subaddr_minor_lookahead; Monero::NetworkType type; keypair view; keypair spend; - bool generated_locally; + bool generated_locally; //!< True iff wallet was generated and not recovered }; WIRE_DECLARE_OBJECT(account); + //! All functions should provide the strong-exception guarantee struct wallet { Monero::WalletListener* listener; rpc::http_client client; account primary; + expect server_lookahead; //!< Status of major lookahead server-side. Inclusive std::atomic last_sync; std::uint64_t blockchain_height; std::uint64_t per_byte_fee; @@ -222,14 +244,18 @@ namespace lwsf { namespace internal { namespace backend //! De-serialize `this` from msgpack. Locks+replaces contents. std::error_code from_bytes(epee::byte_slice source); - //! Attempt + //! Attempt login and sync subaddresses. \return no errors if login succeeded std::error_code login(); - //! Refreshes txes information. Strong exception guarantee. Locks contents. + //! Refreshes txes information. Strong exception guarantee. Spuriously returns success. std::error_code refresh(bool mandatory = false); std::error_code add_subaccount(std::string label); + std::error_code add_subaddress(const std::uint32_t accountIndex, std::string label); + + std::error_code set_lookahead(std::uint32_t major, std::uint32_t minor); + std::error_code restore_height(const std::uint64_t height); std::error_code send_tx(epee::byte_slice tx_bytes); diff --git a/src/error.cpp b/src/error.cpp index 4bb87b3..d9295c3 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -35,20 +35,28 @@ namespace lwsf { case error::none: return "No error"; + case error::approval: + return "Account needs approval on light-wallet-server"; case error::configuration: return "Configuration issue"; - case error::connect_failure: - return "Connection to server failed"; + case error::create: + return "Account creation not possible"; case error::crypto_failure: return "Cryptography issue"; case error::invalid_encoding: return "File had invalid encoding"; case error::invalid_scheme: return "Invalid URL scheme"; + case error::network_type: + return "Mismatch on network type"; case error::read_failure: return "Failed to read file"; - case error::rpc_failure: - return "RPC failed"; + case error::subaddr_disabled: + return "light-wallet-server has subaddresses disabled"; + case error::subaddr_local: + return "lwsf limits on subaddresses too small"; + case error::subaddr_server: + return "light-wallet-server has limits on subaddresses that are too small"; case error::unexpected_userinfo: return "Unexpected user+pass field in URL"; case error::unsupported_format: diff --git a/src/error.h b/src/error.h index da54a3d..1236e86 100644 --- a/src/error.h +++ b/src/error.h @@ -34,13 +34,17 @@ namespace lwsf enum class error : int { none = 0, //!< Must be zero for `expect<..>` + approval, //!< Account needs approval configuration, //!< Bad Configuration - connect_failure, //!< Connection to server failed + create, //!< Account creation not possible crypto_failure, //!< Failure in crypto invalid_encoding, //!< Invalid percent encoding invalid_scheme, //!< Invalid network scheme + network_type, //!< Mismatch on network type read_failure, //!< Failed to read file - rpc_failure, //!< RPC failed + subaddr_disabled, //!< Server has subaddresses disabled + subaddr_local, //!< Local limits on subaddresses too small + subaddr_server, //!< Server limits on subaddresses too small unexpected_userinfo,//!< Unexpected user+pass provided unsupported_format, //!< File format could not be unpacked write_failure //!< Failed to write file diff --git a/src/lwsf_config.h b/src/lwsf_config.h index 7020881..51b879d 100644 --- a/src/lwsf_config.h +++ b/src/lwsf_config.h @@ -28,15 +28,24 @@ #pragma once +#include +#include +#include + namespace lwsf { namespace config { - constexpr const std::chrono::seconds connect_timeout{2}; + constexpr const std::chrono::seconds connect_timeout{5}; constexpr const std::chrono::seconds daemon_status_cache{10}; + constexpr const std::string_view default_account_name{"Untitled 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 max_txes_in_rpc = 2048; constexpr const std::uint32_t mixin_default = 16; constexpr const std::chrono::seconds refresh_interval{30}; constexpr const std::chrono::seconds refresh_interval_min{5}; constexpr const std::chrono::seconds rpc_timeout{5}; + constexpr const std::uint32_t subaddr_major_lookahead = 50; + constexpr const std::uint32_t subaddr_minor_lookahead = 200; constexpr const unsigned subaddr_retry{10}; + }} // lwsf // config diff --git a/src/rpc.cpp b/src/rpc.cpp index 6365bd9..7732fb0 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp @@ -44,28 +44,30 @@ #include "wire/json.h" #include "wire/traits.h" #include "wire/wrapper/array.h" +#include "wire/wrapper/trusted_array.h" #include "wire/wrappers_impl.h" namespace lwsf { namespace internal { namespace rpc { - //! \return Error message string. - const char* get_string(error value) noexcept + namespace { - switch (value) + //! \return Error message string. + const char* get_string(error value) noexcept { - default: - break; + switch (value) + { + default: + break; - case error::none: - return "No rpc errors"; - case error::invoke_failure: - return "HTTP invoke failed"; - case error::not_connected: - return "No connection to HTTP server"; - case error::wrong_response_code: - return "Expected HTTP 200 OK"; + case error::none: + return "No rpc errors"; + case error::no_response: + return "No response from HTTP server"; + case error::invalid_code: + return "Invalid status code from HTTP server"; + } + return nullptr; } - return "Unknown rpc error"; } //! \return Category for `error`. @@ -80,7 +82,10 @@ namespace lwsf { namespace internal { namespace rpc virtual std::string message(int value) const override final { - return get_string(error(value)); + char const * const msg = get_string(error(value)); + if (msg) + return msg; + return "HTTP error code " + std::to_string(value); } }; static const category instance{}; @@ -95,13 +100,15 @@ namespace lwsf { namespace internal { namespace rpc const epee::net_utils::http::http_response_info* response = nullptr; if (!client.invoke(endpoint, "POST", {reinterpret_cast(payload.data()), payload.size()}, config::rpc_timeout, std::addressof(response), headers)) - return {error::invoke_failure}; + return {error::no_response}; if (!response) - return {error::invoke_failure}; - if(response->m_response_code != 200 && response->m_response_code != 201) - return {error::wrong_response_code}; + return {error::no_response}; + if(response->m_response_code == 200 || response->m_response_code == 201) + return response->m_body; - return response->m_body; + if (response->m_response_code <= 0 || std::numeric_limits::max() < response->m_response_code) + return {error::invalid_code}; + return {error(int(response->m_response_code))}; } void write_bytes(wire::writer& dest, const empty&) @@ -227,7 +234,6 @@ namespace lwsf { namespace internal { namespace rpc } } - void read_bytes(wire::json_reader& source, get_address_txs& self) { using max_transactions = wire::max_element_count; @@ -239,12 +245,70 @@ namespace lwsf { namespace internal { namespace rpc ); } + + namespace + { + //! A wrapper that simulates an array by only using the first element + template + class array_head_ + { + T head_; + wire::unwrap_reference_t tail_; + std::size_t count_; + + public: + using value_type = wire::unwrap_reference_t; + using iterator = std::add_pointer_t; + using const_iterator = std::add_pointer_t>; + using reference = std::add_lvalue_reference_t; + using const_reference = std::add_lvalue_reference_t>; + + constexpr array_head_(T head) + : head_(std::move(head)), tail_{}, count_(1) + {} + + void clear() noexcept { count_ = 0; } + bool empty() const noexcept { return !size(); } + std::size_t size() const noexcept { return count_; } + const_iterator begin() const noexcept { return std::addressof(front()); } + const_iterator end() const + { + if (1 < size()) + throw std::logic_error{"Unexpected call to subaddr_adaptr::end"}; + return empty() ? begin() : begin() + 1; + } + + reference front() noexcept { return head_; } + const_reference front() const noexcept { return head_; } + reference back() noexcept { return 1 < size() ? tail_ : front(); } + void push_back() noexcept { ++count_; } + reference emplace_back() noexcept { push_back(); return back(); } + }; + + template + constexpr array_head_ array_head(T val) + { return {std::move(val)}; } + + template + void map_subaddrs(F& format, T& self) + { + auto head = array_head(std::ref(self.head)); + wire::object(format, + WIRE_FIELD(key), + wire::field("value", wire::trusted_array(std::ref(head))) + ); + if (head.empty()) + WIRE_DLOG_THROW(wire::error::schema::array, "unexpected empty array"); + } + } + + WIRE_JSON_DEFINE_OBJECT(subaddrs, map_subaddrs); void read_bytes(wire::json_reader& source, get_subaddrs& self) { - using limits = wire::max_element_count<16384>; - wire::object(source, WIRE_FIELD_ARRAY(all_subaddrs, limits)); + wire::object(source, WIRE_FIELD_ARRAY(all_subaddrs, max_subaddrs)); } + namespace { // This field is a mess between implementations, this cleans it up a bit @@ -341,31 +405,24 @@ namespace lwsf { namespace internal { namespace rpc } - void read_bytes(wire::json_reader& source, subaddrs& self) - { - using limits = wire::min_element_size<2>; - wire::object(source, WIRE_FIELD(key), WIRE_FIELD_ARRAY(value, limits)); - } - void write_bytes(wire::json_writer& dest, const provision_subaddrs_request& self) { wire::object(dest, wire::field("address", std::cref(self.creds.address)), wire::field("view_key", std::cref(self.creds.view_key)), - WIRE_FIELD(maj_i), - WIRE_FIELD(min_i), - WIRE_FIELD(n_maj), - WIRE_FIELD(n_min), - WIRE_FIELD(get_all) + WIRE_FIELD_COPY(maj_i), + WIRE_FIELD_COPY(min_i), + WIRE_FIELD_COPY(n_maj), + WIRE_FIELD_COPY(n_min), + WIRE_FIELD_COPY(get_all) ); } void read_bytes(wire::json_reader& source, provision_subaddrs_response& self) { - using limits = wire::max_element_count<16384>; wire::object(source, - WIRE_FIELD_ARRAY(new_subaddrs, limits), - WIRE_FIELD_ARRAY(all_subaddrs, limits) + WIRE_FIELD_ARRAY(new_subaddrs, max_subaddrs), + WIRE_FIELD_ARRAY(all_subaddrs, max_subaddrs) ); } @@ -379,5 +436,24 @@ namespace lwsf { namespace internal { namespace rpc { wire::object(source, WIRE_FIELD(status)); } + + + void write_bytes(wire::json_writer& dest, const upsert_subaddrs_request& self) + { + wire::object(dest, + wire::field("address", std::cref(self.creds.address)), + wire::field("view_key", std::ref(self.creds.view_key)), + wire::field("subaddrs", wire::array(std::ref(self.subaddrs_))), // always write field (not optional) + WIRE_FIELD_COPY(get_all) + ); + } + + void read_bytes(wire::json_reader& source, upsert_subaddrs_response& self) + { + wire::object(source, + WIRE_FIELD_ARRAY(new_subaddrs, max_subaddrs), + WIRE_FIELD_ARRAY(all_subaddrs, max_subaddrs) + ); + } }}} // lwsf // internal // rpc diff --git a/src/rpc.h b/src/rpc.h index 669191b..1f07fb2 100644 --- a/src/rpc.h +++ b/src/rpc.h @@ -28,10 +28,13 @@ #pragma once +#include #include #include #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 @@ -40,6 +43,7 @@ #include "wire/basic_value.h" #include "wire/fwd.h" #include "wire/json.h" +#include "wire/traits.h" namespace epee { namespace net_utils { @@ -49,16 +53,16 @@ namespace epee { namespace net_utils namespace lwsf { namespace internal { namespace rpc { + using max_subaddrs = wire::max_element_count<43690>; using http_client = epee::net_utils::http::http_simple_client_template< epee::net_utils::blocked_mode_client >; enum class error : int { - none = 0, invoke_failure, not_connected, wrong_response_code + none = 0, no_response = -1, invalid_code = -2 /* Otherwise HTTP error code */ }; - const char* get_string(error value) noexcept; const std::error_category& error_category() noexcept; inline std::error_code make_error_code(const error value) noexcept { @@ -142,16 +146,28 @@ namespace lwsf { namespace internal { namespace rpc std::uint32_t maj_i; std::uint32_t min_i; - address_meta() noexcept + constexpr address_meta() noexcept : maj_i(0), min_i(0) {} - address_meta(const std::uint32_t maj, std::uint32_t min) noexcept + constexpr address_meta(const std::uint32_t maj, std::uint32_t min) noexcept : maj_i(maj), min_i(min) {} + + constexpr bool is_default() const noexcept { return !maj_i && !min_i; } }; WIRE_DECLARE_OBJECT(address_meta); + inline constexpr bool operator<(const address_meta& lhs, const address_meta& rhs) noexcept + { + return lhs.maj_i == rhs.maj_i ? + lhs.min_i < rhs.min_i : lhs.maj_i < rhs.maj_i; + } + inline constexpr bool operator==(const address_meta& lhs, const address_meta& rhs) noexcept + { + return lhs.maj_i == rhs.maj_i && lhs.min_i == rhs.min_i; + } + struct transaction_spend { uint64_string amount; @@ -214,20 +230,42 @@ namespace lwsf { namespace internal { namespace rpc struct subaddrs { subaddrs() - : key(0), value() + : head({0, 0}), key(0) {} + explicit subaddrs(const std::uint32_t key) + : head({0, 0}), key(key) + {} + + std::array head; //!< Only the first element of array std::uint32_t key; - std::vector> value; }; + WIRE_JSON_DECLARE_OBJECT(subaddrs); void read_bytes(wire::json_reader&, subaddrs&); + inline bool operator<(const subaddrs& lhs, const subaddrs& rhs) noexcept + { return lhs.key < rhs.key; } + + template + inline bool operator<(const subaddrs& lhs, const T rhs) noexcept + { + static_assert(std::is_unsigned()); + return lhs.key < rhs; + } + + template + inline bool operator<(const T lhs, const subaddrs& rhs) noexcept + { + static_assert(std::is_unsigned()); + return lhs < rhs.key; + } + struct get_subaddrs { get_subaddrs() = delete; static constexpr const char* endpoint() noexcept { return "/get_subaddrs"; } - std::vector all_subaddrs; + boost::container::flat_set> all_subaddrs; }; void read_bytes(wire::json_reader&, get_subaddrs&); @@ -322,8 +360,8 @@ namespace lwsf { namespace internal { namespace rpc provision_subaddrs_response() = delete; static constexpr const char* endpoint() noexcept { return "/provision_subaddrs"; } - std::vector new_subaddrs; - std::vector all_subaddrs; + boost::container::flat_set new_subaddrs; + boost::container::flat_set all_subaddrs; }; void read_bytes(wire::json_reader&, provision_subaddrs_response&); @@ -343,6 +381,26 @@ namespace lwsf { namespace internal { namespace rpc std::string status; }; void read_bytes(wire::json_reader&, submit_raw_tx_response&); + + + struct upsert_subaddrs_request + { + upsert_subaddrs_request() = delete; + login creds; + boost::container::flat_set> subaddrs_; + bool get_all; + }; + void write_bytes(wire::json_writer&, const upsert_subaddrs_request&); + + struct upsert_subaddrs_response + { + upsert_subaddrs_response() = delete; + static constexpr const char* endpoint() noexcept { return "/upsert_subaddrs"; } + + boost::container::flat_set new_subaddrs; + boost::container::flat_set all_subaddrs; + }; + void read_bytes(wire::json_reader&, upsert_subaddrs_response&); }}} // lwsf // internal // rpc diff --git a/src/subaddress_account.cpp b/src/subaddress_account.cpp index bb35743..81febf8 100644 --- a/src/subaddress_account.cpp +++ b/src/subaddress_account.cpp @@ -43,7 +43,7 @@ namespace lwsf { namespace internal std::uint64_t unlocked; std::uint64_t total; - balance() + constexpr balance() noexcept : unlocked(0), total(0) {} }; @@ -88,7 +88,7 @@ namespace lwsf { namespace internal { { const boost::lock_guard lock{data_->sync}; - data_->primary.subaccounts.at(accountIndex).minor.at(0).label = label; + data_->primary.subaccounts.at(accountIndex).detail[0].label = label; } refresh(); } @@ -115,6 +115,13 @@ namespace lwsf { namespace internal balance.unlocked += receive.second.amount; } + if (!tx.second->spends.empty()) + { + auto& balance = balances[tx.second->spends.begin()->second.sender.maj_i]; + balance.unlocked -= tx.second->fee; + balance.total -= tx.second->fee; + } + for (const auto& spend : tx.second->spends) { auto& balance = balances[spend.second.sender.maj_i]; @@ -123,15 +130,19 @@ namespace lwsf { namespace internal } } - rows_.reserve(data_->primary.subaccounts.size()); - for (const auto& sub : data_->primary.subaccounts) + static constexpr const std::uint32_t max_index = + std::numeric_limits::max(); + + const auto& accts = data_->primary.subaccounts; + rows_.reserve(accts.size()); + for (std::size_t i = 0; i < accts.size() && i <= max_index; ++i) { - const auto& balance = balances[sub.first]; + const auto& balance = balances[i]; rows_.push_back( new Monero::SubaddressAccountRow{ - sub.first, - data_->get_spend_address({sub.first, 0}), - sub.second.minor.empty() ? "" : sub.second.minor.begin()->second.label, + std::uint32_t(i), + data_->get_spend_address({std::uint32_t(i), 0}), + std::string{accts.at(i).primary_label()}, cryptonote::print_money(balance.total), cryptonote::print_money(balance.unlocked) } diff --git a/src/subaddress_minor.cpp b/src/subaddress_minor.cpp index d5bded3..9e23e2d 100644 --- a/src/subaddress_minor.cpp +++ b/src/subaddress_minor.cpp @@ -65,18 +65,19 @@ namespace lwsf { namespace internal free_rows(rows_); const boost::lock_guard lock{data_->sync}; - const auto account = data_->primary.subaccounts.find(accountIndex); - if (account == data_->primary.subaccounts.end()) + if (data_->primary.subaccounts.size() <= accountIndex) return; - rows_.reserve(account->second.minor.size()); - for (const auto& minor : account->second.minor) + const auto& acct = data_->primary.subaccounts[accountIndex]; + static_assert(std::is_same()); + rows_.reserve(acct.used); + for (std::uint32_t i = 0; i < acct.used; ++i) { rows_.push_back( new Monero::SubaddressRow{ - minor.first, - data_->get_spend_address({accountIndex, minor.first}), - minor.second.label + i, + data_->get_spend_address({accountIndex, i}), + std::string{acct.sub_label(i)} } ); } @@ -89,15 +90,16 @@ namespace lwsf { namespace internal } void subaddress_minor::addRow(const std::uint32_t accountIndex, const std::string &label) - { - /* TODO */ + { + data_->add_subaddress(accountIndex, label); + refresh(accountIndex); } void subaddress_minor::setLabel(const std::uint32_t accountIndex, const std::uint32_t addressIndex, const std::string &label) { { const boost::lock_guard lock{data_->sync}; - data_->primary.subaccounts.at(accountIndex).minor.at(addressIndex).label = label; + data_->primary.subaccounts.at(accountIndex).detail[addressIndex].label = label; } refresh(accountIndex); } diff --git a/src/transaction_history.cpp b/src/transaction_history.cpp index e7e9721..fa6e2d8 100644 --- a/src/transaction_history.cpp +++ b/src/transaction_history.cpp @@ -90,36 +90,31 @@ namespace lwsf { namespace internal void transaction_history::refresh() { - std::multimap> temp; - { - const boost::lock_guard lock{data_->sync}; - if (std::numeric_limits::max() < data_->primary.txes.size()) - throw std::runtime_error{"lwsf::internal::transaction_history::refresh exceeds max history size"}; + by_id_.clear(); - for (const auto& tx : data_->primary.txes) - temp.emplace(tx.second->height.value_or(std::numeric_limits::max()), tx.second); - } + const boost::lock_guard lock{data_->sync}; + if (std::numeric_limits::max() < data_->primary.txes.size()) + throw std::runtime_error{"lwsf::internal::transaction_history::refresh exceeds max history size"}; // be careful about exceptions and memory leaks. this should be safe - for (std::size_t i = temp.size(); i < txes_.size(); ++i) + for (std::size_t i = data_->primary.txes.size(); i < txes_.size(); ++i) { const std::unique_ptr destroy{txes_[i]}; txes_[i] = nullptr; } - by_id_.clear(); - txes_.resize(temp.size()); + txes_.resize(data_->primary.txes.size()); try { std::size_t i = 0; - for (auto& backend_tx : temp) + for (const auto& backend_tx : data_->primary.txes) { if (txes_[i]) - static_cast(txes_[i])->update(std::move(backend_tx.second)); + static_cast(txes_[i])->update(backend_tx.second); else - txes_[i] = new transaction_info{data_, std::move(backend_tx.second)}; + txes_[i] = new transaction_info{data_, backend_tx.second}; ++i; } } diff --git a/src/transaction_info.cpp b/src/transaction_info.cpp index 5ebf9cb..fc6c1bf 100644 --- a/src/transaction_info.cpp +++ b/src/transaction_info.cpp @@ -51,9 +51,9 @@ namespace lwsf { namespace internal void transaction_info::update_transfers() { transfers_.clear(); - transfers_.reserve(data_->spends.size()); - for (const auto& spend : data_->spends) - transfers_.emplace_back(spend.second.amount, spend.second.address); + transfers_.reserve(data_->transfers.size()); + for (const auto& out : data_->transfers) + transfers_.emplace_back(out.amount, out.address); } transaction_info::transaction_info(std::shared_ptr wallet, std::shared_ptr data) @@ -114,30 +114,26 @@ namespace lwsf { namespace internal std::string transaction_info::label() const { - try // We could be out-of-sync with another wallet adding subaddresses + const auto get_label = [this] (const rpc::address_meta& meta) -> std::string { - if (data_->direction == Direction_In) + const boost::lock_guard lock{wallet_->sync}; + if (meta.maj_i < wallet_->primary.subaccounts.size()) { - if (!data_->receives.empty()) - { - const auto& receive = data_->receives.begin()->second; - const boost::lock_guard lock{wallet_->sync}; - return wallet_->primary.subaccounts.at(receive.recipient.maj_i).minor.at(receive.recipient.min_i).label; - } - } - else if (!data_->spends.empty()) - { - const auto& spend = data_->spends.begin()->second; - const boost::lock_guard lock{wallet_->sync}; - return wallet_->primary.subaccounts.at(spend.sender.maj_i).minor.at(spend.sender.min_i).label; + const auto& major = wallet_->primary.subaccounts.at(meta.maj_i); + const auto minor = major.detail.find(meta.min_i); + if (minor != major.detail.end()) + return minor->second.label; } + return {}; + }; + + if (data_->direction == Direction_In) + { + if (!data_->receives.empty()) + return get_label(data_->receives.begin()->second.recipient); } -#if BOOST_VERSION >= 107600 - catch (const boost::container::out_of_range&) - {} -#endif - catch (const std::out_of_range&) - {} + else if (!data_->spends.empty()) + return get_label(data_->spends.begin()->second.sender); return {}; } diff --git a/src/wallet.cpp b/src/wallet.cpp index e8a3d74..8e314ef 100644 --- a/src/wallet.cpp +++ b/src/wallet.cpp @@ -324,7 +324,7 @@ namespace lwsf { namespace internal return boost::chrono::nanoseconds{source.count()}; } - void load_wallet(backend::wallet& self, const crypto::secret_key& from, const Monero::NetworkType nettype, const bool generated_locally) + void init_wallet(backend::wallet& self, const crypto::secret_key& from, const Monero::NetworkType nettype, const bool generated_locally) { cryptonote::account_base base{}; base.generate(from, true); @@ -344,11 +344,14 @@ namespace lwsf { namespace internal } } // anonymous - bool wallet::set_error(const std::error_code status) const + bool wallet::set_error(const std::error_code status, const bool update_iff_error) const { const boost::lock_guard lock{error_sync_}; - status_ = status; - exception_error_.clear(); + if (!update_iff_error || status) + { + status_ = status; + exception_error_.clear(); + } return !status_; } @@ -394,7 +397,7 @@ namespace lwsf { namespace internal { // refresh has strong exception guarantee - never in partial state. lock.unlock(); - set_error(data_->refresh(mandatory_refresh)); + set_error(data_->refresh(mandatory_refresh), true); lock.lock(); } else if (thread_state_ == state::skip_once) @@ -491,7 +494,7 @@ namespace lwsf { namespace internal crypto::secret_key recovery; randombytes_buf(std::addressof(unwrap(unwrap(recovery))), sizeof(recovery)); - load_wallet(*data_, recovery, nettype, true); + init_wallet(*data_, recovery, nettype, true); } wallet::wallet(open, Monero::NetworkType nettype, std::string filename, std::string password, const std::uint64_t kdf_rounds) @@ -588,7 +591,7 @@ namespace lwsf { namespace internal if (!seed_offset.empty()) recovery = cryptonote::decrypt_key(recovery, seed_offset); - load_wallet(*data_, recovery, nettype, false); + init_wallet(*data_, recovery, nettype, false); } @@ -805,7 +808,7 @@ namespace lwsf { namespace internal } // this will clear any existing errors - return set_error(status); + return set_error(status, false); } 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) @@ -851,11 +854,8 @@ namespace lwsf { namespace internal void wallet::setRefreshFromBlockHeight(uint64_t refresh_from_block_height) { - try { set_error(data_->restore_height(refresh_from_block_height)); } - catch (const std::exception& e) - { - set_critical(e); - } + try { set_error(data_->restore_height(refresh_from_block_height), true); } + catch (const std::exception& e) { set_critical(e); } } uint64_t wallet::getRefreshFromBlockHeight() const @@ -863,6 +863,12 @@ namespace lwsf { namespace internal const boost::lock_guard lock{data_->sync}; return data_->primary.restore_height; } + + void wallet::setSubaddressLookahead(uint32_t major, uint32_t minor) + { + try { set_error(data_->set_lookahead(major, minor), false); } + catch (const std::exception& e) { set_critical(e); } + } bool wallet::connectToDaemon() { @@ -881,12 +887,12 @@ namespace lwsf { namespace internal std::unique_ptr disconnect{this}; if (data_->client.connect(config::connect_timeout)) { - const std::error_code status = data_->login(); - if (!status) + const std::error_code err = data_->login(); + if (!err) disconnect.release(); - return set_error(status); + return set_error(err, false); } - set_error(error::connect_failure); + set_error(rpc::error::no_response, false); return false; } @@ -906,7 +912,7 @@ namespace lwsf { namespace internal if (!endpoint) { data_->client.set_connector(null_connector{}); - return set_error(endpoint.error()); + return set_error(endpoint.error(), false); } data_->client.set_connector(net::socks::connector{std::move(*endpoint)}); return true; @@ -919,12 +925,18 @@ namespace lwsf { namespace internal std::uint64_t balance = 0; for (const auto& tx : data_->primary.txes) { + if (tx.second->failed) + continue; + for (const auto& receive : tx.second->receives) { if (receive.second.recipient.maj_i == accountIndex) balance += receive.second.amount; } + if (!tx.second->spends.empty() && tx.second->spends.begin()->second.sender.maj_i == accountIndex) + balance -= tx.second->fee; + for (const auto& spend : tx.second->spends) { if (spend.second.sender.maj_i == accountIndex) @@ -944,6 +956,9 @@ namespace lwsf { namespace internal std::uint64_t balance = 0; for (const auto& tx : data_->primary.txes) { + if (tx.second->failed) + continue; + const bool unlocked = tx.second->is_unlocked(chain_height, net_type); for (const auto& receive : tx.second->receives) { @@ -951,6 +966,9 @@ namespace lwsf { namespace internal balance += receive.second.amount; } + if (!tx.second->spends.empty() && tx.second->spends.begin()->second.sender.maj_i == accountIndex) + balance -= tx.second->fee; + for (const auto& spend : tx.second->spends) { if (spend.second.sender.maj_i == accountIndex) @@ -1017,7 +1035,7 @@ namespace lwsf { namespace internal bool wallet::refresh() { - try { return set_error(data_->refresh(true)); } + try { return set_error(data_->refresh(true), true); } catch (const std::exception& e) { set_critical(e); } return false; } @@ -1053,7 +1071,8 @@ namespace lwsf { namespace internal void wallet::addSubaddressAccount(const std::string& label) { - data_->add_subaccount(label); + try { set_error(data_->add_subaccount(label), false); } + catch (const std::exception& e) { set_critical(e); } } std::size_t wallet::numSubaddressAccounts() const @@ -1062,41 +1081,36 @@ namespace lwsf { namespace internal return data_->primary.subaccounts.size(); } - std::size_t wallet::numSubaddresses(const std::uint32_t accountIndex) const { + static_assert(std::numeric_limits::max() <= std::numeric_limits::max()); const boost::lock_guard lock{data_->sync}; - const auto account = data_->primary.subaccounts.find(accountIndex); - if (account != data_->primary.subaccounts.end()) - return account->second.minor.size(); + if (accountIndex < data_->primary.subaccounts.size()) + return data_->primary.subaccounts.at(accountIndex).used; set_critical(std::runtime_error{"numSubaddresses failed, " + std::to_string(accountIndex) + " does not exist"}); return 0; } std::string wallet::getSubaddressLabel(const std::uint32_t accountIndex, const std::uint32_t addressIndex) const { + static_assert(std::numeric_limits::max() <= std::numeric_limits::max()); const boost::lock_guard lock{data_->sync}; - const auto major = data_->primary.subaccounts.find(accountIndex); - if (major != data_->primary.subaccounts.end()) - { - const auto minor = major->second.minor.find(addressIndex); - if (minor != major->second.minor.end()) - return minor->second.label; - } + if (accountIndex < data_->primary.subaccounts.size()) + return std::string{data_->primary.subaccounts.at(accountIndex).sub_label(addressIndex)}; set_critical(std::runtime_error{"getSubaddressLabel failed, " + std::to_string(accountIndex) + "," + std::to_string(addressIndex) + " does not exist"}); return {}; } void wallet::setSubaddressLabel(const std::uint32_t accountIndex, const std::uint32_t addressIndex, const std::string &label) { + static_assert(std::numeric_limits::max() <= std::numeric_limits::max()); const boost::lock_guard lock{data_->sync}; - auto major = data_->primary.subaccounts.find(accountIndex); - if (major != data_->primary.subaccounts.end()) + if (accountIndex < data_->primary.subaccounts.size()) { - auto minor = major->second.minor.find(addressIndex); - if (minor != major->second.minor.end()) + auto& acct = data_->primary.subaccounts[accountIndex]; + if (addressIndex < acct.used) { - minor->second.label = label; + acct.detail.try_emplace(addressIndex).first->second.label = label; return; } } @@ -1200,7 +1214,6 @@ namespace lwsf { namespace internal const auto attribute = data_->primary.attributes.find(key); if (attribute != data_->primary.attributes.end()) return attribute->second; - set_critical(std::runtime_error{"attribute " + key + " does not exist"}); return {}; } @@ -1257,16 +1270,13 @@ namespace lwsf { namespace internal std::string out; out.reserve(iter->second->spends.size() * hex_size); - for (const auto& spend : iter->second->spends) + for (const auto& transfer : iter->second->transfers) { - if (spend.second.secret) + out.insert(out.end(), hex_size, 0); + if (!epee::to_hex::buffer({out.data() + out.size() - hex_size, hex_size}, epee::as_byte_span(unwrap(unwrap(transfer.secret))))) { - out.insert(out.end(), hex_size, 0); - if (!epee::to_hex::buffer({out.data() + out.size() - hex_size, hex_size}, epee::as_byte_span(unwrap(unwrap(*spend.second.secret))))) - { - set_critical(std::runtime_error{"getTxKey conversion to hex failure"}); - return {}; - } + set_critical(std::runtime_error{"getTxKey conversion to hex failure"}); + return {}; } } diff --git a/src/wallet.h b/src/wallet.h index b1ca8ce..4f08958 100644 --- a/src/wallet.h +++ b/src/wallet.h @@ -72,7 +72,7 @@ namespace internal state thread_state_; bool mandatory_refresh_; - bool set_error(std::error_code status) const; + bool set_error(std::error_code status, bool update_iff_error) const; void set_critical(const std::exception& e) const; void stop_refresh_loop(); @@ -239,7 +239,7 @@ namespace internal * \param major - size fot the major index * \param minor - size fot the minor index */ - virtual void setSubaddressLookahead(uint32_t major, uint32_t minor) override { /* TODO */ } + virtual void setSubaddressLookahead(uint32_t major, uint32_t minor) override; /** * @brief connectToDaemon - connects to the daemon. TODO: check if it can be removed diff --git a/src/wire/adapted/array.h b/src/wire/adapted/array.h index 93535f6..5e6e085 100644 --- a/src/wire/adapted/array.h +++ b/src/wire/adapted/array.h @@ -53,18 +53,18 @@ namespace wire template inline void read_bytes(R& source, std::array& dest) { - source.binary(epee::to_mut_span(dest)); + source.string(epee::to_mut_span(dest), true /* exact */); } template inline void read_bytes(R& source, std::array& dest) { - source.binary(epee::to_mut_span(dest)); + source.binary(epee::to_mut_span(dest), true /* exact */); } template inline void write_bytes(W& dest, const std::array& source) { - source.binary(epee::to_span(source)); + source.string(epee::to_span(source)); } template inline void write_bytes(W& dest, const std::array& source)