1use anyhow::Context;
108use nautilus_model::identifiers::ClientOrderId;
109
110const BASE62_CHARS: &[u8; 62] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
112
113const BASE62_DECODE: [u8; 128] = {
116 let mut table = [0xFFu8; 128];
117 let mut i = 0u8;
118 while i < 62 {
119 table[BASE62_CHARS[i as usize] as usize] = i;
120 i += 1;
121 }
122 table
123};
124
125const O_FORMAT_EPOCH: i64 = 1_577_836_800;
127
128const O_FORMAT_B62_LEN: usize = 13;
130
131const UUID_B62_LEN: usize = 22;
133
134const MAX_CLIENT_ORDER_ID_LEN: usize = 36;
136
137const SIGNAL_O_HYPHENS: u8 = b'T';
138const SIGNAL_O_NO_HYPHENS: u8 = b't';
139const SIGNAL_UUID_HYPHENS: u8 = b'U';
140const SIGNAL_UUID_NO_HYPHENS: u8 = b'u';
141const SIGNAL_RAW: u8 = b'R';
142
143#[must_use]
145fn broker_prefix(broker_id: &str) -> String {
146 format!("x-{broker_id}-")
147}
148
149#[must_use]
154pub fn encode_broker_id(client_order_id: &ClientOrderId, broker_id: &str) -> String {
155 let id_str = client_order_id.as_str();
156 let prefix = broker_prefix(broker_id);
157 let budget = MAX_CLIENT_ORDER_ID_LEN - prefix.len();
158
159 if let Some((packed, has_hyphens)) = pack_o_format(id_str) {
160 let signal = if has_hyphens {
161 SIGNAL_O_HYPHENS
162 } else {
163 SIGNAL_O_NO_HYPHENS
164 };
165 let b62 = encode_base62::<O_FORMAT_B62_LEN>(packed);
166 return build_encoded(&prefix, signal, &b62);
167 }
168
169 if let Some((value, has_hyphens)) = parse_uuid_hex(id_str) {
170 let signal = if has_hyphens {
171 SIGNAL_UUID_HYPHENS
172 } else {
173 SIGNAL_UUID_NO_HYPHENS
174 };
175 let b62 = encode_base62::<UUID_B62_LEN>(value);
176 return build_encoded(&prefix, signal, &b62);
177 }
178
179 if id_str.len() < budget {
180 let mut result = String::with_capacity(prefix.len() + 1 + id_str.len());
181 result.push_str(&prefix);
182 result.push(SIGNAL_RAW as char);
183 result.push_str(id_str);
184 return result;
185 }
186
187 log::warn!(
188 "ClientOrderId '{id_str}' ({} chars) exceeds broker ID encoding budget ({budget} chars), sending without prefix",
189 id_str.len(),
190 );
191 id_str.to_string()
192}
193
194#[must_use]
200pub fn decode_broker_id(encoded: &str, broker_id: &str) -> String {
201 match decode_broker_id_checked(encoded, broker_id) {
202 Ok(decoded) => decoded,
203 Err(e) => {
204 log::warn!("Failed to decode broker client order ID: {e}");
205 encoded.to_string()
206 }
207 }
208}
209
210pub(crate) fn decode_client_order_id(
220 encoded: &str,
221 broker_id: &str,
222) -> anyhow::Result<ClientOrderId> {
223 let decoded = decode_broker_id_checked(encoded, broker_id)?;
224 ClientOrderId::new_checked(decoded)
225 .with_context(|| format!("invalid Binance client order ID '{encoded}'"))
226}
227
228fn decode_broker_id_checked(encoded: &str, broker_id: &str) -> anyhow::Result<String> {
229 let prefix = broker_prefix(broker_id);
230 let Some(payload) = encoded.strip_prefix(&prefix) else {
231 return Ok(encoded.to_string());
232 };
233
234 let Some((&signal, data)) = payload.as_bytes().split_first() else {
235 anyhow::bail!("missing broker client order ID signal");
236 };
237
238 match signal {
239 SIGNAL_O_HYPHENS | SIGNAL_O_NO_HYPHENS => {
240 anyhow::ensure!(
241 data.len() == O_FORMAT_B62_LEN,
242 "invalid O-format broker client order ID payload length"
243 );
244 let packed = decode_base62(data).context("invalid O-format broker client order ID")?;
245 Ok(unpack_o_format(packed, signal == SIGNAL_O_HYPHENS))
246 }
247 SIGNAL_UUID_HYPHENS | SIGNAL_UUID_NO_HYPHENS => {
248 anyhow::ensure!(
249 data.len() == UUID_B62_LEN,
250 "invalid UUID broker client order ID payload length"
251 );
252 let value = decode_base62(data).context("invalid UUID broker client order ID")?;
253 Ok(format_uuid(value, signal == SIGNAL_UUID_HYPHENS))
254 }
255 SIGNAL_RAW => {
256 let raw = std::str::from_utf8(data).context("invalid raw broker client order ID")?;
257 anyhow::ensure!(
258 !raw.is_empty(),
259 "missing raw broker client order ID payload"
260 );
261 Ok(raw.to_string())
262 }
263 _ => anyhow::bail!(
264 "unknown broker client order ID signal byte '{}'",
265 signal as char
266 ),
267 }
268}
269
270fn build_encoded(prefix: &str, signal: u8, b62: &[u8]) -> String {
271 let mut result = String::with_capacity(prefix.len() + 1 + b62.len());
272 result.push_str(prefix);
273 result.push(signal as char);
274 result.push_str(std::str::from_utf8(b62).expect("base62 is valid UTF-8"));
276 result
277}
278
279fn encode_base62<const N: usize>(mut value: u128) -> [u8; N] {
280 let mut buf = [b'0'; N];
281 for i in (0..N).rev() {
282 buf[i] = BASE62_CHARS[(value % 62) as usize];
283 value /= 62;
284 }
285 buf
286}
287
288fn decode_base62(encoded: &[u8]) -> Option<u128> {
289 let mut value: u128 = 0;
290
291 for &byte in encoded {
292 let digit = BASE62_DECODE[byte as usize & 0x7F];
293
294 if digit == 0xFF || !byte.is_ascii() {
295 return None;
296 }
297 value = value.checked_mul(62)?.checked_add(digit as u128)?;
298 }
299 Some(value)
300}
301
302fn parse_digits(bytes: &[u8]) -> Option<u32> {
303 let mut n: u32 = 0;
304
305 for &b in bytes {
306 if !b.is_ascii_digit() {
307 return None;
308 }
309 n = n * 10 + (b - b'0') as u32;
310 }
311 Some(n)
312}
313
314fn pack_o_format(id_str: &str) -> Option<(u128, bool)> {
315 let b = id_str.as_bytes();
316
317 if b.first() != Some(&b'O') {
318 return None;
319 }
320
321 let (year, month, day, hour, minute, second, trader, strategy, count, has_hyphens) =
322 if b.get(1) == Some(&b'-') {
323 if b.len() < 23 || b[10] != b'-' || b[17] != b'-' {
326 return None;
327 }
328 let h4 = memchr_byte(b'-', &b[18..])?;
329 let trader_end = 18 + h4;
330 let h5 = memchr_byte(b'-', &b[trader_end + 1..])?;
331 let strategy_end = trader_end + 1 + h5;
332
333 (
334 parse_digits(&b[2..6])?,
335 parse_digits(&b[6..8])?,
336 parse_digits(&b[8..10])?,
337 parse_digits(&b[11..13])?,
338 parse_digits(&b[13..15])?,
339 parse_digits(&b[15..17])?,
340 parse_digits(&b[18..trader_end])?,
341 parse_digits(&b[trader_end + 1..strategy_end])?,
342 parse_digits(&b[strategy_end + 1..])?,
343 true,
344 )
345 } else {
346 if b.len() < 22 {
348 return None;
349 }
350 (
351 parse_digits(&b[1..5])?,
352 parse_digits(&b[5..7])?,
353 parse_digits(&b[7..9])?,
354 parse_digits(&b[9..11])?,
355 parse_digits(&b[11..13])?,
356 parse_digits(&b[13..15])?,
357 parse_digits(&b[15..18])?,
358 parse_digits(&b[18..21])?,
359 parse_digits(&b[21..])?,
360 false,
361 )
362 };
363
364 if trader > 1023 || strategy > 1023 || count > 0xF_FFFF {
365 return None;
366 }
367
368 let secs_since_epoch = civil_to_epoch(year, month, day, hour, minute, second)? - O_FORMAT_EPOCH;
369
370 if secs_since_epoch < 0 {
371 return None;
372 }
373
374 let packed = (secs_since_epoch as u128) << 40
375 | (trader as u128) << 30
376 | (strategy as u128) << 20
377 | (count as u128);
378
379 Some((packed, has_hyphens))
380}
381
382fn unpack_o_format(packed: u128, has_hyphens: bool) -> String {
383 let count = (packed & 0xF_FFFF) as u32;
384 let strategy = ((packed >> 20) & 0x3FF) as u32;
385 let trader = ((packed >> 30) & 0x3FF) as u32;
386 let secs_since_epoch = (packed >> 40) as i64;
387
388 let timestamp = secs_since_epoch + O_FORMAT_EPOCH;
389 let Some((year, month, day, hour, minute, second)) = epoch_to_civil(timestamp) else {
390 log::warn!("Failed to decode O-format timestamp: {timestamp}");
391 return format!("DECODE_ERROR_{packed}");
392 };
393
394 if has_hyphens {
395 format!(
396 "O-{year:04}{month:02}{day:02}-{hour:02}{minute:02}{second:02}-{trader:03}-{strategy:03}-{count}",
397 )
398 } else {
399 format!(
400 "O{year:04}{month:02}{day:02}{hour:02}{minute:02}{second:02}{trader:03}{strategy:03}{count}",
401 )
402 }
403}
404
405fn parse_uuid_hex(id_str: &str) -> Option<(u128, bool)> {
406 let b = id_str.as_bytes();
407
408 if b.len() == 36 && b[8] == b'-' {
409 if b[13] != b'-' || b[18] != b'-' || b[23] != b'-' {
411 return None;
412 }
413 let mut value: u128 = 0;
414
415 for &byte in b {
416 if byte == b'-' {
417 continue;
418 }
419 let nibble = hex_digit(byte)?;
420 value = (value << 4) | nibble as u128;
421 }
422 Some((value, true))
423 } else if b.len() == 32 {
424 let mut value: u128 = 0;
425
426 for &byte in b {
427 let nibble = hex_digit(byte)?;
428 value = (value << 4) | nibble as u128;
429 }
430 Some((value, false))
431 } else {
432 None
433 }
434}
435
436fn format_uuid(value: u128, has_hyphens: bool) -> String {
437 const HEX: &[u8; 16] = b"0123456789abcdef";
438 let bytes = value.to_be_bytes();
439
440 if has_hyphens {
441 let mut buf = [0u8; 36];
442 let mut pos = 0;
443
444 for (i, &b) in bytes.iter().enumerate() {
445 if i == 4 || i == 6 || i == 8 || i == 10 {
446 buf[pos] = b'-';
447 pos += 1;
448 }
449 buf[pos] = HEX[(b >> 4) as usize];
450 buf[pos + 1] = HEX[(b & 0x0F) as usize];
451 pos += 2;
452 }
453 std::str::from_utf8(&buf)
454 .expect("hex is valid UTF-8")
455 .to_string()
456 } else {
457 let mut buf = [0u8; 32];
458 for (i, &b) in bytes.iter().enumerate() {
459 buf[i * 2] = HEX[(b >> 4) as usize];
460 buf[i * 2 + 1] = HEX[(b & 0x0F) as usize];
461 }
462 std::str::from_utf8(&buf)
463 .expect("hex is valid UTF-8")
464 .to_string()
465 }
466}
467
468fn hex_digit(byte: u8) -> Option<u8> {
469 match byte {
470 b'0'..=b'9' => Some(byte - b'0'),
471 b'a'..=b'f' => Some(byte - b'a' + 10),
472 b'A'..=b'F' => Some(byte - b'A' + 10),
473 _ => None,
474 }
475}
476
477fn memchr_byte(needle: u8, haystack: &[u8]) -> Option<usize> {
478 haystack.iter().position(|&b| b == needle)
479}
480
481fn civil_to_epoch(year: u32, month: u32, day: u32, hour: u32, min: u32, sec: u32) -> Option<i64> {
483 if !(1..=12).contains(&month) || !(1..=31).contains(&day) || hour > 23 || min > 59 || sec > 59 {
484 return None;
485 }
486 let y = if month <= 2 {
488 year as i64 - 1
489 } else {
490 year as i64
491 };
492 let era = y.div_euclid(400);
493 let yoe = y.rem_euclid(400) as u64;
494 let m = if month > 2 { month - 3 } else { month + 9 } as u64;
495 let doy = (153 * m + 2) / 5 + day as u64 - 1;
496 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
497 let days = era * 146097 + doe as i64 - 719468;
498 Some(days * 86400 + hour as i64 * 3600 + min as i64 * 60 + sec as i64)
499}
500
501fn epoch_to_civil(timestamp: i64) -> Option<(u32, u32, u32, u32, u32, u32)> {
503 if timestamp < 0 {
504 return None;
505 }
506 let secs_of_day = (timestamp % 86400) as u32;
507 let days = timestamp / 86400;
508
509 let hour = secs_of_day / 3600;
510 let minute = (secs_of_day % 3600) / 60;
511 let second = secs_of_day % 60;
512
513 let z = days + 719468;
515 let era = z.div_euclid(146097);
516 let doe = z.rem_euclid(146097) as u64;
517 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
518 let y = yoe as i64 + era * 400;
519 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
520 let mp = (5 * doy + 2) / 153;
521 let day = (doy - (153 * mp + 2) / 5 + 1) as u32;
522 let month = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
523 let year = if month <= 2 { y + 1 } else { y } as u32;
524
525 Some((year, month, day, hour, minute, second))
526}
527
528#[cfg(test)]
529mod tests {
530 use std::hint::black_box;
531
532 use rstest::rstest;
533
534 use super::{super::consts::BINANCE_NAUTILUS_SPOT_BROKER_ID, *};
535
536 const TEST_BROKER_ID: &str = BINANCE_NAUTILUS_SPOT_BROKER_ID;
537
538 #[rstest]
539 fn test_base62_roundtrip_zero() {
540 let encoded = encode_base62::<13>(0);
541 let decoded = decode_base62(&encoded);
542 assert_eq!(decoded, Some(0));
543 }
544
545 #[rstest]
546 fn test_base62_roundtrip_max_72_bit() {
547 let value: u128 = (1u128 << 72) - 1;
548 let encoded = encode_base62::<13>(value);
549 let decoded = decode_base62(&encoded);
550 assert_eq!(decoded, Some(value));
551 }
552
553 #[rstest]
554 fn test_base62_roundtrip_max_128_bit() {
555 let value: u128 = u128::MAX;
556 let encoded = encode_base62::<22>(value);
557 let decoded = decode_base62(&encoded);
558 assert_eq!(decoded, Some(value));
559 }
560
561 #[rstest]
562 #[case("O-20200101-000000-000-000-0")]
563 #[case("O-20200101-000001-001-001-1")]
564 #[case("O-20260131-174827-001-001-1")]
565 #[case("O-20260131-235959-999-999-4095")]
566 #[case("O-20251215-123456-123-456-789")]
567 #[case("O-20260305-120000-001-001-100")]
568 #[case("O-20260305-120000-001-001-99999")]
569 #[case("O-20260305-120000-001-001-1048575")]
570 fn test_roundtrip_o_format_with_hyphens(#[case] id_str: &str) {
571 let coid = ClientOrderId::from(id_str);
572 let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
573
574 assert!(encoded.starts_with("x-TD67BGP9-T"), "got: {encoded}");
575 assert!(encoded.len() <= 36, "len {} > 36: {encoded}", encoded.len());
576
577 let decoded = decode_broker_id(&encoded, TEST_BROKER_ID);
578 assert_eq!(decoded, id_str);
579 }
580
581 #[rstest]
582 #[case("O202001010000000000000")]
583 #[case("O202601311748270010011")]
584 #[case("O202601312359599999994095")]
585 fn test_roundtrip_o_format_without_hyphens(#[case] id_str: &str) {
586 let coid = ClientOrderId::from(id_str);
587 let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
588
589 assert!(encoded.starts_with("x-TD67BGP9-t"), "got: {encoded}");
590 assert!(encoded.len() <= 36);
591
592 let decoded = decode_broker_id(&encoded, TEST_BROKER_ID);
593 assert_eq!(decoded, id_str);
594 }
595
596 #[rstest]
597 fn test_roundtrip_uuid_with_hyphens() {
598 let id_str = "550e8400-e29b-41d4-a716-446655440000";
599 let coid = ClientOrderId::from(id_str);
600 let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
601
602 assert!(encoded.starts_with("x-TD67BGP9-U"), "got: {encoded}");
603 assert!(encoded.len() <= 36, "len {} > 36: {encoded}", encoded.len());
604
605 let decoded = decode_broker_id(&encoded, TEST_BROKER_ID);
606 assert_eq!(decoded, id_str);
607 }
608
609 #[rstest]
610 fn test_roundtrip_uuid_without_hyphens() {
611 let id_str = "550e8400e29b41d4a716446655440000";
612 let coid = ClientOrderId::from(id_str);
613 let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
614
615 assert!(encoded.starts_with("x-TD67BGP9-u"), "got: {encoded}");
616 assert!(encoded.len() <= 36);
617
618 let decoded = decode_broker_id(&encoded, TEST_BROKER_ID);
619 assert_eq!(decoded, id_str);
620 }
621
622 #[rstest]
623 fn test_roundtrip_uuid_all_zeros() {
624 let id_str = "00000000-0000-0000-0000-000000000000";
625 let coid = ClientOrderId::from(id_str);
626
627 let decoded = decode_broker_id(&encode_broker_id(&coid, TEST_BROKER_ID), TEST_BROKER_ID);
628 assert_eq!(decoded, id_str);
629 }
630
631 #[rstest]
632 fn test_roundtrip_uuid_all_f() {
633 let id_str = "ffffffff-ffff-ffff-ffff-ffffffffffff";
634 let coid = ClientOrderId::from(id_str);
635
636 let decoded = decode_broker_id(&encode_broker_id(&coid, TEST_BROKER_ID), TEST_BROKER_ID);
637 assert_eq!(decoded, id_str);
638 }
639
640 #[rstest]
641 fn test_raw_passthrough_short_id() {
642 let id_str = "my-order-123";
643 let coid = ClientOrderId::from(id_str);
644 let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
645
646 assert!(encoded.starts_with("x-TD67BGP9-R"), "got: {encoded}");
647 assert!(encoded.len() <= 36);
648
649 let decoded = decode_broker_id(&encoded, TEST_BROKER_ID);
650 assert_eq!(decoded, id_str);
651 }
652
653 #[rstest]
654 fn test_raw_passthrough_max_length() {
655 let id_str = "abcdefghijklmnopqrstuvwx"; let coid = ClientOrderId::from(id_str);
657 let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
658
659 assert_eq!(encoded.len(), 36);
660 assert!(encoded.starts_with("x-TD67BGP9-R"));
661
662 let decoded = decode_broker_id(&encoded, TEST_BROKER_ID);
663 assert_eq!(decoded, id_str);
664 }
665
666 #[rstest]
667 fn test_decode_non_prefixed_returns_as_is() {
668 let raw = "O-20260131-174827-001-001-1";
669 assert_eq!(decode_broker_id(raw, TEST_BROKER_ID), raw);
670 }
671
672 #[rstest]
673 fn test_decode_different_prefix_returns_as_is() {
674 let raw = "x-OTHERBROKER-T0000000000000";
675 assert_eq!(decode_broker_id(raw, TEST_BROKER_ID), raw);
676 }
677
678 #[rstest]
679 #[case::empty("", "invalid Binance client order ID ''")]
680 #[case::whitespace(" ", "invalid Binance client order ID ' '")]
681 #[case::non_ascii("client-é", "invalid Binance client order ID 'client-é'")]
682 #[case::missing_signal("x-TD67BGP9-", "missing broker client order ID signal")]
683 #[case::missing_raw_payload("x-TD67BGP9-R", "missing raw broker client order ID payload")]
684 #[case::invalid_o_payload(
685 "x-TD67BGP9-T000000000000!",
686 "invalid O-format broker client order ID"
687 )]
688 #[case::unknown_signal(
689 "x-TD67BGP9-Xlegacy-order",
690 "unknown broker client order ID signal byte 'X'"
691 )]
692 fn test_decode_client_order_id_rejects_invalid_input(
693 #[case] encoded: &str,
694 #[case] expected: &str,
695 ) {
696 let error = decode_client_order_id(encoded, TEST_BROKER_ID).unwrap_err();
697
698 assert_eq!(error.to_string(), expected);
699 }
700
701 #[rstest]
702 fn test_decode_client_order_id_preserves_valid_prefixed_id() {
703 let original = ClientOrderId::from("O-20260305-120000-001-001-100");
704 let encoded = encode_broker_id(&original, TEST_BROKER_ID);
705
706 let decoded = decode_client_order_id(&encoded, TEST_BROKER_ID).unwrap();
707
708 assert_eq!(decoded, original);
709 }
710
711 #[rstest]
712 fn test_decode_client_order_id_preserves_valid_legacy_id() {
713 let decoded = decode_client_order_id("legacy-order-1", TEST_BROKER_ID).unwrap();
714
715 assert_eq!(decoded, ClientOrderId::from("legacy-order-1"));
716 }
717
718 #[rstest]
719 fn test_o_format_trader_overflow_sends_without_prefix() {
720 let id_str = "O-20260131-174827-1024-001-1";
723 let coid = ClientOrderId::from(id_str);
724 let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
725 assert_eq!(encoded, id_str);
726 }
727
728 #[rstest]
729 fn test_o_format_count_overflow_sends_without_prefix() {
730 let id_str = "O-20260131-174827-001-001-1048576";
733 let coid = ClientOrderId::from(id_str);
734 let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
735 assert_eq!(encoded, id_str);
736 }
737
738 #[rstest]
739 fn test_too_long_id_sends_without_prefix() {
740 let id_str = "this-is-a-very-long-order-id-that-exceeds-everything";
741 let coid = ClientOrderId::from(id_str);
742 let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
743
744 assert_eq!(encoded, id_str);
745 }
746
747 #[rstest]
748 fn test_o_format_always_25_chars() {
749 let test_cases = [
750 "O-20200101-000000-000-000-0",
751 "O-20260131-235959-999-999-4095",
752 "O-20260305-120000-001-001-1048575",
753 ];
754
755 for id_str in test_cases {
756 let coid = ClientOrderId::from(id_str);
757 let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
758 assert_eq!(
759 encoded.len(),
760 25,
761 "got {} for {id_str}: {encoded}",
762 encoded.len()
763 );
764 }
765 }
766
767 #[rstest]
768 fn test_uuid_always_34_chars() {
769 let id_str = "550e8400-e29b-41d4-a716-446655440000";
770 let coid = ClientOrderId::from(id_str);
771 let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
772 assert_eq!(encoded.len(), 34, "got {}", encoded.len());
773 }
774
775 #[rstest]
776 fn test_broker_prefix_format() {
777 let prefix = broker_prefix(TEST_BROKER_ID);
778 assert_eq!(prefix, "x-TD67BGP9-");
779 }
780
781 #[rstest]
782 fn test_encoded_chars_are_binance_valid() {
783 let valid = |c: char| {
784 c.is_ascii_alphanumeric() || c == '.' || c == ':' || c == '/' || c == '_' || c == '-'
785 };
786
787 let ids = [
788 "O-20260131-174827-001-001-1",
789 "550e8400-e29b-41d4-a716-446655440000",
790 "short-id",
791 ];
792
793 for id_str in ids {
794 let coid = ClientOrderId::from(id_str);
795 let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
796 assert!(
797 encoded.chars().all(valid),
798 "'{encoded}' contains invalid Binance characters"
799 );
800 }
801 }
802
803 #[rstest]
804 fn test_civil_time_roundtrip() {
805 let epoch = civil_to_epoch(2020, 1, 1, 0, 0, 0).unwrap();
806 assert_eq!(epoch, O_FORMAT_EPOCH);
807 let (y, m, d, h, mi, s) = epoch_to_civil(epoch).unwrap();
808 assert_eq!((y, m, d, h, mi, s), (2020, 1, 1, 0, 0, 0));
809 }
810
811 #[rstest]
812 #[case("O-20260305-120000-001-001-1")]
813 #[case("O-20260131-174827-001-001-1")]
814 #[case("O-20260305-120000-001-001-1048575")]
815 #[case("550e8400-e29b-41d4-a716-446655440000")]
816 #[case("550e8400e29b41d4a716446655440000")]
817 #[case("my-order-42")]
818 #[case("short")]
819 fn test_end_to_end_submit_and_receive(#[case] original_id: &str) {
820 let broker_id = TEST_BROKER_ID;
821 let client_order_id = ClientOrderId::from(original_id);
822
823 let encoded = encode_broker_id(&client_order_id, broker_id);
825 assert!(encoded.len() <= 36, "encoded len {} > 36", encoded.len());
826
827 let decoded = decode_broker_id(&encoded, broker_id);
829
830 assert_eq!(decoded, original_id);
832 assert_eq!(ClientOrderId::new(decoded), client_order_id);
833 }
834
835 #[rstest]
836 fn bench_encode_decode_timing() {
837 let o_coid = ClientOrderId::from("O-20260305-120000-001-001-100");
838 let uuid_coid = ClientOrderId::from("550e8400-e29b-41d4-a716-446655440000");
839 let raw_coid = ClientOrderId::from("my-order-123");
840
841 let iterations = 100_000;
842
843 let start = std::time::Instant::now();
844
845 for _ in 0..iterations {
846 black_box(encode_broker_id(black_box(&o_coid), TEST_BROKER_ID));
847 }
848 let encode_o = start.elapsed();
849
850 let o_encoded = encode_broker_id(&o_coid, TEST_BROKER_ID);
851 let start = std::time::Instant::now();
852
853 for _ in 0..iterations {
854 black_box(decode_broker_id(black_box(&o_encoded), TEST_BROKER_ID));
855 }
856 let decode_o = start.elapsed();
857
858 let start = std::time::Instant::now();
859
860 for _ in 0..iterations {
861 black_box(encode_broker_id(black_box(&uuid_coid), TEST_BROKER_ID));
862 }
863 let encode_uuid = start.elapsed();
864
865 let uuid_encoded = encode_broker_id(&uuid_coid, TEST_BROKER_ID);
866 let start = std::time::Instant::now();
867
868 for _ in 0..iterations {
869 black_box(decode_broker_id(black_box(&uuid_encoded), TEST_BROKER_ID));
870 }
871 let decode_uuid = start.elapsed();
872
873 let start = std::time::Instant::now();
874
875 for _ in 0..iterations {
876 black_box(encode_broker_id(black_box(&raw_coid), TEST_BROKER_ID));
877 }
878 let encode_raw = start.elapsed();
879
880 let raw_encoded = encode_broker_id(&raw_coid, TEST_BROKER_ID);
881 let start = std::time::Instant::now();
882
883 for _ in 0..iterations {
884 black_box(decode_broker_id(black_box(&raw_encoded), TEST_BROKER_ID));
885 }
886 let decode_raw = start.elapsed();
887
888 let passthrough = "O-20260305-120000-001-001-100";
889 let start = std::time::Instant::now();
890
891 for _ in 0..iterations {
892 black_box(decode_broker_id(black_box(passthrough), TEST_BROKER_ID));
893 }
894 let decode_pass = start.elapsed();
895
896 println!("\n--- Broker ID Encoder Performance ({iterations} iterations) ---");
897 println!(
898 "encode O-format: {:>8.1} ns/op",
899 encode_o.as_nanos() as f64 / iterations as f64
900 );
901 println!(
902 "decode O-format: {:>8.1} ns/op",
903 decode_o.as_nanos() as f64 / iterations as f64
904 );
905 println!(
906 "encode UUID: {:>8.1} ns/op",
907 encode_uuid.as_nanos() as f64 / iterations as f64
908 );
909 println!(
910 "decode UUID: {:>8.1} ns/op",
911 decode_uuid.as_nanos() as f64 / iterations as f64
912 );
913 println!(
914 "encode raw: {:>8.1} ns/op",
915 encode_raw.as_nanos() as f64 / iterations as f64
916 );
917 println!(
918 "decode raw: {:>8.1} ns/op",
919 decode_raw.as_nanos() as f64 / iterations as f64
920 );
921 println!(
922 "decode passthrough: {:>8.1} ns/op",
923 decode_pass.as_nanos() as f64 / iterations as f64
924 );
925 }
926}