mirror of
https://github.com/wownero/lwsf-wow.git
synced 2026-10-17 15:31:32 +02:00
Fixed transfers, added subaddress support. Buggy on error reporting.
This commit is contained in:
+488
-119
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user