mirror of
https://github.com/wownero/lwsf-wow.git
synced 2026-10-17 15:31:32 +02:00
1230 lines
36 KiB
C++
1230 lines
36 KiB
C++
// 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
|
|
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
|
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
|
|
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
#include "wallet.h"
|
|
|
|
#if defined (__unix__) || (defined (__APPLE__) && defined (__MACH__))
|
|
#include <fcntl.h>
|
|
#endif
|
|
|
|
#include <algorithm>
|
|
#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 <string_view>
|
|
#include "address_book.h"
|
|
#include "backend.h"
|
|
#include "cryptonote_basic/cryptonote_format_utils.h" // monero/src
|
|
#include "error.h"
|
|
#include "hex.h" // monero/contrib/epee/include
|
|
#include "lwsf_config.h"
|
|
#include "mnemonics/electrum-words.h" // monero/src
|
|
#include "net/net_parse_helpers.h" // monero/contrib/epee/include
|
|
#include "net/parse.h" // monero/src
|
|
#include "net/socks.h" // monero/src
|
|
#include "net/socks_connect.h" // monero/src
|
|
#include "subaddress_account.h"
|
|
#include "subaddress_minor.h"
|
|
#include "transaction_history.h"
|
|
#include "wire.h"
|
|
#include "wire/msgpack.h"
|
|
|
|
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"};
|
|
|
|
struct null_connector
|
|
{
|
|
boost::unique_future<boost::asio::ip::tcp::socket>
|
|
operator()(const std::string&, const std::string&, boost::asio::steady_timer&) const
|
|
{
|
|
throw std::runtime_error{"Unable to connect"};
|
|
}
|
|
};
|
|
|
|
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
|
|
{
|
|
const int fd =
|
|
open(filename.c_str(), O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR);
|
|
if (fd < 0)
|
|
return false;
|
|
|
|
bool rc = false;
|
|
if (0 <= write(fd, contents.data(), contents.size()) && 0 <= fsync(fd))
|
|
{
|
|
const char* cwd = directory.c_str();
|
|
if (strcmp(cwd, "") == 0)
|
|
cwd = ".";
|
|
const int dir = open(cwd, O_RDONLY);
|
|
if (0 <= dir)
|
|
{
|
|
if (0 <= fsync(dir))
|
|
rc = true;
|
|
close(dir);
|
|
}
|
|
}
|
|
|
|
close(fd);
|
|
return rc;
|
|
}
|
|
#else
|
|
#error atomic_file_write not implemented for this platform
|
|
#endif
|
|
|
|
struct unknown_exception : std::exception
|
|
{
|
|
unknown_exception() noexcept
|
|
: std::exception()
|
|
{}
|
|
|
|
const char* what() const noexcept override { return "unknown error"; }
|
|
};
|
|
|
|
constexpr boost::chrono::nanoseconds to_boost(const std::chrono::nanoseconds source) noexcept
|
|
{
|
|
static_assert(
|
|
std::is_same<boost::chrono::nanoseconds::rep, std::chrono::nanoseconds::rep>{}
|
|
);
|
|
return boost::chrono::nanoseconds{source.count()};
|
|
}
|
|
|
|
void load_wallet(backend::account& self, const crypto::secret_key& from, const Monero::NetworkType nettype, const bool generated_locally)
|
|
{
|
|
cryptonote::account_base base{};
|
|
base.generate(from, true);
|
|
|
|
const auto& keys = base.get_keys();
|
|
|
|
self.view.sec = keys.m_view_secret_key;
|
|
self.view.pub = keys.m_account_address.m_view_public_key;
|
|
|
|
self.spend.sec = keys.m_spend_secret_key;
|
|
self.spend.pub = keys.m_account_address.m_spend_public_key;
|
|
|
|
self.generated_locally = generated_locally;
|
|
self.type = nettype;
|
|
}
|
|
} // anonymous
|
|
|
|
bool wallet::set_error(const std::error_code status) const
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock{error_sync_};
|
|
status_ = status;
|
|
exception_error_.clear();
|
|
return !status_;
|
|
}
|
|
|
|
void wallet::set_critical(const std::exception& e) const
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock{error_sync_};
|
|
exception_error_ = e.what();
|
|
status_.clear();
|
|
}
|
|
|
|
void wallet::stop_refresh_loop()
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock{thread_sync_};
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock2{refresh_sync_};
|
|
thread_state_ = state::stop;
|
|
}
|
|
refresh_notify_.notify_all();
|
|
if (thread_.joinable())
|
|
thread_.join();
|
|
}
|
|
|
|
void wallet::refresh_loop()
|
|
{
|
|
struct set_stop_
|
|
{
|
|
void operator()(state* val) const noexcept
|
|
{
|
|
if (val)
|
|
*val = state::stop;
|
|
}
|
|
};
|
|
try
|
|
{
|
|
boost::unique_lock<boost::mutex> lock{refresh_sync_};
|
|
const std::unique_ptr<state, set_stop_> set_stop{std::addressof(thread_state_)};
|
|
while (mandatory_refresh_ || thread_state_ != state::stop)
|
|
{
|
|
const bool mandatory_refresh = mandatory_refresh_;
|
|
mandatory_refresh_ = false;
|
|
|
|
if (thread_state_ == state::run || mandatory_refresh)
|
|
{
|
|
// refresh has strong exception guarantee - never in partial state.
|
|
lock.unlock();
|
|
set_error(data_->refresh(mandatory_refresh));
|
|
lock.lock();
|
|
}
|
|
else if (thread_state_ == state::skip_once)
|
|
thread_state_ = state::run;
|
|
|
|
// check while holding lock and before a wait call
|
|
if (thread_state_ == state::stop)
|
|
return;
|
|
|
|
refresh_notify_.wait_for(lock, to_boost(config::refresh_interval));
|
|
}
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
set_critical(e);
|
|
}
|
|
catch (...)
|
|
{
|
|
set_critical(unknown_exception{});
|
|
}
|
|
}
|
|
|
|
bool wallet::is_wallet_file(const std::string& path)
|
|
{
|
|
return !try_load(path).empty();
|
|
}
|
|
|
|
bool wallet::verify_password(std::string path, const std::string& password)
|
|
{
|
|
// We don't use a separate keys file, remove the suffix is present
|
|
static constexpr const std::string_view keys_suffix{".keys"};
|
|
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);
|
|
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();
|
|
}
|
|
|
|
std::vector<std::string> wallet::find(const std::string& path)
|
|
{
|
|
std::vector<std::string> out;
|
|
std::filesystem::path work_dir(path);
|
|
if(!std::filesystem::is_directory(path))
|
|
return out;
|
|
|
|
std::filesystem::recursive_directory_iterator end_itr;
|
|
for (std::filesystem::recursive_directory_iterator itr(path); itr != end_itr; ++itr)
|
|
{
|
|
if (std::filesystem::is_regular_file(itr->status()))
|
|
{
|
|
std::string filename = itr->path().filename().string();
|
|
if (!try_load(filename).empty())
|
|
out.push_back(std::move(filename));
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
wallet::wallet(create, Monero::NetworkType nettype, std::string filename, std::string password, const std::uint64_t kdf_rounds)
|
|
: data_(std::make_shared<backend::wallet>()),
|
|
addressbook_(),
|
|
history_(),
|
|
subaddresses_(),
|
|
subaddress_minor_(),
|
|
filename_(std::move(filename)),
|
|
password_(std::move(password)),
|
|
exception_error_(),
|
|
status_(),
|
|
thread_(),
|
|
iterations_(kdf_rounds),
|
|
mixin_(config::mixin_default),
|
|
refresh_interval_(config::refresh_interval),
|
|
refresh_notify_(),
|
|
error_sync_(),
|
|
refresh_sync_(),
|
|
thread_sync_(),
|
|
thread_state_(state::stop),
|
|
mandatory_refresh_(false)
|
|
{
|
|
if (sodium_init() < 0)
|
|
throw std::runtime_error{"Failed to initialize libsodium"};
|
|
|
|
// use libsodium random, more portable
|
|
crypto::secret_key recovery;
|
|
randombytes_buf(std::addressof(unwrap(unwrap(recovery))), sizeof(recovery));
|
|
|
|
load_wallet(data_->primary, recovery, nettype, true);
|
|
}
|
|
|
|
wallet::wallet(open, Monero::NetworkType nettype, std::string filename, std::string password, const std::uint64_t kdf_rounds)
|
|
: data_(std::make_shared<backend::wallet>()),
|
|
addressbook_(),
|
|
history_(),
|
|
subaddresses_(),
|
|
subaddress_minor_(),
|
|
filename_(std::move(filename)),
|
|
password_(std::move(password)),
|
|
exception_error_(),
|
|
status_(),
|
|
thread_(),
|
|
iterations_(kdf_rounds),
|
|
mixin_(config::mixin_default),
|
|
refresh_interval_(config::refresh_interval),
|
|
refresh_notify_(),
|
|
error_sync_(),
|
|
refresh_sync_(),
|
|
thread_sync_(),
|
|
thread_state_(state::stop),
|
|
mandatory_refresh_(false)
|
|
{
|
|
if (!data_)
|
|
throw std::invalid_argument{"lwsf::backend::wallet cannot be null"};
|
|
|
|
try
|
|
{
|
|
epee::byte_slice file = try_load(filename_ + ".new");
|
|
if (file.empty())
|
|
file = try_load(filename_);
|
|
|
|
if (!file.empty())
|
|
{
|
|
encrypted_file contents{};
|
|
if (!(status_ = wire::msgpack::from_bytes(std::move(file), contents)))
|
|
{
|
|
epee::byte_slice payload = contents.get_payload(password_);
|
|
if (!payload.empty())
|
|
{
|
|
if (!(status_ = data_->from_bytes(std::move(payload))))
|
|
{
|
|
// lock not needed; data_ was created and unique to us
|
|
if (nettype != data_->primary.type)
|
|
throw std::runtime_error{"Wallet file NetworkType does not match requested"};
|
|
}
|
|
}
|
|
else
|
|
status_ = error::unsupported_format;
|
|
}
|
|
}
|
|
else
|
|
status_ = error::read_failure;
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
status_.clear();
|
|
exception_error_ = e.what();
|
|
}
|
|
}
|
|
|
|
wallet::wallet(from_mnemonic, const Monero::NetworkType nettype, std::string filename, std::string password, const std::uint64_t kdf_rounds, const std::string& mnemonic, const std::string& seed_offset)
|
|
: data_(std::make_shared<backend::wallet>()),
|
|
addressbook_(),
|
|
history_(),
|
|
subaddresses_(),
|
|
subaddress_minor_(),
|
|
filename_(std::move(filename)),
|
|
password_(std::move(password)),
|
|
exception_error_(),
|
|
status_(),
|
|
thread_(),
|
|
iterations_(kdf_rounds),
|
|
mixin_(config::mixin_default),
|
|
refresh_interval_(config::refresh_interval),
|
|
refresh_notify_(),
|
|
error_sync_(),
|
|
refresh_sync_(),
|
|
thread_sync_(),
|
|
thread_state_(state::stop),
|
|
mandatory_refresh_(false)
|
|
{
|
|
if (sodium_init() < 0)
|
|
throw std::runtime_error{"Failed to initialize libsodium"};
|
|
|
|
crypto::secret_key recovery;
|
|
std::string language;
|
|
if (!crypto::ElectrumWords::words_to_bytes(mnemonic, recovery, language))
|
|
{
|
|
exception_error_ = "Electrum-style word list failed verification";
|
|
return;
|
|
}
|
|
|
|
if (!seed_offset.empty())
|
|
recovery = cryptonote::decrypt_key(recovery, seed_offset);
|
|
|
|
load_wallet(data_->primary, recovery, nettype, false);
|
|
}
|
|
|
|
wallet::~wallet() { stop_refresh_loop(); }
|
|
|
|
std::string wallet::seed(const std::string& seed_offset) const
|
|
{
|
|
std::string language;
|
|
crypto::secret_key key;
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock{data_->sync};
|
|
language = data_->primary.language;
|
|
key = data_->primary.spend.sec;
|
|
}
|
|
|
|
if (language.empty())
|
|
language = "English";
|
|
|
|
if (!seed_offset.empty())
|
|
key = cryptonote::encrypt_key(key, seed_offset);
|
|
|
|
epee::wipeable_string electrum_words;
|
|
if (!crypto::ElectrumWords::bytes_to_words(key, electrum_words, language))
|
|
{
|
|
set_critical(std::runtime_error{"Failed to crate seed from key for " + language});
|
|
return {};
|
|
}
|
|
return std::string{electrum_words.data(), electrum_words.size()};
|
|
}
|
|
|
|
std::string wallet::getSeedLanguage() const
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock{data_->sync};
|
|
return data_->primary.language;
|
|
}
|
|
|
|
void wallet::setSeedLanguage(const std::string &arg)
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock{data_->sync};
|
|
data_->primary.language = arg;
|
|
}
|
|
|
|
int wallet::status() const
|
|
{
|
|
int out = 0;
|
|
std::string unused;
|
|
statusWithErrorString(out, unused);
|
|
return out;
|
|
}
|
|
|
|
std::string wallet::errorString() const
|
|
{
|
|
int unused = 0;
|
|
std::string out;
|
|
statusWithErrorString(unused, out);
|
|
return out;
|
|
}
|
|
|
|
void wallet::statusWithErrorString(int& status, std::string& errorString) const
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock{error_sync_};
|
|
if (!exception_error_.empty())
|
|
{
|
|
status = Status_Critical;
|
|
errorString = exception_error_;
|
|
}
|
|
else if (status_)
|
|
{
|
|
status = Status_Error;
|
|
errorString = status_.message();
|
|
}
|
|
else
|
|
{
|
|
status = Status_Ok;
|
|
errorString.clear();
|
|
}
|
|
}
|
|
|
|
bool wallet::setPassword(const std::string &password)
|
|
{
|
|
password_ = password;
|
|
return true;
|
|
}
|
|
|
|
std::string wallet::address(const std::uint32_t accountIndex, const std::uint32_t addressIndex) const
|
|
{
|
|
return data_->get_spend_address({accountIndex, addressIndex});
|
|
}
|
|
|
|
Monero::NetworkType wallet::nettype() const { return data_->primary.type; }
|
|
|
|
std::string wallet::secretViewKey() const
|
|
{
|
|
return epee::to_hex::string(epee::as_byte_span(unwrap(unwrap(data_->primary.view.sec))));
|
|
}
|
|
|
|
std::string wallet::publicViewKey() const
|
|
{
|
|
return epee::to_hex::string(epee::as_byte_span(data_->primary.view.pub));
|
|
}
|
|
|
|
std::string wallet::secretSpendKey() const
|
|
{
|
|
return epee::to_hex::string(epee::as_byte_span(unwrap(unwrap(data_->primary.spend.sec))));
|
|
}
|
|
|
|
std::string wallet::publicSpendKey() const
|
|
{
|
|
return epee::to_hex::string(epee::as_byte_span(data_->primary.spend.pub));
|
|
}
|
|
|
|
void wallet::stop()
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock{refresh_sync_};
|
|
thread_state_ = state::skip_once;
|
|
}
|
|
|
|
bool wallet::store(const std::string& path)
|
|
{
|
|
const std::string& real_path = path.empty() ? filename_ : path;
|
|
|
|
std::error_code status{};
|
|
try
|
|
{
|
|
expect<epee::byte_slice> payload = data_->to_bytes();
|
|
if (payload)
|
|
{
|
|
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 (!(status = wire::msgpack::to_bytes(buffer, contents)))
|
|
{
|
|
const std::filesystem::path file = real_path;
|
|
const std::filesystem::path new_file = real_path + ".new";
|
|
const std::filesystem::path directory =
|
|
std::filesystem::path{real_path}.remove_filename();
|
|
|
|
if (std::filesystem::exists(new_file))
|
|
std::filesystem::rename(new_file, file, status);
|
|
|
|
// blocks until file and directory contents are synced
|
|
if (atomic_file_write(new_file, directory, epee::byte_slice{std::move(buffer)}))
|
|
std::filesystem::rename(new_file, file, status);
|
|
else
|
|
status = error::write_failure;
|
|
}
|
|
}
|
|
else
|
|
status = payload.error();
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
set_critical(e);
|
|
return false;
|
|
}
|
|
|
|
// this will clear any existing errors
|
|
return set_error(status);
|
|
}
|
|
|
|
bool wallet::init(const std::string &daemon_address, uint64_t, const std::string &daemon_username, const std::string &daemon_password, bool use_ssl, bool light_wallet, const std::string &proxy_address)
|
|
{
|
|
if (!light_wallet)
|
|
throw std::invalid_argument{"Only light_wallets are supported with this instance"};
|
|
|
|
// on exceptions, reset state
|
|
struct on_throw
|
|
{
|
|
void operator()(wallet* self) const
|
|
{
|
|
if (self)
|
|
{
|
|
self->stop_refresh_loop();
|
|
self->data_->client.disconnect();
|
|
}
|
|
}
|
|
};
|
|
|
|
try
|
|
{
|
|
std::unique_ptr<wallet, on_throw> rollback{this};
|
|
stop_refresh_loop();
|
|
data_->client.disconnect();
|
|
|
|
if (!proxy_address.empty() && !setProxy(proxy_address))
|
|
return false;
|
|
|
|
epee::net_utils::http::url_content url{};
|
|
if (!epee::net_utils::parse_url(daemon_address, url))
|
|
throw std::runtime_error{"Invalid LWS URL: " + daemon_address};
|
|
if (!url.m_uri_content.m_path.empty())
|
|
throw std::runtime_error{"LWS URL contains path (unsupported)"};
|
|
|
|
if (url.schema == "https")
|
|
use_ssl = true;
|
|
|
|
boost::optional<epee::net_utils::http::login> login;
|
|
if (!daemon_username.empty() || !daemon_password.empty())
|
|
login.emplace(daemon_username, daemon_password);
|
|
|
|
epee::net_utils::ssl_options_t options{
|
|
use_ssl ? epee::net_utils::ssl_support_t::e_ssl_support_enabled : epee::net_utils::ssl_support_t::e_ssl_support_disabled
|
|
};
|
|
|
|
data_->client.set_server(std::move(url.host), std::to_string(url.port), std::move(login), std::move(options));
|
|
if (!connectToDaemon())
|
|
return false;
|
|
|
|
startRefresh();
|
|
rollback.release();
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
set_critical(e);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void wallet::setRefreshFromBlockHeight(uint64_t refresh_from_block_height)
|
|
{
|
|
try { set_error(data_->restore_height(refresh_from_block_height)); }
|
|
catch (const std::exception& e)
|
|
{
|
|
set_critical(e);
|
|
}
|
|
}
|
|
|
|
uint64_t wallet::getRefreshFromBlockHeight() const
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock{data_->sync};
|
|
return data_->primary.restore_height;
|
|
}
|
|
|
|
bool wallet::connectToDaemon()
|
|
{
|
|
struct on_throw
|
|
{
|
|
void operator()(wallet* self) const
|
|
{
|
|
if (self)
|
|
self->data_->client.disconnect();
|
|
}
|
|
};
|
|
|
|
if (data_->client.is_connected())
|
|
return true;
|
|
|
|
std::unique_ptr<wallet, on_throw> disconnect{this};
|
|
if (data_->client.connect(config::connect_timeout))
|
|
{
|
|
const std::error_code status = data_->login();
|
|
if (!status)
|
|
disconnect.release();
|
|
return set_error(status);
|
|
}
|
|
set_error(error::connect_failure);
|
|
return false;
|
|
}
|
|
|
|
Monero::Wallet::ConnectionStatus wallet::connected() const
|
|
{
|
|
return data_->client.is_connected() ?
|
|
ConnectionStatus_Connected : ConnectionStatus_Disconnected;
|
|
}
|
|
|
|
bool wallet::setProxy(const std::string &address)
|
|
{
|
|
auto endpoint = net::get_tcp_endpoint(address);
|
|
if (!endpoint)
|
|
{
|
|
data_->client.set_connector(null_connector{});
|
|
return set_error(endpoint.error());
|
|
}
|
|
data_->client.set_connector(net::socks::connector{std::move(*endpoint)});
|
|
return true;
|
|
}
|
|
|
|
uint64_t wallet::balance(const uint32_t accountIndex) const
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock{data_->sync};
|
|
|
|
std::uint64_t balance = 0;
|
|
for (const auto& tx : data_->primary.txes)
|
|
{
|
|
for (const auto& receive : tx.second->receives)
|
|
{
|
|
if (receive.second.recipient.maj_i == accountIndex)
|
|
balance += receive.second.amount;
|
|
}
|
|
|
|
for (const auto& spend : tx.second->spends)
|
|
{
|
|
if (spend.second.sender.maj_i == accountIndex)
|
|
balance -= spend.second.amount;
|
|
}
|
|
}
|
|
|
|
return balance;
|
|
}
|
|
|
|
uint64_t wallet::unlockedBalance(const uint32_t accountIndex) const
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock{data_->sync};
|
|
const Monero::NetworkType net_type = data_->primary.type;
|
|
const std::uint32_t chain_height = data_->blockchain_height;
|
|
|
|
std::uint64_t balance = 0;
|
|
for (const auto& tx : data_->primary.txes)
|
|
{
|
|
const bool unlocked = tx.second->is_unlocked(chain_height, net_type);
|
|
for (const auto& receive : tx.second->receives)
|
|
{
|
|
if (unlocked && receive.second.recipient.maj_i == accountIndex)
|
|
balance += receive.second.amount;
|
|
}
|
|
|
|
for (const auto& spend : tx.second->spends)
|
|
{
|
|
if (spend.second.sender.maj_i == accountIndex)
|
|
balance -= spend.second.amount;
|
|
}
|
|
}
|
|
|
|
return balance;
|
|
}
|
|
|
|
uint64_t wallet::blockChainHeight() const
|
|
{
|
|
const boost::unique_lock<boost::mutex> lock{data_->sync};
|
|
return data_->primary.scan_height;
|
|
}
|
|
|
|
uint64_t wallet::daemonBlockChainHeight() const
|
|
{
|
|
const boost::unique_lock<boost::mutex> lock{data_->sync};
|
|
return data_->blockchain_height;
|
|
}
|
|
|
|
uint64_t wallet::daemonBlockChainTargetHeight() const
|
|
{ return daemonBlockChainHeight(); }
|
|
|
|
bool wallet::synchronized() const
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock{data_->sync};
|
|
return data_->blockchain_height == data_->primary.scan_height;
|
|
}
|
|
|
|
void wallet::startRefresh()
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock{thread_sync_};
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock2{refresh_sync_};
|
|
const bool no_refresh =
|
|
refresh_interval_ <= std::chrono::milliseconds{0};
|
|
if (no_refresh)
|
|
{
|
|
thread_state_ = state::stop;
|
|
if (!mandatory_refresh_)
|
|
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(); }};
|
|
}
|
|
}
|
|
refresh_notify_.notify_all();
|
|
}
|
|
|
|
void wallet::pauseRefresh()
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock{refresh_sync_};
|
|
if (thread_state_ != state::stop)
|
|
thread_state_ = state::paused;
|
|
}
|
|
|
|
bool wallet::refresh()
|
|
{
|
|
try { return set_error(data_->refresh(true)); }
|
|
catch (const std::exception& e) { set_critical(e); }
|
|
return false;
|
|
}
|
|
|
|
void wallet::refreshAsync()
|
|
{
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock{refresh_sync_};
|
|
mandatory_refresh_ = true;
|
|
}
|
|
startRefresh();
|
|
}
|
|
|
|
void wallet::setAutoRefreshInterval(int millis)
|
|
{
|
|
using rep_type = std::chrono::milliseconds::rep;
|
|
static_assert(std::numeric_limits<int>::max() <= std::numeric_limits<rep_type>::max());
|
|
|
|
boost::unique_lock<boost::mutex> lock{refresh_sync_};
|
|
refresh_interval_ = std::chrono::milliseconds{millis};
|
|
if (millis <= 0)
|
|
{
|
|
thread_state_ = state::stop;
|
|
lock.unlock();
|
|
stop_refresh_loop();
|
|
}
|
|
}
|
|
|
|
int wallet::autoRefreshInterval() const
|
|
{
|
|
return refresh_interval_.count();
|
|
}
|
|
|
|
void wallet::addSubaddressAccount(const std::string& label)
|
|
{
|
|
data_->add_subaccount(label);
|
|
}
|
|
|
|
std::size_t wallet::numSubaddressAccounts() const
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock{data_->sync};
|
|
return data_->primary.subaccounts.size();
|
|
}
|
|
|
|
|
|
std::size_t wallet::numSubaddresses(const std::uint32_t accountIndex) const
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock{data_->sync};
|
|
const auto account = data_->primary.subaccounts.find(accountIndex);
|
|
if (account != data_->primary.subaccounts.end())
|
|
return account->second.minor.size();
|
|
set_critical(std::runtime_error{"numSubaddresses failed, " + std::to_string(accountIndex) + " does not exist"});
|
|
return 0;
|
|
}
|
|
|
|
std::string wallet::getSubaddressLabel(const std::uint32_t accountIndex, const std::uint32_t addressIndex) const
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock{data_->sync};
|
|
const auto major = data_->primary.subaccounts.find(accountIndex);
|
|
if (major != data_->primary.subaccounts.end())
|
|
{
|
|
const auto minor = major->second.minor.find(addressIndex);
|
|
if (minor != major->second.minor.end())
|
|
return minor->second.label;
|
|
}
|
|
set_critical(std::runtime_error{"getSubaddressLabel failed, " + std::to_string(accountIndex) + "," + std::to_string(addressIndex) + " does not exist"});
|
|
return {};
|
|
}
|
|
|
|
void wallet::setSubaddressLabel(const std::uint32_t accountIndex, const std::uint32_t addressIndex, const std::string &label)
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock{data_->sync};
|
|
auto major = data_->primary.subaccounts.find(accountIndex);
|
|
if (major != data_->primary.subaccounts.end())
|
|
{
|
|
auto minor = major->second.minor.find(addressIndex);
|
|
if (minor != major->second.minor.end())
|
|
{
|
|
minor->second.label = label;
|
|
return;
|
|
}
|
|
}
|
|
set_critical(std::runtime_error{"setSubaddressLabel failed, " + std::to_string(accountIndex) + "," + std::to_string(addressIndex) + " does not exist"});
|
|
}
|
|
|
|
void wallet::disposeTransaction(Monero::PendingTransaction * t)
|
|
{
|
|
delete t;
|
|
}
|
|
|
|
uint64_t wallet::estimateTransactionFee(const std::vector<std::pair<std::string, uint64_t>> &destinations,
|
|
Monero::PendingTransaction::Priority priority) const
|
|
{
|
|
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"};
|
|
}
|
|
|
|
bool wallet::exportOutputs(const std::string &filename, bool all)
|
|
{
|
|
throw std::runtime_error{"exportOutputs not implemented"};
|
|
}
|
|
|
|
bool wallet::importOutputs(const std::string &filename)
|
|
{
|
|
throw std::runtime_error{"importOutputs not implemented"};
|
|
}
|
|
|
|
bool wallet::scanTransactions(const std::vector<std::string> &txids)
|
|
{
|
|
throw std::runtime_error{"scanTransactions not implemented"};
|
|
}
|
|
|
|
Monero::AddressBook* wallet::addressBook()
|
|
{
|
|
if (!addressbook_)
|
|
addressbook_ = std::make_unique<address_book>(data_);
|
|
return addressbook_.get();
|
|
}
|
|
|
|
Monero::Subaddress* wallet::subaddress()
|
|
{
|
|
if (!subaddress_minor_)
|
|
subaddress_minor_ = std::make_unique<subaddress_minor>(data_);
|
|
return subaddress_minor_.get();
|
|
}
|
|
|
|
Monero::TransactionHistory* wallet::history()
|
|
{
|
|
if (!history_)
|
|
history_ = std::make_unique<transaction_history>(data_);
|
|
return history_.get();
|
|
}
|
|
|
|
Monero::SubaddressAccount* wallet::subaddressAccount()
|
|
{
|
|
if (!subaddresses_)
|
|
subaddresses_ = std::make_unique<subaddress_account>(data_);
|
|
return subaddresses_.get();
|
|
}
|
|
|
|
void wallet::setListener(Monero::WalletListener* listener)
|
|
{
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock{data_->sync_listener};
|
|
data_->listener = listener;
|
|
}
|
|
if (listener)
|
|
listener->onSetWallet(this);
|
|
}
|
|
|
|
std::uint32_t wallet::defaultMixin() const
|
|
{
|
|
return mixin_;
|
|
}
|
|
|
|
void wallet::setDefaultMixin(const std::uint32_t arg)
|
|
{
|
|
mixin_ = arg;
|
|
}
|
|
|
|
bool wallet::setCacheAttribute(const std::string &key, const std::string &val)
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock{data_->sync};
|
|
data_->primary.attributes.try_emplace(key).first->second = val;
|
|
return true;
|
|
}
|
|
|
|
std::string wallet::getCacheAttribute(const std::string &key) const
|
|
{
|
|
const boost::lock_guard<boost::mutex> lock{data_->sync};
|
|
const auto attribute = data_->primary.attributes.find(key);
|
|
if (attribute != data_->primary.attributes.end())
|
|
return attribute->second;
|
|
set_critical(std::runtime_error{"attribute " + key + " does not exist"});
|
|
return {};
|
|
}
|
|
|
|
bool wallet::setUserNote(const std::string &txid, const std::string ¬e)
|
|
{
|
|
crypto::hash binary_id{};
|
|
if (!epee::string_tools::hex_to_pod(txid, binary_id))
|
|
return false;
|
|
|
|
const boost::lock_guard<boost::mutex> lock{data_->sync};
|
|
auto iter = data_->primary.txes.find(binary_id);
|
|
if (iter == data_->primary.txes.end())
|
|
return false;
|
|
|
|
iter->second->description = note;
|
|
return true;
|
|
}
|
|
|
|
std::string wallet::getUserNote(const std::string &txid) const
|
|
{
|
|
crypto::hash binary_id{};
|
|
if (!epee::string_tools::hex_to_pod(txid, binary_id))
|
|
{
|
|
set_critical(std::runtime_error{"getUserNote given invalid hex id"});
|
|
return {};
|
|
}
|
|
|
|
const boost::lock_guard<boost::mutex> lock{data_->sync};
|
|
auto iter = data_->primary.txes.find(binary_id);
|
|
if (iter == data_->primary.txes.end())
|
|
return {};
|
|
return iter->second->description;
|
|
}
|
|
|
|
std::string wallet::getTxKey(const std::string &txid) const
|
|
{
|
|
crypto::hash binary_id{};
|
|
if (!epee::string_tools::hex_to_pod(txid, binary_id))
|
|
{
|
|
set_critical(std::runtime_error{"getTxKey given invalid hex id"});
|
|
return {};
|
|
}
|
|
|
|
const boost::lock_guard<boost::mutex> lock{data_->sync};
|
|
auto iter = data_->primary.txes.find(binary_id);
|
|
if (iter == data_->primary.txes.end())
|
|
{
|
|
set_critical(std::runtime_error{"getTxKey tx not found"});
|
|
return {};
|
|
}
|
|
|
|
static constexpr const auto hex_size = sizeof(crypto::secret_key) * 2;
|
|
|
|
std::string out;
|
|
out.reserve(iter->second->spends.size() * hex_size);
|
|
|
|
for (const auto& spend : iter->second->spends)
|
|
{
|
|
if (spend.second.secret)
|
|
{
|
|
out.insert(out.end(), hex_size, 0);
|
|
if (!epee::to_hex::buffer({out.data() + out.size() - hex_size, hex_size}, epee::as_byte_span(unwrap(unwrap(*spend.second.secret)))))
|
|
{
|
|
set_critical(std::runtime_error{"getTxKey conversion to hex failure"});
|
|
return {};
|
|
}
|
|
}
|
|
}
|
|
|
|
return out;
|
|
}
|
|
}} // lwsf // internal
|