remove payment_id and integrated_address

release-v0.5.0
wowario 6 years ago
parent 48ef3ff466
commit 2b66b27fe5
No known key found for this signature in database
GPG Key ID: 24DCBE762DE9C111

@ -766,18 +766,6 @@ bool simple_wallet::change_password(const std::vector<std::string> &args)
return true;
}
bool simple_wallet::payment_id(const std::vector<std::string> &args/* = std::vector<std::string>()*/)
{
crypto::hash payment_id;
if (args.size() > 0)
{
fail_msg_writer() << tr("usage: payment_id");
return true;
}
payment_id = crypto::rand<crypto::hash>();
success_msg_writer() << tr("Random payment ID: ") << payment_id;
return true;
}
bool simple_wallet::print_fee_info(const std::vector<std::string> &args/* = std::vector<std::string>()*/)
{
@ -2351,41 +2339,37 @@ simple_wallet::simple_wallet()
tr("Show the incoming transfers, all or filtered by availability and address index.\n\n"
"Output format:\n"
"Amount, Spent(\"T\"|\"F\"), \"locked\"|\"unlocked\", RingCT, Global Index, Transaction Hash, Address Index, [Public Key, Key Image] "));
m_cmd_binder.set_handler("payments",
boost::bind(&simple_wallet::show_payments, this, _1),
tr("payments <PID_1> [<PID_2> ... <PID_N>]"),
tr("Show the payments for the given payment IDs."));
m_cmd_binder.set_handler("bc_height",
boost::bind(&simple_wallet::show_blockchain_height, this, _1),
tr("Show the blockchain height."));
m_cmd_binder.set_handler("transfer", boost::bind(&simple_wallet::transfer, this, _1),
tr("transfer [index=<N1>[,<N2>,...]] [<priority>] [<ring_size>] (<URI> | <address> <amount>) [<payment_id>]"),
tr("Transfer <amount> to <address>. If the parameter \"index=<N1>[,<N2>,...]\" is specified, the wallet uses outputs received by addresses of those indices. If omitted, the wallet randomly chooses address indices to be used. In any case, it tries its best not to combine outputs across multiple addresses. <priority> is the priority of the transaction. The higher the priority, the higher the transaction fee. Valid values in priority order (from lowest to highest) are: unimportant, normal, elevated, priority. If omitted, the default value (see the command \"set priority\") is used. <ring_size> is the number of inputs to include for untraceability. Multiple payments can be made at once by adding URI_2 or <address_2> <amount_2> etcetera (before the payment ID, if it's included)"));
tr("transfer [index=<N1>[,<N2>,...]] [<priority>] [<ring_size>] (<URI> | <address> <amount>)"),
tr("Transfer <amount> to <address>. If the parameter \"index=<N1>[,<N2>,...]\" is specified, the wallet uses outputs received by addresses of those indices. If omitted, the wallet randomly chooses address indices to be used. In any case, it tries its best not to combine outputs across multiple addresses. <priority> is the priority of the transaction. The higher the priority, the higher the transaction fee. Valid values in priority order (from lowest to highest) are: unimportant, normal, elevated, priority. If omitted, the default value (see the command \"set priority\") is used. <ring_size> is the number of inputs to include for untraceability. Multiple payments can be made at once by adding URI_2 or <address_2> <amount_2> etcetera"));
m_cmd_binder.set_handler("locked_transfer",
boost::bind(&simple_wallet::locked_transfer, this, _1),
tr("locked_transfer [index=<N1>[,<N2>,...]] [<priority>] [<ring_size>] (<URI> | <addr> <amount>) <lockblocks> [<payment_id>]"),
tr("Transfer <amount> to <address> and lock it for <lockblocks> (max. 1000000). If the parameter \"index=<N1>[,<N2>,...]\" is specified, the wallet uses outputs received by addresses of those indices. If omitted, the wallet randomly chooses address indices to be used. In any case, it tries its best not to combine outputs across multiple addresses. <priority> is the priority of the transaction. The higher the priority, the higher the transaction fee. Valid values in priority order (from lowest to highest) are: unimportant, normal, elevated, priority. If omitted, the default value (see the command \"set priority\") is used. <ring_size> is the number of inputs to include for untraceability. Multiple payments can be made at once by adding URI_2 or <address_2> <amount_2> etcetera (before the payment ID, if it's included)"));
tr("locked_transfer [index=<N1>[,<N2>,...]] [<priority>] [<ring_size>] (<URI> | <addr> <amount>) <lockblocks>"),
tr("Transfer <amount> to <address> and lock it for <lockblocks> (max. 1000000). If the parameter \"index=<N1>[,<N2>,...]\" is specified, the wallet uses outputs received by addresses of those indices. If omitted, the wallet randomly chooses address indices to be used. In any case, it tries its best not to combine outputs across multiple addresses. <priority> is the priority of the transaction. The higher the priority, the higher the transaction fee. Valid values in priority order (from lowest to highest) are: unimportant, normal, elevated, priority. If omitted, the default value (see the command \"set priority\") is used. <ring_size> is the number of inputs to include for untraceability. Multiple payments can be made at once by adding URI_2 or <address_2> <amount_2> etcetera"));
m_cmd_binder.set_handler("locked_sweep_all",
boost::bind(&simple_wallet::locked_sweep_all, this, _1),
tr("locked_sweep_all [index=<N1>[,<N2>,...]] [<priority>] [<ring_size>] <address> <lockblocks> [<payment_id>]"),
tr("locked_sweep_all [index=<N1>[,<N2>,...]] [<priority>] [<ring_size>] <address> <lockblocks>"),
tr("Send all unlocked balance to an address and lock it for <lockblocks> (max. 1000000). If the parameter \"index<N1>[,<N2>,...]\" is specified, the wallet sweeps outputs received by those address indices. If omitted, the wallet randomly chooses an address index to be used. <priority> is the priority of the sweep. The higher the priority, the higher the transaction fee. Valid values in priority order (from lowest to highest) are: unimportant, normal, elevated, priority. If omitted, the default value (see the command \"set priority\") is used. <ring_size> is the number of inputs to include for untraceability."));
m_cmd_binder.set_handler("sweep_unmixable",
boost::bind(&simple_wallet::sweep_unmixable, this, _1),
tr("Send all unmixable outputs to yourself with ring_size 1"));
m_cmd_binder.set_handler("sweep_all", boost::bind(&simple_wallet::sweep_all, this, _1),
tr("sweep_all [index=<N1>[,<N2>,...]] [<priority>] [<ring_size>] [outputs=<N>] <address> [<payment_id>]"),
tr("sweep_all [index=<N1>[,<N2>,...]] [<priority>] [<ring_size>] [outputs=<N>] <address>"),
tr("Send all unlocked balance to an address. If the parameter \"index<N1>[,<N2>,...]\" is specified, the wallet sweeps outputs received by those address indices. If omitted, the wallet randomly chooses an address index to be used. If the parameter \"outputs=<N>\" is specified and N > 0, wallet splits the transaction into N even outputs."));
m_cmd_binder.set_handler("sweep_below",
boost::bind(&simple_wallet::sweep_below, this, _1),
tr("sweep_below <amount_threshold> [index=<N1>[,<N2>,...]] [<priority>] [<ring_size>] <address> [<payment_id>]"),
tr("sweep_below <amount_threshold> [index=<N1>[,<N2>,...]] [<priority>] [<ring_size>] <address>"),
tr("Send all unlocked outputs below the threshold to an address."));
m_cmd_binder.set_handler("sweep_single",
boost::bind(&simple_wallet::sweep_single, this, _1),
tr("sweep_single [<priority>] [<ring_size>] [outputs=<N>] <key_image> <address> [<payment_id>]"),
tr("sweep_single [<priority>] [<ring_size>] [outputs=<N>] <key_image> <address>"),
tr("Send a single output of the given key image to an address without change."));
m_cmd_binder.set_handler("donate",
boost::bind(&simple_wallet::donate, this, _1),
tr("donate [index=<N1>[,<N2>,...]] [<priority>] [<ring_size>] <amount> [<payment_id>]"),
tr("donate [index=<N1>[,<N2>,...]] [<priority>] [<ring_size>] <amount>"),
tr("Donate <amount> to the development team (donate.getmonero.org)."));
m_cmd_binder.set_handler("sign_transfer",
boost::bind(&simple_wallet::sign_transfer, this, _1),
@ -2418,13 +2402,9 @@ simple_wallet::simple_wallet()
boost::bind(&simple_wallet::print_address, this, _1),
tr("address [ new <label text with white spaces allowed> | all | <index_min> [<index_max>] | label <index> <label text with white spaces allowed>]"),
tr("If no arguments are specified or <index> is specified, the wallet shows the default or specified address. If \"all\" is specified, the wallet shows all the existing addresses in the currently selected account. If \"new \" is specified, the wallet creates a new address with the provided label text (which can be empty). If \"label\" is specified, the wallet sets the label of the address specified by <index> to the provided label text."));
m_cmd_binder.set_handler("integrated_address",
boost::bind(&simple_wallet::print_integrated_address, this, _1),
tr("integrated_address [<payment_id> | <address>]"),
tr("Encode a payment ID into an integrated address for the current wallet public address (no argument uses a random payment ID), or decode an integrated address to standard address and payment ID"));
m_cmd_binder.set_handler("address_book",
boost::bind(&simple_wallet::address_book, this, _1),
tr("address_book [(add ((<address> [pid <id>])|<integrated address>) [<description possibly with whitespaces>])|(delete <index>)]"),
tr("address_book [(add <address> [<description possibly with whitespaces>])|(delete <index>)]"),
tr("Print all entries in the address book, optionally adding/deleting an entry to/from it."));
m_cmd_binder.set_handler("save",
boost::bind(&simple_wallet::save, this, _1),
@ -2452,7 +2432,7 @@ simple_wallet::simple_wallet()
"print-ring-members <1|0>\n "
" Whether to print detailed information about ring members during confirmation.\n "
"store-tx-info <1|0>\n "
" Whether to store outgoing tx info (destination address, payment ID, tx secret key) for future reference.\n "
" Whether to store outgoing tx info (destination address, tx secret key) for future reference.\n "
"default-ring-size <n>\n "
" Set the default ring size (obsolete).\n "
"auto-refresh <1|0>\n "
@ -2616,9 +2596,6 @@ simple_wallet::simple_wallet()
m_cmd_binder.set_handler("password",
boost::bind(&simple_wallet::change_password, this, _1),
tr("Change the wallet's password."));
m_cmd_binder.set_handler("payment_id",
boost::bind(&simple_wallet::payment_id, this, _1),
tr("Generate a new random full size payment id. These will be unencrypted on the blockchain, see integrated_address for encrypted short payment ids."));
m_cmd_binder.set_handler("fee",
boost::bind(&simple_wallet::print_fee_info, this, _1),
tr("Print the information about the current fee and transaction backlog."));
@ -4256,7 +4233,7 @@ void simple_wallet::on_money_received(uint64_t height, const crypto::hash &txid,
tr("NOTE: this transaction uses an encrypted payment ID: consider using subaddresses instead");
else if (get_payment_id_from_tx_extra_nonce(extra_nonce.nonce, payment_id))
message_writer(console_color_red, false) <<
tr("WARNING: this transaction uses an unencrypted payment ID: consider using subaddresses instead");
tr("WARNING: this transaction uses an unencrypted payment ID: these are obsolete. Use subaddresses instead.");
}
}
if (m_auto_refresh_refreshing)
@ -4609,60 +4586,6 @@ bool simple_wallet::show_incoming_transfers(const std::vector<std::string>& args
return true;
}
//----------------------------------------------------------------------------------------------------
bool simple_wallet::show_payments(const std::vector<std::string> &args)
{
if(args.empty())
{
fail_msg_writer() << tr("expected at least one payment ID");
return true;
}
LOCK_IDLE_SCOPE();
PAUSE_READLINE();
message_writer() << boost::format("%68s%68s%12s%21s%16s%16s") %
tr("payment") % tr("transaction") % tr("height") % tr("amount") % tr("unlock time") % tr("addr index");
bool payments_found = false;
for(std::string arg : args)
{
crypto::hash payment_id;
if(tools::wallet2::parse_payment_id(arg, payment_id))
{
std::list<tools::wallet2::payment_details> payments;
m_wallet->get_payments(payment_id, payments);
if(payments.empty())
{
success_msg_writer() << tr("No payments with id ") << payment_id;
continue;
}
for (const tools::wallet2::payment_details& pd : payments)
{
if(!payments_found)
{
payments_found = true;
}
success_msg_writer(true) <<
boost::format("%68s%68s%12s%21s%16s%16s") %
payment_id %
pd.m_tx_hash %
pd.m_block_height %
print_money(pd.m_amount) %
pd.m_unlock_time %
pd.m_subaddr_index.minor;
}
}
else
{
fail_msg_writer() << tr("payment ID has invalid format, expected 16 or 64 character hex string: ") << arg;
}
}
return true;
}
//----------------------------------------------------------------------------------------------------
uint64_t simple_wallet::get_daemon_blockchain_height(std::string& err)
{
if (!m_wallet)
@ -4918,7 +4841,8 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri
r = add_extra_nonce_to_tx_extra(extra, extra_nonce);
local_args.pop_back();
payment_id_seen = true;
message_writer() << tr("Unencrypted payment IDs are bad for privacy: ask the recipient to use subaddresses instead");
fail_msg_writer() << tr("Unencrypted payment IDs are no longer supported: ask the recipient to use subaddresses instead. Transaction cancelled.");
return true;
}
else
{
@ -5037,7 +4961,8 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri
else if (tools::wallet2::parse_payment_id(payment_id_uri, payment_id))
{
set_payment_id_to_tx_extra_nonce(extra_nonce, payment_id);
message_writer() << tr("Unencrypted payment IDs are bad for privacy: ask the recipient to use subaddresses instead");
fail_msg_writer() << tr("Unencrypted payment IDs are no longer supported: ask the recipient to use subaddresses instead. Transaction cancelled.");
return true;
}
else
{
@ -5059,7 +4984,7 @@ bool simple_wallet::transfer_main(int transfer_type, const std::vector<std::stri
// prompt is there is no payment id and confirmation is required
if (!payment_id_seen && m_wallet->confirm_missing_payment_id() && dsts.size() > num_subaddresses)
{
std::string accepted = input_line(tr("No payment id is included with this transaction. Is this okay? (Y/Yes/N/No): "));
std::string accepted = input_line(tr("No payment id is included with this transaction: ask the recipient to use subaddresses instead. Continue anyway? (Y/Yes/N/No): "));
if (std::cin.eof())
return true;
if (!command_line::is_yes(accepted))
@ -5429,7 +5354,7 @@ bool simple_wallet::sweep_main(uint64_t below, bool locked, const std::vector<st
{
auto print_usage = [below]()
{
fail_msg_writer() << boost::format(tr("usage: %s [index=<N1>[,<N2>,...]] [<priority>] [<ring_size>] [outputs=<N>] <address> [<payment_id>]")) % (below ? "sweep_below" : "sweep_all");
fail_msg_writer() << boost::format(tr("usage: %s [index=<N1>[,<N2>,...]] [<priority>] [<ring_size>] [outputs=<N>] <address>")) % (below ? "sweep_below" : "sweep_all");
};
if (args_.size() == 0)
{
@ -5576,7 +5501,7 @@ bool simple_wallet::sweep_main(uint64_t below, bool locked, const std::vector<st
if(!r && local_args.size() == 3)
{
fail_msg_writer() << tr("payment id has invalid format, expected 16 or 64 character hex string: ") << payment_id_str;
fail_msg_writer() << tr("payment id has invalid format, expected 16 character hex string: ") << payment_id_str;
print_usage();
return true;
}
@ -5614,7 +5539,7 @@ bool simple_wallet::sweep_main(uint64_t below, bool locked, const std::vector<st
// prompt is there is no payment id and confirmation is required
if (!payment_id_seen && m_wallet->confirm_missing_payment_id() && !info.is_subaddress)
{
std::string accepted = input_line(tr("No payment id is included with this transaction. Is this okay? (Y/Yes/N/No): "));
std::string accepted = input_line(tr("No payment id is included with this transaction: ask the recipient to use subaddresses instead. Continue anyway? (Y/Yes/N/No): "));
if (std::cin.eof())
return true;
if (!command_line::is_yes(accepted))
@ -5847,7 +5772,7 @@ bool simple_wallet::sweep_single(const std::vector<std::string> &args_)
if (local_args.size() != 2)
{
fail_msg_writer() << tr("usage: sweep_single [<priority>] [<ring_size>] [outputs=<N>] <key_image> <address> [<payment_id>]");
fail_msg_writer() << tr("usage: sweep_single [<priority>] [<ring_size>] [outputs=<N>] <key_image> <address>");
return true;
}
@ -5886,7 +5811,7 @@ bool simple_wallet::sweep_single(const std::vector<std::string> &args_)
// prompt if there is no payment id and confirmation is required
if (!payment_id_seen && m_wallet->confirm_missing_payment_id() && !info.is_subaddress)
{
std::string accepted = input_line(tr("No payment id is included with this transaction. Is this okay? (Y/Yes/N/No): "));
std::string accepted = input_line(tr("No payment id is included with this transaction: ask the recipient to use subaddresses instead. Continue anyway? (Y/Yes/N/No): "));
if (std::cin.eof())
return true;
if (!command_line::is_yes(accepted))
@ -6011,7 +5936,7 @@ bool simple_wallet::donate(const std::vector<std::string> &args_)
std::vector<std::string> local_args = args_;
if(local_args.empty() || local_args.size() > 5)
{
fail_msg_writer() << tr("usage: donate [index=<N1>[,<N2>,...]] [<priority>] [<ring_size>] <amount> [<payment_id>]");
fail_msg_writer() << tr("usage: donate [index=<N1>[,<N2>,...]] [<priority>] [<ring_size>] <amount>");
return true;
}
std::string amount_str;
@ -6090,6 +6015,7 @@ bool simple_wallet::accept_loaded_tx(const std::function<size_t()> get_num_txes,
if (!payment_id_string.empty())
payment_id_string += ", ";
payment_id_string = std::string("unencrypted payment ID ") + epee::string_tools::pod_to_hex(payment_id);
payment_id_string += " (OBSOLETE)";
}
}
}
@ -7659,6 +7585,7 @@ bool simple_wallet::print_address(const std::vector<std::string> &args/* = std::
if (local_args.empty())
{
print_address_sub(index);
message_writer(console_color_white, true) << tr("\nNotice: Type <address new> to generate a new subaddress for each transaction. Reusing the same address is not recommended.\n");
}
else if (local_args.size() == 1 && local_args[0] == "all")
{
@ -7732,56 +7659,6 @@ bool simple_wallet::print_address(const std::vector<std::string> &args/* = std::
return true;
}
//----------------------------------------------------------------------------------------------------
bool simple_wallet::print_integrated_address(const std::vector<std::string> &args/* = std::vector<std::string>()*/)
{
crypto::hash8 payment_id;
if (args.size() > 1)
{
fail_msg_writer() << tr("usage: integrated_address [payment ID]");
return true;
}
if (args.size() == 0)
{
if (m_current_subaddress_account != 0)
{
fail_msg_writer() << tr("Integrated addresses can only be created for account 0");
return true;
}
payment_id = crypto::rand<crypto::hash8>();
success_msg_writer() << tr("Random payment ID: ") << payment_id;
success_msg_writer() << tr("Matching integrated address: ") << m_wallet->get_account().get_public_integrated_address_str(payment_id, m_wallet->nettype());
return true;
}
if(tools::wallet2::parse_short_payment_id(args.back(), payment_id))
{
if (m_current_subaddress_account != 0)
{
fail_msg_writer() << tr("Integrated addresses can only be created for account 0");
return true;
}
success_msg_writer() << m_wallet->get_account().get_public_integrated_address_str(payment_id, m_wallet->nettype());
return true;
}
else {
address_parse_info info;
if(get_account_address_from_str(info, m_wallet->nettype(), args.back()))
{
if (info.has_payment_id)
{
success_msg_writer() << boost::format(tr("Integrated address: %s, payment ID: %s")) %
get_account_address_as_str(m_wallet->nettype(), false, info.address) % epee::string_tools::pod_to_hex(info.payment_id);
}
else
{
success_msg_writer() << (info.is_subaddress ? tr("Subaddress: ") : tr("Standard address: ")) << get_account_address_as_str(m_wallet->nettype(), info.is_subaddress, info.address);
}
return true;
}
}
fail_msg_writer() << tr("failed to parse payment ID or address");
return true;
}
//----------------------------------------------------------------------------------------------------
bool simple_wallet::address_book(const std::vector<std::string> &args/* = std::vector<std::string>()*/)
{
if (args.size() == 0)
@ -7789,7 +7666,7 @@ bool simple_wallet::address_book(const std::vector<std::string> &args/* = std::v
}
else if (args.size() == 1 || (args[0] != "add" && args[0] != "delete"))
{
fail_msg_writer() << tr("usage: address_book [(add (<address> [pid <long or short payment id>])|<integrated address> [<description possibly with whitespaces>])|(delete <index>)]");
fail_msg_writer() << tr("usage: address_book [(add <address> [<description possibly with whitespaces>])|(delete <index>)]");
return true;
}
else if (args[0] == "add")
@ -7853,7 +7730,7 @@ bool simple_wallet::address_book(const std::vector<std::string> &args/* = std::v
auto& row = address_book[i];
success_msg_writer() << tr("Index: ") << i;
success_msg_writer() << tr("Address: ") << get_account_address_as_str(m_wallet->nettype(), row.m_is_subaddress, row.m_address);
success_msg_writer() << tr("Payment ID: ") << row.m_payment_id;
success_msg_writer() << tr("Payment ID: ") << row.m_payment_id << " (OBSOLETE)";
success_msg_writer() << tr("Description: ") << row.m_description << "\n";
}
}

@ -152,7 +152,6 @@ namespace cryptonote
bool show_balance_unlocked(bool detailed = false);
bool show_balance(const std::vector<std::string> &args = std::vector<std::string>());
bool show_incoming_transfers(const std::vector<std::string> &args);
bool show_payments(const std::vector<std::string> &args);
bool show_blockchain_height(const std::vector<std::string> &args);
bool transfer_main(int transfer_type, const std::vector<std::string> &args);
bool transfer(const std::vector<std::string> &args);
@ -173,7 +172,6 @@ namespace cryptonote
void print_accounts();
void print_accounts(const std::string& tag);
bool print_address(const std::vector<std::string> &args = std::vector<std::string>());
bool print_integrated_address(const std::vector<std::string> &args = std::vector<std::string>());
bool address_book(const std::vector<std::string> &args = std::vector<std::string>());
bool save(const std::vector<std::string> &args);
bool save_watch_only(const std::vector<std::string> &args);
@ -211,7 +209,6 @@ namespace cryptonote
bool import_outputs(const std::vector<std::string> &args);
bool show_transfer(const std::vector<std::string> &args);
bool change_password(const std::vector<std::string>& args);
bool payment_id(const std::vector<std::string> &args);
bool print_fee_info(const std::vector<std::string> &args);
bool prepare_multisig(const std::vector<std::string>& args);
bool make_multisig(const std::vector<std::string>& args);

@ -790,14 +790,6 @@ std::string WalletImpl::address(uint32_t accountIndex, uint32_t addressIndex) co
return m_wallet->get_subaddress_as_str({accountIndex, addressIndex});
}
std::string WalletImpl::integratedAddress(const std::string &payment_id) const
{
crypto::hash8 pid;
if (!tools::wallet2::parse_short_payment_id(payment_id, pid)) {
return "";
}
return m_wallet->get_integrated_address_as_str(pid);
}
std::string WalletImpl::secretViewKey() const
{

@ -90,7 +90,6 @@ public:
void statusWithErrorString(int& status, std::string& errorString) const override;
bool setPassword(const std::string &password) override;
std::string address(uint32_t accountIndex = 0, uint32_t addressIndex = 0) const override;
std::string integratedAddress(const std::string &payment_id) const override;
std::string secretViewKey() const override;
std::string publicViewKey() const override;
std::string secretSpendKey() const override;

@ -413,17 +413,6 @@ struct Wallet
//! check if hard fork rules should be used
virtual bool useForkRules(uint8_t version, int64_t early_blocks) const = 0;
/*!
* \brief integratedAddress - returns integrated address for current wallet address and given payment_id.
* if passed "payment_id" param is an empty string or not-valid payment id string
* (16 characters hexadecimal string) - random payment_id will be generated
*
* \param payment_id - 16 characters hexadecimal string or empty string if new random payment id needs to be
* generated
* \return - 106 characters string representing integrated address
*/
virtual std::string integratedAddress(const std::string &payment_id) const = 0;
/*!
* \brief secretViewKey - returns secret view key
* \return - secret view key
*/

Loading…
Cancel
Save