From 945c272f6cc12f128bef15c86b8ef6d44b70b29f Mon Sep 17 00:00:00 2001 From: moneromooo-monero Date: Wed, 22 Jun 2016 22:21:30 +0100 Subject: [PATCH] wallet: add a fee multiplier Fee can now be multiplied by 2 or 3, if users want to give priority to their transactions. There are only three levels to avoid too much fingerprinting. Default is 1 (minimum fee). The default multiplier can be set by "set fee-multiplier X". --- src/simplewallet/simplewallet.cpp | 80 +++++++++++++++++++- src/simplewallet/simplewallet.h | 1 + src/wallet/wallet2.cpp | 49 +++++++++--- src/wallet/wallet2.h | 14 ++-- src/wallet/wallet_errors.h | 10 +++ src/wallet/wallet_rpc_server.cpp | 8 +- src/wallet/wallet_rpc_server_commands_defs.h | 12 +-- 7 files changed, 146 insertions(+), 28 deletions(-) diff --git a/src/simplewallet/simplewallet.cpp b/src/simplewallet/simplewallet.cpp index 22ddb063..2ae42324 100644 --- a/src/simplewallet/simplewallet.cpp +++ b/src/simplewallet/simplewallet.cpp @@ -485,6 +485,68 @@ bool simple_wallet::set_default_mixin(const std::vector &args/* = s } } +bool simple_wallet::set_default_fee_multiplier(const std::vector &args/* = std::vector()*/) +{ + bool success = false; + int fee_multiplier = 0; + if (m_wallet->watch_only()) + { + fail_msg_writer() << tr("wallet is watch-only and cannot transfer"); + return true; + } + try + { + if (strchr(args[1].c_str(), '-')) + { + fail_msg_writer() << tr("fee multiplier must be 0, 1, 2, or 3 "); + return true; + } + if (args[1] == "0") + { + fee_multiplier = 0; + } + else + { + fee_multiplier = boost::lexical_cast(args[1]); + if (fee_multiplier != 1 && fee_multiplier != 2 && fee_multiplier != 3) + { + fail_msg_writer() << tr("fee multiplier must be 0, 1, 2, or 3"); + return true; + } + } + + tools::password_container pwd_container; + success = pwd_container.read_password(); + if (!success) + { + fail_msg_writer() << tr("failed to read wallet password"); + return true; + } + + /* verify password before using so user doesn't accidentally set a new password for rewritten wallet */ + success = m_wallet->verify_password(pwd_container.password()); + if (!success) + { + fail_msg_writer() << tr("invalid password"); + return true; + } + + m_wallet->set_default_fee_multiplier(fee_multiplier); + m_wallet->rewrite(m_wallet_file, pwd_container.password()); + return true; + } + catch(const boost::bad_lexical_cast &) + { + fail_msg_writer() << tr("fee multiplier must be 0, 1, 2 or 3"); + return true; + } + catch(...) + { + fail_msg_writer() << tr("could not change default fee multiplier"); + return true; + } +} + bool simple_wallet::set_auto_refresh(const std::vector &args/* = std::vector()*/) { bool success = false; @@ -587,7 +649,7 @@ simple_wallet::simple_wallet() m_cmd_binder.set_handler("viewkey", boost::bind(&simple_wallet::viewkey, this, _1), tr("Display private view key")); m_cmd_binder.set_handler("spendkey", boost::bind(&simple_wallet::spendkey, this, _1), tr("Display private spend key")); m_cmd_binder.set_handler("seed", boost::bind(&simple_wallet::seed, this, _1), tr("Display Electrum-style mnemonic seed")); - m_cmd_binder.set_handler("set", boost::bind(&simple_wallet::set_variable, this, _1), tr("Available options: seed language - set wallet seed language; always-confirm-transfers <1|0> - whether to confirm unsplit txes; store-tx-info <1|0> - whether to store outgoing tx info (destination address, payment ID, tx secret key) for future reference; default-mixin - set default mixin (default default is 4); auto-refresh <1|0> - whether to automatically sync new blocks from the daemon; refresh-type - set wallet refresh behaviour")); + m_cmd_binder.set_handler("set", boost::bind(&simple_wallet::set_variable, this, _1), tr("Available options: seed language - set wallet seed language; always-confirm-transfers <1|0> - whether to confirm unsplit txes; store-tx-info <1|0> - whether to store outgoing tx info (destination address, payment ID, tx secret key) for future reference; default-mixin - set default mixin (default default is 4); auto-refresh <1|0> - whether to automatically sync new blocks from the daemon; refresh-type - set wallet refresh behaviour; fee-multiplier [1|2|3] - normal/elevated/priority fee")); m_cmd_binder.set_handler("rescan_spent", boost::bind(&simple_wallet::rescan_spent, this, _1), tr("Rescan blockchain for spent outputs")); m_cmd_binder.set_handler("get_tx_key", boost::bind(&simple_wallet::get_tx_key, this, _1), tr("Get transaction key (r) for a given ")); m_cmd_binder.set_handler("check_tx_key", boost::bind(&simple_wallet::check_tx_key, this, _1), tr("Check amount going to
in ")); @@ -609,6 +671,7 @@ bool simple_wallet::set_variable(const std::vector &args) success_msg_writer() << "default-mixin = " << m_wallet->default_mixin(); success_msg_writer() << "auto-refresh = " << m_wallet->auto_refresh(); success_msg_writer() << "refresh-type = " << get_refresh_type_name(m_wallet->get_refresh_type()); + success_msg_writer() << "fee-multiplier = " << m_wallet->get_default_fee_multiplier(); return true; } else @@ -704,6 +767,21 @@ bool simple_wallet::set_variable(const std::vector &args) return true; } } + else if (args[0] == "fee-multiplier") + { + if (args.size() <= 1) + { + fail_msg_writer() << tr("set fee-multiplier: needs an argument: 0, 1, 2, or 3"); + return true; + } + else + { + std::vector local_args = args; + local_args.erase(local_args.begin(), local_args.begin()+2); + set_default_fee_multiplier(local_args); + return true; + } + } } fail_msg_writer() << tr("set: unrecognized argument(s)"); return true; diff --git a/src/simplewallet/simplewallet.h b/src/simplewallet/simplewallet.h index 66f74b45..e0478eb6 100644 --- a/src/simplewallet/simplewallet.h +++ b/src/simplewallet/simplewallet.h @@ -142,6 +142,7 @@ namespace cryptonote bool set_tx_note(const std::vector &args); bool get_tx_note(const std::vector &args); bool status(const std::vector &args); + bool set_default_fee_multiplier(const std::vector &args); uint64_t get_daemon_blockchain_height(std::string& err); bool try_connect_to_daemon(); diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index b6c10c0e..a9d69627 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -94,11 +94,12 @@ void do_prepare_file_names(const std::string& file_path, std::string& keys_file, } } -uint64_t calculate_fee(const cryptonote::blobdata &blob) +uint64_t calculate_fee(const cryptonote::blobdata &blob, uint64_t fee_multiplier) { + THROW_WALLET_EXCEPTION_IF(fee_multiplier <= 0 || fee_multiplier > 3, tools::error::invalid_fee_multiplier); uint64_t bytes = blob.size(); uint64_t kB = (bytes + 1023) / 1024; - return kB * FEE_PER_KB; + return kB * FEE_PER_KB * fee_multiplier; } } //namespace @@ -1054,6 +1055,9 @@ bool wallet2::store_keys(const std::string& keys_file_name, const std::string& p value2.SetUint(m_default_mixin); json.AddMember("default_mixin", value2, json.GetAllocator()); + value2.SetUint(m_default_fee_multiplier); + json.AddMember("default_fee_multiplier", value2, json.GetAllocator()); + value2.SetInt(m_auto_refresh ? 1 :0); json.AddMember("auto_refresh", value2, json.GetAllocator()); @@ -1125,6 +1129,7 @@ bool wallet2::load_keys(const std::string& keys_file_name, const std::string& pa m_watch_only = false; m_always_confirm_transfers = false; m_default_mixin = 0; + m_default_fee_multiplier = 0; m_auto_refresh = true; m_refresh_type = RefreshType::RefreshDefault; } @@ -1158,6 +1163,8 @@ bool wallet2::load_keys(const std::string& keys_file_name, const std::string& pa || (field_store_tx_info_found && (field_store_tx_info != 0)); GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, default_mixin, unsigned int, Uint, false); m_default_mixin = field_default_mixin_found ? field_default_mixin : 0; + GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, default_fee_multiplier, unsigned int, Uint, false); + m_default_fee_multiplier = field_default_fee_multiplier_found ? field_default_fee_multiplier : 0; GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, auto_refresh, int, Int, false); m_auto_refresh = !field_auto_refresh_found || (field_auto_refresh != 0); GET_FIELD_FROM_JSON_RETURN_ON_ERROR(json, refresh_type, int, Int, false); @@ -2081,15 +2088,29 @@ void wallet2::commit_tx(std::vector& ptx_vector) } } +uint64_t wallet2::sanitize_fee_multiplier(uint64_t fee_multiplier) const +{ + // 0, default value used for previous fee argument, defaults to normal fee + if (fee_multiplier == 0) + return m_default_fee_multiplier > 0 ? m_default_fee_multiplier : 1; + // 1 to 3 are allowed as multipliers + if (fee_multiplier >= 1 && fee_multiplier <= 3) + return fee_multiplier; + THROW_WALLET_EXCEPTION_IF (false, error::invalid_fee_multiplier); + return 1; +} + //---------------------------------------------------------------------------------------------------- // separated the call(s) to wallet2::transfer into their own function // // this function will make multiple calls to wallet2::transfer if multiple // transactions will be required -std::vector wallet2::create_transactions(std::vector dsts, const size_t fake_outs_count, const uint64_t unlock_time, const uint64_t fee_UNUSED, const std::vector extra, bool trusted_daemon) +std::vector wallet2::create_transactions(std::vector dsts, const size_t fake_outs_count, const uint64_t unlock_time, uint64_t fee_multiplier, const std::vector extra, bool trusted_daemon) { const std::vector unused_transfers_indices = select_available_outputs_from_histogram(fake_outs_count + 1, true, trusted_daemon); + fee_multiplier = sanitize_fee_multiplier(fee_multiplier); + // failsafe split attempt counter size_t attempt_count = 0; @@ -2120,7 +2141,7 @@ std::vector wallet2::create_transactions(std::vector wallet2::create_transactions_2(std::vector dsts, const size_t fake_outs_count, const uint64_t unlock_time, const uint64_t fee_UNUSED, const std::vector extra, bool trusted_daemon) +std::vector wallet2::create_transactions_2(std::vector dsts, const size_t fake_outs_count, const uint64_t unlock_time, uint64_t fee_multiplier, const std::vector extra, bool trusted_daemon) { std::vector unused_transfers_indices; std::vector unused_dust_indices; @@ -2388,6 +2409,8 @@ std::vector wallet2::create_transactions_2(std::vector wallet2::create_transactions_2(std::vector wallet2::create_transactions_2(std::vector wallet2::create_transactions_all(const cryptonote::account_public_address &address, const size_t fake_outs_count, const uint64_t unlock_time, const uint64_t fee_UNUSED, const std::vector extra, bool trusted_daemon) +std::vector wallet2::create_transactions_all(const cryptonote::account_public_address &address, const size_t fake_outs_count, const uint64_t unlock_time, uint64_t fee_multiplier, const std::vector extra, bool trusted_daemon) { std::vector unused_transfers_indices; std::vector unused_dust_indices; @@ -2598,6 +2621,8 @@ std::vector wallet2::create_transactions_all(const cryptono uint64_t needed_fee, available_for_fee = 0; uint64_t upper_transaction_size_limit = get_upper_tranaction_size_limit(); + fee_multiplier = sanitize_fee_multiplier(fee_multiplier); + // gather all our dust and non dust outputs for (size_t i = 0; i < m_transfers.size(); ++i) { @@ -2626,7 +2651,7 @@ std::vector wallet2::create_transactions_all(const cryptono // get a random unspent output and use it to pay next chunk. We try to alternate // dust and non dust to ensure we never get with only dust, from which we might // get a tx that can't pay for itself - size_t idx = unused_transfers_indices.empty() ? pop_random_value(unused_dust_indices) : unused_dust_indices.empty() ? pop_random_value(unused_transfers_indices) : ((tx.selected_transfers.size() & 1) || accumulated_outputs > FEE_PER_KB * (upper_transaction_size_limit + 1023) / 1024) ? pop_random_value(unused_dust_indices) : pop_random_value(unused_transfers_indices); + size_t idx = unused_transfers_indices.empty() ? pop_random_value(unused_dust_indices) : unused_dust_indices.empty() ? pop_random_value(unused_transfers_indices) : ((tx.selected_transfers.size() & 1) || accumulated_outputs > FEE_PER_KB * fee_multiplier * (upper_transaction_size_limit + 1023) / 1024) ? pop_random_value(unused_dust_indices) : pop_random_value(unused_transfers_indices); const transfer_details &td = m_transfers[idx]; LOG_PRINT_L2("Picking output " << idx << ", amount " << print_money(td.amount())); @@ -2654,7 +2679,7 @@ std::vector wallet2::create_transactions_all(const cryptono transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, unlock_time, needed_fee, extra, detail::digit_split_strategy, tx_dust_policy(::config::DEFAULT_DUST_THRESHOLD), test_tx, test_ptx); auto txBlob = t_serializable_object_to_blob(test_ptx.tx); - needed_fee = calculate_fee(txBlob); + needed_fee = calculate_fee(txBlob, fee_multiplier); available_for_fee = test_ptx.fee + test_ptx.dests[0].amount + test_ptx.change_dts.amount; LOG_PRINT_L2("Made a " << txBlob.size() << " kB tx, with " << print_money(available_for_fee) << " available for fee (" << print_money(needed_fee) << " needed)"); @@ -2667,7 +2692,7 @@ std::vector wallet2::create_transactions_all(const cryptono transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, unlock_time, needed_fee, extra, detail::digit_split_strategy, tx_dust_policy(::config::DEFAULT_DUST_THRESHOLD), test_tx, test_ptx); txBlob = t_serializable_object_to_blob(test_ptx.tx); - needed_fee = calculate_fee(txBlob); + needed_fee = calculate_fee(txBlob, fee_multiplier); LOG_PRINT_L2("Made an attempt at a final " << ((txBlob.size() + 1023)/1024) << " kB tx, with " << print_money(test_ptx.fee) << " fee and " << print_money(test_ptx.change_dts.amount) << " change"); } while (needed_fee > test_ptx.fee); @@ -2994,13 +3019,13 @@ std::vector wallet2::create_unmixable_sweep_transactions(bo { transfer_from(unmixable_outputs, num_outputs_per_tx, (uint64_t)0 /* unlock_time */, 0, detail::digit_split_strategy, dust_policy, extra, tx, ptx); auto txBlob = t_serializable_object_to_blob(ptx.tx); - needed_fee = calculate_fee(txBlob); + needed_fee = calculate_fee(txBlob, 1); // reroll the tx with the actual amount minus the fee // if there's not enough for the fee, it'll throw transfer_from(unmixable_outputs, num_outputs_per_tx, (uint64_t)0 /* unlock_time */, needed_fee, detail::digit_split_strategy, dust_policy, extra, tx, ptx); txBlob = t_serializable_object_to_blob(ptx.tx); - needed_fee = calculate_fee(txBlob); + needed_fee = calculate_fee(txBlob, 1); } while (ptx.fee < needed_fee); ptx_vector.push_back(ptx); diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index c2d387ac..cecf090f 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -88,10 +88,10 @@ namespace tools }; private: - wallet2(const wallet2&) : m_run(true), m_callback(0), m_testnet(false), m_always_confirm_transfers (false), m_store_tx_info(true), m_default_mixin(0), m_refresh_type(RefreshOptimizeCoinbase), m_auto_refresh(true), m_refresh_from_block_height(0) {} + wallet2(const wallet2&) : m_run(true), m_callback(0), m_testnet(false), m_always_confirm_transfers (false), m_store_tx_info(true), m_default_mixin(0), m_default_fee_multiplier(0), m_refresh_type(RefreshOptimizeCoinbase), m_auto_refresh(true), m_refresh_from_block_height(0) {} public: - wallet2(bool testnet = false, bool restricted = false) : m_run(true), m_callback(0), m_testnet(testnet), m_restricted(restricted), is_old_file_format(false), m_store_tx_info(true), m_default_mixin(0), m_refresh_type(RefreshOptimizeCoinbase), m_auto_refresh(true), m_refresh_from_block_height(0) {} + wallet2(bool testnet = false, bool restricted = false) : m_run(true), m_callback(0), m_testnet(testnet), m_restricted(restricted), is_old_file_format(false), m_store_tx_info(true), m_default_mixin(0), m_default_fee_multiplier(0), m_refresh_type(RefreshOptimizeCoinbase), m_auto_refresh(true), m_refresh_from_block_height(0) {} struct transfer_details { uint64_t m_block_height; @@ -291,9 +291,9 @@ namespace tools void commit_tx(pending_tx& ptx_vector); void commit_tx(std::vector& ptx_vector); - std::vector create_transactions(std::vector dsts, const size_t fake_outs_count, const uint64_t unlock_time, const uint64_t fee, const std::vector extra, bool trusted_daemon); - std::vector create_transactions_2(std::vector dsts, const size_t fake_outs_count, const uint64_t unlock_time, const uint64_t fee_UNUSED, const std::vector extra, bool trusted_daemon); - std::vector create_transactions_all(const cryptonote::account_public_address &address, const size_t fake_outs_count, const uint64_t unlock_time, const uint64_t fee_UNUSED, const std::vector extra, bool trusted_daemon); + std::vector create_transactions(std::vector dsts, const size_t fake_outs_count, const uint64_t unlock_time, uint64_t fee_multiplier, const std::vector extra, bool trusted_daemon); + std::vector create_transactions_2(std::vector dsts, const size_t fake_outs_count, const uint64_t unlock_time, uint64_t fee_multiplier, const std::vector extra, bool trusted_daemon); + std::vector create_transactions_all(const cryptonote::account_public_address &address, const size_t fake_outs_count, const uint64_t unlock_time, uint64_t fee_multiplier, const std::vector extra, bool trusted_daemon); std::vector create_unmixable_sweep_transactions(bool trusted_daemon); bool check_connection(); void get_transfers(wallet2::transfer_container& incoming_transfers) const; @@ -363,6 +363,8 @@ namespace tools void store_tx_info(bool store) { m_store_tx_info = store; } uint32_t default_mixin() const { return m_default_mixin; } void default_mixin(uint32_t m) { m_default_mixin = m; } + uint32_t get_default_fee_multiplier() const { return m_default_fee_multiplier; } + void set_default_fee_multiplier(uint32_t m) { m_default_fee_multiplier = m; } bool auto_refresh() const { return m_auto_refresh; } void auto_refresh(bool r) { m_auto_refresh = r; } @@ -423,6 +425,7 @@ namespace tools uint64_t get_upper_tranaction_size_limit(); void check_pending_txes(); std::vector get_unspent_amounts_vector(); + uint64_t sanitize_fee_multiplier(uint64_t fee_multiplier) const; cryptonote::account_base m_account; std::string m_daemon_address; @@ -455,6 +458,7 @@ namespace tools bool m_always_confirm_transfers; bool m_store_tx_info; /*!< request txkey to be returned in RPC, and store in the wallet cache file */ uint32_t m_default_mixin; + uint32_t m_default_fee_multiplier; RefreshType m_refresh_type; bool m_auto_refresh; uint64_t m_refresh_from_block_height; diff --git a/src/wallet/wallet_errors.h b/src/wallet/wallet_errors.h index 184d8a2a..ea0b6a1f 100644 --- a/src/wallet/wallet_errors.h +++ b/src/wallet/wallet_errors.h @@ -56,6 +56,7 @@ namespace tools // file_read_error // file_save_error // invalid_password + // invalid_fee_multiplier // refresh_error * // acc_outs_lookup_error // block_parse_error @@ -226,6 +227,15 @@ namespace tools std::string to_string() const { return wallet_logic_error::to_string(); } }; + struct invalid_fee_multiplier : public wallet_logic_error + { + explicit invalid_fee_multiplier(std::string&& loc) + : wallet_logic_error(std::move(loc), "invalid fee multiplier") + { + } + + std::string to_string() const { return wallet_logic_error::to_string(); } + }; //---------------------------------------------------------------------------------------------------- struct invalid_pregenerated_random : public wallet_logic_error diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp index 5be9ae5b..01ae00fe 100644 --- a/src/wallet/wallet_rpc_server.cpp +++ b/src/wallet/wallet_rpc_server.cpp @@ -232,7 +232,7 @@ namespace tools LOG_PRINT_L1("Requested mixin " << req.mixin << " too low for hard fork 2, using 2"); mixin = 2; } - std::vector ptx_vector = m_wallet.create_transactions(dsts, mixin, req.unlock_time, req.fee, extra, req.trusted_daemon); + std::vector ptx_vector = m_wallet.create_transactions(dsts, mixin, req.unlock_time, req.fee_multiplier, extra, req.trusted_daemon); // reject proposed transactions if there are more than one. see on_transfer_split below. if (ptx_vector.size() != 1) @@ -299,9 +299,9 @@ namespace tools } std::vector ptx_vector; if (req.new_algorithm) - ptx_vector = m_wallet.create_transactions_2(dsts, mixin, req.unlock_time, req.fee, extra, req.trusted_daemon); + ptx_vector = m_wallet.create_transactions_2(dsts, mixin, req.unlock_time, req.fee_multiplier, extra, req.trusted_daemon); else - ptx_vector = m_wallet.create_transactions(dsts, mixin, req.unlock_time, req.fee, extra, req.trusted_daemon); + ptx_vector = m_wallet.create_transactions(dsts, mixin, req.unlock_time, req.fee_multiplier, extra, req.trusted_daemon); m_wallet.commit_tx(ptx_vector); @@ -406,7 +406,7 @@ namespace tools try { - std::vector ptx_vector = m_wallet.create_transactions_all(dsts[0].addr, req.mixin, req.unlock_time, req.fee, extra, req.trusted_daemon); + std::vector ptx_vector = m_wallet.create_transactions_all(dsts[0].addr, req.mixin, req.unlock_time, req.fee_multiplier, extra, req.trusted_daemon); m_wallet.commit_tx(ptx_vector); diff --git a/src/wallet/wallet_rpc_server_commands_defs.h b/src/wallet/wallet_rpc_server_commands_defs.h index f8c04c00..3908476d 100644 --- a/src/wallet/wallet_rpc_server_commands_defs.h +++ b/src/wallet/wallet_rpc_server_commands_defs.h @@ -110,7 +110,7 @@ namespace wallet_rpc struct request { std::list destinations; - uint64_t fee; + uint64_t fee_multiplier; uint64_t mixin; uint64_t unlock_time; std::string payment_id; @@ -119,7 +119,7 @@ namespace wallet_rpc BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(destinations) - KV_SERIALIZE(fee) + KV_SERIALIZE(fee_multiplier) KV_SERIALIZE(mixin) KV_SERIALIZE(unlock_time) KV_SERIALIZE(payment_id) @@ -145,7 +145,7 @@ namespace wallet_rpc struct request { std::list destinations; - uint64_t fee; + uint64_t fee_multiplier; uint64_t mixin; uint64_t unlock_time; std::string payment_id; @@ -155,7 +155,7 @@ namespace wallet_rpc BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(destinations) - KV_SERIALIZE(fee) + KV_SERIALIZE(fee_multiplier) KV_SERIALIZE(mixin) KV_SERIALIZE(unlock_time) KV_SERIALIZE(payment_id) @@ -207,7 +207,7 @@ namespace wallet_rpc struct request { std::string address; - uint64_t fee; + uint64_t fee_multiplier; uint64_t mixin; uint64_t unlock_time; std::string payment_id; @@ -216,7 +216,7 @@ namespace wallet_rpc BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(address) - KV_SERIALIZE(fee) + KV_SERIALIZE(fee_multiplier) KV_SERIALIZE(mixin) KV_SERIALIZE(unlock_time) KV_SERIALIZE(payment_id)