add multi-URI support

This commit is contained in:
U65535F 2025-03-09 18:17:26 +05:30
parent 977dedce2c
commit f753683748
12 changed files with 619 additions and 85 deletions

View file

@ -6532,25 +6532,50 @@ bool simple_wallet::transfer_main(const std::vector<std::string> &args_, bool ca
bool r = true;
// check for a URI
std::string address_uri, payment_id_uri, tx_description, recipient_name, error;
std::string tx_description, error;
std::vector<std::string> 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::vector<std::string> addresses, recipient_names;
std::vector<uint64_t> amounts;
bool has_uri = m_wallet->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)
bool payment_id_seen = false;
for (size_t address_index = 0; address_index < addresses.size(); address_index++)
{
if (!tools::wallet2::parse_short_payment_id(payment_id_uri, info.payment_id))
r = cryptonote::get_account_address_from_str_or_url(info, m_wallet->nettype(), addresses[address_index], oa_prompter);
if (info.has_payment_id)
{
fail_msg_writer() << tr("failed to parse short payment ID from URI");
return false;
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;
}
info.has_payment_id = true;
de.amount = amounts[address_index];
de.original = addresses[address_index];
if (!r)
{
fail_msg_writer() << tr("failed to parse address");
return false;
}
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;
i++;
break;
}
else if (i + 1 < local_args.size())
{
@ -6583,34 +6608,20 @@ bool simple_wallet::transfer_main(const std::vector<std::string> &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;
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);
if (!add_extra_nonce_to_tx_extra(extra, extra_nonce))
{
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)
{
fail_msg_writer() << tr("failed to set up payment id, though it was decoded correctly");
return false;
fail_msg_writer() << tr("failed to set up payment id");
return false;
}
payment_id_seen = true;
}

View file

