diff --git a/CMakeLists.txt b/CMakeLists.txt index 299bc98..38fc672 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,6 +81,7 @@ set_property(TARGET monero::libraries PROPERTY common epee mnemonics + qrcodegen wallet_api ) diff --git a/include/lws_frontend.h b/include/lws_frontend.h index 31a9f52..7b5982f 100644 --- a/include/lws_frontend.h +++ b/include/lws_frontend.h @@ -28,11 +28,19 @@ #pragma once +#include #include +#include #include "wallet/api/wallet2_api.h" // monero/src namespace lwsf { + std::string displayAmount(std::uint64_t amount); + std::uint64_t amountFromString(const std::string &amount); + bool addressValid(const std::string &str, Monero::NetworkType nettype); + + std::vector> qrcode(Monero::Wallet const* wal, std::uint32_t major, std::uint32_t minor); + struct WalletManagerFactory { // logging levels for underlying library diff --git a/src/backend.cpp b/src/backend.cpp index 60eb3cb..9e72568 100644 --- a/src/backend.cpp +++ b/src/backend.cpp @@ -28,6 +28,7 @@ #include "backend.h" +#include #include #include "crypto/crypto.h" // monero/src #include "crypto/crypto-ops.h" // monero/src @@ -212,14 +213,20 @@ namespace lwsf { namespace internal { namespace backend template void map_transaction(F& format, T& self) { - //! TODO complete payment_id storage + // make timestamp storage as portable as possible + using time_point = std::chrono::system_clock::time_point; + std::optional timestamp; + if (self.timestamp) + timestamp = boost::numeric_cast(self.timestamp->time_since_epoch().count()); + auto payment_id = wire::variant(std::ref(self.payment_id)); wire::object(format, + WIRE_FIELD(raw_bytes), wire::optional_field("spends", wire::trusted_array(std::ref(self.spends))), wire::optional_field("receives", wire::trusted_array(std::ref(self.receives))), wire::optional_field("transfers", wire::trusted_array(std::ref(self.transfers))), WIRE_FIELD(description), - WIRE_OPTIONAL_FIELD(timestamp), + wire::optional_field("timestamp", std::ref(timestamp)), WIRE_FIELD(amount), WIRE_FIELD(fee), WIRE_OPTIONAL_FIELD(height), @@ -230,8 +237,16 @@ namespace lwsf { namespace internal { namespace backend WIRE_OPTION("payment_id32", crypto::hash, payment_id), WIRE_FIELD(id), WIRE_FIELD(prefix), - WIRE_FIELD(coinbase) + WIRE_FIELD(coinbase), + WIRE_FIELD_DEFAULTED(failed, false) ); + + if constexpr (!std::is_const()) + { + self.timestamp = std::nullopt; + if (timestamp) + self.timestamp = time_point{time_point::duration{boost::numeric_cast(*timestamp)}}; + } } } // anonymous @@ -268,6 +283,10 @@ namespace lwsf { namespace internal { namespace backend wire::optional_field("attributes", wire::trusted_array(std::ref(self.attributes))), WIRE_FIELD(scan_height), WIRE_FIELD(restore_height), +<<<<<<< Updated upstream +======= + WIRE_FIELD(requested_start), +>>>>>>> Stashed changes WIRE_FIELD_DEFAULTED(lookahead, config::default_lookahead), WIRE_FIELD_DEFAULTED(type, Monero::MAINNET), WIRE_FIELD(view), @@ -318,11 +337,12 @@ namespace lwsf { namespace internal { namespace backend return out.recipient; } - rpc::address_meta update_spend(transfer_spend& out, const rpc::transaction_spend& source) + rpc::address_meta update_spend(transfer_spend& out, const rpc::transaction_spend& source, const crypto::public_key& output_pub) { out.amount = std::uint64_t(source.amount); out.sender = source.sender.value_or(rpc::address_meta{}); out.tx_pub = source.tx_pub_key; + out.output_pub = output_pub; return out.sender; } @@ -357,7 +377,10 @@ namespace lwsf { namespace internal { namespace backend std::vector meta; out.id = source.hash; - out.timestamp = source.timestamp; + if (source.timestamp) + out.timestamp = std::chrono::system_clock::from_time_t(*source.timestamp); + else + out.timestamp = std::nullopt; out.fee = std::uint64_t(source.fee.value_or(rpc::uint64_string(0))); out.height = source.height; out.unlock_time = source.unlock_time; @@ -382,7 +405,7 @@ namespace lwsf { namespace internal { namespace backend { /* Frontend will typically know about spend before backend. So only merge and never erase spends. */ - const rpc::address_meta sub = update_spend(out.spends.try_emplace(image).first->second, spend); + const rpc::address_meta sub = update_spend(out.spends.try_emplace(image).first->second, spend, output_pub); if (need_expansion(self, sub)) meta.push_back(sub); total_spent += std::uint64_t(spend.amount); @@ -484,7 +507,10 @@ namespace lwsf { namespace internal { namespace backend if (!rescanning) { iter->second->height = std::nullopt; +<<<<<<< Updated upstream iter->second->timestamp = std::nullopt; +======= +>>>>>>> Stashed changes iter->second->failed = false; } } @@ -716,7 +742,8 @@ namespace lwsf { namespace internal { namespace backend } transaction::transaction() - : spends(), + : raw_bytes(), + spends(), receives(), transfers(), description(), @@ -733,15 +760,33 @@ namespace lwsf { namespace internal { namespace backend failed(false) {} + transaction::transaction(const transaction& rhs) + : raw_bytes(rhs.raw_bytes.clone()), // required because of this + spends(rhs.spends), + receives(rhs.receives), + transfers(rhs.transfers), + description(rhs.description), + timestamp(rhs.timestamp), + height(rhs.height), + amount(rhs.amount), + fee(rhs.fee), + unlock_time(rhs.unlock_time), + direction(rhs.direction), + payment_id(rhs.payment_id), + id(rhs.id), + prefix(rhs.prefix), + coinbase(rhs.coinbase), + failed(rhs.failed) + {} + bool transaction::is_unlocked(const std::uint64_t chain_height, const Monero::NetworkType type) const { - const std::uint64_t the_height = height.value_or(std::numeric_limits::max()); - if(!is_tx_spendtime_unlocked(chain_height, unlock_time, the_height, type)) + if (!height) return false; - - if(the_height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE > chain_height) + if(!is_tx_spendtime_unlocked(chain_height, unlock_time, *height, type)) + return false; + if(*height + CRYPTONOTE_DEFAULT_TX_SPENDABLE_AGE > chain_height) return false; - return true; } @@ -751,6 +796,10 @@ namespace lwsf { namespace internal { namespace backend primary{}, refresh_error(), lookahead_error(default_subaddr_state), +<<<<<<< Updated upstream +======= + import_error(), +>>>>>>> Stashed changes last_sync(), blockchain_height(0), per_byte_fee(0), @@ -783,17 +832,26 @@ namespace lwsf { namespace internal { namespace backend return rct::rct2pk(rct::addKeys(rct::pk2rct(primary.spend.pub), rct::pk2rct(M))); } - std::string wallet::get_spend_address(const rpc::address_meta& index) const + cryptonote::account_public_address wallet::get_spend_account(const rpc::address_meta& index) const { const bool is_subaddress = !index.is_default(); const auto spend_public = get_spend_public(index); const crypto::public_key view_public = is_subaddress ? rct::rct2pk(rct::scalarmultKey(rct::pk2rct(spend_public), rct::sk2rct(primary.view.sec))) : primary.view.pub; + return {spend_public, view_public}; + } + cryptonote::account_keys wallet::get_primary_keys() const + { + return { + {primary.view.pub, primary.spend.pub}, primary.spend.sec, primary.view.sec + }; + } + + std::string wallet::get_spend_address(const rpc::address_meta& index) const + { return cryptonote::get_account_address_as_str( - get_net_type(), - is_subaddress, - cryptonote::account_public_address{spend_public, view_public} + get_net_type(), !index.is_default(), get_spend_account(index) ); } @@ -830,8 +888,17 @@ namespace lwsf { namespace internal { namespace backend boost::unique_lock lock{sync}; { passed_login = false; +<<<<<<< Updated upstream server_lookahead = 0; lookahead_error = {error::subaddr_disabled}; +======= + blockchain_height = 0; + per_byte_fee = 0; + fee_mask = 0; + server_lookahead = 0; + import_error = {}; + lookahead_error = error::subaddr_disabled; +>>>>>>> Stashed changes const rpc::login_request login{ primary.address, primary.view.sec, true, primary.generated_locally }; @@ -905,6 +972,7 @@ namespace lwsf { namespace internal { namespace backend if (now - last_sync < config::refresh_interval_min) return refresh_error; } + last_sync = now; struct call_refreshed { @@ -930,7 +998,10 @@ namespace lwsf { namespace internal { namespace backend if (failed_login) { lock.lock(); +<<<<<<< Updated upstream last_sync = now; +======= +>>>>>>> Stashed changes return refresh_error = failed_login; } } @@ -944,7 +1015,6 @@ namespace lwsf { namespace internal { namespace backend } lock.lock(); - last_sync = now; passed_login = bool(txs_response) && bool(outs_response); // reset state if (!txs_response) @@ -1191,7 +1261,13 @@ namespace lwsf { namespace internal { namespace backend { boost::unique_lock lock{sync}; +<<<<<<< Updated upstream if (primary.requested_start < height) +======= + if (primary.restore_height <= height) + return import_error = {}; + if (import_error && import_error == error::import_pending) +>>>>>>> Stashed changes return import_error; if (!passed_login) @@ -1224,6 +1300,11 @@ namespace lwsf { namespace internal { namespace backend case 0: return import_error = error::import_pending; case 1: +<<<<<<< Updated upstream +======= + if (import->import_fee.value_or(rpc::uint64_string(0)) == rpc::uint64_string(0)) + return import_error = error::import_pending; +>>>>>>> Stashed changes return import_error = error::import_invalid; case 2: break; @@ -1258,14 +1339,22 @@ namespace lwsf { namespace internal { namespace backend primary.addressbook[i] = address_book_entry{std::move(*import->payment_address), std::move(payment_id), std::move(description)}; return import_error = {}; +<<<<<<< Updated upstream +======= + } + + expect> wallet::get_decoys(const rpc::get_random_outs_request& req) + { + auto resp = rpc::invoke(client, req); + if (!resp) + return resp.error(); + return {std::move(resp->amount_outs)}; +>>>>>>> Stashed changes } std::error_code wallet::send_tx(epee::byte_slice tx_bytes) { const rpc::submit_raw_tx_request request{std::move(tx_bytes)}; - auto response = rpc::invoke(client, request); - if (!response) - return response.error(); - return {}; + return rpc::invoke(client, request).error(); } }}} // lwsf // internal // backend diff --git a/src/backend.h b/src/backend.h index 327b22b..19bd0fc 100644 --- a/src/backend.h +++ b/src/backend.h @@ -48,6 +48,7 @@ #include "crypto/crypto.h" // monero/src #include "crypto/hash.h" // monero/src #include "cryptonote_config.h" // monero/src +#include "cryptonote_basic/account.h" // monero/src #include "lwsf_config.h" #include "net/http_client.h" // monero/contrib/epee/include #include "ringct/rctTypes.h" // monero/src @@ -152,6 +153,10 @@ namespace lwsf { namespace internal { namespace backend transfer_out() : address(), amount(0), secret{} {} + + transfer_out(std::string address, const std::uint64_t amount) + : address(std::move(address)), amount(amount), secret{} + {} }; WIRE_DECLARE_OBJECT(transfer_out); @@ -160,20 +165,20 @@ namespace lwsf { namespace internal { namespace backend transaction(); transaction(transaction&&) = default; - transaction(const transaction&) = default; + transaction(const transaction& rhs); transaction& operator=(transaction&&) = default; - transaction& operator=(const transaction&) = default; bool is_unlocked(std::uint64_t chain_height, Monero::NetworkType type) const; /*! flat_map is used here for faster copies/merging. A single allocation is needed in the copy (done every refresh interval), instead of an allocation per key. */ + epee::byte_slice raw_bytes; // for sends held for 100 confirmations boost::container::flat_map spends; boost::container::flat_map receives; //!< Key is output pub std::vector transfers; std::string description; - std::optional timestamp; + std::optional timestamp; //!< guaranteed to be unix epoch in C++20 std::optional height; std::uint64_t amount; std::uint64_t fee; @@ -237,10 +242,21 @@ namespace lwsf { namespace internal { namespace backend wallet(); + // `sync` mutex is NOT acquired for this group + cryptonote::network_type get_net_type() const; crypto::public_key get_spend_public(const rpc::address_meta& index) const; + cryptonote::account_keys get_primary_keys() const; + cryptonote::account_public_address get_spend_account(const rpc::address_meta& index) const; std::string get_spend_address(const rpc::address_meta& index) const; +<<<<<<< Updated upstream +======= + // End GROUP + + // `sync` mutex IS acquired for this group (until end). Do not hold this mutex + +>>>>>>> Stashed changes //! Serialize `this` wallet to msgpack. Locks contents. expect to_bytes() const; @@ -266,6 +282,7 @@ namespace lwsf { namespace internal { namespace backend std::error_code restore_height(const std::uint64_t height); + expect> get_decoys(const rpc::get_random_outs_request& req); std::error_code send_tx(epee::byte_slice tx_bytes); }; }}} // lwsf // internal // backend diff --git a/src/error.cpp b/src/error.cpp index 1133d4d..885d3a5 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -55,6 +55,29 @@ namespace lwsf return "server has subaddresses disabled"; case error::subaddr_local: return "lwsf (local) limits on subaddresses too small"; +<<<<<<< Updated upstream +======= + case error::tx_critical: + return "Tx construction had critical error"; + case error::tx_decoys: + return "Tx construction failed due to invalid decoy response"; + case error::tx_extra: + return "Tx construction failed to generate tx extra"; + case error::tx_failed: + return "Tx construction failed (construct_tx failure)"; + case error::tx_invalid_address: + return "Tx construction was given invalid address"; + case error::tx_long_pid: + return "Tx construction requested with deprecated long payment id"; + case error::tx_low_funds: + return "Tx construction failed due to insufficient funds"; + case error::tx_size_mismatch: + return "Tx construction requested with amount and destination mismatch"; + case error::tx_sweep: + return "Tx construction sweep with multiple destinations"; + case error::tx_two_pid: + return "Tx construction given two payment ids"; +>>>>>>> Stashed changes case error::unexpected_userinfo: return "Unexpected user+pass field in URL"; diff --git a/src/error.h b/src/error.h index c05db9e..4e94748 100644 --- a/src/error.h +++ b/src/error.h @@ -45,6 +45,19 @@ namespace lwsf subaddr_ahead, //!< Server limits on subaddresses affects lookahead subaddr_disabled, //!< Server has subaddresses disabled subaddr_local, //!< Local limits on subaddresses too small +<<<<<<< Updated upstream +======= + tx_critical, //!< Tx creation had critical error + tx_decoys, //!< Tx creation failed due to invalid decoy response + tx_extra, //!< Tx creation failed to generate tx extra + tx_failed, //!< Tx creation failed (contruct_tx failure) + tx_invalid_address, //!< Tx creation was given invalid address + tx_long_pid, //!< Tx creation with long payment_id (deprecated) + tx_low_funds, //!< Tx creation failed due to insufficient funds + tx_size_mismatch, //!< Tx creation mismatch on amounts and destinations + tx_two_pid, //!< Tx creation given two payment ids + tx_sweep, //!< Tx creation requested sweep to multiple dests +>>>>>>> Stashed changes unexpected_userinfo,//!< Unexpected user+pass provided }; diff --git a/src/lwsf_config.h b/src/lwsf_config.h index cfa2ae8..e56928e 100644 --- a/src/lwsf_config.h +++ b/src/lwsf_config.h @@ -40,6 +40,10 @@ namespace lwsf { namespace config std::uint32_t minor = 0; }; +<<<<<<< Updated upstream +======= + constexpr const std::size_t bulletproof_version = 4; +>>>>>>> Stashed changes constexpr const std::chrono::seconds connect_timeout{5}; constexpr const std::chrono::seconds daemon_status_cache{10}; constexpr const std::string_view default_account_name{"Untitled account"}; @@ -47,8 +51,11 @@ namespace lwsf { namespace config constexpr const lookahead default_minimal_lookahead{5, 15}; constexpr const std::string_view default_primary_name{"Primary account"}; constexpr const std::size_t initial_buffer_size = 1024 * 64; // 64 KiB + constexpr const std::size_t max_inputs = 512; + constexpr const std::size_t max_ring_size = 128; constexpr const std::size_t max_txes_in_rpc = 2048; - constexpr const std::uint32_t mixin_default = 16; + constexpr const std::size_t minimum_outputs = 2; + constexpr const std::uint32_t mixin_default = 15; constexpr const std::chrono::seconds refresh_interval{30}; constexpr const std::chrono::seconds refresh_interval_min{5}; constexpr const std::chrono::seconds rpc_timeout{5}; diff --git a/src/pending_transaction.cpp b/src/pending_transaction.cpp index e51c2e2..105680f 100644 --- a/src/pending_transaction.cpp +++ b/src/pending_transaction.cpp @@ -32,13 +32,23 @@ #include "backend.h" #include "byte_slice.h" // monero/contrib/epee/include #include "cryptonote_basic/cryptonote_format_utils.h" // monero/src +#include "error.h" #include "string_tools.h" // monero/contrib/epee/include namespace lwsf { namespace internal { - pending_transaction::pending_transaction(std::shared_ptr wallet, cryptonote::transaction&& source, Priority priority) - : wallet_(std::move(wallet)), source_(std::move(source)), error_(), priority_(priority) + namespace + { + cryptonote::transaction get_tx(expect&& source) + { + if (source) + return {std::move(*source)}; + return {}; + } + } + pending_transaction::pending_transaction(std::shared_ptr wallet, expect source, std::shared_ptr local) + : wallet_(std::move(wallet)), error_(source.error()), local_(std::move(local)) { if (!wallet_) throw std::invalid_argument{"lwsf::internal::pending_transction cannot be given nullptr backend"}; @@ -47,30 +57,58 @@ namespace lwsf { namespace internal pending_transaction::~pending_transaction() {} + int pending_transaction::status() const + { + return error_ ? Status_Error : Status_Ok; + } + std::string pending_transaction::errorString() const { - if (!error_) - return {}; - return error_.message(); + if (error_) + return error_.message(); + return {}; } bool pending_transaction::commit(const std::string &filename, bool) { // saving to file not yet implemented - if (!filename.empty()) + if (error_ || !filename.empty()) return false; - error_ = wallet_->send_tx( - epee::byte_slice{cryptonote::t_serializable_object_to_blob(source_)} - ); + if (local_) + error_ = wallet_->send_tx(local_->raw_bytes.clone()); + else if (!error_) + error_ = error::tx_failed; + + if (!error_ && local_) + { + const boost::lock_guard lock{wallet_->sync}; + auto entry = wallet_->primary.txes.try_emplace(local_->id).first; + if (!entry->second) + entry->second = local_; + } + return !error_; } + std::uint64_t pending_transaction::amount() const + { + if (!local_) + return 0; + return local_->amount; + } + std::uint64_t pending_transaction::fee() const - { return get_tx_fee(source_); } + { + if (!local_) + return 0; + return local_->fee; + } std::vector pending_transaction::txid() const { - return {epee::string_tools::pod_to_hex(get_transaction_hash(source_))}; + if (!local_) + return {}; + return {epee::to_hex::string(epee::as_byte_span(local_->id))}; } }} // lwsf // internal diff --git a/src/pending_transaction.h b/src/pending_transaction.h index ebb9bd4..57e014b 100644 --- a/src/pending_transaction.h +++ b/src/pending_transaction.h @@ -32,22 +32,28 @@ #include #include #include +#include +#include +#include "common/expect.h" // monero/src #include "cryptonote_basic/cryptonote_basic.h" // monero/src #include "wallet/api/wallet2_api.h" // moneor/src namespace lwsf { namespace internal { - namespace backend { struct wallet; } + namespace backend + { + struct transaction; + struct wallet; + } class pending_transaction final : public Monero::PendingTransaction { const std::shared_ptr wallet_; - const cryptonote::transaction source_; std::error_code error_; - const Priority priority_; + const std::shared_ptr local_; public: - pending_transaction(std::shared_ptr wallet, cryptonote::transaction&& source, Priority priority); + pending_transaction(std::shared_ptr wallet, expect source, std::shared_ptr local = {}); pending_transaction(pending_transaction&&) = delete; pending_transaction(const pending_transaction&) = delete; @@ -57,16 +63,16 @@ namespace lwsf { namespace internal pending_transaction& operator=(pending_transaction&&) = delete; pending_transaction& operator=(const pending_transaction&) = delete; - virtual int status() const override final { return priority_; } + virtual int status() const override final; virtual std::string errorString() const override final; virtual bool commit(const std::string &filename = "", bool overwrite = false) override final; - virtual std::uint64_t amount() const = 0; + virtual std::uint64_t amount() const override final; virtual std::uint64_t dust() const override final { return 0; } virtual std::uint64_t fee() const override final; virtual std::vector txid() const override final; virtual std::uint64_t txCount() const override final { return 1; } - virtual std::vector subaddrAccount() const = 0; - virtual std::vector> subaddrIndices() const = 0; + virtual std::vector subaddrAccount() const override final { return {}; } + virtual std::vector> subaddrIndices() const override final { return {}; } /** * @brief multisigSignData @@ -84,12 +90,12 @@ namespace lwsf { namespace internal * pendingTransaction->signMultisigTx(); * pendingTransaction->commit(); */ - virtual std::string multisigSignData() = 0; - virtual void signMultisigTx() = 0; + virtual std::string multisigSignData() override final { return {}; } + virtual void signMultisigTx() override final {} /** * @brief signersKeys * @return vector of base58-encoded signers' public keys */ - virtual std::vector signersKeys() const = 0; + virtual std::vector signersKeys() const override final { return {}; } }; }} // lwsf // internal diff --git a/src/rpc.cpp b/src/rpc.cpp index 8e82b58..e484d05 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp @@ -246,6 +246,39 @@ namespace lwsf { namespace internal { namespace rpc } + void read_bytes(wire::json_reader& source, random_output& self) + { + epee::byte_slice rct; + wire::object(source, + WIRE_FIELD(global_index), + WIRE_FIELD(public_key), + wire::field("rct", std::ref(rct)) + ); + + if (rct.size() < sizeof(self.rct)) + WIRE_DLOG_THROW_(wire::error::schema::fixed_binary); + std::memcpy(std::addressof(self.rct), rct.data(), sizeof(self.rct)); + } + + void read_bytes(wire::json_reader& source, random_outputs& self) + { + using max_ring = wire::max_element_count; + wire::object(source, WIRE_FIELD_ARRAY(outputs, max_ring), WIRE_FIELD(amount)); + } + + void write_bytes(wire::json_writer& dest, const get_random_outs_request& self) + { + using unused = wire::max_element_count<0>; + wire::object(dest, WIRE_FIELD_ARRAY(amounts, unused), WIRE_FIELD(count)); + } + + void read_bytes(wire::json_reader& source, get_random_outs_response& self) + { + using max_inputs = wire::max_element_count; + wire::object(source, WIRE_FIELD_ARRAY(amount_outs, max_inputs)); + } + + namespace { //! A wrapper that simulates an array by only using the first element diff --git a/src/rpc.h b/src/rpc.h index 6752413..6055a08 100644 --- a/src/rpc.h +++ b/src/rpc.h @@ -227,6 +227,48 @@ namespace lwsf { namespace internal { namespace rpc void read_bytes(wire::json_reader&, get_address_txs&); + struct random_output + { + random_output() + : global_index(uint64_string(0)), public_key{}, rct{} + {} + + uint64_string global_index; + rct::key public_key; + rct::key rct; + }; + void read_bytes(wire::json_reader&, random_output&); + + struct random_outputs + { + random_outputs() + : outputs(), amount(uint64_string(0)) + {} + + std::vector outputs; + uint64_string amount; + }; + void read_bytes(wire::json_reader&, random_outputs&); + + struct get_random_outs_request + { + get_random_outs_request() = delete; + + std::vector amounts; + std::uint32_t count; // mixin + }; + void write_bytes(wire::json_writer&, const get_random_outs_request&); + + struct get_random_outs_response + { + get_random_outs_response() = delete; + static constexpr const char* endpoint() noexcept { return "/get_random_outs"; } + + std::vector amount_outs; + }; + void read_bytes(wire::json_reader&, get_random_outs_response&); + + struct subaddrs { constexpr subaddrs() noexcept diff --git a/src/subaddress_account.cpp b/src/subaddress_account.cpp index b8005c1..86be72c 100644 --- a/src/subaddress_account.cpp +++ b/src/subaddress_account.cpp @@ -119,13 +119,6 @@ namespace lwsf { namespace internal balance.unlocked += receive.second.amount; } - if (!tx.second->spends.empty()) - { - auto& balance = balances[tx.second->spends.begin()->second.sender.maj_i]; - balance.unlocked -= tx.second->fee; - balance.total -= tx.second->fee; - } - for (const auto& spend : tx.second->spends) { auto& balance = balances[spend.second.sender.maj_i]; diff --git a/src/transaction_info.cpp b/src/transaction_info.cpp index fc6c1bf..fca5a0a 100644 --- a/src/transaction_info.cpp +++ b/src/transaction_info.cpp @@ -75,6 +75,15 @@ namespace lwsf { namespace internal update_transfers(); } + std::uint64_t transaction_info::blockHeight() const + { + if (data_->height) + return *data_->height; + if (isFailed()) + return 0; + return std::numeric_limits::max(); + } + std::string transaction_info::description() const { const boost::lock_guard lock{wallet_->sync}; @@ -151,6 +160,15 @@ namespace lwsf { namespace internal return epee::to_hex::string(epee::as_byte_span(data_->id)); } + std::time_t transaction_info::timestamp() const + { + if (data_->timestamp) + return std::chrono::system_clock::to_time_t(*data_->timestamp); + if (isFailed()) + return std::numeric_limits::min(); + return std::numeric_limits::max(); + } + std::string transaction_info::paymentId() const { return std::visit(payment_id_to_hex{}, data_->payment_id); diff --git a/src/transaction_info.h b/src/transaction_info.h index 74901f1..aff69d6 100644 --- a/src/transaction_info.h +++ b/src/transaction_info.h @@ -64,7 +64,7 @@ namespace lwsf { namespace internal virtual bool isCoinbase() const override { return data_->coinbase; } virtual std::uint64_t amount() const override { return data_->amount; } virtual std::uint64_t fee() const override { return data_->fee; } - virtual std::uint64_t blockHeight() const override { return data_->height.value_or(std::numeric_limits::max()); } + virtual std::uint64_t blockHeight() const override; virtual std::string description() const override; virtual std::set subaddrIndex() const override; virtual std::uint32_t subaddrAccount() const override; @@ -72,7 +72,7 @@ namespace lwsf { namespace internal virtual std::uint64_t confirmations() const override; virtual std::uint64_t unlockTime() const override { return data_->unlock_time; } virtual std::string hash() const override; - virtual std::time_t timestamp() const override { return data_->timestamp.value_or(std::numeric_limits::max()); } + virtual std::time_t timestamp() const override; virtual std::string paymentId() const override; virtual const std::vector& transfers() const override { return transfers_; } }; diff --git a/src/wallet.cpp b/src/wallet.cpp index b632741..f7f3114 100644 --- a/src/wallet.cpp +++ b/src/wallet.cpp @@ -1,21 +1,21 @@ // Copyright (c) 2014-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 @@ -43,6 +43,7 @@ #include "address_book.h" #include "backend.h" #include "cryptonote_basic/cryptonote_format_utils.h" // monero/src +#include "cryptonote_core/cryptonote_tx_utils.h" // monero/src #include "error.h" #include "hex.h" // monero/contrib/epee/include #include "lwsf_config.h" @@ -51,6 +52,7 @@ #include "net/parse.h" // monero/src #include "net/socks.h" // monero/src #include "net/socks_connect.h" // monero/src +#include "pending_transaction.h" #include "subaddress_account.h" #include "subaddress_minor.h" #include "transaction_history.h" @@ -58,8 +60,7 @@ #include "wire/msgpack.h" namespace lwsf { namespace internal -{ - +{ namespace { //! The stored wallet file always has this at beginning @@ -76,6 +77,94 @@ namespace lwsf { namespace internal } }; + std::uint64_t calculate_fee_from_weight(const std::uint64_t base_fee, const std::uint64_t weight, const std::uint64_t fee_quantization_mask) + { + uint64_t fee = weight * base_fee; + fee = (fee + fee_quantization_mask - 1) / fee_quantization_mask * fee_quantization_mask; + return fee; + } + + std::size_t estimate_rct_tx_size(const std::size_t n_inputs, const std::size_t n_outputs, const std::uint32_t mixin, const std::size_t extra_size) + { + std::size_t size = 0; + + // tx prefix + + // first few bytes + size += 1 + 6; + + // vin + size += n_inputs * (1+6+(mixin+1)*2+32); + + // vout + size += n_outputs * (6+32); + + // extra + size += extra_size; + + // rct signatures + + // type + size += 1; + + // rangeSigs + //if (bulletproof || bulletproof_plus) + { + std::size_t log_padded_outputs = 0; + while ((1< 2) + { + const uint64_t bp_base = (32 * (/*(bulletproof_plus ?*/ 6 /* : 9)*/ + 7 * 2)) / 2; // notional size of a 2 output proof, normalized to 1 proof (ie, divided by 2) + size_t log_padded_outputs = 2; + while ((1<get_spend_address({0, 0}) != address_string) 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) @@ -716,7 +805,7 @@ namespace lwsf { namespace internal const boost::lock_guard lock{data_->sync}; return data_->primary.language; } - + void wallet::setSeedLanguage(const std::string &arg) { const boost::lock_guard lock{data_->sync}; @@ -758,7 +847,7 @@ namespace lwsf { namespace internal errorString.clear(); } } - + bool wallet::setPassword(const std::string &password) { password_ = password; @@ -891,7 +980,7 @@ namespace lwsf { namespace internal const boost::lock_guard lock{data_->sync}; return data_->primary.restore_height; } - + void wallet::setSubaddressLookahead(uint32_t major, uint32_t minor) { queue_work([this, major, minor] () { return data_->set_lookahead(major, minor); }); @@ -956,9 +1045,6 @@ namespace lwsf { namespace internal balance += receive.second.amount; } - if (!tx.second->spends.empty() && tx.second->spends.begin()->second.sender.maj_i == accountIndex) - balance -= tx.second->fee; - for (const auto& spend : tx.second->spends) { if (spend.second.sender.maj_i == accountIndex) @@ -970,7 +1056,7 @@ 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; @@ -988,9 +1074,6 @@ namespace lwsf { namespace internal balance += receive.second.amount; } - if (!tx.second->spends.empty() && tx.second->spends.begin()->second.sender.maj_i == accountIndex) - balance -= tx.second->fee; - for (const auto& spend : tx.second->spends) { if (spend.second.sender.maj_i == accountIndex) @@ -1010,14 +1093,14 @@ namespace lwsf { namespace internal uint64_t wallet::daemonBlockChainHeight() const { const boost::unique_lock lock{data_->sync}; - return data_->blockchain_height; + return data_->blockchain_height; } uint64_t wallet::daemonBlockChainTargetHeight() const { return daemonBlockChainHeight(); } bool wallet::synchronized() const - { + { const boost::lock_guard lock{data_->sync}; return data_->blockchain_height == data_->primary.scan_height; } @@ -1025,8 +1108,10 @@ namespace lwsf { namespace internal void wallet::startRefresh() { const boost::lock_guard lock{thread_sync_}; + state old_state = state::stop; { const boost::lock_guard lock2{refresh_sync_}; + old_state = thread_state_; const bool no_refresh = refresh_interval_ <= std::chrono::milliseconds{0}; if (no_refresh) @@ -1035,17 +1120,17 @@ namespace lwsf { namespace internal if (!mandatory_refresh_ && work_queue_.empty()) return; } - - const state old_state = thread_state_; - thread_state_ = no_refresh ? state::stop : state::run; - if (old_state == state::stop) - { - if (thread_.joinable()) - thread_.join(); - thread_ = boost::thread{[this] () { this->refresh_loop(); }}; - } + else + thread_state_ = state::run; } refresh_notify_.notify_all(); + + if (old_state == state::stop) + { + if (thread_.joinable()) + thread_.join(); + thread_ = boost::thread{[this] () { this->refresh_loop(); }}; + } } void wallet::pauseRefresh() @@ -1149,6 +1234,472 @@ namespace lwsf { namespace internal set_critical(std::runtime_error{"setSubaddressLabel failed, " + std::to_string(accountIndex) + "," + std::to_string(addressIndex) + " does not exist"}); } + Monero::PendingTransaction* wallet::createTransactionMultDest(const std::vector &dst_addr, const std::string &payment_id, + Monero::optional> amount, uint32_t mixin_count, + Monero::PendingTransaction::Priority priority, + uint32_t subaddr_account, + std::set subaddr_indices) + { + if (!mixin_count) + mixin_count = mixin_; + + std::unique_ptr out; + const auto tx_error = [this] (std::error_code error) + { + return std::make_unique(data_, std::move(error)); + }; + + try + { + /* This design is funky, but guards against exceptions in destructors + of stack elements. Everything on the stack (except for arguments) is + cleaned up before the uniue_ptr is released. */ + out = [&, this] () + { + if (!amount && dst_addr.size() > 1) + return tx_error(error::tx_sweep); + if (amount && amount->size() != dst_addr.size()) + return tx_error(error::tx_size_mismatch); + if (!payment_id.empty()) + return tx_error(error::tx_long_pid); + + cryptonote::network_type ctype{}; + Monero::NetworkType mtype{}; + std::uint64_t per_byte_fee = 0; + std::uint64_t fee_mask = 0; + std::string change_address; + cryptonote::account_keys keys{}; + cryptonote::account_public_address change_account{}; + std::unordered_map, std::shared_ptr>> unspent; + { + data_->refresh(); // get latest outputs, block height, and fee info + + const boost::lock_guard lock{data_->sync}; + per_byte_fee = data_->per_byte_fee; + fee_mask = data_->fee_mask; + + if (!per_byte_fee || !fee_mask || !data_->passed_login) + return tx_error(data_->refresh_error); + + 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; + + if (!amount && subaddr_indices.empty()) + { + const std::uint32_t max = data_->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) + { + if (!tx.second->is_unlocked(height, mtype)) + continue; // ignore locked amounts + + for (const auto& receive : tx.second->receives) + { + /* Subtract by max for map below - that map will sort by amount + then reverse global index. This will have a bias towards newer + outputs, which is standard behavior. */ + static constexpr std::uint64_t max = + std::numeric_limits::max(); + const std::uint64_t amount = receive.second.amount; + const rpc::address_meta source = receive.second.recipient; + if (amount && source.maj_i == subaddr_account && (subaddr_indices.empty() || subaddr_indices.count(source.min_i))) + unspent.try_emplace(receive.first).first->second = {{amount, max - receive.second.global_index}, tx.second}; + } + } + + for (const auto& tx : data_->primary.txes) + { + for (const auto& spend : tx.second->spends) + unspent.erase(spend.second.output_pub); + } + } + + // actually sorted by amount then reverse index (see comment above) + std::map, std::pair>> unspent_by_amount_then_index; + for (const auto& tx : unspent) + unspent_by_amount_then_index.try_emplace(tx.second.first).first->second = {tx.first, tx.second.second}; + unspent.clear(); // free up some memory + + const bool subtract_from_dest = !amount; + if (subtract_from_dest) + { + std::uint64_t total_unspent = 0; + for (const auto& tx : unspent) + { + const auto output = tx.second.second->receives.find(tx.first); + if (output == tx.second.second->receives.end()) + throw std::logic_error{"Expected output pub"}; + total_unspent += output->second.amount; + } + amount = std::vector{total_unspent}; + } + + if (amount->size() < dst_addr.size()) + throw std::logic_error{"Sanity check failed: address vec <= amount vec"}; + + std::string extra_nonce; + std::vector dests; + std::multiset> transfers; + for (std::size_t i = 0; i < dst_addr.size(); ++i) + { + cryptonote::address_parse_info info{}; + if (!cryptonote::get_account_address_from_str(info, ctype, dst_addr.at(i))) + return tx_error(error::tx_invalid_address); + + if (info.has_payment_id) + { + if (!extra_nonce.empty()) + return tx_error(error::tx_two_pid); + cryptonote::set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, info.payment_id); + } + + transfers.insert({amount->at(i), dst_addr.at(i)}); + + auto& dest = dests.emplace_back(); + dest.original = dst_addr.at(i); + dest.addr = info.address; + dest.amount = amount->at(i); + dest.is_subaddress = info.is_subaddress; + dest.is_integrated = info.has_payment_id; + } + + for (std::size_t i = dst_addr.size(); i < amount->size(); ++i) + { + auto& dest = dests.emplace_back(); + dest.original = change_address; + dest.addr = change_account; + dest.amount = amount->at(i); + dest.is_subaddress = subaddr_account; + dest.is_integrated = false; + } + + std::vector extra; + if (!extra_nonce.empty() && !cryptonote::add_extra_nonce_to_tx_extra(extra, extra_nonce)) + return tx_error(error::tx_extra); + + std::vector>> spending; + { + std::uint64_t fee = 0; + std::uint64_t remaining = + std::accumulate(amount->begin(), amount->end(), std::uint64_t(0)); + + while (bool(remaining + fee)) + { + if (unspent_by_amount_then_index.empty()) + { + if (subtract_from_dest && !remaining) + break; + return tx_error(error::tx_low_funds); + } + + /* Check if we can omit change address for more efficiency. We + must have at least 2 outputs for uniformity/privacy. */ + if (1 < dests.size()) + { + fee = estimate_fee(per_byte_fee, spending.size() + 1, dests.size(), mixin_count, extra.size(), fee_mask); + const std::uint64_t needed = remaining + fee; + const auto output = unspent_by_amount_then_index.lower_bound({needed, 0}); + if (output != unspent_by_amount_then_index.end() && needed == output->first.first) + { + remaining = 0; + break; + } + } + + // check + fee = estimate_fee(per_byte_fee, spending.size() + 1, dests.size() + 1, mixin_count, extra.size(), fee_mask); + + const std::uint64_t needed = remaining + fee; + auto output = unspent_by_amount_then_index.lower_bound({needed, 0}); + if (output == unspent_by_amount_then_index.end()) + --output; + + const std::uint64_t this_amount = output->first.first; + const bool complete = needed <= this_amount; + spending.emplace_back(output->second.first, output->second.second); + unspent_by_amount_then_index.erase(output); + + remaining -= this_amount; + if (complete) + break; + } + + if (subtract_from_dest) + { + if (dests.size() != 1) + throw std::logic_error{"Sanity check: subtract_from_dest && dest.size() == 1"}; + if (dests.back().amount < fee) + return tx_error(error::tx_low_funds); + dests.back().amount -= fee; + fee = 0; + } + + const std::uint64_t change = (std::uint64_t(0) - remaining) - fee; + if (change || dests.size() == 1) + { + auto& change_dest = dests.emplace_back(); + change_dest.original = change_address; + change_dest.addr = change_account; + change_dest.amount = change; + change_dest.is_subaddress = bool(subaddr_account); + change_dest.is_integrated = false; + } + + if (dests.size() < config::minimum_outputs) + throw std::logic_error{"Sanity check: config::minium_outputs <= dests.size()"}; + + remaining = 0; + for (const auto& source : spending) + { + const auto output = source.second->receives.find(source.first); + if (output == source.second->receives.end()) + throw std::logic_error{"Failed sanity check: source output does not exist"}; + remaining += output->second.amount; + } + + for (const auto& dest : dests) + remaining -= dest.amount; + + if (fee != remaining) + throw std::logic_error{"Sanity check: inputs.amounts == outputs.amounts + fee"}; + } + unspent_by_amount_then_index.clear(); // free some memory + + const auto rct_amount = [] (const backend::transfer_in& source) + { + return source.rct_mask ? 0 : source.amount; + }; + + std::vector decoys; + { + rpc::get_random_outs_request req{}; + req.count = mixin_count; + 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); + if (!resp) + return tx_error(resp.error()); + decoys = std::move(*resp); + } + + struct by_amount + { + bool operator()(const rpc::random_outputs& lhs, const rpc::random_outputs& rhs) const noexcept + { + return lhs.amount < rhs.amount; + } + bool operator()(const rpc::random_outputs& lhs, const std::uint64_t rhs) const noexcept + { + return lhs.amount < rpc::uint64_string(rhs); + } + }; + std::sort(decoys.begin(), decoys.end(), by_amount{}); + + std::unordered_map subs; + std::vector sources; + for (const auto& spend : spending) + { + 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}}); + } + + auto& entry = sources.emplace_back(); + if (source.rct_mask) + entry.outputs.emplace_back(source.global_index, rct::ctkey{rct::pk2rct(spend.first), rct::commit(source.amount, *source.rct_mask)}); + else + entry.push_output(source.global_index, spend.first, source.amount); + + { + const std::uint64_t amount = rct_amount(source); + const auto ring = std::lower_bound(decoys.begin(), decoys.end(), amount, by_amount{}); + if (ring == decoys.end() || ring->amount != rpc::uint64_string(amount)) + return tx_error(error::tx_decoys); + + for (const auto& decoy : ring->outputs) + entry.outputs.emplace_back(std::uint64_t(decoy.global_index), rct::ctkey{decoy.public_key, decoy.rct}); + + decoys.erase(ring); + } + + entry.real_output_in_tx_index = source.index; + entry.real_out_tx_key = source.tx_pub; + entry.amount = source.amount; + entry.rct = bool(source.rct_mask); + if (entry.rct) + entry.mask = *source.rct_mask; + } + + const auto ring_sort = [] (const auto& lhs, const auto& rhs) + { + return lhs.first < rhs.first; + }; + const auto ring_compare = [] (const auto& lhs, const auto& rhs) + { + return lhs.first == rhs.first; + }; + + std::shuffle(sources.begin(), sources.end(), crypto::random_device{}); + std::shuffle(dests.begin(), dests.end(), crypto::random_device{}); + for (auto& source : sources) + { + if (source.outputs.empty()) + throw std::runtime_error{"Sanity check: ring is !empty"}; + + const std::uint64_t real_source = source.outputs.front().first; + std::sort(source.outputs.begin(), source.outputs.end(), ring_sort); + if (std::unique(source.outputs.begin(), source.outputs.end(), ring_compare) != source.outputs.end()) + throw std::logic_error{"Sanity check: each ring destination is unique"}; + + for (std::size_t i = 0; i < source.outputs.size(); ++i) + { + if (source.outputs.at(i).first == real_source) + { + source.real_output = i; + break; + } + } + } + + static constexpr const rct::RCTConfig config{ + rct::RangeProofPaddedBulletproof, config::bulletproof_version + }; + + crypto::secret_key tx_key; + std::vector tx_keys; + cryptonote::transaction tx; + if (!cryptonote::construct_tx_and_get_tx_key(keys, subs, sources, dests, change_account, extra, tx, tx_key, tx_keys, true /* rct */, config, true /* view_tags */)) + return tx_error(error::tx_failed); + + auto details = std::make_shared(); + details->raw_bytes = epee::byte_slice{cryptonote::t_serializable_object_to_blob(tx)}; + details->timestamp = std::chrono::system_clock::now(); + details->amount = std::accumulate(amount->begin(), amount->end(), std::uint64_t(0)); + details->fee = get_tx_fee(tx); + details->direction = Monero::TransactionInfo::Direction_Out; + if (extra_nonce.size() == sizeof(crypto::hash8)) + { + crypto::hash8 pid{}; + std::memcpy(std::addressof(pid), extra_nonce.data(), sizeof(pid)); + details->payment_id = pid; + } + get_transaction_hash(tx, details->id); + get_transaction_prefix_hash(tx, details->prefix); + + if (sources.size() != tx.vin.size()) + throw std::logic_error{"Sanity check: sources.size() == tx.vin.size()"}; + + for (std::size_t i = 0; i < sources.size(); ++i) + { + const auto& source = sources.at(i); + auto spend = details->spends.try_emplace(boost::get(tx.vin.at(i)).k_image).first; + spend->second.amount = source.amount; + spend->second.output_pub = rct2pk(source.outputs.at(source.real_output).second.dest); + + const auto elem = std::find_if(spending.begin(), spending.end(), [&] (const auto& e) { + return e.first == spend->second.output_pub; + }); + if (elem == spending.end()) + throw std::logic_error{"Sanity check spend not found"}; + + const auto& base = elem->second->receives.at(spend->second.output_pub); + spend->second.sender = base.recipient; + spend->second.tx_pub = base.tx_pub; + } + + if (dests.size() != tx.vout.size()) + throw std::logic_error{"Sanity check: dests.size() == tx.vout.size()"}; + if (dests.size() != tx.rct_signatures.ecdhInfo.size()) + throw std::logic_error{"Sanity check: dests.size() == ecdhInfo.size()"}; + if (!tx_keys.empty() && tx_keys.size() != dests.size()) + throw std::logic_error{"Sanity check tx_keys.empty() || tx_keys.size() == dest.size()"}; + + struct get_output_pub + { + crypto::public_key operator()(const cryptonote::txout_to_script&) const noexcept { return {}; } + crypto::public_key operator()(const cryptonote::txout_to_scripthash&) const noexcept { return {}; } + crypto::public_key operator()(const cryptonote::txout_to_key& src) const noexcept { return src.key; } + crypto::public_key operator()(const cryptonote::txout_to_tagged_key& src) const noexcept { return src.key; } + }; + + for (std::size_t i = 0; i < dests.size(); ++i) + { + const auto& dest = dests.at(i); + if (dest.original != change_address) + continue; + + auto receive = details->receives.try_emplace( + boost::apply_visitor(get_output_pub{}, tx.vout.at(i).target) + ).first; + + receive->second.global_index = std::numeric_limits::max(); + receive->second.amount = dest.amount; + receive->second.recipient = {subaddr_account, 0}; + receive->second.index = i; + receive->second.rct_mask = tx.rct_signatures.ecdhInfo.at(i).mask; + + if (tx_keys.empty()) + crypto::secret_key_to_public_key(tx_key, receive->second.tx_pub); + else + crypto::secret_key_to_public_key(tx_keys.at(i), receive->second.tx_pub); + } + + if (!tx_keys.empty()) + { + if (tx_keys.size() != dests.size()) + throw std::logic_error{"Failed sanity check: tx_keys.size() == dests.size()"}; + + for (std::size_t i = 0; i < tx_keys.size(); ++i) + { + const auto& dest = dests.at(i); + const auto is_transfer = transfers.find({dest.amount, dest.original}); + if (is_transfer == transfers.end()) + continue; + + details->transfers.emplace_back(dest.original, dest.amount).secret = tx_keys.at(i); + transfers.erase(is_transfer); + } + + if (!transfers.empty()) + throw std::logic_error{"Sanity check: missing secret key for outgoing transfer"}; + } + else + { + for (auto& transfer : transfers) + details->transfers.emplace_back(transfer.second, transfer.first).secret = tx_key; + transfers.clear(); + } + + return std::make_unique(data_, std::move(tx), std::move(details)); + }(); + } + catch (const std::exception & e) + { + set_critical(e); + out = tx_error(error::tx_critical); + } + + return out.release(); + } + + Monero::PendingTransaction* wallet::createTransaction(const std::string &dst_addr, const std::string &payment_id, + std::optional amount, uint32_t mixin_count, + Monero::PendingTransaction::Priority priority, + uint32_t subaddr_account, + std::set subaddr_indices) + { + return createTransactionMultDest({dst_addr}, payment_id, amount ? std::optional>{{*amount}} : std::nullopt, mixin_count, priority, subaddr_account, subaddr_indices); + } + void wallet::disposeTransaction(Monero::PendingTransaction * t) { delete t; @@ -1159,12 +1710,12 @@ namespace lwsf { namespace internal { throw std::runtime_error{"Not Implemented yet"}; } - + bool wallet::exportKeyImages(const std::string &filename, bool all) { throw std::runtime_error{"exportKeyImages not implemented"}; } - + bool wallet::importKeyImages(const std::string &filename) { throw std::runtime_error{"importKeyImages not implemented"}; @@ -1204,7 +1755,7 @@ namespace lwsf { namespace internal if (!history_) history_ = std::make_unique(data_); return history_.get(); - } + } Monero::SubaddressAccount* wallet::subaddressAccount() { diff --git a/src/wallet.h b/src/wallet.h index cebd56d..97cf824 100644 --- a/src/wallet.h +++ b/src/wallet.h @@ -340,12 +340,12 @@ namespace internal * @brief rescanBlockchain - rescans the wallet, updating transactions from daemon * @return - true if refreshed successfully; */ - virtual bool rescanBlockchain() override { /* TODO */ return false; } + virtual bool rescanBlockchain() override { return refresh(); } /** * @brief rescanBlockchainAsync - rescans wallet asynchronously, starting from genesys */ - virtual void rescanBlockchainAsync() override { /* TODO */ } + virtual void rescanBlockchainAsync() override { refreshAsync(); } /** * @brief setAutoRefreshInterval - setup interval for automatic refresh. @@ -468,7 +468,7 @@ namespace internal Monero::optional> amount, uint32_t mixin_count, Monero::PendingTransaction::Priority = Monero::PendingTransaction::Priority_Low, uint32_t subaddr_account = 0, - std::set subaddr_indices = {}) override { /* TODO */ return nullptr; } + std::set subaddr_indices = {}) override; /*! * \brief createTransaction creates transaction. if dst_addr is an integrated address, payment_id is ignored @@ -487,7 +487,7 @@ namespace internal std::optional amount, uint32_t mixin_count, Monero::PendingTransaction::Priority = Monero::PendingTransaction::Priority_Low, uint32_t subaddr_account = 0, - std::set subaddr_indices = {}) override { /* TODO */ return nullptr; } + std::set subaddr_indices = {}) override; /*! * \brief createSweepUnmixableTransaction creates transaction with unmixable outputs. diff --git a/src/wallet_manager.cpp b/src/wallet_manager.cpp index cf4f668..8f5e05c 100644 --- a/src/wallet_manager.cpp +++ b/src/wallet_manager.cpp @@ -36,11 +36,13 @@ #include #include "backend.h" #include "common/dns_utils.h" // monero/src +#include "cryptonote_basic/cryptonote_format_utils.h" // monero/src #include "lwsf_config.h" #include "lws_frontend.h" #include "net/http_client.h" // monero/contrib/epee/include #include "net/parse.h" // monero/src #include "net/socks_connect.h" // monero/src +#include "QrCode.hpp" // monero/src/external #include "rpc.h" #include "wallet.h" #include "wallet/api/wallet2_api.h" // monero/src @@ -359,23 +361,10 @@ namespace lwsf std::string resolveOpenAlias(const std::string &address, bool &dnssec_valid) const override { - static constexpr const std::string_view prefix{"oa1:xmr "}; - std::string real_address = - tools::DNSResolver::instance().get_dns_format_from_oa_address(address); - - bool dnssec_available = false; - std::vector records = - tools::DNSResolver::instance().get_txt_record(real_address, dnssec_available, dnssec_valid); - for (std::string_view record : records) - { - if (record.substr(0, prefix.size()) == prefix) - { - record = record.substr(prefix.size()); - return std::string{record.data(), record.size()}; - } - } - - return {}; + std::vector addresses = tools::dns_utils::addresses_from_url(address, dnssec_valid); + if (addresses.empty()) + return {}; + return {std::move(addresses.front())}; } //! checks for an update and returns version, hash and url @@ -399,6 +388,49 @@ namespace lwsf }; } // internal + std::string displayAmount(std::uint64_t amount) + { + return cryptonote::print_money(amount); + } + + std::uint64_t amountFromString(const std::string &amount) + { + std::uint64_t result = 0; + cryptonote::parse_amount(result, amount); + return result; + } + + bool addressValid(const std::string &str, Monero::NetworkType nettype) + { + cryptonote::address_parse_info info; + return get_account_address_from_str(info, static_cast(nettype), str); + } + + std::vector> qrcode(Monero::Wallet const* const wal, const std::uint32_t major, const std::uint32_t minor) + { + if (!wal) + return {}; + + const std::string uri = "monero:" + wal->address(major, minor); + const qrcodegen::QrCode qr = qrcodegen::QrCode::encodeText(uri.c_str(), qrcodegen::QrCode::Ecc::LOW); + const int size = qr.getSize(); + if (size < 0 || std::numeric_limits::max() < size) + return {}; + + std::vector> out; + out.resize(size); + for (std::size_t y = 0; y < size; ++y) + { + auto& row = out.at(y); + row.resize(size); + + for (std::size_t x = 0; x < size; ++x) + row.at(x) = qr.getModule(x, y); + } + + return out; + } + Monero::WalletManager* WalletManagerFactory::getWalletManager() { return new internal::wallet_manager{};