mirror of
https://codeberg.org/wownero/lwsf-wow.git
synced 2026-10-17 05:41:30 +02:00
/feed (websocket) implementation, with fallback to poll interface (#8)
This commit is contained in:
+722
-59
@@ -32,6 +32,8 @@
|
||||
#include <boost/asio/coroutine.hpp>
|
||||
#include <boost/numeric/conversion/cast.hpp>
|
||||
#include <boost/thread/lock_guard.hpp>
|
||||
#include <system_error>
|
||||
|
||||
#include "crypto/crypto.h" // monero/src
|
||||
#include "crypto/crypto-ops.h" // monero/src
|
||||
#include "cryptonote_basic/cryptonote_basic_impl.h" // monero/src
|
||||
@@ -39,6 +41,7 @@
|
||||
#include "error.h"
|
||||
#include "hex.h" // monero/src
|
||||
#include "lwsf_config.h"
|
||||
#include "net/websocket.h"
|
||||
#include "ringct/rctOps.h"
|
||||
#include "wire.h"
|
||||
#include "wire/adapted/crypto.h"
|
||||
@@ -387,6 +390,31 @@ namespace lwsf { namespace internal { namespace backend
|
||||
return self.subaccounts[sub.maj_i].last < sub.min_i; // last is inclusive
|
||||
}
|
||||
|
||||
boost::optional<crypto::public_key> is_real_spend(const account& self, const rpc::transaction_spend& source)
|
||||
{
|
||||
crypto::key_derivation derivation{};
|
||||
if (!crypto::generate_key_derivation(source.tx_pub_key, self.view.sec, derivation))
|
||||
return boost::none;
|
||||
|
||||
crypto::public_key spend_pub{};
|
||||
const crypto::secret_key spend_sec = get_spend_secret(self, source.sender);
|
||||
if (!crypto::secret_key_to_public_key(spend_sec, spend_pub))
|
||||
return boost::none;
|
||||
|
||||
crypto::public_key output_pub{};
|
||||
if (!crypto::derive_public_key(derivation, source.out_index, spend_pub, output_pub))
|
||||
return boost::none;
|
||||
|
||||
crypto::secret_key output_secret{};
|
||||
crypto::derive_secret_key(derivation, source.out_index, spend_sec, output_secret);
|
||||
|
||||
crypto::key_image image{};
|
||||
crypto::generate_key_image(output_pub, output_secret, image);
|
||||
if (source.key_image == image)
|
||||
return output_pub;
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
boost::optional<std::vector<rpc::address_meta>> update_tx(const account& self, transaction& out, const rpc::transaction& source)
|
||||
{
|
||||
/* Let `receives` and `spends` re-populate in `merge_output` and
|
||||
@@ -412,29 +440,12 @@ namespace lwsf { namespace internal { namespace backend
|
||||
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 spend_pub{};
|
||||
const crypto::secret_key spend_sec = get_spend_secret(self, spend.sender);
|
||||
if (!crypto::secret_key_to_public_key(spend_sec, spend_pub))
|
||||
continue;
|
||||
|
||||
crypto::public_key output_pub{};
|
||||
if (!crypto::derive_public_key(derivation, spend.out_index, spend_pub, output_pub))
|
||||
continue;
|
||||
|
||||
crypto::secret_key output_secret{};
|
||||
crypto::derive_secret_key(derivation, spend.out_index, spend_sec, output_secret);
|
||||
|
||||
crypto::key_image image{};
|
||||
crypto::generate_key_image(output_pub, output_secret, image);
|
||||
if (image == spend.key_image)
|
||||
const auto output_pub = is_real_spend(self, spend);
|
||||
if (output_pub)
|
||||
{
|
||||
/* Frontend will typically know about spend before backend. So only
|
||||
merge and never erase spends. */
|
||||
const rpc::address_meta sub = update_spend(out.spends.try_emplace(image).first->second, spend, output_pub);
|
||||
const rpc::address_meta sub = update_spend(out.spends.try_emplace(spend.key_image).first->second, spend, *output_pub);
|
||||
if (need_expansion(self, sub))
|
||||
meta.push_back(sub);
|
||||
total_spent += std::uint64_t(spend.amount);
|
||||
@@ -517,12 +528,9 @@ namespace lwsf { namespace internal { namespace backend
|
||||
{
|
||||
const bool rescanning =
|
||||
source.scanned_block_height < tx.second->height.value_or(0);
|
||||
if (rescanning || !tx.second->description.empty() || !tx.second->transfers.empty())
|
||||
if (rescanning || tx.second->has_metadata())
|
||||
{
|
||||
const auto iter = updated_txes.emplace_hint(
|
||||
updated_txes.end(), tx.first, nullptr
|
||||
);
|
||||
|
||||
const auto iter = updated_txes.emplace(tx.first, nullptr).first;
|
||||
if (!iter->second)
|
||||
{
|
||||
iter->second = std::make_shared<transaction>(*tx.second);
|
||||
@@ -610,15 +618,7 @@ namespace lwsf { namespace internal { namespace backend
|
||||
if (self.primary.restore_height <= self.primary.requested_start && !source.lookahead_fail)
|
||||
self.import_error = {};
|
||||
|
||||
self.fee_mask = unspents.fee_mask;
|
||||
if (unspents.fees.empty())
|
||||
{
|
||||
self.per_byte_fee.resize(1);
|
||||
self.per_byte_fee[0] = unspents.per_byte_fee;
|
||||
}
|
||||
else
|
||||
self.per_byte_fee = std::move(unspents.fees);
|
||||
|
||||
self.update_fees(unspents);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -709,6 +709,447 @@ namespace lwsf { namespace internal { namespace backend
|
||||
runner();
|
||||
}
|
||||
|
||||
void notify_listener(Monero::WalletListener& listener, const std::uint64_t new_height, const std::uint64_t orig_height, const merge_results& merged)
|
||||
{
|
||||
listener.refreshed();
|
||||
if (!merged.new_transactions.empty() || new_height - orig_height)
|
||||
listener.updated();
|
||||
|
||||
for (std::uint64_t i = orig_height; i < new_height; ++i)
|
||||
listener.newBlock(i);
|
||||
|
||||
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)
|
||||
{
|
||||
if (tx->height)
|
||||
listener.moneyReceived(txid, tx->amount);
|
||||
else
|
||||
listener.unconfirmedMoneyReceived(txid, tx->amount);
|
||||
}
|
||||
else
|
||||
listener.moneySpent(txid, tx->amount);
|
||||
}
|
||||
}
|
||||
|
||||
class feed_loop
|
||||
{
|
||||
struct frame
|
||||
{
|
||||
std::weak_ptr<wallet> self;
|
||||
std::weak_ptr<websocket::stream> ws;
|
||||
merge_results* merged;
|
||||
std::function<void(std::error_code)> on_sync;
|
||||
|
||||
explicit frame(std::shared_ptr<wallet> in, merge_results* merged, std::function<void(std::error_code)>&& on_sync)
|
||||
: self(in), ws(in->feed), merged(merged), on_sync(std::move(on_sync))
|
||||
{}
|
||||
};
|
||||
|
||||
std::shared_ptr<frame> frame_;
|
||||
|
||||
/* Convert the simplified '/feed:tx_sync:' message to a split
|
||||
'/get_address_txs' and '/get_unspent_outs' format. This way both legacy
|
||||
and new syncing mechanisms use the same code. This slows down the new
|
||||
sync, but keeps the legacy sync fast, which occurs more frequently. */
|
||||
static expect<std::pair<rpc::get_address_txs, rpc::get_unspent_outs_response>> convert_to_legacy(rpc::feed_tx_sync src)
|
||||
{
|
||||
auto out = std::make_pair(rpc::get_address_txs{}, rpc::get_unspent_outs_response{});
|
||||
|
||||
out.first.lookahead_fail = src.lookahead_fail;
|
||||
out.first.scanned_block_height = src.scanned_block_height;
|
||||
out.first.start_height = src.start_height;
|
||||
out.first.blockchain_height = src.blockchain_height;
|
||||
out.first.lookahead = src.lookahead;
|
||||
|
||||
std::sort(
|
||||
src.transactions.begin(), src.transactions.end(),
|
||||
[] (const auto& lhs, const auto& rhs)
|
||||
{
|
||||
static_assert(std::is_integral<decltype(lhs.height)>(), "optional types sort incorrectly");
|
||||
return lhs.height < rhs.height;
|
||||
}
|
||||
);
|
||||
|
||||
std::map<rpc::feed_tx::output_id, rpc::feed_tx::receive> receives;
|
||||
for (const auto& orig : src.transactions)
|
||||
{
|
||||
for (const auto& receive : orig.receives)
|
||||
{
|
||||
receives.emplace(receive.id, receive);
|
||||
out.second.outputs.emplace_back(receive, orig);
|
||||
}
|
||||
|
||||
auto& tx = out.first.transactions.emplace_back(orig);
|
||||
for (const auto& orig_spend : orig.spends)
|
||||
{
|
||||
const auto receive = receives.find(orig_spend.id);
|
||||
if (receive == receives.end())
|
||||
return {error::feed_missing_output};
|
||||
tx.spent_outputs.emplace_back(orig_spend, receive->second);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
void on_error(wallet& self, boost::unique_lock<boost::mutex>& lock, const std::error_code error)
|
||||
{
|
||||
std::shared_ptr<websocket::stream> feed;
|
||||
std::function<void(std::error_code)> on_sync;
|
||||
|
||||
LWSF_VERIFY(lock.owns_lock());
|
||||
const std::function<void(std::error_code)> on_refresh{self.on_feed_refresh};
|
||||
feed.swap(self.feed);
|
||||
on_sync.swap(frame_->on_sync);
|
||||
|
||||
{
|
||||
const boost::lock_guard<boost::mutex> lock_listener{self.sync_listener};
|
||||
lock.unlock();
|
||||
if (self.listener)
|
||||
self.listener->refreshed();
|
||||
}
|
||||
|
||||
MERROR("Error on websocket '/feed': " << error.message());
|
||||
if (feed)
|
||||
websocket::async_close(std::move(feed));
|
||||
|
||||
if (on_sync)
|
||||
on_sync(self.refresh_error = error);
|
||||
else
|
||||
backend::wallet::refresh(frame_->self.lock(), true, [] (auto) {});
|
||||
|
||||
if (on_refresh)
|
||||
on_refresh(error);
|
||||
}
|
||||
|
||||
static std::string_view get_prefix(const epee::byte_slice& msg) noexcept
|
||||
{
|
||||
void const* const sep = std::memchr(msg.data(), u8':', msg.size());
|
||||
if (!sep)
|
||||
return {};
|
||||
return {
|
||||
reinterpret_cast<const char*>(msg.data()),
|
||||
std::size_t(reinterpret_cast<const std::uint8_t*>(sep) - msg.data() + 1)
|
||||
};
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static expect<T> unpack(epee::byte_slice&& msg)
|
||||
{
|
||||
const std::string_view prefix = get_prefix(msg);
|
||||
if (prefix != T::prefix())
|
||||
return {error::bad_feed_prefix};
|
||||
msg.remove_prefix(prefix.size());
|
||||
|
||||
T out{};
|
||||
const std::error_code error = wire::msgpack::from_bytes(std::move(msg), out);
|
||||
if (error)
|
||||
return error;
|
||||
return out;
|
||||
}
|
||||
|
||||
bool process(wallet& self, expect<rpc::feed_blocks>&& msg, boost::unique_lock<boost::mutex>& lock, merge_results& merged)
|
||||
{
|
||||
if (!msg)
|
||||
{
|
||||
on_error(self, lock, msg.error());
|
||||
return false;
|
||||
}
|
||||
|
||||
LWSF_VERIFY(lock.owns_lock());
|
||||
std::map<rpc::feed_tx::output_id, std::shared_ptr<transaction>> receives;
|
||||
std::unordered_map<crypto::hash, std::shared_ptr<transaction>> updated;
|
||||
std::unordered_multimap<crypto::key_image, std::shared_ptr<transaction>> images;
|
||||
updated.reserve(self.primary.txes.size() + msg->transactions.size());
|
||||
for (const auto& tx : self.primary.txes)
|
||||
{
|
||||
auto elem = updated.try_emplace(tx.first, std::make_shared<transaction>(*tx.second));
|
||||
for (const auto& receive : elem.first->second->receives)
|
||||
receives.emplace(receive.second.get_output_id(), elem.first->second);
|
||||
for (const auto& spend : elem.first->second->spends)
|
||||
images.emplace(spend.first, elem.first->second);
|
||||
}
|
||||
|
||||
for (const auto& new_tx : msg->transactions)
|
||||
{
|
||||
auto tx = updated.try_emplace(new_tx.hash, nullptr).first;
|
||||
const bool is_new = !tx->second;
|
||||
tx->second = std::make_shared<transaction>(new_tx, tx->second.get());
|
||||
if (is_new)
|
||||
merged.new_transactions.push_back(tx->second);
|
||||
|
||||
std::uint64_t receiving = 0;
|
||||
for (const auto& receive : new_tx.receives)
|
||||
{
|
||||
receiving += receive.amount;
|
||||
receives.emplace(receive.id, tx->second);
|
||||
if (need_expansion(self.primary, receive.recipient))
|
||||
merged.merge_subaddr(receive.recipient);
|
||||
}
|
||||
|
||||
std::uint64_t spending = 0;
|
||||
for (const auto& spend : new_tx.spends)
|
||||
{
|
||||
const auto receive = receives.find(spend.id);
|
||||
if (receive == receives.end())
|
||||
{
|
||||
on_error(self, lock, error::feed_missing_output);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::pair<crypto::public_key, transfer_in> const* output = nullptr;
|
||||
for (const auto& existing : receive->second->receives)
|
||||
{
|
||||
if (existing.second.get_output_id() == spend.id)
|
||||
{
|
||||
output = std::addressof(existing);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (output == nullptr)
|
||||
{
|
||||
on_error(self, lock, error::feed_missing_output);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_real_spend(self.primary, {spend, output->second.as_receive(output->first)}))
|
||||
{
|
||||
spending += output->second.amount;
|
||||
tx->second->spends.try_emplace(spend.key_image, output->second, output->first);
|
||||
auto failed = images.equal_range(spend.key_image);
|
||||
for ( ; failed.first != failed.second; ++failed.first)
|
||||
failed.first->second->failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (receiving || spending)
|
||||
{
|
||||
if (new_tx.fee <= spending)
|
||||
spending -= new_tx.fee;
|
||||
|
||||
if (tx->second->amount <= spending)
|
||||
{
|
||||
tx->second->direction = Monero::TransactionInfo::Direction_Out;
|
||||
tx->second->amount = spending - tx->second->amount;
|
||||
}
|
||||
else
|
||||
{
|
||||
tx->second->direction = Monero::TransactionInfo::Direction_In;
|
||||
tx->second->amount = tx->second->amount - spending;
|
||||
}
|
||||
}
|
||||
else
|
||||
updated.erase(new_tx.hash); // used solely as decoy in this tx
|
||||
}
|
||||
|
||||
self.primary.txes.swap(updated);
|
||||
merged.lookahead_fail = msg->lookahead_fail;
|
||||
self.server_lookahead = {msg->lookahead.maj_i, msg->lookahead.min_i};
|
||||
self.blockchain_height = std::max(self.blockchain_height, msg->blockchain_height);
|
||||
self.primary.scan_height = std::max(self.primary.scan_height, msg->scan_end);
|
||||
return true;
|
||||
}
|
||||
|
||||
void process(wallet& self, expect<rpc::feed_error>&& msg, boost::unique_lock<boost::mutex>& lock)
|
||||
{
|
||||
if (!msg)
|
||||
return on_error(self, lock, msg.error());
|
||||
MERROR("Received websocket '/feed' error from server: " << msg->msg);
|
||||
on_error(self, lock, error::feed);
|
||||
}
|
||||
|
||||
bool process(wallet& self, expect<rpc::feed_mempool>&& msg, boost::unique_lock<boost::mutex>& lock, merge_results& merged)
|
||||
{
|
||||
if (!msg)
|
||||
{
|
||||
on_error(self, lock, msg.error());
|
||||
return false;
|
||||
}
|
||||
|
||||
LWSF_VERIFY(lock.owns_lock());
|
||||
std::unordered_map<crypto::hash, std::shared_ptr<transaction>> updated;
|
||||
for (const auto& tx : self.primary.txes)
|
||||
updated.emplace(tx.first, std::make_shared<transaction>(*tx.second));
|
||||
|
||||
auto tx = updated.emplace(msg->hash, nullptr).first;
|
||||
if (!tx->second)
|
||||
{
|
||||
tx->second = std::make_shared<transaction>(*msg);
|
||||
merged.new_transactions.push_back(tx->second);
|
||||
if (need_expansion(self.primary, msg->recipient))
|
||||
merged.merge_subaddr(msg->recipient);
|
||||
|
||||
auto receive = tx->second->receives.try_emplace(msg->public_key);
|
||||
if (receive.second)
|
||||
{
|
||||
tx->second->amount += msg->amount;
|
||||
const auto sub = update_output(receive.first->second, *msg, self.primary.view.sec);
|
||||
if (need_expansion(self.primary, sub))
|
||||
merged.merge_subaddr(sub);
|
||||
}
|
||||
} // else we saw it because we spent it
|
||||
|
||||
self.primary.txes.swap(updated);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool process(wallet& self, expect<rpc::feed_warning>&& msg, boost::unique_lock<boost::mutex>& lock)
|
||||
{
|
||||
if (!msg)
|
||||
{
|
||||
on_error(self, lock, msg.error());
|
||||
return false;
|
||||
}
|
||||
|
||||
MDEBUG("Received websocket '/feed' warning from server: " << msg->msg);
|
||||
if (msg->code != rpc::feed_status::blockchain_reorg)
|
||||
return false; // skip notifying wallet
|
||||
|
||||
LWSF_VERIFY(lock.owns_lock());
|
||||
std::unordered_map<crypto::hash, std::shared_ptr<transaction>> updated;
|
||||
for (const auto& tx : self.primary.txes)
|
||||
{
|
||||
LWSF_VERIFY(tx.second);
|
||||
const bool by_height =
|
||||
tx.second->height.value_or(std::numeric_limits<std::uint64_t>::max()) <= msg->height;
|
||||
if (by_height || tx.second->has_metadata())
|
||||
{
|
||||
auto fresh = updated.emplace(tx.first, std::make_shared<transaction>(*tx.second));
|
||||
if (!by_height)
|
||||
{
|
||||
fresh.first->second->timestamp = boost::none;
|
||||
fresh.first->second->height = boost::none;
|
||||
fresh.first->second->failed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.primary.txes.swap(updated);
|
||||
self.blockchain_height = std::min(self.blockchain_height, msg->height);
|
||||
self.primary.restore_height = std::min(self.primary.restore_height, msg->height);
|
||||
self.primary.scan_height = std::min(self.primary.scan_height, msg->height);
|
||||
return true;
|
||||
}
|
||||
|
||||
public:
|
||||
explicit feed_loop(std::shared_ptr<wallet> self, merge_results* merged, std::function<void(std::error_code)> on_sync)
|
||||
: frame_(std::make_shared<frame>(std::move(self), merged, std::move(on_sync)))
|
||||
{}
|
||||
|
||||
void operator()(std::error_code error = {}, copyable_slice incoming = {})
|
||||
{
|
||||
LWSF_VERIFY(frame_);
|
||||
const auto self_ptr = frame_->self.lock();
|
||||
LWSF_VERIFY(self_ptr);
|
||||
wallet& self = *self_ptr;
|
||||
|
||||
boost::unique_lock<boost::mutex> lock{self.sync};
|
||||
auto ws = frame_->ws.lock();
|
||||
if (ws.get() != self.feed.get()) // sketchy, relies on make_shared not freeing until weak_ptr closes
|
||||
{
|
||||
LWSF_VERIFY(!frame_->on_sync); // violation in `wallet::refresh`
|
||||
if (ws)
|
||||
websocket::async_close(std::move(ws));
|
||||
return;
|
||||
}
|
||||
|
||||
if (error)
|
||||
return on_error(self, lock, error);
|
||||
|
||||
self.last_sync = std::chrono::steady_clock::now();
|
||||
if (frame_->merged)
|
||||
{
|
||||
auto msg = unpack<rpc::feed_tx_sync>(std::move(incoming.data));
|
||||
if (!msg)
|
||||
return on_error(self, lock, msg.error());
|
||||
|
||||
const auto split = convert_to_legacy(std::move(*msg));
|
||||
if (!split)
|
||||
return on_error(self, lock, split.error());
|
||||
|
||||
const std::uint64_t orig_height = self.primary.scan_height;
|
||||
msg = {error::feed}; // release memory
|
||||
*frame_->merged = merge_response(self, split->first, split->second);
|
||||
|
||||
std::function<void(std::error_code)> on_sync;
|
||||
on_sync.swap(frame_->on_sync);
|
||||
const std::function<void(std::error_code)> on_refresh{self.on_feed_refresh};
|
||||
|
||||
const std::uint64_t new_height = std::max(self.primary.scan_height, orig_height);
|
||||
const boost::lock_guard<boost::mutex> listener_lock{self.sync_listener};
|
||||
lock.unlock();
|
||||
|
||||
if (self.listener)
|
||||
notify_listener(*self.listener, new_height, orig_height, *frame_->merged);
|
||||
|
||||
frame_->merged = nullptr;
|
||||
if (on_sync)
|
||||
on_sync({});
|
||||
if (on_refresh)
|
||||
on_refresh({});
|
||||
}
|
||||
else
|
||||
{
|
||||
if (incoming.data.empty())
|
||||
return on_error(self, lock, error::bad_feed_prefix);
|
||||
|
||||
merge_results merged{};
|
||||
const std::uint64_t orig_height = self.primary.scan_height;
|
||||
switch (incoming.data.data()[0])
|
||||
{
|
||||
default:
|
||||
return on_error(self, lock, error::bad_feed_prefix);
|
||||
case 'b':
|
||||
if (process(self, unpack<rpc::feed_blocks>(std::move(incoming.data)), lock, merged))
|
||||
break;
|
||||
return;
|
||||
case 'e':
|
||||
return process(self, unpack<rpc::feed_error>(std::move(incoming.data)), lock);
|
||||
case 'm':
|
||||
if (process(self, unpack<rpc::feed_mempool>(std::move(incoming.data)), lock, merged))
|
||||
break;
|
||||
return;
|
||||
case 'w':
|
||||
if (process(self, unpack<rpc::feed_warning>(std::move(incoming.data)), lock))
|
||||
break;
|
||||
return;
|
||||
}
|
||||
|
||||
const boost::lock_guard<boost::mutex> listener_lock{self.sync_listener};
|
||||
LWSF_VERIFY(lock.owns_lock());
|
||||
if (!self.listener && !self.on_feed_refresh)
|
||||
return;
|
||||
|
||||
const std::function<void(std::error_code)> on_refresh{self.on_feed_refresh};
|
||||
const std::uint64_t new_height = self.primary.scan_height;
|
||||
|
||||
lock.unlock();
|
||||
if (self.listener)
|
||||
notify_listener(*self.listener, new_height, orig_height, merged);
|
||||
if (on_refresh)
|
||||
on_refresh({});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
epee::byte_slice generate_feed_msg(const T& msg)
|
||||
{
|
||||
epee::byte_stream sink;
|
||||
|
||||
const std::string_view prefix = T::prefix();
|
||||
sink.write(prefix.data(), prefix.size());
|
||||
|
||||
const std::error_code error = wire::msgpack::to_bytes(sink, msg);
|
||||
if (error)
|
||||
throw std::system_error{error, std::string{"Failed to create "} + T::prefix()};
|
||||
|
||||
return epee::byte_slice{std::move(sink)};
|
||||
}
|
||||
|
||||
//! Bind N arguments into a callback that takes zero arguments to run
|
||||
template<typename F, typename... T>
|
||||
struct binder
|
||||
@@ -733,14 +1174,14 @@ namespace lwsf { namespace internal { namespace backend
|
||||
F f_;
|
||||
|
||||
template<typename... T>
|
||||
void operator()(T... args)
|
||||
void operator()(T... args) const
|
||||
{
|
||||
// Use `post` instead of `dispatch` due to locking in handlers
|
||||
const auto self = self_.lock();
|
||||
LWSF_VERIFY(self);
|
||||
boost::asio::post(
|
||||
self->strand,
|
||||
binder<F, T...>{self, std::move(f_), std::tuple<T...>(std::move(args)...)}
|
||||
binder<F, T...>{self, f_, std::tuple<T...>(std::move(args)...)}
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -772,6 +1213,24 @@ namespace lwsf { namespace internal { namespace backend
|
||||
LWSF_VERIFY(self);
|
||||
boost::asio::post(self->strand, binder<F>{self, std::move(f)});
|
||||
}
|
||||
|
||||
template<typename T, typename F>
|
||||
struct store_
|
||||
{
|
||||
T* out;
|
||||
F f;
|
||||
|
||||
void operator()(const std::error_code error, T val)
|
||||
{
|
||||
LWSF_VERIFY(out);
|
||||
*out = val;
|
||||
f(error);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T, typename F>
|
||||
store_<T, F> store(T* out, F f)
|
||||
{ return {out, std::move(f)}; }
|
||||
} // anonymous
|
||||
|
||||
WIRE_DEFINE_OBJECT(address_book_entry, map_address_book_entry);
|
||||
@@ -904,6 +1363,64 @@ namespace lwsf { namespace internal { namespace backend
|
||||
failed(false)
|
||||
{}
|
||||
|
||||
transaction::transaction(const rpc::feed_tx& src, const transaction* existing)
|
||||
: raw_bytes(),
|
||||
spends(),
|
||||
receives(),
|
||||
transfers(),
|
||||
description(),
|
||||
timestamp(src.timestamp),
|
||||
height(src.get_height()),
|
||||
amount(0),
|
||||
fee(src.fee),
|
||||
unlock_time(src.unlock_time),
|
||||
direction(Monero::TransactionInfo::Direction_In),
|
||||
payment_id(src.payment_id),
|
||||
id(src.hash),
|
||||
prefix(src.prefix_hash),
|
||||
coinbase(src.coinbase),
|
||||
failed(false)
|
||||
{
|
||||
for (const auto& receive : src.receives)
|
||||
{
|
||||
amount += receive.amount;
|
||||
receives.try_emplace(receive.public_key, receive);
|
||||
}
|
||||
|
||||
if (existing)
|
||||
{
|
||||
transfers = existing->transfers;
|
||||
description = existing->description;
|
||||
}
|
||||
}
|
||||
|
||||
transaction::transaction(const rpc::feed_mempool& src)
|
||||
: raw_bytes(),
|
||||
spends(),
|
||||
receives(),
|
||||
transfers(),
|
||||
description(),
|
||||
timestamp(std::chrono::system_clock::now()),
|
||||
height(),
|
||||
amount(src.amount),
|
||||
fee(src.fee),
|
||||
unlock_time(src.unlock_time),
|
||||
direction(Monero::TransactionInfo::Direction_In),
|
||||
payment_id(src.payment_id),
|
||||
id(src.hash),
|
||||
prefix(src.prefix_hash),
|
||||
coinbase(false),
|
||||
failed(false)
|
||||
{
|
||||
auto elem = receives.try_emplace(src.public_key).first;
|
||||
elem->second.global_index = std::numeric_limits<std::uint64_t>::max();
|
||||
elem->second.amount = src.amount;
|
||||
elem->second.recipient = src.recipient;
|
||||
elem->second.index = src.index;
|
||||
elem->second.rct_mask = src.rct;
|
||||
elem->second.tx_pub = src.tx_pub_key;
|
||||
}
|
||||
|
||||
transaction::transaction(const transaction& rhs)
|
||||
: raw_bytes(rhs.raw_bytes.clone()), // required because of this
|
||||
spends(rhs.spends),
|
||||
@@ -936,8 +1453,11 @@ namespace lwsf { namespace internal { namespace backend
|
||||
|
||||
wallet::wallet(boost::asio::io_context& io)
|
||||
: listener(nullptr),
|
||||
feed(nullptr),
|
||||
strand(io),
|
||||
client{},
|
||||
on_feed_refresh(nullptr),
|
||||
on_feed_fail(nullptr),
|
||||
primary{},
|
||||
per_byte_fee(),
|
||||
login_queue(),
|
||||
@@ -946,13 +1466,16 @@ namespace lwsf { namespace internal { namespace backend
|
||||
lookahead_error(default_subaddr_state),
|
||||
import_error(),
|
||||
last_sync(),
|
||||
last_fees(),
|
||||
blockchain_height(0),
|
||||
fee_mask(0),
|
||||
server_lookahead{},
|
||||
sync(),
|
||||
sync_listener(),
|
||||
sync_queue(),
|
||||
passed_login(false)
|
||||
passed_login(false),
|
||||
probed_lookahead(false),
|
||||
feed_disabled(false)
|
||||
{
|
||||
prep_primary_account(primary.subaccounts.emplace_back());
|
||||
}
|
||||
@@ -963,6 +1486,11 @@ namespace lwsf { namespace internal { namespace backend
|
||||
{
|
||||
client.shutdown();
|
||||
|
||||
const boost::lock_guard<boost::mutex> lock1{sync};
|
||||
feed = nullptr;
|
||||
on_feed_refresh = nullptr;
|
||||
on_feed_fail = nullptr;
|
||||
|
||||
const boost::lock_guard<boost::mutex> lock{sync_queue};
|
||||
login_queue.clear();
|
||||
refresh_queue.clear();
|
||||
@@ -1017,6 +1545,24 @@ namespace lwsf { namespace internal { namespace backend
|
||||
primary.lookahead.minor <= server_lookahead.minor;
|
||||
}
|
||||
|
||||
bool wallet::need_fees() const noexcept
|
||||
{
|
||||
return config::refresh_interval_min <= std::chrono::steady_clock::now() - last_fees;
|
||||
}
|
||||
|
||||
void wallet::update_fees(const rpc::get_unspent_outs_response& unspents)
|
||||
{
|
||||
last_fees = std::chrono::steady_clock::now();
|
||||
fee_mask = unspents.fee_mask;
|
||||
if (unspents.fees.empty())
|
||||
{
|
||||
per_byte_fee.resize(1);
|
||||
per_byte_fee[0] = unspents.per_byte_fee;
|
||||
}
|
||||
else
|
||||
per_byte_fee = unspents.fees;
|
||||
}
|
||||
|
||||
expect<epee::byte_slice> wallet::to_bytes() const
|
||||
{
|
||||
epee::byte_stream dest{};
|
||||
@@ -1045,6 +1591,26 @@ namespace lwsf { namespace internal { namespace backend
|
||||
return status;
|
||||
}
|
||||
|
||||
void wallet::add_pending_tx(std::shared_ptr<transaction> pending)
|
||||
{
|
||||
const auto txid = epee::string_tools::pod_to_hex(pending->id);
|
||||
const std::uint64_t amount = pending->amount;
|
||||
|
||||
{
|
||||
const boost::lock_guard<boost::mutex> lock{sync};
|
||||
auto entry = primary.txes.try_emplace(pending->id).first;
|
||||
if (!entry->second)
|
||||
entry->second = std::move(pending);
|
||||
}
|
||||
const boost::lock_guard<boost::mutex> lock{sync_listener};
|
||||
if (!listener)
|
||||
return;
|
||||
|
||||
listener->refreshed();
|
||||
listener->updated();
|
||||
listener->moneySpent(txid, amount);
|
||||
}
|
||||
|
||||
void wallet::login_is_new(std::shared_ptr<wallet> self, std::function<void(expect<bool>)> f)
|
||||
{
|
||||
struct frame
|
||||
@@ -1095,6 +1661,10 @@ namespace lwsf { namespace internal { namespace backend
|
||||
self.subaddress_error = {};
|
||||
self.import_error = {};
|
||||
self.lookahead_error = self.lookahead_good() ? std::error_code{} : error::subaddr_upgrade;
|
||||
if (self.feed)
|
||||
websocket::async_close(std::move(self.feed));
|
||||
self.feed = nullptr;
|
||||
self.feed_disabled = false;
|
||||
|
||||
frame_->login = rpc::login_request{
|
||||
self.primary.address, self.primary.view.sec, self.primary.lookahead, true, self.primary.generated_locally
|
||||
@@ -1179,6 +1749,7 @@ namespace lwsf { namespace internal { namespace backend
|
||||
rpc::get_subaddrs subaddrs;
|
||||
merge_results merged;
|
||||
std::uint64_t orig_scan_height;
|
||||
rpc::feed feed;
|
||||
const bool mandatory;
|
||||
|
||||
explicit frame(std::shared_ptr<wallet> in, const bool mandatory)
|
||||
@@ -1190,6 +1761,7 @@ namespace lwsf { namespace internal { namespace backend
|
||||
subaddrs{},
|
||||
merged{},
|
||||
orig_scan_height(0),
|
||||
feed{},
|
||||
mandatory(mandatory)
|
||||
{}
|
||||
|
||||
@@ -1244,6 +1816,57 @@ namespace lwsf { namespace internal { namespace backend
|
||||
return self.run(self.refresh_queue, self.refresh_error = error);
|
||||
}
|
||||
|
||||
if (!self.feed_disabled)
|
||||
{
|
||||
if (!self.feed)
|
||||
{
|
||||
BOOST_ASIO_CORO_YIELD rpc::invoke_async(
|
||||
self.client, rpc::empty{}, &frame_->feed, wrap(self_ptr, *this)
|
||||
);
|
||||
if (error == http::error(501))
|
||||
{
|
||||
ws_connect:
|
||||
BOOST_ASIO_CORO_YIELD self.client.ws_async(
|
||||
rpc::feed::endpoint(), rpc::feed::protocol(), store(&self.feed, wrap(self_ptr, *this))
|
||||
);
|
||||
|
||||
if (!error && self.feed)
|
||||
{
|
||||
// must hold lock to write `self.feed`, thus the frame
|
||||
BOOST_ASIO_CORO_YIELD websocket::async_subscribe(
|
||||
self.feed,
|
||||
generate_feed_msg(rpc::feed_login{rpc::login{self.primary.address, self.primary.view.sec}}),
|
||||
wrap(self_ptr, feed_loop{self_ptr, &frame_->merged, wrap(self_ptr, *this)})
|
||||
);
|
||||
if (!error)
|
||||
goto txs_merged;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.feed)
|
||||
websocket::async_close(std::move(self.feed));
|
||||
self.feed = nullptr;
|
||||
self.feed_disabled = true;
|
||||
{
|
||||
std::function<void(std::error_code)> on_feed_fail;
|
||||
std::function<void(std::error_code)> on_feed_refresh;
|
||||
on_feed_fail.swap(self.on_feed_fail);
|
||||
on_feed_refresh.swap(self.on_feed_refresh);
|
||||
if (on_feed_fail)
|
||||
on_feed_fail(error);
|
||||
}
|
||||
}
|
||||
else // existing websocket stream to '/feed'
|
||||
{
|
||||
// ping should be quick, server doesn't hit disk, etc
|
||||
BOOST_ASIO_CORO_YIELD websocket::async_ping(self.feed, wrap(self_ptr, *this));
|
||||
if (!error)
|
||||
return self.run(self.refresh_queue, self.refresh_error);
|
||||
self.feed = nullptr;
|
||||
goto ws_connect;
|
||||
}
|
||||
}
|
||||
|
||||
frame_->orig_scan_height = self.primary.scan_height;
|
||||
frame_->login = rpc::login{self.primary.address, self.primary.view.sec};
|
||||
BOOST_ASIO_CORO_YIELD rpc::invoke_async(
|
||||
@@ -1273,7 +1896,7 @@ namespace lwsf { namespace internal { namespace backend
|
||||
|
||||
// Remember that this function provides the strong exception guarantee.
|
||||
frame_->merged = merge_response(self, frame_->txs_response, frame_->outs_response);
|
||||
|
||||
txs_merged:
|
||||
if (!self.import_error && self.primary.requested_start < self.primary.restore_height)
|
||||
{
|
||||
BOOST_ASIO_CORO_YIELD restore_height(
|
||||
@@ -1342,29 +1965,10 @@ namespace lwsf { namespace internal { namespace backend
|
||||
std::max(frame_->orig_scan_height, self.primary.scan_height);
|
||||
lock.unlock();
|
||||
|
||||
self.listener->refreshed();
|
||||
const auto& merged = frame_->merged;
|
||||
if (!merged.new_transactions.empty() || new_scan_height - frame_->orig_scan_height)
|
||||
self.listener->updated();
|
||||
|
||||
for (std::uint64_t i = frame_->orig_scan_height; i < new_scan_height; ++i)
|
||||
self.listener->newBlock(i);
|
||||
|
||||
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)
|
||||
{
|
||||
if (tx->height)
|
||||
self.listener->moneyReceived(txid, tx->amount);
|
||||
else
|
||||
self.listener->unconfirmedMoneyReceived(txid, tx->amount);
|
||||
}
|
||||
else
|
||||
self.listener->moneySpent(txid, tx->amount);
|
||||
}
|
||||
notify_listener(*self.listener, new_scan_height, frame_->orig_scan_height, frame_->merged);
|
||||
|
||||
self.run(self.refresh_queue, rc);
|
||||
frame_->self.reset(); // prevent re-invoking listener
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1374,6 +1978,65 @@ namespace lwsf { namespace internal { namespace backend
|
||||
post(self, handler{std::make_shared<frame>(self, mandatory)});
|
||||
}
|
||||
|
||||
void wallet::get_fees(std::shared_ptr<wallet> self, std::function<void(std::error_code)> f)
|
||||
{
|
||||
struct frame
|
||||
{
|
||||
std::weak_ptr<wallet> self;
|
||||
rpc::get_unspent_outs_response response;
|
||||
const std::function<void(std::error_code)> f;
|
||||
|
||||
explicit frame(std::shared_ptr<wallet> in, std::function<void(std::error_code)>&& f)
|
||||
: self(std::move(in)),
|
||||
response{},
|
||||
f(std::move(f))
|
||||
{}
|
||||
};
|
||||
|
||||
struct handler : boost::asio::coroutine
|
||||
{
|
||||
std::shared_ptr<frame> frame_;
|
||||
|
||||
explicit handler(std::shared_ptr<frame> in)
|
||||
: boost::asio::coroutine(), frame_(std::move(in))
|
||||
{}
|
||||
|
||||
void operator()(const std::error_code error = {})
|
||||
{
|
||||
LWSF_VERIFY(frame_ && frame_->f);
|
||||
if (error)
|
||||
return frame_->f(error);
|
||||
|
||||
const auto self_ptr = frame_->self.lock();
|
||||
LWSF_VERIFY(self_ptr);
|
||||
wallet& self = *self_ptr;
|
||||
assert(self.strand.running_in_this_thread());
|
||||
const boost::lock_guard<boost::mutex> lock{self.sync};
|
||||
BOOST_ASIO_CORO_REENTER(*this)
|
||||
{
|
||||
if (!self.need_fees())
|
||||
return frame_->f(error);
|
||||
|
||||
if (!self.passed_login)
|
||||
BOOST_ASIO_CORO_YIELD login(self_ptr, wrap(self_ptr, *this));
|
||||
|
||||
BOOST_ASIO_CORO_YIELD rpc::invoke_async(
|
||||
self.client,
|
||||
rpc::get_unspent_outs_request{rpc::login{self.primary.address, self.primary.view.sec}, rpc::uint64_string(0), 0, true},
|
||||
&frame_->response,
|
||||
wrap(self_ptr, *this)
|
||||
);
|
||||
|
||||
self.update_fees(frame_->response);
|
||||
frame_->f(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
LWSF_VERIFY(self && f);
|
||||
post(self, handler{std::make_shared<frame>(self, std::move(f))});
|
||||
}
|
||||
|
||||
void wallet::register_subaccount(std::shared_ptr<wallet> self, const std::uint32_t maj_i, std::function<void(std::error_code)> f)
|
||||
{
|
||||
struct frame
|
||||
|
||||
+63
-16
@@ -113,21 +113,8 @@ namespace lwsf { namespace internal { namespace backend
|
||||
};
|
||||
WIRE_DECLARE_OBJECT(sub_account);
|
||||
|
||||
struct transfer_spend
|
||||
{
|
||||
std::uint64_t amount;
|
||||
rpc::address_meta sender;
|
||||
crypto::public_key tx_pub;
|
||||
crypto::public_key output_pub;
|
||||
|
||||
transfer_spend() noexcept
|
||||
: amount(0), sender{}, tx_pub{}, output_pub{}
|
||||
{}
|
||||
};
|
||||
WIRE_DECLARE_OBJECT(transfer_spend);
|
||||
|
||||
struct transfer_in
|
||||
{
|
||||
{
|
||||
std::uint64_t global_index;
|
||||
std::uint64_t amount;
|
||||
rpc::address_meta recipient;
|
||||
@@ -143,9 +130,52 @@ namespace lwsf { namespace internal { namespace backend
|
||||
rct_mask(),
|
||||
tx_pub{}
|
||||
{}
|
||||
|
||||
transfer_in(const rpc::feed_tx::receive& src) noexcept
|
||||
: global_index(src.id.legacy.index),
|
||||
amount(src.amount),
|
||||
recipient(src.recipient),
|
||||
index(src.index),
|
||||
rct_mask(src.rct),
|
||||
tx_pub(src.tx_pub_key)
|
||||
{}
|
||||
|
||||
rpc::feed_tx::output_id get_output_id() const noexcept
|
||||
{
|
||||
return {
|
||||
rpc::feed_tx::legacy_id{
|
||||
bool(rct_mask) ? unsigned(0) : amount,
|
||||
global_index
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
rpc::feed_tx::receive as_receive(const crypto::public_key& pub) const noexcept
|
||||
{
|
||||
return {
|
||||
amount, get_output_id(), recipient, index, rct_mask, pub, tx_pub
|
||||
};
|
||||
}
|
||||
};
|
||||
WIRE_DECLARE_OBJECT(transfer_in);
|
||||
|
||||
struct transfer_spend
|
||||
{
|
||||
std::uint64_t amount;
|
||||
rpc::address_meta sender;
|
||||
crypto::public_key tx_pub;
|
||||
crypto::public_key output_pub;
|
||||
|
||||
transfer_spend() noexcept
|
||||
: amount(0), sender{}, tx_pub{}, output_pub{}
|
||||
{}
|
||||
|
||||
transfer_spend(const transfer_in& src, const crypto::public_key& pub) noexcept
|
||||
: amount(src.amount), sender(src.recipient), tx_pub(src.tx_pub), output_pub(pub)
|
||||
{}
|
||||
};
|
||||
WIRE_DECLARE_OBJECT(transfer_spend);
|
||||
|
||||
struct transfer_out
|
||||
{
|
||||
std::string address;
|
||||
@@ -165,12 +195,16 @@ namespace lwsf { namespace internal { namespace backend
|
||||
struct transaction
|
||||
{
|
||||
transaction();
|
||||
|
||||
transaction(const rpc::feed_tx& src, const transaction* existing = nullptr);
|
||||
transaction(const rpc::feed_mempool& src);
|
||||
|
||||
transaction(transaction&&) = default;
|
||||
transaction(const transaction& rhs);
|
||||
transaction& operator=(transaction&&) = default;
|
||||
|
||||
bool is_unlocked(std::uint64_t chain_height, Monero::NetworkType type) const;
|
||||
bool has_metadata() const noexcept { return !description.empty() || !transfers.empty(); }
|
||||
|
||||
/*! flat_map is used here for faster copies/merging. A single allocation
|
||||
is needed in the copy (done every refresh interval), instead of an
|
||||
@@ -240,8 +274,11 @@ namespace lwsf { namespace internal { namespace backend
|
||||
// Everything uses `sync` for synchronization unless noted
|
||||
|
||||
Monero::WalletListener* listener; //!< Use `sync_listener`
|
||||
std::shared_ptr<websocket::stream> feed;
|
||||
boost::asio::io_context::strand strand; //<! useful in preventing contention on `sync`
|
||||
http::client client;
|
||||
std::function<void(std::error_code)> on_feed_refresh;
|
||||
std::function<void(std::error_code)> on_feed_fail;
|
||||
account primary;
|
||||
std::string client_prefix;
|
||||
std::vector<std::uint64_t> per_byte_fee; //!< by priority level
|
||||
@@ -252,18 +289,21 @@ namespace lwsf { namespace internal { namespace backend
|
||||
std::error_code lookahead_error; //!< warnings/errors of `server_lookahead` value
|
||||
std::error_code import_error; //!< Error from `import_wallet_request`
|
||||
std::chrono::steady_clock::time_point last_sync;
|
||||
std::chrono::steady_clock::time_point last_fees;
|
||||
std::uint64_t blockchain_height;
|
||||
std::uint64_t fee_mask;
|
||||
config::lookahead server_lookahead; //!< Status of lookahead server-side
|
||||
mutable boost::mutex sync;
|
||||
boost::mutex sync_listener;
|
||||
boost::mutex sync_queue;
|
||||
bool passed_login;
|
||||
bool passed_login; //!< Reset to false to re-initialize all other values
|
||||
bool probed_lookahead; //!< True iff queries were done to determine lookahead re-sync
|
||||
bool feed_disabled;
|
||||
|
||||
wallet(boost::asio::io_context& io);
|
||||
~wallet() noexcept;
|
||||
|
||||
|
||||
//! Extra guard against memory cycles
|
||||
void shutdown();
|
||||
|
||||
@@ -292,13 +332,14 @@ namespace lwsf { namespace internal { namespace backend
|
||||
|
||||
|
||||
// `sync` mutex is NOT acquired for this group
|
||||
|
||||
cryptonote::network_type get_net_type() const;
|
||||
crypto::public_key get_spend_public(const rpc::address_meta& index) const;
|
||||
cryptonote::account_keys get_primary_keys() const;
|
||||
cryptonote::account_public_address get_spend_account(const rpc::address_meta& index) const;
|
||||
std::string get_spend_address(const rpc::address_meta& index) const;
|
||||
bool lookahead_good() const noexcept;
|
||||
bool need_fees() const noexcept;
|
||||
void update_fees(const rpc::get_unspent_outs_response& unspents);
|
||||
|
||||
// End GROUP
|
||||
|
||||
@@ -310,6 +351,9 @@ namespace lwsf { namespace internal { namespace backend
|
||||
//! De-serialize `this` from msgpack. Locks+replaces contents.
|
||||
std::error_code from_bytes(epee::byte_slice source);
|
||||
|
||||
//! Do not hold `sync` or `sync_listener`
|
||||
void add_pending_tx(std::shared_ptr<transaction> pending);
|
||||
|
||||
// End GROUP
|
||||
|
||||
// `sync` is acquired in a `asio::post` function - thread-safe and non-blocking until end
|
||||
@@ -329,6 +373,9 @@ namespace lwsf { namespace internal { namespace backend
|
||||
//! Refreshes txes information. Strong exception guarantee.
|
||||
static void refresh(std::shared_ptr<wallet> self, bool mandatory, std::function<void(std::error_code)>);
|
||||
|
||||
//! Calls `get_unspent_outs` to update the fees when using websockets
|
||||
static void get_fees(std::shared_ptr<wallet> self, std::function<void(std::error_code)>);
|
||||
|
||||
//! Notify server that new major accounts need to be watched.
|
||||
static void register_subaccount(std::shared_ptr<wallet> self, std::uint32_t maj_i, std::function<void(std::error_code)> f);
|
||||
|
||||
|
||||
@@ -40,12 +40,18 @@ namespace lwsf
|
||||
return "No error";
|
||||
case error::approval:
|
||||
return "Account needs approval on light-wallet-server";
|
||||
case error::bad_feed_prefix:
|
||||
return "Websocket prefix on '/feed' message was invalid";
|
||||
case error::configuration:
|
||||
return "Configuration issue";
|
||||
case error::create:
|
||||
return "Account creation not possible";
|
||||
case error::decryption:
|
||||
return "Unable to decrypt file (likely bad password)";
|
||||
case error::feed:
|
||||
return "Websocket '/feed' had error requiring resync";
|
||||
case error::feed_missing_output:
|
||||
return "Websocket '/feed' sent a spent without sending the receive";
|
||||
case error::import_fee:
|
||||
return "Import/restore from height needs payment, see address book";
|
||||
case error::import_invalid:
|
||||
|
||||
@@ -43,9 +43,12 @@ namespace lwsf
|
||||
{
|
||||
none = 0, //!< Must be zero for `expect<..>`
|
||||
approval, //!< Account needs approval
|
||||
bad_feed_prefix, //!< Websocket '/feed' received invalid prefix on msg
|
||||
configuration, //!< Bad Configuration
|
||||
create, //!< Account creation not possible
|
||||
decryption, //!< Failed to decrypt data
|
||||
feed, //!< '/feed' websocket had error requiring restart
|
||||
feed_missing_output,//!< '/feed' websocket sent spend without receive
|
||||
import_fee, //!< `import_wallet_request` has fee
|
||||
import_invalid, //!< `import_wallet_request` has invalid fee
|
||||
import_pending, //!< `import_wallet_request` not fulfilled/approved
|
||||
|
||||
@@ -58,6 +58,7 @@ namespace lwsf { namespace config
|
||||
constexpr const std::size_t max_txes_in_rpc = 2048;
|
||||
constexpr const std::size_t min_outputs = 2;
|
||||
constexpr const std::uint32_t mixin_default = 15;
|
||||
constexpr const std::chrono::seconds ping_timeout{10};
|
||||
constexpr const std::chrono::seconds refresh_interval{30};
|
||||
constexpr const std::chrono::seconds refresh_interval_min{5};
|
||||
constexpr const std::chrono::seconds rpc_timeout{30};
|
||||
|
||||
+140
-21
@@ -34,6 +34,7 @@
|
||||
#include <boost/spirit/include/qi_parse_attr.hpp>
|
||||
#include <boost/spirit/include/qi_sequence.hpp>
|
||||
#include <ctime>
|
||||
#include <limits>
|
||||
#include "hex.h" // monero/contrib/epee/include
|
||||
#include "lwsf_config.h"
|
||||
#include "ringct/rctOps.h" // monero/contrib/epee/include
|
||||
@@ -42,6 +43,7 @@
|
||||
#include "wire/adapted/crypto.h"
|
||||
#include "wire/field.h"
|
||||
#include "wire/json.h"
|
||||
#include "wire/msgpack.h"
|
||||
#include "wire/traits.h"
|
||||
#include "wire/wrapper/array.h"
|
||||
#include "wire/wrapper/defaulted.h"
|
||||
@@ -59,7 +61,7 @@ namespace lwsf { namespace internal { namespace rpc
|
||||
void read_bytes(wire::reader& source, empty&)
|
||||
{ wire::object(source); }
|
||||
|
||||
void write_bytes(wire::json_writer& dest, const login& self)
|
||||
void write_bytes(wire::writer& dest, const login& self)
|
||||
{
|
||||
wire::object(dest, WIRE_FIELD(address), WIRE_FIELD(view_key));
|
||||
}
|
||||
@@ -92,9 +94,144 @@ namespace lwsf { namespace internal { namespace rpc
|
||||
WIRE_FIELD(height),
|
||||
WIRE_FIELD(target_height)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
std::variant<empty, crypto::hash8, crypto::hash> make_payment_id(const boost::optional<epee::byte_slice>& payment_id)
|
||||
{
|
||||
if (payment_id && !payment_id->empty())
|
||||
{
|
||||
if (payment_id->size() == sizeof(crypto::hash8))
|
||||
{
|
||||
crypto::hash8 out{};
|
||||
std::memcpy(std::addressof(out), payment_id->data(), sizeof(crypto::hash8));
|
||||
return out;
|
||||
}
|
||||
else if (payment_id->size() == sizeof(crypto::hash))
|
||||
{
|
||||
crypto::hash out{};
|
||||
std::memcpy(std::addressof(out), payment_id->data(), sizeof(crypto::hash));
|
||||
return out;
|
||||
}
|
||||
else
|
||||
WIRE_DLOG_THROW(wire::error::schema::fixed_binary, "Invalid payment_id size");
|
||||
}
|
||||
|
||||
return empty{};
|
||||
}
|
||||
}
|
||||
|
||||
void read_bytes(wire::json_reader& source, const feed&)
|
||||
{ wire::object(source); }
|
||||
|
||||
static void read_bytes(wire::msgpack_reader& source, feed_tx::legacy_id& self)
|
||||
{ wire::object(source, WIRE_FIELD(amount), WIRE_FIELD(index)); }
|
||||
|
||||
static void read_bytes(wire::msgpack_reader& source, feed_tx::output_id& self)
|
||||
{ wire::object(source, WIRE_FIELD(legacy)); }
|
||||
|
||||
static void read_bytes(wire::msgpack_reader& source, feed_tx::receive& self)
|
||||
{
|
||||
static constexpr std::uint64_t max = std::numeric_limits<std::uint64_t>::max();
|
||||
static constexpr auto txpool = feed_tx::output_id{feed_tx::legacy_id{max, max}};
|
||||
wire::object(source,
|
||||
WIRE_FIELD(amount),
|
||||
WIRE_FIELD_DEFAULTED(id, txpool),
|
||||
WIRE_FIELD_DEFAULTED(recipient, address_meta{}),
|
||||
WIRE_FIELD(index),
|
||||
WIRE_OPTIONAL_FIELD(rct),
|
||||
WIRE_FIELD(public_key),
|
||||
WIRE_FIELD(tx_pub_key)
|
||||
);
|
||||
}
|
||||
|
||||
static void read_bytes(wire::msgpack_reader& source, feed_tx::spend& self)
|
||||
{ wire::object(source, WIRE_FIELD(id), WIRE_FIELD(key_image)); }
|
||||
|
||||
void read_bytes(wire::msgpack_reader& source, feed_tx& self)
|
||||
{
|
||||
using min_receive = wire::min_element_sizeof<crypto::public_key, crypto::public_key>;
|
||||
using min_spend = wire::min_element_sizeof<crypto::key_image>;
|
||||
using time_point = std::chrono::system_clock::time_point;
|
||||
|
||||
time_point::rep timestamp = 0;
|
||||
boost::optional<epee::byte_slice> payment_id;
|
||||
|
||||
wire::object(source,
|
||||
WIRE_FIELD_ARRAY(receives, min_receive),
|
||||
WIRE_FIELD_ARRAY(spends, min_spend),
|
||||
WIRE_FIELD(fee),
|
||||
WIRE_FIELD(unlock_time),
|
||||
WIRE_FIELD_DEFAULTED(height, feed_tx::txpool()),
|
||||
wire::field("timestamp", std::ref(timestamp)),
|
||||
WIRE_FIELD_DEFAULTED(mixin, std::numeric_limits<std::uint32_t>::max()),
|
||||
wire::optional_field("payment_id", std::ref(payment_id)),
|
||||
WIRE_FIELD(hash),
|
||||
WIRE_FIELD(prefix_hash),
|
||||
WIRE_FIELD_DEFAULTED(coinbase, false),
|
||||
WIRE_FIELD_DEFAULTED(mempool, false)
|
||||
);
|
||||
|
||||
self.payment_id = make_payment_id(payment_id);
|
||||
self.timestamp = time_point{std::chrono::seconds{timestamp}};
|
||||
}
|
||||
|
||||
using min_tx_size = wire::min_element_sizeof<crypto::public_key, crypto::public_key>;
|
||||
void read_bytes(wire::msgpack_reader& source, feed_blocks& self)
|
||||
{
|
||||
wire::object(source,
|
||||
WIRE_FIELD_ARRAY(transactions, min_tx_size),
|
||||
WIRE_FIELD(scan_start),
|
||||
WIRE_FIELD(scan_end),
|
||||
WIRE_FIELD(blockchain_height),
|
||||
WIRE_OPTIONAL_FIELD(lookahead_fail),
|
||||
WIRE_FIELD_DEFAULTED(lookahead, address_meta{})
|
||||
);
|
||||
}
|
||||
|
||||
void read_bytes(wire::msgpack_reader& source, feed_error& self)
|
||||
{ wire::object(source, WIRE_FIELD(msg), WIRE_FIELD(code)); }
|
||||
|
||||
void write_bytes(wire::msgpack_writer& dest, const feed_login& self)
|
||||
{ wire::object(dest, WIRE_FIELD(account)); }
|
||||
|
||||
void read_bytes(wire::msgpack_reader& source, feed_mempool& self)
|
||||
{
|
||||
boost::optional<epee::byte_slice> payment_id;
|
||||
wire::object(source,
|
||||
WIRE_FIELD(amount),
|
||||
WIRE_FIELD(fee),
|
||||
WIRE_FIELD(unlock_time),
|
||||
WIRE_FIELD_DEFAULTED(recipient, address_meta{}),
|
||||
WIRE_FIELD_DEFAULTED(mixin, std::numeric_limits<std::uint32_t>::max()),
|
||||
wire::optional_field("payment_id", std::ref(payment_id)),
|
||||
WIRE_OPTIONAL_FIELD(rct),
|
||||
WIRE_FIELD(hash),
|
||||
WIRE_FIELD(prefix_hash),
|
||||
WIRE_FIELD(public_key),
|
||||
WIRE_FIELD(tx_pub_key),
|
||||
WIRE_FIELD(index)
|
||||
);
|
||||
|
||||
self.payment_id = make_payment_id(payment_id);
|
||||
}
|
||||
|
||||
void read_bytes(wire::msgpack_reader& source, feed_tx_sync& self)
|
||||
{
|
||||
wire::object(source,
|
||||
WIRE_FIELD_ARRAY(transactions, min_tx_size),
|
||||
WIRE_FIELD(scanned_block_height),
|
||||
WIRE_FIELD(start_height),
|
||||
WIRE_FIELD(blockchain_height),
|
||||
WIRE_OPTIONAL_FIELD(lookahead_fail),
|
||||
WIRE_FIELD_DEFAULTED(lookahead, address_meta{})
|
||||
);
|
||||
}
|
||||
|
||||
void read_bytes(wire::msgpack_reader& source, feed_warning& self)
|
||||
{ wire::object(source, WIRE_FIELD(msg), WIRE_FIELD(height), WIRE_FIELD(code)); }
|
||||
|
||||
void write_bytes(wire::json_writer& dest, const uint64_string source)
|
||||
{
|
||||
const auto as_string = dest.to_string(std::uintmax_t(source));
|
||||
@@ -147,26 +284,9 @@ namespace lwsf { namespace internal { namespace rpc
|
||||
WIRE_FIELD(mempool)
|
||||
);
|
||||
|
||||
if (payment_id && !payment_id->empty())
|
||||
{
|
||||
if (payment_id->size() == sizeof(crypto::hash8))
|
||||
{
|
||||
self.payment_id = crypto::hash8{};
|
||||
std::memcpy(std::addressof(std::get<crypto::hash8>(self.payment_id)), payment_id->data(), sizeof(crypto::hash8));
|
||||
}
|
||||
else if (payment_id->size() == sizeof(crypto::hash))
|
||||
{
|
||||
self.payment_id = crypto::hash{};
|
||||
std::memcpy(std::addressof(std::get<crypto::hash>(self.payment_id)), payment_id->data(), sizeof(crypto::hash));
|
||||
}
|
||||
else
|
||||
WIRE_DLOG_THROW(wire::error::schema::fixed_binary, "Invalid payment_id size");
|
||||
}
|
||||
else
|
||||
self.payment_id = empty{};
|
||||
self.payment_id = make_payment_id(payment_id);
|
||||
|
||||
self.timestamp.reset();
|
||||
|
||||
if (timestamp)
|
||||
{
|
||||
// %Y-%m-%dT%H:%M:%SZ
|
||||
@@ -349,7 +469,6 @@ namespace lwsf { namespace internal { namespace rpc
|
||||
wire::object(source, WIRE_FIELD_ARRAY(all_subaddrs, max_subaddrs));
|
||||
}
|
||||
|
||||
|
||||
namespace
|
||||
{
|
||||
// This field is a mess between implementations, this cleans it up a bit
|
||||
|
||||
+257
-3
@@ -34,6 +34,7 @@
|
||||
#include <boost/optional/optional.hpp>
|
||||
#include <boost/thread/locks.hpp>
|
||||
#include <boost/variant.hpp>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <system_error>
|
||||
#include <type_traits>
|
||||
@@ -49,6 +50,7 @@
|
||||
#include "wire/basic_value.h"
|
||||
#include "wire/fwd.h"
|
||||
#include "wire/json.h"
|
||||
#include "wire/msgpack/fwd.h"
|
||||
#include "wire/traits.h"
|
||||
|
||||
namespace lwsf { namespace internal { namespace rpc
|
||||
@@ -140,7 +142,7 @@ namespace lwsf { namespace internal { namespace rpc
|
||||
std::string address;
|
||||
crypto::secret_key view_key;
|
||||
};
|
||||
void write_bytes(wire::json_writer&, const login&);
|
||||
void write_bytes(wire::writer&, const login&);
|
||||
|
||||
|
||||
struct login_request
|
||||
@@ -179,6 +181,197 @@ namespace lwsf { namespace internal { namespace rpc
|
||||
void read_bytes(wire::json_reader&, daemon_status&);
|
||||
|
||||
|
||||
enum class feed_status : std::uint16_t
|
||||
{
|
||||
unspecified_error = 0,
|
||||
account_not_found,
|
||||
bad_address,
|
||||
bad_view_key,
|
||||
blockchain_reorg,
|
||||
daemon_unresponsive,
|
||||
parse_error,
|
||||
protocol_error,
|
||||
queue_error,
|
||||
schema_error
|
||||
};
|
||||
WIRE_AS_INTEGER(feed_status);
|
||||
|
||||
struct feed
|
||||
{
|
||||
feed() = delete;
|
||||
static constexpr const char* endpoint() noexcept { return "/feed"; }
|
||||
static constexpr const char* protocol() noexcept { return "lws.feed.v0.msgpack"; }
|
||||
};
|
||||
void read_bytes(wire::json_reader&, const feed&);
|
||||
|
||||
struct feed_tx
|
||||
{
|
||||
struct legacy_id
|
||||
{
|
||||
legacy_id() = delete;
|
||||
std::uint64_t amount;
|
||||
std::uint64_t index;
|
||||
};
|
||||
|
||||
struct output_id { legacy_id legacy; };
|
||||
|
||||
struct receive
|
||||
{
|
||||
std::uint64_t amount;
|
||||
output_id id;
|
||||
address_meta recipient;
|
||||
std::uint16_t index;
|
||||
boost::optional<rct::key> rct;
|
||||
crypto::public_key public_key;
|
||||
crypto::public_key tx_pub_key;
|
||||
|
||||
receive() noexcept
|
||||
: amount(0), id{}, recipient{}, index(0), rct(), public_key{}, tx_pub_key{}
|
||||
{}
|
||||
|
||||
receive(std::uint64_t amount, const output_id& id, const address_meta& recipient, std::uint16_t index, const boost::optional<rct::key> rct, const crypto::public_key& public_key, const crypto::public_key& tx_pub_key) noexcept
|
||||
: amount(amount), id(id), recipient(recipient), index(index), rct(rct), public_key(public_key), tx_pub_key(tx_pub_key)
|
||||
{}
|
||||
};
|
||||
|
||||
struct spend
|
||||
{
|
||||
output_id id;
|
||||
crypto::key_image key_image;
|
||||
|
||||
spend() noexcept
|
||||
: id{}, key_image{}
|
||||
{}
|
||||
};
|
||||
|
||||
std::vector<receive> receives;
|
||||
std::vector<spend> spends;
|
||||
std::uint64_t fee;
|
||||
std::uint64_t unlock_time;
|
||||
std::uint64_t height;
|
||||
std::chrono::system_clock::time_point timestamp;
|
||||
std::uint32_t mixin;
|
||||
std::variant<empty, crypto::hash8, crypto::hash> payment_id;
|
||||
crypto::hash hash;
|
||||
crypto::hash prefix_hash;
|
||||
bool coinbase;
|
||||
bool mempool;
|
||||
|
||||
//! Hack because `boost/std::optional` sort left adjacent
|
||||
static constexpr std::uint64_t txpool() noexcept { return std::uint64_t(-1); }
|
||||
|
||||
feed_tx()
|
||||
: receives(),
|
||||
spends(),
|
||||
unlock_time(0),
|
||||
height(),
|
||||
timestamp(),
|
||||
mixin(),
|
||||
payment_id(),
|
||||
hash{},
|
||||
prefix_hash{},
|
||||
coinbase(false),
|
||||
mempool(false)
|
||||
{}
|
||||
|
||||
boost::optional<std::uint64_t> get_height() const noexcept
|
||||
{ return boost::make_optional(height != txpool(), height); }
|
||||
};
|
||||
void read_bytes(wire::msgpack_reader&, feed_tx&);
|
||||
|
||||
inline constexpr bool operator==(const feed_tx::output_id& lhs, const feed_tx::output_id& rhs) noexcept
|
||||
{
|
||||
return lhs.legacy.index == rhs.legacy.index && lhs.legacy.amount == rhs.legacy.amount;
|
||||
}
|
||||
inline constexpr bool operator!=(const feed_tx::output_id& lhs, const feed_tx::output_id& rhs) noexcept
|
||||
{
|
||||
return lhs.legacy.index != rhs.legacy.index || lhs.legacy.amount != rhs.legacy.amount;
|
||||
}
|
||||
inline constexpr bool operator<(const feed_tx::output_id& lhs, const feed_tx::output_id& rhs) noexcept
|
||||
{
|
||||
return lhs.legacy.amount == rhs.legacy.amount ?
|
||||
lhs.legacy.index < rhs.legacy.index : lhs.legacy.amount < rhs.legacy.amount;
|
||||
}
|
||||
|
||||
struct feed_blocks
|
||||
{
|
||||
feed_blocks() = delete;
|
||||
static constexpr const char* prefix() noexcept { return "blocks:"; }
|
||||
|
||||
std::vector<feed_tx> transactions;
|
||||
std::uint64_t scan_start;
|
||||
std::uint64_t scan_end;
|
||||
std::uint64_t blockchain_height;
|
||||
boost::optional<std::uint64_t> lookahead_fail;
|
||||
address_meta lookahead;
|
||||
};
|
||||
void read_bytes(wire::msgpack_reader&, feed_blocks&);
|
||||
|
||||
struct feed_error
|
||||
{
|
||||
feed_error() = delete;
|
||||
static constexpr const char* prefix() noexcept { return "error:"; }
|
||||
|
||||
std::string msg;
|
||||
feed_status code;
|
||||
};
|
||||
void read_bytes(wire::msgpack_reader&, feed_error&);
|
||||
|
||||
struct feed_login
|
||||
{
|
||||
feed_login() = delete;
|
||||
static constexpr const char* prefix() noexcept { return "login:"; }
|
||||
login account;
|
||||
};
|
||||
void write_bytes(wire::msgpack_writer&, const feed_login&);
|
||||
|
||||
//! One specific output is sent per-message
|
||||
struct feed_mempool
|
||||
{
|
||||
feed_mempool() = delete;
|
||||
static constexpr const char* prefix() noexcept { return "mempool:"; }
|
||||
|
||||
std::uint64_t amount;
|
||||
std::uint64_t fee;
|
||||
std::uint64_t unlock_time;
|
||||
address_meta recipient;
|
||||
std::uint32_t mixin;
|
||||
std::variant<empty, crypto::hash8, crypto::hash> payment_id;
|
||||
boost::optional<rct::key> rct;
|
||||
crypto::hash hash;
|
||||
crypto::hash prefix_hash;
|
||||
crypto::public_key public_key;
|
||||
crypto::public_key tx_pub_key;
|
||||
std::uint16_t index;
|
||||
};
|
||||
void read_bytes(wire::msgpack_reader&, feed_mempool&);
|
||||
|
||||
struct feed_tx_sync
|
||||
{
|
||||
feed_tx_sync() = delete;
|
||||
static constexpr const char* prefix() noexcept { return "tx_sync:"; }
|
||||
|
||||
std::vector<feed_tx> transactions;
|
||||
std::uint64_t scanned_block_height;
|
||||
std::uint64_t start_height;
|
||||
std::uint64_t blockchain_height;
|
||||
boost::optional<std::uint64_t> lookahead_fail;
|
||||
address_meta lookahead;
|
||||
};
|
||||
void read_bytes(wire::msgpack_reader&, feed_tx_sync&);
|
||||
|
||||
struct feed_warning
|
||||
{
|
||||
feed_warning() = delete;
|
||||
static constexpr const char* prefix() noexcept { return "warning:"; }
|
||||
|
||||
std::string msg;
|
||||
std::uint64_t height;
|
||||
feed_status code;
|
||||
};
|
||||
void read_bytes(wire::msgpack_reader&, feed_warning&);
|
||||
|
||||
|
||||
enum class uint64_string : std::uint64_t {};
|
||||
void write_bytes(wire::json_writer&, uint64_string);
|
||||
void read_bytes(wire::json_reader&, uint64_string&);
|
||||
@@ -198,6 +391,14 @@ namespace lwsf { namespace internal { namespace rpc
|
||||
key_image{},
|
||||
tx_pub_key{}
|
||||
{}
|
||||
|
||||
transaction_spend(const feed_tx::spend& spend, const feed_tx::receive& receive) noexcept
|
||||
: amount(uint64_string(receive.amount)),
|
||||
sender(receive.recipient),
|
||||
out_index(receive.index),
|
||||
key_image(spend.key_image),
|
||||
tx_pub_key(receive.tx_pub_key)
|
||||
{}
|
||||
};
|
||||
void read_bytes(wire::json_reader&, transaction_spend&);
|
||||
|
||||
@@ -226,6 +427,24 @@ namespace lwsf { namespace internal { namespace rpc
|
||||
coinbase(false),
|
||||
mempool(false)
|
||||
{}
|
||||
|
||||
transaction(const feed_tx& src)
|
||||
: spent_outputs(),
|
||||
payment_id(src.payment_id),
|
||||
timestamp(std::chrono::system_clock::to_time_t(src.timestamp)),
|
||||
fee(uint64_string(src.fee)),
|
||||
total_received(uint64_string(0)),
|
||||
unlock_time(src.unlock_time),
|
||||
height(src.get_height()),
|
||||
hash(src.hash),
|
||||
coinbase(src.coinbase),
|
||||
mempool(src.mempool)
|
||||
{
|
||||
std::uint64_t total = 0;
|
||||
for (const auto& received : src.receives)
|
||||
total += received.amount;
|
||||
total_received = uint64_string(total);
|
||||
}
|
||||
};
|
||||
void read_bytes(wire::json_reader&, transaction&);
|
||||
|
||||
@@ -333,8 +552,6 @@ namespace lwsf { namespace internal { namespace rpc
|
||||
|
||||
struct ringct
|
||||
{
|
||||
ringct() = delete;
|
||||
|
||||
enum class format : std::uint8_t
|
||||
{
|
||||
none = 0, //!< Not ringct
|
||||
@@ -343,6 +560,19 @@ namespace lwsf { namespace internal { namespace rpc
|
||||
unencrypted
|
||||
};
|
||||
|
||||
ringct() noexcept
|
||||
: mask{}, type(format::none)
|
||||
{}
|
||||
|
||||
ringct(const rct::key& mask, const format type) noexcept
|
||||
: mask(mask), type(type)
|
||||
{}
|
||||
|
||||
ringct(const boost::optional<rct::key>& src) noexcept
|
||||
: mask(src.value_or(rct::key{})),
|
||||
type(bool(src) ? format::unencrypted : format::none)
|
||||
{}
|
||||
|
||||
rct::key mask;
|
||||
format type;
|
||||
};
|
||||
@@ -370,6 +600,30 @@ namespace lwsf { namespace internal { namespace rpc
|
||||
public_key{},
|
||||
tx_pub_key{}
|
||||
{}
|
||||
|
||||
output(const feed_mempool& src) noexcept
|
||||
: amount(uint64_string(src.amount)),
|
||||
global_index(uint64_string(std::numeric_limits<std::uint64_t>::max())),
|
||||
recipient(src.recipient),
|
||||
index(src.index),
|
||||
rct(src.rct),
|
||||
tx_hash(src.hash),
|
||||
tx_prefix_hash(src.prefix_hash),
|
||||
public_key(src.public_key),
|
||||
tx_pub_key(src.tx_pub_key)
|
||||
{}
|
||||
|
||||
output(const feed_tx::receive& src, const feed_tx& tx) noexcept
|
||||
: amount(uint64_string(src.amount)),
|
||||
global_index(uint64_string(src.id.legacy.index)),
|
||||
recipient(src.recipient),
|
||||
index(src.index),
|
||||
rct(src.rct),
|
||||
tx_hash(tx.hash),
|
||||
tx_prefix_hash(tx.prefix_hash),
|
||||
public_key(src.public_key),
|
||||
tx_pub_key(src.tx_pub_key)
|
||||
{}
|
||||
};
|
||||
void read_bytes(wire::json_reader&, output&);
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
# 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.
|
||||
|
||||
set(lwsf-net_sources context.cpp http.cpp wallet_io.cpp)
|
||||
set(lwsf-net_headers context.h http.h wallet_io.h)
|
||||
set(lwsf-net_sources context.cpp http.cpp wallet_io.cpp websocket.cpp)
|
||||
set(lwsf-net_headers context.h http.h wallet_io.h websocket.h)
|
||||
|
||||
add_library(lwsf-net ${lwsf-net_sources} ${lwsf-net_headers})
|
||||
target_link_libraries(lwsf-net PRIVATE monero::libraries)
|
||||
|
||||
+75
-17
@@ -58,6 +58,7 @@
|
||||
#include "misc_log_ex.h" // monero/contrib/epee/include
|
||||
#include "net/net_utils_base.h"
|
||||
#include "net/socks.h" // monero/src
|
||||
#include "net/websocket.h"
|
||||
#include "string_tools.h" // monero/contrib/epee/include
|
||||
|
||||
namespace lwsf { namespace internal { namespace http
|
||||
@@ -211,7 +212,7 @@ namespace lwsf { namespace internal { namespace http
|
||||
void(std::shared_ptr<client_state>, std::function<void(boost::system::error_code)>);
|
||||
|
||||
boost::beast::flat_static_buffer<config::http_parser_buffer_size> buffer;
|
||||
std::function<connect_func> connect;
|
||||
const std::function<connect_func> connect;
|
||||
std::deque<message> outgoing;
|
||||
const std::string host;
|
||||
const std::string prefix;
|
||||
@@ -435,6 +436,14 @@ namespace lwsf { namespace internal { namespace http
|
||||
self.is_connected = true;
|
||||
while (!self.outgoing.empty())
|
||||
{
|
||||
if (self.outgoing.front().verb == boost::beast::http::verb::subscribe)
|
||||
{
|
||||
std::function<server_response_func> notifier;
|
||||
notifier.swap(self.outgoing.front().notifier);
|
||||
self.outgoing.clear();
|
||||
return notifier({}, {});
|
||||
}
|
||||
|
||||
set_timeout(self_, config::rpc_timeout);
|
||||
|
||||
MDEBUG("Sending " << self.outgoing.front().body.size() << " bytes in HTTP " << (self.outgoing.front().is_get() ? "GET" : "POST") << " to " << self.outgoing.front());
|
||||
@@ -490,31 +499,35 @@ namespace lwsf { namespace internal { namespace http
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void queue_async_(std::shared_ptr<client_state> state, std::string&& target, epee::byte_slice&& body, std::function<server_response_func>&& notifier, boost::beast::http::verb verb)
|
||||
{
|
||||
message msg{std::move(body), std::move(target), std::move(notifier), verb};
|
||||
if (!state)
|
||||
return msg.notifier(common_error::kInvalidArgument, epee::byte_slice{});
|
||||
|
||||
MDEBUG("Queueing HTTP " << to_string(verb) << " to " << msg << " using " << state.get());
|
||||
boost::asio::dispatch(
|
||||
state->strand,
|
||||
[state, msg = std::move(msg)] () mutable
|
||||
{
|
||||
const bool empty = state->outgoing.empty();
|
||||
state->outgoing.push_back(std::move(msg));
|
||||
if (empty)
|
||||
boost::asio::post(state->strand, client_loop{state});
|
||||
}
|
||||
);
|
||||
}
|
||||
} // anonymous
|
||||
|
||||
void client::queue_async(std::string&& target, epee::byte_slice&& body, std::function<server_response_func>&& notifier, boost::beast::http::verb verb) const
|
||||
{
|
||||
message msg{std::move(body), std::move(target), std::move(notifier), verb};
|
||||
std::shared_ptr<client_state> state;
|
||||
{
|
||||
const boost::lock_guard<boost::mutex> lock{sync_};
|
||||
state = state_;
|
||||
}
|
||||
|
||||
if (!state)
|
||||
return msg.notifier(common_error::kInvalidArgument, epee::byte_slice{});
|
||||
|
||||
MDEBUG("Queueing HTTP " << (msg.is_get() ? "GET" : "POST") << " to " << msg << " using " << this);
|
||||
boost::asio::dispatch(
|
||||
state->strand,
|
||||
[state, msg = std::move(msg)] () mutable
|
||||
{
|
||||
const bool empty = state->outgoing.empty();
|
||||
state->outgoing.push_back(std::move(msg));
|
||||
if (empty)
|
||||
boost::asio::post(state->strand, client_loop{state});
|
||||
}
|
||||
);
|
||||
queue_async_(std::move(state), std::move(target), std::move(body), std::move(notifier), verb);
|
||||
}
|
||||
|
||||
void client::direct::operator()(std::shared_ptr<client_state> self, std::function<callback_func> f) const
|
||||
@@ -685,6 +698,51 @@ namespace lwsf { namespace internal { namespace http
|
||||
state_ = std::make_shared<client_state>(state_->strand.context(), state_->host, state_->prefix, state_->port, state_->ssl, proxy_);
|
||||
}
|
||||
|
||||
void client::ws_async(std::string target, std::string protocol, std::function<ws_func> notifier)
|
||||
{
|
||||
std::shared_ptr<client_state> self;
|
||||
{
|
||||
const boost::lock_guard<boost::mutex> lock{sync_};
|
||||
self = state_;
|
||||
state_ = std::make_shared<client_state>(self->strand.context(), self->host, self->prefix, self->port, self->ssl, proxy_);
|
||||
}
|
||||
LWSF_VERIFY(self);
|
||||
|
||||
struct start_ws
|
||||
{
|
||||
std::weak_ptr<client_state> self_;
|
||||
std::string target_;
|
||||
std::string protocol_;
|
||||
std::function<ws_func> notifier_;
|
||||
|
||||
void operator()(const std::error_code error, epee::byte_slice)
|
||||
{
|
||||
const auto self = self_.lock();
|
||||
LWSF_VERIFY(self && notifier_);
|
||||
if (error)
|
||||
return notifier_(error, nullptr);
|
||||
|
||||
std::string host;
|
||||
boost::system::error_code ec{};
|
||||
boost::asio::ip::make_address_v6(self->host, ec);
|
||||
const bool https = self->ssl_status == epee::net_utils::ssl_support_t::e_ssl_support_enabled;
|
||||
if (!ec)
|
||||
host = "[" + host + "]:" + std::to_string(self->port);
|
||||
else if ((https && self->port == 443) || (!https && self->port == 80))
|
||||
host = self->host;
|
||||
else
|
||||
host = self->host + ":" + std::to_string(self->port);
|
||||
|
||||
if (self->ssl_status == epee::net_utils::ssl_support_t::e_ssl_support_enabled)
|
||||
websocket::async_start(std::move(self->sock), self->strand.context(), std::move(host), std::move(target_), std::move(protocol_), std::move(notifier_));
|
||||
else
|
||||
websocket::async_start(std::move(self->sock.next_layer()), self->strand.context(), std::move(host), std::move(target_), std::move(protocol_), std::move(notifier_));
|
||||
}
|
||||
};
|
||||
|
||||
queue_async_(self, {}, {}, start_ws{self, std::move(target), std::move(protocol), std::move(notifier)}, boost::beast::http::verb::subscribe);
|
||||
}
|
||||
|
||||
void client::post_async(std::string url, epee::byte_slice json_body, std::function<server_response_func> notifier) const
|
||||
{
|
||||
return queue_async(std::move(url), std::move(json_body), std::move(notifier), boost::beast::http::verb::post);
|
||||
|
||||
+17
-1
@@ -40,7 +40,9 @@
|
||||
#include "byte_slice.h" // monero/contrib/epee/include
|
||||
#include "net/net_ssl.h" // monero/contrib/epee/include
|
||||
|
||||
namespace lwsf { namespace internal { namespace http
|
||||
namespace lwsf { namespace internal {
|
||||
namespace websocket { class stream; }
|
||||
namespace http
|
||||
{
|
||||
enum class error : int
|
||||
{
|
||||
@@ -57,6 +59,9 @@ namespace lwsf { namespace internal { namespace http
|
||||
using server_response_func = void(std::error_code, epee::byte_slice);
|
||||
using callback_func = void(boost::system::error_code);
|
||||
using connect_func = void(std::shared_ptr<client_state>, std::function<callback_func>);
|
||||
using ws_func = void(std::error_code, std::shared_ptr<websocket::stream>);
|
||||
|
||||
std::string make_host(const std::string& host);
|
||||
|
||||
//! Primarily for webhooks, where the response is (basically) ignored.
|
||||
class client
|
||||
@@ -107,6 +112,17 @@ namespace lwsf { namespace internal { namespace http
|
||||
`success()` is returned.
|
||||
\return `success()` if `url` is valid. */
|
||||
void get_async(std::string target, std::function<server_response_func> notifier) const;
|
||||
|
||||
|
||||
/*! Never blocks. Thread safe. Makes a HTTP "upgrade" websocket request
|
||||
using the existing HTTP connection. If the handshake succeeds, the
|
||||
`websocket::stream` handle is sent to `notifier`, else the error is
|
||||
reported by same function.
|
||||
|
||||
Any existing HTTP connection is irrevocably converted to a websocket,
|
||||
however `post_async` and `get_async` will create a new connection, when
|
||||
needed, using the previously supplied values to `init` and `proxy`. */
|
||||
void ws_async(std::string target, std::string protocol, std::function<ws_func> notifier);
|
||||
};
|
||||
}}} // lwsf // internal // http
|
||||
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
// Copyright (c) 2026, 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 "websocket.h"
|
||||
|
||||
#include <boost/asio/bind_executor.hpp>
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/coroutine.hpp>
|
||||
#include <boost/asio/dispatch.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <boost/beast/core/error.hpp>
|
||||
#include <boost/beast/core/multi_buffer.hpp>
|
||||
#include <boost/beast/core/string_type.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/version.hpp>
|
||||
#include <boost/beast/websocket/ssl.hpp>
|
||||
#include <boost/beast/websocket/stream.hpp>
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
|
||||
#include "byte_slice.h" // monero/contrib/epee/include
|
||||
#include "byte_stream.h" // monero/contrib/epee/include
|
||||
#include "error.h"
|
||||
#include "lwsf_config.h"
|
||||
|
||||
namespace lwsf { namespace internal { namespace websocket
|
||||
{
|
||||
using const_flat_buffer = boost::beast::net::const_buffer;
|
||||
|
||||
struct read_loop
|
||||
{
|
||||
std::shared_ptr<stream> self_;
|
||||
std::function<update_func> receive_;
|
||||
void operator()(boost::system::error_code, std::size_t);
|
||||
};
|
||||
|
||||
struct subscription : boost::asio::coroutine
|
||||
{
|
||||
std::shared_ptr<stream> self_;
|
||||
copyable_slice topic_;
|
||||
std::function<update_func> receive_;
|
||||
|
||||
subscription(subscription&&) = default;
|
||||
subscription(const subscription&) = default;
|
||||
explicit subscription(std::shared_ptr<stream> self, epee::byte_slice&& topic, std::function<update_func>&& receive)
|
||||
: self_(std::move(self)), topic_(std::move(topic)), receive_(std::move(receive))
|
||||
{}
|
||||
|
||||
void operator()(boost::system::error_code = {});
|
||||
};
|
||||
|
||||
class stream
|
||||
{
|
||||
std::vector<std::function<ping_func>> pings_;
|
||||
boost::asio::io_context::strand strand_;
|
||||
boost::asio::steady_timer timer_;
|
||||
boost::beast::multi_buffer buffer_;
|
||||
bool writing_;
|
||||
bool closing_;
|
||||
bool subscribed_;
|
||||
|
||||
struct on_timeout
|
||||
{
|
||||
std::shared_ptr<stream> self_;
|
||||
void operator()(const boost::system::error_code error) const
|
||||
{
|
||||
LWSF_VERIFY(self_);
|
||||
if (!error || error != boost::asio::error::operation_aborted)
|
||||
{
|
||||
self_->notify_pings(boost::asio::error::operation_aborted);
|
||||
self_->async_close(self_);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected:
|
||||
|
||||
template<typename F>
|
||||
auto bind(F&& f) -> decltype(boost::asio::bind_executor(strand_, std::forward<F>(f)))
|
||||
{ return boost::asio::bind_executor(strand_, std::forward<F>(f)); }
|
||||
|
||||
template<typename T, typename F>
|
||||
void do_async_read(T& sock, F f)
|
||||
{
|
||||
sock.async_read(buffer_, bind(std::move(f)));
|
||||
}
|
||||
|
||||
template<typename T, typename F>
|
||||
void do_async_write(T& sock, const const_flat_buffer buf, F f)
|
||||
{
|
||||
if (closing_)
|
||||
return f(boost::beast::websocket::error::closed);
|
||||
|
||||
LWSF_VERIFY(f.self_);
|
||||
if (!writing_)
|
||||
{
|
||||
struct on_write
|
||||
{
|
||||
F f_;
|
||||
|
||||
void operator()(const boost::system::error_code error, std::size_t)
|
||||
{
|
||||
const std::shared_ptr<stream> self = f_.self_;
|
||||
LWSF_VERIFY(self);
|
||||
|
||||
self->timer_.cancel();
|
||||
self->writing_ = false;
|
||||
f_(error);
|
||||
if (error)
|
||||
{
|
||||
self->notify_pings(error);
|
||||
self->async_close(self);
|
||||
}
|
||||
else if (!self->pings_.empty())
|
||||
self->async_ping(on_ping{self});
|
||||
}
|
||||
};
|
||||
|
||||
writing_ = true;
|
||||
timer_.expires_after(config::ping_timeout); // timeout for write not response
|
||||
timer_.async_wait(on_timeout{f.self_});
|
||||
sock.async_write(buf, bind(on_write{std::move(f)}));
|
||||
}
|
||||
else
|
||||
pings_.push_back(std::bind(&stream::async_write, f.self_, std::move(f)));
|
||||
}
|
||||
|
||||
template<typename T, typename F>
|
||||
void do_async_ping(T& sock, F&& f)
|
||||
{
|
||||
if (closing_)
|
||||
return f(boost::beast::websocket::error::closed);
|
||||
|
||||
LWSF_VERIFY(!writing_ && f.self_);
|
||||
writing_ = true;
|
||||
timer_.expires_after(config::ping_timeout);
|
||||
timer_.async_wait(on_timeout{f.self_});
|
||||
sock.async_ping(boost::beast::websocket::ping_data(), bind(std::forward<F>(f)));
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void do_async_close(T& sock, std::shared_ptr<stream> self)
|
||||
{
|
||||
LWSF_VERIFY(self);
|
||||
if (!writing_)
|
||||
{
|
||||
const bool run = !closing_;
|
||||
closing_ = true;
|
||||
if (run)
|
||||
sock.async_close(boost::beast::websocket::close_reason(), bind(std::bind(&stream::async_shutdown, self, self)));
|
||||
}
|
||||
else
|
||||
pings_.push_back(std::bind(&stream::async_close, self, self));
|
||||
}
|
||||
|
||||
void do_async_shutdown(tcp_sock& sock, std::shared_ptr<stream>)
|
||||
{ do_cleanup(sock); }
|
||||
|
||||
void do_async_shutdown(boost::asio::ssl::stream<tcp_sock>& sock, std::shared_ptr<stream> self)
|
||||
{
|
||||
LWSF_VERIFY(self);
|
||||
sock.async_shutdown(bind(std::bind(&stream::cleanup, self)));
|
||||
}
|
||||
|
||||
void do_cleanup(tcp_sock& sock)
|
||||
{
|
||||
boost::system::error_code ec{};
|
||||
sock.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
|
||||
sock.close(ec);
|
||||
}
|
||||
|
||||
void do_cleanup(boost::asio::ssl::stream<tcp_sock>& sock)
|
||||
{ do_cleanup(sock.next_layer()); }
|
||||
|
||||
public:
|
||||
explicit stream(boost::asio::io_context& io)
|
||||
: strand_(io), timer_(io), buffer_(), writing_(false), closing_(false), subscribed_(false)
|
||||
{}
|
||||
|
||||
//! \return True if this function has never been invoked previously
|
||||
bool subscribe() noexcept
|
||||
{
|
||||
const bool rc = !subscribed_;
|
||||
subscribed_ = true;
|
||||
return rc;
|
||||
}
|
||||
|
||||
template<typename F>
|
||||
void dispatch(F&& f)
|
||||
{
|
||||
boost::asio::dispatch(strand_, std::forward<F>(f));
|
||||
}
|
||||
|
||||
epee::byte_slice gather_buffer()
|
||||
{
|
||||
epee::byte_stream sink{};
|
||||
sink.reserve(buffer_.size());
|
||||
|
||||
const auto buffers = buffer_.cdata();
|
||||
const auto end = boost::asio::buffer_sequence_end(buffers);
|
||||
for (auto cur = boost::asio::buffer_sequence_begin(buffers); cur != end; ++cur)
|
||||
{
|
||||
const const_flat_buffer this_buf = *cur;
|
||||
sink.write(reinterpret_cast<const std::uint8_t*>(this_buf.data()), this_buf.size());
|
||||
}
|
||||
|
||||
buffer_.consume(buffer_.size());
|
||||
return epee::byte_slice{std::move(sink)};
|
||||
}
|
||||
|
||||
struct on_ping
|
||||
{
|
||||
std::shared_ptr<stream> self_;
|
||||
void operator()(const boost::system::error_code& error)
|
||||
{
|
||||
LWSF_VERIFY(self_);
|
||||
self_->writing_ = false;
|
||||
if (self_->timer_.cancel() || error)
|
||||
{
|
||||
self_->notify_pings(error);
|
||||
if (error)
|
||||
self_->async_close(self_);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void notify_pings(const boost::system::error_code error = {})
|
||||
{
|
||||
std::vector<std::function<ping_func>> pings;
|
||||
pings.swap(pings_);
|
||||
for (const auto& ping : pings)
|
||||
ping(error);
|
||||
}
|
||||
|
||||
bool add_ping_notifier(std::function<ping_func>&& notifier)
|
||||
{
|
||||
pings_.push_back(std::move(notifier));
|
||||
return !writing_;
|
||||
}
|
||||
|
||||
virtual void async_read(read_loop&&) = 0;
|
||||
virtual void async_write(subscription) = 0;
|
||||
virtual void async_ping(on_ping&&) = 0;
|
||||
virtual void async_close(std::shared_ptr<stream>) = 0;
|
||||
virtual void async_shutdown(std::shared_ptr<stream>) = 0;
|
||||
virtual void cleanup() = 0;
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
class stream_ final : public stream
|
||||
{
|
||||
boost::beast::websocket::stream<T> sock_;
|
||||
|
||||
public:
|
||||
explicit stream_(T&& sock, boost::asio::io_context& io)
|
||||
: stream(io), sock_(std::move(sock))
|
||||
{
|
||||
sock_.binary(true);
|
||||
}
|
||||
|
||||
template<typename F>
|
||||
void async_handshake(const std::string& host, const std::string& target, std::string&& protocol, F f)
|
||||
{
|
||||
namespace http = boost::beast::http;
|
||||
struct on_control
|
||||
{
|
||||
std::weak_ptr<stream> self_;
|
||||
void operator()(boost::beast::websocket::frame_type, boost::beast::string_view)
|
||||
{
|
||||
const auto self = self_.lock();
|
||||
if (self)
|
||||
self->notify_pings();
|
||||
}
|
||||
};
|
||||
|
||||
LWSF_VERIFY(f.self_);
|
||||
sock_.control_callback(on_control{f.self_});
|
||||
sock_.set_option(
|
||||
boost::beast::websocket::stream_base::decorator(
|
||||
[protocol = std::move(protocol)] (http::request_header<>& hdr)
|
||||
{ hdr.set(http::field::sec_websocket_protocol, protocol); }
|
||||
)
|
||||
);
|
||||
sock_.async_handshake(host, target, bind(std::move(f)));
|
||||
}
|
||||
|
||||
virtual void async_read(read_loop&& handler) override final
|
||||
{ do_async_read(sock_, std::move(handler)); }
|
||||
|
||||
virtual void async_write(subscription handler) override final
|
||||
{
|
||||
std::uint8_t const* const data = handler.topic_.data.data();
|
||||
const std::size_t size = handler.topic_.data.size();
|
||||
do_async_write(sock_, const_flat_buffer(data, size), std::move(handler));
|
||||
}
|
||||
|
||||
virtual void async_ping(on_ping&& handler) override final
|
||||
{ do_async_ping(sock_, std::move(handler)); }
|
||||
|
||||
virtual void async_close(std::shared_ptr<stream> self) override final
|
||||
{ do_async_close(sock_, std::move(self)); }
|
||||
|
||||
virtual void async_shutdown(std::shared_ptr<stream> self) override final
|
||||
{ do_async_shutdown(sock_.next_layer(), std::move(self)); }
|
||||
|
||||
virtual void cleanup() override final
|
||||
{ do_cleanup(sock_.next_layer()); }
|
||||
};
|
||||
|
||||
struct after_connect
|
||||
{
|
||||
std::shared_ptr<stream> self_;
|
||||
std::function<ready_func> notifier_;
|
||||
|
||||
void operator()(const boost::system::error_code error)
|
||||
{
|
||||
LWSF_VERIFY(notifier_);
|
||||
if (error)
|
||||
notifier_(error, nullptr);
|
||||
else
|
||||
notifier_(error, self_);
|
||||
}
|
||||
};
|
||||
|
||||
void read_loop::operator()(const boost::system::error_code error, std::size_t)
|
||||
{
|
||||
LWSF_VERIFY(self_ && receive_);
|
||||
stream& self = *self_;
|
||||
receive_(error, self.gather_buffer());
|
||||
if (!error)
|
||||
self.async_read(std::move(*this));
|
||||
else
|
||||
self.async_close(self_);
|
||||
}
|
||||
|
||||
void subscription::operator()(boost::system::error_code error)
|
||||
{
|
||||
LWSF_VERIFY(self_ && receive_);
|
||||
stream& self = *self_;
|
||||
BOOST_ASIO_CORO_REENTER(*this)
|
||||
{
|
||||
if (self.subscribe())
|
||||
BOOST_ASIO_CORO_YIELD self.async_write(std::move(*this));
|
||||
else
|
||||
error = boost::asio::error::already_started;
|
||||
|
||||
if (error)
|
||||
{
|
||||
receive_(error, epee::byte_slice{});
|
||||
self.async_close(std::move(self_));
|
||||
}
|
||||
else
|
||||
self.async_read(read_loop{std::move(self_), std::move(receive_)});
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
inline void do_start(std::shared_ptr<stream_<T>> self, const std::string& host, const std::string& target, std::string&& protocol, std::function<ready_func>&& notifier)
|
||||
{
|
||||
self->async_handshake(host, target, std::move(protocol), after_connect{self, std::move(notifier)});
|
||||
}
|
||||
|
||||
void async_start(tcp_sock&& sock, boost::asio::io_context& io, std::string&& host, std::string&& target, std::string&& protocol, std::function<ready_func>&& notifier)
|
||||
{
|
||||
do_start(std::make_shared<stream_<tcp_sock>>(std::move(sock), io), host, target, std::move(protocol), std::move(notifier));
|
||||
}
|
||||
|
||||
void async_start(boost::asio::ssl::stream<tcp_sock>&& sock, boost::asio::io_context& io, std::string&& host, std::string&& target, std::string&& protocol, std::function<ready_func>&& notifier)
|
||||
{
|
||||
do_start(std::make_shared<stream_<boost::asio::ssl::stream<tcp_sock>>>(std::move(sock), io), host, target, std::move(protocol), std::move(notifier));
|
||||
}
|
||||
|
||||
void async_subscribe(std::shared_ptr<stream> self, epee::byte_slice&& topic, std::function<update_func>&& receive)
|
||||
{
|
||||
LWSF_VERIFY(self);
|
||||
self->dispatch(subscription{self, std::move(topic), std::move(receive)});
|
||||
}
|
||||
|
||||
void async_ping(std::shared_ptr<stream> self, std::function<ping_func>&& notifier)
|
||||
{
|
||||
LWSF_VERIFY(self);
|
||||
self->dispatch([self, notifier = std::move(notifier)] () mutable {
|
||||
if(self->add_ping_notifier(std::move(notifier)))
|
||||
self->async_ping(stream::on_ping{self});
|
||||
});
|
||||
}
|
||||
|
||||
void async_close(std::shared_ptr<stream> self)
|
||||
{
|
||||
LWSF_VERIFY(self);
|
||||
self->dispatch(std::bind(&stream::async_close, self, self));
|
||||
}
|
||||
}}} // lwsf // internal // websocket
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) 2026, 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/ssl.hpp>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
#include "byte_slice.h" // monero/contrib/epee/include
|
||||
|
||||
namespace lwsf { namespace internal
|
||||
{
|
||||
|
||||
struct copyable_slice
|
||||
{
|
||||
copyable_slice() noexcept
|
||||
: data(nullptr)
|
||||
{}
|
||||
|
||||
copyable_slice(epee::byte_slice&& data) noexcept
|
||||
: data(std::move(data))
|
||||
{}
|
||||
|
||||
copyable_slice(copyable_slice&&) noexcept = default;
|
||||
copyable_slice(const copyable_slice& rhs) noexcept
|
||||
: data(rhs.data.clone())
|
||||
{}
|
||||
|
||||
epee::byte_slice data;
|
||||
};
|
||||
|
||||
namespace websocket
|
||||
{
|
||||
class stream;
|
||||
using tcp_sock = boost::asio::ip::tcp::socket;
|
||||
using ready_func = void(std::error_code, std::shared_ptr<stream>);
|
||||
using update_func = void(std::error_code, copyable_slice);
|
||||
using ping_func = void(std::error_code);
|
||||
|
||||
void async_start(tcp_sock&& sock, boost::asio::io_context& io, std::string&& host, std::string&& target, std::string&& protocol, std::function<ready_func>&& notifier);
|
||||
void async_start(boost::asio::ssl::stream<tcp_sock>&& sock, boost::asio::io_context& io, std::string&& host, std::string&& target, std::string&& protocol, std::function<ready_func>&& notifier);
|
||||
|
||||
/*! Subscribe to `topic` and call `notifier` on each incoming message. Can
|
||||
only invoke once. Thread-safe.
|
||||
\param topic The raw message to be sent to server
|
||||
\param notifier Called on every received message.
|
||||
\throw std::invalid_argument if invoked multiple times. */
|
||||
void async_subscribe(std::shared_ptr<stream> self, epee::byte_slice&& topic, std::function<update_func>&& notifier);
|
||||
|
||||
//! Ping the server at any time. Multiple calls allowed. Thread safe.
|
||||
void async_ping(std::shared_ptr<stream> self, std::function<ping_func>&& notifier);
|
||||
|
||||
//! Terminate the stream gracefully
|
||||
void async_close(std::shared_ptr<stream> self);
|
||||
} // websocket
|
||||
}} // lwsf // internal
|
||||
@@ -155,10 +155,8 @@ namespace lwsf { namespace internal
|
||||
error_ = error.message();
|
||||
return false;
|
||||
}
|
||||
const boost::lock_guard<boost::mutex> lock{data_.wallet->sync};
|
||||
auto entry = data_.wallet->primary.txes.try_emplace(e->id).first;
|
||||
if (!entry->second)
|
||||
entry->second = std::move(e);
|
||||
|
||||
data_.wallet->add_pending_tx(std::move(e));
|
||||
}
|
||||
}
|
||||
catch(const std::exception& e)
|
||||
|
||||
+57
-12
@@ -59,6 +59,7 @@
|
||||
#include "net/http.h"
|
||||
#include "net/net_parse_helpers.h" // monero/contrib/epee/include
|
||||
#include "net/parse.h" // monero/src
|
||||
#include "net/websocket.h"
|
||||
#include "numeric.h"
|
||||
#include "pending_transaction.h"
|
||||
#include "subaddress_account.h"
|
||||
@@ -304,6 +305,12 @@ namespace lwsf { namespace internal
|
||||
self->set_error(error, false);
|
||||
}
|
||||
|
||||
void wallet::frame::async_clear_error::operator()(const std::error_code error) const
|
||||
{
|
||||
LWSF_VERIFY(self);
|
||||
self->set_error(error, true);
|
||||
}
|
||||
|
||||
bool wallet::set_error(const std::error_code error, const bool clear) const
|
||||
{
|
||||
return status_->set_error(error, clear);
|
||||
@@ -366,10 +373,16 @@ namespace lwsf { namespace internal
|
||||
{
|
||||
void operator()(wallet* val) const
|
||||
{
|
||||
if (!val)
|
||||
return;
|
||||
const boost::lock_guard<boost::mutex> lock{val->status_->sync_};
|
||||
val->status_->thread_state_ = state::stop;
|
||||
LWSF_VERIFY(val && val->data_.wallet);
|
||||
{
|
||||
const boost::lock_guard<boost::mutex> lock{val->status_->sync_};
|
||||
val->status_->thread_state_ = state::stop;
|
||||
}
|
||||
{
|
||||
const boost::lock_guard<boost::mutex> lock{val->data_.wallet->sync};
|
||||
val->data_.wallet->on_feed_refresh = nullptr;
|
||||
val->data_.wallet->on_feed_fail = nullptr;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -445,12 +458,22 @@ namespace lwsf { namespace internal
|
||||
const std::unique_ptr<wallet, set_stop_> set_stop{this};
|
||||
try
|
||||
{
|
||||
LWSF_VERIFY(data_.wallet);
|
||||
|
||||
std::shared_ptr<boost::asio::steady_timer> timer;
|
||||
boost::unique_lock<boost::mutex> lock{status_->sync_};
|
||||
if (status_->thread_state_ != state::stop)
|
||||
{
|
||||
timer = std::make_shared<boost::asio::steady_timer>(data_.context->io_);
|
||||
backend::wallet::refresh(data_.wallet, false /* mandatory */, on_refresh{timer, status_, data_.wallet});
|
||||
const boost::lock_guard<boost::mutex> lock_wallet{data_.wallet->sync};
|
||||
if (!data_.wallet->feed_disabled)
|
||||
{
|
||||
data_.wallet->on_feed_fail = on_refresh{timer, status_, data_.wallet};
|
||||
data_.wallet->on_feed_refresh = frame::async_clear_error{status_};
|
||||
backend::wallet::refresh(data_.wallet, false /* mandatory */, frame::async_clear_error{status_});
|
||||
}
|
||||
else
|
||||
backend::wallet::refresh(data_.wallet, false /* mandatory */, on_refresh{timer, status_, data_.wallet});
|
||||
}
|
||||
do
|
||||
{
|
||||
@@ -688,7 +711,12 @@ namespace lwsf { namespace internal
|
||||
status_->exception_error_ = "view_key, spend_key, and address_string do not match";
|
||||
}
|
||||
|
||||
wallet::~wallet() { stop_refresh_loop(); }
|
||||
wallet::~wallet()
|
||||
{
|
||||
stop_refresh_loop();
|
||||
if (data_.wallet)
|
||||
data_.wallet->shutdown();
|
||||
}
|
||||
|
||||
void wallet::add_subaddress(const std::uint32_t accountIndex, std::string label)
|
||||
{
|
||||
@@ -870,7 +898,7 @@ namespace lwsf { namespace internal
|
||||
return false;
|
||||
|
||||
if (!daemon_username.empty() || !daemon_password.empty())
|
||||
throw std::runtime_error{"HTTP loging not supported"};
|
||||
throw std::runtime_error{"HTTP login not supported"};
|
||||
|
||||
// verify cert if `use_ssl == true`, otherwise autodetect if `https`
|
||||
// specified.
|
||||
@@ -909,9 +937,15 @@ namespace lwsf { namespace internal
|
||||
}
|
||||
}
|
||||
|
||||
// backend permanently assumes `/feed` failure even after changing
|
||||
// http server
|
||||
const boost::unique_lock<boost::mutex> lock{data_.wallet->sync};
|
||||
data_.wallet->passed_login = false;
|
||||
data_.wallet->client.init(data_.context->io_, std::move(url.host), std::move(url.uri), boost::numeric_cast<std::uint16_t>(url.port), std::move(options));
|
||||
std::shared_ptr<websocket::stream> feed;
|
||||
feed.swap(data_.wallet->feed);
|
||||
if (feed)
|
||||
websocket::async_close(std::move(feed)); // on_error
|
||||
}
|
||||
catch (const std::exception& e)
|
||||
{
|
||||
@@ -947,7 +981,7 @@ namespace lwsf { namespace internal
|
||||
bool wallet::connectToDaemon()
|
||||
{
|
||||
boost::unique_lock<boost::mutex> lock{data_.wallet->sync};
|
||||
if (data_.wallet->passed_login && data_.wallet->client.is_connected())
|
||||
if (data_.wallet->passed_login && (data_.wallet->feed || data_.wallet->client.is_connected()))
|
||||
return true;
|
||||
|
||||
lock.unlock();
|
||||
@@ -970,7 +1004,8 @@ namespace lwsf { namespace internal
|
||||
Monero::Wallet::ConnectionStatus wallet::connected() const
|
||||
{
|
||||
const boost::lock_guard<boost::mutex> lock{data_.wallet->sync};
|
||||
return data_.wallet->passed_login && data_.wallet->client.is_connected() ?
|
||||
const bool feed_disabled = data_.wallet->feed_disabled;
|
||||
return data_.wallet->passed_login && ((feed_disabled && data_.wallet->client.is_connected()) || (!feed_disabled && data_.wallet->feed)) ?
|
||||
ConnectionStatus_Connected : ConnectionStatus_Disconnected;
|
||||
}
|
||||
|
||||
@@ -1309,12 +1344,22 @@ namespace lwsf { namespace internal
|
||||
unspent_map unspent{};
|
||||
{
|
||||
wait_for<std::error_code>(
|
||||
[this] (auto&& callable) { backend::wallet::refresh(this->data_.wallet, false, std::move(callable)); }
|
||||
[this] (auto&& f) { backend::wallet::refresh(this->data_.wallet, false, std::move(f)); }
|
||||
);
|
||||
|
||||
const boost::lock_guard<boost::mutex> lock{data_.wallet->sync};
|
||||
fee_mask = data_.wallet->fee_mask;
|
||||
boost::unique_lock<boost::mutex> lock{data_.wallet->sync};
|
||||
if (data_.wallet->need_fees()) // '/feed' doesn't fetch fees
|
||||
{
|
||||
lock.unlock();
|
||||
const std::error_code fee_fail = wait_for<std::error_code>(
|
||||
[this] (auto&& f) { backend::wallet::get_fees(this->data_.wallet, std::move(f)); }
|
||||
);
|
||||
if (fee_fail)
|
||||
throw std::system_error{fee_fail, "Tx construction"};
|
||||
lock.lock();
|
||||
}
|
||||
|
||||
fee_mask = data_.wallet->fee_mask;
|
||||
if (data_.wallet->per_byte_fee.empty() || !fee_mask || !data_.wallet->passed_login)
|
||||
throw std::system_error{data_.wallet->refresh_error, "Tx construction"};
|
||||
|
||||
|
||||
+5
-1
@@ -70,7 +70,11 @@ namespace internal
|
||||
void operator()(std::error_code error) const;
|
||||
};
|
||||
|
||||
bool thead_start();
|
||||
struct async_clear_error
|
||||
{
|
||||
std::shared_ptr<frame> self;
|
||||
void operator()(std::error_code) const;
|
||||
};
|
||||
};
|
||||
|
||||
const net::wallet_io data_;
|
||||
|
||||
+826
-16
File diff suppressed because it is too large
Load Diff
@@ -26,6 +26,6 @@
|
||||
# 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.
|
||||
|
||||
add_library(lwsf-unit-net OBJECT http.test.cpp)
|
||||
add_library(lwsf-unit-net OBJECT http.test.cpp push.test.cpp push.test.h)
|
||||
#target_include_directories(lwsf-unit-net PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} "${CMAKE_SOURCE_DIR}/src")
|
||||
target_link_libraries(lwsf-unit-net PRIVATE lwsf-unit-framework lwsf-net monero::libraries)
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
// Copyright (c) 2026, 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 "push.test.h"
|
||||
|
||||
#include <boost/asio/any_io_executor.hpp>
|
||||
#include <boost/asio/bind_executor.hpp>
|
||||
#include <boost/asio/executor.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <boost/beast/websocket/stream.hpp>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "error.h"
|
||||
|
||||
namespace lwsf_test { namespace net { namespace push
|
||||
{
|
||||
struct connection
|
||||
{
|
||||
boost::beast::websocket::stream<socket> ws_;
|
||||
boost::asio::io_context::strand strand_;
|
||||
std::deque<std::pair<std::string, std ::function<pub>>> msgs_;
|
||||
boost::beast::flat_buffer buffer_;
|
||||
|
||||
connection(socket&& sock)
|
||||
: ws_(std::move(sock)), strand_(ws_.next_layer().get_executor().context()), msgs_(), buffer_()
|
||||
{
|
||||
ws_.binary(true);
|
||||
}
|
||||
|
||||
template<typename F>
|
||||
auto bind(F&& f) -> decltype(boost::asio::bind_executor(strand_, std::forward<F>(f)))
|
||||
{
|
||||
return boost::asio::bind_executor(strand_, std::forward<F>(f));
|
||||
}
|
||||
};
|
||||
|
||||
namespace
|
||||
{
|
||||
struct on_read
|
||||
{
|
||||
std::shared_ptr<connection> self_;
|
||||
std::function<sub> read_;
|
||||
void operator()(const boost::system::error_code error, std::size_t)
|
||||
{
|
||||
LWSF_VERIFY(self_ && read_);
|
||||
if (!error)
|
||||
{
|
||||
const auto buf = self_->buffer_.data();
|
||||
std::string out{reinterpret_cast<const char*>(buf.data()), buf.size()};
|
||||
self_->buffer_.consume(buf.size());
|
||||
read_(std::move(out));
|
||||
}
|
||||
else
|
||||
read_(std::error_code{error});
|
||||
}
|
||||
};
|
||||
|
||||
struct on_write
|
||||
{
|
||||
std::shared_ptr<connection> self_;
|
||||
void operator()(const boost::system::error_code error, std::size_t = {})
|
||||
{
|
||||
LWSF_VERIFY(self_ && !self_->msgs_.empty());
|
||||
|
||||
std::pair<std::string, std::function<pub>> msg;
|
||||
msg.swap(self_->msgs_.front());
|
||||
self_->msgs_.pop_front();
|
||||
|
||||
if (!self_->msgs_.empty())
|
||||
start(std::move(self_));
|
||||
|
||||
if (msg.second)
|
||||
msg.second(error);
|
||||
}
|
||||
|
||||
static void start(std::shared_ptr<connection> self)
|
||||
{
|
||||
LWSF_VERIFY(self && !self->msgs_.empty());
|
||||
const std::string& msg = self->msgs_.front().first;
|
||||
self->ws_.async_write(boost::asio::buffer(msg.data(), msg.size()), self->bind(on_write{self}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void async_handshake(socket&& sock, const request& req, std::function<ready> done)
|
||||
{
|
||||
auto self = std::make_shared<connection>(std::move(sock));
|
||||
self->ws_.async_accept(req, self->bind([self, done = std::move(done)] (auto error) {
|
||||
if (error)
|
||||
done(std::error_code{error});
|
||||
else
|
||||
done(self);
|
||||
}));
|
||||
}
|
||||
|
||||
void async_read(std::shared_ptr<connection> self, std::function<sub> read)
|
||||
{
|
||||
LWSF_VERIFY(self);
|
||||
boost::asio::dispatch(self->strand_, [self, read = std::move(read)] () mutable {
|
||||
self->ws_.async_read(self->buffer_, self->bind(on_read{self, std::move(read)}));
|
||||
});
|
||||
}
|
||||
|
||||
void async_pub(std::shared_ptr<connection> self, std::string msg, std::function<pub> sent)
|
||||
{
|
||||
LWSF_VERIFY(self);
|
||||
boost::asio::dispatch(self->strand_, [self, msg = std::move(msg), sent = std::move(sent)] () mutable {
|
||||
const bool empty = self->msgs_.empty();
|
||||
self->msgs_.push_back({std::move(msg), std::move(sent)});
|
||||
if (empty)
|
||||
on_write::start(std::move(self));
|
||||
});
|
||||
}
|
||||
|
||||
void async_close(std::shared_ptr<connection> self)
|
||||
{
|
||||
LWSF_VERIFY(self);
|
||||
boost::asio::dispatch(self->strand_, [self] {
|
||||
self->ws_.async_close(boost::beast::websocket::close_reason(), [] (auto) {});
|
||||
});
|
||||
}
|
||||
}}} // lwsf_test // net // push
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2026, 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/beast/http/message.hpp>
|
||||
#include <boost/beast/http/string_body.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
#include "common/expect.h"
|
||||
|
||||
namespace lwsf_test { namespace net { namespace push
|
||||
{
|
||||
using socket =
|
||||
boost::asio::basic_stream_socket<
|
||||
boost::asio::ip::tcp,
|
||||
boost::asio::io_context::executor_type
|
||||
>;
|
||||
using request = boost::beast::http::request<boost::beast::http::string_body>;
|
||||
|
||||
struct connection;
|
||||
using ready = void(expect<std::shared_ptr<connection>>);
|
||||
using sub = void(expect<std::string>);
|
||||
using pub = void(std::error_code);
|
||||
|
||||
void async_handshake(socket&& sock, const request& req, std::function<ready> done);
|
||||
void async_read(std::shared_ptr<connection> self, std::function<sub> read);
|
||||
void async_pub(std::shared_ptr<connection> self, std::string msg, std::function<pub> sent);
|
||||
void async_close(std::shared_ptr<connection> self);
|
||||
|
||||
}}} // lwsf_test // net // push
|
||||
Reference in New Issue
Block a user