diff --git a/src/simplewallet/simplewallet.cpp b/src/simplewallet/simplewallet.cpp index 7e9e9872664..012abdbae82 100644 --- a/src/simplewallet/simplewallet.cpp +++ b/src/simplewallet/simplewallet.cpp @@ -73,6 +73,7 @@ #include "multisig/multisig.h" #include "wallet/wallet_args.h" #include "wallet/fee_priority.h" +#include "wallet/uri.hpp" #include "version.h" #include #include "wallet/message_store.h" @@ -6479,15 +6480,18 @@ bool simple_wallet::transfer_main(const std::vector &args_, bool ca { std::string payment_id_str = local_args.back(); crypto::hash payment_id; - bool r = true; if (tools::wallet2::parse_long_payment_id(payment_id_str, payment_id)) { LONG_PAYMENT_ID_SUPPORT_CHECK(); - } - if(!r) - { - fail_msg_writer() << tr("payment id failed to encode"); - return false; + std::string extra_nonce; + set_payment_id_to_tx_extra_nonce(extra_nonce, payment_id); + if (!add_extra_nonce_to_tx_extra(extra, extra_nonce)) + { + fail_msg_writer() << tr("failed to set up payment id"); + return false; + } + payment_id_seen = true; + local_args.pop_back(); } } @@ -6514,6 +6518,7 @@ bool simple_wallet::transfer_main(const std::vector &args_, bool ca vector dsts_info; vector dsts; + std::string tx_description; for (size_t i = 0; i < local_args.size(); ) { dsts_info.emplace_back(); @@ -6522,25 +6527,106 @@ bool simple_wallet::transfer_main(const std::vector &args_, bool ca bool r = true; // check for a URI - std::string address_uri, payment_id_uri, tx_description, recipient_name, error; - std::vector unknown_parameters; - uint64_t amount = 0; - bool has_uri = m_wallet->parse_uri(local_args[i], address_uri, payment_id_uri, amount, tx_description, recipient_name, unknown_parameters, error); + std::string error; + std::vector unknown_parameters, addresses, recipient_names; + std::vector amounts; + bool has_uri = parse_uri(local_args[i], addresses, amounts, recipient_names, tx_description, unknown_parameters, error); if (has_uri) { - r = cryptonote::get_account_address_from_str_or_url(info, m_wallet->nettype(), address_uri, oa_prompter); - if (payment_id_uri.size() == 16) + std::string payment_id_uri; + for (auto &unknown_parameter : unknown_parameters) { - if (!tools::wallet2::parse_short_payment_id(payment_id_uri, info.payment_id)) + if (boost::starts_with(unknown_parameter, "tx_payment_id=")) { - fail_msg_writer() << tr("failed to parse short payment ID from URI"); + payment_id_uri = unknown_parameter.substr(std::string("tx_payment_id=").size()); + } + } + + if (!payment_id_uri.empty() && addresses.size() > 1) + { + fail_msg_writer() << tr("a single transaction-level payment id (tx_payment_id) cannot be used with multiple outputs"); + return false; + } + + if (!payment_id_uri.empty()) + { + if (payment_id_seen) + { + fail_msg_writer() << tr("a single transaction cannot use more than one payment id"); return false; } - info.has_payment_id = true; + + if (payment_id_uri.size() == 16) + { + crypto::hash8 short_pid; + if (!tools::wallet2::parse_short_payment_id(payment_id_uri, short_pid)) + { + fail_msg_writer() << tr("failed to parse short payment ID from URI"); + return false; + } + std::string extra_nonce; + set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, short_pid); + if (!add_extra_nonce_to_tx_extra(extra, extra_nonce)) + { + fail_msg_writer() << tr("failed to set up payment id"); + return false; + } + payment_id_seen = true; + } + else + { + crypto::hash long_pid; + if (!tools::wallet2::parse_long_payment_id(payment_id_uri, long_pid)) + { + fail_msg_writer() << tr("failed to parse payment ID from URI"); + return false; + } + LONG_PAYMENT_ID_SUPPORT_CHECK(); + std::string extra_nonce; + set_payment_id_to_tx_extra_nonce(extra_nonce, long_pid); + if (!add_extra_nonce_to_tx_extra(extra, extra_nonce)) + { + fail_msg_writer() << tr("failed to set up payment id"); + return false; + } + payment_id_seen = true; + } + } + for (size_t j = 0; j < addresses.size(); ++j) + { + r = cryptonote::get_account_address_from_str_or_url(info, m_wallet->nettype(), addresses[j], oa_prompter); + if (!r) + { + fail_msg_writer() << tr("failed to parse address"); + return false; + } + + if (info.has_payment_id) + { + if (payment_id_seen) + { + fail_msg_writer() << tr("a single transaction cannot use more than one payment id"); + return false; + } + std::string extra_nonce; + set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, info.payment_id); + if (!add_extra_nonce_to_tx_extra(extra, extra_nonce)) + { + fail_msg_writer() << tr("failed to set up payment id"); + return false; + } + payment_id_seen = true; + } + + de.amount = amounts[j]; + de.original = addresses[j]; + de.addr = info.address; + de.is_subaddress = info.is_subaddress; + de.is_integrated = info.has_payment_id; + dsts.push_back(de); } - de.amount = amount; - de.original = local_args[i]; ++i; + continue; } else if (i + 1 < local_args.size()) { @@ -6573,33 +6659,18 @@ bool simple_wallet::transfer_main(const std::vector &args_, bool ca de.is_subaddress = info.is_subaddress; de.is_integrated = info.has_payment_id; - if (info.has_payment_id || !payment_id_uri.empty()) + if (info.has_payment_id) { if (payment_id_seen) { fail_msg_writer() << tr("a single transaction cannot use more than one payment id"); return false; } - - crypto::hash payment_id; std::string extra_nonce; - if (info.has_payment_id) - { - set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, info.payment_id); - } - else if (tools::wallet2::parse_payment_id(payment_id_uri, payment_id)) - { - LONG_PAYMENT_ID_SUPPORT_CHECK(); - } - else - { - fail_msg_writer() << tr("failed to parse payment id, though it was detected"); - return false; - } - bool r = add_extra_nonce_to_tx_extra(extra, extra_nonce); - if(!r) + set_encrypted_payment_id_to_tx_extra_nonce(extra_nonce, info.payment_id); + if (!add_extra_nonce_to_tx_extra(extra, extra_nonce)) { - fail_msg_writer() << tr("failed to set up payment id, though it was decoded correctly"); + fail_msg_writer() << tr("failed to set up payment id"); return false; } payment_id_seen = true; @@ -6617,6 +6688,11 @@ bool simple_wallet::transfer_main(const std::vector &args_, bool ca SCOPED_WALLET_UNLOCK_ON_BAD_PASSWORD(return false;); + if (payment_id_seen && dsts.size() > 1) { + fail_msg_writer() << tr("a single transaction-level payment id (tx_payment_id) cannot be used with multiple outputs"); + return false; + } + try { // figure out what tx will be necessary @@ -6747,6 +6823,7 @@ bool simple_wallet::transfer_main(const std::vector &args_, bool ca } } + auto backup_ptx_vector = ptx_vector; // actually commit the transactions const multisig::multisig_account_status ms_status{m_wallet->get_multisig_status()}; if (ms_status.multisig_is_active && called_by_mms) @@ -6812,6 +6889,15 @@ bool simple_wallet::transfer_main(const std::vector &args_, bool ca { commit_or_save(ptx_vector, m_do_not_relay); } + + if (!tx_description.empty()) + { + for (const auto &ptx : backup_ptx_vector) + { + crypto::hash txid = get_transaction_hash(ptx.tx); + m_wallet->set_tx_note(txid, tx_description); + } + } } catch (const std::exception &e) { @@ -8699,12 +8785,21 @@ bool simple_wallet::show_transfers(const std::vector &args_) std::string destinations = "-"; if (!transfer.outputs.empty()) { - destinations = ""; - for (const auto& output : transfer.outputs) + if (transfer.outputs.size() == 1) + { + destinations = (transfer.direction == "in" ? transfer.outputs[0].first.substr(0, 6) : transfer.outputs[0].first); + } + else { - if (!destinations.empty()) - destinations += ", "; - destinations += (transfer.direction == "in" ? output.first.substr(0, 6) : output.first) + ":" + print_money(output.second); + destinations.clear(); + for (const auto& output : transfer.outputs) + { + if (!destinations.empty()) + { + destinations += ", "; + } + destinations += (transfer.direction == "in" ? output.first.substr(0, 6) : output.first) + ":" + print_money(output.second); + } } } diff --git a/src/wallet/CMakeLists.txt b/src/wallet/CMakeLists.txt index b976c02d77e..fb2034133d1 100644 --- a/src/wallet/CMakeLists.txt +++ b/src/wallet/CMakeLists.txt @@ -37,6 +37,7 @@ set(wallet_sources node_rpc_proxy.cpp message_store.cpp message_transporter.cpp + uri.cpp ) monero_find_all_headers(wallet_private_headers "${CMAKE_CURRENT_SOURCE_DIR}") diff --git a/src/wallet/uri.cpp b/src/wallet/uri.cpp new file mode 100644 index 00000000000..775d8465659 --- /dev/null +++ b/src/wallet/uri.cpp @@ -0,0 +1,385 @@ +// Copyright (c) 2018-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 "uri.hpp" +#include +#include +#include +#include +#include +#include "net/http_client.h" + +static std::string convert_to_url_format(const std::string &uri) +{ + std::string s = epee::net_utils::conver_to_url_format(uri); + + // replace '=' with "%3D" and '?' with "%3F". + std::string result; + result.reserve(s.size()); + + for (char c : s) + { + if (c == '=') + { + result.append("%3D"); + } + else if (c == '?') + { + result.append("%3F"); + } + + else + { + result.push_back(c); + } + } + + return result; +} +//---------------------------------------------------------------------------------------------------- +static bool validate_address(const std::string &addr, cryptonote::network_type network_type) +{ + if (network_type == cryptonote::UNDEFINED) + { + cryptonote::address_parse_info info; + + const cryptonote::network_type nets[] = { + cryptonote::network_type::MAINNET, + cryptonote::network_type::TESTNET, + cryptonote::network_type::STAGENET, + cryptonote::network_type::FAKECHAIN + }; + + for (auto n : nets) + { + if (cryptonote::get_account_address_from_str(info, n, addr)) + { + return true; + } + } + } + else + { + cryptonote::address_parse_info info; + return cryptonote::get_account_address_from_str(info, network_type, addr); + } + + return false; +} +//---------------------------------------------------------------------------------------------------- +std::string make_uri(const std::vector &addresses, const std::vector &amounts, const std::vector &recipient_names, const std::string &tx_description, std::string &error, cryptonote::network_type network_type) +{ + if (addresses.empty()) + { + error = "No recipient information like addresses are provided."; + return std::string(); + } + + if (addresses.size() != amounts.size() || addresses.size() != recipient_names.size()) + { + error = (boost::format("The counts of addresses (%1%), amounts (%2%), and recipient names (%3%) do not match.") % addresses.size() % amounts.size() % recipient_names.size()).str(); + return std::string(); + } + + if (addresses.size() == 1) + { + const std::string &address = addresses[0]; + if (!validate_address(address, network_type)) + { + error = std::string("wrong address: ") + address; + return std::string(); + } + + std::string uri = "monero:" + address; + unsigned int n_fields = 0; + + if (amounts[0] > 0) + { + // URI encoded amount is in decimal units, not atomic units + uri += (n_fields++ ? "&" : "?") + std::string("tx_amount=") + cryptonote::print_money(amounts[0]); + } + + if (!recipient_names[0].empty()) + { + uri += (n_fields++ ? "&" : "?") + std::string("recipient_name=") + convert_to_url_format(recipient_names[0]); + } + + if (!tx_description.empty()) + { + uri += (n_fields++ ? "&" : "?") + std::string("tx_description=") + convert_to_url_format(tx_description); + } + + return uri; + } + + + std::string uri = "monero:"; + bool first_param = true; + + for (size_t i = 0; i < addresses.size(); ++i) + { + std::string out_val; + out_val.reserve(256); + + const std::string &address = addresses[i]; + if (!validate_address(address, network_type)) + { + error = std::string("wrong address: ") + address; + return std::string(); + } + + out_val.append(address); + out_val.push_back(';'); + + if (amounts[i] > 0) + out_val += cryptonote::print_money(amounts[i]); + out_val.push_back(';'); + + if (!recipient_names[i].empty()) + out_val += convert_to_url_format(recipient_names[i]); + + uri += (first_param ? "?" : "&"); + first_param = false; + uri.append("output="); + uri += out_val; + } + + if (!tx_description.empty()) + { + uri += (first_param ? "?" : "&"); + uri += std::string("tx_description=") + convert_to_url_format(tx_description); + } + + return uri; +} +//---------------------------------------------------------------------------------------------------- +static bool parse_output_param(const std::string &value, std::vector &addresses, std::vector &amounts, std::vector &names, cryptonote::network_type network_type, std::string &error) +{ + std::vector fields; + boost::split(fields, value, boost::is_any_of(";")); + while (fields.size() < 3) + { + fields.emplace_back(); + } + + const std::string &address = fields[0]; + const std::string &amount_str = fields[1]; + const std::string &name = fields[2]; + + if (address.empty()) + { + error = "output parameter missing address"; + return false; + } + + if (!validate_address(address, network_type)) + { + error = std::string("URI contains improper address: ") + address; + return false; + } + + uint64_t amount = 0; + if (!amount_str.empty() && !cryptonote::parse_amount(amount, amount_str)) + { + error = "Invalid amount: " + amount_str; + return false; + } + + addresses.push_back(address); + amounts.push_back(amount); + names.push_back(name.empty() ? std::string() : epee::net_utils::convert_from_url_format(name)); + return true; +} +//---------------------------------------------------------------------------------------------------- +bool parse_uri(const std::string &uri, std::vector &addresses, std::vector &amounts, std::vector &recipient_names, std::string &tx_description, std::vector &unknown_parameters, std::string &error, cryptonote::network_type network_type) +{ + if (uri.rfind("monero:", 0) != 0) + { + error = "URI has wrong scheme (expected \"monero:\"): " + uri; + return false; + } + + std::string remainder = uri.substr(7); + std::size_t qpos = remainder.find('?'); + std::string path = (qpos == std::string::npos) ? remainder : remainder.substr(0, qpos); + std::string query = (qpos == std::string::npos) ? "" : remainder.substr(qpos + 1); + + addresses.clear(); + amounts.clear(); + recipient_names.clear(); + tx_description.clear(); + unknown_parameters.clear(); + + std::vector args; + if (!query.empty()) + { + boost::split(args, query, boost::is_any_of("&")); + } + + bool has_output = std::any_of(args.begin(), args.end(), [](const std::string &arg) { + return arg.rfind("output=", 0) == 0; + }); + + if (has_output) + { + // multiple recipient uri + if (!path.empty()) + { + error = "URI must not include a path address when using output= parameters (ambiguous)"; + return false; + } + + std::set have_arg; + + for (const std::string &arg : args) + { + if (arg.empty()) + { + continue; + } + + auto eq = arg.find('='); + if (eq == std::string::npos) + { + error = "Bad parameter: " + arg; + return false; + } + std::string key = arg.substr(0, eq); + std::string value = arg.substr(eq + 1); + + if (key != "output") + { + if (have_arg.find(key) != have_arg.end()) + { + error = std::string("URI has more than one instance of ") + key; + return false; + } + have_arg.insert(key); + } + + if (key == "output") + { + if (!parse_output_param(value, addresses, amounts, recipient_names, network_type, error)) + { + return false; + } + } + else if (key == "tx_description") + { + tx_description = epee::net_utils::convert_from_url_format(value); + } + else + { + unknown_parameters.push_back(arg); + } + } + if (addresses.empty()) + { + error = "At least one output required"; + return false; + } + + if (addresses.size() != amounts.size() || addresses.size() != recipient_names.size()) + { + error = "Internal error: parsed output vector sizes mismatch."; + return false; + } + return true; + } + + if (!query.empty()) + { + // single recipient uri + if (!validate_address(path, network_type)) + { + error = std::string("URI contains improper address: ") + path; + return false; + } + + uint64_t amount = 0; + std::string name; + std::set have_arg; + for (const std::string &arg : args) + { + auto eq = arg.find('='); + if (eq == std::string::npos) + { + error = "Bad parameter: " + arg; + return false; + } + std::string key = arg.substr(0, eq); + std::string value = arg.substr(eq + 1); + + if (have_arg.find(key) != have_arg.end()) + { + error = std::string("URI has more than one instance of ") + key; + return false; + } + have_arg.insert(key); + + if (key == "tx_amount") + { + if (!cryptonote::parse_amount(amount, value)) + { + error = "Invalid amount: " + value; + return false; + } + } + else if (key == "recipient_name") + { + name = epee::net_utils::convert_from_url_format(value); + } + else if (key == "tx_description") + { + tx_description = epee::net_utils::convert_from_url_format(value); + } + else + { + unknown_parameters.push_back(arg); + } + } + addresses.push_back(path); + amounts.push_back(amount); + recipient_names.push_back(name); + return true; + } + + // bare address + if (!validate_address(path, network_type)) + { + error = std::string("URI contains improper address: ") + path; + return false; + } + + addresses.push_back(path); + amounts.push_back(0); + recipient_names.push_back(""); + return true; +} +//---------------------------------------------------------------------------------------------------- diff --git a/src/wallet/uri.hpp b/src/wallet/uri.hpp new file mode 100644 index 00000000000..e599d1e63b7 --- /dev/null +++ b/src/wallet/uri.hpp @@ -0,0 +1,39 @@ +// Copyright (c) 2018-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. + +#pragma once + +#include +#include +#include +#include "cryptonote_basic/cryptonote_basic_impl.h" +#include "cryptonote_basic/cryptonote_format_utils.h" + +std::string make_uri(const std::vector &addresses, const std::vector &amounts, const std::vector &recipient_names, const std::string &tx_description, std::string &error, cryptonote::network_type network_type = cryptonote::UNDEFINED); +bool parse_uri(const std::string &uri, std::vector &addresses, std::vector &amounts, std::vector &recipient_names, std::string &tx_description, std::vector &unknown_parameters, std::string &error, cryptonote::network_type network_type = cryptonote::UNDEFINED); diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp index 77613482d8d..5c8092b1caf 100644 --- a/src/wallet/wallet_rpc_server.cpp +++ b/src/wallet/wallet_rpc_server.cpp @@ -40,6 +40,7 @@ using namespace epee; #include "version.h" #include "wallet_rpc_server.h" #include "wallet/wallet_args.h" +#include "wallet/uri.hpp" #include "common/command_line.h" #include "common/i18n.h" #include "common/scoped_message_writer.h" @@ -3237,6 +3238,21 @@ namespace tools return true; } //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_make_uri_v2(const wallet_rpc::COMMAND_RPC_MAKE_URI_V2::request& req, wallet_rpc::COMMAND_RPC_MAKE_URI_V2::response& res, epee::json_rpc::error& er, const connection_context *ctx) + { + std::string error; + std::string uri = make_uri(req.addresses, req.amounts, req.recipient_names, req.tx_description, error); + if (uri.empty()) + { + er.code = WALLET_RPC_ERROR_CODE_WRONG_URI; + er.message = std::string("Cannot make URI from supplied parameters: ") + error; + return false; + } + + res.uri = uri; + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_parse_uri(const wallet_rpc::COMMAND_RPC_PARSE_URI::request& req, wallet_rpc::COMMAND_RPC_PARSE_URI::response& res, epee::json_rpc::error& er, const connection_context *ctx) { if (!m_wallet) return not_open(er); @@ -3250,6 +3266,25 @@ namespace tools return true; } //------------------------------------------------------------------------------------------------------------------------------ + bool wallet_rpc_server::on_parse_uri_v2(const wallet_rpc::COMMAND_RPC_PARSE_URI_V2::request& req, wallet_rpc::COMMAND_RPC_PARSE_URI_V2::response& res, epee::json_rpc::error& er, const connection_context *ctx) + { + std::string error; + std::vector addresses; + std::vector amounts; + std::vector recipient_names; + if (!parse_uri(req.uri, addresses, amounts, recipient_names, res.uri.tx_description, res.unknown_parameters, error)) + { + er.code = WALLET_RPC_ERROR_CODE_WRONG_URI; + er.message = "Error parsing URI: " + error; + return false; + } + + res.uri.addresses = addresses; + res.uri.amounts = amounts; + res.uri.recipient_names = recipient_names; + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ bool wallet_rpc_server::on_get_address_book(const wallet_rpc::COMMAND_RPC_GET_ADDRESS_BOOK_ENTRY::request& req, wallet_rpc::COMMAND_RPC_GET_ADDRESS_BOOK_ENTRY::response& res, epee::json_rpc::error& er, const connection_context *ctx) { if (!m_wallet) return not_open(er); diff --git a/src/wallet/wallet_rpc_server.h b/src/wallet/wallet_rpc_server.h index 742e50d1591..7828fe1e6ac 100644 --- a/src/wallet/wallet_rpc_server.h +++ b/src/wallet/wallet_rpc_server.h @@ -128,7 +128,9 @@ namespace tools MAP_JON_RPC_WE("export_key_images", on_export_key_images, wallet_rpc::COMMAND_RPC_EXPORT_KEY_IMAGES) MAP_JON_RPC_WE("import_key_images", on_import_key_images, wallet_rpc::COMMAND_RPC_IMPORT_KEY_IMAGES) MAP_JON_RPC_WE("make_uri", on_make_uri, wallet_rpc::COMMAND_RPC_MAKE_URI) + MAP_JON_RPC_WE("make_uri_v2", on_make_uri_v2, wallet_rpc::COMMAND_RPC_MAKE_URI_V2) MAP_JON_RPC_WE("parse_uri", on_parse_uri, wallet_rpc::COMMAND_RPC_PARSE_URI) + MAP_JON_RPC_WE("parse_uri_v2", on_parse_uri_v2, wallet_rpc::COMMAND_RPC_PARSE_URI_V2) MAP_JON_RPC_WE("get_address_book", on_get_address_book, wallet_rpc::COMMAND_RPC_GET_ADDRESS_BOOK_ENTRY) MAP_JON_RPC_WE("add_address_book", on_add_address_book, wallet_rpc::COMMAND_RPC_ADD_ADDRESS_BOOK_ENTRY) MAP_JON_RPC_WE("edit_address_book", on_edit_address_book, wallet_rpc::COMMAND_RPC_EDIT_ADDRESS_BOOK_ENTRY) @@ -225,7 +227,9 @@ namespace tools bool on_export_key_images(const wallet_rpc::COMMAND_RPC_EXPORT_KEY_IMAGES::request& req, wallet_rpc::COMMAND_RPC_EXPORT_KEY_IMAGES::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL); bool on_import_key_images(const wallet_rpc::COMMAND_RPC_IMPORT_KEY_IMAGES::request& req, wallet_rpc::COMMAND_RPC_IMPORT_KEY_IMAGES::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL); bool on_make_uri(const wallet_rpc::COMMAND_RPC_MAKE_URI::request& req, wallet_rpc::COMMAND_RPC_MAKE_URI::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL); + bool on_make_uri_v2(const wallet_rpc::COMMAND_RPC_MAKE_URI_V2::request& req, wallet_rpc::COMMAND_RPC_MAKE_URI_V2::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL); bool on_parse_uri(const wallet_rpc::COMMAND_RPC_PARSE_URI::request& req, wallet_rpc::COMMAND_RPC_PARSE_URI::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL); + bool on_parse_uri_v2(const wallet_rpc::COMMAND_RPC_PARSE_URI_V2::request& req, wallet_rpc::COMMAND_RPC_PARSE_URI_V2::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL); bool on_get_address_book(const wallet_rpc::COMMAND_RPC_GET_ADDRESS_BOOK_ENTRY::request& req, wallet_rpc::COMMAND_RPC_GET_ADDRESS_BOOK_ENTRY::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL); bool on_add_address_book(const wallet_rpc::COMMAND_RPC_ADD_ADDRESS_BOOK_ENTRY::request& req, wallet_rpc::COMMAND_RPC_ADD_ADDRESS_BOOK_ENTRY::response& res, epee::json_rpc::error& er, const connection_context *ctx = NULL); bool on_edit_address_book(const wallet_rpc::COMMAND_RPC_EDIT_ADDRESS_BOOK_ENTRY::request& req, wallet_rpc::COMMAND_RPC_EDIT_ADDRESS_BOOK_ENTRY::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 317add94bb5..6f1d25bfb4e 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 29 +#define WALLET_RPC_VERSION_MINOR 30 #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 @@ -1908,6 +1908,74 @@ namespace wallet_rpc typedef epee::misc_utils::struct_init response; }; + struct uri_spec_v2 + { + std::vector addresses; + std::vector amounts; + std::vector recipient_names; + std::string tx_description; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(addresses); + KV_SERIALIZE(amounts); + KV_SERIALIZE(recipient_names); + KV_SERIALIZE(tx_description); + END_KV_SERIALIZE_MAP() + }; + + struct COMMAND_RPC_MAKE_URI_V2 + { + struct request_t + { + std::vector addresses; + std::vector amounts; + std::vector recipient_names; + std::string tx_description; + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(addresses); + KV_SERIALIZE(amounts); + KV_SERIALIZE(recipient_names); + KV_SERIALIZE(tx_description); + END_KV_SERIALIZE_MAP() + }; + typedef epee::misc_utils::struct_init request; + + struct response_t + { + std::string uri; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(uri) + END_KV_SERIALIZE_MAP() + }; + typedef epee::misc_utils::struct_init response; + }; + + struct COMMAND_RPC_PARSE_URI_V2 + { + struct request_t + { + std::string uri; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(uri) + END_KV_SERIALIZE_MAP() + }; + typedef epee::misc_utils::struct_init request; + + struct response_t + { + uri_spec_v2 uri; + std::vector unknown_parameters; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(uri) + KV_SERIALIZE(unknown_parameters) + END_KV_SERIALIZE_MAP() + }; + typedef epee::misc_utils::struct_init response; + }; + struct COMMAND_RPC_ADD_ADDRESS_BOOK_ENTRY { struct request_t diff --git a/tests/functional_tests/uri.py b/tests/functional_tests/uri.py index fb981b3fdc7..080a65a1058 100755 --- a/tests/functional_tests/uri.py +++ b/tests/functional_tests/uri.py @@ -43,6 +43,7 @@ class URITest(): def run_test(self): self.create() self.test_monero_uri() + self.test_monero_multi_uri() def create(self): print('Creating wallet') @@ -56,7 +57,7 @@ def create(self): assert res.seed == seed def test_monero_uri(self): - print('Testing monero: URI') + print('Testing monero: URI - single') wallet = Wallet() utf8string = [u'えんしゅう', u'あまやかす'] @@ -224,6 +225,178 @@ def test_monero_uri(self): assert res.unknown_parameters == [u'unknown=' + quoted_utf8string[0]], res + def test_monero_multi_uri(self): + print('Testing monero: URI - multiple') + wallet = Wallet() + addr1 = '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm' + addr2 = '4BxSHvcgTwu25WooY4BVmgdcKwZu5EksVZSZkDd6ooxSVVqQ4ubxXkhLF6hEqtw96i9cf3cVfLw8UWe95bdDKfRQeYtPwLm1Jiw7AKt2LY' + addr3 = '8AsN91rznfkBGTY8psSNkJBg9SZgxxGGRUhGwRptBhgr5XSQ1XzmA9m8QAnoxydecSh5aLJXdrgXwTDMMZ1AuXsN1EX5Mtm' + addr4 = '48yZbMv8qWUtWxG4Jzgr2A49U1h1SeegLeF6pUGUgm8McF6bu8g7QvQyZ6iqJjLQTz8x8BfpwdKwRDR5eU5M4prUJfww8hBPH6aV9dZX2' + utf8string = [u'えんしゅう', u'あまやかす'] + + self.test_multi_uri_two_payments(wallet, addr1, addr2, utf8string) + self.test_multi_uri_three_payments(wallet, addr1, addr2, addr3, utf8string) + self.test_multi_uri_with_mismatched_amounts(wallet, addr1, addr2) + self.test_multi_uri_trailing_delimiter(wallet, addr1, addr2) + self.test_multi_uri_special_characters(wallet, addr1, addr2) + self.test_multi_uri_integrated_addresses(wallet, addr2, addr4) + self.test_multi_uri_unknown_parameters(wallet, addr1) + self.test_make_uri_single_vs_make_uri_v2_compatibility(wallet, addr1) + self.test_old_make_uri_with_parse_uri_v2(wallet, addr2) + self.test_make_uri_v2_with_old_parse_uri(wallet, addr3) + self.test_unknown_parameters(wallet, addr1) + + def test_multi_uri_two_payments(self, wallet, addr1, addr2, utf8string): + # build multi-recipient URI with two payments. + # monero:?output=;;&output=;;&tx_description=... + addresses = [ addr1, addr2] + amounts = [ 500000000000, 200000000000 ] + recipient_names = [ utf8string[0], utf8string[1]] + res = wallet.make_uri_v2(addresses=addresses, amounts=amounts, recipient_names=recipient_names, tx_description='Multi URI test with two payments') + + parsed = wallet.parse_uri_v2(res.uri) + + assert len(parsed.uri.addresses) == 2, "Expected 2 payments in multi-recipient URI" + assert parsed.uri.addresses[0] == addr1 + assert parsed.uri.amounts[0] == 500000000000 + assert parsed.uri.recipient_names[0] == utf8string[0] + assert parsed.uri.addresses[1] == addr2 + assert parsed.uri.amounts[1] == 200000000000 + assert parsed.uri.recipient_names[1] == utf8string[1] + assert parsed.uri.tx_description == 'Multi URI test with two payments' + + def test_multi_uri_three_payments(self, wallet, addr1, addr2, addr3, utf8string): + # build multi-recipient URI with three payments. + addresses = [ addr1, addr2, addr3 ] + amounts = [ 1000000000000, 500000000000, 250000000000 ] + recipient_names = [ utf8string[0], utf8string[1], '' ] + res = wallet.make_uri_v2(addresses=addresses, amounts=amounts, recipient_names=recipient_names, tx_description='Multi URI test with three payments') + parsed = wallet.parse_uri_v2(res.uri) + + assert len(parsed.uri.addresses) == 3, "Expected 3 payments in multi-recipient URI" + assert parsed.uri.addresses[0] == addr1 + assert parsed.uri.amounts[0] == 1000000000000 + assert parsed.uri.recipient_names[0] == utf8string[0] + assert parsed.uri.addresses[1] == addr2 + assert parsed.uri.amounts[1] == 500000000000 + assert parsed.uri.recipient_names[1] == utf8string[1] + assert parsed.uri.addresses[2] == addr3 + assert parsed.uri.amounts[2] == 250000000000 + assert parsed.uri.recipient_names[2] == '' + assert parsed.uri.tx_description == 'Multi URI test with three payments' + + def test_multi_uri_with_mismatched_amounts(self, wallet, addr1, addr2): + uri = 'monero:?output={a1};0.5;Alice&output={a2};;Bob'.format(a1=addr1, a2=addr2) + parsed = wallet.parse_uri_v2(uri) + + # both outputs should parse; second amount should decode to 0 + assert len(parsed.uri.addresses) == 2, "Expected 2 outputs" + assert parsed.uri.addresses[0] == addr1 + assert parsed.uri.amounts[0] == 500000000000 + assert parsed.uri.recipient_names[0] == 'Alice' + assert parsed.uri.addresses[1] == addr2 + assert parsed.uri.amounts[1] == 0, "Missing amount should decode to 0" + assert parsed.uri.recipient_names[1] == 'Bob' + + def test_multi_uri_trailing_delimiter(self, wallet, addr1, addr2): + uri_trailing = 'monero:?output={a1};0.5;Alice&output={a2};0.2;Bob&'.format(a1=addr1, a2=addr2) + parsed = wallet.parse_uri_v2(uri_trailing) + + assert len(parsed.uri.addresses) == 2, "Trailing delimiter should not add empty payment" + assert parsed.uri.addresses[0] == addr1 + assert parsed.uri.addresses[1] == addr2 + + def test_multi_uri_special_characters(self, wallet, addr1, addr2): + # case: special characters in recipient names and descriptions + special_name = "A&B=Test?" + special_desc = "Desc with spaces & symbols!" + addresses = [ addr1, addr2] + amounts = [ 750000000000, 250000000000 ] + recipient_names = [ special_name, special_name] + + # the RPC should URL-encode these parameters. + res = wallet.make_uri_v2(addresses=addresses, amounts=amounts, recipient_names=recipient_names, tx_description=special_desc) + parsed = wallet.parse_uri_v2(res.uri) + + for recipient_name in parsed.uri.recipient_names: + assert recipient_name == special_name, "Special characters in recipient name mismatch" + assert parsed.uri.tx_description == special_desc, "Special characters in description mismatch" + + def test_multi_uri_integrated_addresses(self, wallet, addr1, addr2): + # build multi-recipient URI with two integrated addresses + uri = 'monero:?output={a1};0.1;&output={a2};0.2;'.format(a1=addr1, a2=addr2) + + ok = False + try: + wallet.parse_uri_v2(uri) + except Exception: + ok = True + + assert ok, f"Expected rejection for multiple integrated addresses but it parsed: {uri}" + + def test_multi_uri_unknown_parameters(self, wallet, addr1): + # build a well-formed multi-recipient URI and tack on unknown parameters. + uri_with_unknown_parameters = 'monero:?output={a};239.39014;&foo=bar&baz=quux'.format(a=addr1) + parsed = wallet.parse_uri_v2(uri_with_unknown_parameters) + + assert parsed.uri.addresses[0] == addr1 + assert parsed.uri.amounts[0] == 239390140000000 + assert parsed.unknown_parameters == ['foo=bar', 'baz=quux'], "Unknown parameters mismatch" + + def test_make_uri_single_vs_make_uri_v2_compatibility(self, wallet, addr): + amount = 250000000000 # 0.25 + name = "Eve" + desc = "compatibility test" + + old = wallet.make_uri(address=addr, amount=amount, recipient_name=name, tx_description=desc) + new = wallet.make_uri_v2(addresses=[addr], amounts=[amount], recipient_names=[name], tx_description=desc) + + assert old.uri, "old.make_uri returned empty uri" + assert new.uri, "new.make_uri_v2 returned empty uri" + assert old.uri == new.uri, "make_uri and make_uri_v2 did not produce identical URIs: {} != {}".format(old.uri, new.uri) + + def test_old_make_uri_with_parse_uri_v2(self, wallet, addr): + amount = 11000000000 + name = "Bob" + desc = "old->new parse test" + + old = wallet.make_uri(address=addr, amount=amount, recipient_name=name, tx_description=desc) + parsed = wallet.parse_uri_v2(old.uri) + + assert len(parsed.uri.addresses) == 1 + assert parsed.uri.addresses[0] == addr + assert parsed.uri.amounts[0] == amount + assert parsed.uri.recipient_names[0] == name + assert parsed.uri.tx_description == desc + assert len(parsed.get('unknown_parameters', [])) == 0 + + def test_make_uri_v2_with_old_parse_uri(self, wallet, addr): + amount = 500000000000 + name = "Trent" + desc = "new->old parse test" + + new = wallet.make_uri_v2(addresses=[addr], amounts=[amount], recipient_names=[name], tx_description=desc) + parsed = wallet.parse_uri(new.uri) + + assert parsed.uri.address == addr + assert parsed.uri.amount == amount + assert parsed.uri.recipient_name == name + assert parsed.uri.tx_description == desc + assert parsed.uri.payment_id == '' + assert len(parsed.get('unknown_parameters', [])) == 0 + + def test_unknown_parameters(self, wallet, addr): + old = wallet.make_uri(address=addr, amount=239390140000000, tx_description='donation') + uri_with_unknown = old.uri + '&foo=bar&baz=quux' + + parsed_v2 = wallet.parse_uri_v2(uri_with_unknown) + assert 'foo=bar' in parsed_v2.unknown_parameters and 'baz=quux' in parsed_v2.unknown_parameters + + v2 = wallet.make_uri_v2(addresses=[addr], amounts=[239390140000000], recipient_names=[''], tx_description='') + v2_with_unknown = v2.uri + '&foo=bar&baz=quux' + parsed_old = wallet.parse_uri(v2_with_unknown) + assert 'foo=bar' in parsed_old.unknown_parameters and 'baz=quux' in parsed_old.unknown_parameters + if __name__ == '__main__': URITest().run_test() diff --git a/tests/unit_tests/uri.cpp b/tests/unit_tests/uri.cpp index f1c2b694b5b..3e86cbad8e3 100644 --- a/tests/unit_tests/uri.cpp +++ b/tests/unit_tests/uri.cpp @@ -28,9 +28,11 @@ #include "gtest/gtest.h" #include "wallet/wallet2.h" +#include "wallet/uri.hpp" #define TEST_ADDRESS "9tTLtauaEKSj7xoVXytVH32R1pLZBk4VV4mZFGEh4wkXhDWqw1soPyf3fGixf1kni31VznEZkWNEza9d5TvjWwq5PaohYHC" #define TEST_INTEGRATED_ADDRESS "A4A1uPj4qaxj7xoVXytVH32R1pLZBk4VV4mZFGEh4wkXhDWqw1soPyf3fGixf1kni31VznEZkWNEza9d5TvjWwq5acaPMJfMbn3ReTsBpp" +#define TEST_INTEGRATED_ADDRESS2 "48UktANa1g71SkdXhHJ72kp4GZf2tvKwBzXjRSe5SZbFxjrjDwpT7obRksYzYpy5KN5wUGagY7q2aqFUDDhYSnA5Z6J82B5XZQGkDox9a" // included payment id: #define PARSE_URI(uri, expected) \ @@ -41,6 +43,16 @@ bool ret = w.parse_uri(uri, address, payment_id, amount, description, recipient_name, unknown_parameters, error); \ ASSERT_EQ(ret, expected); +#define PARSE_MULTI_URI(uri, expected) \ + std::vector addresses; \ + std::vector amounts; \ + std::vector recipient_names; \ + std::string description, error; \ + std::vector unknown_parameters; \ + bool ret = parse_uri(uri, addresses, amounts, recipient_names, \ + description, unknown_parameters, error, cryptonote::TESTNET); \ + ASSERT_EQ(ret, expected); + TEST(uri, empty_string) { PARSE_URI("", false); @@ -213,3 +225,165 @@ TEST(uri, url_encoded_once) ASSERT_EQ(description, "foo 20"); } + +TEST(uri, multiple_addresses_no_params) +{ + PARSE_MULTI_URI("monero:?output=" TEST_ADDRESS ";;&output=" TEST_ADDRESS ";;", true); + ASSERT_EQ(addresses.size(), 2); + ASSERT_EQ(addresses[0], TEST_ADDRESS); + ASSERT_EQ(addresses[1], TEST_ADDRESS); +} + +TEST(uri, multiple_addresses_with_amounts) +{ + PARSE_MULTI_URI("monero:?output=" TEST_ADDRESS ";0.5;&output=" TEST_ADDRESS ";0.2;", true); + ASSERT_EQ(addresses.size(), 2); + ASSERT_EQ(addresses[0], TEST_ADDRESS); + ASSERT_EQ(amounts[0], 500000000000); + ASSERT_EQ(addresses[1], TEST_ADDRESS); + ASSERT_EQ(amounts[1], 200000000000); +} + +TEST(uri, multiple_addresses_with_recipient_names) +{ + PARSE_MULTI_URI("monero:?output=" TEST_ADDRESS ";;Alice&output=" TEST_ADDRESS ";;Bob", true); + ASSERT_EQ(addresses.size(), 2); + ASSERT_EQ(addresses[0], TEST_ADDRESS); + ASSERT_EQ(recipient_names[0], "Alice"); + ASSERT_EQ(addresses[1], TEST_ADDRESS); + ASSERT_EQ(recipient_names[1], "Bob"); +} + +TEST(uri, multiple_addresses_with_mismatched_amounts) +{ + PARSE_MULTI_URI("monero:?output=" TEST_ADDRESS ";0.5;&output=" TEST_ADDRESS ";;", true); + ASSERT_EQ(addresses.size(), 2); + ASSERT_EQ(amounts[0], 500000000000); + ASSERT_EQ(amounts[1], 0); +} + +TEST(uri, multiple_integrated_addresses) +{ + PARSE_MULTI_URI("monero:?output=" TEST_INTEGRATED_ADDRESS ";;&output=" TEST_INTEGRATED_ADDRESS2 ";;", false); +} + +TEST(uri, multiple_addresses_with_mismatched_recipient_names) +{ + PARSE_MULTI_URI("monero:?output=" TEST_ADDRESS ";;Alice&output=" TEST_ADDRESS ";;", true); + ASSERT_EQ(recipient_names.size(), 2); + ASSERT_EQ(recipient_names[0], "Alice"); + ASSERT_EQ(recipient_names[1], ""); +} + +TEST(uri, multiple_addresses_with_partial_params) +{ + PARSE_MULTI_URI("monero:?output=" TEST_ADDRESS ";0.5;Alice&output=" TEST_ADDRESS ";0;", true); + ASSERT_EQ(addresses.size(), 2); + ASSERT_EQ(addresses[0], TEST_ADDRESS); + ASSERT_EQ(amounts[0], 500000000000); + ASSERT_EQ(recipient_names[0], "Alice"); + ASSERT_EQ(addresses[1], TEST_ADDRESS); + ASSERT_EQ(amounts[1], 0); + ASSERT_EQ(recipient_names[1], ""); +} + +TEST(uri, multiple_addresses_with_unknown_params) +{ + PARSE_MULTI_URI("monero:?output=" TEST_ADDRESS ";;&output=" TEST_ADDRESS ";;&unknown_param=123;456", true); + ASSERT_EQ(unknown_parameters.size(), 1); + ASSERT_EQ(unknown_parameters[0], "unknown_param=123;456"); +} + +TEST(uri, multiple_addresses_with_description) +{ + PARSE_MULTI_URI("monero:?output=" TEST_ADDRESS ";;&output=" TEST_ADDRESS ";;&tx_description=Payment%20for%20services", true); + ASSERT_EQ(description, "Payment for services"); +} + +TEST(uri, multiple_addresses_mismatched_params) +{ + PARSE_MULTI_URI("monero:" TEST_ADDRESS ";" TEST_ADDRESS "?tx_amount=0.5&recipient_name=Alice", false); +} + +TEST(uri, multiple_addresses_all_params_correct) +{ + PARSE_MULTI_URI("monero:?output=" TEST_ADDRESS ";0.5;Alice&output=" TEST_ADDRESS ";0.2;Bob&tx_description=Payment%20for%20services", true); + ASSERT_EQ(addresses.size(), 2); + ASSERT_EQ(addresses[0], TEST_ADDRESS); + ASSERT_EQ(amounts[0], 500000000000); + ASSERT_EQ(recipient_names[0], "Alice"); + ASSERT_EQ(addresses[1], TEST_ADDRESS); + ASSERT_EQ(amounts[1], 200000000000); + ASSERT_EQ(recipient_names[1], "Bob"); + ASSERT_EQ(description, "Payment for services"); +} + +TEST(uri, make_uri_single_recipient_compatibility) +{ + tools::wallet2 w(cryptonote::TESTNET); + std::string error_old, error_new; + + std::string old_uri = w.make_uri(TEST_ADDRESS, std::string(), 500000000000ULL, "Payment for services", "Alice", error_old); + + std::vector addresses = { TEST_ADDRESS }; + std::vector amounts = { 500000000000ULL }; + std::vector names = { "Alice" }; + std::string new_uri = make_uri(addresses, amounts, names, "Payment for services", error_new, cryptonote::TESTNET); + + ASSERT_TRUE(error_old.empty()); + ASSERT_TRUE(error_new.empty()); + ASSERT_EQ(old_uri, new_uri); +} + +TEST(uri, wallet2_make_uri_new_parse_uri_compatibility) +{ + tools::wallet2 w(cryptonote::TESTNET); + std::string err; + + // generate a URI using the old wallet2::make_uri + std::string uri = w.make_uri(TEST_ADDRESS, std::string(), 200000000000ULL, "desc", "Bob", err); + ASSERT_TRUE(err.empty()); + + // parse it with the new parse_uri (multi-recipient parser) + std::vector addresses; + std::vector amounts; + std::vector recipient_names; + std::string description; + std::vector unknown_parameters; + + bool ret = parse_uri(uri, addresses, amounts, recipient_names, description, unknown_parameters, err, cryptonote::TESTNET); + ASSERT_TRUE(ret); + ASSERT_TRUE(err.empty()); + ASSERT_EQ(addresses.size(), 1); + ASSERT_EQ(addresses[0], TEST_ADDRESS); + ASSERT_EQ(amounts[0], 200000000000ULL); + ASSERT_EQ(recipient_names[0], std::string("Bob")); + ASSERT_EQ(description, std::string("desc")); + ASSERT_TRUE(unknown_parameters.empty()); +} + +TEST(uri, new_make_uri_wallet2_parse_uri_compatibility) +{ + tools::wallet2 w(cryptonote::TESTNET); + std::string err; + + std::vector addresses = { TEST_ADDRESS }; + std::vector amounts = { 100000000000ULL }; // 0.1 + std::vector names = { "Carol" }; + std::string new_uri = make_uri(addresses, amounts, names, "note", err, cryptonote::TESTNET); + ASSERT_TRUE(err.empty()); + + std::string address, payment_id, recipient_name, description; + uint64_t amount = 0; + std::vector unknown_parameters; + bool ret = w.parse_uri(new_uri, address, payment_id, amount, description, recipient_name, unknown_parameters, err); + + ASSERT_TRUE(ret); + ASSERT_TRUE(err.empty()); + ASSERT_EQ(address, TEST_ADDRESS); + ASSERT_EQ(payment_id, std::string()); + ASSERT_EQ(amount, 100000000000ULL); + ASSERT_EQ(recipient_name, std::string("Carol")); + ASSERT_EQ(description, std::string("note")); + ASSERT_TRUE(unknown_parameters.empty()); +} diff --git a/utils/python-rpc/framework/wallet.py b/utils/python-rpc/framework/wallet.py index d0a516c1fdd..b3fecd0158f 100644 --- a/utils/python-rpc/framework/wallet.py +++ b/utils/python-rpc/framework/wallet.py @@ -1015,6 +1015,31 @@ def make_uri(self, address = '', payment_id = '', amount = 0, tx_description = ' } return self.rpc.send_json_rpc_request(make_uri) + def make_uri_v2(self, addresses, amounts, recipient_names, tx_description): + make_uri_v2 = { + 'method': 'make_uri_v2', + 'jsonrpc': '2.0', + 'params': { + 'addresses': addresses, + 'amounts': amounts, + 'recipient_names': recipient_names, + 'tx_description': tx_description, + }, + 'id': '0' + } + return self.rpc.send_json_rpc_request(make_uri_v2) + + def parse_uri_v2(self, uri): + parse_uri = { + 'method': 'parse_uri_v2', + 'jsonrpc': '2.0', + 'params': { + 'uri': uri, + }, + 'id': '0' + } + return self.rpc.send_json_rpc_request(parse_uri) + def parse_uri(self, uri): parse_uri = { 'method': 'parse_uri',