// 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 "framework.test.h" #include #include #include #include #include #include "cryptonote_basic/account.h" // monero/src #include "cryptonote_core/cryptonote_tx_utils.h" // monero/src #include "db/data.h" #include "db/print.test.h" #include "db/storage.test.h" #include "db/string.h" #include "error.h" #include "hex.h" // monero/epee/contrib/include #include "mempool.h" #include "net/http_client.h" #include "rapidjson/document.h" // monero/external/rapidjson/include #include "rapidjson/stringbuffer.h" // monero/external/rapidjson/incldue #include "rapidjson/prettywriter.h" // monero/external/rapidjson/incldue #include "rest_server.h" #include "rpc/pull.test.h" #include "scanner.test.h" #include "util/account.test.h" #include "util/transaction.test.h" namespace rapidjson { std::ostream& operator<<(std::ostream& out, const Document& src) { StringBuffer buffer; PrettyWriter writer{buffer}; src.Accept(writer); return out << buffer.GetString(); } } namespace { namespace enet = epee::net_utils; constexpr const char fake_daemon[] = "inproc://fake_daemon"; constexpr const char rest_server[] = "http://127.0.0.1:10000"; constexpr const char admin_server[] = "http://127.0.0.1:10001"; struct join { boost::thread& thread; ~join() { thread.join(); } }; struct rct_bytes { rct::key commitment; rct::key mask; rct::key amount; }; std::pair invoke_base(enet::http::http_simple_client& client, const boost::string_ref uri, const boost::string_ref body) { const enet::http::http_response_info* info = nullptr; if (!client.invoke(uri, "POST", body, std::chrono::milliseconds{500}, std::addressof(info), {})) throw std::runtime_error{"HTTP invoke failed"}; return {std::string{info->m_body}, info->m_response_code}; } std::string invoke(enet::http::http_simple_client& client, const boost::string_ref uri, const boost::string_ref body) { auto result = invoke_base(client, uri, body); if (result.second != 200) throw std::runtime_error{"HTTP invoke not 200, instead " + std::to_string(result.second)}; return result.first; } epee::byte_slice get_fee_response() { return epee::byte_slice{ std::string{ "{\"jsonrpc\":2.0,\"id\":0,\"result\":{" "\"estimated_base_fee\":10000,\"fee_mask\":1000,\"size_scale\":256,\"hard_fork_version\":16,\"fees\":[40,41]" "}}" } }; } rct_bytes get_rct_bytes(const crypto::secret_key& user_key, const crypto::public_key& tx_public, const rct::key& ringct_mask, const std::uint64_t amount, const std::uint32_t index, bool coinbase) { rct_bytes out{}; if (!coinbase) { crypto::key_derivation derived; if (!crypto::generate_key_derivation(tx_public, user_key, derived)) MONERO_THROW(lws::error::crypto_failure, "generate_key_derivation failed"); crypto::secret_key scalar; rct::ecdhTuple encrypted{ringct_mask, rct::d2h(amount)}; crypto::derivation_to_scalar(derived, index, scalar); rct::ecdhEncode(encrypted, rct::sk2rct(scalar), false); out.commitment = rct::commit(amount, ringct_mask); out.mask = encrypted.mask; out.amount = encrypted.amount; } else out.mask = rct::identity(); return out; } void verify_json(lest::env& lest_env, std::string name, std::string actual_json, std::string expected_json) { const lest::ctx ctx{lest_env, std::move(name)}; rapidjson::Document expected; expected.Parse(expected_json.c_str()); EXPECT(!expected.HasParseError()); rapidjson::Document actual; actual.Parse(actual_json.c_str()); EXPECT(!actual.HasParseError()); EXPECT(expected == actual); } std::string get_prefix(const std::string& raw) { char const* const sep = std::strchr(raw.c_str(), u8':'); if (!sep) return {}; return {raw.data(), std::size_t(sep - raw.data() + 1)}; } } LWS_CASE("rest_server") { const auto now = std::chrono::system_clock::now(); const auto base = lws_test::make_account(); lws::db::account_address account_address{ base.m_account_address.m_view_public_key, base.m_account_address.m_spend_public_key }; const crypto::secret_key view = base.m_view_secret_key; const std::string address = lws::db::address_string(account_address); const std::string viewkey = epee::to_hex::string(epee::as_byte_span(unwrap(unwrap(view)))); SETUP("Database and login") { std::optional server; lws::db::test::cleanup_db on_scope_exit{}; lws::db::storage db = lws::db::test::get_fresh_db(); auto context = lws::rpc::context::make(lws_test::rpc_rendevous, {}, {}, {}, std::chrono::minutes{0}, false, true); const auto rpc = MONERO_UNWRAP(context.connect()); const auto init_server = [&] (std::shared_ptr pool) { const lws::rest_server::configuration config{ {}, {}, std::chrono::seconds{10}, 1, 20, {}, false, true, true, false }; std::vector addresses{rest_server}; server.emplace( epee::to_span(addresses), std::vector{admin_server}, db.clone(), MONERO_UNWRAP(rpc.clone()), std::move(pool), config ); }; init_server(nullptr); const lws::db::block_info last_block = MONERO_UNWRAP(MONERO_UNWRAP(db.start_read()).get_last_block()); const auto get_account = [&db, &account_address] () -> lws::db::account { return MONERO_UNWRAP(MONERO_UNWRAP(db.start_read()).get_account(account_address)).second; }; const auto get_full_account = [&db, &get_account] () -> lws::account { const lws::db::account acct = get_account(); return MONERO_UNWRAP(MONERO_UNWRAP(db.start_read()).get_full_account(acct)); }; enet::http::http_simple_client client{}; client.set_server("127.0.0.1", "10000", boost::none); EXPECT(client.connect(std::chrono::milliseconds{500})); std::string message = "{\"address\":\"" + address + "\",\"view_key\":\"" + viewkey + "\",\"create_account\":true,\"generated_locally\":true}"; std::string response = invoke(client, "/login", message); EXPECT(response == "{\"new_address\":true,\"generated_locally\":true,\"lookahead\":{\"maj_i\":0,\"min_i\":0}}"); auto account = get_account(); EXPECT(account.id == lws::db::account_id(1)); SECTION("Empty Account") { const std::string scan_height = std::to_string(std::uint64_t(account.scan_height)); const std::string start_height = std::to_string(std::uint64_t(account.start_height)); message = "{\"address\":\"" + address + "\",\"view_key\":\"" + viewkey + "\"}"; response = invoke(client, "/get_address_info", message); EXPECT(response == "{\"locked_funds\":\"0\"," "\"total_received\":\"0\"," "\"total_sent\":\"0\"," "\"scanned_height\":" + scan_height + "," + "\"scanned_block_height\":" + scan_height + "," "\"start_height\":" + start_height + "," "\"transaction_height\":" + scan_height + "," "\"blockchain_height\":" + scan_height + "}" ); response = invoke(client, "/get_address_txs", message); EXPECT(response == "{\"total_received\":\"0\"," "\"scanned_height\":" + scan_height + "," + "\"scanned_block_height\":" + scan_height + "," "\"start_height\":" + start_height + "," "\"transaction_height\":" + scan_height + "," "\"blockchain_height\":" + scan_height + "}" ); std::vector messages; messages.emplace_back(get_fee_response()); boost::thread server_thread(&lws_test::rpc_thread, context.zmq_context(), std::cref(messages)); const join on_scope_exit{server_thread}; message = "{\"address\":\"" + address + "\",\"view_key\":\"" + viewkey + "\",\"amount\":\"0\"}"; response = invoke(client, "/get_unspent_outs", message); EXPECT(response == "{\"per_byte_fee\":39,\"fee_mask\":1000,\"amount\":\"0\",\"fees\":[40,41]}"); } SECTION("Import from height") { EXPECT(account.start_height != lws::db::block_id(0)); const std::string scan_height = std::to_string(std::uint64_t(account.scan_height)); const std::string start_height = std::to_string(std::uint64_t(account.start_height)); const std::string import_height = std::to_string(std::uint64_t(account.start_height) - 1); message = "{\"address\":\"" + address + "\",\"view_key\":\"" + viewkey + "\"}"; response = invoke(client, "/get_address_info", message); EXPECT(response == "{\"locked_funds\":\"0\"," "\"total_received\":\"0\"," "\"total_sent\":\"0\"," "\"scanned_height\":" + scan_height + "," + "\"scanned_block_height\":" + scan_height + "," "\"start_height\":" + start_height + "," "\"transaction_height\":" + scan_height + "," "\"blockchain_height\":" + scan_height + "}" ); message = "{\"address\":\"" + address + "\",\"view_key\":\"" + viewkey + "\", \"from_height\":" + import_height + "}"; response = invoke(client, "/import_wallet_request", message); EXPECT(response == "{\"import_fee\":\"0\"," "\"status\":\"Accepted, waiting for approval\"," "\"new_request\":true," "\"request_fulfilled\":false," "\"lookahead\":{\"maj_i\":0,\"min_i\":0}}" ); EXPECT(db.accept_requests(lws::db::request::import_scan, {std::addressof(account_address), 1}, 0)); response = invoke(client, "/import_wallet_request", message); EXPECT(response == "{\"import_fee\":\"0\"," "\"status\":\"Approved\"," "\"new_request\":false," "\"request_fulfilled\":true," "\"lookahead\":{\"maj_i\":0,\"min_i\":0}}" ); response = invoke(client, "/get_address_info", message); EXPECT(response == "{\"locked_funds\":\"0\"," "\"total_received\":\"0\"," "\"total_sent\":\"0\"," "\"scanned_height\":" + import_height + "," + "\"scanned_block_height\":" + import_height + "," "\"start_height\":" + import_height + "," "\"transaction_height\":" + scan_height + "," "\"blockchain_height\":" + scan_height + "}" ); } SECTION("Import with lookahead") { EXPECT(account.start_height != lws::db::block_id(0)); const std::string scan_height = std::to_string(std::uint64_t(account.scan_height)); const std::string start_height = std::to_string(std::uint64_t(account.start_height)); message = "{\"address\":\"" + address + "\",\"view_key\":\"" + viewkey + "\"}"; response = invoke(client, "/get_address_info", message); EXPECT(response == "{\"locked_funds\":\"0\"," "\"total_received\":\"0\"," "\"total_sent\":\"0\"," "\"scanned_height\":" + scan_height + "," + "\"scanned_block_height\":" + scan_height + "," "\"start_height\":" + start_height + "," "\"transaction_height\":" + scan_height + "," "\"blockchain_height\":" + scan_height + "}" ); message = "{\"address\":\"" + address + "\",\"view_key\":\"" + viewkey + "\", \"lookahead\":{\"maj_i\":2,\"min_i\":3}}"; response = invoke(client, "/import_wallet_request", message); EXPECT(response == "{\"import_fee\":\"0\"," "\"status\":\"Accepted, waiting for approval\"," "\"new_request\":true," "\"request_fulfilled\":false," "\"lookahead\":{\"maj_i\":0,\"min_i\":0}}" ); response = invoke(client, "/import_wallet_request", message); EXPECT(response == "{\"import_fee\":\"0\"," "\"status\":\"Waiting for Approval\"," "\"new_request\":false," "\"request_fulfilled\":false," "\"lookahead\":{\"maj_i\":0,\"min_i\":0}}" ); { auto reader = MONERO_UNWRAP(db.start_read()); const std::vector expected_range{}; EXPECT(MONERO_UNWRAP(reader.get_subaddresses(lws::db::account_id(1))) == expected_range); } EXPECT(db.accept_requests(lws::db::request::import_scan, {std::addressof(account_address), 1}, 6)); response = invoke(client, "/import_wallet_request", message); EXPECT(response == "{\"import_fee\":\"0\"," "\"status\":\"Approved\"," "\"new_request\":false," "\"request_fulfilled\":true," "\"lookahead\":{\"maj_i\":2,\"min_i\":3}}" ); response = invoke(client, "/get_address_info", message); EXPECT(response == "{\"locked_funds\":\"0\"," "\"total_received\":\"0\"," "\"total_sent\":\"0\"," "\"scanned_height\":0," "\"scanned_block_height\":0," "\"start_height\":0," "\"transaction_height\":" + scan_height + "," "\"blockchain_height\":" + scan_height + "," "\"lookahead\":{\"maj_i\":2,\"min_i\":3}}" ); { auto reader = MONERO_UNWRAP(db.start_read()); const auto account = MONERO_UNWRAP(reader.get_account(lws::db::account_status::active, lws::db::account_id(1))); const lws::db::address_index lookahead{lws::db::major_index(2), lws::db::minor_index(3)}; EXPECT(account.lookahead == lookahead); EXPECT(account.lookahead_fail == lws::db::block_id(0)); const std::vector expected_range{ {lws::db::major_index(0), {{lws::db::index_range{lws::db::minor_index(0), lws::db::minor_index(2)}}}}, {lws::db::major_index(1), {{lws::db::index_range{lws::db::minor_index(0), lws::db::minor_index(2)}}}}, }; EXPECT(MONERO_UNWRAP(reader.get_subaddresses(lws::db::account_id(1))) == expected_range); } } SECTION("Import with lookahead failure") { EXPECT(account.start_height != lws::db::block_id(0)); const std::string scan_height = std::to_string(std::uint64_t(account.scan_height)); const std::string start_height = std::to_string(std::uint64_t(account.start_height)); message = "{\"address\":\"" + address + "\",\"view_key\":\"" + viewkey + "\"}"; response = invoke(client, "/get_address_info", message); EXPECT(response == "{\"locked_funds\":\"0\"," "\"total_received\":\"0\"," "\"total_sent\":\"0\"," "\"scanned_height\":" + scan_height + "," + "\"scanned_block_height\":" + scan_height + "," "\"start_height\":" + start_height + "," "\"transaction_height\":" + scan_height + "," "\"blockchain_height\":" + scan_height + "}" ); message = "{\"address\":\"" + address + "\",\"view_key\":\"" + viewkey + "\", \"lookahead\":{\"maj_i\":2,\"min_i\":3}}"; response = invoke(client, "/import_wallet_request", message); EXPECT(response == "{\"import_fee\":\"0\"," "\"status\":\"Accepted, waiting for approval\"," "\"new_request\":true," "\"request_fulfilled\":false," "\"lookahead\":{\"maj_i\":0,\"min_i\":0}}" ); response = invoke(client, "/import_wallet_request", message); EXPECT(response == "{\"import_fee\":\"0\"," "\"status\":\"Waiting for Approval\"," "\"new_request\":false," "\"request_fulfilled\":false," "\"lookahead\":{\"maj_i\":0,\"min_i\":0}}" ); { auto reader = MONERO_UNWRAP(db.start_read()); const std::vector expected_range{}; EXPECT(MONERO_UNWRAP(reader.get_subaddresses(lws::db::account_id(1))) == expected_range); } EXPECT(db.accept_requests(lws::db::request::import_scan, {std::addressof(account_address), 1}, 5)); response = invoke(client, "/import_wallet_request", message); EXPECT(response == "{\"import_fee\":\"0\"," "\"status\":\"Accepted, waiting for approval\"," "\"new_request\":true," "\"request_fulfilled\":false," "\"lookahead\":{\"maj_i\":2,\"min_i\":3}}" ); response = invoke(client, "/get_address_info", message); EXPECT(response == "{\"locked_funds\":\"0\"," "\"total_received\":\"0\"," "\"total_sent\":\"0\"," "\"scanned_height\":0," "\"scanned_block_height\":0," "\"start_height\":0," "\"transaction_height\":" + scan_height + "," "\"blockchain_height\":" + scan_height + "," "\"lookahead_fail\":1," "\"lookahead\":{\"maj_i\":2,\"min_i\":3}}" ); { auto reader = MONERO_UNWRAP(db.start_read()); const auto account = MONERO_UNWRAP(reader.get_account(lws::db::account_status::active, lws::db::account_id(1))); const lws::db::address_index lookahead{lws::db::major_index(2), lws::db::minor_index(3)}; EXPECT(account.lookahead == lookahead); EXPECT(account.lookahead_fail == lws::db::block_id(1)); EXPECT(MONERO_UNWRAP(reader.get_subaddresses(lws::db::account_id(1))).empty()); } } SECTION("One Receive, Zero Spends") { const std::string scan_height = std::to_string(std::uint64_t(account.scan_height) + 5); const std::string start_height = std::to_string(std::uint64_t(account.start_height)); message = "{\"address\":\"" + address + "\",\"view_key\":\"" + viewkey + "\"}"; const lws::db::transaction_link link{ lws::db::block_id(4000), crypto::rand() }; const crypto::public_key tx_public = []() { crypto::secret_key secret; crypto::public_key out; crypto::generate_keys(out, secret); return out; }(); const crypto::hash tx_prefix = crypto::rand(); const crypto::public_key pub = crypto::rand(); const rct::key ringct = crypto::rand(); const auto extra = lws::db::extra(lws::db::extra::coinbase_output | lws::db::extra::ringct_output); const auto payment_id_ = crypto::rand(); const crypto::key_image image = crypto::rand(); lws::account real_account{account, {}, {}}; real_account.add_out( lws::db::output{ link, lws::db::output::spend_meta_{ lws::db::output_id{500, 30}, std::uint64_t(40000), std::uint32_t(16), std::uint32_t(2), tx_public }, std::uint64_t(7000), std::uint64_t(4670), tx_prefix, pub, ringct, {0, 0, 0, 0, 0, 0, 0}, lws::db::pack(extra, sizeof(crypto::hash)), payment_id_, std::uint64_t(100), lws::db::address_index{lws::db::major_index(2), lws::db::minor_index(66)} } ); { std::vector hashes{ last_block.hash, crypto::rand(), crypto::rand(), crypto::rand(), crypto::rand(), crypto::rand() }; EXPECT(db.update(last_block.id, epee::to_span(hashes), {std::addressof(real_account), 1}, {})); } response = invoke(client, "/get_address_info", message); EXPECT(response == "{\"locked_funds\":\"0\"," "\"total_received\":\"40000\"," "\"total_sent\":\"0\"," "\"scanned_height\":" + scan_height + "," + "\"scanned_block_height\":" + scan_height + "," "\"start_height\":" + start_height + "," "\"transaction_height\":" + scan_height + "," "\"blockchain_height\":" + scan_height + "}" ); response = invoke(client, "/get_address_txs", message); EXPECT(response == "{\"total_received\":\"40000\"," "\"scanned_height\":" + scan_height + "," + "\"scanned_block_height\":" + scan_height + "," "\"start_height\":" + start_height + "," "\"transaction_height\":" + scan_height + "," "\"blockchain_height\":" + scan_height + "," "\"transactions\":[" "{\"id\":0," "\"hash\":\"" + epee::to_hex::string(epee::as_byte_span(link.tx_hash)) + "\"," "\"timestamp\":\"1970-01-01T01:56:40Z\"," "\"total_received\":\"40000\"," "\"total_sent\":\"0\"," "\"fee\":\"100\"," "\"unlock_time\":4670," "\"height\":4000," "\"payment_id\":\"" + epee::to_hex::string(epee::as_byte_span(payment_id_.long_)) + "\"," "\"coinbase\":true," "\"mempool\":false," "\"mixin\":16," "\"recipient\":{\"maj_i\":2,\"min_i\":66}}" "]}" ); std::vector messages; messages.emplace_back(get_fee_response()); boost::thread server_thread(&lws_test::rpc_thread, context.zmq_context(), std::cref(messages)); const join on_scope_exit{server_thread}; const auto ringct_expanded = get_rct_bytes(view, tx_public, ringct, 40000, 2, true); message = "{\"address\":\"" + address + "\",\"view_key\":\"" + viewkey + "\",\"amount\":\"0\"}"; response = invoke(client, "/get_unspent_outs", message); EXPECT(response == "{\"per_byte_fee\":39," "\"fee_mask\":1000," "\"amount\":\"40000\"," "\"outputs\":[" "{\"amount\":\"40000\"," "\"public_key\":\"" + epee::to_hex::string(epee::as_byte_span(pub)) + "\"," "\"index\":2," "\"global_index\":30," "\"tx_id\":30," "\"tx_hash\":\"" + epee::to_hex::string(epee::as_byte_span(link.tx_hash)) + "\"," "\"tx_prefix_hash\":\"" + epee::to_hex::string(epee::as_byte_span(tx_prefix)) + "\"," "\"tx_pub_key\":\"" + epee::to_hex::string(epee::as_byte_span(tx_public)) + "\"," "\"timestamp\":\"1970-01-01T01:56:40Z\"," "\"height\":4000," "\"rct\":\"" + epee::to_hex::string(epee::as_byte_span(ringct_expanded)) + "\"," "\"recipient\":{\"maj_i\":2,\"min_i\":66}}" "],\"fees\":[40,41]}" ); } SECTION("One Receive, One Spend, and one mempool") { const auto pool = std::make_shared(); EXPECT(client.disconnect()); init_server(pool); EXPECT(client.connect(std::chrono::milliseconds{500})); lws_test::transaction tx; { std::vector destinations; destinations.emplace_back(); destinations.back().amount = 8000; destinations.back().addr = base.m_account_address; tx = lws_test::make_tx(lest_env, base, destinations, 20, true); const auto hash = get_transaction_hash(tx.tx); pool->reset_txs( lws::mempool::pool_table{ {hash, std::make_shared(cryptonote::transaction{tx.tx}, now)} } ); } const std::string scan_height = std::to_string(std::uint64_t(account.scan_height) + 5); const std::string start_height = std::to_string(std::uint64_t(account.start_height)); message = "{\"address\":\"" + address + "\",\"view_key\":\"" + viewkey + "\"}"; const lws::db::transaction_link link{ lws::db::block_id(4000), crypto::rand() }; const crypto::public_key tx_public = []() { crypto::secret_key secret; crypto::public_key out; crypto::generate_keys(out, secret); return out; }(); const crypto::hash tx_prefix = crypto::rand(); const crypto::public_key pub = crypto::rand(); const rct::key ringct = crypto::rand(); const auto extra = lws::db::extra(lws::db::extra::coinbase_output | lws::db::extra::ringct_output); const auto payment_id_ = crypto::rand(); const crypto::hash payment_id = crypto::rand(); const crypto::key_image image = crypto::rand(); lws::account real_account{account, {}, {}}; real_account.add_out( lws::db::output{ link, lws::db::output::spend_meta_{ lws::db::output_id{0, 35}, std::uint64_t(40000), std::uint32_t(16), std::uint32_t(2), tx_public }, std::uint64_t(7000), std::uint64_t(4670), tx_prefix, pub, ringct, {0, 0, 0, 0, 0, 0, 0}, lws::db::pack(extra, sizeof(crypto::hash)), payment_id_, std::uint64_t(100), lws::db::address_index{lws::db::major_index(2), lws::db::minor_index(66)} } ); real_account.add_spend( lws::db::spend{ link, image, lws::db::output_id{0, 35}, std::uint64_t(66), std::uint64_t(1500), std::uint32_t(16), {0, 0, 0}, 32, payment_id, lws::db::address_index{lws::db::major_index(4), lws::db::minor_index(55)} } ); { std::vector hashes{ last_block.hash, crypto::rand(), crypto::rand(), crypto::rand(), crypto::rand(), crypto::rand() }; EXPECT(db.update(last_block.id, epee::to_span(hashes), {std::addressof(real_account), 1}, {})); } response = invoke(client, "/get_address_info", message); EXPECT(response == "{\"locked_funds\":\"0\"," "\"total_received\":\"40000\"," "\"total_sent\":\"40000\"," "\"scanned_height\":" + scan_height + "," + "\"scanned_block_height\":" + scan_height + "," "\"start_height\":" + start_height + "," "\"transaction_height\":" + scan_height + "," "\"blockchain_height\":" + scan_height + "," "\"spent_outputs\":[{" "\"amount\":\"40000\"," "\"key_image\":\"" + epee::to_hex::string(epee::as_byte_span(image)) + "\"," "\"tx_pub_key\":\"" + epee::to_hex::string(epee::as_byte_span(tx_public)) + "\"," "\"out_index\":2," "\"mixin\":16," "\"sender\":{\"maj_i\":4,\"min_i\":55}" "}]}" ); response = invoke(client, "/get_address_txs", message); const std::string expected = R"({ "total_received":"48000", "scanned_height":)" + scan_height + R"(, "scanned_block_height":)" + scan_height + R"(, "start_height":)" + start_height + R"(, "transaction_height":)" + scan_height + R"(, "blockchain_height":)" + scan_height + R"(, "transactions":[ { "id":0, "hash":")" + epee::to_hex::string(epee::as_byte_span(link.tx_hash)) + R"(", "timestamp":"1970-01-01T01:56:40Z", "total_received":"40000", "total_sent":"40000", "fee":"100", "unlock_time":4670, "height":4000, "payment_id":")" + epee::to_hex::string(epee::as_byte_span(payment_id_.long_)) + R"(", "coinbase":true, "mempool":false, "mixin":16, "recipient":{"maj_i":2,"min_i":66}, "spent_outputs":[{ "amount":"40000", "key_image":")" + epee::to_hex::string(epee::as_byte_span(image)) + R"(", "tx_pub_key":")" + epee::to_hex::string(epee::as_byte_span(tx_public)) + R"(", "out_index":2, "mixin":16, "sender":{"maj_i":4,"min_i":55} }] },{ "id":1, "hash":")" + epee::to_hex::string(epee::as_byte_span(get_transaction_hash(tx.tx))) + R"(", "total_received":"8000", "total_sent":"40000", "fee":"12000", "unlock_time":0, "payment_id":"0000000000000000", "coinbase":false, "mempool":true, "mixin":15, "recipient":{"maj_i":0,"min_i":0}, "spent_outputs":[{ "amount":"40000", "key_image":")" + epee::to_hex::string(epee::as_byte_span(tx.images.at(0))) + R"(", "tx_pub_key":")" + epee::to_hex::string(epee::as_byte_span(tx_public)) + R"(", "out_index":2, "mixin":15, "sender":{"maj_i":2, "min_i": 66} }] } ] })"; verify_json(lest_env, "/get_address_txs", response, expected); std::vector messages; messages.emplace_back(get_fee_response()); boost::thread server_thread(&lws_test::rpc_thread, context.zmq_context(), std::cref(messages)); const join on_scope_exit{server_thread}; const auto ringct_expanded = get_rct_bytes(view, tx_public, ringct, 40000, 2, true); message = "{\"address\":\"" + address + "\",\"view_key\":\"" + viewkey + "\",\"amount\":\"0\"}"; response = invoke(client, "/get_unspent_outs", message); EXPECT(response == "{\"per_byte_fee\":39," "\"fee_mask\":1000," "\"amount\":\"40000\"," "\"outputs\":[" "{\"amount\":\"40000\"," "\"public_key\":\"" + epee::to_hex::string(epee::as_byte_span(pub)) + "\"," "\"index\":2," "\"global_index\":35," "\"tx_id\":35," "\"tx_hash\":\"" + epee::to_hex::string(epee::as_byte_span(link.tx_hash)) + "\"," "\"tx_prefix_hash\":\"" + epee::to_hex::string(epee::as_byte_span(tx_prefix)) + "\"," "\"tx_pub_key\":\"" + epee::to_hex::string(epee::as_byte_span(tx_public)) + "\"," "\"timestamp\":\"1970-01-01T01:56:40Z\"," "\"height\":4000," "\"spend_key_images\":[\"" + epee::to_hex::string(epee::as_byte_span(image)) + "\"]," "\"rct\":\"" + epee::to_hex::string(epee::as_byte_span(ringct_expanded)) + "\"," "\"recipient\":{\"maj_i\":2,\"min_i\":66}}" "],\"fees\":[40,41]}" ); } SECTION("provision_subaddrs") { const std::string scan_height = std::to_string(std::uint64_t(account.scan_height) + 5); const std::string start_height = std::to_string(std::uint64_t(account.start_height)); message = "{\"address\":\"" + address + "\",\"view_key\":\"" + viewkey + "\",\"maj_i\":0,\"min_i\":0,\"n_maj\":2,\"n_min\":5}"; response = invoke(client, "/provision_subaddrs", message); EXPECT(response == "{\"new_subaddrs\":[" "{\"key\":0,\"value\":[[0,4]]}," "{\"key\":1,\"value\":[[0,4]]}" "],\"all_subaddrs\":[" "{\"key\":0,\"value\":[[0,4]]}," "{\"key\":1,\"value\":[[0,4]]}]}" ); message = "{\"address\":\"" + address + "\",\"view_key\":\"" + viewkey + "\",\"maj_i\":2,\"min_i\":5,\"n_maj\":2,\"n_min\":5}"; response = invoke(client, "/provision_subaddrs", message); EXPECT(response == "{\"new_subaddrs\":[" "{\"key\":2,\"value\":[[5,9]]}," "{\"key\":3,\"value\":[[5,9]]}" "],\"all_subaddrs\":[" "{\"key\":0,\"value\":[[0,4]]}," "{\"key\":1,\"value\":[[0,4]]}," "{\"key\":2,\"value\":[[5,9]]}," "{\"key\":3,\"value\":[[5,9]]}]}" ); } SECTION("upsert_subaddrs") { const std::string scan_height = std::to_string(std::uint64_t(account.scan_height) + 5); const std::string start_height = std::to_string(std::uint64_t(account.start_height)); message = "{\"address\":\"" + address + "\",\"view_key\":\"" + viewkey + "\",\"subaddrs\":[{\"key\":0,\"value\":[[1,10]]}]}"; response = invoke(client, "/upsert_subaddrs", message); EXPECT(response == "{\"new_subaddrs\":[" "{\"key\":0,\"value\":[[1,10]]}" "],\"all_subaddrs\":[" "{\"key\":0,\"value\":[[1,10]]}]}" ); message = "{\"address\":\"" + address + "\",\"view_key\":\"" + viewkey + "\",\"subaddrs\":[{\"key\":0,\"value\":[[11,20]]}]}"; response = invoke(client, "/upsert_subaddrs", message); EXPECT(response == "{\"new_subaddrs\":[" "{\"key\":0,\"value\":[[11,20]]}" "],\"all_subaddrs\":[" "{\"key\":0,\"value\":[[1,20]]}]}" ); } SECTION("feed check") { auto response = invoke_base(client, "/feed", ""); EXPECT(response.second == 501); EXPECT(response.first == ""); } SECTION("feed subscription") { const auto pool = std::make_shared(); const std::string scan_height = std::to_string(std::uint64_t(account.scan_height)); namespace pull = lws_test::rpc::pull; const auto local = boost::asio::ip::make_address("127.0.0.1"); boost::asio::io_context io; boost::asio::steady_timer timeout{io}; timeout.expires_after(std::chrono::seconds{5}); timeout.async_wait([&io] (auto) { io.stop(); }); // // invalid version // std::shared_ptr conn = pull::make(io, boost::asio::ip::tcp::endpoint(local, 10000), {}); bool ran = false; boost::system::error_code error{}; std::string response; message = R"(login:{"account":{"address":")" + address + R"(","view_key":")" + viewkey + R"("}})"; const auto handler = [&] (auto ec, auto msg) { ran = true; error = ec; response = msg; }; pull::async_handshake(conn, message, handler); while (!ran && !io.stopped()) { io.restart(); io.run_one(); } EXPECT(!io.stopped()); EXPECT(error == boost::beast::websocket::error::upgrade_declined); // // empty tx sync // conn = pull::make(io, boost::asio::ip::tcp::endpoint(local, 10000), "lws.feed.v0.json"); pull::async_handshake(conn, message, handler); ran = false; error = {}; response = {}; while (!ran && !io.stopped()) { io.restart(); io.run_one(); } std::string expected= R"({"scanned_block_height":)" + scan_height + R"(, "start_height":)" + scan_height + R"(, "blockchain_height":)" + scan_height + "}"; EXPECT(!io.stopped()); EXPECT(error == boost::system::error_code{}); EXPECT(get_prefix(response) == "tx_sync:"); response.erase(0, std::strlen("tx_sync:")); verify_json(lest_env, "tx_sync", response, expected); pull::async_close(conn, handler); ran = false; error = {}; response = {}; while (!ran && !io.stopped()) { io.restart(); io.run_one(); } EXPECT(!io.stopped()); EXPECT(error == boost::system::error_code{}); EXPECT(response == ""); // mempool tx in sync// init_server(pool); conn = pull::make(io, boost::asio::ip::tcp::endpoint(local, 10000), "lws.feed.v0.json"); pull::async_handshake(conn, message, handler); lws_test::transaction tx; { std::vector destinations; destinations.emplace_back(); destinations.back().amount = 8000; destinations.back().addr = base.m_account_address; tx = lws_test::make_tx(lest_env, base, destinations, 20, true); const auto hash = get_transaction_hash(tx.tx); pool->reset_txs( lws::mempool::pool_table{ {hash, std::make_shared(cryptonote::transaction{tx.tx}, now)} } ); } ran = false; error = {}; response = {}; while (!ran && !io.stopped()) { io.restart(); io.run_one(); } expected = R"({"scanned_block_height":)" + scan_height + R"(, "start_height":)" + scan_height + R"(, "blockchain_height":)" + scan_height + R"(, "transactions":[{ "hash":")" + epee::to_hex::string(epee::as_byte_span(get_transaction_hash(tx.tx))) + R"(", "prefix_hash":")" + epee::to_hex::string(epee::as_byte_span(get_transaction_prefix_hash(tx.tx))) + R"(", "timestamp":)" + std::to_string(std::chrono::system_clock::to_time_t(now)) + R"(, "fee":12000, "unlock_time":0, "payment_id": "0000000000000000", "mempool":true, "mixin":15, "receives":[ { "amount":8000, "public_key":")" + epee::to_hex::string(epee::as_byte_span(tx.spend_publics.at(0))) + R"(", "index":0, "tx_pub_key":")" + epee::to_hex::string(epee::as_byte_span(tx.pub_keys.at(0))) + R"(", "rct":")" + epee::to_hex::string(epee::as_byte_span(tx.ringct.at(0))) + R"(" } ] }] })"; EXPECT(!io.stopped()); EXPECT(error == boost::system::error_code{}); EXPECT(get_prefix(response) == "tx_sync:"); response.erase(0, std::strlen("tx_sync:")); verify_json(lest_env, "tx_sync non empty", response, expected); const lws::db::transaction_link link{ lws::db::block_id::txpool, crypto::rand() }; const crypto::public_key tx_public = []() { crypto::secret_key secret; crypto::public_key out; crypto::generate_keys(out, secret); return out; }(); const crypto::hash tx_prefix = crypto::rand(); const crypto::public_key pub = crypto::rand(); const crypto::public_key pub2 = crypto::rand(); const rct::key ringct = crypto::rand(); const auto extra = lws::db::extra(lws::db::extra::ringct_output); const auto payment_id_ = crypto::rand(); rpc.push_update( get_full_account(), lws::mempool_receive{ lws::db::output{ link, lws::db::output::spend_meta_{ lws::db::output_id::txpool(), std::uint64_t(40000), std::uint32_t(16), std::uint32_t(2), tx_public }, std::uint64_t(7000), std::uint64_t(4670), tx_prefix, pub, ringct, {0, 0, 0, 0, 0, 0, 0}, lws::db::pack(extra, sizeof(crypto::hash)), payment_id_, std::uint64_t(100), lws::db::address_index{lws::db::major_index(2), lws::db::minor_index(66)} } } ); pull::async_next_message(conn, handler); ran = false; error = {}; response = {}; while (!ran && !io.stopped()) { io.restart(); io.run_one(); } expected = R"({ "hash":")" + epee::to_hex::string(epee::as_byte_span(link.tx_hash)) + R"(", "prefix_hash":")" + epee::to_hex::string(epee::as_byte_span(tx_prefix)) + R"(", "fee":100, "unlock_time":4670, "payment_id":")" + epee::to_hex::string(epee::as_byte_span(payment_id_.long_)) + R"(", "mixin":16, "amount":40000, "public_key":")" + epee::to_hex::string(epee::as_byte_span(pub)) + R"(", "index":2, "tx_pub_key":")" + epee::to_hex::string(epee::as_byte_span(tx_public)) + R"(", "rct":")" + epee::to_hex::string(epee::as_byte_span(ringct)) + R"(", "recipient":{"maj_i":2,"min_i":66} })"; EXPECT(!io.stopped()); EXPECT(error == boost::system::error_code{}); EXPECT(get_prefix(response) == "mempool:"); response.erase(0, std::strlen("mempool:")); verify_json(lest_env, "mempool", response, expected); const lws::db::block_id new_height = lws::db::block_id(to_uint(last_block.id) + 1); const crypto::key_image image = crypto::rand(); { lws::account account = get_full_account(); account.add_out( lws::db::output{ {new_height, link.tx_hash}, lws::db::output::spend_meta_{ lws::db::output_id{0, 100}, std::uint64_t(40000), std::uint32_t(16), std::uint32_t(2), tx_public }, std::uint64_t(7000), std::uint64_t(4670), tx_prefix, pub, ringct, {0, 0, 0, 0, 0, 0, 0}, lws::db::pack(extra, sizeof(crypto::hash)), payment_id_, std::uint64_t(100), lws::db::address_index{lws::db::major_index(2), lws::db::minor_index(66)} } ); account.add_out( lws::db::output{ {new_height, link.tx_hash}, lws::db::output::spend_meta_{ lws::db::output_id{0, 99}, std::uint64_t(30000), std::uint32_t(16), std::uint32_t(1), tx_public }, std::uint64_t(7000), std::uint64_t(4670), tx_prefix, pub2, ringct, {0, 0, 0, 0, 0, 0, 0}, lws::db::pack(extra, sizeof(crypto::hash)), payment_id_, std::uint64_t(100), lws::db::address_index{lws::db::major_index(0), lws::db::minor_index(0)} } ); account.add_spend( lws::db::spend{ {new_height, link.tx_hash}, image, lws::db::output_id{0, 90}, std::uint64_t(7000), std::uint64_t(0), std::uint16_t(16), {0, 0, 0}, std::uint8_t(0), {}, {} } ); const std::array chain{{ last_block.hash, crypto::rand() }}; MONERO_UNWRAP(db.update(last_block.id, epee::to_span(chain), {std::addressof(account), 1}, nullptr)); rpc.push_updates({std::addressof(account), 1}, new_height); } pull::async_next_message(conn, handler); ran = false; error = {}; response = {}; while (!ran && !io.stopped()) { io.restart(); io.run_one(); } std::string new_height_str = std::to_string(to_uint(new_height)); expected = R"({ "scan_start":)" + scan_height + R"(, "scan_end":)" + new_height_str + R"(, "blockchain_height":)" + new_height_str + R"(, "transactions":[{ "hash":")" + epee::to_hex::string(epee::as_byte_span(link.tx_hash)) + R"(", "prefix_hash":")" + epee::to_hex::string(epee::as_byte_span(tx_prefix)) + R"(", "timestamp":7000, "fee":100, "unlock_time":4670, "height":)" + new_height_str + R"(, "payment_id":")" + epee::to_hex::string(epee::as_byte_span(payment_id_.long_)) + R"(", "mixin":16, "receives":[ { "amount":40000, "public_key":")" + epee::to_hex::string(epee::as_byte_span(pub)) + R"(", "index":2, "id": {"legacy": {"amount": 0, "index": 100}}, "tx_pub_key":")" + epee::to_hex::string(epee::as_byte_span(tx_public)) + R"(", "rct":")" + epee::to_hex::string(epee::as_byte_span(ringct)) + R"(", "recipient":{"maj_i":2,"min_i":66} }, { "amount":30000, "public_key":")" + epee::to_hex::string(epee::as_byte_span(pub2)) + R"(", "index":1, "id": {"legacy": {"amount": 0, "index": 99}}, "tx_pub_key":")" + epee::to_hex::string(epee::as_byte_span(tx_public)) + R"(", "rct":")" + epee::to_hex::string(epee::as_byte_span(ringct)) + R"(" } ], "spends":[ { "id": {"legacy": {"amount": 0, "index": 90}}, "key_image":")" + epee::to_hex::string(epee::as_byte_span(image)) + R"(" } ] }] })"; EXPECT(!io.stopped()); EXPECT(error == boost::system::error_code{}); EXPECT(get_prefix(response) == "blocks:"); response.erase(0, std::strlen("blocks:")); verify_json(lest_env, "blocks", response, expected); pull::async_close(conn, handler); ran = false; error = {}; response = {}; while (!ran && !io.stopped()) { io.restart(); io.run_one(); } EXPECT(!io.stopped()); EXPECT(error == boost::system::error_code{}); EXPECT(response == ""); // invalid view key message = R"(login:{"account":{"address":")" + address + R"(","view_key":")" + epee::to_hex::string(epee::as_byte_span(crypto::ec_scalar{})) + R"("}})"; conn = pull::make(io, boost::asio::ip::tcp::endpoint(local, 10000), "lws.feed.v0.json"); pull::async_handshake(conn, message, handler); ran = false; error = {}; response = {}; while (!ran && !io.stopped()) { io.restart(); io.run_one(); } expected = R"({"msg":"Address/viewkey mismatch", "code":3})"; EXPECT(!io.stopped()); EXPECT(error == boost::system::error_code{}); EXPECT(get_prefix(response) == "error:"); response.erase(0, std::strlen("error:")); verify_json(lest_env, "view key error", response, expected); pull::async_next_message(conn, handler); ran = false; error = {}; response = {}; while (!ran && !io.stopped()) { io.restart(); io.run_one(); } EXPECT(!io.stopped()); EXPECT(error == boost::beast::websocket::error::closed); } } }