Update to use boost::beast (websocket transition). (#4)

This commit is contained in:
Lee *!* Clagett
2026-02-12 20:15:26 -05:00
committed by GitHub
parent 79041d25ca
commit 7a8a364dc7
36 changed files with 4144 additions and 976 deletions
+86 -20
View File
@@ -8,35 +8,96 @@ on:
env: env:
# Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.)
APT_INSTALL_LINUX: 'sudo apt -y install build-essential cmake libboost-all-dev miniupnpc libunbound-dev libunwind8-dev pkg-config libssl-dev libzmq3-dev libsodium-dev libhidapi-dev libnorm-dev libusb-1.0-0-dev libpgm-dev libprotobuf-dev protobuf-compiler' APT_INSTALL_LINUX: 'apt -y install build-essential cmake git libboost-all-dev miniupnpc libunbound-dev libunwind8-dev pkg-config libssl-dev libzmq3-dev libsodium-dev libhidapi-dev libnorm-dev libusb-1.0-0-dev libpgm-dev libprotobuf-dev protobuf-compiler'
APT_SET_CONF: | APT_SET_CONF: |
echo "Acquire::Retries \"3\";" | sudo tee -a /etc/apt/apt.conf.d/80-custom echo "Acquire::Retries \"3\";" | sudo tee -a /etc/apt/apt.conf.d/80-custom
echo "Acquire::http::Timeout \"120\";" | sudo tee -a /etc/apt/apt.conf.d/80-custom echo "Acquire::http::Timeout \"120\";" | sudo tee -a /etc/apt/apt.conf.d/80-custom
echo "Acquire::ftp::Timeout \"120\";" | sudo tee -a /etc/apt/apt.conf.d/80-custom echo "Acquire::ftp::Timeout \"120\";" | sudo tee -a /etc/apt/apt.conf.d/80-custom
BREW_INSTALL_MAC: 'HOMEBREW_NO_AUTO_UPDATE=1 brew install boost hidapi openssl zmq libpgm miniupnpc expat libunwind-headers protobuf unbound' BREW_INSTALL_MAC: 'HOMEBREW_NO_AUTO_UPDATE=1 brew install boost hidapi openssl zmq libpgm miniupnpc expat libunwind-headers protobuf unbound'
CONFIGURE: 'cmake -B "${{github.workspace}}/lwsf/build" -DCMAKE_BUILD_TYPE=Release -DMONERO_SOURCE_DIR="${{github.workspace}}/monero" -DLWSF_MASTER_ENABLE=ON -DLWSF_BUILD_TESTS=ON "${{github.workspace}}/lwsf"' CONFIGURE: '-B "$GITHUB_WORKSPACE/lwsf/build" -DCMAKE_BUILD_TYPE=Release -DMONERO_SOURCE_DIR="$GITHUB_WORKSPACE/monero" -DLWSF_BUILD_TESTS=ON -S "$GITHUB_WORKSPACE/lwsf"'
BUILD: 'cmake --build "${{github.workspace}}/lwsf/build" -j$(nproc)' BUILD: 'cmake --build "$GITHUB_WORKSPACE/lwsf/build" -j$(nproc)'
RUN: 'ctest --test-dir "${{github.workspace}}/lwsf/build"' RUN: 'ctest --test-dir "$GITHUB_WORKSPACE/lwsf/build" --output-on-failure'
jobs: jobs:
unix-build-and-test: sanitizer:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
sanitizer: [address, thread, undefined]
steps:
- name: Install Dependencies
run: |
${{env.APT_SET_CONF}}
sudo apt update
sudo ${{env.APT_INSTALL_LINUX}}
- name: Checkout Monero Source
uses: actions/checkout@v4
with:
repository: monero-project/monero
path: ${{github.workspace}}/monero
submodules: recursive
- name: Checkout LWSF Source
uses: actions/checkout@v4
with:
path: ${{github.workspace}}/lwsf
submodules: recursive
- name: Configure LWSF
run: cmake -DLWSF_SANITIZER=${{matrix.sanitizer}} -DLWSF_MASTER_ENABLE=ON ${{env.CONFIGURE}}
- name: Build LWSF
run: ${{env.BUILD}}
- name: Run Tests
run: ${{env.RUN}}
linux:
runs-on: ubuntu-latest
container: ${{matrix.distro}}
strategy:
fail-fast: false
matrix:
distro: [archlinux:base-devel, debian:bookworm-slim, debian:trixie-slim]
steps:
- name: Install Dependencies (Debian/apt)
if: matrix.distro == 'debian:bookworm-slim' || matrix.distro == 'debian:trixie-slim'
run: |
apt update
${{env.APT_INSTALL_LINUX}}
- name: Install Dependencies (Arch/pacman)
if: matrix.distro == 'archlinux:base-devel'
run: |
sudo pacman --noconfirm -Syu
sudo pacman --noconfirm -S cmake boost boost-libs git hidapi libsodium miniupnpc openssl unbound zeromq
- name: Checkout Monero Source
uses: actions/checkout@v4
with:
repository: monero-project/monero
path: ${{github.workspace}}/monero/
submodules: recursive
- name: Checkout LWS Source
uses: actions/checkout@v4
with:
path: ${{github.workspace}}/lwsf
- name: Configure LWS
run: cmake -DLWSF_MASTER_ENABLE=ON ${{env.CONFIGURE}}
- name: Build LWS
run: ${{env.BUILD}}
- name: Run Tests
run: ${{env.RUN}}
unix:
runs-on: ${{matrix.os}} runs-on: ${{matrix.os}}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [macos-latest, ubuntu-latest] branch: [master, release-v0.18]
os: [macos-15, macos-26, ubuntu-22.04, ubuntu-24.04]
steps: steps:
- name: set apt conf (Debian based Linux) - name: Install Dependencies (Ubuntu)
if: matrix.os == 'ubuntu-latest' if: matrix.os == 'ubuntu-22.04' || matrix.os == 'ubuntu-24.04'
run: ${{env.APT_SET_CONF}} run: |
- name: update apt (Debian based Linux) ${{env.APT_SET_CONF}}
if: matrix.os == 'ubuntu-latest' sudo apt update
run: sudo apt update sudo ${{env.APT_INSTALL_LINUX}}
- name: Install dependencies (Debian based Linux)
if: matrix.os == 'ubuntu-latest'
run: ${{env.APT_INSTALL_LINUX}}
- name: Install dependencies (MacOS) - name: Install dependencies (MacOS)
if: matrix.os == 'macos-latest' if: matrix.os == 'macos-15' || matrix.os == 'macos-26'
run: ${{env.BREW_INSTALL_MAC}} run: ${{env.BREW_INSTALL_MAC}}
- name: Checkout Monero Source - name: Checkout Monero Source
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -44,18 +105,23 @@ jobs:
repository: monero-project/monero repository: monero-project/monero
path: ${{github.workspace}}/monero/ path: ${{github.workspace}}/monero/
submodules: recursive submodules: recursive
ref: ${{matrix.branch}}
- name: Checkout LWSF Source - name: Checkout LWSF Source
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
path: ${{github.workspace}}/lwsf path: ${{github.workspace}}/lwsf
submodules: recursive submodules: recursive
- name: Configure LWS - name: Configure LWS (master)
run: ${{env.CONFIGURE}} if: matrix.branch == 'master'
run: cmake -DLWSF_MASTER_ENABLE=ON ${{env.CONFIGURE}}
- name: Configure LWS (release)
if: matrix.branch == 'release-v0.18'
run: cmake ${{env.CONFIGURE}}
- name: Build LWSF - name: Build LWSF
run: ${{env.BUILD}} run: ${{env.BUILD}}
- name: Run Tests - name: Run Tests
run: ${{env.RUN}} run: ${{env.RUN}}
windows-build-and-test: windows:
runs-on: windows-latest runs-on: windows-latest
defaults: defaults:
run: run:
@@ -90,7 +156,7 @@ jobs:
path: ${{github.workspace}}/lwsf path: ${{github.workspace}}/lwsf
submodules: recursive submodules: recursive
- name: Configure LWS - name: Configure LWS
run: ${{env.CONFIGURE}} run: cmake -DLWSF_MASTER_ENABLE=ON ${{env.CONFIGURE}}
- name: Build LWSF - name: Build LWSF
run: ${{env.BUILD}} run: ${{env.BUILD}}
- name: Run Tests - name: Run Tests
+10
View File
@@ -34,6 +34,16 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
option(LWSF_BUILD_TESTS "Build Tests" OFF) option(LWSF_BUILD_TESTS "Build Tests" OFF)
option(LWSF_SANITIZER "Use specific sanitizer" OFF)
if(NOT LWSF_SANITIZER STREQUAL "OFF")
if (MSVC)
message(FATAL_ERROR "Cannot sanitize with MSVC")
else()
message(STATUS "Using sanitizer=${LWSF_SANITIZER}")
add_compile_options("-fsanitize=${LWSF_SANITIZER}")
add_link_options("-fsanitize=${LWSF_SANITIZER}")
endif()
endif()
if (NOT MONERO_SOURCE_DIR) if (NOT MONERO_SOURCE_DIR)
message(FATAL_ERROR "The argument -DMONERO_SOURCE_DIR must specify a location of a monero source tree") message(FATAL_ERROR "The argument -DMONERO_SOURCE_DIR must specify a location of a monero source tree")
+2
View File
@@ -27,6 +27,7 @@
# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
include_directories(.) include_directories(.)
add_subdirectory(net)
add_subdirectory(wire) add_subdirectory(wire)
set(lwsf_sources set(lwsf_sources
@@ -89,6 +90,7 @@ target_link_libraries(lwsf-api
PUBLIC PUBLIC
monero::libraries monero::libraries
PRIVATE PRIVATE
lwsf-net
lwsf-wire lwsf-wire
lwsf-wire-json lwsf-wire-json
lwsf-wire-msgpack lwsf-wire-msgpack
+18 -15
View File
@@ -32,6 +32,7 @@
#include <utility> #include <utility>
#include "backend.h" #include "backend.h"
#include "cryptonote_basic/cryptonote_basic_impl.h" #include "cryptonote_basic/cryptonote_basic_impl.h"
#include "error.h"
namespace lwsf { namespace internal { namespace lwsf { namespace internal {
@@ -66,9 +67,11 @@ namespace lwsf { namespace internal {
error_ = Status_Ok; error_ = Status_Ok;
} }
address_book::address_book(std::shared_ptr<backend::wallet> data) address_book::address_book(const net::wallet_io& data)
: data_(std::move(data)), addresses_(), error_string_(), error_(Status_Ok) : data_(data), addresses_(), error_string_(), error_(Status_Ok)
{} {
LWSF_VERIFY(data_.wallet);
}
address_book::~address_book() address_book::~address_book()
{ {
@@ -80,7 +83,7 @@ namespace lwsf { namespace internal {
clear_status(); clear_status();
cryptonote::address_parse_info info; cryptonote::address_parse_info info;
if(!cryptonote::get_account_address_from_str(info, data_->get_net_type(), dst_addr)) if(!cryptonote::get_account_address_from_str(info, data_.wallet->get_net_type(), dst_addr))
{ {
error_string_ = "Invalid destination address"; error_string_ = "Invalid destination address";
error_ = Invalid_Address; error_ = Invalid_Address;
@@ -99,8 +102,8 @@ namespace lwsf { namespace internal {
addresses_.push_back(row.get()); addresses_.push_back(row.get());
row.release(); row.release();
const boost::lock_guard<boost::mutex> lock{data_->sync}; const boost::lock_guard<boost::mutex> lock{data_.wallet->sync};
data_->primary.addressbook.push_back( data_.wallet->primary.addressbook.push_back(
backend::address_book_entry{dst_addr, payment_id, description} backend::address_book_entry{dst_addr, payment_id, description}
); );
@@ -120,11 +123,11 @@ namespace lwsf { namespace internal {
addresses_[rowId] = nullptr; addresses_[rowId] = nullptr;
addresses_.erase(addresses_.begin() + rowId); addresses_.erase(addresses_.begin() + rowId);
const boost::lock_guard<boost::mutex> lock{data_->sync}; const boost::lock_guard<boost::mutex> lock{data_.wallet->sync};
if (data_->primary.addressbook.size() <= rowId) if (data_.wallet->primary.addressbook.size() <= rowId)
return false; return false;
data_->primary.addressbook.erase(data_->primary.addressbook.begin() + rowId); data_.wallet->primary.addressbook.erase(data_.wallet->primary.addressbook.begin() + rowId);
force.release(); force.release();
return true; return true;
} }
@@ -143,11 +146,11 @@ namespace lwsf { namespace internal {
index, destroy->getAddress(), destroy->getPaymentId(), description index, destroy->getAddress(), destroy->getPaymentId(), description
}; };
const boost::lock_guard<boost::mutex> lock{data_->sync}; const boost::lock_guard<boost::mutex> lock{data_.wallet->sync};
if (data_->primary.addressbook.size() <= index) if (data_.wallet->primary.addressbook.size() <= index)
return false; return false;
data_->primary.addressbook[index].description = description; data_.wallet->primary.addressbook[index].description = description;
force.release(); force.release();
return true; return true;
} }
@@ -158,12 +161,12 @@ namespace lwsf { namespace internal {
{ {
clear_addresses(addresses_); clear_addresses(addresses_);
const boost::lock_guard<boost::mutex> lock{data_->sync}; const boost::lock_guard<boost::mutex> lock{data_.wallet->sync};
addresses_.resize(data_->primary.addressbook.size()); addresses_.resize(data_.wallet->primary.addressbook.size());
for (std::size_t i = 0; i < addresses_.size(); ++i) for (std::size_t i = 0; i < addresses_.size(); ++i)
{ {
const auto& current = data_->primary.addressbook[i]; const auto& current = data_.wallet->primary.addressbook[i];
addresses_[i] = new Monero::AddressBookRow{ addresses_[i] = new Monero::AddressBookRow{
i, current.address, current.payment_id, current.description i, current.address, current.payment_id, current.description
}; };
+5 -5
View File
@@ -33,14 +33,14 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "lws_frontend.h" #include "lws_frontend.h"
#include "net/wallet_io.h"
#include "wallet/api/wallet2_api.h" // monero/src #include "wallet/api/wallet2_api.h" // monero/src
namespace lwsf { namespace internal { namespace lwsf { namespace internal
namespace backend { struct wallet; } {
class address_book final : public ::Monero::AddressBook class address_book final : public ::Monero::AddressBook
{ {
const std::shared_ptr<backend::wallet> data_; const net::wallet_io data_;
std::vector<Monero::AddressBookRow*> addresses_; std::vector<Monero::AddressBookRow*> addresses_;
std::string error_string_; std::string error_string_;
ErrorCode error_; ErrorCode error_;
@@ -49,7 +49,7 @@ namespace lwsf { namespace internal {
public: public:
explicit address_book(std::shared_ptr<backend::wallet> data); explicit address_book(const net::wallet_io& data);
address_book(const address_book&) = delete; address_book(const address_book&) = delete;
address_book(address_book&&) = delete; address_book(address_book&&) = delete;
+763 -377
View File
File diff suppressed because it is too large Load Diff
+65 -16
View File
@@ -29,6 +29,8 @@
#pragma once #pragma once
#include <atomic> #include <atomic>
#include <boost/asio/io_context.hpp>
#include <boost/asio/strand.hpp>
#include <boost/container/flat_map.hpp> #include <boost/container/flat_map.hpp>
#include <boost/optional/optional.hpp> #include <boost/optional/optional.hpp>
#include <boost/thread/mutex.hpp> #include <boost/thread/mutex.hpp>
@@ -36,6 +38,7 @@
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <filesystem> #include <filesystem>
#include <functional>
#include <map> #include <map>
#include <memory> #include <memory>
#include <unordered_map> #include <unordered_map>
@@ -51,6 +54,7 @@
#include "cryptonote_basic/account.h" // monero/src #include "cryptonote_basic/account.h" // monero/src
#include "lwsf_config.h" #include "lwsf_config.h"
#include "lwsf_rpc.h" #include "lwsf_rpc.h"
#include "net/context.h"
#include "ringct/rctTypes.h" // monero/src #include "ringct/rctTypes.h" // monero/src
#include "wallet/api/wallet2_api.h" // monero/src #include "wallet/api/wallet2_api.h" // monero/src
#include "wire/fwd.h" #include "wire/fwd.h"
@@ -233,11 +237,16 @@ namespace lwsf { namespace internal { namespace backend
//! All functions should provide the strong-exception guarantee //! All functions should provide the strong-exception guarantee
struct wallet struct wallet
{ {
Monero::WalletListener* listener; // Everything uses `sync` for synchronization unless noted
const std::unique_ptr<rpc::http_client> client;
Monero::WalletListener* listener; //!< Use `sync_listener`
boost::asio::io_context::strand strand; //<! useful in preventing contention on `sync`
http::client client;
account primary; account primary;
std::string client_prefix; std::string client_prefix;
std::vector<std::uint64_t> per_byte_fee; //!< by priority level std::vector<std::uint64_t> per_byte_fee; //!< by priority level
std::vector<std::function<void(expect<bool>)>> login_queue; //!< use `sync_queue`
std::vector<std::function<void(std::error_code)>> refresh_queue; //!< use `sync_queue`
std::error_code refresh_error; //!< Cached because `refresh(...)` is rate limited std::error_code refresh_error; //!< Cached because `refresh(...)` is rate limited
std::error_code subaddress_error; //!< Errors with subaddresses (not via lookahead) std::error_code subaddress_error; //!< Errors with subaddresses (not via lookahead)
std::error_code lookahead_error; //!< warnings/errors of `server_lookahead` value std::error_code lookahead_error; //!< warnings/errors of `server_lookahead` value
@@ -248,11 +257,35 @@ namespace lwsf { namespace internal { namespace backend
config::lookahead server_lookahead; //!< Status of lookahead server-side config::lookahead server_lookahead; //!< Status of lookahead server-side
mutable boost::mutex sync; mutable boost::mutex sync;
boost::mutex sync_listener; boost::mutex sync_listener;
boost::mutex sync_refresh; boost::mutex sync_queue;
bool passed_login; bool passed_login;
bool probed_lookahead; //!< True iff queries were done to determine lookahead re-sync bool probed_lookahead; //!< True iff queries were done to determine lookahead re-sync
wallet(); wallet(boost::asio::io_context& io);
//! Push `f` to `dest` with thread synchronization
template<typename T, typename F>
bool push(std::vector<std::function<T>>& dest, F&& f)
{
const boost::lock_guard<boost::mutex> lock{sync_queue};
const bool rc = dest.empty();
dest.push_back(std::forward<F>(f));
return rc;
}
//! Call all functions in `waiting` with thread synchronization
template<typename T, typename... U>
void run(std::vector<std::function<T>>& waiting, const U&... args)
{
std::vector<std::function<T>> ready;
{
const boost::lock_guard<boost::mutex> lock{sync_queue};
ready.swap(waiting);
}
for (const auto& f : ready)
f(args...);
}
// `sync` mutex is NOT acquired for this group // `sync` mutex is NOT acquired for this group
@@ -265,7 +298,7 @@ namespace lwsf { namespace internal { namespace backend
// End GROUP // End GROUP
// `sync` mutex IS acquired for this group (until end). Do not hold this mutex // `sync` mutex IS acquired for this group Do not hold this mutex
//! Serialize `this` wallet to msgpack. Locks contents. //! Serialize `this` wallet to msgpack. Locks contents.
expect<epee::byte_slice> to_bytes() const; expect<epee::byte_slice> to_bytes() const;
@@ -273,29 +306,45 @@ namespace lwsf { namespace internal { namespace backend
//! De-serialize `this` from msgpack. Locks+replaces contents. //! De-serialize `this` from msgpack. Locks+replaces contents.
std::error_code from_bytes(epee::byte_slice source); std::error_code from_bytes(epee::byte_slice source);
// End GROUP
// `sync` is acquired in a `asio::post` function - thread-safe and non-blocking until end
/*! Attempt login and check lookahead status. Updates `lookahead_error`. /*! Attempt login and check lookahead status. Updates `lookahead_error`.
\return No errors if login succeeded, and true if new account */ \return No errors if login succeeded, and true if new account */
expect<bool> login_is_new(); static void login_is_new(std::shared_ptr<wallet> self, std::function<void(expect<bool>)>);
/*! Attempt login and check lookahead status. Updates `lookahead_error`. /*! Attempt login and check lookahead status. Updates `lookahead_error`.
\return No errors if login succeeded. */ \return No errors if login succeeded. */
std::error_code login() { return login_is_new().error(); } template<typename F>
static void login(std::shared_ptr<wallet> self, F f)
{
login_is_new(std::move(self), [f = std::move(f)] (expect<bool> r) mutable { f(r.error()); });
}
//! Refreshes txes information. Strong exception guarantee. //! Refreshes txes information. Strong exception guarantee.
std::error_code refresh(bool mandatory = false); static void refresh(std::shared_ptr<wallet> self, bool mandatory, std::function<void(std::error_code)>);
//! Notify server that new major accounts need to be watched. //! Notify server that new major accounts need to be watched.
std::error_code register_subaccount(std::uint32_t maj_i); static void register_subaccount(std::shared_ptr<wallet> self, std::uint32_t maj_i, std::function<void(std::error_code)> f);
//! Notify server that new minor accounts need to be watched. //! Notify server that new minor accounts need to be watched. Do not hold `self->sync`.
std::error_code register_subaddress(std::uint32_t maj_i, std::uint32_t min_i); static void register_subaddress(std::shared_ptr<wallet> self, std::uint32_t maj_i, std::uint32_t min_i, std::function<void(std::error_code)>);
//! Modify local and possibly server lookahead //! Modify local and possibly server lookahead. Do not hold `self->sync`.
std::error_code set_lookahead(std::uint32_t major, std::uint32_t minor); static void set_lookahead(std::shared_ptr<wallet> self, std::uint32_t major, std::uint32_t minor, std::function<void(std::error_code)>);
expect<rpc::import_response> restore_height(const std::uint64_t height); static void restore_height_raw(std::shared_ptr<wallet> self, const std::uint64_t height, std::function<void(expect<rpc::import_response>)>);
expect<std::vector<rpc::random_outputs>> get_decoys(const rpc::get_random_outs_request& req); template<typename F>
std::error_code send_tx(epee::byte_slice tx_bytes); static void restore_height(std::shared_ptr<wallet> self, const std::uint64_t height, F f)
{
restore_height_raw(std::move(self), height, [f = std::move(f)] (auto r) mutable { f(r.error()); });
}
using decoys_callable = void(expect<std::vector<rpc::random_outputs>>);
static void get_decoys(std::shared_ptr<wallet> self, rpc::get_random_outs_request&& req, std::function<decoys_callable> f);
static void send_tx(std::shared_ptr<wallet> slef, epee::byte_slice tx_bytes, std::function<void(std::error_code)> f);
}; };
}}} // lwsf // internal // backend }}} // lwsf // internal // backend
+18 -1
View File
@@ -27,6 +27,9 @@
#include "error.h" #include "error.h"
#include <cstring>
#include "common/error.h" // monero/src
namespace lwsf namespace lwsf
{ {
const char* get_string(const error value) noexcept const char* get_string(const error value) noexcept
@@ -57,10 +60,14 @@ namespace lwsf
return "server has subaddresses disabled"; return "server has subaddresses disabled";
case error::subaddr_local: case error::subaddr_local:
return "lwsf (local) limits on subaddresses too small"; return "lwsf (local) limits on subaddresses too small";
case error::subaddr_upgrade:
return "server does not support newer subaddr commands";
case error::unexpected_userinfo: case error::unexpected_userinfo:
return "Unexpected user+pass field in URL"; return "Unexpected user+pass field in URL";
case error::unexpected_nullptr: case error::unexpected_nullptr:
return "Unexpected nullptr"; return "Unexpected nullptr";
case error::unknown_exception:
return "Unknown exception";
default: default:
break; break;
@@ -74,7 +81,7 @@ namespace lwsf
{ {
virtual const char* name() const noexcept override final virtual const char* name() const noexcept override final
{ {
return "lws::error_category()"; return "lwsf::error_category()";
} }
virtual std::string message(int value) const override final virtual std::string message(int value) const override final
@@ -85,4 +92,14 @@ namespace lwsf
static const category instance{}; static const category instance{};
return instance; return instance;
} }
void throw_invalid_argument(const int line, const char* file)
{
if (!file)
file = "";
char const* const end = std::strrchr(file, '/');
if (end)
file = end + 1;
throw std::invalid_argument{"Failed pre-condition at " + std::string{file} + ":" + std::to_string(line)};
}
} // lwsf } // lwsf
+11
View File
@@ -28,6 +28,14 @@
#pragma once #pragma once
#include <system_error> #include <system_error>
#include <type_traits>
#define LWSF_VERIFY(x) \
do \
{ \
if (!(x)) \
::lwsf::throw_invalid_argument(__LINE__, __FILE__); \
} while (0)
namespace lwsf namespace lwsf
{ {
@@ -46,8 +54,10 @@ namespace lwsf
subaddr_ahead, //!< Server limits on subaddresses affects lookahead subaddr_ahead, //!< Server limits on subaddresses affects lookahead
subaddr_disabled, //!< Server has subaddresses disabled subaddr_disabled, //!< Server has subaddresses disabled
subaddr_local, //!< Local limits on subaddresses too small subaddr_local, //!< Local limits on subaddresses too small
subaddr_upgrade, //!< Upgrade server for server-side limits
unexpected_userinfo,//!< Unexpected user+pass provided unexpected_userinfo,//!< Unexpected user+pass provided
unexpected_nullptr, //!< Expected non-nullptr unexpected_nullptr, //!< Expected non-nullptr
unknown_exception //!< Unknown exception occured
}; };
//! \return Error message string. //! \return Error message string.
@@ -62,6 +72,7 @@ namespace lwsf
return std::error_code{int(value), error_category()}; return std::error_code{int(value), error_category()};
} }
[[noreturn]] void throw_invalid_argument(int line, const char* file);
} // lwsf } // lwsf
namespace std namespace std
+4
View File
@@ -47,6 +47,9 @@ namespace lwsf { namespace config
constexpr const lookahead default_lookahead{50, 200}; constexpr const lookahead default_lookahead{50, 200};
constexpr const lookahead default_minimal_lookahead{5, 15}; constexpr const lookahead default_minimal_lookahead{5, 15};
constexpr const std::string_view default_primary_name{"Primary account"}; constexpr const std::string_view default_primary_name{"Primary account"};
constexpr const std::size_t http_body_limit = 25 * 1024 * 1024; // 25 MiB
constexpr const std::size_t http_parser_buffer_size = 16 * 1024; // 16 KiB
constexpr const unsigned http_version = 11;
constexpr const std::size_t initial_buffer_size = 1024 * 64; // 64 KiB constexpr const std::size_t initial_buffer_size = 1024 * 64; // 64 KiB
constexpr const std::size_t max_file_read_size = 50 * 1024 * 1024; // 50 MiB constexpr const std::size_t max_file_read_size = 50 * 1024 * 1024; // 50 MiB
constexpr const std::size_t max_inputs_in_rpc = 512; constexpr const std::size_t max_inputs_in_rpc = 512;
@@ -58,4 +61,5 @@ namespace lwsf { namespace config
constexpr const std::chrono::seconds refresh_interval{30}; constexpr const std::chrono::seconds refresh_interval{30};
constexpr const std::chrono::seconds refresh_interval_min{5}; constexpr const std::chrono::seconds refresh_interval_min{5};
constexpr const std::chrono::seconds rpc_timeout{30}; constexpr const std::chrono::seconds rpc_timeout{30};
constexpr const bool use_subaddresses = true; //!< Disables subaddress rpc probing
}} // lwsf // config }} // lwsf // config
-75
View File
@@ -54,81 +54,6 @@
namespace lwsf { namespace internal { namespace rpc namespace lwsf { namespace internal { namespace rpc
{ {
namespace
{
//! \return Error message string.
const char* get_string(error value) noexcept
{
switch (value)
{
default:
break;
case error::none:
return "No rpc errors";
case error::no_response:
return "No response from HTTP server";
case error::invalid_code:
return "Invalid status code from HTTP server";
}
return nullptr;
}
}
//! \return Category for `error`.
const std::error_category& error_category() noexcept
{
struct category final : std::error_category
{
virtual const char* name() const noexcept override final
{
return "lwsf::internal::rpc::error_category()";
}
virtual std::string message(int value) const override final
{
char const * const msg = get_string(error(value));
if (msg)
return msg;
return "HTTP error code " + std::to_string(value);
}
};
static const category instance{};
return instance;
}
expect<std::string> invoke_payload(http_client& client, const boost::string_ref prefix, boost::string_ref endpoint, const epee::byte_slice payload)
{
static const epee::net_utils::http::fields_list headers{
{"Content-Type", "application/json; charset=utf-8"}
};
if (!client.is_connected())
{
if (!client.connect(config::connect_timeout))
return {error::no_response};
}
std::string real;
if (!prefix.empty())
{
real = std::string{prefix} + std::string{endpoint};
endpoint = real;
}
const epee::net_utils::http::http_response_info* response = nullptr;
if (!client.invoke(endpoint, "POST", {reinterpret_cast<const char*>(payload.data()), payload.size()}, config::rpc_timeout, std::addressof(response), headers))
return {error::no_response};
if (!response)
return {error::no_response};
if(response->m_response_code == 200 || response->m_response_code == 201)
return response->m_body;
if (response->m_response_code <= 0 || std::numeric_limits<int>::max() < response->m_response_code)
return {error::invalid_code};
return {error(int(response->m_response_code))};
}
void write_bytes(wire::writer& dest, const empty&) void write_bytes(wire::writer& dest, const empty&)
{ wire::object(dest); } { wire::object(dest); }
void read_bytes(wire::reader& source, empty&) void read_bytes(wire::reader& source, empty&)
+37 -43
View File
@@ -30,7 +30,9 @@
#include <boost/container/flat_map.hpp> #include <boost/container/flat_map.hpp>
#include <boost/container/flat_set.hpp> #include <boost/container/flat_set.hpp>
#include <boost/core/demangle.hpp>
#include <boost/optional/optional.hpp> #include <boost/optional/optional.hpp>
#include <boost/thread/locks.hpp>
#include <boost/variant.hpp> #include <boost/variant.hpp>
#include <ctime> #include <ctime>
#include <system_error> #include <system_error>
@@ -40,57 +42,54 @@
#include "byte_stream.h" // moneor/contrib/epee/include #include "byte_stream.h" // moneor/contrib/epee/include
#include "common/expect.h" // monero/src #include "common/expect.h" // monero/src
#include "crypto/crypto.h" // monero/src #include "crypto/crypto.h" // monero/src
#include "error.h"
#include "lwsf_config.h" #include "lwsf_config.h"
#include "net/http.h"
#include "ringct/rctTypes.h" // monero/src #include "ringct/rctTypes.h" // monero/src
#include "wire/basic_value.h" #include "wire/basic_value.h"
#include "wire/fwd.h" #include "wire/fwd.h"
#include "wire/json.h" #include "wire/json.h"
#include "wire/traits.h" #include "wire/traits.h"
namespace epee { namespace net_utils
{
class blocked_mode_client;
namespace http { template<typename> class http_simple_client_template; }
}}
namespace lwsf { namespace internal { namespace rpc namespace lwsf { namespace internal { namespace rpc
{ {
using max_subaddrs = wire::max_element_count<16384>; using max_subaddrs = wire::max_element_count<16384>;
using http_client = epee::net_utils::http::http_simple_client_template< struct daemon_status;
epee::net_utils::blocked_mode_client
>;
enum class error : int template<typename T, typename F>
struct unpacker
{ {
none = 0, no_response = -1, invalid_code = -2 /* Otherwise HTTP error code */ T* out;
F f;
void operator()(std::error_code error, epee::byte_slice response)
{
LWSF_VERIFY(out);
if (!error)
{
error = wire::json::from_bytes({reinterpret_cast<const char*>(response.data()), response.size()}, *out);
if (error)
MERROR("Failed to unpack " << boost::core::demangle(typeid(T).name()) << ": " << error.message());
}
f(error);
}
}; };
const std::error_category& error_category() noexcept; template<typename T, typename U, typename F>
inline std::error_code make_error_code(const error value) noexcept void invoke_async(const http::client& client, const T& in, U* out, F f)
{ {
return std::error_code{int(value), error_category()}; // /daemon_status historically needed to be a post
} if constexpr (!std::is_empty<T>{} || std::is_same<U, daemon_status>{})
{
//! Send `payload` to `client` at uri `endpoint`, and \return payload response epee::byte_stream sink{};
expect<std::string> invoke_payload(http_client& client, boost::string_ref prefix, boost::string_ref endpoint, epee::byte_slice payload); std::error_code error = wire::json::to_bytes(sink, in);
if (error)
template<typename F, typename G> return f(error);
expect<F> invoke(http_client& client, boost::string_ref prefix, const G& in) client.post_async(U::endpoint(), epee::byte_slice{std::move(sink)}, unpacker<U, F>{out, std::move(f)});
{ }
epee::byte_stream sink{}; else
std::error_code error = wire::json::to_bytes(sink, in); client.get_async(U::endpoint(), unpacker<U, F>{out, std::move(f)});
if (error)
return error;
expect<std::string> result = invoke_payload(client, prefix, F::endpoint(), epee::byte_slice{std::move(sink)});
if (!result)
return result.error();
F out{};
error = wire::json::from_bytes(epee::to_span(*result), out);
if (error)
return error;
return out;
} }
struct empty {}; struct empty {};
@@ -409,6 +408,8 @@ namespace lwsf { namespace internal { namespace rpc
import_response() = delete; import_response() = delete;
import_response(import_response&&) = default; import_response(import_response&&) = default;
import_response(const import_response&) = delete; import_response(const import_response&) = delete;
import_response& operator=(import_response&&) = default;
import_response& operator=(const import_response&) = delete;
static constexpr const char* endpoint() noexcept { return "/import_wallet_request"; } static constexpr const char* endpoint() noexcept { return "/import_wallet_request"; }
boost::optional<std::string> payment_address; boost::optional<std::string> payment_address;
@@ -482,10 +483,3 @@ namespace lwsf { namespace internal { namespace rpc
WIRE_DECLARE_BLOB(lwsf::internal::rpc::ringct); WIRE_DECLARE_BLOB(lwsf::internal::rpc::ringct);
namespace std
{
template<>
struct is_error_code_enum<lwsf::internal::rpc::error>
: true_type
{};
}
+34
View File
@@ -0,0 +1,34 @@
# 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.
set(lwsf-net_sources context.cpp http.cpp wallet_io.cpp)
set(lwsf-net_headers context.h http.h wallet_io.h)
add_library(lwsf-net ${lwsf-net_sources} ${lwsf-net_headers})
target_link_libraries(lwsf-net PRIVATE monero::libraries)
+55
View File
@@ -0,0 +1,55 @@
// 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 "context.h"
#include <boost/thread/locks.hpp>
#include <boost/thread/thread.hpp>
namespace lwsf { namespace internal { namespace net
{
context::context()
: restart_(), io_(), sync_()
{}
std::shared_ptr<int> context::restart_asio()
{
std::shared_ptr<int> out;
const boost::lock_guard<boost::mutex> lock{sync_};
out = restart_.lock();
if (!out || io_.stopped())
{
out.reset(); // need to restart //
while (restart_.lock())
boost::this_thread::sleep_for(boost::chrono::milliseconds{50});
io_.restart();
out = std::make_shared<int>();
restart_ = out;
}
return out;
}
}}} // lwsf // internal // net
+101
View File
@@ -0,0 +1,101 @@
// 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.
#pragma once
#include <boost/asio/io_context.hpp>
#include <boost/thread/mutex.hpp>
#include <chrono>
#include <future>
#include <memory>
#include <utility>
namespace lwsf { namespace internal { namespace net
{
template<typename T>
struct to_future
{
struct inner
{
std::promise<T> promise;
inner()
: promise()
{}
};
std::shared_ptr<inner> inner_;
to_future()
: inner_(std::make_shared<inner>())
{}
void operator()(T value)
{
LWSF_VERIFY(inner_);
inner_->promise.set_value(std::move(value));
}
};
struct context
{
std::weak_ptr<int> restart_;
boost::asio::io_context io_;
boost::mutex sync_;
explicit context();
//! Restart `io_` if needed, and get an object that prevents future restarts.
std::shared_ptr<int> restart_asio();
template<typename T, typename F>
T wait_for(F f)
{
to_future<T> callable{};
auto value = callable.inner_->promise.get_future();
f(std::move(callable));
for (;;)
{
switch (value.wait_for(std::chrono::seconds{0}))
{
case std::future_status::deferred:
case std::future_status::ready:
return value.get();
case std::future_status::timeout:
{
const auto restart_lock = restart_asio();
io_.run_one_for(std::chrono::milliseconds{100});
}
break;
};
}
throw std::logic_error{"unreachable"};
}
};
}}} // lwsf // internal // net
+688
View File
@@ -0,0 +1,688 @@
// Copyright (c) 2024, The Monero Project
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "http.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/ip/address_v6.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/post.hpp>
#include <boost/asio/ssl/context.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/strand.hpp>
#include <boost/beast/core/flat_static_buffer.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/version.hpp>
#include <boost/optional/optional.hpp>
#include <boost/thread/locks.hpp>
#include <cstdint>
#include <deque>
#include <limits>
#include <ostream>
#include "byte_stream.h" // monero/controib/epee/include
#include "common/error.h" // monero/src
#include "error.h"
#include "lwsf_config.h"
#include "misc_log_ex.h" // monero/contrib/epee/include
#include "net/net_utils_base.h"
#include "net/socks.h" // monero/src
#include "string_tools.h" // monero/contrib/epee/include
namespace lwsf { namespace internal { namespace http
{
namespace
{
const char* get_string(error value) noexcept
{
switch (value)
{
default:
break;
case error::none:
return "No rpc errors";
case error::timeout:
return "No response from HTTP server";
case error::invalid_code:
return "Invalid status code from HTTP server";
}
return nullptr;
}
struct message
{
message(epee::byte_slice body, std::string target, std::function<server_response_func>&& notifier, boost::beast::http::verb verb)
: body(std::move(body)),
notifier(std::move(notifier)),
target(std::move(target)),
verb(verb)
{}
message(message&&) = default;
message(const message& rhs)
: body(rhs.body.clone()),
notifier(rhs.notifier),
target(rhs.target),
verb(rhs.verb)
{}
bool is_get() const noexcept { return verb == boost::beast::http::verb::get; }
epee::byte_slice body;
std::function<server_response_func> notifier;
std::string target;
boost::beast::http::verb verb;
};
std::ostream& operator<<(std::ostream& out, const message& src)
{
out << src.target;
return out;
}
struct stream_body
{
using value_type = epee::byte_stream;
struct reader
{
epee::byte_stream& body_;
template<bool isRequest, class Fields>
reader(boost::beast::http::header<isRequest, Fields>&, value_type& body)
: body_(body)
{}
void init(boost::optional<std::uint64_t> const& content_length, boost::system::error_code& ec)
{
static_assert(
std::numeric_limits<std::uint64_t>::max() <= std::numeric_limits<std::size_t>::max()
);
if (content_length)
body_.reserve(*content_length);
ec = {};
}
template<class ConstBufferSequence>
std::size_t put(ConstBufferSequence const& buffers, boost::system::error_code& ec)
{
const std::size_t size = boost::asio::buffer_size(buffers);
body_.write({reinterpret_cast<const std::uint8_t*>(buffers.data()), size});
ec = {};
return size;
}
void finish(boost::system::error_code& ec)
{
ec = {};
}
};
};
struct slice_body
{
using value_type = epee::byte_slice;
static std::size_t size(const value_type& source) noexcept
{
return source.size();
};
struct writer
{
epee::byte_slice body_;
using const_buffers_type = boost::asio::const_buffer;
template<bool is_request, typename Fields>
explicit writer(boost::beast::http::header<is_request, Fields> const&, value_type const& body)
: body_(body.clone())
{}
void init(boost::beast::error_code& ec)
{
ec = {};
}
boost::optional<std::pair<const_buffers_type, bool>> get(boost::beast::error_code& ec)
{
ec = {};
return {{const_buffers_type{body_.data(), body_.size()}, false}};
}
};
};
} // anonymous
//! \return Category for `error`.
const std::error_category& error_category() noexcept
{
struct category final : std::error_category
{
virtual const char* name() const noexcept override final
{
return "lwsf::internal::http::error_category()";
}
virtual std::string message(int value) const override final
{
char const * const msg = get_string(error(value));
if (msg)
return msg;
return "HTTP error code " + std::to_string(value);
}
};
static const category instance{};
return instance;
}
struct client_state
{
using connect_func =
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;
std::deque<message> outgoing;
const std::string host;
const std::string prefix;
boost::asio::steady_timer timer;
boost::asio::io_context::strand strand;
boost::asio::ip::tcp::endpoint endpoint;
boost::beast::http::request<slice_body> request;
const epee::net_utils::ssl_options_t ssl;
boost::asio::ssl::context ssl_context;
boost::asio::ssl::stream<boost::asio::ip::tcp::socket> sock;
boost::optional<boost::beast::http::parser<false, stream_body>> parser;
std::size_t iteration;
const std::uint16_t port;
std::atomic<bool> is_connected;
epee::net_utils::ssl_support_t ssl_status;
client_state(boost::asio::io_context& io, std::string host, std::string prefix, const std::uint16_t port, epee::net_utils::ssl_options_t in, std::function<connect_func> connect)
: buffer{},
connect(std::move(connect)),
outgoing(),
host(std::move(host)),
prefix(std::move(prefix)),
timer(io),
strand(io),
endpoint(),
request{},
ssl(std::move(in)),
ssl_context(ssl.create_context()),
sock(io, ssl_context),
parser(),
iteration(0),
port(port),
is_connected(false),
ssl_status(ssl.support)
{}
template<typename F>
void async_write(F&& callback)
{
assert(!outgoing.empty());
const bool no_body = outgoing.front().body.empty();
if (!prefix.empty())
outgoing.front().target.insert(0, prefix.data(), prefix.size());
request = {
outgoing.front().verb,
outgoing.front().target,
config::http_version,
std::move(outgoing.front().body)
};
request.set(boost::beast::http::field::user_agent, BOOST_BEAST_VERSION_STRING);
if (!no_body)
request.set(boost::beast::http::field::content_type, "application/json");
// Setting Host is tricky. Check for v6 and non-standard ports
boost::system::error_code error{};
boost::asio::ip::make_address_v6(host, error);
const bool https = ssl_status == epee::net_utils::ssl_support_t::e_ssl_support_enabled;
if (!error)
request.set(boost::beast::http::field::host, "[" + host + "]:" + std::to_string(port));
else if ((https && port == 443) || (!https && port == 80))
request.set(boost::beast::http::field::host, host);
else
request.set(boost::beast::http::field::host, host + ":" + std::to_string(port));
request.prepare_payload();
if (https)
boost::beast::http::async_write(sock, request, boost::asio::bind_executor(strand, std::forward<F>(callback)));
else
boost::beast::http::async_write(sock.next_layer(), request, boost::asio::bind_executor(strand, std::forward<F>(callback)));
}
template<typename F>
void async_read(F&& callback)
{
assert(sock);
assert(!outgoing.empty());
parser.emplace();
parser->body_limit(config::http_body_limit);
if (ssl_status == epee::net_utils::ssl_support_t::e_ssl_support_enabled)
boost::beast::http::async_read(sock, buffer, *parser, boost::asio::bind_executor(strand, std::forward<F>(callback)));
else
boost::beast::http::async_read(sock.next_layer(), buffer, *parser, boost::asio::bind_executor(strand, std::forward<F>(callback)));
}
void close()
{
MWARNING("Closing socket to " << host);
++iteration;
is_connected = false;
boost::system::error_code ignore{};
timer.cancel();
sock.next_layer().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ignore);
sock.next_layer().close(ignore);
}
void notify_connection_error()
{
for (auto& elem : outgoing)
{
if (elem.notifier)
elem.notifier(error::timeout, {});
}
outgoing.clear();
close();
}
};
namespace
{
template<typename F>
void set_timeout(std::shared_ptr<client_state> self, const std::chrono::steady_clock::duration timeout, F f)
{
LWSF_VERIFY(self);
struct on_timeout
{
on_timeout() = delete;
std::shared_ptr<client_state> self_;
F f_;
const std::size_t iteration;
void operator()(const boost::system::error_code error)
{
LWSF_VERIFY(self_);
if (!self_ || error == boost::asio::error::operation_aborted)
return;
if (iteration < self_->iteration)
return;
MWARNING("Timeout in HTTP attempt to " << self_->host);
f_(error);
}
};
client_state& me = *self;
me.timer.expires_after(timeout);
me.timer.async_wait(boost::asio::bind_executor(me.strand, on_timeout{std::move(self), std::move(f), me.iteration}));
}
void set_timeout(std::shared_ptr<client_state> self, const std::chrono::steady_clock::duration timeout)
{
LWSF_VERIFY(self);
struct on_timeout
{
on_timeout() = delete;
std::shared_ptr<client_state> self_;
void operator()(boost::system::error_code error = {}) const
{
LWSF_VERIFY(self_);
assert(self_->strand.running_in_this_thread());
self_->close();
}
};
set_timeout(self, timeout, on_timeout{self});
}
class client_loop : public boost::asio::coroutine
{
std::shared_ptr<client_state> self_;
public:
explicit client_loop(std::shared_ptr<client_state> self) noexcept
: boost::asio::coroutine(), self_(std::move(self))
{}
void operator()(boost::system::error_code error = {}, std::size_t = 0)
{
LWSF_VERIFY(self_);
client_state& self = *self_;
assert(self.strand.running_in_this_thread());
BOOST_ASIO_CORO_REENTER(*this)
{
if (!self.is_connected)
{
do_connect:
BOOST_ASIO_CORO_YIELD self.connect(self_, *this);
if (!error && self.ssl_status != epee::net_utils::ssl_support_t::e_ssl_support_disabled)
{
++self.iteration;
set_timeout(self_, config::connect_timeout); // socks needs a reset
self.ssl.configure(self.sock, boost::asio::ssl::stream_base::client, self.host);
MDEBUG("Starting SSL handshake to " << self.host << " for HTTP");
BOOST_ASIO_CORO_YIELD self.sock.async_handshake(
boost::asio::ssl::stream<boost::asio::ip::tcp::socket>::client,
boost::asio::bind_executor(self.strand, std::move(*this))
);
if (error)
{
MERROR("SSL handshake to " << self.host << " failed: " << error.message());
if (self.ssl_status == epee::net_utils::ssl_support_t::e_ssl_support_autodetect)
{
MINFO("Retrying to << " << self.host << " without ssl (autodetect mode)");
self.ssl_status = epee::net_utils::ssl_support_t::e_ssl_support_disabled;
error = {};
++self.iteration;
goto do_connect;
}
}
else
self.ssl_status = epee::net_utils::ssl_support_t::e_ssl_support_enabled;
}
}
if (error)
return self.notify_connection_error();
++self.iteration;
self.is_connected = true;
while (!self.outgoing.empty())
{
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());
BOOST_ASIO_CORO_YIELD self.async_write(std::move(*this));
if (!error)
{
MDEBUG("Starting read from " << self.outgoing.front() << " to previous HTTP message");
BOOST_ASIO_CORO_YIELD self.async_read(std::move(*this));
if (error)
MERROR("Failed to parse HTTP response from " << self.outgoing.front() << ": " << error.message());
else if (self.parser->get().result_int() != 200 && self.parser->get().result_int() != 201)
{
const auto result = self.parser->get().result_int();
MERROR(self.outgoing.front() << " returned " << result << " status code");
if (0 < result && result <= std::numeric_limits<int>::max())
self.outgoing.front().notifier(http::error(result), epee::byte_slice{});
else
self.outgoing.front().notifier(http::error::invalid_code, epee::byte_slice{});
}
else
{
MDEBUG(self.outgoing.front() << " successful");
if (self.outgoing.front().notifier)
self.outgoing.front().notifier({}, epee::byte_slice{std::move(self.parser->get()).body()});
}
}
else
MERROR("Failed HTTP " << (self.outgoing.front().is_get() ? "GET" : "POST") << " to " << self.outgoing.front() << ": " << error.message());
// if write, read, or parse errors
if (error)
return self.notify_connection_error();
self.outgoing.pop_front();
++self.iteration;
if (!self.parser->get().keep_alive())
{
self.close();
if (!self.outgoing.empty())
return client_loop{self_}();
}
}
}
}
};
} // 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});
}
);
}
void client::direct::operator()(std::shared_ptr<client_state> self, std::function<callback_func> f) const
{
struct frame
{
std::function<callback_func> f;
boost::asio::ip::tcp::resolver resolver;
const std::size_t iteration;
frame(client_state& self, std::function<callback_func>&& f)
: f(std::move(f)), resolver(self.strand.context()), iteration(self.iteration)
{}
};
struct handler : boost::asio::coroutine
{
std::shared_ptr<client_state> self_;
std::shared_ptr<frame> frame_;
explicit handler(std::shared_ptr<client_state> self, std::shared_ptr<frame>&& in)
: boost::asio::coroutine(), self_(std::move(self)), frame_(std::move(in))
{}
void operator()(boost::system::error_code error = {})
{
LWSF_VERIFY(self_ && frame_);
client_state& self = *self_;
assert(self.strand.running_in_this_thread());
BOOST_ASIO_CORO_REENTER(*this)
{
struct resolve
{
handler continue_;
void operator()(const boost::system::error_code error, const boost::asio::ip::tcp::resolver::results_type& ips)
{
if (error)
std::move(continue_)(error);
else if (ips.empty())
std::move(continue_)(boost::asio::error::host_not_found);
else if (continue_.self_)
{
continue_.self_->endpoint = *ips.begin();
std::move(continue_)(error);
}
}
};
struct on_timeout
{
std::shared_ptr<client_state> self_;
std::shared_ptr<frame> frame_;
void operator()(boost::system::error_code error) const
{
LWSF_VERIFY(frame_ && self_);
assert(self_->strand.running_in_this_thread());
frame_->resolver.cancel();
self_->close();
frame_->f(boost::asio::error::operation_aborted);
}
};
set_timeout(self_, config::connect_timeout, on_timeout{self_, frame_});
MDEBUG("Resolving " << self.host << " for HTTP");
BOOST_ASIO_CORO_YIELD frame_->resolver.async_resolve(
self.host, std::to_string(self.port), boost::asio::bind_executor(self.strand, resolve{*this})
);
if (!error)
{
MDEBUG("Connecting to " << self.endpoint << " / " << self.host << " for HTTP");
BOOST_ASIO_CORO_YIELD self.sock.next_layer().async_connect(
self.endpoint, boost::asio::bind_executor(self.strand, std::move(*this))
);
if (error)
MERROR("Failed to connect to " << self.host << ": " << error.message());
}
else
MERROR("Failed to resolve TCP/IP address for " << self.host << ": " << error.message());
if (self_->iteration <= frame_->iteration)
frame_->f(error);
}
}
};
LWSF_VERIFY(self);
handler{self, std::make_shared<frame>(*self, std::move(f))}();
}
void client::socks::operator()(std::shared_ptr<client_state> self, std::function<callback_func> f) const
{
struct handler
{
std::shared_ptr<client_state> self_;
std::function<callback_func> f_;
void operator()(boost::system::error_code error, boost::asio::ip::tcp::socket&& sock)
{
LWSF_VERIFY(self_ && f_);
if (error)
MERROR("Failed socks connection: " << error.message());
self_->sock.next_layer() = std::move(sock);
std::function<callback_func> f{std::move(f_)};
boost::asio::dispatch(self_->strand, [f = std::move(f), error] { f(error); });
};
};
LWSF_VERIFY(self && f);
std::shared_ptr<::net::socks::client> proxy = ::net::socks::make_connect_client(
std::move(self->sock.next_layer()), ::net::socks::version::v4a, handler{self, f}
);
bool is_set = false;
std::uint32_t ip_address = 0;
if (epee::string_tools::get_ip_int32_from_string(ip_address, self->host))
is_set = proxy->set_connect_command(epee::net_utils::ipv4_network_address{ip_address, self->port});
else
is_set = proxy->set_connect_command(self->host, self->port);
if (is_set)
{
set_timeout(self, config::connect_timeout, ::net::socks::client::async_close{proxy});
is_set = ::net::socks::client::connect_and_send(std::move(proxy), proxy_address);
}
if (!is_set)
{
MERROR("Failed to initiate socks proxy");
f(boost::asio::error::fault);
}
}
void client::init(boost::asio::io_context& io, std::string host, std::string prefix, std::uint16_t port, epee::net_utils::ssl_options_t ssl)
{
const boost::lock_guard<boost::mutex> lock{sync_};
state_ = std::make_shared<client_state>(io, std::move(host), std::move(prefix), port, std::move(ssl), proxy_);
}
client::~client()
{}
bool client::is_connected() const noexcept
{
const boost::lock_guard<boost::mutex> lock{sync_};
return state_ && state_->is_connected;
}
void client::set_proxy(std::function<connect_func> connector)
{
LWSF_VERIFY(connector);
const boost::lock_guard<boost::mutex> lock{sync_};
proxy_ = std::move(connector);
if (state_)
state_ = std::make_shared<client_state>(state_->strand.context(), state_->host, state_->prefix, state_->port, state_->ssl, proxy_);
}
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);
}
void client::get_async(std::string url, std::function<server_response_func> notifier) const
{
if (!notifier)
throw std::logic_error{"net::http::client::get_async requires callback"};
return queue_async(std::move(url), epee::byte_slice{}, std::move(notifier), boost::beast::http::verb::get);
}
}}} // lwsf // internal // http
+117
View File
@@ -0,0 +1,117 @@
// Copyright (c) 2024, The Monero Project
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#pragma once
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/beast/http/verb.hpp>
#include <boost/system/error_code.hpp>
#include <boost/thread/mutex.hpp>
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <system_error>
#include "byte_slice.h" // monero/contrib/epee/include
#include "net/net_ssl.h" // monero/contrib/epee/include
namespace lwsf { namespace internal { namespace http
{
enum class error : int
{
none = 0, timeout = -1, invalid_code = -2 /* Otherwise HTTP error code */
};
const std::error_category& error_category() noexcept;
inline std::error_code make_error_code(const error value) noexcept
{
return std::error_code{int(value), error_category()};
}
struct client_state;
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>);
//! Primarily for webhooks, where the response is (basically) ignored.
class client
{
std::shared_ptr<client_state> state_;
std::function<connect_func> proxy_;
mutable boost::mutex sync_;
void queue_async(std::string&& target, epee::byte_slice&& body, std::function<server_response_func>&& notifier, boost::beast::http::verb verb) const;
public:
struct direct
{
void operator()(std::shared_ptr<client_state>, std::function<callback_func>) const;
};
struct socks
{
boost::asio::ip::tcp::endpoint proxy_address;
void operator()(std::shared_ptr<client_state>, std::function<callback_func>) const;
};
client()
: state_(nullptr), proxy_(direct{}), sync_()
{}
void init(boost::asio::io_context& io, std::string host, std::string prefix, std::uint16_t port, epee::net_utils::ssl_options_t ssl);
client(client&&) = delete;
client(const client&) = delete;
~client();
client& operator=(client&&) = delete;
client& operator=(const client&) = delete;
//! thread-safe
bool is_connected() const noexcept;
//! thread-safe
void set_proxy(std::function<connect_func> connector);
//! Never blocks. Thread safe. \return `success()` if `url` is valid.
void post_async(std::string target, epee::byte_slice body, std::function<server_response_func> notifier) const;
/*! Never blocks. Thread safe. Calls `notifier` with server response iff
`success()` is returned.
\return `success()` if `url` is valid. */
void get_async(std::string target, std::function<server_response_func> notifier) const;
};
}}} // lwsf // internal // http
namespace std
{
template<>
struct is_error_code_enum<lwsf::internal::http::error>
: true_type
{};
}
+40
View File
@@ -0,0 +1,40 @@
// 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 "wallet_io.h"
#include "backend.h"
#include "net/context.h"
namespace lwsf { namespace internal { namespace net
{
wallet_io::wallet_io()
: context(std::make_shared<net::context>()),
wallet(std::make_shared<backend::wallet>(context->io_))
{}
}}} // lwsf // internal // net
+56
View File
@@ -0,0 +1,56 @@
// 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.
#pragma once
#include <memory>
namespace lwsf { namespace internal
{
namespace backend { struct wallet; }
namespace net
{
struct context;
/*! \brief Pass together because `wallet` has references to `context`.
ASIO routines only take the `wallet` object, otherwise cyclic references
can occur during destruction (when I/O is pending at time of destruction).
Additionally, `context` must remain active until `wallet` is destructed, or
ASIO functions have a dangling reference. Pass this object when `wallet` is
needed, and provide `wallet` to the `static` methods of `backend::wallet`
that perform ASIO routines. */
struct wallet_io
{
std::shared_ptr<net::context> context; //<! Must be first (destruct last)
std::shared_ptr<backend::wallet> wallet; //!< Has references to `context`
wallet_io();
};
} // net
}} // lwsf // intenral
+36 -27
View File
@@ -26,16 +26,17 @@
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF // 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. // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "backend.h"
#include "pending_transaction.h" #include "pending_transaction.h"
#include <boost/filesystem/operations.hpp> #include <boost/filesystem/operations.hpp>
#include <boost/thread/lock_guard.hpp> #include <boost/thread/lock_guard.hpp>
#include <fstream> #include <fstream>
#include <stdexcept> #include <stdexcept>
#include "backend.h"
#include "byte_slice.h" // monero/contrib/epee/include #include "byte_slice.h" // monero/contrib/epee/include
#include "cryptonote_basic/cryptonote_format_utils.h" // monero/src #include "cryptonote_basic/cryptonote_format_utils.h" // monero/src
#include "error.h" #include "error.h"
#include "net/context.h"
#include "numeric.h" #include "numeric.h"
#include "string_tools.h" // monero/contrib/epee/include #include "string_tools.h" // monero/contrib/epee/include
#include "utils/encrypted_file.h" #include "utils/encrypted_file.h"
@@ -100,40 +101,37 @@ namespace lwsf { namespace internal
} }
pending_transaction::pending_transaction(std::shared_ptr<backend::wallet> wallet, std::string error, std::vector<std::shared_ptr<backend::transaction>> local) pending_transaction::pending_transaction(const net::wallet_io& data, std::string error, std::vector<std::shared_ptr<backend::transaction>> local)
: wallet_(std::move(wallet)), local_(std::move(local)), error_(std::move(error)) : data_(data), local_(std::move(local)), error_(std::move(error))
{ {
if (!wallet_) LWSF_VERIFY(data_.context && data_.wallet);
throw std::invalid_argument{"lwsf::internal::pending_transaction cannot be given nullptr backend"};
for (const auto& e : local_) for (const auto& e : local_)
{ {
if (!e) LWSF_VERIFY(bool(e));
throw std::invalid_argument{"lwsf::internal::pending_transaction cannot be given nullptr backend::transaction"};
} }
} }
std::unique_ptr<pending_transaction> pending_transaction::load_from_file(std::shared_ptr<backend::wallet> wallet, const std::string& filename) std::unique_ptr<pending_transaction> pending_transaction::load_from_file(const net::wallet_io& data, const std::string& filename)
{ {
if (!wallet) LWSF_VERIFY(data.wallet);
throw std::invalid_argument{"lwsf::internal::pending_transaction::load_from_file cannot be given nullptr backend"};
epee::byte_slice file = try_load(filename, tx_file_magic); epee::byte_slice file = try_load(filename, tx_file_magic);
if (file.empty()) if (file.empty())
return std::make_unique<pending_transaction>(wallet, "Invalid path/file for tx"); return std::make_unique<pending_transaction>(data, "Invalid path/file for tx");
expect<epee::byte_slice> payload = epee::byte_slice{}; expect<epee::byte_slice> payload = epee::byte_slice{};
{ {
const boost::lock_guard<boost::mutex> lock{wallet->sync}; const boost::lock_guard<boost::mutex> lock{data.wallet->sync};
payload = decrypt(std::move(file), epee::as_byte_span(unwrap(unwrap(wallet->primary.view.sec)))); payload = decrypt(std::move(file), epee::as_byte_span(unwrap(unwrap(data.wallet->primary.view.sec))));
} }
if (!payload) if (!payload)
return std::make_unique<pending_transaction>(wallet, payload.error().message()); return std::make_unique<pending_transaction>(data, payload.error().message());
txes_file dest{}; txes_file dest{};
if (std::error_code error = wire::msgpack::from_bytes(std::move(*payload), dest)) if (std::error_code error = wire::msgpack::from_bytes(std::move(*payload), dest))
return std::make_unique<pending_transaction>(wallet, error.message()); return std::make_unique<pending_transaction>(data, error.message());
return std::make_unique<pending_transaction>(wallet, "", std::move(dest.txes)); return std::make_unique<pending_transaction>(data, "", std::move(dest.txes));
} }
pending_transaction::~pending_transaction() pending_transaction::~pending_transaction()
@@ -144,18 +142,29 @@ namespace lwsf { namespace internal
if (!error_.empty()) if (!error_.empty())
return false; return false;
for (auto e : local_) try
{ {
const std::error_code error = wallet_->send_tx(e->raw_bytes.clone()); for (auto e : local_)
if (error)
{ {
error_ = error.message(); const auto error = data_.context->wait_for<std::error_code>(
return false; [this, &e] (auto&& f)
{ backend::wallet::send_tx(this->data_.wallet, e->raw_bytes.clone(), std::move(f)); }
);
if (error)
{
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);
} }
const boost::lock_guard<boost::mutex> lock{wallet_->sync}; }
auto entry = wallet_->primary.txes.try_emplace(e->id).first; catch(const std::exception& e)
if (!entry->second) {
entry->second = std::move(e); error_ = e.what();
return false;
} }
return true; return true;
} }
@@ -189,12 +198,12 @@ namespace lwsf { namespace internal
return false; return false;
} }
const boost::lock_guard<boost::mutex> lock{wallet_->sync}; const boost::lock_guard<boost::mutex> lock{data_.wallet->sync};
bytes = encrypt( bytes = encrypt(
tx_file_magic, tx_file_magic,
epee::byte_slice{std::move(sink)}, epee::byte_slice{std::move(sink)},
0 /* iterations */, 0 /* iterations */,
epee::as_byte_span(unwrap(unwrap(wallet_->primary.view.sec))) epee::as_byte_span(unwrap(unwrap(data_.wallet->primary.view.sec)))
); );
} }
+5 -8
View File
@@ -36,26 +36,23 @@
#include <vector> #include <vector>
#include "common/expect.h" // monero/src #include "common/expect.h" // monero/src
#include "cryptonote_basic/cryptonote_basic.h" // monero/src #include "cryptonote_basic/cryptonote_basic.h" // monero/src
#include "net/wallet_io.h"
#include "wallet/api/wallet2_api.h" // moneor/src #include "wallet/api/wallet2_api.h" // moneor/src
#include "wire/fwd.h" #include "wire/fwd.h"
namespace lwsf { namespace internal namespace lwsf { namespace internal
{ {
namespace backend namespace backend { struct transaction; }
{
struct transaction;
struct wallet;
}
class pending_transaction final : public Monero::PendingTransaction class pending_transaction final : public Monero::PendingTransaction
{ {
const std::shared_ptr<backend::wallet> wallet_; const net::wallet_io data_;
const std::vector<std::shared_ptr<backend::transaction>> local_; const std::vector<std::shared_ptr<backend::transaction>> local_;
std::string error_; std::string error_;
public: public:
pending_transaction(std::shared_ptr<backend::wallet> wallet, std::string error, std::vector<std::shared_ptr<backend::transaction>> local = {}); pending_transaction(const net::wallet_io& data, std::string error, std::vector<std::shared_ptr<backend::transaction>> local = {});
static std::unique_ptr<pending_transaction> load_from_file(std::shared_ptr<backend::wallet> wallet, const std::string& filename); static std::unique_ptr<pending_transaction> load_from_file(const net::wallet_io& data, const std::string& filename);
pending_transaction(pending_transaction&&) = delete; pending_transaction(pending_transaction&&) = delete;
pending_transaction(const pending_transaction&) = delete; pending_transaction(const pending_transaction&) = delete;
+12 -12
View File
@@ -33,6 +33,7 @@
#include <utility> #include <utility>
#include "backend.h" #include "backend.h"
#include "cryptonote_basic/cryptonote_format_utils.h" // monero/src #include "cryptonote_basic/cryptonote_format_utils.h" // monero/src
#include "error.h"
namespace lwsf { namespace internal namespace lwsf { namespace internal
{ {
@@ -66,11 +67,10 @@ namespace lwsf { namespace internal
} }
} }
subaddress_account::subaddress_account(internal::wallet* wal, std::shared_ptr<backend::wallet> data) subaddress_account::subaddress_account(internal::wallet* wal, const net::wallet_io& data)
: wal_(wal), data_(std::move(data)), rows_() : wal_(wal), data_(data), rows_()
{ {
if (!wal_ || !data_) LWSF_VERIFY(wal_ && data_.wallet);
throw std::invalid_argument{"lwsf::internal::subaddress_account cannot be given nullptr"};
} }
subaddress_account::~subaddress_account() subaddress_account::~subaddress_account()
@@ -88,8 +88,8 @@ namespace lwsf { namespace internal
void subaddress_account::setLabel(uint32_t accountIndex, const std::string &label) void subaddress_account::setLabel(uint32_t accountIndex, const std::string &label)
{ {
{ {
const boost::lock_guard<boost::mutex> lock{data_->sync}; const boost::lock_guard<boost::mutex> lock{data_.wallet->sync};
data_->primary.subaccounts.at(accountIndex).detail.try_emplace(0).first->second.label = label; data_.wallet->primary.subaccounts.at(accountIndex).detail.try_emplace(0).first->second.label = label;
} }
refresh(); refresh();
} }
@@ -101,11 +101,11 @@ namespace lwsf { namespace internal
free_rows(rows_); free_rows(rows_);
std::unordered_map<std::uint32_t, balance> balances; std::unordered_map<std::uint32_t, balance> balances;
const boost::lock_guard<boost::mutex> lock{data_->sync}; const boost::lock_guard<boost::mutex> lock{data_.wallet->sync};
const Monero::NetworkType net_type = data_->primary.type; const Monero::NetworkType net_type = data_.wallet->primary.type;
const std::uint32_t chain_height = data_->blockchain_height; const std::uint32_t chain_height = data_.wallet->blockchain_height;
for (const auto& tx : data_->primary.txes) for (const auto& tx : data_.wallet->primary.txes)
{ {
if (tx.second->failed) if (tx.second->failed)
continue; continue;
@@ -130,7 +130,7 @@ namespace lwsf { namespace internal
static constexpr const std::uint32_t max_index = static constexpr const std::uint32_t max_index =
std::numeric_limits<std::uint32_t>::max(); std::numeric_limits<std::uint32_t>::max();
const auto& accts = data_->primary.subaccounts; const auto& accts = data_.wallet->primary.subaccounts;
rows_.reserve(accts.size()); rows_.reserve(accts.size());
for (std::size_t i = 0; i < accts.size() && i <= max_index; ++i) for (std::size_t i = 0; i < accts.size() && i <= max_index; ++i)
{ {
@@ -138,7 +138,7 @@ namespace lwsf { namespace internal
rows_.push_back( rows_.push_back(
new Monero::SubaddressAccountRow{ new Monero::SubaddressAccountRow{
std::uint32_t(i), std::uint32_t(i),
data_->get_spend_address({std::uint32_t(i), 0}), data_.wallet->get_spend_address({std::uint32_t(i), 0}),
std::string{accts.at(i).primary_label()}, std::string{accts.at(i).primary_label()},
cryptonote::print_money(balance.total), cryptonote::print_money(balance.total),
cryptonote::print_money(balance.unlocked) cryptonote::print_money(balance.unlocked)
+3 -2
View File
@@ -31,6 +31,7 @@
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <vector> #include <vector>
#include "net/wallet_io.h"
#include "wallet.h" #include "wallet.h"
#include "wallet/api/wallet2_api.h" // monero/src #include "wallet/api/wallet2_api.h" // monero/src
@@ -41,12 +42,12 @@ namespace lwsf { namespace internal
class subaddress_account final : public ::Monero::SubaddressAccount class subaddress_account final : public ::Monero::SubaddressAccount
{ {
internal::wallet* const wal_; internal::wallet* const wal_;
const std::shared_ptr<backend::wallet> data_; const net::wallet_io data_;
std::vector<Monero::SubaddressAccountRow*> rows_; std::vector<Monero::SubaddressAccountRow*> rows_;
public: public:
explicit subaddress_account(internal::wallet* wal, std::shared_ptr<backend::wallet> data); explicit subaddress_account(internal::wallet* wal, const net::wallet_io& data);
subaddress_account(const subaddress_account&) = delete; subaddress_account(const subaddress_account&) = delete;
subaddress_account(subaddress_account&&) = delete; subaddress_account(subaddress_account&&) = delete;
+10 -10
View File
@@ -33,6 +33,7 @@
#include <utility> #include <utility>
#include "backend.h" #include "backend.h"
#include "cryptonote_basic/cryptonote_format_utils.h" // monero/src #include "cryptonote_basic/cryptonote_format_utils.h" // monero/src
#include "error.h"
namespace lwsf { namespace internal namespace lwsf { namespace internal
{ {
@@ -46,11 +47,10 @@ namespace lwsf { namespace internal
} }
} }
subaddress_minor::subaddress_minor(internal::wallet* wal, std::shared_ptr<backend::wallet> data) subaddress_minor::subaddress_minor(internal::wallet* wal, const net::wallet_io& data)
: wal_(wal), data_(std::move(data)), rows_() : wal_(wal), data_(data), rows_()
{ {
if (!wal_ || !data_) LWSF_VERIFY(wal_ && data_.wallet);
throw std::invalid_argument{"lwsf::internal::subaddress_minor cannot be given nullptr"};
} }
subaddress_minor::~subaddress_minor() subaddress_minor::~subaddress_minor()
@@ -64,11 +64,11 @@ namespace lwsf { namespace internal
{ {
free_rows(rows_); free_rows(rows_);
const boost::lock_guard<boost::mutex> lock{data_->sync}; const boost::lock_guard<boost::mutex> lock{data_.wallet->sync};
if (data_->primary.subaccounts.size() <= accountIndex) if (data_.wallet->primary.subaccounts.size() <= accountIndex)
return; return;
const auto& acct = data_->primary.subaccounts[accountIndex]; const auto& acct = data_.wallet->primary.subaccounts[accountIndex];
static_assert(std::is_same<std::uint32_t, decltype(acct.last)>()); static_assert(std::is_same<std::uint32_t, decltype(acct.last)>());
rows_.reserve(std::size_t(acct.last) + 1); rows_.reserve(std::size_t(acct.last) + 1);
for (std::uint64_t i = 0; i <= acct.last; ++i) for (std::uint64_t i = 0; i <= acct.last; ++i)
@@ -76,7 +76,7 @@ namespace lwsf { namespace internal
rows_.push_back( rows_.push_back(
new Monero::SubaddressRow{ new Monero::SubaddressRow{
std::uint32_t(i), std::uint32_t(i),
data_->get_spend_address({accountIndex, std::uint32_t(i)}), data_.wallet->get_spend_address({accountIndex, std::uint32_t(i)}),
std::string{acct.sub_label(i)} std::string{acct.sub_label(i)}
} }
); );
@@ -99,8 +99,8 @@ namespace lwsf { namespace internal
void subaddress_minor::setLabel(const std::uint32_t accountIndex, const std::uint32_t addressIndex, const std::string &label) void subaddress_minor::setLabel(const std::uint32_t accountIndex, const std::uint32_t addressIndex, const std::string &label)
{ {
{ {
const boost::lock_guard<boost::mutex> lock{data_->sync}; const boost::lock_guard<boost::mutex> lock{data_.wallet->sync};
auto& acct = data_->primary.subaccounts.at(accountIndex); auto& acct = data_.wallet->primary.subaccounts.at(accountIndex);
if (!addressIndex || !label.empty()) if (!addressIndex || !label.empty())
acct.detail.try_emplace(addressIndex).first->second.label = label; acct.detail.try_emplace(addressIndex).first->second.label = label;
else else
+3 -4
View File
@@ -31,22 +31,21 @@
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <vector> #include <vector>
#include "net/wallet_io.h"
#include "wallet.h" #include "wallet.h"
#include "wallet/api/wallet2_api.h" // monero/src #include "wallet/api/wallet2_api.h" // monero/src
namespace lwsf { namespace internal namespace lwsf { namespace internal
{ {
namespace backend { struct wallet; }
class subaddress_minor final : public ::Monero::Subaddress class subaddress_minor final : public ::Monero::Subaddress
{ {
internal::wallet* const wal_; internal::wallet* const wal_;
const std::shared_ptr<backend::wallet> data_; const net::wallet_io data_;
std::vector<Monero::SubaddressRow*> rows_; std::vector<Monero::SubaddressRow*> rows_;
public: public:
explicit subaddress_minor(internal::wallet* wal, std::shared_ptr<backend::wallet> data); explicit subaddress_minor(internal::wallet* wal, const net::wallet_io& data);
subaddress_minor(const subaddress_minor&) = delete; subaddress_minor(const subaddress_minor&) = delete;
subaddress_minor(subaddress_minor&&) = delete; subaddress_minor(subaddress_minor&&) = delete;
+16 -16
View File
@@ -34,9 +34,10 @@
#include <stdexcept> #include <stdexcept>
#include "backend.h" #include "backend.h"
#include "error.h"
#include "hex.h" // monero/contrib/epee/include
#include "transaction_info.h" #include "transaction_info.h"
#include "hex.h" // monero/contrib/epee/include
namespace lwsf { namespace internal namespace lwsf { namespace internal
{ {
@@ -50,11 +51,10 @@ namespace lwsf { namespace internal
} }
} }
transaction_history::transaction_history(std::shared_ptr<backend::wallet> data) transaction_history::transaction_history(const net::wallet_io& data)
: data_(std::move(data)), txes_(), by_id_() : data_(data), txes_(), by_id_()
{ {
if (!data_) LWSF_VERIFY(data_.wallet);
throw std::invalid_argument{"lwsf::internal::transaction_history cannot be given nullptr"};
} }
transaction_history::~transaction_history() transaction_history::~transaction_history()
@@ -76,12 +76,12 @@ namespace lwsf { namespace internal
return nullptr; return nullptr;
// hold the lock during the map lookup, this is const and should be thread-safe // hold the lock during the map lookup, this is const and should be thread-safe
const boost::lock_guard<boost::mutex> lock{data_->sync}; const boost::lock_guard<boost::mutex> lock{data_.wallet->sync};
auto& info = by_id_[binary_id]; auto& info = by_id_[binary_id];
if (!info) if (!info)
{ {
const auto iter = data_->primary.txes.find(binary_id); const auto iter = data_.wallet->primary.txes.find(binary_id);
if (iter == data_->primary.txes.end()) if (iter == data_.wallet->primary.txes.end())
return nullptr; return nullptr;
info = std::make_unique<transaction_info>(data_, iter->second); info = std::make_unique<transaction_info>(data_, iter->second);
} }
@@ -92,24 +92,24 @@ namespace lwsf { namespace internal
{ {
by_id_.clear(); by_id_.clear();
const boost::lock_guard<boost::mutex> lock{data_->sync}; const boost::lock_guard<boost::mutex> lock{data_.wallet->sync};
if (std::numeric_limits<int>::max() < data_->primary.txes.size()) if (std::numeric_limits<int>::max() < data_.wallet->primary.txes.size())
throw std::runtime_error{"lwsf::internal::transaction_history::refresh exceeds max history size"}; throw std::runtime_error{"lwsf::internal::transaction_history::refresh exceeds max history size"};
// be careful about exceptions and memory leaks. this should be safe // be careful about exceptions and memory leaks. this should be safe
for (std::size_t i = data_->primary.txes.size(); i < txes_.size(); ++i) for (std::size_t i = data_.wallet->primary.txes.size(); i < txes_.size(); ++i)
{ {
const std::unique_ptr<Monero::TransactionInfo> destroy{txes_[i]}; const std::unique_ptr<Monero::TransactionInfo> destroy{txes_[i]};
txes_[i] = nullptr; txes_[i] = nullptr;
} }
txes_.resize(data_->primary.txes.size()); txes_.resize(data_.wallet->primary.txes.size());
try try
{ {
std::size_t i = 0; std::size_t i = 0;
for (const auto& backend_tx : data_->primary.txes) for (const auto& backend_tx : data_.wallet->primary.txes)
{ {
if (txes_[i]) if (txes_[i])
static_cast<transaction_info*>(txes_[i])->update(backend_tx.second); static_cast<transaction_info*>(txes_[i])->update(backend_tx.second);
@@ -131,9 +131,9 @@ namespace lwsf { namespace internal
if (!epee::from_hex::to_buffer(epee::as_mut_byte_span(binary_id), txid)) if (!epee::from_hex::to_buffer(epee::as_mut_byte_span(binary_id), txid))
return; return;
const boost::lock_guard<boost::mutex> lock{data_->sync}; const boost::lock_guard<boost::mutex> lock{data_.wallet->sync};
auto iter = data_->primary.txes.find(binary_id); auto iter = data_.wallet->primary.txes.find(binary_id);
if (iter != data_->primary.txes.end()) if (iter != data_.wallet->primary.txes.end())
iter->second->description = note; iter->second->description = note;
} }
}} // lwsf // internal }} // lwsf // internal
+3 -2
View File
@@ -32,6 +32,7 @@
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#include "crypto/hash.h" // monero/src #include "crypto/hash.h" // monero/src
#include "net/wallet_io.h"
#include "wallet/api/wallet2_api.h" // monero/src #include "wallet/api/wallet2_api.h" // monero/src
namespace lwsf { namespace internal namespace lwsf { namespace internal
@@ -39,12 +40,12 @@ namespace lwsf { namespace internal
namespace backend { class wallet; } namespace backend { class wallet; }
class transaction_history final : public Monero::TransactionHistory class transaction_history final : public Monero::TransactionHistory
{ {
const std::shared_ptr<backend::wallet> data_; const net::wallet_io data_;
std::vector<Monero::TransactionInfo*> txes_; std::vector<Monero::TransactionInfo*> txes_;
mutable std::unordered_map<crypto::hash, std::unique_ptr<Monero::TransactionInfo>> by_id_; mutable std::unordered_map<crypto::hash, std::unique_ptr<Monero::TransactionInfo>> by_id_;
public: public:
explicit transaction_history(std::shared_ptr<backend::wallet> data); explicit transaction_history(const net::wallet_io& data);
transaction_history(const transaction_history&) = delete; transaction_history(const transaction_history&) = delete;
transaction_history(transaction_history&&) = delete; transaction_history(transaction_history&&) = delete;
+11 -12
View File
@@ -31,6 +31,7 @@
#include <boost/container/throw_exception.hpp> #include <boost/container/throw_exception.hpp>
#include <boost/thread/lock_guard.hpp> #include <boost/thread/lock_guard.hpp>
#include <boost/version.hpp> #include <boost/version.hpp>
#include "error.h"
#include "hex.h" #include "hex.h"
namespace namespace
@@ -57,11 +58,10 @@ namespace lwsf { namespace internal
transfers_.emplace_back(out.amount, out.address); transfers_.emplace_back(out.amount, out.address);
} }
transaction_info::transaction_info(std::shared_ptr<backend::wallet> wallet, std::shared_ptr<const backend::transaction> data) transaction_info::transaction_info(const net::wallet_io& io, std::shared_ptr<const backend::transaction> data)
: wallet_(std::move(wallet)), data_(std::move(data)), transfers_() : io_(io), data_(std::move(data)), transfers_()
{ {
if (!wallet_ || !data_) LWSF_VERIFY(io_.wallet && data_);
throw std::invalid_argument{"lwsf::internal::transaction_info cannot be given nullptr"};
update_transfers(); update_transfers();
} }
@@ -70,8 +70,7 @@ namespace lwsf { namespace internal
void transaction_info::update(std::shared_ptr<const backend::transaction> data) void transaction_info::update(std::shared_ptr<const backend::transaction> data)
{ {
if (!data) LWSF_VERIFY(data);
throw std::invalid_argument{"lwsf::internal::transaction_info::update cannot be given nullptr"};
data_ = std::move(data); data_ = std::move(data);
update_transfers(); update_transfers();
} }
@@ -87,7 +86,7 @@ namespace lwsf { namespace internal
std::string transaction_info::description() const std::string transaction_info::description() const
{ {
const boost::lock_guard<boost::mutex> lock{wallet_->sync}; const boost::lock_guard<boost::mutex> lock{io_.wallet->sync};
return data_->description; // this can change outside of refresh func return data_->description; // this can change outside of refresh func
} }
@@ -126,10 +125,10 @@ namespace lwsf { namespace internal
{ {
const auto get_label = [this] (const rpc::address_meta& meta) -> std::string const auto get_label = [this] (const rpc::address_meta& meta) -> std::string
{ {
const boost::lock_guard<boost::mutex> lock{wallet_->sync}; const boost::lock_guard<boost::mutex> lock{io_.wallet->sync};
if (meta.maj_i < wallet_->primary.subaccounts.size()) if (meta.maj_i < io_.wallet->primary.subaccounts.size())
{ {
const auto& major = wallet_->primary.subaccounts.at(meta.maj_i); const auto& major = io_.wallet->primary.subaccounts.at(meta.maj_i);
const auto minor = major.detail.find(meta.min_i); const auto minor = major.detail.find(meta.min_i);
if (minor != major.detail.end()) if (minor != major.detail.end())
return minor->second.label; return minor->second.label;
@@ -152,8 +151,8 @@ namespace lwsf { namespace internal
{ {
if (!data_->height) if (!data_->height)
return 0; return 0;
const boost::lock_guard<boost::mutex> lock{wallet_->sync}; const boost::lock_guard<boost::mutex> lock{io_.wallet->sync};
return std::max(wallet_->blockchain_height, *data_->height) - *data_->height + 1; return std::max(io_.wallet->blockchain_height, *data_->height) - *data_->height + 1;
} }
std::string transaction_info::hash() const std::string transaction_info::hash() const
+3 -2
View File
@@ -33,6 +33,7 @@
#include <vector> #include <vector>
#include "backend.h" #include "backend.h"
#include "net/wallet_io.h"
#include "wallet/api/wallet2_api.h" // monero/src #include "wallet/api/wallet2_api.h" // monero/src
namespace lwsf { namespace internal namespace lwsf { namespace internal
@@ -40,7 +41,7 @@ namespace lwsf { namespace internal
//! \todo isFailed, subaddressIndex, and subaddrAccount //! \todo isFailed, subaddressIndex, and subaddrAccount
class transaction_info final : public Monero::TransactionInfo class transaction_info final : public Monero::TransactionInfo
{ {
const std::shared_ptr<backend::wallet> wallet_; const net::wallet_io io_;
std::shared_ptr<const backend::transaction> data_; std::shared_ptr<const backend::transaction> data_;
std::vector<Transfer> transfers_; std::vector<Transfer> transfers_;
@@ -48,7 +49,7 @@ namespace lwsf { namespace internal
public: public:
explicit transaction_info(std::shared_ptr<backend::wallet> wallet, std::shared_ptr<const backend::transaction> data); explicit transaction_info(const net::wallet_io& io, std::shared_ptr<const backend::transaction> data);
transaction_info(const transaction_info&) = delete; transaction_info(const transaction_info&) = delete;
transaction_info(transaction_info&&) = delete; transaction_info(transaction_info&&) = delete;
+381 -299
View File
File diff suppressed because it is too large Load Diff
+26 -11
View File
@@ -42,19 +42,39 @@
#include <system_error> #include <system_error>
#include "byte_slice.h" // monero/contrib/epee/include #include "byte_slice.h" // monero/contrib/epee/include
#include "crypto/crypto.h" // monero/src #include "crypto/crypto.h" // monero/src
#include "net/wallet_io.h"
#include "wallet/api/wallet2_api.h" // monero/src #include "wallet/api/wallet2_api.h" // monero/src
namespace lwsf namespace lwsf
{ {
namespace internal namespace internal
{ {
namespace backend { struct wallet; }
class wallet final : public Monero::Wallet class wallet final : public Monero::Wallet
{ {
enum class state : std::uint8_t { stop = 0, paused, skip_once, run }; enum class state : std::uint8_t { stop = 0, paused, skip_once, run };
const std::shared_ptr<backend::wallet> data_; struct frame
{
std::string exception_error_;
std::error_code error_;
std::chrono::milliseconds refresh_interval_;
boost::mutex sync_;
state thread_state_;
frame();
bool set_error(std::error_code error, bool clear);
struct async_error
{
std::shared_ptr<frame> self;
void operator()(std::error_code error) const;
};
bool thead_start();
};
const net::wallet_io data_;
const std::shared_ptr<frame> status_;
std::unique_ptr<Monero::AddressBook> addressbook_; std::unique_ptr<Monero::AddressBook> addressbook_;
std::unique_ptr<Monero::TransactionHistory> history_; std::unique_ptr<Monero::TransactionHistory> history_;
std::unique_ptr<Monero::SubaddressAccount> subaddresses_; std::unique_ptr<Monero::SubaddressAccount> subaddresses_;
@@ -63,26 +83,21 @@ namespace internal
std::string password_; std::string password_;
std::string language_; std::string language_;
std::string ca_file_path_; std::string ca_file_path_;
std::deque<std::function<std::error_code()>> work_queue_;
mutable std::string exception_error_;
mutable std::error_code error_;
boost::thread thread_; boost::thread thread_;
const std::uint64_t iterations_; const std::uint64_t iterations_;
std::chrono::milliseconds refresh_interval_;
std::uint32_t mixin_; std::uint32_t mixin_;
boost::condition_variable refresh_notify_; boost::condition_variable refresh_notify_;
mutable boost::mutex error_sync_;
boost::mutex refresh_sync_; //!< Synchronizes with requests from other threads boost::mutex refresh_sync_; //!< Synchronizes with requests from other threads
boost::mutex thread_sync_; //!< Syncrhonizes thread destruction+creation boost::mutex thread_sync_; //!< Syncrhonizes thread destruction+creation
state thread_state_;
bool mandatory_refresh_; bool mandatory_refresh_;
bool set_error(std::error_code status, bool clear = false) const; bool set_error(std::error_code status, bool clear = false) const;
void set_critical(const std::exception& e) const; void set_critical(const std::exception& e) const;
template<typename F> template<typename T, typename F>
void queue_work(F&& f); T wait_for(F&& f) const;
void check_worker_thread();
void stop_refresh_loop(); void stop_refresh_loop();
void refresh_loop(); void refresh_loop();
+40 -16
View File
@@ -44,6 +44,7 @@
#ifdef LWSF_POLYSEED_ENABLE #ifdef LWSF_POLYSEED_ENABLE
#include "polyseed.h" #include "polyseed.h"
#endif #endif
#include "net/context.h"
#include "net/parse.h" // monero/src #include "net/parse.h" // monero/src
#include "net/socks_connect.h" // monero/src #include "net/socks_connect.h" // monero/src
#include "QrCode.hpp" // monero/src/external #include "QrCode.hpp" // monero/src/external
@@ -57,27 +58,51 @@ namespace lwsf
//! \TODO Mark final when completely implemented //! \TODO Mark final when completely implemented
class wallet_manager : public Monero::WalletManager class wallet_manager : public Monero::WalletManager
{ {
rpc::http_client client_; boost::asio::io_context io_;
std::string client_prefix_; http::client client_;
std::string error_; std::string error_;
rpc::daemon_status cached_; rpc::daemon_status cached_;
std::chrono::steady_clock::time_point cached_last_; std::chrono::steady_clock::time_point cached_last_;
boost::mutex sync_;
template<typename T, typename F>
T wait_for(F f)
{
const boost::lock_guard<boost::mutex> lock{sync_};
net::to_future<T> callable{};
auto value = callable.inner_->promise.get_future();
f(std::move(callable));
for (;;)
{
switch (value.wait_for(std::chrono::seconds{0}))
{
case std::future_status::deferred:
case std::future_status::ready:
return value.get();
case std::future_status::timeout:
io_.restart();
io_.run_one();
break;
};
}
throw std::logic_error{"unreachable"};
}
rpc::daemon_status get_daemon_status() rpc::daemon_status get_daemon_status()
{ {
if (std::chrono::steady_clock::now() - cached_last_ > config::daemon_status_cache) if (std::chrono::steady_clock::now() - cached_last_ > config::daemon_status_cache)
{ {
const auto resp = rpc::invoke<rpc::daemon_status>(client_, client_prefix_, rpc::empty{}); rpc::daemon_status out{};
if (!resp) const auto error = wait_for<std::error_code>(
{ [this, &out] (auto&& f) { rpc::invoke_async(client_, rpc::empty{}, std::addressof(out), std::move(f)); }
error_ = resp.error().message(); );
cached_ = rpc::daemon_status{}; if (error)
} error_ = error.message();
else else
{
error_.clear(); error_.clear();
cached_ = std::move(*resp); cached_ = std::move(out);
}
} }
return cached_; return cached_;
} }
@@ -85,7 +110,7 @@ namespace lwsf
public: public:
wallet_manager() wallet_manager()
: client_(), error_(), cached_{}, cached_last_(std::chrono::seconds{0}) : io_(), client_(), error_(), cached_{}, cached_last_(std::chrono::seconds{0}), sync_()
{} {}
virtual ~wallet_manager() /* override */ virtual ~wallet_manager() /* override */
@@ -410,8 +435,7 @@ namespace lwsf
url.port = 80; url.port = 80;
} }
client_prefix_ = std::move(url.uri); client_.init(io_,std::move(url.host), std::move(url.uri), boost::numeric_cast<std::uint16_t>(url.port), std::move(options));
client_.set_server(std::move(url.host), std::to_string(url.port), boost::none, std::move(options));
} }
//! returns whether the daemon can be reached, and its version number //! returns whether the daemon can be reached, and its version number
@@ -465,13 +489,13 @@ namespace lwsf
bool setProxy(const std::string &address) override bool setProxy(const std::string &address) override
{ {
auto endpoint = net::get_tcp_endpoint(address); auto endpoint = ::net::get_tcp_endpoint(address);
if (!endpoint) if (!endpoint)
{ {
error_ = endpoint.error().message(); error_ = endpoint.error().message();
return false; return false;
} }
client_.set_connector(net::socks::connector{std::move(*endpoint)}); client_.set_proxy(http::client::socks{std::move(*endpoint)});
return true; return true;
} }
}; };
+3 -3
View File
@@ -27,11 +27,11 @@
# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
add_library(lwsf-unit-framework framework.test.cpp) add_library(lwsf-unit-framework framework.test.cpp)
target_include_directories(lwsf-unit-framework PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} "${CMAKE_SOURCE_DIR}/src") target_include_directories(lwsf-unit-framework PUBLIC "${lwsf_SOURCE_DIR}/src" "${lwsf_SOURCE_DIR}/tests/unit")
target_link_libraries(lwsf-unit-framework) target_link_libraries(lwsf-unit-framework)
add_executable(lwsf-unit main.cpp backend.test.cpp rpc.test.cpp) add_executable(lwsf-unit main.cpp backend.test.cpp rpc.test.cpp)
target_include_directories(lwsf-unit PRIVATE "${lwsf_SOURCE_DIR}/src") add_subdirectory(net)
target_link_libraries(lwsf-unit PRIVATE lwsf-api lwsf-unit-framework monero::libraries) target_link_libraries(lwsf-unit lwsf-api lwsf-unit-framework lwsf-unit-net monero::libraries)
add_test(NAME lwsf-unit COMMAND lwsf-unit -v) add_test(NAME lwsf-unit COMMAND lwsf-unit -v)
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
# 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.
add_library(lwsf-unit-net OBJECT http.test.cpp)
#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)
+232
View File
@@ -0,0 +1,232 @@
// 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 <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 <string>
#include "common/error.h" // monero/src
#include "net/http.h"
using client_request =
boost::beast::http::request<boost::beast::http::string_body>;
namespace
{
void get_next(boost::asio::io_context& io, boost::asio::ip::tcp::socket& server, boost::beast::flat_buffer& buffer, client_request& req)
{
bool ready = false;
boost::beast::http::async_read(server, buffer, req, [&] (auto, auto) { ready = true; });
while (!ready)
{
io.restart();
io.run_one();
}
}
epee::byte_slice to_slice(std::string src)
{
return epee::byte_slice{std::move(src)};
}
std::string to_string(const epee::byte_slice& src)
{
return {reinterpret_cast<const char*>(src.data()), src.size()};
}
void send(boost::asio::ip::tcp::socket& server, std::string body)
{
boost::beast::http::response<boost::beast::http::string_body> resp;
resp.body() = std::move(body);
resp.prepare_payload();
boost::beast::http::write(server, resp);
}
}
LWS_CASE("net::http")
{
using tcp = boost::asio::ip::tcp;
SETUP("client+server")
{
boost::asio::io_context io{};
tcp::socket server{io};
tcp::acceptor acceptor{io, tcp::endpoint(tcp::v4(), 0)};
acceptor.listen();
boost::beast::flat_buffer buffer{};
lwsf::internal::http::client client{};
{
std::error_code actual;
client.get_async("/foo", [&] (std::error_code error, auto) { actual = error; });
EXPECT(actual == common_error::kInvalidArgument);
}
{
const auto endpoint = acceptor.local_endpoint();
client.init(io, "127.0.0.1", "lws", endpoint.port(), epee::net_utils::ssl_support_t::e_ssl_support_disabled);
EXPECT(!client.is_connected());
bool client_ready = false;
bool server_ready = false;
client.get_async("/foo", [&] (auto, auto) { client_ready = true; });
acceptor.async_accept(server, [&] (auto) { server_ready = true; });
EXPECT(!client.is_connected());
while (!server_ready)
{
io.restart();
io.run_one();
}
client_request request{};
get_next(io, server, buffer, request);
EXPECT(request.method() == boost::beast::http::verb::get);
EXPECT(request.body().empty());
EXPECT(request.target() == "lws/foo");
send(server, "");
EXPECT(!client_ready);
while (!client_ready)
{
io.restart();
io.run_one();
}
EXPECT(client.is_connected());
} // end temp setup variables
SECTION("post")
{
const std::string request_body = "THE REQUEST";
const std::string response = "THE RESPONSE";
epee::byte_slice actual;
client.post_async("/posted", to_slice(request_body), [&] (auto, epee::byte_slice src) { actual = std::move(src); });
client_request request{};
get_next(io, server, buffer, request);
EXPECT(request.method() == boost::beast::http::verb::post);
EXPECT(request.body() == request_body);
EXPECT(request.target() == "lws/posted");
send(server, response);
while (actual.empty())
{
io.restart();
io.run_one();
}
EXPECT(to_string(actual) == response);
}
SECTION("post/get x3")
{
using verb = boost::beast::http::verb;
const std::array<verb, 3> verbs{{verb::post, verb::get, verb::post}};
const std::array<std::string, 3> targets{{"/post1", "/get1", "/post2"}};
const std::array<std::string, 3> requests{{"REQ1", "", "REQ3"}};
const std::array<std::string, 3> responses{{"RESP1", "RESP2", "RESP3"}};
std::array<epee::byte_slice, 3> actuals;
client.post_async(targets[0], to_slice(requests[0]), [&] (auto, epee::byte_slice src) { actuals[0] = std::move(src); });
client.get_async(targets[1], [&] (auto, epee::byte_slice src) { actuals[1] = std::move(src); });
client.post_async(targets[2], to_slice(requests[2]), [&] (auto, epee::byte_slice src) { actuals[2] = std::move(src); });
for (std::size_t i = 0; i < requests.size(); ++i)
{
client_request request{};
get_next(io, server, buffer, request);
EXPECT(request.method() == verbs[i]);
EXPECT(request.body() == requests[i]);
EXPECT(request.target() == "lws" + targets[i]);
send(server, responses[i]);
}
while (actuals[2].empty())
{
io.restart();
io.run_one();
}
for (std::size_t i = 0; i < actuals.size(); ++i)
EXPECT(to_string(actuals[i]) == responses[i]);
}
SECTION("invalid host")
{
std::error_code actual;
client.init(io, ".test", "", 80, epee::net_utils::ssl_support_t::e_ssl_support_disabled);
client.get_async("/", [&] (std::error_code error, auto) { actual = error; });
while (actual == std::error_code{})
{
io.restart();
io.run_one();
}
EXPECT(actual == lwsf::internal::http::error::timeout);
}
SECTION("connect timeout")
{
std::error_code actual;
tcp::acceptor acceptor{io, tcp::endpoint(tcp::v4(), 0)};
acceptor.listen();
client.init(io, "127.0.0.1", "", acceptor.local_endpoint().port(), epee::net_utils::ssl_support_t::e_ssl_support_disabled);
client.get_async("/", [&] (std::error_code error, auto) { actual = error; });
io.restart();
io.run();
EXPECT(actual == lwsf::internal::http::error::timeout);
}
SECTION("response timeout")
{
std::error_code actual;
client.get_async("/get", [&] (std::error_code error, auto) { actual = error; });
io.restart();
io.run();
EXPECT(actual == lwsf::internal::http::error::timeout);
}
} // SETUP
}