fix(send): accept P2SH/multisig recipients in the send + URI address gates

Same defect class as the import-key fix: a hardcoded prefix/length pre-filter
layered over the checksum validators rejected valid addresses before the daemon
saw them. The send-screen recipient gate required a[0]=='R', and the payment-URI
parser accepted only 'R'/'t' with rigid length bands — so every valid P2SH /
multisig address (DragonX SCRIPT_ADDRESS=85 → 'b…') was silently refused, leaving
the Send button disabled with no usable recipient.

Centralize recipient recognition in util/address_validation:
- isTransparentAddress: Base58Check with a 21-byte version+hash160 payload —
  covers P2PKH ('R…', v60) AND P2SH ('b…', v85) on every network, rejects WIF
  keys / typos by real checksum.
- isShieldedAddress: Bech32 + a Sapling payment-address HRP (zs / ztestsapling /
  zregtestsapling), distinguishing a payment address from a viewing key.
- isValidRecipientAddress: either of the above.

send_tab's two validity helpers (the single choke point for all 5 call sites) and
the payment-URI format check now route through these. The URI parser now
checksum-validates the recipient (fail-fast on transcription errors) rather than
being prefix/length-only.

Tests use real checksummed vectors (P2PKH/P2SH/shielded, WIF- and typo-rejection);
testPaymentUri updated off its old fake fixed-char addresses. Suite green (1/1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-09 00:12:17 -05:00
co-authored by Claude Opus 4.8
parent d603a54618
commit c3e81a5fa6
5 changed files with 78 additions and 23 deletions
+21
View File
@@ -148,5 +148,26 @@ std::string bech32Hrp(const std::string& s)
return lower.substr(0, sep);
}
bool isTransparentAddress(const std::string& s)
{
std::vector<std::uint8_t> payload;
// version byte (1) + hash160 (20) = 21 bytes, checksum stripped. Covers P2PKH ('R', v60) and
// P2SH/multisig ('b', v85); the daemon vets the exact version byte for the active network.
return decodeBase58Check(s, payload) && payload.size() == 21;
}
bool isShieldedAddress(const std::string& s)
{
const std::string hrp = bech32Hrp(s);
return hrp == "zs" // mainnet Sapling payment address
|| hrp == "ztestsapling" // testnet
|| hrp == "zregtestsapling"; // regtest
}
bool isValidRecipientAddress(const std::string& s)
{
return isTransparentAddress(s) || isShieldedAddress(s);
}
} // namespace util
} // namespace dragonx