// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "payment_uri.h" #include "address_validation.h" #include #include #include #include #include #include namespace { // Strictly parse a decimal DRGX amount from an (attacker-influenceable) payment URI / QR: digits with at // most one '.', at most 8 fractional digits, in [0, 21000000]. Rejects empty, NaN/inf, signs, whitespace // and trailing garbage that std::stod would otherwise accept — so a crafted URI can't inject a bogus // amount into the Send form. bool parseStrictAmount(const std::string& s, double& out) { if (s.empty() || s.size() > 32) return false; int dots = 0, digits = 0, frac = 0; bool afterDot = false; for (char c : s) { if (c == '.') { if (++dots > 1) return false; afterDot = true; } else if (c >= '0' && c <= '9') { ++digits; if (afterDot && ++frac > 8) return false; } else return false; } if (digits == 0) return false; double v = 0.0; try { v = std::stod(s); } catch (...) { return false; } if (!std::isfinite(v) || v < 0.0 || v > 21000000.0) return false; out = v; return true; } } // namespace namespace dragonx { namespace util { std::string urlEncode(const std::string& value) { std::string out; out.reserve(value.size()); for (unsigned char c : value) { if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') { out += static_cast(c); } else if (c == ' ') { out += "%20"; } else { char hex[4]; std::snprintf(hex, sizeof(hex), "%%%02X", c); out += hex; } } return out; } std::string buildPaymentUri(const std::string& address, double amount, const std::string& label, const std::string& memo) { if (address.empty()) return ""; std::ostringstream uri; uri << "drgx:" << address; bool hasParams = false; auto addParam = [&](const char* key, const std::string& value) { if (value.empty()) return; uri << (hasParams ? "&" : "?") << key << "=" << value; hasParams = true; }; if (amount > 0.0) { std::ostringstream amt; amt << std::fixed << std::setprecision(8) << amount; addParam("amount", amt.str()); } addParam("label", urlEncode(label)); addParam("memo", urlEncode(memo)); return uri.str(); } std::string urlDecode(const std::string& encoded) { std::string result; result.reserve(encoded.size()); for (size_t i = 0; i < encoded.size(); i++) { if (encoded[i] == '%' && i + 2 < encoded.size()) { // Decode hex pair char hex[3] = { encoded[i + 1], encoded[i + 2], '\0' }; char* end; long val = strtol(hex, &end, 16); if (end == hex + 2) { result += static_cast(val); i += 2; continue; } } else if (encoded[i] == '+') { result += ' '; continue; } result += encoded[i]; } return result; } bool isPaymentURI(const std::string& str) { // Check for drgx: or hush: prefix (case insensitive) std::string lower = str; std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower); return lower.substr(0, 5) == "drgx:" || lower.substr(0, 5) == "hush:"; } PaymentURI parsePaymentURI(const std::string& uri) { PaymentURI result; result.valid = false; // Check prefix if (!isPaymentURI(uri)) { result.error = "Invalid URI scheme. Expected drgx: or hush:"; return result; } // Skip the scheme prefix (drgx: or hush:) size_t schemeEnd = uri.find(':'); if (schemeEnd == std::string::npos) { result.error = "Invalid URI format"; return result; } std::string remainder = uri.substr(schemeEnd + 1); // Handle double-slash format (drgx://address) if (remainder.size() >= 2 && remainder[0] == '/' && remainder[1] == '/') { remainder = remainder.substr(2); } // Split address and query string size_t queryStart = remainder.find('?'); if (queryStart == std::string::npos) { // No query parameters, just the address result.address = remainder; } else { result.address = remainder.substr(0, queryStart); std::string queryString = remainder.substr(queryStart + 1); // Parse query parameters std::istringstream queryStream(queryString); std::string param; while (std::getline(queryStream, param, '&')) { size_t eqPos = param.find('='); if (eqPos == std::string::npos) continue; std::string key = param.substr(0, eqPos); std::string value = urlDecode(param.substr(eqPos + 1)); // Convert key to lowercase for case-insensitive matching std::transform(key.begin(), key.end(), key.begin(), ::tolower); if (key == "amount" || key == "amt") { double a = 0.0; if (!parseStrictAmount(value, a)) { result.error = "Invalid amount"; result.valid = false; return result; // reject the whole URI rather than silently treating it as 0 } result.amount = a; } else if (key == "label") { result.label = value; } else if (key == "memo") { result.memo = value; } else if (key == "message" || key == "msg") { result.message = value; } } } // Validate address if (result.address.empty()) { result.error = "No address specified in URI"; return result; } // Address format check via the shared, structure-based recognizers (checksum + real DragonX // address types). This accepts shielded ("zs…"), P2PKH ("R…") and P2SH/multisig ("b…") forms — // the old prefix/length heuristic rejected P2SH and hardcoded a 't' prefix DragonX never emits. const bool validFormat = isShieldedAddress(result.address) || isTransparentAddress(result.address); if (!validFormat) { result.error = "Invalid address format"; return result; } // Validate amount if present if (!std::isfinite(result.amount) || result.amount < 0 || result.amount > 21000000.0) { result.error = "Invalid negative amount"; return result; } result.valid = true; return result; } } // namespace util } // namespace dragonx