@ -2553,11 +2553,21 @@ bool WalletImpl::parse_uri(const std::string &uri, std::string &address, std::st
return m_wallet->parse_uri(uri, address, payment_id, amount, tx_description, recipient_name, unknown_parameters, error);
}
bool WalletImpl::parse_uri(const std::string &uri, std::vector<std::string> &addresses, std::vector<std::uint64_t> &amounts, std::vector<std::string> &recipient_names, std::string &tx_description, std::vector<std::string> &unknown_parameters, std::string &error)
{
return m_wallet->parse_uri(uri, addresses, amounts, recipient_names, tx_description, unknown_parameters, error);
}
std::string WalletImpl::make_uri(const std::string &address, const std::string &payment_id, uint64_t amount, const std::string &tx_description, const std::string &recipient_name, std::string &error) const
{
return m_wallet->make_uri(address, payment_id, amount, tx_description, recipient_name, error);
}
std::string WalletImpl::make_uri(const std::vector<std::string> &addresses, const std::vector<std::uint64_t> &amounts, const std::vector<std::string> &recipient_names, const std::string &tx_description, std::string &error) const
{
return m_wallet->make_uri(addresses, amounts, recipient_names, tx_description, error);
}
std::string WalletImpl::getDefaultDataDir() const
{
return tools::get_default_data_dir();

View file

@ -214,7 +214,9 @@ public:
virtual void startRefresh() override;
virtual void pauseRefresh() override;
virtual bool parse_uri(const std::string &uri, std::string &address, std::string &payment_id, uint64_t &amount, std::string &tx_description, std::string &recipient_name, std::vector<std::string> &unknown_parameters, std::string &error) override;
bool parse_uri(const std::string &uri, std::vector<std::string> &addresses, std::vector<std::uint64_t> &amounts, std::vector<std::string> &recipient_names, std::string &tx_description, std::vector<std::string> &unknown_parameters, std::string &error) override;
virtual std::string make_uri(const std::string &address, const std::string &payment_id, uint64_t amount, const std::string &tx_description, const std::string &recipient_name, std::string &error) const override;
std::string make_uri(const std::vector<std::string> &addresses, const std::vector<std::uint64_t> &amounts, const std::vector<std::string> &recipient_names, const std::string &tx_description, std::string &error) const override;
virtual std::string getDefaultDataDir() const override;
virtual bool blackballOutputs(const std::vector<std::string> &outputs, bool add) override;
virtual bool blackballOutput(const std::string &amount, const std::string &offset) override;

View file

@ -1066,8 +1066,11 @@ struct Wallet
virtual bool verifyMessageWithPublicKey(const std::string &message, const std::string &publicKey, const std::string &signature) const = 0;
virtual bool parse_uri(const std::string &uri, std::string &address, std::string &payment_id, uint64_t &amount, std::string &tx_description, std::string &recipient_name, std::vector<std::string> &unknown_parameters, std::string &error) = 0;
virtual std::string make_uri(const std::string &address, const std::string &payment_id, uint64_t amount, const std::string &tx_description, const std::string &recipient_name, std::string &error) const = 0;
virtual bool parse_uri(const std::string &uri, std::vector<std::string> &addresses, std::vector<std::uint64_t> &amounts, std::vector<std::string> &recipient_names, std::string &tx_description, std::vector<std::string> &unknown_parameters, std::string &error) = 0;
virtual std::string make_uri(const std::string &address, const std::string &payment_id, uint64_t amount, const std::string &tx_description, const std::string &recipient_name, std::string &error) const = 0;
virtual std::string make_uri(const std::vector<std::string> &addresses, const std::vector<std::uint64_t> &amounts, const std::vector<std::string> &recipient_names, const std::string &tx_description, std::string &error) const = 0;
virtual std::string getDefaultDataDir() const = 0;
/*

View file

@ -14942,56 +14942,155 @@ std::string wallet2::decrypt_with_view_secret_key(const std::string &ciphertext,
return decrypt(ciphertext, get_account().get_keys().m_view_secret_key, authenticated);
}
//----------------------------------------------------------------------------------------------------
std::string wallet2::make_uri(const std::string &address, const std::string &payment_id, uint64_t amount, const std::string &tx_description, const std::string &recipient_name, std::string &error) const
std::string wallet2::custom_convert_to_url_format(const std::string &uri) const
{
cryptonote::address_parse_info info;
if(!get_account_address_from_str(info, nettype(), address))
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)
{
error = std::string("wrong address: ") + address;
if (c == '=')
{
result.append("%3D");
}
else if (c == '?')
{
result.append("%3F");
}
else
{
result.push_back(c);
}
}
return result;
}
//----------------------------------------------------------------------------------------------------
std::string wallet2::make_uri(const std::vector<std::string> &addresses, const std::vector<std::uint64_t> &amounts, const std::vector<std::string> &recipient_names, const std::string &tx_description, std::string &error) const
{
if (addresses.empty())
{
error = "No recipient information like addresses are provided.";
return std::string();
}
// we want only one payment id
if (info.has_payment_id && !payment_id.empty())
if (addresses.size() != amounts.size() || addresses.size() != recipient_names.size())
{
error = "A single payment id is allowed";
error = (boost::format("The counts of addresses (%zu), amounts (%zu), and recipient names (%zu) do not match.") % addresses.size() % amounts.size() % recipient_names.size()).str();
return std::string();
}
if (!payment_id.empty())
std::string addresses_str = "";
std::string amounts_str = "";
bool amounts_used = false;
std::string recipients_str = "";
bool recipients_used = false;
for (size_t i = 0; i < addresses.size(); i++)
{
error = "Standalone payment id deprecated, use integrated address instead";
return std::string();
const std::string &address = addresses[i];
cryptonote::address_parse_info info;
if (!get_account_address_from_str(info, nettype(), address))
{
error = std::string("wrong address: ") + address;
return std::string();
}
if (!addresses_str.empty())
{
addresses_str.append(";");
}
addresses_str += address;
if (!amounts_str.empty())
{
amounts_str.append(";");
}
if (amounts[i] > 0)
{
amounts_used = true;
}
amounts_str += cryptonote::print_money(amounts[i]);
if (!recipients_str.empty())
{
recipients_str.append(";");
}
if (!recipient_names[i].empty())
{
recipients_used = true;
recipients_str += custom_convert_to_url_format(recipient_names[i]);
}
}
std::string uri = "monero:" + address;
std::string uri = "monero:" + addresses_str;
unsigned int n_fields = 0;
if (!payment_id.empty())
{
uri += (n_fields++ ? "&" : "?") + std::string("tx_payment_id=") + payment_id;
}
if (amount > 0)
if (amounts_used)
{
// URI encoded amount is in decimal units, not atomic units
uri += (n_fields++ ? "&" : "?") + std::string("tx_amount=") + cryptonote::print_money(amount);
uri += (n_fields++ ? "&" : "?") + std::string("tx_amount=") + amounts_str;
}
if (!recipient_name.empty())
if (recipients_used)
{
uri += (n_fields++ ? "&" : "?") + std::string("recipient_name=") + epee::net_utils::conver_to_url_format(recipient_name);
uri += (n_fields++ ? "&" : "?") + std::string("recipient_name=") + recipients_str;
}
if (!tx_description.empty())
{
uri += (n_fields++ ? "&" : "?") + std::string("tx_description=") + epee::net_utils::conver_to_url_format(tx_description);
uri += (n_fields++ ? "&" : "?") + std::string("tx_description=") + custom_convert_to_url_format(tx_description);
}
return uri;
}
//----------------------------------------------------------------------------------------------------
bool wallet2::parse_uri(const std::string &uri, std::string &address, std::string &payment_id, uint64_t &amount, std::string &tx_description, std::string &recipient_name, std::vector<std::string> &unknown_parameters, std::string &error)
std::string wallet2::make_uri(const std::string &address, const std::string &payment_id, uint64_t amount, const std::string &tx_description, const std::string &recipient_name, std::string &error) const
{
cryptonote::address_parse_info info;
if (!get_account_address_from_str(info, nettype(), address))
{
error = std::string("wrong address: ") + address;
return std::string();
}
if (info.has_payment_id && !payment_id.empty())
{
error = "A single payment id is allowed";
return std::string();
}
std::string uri = "monero:" + address;
unsigned n_fields = 0;
if (!payment_id.empty())
{
error = "Standalone payment id deprecated, use integrated address instead";
return std::string();
}
if (amount > 0)
{
uri += (n_fields++ ? "&" : "?") + ("tx_amount=" + cryptonote::print_money(amount));
}
if (!recipient_name.empty())
{
uri += (n_fields++ ? "&" : "?") + ("recipient_name=" + custom_convert_to_url_format(recipient_name));
}
if (!tx_description.empty())
{
uri += (n_fields++ ? "&" : "?") + ("tx_description=" + custom_convert_to_url_format(tx_description));
}
return uri;
}
//----------------------------------------------------------------------------------------------------
bool wallet2::parse_uri(const std::string &uri, std::vector<std::string> &addresses, std::vector<std::uint64_t> &amounts, std::vector<std::string> &recipient_names, std::string &tx_description, std::vector<std::string> &unknown_parameters, std::string &error)
{
if (uri.substr(0, 7) != "monero:")
{
@ -15001,24 +15100,37 @@ bool wallet2::parse_uri(const std::string &uri, std::string &address, std::strin
std::string remainder = uri.substr(7);
const char *ptr = strchr(remainder.c_str(), '?');
address = ptr ? remainder.substr(0, ptr-remainder.c_str()) : remainder;
std::string addresses_string = ptr ? remainder.substr(0, ptr-remainder.c_str()) : remainder;
boost::split(addresses, addresses_string, boost::is_any_of(";"));
addresses.erase(std::remove(addresses.begin(), addresses.end(), ""), addresses.end());
cryptonote::address_parse_info info;
if(!get_account_address_from_str(info, nettype(), address))
if (addresses.empty())
{
error = std::string("URI has wrong address: ") + address;
error = "No addresses are present in the given URI.";
return false;
}
if (!strchr(remainder.c_str(), '?'))
for (const std::string &address : addresses)
{
cryptonote::address_parse_info info;
if (!get_account_address_from_str(info, nettype(), address))
{
error = std::string("URI contains improper address: ") + address;
return false;
}
}
amounts.assign(addresses.size(), 0);
recipient_names.assign(addresses.size(), "");
if (ptr == nullptr)
return true;
std::string params(ptr+1);
std::vector<std::string> arguments;
std::string body = remainder.substr(address.size() + 1);
if (body.empty())
return true;
boost::split(arguments, body, boost::is_any_of("&"));
boost::split(arguments, params, boost::is_any_of("&"));
std::set<std::string> have_arg;
for (const auto &arg: arguments)
for (const std::string &arg: arguments)
{
std::vector<std::string> kv;
boost::split(kv, arg, boost::is_any_of("="));
@ -15036,31 +15148,40 @@ bool wallet2::parse_uri(const std::string &uri, std::string &address, std::strin
if (kv[0] == "tx_amount")
{
amount = 0;
if (!cryptonote::parse_amount(amount, kv[1]))
std::vector<std::string> amounts_split;
boost::split(amounts_split, kv[1], boost::is_any_of(";"));
if (amounts_split.size() != addresses.size())
{
error = std::string("URI has invalid amount: ") + kv[1];
error = (boost::format("tx_amount count of %zu is not equal to address count of %zu") % amounts_split.size() % addresses.size()).str();
return false;
}
}
else if (kv[0] == "tx_payment_id")
{
if (info.has_payment_id)
for (size_t i = 0; i < amounts_split.size(); i++)
{
error = "Separate payment id given with an integrated address";
return false;
uint64_t amount;
if (!cryptonote::parse_amount(amount, amounts_split[i]))
{
error = std::string("URI has invalid amount: ") + amounts_split[i];
return false;
}
amounts[i] = amount;
}
crypto::hash hash;
if (!wallet2::parse_long_payment_id(kv[1], hash))
{
error = "Invalid payment id: " + kv[1];
return false;
}
payment_id = kv[1];
}
else if (kv[0] == "recipient_name")
{
recipient_name = epee::net_utils::convert_from_url_format(kv[1]);
std::vector<std::string> names_split;
boost::split(names_split, kv[1], boost::is_any_of(";"));
if (names_split.size() != addresses.size())
{
error = "Recipient name count does not match address count";
return false;
}
for (size_t i = 0; i < names_split.size(); i++)
{
recipient_names[i] = epee::net_utils::convert_from_url_format(names_split[i]);
}
}
else if (kv[0] == "tx_description")
{
@ -15071,6 +15192,66 @@ bool wallet2::parse_uri(const std::string &uri, std::string &address, std::strin
unknown_parameters.push_back(arg);
}
}
if (!amounts.empty() && amounts.size() != addresses.size())
{
error = "Amount count does not match address count.";
return false;
}
return true;
}
//----------------------------------------------------------------------------------------------------
bool wallet2::parse_uri(const std::string& uri, std::string& address, std::string& payment_id, uint64_t& amount, std::string& tx_description, std::string& recipient_name, std::vector<std::string>& unknown_parameters, std::string& error)
{
std::vector<std::string> addresses;
std::vector<uint64_t> amounts;
std::vector<std::string> recipient_names;
if (!parse_uri(uri, addresses, amounts, recipient_names, tx_description, unknown_parameters, error))
{
return false;
}
std::vector<std::string> new_unknown_parameters;
for (const auto& unknown_parameter : unknown_parameters) {
if (unknown_parameter.find("tx_payment_id=") != 0)
{
new_unknown_parameters.push_back(unknown_parameter);
continue;
}
payment_id = unknown_parameter.substr(strlen("tx_payment_id="));
cryptonote::address_parse_info info;
if (!get_account_address_from_str(info, nettype(), addresses[0]))
{
error = "URI contains improper address: " + addresses[0];
return false;
}
if (info.has_payment_id)
{
error = "Separate payment id given with integrated address";
return false;
}
crypto::hash hash;
if (!wallet2::parse_long_payment_id(payment_id, hash))
{
error = "Invalid payment id: " + payment_id;
return false;
}
}
unknown_parameters = std::move(new_unknown_parameters);
if (addresses.size() != 1) {
error = "Multiple recipient addresses parsed out of transaction intended for one recipient";
return false;
}
address = addresses[0];
amount = amounts.empty() ? 0 : amounts[0];
recipient_name = recipient_names.empty() ? "" : recipient_names[0];
return true;
}
//----------------------------------------------------------------------------------------------------

View file

@ -1641,8 +1641,11 @@ private:
template<typename T=std::string> T decrypt(const std::string &ciphertext, const crypto::secret_key &skey, bool authenticated = true) const;
std::string decrypt_with_view_secret_key(const std::string &ciphertext, bool authenticated = true) const;
std::string custom_convert_to_url_format(const std::string &uri) const;
std::string make_uri(const std::vector<std::string> &addresses, const std::vector<std::uint64_t> &amounts, const std::vector<std::string> &recipient_names, const std::string &tx_description, std::string &error) const;
std::string make_uri(const std::string &address, const std::string &payment_id, uint64_t amount, const std::string &tx_description, const std::string &recipient_name, std::string &error) const;
bool parse_uri(const std::string &uri, std::string &address, std::string &payment_id, uint64_t &amount, std::string &tx_description, std::string &recipient_name, std::vector<std::string> &unknown_parameters, std::string &error);
bool parse_uri(const std::string &uri, std::vector<std::string> &addresses, std::vector<std::uint64_t> &amounts, std::vector<std::string> &recipient_names, std::string &tx_description, std::vector<std::string> &unknown_parameters, std::string &error);
uint64_t get_blockchain_height_by_date(uint16_t year, uint8_t month, uint8_t day); // 1<=month<=12, 1<=day<=31

View file

@ -3142,6 +3142,22 @@ 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)
{
if (!m_wallet) return not_open(er);
std::string error;
std::string uri = m_wallet->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);
@ -3155,6 +3171,26 @@ 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)
{
if (!m_wallet) return not_open(er);
std::string error;
std::vector<std::string> addresses;
std::vector<std::uint64_t> amounts;
std::vector<std::string> recipient_names;
if (!m_wallet->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);

View file

@ -126,7 +126,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)
@ -222,7 +224,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);

View file

@ -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 28
#define WALLET_RPC_VERSION_MINOR 29
#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
@ -1885,6 +1885,74 @@ namespace wallet_rpc
typedef epee::misc_utils::struct_init<response_t> response;
};
struct uri_spec_v2
{
std::vector<std::string> addresses;
std::vector<uint64_t> amounts;
std::vector<std::string> 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<std::string> addresses;
std::vector<uint64_t> amounts;
std::vector<std::string> 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_t> 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_t> 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_t> request;
struct response_t
{
uri_spec_v2 uri;
std::vector<std::string> unknown_parameters;
BEGIN_KV_SERIALIZE_MAP()
KV_SERIALIZE(uri)
KV_SERIALIZE(unknown_parameters)
END_KV_SERIALIZE_MAP()
};
typedef epee::misc_utils::struct_init<response_t> response;
};
struct COMMAND_RPC_ADD_ADDRESS_BOOK_ENTRY
{
struct request_t

View file

@ -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 @@ class URITest():
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,104 @@ class URITest():
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'
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_unknown_parameters(wallet, addr1)
def test_multi_uri_two_payments(self, wallet, addr1, addr2, utf8string):
# build multi-recipient URI with two payments.
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')
# expect URI like:
# monero:addr1;addr2?tx_amount=0.5;0.2&recipient_name=<url_encoded_name1>;<url_encoded_name2>&tx_description=Multi%20URI%20test%20with%20two%20payments
parsed = wallet.parse_uri_v2(res.uri)
# verify that both payments are present.
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]
# check tx_description at the top level.
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):
# manually build a URI with mismatched amounts (remove Bob's amount).
# simulate this by concatenating amounts incorrectly.
# (this step assumes you have control over the output URI; in practice, the server would reject it. For testing, we assume the RPC returns an error.)
uri_bad = 'monero:' + addr1 + ';' + addr2 + '?tx_amount=0.5&recipient_name=Alice;Bob'
ok = False
try:
wallet.parse_uri_v2(uri_bad)
except Exception:
ok = True
assert ok, "Expected rejection for mismatched payment counts"
def test_multi_uri_trailing_delimiter(self, wallet, addr1, addr2):
# case: trailing semicolon in addresses or parameters should be handled gracefully
uri_trailing = 'monero:' + addr1 + ';' + addr2 + ';' + '?tx_amount=0.5;0.2&recipient_name=Alice;Bob'
# depending on the implementation, a trailing empty value might be dropped.
parsed = wallet.parse_uri_v2(uri_trailing)
assert len(parsed.uri.addresses) == 2, "Trailing delimiter should not add empty payment"
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)
# check that the decoded values match the original.
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_unknown_parameters(self, wallet, addr1):
# build a well-formed multi-recipient URI and tack on unknown parameters.
uri_with_unknown = 'monero:' + addr1 + '?tx_amount=239.39014&foo=bar&baz=quux'
parsed = wallet.parse_uri_v2(uri_with_unknown)
assert parsed.uri.addresses[0] == addr1
assert parsed.uri.amounts[0] == 239390140000000
# unknown parameters should be collected in the unknown_parameters list.
assert parsed.unknown_parameters == ['foo=bar', 'baz=quux'], "Unknown parameters mismatch"
if __name__ == '__main__':
URITest().run_test()

View file

@ -41,6 +41,17 @@
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<std::string> addresses; \
std::vector<uint64_t> amounts; \
std::vector<std::string> recipient_names; \
std::string description, error; \
std::vector<std::string> unknown_parameters; \
tools::wallet2 w(cryptonote::TESTNET); \
bool ret = w.parse_uri(uri, addresses, amounts, recipient_names, \
description, unknown_parameters, error); \
ASSERT_EQ(ret, expected);
TEST(uri, empty_string)
{
PARSE_URI("", false);
@ -213,3 +224,84 @@ TEST(uri, url_encoded_once)
ASSERT_EQ(description, "foo 20");
}
TEST(uri, multiple_addresses_no_params)
{
PARSE_MULTI_URI("monero:" TEST_ADDRESS ";" 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:" TEST_ADDRESS ";" TEST_ADDRESS "?tx_amount=0.5;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:" TEST_ADDRESS ";" TEST_ADDRESS "?recipient_name=Alice;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:" TEST_ADDRESS ";" TEST_ADDRESS "?tx_amount=0.5", false);
}
TEST(uri, multiple_addresses_with_mismatched_recipient_names)
{
PARSE_MULTI_URI("monero:" TEST_ADDRESS ";" TEST_ADDRESS "?recipient_name=Alice", false);
}
TEST(uri, multiple_addresses_with_partial_params)
{
PARSE_MULTI_URI("monero:" TEST_ADDRESS ";" TEST_ADDRESS "?tx_amount=0.5;0&recipient_name=Alice;", 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:" TEST_ADDRESS ";" 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:" TEST_ADDRESS ";" 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:" TEST_ADDRESS ";" TEST_ADDRESS "?tx_amount=0.5;0.2&recipient_name=Alice;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");
}

View file

@ -1002,6 +1002,31 @@ class Wallet(object):
}
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',