I don’t really find this fn to be a UniValue helper, pretty much all the logic is sighash specific. While thinking for a better place to sit, I ended up thinking the better solution may be to keep it where it is but just remove the (imo unnecessary) dependency on UV? Offloading the default value and type check to a helper function like ParseSighashString is opaque, and even though my approach here is slightly more verbose in the callsite, I find the clarity of what’s happening to be worth it (different callsites may require different behaviour in case of wrong types or sighash_str).
What do you think?
(I think returning a std::optional instead of throwing is probably even better, but orthogonal to this pull)
  0diff --git a/src/core_io.h b/src/core_io.h
  1index 997f3bfd5b..ea07042b82 100644
  2--- a/src/core_io.h
  3+++ b/src/core_io.h
  4@@ -46,7 +46,7 @@ bool DecodeHexBlockHeader(CBlockHeader&, const std::string& hex_header);
  5  */
  6 bool ParseHashStr(const std::string& strHex, uint256& result);
  7 std::vector<unsigned char> ParseHexUV(const UniValue& v, const std::string& strName);
  8-int ParseSighashString(const UniValue& sighash);
  9+int ParseSighashString(const std::string& sighash);
 10 
 11 // core_write.cpp
 12 UniValue ValueFromAmount(const CAmount amount);
 13diff --git a/src/core_read.cpp b/src/core_read.cpp
 14index 84cd559b7f..31f121206c 100644
 15--- a/src/core_read.cpp
 16+++ b/src/core_read.cpp
 17@@ -252,26 +252,20 @@ std::vector<unsigned char> ParseHexUV(const UniValue& v, const std::string& strN
 18     return ParseHex(strHex);
 19 }
 20 
 21-int ParseSighashString(const UniValue& sighash)
 22+int ParseSighashString(const std::string& sighash_str)
 23 {
 24-    int hash_type = SIGHASH_DEFAULT;
 25-    if (!sighash.isNull()) {
 26-        static std::map<std::string, int> map_sighash_values = {
 27-            {std::string("DEFAULT"), int(SIGHASH_DEFAULT)},
 28-            {std::string("ALL"), int(SIGHASH_ALL)},
 29-            {std::string("ALL|ANYONECANPAY"), int(SIGHASH_ALL|SIGHASH_ANYONECANPAY)},
 30-            {std::string("NONE"), int(SIGHASH_NONE)},
 31-            {std::string("NONE|ANYONECANPAY"), int(SIGHASH_NONE|SIGHASH_ANYONECANPAY)},
 32-            {std::string("SINGLE"), int(SIGHASH_SINGLE)},
 33-            {std::string("SINGLE|ANYONECANPAY"), int(SIGHASH_SINGLE|SIGHASH_ANYONECANPAY)},
 34-        };
 35-        const std::string& strHashType = sighash.get_str();
 36-        const auto& it = map_sighash_values.find(strHashType);
 37-        if (it != map_sighash_values.end()) {
 38-            hash_type = it->second;
 39-        } else {
 40-            throw std::runtime_error(strHashType + " is not a valid sighash parameter.");
 41-        }
 42+    static std::map<std::string, int> map_sighash_values = {
 43+        {std::string("DEFAULT"), int(SIGHASH_DEFAULT)},
 44+        {std::string("ALL"), int(SIGHASH_ALL)},
 45+        {std::string("ALL|ANYONECANPAY"), int(SIGHASH_ALL|SIGHASH_ANYONECANPAY)},
 46+        {std::string("NONE"), int(SIGHASH_NONE)},
 47+        {std::string("NONE|ANYONECANPAY"), int(SIGHASH_NONE|SIGHASH_ANYONECANPAY)},
 48+        {std::string("SINGLE"), int(SIGHASH_SINGLE)},
 49+        {std::string("SINGLE|ANYONECANPAY"), int(SIGHASH_SINGLE|SIGHASH_ANYONECANPAY)},
 50+    };
 51+    const auto& it{map_sighash_values.find(sighash_str)};
 52+    if (it != map_sighash_values.end()) {
 53+        return it->second;
 54     }
 55-    return hash_type;
 56+    throw std::runtime_error(sighash_str + " is not a valid sighash parameter.");
 57 }
 58diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp
 59index eb0200ccf5..e0e30b2d9e 100644
 60--- a/src/rpc/rawtransaction.cpp
 61+++ b/src/rpc/rawtransaction.cpp
 62@@ -1964,7 +1964,7 @@ RPCHelpMan descriptorprocesspsbt()
 63         EvalDescriptorStringOrObject(descs[i], provider, /*expand_priv=*/true);
 64     }
 65 
 66-    int sighash_type = ParseSighashString(request.params[2]);
 67+    int sighash_type{request.params[2].isNull() ? SIGHASH_DEFAULT: ParseSighashString(request.params[2].get_str())};
 68     bool bip32derivs = request.params[3].isNull() ? true : request.params[3].get_bool();
 69     bool finalize = request.params[4].isNull() ? true : request.params[4].get_bool();
 70 
 71diff --git a/src/rpc/rawtransaction_util.cpp b/src/rpc/rawtransaction_util.cpp
 72index 3a6fa39e4d..7272cca1c5 100644
 73--- a/src/rpc/rawtransaction_util.cpp
 74+++ b/src/rpc/rawtransaction_util.cpp
 75@@ -289,7 +289,7 @@ void ParsePrevouts(const UniValue& prevTxsUnival, FillableSigningProvider* keyst
 76 
 77 void SignTransaction(CMutableTransaction& mtx, const SigningProvider* keystore, const std::map<COutPoint, Coin>& coins, const UniValue& hashType, UniValue& result)
 78 {
 79-    int nHashType = ParseSighashString(hashType);
 80+    int nHashType{hashType.isNull() ? SIGHASH_DEFAULT : ParseSighashString(hashType.get_str())};
 81 
 82     // Script verification errors
 83     std::map<int, bilingual_str> input_errors;
 84diff --git a/src/test/fuzz/parse_univalue.cpp b/src/test/fuzz/parse_univalue.cpp
 85index bfa856211d..5cb2c64f26 100644
 86--- a/src/test/fuzz/parse_univalue.cpp
 87+++ b/src/test/fuzz/parse_univalue.cpp
 88@@ -73,10 +73,6 @@ FUZZ_TARGET(parse_univalue, .init = initialize_parse_univalue)
 89     } catch (const UniValue&) {
 90     } catch (const std::runtime_error&) {
 91     }
 92-    try {
 93-        (void)ParseSighashString(univalue);
 94-    } catch (const std::runtime_error&) {
 95-    }
 96     try {
 97         (void)AmountFromValue(univalue);
 98     } catch (const UniValue&) {
 99diff --git a/src/wallet/rpc/spend.cpp b/src/wallet/rpc/spend.cpp
100index b695d4bed3..3d74b12ef7 100644
101--- a/src/wallet/rpc/spend.cpp
102+++ b/src/wallet/rpc/spend.cpp
103@@ -943,7 +943,7 @@ RPCHelpMan signrawtransactionwithwallet()
104     // Parse the prevtxs array
105     ParsePrevouts(request.params[1], nullptr, coins);
106 
107-    int nHashType = ParseSighashString(request.params[2]);
108+    int nHashType{request.params[2].isNull() ? SIGHASH_DEFAULT: ParseSighashString(request.params[2].get_str())};
109 
110     // Script verification errors
111     std::map<int, bilingual_str> input_errors;
112@@ -1587,7 +1587,7 @@ RPCHelpMan walletprocesspsbt()
113     }
114 
115     // Get the sighash type
116-    int nHashType = ParseSighashString(request.params[2]);
117+    int nHashType{request.params[2].isNull() ? SIGHASH_DEFAULT: ParseSighashString(request.params[2].get_str())};
118 
119     // Fill transaction with our data and also sign
120     bool sign = request.params[1].isNull() ? true : request.params[1].get_bool();