diff --git a/src/simplewallet/simplewallet.cpp b/src/simplewallet/simplewallet.cpp index 28048590ba1..ac1349b83f6 100644 --- a/src/simplewallet/simplewallet.cpp +++ b/src/simplewallet/simplewallet.cpp @@ -6532,25 +6532,70 @@ 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::string payment_id_uri, tx_description, 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::vector uri_data; + bool has_uri = m_wallet->parse_uri(local_args[i], uri_data, payment_id_uri, 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) + + for (size_t j = 0; j < uri_data.size(); j++) { - 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(), uri_data[j].address, oa_prompter); + if (payment_id_uri.size() == 16) { - fail_msg_writer() << tr("failed to parse short payment ID from URI"); + if (!tools::wallet2::parse_short_payment_id(payment_id_uri, info.payment_id)) + { + fail_msg_writer() << tr("failed to parse short payment ID from URI"); + return false; + } + info.has_payment_id = true; + } + de.amount = uri_data[j].amount; + de.original = uri_data[j].address; + if (!r) + { + fail_msg_writer() << tr("failed to parse address"); return false; } - info.has_payment_id = true; + de.addr = info.address; + de.is_subaddress = info.is_subaddress; + de.is_integrated = info.has_payment_id; + + if (info.has_payment_id || !payment_id_uri.empty()) + { + 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) + { + fail_msg_writer() << tr("failed to set up payment id, though it was decoded correctly"); + return false; + } + payment_id_seen = true; + } + dsts.push_back(de); } - de.amount = amount; - de.original = local_args[i]; - ++i; + i++; + break; } else if (i + 1 < local_args.size()) { diff --git a/src/wallet/api/wallet.cpp b/src/wallet/api/wallet.cpp index 6c50002dd1c..e116cf67130 100644 --- a/src/wallet/api/wallet.cpp +++ b/src/wallet/api/wallet.cpp @@ -2548,7 +2548,12 @@ bool WalletImpl::checkBackgroundSync(const std::string &message) const return false; } -bool WalletImpl::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 &unknown_parameters, std::string &error) +bool WalletImpl::parse_uri(const std::string &uri, std::vector &data, std::string &payment_id, std::string &tx_description, std::vector &unknown_parameters, std::string &error) +{ + return m_wallet->parse_uri(uri, data, payment_id, tx_description, unknown_parameters, error); +} + +bool WalletImpl::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& unknown_parameters, std::string& error) { return m_wallet->parse_uri(uri, address, payment_id, amount, tx_description, recipient_name, unknown_parameters, error); } @@ -2558,6 +2563,11 @@ std::string WalletImpl::make_uri(const std::string &address, const std::string & return m_wallet->make_uri(address, payment_id, amount, tx_description, recipient_name, error); } +std::string WalletImpl::make_uri(std::vector data, const std::string &payment_id, const std::string &tx_description, std::string &error) const +{ + return m_wallet->make_uri(data, payment_id, tx_description, error); +} + std::string WalletImpl::getDefaultDataDir() const { return tools::get_default_data_dir(); diff --git a/src/wallet/api/wallet.h b/src/wallet/api/wallet.h index d48d7f130e3..88dc025fa13 100644 --- a/src/wallet/api/wallet.h +++ b/src/wallet/api/wallet.h @@ -214,7 +214,10 @@ class WalletImpl : public Wallet 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 &unknown_parameters, std::string &error) override; + virtual bool parse_uri(const std::string &uri, std::vector &data, std::string &payment_id, std::string &tx_description, std::vector &unknown_parameters, std::string &error); 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; + virtual std::string make_uri(std::vector data, const std::string &payment_id, const std::string &tx_description, std::string &error) const; + virtual std::string getDefaultDataDir() const override; virtual bool blackballOutputs(const std::vector &outputs, bool add) override; virtual bool blackballOutput(const std::string &amount, const std::string &offset) override; diff --git a/src/wallet/api/wallet2_api.h b/src/wallet/api/wallet2_api.h index 2bedcc7d280..46dcf1f9b75 100644 --- a/src/wallet/api/wallet2_api.h +++ b/src/wallet/api/wallet2_api.h @@ -1066,8 +1066,10 @@ 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 &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 getDefaultDataDir() const = 0; /* diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 04be12c137e..f267c37e75a 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -6236,6 +6236,9 @@ std::string wallet2::make_background_keys_file_name(const std::string &wallet_fi //---------------------------------------------------------------------------------------------------- bool wallet2::parse_long_payment_id(const std::string& payment_id_str, crypto::hash& payment_id) { + if (payment_id_str.size() != 64) + return false; + cryptonote::blobdata payment_id_data; if(!epee::string_tools::parse_hexstr_to_binbuff(payment_id_str, payment_id_data)) return false; @@ -14942,56 +14945,133 @@ 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_conver_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; - return std::string(); + if (c == '=') + { + result.append("%3D"); + } + else if (c == '?') + { + result.append("%3F"); + } + + else + { + result.push_back(c); + } } - // we want only one payment id - if (info.has_payment_id && !payment_id.empty()) + return result; +} +//---------------------------------------------------------------------------------------------------- +std::string wallet2::make_uri(std::vector data, const std::string &payment_id, const std::string &tx_description, std::string &error) const +{ + if (data.empty()) { - error = "A single payment id is allowed"; + error = "No recipient data provided."; return std::string(); } - - if (!payment_id.empty()) + std::string addresses = ""; + std::string amounts = ""; + bool amounts_used = false; + std::string recipients = ""; + bool recipients_used = false; + for (const uri_data& entry : data) { - error = "Standalone payment id deprecated, use integrated address instead"; - return std::string(); + cryptonote::address_parse_info info; + if(!get_account_address_from_str(info, nettype(), entry.address)) + { + error = std::string("wrong address: ") + entry.address; + return std::string(); + } + if (info.has_payment_id && !payment_id.empty()) + { + error = "Separate payment id given with an integrated address"; + return std::string(); + } + if (!addresses.empty()) + { + addresses += ";"; + } + addresses += entry.address; + + if (!amounts.empty()) + { + amounts += ";"; + } + if (entry.amount > 0) + { + amounts_used = true; + } + amounts += cryptonote::print_money(entry.amount); + + if (!recipients.empty()) + { + recipients += ";"; + } + if (!entry.recipient_name.empty()) + { + recipients_used = true; + recipients += custom_conver_to_url_format(entry.recipient_name); + } } - std::string uri = "monero:" + address; + std::string uri = "monero:" + addresses; unsigned int n_fields = 0; - if (!payment_id.empty()) + if (amounts_used) { - uri += (n_fields++ ? "&" : "?") + std::string("tx_payment_id=") + payment_id; + // URI encoded amount is in decimal units, not atomic units + uri += (n_fields++ ? "&" : "?") + std::string("tx_amount=") + amounts; } - if (amount > 0) + if (recipients_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("recipient_name=") + recipients; } - if (!recipient_name.empty()) + if (!tx_description.empty()) { - uri += (n_fields++ ? "&" : "?") + std::string("recipient_name=") + epee::net_utils::conver_to_url_format(recipient_name); + uri += (n_fields++ ? "&" : "?") + std::string("tx_description=") + custom_conver_to_url_format(tx_description); } - if (!tx_description.empty()) + if (!payment_id.empty()) { - uri += (n_fields++ ? "&" : "?") + std::string("tx_description=") + epee::net_utils::conver_to_url_format(tx_description); + crypto::hash pid32; + if (!wallet2::parse_long_payment_id(payment_id, pid32)) + { + error = "Invalid payment id"; + return std::string(); + } + uri += (n_fields++ ? "&" : "?") + std::string("tx_payment_id=") + payment_id; } 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 &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 +{ + tools::wallet2::uri_data entry; + entry.address = address; + entry.amount = amount; + entry.recipient_name = recipient_name; + + std::vector data; + data.push_back(entry); + + return make_uri(data, payment_id, tx_description, error); +} +//---------------------------------------------------------------------------------------------------- +bool wallet2::parse_uri(const std::string &uri, std::vector &data, std::string &payment_id, std::string &tx_description, std::vector &unknown_parameters, std::string &error) { if (uri.substr(0, 7) != "monero:") { @@ -15001,24 +15081,40 @@ 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; + std::vector addresses, recipient_names; + std::vector amounts; + 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 specified in 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 constains improper address: ") + address; + return false; + } + uri_data recipient_data; + recipient_data.address = address; + recipient_data.amount = 0; + data.push_back(recipient_data); + } + + if (ptr == NULL) return true; + std::string params(ptr+1); std::vector 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 have_arg; - for (const auto &arg: arguments) + for (const std::string &arg : arguments) { std::vector kv; boost::split(kv, arg, boost::is_any_of("=")); @@ -15036,20 +15132,32 @@ 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 amounts_split; + boost::split(amounts_split, kv[1], boost::is_any_of(";")); + size_t expected_size = addresses.size(); + + // enforce parameter consistency + if (amounts_split.size() != expected_size) { - error = std::string("URI has invalid amount: ") + kv[1]; + error = "Incorrect tx_amount count"; return false; } + + + for (size_t i = 0; i < amounts_split.size(); i++) + { + uint64_t amount; + if (!cryptonote::parse_amount(amount, amounts_split[i])) + { + error = std::string("URI has invalid amount: ") + amounts_split[i]; + return false; + } + amounts.push_back(amount); + } } else if (kv[0] == "tx_payment_id") { - if (info.has_payment_id) - { - error = "Separate payment id given with an integrated address"; - return false; - } + // standalone payment ids are deprecated. use integrated address crypto::hash hash; if (!wallet2::parse_long_payment_id(kv[1], hash)) { @@ -15057,10 +15165,30 @@ bool wallet2::parse_uri(const std::string &uri, std::string &address, std::strin return false; } payment_id = kv[1]; + + if (payment_id.length() != 16 && payment_id.length() != 64) + { + error = "Invalid payment ID length"; + return false; + } } else if (kv[0] == "recipient_name") { - recipient_name = epee::net_utils::convert_from_url_format(kv[1]); + std::vector names_split; + boost::split(names_split, kv[1], boost::is_any_of(";")); + size_t expected_size = addresses.size(); + + // enforce parameter consistency + if (names_split.size() != expected_size) + { + error = "Incorrect recipient_name count"; + return false; + } + + for (size_t i = 0; i < names_split.size(); i++) + { + recipient_names.push_back(epee::net_utils::convert_from_url_format(names_split[i])); + } } else if (kv[0] == "tx_description") { @@ -15071,6 +15199,48 @@ bool wallet2::parse_uri(const std::string &uri, std::string &address, std::strin unknown_parameters.push_back(arg); } } + + if (!recipient_names.empty() && recipient_names.size() != addresses.size()) + { + error = "Incorrect recipient name count. Recipient name count must match address count"; + return false; + } + if (!amounts.empty() && amounts.size() != addresses.size()) + { + error = "Incorrect amount count. Amount count must match address count. Zero may be use as a filler"; + return false; + } + for(size_t i = 0; i < data.size(); i++) + { + if (!amounts.empty()) + { + data[i].amount = amounts[i]; + } + if (!recipient_names.empty()) + { + data[i].recipient_name = recipient_names[i]; + } + } + 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& unknown_parameters, std::string& error) +{ + std::vector data; + if (!parse_uri(uri, data, payment_id, tx_description, unknown_parameters, error)) + { + error = "Failed to parse uri"; + return false; + } + if (data.size() > 1) + { + error = "Multi-recipient URIs currently unsupported"; + return false; + } + address = data[0].address; + amount = data[0].amount; + recipient_name = data[0].recipient_name; + return true; } //---------------------------------------------------------------------------------------------------- diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index 59f279cee0e..0f0d02a8b2e 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -871,6 +871,13 @@ namespace tools bool empty() const { return tx_extra_fields.empty() && primary.empty() && additional.empty(); } }; + struct uri_data + { + std::string address; + uint64_t amount = 0; + std::string recipient_name; + }; + struct detached_blockchain_data { hashchain detached_blockchain; @@ -1640,9 +1647,12 @@ namespace tools std::string encrypt_with_view_secret_key(const std::string &plaintext, bool authenticated = true) const; template 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_conver_to_url_format(const std::string &uri) const; + std::string make_uri(std::vector data, const std::string &payment_id, 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 &unknown_parameters, std::string &error); + bool parse_uri(const std::string &uri, std::vector &data, std::string &payment_id, std::string &tx_description, std::vector &unknown_parameters, std::string &error); + bool parse_uri(const std::string& uri, std::string& address, std::string& payment_id, uint64_t& amount, std::string& description, std::string& recipient_name, std::vector& 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 diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp index 33437606228..83f716ba574 100644 --- a/src/wallet/wallet_rpc_server.cpp +++ b/src/wallet/wallet_rpc_server.cpp @@ -3109,7 +3109,17 @@ namespace tools { if (!m_wallet) return not_open(er); std::string error; - std::string uri = m_wallet->make_uri(req.address, req.payment_id, req.amount, req.tx_description, req.recipient_name, error); + std::vector data; + for (const tools::wallet_rpc::uri_payment &entry : req.payments) + { + tools::wallet2::uri_data entry_data; + entry_data.address = entry.address; + entry_data.amount = entry.amount; + entry_data.recipient_name = entry.recipient_name; + data.push_back(entry_data); + } + std::string uri = m_wallet->make_uri(data, req.payment_id, req.tx_description, error); + if (uri.empty()) { er.code = WALLET_RPC_ERROR_CODE_WRONG_URI; @@ -3125,12 +3135,18 @@ namespace tools { if (!m_wallet) return not_open(er); std::string error; - if (!m_wallet->parse_uri(req.uri, res.uri.address, res.uri.payment_id, res.uri.amount, res.uri.tx_description, res.uri.recipient_name, res.unknown_parameters, error)) + std::vector uri_data; + if (!m_wallet->parse_uri(req.uri, uri_data, res.uri.payment_id, res.uri.tx_description, res.unknown_parameters, error)) { er.code = WALLET_RPC_ERROR_CODE_WRONG_URI; er.message = "Error parsing URI: " + error; return false; } + for (const tools::wallet2::uri_data &entry : uri_data) + { + tools::wallet_rpc::uri_payment entry_spec = {entry.address, entry.amount, entry.recipient_name}; + res.uri.payments.push_back(entry_spec); + } return true; } //------------------------------------------------------------------------------------------------------------------------------ diff --git a/src/wallet/wallet_rpc_server_commands_defs.h b/src/wallet/wallet_rpc_server_commands_defs.h index ab7898299e1..bdb72b93b04 100644 --- a/src/wallet/wallet_rpc_server_commands_defs.h +++ b/src/wallet/wallet_rpc_server_commands_defs.h @@ -1825,27 +1825,44 @@ namespace wallet_rpc typedef epee::misc_utils::struct_init response; }; - struct uri_spec + struct uri_payment { std::string address; - std::string payment_id; uint64_t amount; - std::string tx_description; std::string recipient_name; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(address) - KV_SERIALIZE(payment_id) KV_SERIALIZE(amount) - KV_SERIALIZE(tx_description) KV_SERIALIZE(recipient_name) END_KV_SERIALIZE_MAP() }; + struct uri_spec + { + std::vector payments; + std::string tx_description; + std::string payment_id; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(payments); + KV_SERIALIZE(tx_description); + KV_SERIALIZE(payment_id); + END_KV_SERIALIZE_MAP() + }; + struct COMMAND_RPC_MAKE_URI { - struct request_t: public uri_spec + struct request_t { + std::vector payments; + std::string tx_description; + std::string payment_id; + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(payments); + KV_SERIALIZE(tx_description); + KV_SERIALIZE(payment_id); + END_KV_SERIALIZE_MAP() }; typedef epee::misc_utils::struct_init request; diff --git a/tests/functional_tests/uri.py b/tests/functional_tests/uri.py index fb981b3fdc7..f4b51b3e9b6 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_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'あまやかす'] @@ -83,20 +84,21 @@ def test_monero_uri(self): res = wallet.make_uri(address = address) assert res.uri == 'monero:' + address res = wallet.parse_uri(res.uri) - assert res.uri.address == address + assert res.uri.payments[0].address == address + assert res.uri.payments[0].amount == 0 + assert res.uri.payments[0].recipient_name == '' assert res.uri.payment_id == '' - assert res.uri.amount == 0 assert res.uri.tx_description == '' - assert res.uri.recipient_name == '' + assert not 'unknown_parameters' in res or len(res.unknown_parameters) == 0 res = wallet.make_uri(address = address, amount = 11000000000) assert res.uri == 'monero:' + address + '?tx_amount=0.011' or res.uri == 'monero:' + address + '?tx_amount=0.011000000000' res = wallet.parse_uri(res.uri) - assert res.uri.address == address + assert res.uri.payments[0].address == address + assert res.uri.payments[0].amount == 11000000000 + assert res.uri.payments[0].recipient_name == '' assert res.uri.payment_id == '' - assert res.uri.amount == 11000000000 assert res.uri.tx_description == '' - assert res.uri.recipient_name == '' assert not 'unknown_parameters' in res or len(res.unknown_parameters) == 0 address = '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm' @@ -104,66 +106,73 @@ def test_monero_uri(self): res = wallet.make_uri(address = address, tx_description = utf8string[0]) assert res.uri == 'monero:' + address + '?tx_description=' + quoted_utf8string[0] res = wallet.parse_uri(res.uri) - assert res.uri.address == address + assert res.uri.payments[0].address == address + assert res.uri.payments[0].amount == 0 + assert res.uri.payments[0].recipient_name == '' assert res.uri.payment_id == '' - assert res.uri.amount == 0 assert res.uri.tx_description == utf8string[0] - assert res.uri.recipient_name == '' assert not 'unknown_parameters' in res or len(res.unknown_parameters) == 0 res = wallet.make_uri(address = address, recipient_name = utf8string[0]) assert res.uri == 'monero:' + address + '?recipient_name=' + quoted_utf8string[0] res = wallet.parse_uri(res.uri) - assert res.uri.address == address + assert res.uri.payments[0].address == address + assert res.uri.payments[0].amount == 0 + assert res.uri.payments[0].recipient_name == utf8string[0] assert res.uri.payment_id == '' - assert res.uri.amount == 0 assert res.uri.tx_description == '' - assert res.uri.recipient_name == utf8string[0] assert not 'unknown_parameters' in res or len(res.unknown_parameters) == 0 res = wallet.make_uri(address = address, recipient_name = utf8string[0], tx_description = utf8string[1]) assert res.uri == 'monero:' + address + '?recipient_name=' + quoted_utf8string[0] + '&tx_description=' + quoted_utf8string[1] res = wallet.parse_uri(res.uri) - assert res.uri.address == address + assert res.uri.payments[0].address == address + assert res.uri.payments[0].amount == 0 + assert res.uri.payments[0].recipient_name == utf8string[0] assert res.uri.payment_id == '' - assert res.uri.amount == 0 assert res.uri.tx_description == utf8string[1] - assert res.uri.recipient_name == utf8string[0] assert not 'unknown_parameters' in res or len(res.unknown_parameters) == 0 res = wallet.make_uri(address = address, recipient_name = utf8string[0], tx_description = utf8string[1], amount = 1000000000000) assert res.uri == 'monero:' + address + '?tx_amount=1.000000000000&recipient_name=' + quoted_utf8string[0] + '&tx_description=' + quoted_utf8string[1] res = wallet.parse_uri(res.uri) - assert res.uri.address == address + assert res.uri.payments[0].address == address + assert res.uri.payments[0].amount == 1000000000000 + assert res.uri.payments[0].recipient_name == utf8string[0] assert res.uri.payment_id == '' - assert res.uri.amount == 1000000000000 assert res.uri.tx_description == utf8string[1] - assert res.uri.recipient_name == utf8string[0] assert not 'unknown_parameters' in res or len(res.unknown_parameters) == 0 - # external payment ids are not supported anymore - ok = False - try: res = wallet.make_uri(address = address, recipient_name = utf8string[0], tx_description = utf8string[1], amount = 1000000000000, payment_id = '1' * 64) - except: ok = True - assert ok + # standalone payment ids are supported but not recommended + res = wallet.make_uri(address = address, recipient_name = utf8string[0], tx_description = utf8string[1], amount = 1000000000000, payment_id = '1' * 64) + assert res.uri == 'monero:' + address + '?tx_amount=1.000000000000&recipient_name=' + quoted_utf8string[0] + '&tx_description=' + quoted_utf8string[1] + '&tx_payment_id=' + '1' * 64 + + # in case of standalone payment id removal + # ok = False + # try: + # res = wallet.make_uri(address = address, recipient_name = utf8string[0], tx_description = utf8string[1], amount = 1000000000000, payment_id = '1' * 64) + # except: + # ok = True + # assert ok # spaces must be encoded as %20 res = wallet.make_uri(address = address, tx_description = ' ' + utf8string[1] + ' ' + utf8string[0] + ' ', amount = 1000000000000) assert res.uri == 'monero:' + address + '?tx_amount=1.000000000000&tx_description=%20' + quoted_utf8string[1] + '%20' + quoted_utf8string[0] + '%20' res = wallet.parse_uri(res.uri) - assert res.uri.address == address + assert res.uri.payments[0].address == address + assert res.uri.payments[0].amount == 1000000000000 + assert res.uri.payments[0].recipient_name == '' assert res.uri.payment_id == '' - assert res.uri.amount == 1000000000000 assert res.uri.tx_description == ' ' + utf8string[1] + ' ' + utf8string[0] + ' ' - assert res.uri.recipient_name == '' assert not 'unknown_parameters' in res or len(res.unknown_parameters) == 0 # the example from the docs res = wallet.parse_uri('monero:46BeWrHpwXmHDpDEUmZBWZfoQpdc6HaERCNmx1pEYL2rAcuwufPN9rXHHtyUA4QVy66qeFQkn6sfK8aHYjA3jk3o1Bv16em?tx_amount=239.39014&tx_description=donation') - assert res.uri.address == '46BeWrHpwXmHDpDEUmZBWZfoQpdc6HaERCNmx1pEYL2rAcuwufPN9rXHHtyUA4QVy66qeFQkn6sfK8aHYjA3jk3o1Bv16em' - assert res.uri.amount == 239390140000000 + assert res.uri.payments[0].address == '46BeWrHpwXmHDpDEUmZBWZfoQpdc6HaERCNmx1pEYL2rAcuwufPN9rXHHtyUA4QVy66qeFQkn6sfK8aHYjA3jk3o1Bv16em' + assert res.uri.payments[0].amount == 239390140000000 + assert res.uri.payments[0].recipient_name == '' assert res.uri.tx_description == 'donation' - assert res.uri.recipient_name == '' + assert res.uri.payment_id == '' assert not 'unknown_parameters' in res or len(res.unknown_parameters) == 0 @@ -194,8 +203,9 @@ def test_monero_uri(self): 'monero:42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm&tx_amount=10=&', 'monero:42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm&tx_amount=10=&foo=bar', 'monero:42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm?tx_amount=10&tx_amount=20', - 'monero:42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm?tx_payment_id=1111111111111111', - 'monero:4BxSHvcgTwu25WooY4BVmgdcKwZu5EksVZSZkDd6ooxSVVqQ4ubxXkhLF6hEqtw96i9cf3cVfLw8UWe95bdDKfRQeYtPwLm1Jiw7AKt2LY?tx_payment_id=' + '1' * 64, + # Standalone payment ids are still supported, in case of complete functionality removal: uncomment this. + # 'monero:42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm?tx_payment_id=1111111111111111', + # 'monero:4BxSHvcgTwu25WooY4BVmgdcKwZu5EksVZSZkDd6ooxSVVqQ4ubxXkhLF6hEqtw96i9cf3cVfLw8UWe95bdDKfRQeYtPwLm1Jiw7AKt2LY?tx_payment_id=' + '1' * 64, 'monero:9ujeXrjzf7bfeK3KZdCqnYaMwZVFuXemPU8Ubw335rj2FN1CdMiWNyFV3ksEfMFvRp9L9qum5UxkP5rN9aLcPxbH1au4WAB', 'monero:5K8mwfjumVseCcQEjNbf59Um6R9NfVUNkHTLhhPCmNvgDLVS88YW5tScnm83rw9mfgYtchtDDTW5jEfMhygi27j1QYphX38hg6m4VMtN29', 'monero:7A1Hr63MfgUa8pkWxueD5xBqhQczkusYiCMYMnJGcGmuQxa7aDBxN1G7iCuLCNB3VPeb2TW7U9FdxB27xKkWKfJ8VhUZthF', @@ -207,23 +217,120 @@ def test_monero_uri(self): # unknown parameters but otherwise valid res = wallet.parse_uri('monero:' + address + '?tx_amount=239.39014&foo=bar') - assert res.uri.address == address - assert res.uri.amount == 239390140000000 + assert res.uri.payments[0].address == address + assert res.uri.payments[0].amount == 239390140000000 assert res.unknown_parameters == ['foo=bar'], res res = wallet.parse_uri('monero:' + address + '?tx_amount=239.39014&foo=bar&baz=quux') - assert res.uri.address == address - assert res.uri.amount == 239390140000000 + assert res.uri.payments[0].address == address + assert res.uri.payments[0].amount == 239390140000000 assert res.unknown_parameters == ['foo=bar', 'baz=quux'], res res = wallet.parse_uri('monero:' + address + '?tx_amount=239.39014&%20=%20') - assert res.uri.address == address - assert res.uri.amount == 239390140000000 + assert res.uri.payments[0].address == address + assert res.uri.payments[0].amount == 239390140000000 assert res.unknown_parameters == ['%20=%20'], res res = wallet.parse_uri('monero:' + address + '?tx_amount=239.39014&unknown=' + quoted_utf8string[0]) - assert res.uri.address == address - assert res.uri.amount == 239390140000000 + assert res.uri.payments[0].address == address + assert res.uri.payments[0].amount == 239390140000000 assert res.unknown_parameters == [u'unknown=' + quoted_utf8string[0]], res + + + def test_multi_uri(self): + print('Testing multi-recipient monero: URI') + wallet = Wallet() + addr1 = '42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm' + addr2 = '4BxSHvcgTwu25WooY4BVmgdcKwZu5EksVZSZkDd6ooxSVVqQ4ubxXkhLF6hEqtw96i9cf3cVfLw8UWe95bdDKfRQeYtPwLm1Jiw7AKt2LY' + addr3 = '8AsN91rznfkBGTY8psSNkJBg9SZgxxGGRUhGwRptBhgr5XSQ1XzmA9m8QAnoxydecSh5aLJXdrgXwTDMMZ1AuXsN1EX5Mtm' + utf8string = [u'えんしゅう', u'あまやかす'] + quoted_utf8string = [urllib_quote(x.encode('utf8')) for x in utf8string] + + # build multi-recipient URI with two payments. + payments = [ + {'address': addr1, 'amount': 500000000000, 'recipient_name': utf8string[0]}, + {'address': addr2, 'amount': 200000000000, 'recipient_name': utf8string[1]} + ] + res = wallet.make_uri(payments=payments, payment_id='', tx_description='multi test') + # expect URI like: + # monero:addr1;addr2?tx_amount=0.5;0.2&recipient_name=;&tx_description=multi%20test + parsed = wallet.parse_uri(res.uri) + # verify that both payments are present. + assert len(parsed.uri.payments) == 2, "Expected 2 payments in multi-recipient URI" + assert parsed.uri.payments[0].address == addr1 + assert parsed.uri.payments[0].amount == 500000000000 + assert parsed.uri.payments[0].recipient_name == utf8string[0] + assert parsed.uri.payments[1].address == addr2 + assert parsed.uri.payments[1].amount == 200000000000 + assert parsed.uri.payments[1].recipient_name == utf8string[1] + # check tx_description at the top level. + assert parsed.uri.tx_description == 'multi test' + assert parsed.uri.payment_id == '' + + # build multi-recipient URI with three payments. + payments = [ + {'address': addr1, 'amount': 1000000000000, 'recipient_name': utf8string[0]}, + {'address': addr2, 'amount': 500000000000, 'recipient_name': utf8string[1]}, + {'address': addr3, 'amount': 250000000000, 'recipient_name': ''} + ] + res = wallet.make_uri(payments=payments, payment_id='', tx_description='three pay') + parsed = wallet.parse_uri(res.uri) + assert len(parsed.uri.payments) == 3, "Expected 3 payments in multi-recipient URI" + assert parsed.uri.payments[0].address == addr1 + assert parsed.uri.payments[0].amount == 1000000000000 + assert parsed.uri.payments[0].recipient_name == utf8string[0] + assert parsed.uri.payments[1].address == addr2 + assert parsed.uri.payments[1].amount == 500000000000 + assert parsed.uri.payments[1].recipient_name == utf8string[1] + assert parsed.uri.payments[2].address == addr3 + assert parsed.uri.payments[2].amount == 250000000000 + assert parsed.uri.payments[2].recipient_name == '' + assert parsed.uri.tx_description == 'three pay' + + payments = [ + {'address': addr1, 'amount': 500000000000, 'recipient_name': 'Alice'}, + {'address': addr2, 'amount': 0, 'recipient_name': 'Bob'} + ] + # manually build a URI with mismatched amounts (remove Bob's amount). + # we 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(uri_bad) + except Exception: + ok = True + assert ok, "Expected rejection for mismatched payment counts" + # 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(uri_trailing) + assert len(parsed.uri.payments) == 2, "Trailing delimiter should not add empty payment" + # case: special characters in recipient names and descriptions + special_name = "A&B=Test?" + special_desc = "Desc with spaces & symbols!" + payments = [ + {'address': addr1, 'amount': 750000000000, 'recipient_name': special_name}, + {'address': addr2, 'amount': 250000000000, 'recipient_name': special_name} + ] + + # the RPC should URL-encode these parameters. + res = wallet.make_uri(payments=payments, tx_description=special_desc) + parsed = wallet.parse_uri(res.uri) + # check that the decoded values match the original. + for pay in parsed.uri.payments: + assert pay.recipient_name == special_name, "Special characters in recipient name mismatch" + assert parsed.uri.tx_description == special_desc, "Special characters in description mismatch" + + # build a well-formed multi-recipient URI and tack on unknown parameters. + payments = [ + {'address': addr1, 'amount': 239390140000000, 'recipient_name': ''} + ] + uri_with_unknown = 'monero:' + addr1 + '?tx_amount=239.39014&foo=bar&baz=quux' + parsed = wallet.parse_uri(uri_with_unknown) + assert parsed.uri.payments[0].address == addr1 + assert parsed.uri.payments[0].amount == 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() + URITest().run_test() \ No newline at end of file diff --git a/tests/unit_tests/uri.cpp b/tests/unit_tests/uri.cpp index f1c2b694b5b..2322464b6c3 100644 --- a/tests/unit_tests/uri.cpp +++ b/tests/unit_tests/uri.cpp @@ -41,6 +41,14 @@ bool ret = w.parse_uri(uri, address, payment_id, amount, description, recipient_name, unknown_parameters, error); \ ASSERT_EQ(ret, expected); +#define PARSE_URI_MULTI(uri, expected) \ + std::vector data; \ + std::string payment_id, description, error; \ + std::vector unknown_parameters; \ + tools::wallet2 w(cryptonote::TESTNET); \ + bool ret = w.parse_uri(uri, data, payment_id, description, unknown_parameters, error); \ + ASSERT_EQ(ret, expected); + TEST(uri, empty_string) { PARSE_URI("", false); @@ -213,3 +221,95 @@ TEST(uri, url_encoded_once) ASSERT_EQ(description, "foo 20"); } + +TEST(uri, multiple_addresses_no_params) +{ + PARSE_URI_MULTI("monero:" TEST_ADDRESS ";" TEST_ADDRESS, true); + ASSERT_EQ(data.size(), 2); + ASSERT_EQ(data[0].address, TEST_ADDRESS); + ASSERT_EQ(data[1].address, TEST_ADDRESS); +} + +TEST(uri, multiple_addresses_with_amounts) +{ + PARSE_URI_MULTI("monero:" TEST_ADDRESS ";" TEST_ADDRESS "?tx_amount=0.5;0.2", true); + ASSERT_EQ(data.size(), 2); + ASSERT_EQ(data[0].address, TEST_ADDRESS); + ASSERT_EQ(data[0].amount, 500000000000); + ASSERT_EQ(data[1].address, TEST_ADDRESS); + ASSERT_EQ(data[1].amount, 200000000000); +} + +TEST(uri, multiple_addresses_with_recipient_names) +{ + PARSE_URI_MULTI("monero:" TEST_ADDRESS ";" TEST_ADDRESS "?recipient_name=Alice;Bob", true); + ASSERT_EQ(data.size(), 2); + ASSERT_EQ(data[0].address, TEST_ADDRESS); + ASSERT_EQ(data[0].recipient_name, "Alice"); + ASSERT_EQ(data[1].address, TEST_ADDRESS); + ASSERT_EQ(data[1].recipient_name, "Bob"); +} + +TEST(uri, multiple_addresses_with_mismatched_amounts) +{ + PARSE_URI_MULTI("monero:" TEST_ADDRESS ";" TEST_ADDRESS "?tx_amount=0.5", false); +} + +TEST(uri, multiple_addresses_with_mismatched_recipient_names) +{ + PARSE_URI_MULTI("monero:" TEST_ADDRESS ";" TEST_ADDRESS "?recipient_name=Alice", false); +} + +TEST(uri, multiple_addresses_with_partial_params) +{ + PARSE_URI_MULTI("monero:" TEST_ADDRESS ";" TEST_ADDRESS "?tx_amount=0.5;0&recipient_name=Alice;", true); + ASSERT_EQ(data.size(), 2); + ASSERT_EQ(data[0].address, TEST_ADDRESS); + ASSERT_EQ(data[0].amount, 500000000000); + ASSERT_EQ(data[0].recipient_name, "Alice"); + ASSERT_EQ(data[1].address, TEST_ADDRESS); + ASSERT_EQ(data[1].amount, 0); + ASSERT_EQ(data[1].recipient_name, ""); +} + +TEST(uri, multiple_addresses_with_unknown_params) +{ + PARSE_URI_MULTI("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_payment_id) +{ + PARSE_URI_MULTI("monero:" TEST_ADDRESS ";" TEST_ADDRESS "?tx_payment_id=1234567890123456789012345678901234567890123456789012345678901234", true); + ASSERT_EQ(payment_id, "1234567890123456789012345678901234567890123456789012345678901234"); +} + +TEST(uri, multiple_addresses_with_invalid_payment_id) +{ + PARSE_URI_MULTI("monero:" TEST_ADDRESS ";" TEST_ADDRESS "?tx_payment_id=123456", false); +} + +TEST(uri, multiple_addresses_with_description) +{ + PARSE_URI_MULTI("monero:" TEST_ADDRESS ";" TEST_ADDRESS "?tx_description=Payment%20for%20services", true); + ASSERT_EQ(description, "Payment for services"); +} + +TEST(uri, multiple_addresses_mismatched_params) +{ + PARSE_URI_MULTI("monero:" TEST_ADDRESS ";" TEST_ADDRESS "?tx_amount=0.5&recipient_name=Alice", false); +} + +TEST(uri, multiple_addresses_all_params_correct) +{ + PARSE_URI_MULTI("monero:" TEST_ADDRESS ";" TEST_ADDRESS "?tx_amount=0.5;0.2&recipient_name=Alice;Bob&tx_description=Payment%20for%20services", true); + ASSERT_EQ(data.size(), 2); + ASSERT_EQ(data[0].address, TEST_ADDRESS); + ASSERT_EQ(data[0].amount, 500000000000); + ASSERT_EQ(data[0].recipient_name, "Alice"); + ASSERT_EQ(data[1].address, TEST_ADDRESS); + ASSERT_EQ(data[1].amount, 200000000000); + ASSERT_EQ(data[1].recipient_name, "Bob"); + ASSERT_EQ(description, "Payment for services"); +} \ No newline at end of file diff --git a/utils/python-rpc/framework/wallet.py b/utils/python-rpc/framework/wallet.py index 0753b4fd4ad..7a6f808504a 100644 --- a/utils/python-rpc/framework/wallet.py +++ b/utils/python-rpc/framework/wallet.py @@ -987,21 +987,47 @@ def get_attribute(self, key): } return self.rpc.send_json_rpc_request(get_attribute) - def make_uri(self, address = '', payment_id = '', amount = 0, tx_description = '', recipient_name = ''): + + def make_uri(self, payments=None, address='', payment_id='', amount=0, tx_description='', recipient_name=''): + """ + Unified make_uri method. + + If 'payments' is provided (a list), the new multi-recipient format is used (old format, but multi data is separated by semi-colon). + Otherwise, if no payments list is provided, the legacy format is assumed and + a single payment is created from the legacy parameters (address, amount, recipient_name). + + Existing parameter names remain unchanged for backwards compatibility. + The underlying C++ implementation provides overloads: + - make_uri(vector, payment_id, tx_description, error) + - make_uri(address, payment_id, amount, tx_description, recipient_name, error) + + Either provide: + #1 payments{address,amount,recipient_name}, payment_id, tx_description (supports multi-uri) + #2: address, payment_id, amount, tx_description, recipient_name (obsolete, legacy, kept for backwards-compatibility) + Standalone payment IDs are not supported and will result into errors. + """ + # if payments is None or an empty list, assume legacy mode: + if not payments: + # in legacy mode, the address must be nonempty. + if not address: + raise Exception("No address provided") + payments = [{ + 'address': address, + 'amount': amount, + 'recipient_name': recipient_name + }] make_uri = { 'method': 'make_uri', 'jsonrpc': '2.0', 'params': { - 'address': address, + 'payments': payments, 'payment_id': payment_id, - 'amount': amount, 'tx_description': tx_description, - 'recipient_name': recipient_name, }, 'id': '0' } return self.rpc.send_json_rpc_request(make_uri) - + def parse_uri(self, uri): parse_uri = { 'method': 'parse_uri',