1use crate::error::SignerError;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18#[repr(u8)]
19pub enum FieldType {
20 UInt16 = 1,
22 UInt32 = 2,
24 Amount = 6,
26 Blob = 7,
28 AccountID = 8,
30 Hash256 = 5,
32}
33
34#[must_use]
39pub fn encode_field_id(type_code: u8, field_code: u8) -> Vec<u8> {
40 if type_code < 16 && field_code < 16 {
41 vec![(type_code << 4) | field_code]
42 } else if type_code < 16 {
43 vec![type_code << 4, field_code]
44 } else if field_code < 16 {
45 vec![field_code, type_code]
46 } else {
47 vec![0, type_code, field_code]
48 }
49}
50
51#[must_use]
53pub fn encode_uint32(type_code: u8, field_code: u8, value: u32) -> Vec<u8> {
54 let mut buf = encode_field_id(type_code, field_code);
55 buf.extend_from_slice(&value.to_be_bytes());
56 buf
57}
58
59#[must_use]
61pub fn encode_uint16(type_code: u8, field_code: u8, value: u16) -> Vec<u8> {
62 let mut buf = encode_field_id(type_code, field_code);
63 buf.extend_from_slice(&value.to_be_bytes());
64 buf
65}
66
67#[must_use]
72pub fn encode_xrp_amount(drops: u64) -> Vec<u8> {
73 let encoded = drops | 0x4000_0000_0000_0000;
75 encoded.to_be_bytes().to_vec()
76}
77
78#[must_use]
80pub fn encode_account_id(type_code: u8, field_code: u8, account: &[u8; 20]) -> Vec<u8> {
81 let mut buf = encode_field_id(type_code, field_code);
82 buf.push(20); buf.extend_from_slice(account);
84 buf
85}
86
87#[must_use]
89pub fn encode_blob(type_code: u8, field_code: u8, data: &[u8]) -> Vec<u8> {
90 let mut buf = encode_field_id(type_code, field_code);
91 encode_vl_length(&mut buf, data.len());
92 buf.extend_from_slice(data);
93 buf
94}
95
96fn encode_vl_length(buf: &mut Vec<u8>, len: usize) {
97 if len <= 192 {
98 buf.push(len as u8);
99 } else if len <= 12_480 {
100 let adjusted = len - 193;
101 buf.push((adjusted >> 8) as u8 + 193);
102 buf.push((adjusted & 0xFF) as u8);
103 } else {
104 let adjusted = len - 12_481;
105 buf.push(241u8 + (adjusted >> 16) as u8);
106 buf.push(((adjusted >> 8) & 0xFF) as u8);
107 buf.push((adjusted & 0xFF) as u8);
108 }
109}
110
111mod fields {
117 pub const TRANSACTION_TYPE: (u8, u8) = (1, 2);
119 pub const FLAGS: (u8, u8) = (2, 2);
121 pub const SEQUENCE: (u8, u8) = (2, 4);
122 pub const LAST_LEDGER_SEQUENCE: (u8, u8) = (2, 27);
123 pub const AMOUNT: (u8, u8) = (6, 1);
125 pub const FEE: (u8, u8) = (6, 8);
126 pub const ACCOUNT: (u8, u8) = (8, 1);
128 pub const DESTINATION: (u8, u8) = (8, 3);
129}
130
131pub const TT_PAYMENT: u16 = 0;
133pub const TT_TRUST_SET: u16 = 20;
135
136pub fn serialize_payment(
146 account: &[u8; 20],
147 destination: &[u8; 20],
148 amount_drops: u64,
149 fee_drops: u64,
150 sequence: u32,
151 last_ledger_sequence: u32,
152) -> Vec<u8> {
153 let mut buf = Vec::new();
154
155 buf.extend_from_slice(&encode_uint16(
158 fields::TRANSACTION_TYPE.0,
159 fields::TRANSACTION_TYPE.1,
160 TT_PAYMENT,
161 ));
162 buf.extend_from_slice(&encode_uint32(fields::FLAGS.0, fields::FLAGS.1, 0));
164 buf.extend_from_slice(&encode_uint32(
165 fields::SEQUENCE.0,
166 fields::SEQUENCE.1,
167 sequence,
168 ));
169 buf.extend_from_slice(&encode_uint32(
170 fields::LAST_LEDGER_SEQUENCE.0,
171 fields::LAST_LEDGER_SEQUENCE.1,
172 last_ledger_sequence,
173 ));
174 let mut amount_field = encode_field_id(fields::AMOUNT.0, fields::AMOUNT.1);
176 amount_field.extend_from_slice(&encode_xrp_amount(amount_drops));
177 buf.extend_from_slice(&amount_field);
178
179 let mut fee_field = encode_field_id(fields::FEE.0, fields::FEE.1);
180 fee_field.extend_from_slice(&encode_xrp_amount(fee_drops));
181 buf.extend_from_slice(&fee_field);
182
183 buf.extend_from_slice(&encode_account_id(
185 fields::ACCOUNT.0,
186 fields::ACCOUNT.1,
187 account,
188 ));
189 buf.extend_from_slice(&encode_account_id(
190 fields::DESTINATION.0,
191 fields::DESTINATION.1,
192 destination,
193 ));
194
195 buf
196}
197
198#[derive(Debug, Clone)]
204pub struct IssuedAmount {
205 pub currency: [u8; 20],
207 pub issuer: [u8; 20],
209 pub value: String,
211}
212
213pub fn serialize_trust_set(
222 account: &[u8; 20],
223 limit_amount: &IssuedAmount,
224 fee_drops: u64,
225 sequence: u32,
226 last_ledger_sequence: u32,
227) -> Result<Vec<u8>, SignerError> {
228 let mut buf = Vec::new();
229
230 buf.extend_from_slice(&encode_uint16(
232 fields::TRANSACTION_TYPE.0,
233 fields::TRANSACTION_TYPE.1,
234 TT_TRUST_SET,
235 ));
236 buf.extend_from_slice(&encode_uint32(fields::FLAGS.0, fields::FLAGS.1, 0));
238 buf.extend_from_slice(&encode_uint32(
240 fields::SEQUENCE.0,
241 fields::SEQUENCE.1,
242 sequence,
243 ));
244 buf.extend_from_slice(&encode_uint32(
246 fields::LAST_LEDGER_SEQUENCE.0,
247 fields::LAST_LEDGER_SEQUENCE.1,
248 last_ledger_sequence,
249 ));
250 let mut fee_field = encode_field_id(fields::FEE.0, fields::FEE.1);
252 fee_field.extend_from_slice(&encode_xrp_amount(fee_drops));
253 buf.extend_from_slice(&fee_field);
254 buf.extend_from_slice(&encode_account_id(
256 fields::ACCOUNT.0,
257 fields::ACCOUNT.1,
258 account,
259 ));
260
261 buf.extend_from_slice(&encode_field_id(14, 3)); let encoded_amount = encode_issued_amount(limit_amount)?;
266 buf.extend_from_slice(&encoded_amount);
267
268 Ok(buf)
269}
270
271fn encode_issued_amount(amount: &IssuedAmount) -> Result<Vec<u8>, SignerError> {
272 let mut buf = Vec::new();
273 let value_bytes = encode_iou_value(&amount.value)?;
275 buf.extend_from_slice(&value_bytes);
276 buf.extend_from_slice(&amount.currency);
277 buf.extend_from_slice(&amount.issuer);
278 Ok(buf)
279}
280
281fn parse_xrpl_decimal(value: &str) -> Result<(bool, u64, i8), SignerError> {
286 let s = value.trim();
287 if s.is_empty() {
288 return Err(SignerError::ParseError("empty IOU value".into()));
289 }
290
291 let (negative, abs_str) = if let Some(rest) = s.strip_prefix('-') {
292 (true, rest)
293 } else {
294 (false, s)
295 };
296
297 let (int_part, frac_part) = if let Some((i, f)) = abs_str.split_once('.') {
299 (i, f)
300 } else {
301 (abs_str, "")
302 };
303
304 if !int_part.chars().all(|c| c.is_ascii_digit())
306 || !frac_part.chars().all(|c| c.is_ascii_digit())
307 {
308 return Err(SignerError::ParseError(format!(
309 "invalid IOU value: {value}"
310 )));
311 }
312 if int_part.is_empty() && frac_part.is_empty() {
313 return Err(SignerError::ParseError(format!(
314 "invalid IOU value: {value}"
315 )));
316 }
317
318 let combined = format!("{int_part}{frac_part}");
320 let frac_len = frac_part.len() as i32;
321
322 let stripped = combined.trim_start_matches('0');
324 if stripped.is_empty() {
325 return Ok((false, 0, 0));
327 }
328
329 let mut mantissa: u64 = stripped
331 .parse()
332 .map_err(|_| SignerError::ParseError(format!("IOU value too large: {value}")))?;
333 let mut exponent: i32 = -(frac_len) + (combined.len() as i32 - stripped.len() as i32);
334
335 const MIN_MANTISSA: u64 = 1_000_000_000_000_000;
337 const MAX_MANTISSA: u64 = 10_000_000_000_000_000;
338
339 while mantissa < MIN_MANTISSA {
340 mantissa *= 10;
341 exponent -= 1;
342 }
343 while mantissa >= MAX_MANTISSA {
344 mantissa /= 10;
345 exponent += 1;
346 }
347
348 if !(-96..=80).contains(&exponent) {
350 return Err(SignerError::ParseError(format!(
351 "IOU exponent {exponent} out of range [-96, 80]"
352 )));
353 }
354
355 Ok((negative, mantissa, exponent as i8))
356}
357
358fn encode_iou_value(value: &str) -> Result<[u8; 8], SignerError> {
359 let (negative, mantissa, exponent) = parse_xrpl_decimal(value)?;
360
361 if mantissa == 0 {
362 return Ok(0x8000_0000_0000_0000u64.to_be_bytes());
363 }
364
365 let mut encoded: u64 = 0x8000_0000_0000_0000; if !negative {
371 encoded |= 0x4000_0000_0000_0000; }
373 let biased_exp = (exponent as i32 + 97) as u64;
374 encoded |= (biased_exp & 0xFF) << 54;
375 encoded |= mantissa & 0x003F_FFFF_FFFF_FFFF;
376
377 Ok(encoded.to_be_bytes())
378}
379
380pub const MULTISIGN_PREFIX: [u8; 4] = [0x53, 0x54, 0x58, 0x00];
390
391pub fn multisign_hash(tx_blob: &[u8], signer_account: &[u8; 20]) -> [u8; 32] {
395 use sha2::{Digest, Sha512};
396 let mut hasher = Sha512::new();
397 hasher.update(MULTISIGN_PREFIX);
398 hasher.update(tx_blob);
399 hasher.update(signer_account);
400 let full = hasher.finalize();
401 let mut out = [0u8; 32];
402 out.copy_from_slice(&full[..32]);
403 out
404}
405
406#[derive(Debug, Clone)]
408pub struct SignerEntry {
409 pub account: [u8; 20],
411 pub weight: u16,
413}
414
415pub fn serialize_signer_list(signers: &[SignerEntry], quorum: u32) -> Vec<u8> {
419 let mut buf = Vec::new();
420 buf.extend_from_slice(&quorum.to_be_bytes());
421 for signer in signers {
422 buf.extend_from_slice(&signer.account);
423 buf.extend_from_slice(&signer.weight.to_be_bytes());
424 }
425 buf
426}
427
428#[cfg(test)]
433#[allow(clippy::unwrap_used, clippy::expect_used)]
434mod tests {
435 use super::*;
436
437 #[test]
440 fn test_field_id_compact() {
441 assert_eq!(encode_field_id(1, 2), vec![0x12]);
443 }
444
445 #[test]
446 fn test_field_id_large_field() {
447 assert_eq!(encode_field_id(1, 27), vec![0x10, 27]);
449 }
450
451 #[test]
452 fn test_xrp_amount_encoding() {
453 let drops = 1_000_000u64; let encoded = encode_xrp_amount(drops);
455 assert_eq!(encoded.len(), 8);
456 let val = u64::from_be_bytes(encoded.try_into().unwrap());
458 assert!(val & 0x4000_0000_0000_0000 != 0);
459 }
460
461 #[test]
462 fn test_uint32_encoding() {
463 let encoded = encode_uint32(2, 4, 42);
464 assert_eq!(encoded[0], 0x24); assert_eq!(&encoded[1..5], &42u32.to_be_bytes());
466 }
467
468 #[test]
469 fn test_account_id_encoding() {
470 let account = [0xAA; 20];
471 let encoded = encode_account_id(8, 1, &account);
472 assert_eq!(encoded[0], 0x81); assert_eq!(encoded[1], 20); assert_eq!(&encoded[2..22], &account);
475 }
476
477 #[test]
478 fn test_vl_length_small() {
479 let mut buf = Vec::new();
480 encode_vl_length(&mut buf, 10);
481 assert_eq!(buf, vec![10]);
482 }
483
484 #[test]
485 fn test_vl_length_medium() {
486 let mut buf = Vec::new();
487 encode_vl_length(&mut buf, 200);
488 assert_eq!(buf.len(), 2);
489 }
490
491 #[test]
494 fn test_payment_serialization() {
495 let from = [0xAA; 20];
496 let to = [0xBB; 20];
497 let blob = serialize_payment(&from, &to, 1_000_000, 12, 1, 100);
498 assert!(!blob.is_empty());
499 assert_eq!(blob[0], 0x12);
501 }
502
503 #[test]
504 fn test_payment_different_amount() {
505 let from = [0xAA; 20];
506 let to = [0xBB; 20];
507 let blob1 = serialize_payment(&from, &to, 1_000, 12, 1, 100);
508 let blob2 = serialize_payment(&from, &to, 2_000, 12, 1, 100);
509 assert_ne!(blob1, blob2);
510 }
511
512 #[test]
515 fn test_trust_set_serialization() {
516 let account = [0xAA; 20];
517 let limit = IssuedAmount {
518 currency: {
519 let mut c = [0u8; 20];
520 c[12..15].copy_from_slice(b"USD");
521 c
522 },
523 issuer: [0xBB; 20],
524 value: "100".to_string(),
525 };
526 let blob = serialize_trust_set(&account, &limit, 12, 1, 100).unwrap();
527 assert!(!blob.is_empty());
528 }
529
530 #[test]
531 fn test_iou_zero_encoding() {
532 let result = encode_iou_value("0").unwrap();
533 assert_eq!(result, 0x8000_0000_0000_0000u64.to_be_bytes());
534 }
535
536 #[test]
537 fn test_iou_positive_value() {
538 let result = encode_iou_value("100").unwrap();
539 let val = u64::from_be_bytes(result);
540 assert!(val & 0x8000_0000_0000_0000 != 0);
542 assert!(val & 0x4000_0000_0000_0000 != 0);
543 }
544
545 #[test]
546 fn test_iou_negative_value() {
547 let result = encode_iou_value("-50.5").unwrap();
548 let val = u64::from_be_bytes(result);
549 assert!(val & 0x8000_0000_0000_0000 != 0);
551 assert!(val & 0x4000_0000_0000_0000 == 0);
552 }
553
554 #[test]
555 fn test_iou_invalid_value_rejected() {
556 assert!(encode_iou_value("abc").is_err());
557 assert!(encode_iou_value("").is_err());
558 }
559
560 #[test]
561 fn test_iou_decimal_precision() {
562 let result = encode_iou_value("0.001").unwrap();
564 assert_ne!(result, 0x8000_0000_0000_0000u64.to_be_bytes());
565 }
566
567 #[test]
570 fn test_multisign_prefix() {
571 assert_eq!(&MULTISIGN_PREFIX, b"STX\0");
572 }
573
574 #[test]
575 fn test_multisign_hash_deterministic() {
576 let tx_blob = vec![0xAA; 100];
577 let account = [0xBB; 20];
578 let h1 = multisign_hash(&tx_blob, &account);
579 let h2 = multisign_hash(&tx_blob, &account);
580 assert_eq!(h1, h2);
581 }
582
583 #[test]
584 fn test_multisign_hash_different_account() {
585 let tx_blob = vec![0xAA; 100];
586 let h1 = multisign_hash(&tx_blob, &[0xBB; 20]);
587 let h2 = multisign_hash(&tx_blob, &[0xCC; 20]);
588 assert_ne!(h1, h2);
589 }
590
591 #[test]
592 fn test_signer_list() {
593 let signers = vec![
594 SignerEntry {
595 account: [0xAA; 20],
596 weight: 1,
597 },
598 SignerEntry {
599 account: [0xBB; 20],
600 weight: 2,
601 },
602 ];
603 let data = serialize_signer_list(&signers, 3);
604 assert_eq!(data.len(), 48);
606 }
607}