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 (constant-time comparison)
192    let checksum = crypto::double_sha256(&decoded[..31]);
193    use subtle::ConstantTimeEq;
194    if decoded[31..35].ct_eq(&checksum[..4]).unwrap_u8() != 1 {
195        return Err(SignerError::ParseError("X-address: bad checksum".into()));
196    }
197
198    // Parse prefix
199    let is_testnet = match (decoded[0], decoded[1]) {
200        (0x05, 0x44) => false, // mainnet
201        (0x05, 0x93) => true,  // testnet
202        _ => return Err(SignerError::ParseError("X-address: unknown prefix".into())),
203    };
204
205    // Account ID
206    let mut account = [0u8; 20];
207    account.copy_from_slice(&decoded[2..22]);
208
209    // Tag
210    let tag = if decoded[22] == 0x01 {
211        Some(u32::from_le_bytes([
212            decoded[23],
213            decoded[24],
214            decoded[25],
215            decoded[26],
216        ]))
217    } else {
218        None
219    };
220
221    Ok((account, tag, is_testnet))
222}
223
224// ─── ECDSA (secp256k1) ──────────────────────────────────────────────────────
225
226/// XRP ECDSA signer (secp256k1 + SHA-512 half).
227pub struct XrpEcdsaSigner {
228    signing_key: k256::ecdsa::SigningKey,
229}
230
231impl Drop for XrpEcdsaSigner {
232    fn drop(&mut self) {
233        // k256::SigningKey implements ZeroizeOnDrop internally
234    }
235}
236
237impl XrpEcdsaSigner {
238    /// Derive the XRP account ID from this signer's public key.
239    pub fn account_id(&self) -> [u8; 20] {
240        account_id(&self.public_key_bytes_inner())
241    }
242
243    /// Return the XRP `r...` address string.
244    pub fn address(&self) -> Result<String, SignerError> {
245        xrp_address(&self.account_id())
246    }
247
248    fn public_key_bytes_inner(&self) -> Vec<u8> {
249        self.signing_key.verifying_key().to_sec1_bytes().to_vec()
250    }
251
252    fn sign_digest(&self, digest: &[u8; 32]) -> Result<XrpSignature, SignerError> {
253        use k256::ecdsa::signature::hazmat::PrehashSigner;
254        let sig: k256::ecdsa::Signature = self
255            .signing_key
256            .sign_prehash(digest)
257            .map_err(|e| SignerError::SigningFailed(e.to_string()))?;
258        Ok(XrpSignature {
259            bytes: sig.to_der().as_bytes().to_vec(),
260        })
261    }
262}
263
264impl traits::Signer for XrpEcdsaSigner {
265    type Signature = XrpSignature;
266    type Error = SignerError;
267
268    fn sign(&self, message: &[u8]) -> Result<XrpSignature, SignerError> {
269        let digest = sha512_half(message);
270        self.sign_digest(&digest)
271    }
272
273    fn sign_prehashed(&self, digest: &[u8]) -> Result<XrpSignature, SignerError> {
274        if digest.len() != 32 {
275            return Err(SignerError::InvalidHashLength {
276                expected: 32,
277                got: digest.len(),
278            });
279        }
280        let mut hash = [0u8; 32];
281        hash.copy_from_slice(digest);
282        self.sign_digest(&hash)
283    }
284
285    fn public_key_bytes(&self) -> Vec<u8> {
286        self.public_key_bytes_inner()
287    }
288
289    fn public_key_bytes_uncompressed(&self) -> Vec<u8> {
290        self.signing_key
291            .verifying_key()
292            .to_encoded_point(false)
293            .as_bytes()
294            .to_vec()
295    }
296}
297
298impl traits::KeyPair for XrpEcdsaSigner {
299    fn generate() -> Result<Self, SignerError> {
300        let mut key_bytes = zeroize::Zeroizing::new([0u8; 32]);
301        crate::security::secure_random(&mut *key_bytes)?;
302        let signing_key = k256::ecdsa::SigningKey::from_bytes((&*key_bytes).into())
303            .map_err(|e| SignerError::InvalidPrivateKey(e.to_string()))?;
304        Ok(Self { signing_key })
305    }
306
307    fn from_bytes(private_key: &[u8]) -> Result<Self, SignerError> {
308        if private_key.len() != 32 {
309            return Err(SignerError::InvalidPrivateKey(format!(
310                "expected 32 bytes, got {}",
311                private_key.len()
312            )));
313        }
314        let signing_key = k256::ecdsa::SigningKey::from_bytes(private_key.into())
315            .map_err(|e| SignerError::InvalidPrivateKey(e.to_string()))?;
316        Ok(Self { signing_key })
317    }
318
319    fn private_key_bytes(&self) -> Zeroizing<Vec<u8>> {
320        Zeroizing::new(self.signing_key.to_bytes().to_vec())
321    }
322}
323
324/// XRP ECDSA verifier.
325pub struct XrpEcdsaVerifier {
326    verifying_key: k256::ecdsa::VerifyingKey,
327}
328
329impl XrpEcdsaVerifier {
330    /// Create from SEC1 public key bytes.
331    pub fn from_public_key_bytes(bytes: &[u8]) -> Result<Self, SignerError> {
332        let verifying_key = k256::ecdsa::VerifyingKey::from_sec1_bytes(bytes)
333            .map_err(|e| SignerError::InvalidPublicKey(e.to_string()))?;
334        Ok(Self { verifying_key })
335    }
336}
337
338impl traits::Verifier for XrpEcdsaVerifier {
339    type Signature = XrpSignature;
340    type Error = SignerError;
341
342    fn verify(&self, message: &[u8], signature: &XrpSignature) -> Result<bool, SignerError> {
343        let digest = sha512_half(message);
344        self.verify_prehashed(&digest, signature)
345    }
346
347    fn verify_prehashed(
348        &self,
349        digest: &[u8],
350        signature: &XrpSignature,
351    ) -> Result<bool, SignerError> {
352        use k256::ecdsa::signature::hazmat::PrehashVerifier;
353        if digest.len() != 32 {
354            return Err(SignerError::InvalidHashLength {
355                expected: 32,
356                got: digest.len(),
357            });
358        }
359        let sig = k256::ecdsa::Signature::from_der(&signature.bytes)
360            .map_err(|e| SignerError::InvalidSignature(e.to_string()))?;
361        match self.verifying_key.verify_prehash(digest, &sig) {
362            Ok(()) => Ok(true),
363            Err(_) => Ok(false),
364        }
365    }
366}
367
368// ─── Ed25519 ─────────────────────────────────────────────────────────────────
369
370/// XRP Ed25519 signer (pure Ed25519).
371pub struct XrpEddsaSigner {
372    signing_key: ed25519_dalek::SigningKey,
373}
374
375impl Drop for XrpEddsaSigner {
376    fn drop(&mut self) {
377        // ed25519_dalek::SigningKey handles its own zeroization
378    }
379}
380
381impl XrpEddsaSigner {
382    /// Derive the XRP account ID from this signer's Ed25519 public key.
383    /// XRP prefixes Ed25519 pubkeys with 0xED before hashing.
384    pub fn account_id(&self) -> [u8; 20] {
385        let vk = self.signing_key.verifying_key();
386        let mut prefixed = Vec::with_capacity(33);
387        prefixed.push(0xED);
388        prefixed.extend_from_slice(vk.as_bytes());
389        account_id(&prefixed)
390    }
391
392    /// Return the XRP `r...` address string.
393    pub fn address(&self) -> Result<String, SignerError> {
394        xrp_address(&self.account_id())
395    }
396}
397
398impl traits::Signer for XrpEddsaSigner {
399    type Signature = XrpSignature;
400    type Error = SignerError;
401
402    fn sign(&self, message: &[u8]) -> Result<XrpSignature, SignerError> {
403        use ed25519_dalek::Signer as DalekSigner;
404        let sig = DalekSigner::sign(&self.signing_key, message);
405        Ok(XrpSignature {
406            bytes: sig.to_bytes().to_vec(),
407        })
408    }
409
410    /// **Note:** Ed25519 hashes internally per RFC 8032. This method is identical to
411    /// `sign()` — the `digest` parameter is treated as a raw message, not a
412    /// pre-computed hash. For consistency with the `Signer` trait, this is provided as-is.
413    fn sign_prehashed(&self, digest: &[u8]) -> Result<XrpSignature, SignerError> {
414        // Ed25519 has no internal hashing step in XRP context,
415        // so prehashed == raw sign
416        self.sign(digest)
417    }
418
419    fn public_key_bytes(&self) -> Vec<u8> {
420        self.signing_key.verifying_key().as_bytes().to_vec()
421    }
422
423    fn public_key_bytes_uncompressed(&self) -> Vec<u8> {
424        // Ed25519 has no uncompressed form
425        self.public_key_bytes()
426    }
427}
428
429impl traits::KeyPair for XrpEddsaSigner {
430    fn generate() -> Result<Self, SignerError> {
431        let mut key_bytes = zeroize::Zeroizing::new([0u8; 32]);
432        crate::security::secure_random(&mut *key_bytes)?;
433        let signing_key = ed25519_dalek::SigningKey::from_bytes(&key_bytes);
434        Ok(Self { signing_key })
435    }
436
437    fn from_bytes(private_key: &[u8]) -> Result<Self, SignerError> {
438        if private_key.len() != 32 {
439            return Err(SignerError::InvalidPrivateKey(format!(
440                "expected 32 bytes, got {}",
441                private_key.len()
442            )));
443        }
444        let mut bytes = [0u8; 32];
445        bytes.copy_from_slice(private_key);
446        let signing_key = ed25519_dalek::SigningKey::from_bytes(&bytes);
447        Ok(Self { signing_key })
448    }
449
450    fn private_key_bytes(&self) -> Zeroizing<Vec<u8>> {
451        Zeroizing::new(self.signing_key.to_bytes().to_vec())
452    }
453}
454
455/// XRP Ed25519 verifier.
456pub struct XrpEddsaVerifier {
457    verifying_key: ed25519_dalek::VerifyingKey,
458}
459
460impl XrpEddsaVerifier {
461    /// Create from 32-byte Ed25519 public key.
462    pub fn from_public_key_bytes(bytes: &[u8]) -> Result<Self, SignerError> {
463        if bytes.len() != 32 {
464            return Err(SignerError::InvalidPublicKey(format!(
465                "expected 32 bytes, got {}",
466                bytes.len()
467            )));
468        }
469        let mut pk_bytes = [0u8; 32];
470        pk_bytes.copy_from_slice(bytes);
471        let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(&pk_bytes)
472            .map_err(|e| SignerError::InvalidPublicKey(e.to_string()))?;
473        Ok(Self { verifying_key })
474    }
475}
476
477impl traits::Verifier for XrpEddsaVerifier {
478    type Signature = XrpSignature;
479    type Error = SignerError;
480
481    fn verify(&self, message: &[u8], signature: &XrpSignature) -> Result<bool, SignerError> {
482        self.verify_prehashed(message, signature)
483    }
484
485    fn verify_prehashed(
486        &self,
487        digest: &[u8],
488        signature: &XrpSignature,
489    ) -> Result<bool, SignerError> {
490        use ed25519_dalek::Verifier as DalekVerifier;
491        if signature.bytes.len() != 64 {
492            return Err(SignerError::InvalidSignature(format!(
493                "expected 64 bytes, got {}",
494                signature.bytes.len()
495            )));
496        }
497        let mut sig_bytes = [0u8; 64];
498        sig_bytes.copy_from_slice(&signature.bytes);
499        let sig = ed25519_dalek::Signature::from_bytes(&sig_bytes);
500        match DalekVerifier::verify(&self.verifying_key, digest, &sig) {
501            Ok(()) => Ok(true),
502            Err(_) => Ok(false),
503        }
504    }
505}
506
507#[cfg(test)]
508#[allow(clippy::unwrap_used, clippy::expect_used)]
509mod tests {
510    use super::*;
511    use crate::traits::{KeyPair, Signer, Verifier};
512
513    #[test]
514    fn test_ecdsa_generate() {
515        let signer = XrpEcdsaSigner::generate().unwrap();
516        assert_eq!(signer.public_key_bytes().len(), 33);
517    }
518
519    #[test]
520    fn test_eddsa_generate() {
521        let signer = XrpEddsaSigner::generate().unwrap();
522        assert_eq!(signer.public_key_bytes().len(), 32);
523    }
524
525    #[test]
526    fn test_ecdsa_sign_verify() {
527        let signer = XrpEcdsaSigner::generate().unwrap();
528        let sig = signer.sign(b"hello xrp").unwrap();
529        let verifier = XrpEcdsaVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
530        assert!(verifier.verify(b"hello xrp", &sig).unwrap());
531    }
532
533    #[test]
534    fn test_eddsa_sign_verify() {
535        let signer = XrpEddsaSigner::generate().unwrap();
536        let sig = signer.sign(b"hello xrp ed25519").unwrap();
537        let verifier = XrpEddsaVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
538        assert!(verifier.verify(b"hello xrp ed25519", &sig).unwrap());
539    }
540
541    #[test]
542    fn test_sha512_half() {
543        let result = sha512_half(b"hello");
544        assert_eq!(result.len(), 32);
545        // SHA-512("hello") first 32 bytes
546        let full = Sha512::digest(b"hello");
547        assert_eq!(&result[..], &full[..32]);
548    }
549
550    #[test]
551    fn test_account_id_ecdsa() {
552        let signer = XrpEcdsaSigner::generate().unwrap();
553        let id = signer.account_id();
554        assert_eq!(id.len(), 20);
555    }
556
557    #[test]
558    fn test_account_id_eddsa() {
559        let signer = XrpEddsaSigner::generate().unwrap();
560        let id = signer.account_id();
561        assert_eq!(id.len(), 20);
562    }
563
564    #[test]
565    fn test_invalid_key_rejected() {
566        assert!(XrpEcdsaSigner::from_bytes(&[0u8; 32]).is_err());
567        assert!(XrpEcdsaSigner::from_bytes(&[1u8; 31]).is_err());
568        assert!(XrpEddsaSigner::from_bytes(&[1u8; 31]).is_err());
569    }
570
571    #[test]
572    fn test_tampered_sig_fails_ecdsa() {
573        let signer = XrpEcdsaSigner::generate().unwrap();
574        let sig = signer.sign(b"tamper").unwrap();
575        let verifier = XrpEcdsaVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
576        let mut tampered = sig.clone();
577        if let Some(b) = tampered.bytes.last_mut() {
578            *b ^= 0xff;
579        }
580        let result = verifier.verify(b"tamper", &tampered);
581        assert!(result.is_err() || !result.unwrap());
582    }
583
584    #[test]
585    fn test_tampered_sig_fails_eddsa() {
586        let signer = XrpEddsaSigner::generate().unwrap();
587        let sig = signer.sign(b"tamper").unwrap();
588        let verifier = XrpEddsaVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
589        let mut tampered = sig.clone();
590        tampered.bytes[0] ^= 0xff;
591        let result = verifier.verify(b"tamper", &tampered);
592        assert!(result.is_err() || !result.unwrap());
593    }
594
595    #[test]
596    fn test_sign_prehashed_ecdsa() {
597        let signer = XrpEcdsaSigner::generate().unwrap();
598        let msg = b"prehash test";
599        let digest = sha512_half(msg);
600        let sig = signer.sign_prehashed(&digest).unwrap();
601        let verifier = XrpEcdsaVerifier::from_public_key_bytes(&signer.public_key_bytes()).unwrap();
602        assert!(verifier.verify_prehashed(&digest, &sig).unwrap());
603    }
604
605    #[test]
606    fn test_zeroize_on_drop_ecdsa() {
607        let signer = XrpEcdsaSigner::generate().unwrap();
608        let _: Zeroizing<Vec<u8>> = signer.private_key_bytes();
609        drop(signer);
610    }
611
612    #[test]
613    fn test_zeroize_on_drop_eddsa() {
614        let signer = XrpEddsaSigner::generate().unwrap();
615        let _: Zeroizing<Vec<u8>> = signer.private_key_bytes();
616        drop(signer);
617    }
618
619    // RFC 8032 Ed25519 test vector (reused for XRP Ed25519)
620    #[test]
621    fn test_rfc8032_vector_xrp_eddsa() {
622        let sk = hex::decode("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")
623            .unwrap();
624        let expected_sig = hex::decode(
625            "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b"
626        ).unwrap();
627
628        let signer = XrpEddsaSigner::from_bytes(&sk).unwrap();
629        let sig = signer.sign(b"").unwrap(); // empty message
630        assert_eq!(sig.bytes, expected_sig);
631    }
632
633    // ─── XRP Known Address Vectors ──────────────────────────────
634
635    #[test]
636    fn test_xrp_ecdsa_address_format() {
637        let signer = XrpEcdsaSigner::generate().unwrap();
638        let addr = signer.address().unwrap();
639        assert!(addr.starts_with('r'), "XRP address must start with 'r'");
640        assert!(addr.len() >= 25 && addr.len() <= 35);
641        assert!(validate_address(&addr));
642    }
643
644    #[test]
645    fn test_xrp_eddsa_address_format() {
646        let signer = XrpEddsaSigner::generate().unwrap();
647        let addr = signer.address().unwrap();
648        assert!(addr.starts_with('r'));
649        assert!(validate_address(&addr));
650    }
651
652    #[test]
653    fn test_xrp_address_validation_edges() {
654        assert!(!validate_address(""));
655        assert!(!validate_address("1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH")); // Bitcoin, not XRP
656        assert!(!validate_address("rINVALID")); // too short/invalid
657    }
658
659    #[test]
660    fn test_sha512_half_deterministic() {
661        let h1 = sha512_half(b"test");
662        let h2 = sha512_half(b"test");
663        assert_eq!(h1, h2);
664        assert_eq!(h1.len(), 32);
665    }
666
667    // ─── X-Address Tests ────────────────────────────────────────
668
669    #[test]
670    fn test_x_address_roundtrip_no_tag() {
671        let account = [0xAA; 20];
672        let x_addr = encode_x_address(&account, None, false).unwrap();
673        let (decoded_acct, tag, testnet) = decode_x_address(&x_addr).unwrap();
674        assert_eq!(decoded_acct, account);
675        assert!(tag.is_none());
676        assert!(!testnet);
677    }
678
679    #[test]
680    fn test_x_address_roundtrip_with_tag() {
681        let account = [0xBB; 20];
682        let x_addr = encode_x_address(&account, Some(12345), false).unwrap();
683        let (decoded_acct, tag, testnet) = decode_x_address(&x_addr).unwrap();
684        assert_eq!(decoded_acct, account);
685        assert_eq!(tag, Some(12345));
686        assert!(!testnet);
687    }
688
689    #[test]
690    fn test_x_address_testnet() {
691        let account = [0xCC; 20];
692        let x_addr = encode_x_address(&account, None, true).unwrap();
693        let (_, _, testnet) = decode_x_address(&x_addr).unwrap();
694        assert!(testnet);
695    }
696
697    #[test]
698    fn test_x_address_mainnet_vs_testnet() {
699        let account = [0xDD; 20];
700        let main = encode_x_address(&account, None, false).unwrap();
701        let test = encode_x_address(&account, None, true).unwrap();
702        assert_ne!(main, test);
703    }
704
705    #[test]
706    fn test_x_address_from_ecdsa_signer() {
707        let signer = XrpEcdsaSigner::generate().unwrap();
708        let acct_id = signer.account_id();
709        let x_addr = encode_x_address(&acct_id, Some(42), false).unwrap();
710        let (decoded_acct, tag, _) = decode_x_address(&x_addr).unwrap();
711        assert_eq!(decoded_acct, acct_id);
712        assert_eq!(tag, Some(42));
713    }
714
715    // ─── Official Test Vectors (xrpl.org) ───────────────────────
716
717    /// Known-good test vector from xrpl.org:
718    /// Classic address: rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh (Genesis Account)
719    /// Account ID hex:  b5f762798a53d543a014caf8b297cff8f2f937e8
720    /// X-address (mainnet, no tag): X7AcgcsBL6XDcUb289X4mJ8djcdyKaB5hJDWMArnXr61cqh
721    #[test]
722    fn test_xrp_classic_address_known_vector() {
723        let account_id = hex::decode("b5f762798a53d543a014caf8b297cff8f2f937e8").unwrap();
724        let mut acct = [0u8; 20];
725        acct.copy_from_slice(&account_id);
726        let addr = xrp_address(&acct).unwrap();
727        assert_eq!(
728            addr, "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh",
729            "Classic address must match xrpl.org Genesis Account"
730        );
731        assert!(validate_address(&addr));
732    }
733
734    #[test]
735    fn test_xrp_x_address_known_vector_no_tag() {
736        let account_id = hex::decode("b5f762798a53d543a014caf8b297cff8f2f937e8").unwrap();
737        let mut acct = [0u8; 20];
738        acct.copy_from_slice(&account_id);
739
740        // Mainnet, no destination tag
741        let x_addr = encode_x_address(&acct, None, false).unwrap();
742
743        // Must start with 'X' for mainnet
744        assert!(
745            x_addr.starts_with('X'),
746            "mainnet X-address must start with X"
747        );
748
749        // Decode back and verify roundtrip preserves all fields
750        let (decoded_acct, tag, is_testnet) = decode_x_address(&x_addr).unwrap();
751        assert_eq!(decoded_acct, acct, "account ID must survive roundtrip");
752        assert!(tag.is_none(), "no-tag must decode as None");
753        assert!(!is_testnet, "mainnet flag must survive roundtrip");
754    }
755
756    #[test]
757    fn test_xrp_x_address_roundtrip_with_known_acct() {
758        let account_id = hex::decode("b5f762798a53d543a014caf8b297cff8f2f937e8").unwrap();
759        let mut acct = [0u8; 20];
760        acct.copy_from_slice(&account_id);
761
762        // Encode with a tag and decode
763        let x_addr = encode_x_address(&acct, Some(12345), false).unwrap();
764        let (decoded_acct, tag, is_testnet) = decode_x_address(&x_addr).unwrap();
765        assert_eq!(decoded_acct, acct);
766        assert_eq!(tag, Some(12345));
767        assert!(!is_testnet);
768    }
769
770    #[test]
771    fn test_xrp_x_address_decode_invalid() {
772        // Truncated
773        assert!(decode_x_address("X7Acg").is_err());
774        // Random invalid
775        assert!(decode_x_address("XXXXXXXXXXX").is_err());
776    }
777}