Skip to main content

chains_sdk/xrp/
mod.rs

1//! XRP signer supporting both ECDSA (secp256k1 + SHA-512 half) and Ed25519.
2//!
3//! XRP allows two key types: secp256k1 and Ed25519.
4
5pub mod advanced;
6pub mod transaction;
7
8use crate::crypto;
9use crate::error::SignerError;
10use crate::traits;
11use sha2::{Digest, Sha512};
12use zeroize::Zeroizing;
13
14/// XRP signature (variable format depending on key type).
15#[derive(Debug, Clone, PartialEq, Eq)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17#[must_use]
18pub struct XrpSignature {
19    /// DER-encoded for ECDSA, 64-byte for Ed25519.
20    pub bytes: Vec<u8>,
21}
22
23impl core::fmt::Display for XrpSignature {
24    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
25        write!(f, "0x")?;
26        for byte in &self.bytes {
27            write!(f, "{byte:02x}")?;
28        }
29        Ok(())
30    }
31}
32
33impl XrpSignature {
34    /// Export the signature bytes.
35    pub fn to_bytes(&self) -> Vec<u8> {
36        self.bytes.clone()
37    }
38
39    /// Import from raw bytes.
40    ///
41    /// Validates structural format: DER-encoded ECDSA (starts with `0x30`)
42    /// or 64-byte Ed25519 signature.
43    pub fn from_bytes(bytes: &[u8]) -> Result<Self, SignerError> {
44        if bytes.is_empty() {
45            return Err(SignerError::InvalidSignature("empty signature".into()));
46        }
47        // Validate: either DER ECDSA (0x30 tag) or 64-byte Ed25519
48        if bytes[0] == 0x30 {
49            // DER ECDSA: verify the length byte matches
50            if bytes.len() < 3 || bytes.len() > 73 {
51                return Err(SignerError::InvalidSignature(format!(
52                    "invalid DER signature length: {}",
53                    bytes.len()
54                )));
55            }
56        } else if bytes.len() != 64 {
57            return Err(SignerError::InvalidSignature(format!(
58                "expected 64-byte Ed25519 or DER ECDSA, got {} bytes starting with 0x{:02x}",
59                bytes.len(),
60                bytes[0]
61            )));
62        }
63        Ok(Self {
64            bytes: bytes.to_vec(),
65        })
66    }
67}
68
69/// Compute SHA-512 half (first 32 bytes of SHA-512 digest).
70pub fn sha512_half(data: &[u8]) -> [u8; 32] {
71    let full = Sha512::digest(data);
72    let mut out = [0u8; 32];
73    out.copy_from_slice(&full[..32]);
74    out
75}
76
77/// Derive XRP account ID: RIPEMD160(SHA256(pubkey_bytes)).
78pub fn account_id(pubkey_bytes: &[u8]) -> [u8; 20] {
79    crypto::hash160(pubkey_bytes)
80}
81
82/// XRP Base58 alphabet (differs from Bitcoin's alphabet).
83fn xrp_alphabet() -> Result<bs58::Alphabet, SignerError> {
84    bs58::Alphabet::new(b"rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz")
85        .map_err(|e| SignerError::InvalidPublicKey(format!("XRP alphabet: {e}")))
86}
87
88/// Generate an XRP `r...` address from a 20-byte account ID.
89///
90/// Uses XRP's custom Base58Check with version byte 0x00.
91pub fn xrp_address(account_id: &[u8; 20]) -> Result<String, SignerError> {
92    let mut payload = vec![0x00u8]; // version byte
93    payload.extend_from_slice(account_id);
94    // XRP uses double-SHA256 for checksum (same as Bitcoin)
95    let checksum = crypto::double_sha256(&payload);
96    payload.extend_from_slice(&checksum[..4]);
97    Ok(bs58::encode(payload)
98        .with_alphabet(&xrp_alphabet()?)
99        .into_string())
100}
101
102/// Validate an XRP `r...` address string.
103///
104/// Checks: starts with 'r', 25-byte Base58Check decode, version 0x00, valid checksum.
105pub fn validate_address(address: &str) -> bool {
106    if !address.starts_with('r') {
107        return false;
108    }
109    let alphabet = match xrp_alphabet() {
110        Ok(a) => a,
111        Err(_) => return false,
112    };
113    let decoded = match bs58::decode(address).with_alphabet(&alphabet).into_vec() {
114        Ok(d) => d,
115        Err(_) => return false,
116    };
117    if decoded.len() != 25 || decoded[0] != 0x00 {
118        return false;
119    }
120    use subtle::ConstantTimeEq;
121    let checksum = crypto::double_sha256(&decoded[..21]);
122    checksum[..4].ct_eq(&decoded[21..25]).unwrap_u8() == 1
123}
124
125// ═══════════════════════════════════════════════════════════════════
126// X-Address Encoding (XLS-7d)
127// ═══════════════════════════════════════════════════════════════════
128
129/// Encode an account ID and optional destination tag into an **X-address**.
130///
131/// X-addresses combine the account ID and destination tag into a single string.
132/// - Mainnet: starts with `X`
133/// - Testnet: starts with `T`
134///
135/// Format: `0x05 0x44` (mainnet) | `0x05 0x93` (testnet) + account_id + flags + tag_bytes + checksum
136pub fn encode_x_address(
137    account_id: &[u8; 20],
138    tag: Option<u32>,
139    is_testnet: bool,
140) -> Result<String, SignerError> {
141    let mut payload = Vec::with_capacity(31);
142
143    // 2-byte prefix
144    if is_testnet {
145        payload.extend_from_slice(&[0x05, 0x93]);
146    } else {
147        payload.extend_from_slice(&[0x05, 0x44]);
148    }
149
150    // 20-byte account ID
151    payload.extend_from_slice(account_id);
152
153    // flags + tag (9 bytes)
154    match tag {
155        Some(t) => {
156            payload.push(0x01); // has tag
157            payload.extend_from_slice(&t.to_le_bytes()); // 4 bytes LE
158            payload.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // 4 reserved bytes
159        }
160        None => {
161            payload.push(0x00); // no tag
162            payload.extend_from_slice(&[0x00; 8]); // 8 zero bytes
163        }
164    }
165
166    // 4-byte checksum (double SHA-256)
167    let checksum = crypto::double_sha256(&payload);
168    payload.extend_from_slice(&checksum[..4]);
169
170    Ok(bs58::encode(payload)
171        .with_alphabet(&xrp_alphabet()?)
172        .into_string())
173}
174
175/// Decode an X-address into an account ID and optional destination tag.
176///
177/// Returns `(account_id, optional_tag, is_testnet)`.
178pub fn decode_x_address(x_address: &str) -> Result<([u8; 20], Option<u32>, bool), SignerError> {
179    let decoded = bs58::decode(x_address)
180        .with_alphabet(&xrp_alphabet()?)
181        .into_vec()
182        .map_err(|_| SignerError::ParseError("invalid X-address Base58".into()))?;
183
184    if decoded.len() != 35 {
185        return Err(SignerError::ParseError(format!(
186            "X-address: expected 35 bytes, got {}",
187            decoded.len()
188        )));
189    }
190
191    // Verify checksum
192    let checksum = crypto::double_sha256(&decoded[..31]);
193    if decoded[31..35] != checksum[..4] {
194        return Err(SignerError::ParseError("X-address: bad checksum".into()));
195    }
196
197    // Parse prefix
198    let is_testnet = match (decoded[0], decoded[1]) {
199        (0x05, 0x44) => false, // mainnet
200        (0x05, 0x93) => true,  // testnet
201        _ => return Err(SignerError::ParseError("X-address: unknown prefix".into())),
202    };
203
204    // Account ID
205    let mut account = [0u8; 20];
206    account.copy_from_slice(&decoded[2..22]);
207
208    // Tag
209    let tag = if decoded[22] == 0x01 {
210        Some(u32::from_le_bytes([
211            decoded[23],
212            decoded[24],
213            decoded[25],
214            decoded[26],
215        ]))
216    } else {
217        None
218    };
219
220    Ok((account, tag, is_testnet))
221}
222
223// ─── ECDSA (secp256k1) ──────────────────────────────────────────────────────
224
225/// XRP ECDSA signer (secp256k1 + SHA-512 half).
226pub struct XrpEcdsaSigner {
227    signing_key: k256::ecdsa::SigningKey,
228}
229
230impl Drop for XrpEcdsaSigner {
231    fn drop(&mut self) {
232        // k256::SigningKey implements ZeroizeOnDrop internally
233    }
234}
235
236impl XrpEcdsaSigner {
237    /// Derive the XRP account ID from this signer's public key.
238    pub fn account_id(&self) -> [u8; 20] {
239        account_id(&self.public_key_bytes_inner())
240    }
241
242    /// Return the XRP `r...` address string.
243    pub fn address(&self) -> Result<String, SignerError> {
244        xrp_address(&self.account_id())
245    }
246
247    fn public_key_bytes_inner(&self) -> Vec<u8> {
248        self.signing_key.verifying_key().to_sec1_bytes().to_vec()
249    }
250
251    fn sign_digest(&self, digest: &[u8; 32]) -> Result<XrpSignature, SignerError> {
252        use k256::ecdsa::signature::hazmat::PrehashSigner;
253        let sig: k256::ecdsa::Signature = self
254            .signing_key
255            .sign_prehash(digest)
256            .map_err(|e| SignerError::SigningFailed(e.to_string()))?;
257        Ok(XrpSignature {
258            bytes: sig.to_der().as_bytes().to_vec(),
259        })
260    }
261}
262
263impl traits::Signer for XrpEcdsaSigner {
264    type Signature = XrpSignature;
265    type Error = SignerError;
266
267    fn sign(&self, message: &[u8]) -> Result<XrpSignature, SignerError> {
268        let digest = sha512_half(message);
269        self.sign_digest(&digest)
270    }
271
272    fn sign_prehashed(&self, digest: &[u8]) -> Result<XrpSignature, SignerError> {
273        if digest.len() != 32 {
274            return Err(SignerError::InvalidHashLength {
275                expected: 32,
276                got: digest.len(),
277            });
278        }
279        let mut hash = [0u8; 32];
280        hash.copy_from_slice(digest);
281        self.sign_digest(&hash)
282    }
283
284    fn public_key_bytes(&self) -> Vec<u8> {
285        self.public_key_bytes_inner()
286    }
287
288    fn public_key_bytes_uncompressed(&self) -> Vec<u8> {
289        self.signing_key
290            .verifying_key()
291            .to_encoded_point(false)
292            .as_bytes()
293            .to_vec()
294    }
295}
296
297impl traits::KeyPair for XrpEcdsaSigner {
298    fn generate() -> Result<Self, SignerError> {
299        let mut key_bytes = zeroize::Zeroizing::new([0u8; 32]);
300        crate::security::secure_random(&mut *key_bytes)?;
301        let signing_key = k256::ecdsa::SigningKey::from_bytes((&*key_bytes).into())
302            .map_err(|e| SignerError::InvalidPrivateKey(e.to_string()))?;
303        Ok(Self { signing_key })
304    }
305
306    fn from_bytes(private_key: &[u8]) -> Result<Self, SignerError> {
307        if private_key.len() != 32 {
308            return Err(SignerError::InvalidPrivateKey(format!(
309                "expected 32 bytes, got {}",
310                private_key.len()
311            )));
312        }
313        let signing_key = k256::ecdsa::SigningKey::from_bytes(private_key.into())
314            .map_err(|e| SignerError::InvalidPrivateKey(e.to_string()))?;
315        Ok(Self { signing_key })
316    }
317
318    fn private_key_bytes(&self) -> Zeroizing<Vec<u8>> {
319        Zeroizing::new(self.signing_key.to_bytes().to_vec())
320    }
321}
322
323/// XRP ECDSA verifier.
324pub struct XrpEcdsaVerifier {
325    verifying_key: k256::ecdsa::VerifyingKey,
326}
327
328impl XrpEcdsaVerifier {
329    /// Create from SEC1 public key bytes.
330    pub fn from_public_key_bytes(bytes: &[u8]) -> Result<Self, SignerError> {
331        let verifying_key = k256::ecdsa::VerifyingKey::from_sec1_bytes(bytes)
332            .map_err(|e| SignerError::InvalidPublicKey(e.to_string()))?;
333        Ok(Self { verifying_key })
334    }
335}
336
337impl traits::Verifier for XrpEcdsaVerifier {
338    type Signature = XrpSignature;
339    type Error = SignerError;
340
341    fn verify(&self, message: &[u8], signature: &XrpSignature) -> Result<bool, SignerError> {
342        let digest = sha512_half(message);
343        self.verify_prehashed(&digest, signature)
344    }
345
346    fn verify_prehashed(
347        &self,
348        digest: &[u8],
349        signature: &XrpSignature,
350    ) -> Result<bool, SignerError> {
351        use k256::ecdsa::signature::hazmat::PrehashVerifier;
352        if digest.len() != 32 {
353            return Err(SignerError::InvalidHashLength {
354                expected: 32,
355                got: digest.len(),
356            });
357        }
358        let sig = k256::ecdsa::Signature::from_der(&signature.bytes)
359            .map_err(|e| SignerError::InvalidSignature(e.to_string()))?;
360        match self.verifying_key.verify_prehash(digest, &sig) {
361            Ok(()) => Ok(true),
362            Err(_) => Ok(false),
363        }
364    }
365}
366
367// ─── Ed25519 ─────────────────────────────────────────────────────────────────
368
369/// XRP Ed25519 signer (pure Ed25519).
370pub struct XrpEddsaSigner {
371    signing_key: ed25519_dalek::SigningKey,
372}
373
374impl Drop for XrpEddsaSigner {
375    fn drop(&mut self) {
376        // ed25519_dalek::SigningKey handles its own zeroization
377    }
378}
379
380impl XrpEddsaSigner {
381    /// Derive the XRP account ID from this signer's Ed25519 public key.
382    /// XRP prefixes Ed25519 pubkeys with 0xED before hashing.
383    pub fn account_id(&self) -> [u8; 20] {
384        let vk = self.signing_key.verifying_key();
385        let mut prefixed = Vec::with_capacity(33);
386        prefixed.push(0xED);
387        prefixed.extend_from_slice(vk.as_bytes());
388        account_id(&prefixed)
389    }
390
391    /// Return the XRP `r...` address string.
392    pub fn address(&self) -> Result<String, SignerError> {
393        xrp_address(&self.account_id())
394    }
395}
396
397impl traits::Signer for XrpEddsaSigner {
398    type Signature = XrpSignature;
399    type Error = SignerError;
400
401    fn sign(&self, message: &[u8]) -> Result<XrpSignature, SignerError> {
402        use ed25519_dalek::Signer as DalekSigner;
403        let sig = DalekSigner::sign(&self.signing_key, message);
404        Ok(XrpSignature {
405            bytes: sig.to_bytes().to_vec(),
406        })
407    }
408
409    /// **Note:** Ed25519 hashes internally per RFC 8032. This method is identical to
410    /// `sign()` — the `digest` parameter is treated as a raw message, not a
411    /// pre-computed hash. For consistency with the `Signer` trait, this is provided as-is.
412    fn sign_prehashed(&self, digest: &[u8]) -> Result<XrpSignature, SignerError> {
413        // Ed25519 has no internal hashing step in XRP context,
414        // so prehashed == raw sign
415        self.sign(digest)
416    }
417
418    fn public_key_bytes(&self) -> Vec<u8> {
419        self.signing_key.verifying_key().as_bytes().to_vec()
420    }
421
422    fn public_key_bytes_uncompressed(&self) -> Vec<u8> {
423        // Ed25519 has no uncompressed form
424        self.public_key_bytes()
425    }
426}
427
428impl traits::KeyPair for XrpEddsaSigner {
429    fn generate() -> Result<Self, SignerError> {
430        let mut key_bytes = zeroize::Zeroizing::new([0u8; 32]);
431        crate::security::secure_random(&mut *key_bytes)?;
432        let signing_key = ed25519_dalek::SigningKey::from_bytes(&key_bytes);
433        Ok(Self { signing_key })
434    }
435
436    fn from_bytes(private_key: &[u8]) -> Result<Self, SignerError> {
437        if private_key.len() != 32 {
438            return Err(SignerError::InvalidPrivateKey(format!(
439                "expected 32 bytes, got {}",
440                private_key.len()
441            )));
442        }
443        let mut bytes = [0u8; 32];
444        bytes.copy_from_slice(private_key);
445        let signing_key = ed25519_dalek::SigningKey::from_bytes(&bytes);
446        Ok(Self { signing_key })
447    }
448
449    fn private_key_bytes(&self) -> Zeroizing<Vec<u8>> {
450        Zeroizing::new(self.signing_key.to_bytes().to_vec())
451    }
452}
453
454/// XRP Ed25519 verifier.
455pub struct XrpEddsaVerifier {
456    verifying_key: ed25519_dalek::VerifyingKey,
457}
458
459impl XrpEddsaVerifier {
460    /// Create from 32-byte Ed25519 public key.
461    pub fn from_public_key_bytes(bytes: &[u8]) -> Result<Self, SignerError> {
462        if bytes.len() != 32 {
463            return Err(SignerError::InvalidPublicKey(format!(
464                "expected 32 bytes, got {}",
465                bytes.len()
466            )));
467        }
468        let mut pk_bytes = [0u8; 32];
469        pk_bytes.copy_from_slice(bytes);
470        let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(&pk_bytes)
471            .map_err(|e| SignerError::InvalidPublicKey(e.to_string()))?;
472        Ok(Self { verifying_key })
473    }
474}
475
476impl traits::Verifier for XrpEddsaVerifier {
477    type Signature = XrpSignature;
478    type Error = SignerError;
479
480    fn verify(&self, message: &[u8], signature: &XrpSignature) -> Result<bool, SignerError> {
481        self.verify_prehashed(message, signature)
482    }
483
484    fn verify_prehashed(
485        &self,
486        digest: &[u8],
487        signature: &XrpSignature,
488    ) -> Result<bool, SignerError> {
489        use ed25519_dalek::Verifier as DalekVerifier;
490        if signature.bytes.len() != 64 {
491            return Err(SignerError::InvalidSignature(format!(
492                "expected 64 bytes, got {}",
493                signature.bytes.len()
494            )));
495        }
496        let mut sig_bytes = [0u8; 64];
497        sig_bytes.copy_from_slice(&signature.bytes);
498        let sig = ed25519_dalek::Signature::from_bytes(&sig_bytes);
499        match DalekVerifier::verify(&self.verifying_key, digest, &sig) {
500            Ok(()) => Ok(true),
501            Err(_) => Ok(false),
502        }
503    }
504}
505
506#[cfg(test)]
507#[allow(clippy::unwrap_used, clippy::expect_used)]
508mod tests {
509    use super::*;
510    use crate::traits::{KeyPair, Signer, Verifier};
511
512    #[test]
513    fn test_ecdsa_generate() {
514        let signer = XrpEcdsaSigner::generate().unwrap();
515        assert_eq!(signer.public_key_bytes().len(), 33);
516    }
517
518    #[test]
519    fn test_eddsa_generate() {
520        let signer = XrpEddsaSigner::generate().unwrap();
521        assert_eq!(signer.public_key_bytes().len(), 32);
522    }
523
524    #[test]
525    fn test_ecdsa_sign_verify() {
526        let signer = XrpEcdsaSigner::generate().unwrap();
527        let sig = signer.sign(b"hello xrp").unwrap();
528        let verifier = XrpEcdsaVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
529        assert!(verifier.verify(b"hello xrp", &sig).unwrap());
530    }
531
532    #[test]
533    fn test_eddsa_sign_verify() {
534        let signer = XrpEddsaSigner::generate().unwrap();
535        let sig = signer.sign(b"hello xrp ed25519").unwrap();
536        let verifier = XrpEddsaVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
537        assert!(verifier.verify(b"hello xrp ed25519", &sig).unwrap());
538    }
539
540    #[test]
541    fn test_sha512_half() {
542        let result = sha512_half(b"hello");
543        assert_eq!(result.len(), 32);
544        // SHA-512("hello") first 32 bytes
545        let full = Sha512::digest(b"hello");
546        assert_eq!(&result[..], &full[..32]);
547    }
548
549    #[test]
550    fn test_account_id_ecdsa() {
551        let signer = XrpEcdsaSigner::generate().unwrap();
552        let id = signer.account_id();
553        assert_eq!(id.len(), 20);
554    }
555
556    #[test]
557    fn test_account_id_eddsa() {
558        let signer = XrpEddsaSigner::generate().unwrap();
559        let id = signer.account_id();
560        assert_eq!(id.len(), 20);
561    }
562
563    #[test]
564    fn test_invalid_key_rejected() {
565        assert!(XrpEcdsaSigner::from_bytes(&[0u8; 32]).is_err());
566        assert!(XrpEcdsaSigner::from_bytes(&[1u8; 31]).is_err());
567        assert!(XrpEddsaSigner::from_bytes(&[1u8; 31]).is_err());
568    }
569
570    #[test]
571    fn test_tampered_sig_fails_ecdsa() {
572        let signer = XrpEcdsaSigner::generate().unwrap();
573        let sig = signer.sign(b"tamper").unwrap();
574        let verifier = XrpEcdsaVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
575        let mut tampered = sig.clone();
576        if let Some(b) = tampered.bytes.last_mut() {
577            *b ^= 0xff;
578        }
579        let result = verifier.verify(b"tamper", &tampered);
580        assert!(result.is_err() || !result.unwrap());
581    }
582
583    #[test]
584    fn test_tampered_sig_fails_eddsa() {
585        let signer = XrpEddsaSigner::generate().unwrap();
586        let sig = signer.sign(b"tamper").unwrap();
587        let verifier = XrpEddsaVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
588        let mut tampered = sig.clone();
589        tampered.bytes[0] ^= 0xff;
590        let result = verifier.verify(b"tamper", &tampered);
591        assert!(result.is_err() || !result.unwrap());
592    }
593
594    #[test]
595    fn test_sign_prehashed_ecdsa() {
596        let signer = XrpEcdsaSigner::generate().unwrap();
597        let msg = b"prehash test";
598        let digest = sha512_half(msg);
599        let sig = signer.sign_prehashed(&digest).unwrap();
600        let verifier = XrpEcdsaVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
601        assert!(verifier.verify_prehashed(&digest, &sig).unwrap());
602    }
603
604    #[test]
605    fn test_zeroize_on_drop_ecdsa() {
606        let signer = XrpEcdsaSigner::generate().unwrap();
607        let _: Zeroizing<Vec<u8>> = signer.private_key_bytes();
608        drop(signer);
609    }
610
611    #[test]
612    fn test_zeroize_on_drop_eddsa() {
613        let signer = XrpEddsaSigner::generate().unwrap();
614        let _: Zeroizing<Vec<u8>> = signer.private_key_bytes();
615        drop(signer);
616    }
617
618    // RFC 8032 Ed25519 test vector (reused for XRP Ed25519)
619    #[test]
620    fn test_rfc8032_vector_xrp_eddsa() {
621        let sk = hex::decode("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")
622            .unwrap();
623        let expected_sig = hex::decode(
624            "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b"
625        ).unwrap();
626
627        let signer = XrpEddsaSigner::from_bytes(&sk).unwrap();
628        let sig = signer.sign(b"").unwrap(); // empty message
629        assert_eq!(sig.bytes, expected_sig);
630    }
631
632    // ─── XRP Known Address Vectors ──────────────────────────────
633
634    #[test]
635    fn test_xrp_ecdsa_address_format() {
636        let signer = XrpEcdsaSigner::generate().unwrap();
637        let addr = signer.address().unwrap();
638        assert!(addr.starts_with('r'), "XRP address must start with 'r'");
639        assert!(addr.len() >= 25 && addr.len() <= 35);
640        assert!(validate_address(&addr));
641    }
642
643    #[test]
644    fn test_xrp_eddsa_address_format() {
645        let signer = XrpEddsaSigner::generate().unwrap();
646        let addr = signer.address().unwrap();
647        assert!(addr.starts_with('r'));
648        assert!(validate_address(&addr));
649    }
650
651    #[test]
652    fn test_xrp_address_validation_edges() {
653        assert!(!validate_address(""));
654        assert!(!validate_address("1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH")); // Bitcoin, not XRP
655        assert!(!validate_address("rINVALID")); // too short/invalid
656    }
657
658    #[test]
659    fn test_sha512_half_deterministic() {
660        let h1 = sha512_half(b"test");
661        let h2 = sha512_half(b"test");
662        assert_eq!(h1, h2);
663        assert_eq!(h1.len(), 32);
664    }
665
666    // ─── X-Address Tests ────────────────────────────────────────
667
668    #[test]
669    fn test_x_address_roundtrip_no_tag() {
670        let account = [0xAA; 20];
671        let x_addr = encode_x_address(&account, None, false).unwrap();
672        let (decoded_acct, tag, testnet) = decode_x_address(&x_addr).unwrap();
673        assert_eq!(decoded_acct, account);
674        assert!(tag.is_none());
675        assert!(!testnet);
676    }
677
678    #[test]
679    fn test_x_address_roundtrip_with_tag() {
680        let account = [0xBB; 20];
681        let x_addr = encode_x_address(&account, Some(12345), false).unwrap();
682        let (decoded_acct, tag, testnet) = decode_x_address(&x_addr).unwrap();
683        assert_eq!(decoded_acct, account);
684        assert_eq!(tag, Some(12345));
685        assert!(!testnet);
686    }
687
688    #[test]
689    fn test_x_address_testnet() {
690        let account = [0xCC; 20];
691        let x_addr = encode_x_address(&account, None, true).unwrap();
692        let (_, _, testnet) = decode_x_address(&x_addr).unwrap();
693        assert!(testnet);
694    }
695
696    #[test]
697    fn test_x_address_mainnet_vs_testnet() {
698        let account = [0xDD; 20];
699        let main = encode_x_address(&account, None, false).unwrap();
700        let test = encode_x_address(&account, None, true).unwrap();
701        assert_ne!(main, test);
702    }
703
704    #[test]
705    fn test_x_address_from_ecdsa_signer() {
706        let signer = XrpEcdsaSigner::generate().unwrap();
707        let acct_id = signer.account_id();
708        let x_addr = encode_x_address(&acct_id, Some(42), false).unwrap();
709        let (decoded_acct, tag, _) = decode_x_address(&x_addr).unwrap();
710        assert_eq!(decoded_acct, acct_id);
711        assert_eq!(tag, Some(42));
712    }
713
714    // ─── Official Test Vectors (xrpl.org) ───────────────────────
715
716    /// Known-good test vector from xrpl.org:
717    /// Classic address: rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh (Genesis Account)
718    /// Account ID hex:  b5f762798a53d543a014caf8b297cff8f2f937e8
719    /// X-address (mainnet, no tag): X7AcgcsBL6XDcUb289X4mJ8djcdyKaB5hJDWMArnXr61cqh
720    #[test]
721    fn test_xrp_classic_address_known_vector() {
722        let account_id = hex::decode("b5f762798a53d543a014caf8b297cff8f2f937e8").unwrap();
723        let mut acct = [0u8; 20];
724        acct.copy_from_slice(&account_id);
725        let addr = xrp_address(&acct).unwrap();
726        assert_eq!(
727            addr, "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
728            "Classic address must match xrpl.org Genesis Account"
729        );
730        assert!(validate_address(&addr));
731    }
732
733    #[test]
734    fn test_xrp_x_address_known_vector_no_tag() {
735        let account_id = hex::decode("b5f762798a53d543a014caf8b297cff8f2f937e8").unwrap();
736        let mut acct = [0u8; 20];
737        acct.copy_from_slice(&account_id);
738
739        // Mainnet, no destination tag
740        let x_addr = encode_x_address(&acct, None, false).unwrap();
741
742        // Must start with 'X' for mainnet
743        assert!(
744            x_addr.starts_with('X'),
745            "mainnet X-address must start with X"
746        );
747
748        // Decode back and verify roundtrip preserves all fields
749        let (decoded_acct, tag, is_testnet) = decode_x_address(&x_addr).unwrap();
750        assert_eq!(decoded_acct, acct, "account ID must survive roundtrip");
751        assert!(tag.is_none(), "no-tag must decode as None");
752        assert!(!is_testnet, "mainnet flag must survive roundtrip");
753    }
754
755    #[test]
756    fn test_xrp_x_address_roundtrip_with_known_acct() {
757        let account_id = hex::decode("b5f762798a53d543a014caf8b297cff8f2f937e8").unwrap();
758        let mut acct = [0u8; 20];
759        acct.copy_from_slice(&account_id);
760
761        // Encode with a tag and decode
762        let x_addr = encode_x_address(&acct, Some(12345), false).unwrap();
763        let (decoded_acct, tag, is_testnet) = decode_x_address(&x_addr).unwrap();
764        assert_eq!(decoded_acct, acct);
765        assert_eq!(tag, Some(12345));
766        assert!(!is_testnet);
767    }
768
769    #[test]
770    fn test_xrp_x_address_decode_invalid() {
771        // Truncated
772        assert!(decode_x_address("X7Acg").is_err());
773        // Random invalid
774        assert!(decode_x_address("XXXXXXXXXXX").is_err());
775    }
776}