aboutsummaryrefslogtreecommitdiff
path: root/src/wallet
diff options
context:
space:
mode:
Diffstat (limited to 'src/wallet')
-rw-r--r--src/wallet/api/pending_transaction.cpp1
-rw-r--r--src/wallet/api/transaction_history.cpp1
-rw-r--r--src/wallet/api/wallet.cpp15
-rw-r--r--src/wallet/api/wallet.h5
-rw-r--r--src/wallet/api/wallet2_api.h11
-rw-r--r--src/wallet/message_store.cpp7
-rw-r--r--src/wallet/node_rpc_proxy.cpp2
-rw-r--r--src/wallet/node_rpc_proxy.h1
-rw-r--r--src/wallet/ringdb.cpp3
-rw-r--r--src/wallet/wallet2.cpp128
-rw-r--r--src/wallet/wallet2.h14
-rw-r--r--src/wallet/wallet_rpc_payments.cpp91
-rw-r--r--src/wallet/wallet_rpc_server.cpp80
-rw-r--r--src/wallet/wallet_rpc_server.h2
-rw-r--r--src/wallet/wallet_rpc_server_commands_defs.h26
15 files changed, 283 insertions, 104 deletions
diff --git a/src/wallet/api/pending_transaction.cpp b/src/wallet/api/pending_transaction.cpp
index 24f6d37db..b28ffd64c 100644
--- a/src/wallet/api/pending_transaction.cpp
+++ b/src/wallet/api/pending_transaction.cpp
@@ -40,6 +40,7 @@
#include <vector>
#include <sstream>
#include <boost/format.hpp>
+#include <boost/filesystem.hpp>
using namespace std;
diff --git a/src/wallet/api/transaction_history.cpp b/src/wallet/api/transaction_history.cpp
index b6d52c58d..d8e4aab65 100644
--- a/src/wallet/api/transaction_history.cpp
+++ b/src/wallet/api/transaction_history.cpp
@@ -139,6 +139,7 @@ void TransactionHistoryImpl::refresh()
ti->m_paymentid = payment_id;
ti->m_coinbase = pd.m_coinbase;
ti->m_amount = pd.m_amount;
+ ti->m_fee = pd.m_fee;
ti->m_direction = TransactionInfo::Direction_In;
ti->m_hash = string_tools::pod_to_hex(pd.m_tx_hash);
ti->m_blockheight = pd.m_block_height;
diff --git a/src/wallet/api/wallet.cpp b/src/wallet/api/wallet.cpp
index 9acc8484c..3bbd9ce0b 100644
--- a/src/wallet/api/wallet.cpp
+++ b/src/wallet/api/wallet.cpp
@@ -45,10 +45,8 @@
#include <sstream>
#include <unordered_map>
-#ifdef WIN32
#include <boost/locale.hpp>
#include <boost/filesystem.hpp>
-#endif
using namespace std;
using namespace cryptonote;
@@ -791,11 +789,11 @@ bool WalletImpl::close(bool store)
return result;
}
-std::string WalletImpl::seed() const
+std::string WalletImpl::seed(const std::string& seed_offset) const
{
epee::wipeable_string seed;
if (m_wallet)
- m_wallet->get_seed(seed);
+ m_wallet->get_seed(seed, seed_offset);
return std::string(seed.data(), seed.size()); // TODO
}
@@ -1168,7 +1166,7 @@ bool WalletImpl::submitTransaction(const string &fileName) {
return true;
}
-bool WalletImpl::exportKeyImages(const string &filename)
+bool WalletImpl::exportKeyImages(const string &filename, bool all)
{
if (m_wallet->watch_only())
{
@@ -1178,7 +1176,7 @@ bool WalletImpl::exportKeyImages(const string &filename)
try
{
- if (!m_wallet->export_key_images(filename))
+ if (!m_wallet->export_key_images(filename), all)
{
setStatusError(tr("failed to save file ") + filename);
return false;
@@ -2104,6 +2102,11 @@ bool WalletImpl::watchOnly() const
return m_wallet->watch_only();
}
+bool WalletImpl::isDeterministic() const
+{
+ return m_wallet->is_deterministic();
+}
+
void WalletImpl::clearStatus() const
{
boost::lock_guard<boost::mutex> l(m_statusMutex);
diff --git a/src/wallet/api/wallet.h b/src/wallet/api/wallet.h
index 3bf3e759b..0f3614bb4 100644
--- a/src/wallet/api/wallet.h
+++ b/src/wallet/api/wallet.h
@@ -81,7 +81,7 @@ public:
const std::string &device_name);
Device getDeviceType() const override;
bool close(bool store = true);
- std::string seed() const override;
+ std::string seed(const std::string& seed_offset = "") const override;
std::string getSeedLanguage() const override;
void setSeedLanguage(const std::string &arg) override;
// void setListener(Listener *) {}
@@ -129,6 +129,7 @@ public:
void setRecoveringFromDevice(bool recoveringFromDevice) override;
void setSubaddressLookahead(uint32_t major, uint32_t minor) override;
bool watchOnly() const override;
+ bool isDeterministic() const override;
bool rescanSpent() override;
NetworkType nettype() const override {return static_cast<NetworkType>(m_wallet->nettype());}
void hardForkInfo(uint8_t &version, uint64_t &earliest_height) const override;
@@ -164,7 +165,7 @@ public:
virtual PendingTransaction * createSweepUnmixableTransaction() override;
bool submitTransaction(const std::string &fileName) override;
virtual UnsignedTransaction * loadUnsignedTx(const std::string &unsigned_filename) override;
- bool exportKeyImages(const std::string &filename) override;
+ bool exportKeyImages(const std::string &filename, bool all = false) override;
bool importKeyImages(const std::string &filename) override;
virtual void disposeTransaction(PendingTransaction * t) override;
diff --git a/src/wallet/api/wallet2_api.h b/src/wallet/api/wallet2_api.h
index 44928a422..b1cebedaf 100644
--- a/src/wallet/api/wallet2_api.h
+++ b/src/wallet/api/wallet2_api.h
@@ -446,7 +446,7 @@ struct Wallet
};
virtual ~Wallet() = 0;
- virtual std::string seed() const = 0;
+ virtual std::string seed(const std::string& seed_offset = "") const = 0;
virtual std::string getSeedLanguage() const = 0;
virtual void setSeedLanguage(const std::string &arg) = 0;
//! returns wallet status (Status_Ok | Status_Error)
@@ -627,6 +627,12 @@ struct Wallet
virtual bool watchOnly() const = 0;
/**
+ * @brief isDeterministic - checks if wallet keys are deterministic
+ * @return - true if deterministic
+ */
+ virtual bool isDeterministic() const = 0;
+
+ /**
* @brief blockChainHeight - returns current blockchain height
* @return
*/
@@ -901,9 +907,10 @@ struct Wallet
/*!
* \brief exportKeyImages - exports key images to file
* \param filename
+ * \param all - export all key images or only those that have not yet been exported
* \return - true on success
*/
- virtual bool exportKeyImages(const std::string &filename) = 0;
+ virtual bool exportKeyImages(const std::string &filename, bool all = false) = 0;
/*!
* \brief importKeyImages - imports key images from file
diff --git a/src/wallet/message_store.cpp b/src/wallet/message_store.cpp
index 303b576c7..b7b29420b 100644
--- a/src/wallet/message_store.cpp
+++ b/src/wallet/message_store.cpp
@@ -30,6 +30,8 @@
#include <boost/archive/portable_binary_iarchive.hpp>
#include <boost/format.hpp>
#include <boost/algorithm/string.hpp>
+#include <boost/system/error_code.hpp>
+#include <boost/filesystem.hpp>
#include <fstream>
#include <sstream>
#include "file_io_utils.h"
@@ -1340,7 +1342,10 @@ bool message_store::check_for_messages(const multisig_wallet_state &state, std::
}
}
std::vector<transport_message> transport_messages;
- bool r = m_transporter.receive_messages(destinations, transport_messages);
+ if (!m_transporter.receive_messages(destinations, transport_messages))
+ {
+ return false;
+ }
if (!m_run.load(std::memory_order_relaxed))
{
// Stop was called, don't waste time processing the messages
diff --git a/src/wallet/node_rpc_proxy.cpp b/src/wallet/node_rpc_proxy.cpp
index 48a602bf3..a576c267c 100644
--- a/src/wallet/node_rpc_proxy.cpp
+++ b/src/wallet/node_rpc_proxy.cpp
@@ -32,6 +32,8 @@
#include "rpc/rpc_payment_costs.h"
#include "storages/http_abstract_invoke.h"
+#include <boost/thread.hpp>
+
#define RETURN_ON_RPC_RESPONSE_ERROR(r, error, res, method) \
do { \
CHECK_AND_ASSERT_MES(error.code == 0, error.message, error.message); \
diff --git a/src/wallet/node_rpc_proxy.h b/src/wallet/node_rpc_proxy.h
index 51b7f01dd..f5e3fca5f 100644
--- a/src/wallet/node_rpc_proxy.h
+++ b/src/wallet/node_rpc_proxy.h
@@ -30,6 +30,7 @@
#include <string>
#include <boost/thread/mutex.hpp>
+#include <boost/thread/recursive_mutex.hpp>
#include "include_base_utils.h"
#include "net/abstract_http_client.h"
#include "rpc/core_rpc_server_commands_defs.h"
diff --git a/src/wallet/ringdb.cpp b/src/wallet/ringdb.cpp
index dfeb987ca..025a2037f 100644
--- a/src/wallet/ringdb.cpp
+++ b/src/wallet/ringdb.cpp
@@ -42,9 +42,6 @@
#define V1TAG ((uint64_t)798237759845202)
-static const char zerokey[8] = {0};
-static const MDB_val zerokeyval = { sizeof(zerokey), (void *)zerokey };
-
static int compare_hash32(const MDB_val *a, const MDB_val *b)
{
uint32_t *va = (uint32_t*) a->mv_data;
diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp
index d80335db8..0af896c76 100644
--- a/src/wallet/wallet2.cpp
+++ b/src/wallet/wallet2.cpp
@@ -30,6 +30,7 @@
#include <numeric>
#include <tuple>
+#include <queue>
#include <boost/format.hpp>
#include <boost/optional/optional.hpp>
#include <boost/algorithm/string/classification.hpp>
@@ -344,8 +345,6 @@ std::string get_weight_string(const cryptonote::transaction &tx, size_t blob_siz
std::unique_ptr<tools::wallet2> make_basic(const boost::program_options::variables_map& vm, bool unattended, const options& opts, const std::function<boost::optional<tools::password_container>(const char *, bool)> &password_prompter)
{
- namespace ip = boost::asio::ip;
-
const bool testnet = command_line::get_arg(vm, opts.testnet);
const bool stagenet = command_line::get_arg(vm, opts.stagenet);
const network_type nettype = testnet ? TESTNET : stagenet ? STAGENET : MAINNET;
@@ -367,7 +366,7 @@ std::unique_ptr<tools::wallet2> make_basic(const boost::program_options::variabl
// user specified CA file or fingeprints implies enabled SSL by default
epee::net_utils::ssl_options_t ssl_options = epee::net_utils::ssl_support_t::e_ssl_support_enabled;
- if (command_line::get_arg(vm, opts.daemon_ssl_allow_any_cert))
+ if (daemon_ssl_allow_any_cert)
ssl_options.verification = epee::net_utils::ssl_verification_t::none;
else if (!daemon_ssl_ca_file.empty() || !daemon_ssl_allowed_fingerprints.empty())
{
@@ -1004,9 +1003,6 @@ bool get_pruned_tx(const cryptonote::COMMAND_RPC_GET_TRANSACTIONS::entry &entry,
namespace tools
{
-// for now, limit to 30 attempts. TODO: discuss a good number to limit to.
-const size_t MAX_SPLIT_ATTEMPTS = 30;
-
constexpr const std::chrono::seconds wallet2::rpc_timeout;
const char* wallet2::tr(const char* str) { return i18n_translate(str, "tools::wallet2"); }
@@ -1602,6 +1598,47 @@ std::string wallet2::get_subaddress_label(const cryptonote::subaddress_index& in
return m_subaddress_labels[index.major][index.minor];
}
//----------------------------------------------------------------------------------------------------
+void wallet2::scan_tx(const std::vector<crypto::hash> &txids)
+{
+ // Get the transactions from daemon in batches and add them to a priority queue ordered in chronological order
+ auto cmp_tx_entry = [](const cryptonote::COMMAND_RPC_GET_TRANSACTIONS::entry& l, const cryptonote::COMMAND_RPC_GET_TRANSACTIONS::entry& r)
+ { return l.block_height > r.block_height; };
+
+ std::priority_queue<cryptonote::COMMAND_RPC_GET_TRANSACTIONS::entry, std::vector<COMMAND_RPC_GET_TRANSACTIONS::entry>, decltype(cmp_tx_entry)> txq(cmp_tx_entry);
+ const size_t SLICE_SIZE = 100; // RESTRICTED_TRANSACTIONS_COUNT as defined in rpc/core_rpc_server.cpp, hardcoded in daemon code
+ for(size_t slice = 0; slice < txids.size(); slice += SLICE_SIZE) {
+ cryptonote::COMMAND_RPC_GET_TRANSACTIONS::request req = AUTO_VAL_INIT(req);
+ cryptonote::COMMAND_RPC_GET_TRANSACTIONS::response res = AUTO_VAL_INIT(res);
+ req.decode_as_json = false;
+ req.prune = true;
+
+ size_t ntxes = slice + SLICE_SIZE > txids.size() ? txids.size() - slice : SLICE_SIZE;
+ for (size_t i = slice; i < slice + ntxes; ++i)
+ req.txs_hashes.push_back(epee::string_tools::pod_to_hex(txids[i]));
+
+ {
+ const boost::lock_guard<boost::recursive_mutex> lock{m_daemon_rpc_mutex};
+ req.client = get_client_signature();
+ bool r = epee::net_utils::invoke_http_json("/gettransactions", req, res, *m_http_client, rpc_timeout);
+ THROW_WALLET_EXCEPTION_IF(!r, error::wallet_internal_error, "Failed to get transaction from daemon");
+ THROW_WALLET_EXCEPTION_IF(res.txs.size() != req.txs_hashes.size(), error::wallet_internal_error, "Failed to get transaction from daemon");
+ }
+
+ for (auto& tx_info : res.txs)
+ txq.push(tx_info);
+ }
+
+ // Process the transactions in chronologically ascending order
+ while(!txq.empty()) {
+ auto& tx_info = txq.top();
+ cryptonote::transaction tx;
+ crypto::hash tx_hash;
+ THROW_WALLET_EXCEPTION_IF(!get_pruned_tx(tx_info, tx, tx_hash), error::wallet_internal_error, "Failed to get transaction from daemon (2)");
+ process_new_transaction(tx_hash, tx, tx_info.output_indices, tx_info.block_height, 0, tx_info.block_timestamp, false, tx_info.in_pool, tx_info.double_spend_seen, {}, {});
+ txq.pop();
+ }
+}
+//----------------------------------------------------------------------------------------------------
void wallet2::set_subaddress_label(const cryptonote::subaddress_index& index, const std::string &label)
{
THROW_WALLET_EXCEPTION_IF(index.major >= m_subaddress_labels.size(), error::account_index_outofbound);
@@ -5669,6 +5706,16 @@ void wallet2::load(const std::string& wallet_, const epee::wipeable_string& pass
if (::serialization::serialize(ar, *this))
if (::serialization::check_stream_state(ar))
loaded = true;
+ if (!loaded)
+ {
+ std::stringstream iss;
+ iss << cache_data;
+ binary_archive<false> ar(iss);
+ ar.enable_varint_bug_backward_compatibility();
+ if (::serialization::serialize(ar, *this))
+ if (::serialization::check_stream_state(ar))
+ loaded = true;
+ }
}
catch(...) { }
@@ -5988,7 +6035,7 @@ uint64_t wallet2::balance(uint32_t index_major, bool strict) const
{
uint64_t amount = 0;
if(m_light_wallet)
- return m_light_wallet_unlocked_balance;
+ return m_light_wallet_balance;
for (const auto& i : balance_per_subaddress(index_major, strict))
amount += i.second;
return amount;
@@ -6002,7 +6049,7 @@ uint64_t wallet2::unlocked_balance(uint32_t index_major, bool strict, uint64_t *
if (time_to_unlock)
*time_to_unlock = 0;
if(m_light_wallet)
- return m_light_wallet_balance;
+ return m_light_wallet_unlocked_balance;
for (const auto& i : unlocked_balance_per_subaddress(index_major, strict))
{
amount += i.second.first;
@@ -6042,6 +6089,14 @@ std::map<uint32_t, uint64_t> wallet2::balance_per_subaddress(uint32_t index_majo
found->second += utx.second.m_change;
}
}
+
+ for (const auto& utx: m_unconfirmed_payments)
+ {
+ if (utx.second.m_pd.m_subaddr_index.major == index_major)
+ {
+ amount_per_subaddr[utx.second.m_pd.m_subaddr_index.minor] += utx.second.m_pd.m_amount;
+ }
+ }
}
return amount_per_subaddr;
}
@@ -7984,7 +8039,6 @@ void wallet2::light_wallet_get_outs(std::vector<std::vector<tools::wallet2::get_
// Check if we got enough outputs for each amount
for(auto& out: ores.amount_outs) {
- const uint64_t out_amount = boost::lexical_cast<uint64_t>(out.amount);
THROW_WALLET_EXCEPTION_IF(out.outputs.size() < light_wallet_requested_outputs_count , error::wallet_internal_error, "Not enough outputs for amount: " + boost::lexical_cast<std::string>(out.amount));
MDEBUG(out.outputs.size() << " outputs for amount "+ boost::lexical_cast<std::string>(out.amount) + " received from light wallet node");
}
@@ -9778,13 +9832,18 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
TX() : weight(0), needed_fee(0) {}
- void add(const cryptonote::tx_destination_entry &de, uint64_t amount, unsigned int original_output_index, bool merge_destinations) {
+ /* Add an output to the transaction.
+ * Returns True if the output was added, False if there are no more available output slots.
+ */
+ bool add(const cryptonote::tx_destination_entry &de, uint64_t amount, unsigned int original_output_index, bool merge_destinations, size_t max_dsts) {
if (merge_destinations)
{
std::vector<cryptonote::tx_destination_entry>::iterator i;
i = std::find_if(dsts.begin(), dsts.end(), [&](const cryptonote::tx_destination_entry &d) { return !memcmp (&d.addr, &de.addr, sizeof(de.addr)); });
if (i == dsts.end())
{
+ if (dsts.size() >= max_dsts)
+ return false;
dsts.push_back(de);
i = dsts.end() - 1;
i->amount = 0;
@@ -9797,12 +9856,15 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
std::string("original_output_index too large: ") + std::to_string(original_output_index) + " > " + std::to_string(dsts.size()));
if (original_output_index == dsts.size())
{
+ if (dsts.size() >= max_dsts)
+ return false;
dsts.push_back(de);
dsts.back().amount = 0;
}
THROW_WALLET_EXCEPTION_IF(memcmp(&dsts[original_output_index].addr, &de.addr, sizeof(de.addr)), error::wallet_internal_error, "Mismatched destination address");
dsts[original_output_index].amount += amount;
}
+ return true;
}
};
std::vector<TX> txes;
@@ -10072,6 +10134,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
// clear any fake outs we'd already gathered, since we'll need a new set
outs.clear();
+ bool out_slots_exhausted = false;
if (adding_fee)
{
LOG_PRINT_L2("We need more fee, adding it to fee");
@@ -10084,20 +10147,32 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
// we can fully pay that destination
LOG_PRINT_L2("We can fully pay " << get_account_address_as_str(m_nettype, dsts[0].is_subaddress, dsts[0].addr) <<
" for " << print_money(dsts[0].amount));
- tx.add(dsts[0], dsts[0].amount, original_output_index, m_merge_destinations);
+ if (!tx.add(dsts[0], dsts[0].amount, original_output_index, m_merge_destinations, BULLETPROOF_MAX_OUTPUTS-1))
+ {
+ LOG_PRINT_L2("Didn't pay: ran out of output slots");
+ out_slots_exhausted = true;
+ break;
+ }
available_amount -= dsts[0].amount;
dsts[0].amount = 0;
pop_index(dsts, 0);
++original_output_index;
}
- if (available_amount > 0 && !dsts.empty() && estimate_tx_weight(use_rct, tx.selected_transfers.size(), fake_outs_count, tx.dsts.size()+1, extra.size(), bulletproof, clsag) < TX_WEIGHT_TARGET(upper_transaction_weight_limit)) {
+ if (!out_slots_exhausted && available_amount > 0 && !dsts.empty() && estimate_tx_weight(use_rct, tx.selected_transfers.size(), fake_outs_count, tx.dsts.size()+1, extra.size(), bulletproof, clsag) < TX_WEIGHT_TARGET(upper_transaction_weight_limit)) {
// we can partially fill that destination
LOG_PRINT_L2("We can partially pay " << get_account_address_as_str(m_nettype, dsts[0].is_subaddress, dsts[0].addr) <<
" for " << print_money(available_amount) << "/" << print_money(dsts[0].amount));
- tx.add(dsts[0], available_amount, original_output_index, m_merge_destinations);
- dsts[0].amount -= available_amount;
- available_amount = 0;
+ if (tx.add(dsts[0], available_amount, original_output_index, m_merge_destinations, BULLETPROOF_MAX_OUTPUTS-1))
+ {
+ dsts[0].amount -= available_amount;
+ available_amount = 0;
+ }
+ else
+ {
+ LOG_PRINT_L2("Didn't pay: ran out of output slots");
+ out_slots_exhausted = true;
+ }
}
}
@@ -10105,8 +10180,15 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp
LOG_PRINT_L2("Considering whether to create a tx now, " << tx.selected_transfers.size() << " inputs, tx limit "
<< upper_transaction_weight_limit);
bool try_tx = false;
+
+ // If the new transaction is full, create it and start a new one
+ if (out_slots_exhausted)
+ {
+ LOG_PRINT_L2("Transaction is full, will create it and start a new tx");
+ try_tx = true;
+ }
// if we have preferred picks, but haven't yet used all of them, continue
- if (preferred_inputs.empty())
+ else if (preferred_inputs.empty())
{
if (adding_fee)
{
@@ -10303,8 +10385,6 @@ bool wallet2::sanity_check(const std::vector<wallet2::pending_tx> &ptx_vector, s
{
MDEBUG("sanity_check: " << ptx_vector.size() << " txes, " << dsts.size() << " destinations");
- hw::device &hwdev = m_account.get_device();
-
THROW_WALLET_EXCEPTION_IF(ptx_vector.empty(), error::wallet_internal_error, "No transactions");
// check every party in there does receive at least the required amount
@@ -10341,7 +10421,6 @@ bool wallet2::sanity_check(const std::vector<wallet2::pending_tx> &ptx_vector, s
for (const auto &r: required)
{
const account_public_address &address = r.first;
- const crypto::public_key &view_pkey = address.m_view_public_key;
uint64_t total_received = 0;
for (const auto &ptx: ptx_vector)
@@ -12459,7 +12538,7 @@ crypto::public_key wallet2::get_tx_pub_key_from_received_outs(const tools::walle
bool wallet2::export_key_images(const std::string &filename, bool all) const
{
PERF_TIMER(export_key_images);
- std::pair<size_t, std::vector<std::pair<crypto::key_image, crypto::signature>>> ski = export_key_images(all);
+ std::pair<uint64_t, std::vector<std::pair<crypto::key_image, crypto::signature>>> ski = export_key_images(all);
std::string magic(KEY_IMAGE_EXPORT_FILE_MAGIC, strlen(KEY_IMAGE_EXPORT_FILE_MAGIC));
const cryptonote::account_public_address &keys = get_account().get_keys().m_account_address;
const uint32_t offset = ski.first;
@@ -12486,7 +12565,7 @@ bool wallet2::export_key_images(const std::string &filename, bool all) const
}
//----------------------------------------------------------------------------------------------------
-std::pair<size_t, std::vector<std::pair<crypto::key_image, crypto::signature>>> wallet2::export_key_images(bool all) const
+std::pair<uint64_t, std::vector<std::pair<crypto::key_image, crypto::signature>>> wallet2::export_key_images(bool all) const
{
PERF_TIMER(export_key_images_raw);
std::vector<std::pair<crypto::key_image, crypto::signature>> ski;
@@ -12983,7 +13062,7 @@ void wallet2::import_blockchain(const std::tuple<size_t, crypto::hash, std::vect
m_last_block_reward = cryptonote::get_outs_money_amount(genesis.miner_tx);
}
//----------------------------------------------------------------------------------------------------
-std::pair<size_t, std::vector<tools::wallet2::transfer_details>> wallet2::export_outputs(bool all) const
+std::pair<uint64_t, std::vector<tools::wallet2::transfer_details>> wallet2::export_outputs(bool all) const
{
PERF_TIMER(export_outputs);
std::vector<tools::wallet2::transfer_details> outs;
@@ -13023,7 +13102,7 @@ std::string wallet2::export_outputs_to_str(bool all) const
return magic + ciphertext;
}
//----------------------------------------------------------------------------------------------------
-size_t wallet2::import_outputs(const std::pair<size_t, std::vector<tools::wallet2::transfer_details>> &outputs)
+size_t wallet2::import_outputs(const std::pair<uint64_t, std::vector<tools::wallet2::transfer_details>> &outputs)
{
PERF_TIMER(import_outputs);
@@ -13129,7 +13208,7 @@ size_t wallet2::import_outputs_from_str(const std::string &outputs_st)
try
{
std::string body(data, headerlen);
- std::pair<size_t, std::vector<tools::wallet2::transfer_details>> outputs;
+ std::pair<uint64_t, std::vector<tools::wallet2::transfer_details>> outputs;
try
{
std::stringstream iss;
@@ -13229,7 +13308,6 @@ rct::multisig_kLRki wallet2::get_multisig_composite_kLRki(size_t n, const std::u
{
CHECK_AND_ASSERT_THROW_MES(n < m_transfers.size(), "Bad transfer index");
- const transfer_details &td = m_transfers[n];
rct::multisig_kLRki kLRki = get_multisig_kLRki(n, rct::skGen());
// pick a L/R pair from every other participant but one
diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h
index 2e455c40c..e96a6b51c 100644
--- a/src/wallet/wallet2.h
+++ b/src/wallet/wallet2.h
@@ -327,7 +327,7 @@ private:
uint64_t m_block_height;
cryptonote::transaction_prefix m_tx;
crypto::hash m_txid;
- size_t m_internal_output_index;
+ uint64_t m_internal_output_index;
uint64_t m_global_output_index;
bool m_spent;
bool m_frozen;
@@ -338,7 +338,7 @@ private:
bool m_rct;
bool m_key_image_known;
bool m_key_image_request; // view wallets: we want to request it; cold wallets: it was requested
- size_t m_pk_index;
+ uint64_t m_pk_index;
cryptonote::subaddress_index m_subaddr_index;
bool m_key_image_partial;
std::vector<rct::key> m_multisig_k;
@@ -1264,6 +1264,8 @@ private:
std::string get_spend_proof(const crypto::hash &txid, const std::string &message);
bool check_spend_proof(const crypto::hash &txid, const std::string &message, const std::string &sig_str);
+ void scan_tx(const std::vector<crypto::hash> &txids);
+
/*!
* \brief Generates a proof that proves the reserve of unspent funds
* \param account_minreserve When specified, collect outputs only belonging to the given account and prove the smallest reserve above the given amount
@@ -1370,9 +1372,9 @@ private:
bool verify_with_public_key(const std::string &data, const crypto::public_key &public_key, const std::string &signature) const;
// Import/Export wallet data
- std::pair<size_t, std::vector<tools::wallet2::transfer_details>> export_outputs(bool all = false) const;
+ std::pair<uint64_t, std::vector<tools::wallet2::transfer_details>> export_outputs(bool all = false) const;
std::string export_outputs_to_str(bool all = false) const;
- size_t import_outputs(const std::pair<size_t, std::vector<tools::wallet2::transfer_details>> &outputs);
+ size_t import_outputs(const std::pair<uint64_t, std::vector<tools::wallet2::transfer_details>> &outputs);
size_t import_outputs_from_str(const std::string &outputs_st);
payment_container export_payments() const;
void import_payments(const payment_container &payments);
@@ -1380,7 +1382,7 @@ private:
std::tuple<size_t, crypto::hash, std::vector<crypto::hash>> export_blockchain() const;
void import_blockchain(const std::tuple<size_t, crypto::hash, std::vector<crypto::hash>> &bc);
bool export_key_images(const std::string &filename, bool all = false) const;
- std::pair<size_t, std::vector<std::pair<crypto::key_image, crypto::signature>>> export_key_images(bool all = false) const;
+ std::pair<uint64_t, std::vector<std::pair<crypto::key_image, crypto::signature>>> export_key_images(bool all = false) const;
uint64_t import_key_images(const std::vector<std::pair<crypto::key_image, crypto::signature>> &signed_key_images, size_t offset, uint64_t &spent, uint64_t &unspent, bool check_spent = true);
uint64_t import_key_images(const std::string &filename, uint64_t &spent, uint64_t &unspent);
bool import_key_images(std::vector<crypto::key_image> key_images, size_t offset=0, boost::optional<std::unordered_set<size_t>> selected_transfers=boost::none);
@@ -1426,7 +1428,7 @@ private:
bool get_rpc_payment_info(bool mining, bool &payment_required, uint64_t &credits, uint64_t &diff, uint64_t &credits_per_hash_found, cryptonote::blobdata &hashing_blob, uint64_t &height, uint64_t &seed_height, crypto::hash &seed_hash, crypto::hash &next_seed_hash, uint32_t &cookie);
bool daemon_requires_payment();
bool make_rpc_payment(uint32_t nonce, uint32_t cookie, uint64_t &credits, uint64_t &balance);
- bool search_for_rpc_payment(uint64_t credits_target, const std::function<bool(uint64_t, uint64_t)> &startfunc, const std::function<bool(unsigned)> &contfunc, const std::function<bool(uint64_t)> &foundfunc = NULL, const std::function<void(const std::string&)> &errorfunc = NULL);
+ bool search_for_rpc_payment(uint64_t credits_target, uint32_t n_threads, const std::function<bool(uint64_t, uint64_t)> &startfunc, const std::function<bool(unsigned)> &contfunc, const std::function<bool(uint64_t)> &foundfunc = NULL, const std::function<void(const std::string&)> &errorfunc = NULL);
template<typename T> void handle_payment_changes(const T &res, std::true_type) {
if (res.status == CORE_RPC_STATUS_OK || res.status == CORE_RPC_STATUS_PAYMENT_REQUIRED)
m_rpc_payment_state.credits = res.credits;
diff --git a/src/wallet/wallet_rpc_payments.cpp b/src/wallet/wallet_rpc_payments.cpp
index 6527b1384..bf278f695 100644
--- a/src/wallet/wallet_rpc_payments.cpp
+++ b/src/wallet/wallet_rpc_payments.cpp
@@ -42,6 +42,7 @@
#include "cryptonote_basic/blobdatatype.h"
#include "common/i18n.h"
#include "common/util.h"
+#include "common/threadpool.h"
#undef MONERO_DEFAULT_LOG_CATEGORY
#define MONERO_DEFAULT_LOG_CATEGORY "wallet.wallet2.rpc_payments"
@@ -101,7 +102,7 @@ bool wallet2::make_rpc_payment(uint32_t nonce, uint32_t cookie, uint64_t &credit
return true;
}
//----------------------------------------------------------------------------------------------------
-bool wallet2::search_for_rpc_payment(uint64_t credits_target, const std::function<bool(uint64_t, uint64_t)> &startfunc, const std::function<bool(unsigned)> &contfunc, const std::function<bool(uint64_t)> &foundfunc, const std::function<void(const std::string&)> &errorfunc)
+bool wallet2::search_for_rpc_payment(uint64_t credits_target, uint32_t n_threads, const std::function<bool(uint64_t, uint64_t)> &startfunc, const std::function<bool(unsigned)> &contfunc, const std::function<bool(uint64_t)> &foundfunc, const std::function<void(const std::string&)> &errorfunc)
{
bool need_payment = false;
bool payment_required;
@@ -139,49 +140,65 @@ bool wallet2::search_for_rpc_payment(uint64_t credits_target, const std::functio
continue;
}
- crypto::hash hash;
- const uint32_t local_nonce = nonce++; // wrapping's OK
- *(uint32_t*)(hashing_blob.data() + 39) = SWAP32LE(local_nonce);
- const uint8_t major_version = hashing_blob[0];
- if (major_version >= RX_BLOCK_VERSION)
- {
- const int miners = 1;
- crypto::rx_slow_hash(height, seed_height, seed_hash.data, hashing_blob.data(), hashing_blob.size(), hash.data, miners, 0);
- }
- else
+ if(n_threads == 0)
+ n_threads = boost::thread::hardware_concurrency();
+
+ std::vector<crypto::hash> hash(n_threads);
+ tools::threadpool& tpool = tools::threadpool::getInstance();
+ tools::threadpool::waiter waiter(tpool);
+
+ const uint32_t local_nonce = nonce += n_threads; // wrapping's OK
+ for (size_t i = 0; i < n_threads; i++)
{
- int cn_variant = hashing_blob[0] >= 7 ? hashing_blob[0] - 6 : 0;
- crypto::cn_slow_hash(hashing_blob.data(), hashing_blob.size(), hash, cn_variant, height);
+ tpool.submit(&waiter, [&, i] {
+ *(uint32_t*)(hashing_blob.data() + 39) = SWAP32LE(local_nonce-i);
+ const uint8_t major_version = hashing_blob[0];
+ if (major_version >= RX_BLOCK_VERSION)
+ {
+ const int miners = 1;
+ crypto::rx_slow_hash(height, seed_height, seed_hash.data, hashing_blob.data(), hashing_blob.size(), hash[i].data, miners, 0);
+ }
+ else
+ {
+ int cn_variant = hashing_blob[0] >= 7 ? hashing_blob[0] - 6 : 0;
+ crypto::cn_slow_hash(hashing_blob.data(), hashing_blob.size(), hash[i], cn_variant, height);
+ }
+ });
}
- ++n_hashes;
- if (cryptonote::check_hash(hash, diff))
+ waiter.wait();
+ n_hashes += n_threads;
+
+ for(size_t i=0; i < n_threads; i++)
{
- uint64_t credits, balance;
- try
+ if (cryptonote::check_hash(hash[i], diff))
{
- make_rpc_payment(local_nonce, cookie, credits, balance);
- if (credits != credits_per_hash_found)
+ uint64_t credits, balance;
+ try
{
- MERROR("Found nonce, but daemon did not credit us with the expected amount");
+ make_rpc_payment(local_nonce-i, cookie, credits, balance);
+ if (credits != credits_per_hash_found)
+ {
+ MERROR("Found nonce, but daemon did not credit us with the expected amount");
+ if (errorfunc)
+ errorfunc("Found nonce, but daemon did not credit us with the expected amount");
+ return false;
+ }
+ MDEBUG("Found nonce " << local_nonce-i << " at diff " << diff << ", gets us " << credits_per_hash_found << ", now " << balance << " credits");
+ if (!foundfunc(credits))
+ break;
+ }
+ catch (const tools::error::wallet_coded_rpc_error &e)
+ {
+ MWARNING("Found a local_nonce at diff " << diff << ", but failed to send it to the daemon");
if (errorfunc)
- errorfunc("Found nonce, but daemon did not credit us with the expected amount");
- return false;
+ errorfunc("Found nonce, but daemon errored out with error " + std::to_string(e.code()) + ": " + e.status() + ", continuing");
+ }
+ catch (const std::exception &e)
+ {
+ MWARNING("Found a local_nonce at diff " << diff << ", but failed to send it to the daemon");
+ if (errorfunc)
+ errorfunc("Found nonce, but daemon errored out with: '" + std::string(e.what()) + "', continuing");
}
- MDEBUG("Found nonce " << local_nonce << " at diff " << diff << ", gets us " << credits_per_hash_found << ", now " << balance << " credits");
- if (!foundfunc(credits))
- break;
- }
- catch (const tools::error::wallet_coded_rpc_error &e)
- {
- MWARNING("Found a local_nonce at diff " << diff << ", but failed to send it to the daemon");
- if (errorfunc)
- errorfunc("Found nonce, but daemon errored out with error " + std::to_string(e.code()) + ": " + e.status() + ", continuing");
- }
- catch (const std::exception &e)
- {
- MWARNING("Found a local_nonce at diff " << diff << ", but failed to send it to the daemon");
- if (errorfunc)
- errorfunc("Found nonce, but daemon errored out with: '" + std::string(e.what()) + "', continuing");
}
}
}
diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp
index 03db8b70f..b39a40b64 100644
--- a/src/wallet/wallet_rpc_server.cpp
+++ b/src/wallet/wallet_rpc_server.cpp
@@ -687,7 +687,7 @@ namespace tools
{
if (!m_wallet) return not_open(er);
const std::pair<std::map<std::string, std::string>, std::vector<std::string>> account_tags = m_wallet->get_account_tags();
- for (const std::pair<std::string, std::string>& p : account_tags.first)
+ for (const std::pair<const std::string, std::string>& p : account_tags.first)
{
res.account_tags.resize(res.account_tags.size() + 1);
auto& info = res.account_tags.back();
@@ -1282,7 +1282,6 @@ namespace tools
dests.erase(cd.change_dts.addr);
}
- size_t n_dummy_outputs = 0;
for (auto i = dests.begin(); i != dests.end(); )
{
if (i->second.second > 0)
@@ -1888,6 +1887,7 @@ namespace tools
rpc_transfers.tx_hash = epee::string_tools::pod_to_hex(td.m_txid);
rpc_transfers.subaddr_index = {td.m_subaddr_index.major, td.m_subaddr_index.minor};
rpc_transfers.key_image = td.m_key_image_known ? epee::string_tools::pod_to_hex(td.m_key_image) : "";
+ rpc_transfers.pubkey = epee::string_tools::pod_to_hex(td.get_public_key());
rpc_transfers.block_height = td.m_block_height;
rpc_transfers.frozen = td.m_frozen;
rpc_transfers.unlocked = m_wallet->is_transfer_unlocked(td);
@@ -2702,7 +2702,7 @@ namespace tools
if (!m_wallet) return not_open(er);
try
{
- std::pair<size_t, std::vector<std::pair<crypto::key_image, crypto::signature>>> ski = m_wallet->export_key_images(req.all);
+ std::pair<uint64_t, std::vector<std::pair<crypto::key_image, crypto::signature>>> ski = m_wallet->export_key_images(req.all);
res.offset = ski.first;
res.signed_key_images.resize(ski.second.size());
for (size_t n = 0; n < ski.second.size(); ++n)
@@ -3014,6 +3014,41 @@ namespace tools
return true;
}
//------------------------------------------------------------------------------------------------------------------------------
+ bool wallet_rpc_server::on_scan_tx(const wallet_rpc::COMMAND_RPC_SCAN_TX::request& req, wallet_rpc::COMMAND_RPC_SCAN_TX::response& res, epee::json_rpc::error& er, const connection_context *ctx)
+ {
+ if (!m_wallet) return not_open(er);
+ if (m_restricted)
+ {
+ er.code = WALLET_RPC_ERROR_CODE_DENIED;
+ er.message = "Command unavailable in restricted mode.";
+ return false;
+ }
+
+ std::vector<crypto::hash> txids;
+ std::list<std::string>::const_iterator i = req.txids.begin();
+ while (i != req.txids.end())
+ {
+ cryptonote::blobdata txid_blob;
+ if(!epee::string_tools::parse_hexstr_to_binbuff(*i++, txid_blob) || txid_blob.size() != sizeof(crypto::hash))
+ {
+ er.code = WALLET_RPC_ERROR_CODE_WRONG_TXID;
+ er.message = "TX ID has invalid format";
+ return false;
+ }
+
+ crypto::hash txid = *reinterpret_cast<const crypto::hash*>(txid_blob.data());
+ txids.push_back(txid);
+ }
+
+ try {
+ m_wallet->scan_tx(txids);
+ } catch (const std::exception &e) {
+ handle_rpc_exception(std::current_exception(), er, WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR);
+ return false;
+ }
+ return true;
+ }
+ //------------------------------------------------------------------------------------------------------------------------------
bool wallet_rpc_server::on_rescan_spent(const wallet_rpc::COMMAND_RPC_RESCAN_SPENT::request& req, wallet_rpc::COMMAND_RPC_RESCAN_SPENT::response& res, epee::json_rpc::error& er, const connection_context *ctx)
{
if (!m_wallet) return not_open(er);
@@ -3119,17 +3154,7 @@ namespace tools
}
std::string wallet_file = req.filename.empty() ? "" : (m_wallet_dir + "/" + req.filename);
{
- std::vector<std::string> languages;
- crypto::ElectrumWords::get_language_list(languages, false);
- std::vector<std::string>::iterator it;
-
- it = std::find(languages.begin(), languages.end(), req.language);
- if (it == languages.end())
- {
- crypto::ElectrumWords::get_language_list(languages, true);
- it = std::find(languages.begin(), languages.end(), req.language);
- }
- if (it == languages.end())
+ if (!crypto::ElectrumWords::is_valid_language(req.language))
{
er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR;
er.message = "Unknown language: " + req.language;
@@ -3547,6 +3572,17 @@ namespace tools
return false;
}
+ if (!req.language.empty())
+ {
+ if (!crypto::ElectrumWords::is_valid_language(req.language))
+ {
+ er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR;
+ er.message = "The specified seed language is invalid.";
+ return false;
+ }
+ wal->set_seed_language(req.language);
+ }
+
// set blockheight if given
try
{
@@ -3690,12 +3726,7 @@ namespace tools
er.message = "Wallet was using the old seed language. You need to specify a new seed language.";
return false;
}
- std::vector<std::string> language_list;
- std::vector<std::string> language_list_en;
- crypto::ElectrumWords::get_language_list(language_list);
- crypto::ElectrumWords::get_language_list(language_list_en, true);
- if (std::find(language_list.begin(), language_list.end(), req.language) == language_list.end() &&
- std::find(language_list_en.begin(), language_list_en.end(), req.language) == language_list_en.end())
+ if (!crypto::ElectrumWords::is_valid_language(req.language))
{
er.code = WALLET_RPC_ERROR_CODE_UNKNOWN_ERROR;
er.message = "Wallet was using the old seed language, and the specified new seed language is invalid.";
@@ -4438,7 +4469,14 @@ public:
wal->stop();
});
- wal->refresh(wal->is_trusted_daemon());
+ try
+ {
+ wal->refresh(wal->is_trusted_daemon());
+ }
+ catch (const std::exception& e)
+ {
+ LOG_ERROR(tools::wallet_rpc_server::tr("Initial refresh failed: ") << e.what());
+ }
// if we ^C during potentially length load/refresh, there's no server loop yet
if (quit)
{
diff --git a/src/wallet/wallet_rpc_server.h b/src/wallet/wallet_rpc_server.h
index 6e39eca1e..9f9e3c134 100644
--- a/src/wallet/wallet_rpc_server.h
+++ b/src/wallet/wallet_rpc_server.h
@@ -131,6 +131,7 @@ namespace tools
MAP_JON_RPC_WE("delete_address_book",on_delete_address_book,wallet_rpc::COMMAND_RPC_DELETE_ADDRESS_BOOK_ENTRY)
MAP_JON_RPC_WE("refresh", on_refresh, wallet_rpc::COMMAND_RPC_REFRESH)
MAP_JON_RPC_WE("auto_refresh", on_auto_refresh, wallet_rpc::COMMAND_RPC_AUTO_REFRESH)
+ MAP_JON_RPC_WE("scan_tx", on_scan_tx, wallet_rpc::COMMAND_RPC_SCAN_TX)
MAP_JON_RPC_WE("rescan_spent", on_rescan_spent, wallet_rpc::COMMAND_RPC_RESCAN_SPENT)
MAP_JON_RPC_WE("start_mining", on_start_mining, wallet_rpc::COMMAND_RPC_START_MINING)
MAP_JON_RPC_WE("stop_mining", on_stop_mining, wallet_rpc::COMMAND_RPC_STOP_MINING)
@@ -218,6 +219,7 @@ namespace tools
bool on_delete_address_book(const wallet_rpc::COMMAND_RPC_DELETE_ADDRESS_BOOK_ENTRY::request& req, wallet_rpc::COMMAND_RPC_DELETE_ADDRESS_BOOK_ENTRY::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL);
bool on_refresh(const wallet_rpc::COMMAND_RPC_REFRESH::request& req, wallet_rpc::COMMAND_RPC_REFRESH::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL);
bool on_auto_refresh(const wallet_rpc::COMMAND_RPC_AUTO_REFRESH::request& req, wallet_rpc::COMMAND_RPC_AUTO_REFRESH::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL);
+ bool on_scan_tx(const wallet_rpc::COMMAND_RPC_SCAN_TX::request& req, wallet_rpc::COMMAND_RPC_SCAN_TX::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL);
bool on_rescan_spent(const wallet_rpc::COMMAND_RPC_RESCAN_SPENT::request& req, wallet_rpc::COMMAND_RPC_RESCAN_SPENT::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL);
bool on_start_mining(const wallet_rpc::COMMAND_RPC_START_MINING::request& req, wallet_rpc::COMMAND_RPC_START_MINING::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL);
bool on_stop_mining(const wallet_rpc::COMMAND_RPC_STOP_MINING::request& req, wallet_rpc::COMMAND_RPC_STOP_MINING::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL);
diff --git a/src/wallet/wallet_rpc_server_commands_defs.h b/src/wallet/wallet_rpc_server_commands_defs.h
index 81f83fb18..0002508a2 100644
--- a/src/wallet/wallet_rpc_server_commands_defs.h
+++ b/src/wallet/wallet_rpc_server_commands_defs.h
@@ -47,7 +47,7 @@
// advance which version they will stop working with
// Don't go over 32767 for any of these
#define WALLET_RPC_VERSION_MAJOR 1
-#define WALLET_RPC_VERSION_MINOR 20
+#define WALLET_RPC_VERSION_MINOR 21
#define MAKE_WALLET_RPC_VERSION(major,minor) (((major)<<16)|(minor))
#define WALLET_RPC_VERSION MAKE_WALLET_RPC_VERSION(WALLET_RPC_VERSION_MAJOR, WALLET_RPC_VERSION_MINOR)
namespace tools
@@ -1010,6 +1010,7 @@ namespace wallet_rpc
std::string tx_hash;
cryptonote::subaddress_index subaddr_index;
std::string key_image;
+ std::string pubkey; // owned output public key found
uint64_t block_height;
bool frozen;
bool unlocked;
@@ -1021,6 +1022,7 @@ namespace wallet_rpc
KV_SERIALIZE(tx_hash)
KV_SERIALIZE(subaddr_index)
KV_SERIALIZE(key_image)
+ KV_SERIALIZE(pubkey);
KV_SERIALIZE(block_height)
KV_SERIALIZE(frozen)
KV_SERIALIZE(unlocked)
@@ -2028,6 +2030,26 @@ namespace wallet_rpc
typedef epee::misc_utils::struct_init<response_t> response;
};
+ struct COMMAND_RPC_SCAN_TX
+ {
+ struct request_t
+ {
+ std::list<std::string> txids;
+
+ BEGIN_KV_SERIALIZE_MAP()
+ KV_SERIALIZE(txids)
+ END_KV_SERIALIZE_MAP()
+ };
+ typedef epee::misc_utils::struct_init<request_t> request;
+
+ struct response_t
+ {
+ BEGIN_KV_SERIALIZE_MAP()
+ END_KV_SERIALIZE_MAP()
+ };
+ typedef epee::misc_utils::struct_init<response_t> response;
+ };
+
struct COMMAND_RPC_START_MINING
{
struct request_t
@@ -2192,6 +2214,7 @@ namespace wallet_rpc
std::string viewkey;
std::string password;
bool autosave_current;
+ std::string language;
BEGIN_KV_SERIALIZE_MAP()
KV_SERIALIZE_OPT(restore_height, (uint64_t)0)
@@ -2201,6 +2224,7 @@ namespace wallet_rpc
KV_SERIALIZE(viewkey)
KV_SERIALIZE(password)
KV_SERIALIZE_OPT(autosave_current, true)
+ KV_SERIALIZE(language)
END_KV_SERIALIZE_MAP()
};