mirror of
https://github.com/wownero/lwsf-wow.git
synced 2026-10-17 15:31:32 +02:00
Add support for storing pending txes to file
This commit is contained in:
@@ -39,6 +39,7 @@ set(lwsf_sources
|
||||
subaddress_minor.cpp
|
||||
transaction_history.cpp
|
||||
transaction_info.cpp
|
||||
utils/encrypted_file.cpp
|
||||
wallet.cpp
|
||||
wallet_manager.cpp
|
||||
)
|
||||
@@ -50,6 +51,8 @@ set(lwsf_headers
|
||||
rpc.h
|
||||
transaction_history.h
|
||||
transaction_info.h
|
||||
utils/encrypted_file.h
|
||||
utils/numeric.h
|
||||
wallet.h
|
||||
)
|
||||
|
||||
|
||||
@@ -41,6 +41,8 @@ namespace lwsf
|
||||
return "Configuration issue";
|
||||
case error::create:
|
||||
return "Account creation not possible";
|
||||
case error::decryption:
|
||||
return "Unable to decrypt file (likely bad password)";
|
||||
case error::import_fee:
|
||||
return "Import/restore from height needs payment, see address book";
|
||||
case error::import_invalid:
|
||||
@@ -57,6 +59,8 @@ namespace lwsf
|
||||
return "lwsf (local) limits on subaddresses too small";
|
||||
case error::unexpected_userinfo:
|
||||
return "Unexpected user+pass field in URL";
|
||||
case error::unexpected_nullptr:
|
||||
return "Unexpected nullptr";
|
||||
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace lwsf
|
||||
approval, //!< Account needs approval
|
||||
configuration, //!< Bad Configuration
|
||||
create, //!< Account creation not possible
|
||||
decryption, //!< Failed to decrypt data
|
||||
import_fee, //!< `import_wallet_request` has fee
|
||||
import_invalid, //!< `import_wallet_request` has invalid fee
|
||||
import_pending, //!< `import_wallet_request` not fulfilled/approved
|
||||
@@ -46,6 +47,7 @@ namespace lwsf
|
||||
subaddr_disabled, //!< Server has subaddresses disabled
|
||||
subaddr_local, //!< Local limits on subaddresses too small
|
||||
unexpected_userinfo,//!< Unexpected user+pass provided
|
||||
unexpected_nullptr, //!< Expected non-nullptr
|
||||
};
|
||||
|
||||
//! \return Error message string.
|
||||
|
||||
@@ -48,6 +48,7 @@ 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_file_read_size = 50 * 1024 * 1024; // 50 MiB
|
||||
constexpr const std::size_t max_inputs_in_rpc = 512;
|
||||
constexpr const std::size_t max_outputs_in_construction = 16;
|
||||
constexpr const std::size_t max_ring_size_in_rpc = 128;
|
||||
|
||||
+127
-11
@@ -28,6 +28,8 @@
|
||||
|
||||
#include "pending_transaction.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include "backend.h"
|
||||
#include "byte_slice.h" // monero/contrib/epee/include
|
||||
@@ -35,12 +37,59 @@
|
||||
#include "error.h"
|
||||
#include "numeric.h"
|
||||
#include "string_tools.h" // monero/contrib/epee/include
|
||||
|
||||
#include "utils/encrypted_file.h"
|
||||
#include "wire.h"
|
||||
#include "wire/msgpack.h"
|
||||
#include "wire/wrapper/trusted_array.h"
|
||||
|
||||
namespace lwsf { namespace internal
|
||||
{
|
||||
namespace backend
|
||||
{
|
||||
static void read_bytes(wire::reader& src, std::shared_ptr<transaction>& dest)
|
||||
{
|
||||
if (!dest)
|
||||
dest = std::make_shared<backend::transaction>();
|
||||
read_bytes(src, *dest);
|
||||
}
|
||||
|
||||
static void write_bytes(wire::writer& dest, const std::shared_ptr<transaction>& src)
|
||||
{
|
||||
if (!src)
|
||||
throw std::logic_error{"Unexpected nullptr in write_bytes"};
|
||||
write_bytes(dest, *src);
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr std::string_view tx_file_magic{"lwsf-tx-1.0"};
|
||||
|
||||
struct txes_file
|
||||
{
|
||||
std::vector<std::shared_ptr<backend::transaction>> txes;
|
||||
};
|
||||
|
||||
void read_bytes(wire::reader& src, std::shared_ptr<backend::transaction>& dest)
|
||||
{
|
||||
if (!dest)
|
||||
dest = std::make_shared<backend::transaction>();
|
||||
read_bytes(src, *dest);
|
||||
}
|
||||
void write_bytes(wire::writer& dest, const std::shared_ptr<backend::transaction> src)
|
||||
{
|
||||
if (!src)
|
||||
WIRE_DLOG_THROW_(error::unexpected_nullptr);
|
||||
write_bytes(dest, *src);
|
||||
}
|
||||
|
||||
template<typename F, typename T>
|
||||
void txes_map(F& format, T& self)
|
||||
{
|
||||
wire::object(format, wire::field("txes", wire::trusted_array(std::ref(self.txes))));
|
||||
}
|
||||
WIRE_DEFINE_OBJECT(txes_file, txes_map);
|
||||
|
||||
cryptonote::transaction get_tx(expect<cryptonote::transaction>&& source)
|
||||
{
|
||||
if (source)
|
||||
@@ -48,31 +97,50 @@ namespace lwsf { namespace internal
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pending_transaction::pending_transaction(std::shared_ptr<backend::wallet> wallet, std::string error, std::vector<std::shared_ptr<backend::transaction>> local)
|
||||
: wallet_(std::move(wallet)), local_(std::move(local)), error_(std::move(error))
|
||||
{
|
||||
if (!wallet_)
|
||||
throw std::invalid_argument{"lwsf::internal::pending_transaction cannot be given nullptr backend"};
|
||||
|
||||
for (const auto& e : local)
|
||||
for (const auto& e : local_)
|
||||
{
|
||||
if (!e)
|
||||
throw std::invalid_argument{"lwsf::internal::pending_transaction cannot be given nullptr backend::transaction"};
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<pending_transaction> pending_transaction::load_from_file(std::shared_ptr<backend::wallet> wallet, const std::string& filename)
|
||||
{
|
||||
if (!wallet)
|
||||
throw std::invalid_argument{"lwsf::internal::pending_transaction::load_from_file cannot be given nullptr backend"};
|
||||
|
||||
epee::byte_slice file = try_load(filename, tx_file_magic);
|
||||
if (file.empty())
|
||||
return std::make_unique<pending_transaction>(wallet, "Invalid path/file for tx");
|
||||
|
||||
expect<epee::byte_slice> payload = epee::byte_slice{};
|
||||
{
|
||||
const boost::lock_guard<boost::mutex> lock{wallet->sync};
|
||||
payload = decrypt(std::move(file), epee::as_byte_span(unwrap(unwrap(wallet->primary.view.sec))));
|
||||
}
|
||||
if (!payload)
|
||||
return std::make_unique<pending_transaction>(wallet, payload.error().message());
|
||||
|
||||
txes_file dest{};
|
||||
if (std::error_code error = wire::msgpack::from_bytes(std::move(*payload), dest))
|
||||
return std::make_unique<pending_transaction>(wallet, error.message());
|
||||
return std::make_unique<pending_transaction>(wallet, "", std::move(dest.txes));
|
||||
}
|
||||
|
||||
pending_transaction::~pending_transaction()
|
||||
{}
|
||||
|
||||
int pending_transaction::status() const
|
||||
bool pending_transaction::send()
|
||||
{
|
||||
return error_.empty() ? Status_Ok : Status_Error;
|
||||
}
|
||||
|
||||
bool pending_transaction::commit(const std::string &filename, bool)
|
||||
{
|
||||
// saving to file not yet implemented
|
||||
if (!error_.empty() || !filename.empty())
|
||||
if (!error_.empty())
|
||||
return false;
|
||||
|
||||
for (auto e : local_)
|
||||
@@ -88,10 +156,58 @@ namespace lwsf { namespace internal
|
||||
if (!entry->second)
|
||||
entry->second = std::move(e);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int pending_transaction::status() const
|
||||
{
|
||||
return error_.empty() ? Status_Ok : Status_Error;
|
||||
}
|
||||
|
||||
bool pending_transaction::commit(const std::string &filename, const bool overwrite)
|
||||
{
|
||||
assert(wallet_); // see constructor
|
||||
if (!error_.empty())
|
||||
return false;
|
||||
if (filename.empty())
|
||||
return send();
|
||||
|
||||
if (std::filesystem::exists(filename) && !overwrite)
|
||||
{
|
||||
error_ = "File already exists";
|
||||
return false;
|
||||
}
|
||||
|
||||
expect<epee::byte_slice> bytes = epee::byte_slice{};
|
||||
{
|
||||
epee::byte_stream sink{};
|
||||
const txes_file source{local_};
|
||||
if (std::error_code error = wire::msgpack::to_bytes(sink, source))
|
||||
{
|
||||
error_ = error.message();
|
||||
return false;
|
||||
}
|
||||
|
||||
const boost::lock_guard<boost::mutex> lock{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)))
|
||||
);
|
||||
}
|
||||
|
||||
if (!bytes)
|
||||
{
|
||||
error_ = bytes.error().message();
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ofstream out{filename};
|
||||
out.write(reinterpret_cast<const char*>(bytes->data()), bytes->size());
|
||||
return out.good();
|
||||
}
|
||||
|
||||
std::uint64_t pending_transaction::amount() const
|
||||
{
|
||||
safe_uint64_t out{};
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#include "common/expect.h" // monero/src
|
||||
#include "cryptonote_basic/cryptonote_basic.h" // monero/src
|
||||
#include "wallet/api/wallet2_api.h" // moneor/src
|
||||
#include "wire/fwd.h"
|
||||
|
||||
namespace lwsf { namespace internal
|
||||
{
|
||||
@@ -53,7 +54,8 @@ namespace lwsf { namespace internal
|
||||
std::string error_;
|
||||
|
||||
public:
|
||||
pending_transaction(std::shared_ptr<backend::wallet> wallet, std::string error, std::vector<std::shared_ptr<backend::transaction>> local = {});
|
||||
pending_transaction(std::shared_ptr<backend::wallet> wallet, std::string error, std::vector<std::shared_ptr<backend::transaction>> local = {});
|
||||
static std::unique_ptr<pending_transaction> load_from_file(std::shared_ptr<backend::wallet> wallet, const std::string& filename);
|
||||
|
||||
pending_transaction(pending_transaction&&) = delete;
|
||||
pending_transaction(const pending_transaction&) = delete;
|
||||
@@ -63,6 +65,8 @@ namespace lwsf { namespace internal
|
||||
pending_transaction& operator=(pending_transaction&&) = delete;
|
||||
pending_transaction& operator=(const pending_transaction&) = delete;
|
||||
|
||||
bool send();
|
||||
|
||||
virtual int status() const override final;
|
||||
virtual std::string errorString() const override final { return error_; }
|
||||
virtual bool commit(const std::string &filename = "", bool overwrite = false) override final;
|
||||
@@ -98,4 +102,6 @@ namespace lwsf { namespace internal
|
||||
*/
|
||||
virtual std::vector<std::string> signersKeys() const override final { return {}; }
|
||||
};
|
||||
|
||||
WIRE_DECLARE_OBJECT(pending_transaction);
|
||||
}} // lwsf // internal
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
// 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 "encrypted_file.h"
|
||||
|
||||
#include <fstream>
|
||||
#include <sodium/core.h>
|
||||
#include <sodium/crypto_pwhash_argon2id.h>
|
||||
#include <sodium/randombytes.h>
|
||||
#include <sodium/crypto_aead_chacha20poly1305.h>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "byte_stream.h" // monero/contrib/epee/include
|
||||
#include "error.h"
|
||||
#include "lwsf_config.h"
|
||||
#include "wire.h"
|
||||
#include "wire/msgpack.h"
|
||||
|
||||
namespace lwsf { namespace internal
|
||||
{
|
||||
namespace
|
||||
{
|
||||
struct encrypted_file
|
||||
{
|
||||
std::string cipher; //!< Name of cipher+authentication used
|
||||
std::string pwhasher; //!< Name of pwhasher used
|
||||
epee::byte_slice salt; //!< Pwhashing salt
|
||||
epee::byte_slice nonce; //!< Encryption iv/nonce
|
||||
epee::byte_slice epayload; //!< Encrypted contents
|
||||
std::uint64_t iterations; //!< Pwhashing iterations
|
||||
std::uint32_t memory; //!< Pwhashing memory
|
||||
|
||||
encrypted_file() = delete;
|
||||
|
||||
static epee::byte_slice get_random(const std::size_t length)
|
||||
{
|
||||
if (sodium_init() < 0)
|
||||
throw std::runtime_error{"Failed to initialize libsodium"};
|
||||
|
||||
epee::byte_stream out;
|
||||
out.put_n(0, length);
|
||||
randombytes_buf(out.data(), length);
|
||||
return epee::byte_slice{std::move(out)};
|
||||
}
|
||||
|
||||
static constexpr const char* pwhasher_name() noexcept
|
||||
{
|
||||
return "argon2id";
|
||||
}
|
||||
static constexpr int pwhash_algorithm() noexcept
|
||||
{
|
||||
return crypto_pwhash_argon2id_ALG_ARGON2ID13;
|
||||
}
|
||||
static constexpr std::size_t salt_size() noexcept
|
||||
{
|
||||
return crypto_pwhash_argon2id_SALTBYTES;
|
||||
}
|
||||
static constexpr std::uint64_t ops_min() noexcept { return 5; }
|
||||
static constexpr std::size_t memory_limit() noexcept { return 7 * 1024 * 1024; }
|
||||
static epee::byte_slice get_salt() { return get_random(salt_size()); }
|
||||
|
||||
static constexpr const char* cipher_name() noexcept
|
||||
{
|
||||
return "chacha20-poly1305_ietf";
|
||||
}
|
||||
static constexpr unsigned long long max_size() noexcept
|
||||
{
|
||||
return crypto_aead_chacha20poly1305_ietf_MESSAGEBYTES_MAX;
|
||||
}
|
||||
static constexpr std::size_t key_size() noexcept
|
||||
{
|
||||
return crypto_aead_chacha20poly1305_ietf_KEYBYTES;
|
||||
}
|
||||
static constexpr std::size_t tag_size() noexcept
|
||||
{
|
||||
return crypto_aead_chacha20poly1305_ietf_ABYTES;
|
||||
}
|
||||
static constexpr std::size_t nonce_size() noexcept
|
||||
{
|
||||
return crypto_aead_chacha20poly1305_ietf_NPUBBYTES;
|
||||
}
|
||||
static epee::byte_slice get_nonce() { return get_random(nonce_size()); }
|
||||
|
||||
std::array<std::uint8_t, 32> get_key(const epee::span<const std::uint8_t> password) const
|
||||
{
|
||||
if (sodium_init() < 0)
|
||||
throw std::runtime_error{"Failed to initialize libsodium"};
|
||||
|
||||
std::array<std::uint8_t, key_size()> key{{}};
|
||||
if (crypto_pwhash_argon2id(
|
||||
key.data(), key.size(),
|
||||
reinterpret_cast<const char*>(password.data()), password.size(),
|
||||
salt.data(),
|
||||
iterations, memory, pwhash_algorithm()) != 0)
|
||||
{
|
||||
throw std::runtime_error{std::string{pwhasher_name()} + " failed"};
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
static encrypted_file make(const epee::byte_slice& upayload, const std::uint64_t iterations, const epee::span<const std::uint8_t> password)
|
||||
{
|
||||
if (sodium_init() < 0)
|
||||
throw std::runtime_error{"Failed to initialize libsodium"};
|
||||
|
||||
if (max_size() < upayload.size())
|
||||
throw std::runtime_error{std::string{"Exceeded max size for "} + cipher_name()};
|
||||
if (std::numeric_limits<std::size_t>::max() - upayload.size() < tag_size())
|
||||
throw std::runtime_error{"Exceeded max size_t after authentication tag"};
|
||||
|
||||
encrypted_file out{
|
||||
cipher_name(),
|
||||
pwhasher_name(),
|
||||
get_salt(),
|
||||
get_nonce(),
|
||||
nullptr,
|
||||
std::max(iterations, ops_min()),
|
||||
memory_limit()
|
||||
};
|
||||
|
||||
const auto key = out.get_key(password);
|
||||
|
||||
epee::byte_stream buffer;
|
||||
buffer.put_n(0, upayload.size() + tag_size());
|
||||
|
||||
unsigned long long out_bytes = buffer.size();
|
||||
if (crypto_aead_chacha20poly1305_ietf_encrypt(
|
||||
buffer.data(), std::addressof(out_bytes),
|
||||
upayload.data(), upayload.size(),
|
||||
nullptr, 0,
|
||||
nullptr,
|
||||
out.nonce.data(), key.data()) != 0)
|
||||
{
|
||||
// this should basically never happen, so throw
|
||||
throw std::runtime_error{std::string{cipher_name()} + " encryption failed"};
|
||||
}
|
||||
|
||||
out.epayload = epee::byte_slice{std::move(buffer)}.take_slice(out_bytes);
|
||||
return out;
|
||||
}
|
||||
|
||||
//! \return Unencrypted payload, or `nullptr`.
|
||||
epee::byte_slice get_payload(const epee::span<const std::uint8_t> password) const
|
||||
{
|
||||
if (nonce.size() != nonce_size())
|
||||
return nullptr;
|
||||
if (salt.size() != salt_size())
|
||||
return nullptr;
|
||||
if (cipher != cipher_name())
|
||||
return nullptr;
|
||||
if (pwhasher != pwhasher_name())
|
||||
return nullptr;
|
||||
if (max_size() < epayload.size())
|
||||
return nullptr;
|
||||
|
||||
if (sodium_init() < 0)
|
||||
throw std::runtime_error{"Failed to initialize libsodium"};
|
||||
|
||||
const auto key = get_key(password);
|
||||
static_assert(key_size() == key.size());
|
||||
|
||||
epee::byte_stream buffer;
|
||||
buffer.put_n(0, epayload.size());
|
||||
|
||||
static_assert(
|
||||
std::numeric_limits<std::size_t>::max() <= std::numeric_limits<unsigned long long>::max()
|
||||
);
|
||||
|
||||
unsigned long long out_bytes = buffer.size();
|
||||
if (crypto_aead_chacha20poly1305_ietf_decrypt(
|
||||
buffer.data(), std::addressof(out_bytes),
|
||||
nullptr,
|
||||
epayload.data(), epayload.size(),
|
||||
nullptr, 0,
|
||||
nonce.data(), key.data()) != 0)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return epee::byte_slice{std::move(buffer)}.take_slice(out_bytes);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename F, typename T>
|
||||
void map_encrypted_file(F& format, T& self)
|
||||
{
|
||||
wire::object(format,
|
||||
WIRE_FIELD(cipher),
|
||||
WIRE_FIELD(pwhasher),
|
||||
WIRE_FIELD(salt),
|
||||
WIRE_FIELD(nonce),
|
||||
WIRE_FIELD(epayload),
|
||||
WIRE_FIELD(iterations),
|
||||
WIRE_FIELD(memory)
|
||||
);
|
||||
}
|
||||
|
||||
WIRE_DEFINE_OBJECT(encrypted_file, map_encrypted_file);
|
||||
}
|
||||
|
||||
epee::byte_slice try_load(const std::string& filename, const std::string_view file_magic)
|
||||
{
|
||||
std::ifstream file{filename, std::ios::binary};
|
||||
if (!file.is_open())
|
||||
return nullptr;
|
||||
|
||||
{
|
||||
std::string magic;
|
||||
magic.resize(file_magic.size());
|
||||
file.read(magic.data(), magic.size());
|
||||
if (!file.good() || magic != file_magic)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
file.seekg(0, std::ios::end);
|
||||
const auto size = file.tellg();
|
||||
if (size < 0 || std::size_t(size) < file_magic.size())
|
||||
return nullptr;
|
||||
|
||||
if (config::max_file_read_size < std::size_t(size))
|
||||
return nullptr;
|
||||
|
||||
epee::byte_stream buffer;
|
||||
buffer.put_n(0, std::size_t(size) - file_magic.size());
|
||||
|
||||
file.seekg(file_magic.size());
|
||||
file.read(reinterpret_cast<char*>(buffer.data()), buffer.size());
|
||||
|
||||
if (file.good())
|
||||
return epee::byte_slice{std::move(buffer)};
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
expect<epee::byte_slice> encrypt(const std::string_view file_magic, epee::byte_slice payload, const std::uint64_t iterations, const epee::span<const std::uint8_t> password)
|
||||
{
|
||||
const auto contents = encrypted_file::make(payload, iterations, password);
|
||||
const std::size_t payload_size = payload.size();
|
||||
payload = nullptr; // free up some memory that is no longer needed
|
||||
|
||||
epee::byte_stream buffer;
|
||||
buffer.reserve(payload_size + file_magic.size() + 2048);
|
||||
buffer.write(file_magic.data(), file_magic.size());
|
||||
|
||||
if (std::error_code error = wire::msgpack::to_bytes(buffer, contents))
|
||||
return error;
|
||||
return epee::byte_slice{std::move(buffer)};
|
||||
}
|
||||
|
||||
expect<epee::byte_slice> decrypt(epee::byte_slice encrypted, const epee::span<const std::uint8_t> password)
|
||||
{
|
||||
encrypted_file contents{};
|
||||
if (std::error_code error = wire::msgpack::from_bytes(std::move(encrypted), contents))
|
||||
return error;
|
||||
auto payload = contents.get_payload(epee::to_span(password));
|
||||
if (payload.empty())
|
||||
return {error::decryption};
|
||||
return payload;
|
||||
}
|
||||
|
||||
}} // lwsf // internal
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// 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 <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "byte_slice.h" // monero/contrib/epee/include
|
||||
#include "common/expect.h" // monero/src
|
||||
#include "span.h" // monero/contrib/epee/include
|
||||
|
||||
namespace lwsf { namespace internal
|
||||
{
|
||||
//! \return `filename` contents with `file_magic` verified and stripped off.
|
||||
epee::byte_slice try_load(const std::string& filename, std::string_view file_magic);
|
||||
|
||||
//! `file_magic` is pre-pended to file bytes for identification.
|
||||
expect<epee::byte_slice> encrypt(std::string_view file_magic, epee::byte_slice payload, std::uint64_t iterations, epee::span<const std::uint8_t> password);
|
||||
|
||||
//! Use `try_load` for to retrieve `contents`, then decrypt with `password`.
|
||||
expect<epee::byte_slice> decrypt(epee::byte_slice contents, epee::span<const std::uint8_t> password);
|
||||
}} // lwsf // internal
|
||||
@@ -0,0 +1,90 @@
|
||||
// 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 <cstdint>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
|
||||
namespace lwsf { namespace internal
|
||||
{
|
||||
template<typename T>
|
||||
class safe_integer
|
||||
{
|
||||
static_assert(std::is_integral<T>());
|
||||
T value_;
|
||||
public:
|
||||
constexpr safe_integer() noexcept
|
||||
: value_(0)
|
||||
{}
|
||||
|
||||
constexpr safe_integer(const T value) noexcept
|
||||
: value_(value)
|
||||
{}
|
||||
|
||||
template<typename U>
|
||||
constexpr safe_integer(const U value) noexcept
|
||||
: value_(value)
|
||||
{
|
||||
static_assert(std::is_same<U, void>(), "invalid conversion");
|
||||
}
|
||||
|
||||
constexpr safe_integer(const safe_integer&) noexcept = default;
|
||||
constexpr safe_integer& operator=(const safe_integer&) noexcept = default;
|
||||
|
||||
constexpr operator T() const noexcept { return value_; }
|
||||
|
||||
template<typename U>
|
||||
constexpr operator U() const noexcept
|
||||
{ static_assert(std::is_same<U, void>(), "invalid conversion"); };
|
||||
|
||||
safe_integer& add(safe_integer rhs)
|
||||
{
|
||||
if (std::numeric_limits<T>::max() - value_ < rhs.value_)
|
||||
throw std::overflow_error{"safe_integer overflow in addition"};
|
||||
value_ += rhs.value_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
safe_integer& subtract(safe_integer rhs)
|
||||
{
|
||||
if (value_ - std::numeric_limits<T>::min() < rhs.value_)
|
||||
throw std::underflow_error{"safe_integer underflow in subtraction"};
|
||||
value_ -= rhs.value_;
|
||||
return *this;
|
||||
}
|
||||
|
||||
safe_integer& operator+=(safe_integer rhs) { return add(rhs); }
|
||||
safe_integer& operator-=(safe_integer rhs) { return subtract(rhs); }
|
||||
};
|
||||
|
||||
using safe_uint64_t = safe_integer<std::uint64_t>;
|
||||
}} // lwsf // internal
|
||||
|
||||
+20
-239
@@ -36,11 +36,8 @@
|
||||
#include <boost/range/combine.hpp>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sodium/core.h>
|
||||
#include <sodium/crypto_pwhash_argon2id.h>
|
||||
#include <sodium/randombytes.h>
|
||||
#include <sodium/crypto_aead_chacha20poly1305.h>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
#include "address_book.h"
|
||||
@@ -61,6 +58,7 @@
|
||||
#include "subaddress_account.h"
|
||||
#include "subaddress_minor.h"
|
||||
#include "transaction_history.h"
|
||||
#include "utils/encrypted_file.h"
|
||||
#include "wire.h"
|
||||
#include "wire/msgpack.h"
|
||||
|
||||
@@ -77,7 +75,7 @@ namespace lwsf { namespace internal
|
||||
namespace
|
||||
{
|
||||
//! The stored wallet file always has this at beginning
|
||||
static constexpr std::string_view file_magic{"lwsf-wallet-1.0"};
|
||||
static constexpr std::string_view wallet_file_magic{"lwsf-wallet-1.0"};
|
||||
|
||||
struct null_connector
|
||||
{
|
||||
@@ -180,209 +178,6 @@ namespace lwsf { namespace internal
|
||||
return calculate_fee_from_weight(base_fee, estimate_tx_weight(n_inputs, n_outputs, mixin, extra_size), fee_mask);
|
||||
}
|
||||
|
||||
struct encrypted_file
|
||||
{
|
||||
std::string cipher; //!< Name of cipher+authentication used
|
||||
std::string pwhasher; //!< Name of pwhasher used
|
||||
epee::byte_slice salt; //!< Pwhashing salt
|
||||
epee::byte_slice nonce; //!< Encryption iv/nonce
|
||||
epee::byte_slice epayload; //!< Encrypted contents
|
||||
std::uint64_t iterations; //!< Pwhashing iterations
|
||||
std::uint32_t memory; //!< Pwhashing memory
|
||||
|
||||
encrypted_file() = delete;
|
||||
|
||||
//! \pre `sodium_init()` has been called
|
||||
static epee::byte_slice get_random(const std::size_t length)
|
||||
{
|
||||
epee::byte_stream out;
|
||||
out.put_n(0, length);
|
||||
randombytes_buf(out.data(), length);
|
||||
return epee::byte_slice{std::move(out)};
|
||||
}
|
||||
|
||||
static constexpr const char* pwhasher_name() noexcept
|
||||
{
|
||||
return "argon2id";
|
||||
}
|
||||
static constexpr int pwhash_algorithm() noexcept
|
||||
{
|
||||
return crypto_pwhash_argon2id_ALG_ARGON2ID13;
|
||||
}
|
||||
static constexpr std::size_t salt_size() noexcept
|
||||
{
|
||||
return crypto_pwhash_argon2id_SALTBYTES;
|
||||
}
|
||||
static constexpr std::uint64_t ops_min() noexcept { return 5; }
|
||||
static constexpr std::size_t memory_limit() noexcept { return 7 * 1024 * 1024; }
|
||||
static epee::byte_slice get_salt() { return get_random(salt_size()); }
|
||||
|
||||
static constexpr const char* cipher_name() noexcept
|
||||
{
|
||||
return "chacha20-poly1305_ietf";
|
||||
}
|
||||
static constexpr unsigned long long max_size() noexcept
|
||||
{
|
||||
return crypto_aead_chacha20poly1305_ietf_MESSAGEBYTES_MAX;
|
||||
}
|
||||
static constexpr std::size_t key_size() noexcept
|
||||
{
|
||||
return crypto_aead_chacha20poly1305_ietf_KEYBYTES;
|
||||
}
|
||||
static constexpr std::size_t tag_size() noexcept
|
||||
{
|
||||
return crypto_aead_chacha20poly1305_ietf_ABYTES;
|
||||
}
|
||||
static constexpr std::size_t nonce_size() noexcept
|
||||
{
|
||||
return crypto_aead_chacha20poly1305_ietf_NPUBBYTES;
|
||||
}
|
||||
static epee::byte_slice get_nonce() { return get_random(nonce_size()); }
|
||||
|
||||
//! \pre `sodium_init()` has been called
|
||||
std::array<std::uint8_t, 32> get_key(const std::string& password) const
|
||||
{
|
||||
std::array<std::uint8_t, key_size()> key{{}};
|
||||
if (crypto_pwhash_argon2id(
|
||||
key.data(), key.size(),
|
||||
password.data(), password.size(),
|
||||
salt.data(),
|
||||
iterations, memory, pwhash_algorithm()) != 0)
|
||||
{
|
||||
throw std::runtime_error{std::string{pwhasher_name()} + " failed"};
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
static encrypted_file
|
||||
make(const epee::byte_slice& upayload, const std::uint64_t iterations, const std::string& password)
|
||||
{
|
||||
if (sodium_init() < 0)
|
||||
throw std::runtime_error{"Failed to initialize libsodium"};
|
||||
|
||||
if (max_size() < upayload.size())
|
||||
throw std::runtime_error{std::string{"Exceeded max size for "} + cipher_name()};
|
||||
if (std::numeric_limits<std::size_t>::max() - upayload.size() < tag_size())
|
||||
throw std::runtime_error{"Exceeded max size_t after authentication tag"};
|
||||
|
||||
encrypted_file out{
|
||||
cipher_name(),
|
||||
pwhasher_name(),
|
||||
get_salt(),
|
||||
get_nonce(),
|
||||
nullptr,
|
||||
std::max(iterations, ops_min()),
|
||||
memory_limit()
|
||||
};
|
||||
|
||||
const auto key = out.get_key(password);
|
||||
|
||||
epee::byte_stream buffer;
|
||||
buffer.put_n(0, upayload.size() + tag_size());
|
||||
|
||||
unsigned long long out_bytes = buffer.size();
|
||||
if (crypto_aead_chacha20poly1305_ietf_encrypt(
|
||||
buffer.data(), std::addressof(out_bytes),
|
||||
upayload.data(), upayload.size(),
|
||||
nullptr, 0,
|
||||
nullptr,
|
||||
out.nonce.data(), key.data()) != 0)
|
||||
{
|
||||
throw std::runtime_error{std::string{cipher_name()} + " encryption failed"};
|
||||
}
|
||||
|
||||
out.epayload = epee::byte_slice{std::move(buffer)}.take_slice(out_bytes);
|
||||
return out;
|
||||
}
|
||||
|
||||
//! \return Unencrypted payload, or `nullptr`.
|
||||
epee::byte_slice get_payload(const std::string& password) const
|
||||
{
|
||||
if (nonce.size() != nonce_size())
|
||||
return nullptr;
|
||||
if (salt.size() != salt_size())
|
||||
return nullptr;
|
||||
if (cipher != cipher_name())
|
||||
return nullptr;
|
||||
if (pwhasher != pwhasher_name())
|
||||
return nullptr;
|
||||
if (max_size() < epayload.size())
|
||||
return nullptr;
|
||||
|
||||
if (sodium_init() < 0)
|
||||
throw std::runtime_error{"Failed to initialize libsodium"};
|
||||
|
||||
const auto key = get_key(password);
|
||||
static_assert(key_size() == key.size());
|
||||
|
||||
epee::byte_stream buffer;
|
||||
buffer.put_n(0, epayload.size());
|
||||
|
||||
static_assert(
|
||||
std::numeric_limits<std::size_t>::max() <= std::numeric_limits<unsigned long long>::max()
|
||||
);
|
||||
|
||||
unsigned long long out_bytes = buffer.size();
|
||||
if (crypto_aead_chacha20poly1305_ietf_decrypt(
|
||||
buffer.data(), std::addressof(out_bytes),
|
||||
nullptr,
|
||||
epayload.data(), epayload.size(),
|
||||
nullptr, 0,
|
||||
nonce.data(), key.data()) != 0)
|
||||
{
|
||||
throw std::runtime_error{std::string{cipher_name()} + " decryption failed"};
|
||||
}
|
||||
|
||||
return epee::byte_slice{std::move(buffer)}.take_slice(out_bytes);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename F, typename T>
|
||||
void map_encrypted_file(F& format, T& self)
|
||||
{
|
||||
wire::object(format,
|
||||
WIRE_FIELD(cipher),
|
||||
WIRE_FIELD(pwhasher),
|
||||
WIRE_FIELD(salt),
|
||||
WIRE_FIELD(nonce),
|
||||
WIRE_FIELD(epayload),
|
||||
WIRE_FIELD(iterations),
|
||||
WIRE_FIELD(memory)
|
||||
);
|
||||
}
|
||||
|
||||
WIRE_DEFINE_OBJECT(encrypted_file, map_encrypted_file);
|
||||
|
||||
epee::byte_slice try_load(const std::string& filename)
|
||||
{
|
||||
std::ifstream file{filename, std::ios::binary};
|
||||
if (!file.is_open())
|
||||
return nullptr;
|
||||
|
||||
{
|
||||
std::string magic;
|
||||
magic.resize(file_magic.size());
|
||||
file.read(magic.data(), magic.size());
|
||||
if (!file.good() || magic != file_magic)
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
file.seekg(0, std::ios::end);
|
||||
const auto size = file.tellg();
|
||||
if (size < 0 || std::size_t(size) < file_magic.size())
|
||||
return nullptr;
|
||||
|
||||
epee::byte_stream buffer;
|
||||
buffer.put_n(0, std::size_t(size) - file_magic.size());
|
||||
|
||||
file.seekg(file_magic.size());
|
||||
file.read(reinterpret_cast<char*>(buffer.data()), buffer.size());
|
||||
|
||||
if (file.good())
|
||||
return epee::byte_slice{std::move(buffer)};
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
#if defined (__unix__) || (defined (__APPLE__) && defined (__MACH__))
|
||||
bool atomic_file_write(const std::filesystem::path& filename, const std::filesystem::path& directory, epee::byte_slice contents) noexcept
|
||||
{
|
||||
@@ -571,7 +366,7 @@ namespace lwsf { namespace internal
|
||||
|
||||
bool wallet::is_wallet_file(const std::string& path)
|
||||
{
|
||||
return !try_load(path).empty();
|
||||
return !try_load(path, wallet_file_magic).empty();
|
||||
}
|
||||
|
||||
bool wallet::verify_password(std::string path, const std::string& password)
|
||||
@@ -581,14 +376,10 @@ namespace lwsf { namespace internal
|
||||
if (keys_suffix.size() <= path.size() && std::string_view{path.data() + path.size() - keys_suffix.size()} == keys_suffix)
|
||||
path.erase(path.size() - keys_suffix.size());
|
||||
|
||||
epee::byte_slice file = try_load(path);
|
||||
epee::byte_slice file = try_load(path, wallet_file_magic);
|
||||
if (file.empty())
|
||||
return false;
|
||||
|
||||
encrypted_file contents{};
|
||||
if (wire::msgpack::from_bytes(std::move(file), contents))
|
||||
return false;
|
||||
return !contents.get_payload(password).empty();
|
||||
return bool(decrypt(try_load(path, wallet_file_magic), epee::strspan<std::uint8_t>(password)));
|
||||
}
|
||||
|
||||
std::vector<std::string> wallet::find(const std::string& path)
|
||||
@@ -604,7 +395,7 @@ namespace lwsf { namespace internal
|
||||
if (std::filesystem::is_regular_file(itr->status()))
|
||||
{
|
||||
std::string filename = itr->path().filename().string();
|
||||
if (!try_load(filename).empty())
|
||||
if (!try_load(filename, wallet_file_magic).empty())
|
||||
out.push_back(std::move(filename));
|
||||
}
|
||||
}
|
||||
@@ -667,19 +458,14 @@ namespace lwsf { namespace internal
|
||||
{
|
||||
try
|
||||
{
|
||||
epee::byte_slice file = try_load(filename_ + ".new");
|
||||
epee::byte_slice file = try_load(filename_ + ".new", wallet_file_magic);
|
||||
if (file.empty())
|
||||
file = try_load(filename_);
|
||||
file = try_load(filename_, wallet_file_magic);
|
||||
|
||||
if (file.empty())
|
||||
throw std::runtime_error{"Unable to open wallet file " + filename_};
|
||||
|
||||
encrypted_file contents{};
|
||||
if (std::error_code error = wire::msgpack::from_bytes(std::move(file), contents))
|
||||
throw std::system_error{error};
|
||||
|
||||
epee::byte_slice payload = contents.get_payload(password_);
|
||||
if (std::error_code error = data_->from_bytes(std::move(payload)))
|
||||
if (std::error_code error = data_->from_bytes(decrypt(std::move(file), epee::strspan<std::uint8_t>(password_)).value()))
|
||||
throw std::system_error{error};
|
||||
|
||||
// lock not needed; data_ was created and unique to us
|
||||
@@ -714,9 +500,6 @@ namespace lwsf { namespace internal
|
||||
thread_state_(state::stop),
|
||||
mandatory_refresh_(false)
|
||||
{
|
||||
if (sodium_init() < 0)
|
||||
throw std::runtime_error{"Failed to initialize libsodium"};
|
||||
|
||||
crypto::secret_key recovery;
|
||||
std::string language;
|
||||
if (!crypto::ElectrumWords::words_to_bytes(mnemonic, recovery, language))
|
||||
@@ -926,18 +709,6 @@ namespace lwsf { namespace internal
|
||||
|
||||
try
|
||||
{
|
||||
epee::byte_slice payload = data_->to_bytes().value();
|
||||
const auto contents = encrypted_file::make(payload, iterations_, password_);
|
||||
const auto payload_size = payload.size();
|
||||
payload = nullptr; // free up some memory that is no longer needed
|
||||
|
||||
epee::byte_stream buffer;
|
||||
buffer.reserve(payload_size + 2048);
|
||||
buffer.write(file_magic.data(), file_magic.size());
|
||||
|
||||
if (std::error_code error = wire::msgpack::to_bytes(buffer, contents))
|
||||
throw std::system_error{error};
|
||||
|
||||
const std::filesystem::path file = real_path;
|
||||
const std::filesystem::path new_file = real_path + ".new";
|
||||
const std::filesystem::path directory =
|
||||
@@ -947,7 +718,9 @@ namespace lwsf { namespace internal
|
||||
std::filesystem::rename(new_file, file);
|
||||
|
||||
// blocks until file and directory contents are synced
|
||||
if (!atomic_file_write(new_file, directory, epee::byte_slice{std::move(buffer)}))
|
||||
epee::byte_slice blob =
|
||||
encrypt(wallet_file_magic, data_->to_bytes().value(), iterations_, epee::strspan<std::uint8_t>(password_)).value();
|
||||
if (!atomic_file_write(new_file, directory, std::move(blob)))
|
||||
throw std::runtime_error{"Failed to write file " + real_path};
|
||||
|
||||
std::filesystem::rename(new_file, file);
|
||||
@@ -1958,6 +1731,14 @@ namespace lwsf { namespace internal
|
||||
return createTransactionMultDest({dst_addr}, payment_id, amount ? std::optional<std::vector<uint64_t>>{{*amount}} : std::nullopt, mixin_count, priority, subaddr_account, subaddr_indices);
|
||||
}
|
||||
|
||||
bool wallet::submitTransaction(const std::string &fileName)
|
||||
{
|
||||
const auto tx = pending_transaction::load_from_file(data_, fileName);
|
||||
if (!tx)
|
||||
throw std::runtime_error{"Expected non-null from load_from_file"};
|
||||
return tx->status() == Monero::PendingTransaction::Status_Ok && tx->send();
|
||||
}
|
||||
|
||||
void wallet::disposeTransaction(Monero::PendingTransaction * t)
|
||||
{
|
||||
delete t;
|
||||
|
||||
+2
-2
@@ -449,7 +449,7 @@ namespace internal
|
||||
* @param signData encrypted unsigned transaction. Obtained with PendingTransaction::multisigSignData
|
||||
* @return PendingTransaction
|
||||
*/
|
||||
virtual Monero::PendingTransaction* restoreMultisigTransaction(const std::string& signData) override { /* TODO */ return nullptr; }
|
||||
virtual Monero::PendingTransaction* restoreMultisigTransaction(const std::string& signData) override { /* TODO */ return nullptr; }
|
||||
|
||||
/*!
|
||||
* \brief createTransactionMultDest creates transaction with multiple destinations. if dst_addr is an integrated address, payment_id is ignored
|
||||
@@ -508,7 +508,7 @@ namespace internal
|
||||
* \brief submitTransaction - submits transaction in signed tx file
|
||||
* \return - true on success
|
||||
*/
|
||||
virtual bool submitTransaction(const std::string &fileName) override { /* TODO */ return false; }
|
||||
virtual bool submitTransaction(const std::string &fileName) override;
|
||||
|
||||
|
||||
/*!
|
||||
|
||||
Reference in New Issue
Block a user