mirror of
https://codeberg.org/wownero/lwsf-wow.git
synced 2026-10-17 05:41:30 +02:00
2103 lines
78 KiB
C++
2103 lines
78 KiB
C++
// Copyright (c) 2025, 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 "framework.test.h"
|
|
#include "backend.h"
|
|
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <boost/asio/coroutine.hpp>
|
|
#include <boost/asio/io_context.hpp>
|
|
#include <boost/asio/ip/tcp.hpp>
|
|
#include <boost/beast/core/flat_buffer.hpp>
|
|
#include <boost/beast/http/message.hpp>
|
|
#include <boost/beast/http/parser.hpp>
|
|
#include <boost/beast/http/read.hpp>
|
|
#include <boost/beast/http/string_body.hpp>
|
|
#include <boost/beast/http/write.hpp>
|
|
#include <boost/beast/websocket/error.hpp>
|
|
#include <boost/optional/optional.hpp>
|
|
#include <boost/thread/lock_guard.hpp>
|
|
#include <boost/thread/mutex.hpp>
|
|
#include <boost/thread/thread.hpp>
|
|
#include <boost/utility/string_view.hpp>
|
|
#include <boost/variant/variant.hpp>
|
|
#include <deque>
|
|
#include <functional>
|
|
#include <ostream>
|
|
#include <random>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <system_error>
|
|
#include <utility>
|
|
|
|
#include "hex.h" // monero/contrib/include
|
|
#include "rapidjson/document.h" // monero/external/rapidjson/include
|
|
#include "rapidjson/error/en.h" // monero/external/rapidjson/include
|
|
#include "rapidjson/stringbuffer.h" // monero/external/rapidjson/incldue
|
|
#include "rapidjson/writer.h" // monero/external/rapidjson/incldue
|
|
#include "ringct/rctOps.h" // monero/src
|
|
#include "net/http.h"
|
|
#include "net/websocket.h"
|
|
#include "net/push.test.h"
|
|
#include "wire/adapted/crypto.h"
|
|
#include "wire/msgpack.h"
|
|
|
|
namespace rapidjson
|
|
{
|
|
std::ostream& operator<<(std::ostream& out, const Document& src)
|
|
{
|
|
StringBuffer buffer;
|
|
Writer<StringBuffer> writer{buffer};
|
|
src.Accept(writer);
|
|
|
|
return out << buffer.GetString();
|
|
}
|
|
}
|
|
|
|
namespace
|
|
{
|
|
using client_request =
|
|
boost::beast::http::request<boost::beast::http::string_body>;
|
|
|
|
template<typename T>
|
|
std::string to_hex(const T& arg)
|
|
{
|
|
return epee::to_hex::string(epee::as_byte_span(arg));
|
|
}
|
|
|
|
template<typename T>
|
|
T from_hex(const boost::string_ref hex)
|
|
{
|
|
T out{};
|
|
if (!epee::from_hex::to_buffer(epee::as_mut_byte_span(out), hex))
|
|
throw std::runtime_error{"invalid hex"};
|
|
return out;
|
|
}
|
|
|
|
void to_msgpack(wire::msgpack_writer& dest, const rapidjson::Value& src)
|
|
{
|
|
switch (src.GetType())
|
|
{
|
|
default:
|
|
case rapidjson::kNullType:
|
|
throw std::logic_error{"unexpected json value - " + std::to_string(src.GetType())};
|
|
case rapidjson::kFalseType:
|
|
dest.boolean(false);
|
|
break;
|
|
case rapidjson::kTrueType:
|
|
dest.boolean(true);
|
|
break;
|
|
case rapidjson::kObjectType:
|
|
dest.start_object(src.MemberCount());
|
|
{
|
|
const auto end = src.MemberEnd();
|
|
for (auto cur = src.MemberBegin(); cur != end; ++cur)
|
|
{
|
|
dest.key({cur->name.GetString(), cur->name.GetStringLength()});
|
|
to_msgpack(dest, cur->value);
|
|
}
|
|
}
|
|
dest.end_object();
|
|
break;
|
|
case rapidjson::kArrayType:
|
|
dest.start_array(src.Size());
|
|
{
|
|
for (std::size_t i = 0; i < src.Size(); ++i)
|
|
to_msgpack(dest, src[i]);
|
|
}
|
|
dest.end_array();
|
|
break;
|
|
case rapidjson::kStringType:
|
|
{
|
|
std::string binary;
|
|
const boost::string_ref str{src.GetString(), src.GetStringLength()};
|
|
if (epee::from_hex::to_string(binary, str))
|
|
dest.binary(epee::to_byte_span(epee::to_span(binary)));
|
|
else
|
|
dest.string(str);
|
|
}
|
|
break;
|
|
case rapidjson::kNumberType:
|
|
if (src.IsUint()) dest.unsigned_integer(src.GetUint());
|
|
else if (src.IsUint64()) dest.unsigned_integer(std::uintmax_t(src.GetUint64()));
|
|
else if (src.IsInt()) dest.integer(src.GetInt());
|
|
else if (src.IsInt64()) dest.integer(std::intmax_t(src.GetInt64()));
|
|
else throw std::logic_error{"unexpected json number"};
|
|
break;
|
|
}
|
|
}
|
|
|
|
std::string to_msgpack(const std::string& src)
|
|
{
|
|
rapidjson::Document ir;
|
|
ir.Parse(src.c_str());
|
|
if (ir.HasParseError())
|
|
throw std::runtime_error{GetParseError_En(ir.GetParseError())};
|
|
|
|
wire::msgpack_slice_writer dest{};
|
|
to_msgpack(dest, ir);
|
|
|
|
const auto bytes = dest.take_sink();
|
|
return std::string{reinterpret_cast<const char*>(bytes.data()), bytes.size()};
|
|
}
|
|
|
|
struct empty_slice
|
|
{
|
|
operator epee::byte_slice() const { return {}; }
|
|
};
|
|
|
|
struct empty_decoys
|
|
{
|
|
operator lwsf::internal::rpc::get_random_outs_request() const { return {}; }
|
|
};
|
|
|
|
struct login
|
|
{
|
|
struct details
|
|
{
|
|
std::string address;
|
|
crypto::secret_key view_key;
|
|
};
|
|
|
|
details account;
|
|
};
|
|
|
|
void read_bytes(wire::reader& src, login::details& self)
|
|
{
|
|
wire::object(src, WIRE_FIELD(address), WIRE_FIELD(view_key));
|
|
}
|
|
|
|
void read_bytes(wire::reader& src, login& self)
|
|
{
|
|
wire::object(src, WIRE_FIELD(account));
|
|
}
|
|
|
|
struct response_loop : boost::asio::coroutine
|
|
{
|
|
struct frame
|
|
{
|
|
lest::env& lest_env;
|
|
lwsf_test::net::push::socket server;
|
|
boost::beast::flat_buffer buffer;
|
|
boost::optional<client_request> request;
|
|
boost::beast::http::response<boost::beast::http::string_body> response;
|
|
std::deque<std::pair<std::string, std::string>> requests;
|
|
std::deque<boost::variant<std::string, boost::beast::http::status>> responses;
|
|
std::shared_ptr<lwsf_test::net::push::connection> ws;
|
|
|
|
explicit frame(lest::env& lest_env, boost::asio::io_context& io)
|
|
: lest_env(lest_env), server(io), buffer(), request(), response(), requests(), responses(), ws()
|
|
{}
|
|
|
|
~frame()
|
|
{
|
|
if (ws)
|
|
lwsf_test::net::push::async_close(std::move(ws));
|
|
}
|
|
};
|
|
|
|
std::shared_ptr<frame> self;
|
|
|
|
explicit response_loop(std::shared_ptr<frame> in)
|
|
: boost::asio::coroutine(), self(in)
|
|
{}
|
|
|
|
void operator()(boost::system::error_code = {}, std::size_t = {})
|
|
{
|
|
auto& lest_env = self->lest_env;
|
|
LWSF_VERIFY(self);
|
|
BOOST_ASIO_CORO_REENTER(*this)
|
|
{
|
|
while (!self->responses.empty())
|
|
{
|
|
self->request.emplace();
|
|
BOOST_ASIO_CORO_YIELD boost::beast::http::async_read(self->server, self->buffer, *self->request, *this);
|
|
|
|
EXPECT(self->request->keep_alive());
|
|
EXPECT(self->requests.size() == self->responses.size());
|
|
EXPECT(self->requests.front().first == self->request->target());
|
|
if (!self->requests.front().second.empty())
|
|
{
|
|
rapidjson::Document expected;
|
|
expected.Parse(self->requests.front().second.c_str());
|
|
EXPECT(!expected.HasParseError());
|
|
|
|
rapidjson::Document actual;
|
|
actual.Parse(self->request->body().c_str());
|
|
EXPECT(!expected.HasParseError());
|
|
|
|
EXPECT(expected == actual);
|
|
}
|
|
self->requests.pop_front();
|
|
|
|
{
|
|
LWSF_VERIFY(!self->responses.empty());
|
|
boost::variant<std::string, boost::beast::http::status> response;
|
|
response.swap(self->responses.front());
|
|
self->responses.pop_front();
|
|
{
|
|
const auto string = boost::get<std::string>(std::addressof(response));
|
|
if (!string)
|
|
{
|
|
const auto status = boost::get<boost::beast::http::status>(response);
|
|
if (status == boost::beast::http::status::switching_protocols)
|
|
{
|
|
auto self_ = self;
|
|
lwsf_test::net::push::async_handshake(std::move(self->server), *self->request, [self_] (auto ws) {
|
|
self_->ws = ws.value();
|
|
});
|
|
return;
|
|
}
|
|
else
|
|
self->response = {status, 11, std::string{}};
|
|
}
|
|
else
|
|
self->response = {boost::beast::http::status::ok, 11, *string};
|
|
}
|
|
}
|
|
self->response.keep_alive(true);
|
|
self->response.prepare_payload();
|
|
|
|
BOOST_ASIO_CORO_YIELD boost::beast::http::async_write(self->server, self->response, *this);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
std::string get_response(const boost::string_view target)
|
|
{
|
|
static constexpr const char get_address_txs[] =
|
|
R"({
|
|
"total_received": "5000",
|
|
"scanned_height": 150,
|
|
"scanned_block_height": 150,
|
|
"start_height": 10,
|
|
"blockchain_height": 200,
|
|
"transactions": []
|
|
})";
|
|
|
|
static constexpr const char get_unspent_outs[] =
|
|
R"({"per_byte_fee": 5, "fee_mask": 100, "amount": "200", "outputs": []})";
|
|
static constexpr const char import_wallet_request[] =
|
|
R"({"lookahead": {"maj_i": 50, "min_i": 200}, "status": "OK", "new_request": true, "request_fulfilled": true})";
|
|
|
|
static constexpr const std::array<std::pair<boost::string_view, boost::string_view>, 10> endpoints =
|
|
{{
|
|
{"/get_address_txs", get_address_txs},
|
|
{"/get_random_outs", R"({"amount_outs": []})"},
|
|
{"/get_subaddrs", R"({"all_subaddrs": []})"},
|
|
{"/get_unspent_outs", get_unspent_outs},
|
|
{"/get_version", R"({"max_subaddresses": 10000})"},
|
|
{"/import_wallet_request", import_wallet_request},
|
|
{"/login", R"({"new_address": true})"},
|
|
{"/provision_subaddrs", R"({"new_subaddrs": [], "all_subaddrs": []})"},
|
|
{"/submit_raw_tx", R"({"status": "OK"})"},
|
|
{"/upsert_subaddrs", "{}"}
|
|
}};
|
|
|
|
const auto found =
|
|
std::lower_bound(endpoints.begin(), endpoints.end(), std::make_pair(target, boost::string_view{}));
|
|
if (found != endpoints.end() && found->first == target)
|
|
return std::string{found->second};
|
|
|
|
throw std::runtime_error{"Unexpected target: " + std::string{target}};
|
|
}
|
|
|
|
struct dummy_server : boost::asio::coroutine
|
|
{
|
|
struct frame
|
|
{
|
|
boost::asio::ip::tcp::socket server;
|
|
boost::beast::flat_buffer buffer;
|
|
boost::optional<client_request> request;
|
|
boost::beast::http::response<boost::beast::http::string_body> response;
|
|
std::mt19937 eng;
|
|
std::uniform_int_distribution<unsigned> dist;
|
|
|
|
explicit frame(boost::asio::io_context& io)
|
|
: server(io), buffer(), request(), response(), eng(crypto::rand<std::uint32_t>()), dist(0, 99)
|
|
{}
|
|
};
|
|
|
|
std::shared_ptr<frame> self;
|
|
|
|
explicit dummy_server(std::shared_ptr<frame> self)
|
|
: boost::asio::coroutine(), self(std::move(self))
|
|
{}
|
|
|
|
void operator()(const boost::system::error_code error, std::size_t = {})
|
|
{
|
|
if (error)
|
|
{
|
|
if (error != boost::asio::error::operation_aborted)
|
|
throw boost::system::system_error{error, "server failure"};
|
|
return;
|
|
}
|
|
|
|
LWSF_VERIFY(self);
|
|
BOOST_ASIO_CORO_REENTER(*this)
|
|
{
|
|
while (true)
|
|
{
|
|
self->request.emplace();
|
|
BOOST_ASIO_CORO_YIELD boost::beast::http::async_read(self->server, self->buffer, *self->request, *this);
|
|
|
|
LWSF_VERIFY(self->request->keep_alive());
|
|
if (self->request->target() == "/feed")
|
|
self->response = {boost::beast::http::status::not_found, 11, ""};
|
|
else if (self->dist(self->eng))
|
|
self->response = {boost::beast::http::status::ok, 11, get_response(self->request->target())};
|
|
else
|
|
self->response = {boost::beast::http::status::internal_server_error, 11, ""};
|
|
self->response.keep_alive(true);
|
|
self->response.prepare_payload();
|
|
|
|
BOOST_ASIO_CORO_YIELD boost::beast::http::async_write(self->server, self->response, *this);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
LWS_CASE("backend::account")
|
|
{
|
|
using subaddrs = lwsf::internal::rpc::subaddrs;
|
|
|
|
SETUP("empty with zero lookahead")
|
|
{
|
|
lwsf::internal::backend::account account{.lookahead = {}};
|
|
account.subaccounts.emplace_back().detail.try_emplace(0);
|
|
|
|
boost::container::flat_map<std::uint32_t, subaddrs> majors{};
|
|
EXPECT(account.needed_subaddresses(majors) == 0);
|
|
|
|
SECTION("standard lookahead")
|
|
{
|
|
account.lookahead = {50, 200};
|
|
EXPECT(account.needed_subaddresses(majors) == 10000);
|
|
|
|
account.subaccounts.back().last = 10;
|
|
EXPECT(account.needed_subaddresses(majors) == 10010);
|
|
|
|
account.subaccounts.emplace_back().last = 10;
|
|
EXPECT(account.needed_subaddresses(majors) == 10220);
|
|
}
|
|
|
|
SECTION("standard lookahead with overlapping custom lookaheads")
|
|
{
|
|
account.lookahead = {50, 200};
|
|
majors.try_emplace(0, subaddrs{}).first->second.value = {{10, 20}, {50, 155}};
|
|
|
|
EXPECT(account.needed_subaddresses(majors) == 10000);
|
|
}
|
|
|
|
SECTION("standard lookahead with non-overlapping custom lookaheads")
|
|
{
|
|
account.lookahead = {50, 200};
|
|
majors.try_emplace(0, subaddrs{}).first->second.value = {{199, 201}};
|
|
majors.try_emplace(50, subaddrs{}).first->second.value = {{199, 201}};
|
|
|
|
EXPECT(account.needed_subaddresses(majors) == 10005);
|
|
}
|
|
}
|
|
}
|
|
|
|
LWS_CASE("backend::wallet (non-websocket)")
|
|
{
|
|
using bwallet = lwsf::internal::backend::wallet;
|
|
using tcp = boost::asio::ip::tcp;
|
|
const auto no_ssl = epee::net_utils::ssl_support_t::e_ssl_support_disabled;
|
|
|
|
SETUP("base empty wallet and basic server")
|
|
{
|
|
boost::asio::io_context io;
|
|
const auto wallet = std::make_shared<lwsf::internal::backend::wallet>(io);
|
|
crypto::generate_keys(wallet->primary.view.pub, wallet->primary.view.sec);
|
|
crypto::generate_keys(wallet->primary.spend.pub, wallet->primary.spend.sec);
|
|
wallet->primary.address = wallet->get_spend_address({0, 0});
|
|
|
|
tcp::acceptor acceptor{io, tcp::endpoint(tcp::v4(), 0)};
|
|
acceptor.listen();
|
|
|
|
SECTION("uninitialized http client")
|
|
{
|
|
using namespace std::placeholders;
|
|
const std::array<std::pair<std::function<void(std::function<void(std::error_code)>)>, std::string>, 6> funcs1
|
|
{{
|
|
{std::bind(bwallet::refresh, wallet, true, _1), "refresh"},
|
|
{std::bind(bwallet::get_fees, wallet, _1), "get_fees"},
|
|
{std::bind(bwallet::register_subaccount, wallet, 0, _1), "register_subaccount"},
|
|
{std::bind(bwallet::register_subaddress, wallet, 0, 0, _1), "register_subaddress"},
|
|
{std::bind(bwallet::set_lookahead, wallet, 0, 0, _1), "set_lookahead"},
|
|
{std::bind(bwallet::send_tx, wallet, empty_slice{}, _1), "send_tx"}
|
|
}};
|
|
|
|
for (const auto& func : funcs1)
|
|
{
|
|
const lest::ctx ctx{lest_env, func.second};
|
|
|
|
std::error_code actual{};
|
|
func.first([&] (auto result) { actual = result; });
|
|
|
|
io.restart();
|
|
io.run();
|
|
|
|
EXPECT(actual == common_error::kInvalidArgument);
|
|
}
|
|
|
|
std::error_code actual = {};
|
|
const auto func = [&](auto result) { actual = result.error(); };
|
|
|
|
bwallet::login_is_new(wallet, func);
|
|
io.restart();
|
|
io.run();
|
|
EXPECT(actual == common_error::kInvalidArgument);
|
|
|
|
bwallet::restore_height_raw(wallet, 0, func);
|
|
io.restart();
|
|
io.run();
|
|
EXPECT(actual == common_error::kInvalidArgument);
|
|
|
|
actual = {};
|
|
bwallet::get_decoys(wallet, lwsf::internal::rpc::get_random_outs_request{}, func);
|
|
io.restart();
|
|
io.run();
|
|
EXPECT(actual == common_error::kInvalidArgument);
|
|
}
|
|
|
|
SECTION("login_is_new")
|
|
{
|
|
const auto frame = std::make_shared<response_loop::frame>(lest_env, io);
|
|
wallet->client.init(io, "127.0.0.1", "", acceptor.local_endpoint().port(), no_ssl);
|
|
|
|
bool done = false;
|
|
expect<bool> result = false;
|
|
const auto record_response = [&] (expect<bool> val) { done = true; result = val; };
|
|
bwallet::login_is_new(wallet, record_response);
|
|
|
|
wallet->passed_login = false;
|
|
acceptor.async_accept(frame->server, response_loop{frame});
|
|
|
|
SECTION("new address -> true")
|
|
{
|
|
frame->requests = {{"/login", ""}, {"/upsert_subaddrs", ""}};
|
|
frame->responses = {R"({"new_address": true})", ""};
|
|
|
|
while (!done)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
|
|
EXPECT(frame->responses.size() == 0);
|
|
EXPECT(result);
|
|
EXPECT(*result);
|
|
EXPECT(wallet->passed_login);
|
|
EXPECT(wallet->primary.restore_height == 0);
|
|
EXPECT(wallet->server_lookahead.major == 0);
|
|
EXPECT(wallet->server_lookahead.minor == 0);
|
|
}
|
|
|
|
SECTION("new address -> true with queued")
|
|
{
|
|
frame->requests = {{"/login", ""}, {"/upsert_subaddrs", ""}};
|
|
frame->responses = {R"({"new_address": true})", ""};
|
|
|
|
bool done2 = true;
|
|
expect<bool> result2 = false;
|
|
bwallet::login_is_new(wallet, [&](auto val) { done2 = true; result2 = val; });
|
|
while (!done || !done2)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
|
|
EXPECT(frame->responses.size() == 0);
|
|
EXPECT(result);
|
|
EXPECT(*result);
|
|
EXPECT(result2);
|
|
EXPECT(*result2);
|
|
EXPECT(wallet->passed_login);
|
|
EXPECT(wallet->primary.restore_height == 0);
|
|
EXPECT(wallet->server_lookahead.major == 0);
|
|
EXPECT(wallet->server_lookahead.minor == 0);
|
|
}
|
|
|
|
SECTION("new address -> false + start_height")
|
|
{
|
|
frame->requests = {{"/login", ""}, {"/upsert_subaddrs", ""}};
|
|
frame->responses = {R"({"new_address": false, "start_height": 100})", ""};
|
|
|
|
while (!done)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
|
|
EXPECT(frame->responses.size() == 0);
|
|
EXPECT(result);
|
|
EXPECT(!*result);
|
|
EXPECT(wallet->passed_login);
|
|
EXPECT(wallet->primary.restore_height == 100);
|
|
EXPECT(wallet->server_lookahead.major == 0);
|
|
EXPECT(wallet->server_lookahead.minor == 0);
|
|
|
|
wallet->primary.restore_height = 0;
|
|
}
|
|
|
|
SECTION("new address -> true with lookahead")
|
|
{
|
|
frame->requests = {{"/login", ""}};
|
|
frame->responses = {
|
|
R"({"new_address": true, "lookahead": {"maj_i": 100, "min_i": 50}})"
|
|
};
|
|
|
|
while (!done)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
|
|
EXPECT(frame->responses.size() == 0);
|
|
EXPECT(result);
|
|
EXPECT(*result);
|
|
EXPECT(wallet->passed_login);
|
|
EXPECT(wallet->primary.restore_height == 0);
|
|
EXPECT(wallet->server_lookahead.major == 100);
|
|
EXPECT(wallet->server_lookahead.minor == 50);
|
|
}
|
|
|
|
SECTION("address is disabled")
|
|
{
|
|
frame->requests = {{"/login", ""}};
|
|
frame->responses = {boost::beast::http::status::forbidden};
|
|
|
|
while (!done)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
|
|
EXPECT(frame->responses.size() == 0);
|
|
EXPECT(result == lwsf::error::approval);
|
|
EXPECT(!wallet->passed_login);
|
|
|
|
}
|
|
|
|
SECTION("404 (bad server entirely)")
|
|
{
|
|
frame->requests = {{"/login", ""}, {"/login", ""}};
|
|
frame->responses = {
|
|
boost::beast::http::status::not_found,
|
|
boost::beast::http::status::not_found
|
|
};
|
|
|
|
while (!done)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
|
|
EXPECT(frame->responses.size() == 0);
|
|
EXPECT(!result);
|
|
EXPECT(result == lwsf::internal::http::error(404));
|
|
EXPECT(!wallet->passed_login);
|
|
}
|
|
}
|
|
|
|
SECTION("refresh")
|
|
{
|
|
const auto frame = std::make_shared<response_loop::frame>(lest_env, io);
|
|
wallet->client.init(io, "127.0.0.1", "", acceptor.local_endpoint().port(), no_ssl);
|
|
|
|
bool done = false;
|
|
std::error_code result{};
|
|
const auto record_response = [&] (auto val) { done = true; result = val; };
|
|
bwallet::refresh(wallet, true, record_response);
|
|
|
|
wallet->passed_login = false;
|
|
acceptor.async_accept(frame->server, response_loop{frame});
|
|
|
|
SECTION("account is disabled")
|
|
{
|
|
frame->requests = {{"/login", ""}};
|
|
frame->responses = {boost::beast::http::status::forbidden};
|
|
|
|
while (!done)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
|
|
EXPECT(frame->responses.size() == 0);
|
|
EXPECT(result == lwsf::error::approval);
|
|
EXPECT(!wallet->passed_login);
|
|
EXPECT(wallet->refresh_error == lwsf::error::approval);
|
|
}
|
|
|
|
SECTION("get_address_txs unexpectedly fails with queued")
|
|
{
|
|
frame->requests = {
|
|
{"/login", ""},
|
|
{"/upsert_subaddrs", ""},
|
|
{"/feed", ""},
|
|
{"/get_address_txs", ""}
|
|
};
|
|
frame->responses = {
|
|
R"({"new_address": true})",
|
|
"",
|
|
boost::beast::http::status::not_found,
|
|
boost::beast::http::status::forbidden
|
|
};
|
|
|
|
bool done2 = false;
|
|
std::error_code result2{};
|
|
bwallet::refresh(wallet, true, [&] (auto val) { done2 = true; result2 = val; });
|
|
|
|
while (!done || !done2)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
|
|
EXPECT(frame->responses.size() == 0);
|
|
EXPECT(result == lwsf::error::approval);
|
|
EXPECT(result2 == lwsf::error::approval);
|
|
EXPECT(!wallet->passed_login);
|
|
EXPECT(wallet->refresh_error == lwsf::error::approval);
|
|
}
|
|
|
|
SECTION("0.3 server")
|
|
{
|
|
frame->requests = {
|
|
{"/login", ""},
|
|
{"/upsert_subaddrs", ""},
|
|
{"/feed", ""},
|
|
{"/get_address_txs", ""},
|
|
{"/get_unspent_outs", ""},
|
|
{"/get_version", ""}
|
|
};
|
|
frame->responses = {
|
|
R"({"new_address": true})",
|
|
"",
|
|
boost::beast::http::status::not_found,
|
|
R"({
|
|
"total_received": "5000",
|
|
"scanned_height": 150,
|
|
"scanned_block_height": 150,
|
|
"start_height": 10,
|
|
"blockchain_height": 200,
|
|
"transactions": [{
|
|
"id": 1,
|
|
"hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1",
|
|
"total_received": "5000",
|
|
"total_sent": "10000",
|
|
"unlock_time": 0,
|
|
"spent_outputs": [{
|
|
"amount": "10000",
|
|
"key_image": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee2",
|
|
"tx_pub_key": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee3",
|
|
"out_index": 0,
|
|
"mixin": 15
|
|
}],
|
|
"coinbase": false,
|
|
"mempool": true,
|
|
"mixin": 15
|
|
}]
|
|
})",
|
|
R"({
|
|
"per_byte_fee": 5,
|
|
"fee_mask": 100,
|
|
"amount": "200",
|
|
"outputs": [{
|
|
"amount": "5000",
|
|
"index": 5,
|
|
"global_index": "50000",
|
|
"rct":
|
|
"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee4",
|
|
"tx_hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1",
|
|
"tx_prefix_hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee5",
|
|
"public_key": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee6",
|
|
"tx_pub_key": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee3",
|
|
"spend_key_images": ["deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee2"],
|
|
"timestamp": "2026-01-01T00:00:00Z",
|
|
"height": 0
|
|
}]
|
|
})",
|
|
boost::beast::http::status::not_found
|
|
};
|
|
|
|
while (!done)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
|
|
const lwsf::internal::rpc::address_meta recipient{0, 0};
|
|
|
|
EXPECT(frame->responses.size() == 0);
|
|
EXPECT(result == lwsf::error::subaddr_upgrade);
|
|
EXPECT(wallet->passed_login);
|
|
EXPECT(wallet->refresh_error == lwsf::error::subaddr_upgrade);
|
|
EXPECT(wallet->lookahead_error == lwsf::error::subaddr_upgrade);
|
|
EXPECT(wallet->primary.restore_height == 10);
|
|
EXPECT(wallet->primary.scan_height == 150);
|
|
EXPECT(wallet->primary.requested_start == 10);
|
|
EXPECT(wallet->blockchain_height == 200);
|
|
EXPECT(wallet->fee_mask == 100);
|
|
EXPECT(wallet->per_byte_fee.size() == 1);
|
|
EXPECT(wallet->per_byte_fee.at(0) == 5);
|
|
EXPECT(wallet->primary.txes.size() == 1);
|
|
auto tx = wallet->primary.txes.find(from_hex<crypto::hash>("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1"));
|
|
EXPECT(tx != wallet->primary.txes.end());
|
|
EXPECT(tx->second != nullptr);
|
|
EXPECT(tx->second->spends.size() == 0);
|
|
EXPECT(tx->second->receives.size() == 1);
|
|
EXPECT(tx->second->receives.nth(0)->first == from_hex<crypto::public_key>("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee6"));
|
|
EXPECT(tx->second->receives.nth(0)->second.tx_pub == from_hex<crypto::public_key>("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee3"));
|
|
EXPECT(tx->second->receives.nth(0)->second.amount == 5000);
|
|
EXPECT(tx->second->receives.nth(0)->second.recipient == recipient);
|
|
EXPECT(tx->second->receives.nth(0)->second.index == 5);
|
|
EXPECT(tx->second->receives.nth(0)->second.global_index == 50000);
|
|
EXPECT(bool(tx->second->receives.nth(0)->second.rct_mask));
|
|
EXPECT(wallet->primary.subaccounts.size() == 1);
|
|
EXPECT(wallet->primary.subaccounts.at(0).last == 0);
|
|
}
|
|
|
|
SECTION("1.0 server with good lookahead")
|
|
{
|
|
frame->requests = {
|
|
{"/login", ""},
|
|
{"/feed", ""},
|
|
{"/get_address_txs", ""},
|
|
{"/get_unspent_outs", ""},
|
|
};
|
|
frame->responses = {
|
|
R"({"new_address": true, "lookahead": {"maj_i": 50, "min_i": 200}})",
|
|
boost::beast::http::status::not_found,
|
|
R"({
|
|
"total_received": "4000",
|
|
"scanned_height": 175,
|
|
"scanned_block_height": 175,
|
|
"start_height": 10,
|
|
"blockchain_height": 210,
|
|
"lookahead": {"maj_i": 50, "min_i": 200},
|
|
"transactions": [{
|
|
"id": 1,
|
|
"hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1",
|
|
"total_received": "4000",
|
|
"total_sent": "10000",
|
|
"unlock_time": 0,
|
|
"spent_outputs": [{
|
|
"amount": "10000",
|
|
"key_image": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee2",
|
|
"tx_pub_key": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee3",
|
|
"out_index": 0,
|
|
"mixin": 15
|
|
}],
|
|
"coinbase": false,
|
|
"mempool": true,
|
|
"mixin": 15
|
|
}]
|
|
})",
|
|
R"({
|
|
"per_byte_fee": 5,
|
|
"fee_mask": 100,
|
|
"fees": [110, 500, 700],
|
|
"amount": "4000",
|
|
"outputs": [{
|
|
"amount": "4000",
|
|
"index": 0,
|
|
"global_index": "60000",
|
|
"rct":
|
|
"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee4",
|
|
"tx_hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1",
|
|
"tx_prefix_hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee5",
|
|
"public_key": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee6",
|
|
"tx_pub_key": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee3",
|
|
"spend_key_images": ["deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee2"],
|
|
"timestamp": "2026-01-01T00:00:00Z",
|
|
"height": 0,
|
|
"recipient": {"maj_i": 1, "min_i": 2}
|
|
}]
|
|
})"
|
|
};
|
|
|
|
while (!done)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
|
|
const lwsf::internal::rpc::address_meta recipient{1, 2};
|
|
|
|
EXPECT(frame->responses.size() == 0);
|
|
EXPECT(result == std::error_code{});
|
|
EXPECT(wallet->passed_login);
|
|
EXPECT(wallet->refresh_error == std::error_code{});
|
|
EXPECT(wallet->lookahead_error == std::error_code{});
|
|
EXPECT(wallet->subaddress_error == std::error_code{});
|
|
EXPECT(wallet->import_error == std::error_code{});
|
|
EXPECT(wallet->primary.restore_height == 10);
|
|
EXPECT(wallet->primary.scan_height == 175);
|
|
EXPECT(wallet->primary.requested_start == 10);
|
|
EXPECT(wallet->blockchain_height == 210);
|
|
EXPECT(wallet->fee_mask == 100);
|
|
EXPECT(wallet->per_byte_fee.size() == 3);
|
|
EXPECT(wallet->per_byte_fee.at(0) == 110);
|
|
EXPECT(wallet->per_byte_fee.at(1) == 500);
|
|
EXPECT(wallet->per_byte_fee.at(2) == 700);
|
|
EXPECT(wallet->primary.txes.size() == 1);
|
|
auto tx = wallet->primary.txes.find(from_hex<crypto::hash>("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1"));
|
|
EXPECT(tx != wallet->primary.txes.end());
|
|
EXPECT(tx->second != nullptr);
|
|
EXPECT(tx->second->spends.size() == 0);
|
|
EXPECT(tx->second->receives.size() == 1);
|
|
EXPECT(tx->second->receives.nth(0)->first == from_hex<crypto::public_key>("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee6"));
|
|
EXPECT(tx->second->receives.nth(0)->second.tx_pub == from_hex<crypto::public_key>("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee3"));
|
|
EXPECT(tx->second->receives.nth(0)->second.amount == 4000);
|
|
EXPECT(tx->second->receives.nth(0)->second.recipient == recipient);
|
|
EXPECT(tx->second->receives.nth(0)->second.index == 0);
|
|
EXPECT(tx->second->receives.nth(0)->second.global_index == 60000);
|
|
EXPECT(bool(tx->second->receives.nth(0)->second.rct_mask));
|
|
EXPECT(wallet->primary.subaccounts.size() == 2);
|
|
EXPECT(wallet->primary.subaccounts.at(0).last == 0);
|
|
EXPECT(wallet->primary.subaccounts.at(1).last == 2);
|
|
}
|
|
|
|
SECTION("1.0 server with lookahead fail and limited max subaddresses")
|
|
{
|
|
std::string subs;
|
|
for (unsigned i = 0; i < 50; ++i)
|
|
subs += R"({"key":)" + std::to_string(i) + R"(, "value": [[0, 199]]},)";
|
|
subs.pop_back();
|
|
|
|
const std::string upsert_body =
|
|
R"({"address":")" + wallet->get_spend_address({0, 0}) +
|
|
R"(","view_key":")" + to_hex(unwrap(unwrap(wallet->primary.view.sec))) +
|
|
R"(","subaddrs":[{"key":1,"value":[[0,2]]}],"get_all":false})";
|
|
|
|
rct::key ringct{};
|
|
crypto::key_image image{};
|
|
lwsf::internal::backend::keypair tx_key{};
|
|
lwsf::internal::backend::keypair output_key{};
|
|
crypto::generate_keys(tx_key.pub, tx_key.sec);
|
|
{
|
|
crypto::secret_key scalar{};
|
|
crypto::key_derivation derived{};
|
|
EXPECT(crypto::generate_key_derivation(tx_key.pub, wallet->primary.view.sec, derived));
|
|
EXPECT(crypto::derive_public_key(derived, 3, wallet->primary.spend.pub, output_key.pub));
|
|
crypto::derive_secret_key(derived, 3, wallet->primary.spend.sec, output_key.sec);
|
|
crypto::generate_key_image(output_key.pub, output_key.sec, image);
|
|
|
|
crypto::derivation_to_scalar(derived, 3, scalar);
|
|
|
|
rct::ecdhTuple commitment{};
|
|
rct::ecdhDecode(commitment, rct::sk2rct(scalar), true);
|
|
ringct = commitment.mask;
|
|
}
|
|
|
|
EXPECT(wallet->primary.subaccounts.size() == 2); // from last section
|
|
frame->requests = {
|
|
{"/login", ""},
|
|
{"/upsert_subaddrs", upsert_body},
|
|
{"/feed", ""},
|
|
{"/get_address_txs", ""},
|
|
{"/get_unspent_outs", ""},
|
|
{"/get_version", ""},
|
|
{"/get_subaddrs", ""}
|
|
};
|
|
frame->responses = {
|
|
R"({"new_address": true, "lookahead": {"maj_i": 50, "min_i": 200}})",
|
|
"",
|
|
boost::beast::http::status::not_found,
|
|
R"({
|
|
"total_received": "4000",
|
|
"scanned_height": 175,
|
|
"scanned_block_height": 175,
|
|
"start_height": 5,
|
|
"blockchain_height": 210,
|
|
"lookahead": {"maj_i": 50, "min_i": 200},
|
|
"lookahead_fail": 150,
|
|
"transactions": [{
|
|
"id": 1,
|
|
"hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1",
|
|
"total_received": "4000",
|
|
"total_sent": "10000",
|
|
"unlock_time": 0,
|
|
"spent_outputs": [{
|
|
"amount": "10000",
|
|
"key_image": ")" + to_hex(image) + R"(",
|
|
"tx_pub_key": ")" + to_hex(tx_key.pub) + R"(",
|
|
"out_index": 3,
|
|
"mixin": 15
|
|
}],
|
|
"coinbase": false,
|
|
"mempool": true,
|
|
"mixin": 15
|
|
}]
|
|
})",
|
|
R"({
|
|
"per_byte_fee": 5,
|
|
"fee_mask": 100,
|
|
"fees": [110, 500, 700],
|
|
"amount": "4000",
|
|
"lookahead_fail": 150,
|
|
"outputs": [{
|
|
"amount": "4000",
|
|
"index": 3,
|
|
"global_index": "60000",
|
|
"rct": "recomputed",
|
|
"tx_hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1",
|
|
"tx_prefix_hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee5",
|
|
"public_key": ")" + to_hex(output_key.pub) + R"(",
|
|
"tx_pub_key": ")" + to_hex(tx_key.pub) + R"(",
|
|
"spend_key_images": [")" + to_hex(image) + R"("],
|
|
"timestamp": "2026-01-01T00:00:00Z",
|
|
"height": 150,
|
|
"recipient": {"maj_i": 1, "min_i": 2}
|
|
}]
|
|
})",
|
|
R"({"max_subaddresses": 10000})",
|
|
R"({"all_subaddrs": [)" + subs + "]}"
|
|
};
|
|
|
|
while (!done)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
|
|
const lwsf::internal::rpc::address_meta sender{0, 0};
|
|
const lwsf::internal::rpc::address_meta recipient{1, 2};
|
|
|
|
EXPECT(frame->responses.size() == 0);
|
|
EXPECT(result == lwsf::error::subaddr_ahead);
|
|
EXPECT(wallet->passed_login);
|
|
EXPECT(wallet->refresh_error == lwsf::error::subaddr_ahead);
|
|
EXPECT(wallet->lookahead_error == lwsf::error::subaddr_ahead);
|
|
EXPECT(wallet->subaddress_error == std::error_code{});
|
|
EXPECT(wallet->import_error == std::error_code{});
|
|
EXPECT(wallet->primary.restore_height == 5);
|
|
EXPECT(wallet->primary.scan_height == 175);
|
|
EXPECT(wallet->primary.requested_start == 5);
|
|
EXPECT(wallet->blockchain_height == 210);
|
|
EXPECT(wallet->fee_mask == 100);
|
|
EXPECT(wallet->per_byte_fee.size() == 3);
|
|
EXPECT(wallet->per_byte_fee.at(0) == 110);
|
|
EXPECT(wallet->per_byte_fee.at(1) == 500);
|
|
EXPECT(wallet->per_byte_fee.at(2) == 700);
|
|
EXPECT(wallet->primary.txes.size() == 1);
|
|
auto tx = wallet->primary.txes.find(from_hex<crypto::hash>("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1"));
|
|
EXPECT(tx != wallet->primary.txes.end());
|
|
EXPECT(tx->second != nullptr);
|
|
EXPECT(tx->second->spends.size() == 1);
|
|
EXPECT(tx->second->spends.nth(0)->first == image);
|
|
EXPECT(tx->second->spends.nth(0)->second.amount == 10000);
|
|
EXPECT(tx->second->spends.nth(0)->second.sender == sender);
|
|
EXPECT(tx->second->spends.nth(0)->second.tx_pub == tx_key.pub);
|
|
EXPECT(tx->second->spends.nth(0)->second.output_pub == output_key.pub);
|
|
EXPECT(tx->second->receives.size() == 1);
|
|
EXPECT(tx->second->receives.nth(0)->first == output_key.pub);
|
|
EXPECT(tx->second->receives.nth(0)->second.tx_pub == tx_key.pub);
|
|
EXPECT(tx->second->receives.nth(0)->second.amount == 4000);
|
|
EXPECT(tx->second->receives.nth(0)->second.recipient == recipient);
|
|
EXPECT(tx->second->receives.nth(0)->second.index == 3);
|
|
EXPECT(tx->second->receives.nth(0)->second.global_index == 60000);
|
|
EXPECT(bool(tx->second->receives.nth(0)->second.rct_mask));
|
|
EXPECT(*tx->second->receives.nth(0)->second.rct_mask == ringct);
|
|
EXPECT(wallet->primary.subaccounts.size() == 2);
|
|
EXPECT(wallet->primary.subaccounts.at(0).last == 0);
|
|
EXPECT(wallet->primary.subaccounts.at(1).last == 2);
|
|
}
|
|
|
|
SECTION("1.0 server with lookahead fail and max subaddress availability")
|
|
{
|
|
std::string subs;
|
|
for (unsigned i = 0; i < 50; ++i)
|
|
subs += R"({"key":)" + std::to_string(i) + R"(, "value": [[0, 199]]},)";
|
|
subs.pop_back();
|
|
|
|
const std::string upsert_body =
|
|
R"({"address":")" + wallet->get_spend_address({0, 0}) +
|
|
R"(","view_key":")" + to_hex(unwrap(unwrap(wallet->primary.view.sec))) +
|
|
R"(","subaddrs":[{"key":1,"value":[[0,2]]}],"get_all":false})";
|
|
|
|
const std::string import_body =
|
|
R"({"address":")" + wallet->get_spend_address({0, 0}) +
|
|
R"(","view_key":")" + to_hex(unwrap(unwrap(wallet->primary.view.sec))) +
|
|
R"(","from_height":150,"lookahead":{"maj_i":50,"min_i":200}})";
|
|
|
|
rct::key ringct{};
|
|
crypto::key_image image{};
|
|
lwsf::internal::backend::keypair tx_key{};
|
|
lwsf::internal::backend::keypair output_key{};
|
|
crypto::generate_keys(tx_key.pub, tx_key.sec);
|
|
{
|
|
crypto::secret_key scalar{};
|
|
crypto::key_derivation derived{};
|
|
EXPECT(crypto::generate_key_derivation(tx_key.pub, wallet->primary.view.sec, derived));
|
|
EXPECT(crypto::derive_public_key(derived, 3, wallet->primary.spend.pub, output_key.pub));
|
|
crypto::derive_secret_key(derived, 3, wallet->primary.spend.sec, output_key.sec);
|
|
crypto::generate_key_image(output_key.pub, output_key.sec, image);
|
|
|
|
crypto::derivation_to_scalar(derived, 3, scalar);
|
|
|
|
rct::ecdhTuple commitment{};
|
|
rct::ecdhDecode(commitment, rct::sk2rct(scalar), true);
|
|
ringct = commitment.mask;
|
|
}
|
|
|
|
EXPECT(wallet->primary.subaccounts.size() == 2); // from last section
|
|
frame->requests = {
|
|
{"/login", ""},
|
|
{"/upsert_subaddrs", upsert_body},
|
|
{"/feed", ""},
|
|
{"/get_address_txs", ""},
|
|
{"/get_unspent_outs", ""},
|
|
{"/get_version", ""},
|
|
{"/get_subaddrs", ""},
|
|
{"/import_wallet_request", import_body}
|
|
};
|
|
frame->responses = {
|
|
R"({"new_address": true, "lookahead": {"maj_i": 50, "min_i": 200}})",
|
|
"",
|
|
boost::beast::http::status::not_found,
|
|
R"({
|
|
"total_received": "4000",
|
|
"scanned_height": 175,
|
|
"scanned_block_height": 175,
|
|
"start_height": 5,
|
|
"blockchain_height": 210,
|
|
"lookahead": {"maj_i": 50, "min_i": 200},
|
|
"lookahead_fail": 150,
|
|
"transactions": [{
|
|
"id": 1,
|
|
"hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1",
|
|
"total_received": "4000",
|
|
"total_sent": "10000",
|
|
"unlock_time": 0,
|
|
"spent_outputs": [{
|
|
"amount": "10000",
|
|
"key_image": ")" + to_hex(image) + R"(",
|
|
"tx_pub_key": ")" + to_hex(tx_key.pub) + R"(",
|
|
"out_index": 3,
|
|
"mixin": 15
|
|
}],
|
|
"coinbase": false,
|
|
"mempool": true,
|
|
"mixin": 15
|
|
}]
|
|
})",
|
|
R"({
|
|
"per_byte_fee": 5,
|
|
"fee_mask": 100,
|
|
"fees": [110, 500, 700],
|
|
"amount": "4000",
|
|
"lookahead_fail": 150,
|
|
"outputs": [{
|
|
"amount": "4000",
|
|
"index": 3,
|
|
"global_index": "60000",
|
|
"rct": "recomputed",
|
|
"tx_hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1",
|
|
"tx_prefix_hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee5",
|
|
"public_key": ")" + to_hex(output_key.pub) + R"(",
|
|
"tx_pub_key": ")" + to_hex(tx_key.pub) + R"(",
|
|
"spend_key_images": [")" + to_hex(image) + R"("],
|
|
"timestamp": "2026-01-01T00:00:00Z",
|
|
"height": 150,
|
|
"recipient": {"maj_i": 1, "min_i": 2}
|
|
}]
|
|
})",
|
|
R"({"max_subaddresses": 30000})",
|
|
R"({"all_subaddrs": [)" + subs + "]}",
|
|
R"({"lookahead": {"maj_i": 50, "min_i": 200}, "status": "OK", "new_request": true, "request_fulfilled": true})"
|
|
};
|
|
|
|
while (!done)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
|
|
const lwsf::internal::rpc::address_meta sender{0, 0};
|
|
const lwsf::internal::rpc::address_meta recipient{1, 2};
|
|
|
|
EXPECT(frame->responses.size() == 0);
|
|
EXPECT(result == std::error_code{});
|
|
EXPECT(wallet->passed_login);
|
|
EXPECT(wallet->refresh_error == std::error_code{});
|
|
EXPECT(wallet->lookahead_error == std::error_code{});
|
|
EXPECT(wallet->subaddress_error == std::error_code{});
|
|
EXPECT(wallet->import_error == std::error_code{});
|
|
EXPECT(wallet->primary.restore_height == 5);
|
|
EXPECT(wallet->primary.scan_height == 175);
|
|
EXPECT(wallet->primary.requested_start == 5);
|
|
EXPECT(wallet->blockchain_height == 210);
|
|
EXPECT(wallet->fee_mask == 100);
|
|
EXPECT(wallet->per_byte_fee.size() == 3);
|
|
EXPECT(wallet->per_byte_fee.at(0) == 110);
|
|
EXPECT(wallet->per_byte_fee.at(1) == 500);
|
|
EXPECT(wallet->per_byte_fee.at(2) == 700);
|
|
EXPECT(wallet->primary.txes.size() == 1);
|
|
auto tx = wallet->primary.txes.find(from_hex<crypto::hash>("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1"));
|
|
EXPECT(tx != wallet->primary.txes.end());
|
|
EXPECT(tx->second != nullptr);
|
|
EXPECT(tx->second->spends.size() == 1);
|
|
EXPECT(tx->second->spends.nth(0)->first == image);
|
|
EXPECT(tx->second->spends.nth(0)->second.amount == 10000);
|
|
EXPECT(tx->second->spends.nth(0)->second.sender == sender);
|
|
EXPECT(tx->second->spends.nth(0)->second.tx_pub == tx_key.pub);
|
|
EXPECT(tx->second->spends.nth(0)->second.output_pub == output_key.pub);
|
|
EXPECT(tx->second->receives.size() == 1);
|
|
EXPECT(tx->second->receives.nth(0)->first == output_key.pub);
|
|
EXPECT(tx->second->receives.nth(0)->second.tx_pub == tx_key.pub);
|
|
EXPECT(tx->second->receives.nth(0)->second.amount == 4000);
|
|
EXPECT(tx->second->receives.nth(0)->second.recipient == recipient);
|
|
EXPECT(tx->second->receives.nth(0)->second.index == 3);
|
|
EXPECT(tx->second->receives.nth(0)->second.global_index == 60000);
|
|
EXPECT(bool(tx->second->receives.nth(0)->second.rct_mask));
|
|
EXPECT(*tx->second->receives.nth(0)->second.rct_mask == ringct);
|
|
EXPECT(wallet->primary.subaccounts.size() == 2);
|
|
EXPECT(wallet->primary.subaccounts.at(0).last == 0);
|
|
EXPECT(wallet->primary.subaccounts.at(1).last == 2);
|
|
}
|
|
|
|
SECTION("1.0 server with lookahead fail and new start")
|
|
{
|
|
wallet->primary.requested_start = 1;
|
|
|
|
std::string subs;
|
|
for (unsigned i = 0; i < 50; ++i)
|
|
subs += R"({"key":)" + std::to_string(i) + R"(, "value": [[0, 199]]},)";
|
|
subs.pop_back();
|
|
|
|
const std::string upsert_body =
|
|
R"({"address":")" + wallet->get_spend_address({0, 0}) +
|
|
R"(","view_key":")" + to_hex(unwrap(unwrap(wallet->primary.view.sec))) +
|
|
R"(","subaddrs":[{"key":1,"value":[[0,2]]}],"get_all":false})";
|
|
|
|
const std::string import_body =
|
|
R"({"address":")" + wallet->get_spend_address({0, 0}) +
|
|
R"(","view_key":")" + to_hex(unwrap(unwrap(wallet->primary.view.sec))) +
|
|
R"(","from_height":1,"lookahead":{"maj_i":50,"min_i":200}})";
|
|
|
|
rct::key ringct{};
|
|
crypto::key_image image{};
|
|
lwsf::internal::backend::keypair tx_key{};
|
|
lwsf::internal::backend::keypair output_key{};
|
|
crypto::generate_keys(tx_key.pub, tx_key.sec);
|
|
{
|
|
crypto::secret_key scalar{};
|
|
crypto::key_derivation derived{};
|
|
EXPECT(crypto::generate_key_derivation(tx_key.pub, wallet->primary.view.sec, derived));
|
|
EXPECT(crypto::derive_public_key(derived, 3, wallet->primary.spend.pub, output_key.pub));
|
|
crypto::derive_secret_key(derived, 3, wallet->primary.spend.sec, output_key.sec);
|
|
crypto::generate_key_image(output_key.pub, output_key.sec, image);
|
|
|
|
crypto::derivation_to_scalar(derived, 3, scalar);
|
|
|
|
rct::ecdhTuple commitment{};
|
|
rct::ecdhDecode(commitment, rct::sk2rct(scalar), true);
|
|
ringct = commitment.mask;
|
|
}
|
|
|
|
EXPECT(wallet->primary.subaccounts.size() == 2); // from last section
|
|
frame->requests = {
|
|
{"/login", ""},
|
|
{"/upsert_subaddrs", upsert_body},
|
|
{"/feed", ""},
|
|
{"/get_address_txs", ""},
|
|
{"/get_unspent_outs", ""},
|
|
{"/import_wallet_request", import_body}
|
|
};
|
|
frame->responses = {
|
|
R"({"new_address": true, "lookahead": {"maj_i": 50, "min_i": 200}})",
|
|
"",
|
|
boost::beast::http::status::not_found,
|
|
R"({
|
|
"total_received": "4000",
|
|
"scanned_height": 175,
|
|
"scanned_block_height": 175,
|
|
"start_height": 5,
|
|
"blockchain_height": 210,
|
|
"lookahead": {"maj_i": 50, "min_i": 200},
|
|
"lookahead_fail": 150,
|
|
"transactions": [{
|
|
"id": 1,
|
|
"hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1",
|
|
"total_received": "4000",
|
|
"total_sent": "10000",
|
|
"unlock_time": 0,
|
|
"spent_outputs": [{
|
|
"amount": "10000",
|
|
"key_image": ")" + to_hex(image) + R"(",
|
|
"tx_pub_key": ")" + to_hex(tx_key.pub) + R"(",
|
|
"out_index": 3,
|
|
"mixin": 15
|
|
}],
|
|
"coinbase": false,
|
|
"mempool": true,
|
|
"mixin": 15
|
|
}]
|
|
})",
|
|
R"({
|
|
"per_byte_fee": 5,
|
|
"fee_mask": 100,
|
|
"fees": [110, 500, 700],
|
|
"amount": "4000",
|
|
"lookahead_fail": 150,
|
|
"outputs": [{
|
|
"amount": "4000",
|
|
"index": 3,
|
|
"global_index": "60000",
|
|
"rct": "recomputed",
|
|
"tx_hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1",
|
|
"tx_prefix_hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee5",
|
|
"public_key": ")" + to_hex(output_key.pub) + R"(",
|
|
"tx_pub_key": ")" + to_hex(tx_key.pub) + R"(",
|
|
"spend_key_images": [")" + to_hex(image) + R"("],
|
|
"timestamp": "2026-01-01T00:00:00Z",
|
|
"height": 150,
|
|
"recipient": {"maj_i": 1, "min_i": 2}
|
|
}]
|
|
})",
|
|
R"({"lookahead": {"maj_i": 50, "min_i": 200}, "status": "OK", "new_request": true, "request_fulfilled": true})"
|
|
};
|
|
|
|
while (!done)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
|
|
const lwsf::internal::rpc::address_meta sender{0, 0};
|
|
const lwsf::internal::rpc::address_meta recipient{1, 2};
|
|
|
|
EXPECT(frame->responses.size() == 0);
|
|
EXPECT(result == std::error_code{});
|
|
EXPECT(wallet->passed_login);
|
|
EXPECT(wallet->refresh_error == std::error_code{});
|
|
EXPECT(wallet->lookahead_error == std::error_code{});
|
|
EXPECT(wallet->subaddress_error == std::error_code{});
|
|
EXPECT(wallet->import_error == std::error_code{});
|
|
EXPECT(wallet->primary.restore_height == 1);
|
|
EXPECT(wallet->primary.scan_height == 175);
|
|
EXPECT(wallet->primary.requested_start == 1);
|
|
EXPECT(wallet->blockchain_height == 210);
|
|
EXPECT(wallet->fee_mask == 100);
|
|
EXPECT(wallet->per_byte_fee.size() == 3);
|
|
EXPECT(wallet->per_byte_fee.at(0) == 110);
|
|
EXPECT(wallet->per_byte_fee.at(1) == 500);
|
|
EXPECT(wallet->per_byte_fee.at(2) == 700);
|
|
EXPECT(wallet->primary.txes.size() == 1);
|
|
auto tx = wallet->primary.txes.find(from_hex<crypto::hash>("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1"));
|
|
EXPECT(tx != wallet->primary.txes.end());
|
|
EXPECT(tx->second != nullptr);
|
|
EXPECT(tx->second->spends.size() == 1);
|
|
EXPECT(tx->second->spends.nth(0)->first == image);
|
|
EXPECT(tx->second->spends.nth(0)->second.amount == 10000);
|
|
EXPECT(tx->second->spends.nth(0)->second.sender == sender);
|
|
EXPECT(tx->second->spends.nth(0)->second.tx_pub == tx_key.pub);
|
|
EXPECT(tx->second->spends.nth(0)->second.output_pub == output_key.pub);
|
|
EXPECT(tx->second->receives.size() == 1);
|
|
EXPECT(tx->second->receives.nth(0)->first == output_key.pub);
|
|
EXPECT(tx->second->receives.nth(0)->second.tx_pub == tx_key.pub);
|
|
EXPECT(tx->second->receives.nth(0)->second.amount == 4000);
|
|
EXPECT(tx->second->receives.nth(0)->second.recipient == recipient);
|
|
EXPECT(tx->second->receives.nth(0)->second.index == 3);
|
|
EXPECT(tx->second->receives.nth(0)->second.global_index == 60000);
|
|
EXPECT(bool(tx->second->receives.nth(0)->second.rct_mask));
|
|
EXPECT(*tx->second->receives.nth(0)->second.rct_mask == ringct);
|
|
EXPECT(wallet->primary.subaccounts.size() == 2);
|
|
EXPECT(wallet->primary.subaccounts.at(0).last == 0);
|
|
EXPECT(wallet->primary.subaccounts.at(1).last == 2);
|
|
}
|
|
}
|
|
|
|
SECTION("call rpcs in randomized order with threads")
|
|
{
|
|
static constexpr const unsigned high_level_calls = 1000;
|
|
|
|
const auto frame = std::make_shared<dummy_server::frame>(io);
|
|
wallet->client.init(io, "127.0.0.1", "", acceptor.local_endpoint().port(), no_ssl);
|
|
wallet->primary.subaccounts.emplace_back().last = 1;
|
|
acceptor.async_accept(frame->server, dummy_server{frame});
|
|
|
|
std::atomic<unsigned> count{0};
|
|
const auto completed = [&] (auto) { ++count; };
|
|
|
|
const std::array<std::function<void()>, 9> rpcs
|
|
{{
|
|
std::bind(bwallet::login_is_new, wallet, completed),
|
|
std::bind(bwallet::refresh, wallet, true, completed),
|
|
std::bind(bwallet::get_fees, wallet, completed),
|
|
std::bind(bwallet::register_subaccount, wallet, 1, completed),
|
|
std::bind(bwallet::register_subaddress, wallet, 1, 0, completed),
|
|
std::bind(bwallet::set_lookahead, wallet, 1, 1, completed),
|
|
std::bind(bwallet::restore_height_raw, wallet, 0, completed),
|
|
std::bind(bwallet::get_decoys, wallet, empty_decoys{}, completed),
|
|
std::bind(bwallet::send_tx, wallet, empty_slice{}, completed)
|
|
}};
|
|
|
|
struct joiner
|
|
{
|
|
boost::thread thread;
|
|
|
|
joiner(const joiner&) = delete;
|
|
joiner(joiner&&) = default;
|
|
~joiner() noexcept { if (thread.joinable()) thread.join(); }
|
|
};
|
|
std::vector<joiner> threads;
|
|
|
|
struct on_exit_
|
|
{
|
|
boost::asio::io_context& io;
|
|
~on_exit_() noexcept { io.stop(); }
|
|
} on_exit{io};
|
|
|
|
std::string asio_fail;
|
|
boost::mutex sync_asio;
|
|
const auto run_asio = [&]
|
|
{
|
|
try { (io.run()); }
|
|
catch (const std::exception& e)
|
|
{
|
|
io.stop();
|
|
const boost::lock_guard<boost::mutex> lock{sync_asio};
|
|
asio_fail = e.what();
|
|
}
|
|
};
|
|
const auto run_client = [&]
|
|
{
|
|
static_assert(high_level_calls % 2 == 0);
|
|
static_assert(0 < rpcs.size());
|
|
static_assert(rpcs.size() <= std::numeric_limits<unsigned>::max());
|
|
|
|
std::mt19937 eng(crypto::rand<std::uint32_t>());
|
|
std::uniform_int_distribution<unsigned> dist(0, rpcs.size() - 1);
|
|
|
|
unsigned i = high_level_calls / 2;
|
|
while (i--)
|
|
{
|
|
rpcs.at(dist(eng))();
|
|
// the above call is much quicker than the response threads
|
|
boost::this_thread::sleep_for(boost::chrono::milliseconds{1});
|
|
}
|
|
};
|
|
|
|
threads.push_back(joiner{boost::thread{run_asio}});
|
|
threads.push_back(joiner{boost::thread{run_asio}});
|
|
threads.push_back(joiner{boost::thread{run_client}});
|
|
run_client();
|
|
|
|
while (count < high_level_calls && !io.stopped())
|
|
boost::this_thread::sleep_for(boost::chrono::milliseconds{50});
|
|
|
|
const boost::lock_guard<boost::mutex> lock{sync_asio};
|
|
EXPECT(asio_fail == "");
|
|
EXPECT(count == high_level_calls);
|
|
EXPECT(!io.stopped());
|
|
}
|
|
}
|
|
}
|
|
|
|
LWS_CASE("backend::wallet /feed (websocket)")
|
|
{
|
|
using bwallet = lwsf::internal::backend::wallet;
|
|
using tcp = boost::asio::ip::tcp;
|
|
const auto no_ssl = epee::net_utils::ssl_support_t::e_ssl_support_disabled;
|
|
|
|
SETUP("base empty wallet and basic server")
|
|
{
|
|
boost::asio::io_context io;
|
|
const auto wallet = std::make_shared<lwsf::internal::backend::wallet>(io);
|
|
crypto::generate_keys(wallet->primary.view.pub, wallet->primary.view.sec);
|
|
crypto::generate_keys(wallet->primary.spend.pub, wallet->primary.spend.sec);
|
|
wallet->primary.address = wallet->get_spend_address({0, 0});
|
|
|
|
tcp::acceptor acceptor{io, tcp::endpoint(tcp::v4(), 0)};
|
|
acceptor.listen();
|
|
|
|
const auto login_verify = [&] (std::string sub)
|
|
{
|
|
EXPECT(boost::string_view{sub}.starts_with("login:"));
|
|
sub.erase(0, std::strlen("login:"));
|
|
|
|
login user{};
|
|
EXPECT(wire::msgpack::from_bytes(epee::byte_slice{std::move(sub)}, user) == std::error_code{});
|
|
EXPECT(user.account.address == wallet->get_spend_address({0, 0}));
|
|
EXPECT(user.account.view_key == wallet->primary.view.sec);
|
|
};
|
|
|
|
const std::string upsert_body =
|
|
R"({"address":")" + wallet->get_spend_address({0, 0}) +
|
|
R"(","view_key":")" + to_hex(unwrap(unwrap(wallet->primary.view.sec))) +
|
|
R"(","subaddrs":[{"key":1,"value":[[0,2]]}],"get_all":false})";
|
|
|
|
const auto frame = std::make_shared<response_loop::frame>(lest_env, io);
|
|
const auto frame2 = std::make_shared<response_loop::frame>(lest_env, io);
|
|
wallet->client.init(io, "127.0.0.1", "", acceptor.local_endpoint().port(), no_ssl);
|
|
|
|
bool done = false;
|
|
std::size_t refreshed = 0;
|
|
std::error_code result{};
|
|
std::error_code ws_result{};
|
|
const auto record_response = [&] (auto val) { done = true; result = val; };
|
|
bwallet::refresh(wallet, true, record_response);
|
|
|
|
wallet->passed_login = false;
|
|
wallet->feed_disabled = false;
|
|
wallet->on_feed_fail = [&] (auto val) { ws_result = val; };
|
|
wallet->on_feed_refresh = [&] (auto err) { EXPECT(!err); ++refreshed; };
|
|
acceptor.async_accept(frame->server, response_loop{frame});
|
|
|
|
SECTION("feed is aborted during upgrade")
|
|
{
|
|
wallet->on_feed_fail = [&] (auto err) {
|
|
EXPECT(err == boost::system::error_code{boost::beast::websocket::error::upgrade_declined});
|
|
acceptor.async_accept(frame2->server, response_loop{frame2});
|
|
};
|
|
|
|
frame->requests = {
|
|
{"/login", ""},
|
|
{"/feed", ""},
|
|
{"/feed", ""}
|
|
};
|
|
frame->responses = {
|
|
R"({"new_address": true, "lookahead": {"maj_i": 50, "min_i": 200}})",
|
|
boost::beast::http::status::not_implemented,
|
|
boost::beast::http::status::not_found
|
|
};
|
|
|
|
frame2->requests = {{"/get_address_txs", ""}};
|
|
frame2->responses = {boost::beast::http::status::forbidden};
|
|
|
|
while (!done)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
|
|
EXPECT(refreshed == 0);
|
|
EXPECT(!wallet->on_feed_fail);
|
|
EXPECT(!wallet->on_feed_refresh);
|
|
EXPECT(frame->responses.size() == 0);
|
|
EXPECT(frame2->responses.size() == 0);
|
|
EXPECT(result == lwsf::error::approval);
|
|
EXPECT(!wallet->passed_login);
|
|
EXPECT(wallet->feed_disabled);
|
|
EXPECT(wallet->refresh_error == lwsf::error::approval);
|
|
}
|
|
|
|
SECTION("feed is aborted after upgrade")
|
|
{
|
|
wallet->on_feed_fail = [&] (auto err) {
|
|
EXPECT(err == lwsf::error::bad_feed_prefix);
|
|
acceptor.async_accept(frame2->server, response_loop{frame2});
|
|
};
|
|
|
|
const std::string error =R"({"msg": "account not found", "code": 1})";
|
|
|
|
frame->requests = {
|
|
{"/login", ""},
|
|
{"/feed", ""},
|
|
{"/feed", ""}
|
|
};
|
|
frame->responses = {
|
|
R"({"new_address": true, "lookahead": {"maj_i": 50, "min_i": 200}})",
|
|
boost::beast::http::status::not_implemented,
|
|
boost::beast::http::status::switching_protocols
|
|
};
|
|
|
|
while (!frame->ws)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
|
|
EXPECT(refreshed == 0);
|
|
EXPECT(wallet->on_feed_fail);
|
|
EXPECT(wallet->on_feed_refresh);
|
|
EXPECT(frame->responses.size() == 0);
|
|
EXPECT(frame2->responses.size() == 0);
|
|
EXPECT(result == std::error_code{});
|
|
EXPECT(wallet->passed_login);
|
|
EXPECT(!wallet->feed_disabled);
|
|
EXPECT(wallet->refresh_error == std::error_code{});
|
|
EXPECT(wallet->lookahead_error == std::error_code{});
|
|
EXPECT(wallet->subaddress_error == std::error_code{});
|
|
EXPECT(wallet->import_error == std::error_code{});
|
|
|
|
wallet->on_feed_refresh =
|
|
[&] (auto err) { EXPECT(err == lwsf::error::bad_feed_prefix); ++refreshed; };
|
|
|
|
lwsf_test::net::push::async_read(frame->ws, [&] (auto user) {
|
|
login_verify(user.value());
|
|
lwsf_test::net::push::async_pub(
|
|
frame->ws, "error:" + to_msgpack(error), [&] (auto err) { EXPECT(!err); }
|
|
);
|
|
});
|
|
|
|
frame2->requests = {{"/get_address_txs", ""}};
|
|
frame2->responses = {boost::beast::http::status::forbidden};
|
|
|
|
while (!done)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
|
|
EXPECT(refreshed == 1);
|
|
EXPECT(!wallet->on_feed_fail);
|
|
EXPECT(!wallet->on_feed_refresh);
|
|
EXPECT(frame->responses.size() == 0);
|
|
EXPECT(frame2->responses.size() == 0);
|
|
EXPECT(result == lwsf::error::approval);
|
|
EXPECT(!wallet->passed_login);
|
|
EXPECT(wallet->feed_disabled);
|
|
EXPECT(wallet->refresh_error == lwsf::error::approval);
|
|
}
|
|
|
|
SECTION("feed sync (no initial txes)")
|
|
{
|
|
rct::key ringct{};
|
|
lwsf::internal::backend::keypair tx_key{};
|
|
lwsf::internal::backend::keypair output_key{};
|
|
crypto::generate_keys(tx_key.pub, tx_key.sec);
|
|
const crypto::hash tx_hash = crypto::rand<crypto::hash>();
|
|
const crypto::hash prefix_hash = crypto::rand<crypto::hash>();
|
|
{
|
|
crypto::secret_key scalar{};
|
|
crypto::key_derivation derived{};
|
|
EXPECT(crypto::generate_key_derivation(tx_key.pub, wallet->primary.view.sec, derived));
|
|
EXPECT(crypto::derive_public_key(derived, 3, wallet->primary.spend.pub, output_key.pub));
|
|
crypto::derive_secret_key(derived, 3, wallet->primary.spend.sec, output_key.sec);
|
|
|
|
crypto::derivation_to_scalar(derived, 3, scalar);
|
|
|
|
rct::ecdhTuple commitment{};
|
|
rct::ecdhDecode(commitment, rct::sk2rct(scalar), true);
|
|
ringct = commitment.mask;
|
|
}
|
|
|
|
const std::string tx_sync =
|
|
R"({
|
|
"scanned_block_height": 950,
|
|
"start_height": 900,
|
|
"blockchain_height": 1000,
|
|
"lookahead": {"maj_i": 50, "min_i": 200}
|
|
})";
|
|
|
|
const std::string blocks =
|
|
R"({
|
|
"scan_start": 950,
|
|
"scan_end": 975,
|
|
"blockchain_height": 1001,
|
|
"transactions": [{
|
|
"hash": ")" + to_hex(tx_hash) + R"(",
|
|
"prefix_hash": ")" + to_hex(prefix_hash) + R"(",
|
|
"timestamp": 9000,
|
|
"fee": 105,
|
|
"unlock_time": 5000,
|
|
"height": 961,
|
|
"mixin": 16,
|
|
"receives": [{
|
|
"amount": 2566,
|
|
"public_key": ")" + to_hex(output_key.pub) + R"(",
|
|
"index": 3,
|
|
"id": {"legacy": {"amount": 0, "index": 1000}},
|
|
"tx_pub_key": ")" + to_hex(tx_key.pub) + R"(",
|
|
"rct": ")" + to_hex(ringct) + R"("
|
|
}]
|
|
}]
|
|
})";
|
|
|
|
const std::string warning = R"({"msg": "e", "height": 960, "code": 4})";
|
|
|
|
frame->requests = {
|
|
{"/login", ""},
|
|
{"/feed", ""},
|
|
{"/feed", ""}
|
|
};
|
|
frame->responses = {
|
|
R"({"new_address": true, "lookahead": {"maj_i": 50, "min_i": 200}})",
|
|
boost::beast::http::status::not_implemented,
|
|
boost::beast::http::status::switching_protocols
|
|
};
|
|
|
|
while (!frame->ws)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
|
|
EXPECT(!done);
|
|
EXPECT(refreshed == 0);
|
|
EXPECT(!wallet->feed_disabled);
|
|
EXPECT(wallet->on_feed_fail);
|
|
EXPECT(wallet->on_feed_refresh);
|
|
EXPECT(frame->responses.size() == 0);
|
|
EXPECT(result == std::error_code{});
|
|
EXPECT(wallet->refresh_error == std::error_code{});
|
|
EXPECT(wallet->lookahead_error == std::error_code{});
|
|
EXPECT(wallet->subaddress_error == std::error_code{});
|
|
EXPECT(wallet->import_error == std::error_code{});
|
|
EXPECT(wallet->passed_login);
|
|
|
|
lwsf_test::net::push::async_read(frame->ws, [&] (auto user) {
|
|
login_verify(user.value());
|
|
lwsf_test::net::push::async_pub(
|
|
frame->ws, "tx_sync:" + to_msgpack(tx_sync), [&] (auto err) { EXPECT(!err); }
|
|
);
|
|
});
|
|
|
|
while (!done)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
|
|
EXPECT(refreshed == 1);
|
|
EXPECT(result == std::error_code{});
|
|
EXPECT(ws_result == std::error_code{});
|
|
EXPECT(wallet->on_feed_fail);
|
|
EXPECT(wallet->on_feed_refresh);
|
|
EXPECT(wallet->refresh_error == std::error_code{});
|
|
EXPECT(wallet->lookahead_error == std::error_code{});
|
|
EXPECT(wallet->subaddress_error == std::error_code{});
|
|
EXPECT(wallet->import_error == std::error_code{});
|
|
EXPECT(wallet->primary.restore_height == 900);
|
|
EXPECT(wallet->primary.scan_height == 950);
|
|
EXPECT(wallet->primary.requested_start == 900);
|
|
EXPECT(wallet->blockchain_height == 1000);
|
|
EXPECT(wallet->primary.txes.empty());
|
|
|
|
lwsf_test::net::push::async_pub(
|
|
frame->ws, "blocks:" + to_msgpack(blocks), [&] (auto err) { EXPECT(!err); }
|
|
);
|
|
|
|
while (refreshed <= 1)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
|
|
EXPECT(refreshed == 2);
|
|
EXPECT(result == std::error_code{});
|
|
EXPECT(ws_result == std::error_code{});
|
|
EXPECT(!wallet->feed_disabled);
|
|
EXPECT(wallet->on_feed_fail);
|
|
EXPECT(wallet->on_feed_refresh);
|
|
EXPECT(wallet->refresh_error == std::error_code{});
|
|
EXPECT(wallet->lookahead_error == std::error_code{});
|
|
EXPECT(wallet->subaddress_error == std::error_code{});
|
|
EXPECT(wallet->import_error == std::error_code{});
|
|
EXPECT(wallet->primary.restore_height == 900);
|
|
EXPECT(wallet->primary.scan_height == 975);
|
|
EXPECT(wallet->primary.requested_start == 900);
|
|
EXPECT(wallet->blockchain_height == 1001);
|
|
EXPECT(wallet->primary.txes.size() == 1);
|
|
EXPECT(wallet->primary.txes.count(tx_hash) == 1);
|
|
const auto tx = wallet->primary.txes.at(tx_hash);
|
|
EXPECT(tx != nullptr);
|
|
EXPECT(tx->spends.empty());
|
|
EXPECT(tx->receives.size() == 1);
|
|
EXPECT(tx->receives.count(output_key.pub) == 1);
|
|
const auto& in = tx->receives.at(output_key.pub);
|
|
EXPECT(in.global_index == 1000);
|
|
EXPECT(in.amount == 2566);
|
|
EXPECT(in.recipient == lwsf::internal::rpc::address_meta{});
|
|
EXPECT(in.index == 3);
|
|
EXPECT(bool(in.rct_mask));
|
|
EXPECT(in.rct_mask.value_or(rct::key{}) == ringct);
|
|
EXPECT(in.tx_pub == tx_key.pub);
|
|
EXPECT(tx->transfers.empty());
|
|
EXPECT(tx->description.empty());
|
|
EXPECT(bool(tx->timestamp));
|
|
EXPECT(tx->timestamp.value_or(std::chrono::system_clock::time_point{}) == std::chrono::system_clock::time_point{std::chrono::seconds{9000}});
|
|
EXPECT(bool(tx->height));
|
|
EXPECT(tx->height.value_or(0) == 961);
|
|
EXPECT(tx->amount == 2566);
|
|
EXPECT(tx->fee == 105);
|
|
EXPECT(tx->unlock_time == 5000);
|
|
EXPECT(tx->direction == Monero::TransactionInfo::Direction_In);
|
|
EXPECT(std::get_if<lwsf::internal::rpc::empty>(std::addressof(tx->payment_id)));
|
|
EXPECT(tx->id == tx_hash);
|
|
EXPECT(tx->prefix == prefix_hash);
|
|
EXPECT(tx->coinbase == false);
|
|
EXPECT(tx->failed == false);
|
|
|
|
lwsf_test::net::push::async_pub(
|
|
frame->ws, "warning:" + to_msgpack(warning), [&] (auto err) { EXPECT(!err); }
|
|
);
|
|
|
|
while (refreshed <= 2)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
|
|
EXPECT(refreshed == 3);
|
|
EXPECT(result == std::error_code{});
|
|
EXPECT(ws_result == std::error_code{});
|
|
EXPECT(!wallet->feed_disabled);
|
|
EXPECT(wallet->on_feed_fail);
|
|
EXPECT(wallet->on_feed_refresh);
|
|
EXPECT(wallet->refresh_error == std::error_code{});
|
|
EXPECT(wallet->lookahead_error == std::error_code{});
|
|
EXPECT(wallet->subaddress_error == std::error_code{});
|
|
EXPECT(wallet->import_error == std::error_code{});
|
|
EXPECT(wallet->primary.restore_height == 900);
|
|
EXPECT(wallet->primary.scan_height == 960);
|
|
EXPECT(wallet->primary.requested_start == 900);
|
|
EXPECT(wallet->blockchain_height == 960);
|
|
EXPECT(wallet->primary.txes.empty());
|
|
}
|
|
|
|
SECTION("feed sync (initial txes)")
|
|
{
|
|
rct::key ringct{};
|
|
rct::key ringct2{};
|
|
crypto::key_image image{};
|
|
lwsf::internal::backend::keypair tx_key{};
|
|
lwsf::internal::backend::keypair tx_key2{};
|
|
lwsf::internal::backend::keypair output_key{};
|
|
lwsf::internal::backend::keypair output_key2{};
|
|
crypto::generate_keys(tx_key.pub, tx_key.sec);
|
|
crypto::generate_keys(tx_key2.pub, tx_key2.sec);
|
|
const crypto::hash8 pid = crypto::rand<crypto::hash8>();
|
|
const crypto::hash tx_hash = crypto::rand<crypto::hash>();
|
|
const crypto::hash tx_hash2 = crypto::rand<crypto::hash>();
|
|
const crypto::hash prefix_hash = crypto::rand<crypto::hash>();
|
|
const crypto::hash prefix_hash2 = crypto::rand<crypto::hash>();
|
|
{
|
|
crypto::secret_key scalar{};
|
|
crypto::key_derivation derived{};
|
|
EXPECT(crypto::generate_key_derivation(tx_key.pub, wallet->primary.view.sec, derived));
|
|
EXPECT(crypto::derive_public_key(derived, 0, wallet->primary.spend.pub, output_key.pub));
|
|
crypto::derive_secret_key(derived, 0, wallet->primary.spend.sec, output_key.sec);
|
|
crypto::generate_key_image(output_key.pub, output_key.sec, image);
|
|
|
|
crypto::derivation_to_scalar(derived, 0, scalar);
|
|
|
|
rct::ecdhTuple commitment{};
|
|
rct::ecdhDecode(commitment, rct::sk2rct(scalar), true);
|
|
ringct = commitment.mask;
|
|
}
|
|
{
|
|
crypto::secret_key scalar{};
|
|
crypto::key_derivation derived{};
|
|
EXPECT(crypto::generate_key_derivation(tx_key2.pub, wallet->primary.view.sec, derived));
|
|
EXPECT(crypto::derive_public_key(derived, 3, wallet->primary.spend.pub, output_key2.pub));
|
|
crypto::derive_secret_key(derived, 3, wallet->primary.spend.sec, output_key2.sec);
|
|
|
|
crypto::derivation_to_scalar(derived, 3, scalar);
|
|
|
|
rct::ecdhTuple commitment{};
|
|
rct::ecdhDecode(commitment, rct::sk2rct(scalar), true);
|
|
ringct2 = commitment.mask;
|
|
}
|
|
|
|
const std::string tx_sync =
|
|
R"({
|
|
"scanned_block_height": 1950,
|
|
"start_height": 900,
|
|
"blockchain_height": 11000,
|
|
"lookahead": {"maj_i": 50, "min_i": 200},
|
|
"transactions": [
|
|
{
|
|
"hash": ")" + to_hex(tx_hash) + R"(",
|
|
"prefix_hash": ")" + to_hex(prefix_hash) + R"(",
|
|
"timestamp": 7000,
|
|
"fee": 103,
|
|
"unlock_time": 4000,
|
|
"height": 1955,
|
|
"mixin": 16,
|
|
"receives": [{
|
|
"amount": 1366,
|
|
"public_key": ")" + to_hex(output_key.pub) + R"(",
|
|
"index": 0,
|
|
"id": {"legacy": {"amount": 0, "index": 1000}},
|
|
"tx_pub_key": ")" + to_hex(tx_key.pub) + R"(",
|
|
"rct": ")" + to_hex(ringct) + R"("
|
|
}]
|
|
},{
|
|
"hash": ")" + to_hex(tx_hash2) + R"(",
|
|
"prefix_hash": ")" + to_hex(prefix_hash2) + R"(",
|
|
"timestamp": 9000,
|
|
"payment_id": ")" + to_hex(pid) + R"(",
|
|
"fee": 105,
|
|
"unlock_time": 5000,
|
|
"height": 1961,
|
|
"mixin": 16,
|
|
"receives": [
|
|
{
|
|
"amount": 566,
|
|
"public_key": ")" + to_hex(output_key.pub) + R"(",
|
|
"index": 0,
|
|
"id": {"legacy": {"amount": 0, "index": 2000}},
|
|
"tx_pub_key": ")" + to_hex(tx_key.pub) + R"(",
|
|
"rct": ")" + to_hex(ringct) + R"("
|
|
},{
|
|
"amount": 655,
|
|
"public_key": ")" + to_hex(output_key2.pub) + R"(",
|
|
"index": 3,
|
|
"id": {"legacy": {"amount": 0, "index": 2001}},
|
|
"tx_pub_key": ")" + to_hex(tx_key2.pub) + R"(",
|
|
"rct": ")" + to_hex(ringct2) + R"("
|
|
}
|
|
], "spends": [{
|
|
"id": {"legacy": {"amount": 0, "index": 1000}},
|
|
"key_image": ")" + to_hex(image) + R"("
|
|
}]
|
|
}
|
|
]
|
|
})";
|
|
|
|
const std::string error = R"({"msg": "e", "code": 5})";
|
|
|
|
frame->requests = {
|
|
{"/login", ""},
|
|
{"/feed", ""},
|
|
{"/feed", ""}
|
|
};
|
|
frame->responses = {
|
|
R"({"new_address": true, "lookahead": {"maj_i": 50, "min_i": 200}})",
|
|
boost::beast::http::status::not_implemented,
|
|
boost::beast::http::status::switching_protocols
|
|
};
|
|
|
|
while (!frame->ws)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
|
|
EXPECT(!done);
|
|
EXPECT(refreshed == 0);
|
|
EXPECT(!wallet->feed_disabled);
|
|
EXPECT(wallet->on_feed_fail);
|
|
EXPECT(wallet->on_feed_refresh);
|
|
EXPECT(frame->responses.size() == 0);
|
|
EXPECT(result == std::error_code{});
|
|
EXPECT(wallet->refresh_error == std::error_code{});
|
|
EXPECT(wallet->lookahead_error == std::error_code{});
|
|
EXPECT(wallet->subaddress_error == std::error_code{});
|
|
EXPECT(wallet->import_error == std::error_code{});
|
|
EXPECT(wallet->passed_login);
|
|
|
|
lwsf_test::net::push::async_read(frame->ws, [&] (auto user) {
|
|
login_verify(user.value());
|
|
lwsf_test::net::push::async_pub(
|
|
frame->ws, "tx_sync:" + to_msgpack(tx_sync), [&] (auto err) { EXPECT(!err); }
|
|
);
|
|
});
|
|
|
|
while (!done)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
|
|
EXPECT(refreshed == 1);
|
|
EXPECT(result == std::error_code{});
|
|
EXPECT(ws_result == std::error_code{});
|
|
EXPECT(wallet->on_feed_fail);
|
|
EXPECT(wallet->on_feed_refresh);
|
|
EXPECT(wallet->refresh_error == std::error_code{});
|
|
EXPECT(wallet->lookahead_error == std::error_code{});
|
|
EXPECT(wallet->subaddress_error == std::error_code{});
|
|
EXPECT(wallet->import_error == std::error_code{});
|
|
EXPECT(wallet->primary.restore_height == 900);
|
|
EXPECT(wallet->primary.scan_height == 1950);
|
|
EXPECT(wallet->primary.requested_start == 900);
|
|
EXPECT(wallet->blockchain_height == 11000);
|
|
EXPECT(wallet->primary.txes.size() == 2);
|
|
EXPECT(wallet->primary.txes.count(tx_hash) == 1);
|
|
{
|
|
const auto tx = wallet->primary.txes.at(tx_hash);
|
|
EXPECT(tx != nullptr);
|
|
EXPECT(tx->spends.empty());
|
|
EXPECT(tx->receives.size() == 1);
|
|
EXPECT(tx->receives.count(output_key.pub) == 1);
|
|
const auto& in = tx->receives.at(output_key.pub);
|
|
EXPECT(in.global_index == 1000);
|
|
EXPECT(in.amount == 1366);
|
|
EXPECT(in.recipient == lwsf::internal::rpc::address_meta{});
|
|
EXPECT(in.index == 0);
|
|
EXPECT(bool(in.rct_mask));
|
|
EXPECT(in.rct_mask.value_or(rct::key{}) == ringct);
|
|
EXPECT(in.tx_pub == tx_key.pub);
|
|
EXPECT(tx->transfers.empty());
|
|
EXPECT(tx->description.empty());
|
|
EXPECT(bool(tx->timestamp));
|
|
EXPECT(tx->timestamp.value_or(std::chrono::system_clock::time_point{}) == std::chrono::system_clock::time_point{std::chrono::seconds{7000}});
|
|
EXPECT(bool(tx->height));
|
|
EXPECT(tx->height.value_or(0) == 1955);
|
|
EXPECT(tx->amount == 1366);
|
|
EXPECT(tx->fee == 103);
|
|
EXPECT(tx->unlock_time == 4000);
|
|
EXPECT(tx->direction == Monero::TransactionInfo::Direction_In);
|
|
EXPECT(std::get_if<lwsf::internal::rpc::empty>(std::addressof(tx->payment_id)));
|
|
EXPECT(tx->id == tx_hash);
|
|
EXPECT(tx->prefix == prefix_hash);
|
|
EXPECT(tx->coinbase == false);
|
|
EXPECT(tx->failed == false);
|
|
}
|
|
{
|
|
const auto tx = wallet->primary.txes.at(tx_hash2);
|
|
EXPECT(tx != nullptr);
|
|
EXPECT(tx->spends.size() == 1);
|
|
EXPECT(tx->spends.count(image) == 1);
|
|
const auto& out = tx->spends.at(image);
|
|
EXPECT(out.amount == 1366);
|
|
EXPECT(out.sender == lwsf::internal::rpc::address_meta{});
|
|
EXPECT(out.tx_pub == tx_key.pub);
|
|
EXPECT(out.output_pub == output_key.pub);
|
|
EXPECT(tx->receives.size() == 2);
|
|
EXPECT(tx->receives.count(output_key.pub) == 1);
|
|
EXPECT(tx->receives.count(output_key2.pub) == 1);
|
|
{
|
|
const auto& in = tx->receives.at(output_key.pub);
|
|
EXPECT(in.global_index == 2000);
|
|
EXPECT(in.amount == 566);
|
|
EXPECT(in.recipient == lwsf::internal::rpc::address_meta{});
|
|
EXPECT(in.index == 0);
|
|
EXPECT(bool(in.rct_mask));
|
|
EXPECT(in.rct_mask.value_or(rct::key{}) == ringct);
|
|
EXPECT(in.tx_pub == tx_key.pub);
|
|
}
|
|
{
|
|
const auto& in = tx->receives.at(output_key2.pub);
|
|
EXPECT(in.global_index == 2001);
|
|
EXPECT(in.amount == 655);
|
|
EXPECT(in.recipient == lwsf::internal::rpc::address_meta{});
|
|
EXPECT(in.index == 3);
|
|
EXPECT(bool(in.rct_mask));
|
|
EXPECT(in.rct_mask.value_or(rct::key{}) == ringct2);
|
|
EXPECT(in.tx_pub == tx_key2.pub);
|
|
}
|
|
EXPECT(tx->transfers.empty());
|
|
EXPECT(tx->description.empty());
|
|
EXPECT(bool(tx->timestamp));
|
|
EXPECT(tx->timestamp.value_or(std::chrono::system_clock::time_point{}) == std::chrono::system_clock::time_point{std::chrono::seconds{9000}});
|
|
EXPECT(bool(tx->height));
|
|
EXPECT(tx->height.value_or(0) == 1961);
|
|
EXPECT(tx->amount == 40);
|
|
EXPECT(tx->fee == 105);
|
|
EXPECT(tx->unlock_time == 5000);
|
|
EXPECT(tx->direction == Monero::TransactionInfo::Direction_Out);
|
|
EXPECT(std::get_if<crypto::hash8>(std::addressof(tx->payment_id)));
|
|
EXPECT(std::get<crypto::hash8>(tx->payment_id) == pid);
|
|
EXPECT(tx->id == tx_hash2);
|
|
EXPECT(tx->prefix == prefix_hash2);
|
|
EXPECT(tx->coinbase == false);
|
|
EXPECT(tx->failed == false);
|
|
}
|
|
|
|
wallet->on_feed_refresh =
|
|
[&] (auto err) { EXPECT(err == lwsf::error::feed); ++refreshed; };
|
|
lwsf_test::net::push::async_pub(
|
|
frame->ws, "error:" + to_msgpack(error), [&] (auto err) { EXPECT(!err); }
|
|
);
|
|
|
|
while (refreshed <= 1)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
|
|
EXPECT(refreshed == 2);
|
|
EXPECT(result == std::error_code{});
|
|
EXPECT(ws_result == std::error_code{});
|
|
EXPECT(wallet->on_feed_fail);
|
|
EXPECT(wallet->on_feed_refresh);
|
|
EXPECT(wallet->refresh_error == std::error_code{});
|
|
EXPECT(wallet->lookahead_error == std::error_code{});
|
|
EXPECT(wallet->subaddress_error == std::error_code{});
|
|
EXPECT(wallet->import_error == std::error_code{});
|
|
EXPECT(wallet->primary.restore_height == 900);
|
|
EXPECT(wallet->primary.scan_height == 1950);
|
|
EXPECT(wallet->primary.requested_start == 900);
|
|
EXPECT(wallet->blockchain_height == 11000);
|
|
EXPECT(wallet->primary.txes.size() == 2);
|
|
|
|
acceptor.async_accept(frame2->server, response_loop{frame2});
|
|
frame2->requests = {{"/feed", ""}, {"/feed", ""}};
|
|
frame2->responses = {
|
|
boost::beast::http::status::not_implemented,
|
|
boost::beast::http::status::switching_protocols
|
|
};
|
|
|
|
while (!frame2->ws)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
|
|
EXPECT(refreshed == 2);
|
|
EXPECT(result == std::error_code{});
|
|
EXPECT(ws_result == std::error_code{});
|
|
EXPECT(wallet->on_feed_fail);
|
|
EXPECT(wallet->on_feed_refresh);
|
|
EXPECT(wallet->refresh_error == std::error_code{});
|
|
EXPECT(wallet->lookahead_error == std::error_code{});
|
|
EXPECT(wallet->subaddress_error == std::error_code{});
|
|
EXPECT(wallet->import_error == std::error_code{});
|
|
EXPECT(wallet->primary.restore_height == 900);
|
|
EXPECT(wallet->primary.scan_height == 1950);
|
|
EXPECT(wallet->primary.requested_start == 900);
|
|
EXPECT(wallet->blockchain_height == 11000);
|
|
EXPECT(wallet->primary.txes.size() == 2);
|
|
}
|
|
|
|
/*
|
|
* Websocket Cleanup
|
|
*/
|
|
|
|
std::shared_ptr<lwsf::internal::websocket::stream> ws;
|
|
ws.swap(wallet->feed);
|
|
if (ws)
|
|
{
|
|
const std::size_t refreshed_count = refreshed;
|
|
wallet->on_feed_refresh = [&] (auto) { ++refreshed; };
|
|
lwsf::internal::websocket::async_close(std::move(ws));
|
|
lwsf_test::net::push::async_read(frame->ws, [] (auto) {});
|
|
|
|
while (refreshed_count == refreshed)
|
|
{
|
|
io.restart();
|
|
io.run_one();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|