From 97878fa0656035e2afc7422e9d733b2c66fd93c1 Mon Sep 17 00:00:00 2001 From: Duke Date: Tue, 20 May 2025 11:47:22 -0400 Subject: [PATCH 01/11] There can be only one OP_RETURN in a tx --- src/wallet/rpcwallet.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index ef1be630a..3b5d06fb4 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -5021,6 +5021,7 @@ UniValue z_sendmany(const UniValue& params, bool fHelp, const CPubKey& mypk) "3. minconf (numeric, optional, default=1) Only use funds confirmed at least this many times.\n" "4. fee (numeric, optional, default=" + strprintf("%s", FormatMoney(ASYNC_RPC_OPERATION_DEFAULT_MINERS_FEE)) + ") The fee amount to attach to this transaction.\n" + "5. opreturn (string, optional) Hex encoded data for OP_RETURN.\n" "\nResult:\n" "\"operationid\" (string) An operationid to pass to z_getoperationstatus to get the result of the operation.\n" "\nExamples:\n" @@ -5152,8 +5153,7 @@ UniValue z_sendmany(const UniValue& params, bool fHelp, const CPubKey& mypk) CAmount nTotalOut = 0; // Optional OP_RETURN data CScript opret; - // TODO: enforce that only a single opreturn exists - UniValue opretValue; + UniValue opretValue = params.size() >= 4 ? params[4].get_str() : ""; bool containsSaplingOutput = false; @@ -5164,7 +5164,7 @@ UniValue z_sendmany(const UniValue& params, bool fHelp, const CPubKey& mypk) // sanity check, report error if unknown key-value pairs for (const string& name_ : o.getKeys()) { std::string s = name_; - if (s != "address" && s != "amount" && s!="memo" && s!="opreturn") { + if (s != "address" && s != "amount" && s!="memo") { throw JSONRPCError(RPC_INVALID_PARAMETER, string("Invalid parameter, unknown key: ")+s); } } From 492fbcd0230092375ed4ace4774e3bf7468da6d7 Mon Sep 17 00:00:00 2001 From: Duke Date: Tue, 20 May 2025 12:21:47 -0400 Subject: [PATCH 02/11] Leave opretValue null unless a 5th argument is given to avoid adding empty opreturns --- src/wallet/rpcwallet.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 3b5d06fb4..0193f9fd2 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -5153,7 +5153,10 @@ UniValue z_sendmany(const UniValue& params, bool fHelp, const CPubKey& mypk) CAmount nTotalOut = 0; // Optional OP_RETURN data CScript opret; - UniValue opretValue = params.size() >= 4 ? params[4].get_str() : ""; + UniValue opretValue; + if(params.size() == 5) { + opretValue = params[4].get_str(); + } bool containsSaplingOutput = false; From fb3eb56c8f8aa350d816a11aecd7683f0ebfcab1 Mon Sep 17 00:00:00 2001 From: Duke Date: Tue, 20 May 2025 12:29:56 -0400 Subject: [PATCH 03/11] Begin supporting utf8: in z_sendmany opreturn --- src/wallet/rpcwallet.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 0193f9fd2..54a5310dd 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -5156,6 +5156,11 @@ UniValue z_sendmany(const UniValue& params, bool fHelp, const CPubKey& mypk) UniValue opretValue; if(params.size() == 5) { opretValue = params[4].get_str(); + + if(opretValue.get_str().substr(0,5) == "utf8:") { + // TODO: 1) remove the first 5 chars + // 2) convert remaining text to hex and store in opretValue + } } bool containsSaplingOutput = false; From 48e0a989c1101103df1986ba9acc033a0a455a51 Mon Sep 17 00:00:00 2001 From: Duke Date: Thu, 29 May 2025 02:39:58 -0400 Subject: [PATCH 04/11] Decode utf8 in opreturn and convert to hex --- src/wallet/rpcwallet.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 54a5310dd..1852db336 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -5157,9 +5157,14 @@ UniValue z_sendmany(const UniValue& params, bool fHelp, const CPubKey& mypk) if(params.size() == 5) { opretValue = params[4].get_str(); + // Support a prefix "utf8:" which allows giving utf8 text instead of hex if(opretValue.get_str().substr(0,5) == "utf8:") { - // TODO: 1) remove the first 5 chars - // 2) convert remaining text to hex and store in opretValue + auto str = opretValue.get_str().substr(5); + if (utf8::is_valid(str)) { + opretValue = HexStr(str); + } else { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid utf8 in opreturn"); + } } } From b6418912f2f5ffb8ddd3d733da195efdf780d418 Mon Sep 17 00:00:00 2001 From: Duke Date: Thu, 29 May 2025 13:53:21 -0400 Subject: [PATCH 05/11] Document utf8: prefix in z_sendmany opreturn and add example to RPC help --- src/wallet/rpcwallet.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 1852db336..283f81810 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -5021,7 +5021,7 @@ UniValue z_sendmany(const UniValue& params, bool fHelp, const CPubKey& mypk) "3. minconf (numeric, optional, default=1) Only use funds confirmed at least this many times.\n" "4. fee (numeric, optional, default=" + strprintf("%s", FormatMoney(ASYNC_RPC_OPERATION_DEFAULT_MINERS_FEE)) + ") The fee amount to attach to this transaction.\n" - "5. opreturn (string, optional) Hex encoded data for OP_RETURN.\n" + "5. opreturn (string, optional) Hex encoded data for OP_RETURN. Or a utf8 string prefixed with 'utf8:' which will be automatically converted to hex\n" "\nResult:\n" "\"operationid\" (string) An operationid to pass to z_getoperationstatus to get the result of the operation.\n" "\nExamples:\n" @@ -5029,6 +5029,8 @@ UniValue z_sendmany(const UniValue& params, bool fHelp, const CPubKey& mypk) + HelpExampleRpc("z_sendmany", "\"RD6GgnrMpPaTSMn8vai6yiGA7mN4QGPV\", [{\"address\": \"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" ,\"amount\": 5.0}]") + HelpExampleCli("z_sendmany", "\"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" '[{\"address\": \"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" ,\"amount\": 3.14}]'") + HelpExampleRpc("z_sendmany", "\"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\", [{\"address\": \"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" ,\"amount\": 3.14}]") + + HelpExampleCli("z_sendmany", "\"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" '[{\"address\": \"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" ,\"amount\": 3.14}]' 1 0.0001 \"utf8: this will be converted to hex") + + HelpExampleRpc("z_sendmany", "\"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" '[{\"address\": \"zs14d8tc0hl9q0vg5l28uec5vk6sk34fkj2n8s7jalvw5fxpy6v39yn4s2ga082lymrkjk0x2nqg37\" ,\"amount\": 3.14}]' 1 0.0001 \"utf8: this will be converted to hex") ); LOCK2(cs_main, pwalletMain->cs_wallet); From d6b7fc633f8285579129b20f546f936dd09baa4c Mon Sep 17 00:00:00 2001 From: Duke Date: Thu, 29 May 2025 15:06:47 -0400 Subject: [PATCH 06/11] Add new opreturn param for z_sendmany to conversion list --- src/rpc/client.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 517f254c1..94266413e 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -150,6 +150,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "z_sendmany", 1}, { "z_sendmany", 2}, { "z_sendmany", 3}, + { "z_sendmany", 4}, { "z_shieldcoinbase", 2}, { "z_shieldcoinbase", 3}, { "z_getoperationstatus", 0}, From 1a70e754ceca2d63a40e0d5b587fe2216daa6f6c Mon Sep 17 00:00:00 2001 From: Duke Date: Thu, 29 May 2025 15:08:27 -0400 Subject: [PATCH 07/11] Revert "Add new opreturn param for z_sendmany to conversion list" This reverts commit d6b7fc633f8285579129b20f546f936dd09baa4c. --- src/rpc/client.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 94266413e..517f254c1 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -150,7 +150,6 @@ static const CRPCConvertParam vRPCConvertParams[] = { "z_sendmany", 1}, { "z_sendmany", 2}, { "z_sendmany", 3}, - { "z_sendmany", 4}, { "z_shieldcoinbase", 2}, { "z_shieldcoinbase", 3}, { "z_getoperationstatus", 0}, From a710dd2099faf5de10aba0e0faa8c97dff968e51 Mon Sep 17 00:00:00 2001 From: Duke Date: Thu, 29 May 2025 15:08:40 -0400 Subject: [PATCH 08/11] Allow an optional opreturn param --- src/wallet/rpcwallet.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 283f81810..a622dddba 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -5003,9 +5003,9 @@ UniValue z_sendmany(const UniValue& params, bool fHelp, const CPubKey& mypk) if (!EnsureWalletIsAvailable(fHelp)) return NullUniValue; - if (fHelp || params.size() < 2 || params.size() > 4) + if (fHelp || params.size() < 2 || params.size() > 5) throw runtime_error( - "z_sendmany \"fromaddress\" [{\"address\":... ,\"amount\":...},...] ( minconf ) ( fee )\n" + "z_sendmany \"fromaddress\" [{\"address\":... ,\"amount\":...},...] ( minconf ) ( fee ) (opreturn)\n" "\nSend multiple times. Amounts are decimal numbers with at most 8 digits of precision." "\nChange generated from a taddr flows to a new taddr address, while change generated from a zaddr returns to itself." "\nWhen sending coinbase UTXOs to a zaddr, change is not allowed. The entire value of the UTXO(s) must be consumed." From 38901a073bf3905dcfbebf25412ec5eba0b8ca2d Mon Sep 17 00:00:00 2001 From: Duke Date: Thu, 29 May 2025 15:24:17 -0400 Subject: [PATCH 09/11] opreturn is no longer a valid json key in z_sendmany --- src/wallet/rpcwallet.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index a622dddba..8d33cd1d9 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -5198,11 +5198,6 @@ UniValue z_sendmany(const UniValue& params, bool fHelp, const CPubKey& mypk) // throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Extreme Privacy! You must send to a zaddr"); //} - UniValue this_opret = find_value(o, "opreturn"); - if (!this_opret.isNull()) { - opretValue = this_opret; - } - // Create the CScript representation of the OP_RETURN if (!opretValue.isNull()) { opret << OP_RETURN << ParseHex(opretValue.get_str().c_str()); From cc1c0b30b0d5ab59534a4cc05505ba0db0384256 Mon Sep 17 00:00:00 2001 From: Duke Date: Thu, 29 May 2025 15:31:47 -0400 Subject: [PATCH 10/11] There is now only one opreturn possible in z_sendmany so pull the assignment to opret out of the loop --- src/wallet/rpcwallet.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 8d33cd1d9..fe0947a47 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -5172,6 +5172,11 @@ UniValue z_sendmany(const UniValue& params, bool fHelp, const CPubKey& mypk) bool containsSaplingOutput = false; + // Create the CScript representation of the OP_RETURN + if (!opretValue.isNull()) { + opret << OP_RETURN << ParseHex(opretValue.get_str().c_str()); + } + for (const UniValue& o : outputs.getValues()) { if (!o.isObject()) throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected object"); @@ -5198,11 +5203,6 @@ UniValue z_sendmany(const UniValue& params, bool fHelp, const CPubKey& mypk) // throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Extreme Privacy! You must send to a zaddr"); //} - // Create the CScript representation of the OP_RETURN - if (!opretValue.isNull()) { - opret << OP_RETURN << ParseHex(opretValue.get_str().c_str()); - } - UniValue memoValue = find_value(o, "memo"); string memo; if (!memoValue.isNull()) { From a520a3e655320b7a295020ac5014198c184a3c6d Mon Sep 17 00:00:00 2001 From: Duke Date: Thu, 29 May 2025 15:40:07 -0400 Subject: [PATCH 11/11] Support using non-hex utf8 strings with z_sendmany memo --- src/wallet/rpcwallet.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index fe0947a47..5f26a4034 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -5209,8 +5209,16 @@ UniValue z_sendmany(const UniValue& params, bool fHelp, const CPubKey& mypk) memo = memoValue.get_str(); if (!isZaddr) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Memo cannot be used with a taddr. It can only be used with a zaddr."); + } else if(memo.substr(0,5) == "utf8:") { + // Support a prefix "utf8:" which allows giving utf8 text instead of hex + auto str = memo.substr(5); + if (utf8::is_valid(str)) { + memo = HexStr(str); + } else { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid utf8 in memo"); + } } else if (!IsHex(memo)) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected memo data in hexadecimal format."); + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, expected memo data in hexadecimal format or to use 'utf8:' prefix."); } if (memo.length() > HUSH_MEMO_SIZE*2) { throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Invalid parameter, size of memo is larger than maximum allowed %d", HUSH_MEMO_SIZE ));