mirror of
https://codeberg.org/wownero/lwsf-wow.git
synced 2026-10-17 05:41:30 +02:00
758 lines
26 KiB
C++
758 lines
26 KiB
C++
// Copyright (c) 2024, The Monero Project
|
|
//
|
|
// All rights reserved.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without modification, are
|
|
// permitted provided that the following conditions are met:
|
|
//
|
|
// 1. Redistributions of source code must retain the above copyright notice, this list of
|
|
// conditions and the following disclaimer.
|
|
//
|
|
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
|
|
// of conditions and the following disclaimer in the documentation and/or other
|
|
// materials provided with the distribution.
|
|
//
|
|
// 3. Neither the name of the copyright holder nor the names of its contributors may be
|
|
// used to endorse or promote products derived from this software without specific
|
|
// prior written permission.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
|
|
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
|
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
|
|
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
|
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
|
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
#include "backend.h"
|
|
|
|
#include <boost/thread/lock_guard.hpp>
|
|
#include "crypto/crypto.h" // monero/src
|
|
#include "crypto/crypto-ops.h" // monero/src
|
|
#include "cryptonote_basic/cryptonote_basic_impl.h" // monero/src
|
|
#include "error.h"
|
|
#include "lwsf_config.h"
|
|
#include "ringct/rctOps.h"
|
|
#include "wire.h"
|
|
#include "wire/adapted/crypto.h"
|
|
#include "wire/adapted/pair.h"
|
|
#include "wire/msgpack.h"
|
|
#include "wire/wrapper/trusted_array.h"
|
|
#include "wire/wrapper/variant.h"
|
|
|
|
namespace Monero
|
|
{
|
|
WIRE_AS_INTEGER(TransactionInfo::Direction);
|
|
WIRE_AS_INTEGER(Monero::NetworkType);
|
|
}
|
|
|
|
namespace lwsf { namespace internal { namespace backend
|
|
{
|
|
namespace
|
|
{
|
|
cryptonote::network_type convert_net_type(const Monero::NetworkType in)
|
|
{
|
|
switch(in)
|
|
{
|
|
case Monero::NetworkType::MAINNET:
|
|
return cryptonote::network_type::MAINNET;
|
|
case Monero::NetworkType::TESTNET:
|
|
return cryptonote::network_type::TESTNET;
|
|
case Monero::NetworkType::STAGENET:
|
|
return cryptonote::network_type::STAGENET;
|
|
default:
|
|
break;
|
|
}
|
|
return cryptonote::network_type::UNDEFINED;
|
|
}
|
|
|
|
bool is_tx_spendtime_unlocked(const std::uint64_t chain_height, const std::uint64_t unlock_time, const std::uint64_t block_height, Monero::NetworkType nettype_in)
|
|
{
|
|
const auto nettype = convert_net_type(nettype_in);
|
|
if(unlock_time < CRYPTONOTE_MAX_BLOCK_NUMBER)
|
|
{
|
|
//interpret as block index
|
|
if(chain_height - 1 + CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_BLOCKS >= unlock_time)
|
|
return true;
|
|
else
|
|
return false;
|
|
}else
|
|
{
|
|
//interpret as time
|
|
const uint64_t adjusted_time = std::time(nullptr);
|
|
// XXX: this needs to be fast, so we'd need to get the starting heights
|
|
// from the daemon to be correct once voting kicks in
|
|
uint64_t v2height = nettype == cryptonote::TESTNET ? 624634 : nettype == cryptonote::STAGENET ? 32000 : 1009827;
|
|
uint64_t leeway = chain_height < v2height ? CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V1 : CRYPTONOTE_LOCKED_TX_ALLOWED_DELTA_SECONDS_V2;
|
|
if(adjusted_time + leeway >= unlock_time)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
crypto::secret_key get_subaddress_secret_key(const crypto::secret_key &a, const std::uint32_t major, const std::uint32_t minor)
|
|
{
|
|
char data[sizeof(::config::HASH_KEY_SUBADDRESS) + sizeof(crypto::secret_key) + 2 * sizeof(uint32_t)];
|
|
memcpy(data, ::config::HASH_KEY_SUBADDRESS, sizeof(::config::HASH_KEY_SUBADDRESS));
|
|
memcpy(data + sizeof(::config::HASH_KEY_SUBADDRESS), &a, sizeof(crypto::secret_key));
|
|
std::uint32_t idx = SWAP32LE(major);
|
|
memcpy(data + sizeof(::config::HASH_KEY_SUBADDRESS) + sizeof(crypto::secret_key), &idx, sizeof(uint32_t));
|
|
idx = SWAP32LE(minor);
|
|
memcpy(data + sizeof(::config::HASH_KEY_SUBADDRESS) + sizeof(crypto::secret_key) + sizeof(uint32_t), &idx, sizeof(uint32_t));
|
|
crypto::secret_key m;
|
|
crypto::hash_to_scalar(data, sizeof(data), m);
|
|
return m;
|
|
}
|
|
|
|
template<typename F, typename T>
|
|
void map_address_book_entry(F& format, T& self)
|
|
{
|
|
wire::object(format,
|
|
WIRE_FIELD(address),
|
|
WIRE_FIELD(payment_id),
|
|
WIRE_FIELD(description)
|
|
);
|
|
}
|
|
|
|
template<typename F, typename T>
|
|
void map_subaddress(F& format, T& self)
|
|
{
|
|
wire::object(format, WIRE_FIELD(label));
|
|
}
|
|
|
|
template<typename F, typename T>
|
|
void map_sub_account(F& format, T& self)
|
|
{
|
|
wire::object(format,
|
|
wire::field("minor", wire::trusted_array(std::ref(self.minor)))
|
|
);
|
|
}
|
|
|
|
template<typename F, typename T>
|
|
void map_transfer_out(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)
|
|
);
|
|
}
|
|
|
|
template<typename F, typename T>
|
|
void map_transfer_in(F& format, T& self)
|
|
{
|
|
wire::object(format,
|
|
WIRE_FIELD(global_index),
|
|
WIRE_FIELD(amount),
|
|
WIRE_FIELD(recipient),
|
|
WIRE_FIELD(index),
|
|
WIRE_OPTIONAL_FIELD(rct_mask),
|
|
WIRE_FIELD(tx_pub)
|
|
);
|
|
}
|
|
|
|
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_FIELD(description),
|
|
WIRE_OPTIONAL_FIELD(timestamp),
|
|
WIRE_FIELD(amount),
|
|
WIRE_OPTIONAL_FIELD(height),
|
|
WIRE_FIELD(unlock_time),
|
|
WIRE_FIELD(direction),
|
|
WIRE_OPTION("payment_id0", rpc::empty, payment_id),
|
|
WIRE_OPTION("payment_id8", crypto::hash8, payment_id),
|
|
WIRE_OPTION("payment_id32", crypto::hash, payment_id),
|
|
WIRE_FIELD(id),
|
|
WIRE_FIELD(prefix),
|
|
WIRE_FIELD(coinbase)
|
|
);
|
|
}
|
|
} // anonymous
|
|
|
|
static void read_bytes(wire::reader& source, std::pair<crypto::hash, std::shared_ptr<transaction>>& dest)
|
|
{
|
|
if (!dest.second)
|
|
dest.second = std::make_shared<transaction>();
|
|
read_bytes(source, *dest.second);
|
|
dest.first = dest.second->id;
|
|
}
|
|
static void write_bytes(wire::writer& dest, const std::pair<const crypto::hash, std::shared_ptr<transaction>>& source)
|
|
{
|
|
if (!source.second)
|
|
WIRE_DLOG_THROW(wire::error::schema::object, "Unexpected nullptr");
|
|
write_bytes(dest, *source.second);
|
|
}
|
|
|
|
namespace
|
|
{
|
|
template<typename F, typename T>
|
|
void map_keypair(F& format, T& self)
|
|
{
|
|
wire::object(format, WIRE_FIELD(sec), WIRE_FIELD(pub));
|
|
}
|
|
|
|
template<typename F, typename T>
|
|
void map_account(F& format, T& self)
|
|
{
|
|
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_FIELD(scan_height),
|
|
WIRE_FIELD(restore_height),
|
|
WIRE_FIELD(type),
|
|
WIRE_FIELD(view),
|
|
WIRE_FIELD(spend),
|
|
WIRE_FIELD(generated_locally)
|
|
);
|
|
}
|
|
|
|
rct::key get_mask(const crypto::secret_key& view_key, const rpc::output& source)
|
|
{
|
|
crypto::key_derivation derived{};
|
|
if (!crypto::generate_key_derivation(source.tx_pub_key, view_key, derived))
|
|
throw std::runtime_error{"generate_key_derivation failure"};
|
|
|
|
crypto::secret_key scalar{};
|
|
crypto::derivation_to_scalar(derived, source.index, scalar);
|
|
|
|
rct::ecdhTuple commitment{source.rct.mask};
|
|
rct::ecdhDecode(commitment, rct::sk2rct(scalar), source.rct.type == rpc::ringct::format::recompute);
|
|
|
|
return commitment.mask;
|
|
}
|
|
|
|
void update_output(transfer_in& out, const rpc::output& source, const crypto::secret_key& view_key)
|
|
{
|
|
out.global_index = std::uint64_t(source.global_index);
|
|
out.amount = std::uint64_t(source.amount);
|
|
out.recipient = source.recipient.value_or(rpc::address_meta{});
|
|
out.index = source.index;
|
|
out.tx_pub = source.tx_pub_key;
|
|
|
|
switch (source.rct.type)
|
|
{
|
|
default:
|
|
throw std::runtime_error{"Unexpected ringct mask type"};
|
|
case rpc::ringct::format::none:
|
|
out.rct_mask = std::nullopt;
|
|
break;
|
|
case rpc::ringct::format::encrypted:
|
|
case rpc::ringct::format::recompute:
|
|
out.rct_mask = get_mask(view_key, source);
|
|
break;
|
|
case rpc::ringct::format::unencrypted:
|
|
out.rct_mask = source.rct.mask;
|
|
break;
|
|
}
|
|
}
|
|
|
|
void update_spend(transfer_out& out, const rpc::transaction_spend& source)
|
|
{
|
|
out.amount = std::uint64_t(source.amount);
|
|
out.sender = source.sender.value_or(rpc::address_meta{});
|
|
out.tx_pub = source.tx_pub_key;
|
|
}
|
|
|
|
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))
|
|
return self.spend.sec;
|
|
|
|
// m = Hs(a || index_major || index_minor)
|
|
const crypto::secret_key m = get_subaddress_secret_key(self.view.sec, index->maj_i, index->min_i);
|
|
|
|
// D = B + M
|
|
crypto::secret_key out;
|
|
sc_add(to_bytes(out), to_bytes(m), to_bytes(self.spend.sec));
|
|
return out;
|
|
}
|
|
|
|
bool update_tx(const account& self, transaction& out, const rpc::transaction& source)
|
|
{
|
|
/* Let `receives` re-populate in `merge_output`. This works because the
|
|
server supplies all info - there is no local info to keep. */
|
|
out.receives.clear();
|
|
|
|
out.id = source.hash;
|
|
out.timestamp = source.timestamp;
|
|
out.fee = std::uint64_t(source.fee.value_or(rpc::uint64_string(0)));
|
|
out.height = source.height;
|
|
out.unlock_time = source.unlock_time;
|
|
out.payment_id = source.payment_id;
|
|
out.coinbase = source.coinbase;
|
|
|
|
std::uint64_t total_spent = 0;
|
|
for (const auto& spend : source.spent_outputs)
|
|
{
|
|
crypto::key_derivation derivation{};
|
|
if (!crypto::generate_key_derivation(spend.tx_pub_key, self.view.sec, derivation))
|
|
continue;
|
|
crypto::public_key output_pub{};
|
|
if (!crypto::derive_public_key(derivation, spend.out_index, self.spend.pub, output_pub))
|
|
continue;
|
|
crypto::secret_key output_secret{};
|
|
crypto::derive_secret_key(derivation, spend.out_index, get_spend_secret(self, spend.sender), output_secret);
|
|
|
|
crypto::key_image image{};
|
|
crypto::generate_key_image(output_pub, output_secret, image);
|
|
if (image == spend.key_image)
|
|
{
|
|
/* Frontend will typically know about spend before backend. So only
|
|
merge and never erase spends. */
|
|
update_spend(out.spends.try_emplace(image).first->second, spend);
|
|
total_spent += std::uint64_t(spend.amount);
|
|
}
|
|
}
|
|
|
|
if (!std::uint64_t(source.total_received) && !total_spent)
|
|
return false; // used as decoy
|
|
|
|
if (out.fee <= total_spent)
|
|
total_spent -= out.fee;
|
|
|
|
if (rpc::uint64_string(total_spent) < source.total_received)
|
|
{
|
|
out.direction = Monero::TransactionInfo::Direction_In;
|
|
out.amount = std::uint64_t(source.total_received) - total_spent;
|
|
}
|
|
else
|
|
{
|
|
out.direction = Monero::TransactionInfo::Direction_Out;
|
|
out.amount = total_spent - std::uint64_t(source.total_received);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void 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);
|
|
}
|
|
|
|
std::vector<std::shared_ptr<transaction>>
|
|
merge_response(wallet& self, const rpc::get_address_txs& source, const rpc::get_unspent_outs_response& unspents)
|
|
{
|
|
std::vector<std::shared_ptr<transaction>> 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
|
|
what the existing (JS) MyMonero frontend does. This has the benefit
|
|
of allowing `shared_ptr<transaction>` objects to be "given away" to
|
|
other parts of the frontend without a mutex.
|
|
|
|
ADDITIONALLY, the strong exception guarantee is provided by the
|
|
`refresh()` method; the wallet is never in a partial-state. Swapping
|
|
the transactions at the end helps with this guarantee. */
|
|
|
|
std::unordered_map<crypto::hash, std::shared_ptr<transaction>> updated_txes;
|
|
updated_txes.reserve(
|
|
std::max(self.primary.txes.size(), source.transactions.size())
|
|
);
|
|
|
|
std::unordered_multimap<crypto::key_image, std::shared_ptr<transaction>> images;
|
|
|
|
/* The frontend will know about the spend first, iff the frontend was
|
|
used to perform the spend. We copy _all_ transactions that have a
|
|
spend secret, even if the backend doesn't acknowledge it, otherwise the
|
|
secret information will be lost in many situations. If the spend
|
|
never gets confirmed, this will just sit in the transaction list. */
|
|
for (const auto& tx : self.primary.txes)
|
|
{
|
|
if (tx.second)
|
|
{
|
|
for (const auto& spend : tx.second->spends)
|
|
{
|
|
if (spend.second.secret)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const auto& tx : source.transactions)
|
|
{
|
|
auto inserted = updated_txes.try_emplace(tx.hash, nullptr);
|
|
if (inserted.second)
|
|
{
|
|
const auto existing = self.primary.txes.find(tx.hash);
|
|
if (existing != self.primary.txes.end() && existing->second)
|
|
inserted.first->second = std::make_shared<transaction>(*existing->second);
|
|
else
|
|
inserted.first->second = std::make_shared<transaction>();
|
|
|
|
if (existing == self.primary.txes.end())
|
|
out.push_back(inserted.first->second);
|
|
}
|
|
if (update_tx(self.primary, *inserted.first->second, tx))
|
|
{
|
|
for (const auto& spend : inserted.first->second->spends)
|
|
{
|
|
for (auto range = images.equal_range(spend.first); range.first != range.second; ++range.first)
|
|
range.first->second->failed = true;
|
|
}
|
|
}
|
|
else
|
|
updated_txes.erase(tx.hash);
|
|
}
|
|
|
|
for (const auto& output : unspents.outputs)
|
|
{
|
|
auto iter = updated_txes.find(output.tx_hash);
|
|
if (iter != updated_txes.end())
|
|
merge_output(iter->second, output, self.primary.view.sec);
|
|
}
|
|
|
|
// don't touch `self` until end to provide strong exception guarantee
|
|
self.primary.txes.swap(updated_txes);
|
|
|
|
self.blockchain_height = source.blockchain_height;
|
|
self.primary.restore_height = source.start_height;
|
|
self.primary.scan_height = source.scanned_block_height;
|
|
|
|
self.per_byte_fee = unspents.per_byte_fee;
|
|
self.fee_mask = unspents.fee_mask;
|
|
return out;
|
|
}
|
|
|
|
void merge_subaddrs(account& self, const epee::span<const rpc::subaddrs> subaddrs)
|
|
{
|
|
/* Purge subaccounts that do not have info (label) and are not mentioned
|
|
by the server. */
|
|
|
|
std::map<std::uint32_t, sub_account> subaccounts;
|
|
subaccounts.swap(self.subaccounts);
|
|
|
|
// Never remove special {0,0} account
|
|
self.subaccounts[0].minor[0] = std::move(subaccounts[0].minor[0]);
|
|
|
|
// Keep all subaccounts with a info
|
|
for (auto& account : subaccounts)
|
|
{
|
|
for (auto& subaccount : account.second.minor)
|
|
{
|
|
if (subaccount.second.preserve())
|
|
self.subaccounts[account.first].minor[subaccount.first] = std::move(subaccount.second);
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
} // 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_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)
|
|
{
|
|
map_account(source, dest);
|
|
dest.address = cryptonote::get_account_address_as_str(
|
|
convert_net_type(dest.type), false, cryptonote::account_public_address{dest.spend.pub, dest.view.pub}
|
|
);
|
|
}
|
|
void write_bytes(wire::writer& dest, const account& source)
|
|
{ map_account(dest, source); }
|
|
|
|
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());
|
|
if(!is_tx_spendtime_unlocked(chain_height, unlock_time, the_height, type))
|
|
return false;
|
|
|
|
if(the_height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE > chain_height)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
wallet::wallet()
|
|
: listener(nullptr),
|
|
client(),
|
|
primary{},
|
|
last_sync(0),
|
|
blockchain_height(0),
|
|
per_byte_fee(0),
|
|
fee_mask(0),
|
|
sync(),
|
|
sync_listener(),
|
|
sync_refresh()
|
|
{
|
|
primary.subaccounts[0].minor[0];
|
|
}
|
|
|
|
cryptonote::network_type wallet::get_net_type() const
|
|
{ return convert_net_type(primary.type); }
|
|
|
|
crypto::public_key wallet::get_spend_public(const rpc::address_meta& index) const
|
|
{
|
|
if (index.maj_i == 0 && index.min_i == 0)
|
|
return primary.spend.pub;
|
|
|
|
// m = Hs(a || index_major || index_minor)
|
|
crypto::secret_key m = get_subaddress_secret_key(primary.view.sec, index.maj_i, index.min_i);
|
|
|
|
// M = m*G
|
|
crypto::public_key M;
|
|
crypto::secret_key_to_public_key(m, M);
|
|
|
|
// D = B + M
|
|
return rct::rct2pk(rct::addKeys(rct::pk2rct(primary.spend.pub), rct::pk2rct(M)));
|
|
}
|
|
|
|
std::string wallet::get_spend_address(const rpc::address_meta& index) const
|
|
{
|
|
return cryptonote::get_account_address_as_str(
|
|
get_net_type(),
|
|
(index.maj_i != 0 || index.min_i != 0),
|
|
cryptonote::account_public_address{get_spend_public(index), primary.view.pub}
|
|
);
|
|
}
|
|
|
|
expect<epee::byte_slice> wallet::to_bytes() const
|
|
{
|
|
epee::byte_stream dest{};
|
|
dest.reserve(config::initial_buffer_size);
|
|
|
|
const boost::lock_guard<boost::mutex> lock{sync};
|
|
const std::error_code error = wire::msgpack::to_bytes(dest, primary);
|
|
if (error)
|
|
return error;
|
|
return epee::byte_slice{std::move(dest)};
|
|
}
|
|
|
|
std::error_code wallet::from_bytes(epee::byte_slice source)
|
|
{
|
|
/* The move call shouldn't throw an exception. So either the entire
|
|
contents of the file get loaded, or the wallet remains unchanged. */
|
|
|
|
account reload{};
|
|
std::error_code status = wire::msgpack::from_bytes(std::move(source), reload);
|
|
if (!status)
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock{sync};
|
|
primary = std::move(reload);
|
|
}
|
|
return status;
|
|
}
|
|
|
|
std::error_code wallet::login()
|
|
{
|
|
{
|
|
const rpc::login_request login{
|
|
primary.address, primary.view.sec, true, primary.generated_locally
|
|
};
|
|
|
|
const auto response = rpc::invoke<rpc::login_response>(client, login);
|
|
if (!response)
|
|
return response.error();
|
|
|
|
const boost::lock_guard<boost::mutex> lock{sync};
|
|
if (response->start_height)
|
|
primary.restore_height = *response->start_height;
|
|
}
|
|
|
|
const rpc::login login{primary.address, primary.view.sec};
|
|
const auto response = rpc::invoke<rpc::get_subaddrs>(client, login);
|
|
if (response)
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock{sync};
|
|
merge_subaddrs(primary, epee::to_span(response->all_subaddrs));
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
std::error_code wallet::refresh(const bool mandatory)
|
|
{
|
|
/* Remember that this function provides the strong exception guarantee.
|
|
The state of the wallet should always be valid and never in a partially
|
|
updated state. */
|
|
|
|
if (!mandatory)
|
|
{
|
|
const auto now = std::chrono::steady_clock::now();
|
|
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 {};
|
|
}
|
|
|
|
expect<rpc::get_unspent_outs_response> outs_response{common_error::kInvalidArgument};
|
|
|
|
boost::unique_lock<boost::mutex> lock_refresh{sync_refresh};
|
|
boost::unique_lock<boost::mutex> lock{sync};
|
|
const auto now = std::chrono::steady_clock::now();
|
|
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 {};
|
|
}
|
|
|
|
const rpc::login login{primary.address, primary.view.sec};
|
|
lock.unlock();
|
|
const auto txs_response = rpc::invoke<rpc::get_address_txs>(client, login);
|
|
if (txs_response)
|
|
{
|
|
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);
|
|
}
|
|
|
|
if (!txs_response)
|
|
return txs_response.error();
|
|
if (!outs_response)
|
|
return outs_response.error();
|
|
|
|
lock.lock();
|
|
last_sync = now.time_since_epoch().count();
|
|
const std::uint64_t orig_scan_height = primary.scan_height;
|
|
const auto new_txes = merge_response(*this, *txs_response, *outs_response);
|
|
|
|
const boost::lock_guard<boost::mutex> lock_listener{sync_listener};
|
|
if (!listener)
|
|
return {};
|
|
|
|
// Call listener functions without holding `sync`, in case a call is made
|
|
// back into the library.
|
|
const std::uint64_t new_scan_height =
|
|
std::max(orig_scan_height, primary.scan_height);
|
|
lock.unlock();
|
|
lock_refresh.unlock();
|
|
|
|
listener->refreshed();
|
|
if (!new_txes.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)
|
|
{
|
|
const auto txid = epee::string_tools::pod_to_hex(tx->id);
|
|
if (tx->direction == Monero::TransactionInfo::Direction_In)
|
|
{
|
|
if (tx->height)
|
|
listener->moneyReceived(txid, tx->amount);
|
|
else
|
|
listener->unconfirmedMoneyReceived(txid, tx->amount);
|
|
}
|
|
else
|
|
listener->moneySpent(txid, tx->amount);
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
std::error_code wallet::add_subaccount(std::string label)
|
|
{
|
|
for (unsigned i = 0; i < config::subaddr_retry; ++i)
|
|
{
|
|
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 (std::numeric_limits<std::uint32_t>::max() < next)
|
|
return {error::rpc_failure};
|
|
|
|
const rpc::provision_subaddrs_request request{
|
|
rpc::login{primary.address, primary.view.sec}, std::uint32_t(next), 0, 1, 1, true
|
|
};
|
|
const auto response = rpc::invoke<rpc::provision_subaddrs_response>(client, request);
|
|
if (!response)
|
|
return response.error();
|
|
|
|
bool matched = false;
|
|
const boost::lock_guard<boost::mutex> lock{sync};
|
|
for (const auto& major : response->new_subaddrs)
|
|
{
|
|
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 (matched)
|
|
return {};
|
|
else
|
|
merge_subaddrs(primary, epee::to_span(response->all_subaddrs));
|
|
}
|
|
return {error::rpc_failure};
|
|
}
|
|
|
|
std::error_code wallet::restore_height(const std::uint64_t height)
|
|
{
|
|
/* TODO */
|
|
throw std::logic_error{"restore_height not implemented"};
|
|
}
|
|
|
|
std::error_code wallet::send_tx(epee::byte_slice tx_bytes)
|
|
{
|
|
const rpc::submit_raw_tx_request request{std::move(tx_bytes)};
|
|
auto response = rpc::invoke<rpc::submit_raw_tx_response>(client, request);
|
|
if (!response)
|
|
return response.error();
|
|
return {};
|
|
}
|
|
}}} // lwsf // internal // backend
|