1pub mod definitions;
8pub mod types;
9
10use types::AccountId;
11
12use alloc::borrow::Cow;
13use alloc::string::String;
14use core::convert::TryFrom;
15use serde::Serialize;
16use serde_json::Value;
17
18pub mod binary_wrappers;
19pub mod exceptions;
20pub(crate) mod test_cases;
21pub mod utils;
22
23pub use binary_wrappers::*;
24
25use self::binary_wrappers::{
26 decode_ledger_data_inner, decode_st_object, serialize_json, BATCH_PREFIX,
27 PAYMENT_CHANNEL_CLAIM_PREFIX, TRANSACTION_MULTISIG_PREFIX, TRANSACTION_SIGNATURE_PREFIX,
28};
29
30use super::exceptions::XRPLCoreResult;
31
32pub fn encode<T>(signed_transaction: &T) -> XRPLCoreResult<String>
34where
35 T: Serialize,
36{
37 serialize_json(signed_transaction, None, None, false)
38}
39
40pub fn encode_for_signing<T>(prepared_transaction: &T) -> XRPLCoreResult<String>
42where
43 T: Serialize,
44{
45 serialize_json(
46 prepared_transaction,
47 Some(TRANSACTION_SIGNATURE_PREFIX.to_be_bytes().as_ref()),
48 None,
49 true,
50 )
51}
52
53pub fn encode_for_multisigning<T>(
56 prepared_transaction: &T,
57 signing_account: Cow<'_, str>,
58) -> XRPLCoreResult<String>
59where
60 T: Serialize,
61{
62 let signing_account_id = AccountId::try_from(signing_account.as_ref()).unwrap();
63
64 serialize_json(
65 prepared_transaction,
66 Some(TRANSACTION_MULTISIG_PREFIX.as_ref()),
67 Some(signing_account_id.as_ref()),
68 true,
69 )
70}
71
72pub fn encode_for_signing_claim(channel: &str, amount: &str) -> XRPLCoreResult<String> {
83 let channel_bytes = hex::decode(channel).map_err(|_| {
84 super::exceptions::XRPLCoreException::XRPLBinaryCodecError(
85 exceptions::XRPLBinaryCodecException::InvalidHashLength {
86 expected: 64,
87 found: channel.len(),
88 },
89 )
90 })?;
91 if channel_bytes.len() != 32 {
92 return Err(super::exceptions::XRPLCoreException::XRPLBinaryCodecError(
93 exceptions::XRPLBinaryCodecException::InvalidHashLength {
94 expected: 32,
95 found: channel_bytes.len(),
96 },
97 ));
98 }
99 let amount_val: u64 = amount.parse().map_err(|e| {
100 super::exceptions::XRPLCoreException::XRPLBinaryCodecError(
101 exceptions::XRPLBinaryCodecException::ParseIntError(e),
102 )
103 })?;
104
105 let mut buf = alloc::vec::Vec::with_capacity(44);
106 buf.extend_from_slice(&PAYMENT_CHANNEL_CLAIM_PREFIX);
107 buf.extend_from_slice(&channel_bytes);
108 buf.extend_from_slice(&amount_val.to_be_bytes());
109 Ok(hex::encode_upper(&buf))
110}
111
112pub fn encode_for_signing_batch(flags: u32, tx_ids: &[&str]) -> XRPLCoreResult<String> {
124 let mut buf = alloc::vec::Vec::with_capacity(4 + 4 + 4 + tx_ids.len() * 32);
125 buf.extend_from_slice(&BATCH_PREFIX);
126 buf.extend_from_slice(&flags.to_be_bytes());
127 buf.extend_from_slice(&(tx_ids.len() as u32).to_be_bytes());
128 for tx_id in tx_ids {
129 let id_bytes = hex::decode(tx_id).map_err(|_| {
130 super::exceptions::XRPLCoreException::XRPLBinaryCodecError(
131 exceptions::XRPLBinaryCodecException::InvalidHashLength {
132 expected: 64,
133 found: tx_id.len(),
134 },
135 )
136 })?;
137 if id_bytes.len() != 32 {
138 return Err(super::exceptions::XRPLCoreException::XRPLBinaryCodecError(
139 exceptions::XRPLBinaryCodecException::InvalidHashLength {
140 expected: 32,
141 found: id_bytes.len(),
142 },
143 ));
144 }
145 buf.extend_from_slice(&id_bytes);
146 }
147 Ok(hex::encode_upper(&buf))
148}
149
150pub fn decode(hex_string: &str) -> XRPLCoreResult<Value> {
156 let mut parser = BinaryParser::try_from(hex_string)?;
157 decode_st_object(&mut parser)
158}
159
160pub fn decode_ledger_data(hex_string: &str) -> XRPLCoreResult<Value> {
173 decode_ledger_data_inner(hex_string)
174}
175
176#[cfg(all(test, feature = "std"))]
177mod test {
178 use alloc::vec;
179
180 use super::*;
181 use crate::utils::testing::test_constants::*;
182
183 #[path = "binary_json_tests.rs"]
184 mod binary_json_tests;
185 #[path = "binary_serializer_tests.rs"]
186 mod binary_serializer_tests;
187 #[path = "tx_encode_decode_tests.rs"]
188 mod tx_encode_decode_tests;
189 #[path = "x_address_tests.rs"]
190 mod x_address_tests;
191
192 use crate::core::binarycodec::definitions::{
193 get_field_instance, get_ledger_entry_type_code, get_transaction_type_code,
194 };
195 use crate::core::binarycodec::utils::{decode_field_name, encode_field_name};
196 use crate::models::transactions::{
197 mptoken_authorize::MPTokenAuthorize,
198 mptoken_issuance_create::{MPTokenIssuanceCreate, MPTokenIssuanceCreateFlag},
199 mptoken_issuance_destroy::MPTokenIssuanceDestroy,
200 mptoken_issuance_set::{MPTokenIssuanceSet, MPTokenIssuanceSetFlag},
201 CommonFields, TransactionType,
202 };
203
204 #[test]
207 fn test_mpt_field_name_encoding() {
208 let cases = [
212 ("MPTokenIssuanceID", "0115"), ("ShareMPTID", "0215"), ("Holder", "8B"), ("AssetScale", "0510"), ("MaximumAmount", "3018"), ("MPTAmount", "301A"), ("MPTokenMetadata", "701E"), ];
220
221 for (field_name, expected_hex) in &cases {
222 let encoded = encode_field_name(field_name)
223 .unwrap_or_else(|e| panic!("failed to encode field {}: {:?}", field_name, e));
224 let hex = hex::encode_upper(encoded);
225 assert_eq!(
226 &hex, expected_hex,
227 "encode mismatch for field {}",
228 field_name
229 );
230
231 let decoded = decode_field_name(expected_hex)
232 .unwrap_or_else(|e| panic!("failed to decode hex {}: {:?}", expected_hex, e));
233 assert_eq!(
234 decoded, *field_name,
235 "decode mismatch for hex {}",
236 expected_hex
237 );
238 }
239 }
240
241 #[test]
244 fn test_mpt_transaction_type_codes() {
245 assert_eq!(
246 get_transaction_type_code("MPTokenIssuanceCreate"),
247 Some(&54)
248 );
249 assert_eq!(
250 get_transaction_type_code("MPTokenIssuanceDestroy"),
251 Some(&55)
252 );
253 assert_eq!(get_transaction_type_code("MPTokenIssuanceSet"), Some(&56));
254 assert_eq!(get_transaction_type_code("MPTokenAuthorize"), Some(&57));
255 }
256
257 #[test]
258 fn test_mpt_ledger_entry_type_codes() {
259 assert_eq!(get_ledger_entry_type_code("MPTokenIssuance"), Some(&126));
260 assert_eq!(get_ledger_entry_type_code("MPToken"), Some(&127));
261 }
262
263 #[test]
266 fn test_mpt_field_instances() {
267 let fi = get_field_instance("MPTokenIssuanceID").expect("MPTokenIssuanceID not found");
268 assert_eq!(fi.associated_type, "Hash192");
269 assert_eq!(fi.nth, 1);
270 assert!(fi.is_serialized);
271 assert!(fi.is_signing);
272
273 let fi = get_field_instance("Holder").expect("Holder not found");
274 assert_eq!(fi.associated_type, "AccountID");
275 assert_eq!(fi.nth, 11);
276 assert!(fi.is_vl_encoded);
277
278 let fi = get_field_instance("MPTAmount").expect("MPTAmount not found");
279 assert_eq!(fi.associated_type, "UInt64");
280 assert_eq!(fi.nth, 26);
281
282 let fi = get_field_instance("MPTokenMetadata").expect("MPTokenMetadata not found");
283 assert_eq!(fi.associated_type, "Blob");
284 assert_eq!(fi.nth, 30);
285 assert!(fi.is_vl_encoded);
286
287 let fi = get_field_instance("AssetScale").expect("AssetScale not found");
288 assert_eq!(fi.associated_type, "UInt8");
289 assert_eq!(fi.nth, 5);
290
291 let fi = get_field_instance("MaximumAmount").expect("MaximumAmount not found");
292 assert_eq!(fi.associated_type, "UInt64");
293 assert_eq!(fi.nth, 24);
294
295 let fi = get_field_instance("ShareMPTID").expect("ShareMPTID not found");
296 assert_eq!(fi.associated_type, "Hash192");
297 assert_eq!(fi.nth, 2);
298 }
299
300 #[test]
306 fn test_encode_mptoken_issuance_create() {
307 let txn = MPTokenIssuanceCreate {
308 common_fields: CommonFields {
309 account: ACCOUNT_GENESIS.into(),
310 transaction_type: TransactionType::MPTokenIssuanceCreate,
311 fee: Some("10".into()),
312 sequence: Some(1),
313 flags: vec![MPTokenIssuanceCreateFlag::TfMPTCanTransfer].into(),
314 ..Default::default()
315 },
316 asset_scale: Some(2),
317 maximum_amount: Some("1000000".into()),
318 transfer_fee: Some(314),
319 mptoken_metadata: Some("CAFEBABE".into()),
320 ..Default::default()
321 };
322
323 let hex = encode(&txn).expect("encode MPTokenIssuanceCreate failed");
324
325 assert!(
327 hex.starts_with("120036"),
328 "expected hex to start with 120036 (MPTokenIssuanceCreate), got: {}",
329 &hex[..core::cmp::min(20, hex.len())]
330 );
331
332 assert!(
334 hex.contains("14013A"),
335 "expected TransferFee 314 (14013A) in hex"
336 );
337
338 assert!(
340 hex.contains("2200000020"),
341 "expected Flags TfMPTCanTransfer (2200000020) in hex"
342 );
343
344 assert!(
346 hex.contains("2400000001"),
347 "expected Sequence 1 (2400000001) in hex"
348 );
349
350 assert!(
352 hex.contains("051002"),
353 "expected AssetScale 2 (051002) in hex"
354 );
355
356 assert!(
358 hex.contains("CAFEBABE"),
359 "expected MPTokenMetadata hex payload in encoded output"
360 );
361 }
362
363 #[test]
364 fn test_encode_mptoken_issuance_create_with_flags() {
365 let txn = MPTokenIssuanceCreate {
366 common_fields: CommonFields {
367 account: ACCOUNT_GENESIS.into(),
368 transaction_type: TransactionType::MPTokenIssuanceCreate,
369 fee: Some("12".into()),
370 sequence: Some(5),
371 flags: vec![
372 MPTokenIssuanceCreateFlag::TfMPTCanTransfer,
373 MPTokenIssuanceCreateFlag::TfMPTCanLock,
374 ]
375 .into(),
376 ..Default::default()
377 },
378 ..Default::default()
379 };
380
381 let hex = encode(&txn).expect("encode MPTokenIssuanceCreate with flags failed");
382
383 assert!(hex.starts_with("120036"), "wrong transaction type");
384
385 assert!(
387 hex.contains("2200000022"),
388 "expected Flags 0x22 (2200000022) in hex, got: {}",
389 hex
390 );
391 }
392
393 #[test]
396 fn test_encode_mptoken_issuance_destroy() {
397 let txn = MPTokenIssuanceDestroy {
398 common_fields: CommonFields {
399 account: ACCOUNT_GENESIS.into(),
400 transaction_type: TransactionType::MPTokenIssuanceDestroy,
401 fee: Some("10".into()),
402 sequence: Some(1),
403 ..Default::default()
404 },
405 mptoken_issuance_id: "00000001A407AF5856CEFBF81F3D4A00AABBCCDD11223344".into(),
406 };
407
408 let hex = encode(&txn).expect("encode MPTokenIssuanceDestroy failed");
409
410 assert!(
412 hex.starts_with("120037"),
413 "expected hex to start with 120037 (MPTokenIssuanceDestroy), got: {}",
414 &hex[..core::cmp::min(20, hex.len())]
415 );
416
417 assert!(
420 hex.contains("00000001A407AF5856CEFBF81F3D4A00AABBCCDD11223344"),
421 "expected MPTokenIssuanceID hash in encoded output"
422 );
423 }
424
425 #[test]
428 fn test_encode_mptoken_issuance_set() {
429 let txn = MPTokenIssuanceSet {
430 common_fields: CommonFields {
431 account: ACCOUNT_GENESIS.into(),
432 transaction_type: TransactionType::MPTokenIssuanceSet,
433 fee: Some("10".into()),
434 sequence: Some(1),
435 flags: vec![MPTokenIssuanceSetFlag::TfMPTLock].into(),
436 ..Default::default()
437 },
438 mptoken_issuance_id: "00000001A407AF5856CEFBF81F3D4A00AABBCCDD11223344".into(),
439 holder: Some(ACCOUNT_ALT.into()),
440 ..Default::default()
441 };
442
443 let hex = encode(&txn).expect("encode MPTokenIssuanceSet failed");
444
445 assert!(
447 hex.starts_with("120038"),
448 "expected hex to start with 120038 (MPTokenIssuanceSet), got: {}",
449 &hex[..core::cmp::min(20, hex.len())]
450 );
451
452 assert!(
454 hex.contains("2200000001"),
455 "expected Flags TfMPTLock (2200000001) in hex"
456 );
457
458 assert!(
460 hex.contains("00000001A407AF5856CEFBF81F3D4A00AABBCCDD11223344"),
461 "expected MPTokenIssuanceID in encoded output"
462 );
463
464 assert!(hex.contains("8B"), "expected Holder field ID (8B) in hex");
466 }
467
468 #[test]
470 fn test_encode_mptoken_authorize() {
471 let txn = MPTokenAuthorize {
472 common_fields: CommonFields {
473 account: ACCOUNT_GENESIS.into(),
474 transaction_type: TransactionType::MPTokenAuthorize,
475 fee: Some("10".into()),
476 sequence: Some(1),
477 ..Default::default()
478 },
479 mptoken_issuance_id: "00000001A407AF5856CEFBF81F3D4A00AABBCCDD11223344".into(),
480 ..Default::default()
481 };
482
483 let hex = encode(&txn).expect("encode MPTokenAuthorize failed");
484
485 assert!(
487 hex.starts_with("120039"),
488 "expected hex to start with 120039 (MPTokenAuthorize), got: {}",
489 &hex[..core::cmp::min(20, hex.len())]
490 );
491
492 assert!(
494 hex.contains("00000001A407AF5856CEFBF81F3D4A00AABBCCDD11223344"),
495 "expected MPTokenIssuanceID in encoded output"
496 );
497 }
498
499 #[test]
502 fn test_encode_for_signing_mptoken_issuance_create() {
503 let txn = MPTokenIssuanceCreate {
504 common_fields: CommonFields {
505 account: ACCOUNT_GENESIS.into(),
506 transaction_type: TransactionType::MPTokenIssuanceCreate,
507 fee: Some("10".into()),
508 sequence: Some(1),
509 ..Default::default()
510 },
511 ..Default::default()
512 };
513
514 let hex = encode_for_signing(&txn).expect("encode_for_signing failed");
515
516 assert!(
518 hex.starts_with("53545800"),
519 "expected signing prefix 53545800, got: {}",
520 &hex[..core::cmp::min(20, hex.len())]
521 );
522
523 assert!(
525 hex[8..].starts_with("120036"),
526 "expected TransactionType after signing prefix"
527 );
528 }
529
530 #[test]
533 fn test_encode_for_multisigning_mptoken_authorize() {
534 let txn = MPTokenAuthorize {
535 common_fields: CommonFields {
536 account: ACCOUNT_GENESIS.into(),
537 transaction_type: TransactionType::MPTokenAuthorize,
538 fee: Some("10".into()),
539 sequence: Some(1),
540 ..Default::default()
541 },
542 mptoken_issuance_id: "00000001A407AF5856CEFBF81F3D4A00AABBCCDD11223344".into(),
543 ..Default::default()
544 };
545
546 let hex = encode_for_multisigning(&txn, ACCOUNT_ALT.into())
547 .expect("encode_for_multisigning failed");
548
549 assert!(
551 hex.starts_with("534D5400"),
552 "expected multisign prefix 534D5400, got: {}",
553 &hex[..core::cmp::min(20, hex.len())]
554 );
555
556 assert!(
558 hex[8..].starts_with("120039"),
559 "expected MPTokenAuthorize type after multisign prefix"
560 );
561
562 assert!(
565 hex.len() > 40,
566 "encoded output too short for multisign suffix"
567 );
568 }
569
570 #[test]
572 fn test_encode_deterministic() {
573 let txn = MPTokenIssuanceDestroy {
574 common_fields: CommonFields {
575 account: ACCOUNT_GENESIS.into(),
576 transaction_type: TransactionType::MPTokenIssuanceDestroy,
577 fee: Some("10".into()),
578 sequence: Some(42),
579 ..Default::default()
580 },
581 mptoken_issuance_id: "AABBCCDD11223344AABBCCDD11223344AABBCCDD11223344".into(),
582 };
583
584 let hex1 = encode(&txn).expect("first encode failed");
585 let hex2 = encode(&txn).expect("second encode failed");
586 assert_eq!(hex1, hex2, "encoding should be deterministic");
587 }
588
589 #[test]
594 fn test_hash192_encode_decode_roundtrip() {
595 use crate::core::binarycodec::{decode, encode};
596
597 let issuance_id = "00000001A407AF5856CEFBF81F3D4A00AABBCCDD11223344";
598 let txn = MPTokenIssuanceDestroy {
599 common_fields: CommonFields {
600 account: ACCOUNT_GENESIS.into(),
601 transaction_type: TransactionType::MPTokenIssuanceDestroy,
602 fee: Some("10".into()),
603 sequence: Some(1),
604 ..Default::default()
605 },
606 mptoken_issuance_id: issuance_id.into(),
607 };
608
609 let encoded = encode(&txn).expect("encode MPTokenIssuanceDestroy failed");
610 let decoded = decode(&encoded).expect("decode failed");
611
612 let decoded_id = decoded
613 .get("MPTokenIssuanceID")
614 .expect("MPTokenIssuanceID missing from decoded output")
615 .as_str()
616 .expect("MPTokenIssuanceID is not a string");
617
618 assert_eq!(
619 decoded_id.to_uppercase(),
620 issuance_id.to_uppercase(),
621 "Hash192 value did not survive encode->decode round-trip"
622 );
623 }
624
625 #[test]
632 fn test_mpt_ledger_entry_exact_vectors() {
633 use super::{decode, serialize_json};
634 use serde_json::json;
635
636 let mpt_issuance_binary = concat!(
640 "11007E220000006224000002DF25000002E434000000000000000030187FFFFFFFFFFFFFFF",
641 "30190000000000000064552E78C1FFBDDAEE077253CEB12CFEA83689AA0899F94762190A357208DADC76FE",
642 "701EC1EC7B226E616D65223A2255532054726561737572792042696C6C20546F6B656E222C2273796D626F",
643 "6C223A225553544254222C22646563696D616C73223A322C22746F74616C537570706C79223A313030303030",
644 "302C22697373756572223A225553205472656173757279222C22697373756544617465223A22323032342D30",
645 "332D3235222C226D6174757269747944617465223A22323032352D30332D3235222C226661636556616C7565",
646 "223A2231303030222C22696E74657265737452617465223A22322E35222C22696E7465726573744672657175",
647 "656E6379223A22517561727465726C79222C22636F6C6C61746572616C223A22555320476F7665726E6D656E",
648 "74222C226A7572697364696374696F6E223A22556E6974656420537461746573222C22726567756C61746F72",
649 "79436F6D706C69616E6365223A2253454320526567756C6174696F6E73222C22736563757269747954797065",
650 "223A2254726561737572792042696C6C222C2265787465726E616C5F75726C223A2268747470733A2F2F6578",
651 "616D706C652E636F6D2F742D62696C6C2D746F6B656E2D6D657461646174612E6A736F6E227D",
652 "8414A4D893CFBC4DC6AE877EB585F90A3B47528B958D051003"
653 );
654
655 let decoded_issuance = decode(mpt_issuance_binary).expect("decode MPTokenIssuance failed");
656 assert_eq!(
657 decoded_issuance["LedgerEntryType"],
658 json!("MPTokenIssuance")
659 );
660 assert_eq!(decoded_issuance["AssetScale"], json!(3));
661 assert_eq!(decoded_issuance["Flags"], json!(98));
662 assert_eq!(decoded_issuance["Sequence"], json!(735));
663 assert_eq!(
664 decoded_issuance["MaximumAmount"],
665 json!("9223372036854775807")
666 );
667 assert_eq!(decoded_issuance["OutstandingAmount"], json!("100"));
668 assert_eq!(
669 decoded_issuance["Issuer"],
670 json!("rGpdGXDV2RFPeLEfWS9RFo5Nh9cpVDToZa")
671 );
672 assert_eq!(
673 decoded_issuance["PreviousTxnID"],
674 json!("2E78C1FFBDDAEE077253CEB12CFEA83689AA0899F94762190A357208DADC76FE")
675 );
676 assert_eq!(decoded_issuance["PreviousTxnLgrSeq"], json!(740));
677
678 let re_encoded = serialize_json(&decoded_issuance, None, None, false)
680 .expect("re-encode MPTokenIssuance failed");
681 assert_eq!(
682 re_encoded.to_uppercase(),
683 mpt_issuance_binary.to_uppercase(),
684 "MPTokenIssuance re-encode does not match authoritative vector"
685 );
686
687 let mptoken_binary = concat!(
691 "11007F220000000025000002E5340000000000000000",
692 "301A00000000000000645522",
693 "2EF3C7E82D8A44984A66E2B8E357CB536EC2547359CCF70E56E14BC4C284C8",
694 "81143930DB9A74C26D96CB58ADFFD7E8BB78BCFE623",
695 "40115000002DF71CAE59C9B7E56587FFF74D4EA5830D9BE3CE0CC"
696 );
697
698 let decoded_token = decode(mptoken_binary).expect("decode MPToken failed");
699 assert_eq!(decoded_token["LedgerEntryType"], json!("MPToken"));
700 assert_eq!(decoded_token["Flags"], json!(0));
701 assert_eq!(decoded_token["MPTAmount"], json!("100"));
702 assert_eq!(
703 decoded_token["MPTokenIssuanceID"],
704 json!("000002DF71CAE59C9B7E56587FFF74D4EA5830D9BE3CE0CC")
705 );
706 assert_eq!(
707 decoded_token["Account"],
708 json!("raDQsd1s8rqGjL476g59a9vVNi1rSwrC44")
709 );
710 assert_eq!(
711 decoded_token["PreviousTxnID"],
712 json!("222EF3C7E82D8A44984A66E2B8E357CB536EC2547359CCF70E56E14BC4C284C8")
713 );
714 assert_eq!(decoded_token["PreviousTxnLgrSeq"], json!(741));
715
716 let re_encoded_token =
718 serialize_json(&decoded_token, None, None, false).expect("re-encode MPToken failed");
719 assert_eq!(
720 re_encoded_token.to_uppercase(),
721 mptoken_binary.to_uppercase(),
722 "MPToken re-encode does not match authoritative vector"
723 );
724 }
725}