Fixed transfers, added subaddress support. Buggy on error reporting.

This commit is contained in:
Lee Clagett
2025-04-18 22:17:16 -04:00
parent e3b56a76fd
commit d7fb7961f8
14 changed files with 872 additions and 308 deletions
+488 -119
View File
@@ -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<typename T, typename U>
std::uint32_t add_uint32_clamp(const T index, const U lookahead)
{
static_assert(std::is_unsigned<T>());
static_assert(std::is_same<U, std::uint32_t>());
if (std::numeric_limits<std::uint32_t>::max() - lookahead <= index)
return std::numeric_limits<std::uint32_t>::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<typename F, typename T>
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<typename F, typename T>
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<typename F, typename T>
void map_transfer_out(F& format, T& self)
{
wire::object(format, WIRE_FIELD(address), WIRE_FIELD(amount), WIRE_FIELD(secret));
}
template<typename F, typename T>
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<rpc::address_meta>& 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<std::vector<rpc::address_meta>> 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<rpc::address_meta> 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<transaction>& out, const rpc::output& source, const crypto::secret_key& view_key)
rpc::address_meta merge_output(const std::shared_ptr<transaction>& 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<std::shared_ptr<transaction>>
merge_response(wallet& self, const rpc::get_address_txs& source, const rpc::get_unspent_outs_response& unspents)
struct merge_results
{
std::vector<std::shared_ptr<transaction>> out;
std::vector<std::shared_ptr<transaction>> new_transactions;
boost::container::flat_set<rpc::subaddrs, std::less<>> 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<transaction>(*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<transaction>(*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<transaction>();
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<std::size_t>::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<const rpc::subaddrs> 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<rpc::subaddrs, std::less<>>& 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<std::uint32_t, sub_account> 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<rpc::subaddrs, std::less<>>& out)
{
static constexpr const std::uint32_t max_index =
std::numeric_limits<std::uint32_t>::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<std::uint64_t>::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<boost::mutex> 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<rpc::login_response>(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<boost::mutex> 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<rpc::get_subaddrs>(client, login);
if (response)
// Converting `rpc::invoke` into exceptions is easier here
try
{
const boost::lock_guard<boost::mutex> 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<rpc::get_subaddrs>(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<rpc::upsert_subaddrs_response>(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<boost::mutex> lock_refresh{sync_refresh};
boost::unique_lock<boost::mutex> 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<boost::mutex> lock{ptr->sync_listener};
if (!ptr->listener) return;
ptr->listener->refreshed();
}
};
std::unique_ptr<wallet, call_refreshed> 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<rpc::get_unspent_outs_response>(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<rpc::upsert_subaddrs_response>(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<rpc::provision_subaddrs_response>(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<boost::mutex> 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<boost::mutex> lock{sync};
if (std::numeric_limits<std::uint32_t>::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<boost::mutex> lock{sync};
for (const auto& account : primary.subaccounts)
{
if (account.first != next + 1)
break;
++next;
}
++next;
}
if (!server_lookahead)
break;
if (std::numeric_limits<std::uint32_t>::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<rpc::provision_subaddrs_response>(client, request);
lock.lock();
if (!response)
return response.error();
server_lookahead = handle_lookahead_error(response.error());
bool matched = false;
const boost::lock_guard<boost::mutex> 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<boost::mutex> lock{sync};
auto maj = std::addressof(primary.subaccounts.at(accountIndex));
if (std::numeric_limits<std::uint32_t>::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<rpc::provision_subaddrs_response>(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<boost::mutex> 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<rpc::upsert_subaddrs_response>(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 */)
+57 -31
View File
@@ -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<std::uint32_t, subaddress> minor;
boost::container::flat_map<std::uint32_t, subaddress> 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<crypto::secret_key> 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::key> 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<crypto::key_image, transfer_out, memory> spends;
boost::container::flat_map<crypto::key_image, transfer_spend, memory> spends;
boost::container::flat_map<crypto::public_key, transfer_in, memory> receives; //!< Key is output pub
std::vector<transfer_out> transfers;
std::string description;
std::optional<std::time_t> timestamp;
std::optional<std::uint64_t> 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<address_book_entry> addressbook;
std::map<std::uint32_t, sub_account> subaccounts;
std::vector<sub_account> subaccounts; //! Enabled major subaccounts. Index indicates `sub.major` value.
std::unordered_map<crypto::hash, std::shared_ptr<transaction>> txes;
std::unordered_map<std::string, std::string> 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<std::uint32_t> server_lookahead; //!< Status of major lookahead server-side. Inclusive
std::atomic<std::chrono::steady_clock::time_point::duration::rep> 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);
+12 -4
View File
@@ -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:
+6 -2
View File
@@ -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
+10 -1
View File
@@ -28,15 +28,24 @@
#pragma once
#include <chrono>
#include <cstdint>
#include <string_view>
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
+113 -37
View File
@@ -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<const char*>(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<int>::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<config::max_txes_in_rpc>;
@@ -239,12 +245,70 @@ namespace lwsf { namespace internal { namespace rpc
);
}
namespace
{
//! A wrapper that simulates an array by only using the first element
template<typename T>
class array_head_
{
T head_;
wire::unwrap_reference_t<T> tail_;
std::size_t count_;
public:
using value_type = wire::unwrap_reference_t<T>;
using iterator = std::add_pointer_t<value_type>;
using const_iterator = std::add_pointer_t<std::add_const_t<value_type>>;
using reference = std::add_lvalue_reference_t<value_type>;
using const_reference = std::add_lvalue_reference_t<std::add_const_t<value_type>>;
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<typename T>
constexpr array_head_<T> array_head(T val)
{ return {std::move(val)}; }
template<typename F, typename T>
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
+67 -9
View File
@@ -28,10 +28,13 @@
#pragma once
#include <boost/container/flat_set.hpp>
#include <boost/variant.hpp>
#include <ctime>
#include <optional>
#include <system_error>
#include <type_traits>
#include <vector>
#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<std::uint32_t, 2> head; //!< Only the first element of array
std::uint32_t key;
std::vector<std::array<std::uint32_t, 2>> 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<typename T>
inline bool operator<(const subaddrs& lhs, const T rhs) noexcept
{
static_assert(std::is_unsigned<T>());
return lhs.key < rhs;
}
template<typename T>
inline bool operator<(const T lhs, const subaddrs& rhs) noexcept
{
static_assert(std::is_unsigned<T>());
return lhs < rhs.key;
}
struct get_subaddrs
{
get_subaddrs() = delete;
static constexpr const char* endpoint() noexcept { return "/get_subaddrs"; }
std::vector<subaddrs> all_subaddrs;
boost::container::flat_set<subaddrs, std::less<>> 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<subaddrs> new_subaddrs;
std::vector<subaddrs> all_subaddrs;
boost::container::flat_set<subaddrs> new_subaddrs;
boost::container::flat_set<subaddrs> 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, std::less<>> 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<subaddrs> new_subaddrs;
boost::container::flat_set<subaddrs> all_subaddrs;
};
void read_bytes(wire::json_reader&, upsert_subaddrs_response&);
}}} // lwsf // internal // rpc
+19 -8
View File
@@ -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<boost::mutex> 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<std::uint32_t>::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)
}
+12 -10
View File
@@ -65,18 +65,19 @@ namespace lwsf { namespace internal
free_rows(rows_);
const boost::lock_guard<boost::mutex> 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<std::uint32_t, decltype(acct.used)>());
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<boost::mutex> lock{data_->sync};
data_->primary.subaccounts.at(accountIndex).minor.at(addressIndex).label = label;
data_->primary.subaccounts.at(accountIndex).detail[addressIndex].label = label;
}
refresh(accountIndex);
}
+9 -14
View File
@@ -90,36 +90,31 @@ namespace lwsf { namespace internal
void transaction_history::refresh()
{
std::multimap<std::uint64_t, std::shared_ptr<const backend::transaction>> temp;
{
const boost::lock_guard<boost::mutex> lock{data_->sync};
if (std::numeric_limits<int>::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<std::uint64_t>::max()), tx.second);
}
const boost::lock_guard<boost::mutex> lock{data_->sync};
if (std::numeric_limits<int>::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<Monero::TransactionInfo> 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<transaction_info*>(txes_[i])->update(std::move(backend_tx.second));
static_cast<transaction_info*>(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;
}
}
+19 -23
View File
@@ -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<backend::wallet> wallet, std::shared_ptr<const backend::transaction> 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<boost::mutex> 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<boost::mutex> 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<boost::mutex> 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 {};
}
+55 -45
View File
@@ -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<boost::mutex> 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<boost::mutex> 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<wallet, on_throw> 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<std::uint32_t>::max() <= std::numeric_limits<std::size_t>::max());
const boost::lock_guard<boost::mutex> 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<std::uint32_t>::max() <= std::numeric_limits<std::size_t>::max());
const boost::lock_guard<boost::mutex> 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<std::uint32_t>::max() <= std::numeric_limits<std::size_t>::max());
const boost::lock_guard<boost::mutex> 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 {};
}
}
+2 -2
View File
@@ -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
+3 -3
View File
@@ -53,18 +53,18 @@ namespace wire
template<typename R, std::size_t N>
inline void read_bytes(R& source, std::array<char, N>& dest)
{
source.binary(epee::to_mut_span(dest));
source.string(epee::to_mut_span(dest), true /* exact */);
}
template<typename R, std::size_t N>
inline void read_bytes(R& source, std::array<std::uint8_t, N>& dest)
{
source.binary(epee::to_mut_span(dest));
source.binary(epee::to_mut_span(dest), true /* exact */);
}
template<typename W, std::size_t N>
inline void write_bytes(W& dest, const std::array<char, N>& source)
{
source.binary(epee::to_span(source));
source.string(epee::to_span(source));
}
template<typename W, std::size_t N>
inline void write_bytes(W& dest, const std::array<std::uint8_t, N>& source)