diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index e4a0df0..e216549 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -8,35 +8,96 @@ on: env: # 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: | 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::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' - 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"' - BUILD: 'cmake --build "${{github.workspace}}/lwsf/build" -j$(nproc)' - RUN: 'ctest --test-dir "${{github.workspace}}/lwsf/build"' + 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)' + RUN: 'ctest --test-dir "$GITHUB_WORKSPACE/lwsf/build" --output-on-failure' 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}} strategy: fail-fast: false matrix: - os: [macos-latest, ubuntu-latest] + branch: [master, release-v0.18] + os: [macos-15, macos-26, ubuntu-22.04, ubuntu-24.04] steps: - - name: set apt conf (Debian based Linux) - if: matrix.os == 'ubuntu-latest' - run: ${{env.APT_SET_CONF}} - - name: update apt (Debian based Linux) - if: matrix.os == 'ubuntu-latest' - run: sudo apt update - - name: Install dependencies (Debian based Linux) - if: matrix.os == 'ubuntu-latest' - run: ${{env.APT_INSTALL_LINUX}} + - name: Install Dependencies (Ubuntu) + if: matrix.os == 'ubuntu-22.04' || matrix.os == 'ubuntu-24.04' + run: | + ${{env.APT_SET_CONF}} + sudo apt update + sudo ${{env.APT_INSTALL_LINUX}} - name: Install dependencies (MacOS) - if: matrix.os == 'macos-latest' + if: matrix.os == 'macos-15' || matrix.os == 'macos-26' run: ${{env.BREW_INSTALL_MAC}} - name: Checkout Monero Source uses: actions/checkout@v4 @@ -44,18 +105,23 @@ jobs: repository: monero-project/monero path: ${{github.workspace}}/monero/ submodules: recursive + ref: ${{matrix.branch}} - name: Checkout LWSF Source uses: actions/checkout@v4 with: path: ${{github.workspace}}/lwsf submodules: recursive - - name: Configure LWS - run: ${{env.CONFIGURE}} + - name: Configure LWS (master) + 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 run: ${{env.BUILD}} - name: Run Tests run: ${{env.RUN}} - windows-build-and-test: + windows: runs-on: windows-latest defaults: run: @@ -90,7 +156,7 @@ jobs: path: ${{github.workspace}}/lwsf submodules: recursive - name: Configure LWS - run: ${{env.CONFIGURE}} + run: cmake -DLWSF_MASTER_ENABLE=ON ${{env.CONFIGURE}} - name: Build LWSF run: ${{env.BUILD}} - name: Run Tests diff --git a/CMakeLists.txt b/CMakeLists.txt index 6bf813b..571932e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,16 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) 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) message(FATAL_ERROR "The argument -DMONERO_SOURCE_DIR must specify a location of a monero source tree") diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ff831d8..fcad232 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -27,6 +27,7 @@ # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. include_directories(.) +add_subdirectory(net) add_subdirectory(wire) set(lwsf_sources @@ -89,6 +90,7 @@ target_link_libraries(lwsf-api PUBLIC monero::libraries PRIVATE + lwsf-net lwsf-wire lwsf-wire-json lwsf-wire-msgpack diff --git a/src/address_book.cpp b/src/address_book.cpp index 94f4b65..2a3d30f 100644 --- a/src/address_book.cpp +++ b/src/address_book.cpp @@ -32,6 +32,7 @@ #include #include "backend.h" #include "cryptonote_basic/cryptonote_basic_impl.h" +#include "error.h" namespace lwsf { namespace internal { @@ -66,9 +67,11 @@ namespace lwsf { namespace internal { error_ = Status_Ok; } - address_book::address_book(std::shared_ptr data) - : data_(std::move(data)), addresses_(), error_string_(), error_(Status_Ok) - {} + address_book::address_book(const net::wallet_io& data) + : data_(data), addresses_(), error_string_(), error_(Status_Ok) + { + LWSF_VERIFY(data_.wallet); + } address_book::~address_book() { @@ -80,7 +83,7 @@ namespace lwsf { namespace internal { clear_status(); 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_ = Invalid_Address; @@ -99,8 +102,8 @@ namespace lwsf { namespace internal { addresses_.push_back(row.get()); row.release(); - const boost::lock_guard lock{data_->sync}; - data_->primary.addressbook.push_back( + const boost::lock_guard lock{data_.wallet->sync}; + data_.wallet->primary.addressbook.push_back( backend::address_book_entry{dst_addr, payment_id, description} ); @@ -120,11 +123,11 @@ namespace lwsf { namespace internal { addresses_[rowId] = nullptr; addresses_.erase(addresses_.begin() + rowId); - const boost::lock_guard lock{data_->sync}; - if (data_->primary.addressbook.size() <= rowId) + const boost::lock_guard lock{data_.wallet->sync}; + if (data_.wallet->primary.addressbook.size() <= rowId) return false; - data_->primary.addressbook.erase(data_->primary.addressbook.begin() + rowId); + data_.wallet->primary.addressbook.erase(data_.wallet->primary.addressbook.begin() + rowId); force.release(); return true; } @@ -143,11 +146,11 @@ namespace lwsf { namespace internal { index, destroy->getAddress(), destroy->getPaymentId(), description }; - const boost::lock_guard lock{data_->sync}; - if (data_->primary.addressbook.size() <= index) + const boost::lock_guard lock{data_.wallet->sync}; + if (data_.wallet->primary.addressbook.size() <= index) return false; - data_->primary.addressbook[index].description = description; + data_.wallet->primary.addressbook[index].description = description; force.release(); return true; } @@ -158,12 +161,12 @@ namespace lwsf { namespace internal { { clear_addresses(addresses_); - const boost::lock_guard lock{data_->sync}; - addresses_.resize(data_->primary.addressbook.size()); + const boost::lock_guard lock{data_.wallet->sync}; + addresses_.resize(data_.wallet->primary.addressbook.size()); 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{ i, current.address, current.payment_id, current.description }; diff --git a/src/address_book.h b/src/address_book.h index 4002bf4..ebd11bb 100644 --- a/src/address_book.h +++ b/src/address_book.h @@ -33,14 +33,14 @@ #include #include #include "lws_frontend.h" +#include "net/wallet_io.h" #include "wallet/api/wallet2_api.h" // monero/src -namespace lwsf { namespace internal { - namespace backend { struct wallet; } - +namespace lwsf { namespace internal +{ class address_book final : public ::Monero::AddressBook { - const std::shared_ptr data_; + const net::wallet_io data_; std::vector addresses_; std::string error_string_; ErrorCode error_; @@ -49,7 +49,7 @@ namespace lwsf { namespace internal { public: - explicit address_book(std::shared_ptr data); + explicit address_book(const net::wallet_io& data); address_book(const address_book&) = delete; address_book(address_book&&) = delete; diff --git a/src/backend.cpp b/src/backend.cpp index 3eabc3a..3433f8b 100644 --- a/src/backend.cpp +++ b/src/backend.cpp @@ -29,6 +29,7 @@ #include "net/http_client.h" // monero/contrib/epee/include #include "backend.h" +#include #include #include #include "crypto/crypto.h" // monero/src @@ -78,10 +79,10 @@ namespace lwsf { namespace internal { namespace backend namespace { constexpr const error default_subaddr_state = error::subaddr_disabled; - constexpr const auto rpc_unapproved = rpc::error(403); - constexpr const auto rpc_max_subaddresses = rpc::error(409); - constexpr const auto rpc_internal_error = rpc::error(500); - constexpr const auto rpc_not_implemented = rpc::error(501); + constexpr const auto rpc_unapproved = http::error(403); + constexpr const auto rpc_max_subaddresses = http::error(409); + constexpr const auto rpc_internal_error = http::error(500); + constexpr const auto rpc_not_implemented = http::error(501); template std::uint32_t add_uint32_clamp(const T index, const U lookahead) @@ -388,9 +389,12 @@ namespace lwsf { namespace internal { namespace backend boost::optional> update_tx(const account& self, transaction& out, const rpc::transaction& source) { - /* Let `receives` re-populate in `merge_output`. This works because the - server supplies all info - there is no local info to keep. */ + /* Let `receives` and `spends` re-populate in `merge_output` and + `update_spend` respectively. This works because the server supplies all + info - there is no local info to keep. The spend secret and address is + kept separate in `transfers` */ out.receives.clear(); + out.spends.clear(); std::vector meta; @@ -598,10 +602,14 @@ namespace lwsf { namespace internal { namespace backend self.blockchain_height = source.blockchain_height; self.primary.restore_height = source.start_height; + self.primary.requested_start = std::min(self.primary.requested_start, self.primary.restore_height); self.primary.scan_height = source.scanned_block_height; self.server_lookahead.major = source.lookahead.maj_i; self.server_lookahead.minor = source.lookahead.min_i; + if (self.primary.restore_height <= self.primary.requested_start && !source.lookahead_fail) + self.import_error = {}; + self.fee_mask = unspents.fee_mask; if (unspents.fees.empty()) { @@ -638,27 +646,120 @@ namespace lwsf { namespace internal { namespace backend return self.needed_subaddresses(std::move(subaddrs)) <= max_subaddresses; } - std::error_code prep_subs(rpc::http_client& client, const boost::string_ref client_prefix, const rpc::login& creds, std::vector required_subs, const boost::optional force_lookahead) - { + template + void prep_subs(std::shared_ptr self, rpc::login&& creds, const boost::optional& force_lookahead, F f) + { + struct frame + { + const std::shared_ptr self; + F f; + rpc::upsert_subaddrs_request request; + rpc::upsert_subaddrs_response response; + + frame(std::shared_ptr&& self, rpc::login&& creds, F&& f) + : self(std::move(self)), f(std::move(f)), request{std::move(creds)}, response{} + {} + }; + + struct handler : boost::asio::coroutine + { + std::shared_ptr frame_; + + explicit handler(std::shared_ptr in) + : boost::asio::coroutine(), frame_(std::move(in)) + {} + + void operator()(const std::error_code error = {}) + { + LWSF_VERIFY(frame_ && frame_->self); + wallet& self = *frame_->self; + BOOST_ASIO_CORO_REENTER(*this) + { + BOOST_ASIO_CORO_YIELD rpc::invoke_async( + self.client, frame_->request, std::addressof(frame_->response), *this + ); + frame_->f(error); + } + } + }; + + LWSF_VERIFY(self); + const auto& required_subs = self->primary.subaccounts; if (std::numeric_limits::max() < required_subs.size()) throw std::runtime_error{"prep_subs exceeded max major addresses"}; - const rpc::address_meta lookahead = force_lookahead.value_or(rpc::address_meta{}); + handler runner{std::make_shared(std::move(self), std::move(creds), std::move(f))}; - rpc::upsert_subaddrs_request request{creds}; + const rpc::address_meta lookahead = force_lookahead.value_or(rpc::address_meta{}); const std::uint32_t stop = add_uint32_clamp(required_subs.size(), lookahead.maj_i); - bool needed = bool(stop); + bool needed = 2 <= stop; for (std::size_t i = 0; i < stop; ++i) { std::uint32_t last = i < required_subs.size() ? required_subs[i].last : 0; last = add_uint32_clamp(last, lookahead.min_i); if (last) - request.subaddrs_.emplace(std::uint32_t(i), rpc::subaddrs{last}); + runner.frame_->request.subaddrs_.emplace(std::uint32_t(i), rpc::subaddrs{last}); needed |= bool(last); } + if (!needed) - return {}; - return rpc::invoke(client, client_prefix, request).error(); + return runner.frame_->f(std::error_code{}); + runner(); + } + + //! Bind N arguments into a callback that takes zero arguments to run + template + struct binder + { + std::shared_ptr self; + F f; + std::tuple args; + + template + void run(std::index_sequence) + { f(std::move(std::get(args)...)); } + + void operator()() + { run(std::make_index_sequence{}); } + }; + + //! Forward N arguments in a callback to be invoked on `wallet::strand + template + struct callback_on_strand + { + std::shared_ptr self; + F f; + + template + void operator()(T... args) + { + // Use `post` instead of `dispatch` due to locking in handlers + LWSF_VERIFY(self); + boost::asio::post( + self->strand, + binder{self, std::move(f), std::tuple(std::move(args)...)} + ); + } + }; + + /*! The ASIO handlers in this file all use `boost::asio::coroutine` which + is a light-weight async routine that looks traditional stack based code. + Unfortunately, a lock must be held for many of the functions, because the + API (basically) demands it. These handlers must release their lock before + being called again or deadlock occurs. To get around this, we wrap all + code needed to acquire `backend::wallet::sync` in a handler that posts to + the strand, so the handler never calls itself directly. + + As an example, if `rpc::invoke_async` calls the handler immediately, this + would cause deadlock, except the outter handler simply posts the + operation to be deferred later, the lock is released, and then asio runs + the handler in the next loop iteration. A bit gross, but this makes + `backend::wallet::get_decoys` and `backend::wallet::send_tx` super fast + and never blocking on anything (except briefly when queueing http stuff). */ + template + callback_on_strand post_on_strand(std::shared_ptr self, F f) + { + return {std::move(self), std::move(f)}; } } // anonymous @@ -822,11 +923,14 @@ namespace lwsf { namespace internal { namespace backend return true; } - wallet::wallet() + wallet::wallet(boost::asio::io_context& io) : listener(nullptr), - client(std::make_unique()), + strand(io), + client{}, primary{}, per_byte_fee(), + login_queue(), + refresh_queue(), refresh_error(), lookahead_error(default_subaddr_state), import_error(), @@ -836,7 +940,7 @@ namespace lwsf { namespace internal { namespace backend server_lookahead{}, sync(), sync_listener(), - sync_refresh(), + sync_queue(), passed_login(false) { prep_primary_account(primary.subaccounts.emplace_back()); @@ -919,410 +1023,692 @@ namespace lwsf { namespace internal { namespace backend return status; } - expect wallet::login_is_new() + void wallet::login_is_new(std::shared_ptr self, std::function)> f) { - // Remember that this function provides the strong exception guarantee. - bool new_account = false; - boost::unique_lock lock{sync}; + struct frame { - if (passed_login) - return false; // cannot be new account anymore + const std::shared_ptr self; + rpc::login_request login; + rpc::login_response response; + unsigned i; - passed_login = false; - probed_lookahead = false; - per_byte_fee.clear(); - fee_mask = 0; - server_lookahead = {}; - refresh_error = {}; - subaddress_error = {}; - import_error = {}; - lookahead_error = lookahead_good() ? std::error_code{} : error::subaddr_disabled; + explicit frame(std::shared_ptr in) + : self(std::move(in)), + login{}, + response{}, + i(0) + {} + }; - rpc::login_request login{ - primary.address, primary.view.sec, primary.lookahead, true, primary.generated_locally - }; + struct handler : boost::asio::coroutine + { + std::shared_ptr frame_; - for (unsigned i = 0; i < 2; ++i) + explicit handler(std::shared_ptr frame) + : boost::asio::coroutine(), frame_(std::move(frame)) + {} + + void operator()(std::error_code error = {}) { - /* Do not release `lock` during login calls, want to temporarily block - everything until complete or timeout. */ + LWSF_VERIFY(frame_ && frame_->self); - const auto response = rpc::invoke(*client, client_prefix, login); - if (!response) + // Remember that this function provides the strong exception guarantee. + wallet& self = *frame_->self; + assert(self.strand.running_in_this_thread()); + boost::optional force_lookahead; + const boost::lock_guard lock{self.sync}; + BOOST_ASIO_CORO_REENTER(*this) { - if (response == rpc_unapproved) - return {error::approval}; - else if (response == rpc_internal_error) - return {error::network_type}; // almost always this - else if (response == rpc_not_implemented) - return {error::create}; - else if (0 < i) - return response.error(); - login.lookahead = {}; - } - else // response - { - new_account = response->new_address; - passed_login = true; + if (self.passed_login) + return self.run(self.login_queue, false); // not a new account - if (response->start_height) - primary.restore_height = *response->start_height; + self.passed_login = false; + self.probed_lookahead = false; + self.per_byte_fee.clear(); + self.fee_mask = 0; + self.server_lookahead = {}; + self.refresh_error = {}; + self.subaddress_error = {}; + self.import_error = {}; + self.lookahead_error = self.lookahead_good() ? std::error_code{} : error::subaddr_upgrade; - if (response->lookahead) - { - server_lookahead = {response->lookahead->maj_i, response->lookahead->min_i}; - lookahead_error = {}; + frame_->login = rpc::login_request{ + self.primary.address, self.primary.view.sec, self.primary.lookahead, true, self.primary.generated_locally + }; + + for ( ; frame_->i < 2; ++frame_->i) + { + BOOST_ASIO_CORO_YIELD rpc::invoke_async( + self.client, frame_->login, std::addressof(frame_->response), post_on_strand(frame_->self, *this) + ); + + if (error) + { + if (error == rpc_unapproved) + return self.run(self.login_queue, std::error_code{error::approval}); + else if (error == rpc_internal_error) + return self.run(self.login_queue, std::error_code{error::network_type}); // almost always this + else if (error == rpc_not_implemented) + return self.run(self.login_queue, std::error_code{error::create}); + else if (0 < frame_->i) + return self.run(self.login_queue, error); + frame_->login.lookahead = {}; + } + else // response + { + self.passed_login = true; + + if (frame_->response.start_height) + { + self.primary.restore_height = *frame_->response.start_height; + self.primary.requested_start = std::min(self.primary.requested_start, self.primary.restore_height); + } + + if (frame_->response.lookahead) + { + self.server_lookahead = {frame_->response.lookahead->maj_i, frame_->response.lookahead->min_i}; + self.lookahead_error = {}; + } + else + force_lookahead = self.primary.lookahead; + + if (config::use_subaddresses) + { + /* Make sure all registered subs are known to this backend. This can + differ from lookahead because API allows arbitrary major,minor + requests to be performed. */ + BOOST_ASIO_CORO_YIELD prep_subs( + frame_->self, {frame_->login.address, frame_->login.view_key}, force_lookahead, post_on_strand(frame_->self, *this) + ); + + if (error && !self.subaddress_error) + self.subaddress_error = handle_subaddress_error(error); + } + + break; // retry loop + } } - /* Make sure all registered subs are known to this backend. This can - differ from lookahead because API allows arbitrary major,minor - requests to be performed. */ - auto subs = primary.subaccounts; - boost::optional force_lookahead; - if (!response->lookahead) - force_lookahead = primary.lookahead; - lock.unlock(); - const std::error_code error = - prep_subs(*client, client_prefix, {login.address, login.view_key}, std::move(subs), force_lookahead); - lock.lock(); - if (error && !subaddress_error) - subaddress_error = handle_subaddress_error(error); - - break; // retry loop + if (!self.lookahead_good() && !self.lookahead_error) + self.lookahead_error = {error::subaddr_ahead}; + self.run(self.login_queue, frame_->response.new_address); } } - } - - if (!lookahead_good() && !lookahead_error) - lookahead_error = {error::subaddr_ahead}; - return new_account; - } - - std::error_code wallet::refresh(const bool mandatory) - { - // Remember that this function provides the strong exception guarantee. - - boost::unique_lock lock_refresh{sync_refresh}; - const auto now = std::chrono::steady_clock::now(); - - if (!mandatory) - { - if (now - last_sync < config::refresh_interval_min) - return refresh_error; - } - last_sync = now; - - struct call_refreshed - { - void operator()(wallet* ptr) const - { - if (!ptr) return; - const boost::lock_guard lock{ptr->sync_listener}; - if (!ptr->listener) return; - ptr->listener->refreshed(); - } }; - std::unique_ptr refresh_on_exit{this}; - - boost::unique_lock lock{sync}; - const bool try_login = !passed_login; - const rpc::login login{primary.address, primary.view.sec}; - - lock.unlock(); - if (try_login) - { - const std::error_code failed_login = this->login(); - if (failed_login) - { - lock.lock(); - return refresh_error = failed_login; - } - } - - expect outs_response{common_error::kInvalidArgument}; - const auto txs_response = rpc::invoke(*client, client_prefix, login); - if (txs_response) - { - const rpc::get_unspent_outs_request request{login, rpc::uint64_string(0), 0, true}; - outs_response = rpc::invoke(*client, client_prefix, request); - } - lock.lock(); - - passed_login = bool(txs_response) && bool(outs_response); // reset state - - if (!txs_response) - { - if (txs_response == rpc_unapproved) - return refresh_error = {error::approval}; - return refresh_error = txs_response.error(); - } - if (!outs_response) - return refresh_error = outs_response.error(); - - const std::uint64_t orig_scan_height = primary.scan_height; - auto merged = merge_response(*this, *txs_response, *outs_response); - - if (merged.lookahead_fail || !lookahead_good()) - { - const std::uint64_t height = merged.lookahead_fail.value_or(this->primary.restore_height); - if (!lookahead_error) - lookahead_error = {error::subaddr_ahead}; - if (!this->probed_lookahead) - { - this->probed_lookahead = true; // check just once for re-scan - const auto info = rpc::invoke(*client, client_prefix, rpc::empty{}); - if (info) - { - const auto subaddrs = rpc::invoke(*client, client_prefix, login); - if (subaddrs && should_attempt_rescan(primary, std::move(subaddrs->all_subaddrs), info->max_subaddresses)) - { - lock.unlock(); - restore_height(height); - lock.lock(); - } - } - else - lookahead_error = info.error(); - } - } - else if (lookahead_good()) - { - lookahead_error = {}; - probed_lookahead = false; - if (primary.restore_height <= primary.requested_start) - import_error = {}; - } - - if (!import_error && primary.requested_start < primary.restore_height) - { - const auto height = primary.requested_start; - lock.unlock(); - restore_height(height); - lock.lock(); - } - - // return error if subaddresses enabled, and recovered wallet - refresh_on_exit.release(); // release before acquiring `sync_listener`. - const std::error_code rc = refresh_error = - import_error ? - import_error : subaddress_error ? - subaddress_error : lookahead_error; - const boost::lock_guard lock_listener{sync_listener}; - if (!listener) - return rc; - - // Call listener functions without holding `sync`, in case a call is made - // back into the library. - const std::uint64_t new_scan_height = - std::max(orig_scan_height, primary.scan_height); - lock.unlock(); - lock_refresh.unlock(); - - listener->refreshed(); - if (!merged.new_transactions.empty() || new_scan_height - orig_scan_height) - listener->updated(); - - for (std::uint64_t i = orig_scan_height; i < new_scan_height; ++i) - listener->newBlock(i); - - for (const auto& tx : merged.new_transactions) - { - const auto txid = epee::string_tools::pod_to_hex(tx->id); - if (tx->direction == Monero::TransactionInfo::Direction_In) - { - if (tx->height) - listener->moneyReceived(txid, tx->amount); - else - listener->unconfirmedMoneyReceived(txid, tx->amount); - } - else - listener->moneySpent(txid, tx->amount); - } - - return rc; + // Post in case of nested callback + LWSF_VERIFY(self); + if (self->push(self->login_queue, std::move(f))) + boost::asio::post(self->strand, handler{std::make_shared(self)}); } - - std::error_code wallet::register_subaccount(const std::uint32_t maj_i) - { - boost::unique_lock lock{sync}; - if (primary.subaccounts.size() <= maj_i) - throw std::runtime_error{"wallet::register_subaccount exceeded subaddress indexes"}; - if (!passed_login) - { - lock.unlock(); - const std::error_code error = login(); - if (error) - return error; - lock.lock(); - } - - const std::uint32_t minor_count = std::max(std::uint32_t(1), primary.lookahead.minor); - const rpc::provision_subaddrs_request request{ - rpc::login{primary.address, primary.view.sec}, - maj_i, 0, 1, minor_count, false /* get_all */ - }; - - lock.unlock(); - const auto response = rpc::invoke(*client, client_prefix, request); - lock.lock(); - - if (!response && !subaddress_error) - subaddress_error = handle_subaddress_error(response.error()); - return subaddress_error; - } - - std::error_code wallet::register_subaddress(const std::uint32_t maj_i, const std::uint32_t min_i) + void wallet::refresh(std::shared_ptr self, const bool mandatory, std::function f) { - boost::unique_lock lock{sync}; - if (primary.subaccounts.size() <= maj_i) - throw std::invalid_argument{"wallet::register_subaddress exceeded major index"}; - - if (primary.subaccounts.at(maj_i).last < min_i) - throw std::invalid_argument{"wallet::register_subaddress exceeded minor index"}; - - if (!passed_login) + // everything used across async calls + struct frame { - lock.unlock(); - const std::error_code error = login(); - if (error) - return error; - lock.lock(); - } + std::shared_ptr self; + rpc::login login; + rpc::get_address_txs txs_response; + rpc::get_unspent_outs_response outs_response; + rpc::get_version info; + rpc::get_subaddrs subaddrs; + merge_results merged; + std::uint64_t orig_scan_height; + const bool mandatory; - const std::uint32_t needed_min_i = add_uint32_clamp(min_i, primary.lookahead.minor); - const std::uint32_t needed_count = add_uint32_clamp(unsigned(1), needed_min_i); - const rpc::provision_subaddrs_request request{ - rpc::login{primary.address, primary.view.sec}, - maj_i, 0, 1, needed_count, false /* get_all */ - }; + explicit frame(std::shared_ptr in, const bool mandatory) + : self(std::move(in)), + login{}, + txs_response{}, + outs_response{}, + info{}, + subaddrs{}, + merged{}, + orig_scan_height(0), + mandatory(mandatory) + {} - lock.unlock(); - const auto response = rpc::invoke(*client, client_prefix, request); - lock.lock(); - - if (!response && !subaddress_error) - subaddress_error = handle_subaddress_error(response.error()); - return subaddress_error; - } - - std::error_code wallet::set_lookahead(std::uint32_t major, std::uint32_t minor) - { - boost::unique_lock lock{sync}; - primary.lookahead.major = major; - primary.lookahead.minor = minor; - - const std::uint64_t from_height = primary.scan_height; - lock.unlock(); - return restore_height(from_height).error(); - } - - expect wallet::restore_height(const std::uint64_t height) - { - const auto update_import = [this](expect value) - { - if (value) + ~frame() { - this->import_error = {}; - if (value->lookahead) + if (self) { - this->server_lookahead = {value->lookahead->maj_i, value->lookahead->min_i}; - this->lookahead_error = {}; + const boost::lock_guard lock{self->sync_listener}; + if (self->listener) + self->listener->refreshed(); + } + } + }; + + struct handler : boost::asio::coroutine + { + std::shared_ptr frame_; + + explicit handler(std::shared_ptr in) + : boost::asio::coroutine(), frame_(std::move(in)) + {} + + void operator()(std::error_code error = {}) + { + LWSF_VERIFY(frame_ && frame_->self); + + bool import_called = false; + std::uint64_t from_height = 0; + + wallet& self = *frame_->self; + assert(self.strand.running_in_this_thread()); + boost::unique_lock lock{self.sync}; + BOOST_ASIO_CORO_REENTER(*this) + { + { + const auto now = std::chrono::steady_clock::now(); + if (!frame_->mandatory) + { + if (now - self.last_sync < config::refresh_interval_min) + return self.run(self.refresh_queue, self.refresh_error); + } + self.last_sync = now; + } + + if (!self.passed_login) + { + BOOST_ASIO_CORO_YIELD login(frame_->self, post_on_strand(frame_->self, *this)); + if (error) + return self.run(self.refresh_queue, self.refresh_error = error); + } + + frame_->orig_scan_height = self.primary.scan_height; + frame_->login = rpc::login{self.primary.address, self.primary.view.sec}; + BOOST_ASIO_CORO_YIELD rpc::invoke_async( + self.client, frame_->login, std::addressof(frame_->txs_response), post_on_strand(frame_->self, *this) + ); + + if (error) + { + self.passed_login = false; + if (error == rpc_unapproved) + return self.run(self.refresh_queue, self.refresh_error = error::approval); + return self.run(self.refresh_queue, self.refresh_error = error); + } + + BOOST_ASIO_CORO_YIELD rpc::invoke_async( + self.client, + rpc::get_unspent_outs_request{frame_->login, rpc::uint64_string(0), 0, true}, + std::addressof(frame_->outs_response), + post_on_strand(frame_->self, *this) + ); + + if (error) + { + self.passed_login = false; + return self.run(self.refresh_queue, self.refresh_error = error); + } + + // Remember that this function provides the strong exception guarantee. + frame_->merged = merge_response(self, frame_->txs_response, frame_->outs_response); + + if (!self.import_error && self.primary.requested_start < self.primary.restore_height) + { + BOOST_ASIO_CORO_YIELD restore_height( + frame_->self, self.primary.requested_start, post_on_strand(frame_->self, *this) + ); + import_called = true; + } + + if (config::use_subaddresses && !import_called && (frame_->merged.lookahead_fail || !self.lookahead_good())) + { + if (!self.lookahead_error) + self.lookahead_error = {error::subaddr_ahead}; + if (!self.probed_lookahead) + { + self.probed_lookahead = true; // block re-attempts temporarily + BOOST_ASIO_CORO_YIELD rpc::invoke_async( + self.client, rpc::empty{}, std::addressof(frame_->info), post_on_strand(frame_->self, *this) + ); + + if (error == http::error(404)) + error = {lwsf::error::subaddr_upgrade}; + + if (!error) + { + BOOST_ASIO_CORO_YIELD rpc::invoke_async( + self.client, frame_->login, std::addressof(frame_->subaddrs), post_on_strand(frame_->self, *this) + ); + + if (!error && should_attempt_rescan(self.primary, std::move(frame_->subaddrs.all_subaddrs), frame_->info.max_subaddresses)) + { + from_height = frame_->merged.lookahead_fail.value_or(self.primary.requested_start); + BOOST_ASIO_CORO_YIELD + restore_height(frame_->self, from_height, post_on_strand(frame_->self, *this)); + if (!error && self.lookahead_good()) + self.probed_lookahead = false; // run again if/when restore_height fails during scanning + error = {}; // restore_height` updated `import_error` + } + } + + // collect all errors from above + if (error) + self.lookahead_error = error; + } + } + else if (config::use_subaddresses && self.lookahead_good()) + { + self.lookahead_error = {}; + self.probed_lookahead = false; + if (self.primary.restore_height <= self.primary.requested_start) + self.import_error = {}; + } + + + // return error if subaddresses enabled, and recovered wallet + const std::shared_ptr strong_count = frame_->self; + frame_->self.reset(); // release before acquiring `sync_listener`. + const std::error_code rc = self.refresh_error = + self.import_error ? + self.import_error : self.subaddress_error ? + self.subaddress_error : self.lookahead_error; + const boost::lock_guard lock_listener{self.sync_listener}; + if (!self.listener) + return self.run(self.refresh_queue, rc); + + // Call listener functions without holding `sync`, in case a call is made + // back into the library. + const std::uint64_t new_scan_height = + std::max(frame_->orig_scan_height, self.primary.scan_height); + lock.unlock(); + + self.listener->refreshed(); + const auto& merged = frame_->merged; + if (!merged.new_transactions.empty() || new_scan_height - frame_->orig_scan_height) + self.listener->updated(); + + for (std::uint64_t i = frame_->orig_scan_height; i < new_scan_height; ++i) + self.listener->newBlock(i); + + for (const auto& tx : merged.new_transactions) + { + const auto txid = epee::string_tools::pod_to_hex(tx->id); + if (tx->direction == Monero::TransactionInfo::Direction_In) + { + if (tx->height) + self.listener->moneyReceived(txid, tx->amount); + else + self.listener->unconfirmedMoneyReceived(txid, tx->amount); + } + else + self.listener->moneySpent(txid, tx->amount); + } + + self.run(self.refresh_queue, rc); + } + } + }; + + LWSF_VERIFY(self); + if (self->push(self->refresh_queue, std::move(f))) + boost::asio::post(self->strand, handler{std::make_shared(self, mandatory)}); + } + + void wallet::register_subaccount(std::shared_ptr self, const std::uint32_t maj_i, std::function f) + { + struct frame + { + const std::shared_ptr self; + const std::function f; + rpc::provision_subaddrs_response response; + const std::uint32_t maj_i; + + explicit frame(std::shared_ptr self, std::function&& f, const std::uint32_t maj_i) + : self(std::move(self)), f(std::move(f)), response{}, maj_i(maj_i) + {} + }; + + struct handler : boost::asio::coroutine + { + std::shared_ptr frame_; + + explicit handler(std::shared_ptr in) noexcept + : boost::asio::coroutine(), frame_(std::move(in)) + {} + + static rpc::provision_subaddrs_request get_request(wallet& self, const std::uint32_t maj_i) + { + const std::uint32_t minor_count = std::max(std::uint32_t(1), self.primary.lookahead.minor); + return { + rpc::login{self.primary.address, self.primary.view.sec}, + maj_i, 0, 1, minor_count, false, /* get_all */ + }; + } + + void operator()(const std::error_code error = {}) + { + LWSF_VERIFY(frame_ && frame_->self && frame_->f); + + wallet& self = *frame_->self; + assert(self.strand.running_in_this_thread()); + const boost::lock_guard lock{self.sync}; + BOOST_ASIO_CORO_REENTER(*this) + { + LWSF_VERIFY(frame_->maj_i < self.primary.subaccounts.size()); + + if (!self.passed_login) + { + BOOST_ASIO_CORO_YIELD login(frame_->self, post_on_strand(frame_->self, *this)); + if (error) + return frame_->f(error); + } + + BOOST_ASIO_CORO_YIELD rpc::invoke_async( + self.client, + get_request(self, frame_->maj_i), + std::addressof(frame_->response), + post_on_strand(frame_->self, *this) + ); + + if (error && !self.subaddress_error) + self.subaddress_error = handle_subaddress_error(error); + frame_->f(self.subaddress_error); + } + } + }; + + LWSF_VERIFY(config::use_subaddresses); + LWSF_VERIFY(self); + boost::asio::post(self->strand, handler{std::make_shared(self, std::move(f), maj_i)}); + } + + void wallet::register_subaddress(std::shared_ptr self, const std::uint32_t maj_i, const std::uint32_t min_i, std::function f) + { + struct frame + { + const std::shared_ptr self; + const std::function f; + rpc::provision_subaddrs_response response; + const std::uint32_t maj_i; + const std::uint32_t min_i; + + frame(std::shared_ptr self, std::uint32_t maj_i, std::uint32_t min_i, std::function&& f) + : self(std::move(self)), f(std::move(f)), response{}, maj_i(maj_i), min_i(min_i) + {} + + rpc::provision_subaddrs_request get_request(wallet& self) + { + const std::uint32_t needed_min_i = add_uint32_clamp(min_i, self.primary.lookahead.minor); + const std::uint32_t needed_count = add_uint32_clamp(unsigned(1), needed_min_i); + return { + rpc::login{self.primary.address, self.primary.view.sec}, + maj_i, 0, 1, needed_count, false /* get_all */ + }; + } + }; + + struct handler : boost::asio::coroutine + { + std::shared_ptr frame_; + + explicit handler(std::shared_ptr in) + : boost::asio::coroutine(), frame_(std::move(in)) + {} + + void operator()(const std::error_code error = {}) + { + LWSF_VERIFY(frame_ && frame_->self); + wallet& self = *frame_->self; + assert(self.strand.running_in_this_thread()); + const boost::lock_guard lock{self.sync}; + BOOST_ASIO_CORO_REENTER(*this) + { + if (!self.passed_login) + { + BOOST_ASIO_CORO_YIELD login(frame_->self, post_on_strand(frame_->self, *this)); + if (error) + return frame_->f(error); + } + + BOOST_ASIO_CORO_YIELD rpc::invoke_async( + self.client, frame_->get_request(self), std::addressof(frame_->response), post_on_strand(frame_->self, *this) + ); + + if (error && !self.subaddress_error) + self.subaddress_error = handle_subaddress_error(error); + frame_->f(self.subaddress_error); + } + } + }; + + LWSF_VERIFY(config::use_subaddresses); + LWSF_VERIFY(self); + const boost::lock_guard lock{self->sync}; + LWSF_VERIFY(maj_i < self->primary.subaccounts.size()); + LWSF_VERIFY(min_i <= self->primary.subaccounts[maj_i].last); + boost::asio::post(self->strand, handler{std::make_shared(self, maj_i, min_i, std::move(f))}); + } + + void wallet::set_lookahead(std::shared_ptr self, std::uint32_t major, std::uint32_t minor, std::function f) + { + LWSF_VERIFY(config::use_subaddresses); + LWSF_VERIFY(self); + const boost::lock_guard lock{self->sync}; + self->primary.lookahead.major = major; + self->primary.lookahead.minor = minor; + + const std::uint64_t from_height = self->primary.scan_height; + restore_height(std::move(self), from_height, std::move(f)); + } + + void wallet::restore_height_raw(std::shared_ptr self, const std::uint64_t height, std::function)> f) + { + struct frame + { + const std::shared_ptr self; + const std::function)> f; + rpc::import_response response; + const std::uint64_t height; + + frame(std::shared_ptr self, const std::uint64_t height, std::function)>&& f) + : self(std::move(self)), f(std::move(f)), response{}, height(height) + {} + + void done(expect result) + { + LWSF_VERIFY(self && f); + if (result) + { + self->primary.restore_height = std::min(height, self->primary.restore_height); + self->import_error = {}; + if (result->lookahead) + { + self->server_lookahead = {result->lookahead->maj_i, result->lookahead->min_i}; + self->lookahead_error = {}; + } + else + self->lookahead_error = {error::subaddr_upgrade}; } else - this->lookahead_error = {error::subaddr_disabled}; + { + if (result == rpc_max_subaddresses) + self->import_error = {error::subaddr_ahead}; + else + self->import_error = result.error(); + } + f(std::move(result)); } - else - { - if (value == rpc_max_subaddresses) - this->import_error = {error::subaddr_ahead}; - else - this->import_error = value.error(); - } - return value; }; - boost::unique_lock lock{sync}; - if (!passed_login) + struct handler : boost::asio::coroutine { - lock.unlock(); - const std::error_code error = login(); - if (error) - return error; - lock.lock(); - } + std::shared_ptr frame_; - if (primary.restore_height <= height && lookahead_good() && !lookahead_error) - return update_import(rpc::import_response{.lookahead = rpc::address_meta{server_lookahead}}); + explicit handler(std::shared_ptr in) + : boost::asio::coroutine(), frame_(std::move(in)) + {} - rpc::import_request login{{primary.address, primary.view.sec}, height, primary.lookahead}; + void operator()(const std::error_code error = {}) + { + LWSF_VERIFY(frame_ && frame_->self); + wallet& self = *frame_->self; + assert(self.strand.running_in_this_thread()); + const boost::lock_guard lock{self.sync}; + BOOST_ASIO_CORO_REENTER(*this) + { + if (!self.passed_login) + { + BOOST_ASIO_CORO_YIELD login(frame_->self, post_on_strand(frame_->self, *this)); + if (error) + return frame_->done(error); + } - lock.unlock(); - auto import = rpc::invoke(*client, client_prefix, login); - lock.lock(); + if (self.primary.restore_height <= frame_->height && self.lookahead_good() && !self.lookahead_error) + return frame_->done(rpc::import_response{.lookahead = rpc::address_meta{self.server_lookahead}}); + + BOOST_ASIO_CORO_YIELD rpc::invoke_async( + self.client, + rpc::import_request{{self.primary.address, self.primary.view.sec}, frame_->height, self.primary.lookahead}, + std::addressof(frame_->response), + post_on_strand(frame_->self, *this) + ); - if (!import || import->request_fulfilled) - return update_import(std::move(import)); + if (error) + return frame_->done(error); + else if (frame_->response.request_fulfilled) + return frame_->done(std::move(frame_->response)); - const unsigned total = - unsigned(bool(import->import_fee)) + bool(import->payment_address); - switch (total) - { - default: - case 0: - return update_import({error::import_pending}); - case 1: - if (import->import_fee.value_or(rpc::uint64_string(0)) == rpc::uint64_string(0)) - return update_import({error::import_pending}); - return update_import({error::import_invalid}); - case 2: - break; - } + const unsigned total = + unsigned(bool(frame_->response.import_fee)) + bool(frame_->response.payment_address); + switch (total) + { + default: + case 0: + return frame_->done({error::import_pending}); + case 1: + if (frame_->response.import_fee.value_or(rpc::uint64_string(0)) == rpc::uint64_string(0)) + return frame_->done({error::import_pending}); + return frame_->done({error::import_invalid}); + case 2: + break; + } - cryptonote::address_parse_info info{}; - if (!cryptonote::get_account_address_from_str(info, convert_net_type(primary.type), *import->payment_address)) - return update_import({error::import_invalid}); - if (info.has_payment_id && import->payment_id) - return update_import({error::import_invalid}); - if (import->payment_id && (import->payment_id->empty() || (import->payment_id->size() != sizeof(crypto::hash8) && import->payment_id->size() != sizeof(crypto::hash)))) - return update_import({error::import_invalid}); + cryptonote::address_parse_info info{}; + if (!cryptonote::get_account_address_from_str(info, convert_net_type(self.primary.type), *frame_->response.payment_address)) + return frame_->done({error::import_invalid}); + if (info.has_payment_id && frame_->response.payment_id) + return frame_->done({error::import_invalid}); + if (frame_->response.payment_id && (frame_->response.payment_id->empty() || (frame_->response.payment_id->size() != sizeof(crypto::hash8) && frame_->response.payment_id->size() != sizeof(crypto::hash)))) + return frame_->done({error::import_invalid}); -#ifdef LWSF_MASTER_ENABLE - std::string payment_id; - if (import->payment_id) - payment_id = epee::to_hex::string(epee::to_span(*import->payment_id)); + #ifdef LWSF_MASTER_ENABLE + std::string payment_id; + if (frame_->response.payment_id) + payment_id = epee::to_hex::string(epee::to_span(*frame_->response.payment_id)); - std::size_t i = 0; - for (; i < primary.addressbook.size(); ++i) - { - const bool existing = - primary.addressbook[i].address == *import->payment_address && - primary.addressbook[i].payment_id == payment_id; - if (existing) - break; - } + std::size_t i = 0; + for (; i < self.primary.addressbook.size(); ++i) + { + const bool existing = + self.primary.addressbook[i].address == *frame_->response.payment_address && + self.primary.addressbook[i].payment_id == payment_id; + if (existing) + break; + } - std::string description = "Payment of " + cryptonote::print_money(*import->import_fee) + " XMR is needed to import/restore height"; - if (i == primary.addressbook.size()) - primary.addressbook.push_back(address_book_entry{std::move(*import->payment_address), std::move(payment_id), std::move(description)}); - else - primary.addressbook[i] = address_book_entry{std::move(*import->payment_address), std::move(payment_id), std::move(description)}; -#endif - return update_import({error::import_pending}); + std::string description = "Payment of " + cryptonote::print_money(*frame_->response.import_fee) + " XMR is needed to import/restore height"; + if (i == self.primary.addressbook.size()) + self.primary.addressbook.push_back(address_book_entry{std::move(*frame_->response.payment_address), std::move(payment_id), std::move(description)}); + else + self.primary.addressbook[i] = address_book_entry{std::move(*frame_->response.payment_address), std::move(payment_id), std::move(description)}; + #endif + frame_->done({error::import_pending}); + } + } + }; + + LWSF_VERIFY(self); + boost::asio::post(self->strand, handler{std::make_shared(self, height, std::move(f))}); } - expect> wallet::get_decoys(const rpc::get_random_outs_request& req) + void wallet::get_decoys(std::shared_ptr self, rpc::get_random_outs_request&& req, std::function f) { - auto resp = rpc::invoke(*client, client_prefix, req); - if (!resp) - return resp.error(); - return {std::move(resp->amount_outs)}; + struct frame + { + const std::shared_ptr self; + const std::function f; + rpc::get_random_outs_request request; + rpc::get_random_outs_response response; + + explicit frame(std::shared_ptr&& self, rpc::get_random_outs_request&& req, std::function&& f) + : self(std::move(self)), f(std::move(f)), request(std::move(req)), response{} + {} + }; + + struct handler : boost::asio::coroutine + { + std::shared_ptr frame_; + + explicit handler(std::shared_ptr in) + : boost::asio::coroutine(), frame_(std::move(in)) + {} + + void operator()(const std::error_code error = {}) + { + LWSF_VERIFY(frame_ && frame_->self); + + wallet& self = *frame_->self; + BOOST_ASIO_CORO_REENTER(*this) + { + BOOST_ASIO_CORO_YIELD rpc::invoke_async( + self.client, frame_->request, std::addressof(frame_->response), *this + ); + + if (error) + frame_->f(error); + else + frame_->f(std::move(frame_->response.amount_outs)); + }; + } + }; + + handler{std::make_shared(std::move(self), std::move(req), std::move(f))}(); } - std::error_code wallet::send_tx(epee::byte_slice tx_bytes) + void wallet::send_tx(std::shared_ptr self, epee::byte_slice tx_bytes, std::function f) { - const rpc::submit_raw_tx_request request{std::move(tx_bytes)}; - return rpc::invoke(*client, client_prefix, request).error(); + struct frame + { + const std::shared_ptr self; + const std::function f; + epee::byte_slice tx_bytes; + rpc::submit_raw_tx_response response; + + explicit frame(std::shared_ptr&& self, epee::byte_slice&& tx_bytes, std::function&& f) + : self(std::move(self)), f(std::move(f)), tx_bytes(std::move(tx_bytes)), response{} + {} + }; + + struct handler : boost::asio::coroutine + { + std::shared_ptr frame_; + + explicit handler(std::shared_ptr&& in) + : frame_(std::move(in)) + {} + + void operator()(const std::error_code error = {}) + { + LWSF_VERIFY(frame_ && frame_->self); + wallet& self = *frame_->self; + BOOST_ASIO_CORO_REENTER(*this) + { + BOOST_ASIO_CORO_YIELD rpc::invoke_async( + self.client, + rpc::submit_raw_tx_request{std::move(frame_->tx_bytes)}, + std::addressof(frame_->response), + *this + ); + frame_->f(error); + } + } + }; + + handler{std::make_shared(std::move(self), std::move(tx_bytes), std::move(f))}(); } }}} // lwsf // internal // backend diff --git a/src/backend.h b/src/backend.h index 276a595..dfa3234 100644 --- a/src/backend.h +++ b/src/backend.h @@ -29,6 +29,8 @@ #pragma once #include +#include +#include #include #include #include @@ -36,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -51,6 +54,7 @@ #include "cryptonote_basic/account.h" // monero/src #include "lwsf_config.h" #include "lwsf_rpc.h" +#include "net/context.h" #include "ringct/rctTypes.h" // monero/src #include "wallet/api/wallet2_api.h" // monero/src #include "wire/fwd.h" @@ -233,11 +237,16 @@ namespace lwsf { namespace internal { namespace backend //! All functions should provide the strong-exception guarantee struct wallet { - Monero::WalletListener* listener; - const std::unique_ptr client; + // Everything uses `sync` for synchronization unless noted + + Monero::WalletListener* listener; //!< Use `sync_listener` + boost::asio::io_context::strand strand; // per_byte_fee; //!< by priority level + std::vector)>> login_queue; //!< use `sync_queue` + std::vector> refresh_queue; //!< use `sync_queue` 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 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 mutable boost::mutex sync; boost::mutex sync_listener; - boost::mutex sync_refresh; + boost::mutex sync_queue; bool passed_login; 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 + bool push(std::vector>& dest, F&& f) + { + const boost::lock_guard lock{sync_queue}; + const bool rc = dest.empty(); + dest.push_back(std::forward(f)); + return rc; + } + + //! Call all functions in `waiting` with thread synchronization + template + void run(std::vector>& waiting, const U&... args) + { + std::vector> ready; + { + const boost::lock_guard lock{sync_queue}; + ready.swap(waiting); + } + for (const auto& f : ready) + f(args...); + } + // `sync` mutex is NOT acquired for this group @@ -265,7 +298,7 @@ namespace lwsf { namespace internal { namespace backend // 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. expect to_bytes() const; @@ -273,29 +306,45 @@ namespace lwsf { namespace internal { namespace backend //! De-serialize `this` from msgpack. Locks+replaces contents. 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`. \return No errors if login succeeded, and true if new account */ - expect login_is_new(); + static void login_is_new(std::shared_ptr self, std::function)>); /*! Attempt login and check lookahead status. Updates `lookahead_error`. \return No errors if login succeeded. */ - std::error_code login() { return login_is_new().error(); } + template + static void login(std::shared_ptr self, F f) + { + login_is_new(std::move(self), [f = std::move(f)] (expect r) mutable { f(r.error()); }); + } //! Refreshes txes information. Strong exception guarantee. - std::error_code refresh(bool mandatory = false); + static void refresh(std::shared_ptr self, bool mandatory, std::function); //! 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 self, std::uint32_t maj_i, std::function f); - //! Notify server that new minor accounts need to be watched. - std::error_code register_subaddress(std::uint32_t maj_i, std::uint32_t min_i); + //! Notify server that new minor accounts need to be watched. Do not hold `self->sync`. + static void register_subaddress(std::shared_ptr self, std::uint32_t maj_i, std::uint32_t min_i, std::function); - //! Modify local and possibly server lookahead - std::error_code set_lookahead(std::uint32_t major, std::uint32_t minor); + //! Modify local and possibly server lookahead. Do not hold `self->sync`. + static void set_lookahead(std::shared_ptr self, std::uint32_t major, std::uint32_t minor, std::function); - expect restore_height(const std::uint64_t height); + static void restore_height_raw(std::shared_ptr self, const std::uint64_t height, std::function)>); - expect> get_decoys(const rpc::get_random_outs_request& req); - std::error_code send_tx(epee::byte_slice tx_bytes); + template + static void restore_height(std::shared_ptr 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>); + static void get_decoys(std::shared_ptr self, rpc::get_random_outs_request&& req, std::function f); + + static void send_tx(std::shared_ptr slef, epee::byte_slice tx_bytes, std::function f); }; }}} // lwsf // internal // backend diff --git a/src/error.cpp b/src/error.cpp index 97d9cee..1dc82fc 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -27,6 +27,9 @@ #include "error.h" +#include +#include "common/error.h" // monero/src + namespace lwsf { const char* get_string(const error value) noexcept @@ -57,10 +60,14 @@ namespace lwsf return "server has subaddresses disabled"; case error::subaddr_local: return "lwsf (local) limits on subaddresses too small"; + case error::subaddr_upgrade: + return "server does not support newer subaddr commands"; case error::unexpected_userinfo: return "Unexpected user+pass field in URL"; case error::unexpected_nullptr: return "Unexpected nullptr"; + case error::unknown_exception: + return "Unknown exception"; default: break; @@ -74,7 +81,7 @@ namespace lwsf { 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 @@ -85,4 +92,14 @@ namespace lwsf static const category 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 diff --git a/src/error.h b/src/error.h index 7e513d4..901a9a2 100644 --- a/src/error.h +++ b/src/error.h @@ -28,6 +28,14 @@ #pragma once #include +#include + +#define LWSF_VERIFY(x) \ + do \ + { \ + if (!(x)) \ + ::lwsf::throw_invalid_argument(__LINE__, __FILE__); \ + } while (0) namespace lwsf { @@ -46,8 +54,10 @@ namespace lwsf subaddr_ahead, //!< Server limits on subaddresses affects lookahead subaddr_disabled, //!< Server has subaddresses disabled subaddr_local, //!< Local limits on subaddresses too small + subaddr_upgrade, //!< Upgrade server for server-side limits unexpected_userinfo,//!< Unexpected user+pass provided unexpected_nullptr, //!< Expected non-nullptr + unknown_exception //!< Unknown exception occured }; //! \return Error message string. @@ -62,6 +72,7 @@ namespace lwsf return std::error_code{int(value), error_category()}; } + [[noreturn]] void throw_invalid_argument(int line, const char* file); } // lwsf namespace std diff --git a/src/lwsf_config.h b/src/lwsf_config.h index 7687eb3..a2bd9e9 100644 --- a/src/lwsf_config.h +++ b/src/lwsf_config.h @@ -47,6 +47,9 @@ namespace lwsf { namespace config constexpr const lookahead default_lookahead{50, 200}; constexpr const lookahead default_minimal_lookahead{5, 15}; 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 max_file_read_size = 50 * 1024 * 1024; // 50 MiB 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_min{5}; constexpr const std::chrono::seconds rpc_timeout{30}; + constexpr const bool use_subaddresses = true; //!< Disables subaddress rpc probing }} // lwsf // config diff --git a/src/lwsf_rpc.cpp b/src/lwsf_rpc.cpp index 293cde7..95a067a 100644 --- a/src/lwsf_rpc.cpp +++ b/src/lwsf_rpc.cpp @@ -54,81 +54,6 @@ 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 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(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::max() < response->m_response_code) - return {error::invalid_code}; - return {error(int(response->m_response_code))}; - } - void write_bytes(wire::writer& dest, const empty&) { wire::object(dest); } void read_bytes(wire::reader& source, empty&) diff --git a/src/lwsf_rpc.h b/src/lwsf_rpc.h index 427cc03..91db559 100644 --- a/src/lwsf_rpc.h +++ b/src/lwsf_rpc.h @@ -30,7 +30,9 @@ #include #include +#include #include +#include #include #include #include @@ -40,57 +42,54 @@ #include "byte_stream.h" // moneor/contrib/epee/include #include "common/expect.h" // monero/src #include "crypto/crypto.h" // monero/src +#include "error.h" #include "lwsf_config.h" +#include "net/http.h" #include "ringct/rctTypes.h" // monero/src #include "wire/basic_value.h" #include "wire/fwd.h" #include "wire/json.h" #include "wire/traits.h" -namespace epee { namespace net_utils -{ - class blocked_mode_client; - namespace http { template class http_simple_client_template; } -}} - namespace lwsf { namespace internal { namespace rpc { using max_subaddrs = wire::max_element_count<16384>; - using http_client = epee::net_utils::http::http_simple_client_template< - epee::net_utils::blocked_mode_client - >; + struct daemon_status; - enum class error : int + template + 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(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; - inline std::error_code make_error_code(const error value) noexcept + template + void invoke_async(const http::client& client, const T& in, U* out, F f) { - return std::error_code{int(value), error_category()}; - } - - //! Send `payload` to `client` at uri `endpoint`, and \return payload response - expect invoke_payload(http_client& client, boost::string_ref prefix, boost::string_ref endpoint, epee::byte_slice payload); - - template - expect invoke(http_client& client, boost::string_ref prefix, const G& in) - { - epee::byte_stream sink{}; - std::error_code error = wire::json::to_bytes(sink, in); - if (error) - return error; - - expect 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; + // /daemon_status historically needed to be a post + if constexpr (!std::is_empty{} || std::is_same{}) + { + epee::byte_stream sink{}; + std::error_code error = wire::json::to_bytes(sink, in); + if (error) + return f(error); + client.post_async(U::endpoint(), epee::byte_slice{std::move(sink)}, unpacker{out, std::move(f)}); + } + else + client.get_async(U::endpoint(), unpacker{out, std::move(f)}); } struct empty {}; @@ -409,6 +408,8 @@ namespace lwsf { namespace internal { namespace rpc import_response() = delete; import_response(import_response&&) = default; 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"; } boost::optional payment_address; @@ -482,10 +483,3 @@ namespace lwsf { namespace internal { namespace rpc WIRE_DECLARE_BLOB(lwsf::internal::rpc::ringct); -namespace std -{ - template<> - struct is_error_code_enum - : true_type - {}; -} diff --git a/src/net/CMakeLists.txt b/src/net/CMakeLists.txt new file mode 100644 index 0000000..e5e7539 --- /dev/null +++ b/src/net/CMakeLists.txt @@ -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) + diff --git a/src/net/context.cpp b/src/net/context.cpp new file mode 100644 index 0000000..1fb8c66 --- /dev/null +++ b/src/net/context.cpp @@ -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 +#include + +namespace lwsf { namespace internal { namespace net +{ + context::context() + : restart_(), io_(), sync_() + {} + + std::shared_ptr context::restart_asio() + { + std::shared_ptr out; + const boost::lock_guard 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(); + restart_ = out; + } + return out; + } +}}} // lwsf // internal // net diff --git a/src/net/context.h b/src/net/context.h new file mode 100644 index 0000000..eb3f9c1 --- /dev/null +++ b/src/net/context.h @@ -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 +#include +#include +#include +#include +#include + +namespace lwsf { namespace internal { namespace net +{ + template + struct to_future + { + struct inner + { + std::promise promise; + inner() + : promise() + {} + }; + + std::shared_ptr inner_; + + to_future() + : inner_(std::make_shared()) + {} + + void operator()(T value) + { + LWSF_VERIFY(inner_); + inner_->promise.set_value(std::move(value)); + } + }; + + struct context + { + std::weak_ptr 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 restart_asio(); + + template + T wait_for(F f) + { + to_future 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 diff --git a/src/net/http.cpp b/src/net/http.cpp new file mode 100644 index 0000000..90327e8 --- /dev/null +++ b/src/net/http.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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&& 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 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 + reader(boost::beast::http::header&, value_type& body) + : body_(body) + {} + + void init(boost::optional const& content_length, boost::system::error_code& ec) + { + static_assert( + std::numeric_limits::max() <= std::numeric_limits::max() + ); + if (content_length) + body_.reserve(*content_length); + ec = {}; + } + + template + 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(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 + explicit writer(boost::beast::http::header const&, value_type const& body) + : body_(body.clone()) + {} + + void init(boost::beast::error_code& ec) + { + ec = {}; + } + + boost::optional> 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, std::function); + + boost::beast::flat_static_buffer buffer; + std::function connect; + std::deque 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 request; + const epee::net_utils::ssl_options_t ssl; + boost::asio::ssl::context ssl_context; + boost::asio::ssl::stream sock; + boost::optional> parser; + std::size_t iteration; + const std::uint16_t port; + std::atomic 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) + : 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 + 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(callback))); + else + boost::beast::http::async_write(sock.next_layer(), request, boost::asio::bind_executor(strand, std::forward(callback))); + } + + template + 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(callback))); + else + boost::beast::http::async_read(sock.next_layer(), buffer, *parser, boost::asio::bind_executor(strand, std::forward(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 + void set_timeout(std::shared_ptr self, const std::chrono::steady_clock::duration timeout, F f) + { + LWSF_VERIFY(self); + + struct on_timeout + { + on_timeout() = delete; + std::shared_ptr 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 self, const std::chrono::steady_clock::duration timeout) + { + LWSF_VERIFY(self); + + struct on_timeout + { + on_timeout() = delete; + std::shared_ptr 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 self_; + + public: + explicit client_loop(std::shared_ptr 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::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::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&& notifier, boost::beast::http::verb verb) const + { + message msg{std::move(body), std::move(target), std::move(notifier), verb}; + std::shared_ptr state; + { + const boost::lock_guard 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 self, std::function f) const + { + struct frame + { + std::function f; + boost::asio::ip::tcp::resolver resolver; + const std::size_t iteration; + + frame(client_state& self, std::function&& f) + : f(std::move(f)), resolver(self.strand.context()), iteration(self.iteration) + {} + }; + + struct handler : boost::asio::coroutine + { + std::shared_ptr self_; + std::shared_ptr frame_; + + explicit handler(std::shared_ptr self, std::shared_ptr&& 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 self_; + std::shared_ptr 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(*self, std::move(f))}(); + } + + void client::socks::operator()(std::shared_ptr self, std::function f) const + { + struct handler + { + std::shared_ptr self_; + std::function 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 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 lock{sync_}; + state_ = std::make_shared(io, std::move(host), std::move(prefix), port, std::move(ssl), proxy_); + } + + client::~client() + {} + + bool client::is_connected() const noexcept + { + const boost::lock_guard lock{sync_}; + return state_ && state_->is_connected; + } + + void client::set_proxy(std::function connector) + { + LWSF_VERIFY(connector); + const boost::lock_guard lock{sync_}; + proxy_ = std::move(connector); + if (state_) + state_ = std::make_shared(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 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 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 + diff --git a/src/net/http.h b/src/net/http.h new file mode 100644 index 0000000..6e24797 --- /dev/null +++ b/src/net/http.h @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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, std::function); + + //! Primarily for webhooks, where the response is (basically) ignored. + class client + { + + std::shared_ptr state_; + std::function proxy_; + mutable boost::mutex sync_; + + void queue_async(std::string&& target, epee::byte_slice&& body, std::function&& notifier, boost::beast::http::verb verb) const; + + public: + struct direct + { + void operator()(std::shared_ptr, std::function) const; + }; + + struct socks + { + boost::asio::ip::tcp::endpoint proxy_address; + void operator()(std::shared_ptr, std::function) 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 connector); + + //! Never blocks. Thread safe. \return `success()` if `url` is valid. + void post_async(std::string target, epee::byte_slice body, std::function 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 notifier) const; + }; +}}} // lwsf // internal // http + +namespace std +{ + template<> + struct is_error_code_enum + : true_type + {}; +} diff --git a/src/net/wallet_io.cpp b/src/net/wallet_io.cpp new file mode 100644 index 0000000..bf6f8a8 --- /dev/null +++ b/src/net/wallet_io.cpp @@ -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()), + wallet(std::make_shared(context->io_)) + {} +}}} // lwsf // internal // net diff --git a/src/net/wallet_io.h b/src/net/wallet_io.h new file mode 100644 index 0000000..c04227b --- /dev/null +++ b/src/net/wallet_io.h @@ -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 + +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 context; // wallet; //!< Has references to `context` + + wallet_io(); + }; + } // net +}} // lwsf // intenral diff --git a/src/pending_transaction.cpp b/src/pending_transaction.cpp index 6b1fb8a..9334aad 100644 --- a/src/pending_transaction.cpp +++ b/src/pending_transaction.cpp @@ -26,16 +26,17 @@ // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#include "backend.h" #include "pending_transaction.h" #include #include #include #include -#include "backend.h" #include "byte_slice.h" // monero/contrib/epee/include #include "cryptonote_basic/cryptonote_format_utils.h" // monero/src #include "error.h" +#include "net/context.h" #include "numeric.h" #include "string_tools.h" // monero/contrib/epee/include #include "utils/encrypted_file.h" @@ -100,40 +101,37 @@ namespace lwsf { namespace internal } - pending_transaction::pending_transaction(std::shared_ptr wallet, std::string error, std::vector> local) - : wallet_(std::move(wallet)), local_(std::move(local)), error_(std::move(error)) + pending_transaction::pending_transaction(const net::wallet_io& data, std::string error, std::vector> local) + : data_(data), local_(std::move(local)), error_(std::move(error)) { - if (!wallet_) - throw std::invalid_argument{"lwsf::internal::pending_transaction cannot be given nullptr backend"}; + LWSF_VERIFY(data_.context && data_.wallet); for (const auto& e : local_) { - if (!e) - throw std::invalid_argument{"lwsf::internal::pending_transaction cannot be given nullptr backend::transaction"}; + LWSF_VERIFY(bool(e)); } } - std::unique_ptr pending_transaction::load_from_file(std::shared_ptr wallet, const std::string& filename) + std::unique_ptr pending_transaction::load_from_file(const net::wallet_io& data, const std::string& filename) { - if (!wallet) - throw std::invalid_argument{"lwsf::internal::pending_transaction::load_from_file cannot be given nullptr backend"}; + LWSF_VERIFY(data.wallet); epee::byte_slice file = try_load(filename, tx_file_magic); if (file.empty()) - return std::make_unique(wallet, "Invalid path/file for tx"); + return std::make_unique(data, "Invalid path/file for tx"); expect payload = epee::byte_slice{}; { - const boost::lock_guard lock{wallet->sync}; - payload = decrypt(std::move(file), epee::as_byte_span(unwrap(unwrap(wallet->primary.view.sec)))); + const boost::lock_guard lock{data.wallet->sync}; + payload = decrypt(std::move(file), epee::as_byte_span(unwrap(unwrap(data.wallet->primary.view.sec)))); } if (!payload) - return std::make_unique(wallet, payload.error().message()); + return std::make_unique(data, payload.error().message()); txes_file dest{}; if (std::error_code error = wire::msgpack::from_bytes(std::move(*payload), dest)) - return std::make_unique(wallet, error.message()); - return std::make_unique(wallet, "", std::move(dest.txes)); + return std::make_unique(data, error.message()); + return std::make_unique(data, "", std::move(dest.txes)); } pending_transaction::~pending_transaction() @@ -144,18 +142,29 @@ namespace lwsf { namespace internal if (!error_.empty()) return false; - for (auto e : local_) + try { - const std::error_code error = wallet_->send_tx(e->raw_bytes.clone()); - if (error) + for (auto e : local_) { - error_ = error.message(); - return false; + const auto error = data_.context->wait_for( + [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 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 lock{wallet_->sync}; - auto entry = wallet_->primary.txes.try_emplace(e->id).first; - if (!entry->second) - entry->second = std::move(e); + } + catch(const std::exception& e) + { + error_ = e.what(); + return false; } return true; } @@ -189,12 +198,12 @@ namespace lwsf { namespace internal return false; } - const boost::lock_guard lock{wallet_->sync}; + const boost::lock_guard lock{data_.wallet->sync}; bytes = encrypt( tx_file_magic, epee::byte_slice{std::move(sink)}, 0 /* iterations */, - epee::as_byte_span(unwrap(unwrap(wallet_->primary.view.sec))) + epee::as_byte_span(unwrap(unwrap(data_.wallet->primary.view.sec))) ); } diff --git a/src/pending_transaction.h b/src/pending_transaction.h index 0e0499c..b47b402 100644 --- a/src/pending_transaction.h +++ b/src/pending_transaction.h @@ -36,26 +36,23 @@ #include #include "common/expect.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 "wire/fwd.h" namespace lwsf { namespace internal { - namespace backend - { - struct transaction; - struct wallet; - } + namespace backend { struct transaction; } class pending_transaction final : public Monero::PendingTransaction { - const std::shared_ptr wallet_; + const net::wallet_io data_; const std::vector> local_; std::string error_; public: - pending_transaction(std::shared_ptr wallet, std::string error, std::vector> local = {}); - static std::unique_ptr load_from_file(std::shared_ptr wallet, const std::string& filename); + pending_transaction(const net::wallet_io& data, std::string error, std::vector> local = {}); + static std::unique_ptr load_from_file(const net::wallet_io& data, const std::string& filename); pending_transaction(pending_transaction&&) = delete; pending_transaction(const pending_transaction&) = delete; diff --git a/src/subaddress_account.cpp b/src/subaddress_account.cpp index 86be72c..0de2160 100644 --- a/src/subaddress_account.cpp +++ b/src/subaddress_account.cpp @@ -33,6 +33,7 @@ #include #include "backend.h" #include "cryptonote_basic/cryptonote_format_utils.h" // monero/src +#include "error.h" namespace lwsf { namespace internal { @@ -66,11 +67,10 @@ namespace lwsf { namespace internal } } - subaddress_account::subaddress_account(internal::wallet* wal, std::shared_ptr data) - : wal_(wal), data_(std::move(data)), rows_() + subaddress_account::subaddress_account(internal::wallet* wal, const net::wallet_io& data) + : wal_(wal), data_(data), rows_() { - if (!wal_ || !data_) - throw std::invalid_argument{"lwsf::internal::subaddress_account cannot be given nullptr"}; + LWSF_VERIFY(wal_ && data_.wallet); } subaddress_account::~subaddress_account() @@ -88,8 +88,8 @@ namespace lwsf { namespace internal void subaddress_account::setLabel(uint32_t accountIndex, const std::string &label) { { - const boost::lock_guard lock{data_->sync}; - data_->primary.subaccounts.at(accountIndex).detail.try_emplace(0).first->second.label = label; + const boost::lock_guard lock{data_.wallet->sync}; + data_.wallet->primary.subaccounts.at(accountIndex).detail.try_emplace(0).first->second.label = label; } refresh(); } @@ -101,11 +101,11 @@ namespace lwsf { namespace internal free_rows(rows_); std::unordered_map balances; - const boost::lock_guard lock{data_->sync}; - const Monero::NetworkType net_type = data_->primary.type; - const std::uint32_t chain_height = data_->blockchain_height; + const boost::lock_guard lock{data_.wallet->sync}; + const Monero::NetworkType net_type = data_.wallet->primary.type; + 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) continue; @@ -130,7 +130,7 @@ namespace lwsf { namespace internal static constexpr const std::uint32_t max_index = std::numeric_limits::max(); - const auto& accts = data_->primary.subaccounts; + const auto& accts = data_.wallet->primary.subaccounts; rows_.reserve(accts.size()); for (std::size_t i = 0; i < accts.size() && i <= max_index; ++i) { @@ -138,7 +138,7 @@ namespace lwsf { namespace internal rows_.push_back( new Monero::SubaddressAccountRow{ 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()}, cryptonote::print_money(balance.total), cryptonote::print_money(balance.unlocked) diff --git a/src/subaddress_account.h b/src/subaddress_account.h index d92ece2..e288e07 100644 --- a/src/subaddress_account.h +++ b/src/subaddress_account.h @@ -31,6 +31,7 @@ #include #include #include +#include "net/wallet_io.h" #include "wallet.h" #include "wallet/api/wallet2_api.h" // monero/src @@ -41,12 +42,12 @@ namespace lwsf { namespace internal class subaddress_account final : public ::Monero::SubaddressAccount { internal::wallet* const wal_; - const std::shared_ptr data_; + const net::wallet_io data_; std::vector rows_; public: - explicit subaddress_account(internal::wallet* wal, std::shared_ptr data); + explicit subaddress_account(internal::wallet* wal, const net::wallet_io& data); subaddress_account(const subaddress_account&) = delete; subaddress_account(subaddress_account&&) = delete; diff --git a/src/subaddress_minor.cpp b/src/subaddress_minor.cpp index a70c26a..5a8dca5 100644 --- a/src/subaddress_minor.cpp +++ b/src/subaddress_minor.cpp @@ -33,6 +33,7 @@ #include #include "backend.h" #include "cryptonote_basic/cryptonote_format_utils.h" // monero/src +#include "error.h" namespace lwsf { namespace internal { @@ -46,11 +47,10 @@ namespace lwsf { namespace internal } } - subaddress_minor::subaddress_minor(internal::wallet* wal, std::shared_ptr data) - : wal_(wal), data_(std::move(data)), rows_() + subaddress_minor::subaddress_minor(internal::wallet* wal, const net::wallet_io& data) + : wal_(wal), data_(data), rows_() { - if (!wal_ || !data_) - throw std::invalid_argument{"lwsf::internal::subaddress_minor cannot be given nullptr"}; + LWSF_VERIFY(wal_ && data_.wallet); } subaddress_minor::~subaddress_minor() @@ -64,11 +64,11 @@ namespace lwsf { namespace internal { free_rows(rows_); - const boost::lock_guard lock{data_->sync}; - if (data_->primary.subaccounts.size() <= accountIndex) + const boost::lock_guard lock{data_.wallet->sync}; + if (data_.wallet->primary.subaccounts.size() <= accountIndex) return; - const auto& acct = data_->primary.subaccounts[accountIndex]; + const auto& acct = data_.wallet->primary.subaccounts[accountIndex]; static_assert(std::is_same()); rows_.reserve(std::size_t(acct.last) + 1); for (std::uint64_t i = 0; i <= acct.last; ++i) @@ -76,7 +76,7 @@ namespace lwsf { namespace internal rows_.push_back( new Monero::SubaddressRow{ 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)} } ); @@ -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) { { - const boost::lock_guard lock{data_->sync}; - auto& acct = data_->primary.subaccounts.at(accountIndex); + const boost::lock_guard lock{data_.wallet->sync}; + auto& acct = data_.wallet->primary.subaccounts.at(accountIndex); if (!addressIndex || !label.empty()) acct.detail.try_emplace(addressIndex).first->second.label = label; else diff --git a/src/subaddress_minor.h b/src/subaddress_minor.h index 1cd6c9c..e2330c0 100644 --- a/src/subaddress_minor.h +++ b/src/subaddress_minor.h @@ -31,22 +31,21 @@ #include #include #include +#include "net/wallet_io.h" #include "wallet.h" #include "wallet/api/wallet2_api.h" // monero/src namespace lwsf { namespace internal { - namespace backend { struct wallet; } - class subaddress_minor final : public ::Monero::Subaddress { internal::wallet* const wal_; - const std::shared_ptr data_; + const net::wallet_io data_; std::vector rows_; public: - explicit subaddress_minor(internal::wallet* wal, std::shared_ptr data); + explicit subaddress_minor(internal::wallet* wal, const net::wallet_io& data); subaddress_minor(const subaddress_minor&) = delete; subaddress_minor(subaddress_minor&&) = delete; diff --git a/src/transaction_history.cpp b/src/transaction_history.cpp index 7bbd590..bda349a 100644 --- a/src/transaction_history.cpp +++ b/src/transaction_history.cpp @@ -34,9 +34,10 @@ #include #include "backend.h" +#include "error.h" +#include "hex.h" // monero/contrib/epee/include #include "transaction_info.h" -#include "hex.h" // monero/contrib/epee/include namespace lwsf { namespace internal { @@ -50,11 +51,10 @@ namespace lwsf { namespace internal } } - transaction_history::transaction_history(std::shared_ptr data) - : data_(std::move(data)), txes_(), by_id_() + transaction_history::transaction_history(const net::wallet_io& data) + : data_(data), txes_(), by_id_() { - if (!data_) - throw std::invalid_argument{"lwsf::internal::transaction_history cannot be given nullptr"}; + LWSF_VERIFY(data_.wallet); } transaction_history::~transaction_history() @@ -76,12 +76,12 @@ namespace lwsf { namespace internal return nullptr; // hold the lock during the map lookup, this is const and should be thread-safe - const boost::lock_guard lock{data_->sync}; + const boost::lock_guard lock{data_.wallet->sync}; auto& info = by_id_[binary_id]; if (!info) { - const auto iter = data_->primary.txes.find(binary_id); - if (iter == data_->primary.txes.end()) + const auto iter = data_.wallet->primary.txes.find(binary_id); + if (iter == data_.wallet->primary.txes.end()) return nullptr; info = std::make_unique(data_, iter->second); } @@ -92,24 +92,24 @@ namespace lwsf { namespace internal { by_id_.clear(); - const boost::lock_guard lock{data_->sync}; - if (std::numeric_limits::max() < data_->primary.txes.size()) + const boost::lock_guard lock{data_.wallet->sync}; + if (std::numeric_limits::max() < data_.wallet->primary.txes.size()) throw std::runtime_error{"lwsf::internal::transaction_history::refresh exceeds max history size"}; // be careful about exceptions and memory leaks. this should be safe - for (std::size_t i = 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 destroy{txes_[i]}; txes_[i] = nullptr; } - txes_.resize(data_->primary.txes.size()); + txes_.resize(data_.wallet->primary.txes.size()); try { 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]) static_cast(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)) return; - const boost::lock_guard lock{data_->sync}; - auto iter = data_->primary.txes.find(binary_id); - if (iter != data_->primary.txes.end()) + const boost::lock_guard lock{data_.wallet->sync}; + auto iter = data_.wallet->primary.txes.find(binary_id); + if (iter != data_.wallet->primary.txes.end()) iter->second->description = note; } }} // lwsf // internal diff --git a/src/transaction_history.h b/src/transaction_history.h index 4963d73..443015b 100644 --- a/src/transaction_history.h +++ b/src/transaction_history.h @@ -32,6 +32,7 @@ #include #include #include "crypto/hash.h" // monero/src +#include "net/wallet_io.h" #include "wallet/api/wallet2_api.h" // monero/src namespace lwsf { namespace internal @@ -39,12 +40,12 @@ namespace lwsf { namespace internal namespace backend { class wallet; } class transaction_history final : public Monero::TransactionHistory { - const std::shared_ptr data_; + const net::wallet_io data_; std::vector txes_; mutable std::unordered_map> by_id_; public: - explicit transaction_history(std::shared_ptr data); + explicit transaction_history(const net::wallet_io& data); transaction_history(const transaction_history&) = delete; transaction_history(transaction_history&&) = delete; diff --git a/src/transaction_info.cpp b/src/transaction_info.cpp index 91b006c..8e6c05e 100644 --- a/src/transaction_info.cpp +++ b/src/transaction_info.cpp @@ -31,6 +31,7 @@ #include #include #include +#include "error.h" #include "hex.h" namespace @@ -57,11 +58,10 @@ namespace lwsf { namespace internal transfers_.emplace_back(out.amount, out.address); } - transaction_info::transaction_info(std::shared_ptr wallet, std::shared_ptr data) - : wallet_(std::move(wallet)), data_(std::move(data)), transfers_() + transaction_info::transaction_info(const net::wallet_io& io, std::shared_ptr data) + : io_(io), data_(std::move(data)), transfers_() { - if (!wallet_ || !data_) - throw std::invalid_argument{"lwsf::internal::transaction_info cannot be given nullptr"}; + LWSF_VERIFY(io_.wallet && data_); update_transfers(); } @@ -70,8 +70,7 @@ namespace lwsf { namespace internal void transaction_info::update(std::shared_ptr data) { - if (!data) - throw std::invalid_argument{"lwsf::internal::transaction_info::update cannot be given nullptr"}; + LWSF_VERIFY(data); data_ = std::move(data); update_transfers(); } @@ -87,7 +86,7 @@ namespace lwsf { namespace internal std::string transaction_info::description() const { - const boost::lock_guard lock{wallet_->sync}; + const boost::lock_guard lock{io_.wallet->sync}; 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 boost::lock_guard lock{wallet_->sync}; - if (meta.maj_i < wallet_->primary.subaccounts.size()) + const boost::lock_guard lock{io_.wallet->sync}; + 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); if (minor != major.detail.end()) return minor->second.label; @@ -152,8 +151,8 @@ namespace lwsf { namespace internal { if (!data_->height) return 0; - const boost::lock_guard lock{wallet_->sync}; - return std::max(wallet_->blockchain_height, *data_->height) - *data_->height + 1; + const boost::lock_guard lock{io_.wallet->sync}; + return std::max(io_.wallet->blockchain_height, *data_->height) - *data_->height + 1; } std::string transaction_info::hash() const diff --git a/src/transaction_info.h b/src/transaction_info.h index aff69d6..67d815f 100644 --- a/src/transaction_info.h +++ b/src/transaction_info.h @@ -33,6 +33,7 @@ #include #include "backend.h" +#include "net/wallet_io.h" #include "wallet/api/wallet2_api.h" // monero/src namespace lwsf { namespace internal @@ -40,7 +41,7 @@ namespace lwsf { namespace internal //! \todo isFailed, subaddressIndex, and subaddrAccount class transaction_info final : public Monero::TransactionInfo { - const std::shared_ptr wallet_; + const net::wallet_io io_; std::shared_ptr data_; std::vector transfers_; @@ -48,7 +49,7 @@ namespace lwsf { namespace internal public: - explicit transaction_info(std::shared_ptr wallet, std::shared_ptr data); + explicit transaction_info(const net::wallet_io& io, std::shared_ptr data); transaction_info(const transaction_info&) = delete; transaction_info(transaction_info&&) = delete; diff --git a/src/wallet.cpp b/src/wallet.cpp index ff40514..7c17110 100644 --- a/src/wallet.cpp +++ b/src/wallet.cpp @@ -38,6 +38,7 @@ #include #include +#include #include #include #include @@ -55,10 +56,9 @@ #include "hex.h" // monero/contrib/epee/include #include "lwsf_config.h" #include "mnemonics/electrum-words.h" // monero/src +#include "net/http.h" #include "net/net_parse_helpers.h" // monero/contrib/epee/include #include "net/parse.h" // monero/src -#include "net/socks.h" // monero/src -#include "net/socks_connect.h" // monero/src #include "numeric.h" #include "pending_transaction.h" #include "subaddress_account.h" @@ -85,12 +85,9 @@ namespace lwsf { namespace internal struct null_connector { - boost::unique_future - operator()(const std::string&, const std::string&, boost::asio::steady_timer&) const + void operator()(std::shared_ptr, std::function f) const { - return boost::make_exceptional_future( - std::runtime_error{"invalid proxy value"} - ); + f(boost::asio::error::connection_refused); } }; @@ -283,40 +280,82 @@ namespace lwsf { namespace internal } } // anonymous - bool wallet::set_error(const std::error_code error, const bool clear) const + wallet::frame::frame() + : exception_error_(), + error_(), + refresh_interval_(config::refresh_interval), + sync_(), + thread_state_(state::stop) + {} + + bool wallet::frame::set_error(const std::error_code error, const bool clear) { if (clear || error) { - const boost::lock_guard lock{error_sync_}; + const boost::lock_guard lock{sync_}; error_ = error; } return !error; } - void wallet::set_critical(const std::exception& e) const + void wallet::frame::async_error::operator()(const std::error_code error) const { - const boost::lock_guard lock{error_sync_}; - exception_error_ = e.what(); + LWSF_VERIFY(self); + self->set_error(error, false); } - template - void wallet::queue_work(F&& f) + bool wallet::set_error(const std::error_code error, const bool clear) const { + return status_->set_error(error, clear); + } + + void wallet::set_critical(const std::exception& e) const + { + + const boost::lock_guard lock{status_->sync_}; + status_->exception_error_ = e.what(); + } + + template + T wallet::wait_for(F&& f) const + { + try { - const boost::lock_guard lock{refresh_sync_}; - work_queue_.push_back(std::forward(f)); + return data_.context->wait_for(std::forward(f)); + } + catch (const std::exception& e) + { set_critical(e); throw; } + catch (...) + { set_critical(unknown_exception{}); throw; } + throw std::runtime_error{"unreachable"}; + } + + void wallet::check_worker_thread() + { + const boost::lock_guard lock{thread_sync_}; + bool run = false; + { + const boost::lock_guard lock2{status_->sync_}; + run = (status_->thread_state_ == state::stop); + } + + if (run) + { + data_.context->io_.stop(); + if (thread_.joinable()) + thread_.join(); + thread_ = boost::thread{[this] () { this->refresh_loop(); }}; } - startRefresh(); } void wallet::stop_refresh_loop() { const boost::lock_guard lock{thread_sync_}; { - const boost::lock_guard lock2{refresh_sync_}; - thread_state_ = state::stop; + const boost::lock_guard lock2{status_->sync_}; + status_->thread_state_ = state::stop; } - refresh_notify_.notify_all(); + data_.context->io_.stop(); if (thread_.joinable()) thread_.join(); } @@ -325,63 +364,107 @@ namespace lwsf { namespace internal { struct set_stop_ { - void operator()(state* val) const noexcept + void operator()(wallet* val) const { - if (val) - *val = state::stop; + if (!val) + return; + const boost::lock_guard lock{val->status_->sync_}; + val->status_->thread_state_ = state::stop; } }; - try + + struct on_refresh { - std::chrono::steady_clock::time_point last_refresh; - boost::unique_lock lock{refresh_sync_}; - const std::unique_ptr set_stop{std::addressof(thread_state_)}; - while (mandatory_refresh_ || thread_state_ != state::stop || !work_queue_.empty()) + std::weak_ptr timer_; + std::shared_ptr status_; + std::shared_ptr wallet_; + + explicit on_refresh(std::weak_ptr timer, std::shared_ptr in, std::shared_ptr in2) + : timer_(std::move(timer)), status_(std::move(in)), wallet_(std::move(in2)) + {} + + on_refresh(on_refresh&&) = default; + on_refresh(const on_refresh&) = default; + + void operator()(const std::error_code error) const { - while (!work_queue_.empty()) + LWSF_VERIFY(status_); + status_->set_error(error, true /* clear */); + const auto timer = timer_.lock(); + if (!timer) + return; // exiting refresh loop + + std::chrono::milliseconds interval; { - const std::function work{std::move(work_queue_.front())}; - work_queue_.pop_front(); - lock.unlock(); - if (work) - set_error(work()); - lock.lock(); + const boost::lock_guard lock{status_->sync_}; + interval = status_->refresh_interval_; } - - const bool mandatory_refresh = mandatory_refresh_; - mandatory_refresh_ = false; - - const auto now = std::chrono::steady_clock::now(); - if (mandatory_refresh_ || (refresh_interval_ <= now - last_refresh && thread_state_ == state::run)) - { - // refresh has strong exception guarantee - never in partial state. - lock.unlock(); - last_refresh = now; - set_error(data_->refresh(mandatory_refresh), true /*clear*/); - lock.lock(); - } - else if (thread_state_ == state::skip_once) - thread_state_ = state::run; - - // check while holding lock and before a wait call - if (thread_state_ == state::stop) + if (interval <= std::chrono::milliseconds{0}) return; - const auto last_state = thread_state_; - refresh_notify_.wait_for( - lock, to_boost(refresh_interval_), [this, last_state] () { - return mandatory_refresh_ || thread_state_ != last_state || !work_queue_.empty(); - }); + timer->expires_after(interval); + on_refresh copy{*this}; + timer->async_wait( + [copy = std::move(copy)] (const boost::system::error_code error) + { + if (error == boost::asio::error::operation_aborted) + return; + + LWSF_VERIFY(copy.status_); + boost::unique_lock lock{copy.status_->sync_}; + switch (copy.status_->thread_state_) + { + case state::stop: + break; + case state::skip_once: + copy.status_->thread_state_ = state::run; + /* fallthrough */ + default: + case state::paused: + { + std::error_code dummy; + { + const boost::lock_guard lock2{copy.wallet_->sync}; + dummy = copy.wallet_->refresh_error; + } + lock.unlock(); + copy(dummy); + } + break; + case state::run: + lock.unlock(); + backend::wallet::refresh(copy.wallet_, false /* mandatory */, copy); + break; + } + } + ); } + }; + + const std::unique_ptr set_stop{this}; + try + { + std::shared_ptr timer; + boost::unique_lock lock{status_->sync_}; + if (status_->thread_state_ != state::stop) + { + timer = std::make_shared(data_.context->io_); + backend::wallet::refresh(data_.wallet, false /* mandatory */, on_refresh{timer, status_, data_.wallet}); + } + do + { + lock.unlock(); + { + const std::shared_ptr restart_lock = data_.context->restart_asio(); + data_.context->io_.run(); + } + lock.lock(); + } while (status_->thread_state_ != state::stop); } catch (const std::exception& e) - { - set_critical(e); - } + { set_critical(e); } catch (...) - { - set_critical(unknown_exception{}); - } + { set_critical(unknown_exception{}); } } bool wallet::is_wallet_file(const std::string& path) @@ -423,48 +506,38 @@ namespace lwsf { namespace internal } wallet::wallet(error, std::string msg) - : data_(std::make_shared()), + : data_(), + status_(std::make_shared()), addressbook_(), history_(), subaddresses_(), subaddress_minor_(), filename_(), password_(), - work_queue_(), - exception_error_(std::move(msg)), - error_(), thread_(), iterations_(1), mixin_(config::mixin_default), - refresh_interval_(config::refresh_interval), refresh_notify_(), - error_sync_(), - refresh_sync_(), thread_sync_(), - thread_state_(state::stop), mandatory_refresh_(false) - {} + { + status_->exception_error_ = std::move(msg); + } wallet::wallet(create, Monero::NetworkType nettype, std::string filename, std::string password, const std::uint64_t kdf_rounds) - : data_(std::make_shared()), + : data_(), + status_(std::make_shared()), addressbook_(), history_(), subaddresses_(), subaddress_minor_(), filename_(std::move(filename)), password_(std::move(password)), - work_queue_(), - exception_error_(), - error_(), thread_(), iterations_(kdf_rounds), mixin_(config::mixin_default), - refresh_interval_(config::refresh_interval), refresh_notify_(), - error_sync_(), - refresh_sync_(), thread_sync_(), - thread_state_(state::stop), mandatory_refresh_(false) { if (sodium_init() < 0) @@ -474,29 +547,23 @@ namespace lwsf { namespace internal crypto::secret_key recovery; randombytes_buf(std::addressof(unwrap(unwrap(recovery))), sizeof(recovery)); - init_wallet(*data_, recovery, nettype, true); + init_wallet(*data_.wallet, recovery, nettype, true); } wallet::wallet(open, Monero::NetworkType nettype, std::string filename, std::string password, const std::uint64_t kdf_rounds) - : data_(std::make_shared()), + : data_(), + status_(std::make_shared()), addressbook_(), history_(), subaddresses_(), subaddress_minor_(), filename_(std::move(filename)), password_(std::move(password)), - work_queue_(), - exception_error_(), - error_(), thread_(), iterations_(kdf_rounds), mixin_(config::mixin_default), - refresh_interval_(config::refresh_interval), refresh_notify_(), - error_sync_(), - refresh_sync_(), thread_sync_(), - thread_state_(state::stop), mandatory_refresh_(false) { try @@ -508,75 +575,63 @@ namespace lwsf { namespace internal if (file.empty()) throw std::runtime_error{"Unable to open wallet file " + filename_}; - if (std::error_code error = data_->from_bytes(decrypt(std::move(file), epee::strspan(password_)).value())) + if (std::error_code error = data_.wallet->from_bytes(decrypt(std::move(file), epee::strspan(password_)).value())) throw std::system_error{error}; // lock not needed; data_ was created and unique to us - if (nettype != data_->primary.type) + if (nettype != data_.wallet->primary.type) throw std::runtime_error{"Wallet file NetworkType does not match requested"}; } catch (const std::exception& e) { - exception_error_ = e.what(); + status_->exception_error_ = e.what(); } } wallet::wallet(from_mnemonic, const Monero::NetworkType nettype, std::string filename, std::string password, const std::uint64_t kdf_rounds, const std::string& mnemonic, const std::string& seed_offset) - : data_(std::make_shared()), + : data_(), + status_(std::make_shared()), addressbook_(), history_(), subaddresses_(), subaddress_minor_(), filename_(std::move(filename)), password_(std::move(password)), - work_queue_(), - exception_error_(), - error_(), thread_(), iterations_(kdf_rounds), mixin_(config::mixin_default), - refresh_interval_(config::refresh_interval), refresh_notify_(), - error_sync_(), - refresh_sync_(), thread_sync_(), - thread_state_(state::stop), mandatory_refresh_(false) { crypto::secret_key recovery; std::string language; if (!crypto::ElectrumWords::words_to_bytes(mnemonic, recovery, language)) { - exception_error_ = "Electrum-style word list failed verification"; + status_->exception_error_ = "Electrum-style word list failed verification"; return; } if (!seed_offset.empty()) recovery = cryptonote::decrypt_key(recovery, seed_offset); - init_wallet(*data_, recovery, nettype, false); + init_wallet(*data_.wallet, recovery, nettype, false); } wallet::wallet(from_keys, const Monero::NetworkType nettype, std::string filename, std::string password, const std::uint64_t kdf_rounds, const boost::optional& view_key, const boost::optional& spend_key) - : data_(std::make_shared()), + : data_(), + status_(std::make_shared()), addressbook_(), history_(), subaddresses_(), subaddress_minor_(), filename_(std::move(filename)), password_(std::move(password)), - work_queue_(), - exception_error_(), - error_(), thread_(), iterations_(kdf_rounds), mixin_(config::mixin_default), - refresh_interval_(config::refresh_interval), refresh_notify_(), - error_sync_(), - refresh_sync_(), thread_sync_(), - thread_state_(state::stop), mandatory_refresh_(false) { if (sodium_init() < 0) @@ -584,34 +639,34 @@ namespace lwsf { namespace internal if (!view_key) { - exception_error_ = "view_key is invalid"; + status_->exception_error_ = "view_key is invalid"; return; } if (!spend_key) { - exception_error_ = "spend_key is invalid"; + status_->exception_error_ = "spend_key is invalid"; return; } - data_->primary.generated_locally = false; - data_->primary.type = nettype; - data_->primary.view.sec = *view_key; - data_->primary.spend.sec = *spend_key; + data_.wallet->primary.generated_locally = false; + data_.wallet->primary.type = nettype; + data_.wallet->primary.view.sec = *view_key; + data_.wallet->primary.spend.sec = *spend_key; - if (!crypto::secret_key_to_public_key(data_->primary.view.sec, data_->primary.view.pub)) + if (!crypto::secret_key_to_public_key(data_.wallet->primary.view.sec, data_.wallet->primary.view.pub)) { - exception_error_ = "view_pub could not be computed"; + status_->exception_error_ = "view_pub could not be computed"; return; } - if (!crypto::secret_key_to_public_key(data_->primary.spend.sec, data_->primary.spend.pub)) + if (!crypto::secret_key_to_public_key(data_.wallet->primary.spend.sec, data_.wallet->primary.spend.pub)) { - exception_error_ = "spend_pub could not be computed"; + status_->exception_error_ = "spend_pub could not be computed"; return; } - data_->primary.address = data_->get_spend_address({0, 0}); + data_.wallet->primary.address = data_.wallet->get_spend_address({0, 0}); } namespace @@ -628,29 +683,32 @@ namespace lwsf { namespace internal wallet::wallet(from_keys, const Monero::NetworkType nettype, std::string filename, std::string password, const std::uint64_t kdf_rounds, const std::string& address_string, const std::string& view_key, const std::string& spend_key) : wallet(from_keys{}, nettype, std::move(filename), std::move(password), kdf_rounds, to_secret_key(view_key), to_secret_key(spend_key)) { - if (data_->get_spend_address({0, 0}) != address_string) - exception_error_ = "view_key, spend_key, and address_string do not match"; + if (data_.wallet->get_spend_address({0, 0}) != address_string) + status_->exception_error_ = "view_key, spend_key, and address_string do not match"; } wallet::~wallet() { stop_refresh_loop(); } void wallet::add_subaddress(const std::uint32_t accountIndex, std::string label) { - const boost::lock_guard lock{data_->sync}; - auto& accts = data_->primary.subaccounts; + std::uint32_t min_i = 0; + { + const boost::lock_guard lock{data_.wallet->sync}; + auto& accts = data_.wallet->primary.subaccounts; - if (accts.size() <= accountIndex) - throw std::runtime_error{"add_subaddress: account does not exist"}; + if (accts.size() <= accountIndex) + throw std::runtime_error{"add_subaddress: account does not exist"}; - auto& acct = accts.at(accountIndex); - if (std::numeric_limits::max() <= acct.last) - throw std::runtime_error{"add_subaddress: exceeded minor indexes"}; + auto& acct = accts.at(accountIndex); + if (std::numeric_limits::max() <= acct.last) + throw std::runtime_error{"add_subaddress: exceeded minor indexes"}; - const std::uint32_t min_i = ++acct.last; - if (!label.empty()) - acct.detail.try_emplace(min_i).first->second.label = std::move(label); - - queue_work([=] () { return data_->register_subaddress(accountIndex, min_i); }); + min_i = ++acct.last; + if (!label.empty()) + acct.detail.try_emplace(min_i).first->second.label = std::move(label); + } + backend::wallet::register_subaddress(data_.wallet, accountIndex, min_i, frame::async_error{status_}); + check_worker_thread(); } std::string wallet::seed(const std::string& seed_offset) const @@ -658,9 +716,9 @@ namespace lwsf { namespace internal std::string language; crypto::secret_key key; { - const boost::lock_guard lock{data_->sync}; - language = data_->primary.language; - key = data_->primary.spend.sec; + const boost::lock_guard lock{data_.wallet->sync}; + language = data_.wallet->primary.language; + key = data_.wallet->primary.spend.sec; } if (language.empty()) @@ -680,14 +738,14 @@ namespace lwsf { namespace internal std::string wallet::getSeedLanguage() const { - const boost::lock_guard lock{data_->sync}; - return data_->primary.language; + const boost::lock_guard lock{data_.wallet->sync}; + return data_.wallet->primary.language; } void wallet::setSeedLanguage(const std::string &arg) { - const boost::lock_guard lock{data_->sync}; - data_->primary.language = arg; + const boost::lock_guard lock{data_.wallet->sync}; + data_.wallet->primary.language = arg; } int wallet::status() const @@ -708,16 +766,16 @@ namespace lwsf { namespace internal void wallet::statusWithErrorString(int& status, std::string& errorString) const { - const boost::lock_guard lock{error_sync_}; - if (!exception_error_.empty()) + const boost::lock_guard lock{status_->sync_}; + if (!status_->exception_error_.empty()) { status = Status_Critical; - errorString = exception_error_; + errorString = status_->exception_error_; } - else if (error_) + else if (status_->error_) { status = Status_Error; - errorString = error_.message(); + errorString = status_->error_.message(); } else { @@ -734,35 +792,35 @@ namespace lwsf { namespace internal std::string wallet::address(const std::uint32_t accountIndex, const std::uint32_t addressIndex) const { - return data_->get_spend_address({accountIndex, addressIndex}); + return data_.wallet->get_spend_address({accountIndex, addressIndex}); } - Monero::NetworkType wallet::nettype() const { return data_->primary.type; } + Monero::NetworkType wallet::nettype() const { return data_.wallet->primary.type; } std::string wallet::secretViewKey() const { - return epee::to_hex::string(epee::as_byte_span(unwrap(unwrap(data_->primary.view.sec)))); + return epee::to_hex::string(epee::as_byte_span(unwrap(unwrap(data_.wallet->primary.view.sec)))); } std::string wallet::publicViewKey() const { - return epee::to_hex::string(epee::as_byte_span(data_->primary.view.pub)); + return epee::to_hex::string(epee::as_byte_span(data_.wallet->primary.view.pub)); } std::string wallet::secretSpendKey() const { - return epee::to_hex::string(epee::as_byte_span(unwrap(unwrap(data_->primary.spend.sec)))); + return epee::to_hex::string(epee::as_byte_span(unwrap(unwrap(data_.wallet->primary.spend.sec)))); } std::string wallet::publicSpendKey() const { - return epee::to_hex::string(epee::as_byte_span(data_->primary.spend.pub)); + return epee::to_hex::string(epee::as_byte_span(data_.wallet->primary.spend.pub)); } void wallet::stop() { - const boost::lock_guard lock{refresh_sync_}; - thread_state_ = state::skip_once; + const boost::lock_guard lock{status_->sync_}; + status_->thread_state_ = state::skip_once; } bool wallet::store(const std::string& path) @@ -781,7 +839,7 @@ namespace lwsf { namespace internal // blocks until file and directory contents are synced epee::byte_slice blob = - encrypt(wallet_file_magic, data_->to_bytes().value(), iterations_, epee::strspan(password_)).value(); + encrypt(wallet_file_magic, data_.wallet->to_bytes().value(), iterations_, epee::strspan(password_)).value(); if (!atomic_file_write(new_file, directory, std::move(blob))) throw std::runtime_error{"Failed to write file " + real_path}; @@ -807,12 +865,11 @@ namespace lwsf { namespace internal if (!epee::net_utils::parse_url(daemon_address, url)) throw std::runtime_error{"Invalid LWS URL: " + daemon_address}; - if (!proxy_address.empty() && !setProxy(proxy_address)) + if (!setProxy(proxy_address)) return false; - boost::optional login; if (!daemon_username.empty() || !daemon_password.empty()) - login.emplace(daemon_username, daemon_password); + throw std::runtime_error{"HTTP loging not supported"}; // verify cert if `use_ssl == true`, otherwise autodetect if `https` // specified. @@ -851,10 +908,9 @@ namespace lwsf { namespace internal } } - const boost::unique_lock lock{data_->sync}; - data_->client->set_server(std::move(url.host), std::to_string(url.port), std::move(login), std::move(options)); - data_->passed_login = false; - data_->client_prefix = std::move(url.uri); + const boost::unique_lock lock{data_.wallet->sync}; + data_.wallet->passed_login = false; + data_.wallet->client.init(data_.context->io_, std::move(url.host), std::move(url.uri), boost::numeric_cast(url.port), std::move(options)); } catch (const std::exception& e) { @@ -867,62 +923,71 @@ namespace lwsf { namespace internal void wallet::setRefreshFromBlockHeight(const std::uint64_t height) { - const boost::lock_guard lock{data_->sync}; - data_->primary.requested_start = std::min(data_->primary.requested_start, height); - queue_work([this, height] () { return data_->restore_height(height).error(); }); + { + const boost::lock_guard lock{data_.wallet->sync}; + data_.wallet->primary.requested_start = std::min(data_.wallet->primary.requested_start, height); + } + backend::wallet::restore_height(data_.wallet, height, frame::async_error{status_}); + check_worker_thread(); } uint64_t wallet::getRefreshFromBlockHeight() const { - const boost::lock_guard lock{data_->sync}; - return data_->primary.restore_height; + const boost::lock_guard lock{data_.wallet->sync}; + return data_.wallet->primary.restore_height; } void wallet::setSubaddressLookahead(uint32_t major, uint32_t minor) { - queue_work([this, major, minor] () { return data_->set_lookahead(major, minor); }); + backend::wallet::set_lookahead(data_.wallet, major, minor, frame::async_error{status_}); + check_worker_thread(); } bool wallet::connectToDaemon() { - const bool connected = data_->client->is_connected(); - boost::unique_lock lock{data_->sync}; - if (connected && data_->passed_login) + boost::unique_lock lock{data_.wallet->sync}; + if (data_.wallet->passed_login && data_.wallet->client.is_connected()) return true; - if (connected || data_->client->connect(config::connect_timeout)) + lock.unlock(); + try { - lock.unlock(); - return set_error(data_->login()); + auto data = data_.wallet; + const std::error_code error = wait_for( + [data = std::move(data)] (auto&& f) { backend::wallet::login(data, std::move(f)); } + ); + if (error) + return false; } - return set_error(rpc::error::no_response); + catch (...) + { return false; } + + lock.lock(); + return data_.wallet->passed_login && data_.wallet->client.is_connected(); } Monero::Wallet::ConnectionStatus wallet::connected() const { - if (!data_->client->is_connected()) - return ConnectionStatus_Disconnected; - - const boost::lock_guard lock{data_->sync}; - return data_->passed_login ? + const boost::lock_guard lock{data_.wallet->sync}; + return data_.wallet->passed_login && data_.wallet->client.is_connected() ? ConnectionStatus_Connected : ConnectionStatus_Disconnected; } bool wallet::setProxy(const std::string &address) { - data_->client->disconnect(); if (address.empty()) - data_->client->set_connector(epee::net_utils::direct_connect{}); - else { - auto endpoint = net::get_tcp_endpoint(address); - if (!endpoint) - { - data_->client->set_connector(null_connector{}); - return set_error(endpoint.error()); - } - data_->client->set_connector(net::socks::connector{std::move(*endpoint)}); + data_.wallet->client.set_proxy(http::client::direct{}); + return true; } + + auto endpoint = ::net::get_tcp_endpoint(address); + if (!endpoint) + { + data_.wallet->client.set_proxy(null_connector{}); + return set_error(endpoint.error()); + } + data_.wallet->client.set_proxy(http::client::socks{*endpoint}); return true; } @@ -933,10 +998,10 @@ namespace lwsf { namespace internal uint64_t wallet::balance(const uint32_t accountIndex) const { - const boost::lock_guard lock{data_->sync}; + const boost::lock_guard lock{data_.wallet->sync}; std::uint64_t balance = 0; - for (const auto& tx : data_->primary.txes) + for (const auto& tx : data_.wallet->primary.txes) { if (tx.second->failed) continue; @@ -959,12 +1024,12 @@ namespace lwsf { namespace internal uint64_t wallet::unlockedBalance(const uint32_t accountIndex) const { - const boost::lock_guard lock{data_->sync}; - const Monero::NetworkType net_type = data_->primary.type; - const std::uint32_t chain_height = data_->blockchain_height; + const boost::lock_guard lock{data_.wallet->sync}; + const Monero::NetworkType net_type = data_.wallet->primary.type; + const std::uint32_t chain_height = data_.wallet->blockchain_height; std::uint64_t balance = 0; - for (const auto& tx : data_->primary.txes) + for (const auto& tx : data_.wallet->primary.txes) { if (tx.second->failed) continue; @@ -988,14 +1053,14 @@ namespace lwsf { namespace internal uint64_t wallet::blockChainHeight() const { - const boost::unique_lock lock{data_->sync}; - return data_->primary.scan_height; + const boost::unique_lock lock{data_.wallet->sync}; + return data_.wallet->primary.scan_height; } uint64_t wallet::daemonBlockChainHeight() const { - const boost::unique_lock lock{data_->sync}; - return data_->blockchain_height; + const boost::unique_lock lock{data_.wallet->sync}; + return data_.wallet->blockchain_height; } uint64_t wallet::daemonBlockChainTargetHeight() const @@ -1003,15 +1068,15 @@ namespace lwsf { namespace internal bool wallet::synchronized() const { - const boost::lock_guard lock{data_->sync}; - return data_->blockchain_height == data_->primary.scan_height; + const boost::lock_guard lock{data_.wallet->sync}; + return data_.wallet->blockchain_height == data_.wallet->primary.scan_height; } #ifdef LWSF_POLYSEED_ENABLE void wallet::setPolyseed(epee::byte_slice seed, std::string passphrase) { - const boost::lock_guard lock{data_->sync}; - data_->primary.poly = backend::account::polyseed{std::move(seed), std::move(passphrase)}; + const boost::lock_guard lock{data_.wallet->sync}; + data_.wallet->primary.poly = backend::account::polyseed{std::move(seed), std::move(passphrase)}; } bool wallet::getPolyseed(std::string &seed, std::string &passphrase) const @@ -1029,14 +1094,14 @@ namespace lwsf { namespace internal seed.clear(); passphrase.clear(); - const boost::lock_guard lock{data_->sync}; - if (!data_->primary.poly) + const boost::lock_guard lock{data_.wallet->sync}; + if (!data_.wallet->primary.poly) return false; - if (data_->primary.poly->seed.size() != sizeof(polyseed_storage)) + if (data_.wallet->primary.poly->seed.size() != sizeof(polyseed_storage)) return false; polyseed_data* temp = nullptr; - if (polyseed_load(data_->primary.poly->seed.data(), &temp) != POLYSEED_OK) + if (polyseed_load(data_.wallet->primary.poly->seed.data(), &temp) != POLYSEED_OK) return false; cleanup.reset(temp); @@ -1045,7 +1110,7 @@ namespace lwsf { namespace internal for (int i = 0; i < langs; ++i) { lang = polyseed_get_lang(i); - if (lang && polyseed_get_lang_name(lang) == data_->primary.language) + if (lang && polyseed_get_lang_name(lang) == data_.wallet->primary.language) break; } @@ -1054,7 +1119,7 @@ namespace lwsf { namespace internal seed.resize(POLYSEED_STR_SIZE); seed.resize(polyseed_encode(temp, lang, POLYSEED_MONERO, &seed[0])); - passphrase = data_->primary.poly->passphrase; + passphrase = data_.wallet->primary.poly->passphrase; return true; } #endif // LWSF_POLYSEED_ENABLE @@ -1062,25 +1127,19 @@ namespace lwsf { namespace internal void wallet::startRefresh() { const boost::lock_guard lock{thread_sync_}; - state old_state = state::stop; + bool run = false; { - const boost::lock_guard lock2{refresh_sync_}; - old_state = thread_state_; - const bool no_refresh = - refresh_interval_ <= std::chrono::milliseconds{0}; - if (no_refresh) + const boost::lock_guard lock2{status_->sync_}; + if (status_->thread_state_ == state::stop) { - thread_state_ = state::stop; - if (!mandatory_refresh_ && work_queue_.empty()) - return; + status_->thread_state_ = state::run; + run = true; } - else - thread_state_ = state::run; } - refresh_notify_.notify_all(); - if (old_state == state::stop) + if (run) { + data_.context->io_.stop(); if (thread_.joinable()) thread_.join(); thread_ = boost::thread{[this] () { this->refresh_loop(); }}; @@ -1089,25 +1148,28 @@ namespace lwsf { namespace internal void wallet::pauseRefresh() { - const boost::lock_guard lock{refresh_sync_}; - if (thread_state_ != state::stop) - thread_state_ = state::paused; + const boost::lock_guard lock{status_->sync_}; + if (status_->thread_state_ != state::stop) + status_->thread_state_ = state::paused; } bool wallet::refresh() { - try { return set_error(data_->refresh(true), true /* clear */); } - catch (const std::exception& e) { set_critical(e); } + try + { + const auto refresh = [this] (auto&& f) + { backend::wallet::refresh(this->data_.wallet, true, std::move(f)); }; + + return set_error(wait_for(refresh), true /* clear */); + } + catch (...) {} return false; } void wallet::refreshAsync() { - { - const boost::lock_guard lock{refresh_sync_}; - mandatory_refresh_ = true; - } - startRefresh(); + backend::wallet::refresh(data_.wallet, true /* mandatory */, frame::async_error{status_}); + check_worker_thread(); } void wallet::setAutoRefreshInterval(int millis) @@ -1115,11 +1177,11 @@ namespace lwsf { namespace internal using rep_type = std::chrono::milliseconds::rep; static_assert(std::numeric_limits::max() <= std::numeric_limits::max()); - boost::unique_lock lock{refresh_sync_}; - refresh_interval_ = std::chrono::milliseconds{millis}; + boost::unique_lock lock{status_->sync_}; + status_->refresh_interval_ = std::chrono::milliseconds{millis}; if (millis <= 0) { - thread_state_ = state::stop; + status_->thread_state_ = state::stop; lock.unlock(); stop_refresh_loop(); } @@ -1127,34 +1189,38 @@ namespace lwsf { namespace internal int wallet::autoRefreshInterval() const { - return refresh_interval_.count(); + return status_->refresh_interval_.count(); } void wallet::addSubaddressAccount(const std::string& label) { - const boost::lock_guard lock{data_->sync}; - auto& accts = data_->primary.subaccounts; + std::size_t index = 0; + { + const boost::lock_guard lock{data_.wallet->sync}; + auto& accts = data_.wallet->primary.subaccounts; - const std::size_t index = accts.size(); - if (std::numeric_limits::max() < index) - throw std::runtime_error{"addSubddressAccount exceeded subaddress indexes"}; + index = accts.size(); + if (std::numeric_limits::max() < index) + throw std::runtime_error{"addSubddressAccount exceeded subaddress indexes"}; - accts.emplace_back().detail.try_emplace(0).first->second.label = label; - queue_work([this, index] () { return data_->register_subaccount(index); }); + accts.emplace_back().detail.try_emplace(0).first->second.label = label; + } + backend::wallet::register_subaccount(data_.wallet, index, frame::async_error{status_}); + check_worker_thread(); } std::size_t wallet::numSubaddressAccounts() const { - const boost::lock_guard lock{data_->sync}; - return data_->primary.subaccounts.size(); + const boost::lock_guard lock{data_.wallet->sync}; + return data_.wallet->primary.subaccounts.size(); } std::size_t wallet::numSubaddresses(const std::uint32_t accountIndex) const { static_assert(std::numeric_limits::max() <= std::numeric_limits::max()); - const boost::lock_guard lock{data_->sync}; - if (accountIndex < data_->primary.subaccounts.size()) - return std::size_t(data_->primary.subaccounts.at(accountIndex).last) + 1; + const boost::lock_guard lock{data_.wallet->sync}; + if (accountIndex < data_.wallet->primary.subaccounts.size()) + return std::size_t(data_.wallet->primary.subaccounts.at(accountIndex).last) + 1; set_critical(std::runtime_error{"numSubaddresses failed, " + std::to_string(accountIndex) + " does not exist"}); return 0; } @@ -1162,9 +1228,9 @@ namespace lwsf { namespace internal std::string wallet::getSubaddressLabel(const std::uint32_t accountIndex, const std::uint32_t addressIndex) const { static_assert(std::numeric_limits::max() <= std::numeric_limits::max()); - const boost::lock_guard lock{data_->sync}; - if (accountIndex < data_->primary.subaccounts.size()) - return std::string{data_->primary.subaccounts.at(accountIndex).sub_label(addressIndex)}; + const boost::lock_guard lock{data_.wallet->sync}; + if (accountIndex < data_.wallet->primary.subaccounts.size()) + return std::string{data_.wallet->primary.subaccounts.at(accountIndex).sub_label(addressIndex)}; set_critical(std::runtime_error{"getSubaddressLabel failed, " + std::to_string(accountIndex) + "," + std::to_string(addressIndex) + " does not exist"}); return {}; } @@ -1172,10 +1238,10 @@ namespace lwsf { namespace internal void wallet::setSubaddressLabel(const std::uint32_t accountIndex, const std::uint32_t addressIndex, const std::string &label) { static_assert(std::numeric_limits::max() <= std::numeric_limits::max()); - const boost::lock_guard lock{data_->sync}; - if (accountIndex < data_->primary.subaccounts.size()) + const boost::lock_guard lock{data_.wallet->sync}; + if (accountIndex < data_.wallet->primary.subaccounts.size()) { - auto& acct = data_->primary.subaccounts[accountIndex]; + auto& acct = data_.wallet->primary.subaccounts[accountIndex]; if (addressIndex <= acct.last) { if (!addressIndex || !label.empty()) @@ -1241,34 +1307,36 @@ namespace lwsf { namespace internal cryptonote::account_public_address change_account{}; unspent_map unspent{}; { - data_->refresh(); // get latest outputs, block height, and fee info + wait_for( + [this] (auto&& callable) { backend::wallet::refresh(this->data_.wallet, false, std::move(callable)); } + ); + + const boost::lock_guard lock{data_.wallet->sync}; + fee_mask = data_.wallet->fee_mask; - const boost::lock_guard lock{data_->sync}; - fee_mask = data_->fee_mask; + if (data_.wallet->per_byte_fee.empty() || !fee_mask || !data_.wallet->passed_login) + throw std::system_error{data_.wallet->refresh_error, "Tx construction"}; - if (data_->per_byte_fee.empty() || !fee_mask || !data_->passed_login) - throw std::system_error{data_->refresh_error, "Tx construction"}; - - if (data_->per_byte_fee.size() <= priority_int) - per_byte_fee = data_->per_byte_fee.back(); + if (data_.wallet->per_byte_fee.size() <= priority_int) + per_byte_fee = data_.wallet->per_byte_fee.back(); else - per_byte_fee = data_->per_byte_fee[priority_int]; + per_byte_fee = data_.wallet->per_byte_fee[priority_int]; - mtype = data_->primary.type; - ctype = data_->get_net_type(); - change_address = data_->get_spend_address({subaddr_account, 0}); - keys = data_->get_primary_keys(); - change_account = data_->get_spend_account({subaddr_account, 0}); - const std::uint64_t height = data_->blockchain_height; + mtype = data_.wallet->primary.type; + ctype = data_.wallet->get_net_type(); + change_address = data_.wallet->get_spend_address({subaddr_account, 0}); + keys = data_.wallet->get_primary_keys(); + change_account = data_.wallet->get_spend_account({subaddr_account, 0}); + const std::uint64_t height = data_.wallet->blockchain_height; if (!amount && subaddr_indices.empty()) { - const std::uint32_t max = data_->primary.subaccounts.at(subaddr_account).last; + const std::uint32_t max = data_.wallet->primary.subaccounts.at(subaddr_account).last; for (std::uint64_t index = 0; index <= max; ++index) subaddr_indices.insert(std::uint32_t(index)); } - for (const auto& tx : data_->primary.txes) + for (const auto& tx : data_.wallet->primary.txes) { if (!tx.second->is_unlocked(height, mtype)) continue; // ignore locked amounts @@ -1287,7 +1355,7 @@ namespace lwsf { namespace internal } } - for (const auto& tx : data_->primary.txes) + for (const auto& tx : data_.wallet->primary.txes) { for (const auto& spend : tx.second->spends) unspent.erase(spend.second.output_pub); @@ -1438,7 +1506,10 @@ namespace lwsf { namespace internal for (const auto& spend : spending) req.amounts.push_back(rpc::uint64_string(rct_amount(spend.second->receives.at(spend.first)))); - auto resp = data_->get_decoys(req); + const auto resp = wait_for>( + [this, &req] (auto&& callable) + { backend::wallet::get_decoys(this->data_.wallet, std::move(req), std::move(callable)); } + ); if (!resp) throw std::system_error{resp.error()}; decoys = std::move(*resp); @@ -1463,8 +1534,8 @@ namespace lwsf { namespace internal { const auto& source = spend.second->receives.at(spend.first); { - const boost::lock_guard lock{data_->sync}; - subs.insert({data_->get_spend_public(source.recipient), {source.recipient.maj_i, source.recipient.min_i}}); + const boost::lock_guard lock{data_.wallet->sync}; + subs.insert({data_.wallet->get_spend_public(source.recipient), {source.recipient.maj_i, source.recipient.min_i}}); } auto& entry = sources.emplace_back(); @@ -1957,8 +2028,8 @@ namespace lwsf { namespace internal void wallet::setListener(Monero::WalletListener* listener) { { - const boost::lock_guard lock{data_->sync_listener}; - data_->listener = listener; + const boost::lock_guard lock{data_.wallet->sync_listener}; + data_.wallet->listener = listener; } if (listener) listener->onSetWallet(this); @@ -1976,16 +2047,16 @@ namespace lwsf { namespace internal bool wallet::setCacheAttribute(const std::string &key, const std::string &val) { - const boost::lock_guard lock{data_->sync}; - data_->primary.attributes.try_emplace(key).first->second = val; + const boost::lock_guard lock{data_.wallet->sync}; + data_.wallet->primary.attributes.try_emplace(key).first->second = val; return true; } std::string wallet::getCacheAttribute(const std::string &key) const { - const boost::lock_guard lock{data_->sync}; - const auto attribute = data_->primary.attributes.find(key); - if (attribute != data_->primary.attributes.end()) + const boost::lock_guard lock{data_.wallet->sync}; + const auto attribute = data_.wallet->primary.attributes.find(key); + if (attribute != data_.wallet->primary.attributes.end()) return attribute->second; return {}; } @@ -1996,9 +2067,9 @@ namespace lwsf { namespace internal if (!epee::string_tools::hex_to_pod(txid, binary_id)) return false; - const boost::lock_guard lock{data_->sync}; - auto iter = data_->primary.txes.find(binary_id); - if (iter == data_->primary.txes.end()) + const boost::lock_guard lock{data_.wallet->sync}; + auto iter = data_.wallet->primary.txes.find(binary_id); + if (iter == data_.wallet->primary.txes.end()) return false; iter->second->description = note; @@ -2014,9 +2085,9 @@ namespace lwsf { namespace internal return {}; } - const boost::lock_guard lock{data_->sync}; - auto iter = data_->primary.txes.find(binary_id); - if (iter == data_->primary.txes.end()) + const boost::lock_guard lock{data_.wallet->sync}; + auto iter = data_.wallet->primary.txes.find(binary_id); + if (iter == data_.wallet->primary.txes.end()) return {}; return iter->second->description; } @@ -2030,9 +2101,9 @@ namespace lwsf { namespace internal return {}; } - const boost::lock_guard lock{data_->sync}; - auto iter = data_->primary.txes.find(binary_id); - if (iter == data_->primary.txes.end()) + const boost::lock_guard lock{data_.wallet->sync}; + auto iter = data_.wallet->primary.txes.find(binary_id); + if (iter == data_.wallet->primary.txes.end()) { set_critical(std::runtime_error{"getTxKey tx not found"}); return {}; @@ -2050,12 +2121,15 @@ namespace lwsf { namespace internal { isNewWallet = false; { - const boost::lock_guard lock{data_->sync}; - if (data_->passed_login) + const boost::lock_guard lock{data_.wallet->sync}; + if (data_.wallet->passed_login) return true; } - const expect is_new = data_->login_is_new(); + const expect is_new = wait_for>( + [this] (auto&& f) { backend::wallet::login_is_new(this->data_.wallet, std::move(f)); } + ); + if (!is_new) return false; isNewWallet = *is_new; @@ -2064,7 +2138,15 @@ namespace lwsf { namespace internal bool wallet::lightWalletImportWalletRequest(std::string &payment_id, uint64_t &fee, bool &new_request, bool &request_fulfilled, std::string &payment_address, std::string &status) { - expect import = data_->restore_height(0); + std::uint64_t height = 0; + { + const boost::lock_guard lock{data_.wallet->sync}; + height = data_.wallet->primary.requested_start; + } + const auto import = wait_for>( + [this, height] (auto&& f) { backend::wallet::restore_height_raw(this->data_.wallet, height, std::move(f)); } + ); + if (!import) return false; fee = std::uint64_t(import->import_fee.value_or(rpc::uint64_string(0))); @@ -2075,5 +2157,5 @@ namespace lwsf { namespace internal return true; } #endif // LWSF_MASTER_ENABLE - + }} // lwsf // internal diff --git a/src/wallet.h b/src/wallet.h index eb9cbf5..05c6485 100644 --- a/src/wallet.h +++ b/src/wallet.h @@ -42,19 +42,39 @@ #include #include "byte_slice.h" // monero/contrib/epee/include #include "crypto/crypto.h" // monero/src +#include "net/wallet_io.h" #include "wallet/api/wallet2_api.h" // monero/src namespace lwsf { namespace internal { - namespace backend { struct wallet; } - class wallet final : public Monero::Wallet { enum class state : std::uint8_t { stop = 0, paused, skip_once, run }; - const std::shared_ptr 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 self; + void operator()(std::error_code error) const; + }; + + bool thead_start(); + }; + + const net::wallet_io data_; + const std::shared_ptr status_; std::unique_ptr addressbook_; std::unique_ptr history_; std::unique_ptr subaddresses_; @@ -63,26 +83,21 @@ namespace internal std::string password_; std::string language_; std::string ca_file_path_; - std::deque> work_queue_; - mutable std::string exception_error_; - mutable std::error_code error_; boost::thread thread_; const std::uint64_t iterations_; - std::chrono::milliseconds refresh_interval_; std::uint32_t mixin_; boost::condition_variable refresh_notify_; - mutable boost::mutex error_sync_; boost::mutex refresh_sync_; //!< Synchronizes with requests from other threads boost::mutex thread_sync_; //!< Syncrhonizes thread destruction+creation - state thread_state_; bool mandatory_refresh_; bool set_error(std::error_code status, bool clear = false) const; void set_critical(const std::exception& e) const; - template - void queue_work(F&& f); + template + T wait_for(F&& f) const; + void check_worker_thread(); void stop_refresh_loop(); void refresh_loop(); diff --git a/src/wallet_manager.cpp b/src/wallet_manager.cpp index 53dc20f..85fb50a 100644 --- a/src/wallet_manager.cpp +++ b/src/wallet_manager.cpp @@ -44,6 +44,7 @@ #ifdef LWSF_POLYSEED_ENABLE #include "polyseed.h" #endif +#include "net/context.h" #include "net/parse.h" // monero/src #include "net/socks_connect.h" // monero/src #include "QrCode.hpp" // monero/src/external @@ -57,27 +58,51 @@ namespace lwsf //! \TODO Mark final when completely implemented class wallet_manager : public Monero::WalletManager { - rpc::http_client client_; - std::string client_prefix_; + boost::asio::io_context io_; + http::client client_; std::string error_; rpc::daemon_status cached_; std::chrono::steady_clock::time_point cached_last_; + boost::mutex sync_; + + template + T wait_for(F f) + { + const boost::lock_guard lock{sync_}; + + net::to_future 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() { if (std::chrono::steady_clock::now() - cached_last_ > config::daemon_status_cache) { - const auto resp = rpc::invoke(client_, client_prefix_, rpc::empty{}); - if (!resp) - { - error_ = resp.error().message(); - cached_ = rpc::daemon_status{}; - } + rpc::daemon_status out{}; + const auto error = wait_for( + [this, &out] (auto&& f) { rpc::invoke_async(client_, rpc::empty{}, std::addressof(out), std::move(f)); } + ); + if (error) + error_ = error.message(); else - { error_.clear(); - cached_ = std::move(*resp); - } + cached_ = std::move(out); } return cached_; } @@ -85,7 +110,7 @@ namespace lwsf public: 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 */ @@ -410,8 +435,7 @@ namespace lwsf url.port = 80; } - client_prefix_ = std::move(url.uri); - client_.set_server(std::move(url.host), std::to_string(url.port), boost::none, std::move(options)); + client_.init(io_,std::move(url.host), std::move(url.uri), boost::numeric_cast(url.port), std::move(options)); } //! returns whether the daemon can be reached, and its version number @@ -465,13 +489,13 @@ namespace lwsf bool setProxy(const std::string &address) override { - auto endpoint = net::get_tcp_endpoint(address); + auto endpoint = ::net::get_tcp_endpoint(address); if (!endpoint) { error_ = endpoint.error().message(); return false; } - client_.set_connector(net::socks::connector{std::move(*endpoint)}); + client_.set_proxy(http::client::socks{std::move(*endpoint)}); return true; } }; diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 8db6f58..0f44fd2 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -27,11 +27,11 @@ # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 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) add_executable(lwsf-unit main.cpp backend.test.cpp rpc.test.cpp) -target_include_directories(lwsf-unit PRIVATE "${lwsf_SOURCE_DIR}/src") -target_link_libraries(lwsf-unit PRIVATE lwsf-api lwsf-unit-framework monero::libraries) +add_subdirectory(net) +target_link_libraries(lwsf-unit lwsf-api lwsf-unit-framework lwsf-unit-net monero::libraries) add_test(NAME lwsf-unit COMMAND lwsf-unit -v) diff --git a/tests/unit/backend.test.cpp b/tests/unit/backend.test.cpp index ef0698d..ec2d954 100644 --- a/tests/unit/backend.test.cpp +++ b/tests/unit/backend.test.cpp @@ -28,6 +28,242 @@ #include "framework.test.h" #include "backend.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "hex.h" // monero/contrib/include +#include "rapidjson/document.h" // monero/external/rapidjson/include +#include "rapidjson/stringbuffer.h" // monero/external/rapidjson/incldue +#include "rapidjson/writer.h" // monero/external/rapidjson/incldue +#include "ringct/rctOps.h" // monero/src + +namespace rapidjson +{ + std::ostream& operator<<(std::ostream& out, const Document& src) + { + StringBuffer buffer; + Writer writer{buffer}; + src.Accept(writer); + + return out << buffer.GetString(); + } +} + +namespace +{ + using client_request = + boost::beast::http::request; + + template + std::string to_hex(const T& arg) + { + return epee::to_hex::string(epee::as_byte_span(arg)); + } + + template + T from_hex(const boost::string_ref hex) + { + T out{}; + if (!epee::from_hex::to_buffer(epee::as_mut_byte_span(out), hex)) + throw std::runtime_error{"invalid hex"}; + return out; + } + + struct empty_slice + { + operator epee::byte_slice() const { return {}; } + }; + + struct empty_decoys + { + operator lwsf::internal::rpc::get_random_outs_request() const { return {}; } + }; + + struct response_loop : boost::asio::coroutine + { + struct frame + { + lest::env& lest_env; + boost::asio::ip::tcp::socket server; + boost::beast::flat_buffer buffer; + boost::optional request; + boost::beast::http::response response; + std::deque> requests; + std::deque> responses; + + explicit frame(lest::env& lest_env, boost::asio::io_context& io) + : lest_env(lest_env), server(io), buffer(), request(), response(), requests(), responses() + {} + }; + + std::shared_ptr self; + + explicit response_loop(std::shared_ptr in) + : boost::asio::coroutine(), self(in) + {} + + void operator()(boost::system::error_code = {}, std::size_t = {}) + { + auto& lest_env = self->lest_env; + LWSF_VERIFY(self); + BOOST_ASIO_CORO_REENTER(*this) + { + while (!self->responses.empty()) + { + self->request.emplace(); + BOOST_ASIO_CORO_YIELD boost::beast::http::async_read(self->server, self->buffer, *self->request, *this); + + EXPECT(self->request->keep_alive()); + EXPECT(self->requests.size() == self->responses.size()); + EXPECT(self->requests.front().first == self->request->target()); + if (!self->requests.front().second.empty()) + { + rapidjson::Document expected; + expected.Parse(self->requests.front().second.c_str()); + EXPECT(!expected.HasParseError()); + + rapidjson::Document actual; + actual.Parse(self->request->body().c_str()); + EXPECT(!expected.HasParseError()); + + EXPECT(expected == actual); + } + self->requests.pop_front(); + + { + LWSF_VERIFY(!self->responses.empty()); + const auto string = boost::get(std::addressof(self->responses.front())); + if (string) + self->response = {boost::beast::http::status::ok, 11, *string}; + else + self->response = {boost::get(self->responses.front()), 11, ""}; + } + self->response.keep_alive(true); + self->response.prepare_payload(); + + BOOST_ASIO_CORO_YIELD boost::beast::http::async_write(self->server, self->response, *this); + self->responses.pop_front(); + } + } + } + }; + + std::string get_response(const boost::string_view target) + { + static constexpr const char get_address_txs[] = + R"({ + "total_received": "5000", + "scanned_height": 150, + "scanned_block_height": 150, + "start_height": 10, + "blockchain_height": 200, + "transactions": [] + })"; + + static constexpr const char get_unspent_outs[] = + R"({"per_byte_fee": 5, "fee_mask": 100, "amount": "200", "outputs": []})"; + static constexpr const char import_wallet_request[] = + R"({"lookahead": {"maj_i": 50, "min_i": 200}, "status": "OK", "new_request": true, "request_fulfilled": true})"; + + static constexpr const std::array, 10> endpoints = + {{ + {"/get_address_txs", get_address_txs}, + {"/get_random_outs", R"({"amount_outs": []})"}, + {"/get_subaddrs", R"({"all_subaddrs": []})"}, + {"/get_unspent_outs", get_unspent_outs}, + {"/get_version", R"({"max_subaddresses": 10000})"}, + {"/import_wallet_request", import_wallet_request}, + {"/login", R"({"new_address": true})"}, + {"/provision_subaddrs", R"({"new_subaddrs": [], "all_subaddrs": []})"}, + {"/submit_raw_tx", R"({"status": "OK"})"}, + {"/upsert_subaddrs", "{}"} + }}; + + const auto found = + std::lower_bound(endpoints.begin(), endpoints.end(), std::make_pair(target, boost::string_view{})); + if (found != endpoints.end() && found->first == target) + return std::string{found->second}; + + throw std::runtime_error{"Unexpected target: " + std::string{target}}; + } + + struct dummy_server : boost::asio::coroutine + { + struct frame + { + boost::asio::ip::tcp::socket server; + boost::beast::flat_buffer buffer; + boost::optional request; + boost::beast::http::response response; + std::mt19937 eng; + std::uniform_int_distribution dist; + + explicit frame(boost::asio::io_context& io) + : server(io), buffer(), request(), response(), eng(crypto::rand()), dist(0, 99) + {} + }; + + std::shared_ptr self; + + explicit dummy_server(std::shared_ptr self) + : boost::asio::coroutine(), self(std::move(self)) + {} + + void operator()(const boost::system::error_code error, std::size_t = {}) + { + if (error) + { + if (error != boost::asio::error::operation_aborted) + throw boost::system::system_error{error, "server failure"}; + return; + } + + LWSF_VERIFY(self); + BOOST_ASIO_CORO_REENTER(*this) + { + while (true) + { + self->request.emplace(); + BOOST_ASIO_CORO_YIELD boost::beast::http::async_read(self->server, self->buffer, *self->request, *this); + + LWSF_VERIFY(self->request->keep_alive()); + if (self->dist(self->eng)) + self->response = {boost::beast::http::status::ok, 11, get_response(self->request->target())}; + else + self->response = {boost::beast::http::status::internal_server_error, 11, ""}; + self->response.keep_alive(true); + self->response.prepare_payload(); + + BOOST_ASIO_CORO_YIELD boost::beast::http::async_write(self->server, self->response, *this); + } + } + } + }; +} + LWS_CASE("backend::acount") { using subaddrs = lwsf::internal::rpc::subaddrs; @@ -71,3 +307,986 @@ LWS_CASE("backend::acount") } } +LWS_CASE("backend::wallet") +{ + using bwallet = lwsf::internal::backend::wallet; + using tcp = boost::asio::ip::tcp; + const auto no_ssl = epee::net_utils::ssl_support_t::e_ssl_support_disabled; + + SETUP("base empty wallet and basic server") + { + boost::asio::io_context io; + const auto wallet = std::make_shared(io); + crypto::generate_keys(wallet->primary.view.pub, wallet->primary.view.sec); + crypto::generate_keys(wallet->primary.spend.pub, wallet->primary.spend.sec); + wallet->primary.address = wallet->get_spend_address({0, 0}); + + tcp::acceptor acceptor{io, tcp::endpoint(tcp::v4(), 0)}; + acceptor.listen(); + + SECTION("uninitialized http client") + { + using namespace std::placeholders; + const std::array)>, std::string>, 5> funcs1 + {{ + {std::bind(bwallet::refresh, wallet, true, _1), "refresh"}, + {std::bind(bwallet::register_subaccount, wallet, 0, _1), "register_subaccount"}, + {std::bind(bwallet::register_subaddress, wallet, 0, 0, _1), "register_subaddress"}, + {std::bind(bwallet::set_lookahead, wallet, 0, 0, _1), "set_lookahead"}, + {std::bind(bwallet::send_tx, wallet, empty_slice{}, _1), "send_tx"} + }}; + + for (const auto& func : funcs1) + { + std::error_code actual{}; + func.first([&] (auto result) { actual = result; }); + + io.restart(); + io.run(); + SECTION(func.second) + EXPECT(actual == common_error::kInvalidArgument); + } + + std::error_code actual = {}; + const auto func = [&](auto result) { actual = result.error(); }; + + bwallet::login_is_new(wallet, func); + io.restart(); + io.run(); + EXPECT(actual == common_error::kInvalidArgument); + + bwallet::restore_height_raw(wallet, 0, func); + io.restart(); + io.run(); + EXPECT(actual == common_error::kInvalidArgument); + + actual = {}; + bwallet::get_decoys(wallet, lwsf::internal::rpc::get_random_outs_request{}, func); + io.restart(); + io.run(); + EXPECT(actual == common_error::kInvalidArgument); + } + + SECTION("login_is_new") + { + const auto frame = std::make_shared(lest_env, io); + wallet->client.init(io, "127.0.0.1", "", acceptor.local_endpoint().port(), no_ssl); + + bool done = false; + expect result = false; + const auto record_response = [&] (expect val) { done = true; result = val; }; + bwallet::login_is_new(wallet, record_response); + + wallet->passed_login = false; + acceptor.async_accept(frame->server, response_loop{frame}); + + SECTION("new address -> true") + { + frame->requests = {{"/login", ""}, {"/upsert_subaddrs", ""}}; + frame->responses = {R"({"new_address": true})", ""}; + + while (!done) + { + io.restart(); + io.run_one(); + } + + EXPECT(frame->responses.size() == 0); + EXPECT(result); + EXPECT(*result); + EXPECT(wallet->passed_login); + EXPECT(wallet->primary.restore_height == 0); + EXPECT(wallet->server_lookahead.major == 0); + EXPECT(wallet->server_lookahead.minor == 0); + } + + SECTION("new address -> true with queued") + { + frame->requests = {{"/login", ""}, {"/upsert_subaddrs", ""}}; + frame->responses = {R"({"new_address": true})", ""}; + + bool done2 = true; + expect result2 = false; + bwallet::login_is_new(wallet, [&](auto val) { done2 = true; result2 = val; }); + while (!done || !done2) + { + io.restart(); + io.run_one(); + } + + EXPECT(frame->responses.size() == 0); + EXPECT(result); + EXPECT(*result); + EXPECT(result2); + EXPECT(*result2); + EXPECT(wallet->passed_login); + EXPECT(wallet->primary.restore_height == 0); + EXPECT(wallet->server_lookahead.major == 0); + EXPECT(wallet->server_lookahead.minor == 0); + } + + SECTION("new address -> false + start_height") + { + frame->requests = {{"/login", ""}, {"/upsert_subaddrs", ""}}; + frame->responses = {R"({"new_address": false, "start_height": 100})", ""}; + + while (!done) + { + io.restart(); + io.run_one(); + } + + EXPECT(frame->responses.size() == 0); + EXPECT(result); + EXPECT(!*result); + EXPECT(wallet->passed_login); + EXPECT(wallet->primary.restore_height == 100); + EXPECT(wallet->server_lookahead.major == 0); + EXPECT(wallet->server_lookahead.minor == 0); + + wallet->primary.restore_height = 0; + } + + SECTION("new address -> true with lookahead") + { + frame->requests = {{"/login", ""}}; + frame->responses = { + R"({"new_address": true, "lookahead": {"maj_i": 100, "min_i": 50}})" + }; + + while (!done) + { + io.restart(); + io.run_one(); + } + + EXPECT(frame->responses.size() == 0); + EXPECT(result); + EXPECT(*result); + EXPECT(wallet->passed_login); + EXPECT(wallet->primary.restore_height == 0); + EXPECT(wallet->server_lookahead.major == 100); + EXPECT(wallet->server_lookahead.minor == 50); + } + + SECTION("address is disabled") + { + frame->requests = {{"/login", ""}}; + frame->responses = {boost::beast::http::status::forbidden}; + + while (!done) + { + io.restart(); + io.run_one(); + } + + EXPECT(frame->responses.size() == 0); + EXPECT(result == lwsf::error::approval); + EXPECT(!wallet->passed_login); + + } + + SECTION("404 (bad server entirely)") + { + frame->requests = {{"/login", ""}, {"/login", ""}}; + frame->responses = { + boost::beast::http::status::not_found, + boost::beast::http::status::not_found + }; + + while (!done) + { + io.restart(); + io.run_one(); + } + + EXPECT(frame->responses.size() == 0); + EXPECT(!result); + EXPECT(result == lwsf::internal::http::error(404)); + EXPECT(!wallet->passed_login); + } + } + + SECTION("refresh") + { + const auto frame = std::make_shared(lest_env, io); + wallet->client.init(io, "127.0.0.1", "", acceptor.local_endpoint().port(), no_ssl); + + bool done = false; + std::error_code result{}; + const auto record_response = [&] (auto val) { done = true; result = val; }; + bwallet::refresh(wallet, true, record_response); + + wallet->passed_login = false; + acceptor.async_accept(frame->server, response_loop{frame}); + + SECTION("account is disabled") + { + frame->requests = {{"/login", ""}}; + frame->responses = {boost::beast::http::status::forbidden}; + + while (!done) + { + io.restart(); + io.run_one(); + } + + EXPECT(frame->responses.size() == 0); + EXPECT(result == lwsf::error::approval); + EXPECT(!wallet->passed_login); + EXPECT(wallet->refresh_error == lwsf::error::approval); + } + + SECTION("get_address_txs unexpectedly fails with queued") + { + frame->requests = { + {"/login", ""}, + {"/upsert_subaddrs", ""}, + {"/get_address_txs", ""} + }; + frame->responses = { + R"({"new_address": true})", "", boost::beast::http::status::forbidden + }; + + bool done2 = false; + std::error_code result2{}; + bwallet::refresh(wallet, true, [&] (auto val) { done2 = true; result2 = val; }); + + while (!done || !done2) + { + io.restart(); + io.run_one(); + } + + EXPECT(frame->responses.size() == 0); + EXPECT(result == lwsf::error::approval); + EXPECT(result2 == lwsf::error::approval); + EXPECT(!wallet->passed_login); + EXPECT(wallet->refresh_error == lwsf::error::approval); + } + + SECTION("0.3 server") + { + frame->requests = { + {"/login", ""}, + {"/upsert_subaddrs", ""}, + {"/get_address_txs", ""}, + {"/get_unspent_outs", ""}, + {"/get_version", ""} + }; + frame->responses = { + R"({"new_address": true})", + "", + R"({ + "total_received": "5000", + "scanned_height": 150, + "scanned_block_height": 150, + "start_height": 10, + "blockchain_height": 200, + "transactions": [{ + "id": 1, + "hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1", + "total_received": "5000", + "total_sent": "10000", + "unlock_time": 0, + "spent_outputs": [{ + "amount": "10000", + "key_image": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee2", + "tx_pub_key": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee3", + "out_index": 0, + "mixin": 15 + }], + "coinbase": false, + "mempool": true, + "mixin": 15 + }] + })", + R"({ + "per_byte_fee": 5, + "fee_mask": 100, + "amount": "200", + "outputs": [{ + "amount": "5000", + "index": 5, + "global_index": "50000", + "rct": + "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee4", + "tx_hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1", + "tx_prefix_hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee5", + "public_key": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee6", + "tx_pub_key": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee3", + "spend_key_images": ["deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee2"], + "timestamp": "2026-01-01T00:00:00Z", + "height": 0 + }] + })", + boost::beast::http::status::not_found + }; + + while (!done) + { + io.restart(); + io.run_one(); + } + + const lwsf::internal::rpc::address_meta recipient{0, 0}; + + EXPECT(frame->responses.size() == 0); + EXPECT(result == lwsf::error::subaddr_upgrade); + EXPECT(wallet->passed_login); + EXPECT(wallet->refresh_error == lwsf::error::subaddr_upgrade); + EXPECT(wallet->lookahead_error == lwsf::error::subaddr_upgrade); + EXPECT(wallet->primary.restore_height == 10); + EXPECT(wallet->primary.scan_height == 150); + EXPECT(wallet->primary.requested_start == 10); + EXPECT(wallet->blockchain_height == 200); + EXPECT(wallet->fee_mask == 100); + EXPECT(wallet->per_byte_fee.size() == 1); + EXPECT(wallet->per_byte_fee.at(0) == 5); + EXPECT(wallet->primary.txes.size() == 1); + auto tx = wallet->primary.txes.find(from_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1")); + EXPECT(tx != wallet->primary.txes.end()); + EXPECT(tx->second != nullptr); + EXPECT(tx->second->spends.size() == 0); + EXPECT(tx->second->receives.size() == 1); + EXPECT(tx->second->receives.nth(0)->first == from_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee6")); + EXPECT(tx->second->receives.nth(0)->second.tx_pub == from_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee3")); + EXPECT(tx->second->receives.nth(0)->second.amount == 5000); + EXPECT(tx->second->receives.nth(0)->second.recipient == recipient); + EXPECT(tx->second->receives.nth(0)->second.index == 5); + EXPECT(tx->second->receives.nth(0)->second.global_index == 50000); + EXPECT(bool(tx->second->receives.nth(0)->second.rct_mask)); + EXPECT(wallet->primary.subaccounts.size() == 1); + EXPECT(wallet->primary.subaccounts.at(0).last == 0); + } + + SECTION("1.0 server with good lookahead") + { + frame->requests = { + {"/login", ""}, + {"/get_address_txs", ""}, + {"/get_unspent_outs", ""}, + }; + frame->responses = { + R"({"new_address": true, "lookahead": {"maj_i": 50, "min_i": 200}})", + R"({ + "total_received": "4000", + "scanned_height": 175, + "scanned_block_height": 175, + "start_height": 10, + "blockchain_height": 210, + "lookahead": {"maj_i": 50, "min_i": 200}, + "transactions": [{ + "id": 1, + "hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1", + "total_received": "4000", + "total_sent": "10000", + "unlock_time": 0, + "spent_outputs": [{ + "amount": "10000", + "key_image": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee2", + "tx_pub_key": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee3", + "out_index": 0, + "mixin": 15 + }], + "coinbase": false, + "mempool": true, + "mixin": 15 + }] + })", + R"({ + "per_byte_fee": 5, + "fee_mask": 100, + "fees": [110, 500, 700], + "amount": "4000", + "outputs": [{ + "amount": "4000", + "index": 0, + "global_index": "60000", + "rct": + "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee4", + "tx_hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1", + "tx_prefix_hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee5", + "public_key": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee6", + "tx_pub_key": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee3", + "spend_key_images": ["deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee2"], + "timestamp": "2026-01-01T00:00:00Z", + "height": 0, + "recipient": {"maj_i": 1, "min_i": 2} + }] + })" + }; + + while (!done) + { + io.restart(); + io.run_one(); + } + + const lwsf::internal::rpc::address_meta recipient{1, 2}; + + EXPECT(frame->responses.size() == 0); + EXPECT(result == std::error_code{}); + EXPECT(wallet->passed_login); + EXPECT(wallet->refresh_error == std::error_code{}); + EXPECT(wallet->lookahead_error == std::error_code{}); + EXPECT(wallet->subaddress_error == std::error_code{}); + EXPECT(wallet->import_error == std::error_code{}); + EXPECT(wallet->primary.restore_height == 10); + EXPECT(wallet->primary.scan_height == 175); + EXPECT(wallet->primary.requested_start == 10); + EXPECT(wallet->blockchain_height == 210); + EXPECT(wallet->fee_mask == 100); + EXPECT(wallet->per_byte_fee.size() == 3); + EXPECT(wallet->per_byte_fee.at(0) == 110); + EXPECT(wallet->per_byte_fee.at(1) == 500); + EXPECT(wallet->per_byte_fee.at(2) == 700); + EXPECT(wallet->primary.txes.size() == 1); + auto tx = wallet->primary.txes.find(from_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1")); + EXPECT(tx != wallet->primary.txes.end()); + EXPECT(tx->second != nullptr); + EXPECT(tx->second->spends.size() == 0); + EXPECT(tx->second->receives.size() == 1); + EXPECT(tx->second->receives.nth(0)->first == from_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee6")); + EXPECT(tx->second->receives.nth(0)->second.tx_pub == from_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee3")); + EXPECT(tx->second->receives.nth(0)->second.amount == 4000); + EXPECT(tx->second->receives.nth(0)->second.recipient == recipient); + EXPECT(tx->second->receives.nth(0)->second.index == 0); + EXPECT(tx->second->receives.nth(0)->second.global_index == 60000); + EXPECT(bool(tx->second->receives.nth(0)->second.rct_mask)); + EXPECT(wallet->primary.subaccounts.size() == 2); + EXPECT(wallet->primary.subaccounts.at(0).last == 0); + EXPECT(wallet->primary.subaccounts.at(1).last == 2); + } + + SECTION("1.0 server with lookahead fail and limited max subaddresses") + { + std::string subs; + for (unsigned i = 0; i < 50; ++i) + subs += R"({"key":)" + std::to_string(i) + R"(, "value": [[0, 199]]},)"; + subs.pop_back(); + + const std::string upsert_body = + R"({"address":")" + wallet->get_spend_address({0, 0}) + + R"(","view_key":")" + to_hex(unwrap(unwrap(wallet->primary.view.sec))) + + R"(","subaddrs":[{"key":1,"value":[[0,2]]}],"get_all":false})"; + + rct::key ringct{}; + crypto::key_image image{}; + lwsf::internal::backend::keypair tx_key{}; + lwsf::internal::backend::keypair output_key{}; + crypto::generate_keys(tx_key.pub, tx_key.sec); + { + crypto::secret_key scalar{}; + crypto::key_derivation derived{}; + EXPECT(crypto::generate_key_derivation(tx_key.pub, wallet->primary.view.sec, derived)); + EXPECT(crypto::derive_public_key(derived, 3, wallet->primary.spend.pub, output_key.pub)); + crypto::derive_secret_key(derived, 3, wallet->primary.spend.sec, output_key.sec); + crypto::generate_key_image(output_key.pub, output_key.sec, image); + + crypto::derivation_to_scalar(derived, 3, scalar); + + rct::ecdhTuple commitment{}; + rct::ecdhDecode(commitment, rct::sk2rct(scalar), true); + ringct = commitment.mask; + } + + EXPECT(wallet->primary.subaccounts.size() == 2); // from last section + frame->requests = { + {"/login", ""}, + {"/upsert_subaddrs", upsert_body}, + {"/get_address_txs", ""}, + {"/get_unspent_outs", ""}, + {"/get_version", ""}, + {"/get_subaddrs", ""} + }; + frame->responses = { + R"({"new_address": true, "lookahead": {"maj_i": 50, "min_i": 200}})", + "", + R"({ + "total_received": "4000", + "scanned_height": 175, + "scanned_block_height": 175, + "start_height": 5, + "blockchain_height": 210, + "lookahead": {"maj_i": 50, "min_i": 200}, + "lookahead_fail": 150, + "transactions": [{ + "id": 1, + "hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1", + "total_received": "4000", + "total_sent": "10000", + "unlock_time": 0, + "spent_outputs": [{ + "amount": "10000", + "key_image": ")" + to_hex(image) + R"(", + "tx_pub_key": ")" + to_hex(tx_key.pub) + R"(", + "out_index": 3, + "mixin": 15 + }], + "coinbase": false, + "mempool": true, + "mixin": 15 + }] + })", + R"({ + "per_byte_fee": 5, + "fee_mask": 100, + "fees": [110, 500, 700], + "amount": "4000", + "lookahead_fail": 150, + "outputs": [{ + "amount": "4000", + "index": 3, + "global_index": "60000", + "rct": "recomputed", + "tx_hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1", + "tx_prefix_hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee5", + "public_key": ")" + to_hex(output_key.pub) + R"(", + "tx_pub_key": ")" + to_hex(tx_key.pub) + R"(", + "spend_key_images": [")" + to_hex(image) + R"("], + "timestamp": "2026-01-01T00:00:00Z", + "height": 150, + "recipient": {"maj_i": 1, "min_i": 2} + }] + })", + R"({"max_subaddresses": 10000})", + R"({"all_subaddrs": [)" + subs + "]}" + }; + + while (!done) + { + io.restart(); + io.run_one(); + } + + const lwsf::internal::rpc::address_meta sender{0, 0}; + const lwsf::internal::rpc::address_meta recipient{1, 2}; + + EXPECT(frame->responses.size() == 0); + EXPECT(result == lwsf::error::subaddr_ahead); + EXPECT(wallet->passed_login); + EXPECT(wallet->refresh_error == lwsf::error::subaddr_ahead); + EXPECT(wallet->lookahead_error == lwsf::error::subaddr_ahead); + EXPECT(wallet->subaddress_error == std::error_code{}); + EXPECT(wallet->import_error == std::error_code{}); + EXPECT(wallet->primary.restore_height == 5); + EXPECT(wallet->primary.scan_height == 175); + EXPECT(wallet->primary.requested_start == 5); + EXPECT(wallet->blockchain_height == 210); + EXPECT(wallet->fee_mask == 100); + EXPECT(wallet->per_byte_fee.size() == 3); + EXPECT(wallet->per_byte_fee.at(0) == 110); + EXPECT(wallet->per_byte_fee.at(1) == 500); + EXPECT(wallet->per_byte_fee.at(2) == 700); + EXPECT(wallet->primary.txes.size() == 1); + auto tx = wallet->primary.txes.find(from_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1")); + EXPECT(tx != wallet->primary.txes.end()); + EXPECT(tx->second != nullptr); + EXPECT(tx->second->spends.size() == 1); + EXPECT(tx->second->spends.nth(0)->first == image); + EXPECT(tx->second->spends.nth(0)->second.amount == 10000); + EXPECT(tx->second->spends.nth(0)->second.sender == sender); + EXPECT(tx->second->spends.nth(0)->second.tx_pub == tx_key.pub); + EXPECT(tx->second->spends.nth(0)->second.output_pub == output_key.pub); + EXPECT(tx->second->receives.size() == 1); + EXPECT(tx->second->receives.nth(0)->first == output_key.pub); + EXPECT(tx->second->receives.nth(0)->second.tx_pub == tx_key.pub); + EXPECT(tx->second->receives.nth(0)->second.amount == 4000); + EXPECT(tx->second->receives.nth(0)->second.recipient == recipient); + EXPECT(tx->second->receives.nth(0)->second.index == 3); + EXPECT(tx->second->receives.nth(0)->second.global_index == 60000); + EXPECT(bool(tx->second->receives.nth(0)->second.rct_mask)); + EXPECT(*tx->second->receives.nth(0)->second.rct_mask == ringct); + EXPECT(wallet->primary.subaccounts.size() == 2); + EXPECT(wallet->primary.subaccounts.at(0).last == 0); + EXPECT(wallet->primary.subaccounts.at(1).last == 2); + } + + SECTION("1.0 server with lookahead fail and max subaddress availability") + { + std::string subs; + for (unsigned i = 0; i < 50; ++i) + subs += R"({"key":)" + std::to_string(i) + R"(, "value": [[0, 199]]},)"; + subs.pop_back(); + + const std::string upsert_body = + R"({"address":")" + wallet->get_spend_address({0, 0}) + + R"(","view_key":")" + to_hex(unwrap(unwrap(wallet->primary.view.sec))) + + R"(","subaddrs":[{"key":1,"value":[[0,2]]}],"get_all":false})"; + + const std::string import_body = + R"({"address":")" + wallet->get_spend_address({0, 0}) + + R"(","view_key":")" + to_hex(unwrap(unwrap(wallet->primary.view.sec))) + + R"(","from_height":150,"lookahead":{"maj_i":50,"min_i":200}})"; + + rct::key ringct{}; + crypto::key_image image{}; + lwsf::internal::backend::keypair tx_key{}; + lwsf::internal::backend::keypair output_key{}; + crypto::generate_keys(tx_key.pub, tx_key.sec); + { + crypto::secret_key scalar{}; + crypto::key_derivation derived{}; + EXPECT(crypto::generate_key_derivation(tx_key.pub, wallet->primary.view.sec, derived)); + EXPECT(crypto::derive_public_key(derived, 3, wallet->primary.spend.pub, output_key.pub)); + crypto::derive_secret_key(derived, 3, wallet->primary.spend.sec, output_key.sec); + crypto::generate_key_image(output_key.pub, output_key.sec, image); + + crypto::derivation_to_scalar(derived, 3, scalar); + + rct::ecdhTuple commitment{}; + rct::ecdhDecode(commitment, rct::sk2rct(scalar), true); + ringct = commitment.mask; + } + + EXPECT(wallet->primary.subaccounts.size() == 2); // from last section + frame->requests = { + {"/login", ""}, + {"/upsert_subaddrs", upsert_body}, + {"/get_address_txs", ""}, + {"/get_unspent_outs", ""}, + {"/get_version", ""}, + {"/get_subaddrs", ""}, + {"/import_wallet_request", import_body} + }; + frame->responses = { + R"({"new_address": true, "lookahead": {"maj_i": 50, "min_i": 200}})", + "", + R"({ + "total_received": "4000", + "scanned_height": 175, + "scanned_block_height": 175, + "start_height": 5, + "blockchain_height": 210, + "lookahead": {"maj_i": 50, "min_i": 200}, + "lookahead_fail": 150, + "transactions": [{ + "id": 1, + "hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1", + "total_received": "4000", + "total_sent": "10000", + "unlock_time": 0, + "spent_outputs": [{ + "amount": "10000", + "key_image": ")" + to_hex(image) + R"(", + "tx_pub_key": ")" + to_hex(tx_key.pub) + R"(", + "out_index": 3, + "mixin": 15 + }], + "coinbase": false, + "mempool": true, + "mixin": 15 + }] + })", + R"({ + "per_byte_fee": 5, + "fee_mask": 100, + "fees": [110, 500, 700], + "amount": "4000", + "lookahead_fail": 150, + "outputs": [{ + "amount": "4000", + "index": 3, + "global_index": "60000", + "rct": "recomputed", + "tx_hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1", + "tx_prefix_hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee5", + "public_key": ")" + to_hex(output_key.pub) + R"(", + "tx_pub_key": ")" + to_hex(tx_key.pub) + R"(", + "spend_key_images": [")" + to_hex(image) + R"("], + "timestamp": "2026-01-01T00:00:00Z", + "height": 150, + "recipient": {"maj_i": 1, "min_i": 2} + }] + })", + R"({"max_subaddresses": 30000})", + R"({"all_subaddrs": [)" + subs + "]}", + R"({"lookahead": {"maj_i": 50, "min_i": 200}, "status": "OK", "new_request": true, "request_fulfilled": true})" + }; + + while (!done) + { + io.restart(); + io.run_one(); + } + + const lwsf::internal::rpc::address_meta sender{0, 0}; + const lwsf::internal::rpc::address_meta recipient{1, 2}; + + EXPECT(frame->responses.size() == 0); + EXPECT(result == std::error_code{}); + EXPECT(wallet->passed_login); + EXPECT(wallet->refresh_error == std::error_code{}); + EXPECT(wallet->lookahead_error == std::error_code{}); + EXPECT(wallet->subaddress_error == std::error_code{}); + EXPECT(wallet->import_error == std::error_code{}); + EXPECT(wallet->primary.restore_height == 5); + EXPECT(wallet->primary.scan_height == 175); + EXPECT(wallet->primary.requested_start == 5); + EXPECT(wallet->blockchain_height == 210); + EXPECT(wallet->fee_mask == 100); + EXPECT(wallet->per_byte_fee.size() == 3); + EXPECT(wallet->per_byte_fee.at(0) == 110); + EXPECT(wallet->per_byte_fee.at(1) == 500); + EXPECT(wallet->per_byte_fee.at(2) == 700); + EXPECT(wallet->primary.txes.size() == 1); + auto tx = wallet->primary.txes.find(from_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1")); + EXPECT(tx != wallet->primary.txes.end()); + EXPECT(tx->second != nullptr); + EXPECT(tx->second->spends.size() == 1); + EXPECT(tx->second->spends.nth(0)->first == image); + EXPECT(tx->second->spends.nth(0)->second.amount == 10000); + EXPECT(tx->second->spends.nth(0)->second.sender == sender); + EXPECT(tx->second->spends.nth(0)->second.tx_pub == tx_key.pub); + EXPECT(tx->second->spends.nth(0)->second.output_pub == output_key.pub); + EXPECT(tx->second->receives.size() == 1); + EXPECT(tx->second->receives.nth(0)->first == output_key.pub); + EXPECT(tx->second->receives.nth(0)->second.tx_pub == tx_key.pub); + EXPECT(tx->second->receives.nth(0)->second.amount == 4000); + EXPECT(tx->second->receives.nth(0)->second.recipient == recipient); + EXPECT(tx->second->receives.nth(0)->second.index == 3); + EXPECT(tx->second->receives.nth(0)->second.global_index == 60000); + EXPECT(bool(tx->second->receives.nth(0)->second.rct_mask)); + EXPECT(*tx->second->receives.nth(0)->second.rct_mask == ringct); + EXPECT(wallet->primary.subaccounts.size() == 2); + EXPECT(wallet->primary.subaccounts.at(0).last == 0); + EXPECT(wallet->primary.subaccounts.at(1).last == 2); + } + + SECTION("1.0 server with lookahead fail and new start") + { + wallet->primary.requested_start = 1; + + std::string subs; + for (unsigned i = 0; i < 50; ++i) + subs += R"({"key":)" + std::to_string(i) + R"(, "value": [[0, 199]]},)"; + subs.pop_back(); + + const std::string upsert_body = + R"({"address":")" + wallet->get_spend_address({0, 0}) + + R"(","view_key":")" + to_hex(unwrap(unwrap(wallet->primary.view.sec))) + + R"(","subaddrs":[{"key":1,"value":[[0,2]]}],"get_all":false})"; + + const std::string import_body = + R"({"address":")" + wallet->get_spend_address({0, 0}) + + R"(","view_key":")" + to_hex(unwrap(unwrap(wallet->primary.view.sec))) + + R"(","from_height":1,"lookahead":{"maj_i":50,"min_i":200}})"; + + rct::key ringct{}; + crypto::key_image image{}; + lwsf::internal::backend::keypair tx_key{}; + lwsf::internal::backend::keypair output_key{}; + crypto::generate_keys(tx_key.pub, tx_key.sec); + { + crypto::secret_key scalar{}; + crypto::key_derivation derived{}; + EXPECT(crypto::generate_key_derivation(tx_key.pub, wallet->primary.view.sec, derived)); + EXPECT(crypto::derive_public_key(derived, 3, wallet->primary.spend.pub, output_key.pub)); + crypto::derive_secret_key(derived, 3, wallet->primary.spend.sec, output_key.sec); + crypto::generate_key_image(output_key.pub, output_key.sec, image); + + crypto::derivation_to_scalar(derived, 3, scalar); + + rct::ecdhTuple commitment{}; + rct::ecdhDecode(commitment, rct::sk2rct(scalar), true); + ringct = commitment.mask; + } + + EXPECT(wallet->primary.subaccounts.size() == 2); // from last section + frame->requests = { + {"/login", ""}, + {"/upsert_subaddrs", upsert_body}, + {"/get_address_txs", ""}, + {"/get_unspent_outs", ""}, + {"/import_wallet_request", import_body} + }; + frame->responses = { + R"({"new_address": true, "lookahead": {"maj_i": 50, "min_i": 200}})", + "", + R"({ + "total_received": "4000", + "scanned_height": 175, + "scanned_block_height": 175, + "start_height": 5, + "blockchain_height": 210, + "lookahead": {"maj_i": 50, "min_i": 200}, + "lookahead_fail": 150, + "transactions": [{ + "id": 1, + "hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1", + "total_received": "4000", + "total_sent": "10000", + "unlock_time": 0, + "spent_outputs": [{ + "amount": "10000", + "key_image": ")" + to_hex(image) + R"(", + "tx_pub_key": ")" + to_hex(tx_key.pub) + R"(", + "out_index": 3, + "mixin": 15 + }], + "coinbase": false, + "mempool": true, + "mixin": 15 + }] + })", + R"({ + "per_byte_fee": 5, + "fee_mask": 100, + "fees": [110, 500, 700], + "amount": "4000", + "lookahead_fail": 150, + "outputs": [{ + "amount": "4000", + "index": 3, + "global_index": "60000", + "rct": "recomputed", + "tx_hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1", + "tx_prefix_hash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee5", + "public_key": ")" + to_hex(output_key.pub) + R"(", + "tx_pub_key": ")" + to_hex(tx_key.pub) + R"(", + "spend_key_images": [")" + to_hex(image) + R"("], + "timestamp": "2026-01-01T00:00:00Z", + "height": 150, + "recipient": {"maj_i": 1, "min_i": 2} + }] + })", + R"({"lookahead": {"maj_i": 50, "min_i": 200}, "status": "OK", "new_request": true, "request_fulfilled": true})" + }; + + while (!done) + { + io.restart(); + io.run_one(); + } + + const lwsf::internal::rpc::address_meta sender{0, 0}; + const lwsf::internal::rpc::address_meta recipient{1, 2}; + + EXPECT(frame->responses.size() == 0); + EXPECT(result == std::error_code{}); + EXPECT(wallet->passed_login); + EXPECT(wallet->refresh_error == std::error_code{}); + EXPECT(wallet->lookahead_error == std::error_code{}); + EXPECT(wallet->subaddress_error == std::error_code{}); + EXPECT(wallet->import_error == std::error_code{}); + EXPECT(wallet->primary.restore_height == 1); + EXPECT(wallet->primary.scan_height == 175); + EXPECT(wallet->primary.requested_start == 1); + EXPECT(wallet->blockchain_height == 210); + EXPECT(wallet->fee_mask == 100); + EXPECT(wallet->per_byte_fee.size() == 3); + EXPECT(wallet->per_byte_fee.at(0) == 110); + EXPECT(wallet->per_byte_fee.at(1) == 500); + EXPECT(wallet->per_byte_fee.at(2) == 700); + EXPECT(wallet->primary.txes.size() == 1); + auto tx = wallet->primary.txes.find(from_hex("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbee1")); + EXPECT(tx != wallet->primary.txes.end()); + EXPECT(tx->second != nullptr); + EXPECT(tx->second->spends.size() == 1); + EXPECT(tx->second->spends.nth(0)->first == image); + EXPECT(tx->second->spends.nth(0)->second.amount == 10000); + EXPECT(tx->second->spends.nth(0)->second.sender == sender); + EXPECT(tx->second->spends.nth(0)->second.tx_pub == tx_key.pub); + EXPECT(tx->second->spends.nth(0)->second.output_pub == output_key.pub); + EXPECT(tx->second->receives.size() == 1); + EXPECT(tx->second->receives.nth(0)->first == output_key.pub); + EXPECT(tx->second->receives.nth(0)->second.tx_pub == tx_key.pub); + EXPECT(tx->second->receives.nth(0)->second.amount == 4000); + EXPECT(tx->second->receives.nth(0)->second.recipient == recipient); + EXPECT(tx->second->receives.nth(0)->second.index == 3); + EXPECT(tx->second->receives.nth(0)->second.global_index == 60000); + EXPECT(bool(tx->second->receives.nth(0)->second.rct_mask)); + EXPECT(*tx->second->receives.nth(0)->second.rct_mask == ringct); + EXPECT(wallet->primary.subaccounts.size() == 2); + EXPECT(wallet->primary.subaccounts.at(0).last == 0); + EXPECT(wallet->primary.subaccounts.at(1).last == 2); + } + } + + SECTION("call rpcs in randomized order with threads") + { + static constexpr const unsigned high_level_calls = 1000; + + const auto frame = std::make_shared(io); + wallet->client.init(io, "127.0.0.1", "", acceptor.local_endpoint().port(), no_ssl); + wallet->primary.subaccounts.emplace_back().last = 1; + acceptor.async_accept(frame->server, dummy_server{frame}); + + std::atomic count{0}; + const auto completed = [&] (auto) { ++count; }; + + const std::array, 8> rpcs + {{ + std::bind(bwallet::login_is_new, wallet, completed), + std::bind(bwallet::refresh, wallet, true, completed), + std::bind(bwallet::register_subaccount, wallet, 1, completed), + std::bind(bwallet::register_subaddress, wallet, 1, 0, completed), + std::bind(bwallet::set_lookahead, wallet, 1, 1, completed), + std::bind(bwallet::restore_height_raw, wallet, 0, completed), + std::bind(bwallet::get_decoys, wallet, empty_decoys{}, completed), + std::bind(bwallet::send_tx, wallet, empty_slice{}, completed) + }}; + + struct joiner + { + boost::thread thread; + + joiner(const joiner&) = delete; + joiner(joiner&&) = default; + ~joiner() noexcept { if (thread.joinable()) thread.join(); } + }; + std::vector threads; + + struct on_exit_ + { + boost::asio::io_context& io; + ~on_exit_() noexcept { io.stop(); } + } on_exit{io}; + + std::string asio_fail; + boost::mutex sync_asio; + const auto run_asio = [&] + { + try { (io.run()); } + catch (const std::exception& e) + { + io.stop(); + const boost::lock_guard lock{sync_asio}; + asio_fail = e.what(); + } + }; + const auto run_client = [&] + { + static_assert(high_level_calls % 2 == 0); + static_assert(0 < rpcs.size()); + static_assert(rpcs.size() <= std::numeric_limits::max()); + + std::mt19937 eng(crypto::rand()); + std::uniform_int_distribution dist(0, rpcs.size() - 1); + + unsigned i = high_level_calls / 2; + while (i--) + { + rpcs.at(dist(eng))(); + // the above call is much quicker than the response threads + boost::this_thread::sleep_for(boost::chrono::milliseconds{1}); + } + }; + + threads.push_back(joiner{boost::thread{run_asio}}); + threads.push_back(joiner{boost::thread{run_asio}}); + threads.push_back(joiner{boost::thread{run_client}}); + run_client(); + + while (count < high_level_calls && !io.stopped()) + boost::this_thread::sleep_for(boost::chrono::milliseconds{50}); + + const boost::lock_guard lock{sync_asio}; + EXPECT(asio_fail == ""); + EXPECT(count == high_level_calls); + EXPECT(!io.stopped()); + } + } +} + diff --git a/tests/unit/net/CMakeLists.txt b/tests/unit/net/CMakeLists.txt new file mode 100644 index 0000000..0de1fc2 --- /dev/null +++ b/tests/unit/net/CMakeLists.txt @@ -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) diff --git a/tests/unit/net/http.test.cpp b/tests/unit/net/http.test.cpp new file mode 100644 index 0000000..2d6acfa --- /dev/null +++ b/tests/unit/net/http.test.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include "common/error.h" // monero/src +#include "net/http.h" + +using client_request = + boost::beast::http::request; + +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(src.data()), src.size()}; + } + + void send(boost::asio::ip::tcp::socket& server, std::string body) + { + boost::beast::http::response 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 verbs{{verb::post, verb::get, verb::post}}; + const std::array targets{{"/post1", "/get1", "/post2"}}; + const std::array requests{{"REQ1", "", "REQ3"}}; + const std::array responses{{"RESP1", "RESP2", "RESP3"}}; + std::array 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 +}