1pub 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#[derive(Debug, Clone, PartialEq, Eq)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17#[must_use]
18pub struct XrpSignature {
19 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 pub fn to_bytes(&self) -> Vec<u8> {
36 self.bytes.clone()
37 }
38
39 pub fn from_bytes(bytes: &[u8]) -> Result<Self, SignerError> {
44 if bytes.is_empty() {
45 return Err(SignerError::InvalidSignature("empty signature".into()));
46 }
47 if bytes[0] == 0x30 {
49 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
69pub 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
77pub fn account_id(pubkey_bytes: &[u8]) -> [u8; 20] {
79 crypto::hash160(pubkey_bytes)
80}
81
82fn xrp_alphabet() -> Result<bs58::Alphabet, SignerError> {
84 bs58::Alphabet::new(b"rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz")
85 .map_err(|e| SignerError::InvalidPublicKey(format!("XRP alphabet: {e}")))
86}
87
88pub fn xrp_address(account_id: &[u8; 20]) -> Result<String, SignerError> {
92 let mut payload = vec![0x00u8]; payload.extend_from_slice(account_id);
94 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
102pub 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
125pub 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 if is_testnet {
145 payload.extend_from_slice(&[0x05, 0x93]);
146 } else {
147 payload.extend_from_slice(&[0x05, 0x44]);
148 }
149
150 payload.extend_from_slice(account_id);
152
153 match tag {
155 Some(t) => {
156 payload.push(0x01); payload.extend_from_slice(&t.to_le_bytes()); payload.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); }
160 None => {
161 payload.push(0x00); payload.extend_from_slice(&[0x00; 8]); }
164 }
165
166 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
175pub 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 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 let is_testnet = match (decoded[0], decoded[1]) {
200 (0x05, 0x44) => false, (0x05, 0x93) => true, _ => return Err(SignerError::ParseError("X-address: unknown prefix".into())),
203 };
204
205 let mut account = [0u8; 20];
207 account.copy_from_slice(&decoded[2..22]);
208
209 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
224pub struct XrpEcdsaSigner {
228 signing_key: k256::ecdsa::SigningKey,
229}
230
231impl Drop for XrpEcdsaSigner {
232 fn drop(&mut self) {
233 }
235}
236
237impl XrpEcdsaSigner {
238 pub fn account_id(&self) -> [u8; 20] {
240 account_id(&self.public_key_bytes_inner())
241 }
242
243 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
324pub struct XrpEcdsaVerifier {
326 verifying_key: k256::ecdsa::VerifyingKey,
327}
328
329impl XrpEcdsaVerifier {
330 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
368pub struct XrpEddsaSigner {
372 signing_key: ed25519_dalek::SigningKey,
373}
374
375impl Drop for XrpEddsaSigner {
376 fn drop(&mut self) {
377 }
379}
380
381impl XrpEddsaSigner {
382 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 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 fn sign_prehashed(&self, digest: &[u8]) -> Result<XrpSignature, SignerError> {
414 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 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
455pub struct XrpEddsaVerifier {
457 verifying_key: ed25519_dalek::VerifyingKey,
458}
459
460impl XrpEddsaVerifier {
461 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 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 #[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(); assert_eq!(sig.bytes, expected_sig);
631 }
632
633 #[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")); assert!(!validate_address("rINVALID")); }
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 #[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 #[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 let x_addr = encode_x_address(&acct, None, false).unwrap();
742
743 assert!(
745 x_addr.starts_with('X'),
746 "mainnet X-address must start with X"
747 );
748
749 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 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 assert!(decode_x_address("X7Acg").is_err());
774 assert!(decode_x_address("XXXXXXXXXXX").is_err());
776 }
777}