1use std::fmt::Write;
138
139use anyhow::Context;
140use nautilus_model::identifiers::ClientOrderId;
141
142const BASE62_CHARS: &[u8; 62] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
144
145const BASE62_DECODE: [u8; 128] = {
148 let mut table = [0xFFu8; 128];
149 let mut i = 0u8;
150 while i < 62 {
151 table[BASE62_CHARS[i as usize] as usize] = i;
152 i += 1;
153 }
154 table
155};
156
157const O_FORMAT_EPOCH: i64 = 1_577_836_800;
159
160const O_FORMAT_B62_LEN: usize = 13;
162
163const UUID_B62_LEN: usize = 22;
165
166const MAX_CLIENT_ORDER_ID_LEN: usize = 36;
168
169const SIGNAL_O_HYPHENS: u8 = b'T';
170const SIGNAL_O_NO_HYPHENS: u8 = b't';
171const SIGNAL_UUID_HYPHENS: u8 = b'U';
172const SIGNAL_UUID_NO_HYPHENS: u8 = b'u';
173const SIGNAL_RAW: u8 = b'R';
174const SIGNAL_O_TAGS: u8 = b'A';
175const O_TAGS_B62_LEN: usize = 13;
176const O_TAGS_COUNT_BITS: u32 = 22;
177const O_TAGS_COUNT_MAX: u32 = (1 << O_TAGS_COUNT_BITS) - 1;
178
179#[must_use]
184pub fn encode_broker_id(client_order_id: &ClientOrderId, broker_id: &str) -> String {
185 let id_str = client_order_id.as_str();
186 let budget = MAX_CLIENT_ORDER_ID_LEN - (3 + broker_id.len());
187
188 if let Some((packed, has_hyphens)) = pack_o_format(id_str) {
189 let signal = if has_hyphens {
190 SIGNAL_O_HYPHENS
191 } else {
192 SIGNAL_O_NO_HYPHENS
193 };
194 let b62 = encode_base62::<O_FORMAT_B62_LEN>(packed);
195 return build_encoded(broker_id, signal, &b62);
196 }
197
198 if let Some((value, has_hyphens)) = parse_uuid_hex(id_str) {
199 let signal = if has_hyphens {
200 SIGNAL_UUID_HYPHENS
201 } else {
202 SIGNAL_UUID_NO_HYPHENS
203 };
204 let b62 = encode_base62::<UUID_B62_LEN>(value);
205 return build_encoded(broker_id, signal, &b62);
206 }
207
208 if id_str.len() < budget {
209 return build_encoded(broker_id, SIGNAL_RAW, id_str.as_bytes());
210 }
211
212 if 3 + broker_id.len() + O_TAGS_B62_LEN < budget
214 && let Some(packed) = pack_o_tags(id_str)
215 {
216 return build_encoded(
217 broker_id,
218 SIGNAL_O_TAGS,
219 &encode_base62::<O_TAGS_B62_LEN>(packed),
220 );
221 }
222
223 log::warn!(
224 "ClientOrderId '{id_str}' ({} chars) exceeds broker ID encoding budget ({budget} chars), sending without prefix",
225 id_str.len(),
226 );
227 id_str.to_string()
228}
229
230#[must_use]
236pub fn decode_broker_id(encoded: &str, broker_id: &str) -> String {
237 match decode_broker_id_checked(encoded, broker_id) {
238 Ok(decoded) => decoded,
239 Err(e) => {
240 log::warn!("Failed to decode broker client order ID: {e}");
241 encoded.to_string()
242 }
243 }
244}
245
246pub(crate) fn decode_client_order_id(
256 encoded: &str,
257 broker_id: &str,
258) -> anyhow::Result<ClientOrderId> {
259 let decoded = decode_broker_id_checked(encoded, broker_id)?;
260 ClientOrderId::new_checked(decoded)
261 .with_context(|| format!("invalid Binance client order ID '{encoded}'"))
262}
263
264fn decode_broker_id_checked(encoded: &str, broker_id: &str) -> anyhow::Result<String> {
265 if let Some(data) = tagged_payload(encoded, broker_id) {
266 let packed =
267 decode_base62(data).context("invalid tagged O-format broker client order ID")?;
268 return unpack_o_tags(packed);
269 }
270
271 let Some(payload) = encoded
272 .strip_prefix("x-")
273 .and_then(|s| s.strip_prefix(broker_id))
274 .and_then(|s| s.strip_prefix('-'))
275 else {
276 return Ok(encoded.to_string());
277 };
278
279 let Some((&signal, data)) = payload.as_bytes().split_first() else {
280 anyhow::bail!("missing broker client order ID signal");
281 };
282
283 match signal {
284 SIGNAL_O_HYPHENS | SIGNAL_O_NO_HYPHENS => {
285 anyhow::ensure!(
286 data.len() == O_FORMAT_B62_LEN,
287 "invalid O-format broker client order ID payload length"
288 );
289 let packed = decode_base62(data).context("invalid O-format broker client order ID")?;
290 Ok(unpack_o_format(packed, signal == SIGNAL_O_HYPHENS))
291 }
292 SIGNAL_UUID_HYPHENS | SIGNAL_UUID_NO_HYPHENS => {
293 anyhow::ensure!(
294 data.len() == UUID_B62_LEN,
295 "invalid UUID broker client order ID payload length"
296 );
297 let value = decode_base62(data).context("invalid UUID broker client order ID")?;
298 Ok(format_uuid(value, signal == SIGNAL_UUID_HYPHENS))
299 }
300 SIGNAL_RAW => {
301 let raw = std::str::from_utf8(data).context("invalid raw broker client order ID")?;
302 anyhow::ensure!(
303 !raw.is_empty(),
304 "missing raw broker client order ID payload"
305 );
306 Ok(raw.to_string())
307 }
308 _ => anyhow::bail!(
309 "unknown broker client order ID signal byte '{}'",
310 signal as char
311 ),
312 }
313}
314
315pub(crate) fn legacy_client_order_id(encoded: &str, broker_id: &str) -> Option<String> {
316 let packed = decode_base62(tagged_payload(encoded, broker_id)?)?;
317 unpack_o_tags(packed).ok()
318}
319
320fn tagged_payload<'a>(encoded: &'a str, broker_id: &str) -> Option<&'a [u8]> {
321 if encoded.len() != 3 + broker_id.len() + O_TAGS_B62_LEN
322 || encoded.len() >= MAX_CLIENT_ORDER_ID_LEN - (3 + broker_id.len())
323 {
324 return None;
325 }
326
327 let payload = encoded.strip_prefix("x-")?.strip_prefix(broker_id)?;
328 let (&signal, data) = payload.as_bytes().split_first()?;
329 (signal == SIGNAL_O_TAGS).then_some(data)
330}
331
332fn build_encoded(broker_id: &str, signal: u8, payload: &[u8]) -> String {
333 let tagged = signal == SIGNAL_O_TAGS;
334 let mut result =
335 String::with_capacity(3 + broker_id.len() + usize::from(!tagged) + payload.len());
336 result.push_str("x-");
337 result.push_str(broker_id);
338
339 if !tagged {
340 result.push('-');
341 }
342
343 result.push(signal as char);
344 result.push_str(std::str::from_utf8(payload).expect("encoded ID is valid UTF-8"));
345 result
346}
347
348fn encode_base62<const N: usize>(mut value: u128) -> [u8; N] {
349 const DIGITS: usize = 10;
350 const RADIX: u128 = 62u128.pow(DIGITS as u32);
351 let mut buf = [b'0'; N];
352 for chunk in buf.rchunks_mut(DIGITS) {
353 let mut part = (value % RADIX) as u64;
354 value /= RADIX;
355
356 for byte in chunk.iter_mut().rev() {
357 *byte = BASE62_CHARS[(part % 62) as usize];
358 part /= 62;
359 }
360 }
361 buf
362}
363
364fn decode_base62(encoded: &[u8]) -> Option<u128> {
365 let mut value: u128 = 0;
366
367 for &byte in encoded {
368 let digit = BASE62_DECODE[byte as usize & 0x7F];
369
370 if digit == 0xFF || !byte.is_ascii() {
371 return None;
372 }
373 value = value.checked_mul(62)?.checked_add(digit as u128)?;
374 }
375 Some(value)
376}
377
378fn parse_digits(bytes: &[u8]) -> Option<u32> {
379 let mut n: u32 = 0;
380
381 for &b in bytes {
382 if !b.is_ascii_digit() {
383 return None;
384 }
385 n = n * 10 + (b - b'0') as u32;
386 }
387 Some(n)
388}
389
390fn pack_o_format(id_str: &str) -> Option<(u128, bool)> {
391 let b = id_str.as_bytes();
392
393 if b.first() != Some(&b'O') {
394 return None;
395 }
396
397 let (year, month, day, hour, minute, second, trader, strategy, count, has_hyphens) =
398 if b.get(1) == Some(&b'-') {
399 if b.len() < 23 || b[10] != b'-' || b[17] != b'-' {
402 return None;
403 }
404 let h4 = memchr_byte(b'-', &b[18..])?;
405 let trader_end = 18 + h4;
406 let h5 = memchr_byte(b'-', &b[trader_end + 1..])?;
407 let strategy_end = trader_end + 1 + h5;
408
409 (
410 parse_digits(&b[2..6])?,
411 parse_digits(&b[6..8])?,
412 parse_digits(&b[8..10])?,
413 parse_digits(&b[11..13])?,
414 parse_digits(&b[13..15])?,
415 parse_digits(&b[15..17])?,
416 parse_digits(&b[18..trader_end])?,
417 parse_digits(&b[trader_end + 1..strategy_end])?,
418 parse_digits(&b[strategy_end + 1..])?,
419 true,
420 )
421 } else {
422 if b.len() < 22 {
424 return None;
425 }
426 (
427 parse_digits(&b[1..5])?,
428 parse_digits(&b[5..7])?,
429 parse_digits(&b[7..9])?,
430 parse_digits(&b[9..11])?,
431 parse_digits(&b[11..13])?,
432 parse_digits(&b[13..15])?,
433 parse_digits(&b[15..18])?,
434 parse_digits(&b[18..21])?,
435 parse_digits(&b[21..])?,
436 false,
437 )
438 };
439
440 if trader > 1023 || strategy > 1023 || count > 0xF_FFFF {
441 return None;
442 }
443
444 let secs_since_epoch = civil_to_epoch(year, month, day, hour, minute, second)? - O_FORMAT_EPOCH;
445
446 if secs_since_epoch < 0 {
447 return None;
448 }
449
450 let packed = (secs_since_epoch as u128) << 40
451 | (trader as u128) << 30
452 | (strategy as u128) << 20
453 | (count as u128);
454
455 Some((packed, has_hyphens))
456}
457
458fn unpack_o_format(packed: u128, has_hyphens: bool) -> String {
459 let count = (packed & 0xF_FFFF) as u32;
460 let strategy = ((packed >> 20) & 0x3FF) as u32;
461 let trader = ((packed >> 30) & 0x3FF) as u32;
462 let secs_since_epoch = (packed >> 40) as i64;
463
464 let timestamp = secs_since_epoch + O_FORMAT_EPOCH;
465 let Some((year, month, day, hour, minute, second)) = epoch_to_civil(timestamp) else {
466 log::warn!("Failed to decode O-format timestamp: {timestamp}");
467 return format!("DECODE_ERROR_{packed}");
468 };
469
470 let mut result = String::with_capacity(MAX_CLIENT_ORDER_ID_LEN);
471
472 if has_hyphens {
473 write!(result, "O-{year:04}{month:02}{day:02}-{hour:02}{minute:02}{second:02}-{trader:03}-{strategy:03}-{count}")
474 .expect("writing to String should not fail");
475 } else {
476 write!(result, "O{year:04}{month:02}{day:02}{hour:02}{minute:02}{second:02}{trader:03}{strategy:03}{count}")
477 .expect("writing to String should not fail");
478 }
479
480 result
481}
482
483fn pack_o_tags(id: &str) -> Option<u128> {
484 let mut parts = id.split('-');
485 if parts.next()? != "O" {
486 return None;
487 }
488
489 let date = parts.next()?.as_bytes();
490 let time = parts.next()?.as_bytes();
491 let trader = parts.next()?;
492 let strategy = parts.next()?;
493 let count = parts.next()?;
494 if parts.next().is_some() || date.len() != 8 || time.len() != 6 {
495 return None;
496 }
497
498 let epoch = civil_to_epoch(
499 parse_digits(&date[..4])?,
500 parse_digits(&date[4..6])?,
501 parse_digits(&date[6..])?,
502 parse_digits(&time[..2])?,
503 parse_digits(&time[2..4])?,
504 parse_digits(&time[4..])?,
505 )?;
506 let seconds = u32::try_from(epoch - O_FORMAT_EPOCH).ok()?;
507 let count = count.parse::<u32>().ok()?;
508 if count > O_TAGS_COUNT_MAX {
509 return None;
510 }
511
512 for swapped in [false, true] {
513 let (tag, numeric) = if swapped {
514 (strategy, trader)
515 } else {
516 (trader, strategy)
517 };
518
519 let Some(tag) = pack_tag(tag) else {
520 continue;
521 };
522
523 let Ok(numeric) = numeric.parse::<u32>() else {
524 continue;
525 };
526
527 if numeric > 1023 {
528 continue;
529 }
530
531 let packed = ((seconds as u128) << 45)
532 | ((swapped as u128) << 44)
533 | ((tag as u128) << 32)
534 | ((numeric as u128) << O_TAGS_COUNT_BITS)
535 | count as u128;
536
537 if unpack_o_tags(packed).ok()?.as_str() == id {
538 return Some(packed);
539 }
540 }
541
542 None
543}
544
545fn unpack_o_tags(packed: u128) -> anyhow::Result<String> {
546 anyhow::ensure!(
547 packed >> 77 == 0,
548 "tagged O-format payload exceeds its bit budget"
549 );
550 let count = (packed & O_TAGS_COUNT_MAX as u128) as u32;
551 let numeric = ((packed >> O_TAGS_COUNT_BITS) & 0x3FF) as u32;
552 let tag = unpack_tag(((packed >> 32) & 0xFFF) as u32)?;
553 let swapped = ((packed >> 44) & 1) != 0;
554 let (year, month, day, hour, minute, second) =
555 epoch_to_civil((packed >> 45) as i64 + O_FORMAT_EPOCH)
556 .context("invalid tagged O-format timestamp")?;
557 let mut result = String::with_capacity(MAX_CLIENT_ORDER_ID_LEN);
558 write!(
559 result,
560 "O-{year:04}{month:02}{day:02}-{hour:02}{minute:02}{second:02}-"
561 )
562 .expect("writing to String should not fail");
563
564 if swapped {
565 write!(result, "{numeric:03}-{tag}-{count}").expect("writing to String should not fail");
566 } else {
567 write!(result, "{tag}-{numeric:03}-{count}").expect("writing to String should not fail");
568 }
569
570 Ok(result)
571}
572
573fn pack_tag(tag: &str) -> Option<u32> {
574 if !(1..=2).contains(&tag.len()) {
575 return None;
576 }
577
578 let mut value = 0;
579
580 for byte in tag.bytes() {
581 let digit = *BASE62_DECODE.get(byte as usize)?;
582 if digit == 0xFF {
583 return None;
584 }
585
586 value = value * 62 + u32::from(digit);
587 }
588
589 match tag.len() {
590 1 => Some(value + 1),
591 2 => Some(value + 63),
592 _ => None,
593 }
594}
595
596fn unpack_tag(value: u32) -> anyhow::Result<String> {
597 anyhow::ensure!((1..=3906).contains(&value), "invalid tagged O-format tag");
598 let mut tag = String::with_capacity(2);
599
600 if value <= 62 {
601 tag.push(BASE62_CHARS[(value - 1) as usize] as char);
602 } else {
603 let value = value - 63;
604 tag.push(BASE62_CHARS[(value / 62) as usize] as char);
605 tag.push(BASE62_CHARS[(value % 62) as usize] as char);
606 }
607
608 Ok(tag)
609}
610
611fn parse_uuid_hex(id_str: &str) -> Option<(u128, bool)> {
612 let b = id_str.as_bytes();
613
614 if b.len() == 36 && b[8] == b'-' {
615 if b[13] != b'-' || b[18] != b'-' || b[23] != b'-' {
617 return None;
618 }
619 let mut value: u128 = 0;
620
621 for &byte in b {
622 if byte == b'-' {
623 continue;
624 }
625 let nibble = hex_digit(byte)?;
626 value = (value << 4) | nibble as u128;
627 }
628 Some((value, true))
629 } else if b.len() == 32 {
630 let mut value: u128 = 0;
631
632 for &byte in b {
633 let nibble = hex_digit(byte)?;
634 value = (value << 4) | nibble as u128;
635 }
636 Some((value, false))
637 } else {
638 None
639 }
640}
641
642fn format_uuid(value: u128, has_hyphens: bool) -> String {
643 const HEX: &[u8; 16] = b"0123456789abcdef";
644 let bytes = value.to_be_bytes();
645
646 if has_hyphens {
647 let mut buf = [0u8; 36];
648 let mut pos = 0;
649
650 for (i, &b) in bytes.iter().enumerate() {
651 if i == 4 || i == 6 || i == 8 || i == 10 {
652 buf[pos] = b'-';
653 pos += 1;
654 }
655 buf[pos] = HEX[(b >> 4) as usize];
656 buf[pos + 1] = HEX[(b & 0x0F) as usize];
657 pos += 2;
658 }
659 std::str::from_utf8(&buf)
660 .expect("hex is valid UTF-8")
661 .to_string()
662 } else {
663 let mut buf = [0u8; 32];
664 for (i, &b) in bytes.iter().enumerate() {
665 buf[i * 2] = HEX[(b >> 4) as usize];
666 buf[i * 2 + 1] = HEX[(b & 0x0F) as usize];
667 }
668 std::str::from_utf8(&buf)
669 .expect("hex is valid UTF-8")
670 .to_string()
671 }
672}
673
674fn hex_digit(byte: u8) -> Option<u8> {
675 match byte {
676 b'0'..=b'9' => Some(byte - b'0'),
677 b'a'..=b'f' => Some(byte - b'a' + 10),
678 b'A'..=b'F' => Some(byte - b'A' + 10),
679 _ => None,
680 }
681}
682
683fn memchr_byte(needle: u8, haystack: &[u8]) -> Option<usize> {
684 haystack.iter().position(|&b| b == needle)
685}
686
687fn civil_to_epoch(year: u32, month: u32, day: u32, hour: u32, min: u32, sec: u32) -> Option<i64> {
689 if !(1..=12).contains(&month) || !(1..=31).contains(&day) || hour > 23 || min > 59 || sec > 59 {
690 return None;
691 }
692 let y = if month <= 2 {
694 year as i64 - 1
695 } else {
696 year as i64
697 };
698 let era = y.div_euclid(400);
699 let yoe = y.rem_euclid(400) as u64;
700 let m = if month > 2 { month - 3 } else { month + 9 } as u64;
701 let doy = (153 * m + 2) / 5 + day as u64 - 1;
702 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
703 let days = era * 146097 + doe as i64 - 719468;
704 Some(days * 86400 + hour as i64 * 3600 + min as i64 * 60 + sec as i64)
705}
706
707fn epoch_to_civil(timestamp: i64) -> Option<(u32, u32, u32, u32, u32, u32)> {
709 if timestamp < 0 {
710 return None;
711 }
712 let secs_of_day = (timestamp % 86400) as u32;
713 let days = timestamp / 86400;
714
715 let hour = secs_of_day / 3600;
716 let minute = (secs_of_day % 3600) / 60;
717 let second = secs_of_day % 60;
718
719 let z = days + 719468;
721 let era = z.div_euclid(146097);
722 let doe = z.rem_euclid(146097) as u64;
723 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
724 let y = yoe as i64 + era * 400;
725 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
726 let mp = (5 * doy + 2) / 153;
727 let day = (doy - (153 * mp + 2) / 5 + 1) as u32;
728 let month = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
729 let year = if month <= 2 { y + 1 } else { y } as u32;
730
731 Some((year, month, day, hour, minute, second))
732}
733
734#[cfg(test)]
735mod tests {
736 use std::hint::black_box;
737
738 use rstest::rstest;
739
740 use super::{super::consts::BINANCE_NAUTILUS_SPOT_BROKER_ID, *};
741
742 const TEST_BROKER_ID: &str = BINANCE_NAUTILUS_SPOT_BROKER_ID;
743
744 #[rstest]
745 #[case("O-20260922-160119-V2-000-8")]
746 #[case("O-20260922-200130-V2-001-24")]
747 #[case("O-20260923-000138-V2-001-1")]
748 #[case("O-20260923-000138-V2-001-4194303")]
749 #[case("O-20260923-000138-001-aZ-42")]
750 fn test_tagged_id_preserves_exact_identity(#[case] original: &str) {
751 let id = ClientOrderId::from(original);
752 let encoded = encode_broker_id(&id, TEST_BROKER_ID);
753 assert_eq!(encoded.len(), 24);
754 assert!(encoded.starts_with("x-TD67BGP9A"));
755 assert_eq!(
756 decode_client_order_id(&encoded, TEST_BROKER_ID).unwrap(),
757 id
758 );
759
760 let custom = ClientOrderId::from(encoded.as_str());
762 let wrapped = encode_broker_id(&custom, TEST_BROKER_ID);
763 assert_eq!(wrapped, format!("x-TD67BGP9-R{encoded}"));
764 assert_eq!(
765 decode_client_order_id(&wrapped, TEST_BROKER_ID).unwrap(),
766 custom
767 );
768 }
769
770 #[rstest]
771 #[case("O-20260923-000138-V2-001-4194304")]
772 #[case("O-20260923-000138-LONG-001-1")]
773 #[case("O-20260923-000138-V2-001-01")]
774 #[case("O-20260230-000138-V2-001-1")]
775 fn test_unsupported_tagged_id_remains_unchanged(#[case] original: &str) {
776 assert_eq!(
777 encode_broker_id(&ClientOrderId::from(original), TEST_BROKER_ID),
778 original
779 );
780 }
781
782 #[rstest]
783 fn test_historical_raw_id_does_not_enter_short_namespace() {
784 let original = "x-TD67BGP9A00000000000000";
785 assert_eq!(original.len(), 25);
786 assert_eq!(
787 encode_broker_id(&ClientOrderId::from(original), TEST_BROKER_ID),
788 original
789 );
790 assert_eq!(
791 decode_client_order_id(original, TEST_BROKER_ID)
792 .unwrap()
793 .as_str(),
794 original
795 );
796 }
797
798 #[rstest]
799 #[case("O-20260305-120000-001-001-100", "x-TD67BGP9-T047IBdyLevrb2")]
800 #[case("O20260305120000001001100", "x-TD67BGP9-t047IBdyLevrb2")]
801 #[case(
802 "550e8400-e29b-41d4-a716-446655440000",
803 "x-TD67BGP9-U2aUyqjCzEIiEcYMKj7TZtw"
804 )]
805 #[case(
806 "550e8400e29b41d4a716446655440000",
807 "x-TD67BGP9-u2aUyqjCzEIiEcYMKj7TZtw"
808 )]
809 #[case("my-order-123", "x-TD67BGP9-Rmy-order-123")]
810 fn test_historical_wire_vectors(#[case] original: &str, #[case] encoded: &str) {
811 let id = ClientOrderId::from(original);
812 assert_eq!(encode_broker_id(&id, TEST_BROKER_ID), encoded);
813 assert_eq!(decode_client_order_id(encoded, TEST_BROKER_ID).unwrap(), id);
814 }
815
816 #[rstest]
817 fn test_base62_chunked_encoding_matches_digit_reference() {
818 let mut state = 0x1234_5678_90ab_cdef_2345_6789_abcd_ef01u128;
819
820 for value in [0, 1, 61, 62, 62u128.pow(10) - 1, 62u128.pow(10), u128::MAX]
821 .into_iter()
822 .chain((0..10_000).map(|_| {
823 state ^= state << 13;
824 state ^= state >> 7;
825 state ^= state << 17;
826 state
827 }))
828 {
829 let mut expected = [b'0'; 22];
830 let mut remaining = value;
831
832 for byte in expected.iter_mut().rev() {
833 *byte = BASE62_CHARS[(remaining % 62) as usize];
834 remaining /= 62;
835 }
836
837 assert_eq!(encode_base62::<22>(value), expected);
838 assert_eq!(encode_base62::<13>(value).as_slice(), &expected[9..]);
839 assert_eq!(decode_base62(&expected), Some(value));
840 }
841 }
842
843 #[rstest]
844 #[case("O-20200101-000000-0-000-0")]
845 #[case("O-21560207-062815-zz-1023-4194303")]
846 #[case("O-20260922-160119-000-00-1")]
847 fn test_tagged_packing_boundaries(#[case] original: &str) {
848 let packed = pack_o_tags(original).unwrap();
849 assert_eq!(unpack_o_tags(packed).unwrap(), original);
850 }
851
852 #[rstest]
853 fn test_base62_roundtrip_zero() {
854 let encoded = encode_base62::<13>(0);
855 let decoded = decode_base62(&encoded);
856 assert_eq!(decoded, Some(0));
857 }
858
859 #[rstest]
860 fn test_base62_roundtrip_max_72_bit() {
861 let value: u128 = (1u128 << 72) - 1;
862 let encoded = encode_base62::<13>(value);
863 let decoded = decode_base62(&encoded);
864 assert_eq!(decoded, Some(value));
865 }
866
867 #[rstest]
868 fn test_base62_roundtrip_max_128_bit() {
869 let value: u128 = u128::MAX;
870 let encoded = encode_base62::<22>(value);
871 let decoded = decode_base62(&encoded);
872 assert_eq!(decoded, Some(value));
873 }
874
875 #[rstest]
876 #[case("O-20200101-000000-000-000-0")]
877 #[case("O-20200101-000001-001-001-1")]
878 #[case("O-20260131-174827-001-001-1")]
879 #[case("O-20260131-235959-999-999-4095")]
880 #[case("O-20251215-123456-123-456-789")]
881 #[case("O-20260305-120000-001-001-100")]
882 #[case("O-20260305-120000-001-001-99999")]
883 #[case("O-20260305-120000-001-001-1048575")]
884 fn test_roundtrip_o_format_with_hyphens(#[case] id_str: &str) {
885 let coid = ClientOrderId::from(id_str);
886 let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
887
888 assert!(encoded.starts_with("x-TD67BGP9-T"), "got: {encoded}");
889 assert!(encoded.len() <= 36, "len {} > 36: {encoded}", encoded.len());
890
891 let decoded = decode_broker_id(&encoded, TEST_BROKER_ID);
892 assert_eq!(decoded, id_str);
893 }
894
895 #[rstest]
896 #[case("O202001010000000000000")]
897 #[case("O202601311748270010011")]
898 #[case("O202601312359599999994095")]
899 fn test_roundtrip_o_format_without_hyphens(#[case] id_str: &str) {
900 let coid = ClientOrderId::from(id_str);
901 let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
902
903 assert!(encoded.starts_with("x-TD67BGP9-t"), "got: {encoded}");
904 assert!(encoded.len() <= 36);
905
906 let decoded = decode_broker_id(&encoded, TEST_BROKER_ID);
907 assert_eq!(decoded, id_str);
908 }
909
910 #[rstest]
911 fn test_roundtrip_uuid_with_hyphens() {
912 let id_str = "550e8400-e29b-41d4-a716-446655440000";
913 let coid = ClientOrderId::from(id_str);
914 let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
915
916 assert!(encoded.starts_with("x-TD67BGP9-U"), "got: {encoded}");
917 assert!(encoded.len() <= 36, "len {} > 36: {encoded}", encoded.len());
918
919 let decoded = decode_broker_id(&encoded, TEST_BROKER_ID);
920 assert_eq!(decoded, id_str);
921 }
922
923 #[rstest]
924 fn test_roundtrip_uuid_without_hyphens() {
925 let id_str = "550e8400e29b41d4a716446655440000";
926 let coid = ClientOrderId::from(id_str);
927 let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
928
929 assert!(encoded.starts_with("x-TD67BGP9-u"), "got: {encoded}");
930 assert!(encoded.len() <= 36);
931
932 let decoded = decode_broker_id(&encoded, TEST_BROKER_ID);
933 assert_eq!(decoded, id_str);
934 }
935
936 #[rstest]
937 fn test_roundtrip_uuid_all_zeros() {
938 let id_str = "00000000-0000-0000-0000-000000000000";
939 let coid = ClientOrderId::from(id_str);
940
941 let decoded = decode_broker_id(&encode_broker_id(&coid, TEST_BROKER_ID), TEST_BROKER_ID);
942 assert_eq!(decoded, id_str);
943 }
944
945 #[rstest]
946 fn test_roundtrip_uuid_all_f() {
947 let id_str = "ffffffff-ffff-ffff-ffff-ffffffffffff";
948 let coid = ClientOrderId::from(id_str);
949
950 let decoded = decode_broker_id(&encode_broker_id(&coid, TEST_BROKER_ID), TEST_BROKER_ID);
951 assert_eq!(decoded, id_str);
952 }
953
954 #[rstest]
955 fn test_raw_passthrough_short_id() {
956 let id_str = "my-order-123";
957 let coid = ClientOrderId::from(id_str);
958 let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
959
960 assert!(encoded.starts_with("x-TD67BGP9-R"), "got: {encoded}");
961 assert!(encoded.len() <= 36);
962
963 let decoded = decode_broker_id(&encoded, TEST_BROKER_ID);
964 assert_eq!(decoded, id_str);
965 }
966
967 #[rstest]
968 fn test_raw_passthrough_max_length() {
969 let id_str = "abcdefghijklmnopqrstuvwx"; let coid = ClientOrderId::from(id_str);
971 let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
972
973 assert_eq!(encoded.len(), 36);
974 assert!(encoded.starts_with("x-TD67BGP9-R"));
975
976 let decoded = decode_broker_id(&encoded, TEST_BROKER_ID);
977 assert_eq!(decoded, id_str);
978 }
979
980 #[rstest]
981 fn test_decode_non_prefixed_returns_as_is() {
982 let raw = "O-20260131-174827-001-001-1";
983 assert_eq!(decode_broker_id(raw, TEST_BROKER_ID), raw);
984 }
985
986 #[rstest]
987 fn test_decode_different_prefix_returns_as_is() {
988 let raw = "x-OTHERBROKER-T0000000000000";
989 assert_eq!(decode_broker_id(raw, TEST_BROKER_ID), raw);
990 }
991
992 #[rstest]
993 #[case::empty("", "invalid Binance client order ID ''")]
994 #[case::whitespace(" ", "invalid Binance client order ID ' '")]
995 #[case::non_ascii("client-é", "invalid Binance client order ID 'client-é'")]
996 #[case::missing_signal("x-TD67BGP9-", "missing broker client order ID signal")]
997 #[case::missing_raw_payload("x-TD67BGP9-R", "missing raw broker client order ID payload")]
998 #[case::invalid_o_payload(
999 "x-TD67BGP9-T000000000000!",
1000 "invalid O-format broker client order ID"
1001 )]
1002 #[case::unknown_signal(
1003 "x-TD67BGP9-Xlegacy-order",
1004 "unknown broker client order ID signal byte 'X'"
1005 )]
1006 fn test_decode_client_order_id_rejects_invalid_input(
1007 #[case] encoded: &str,
1008 #[case] expected: &str,
1009 ) {
1010 let error = decode_client_order_id(encoded, TEST_BROKER_ID).unwrap_err();
1011
1012 assert_eq!(error.to_string(), expected);
1013 }
1014
1015 #[rstest]
1016 fn test_decode_client_order_id_preserves_valid_prefixed_id() {
1017 let original = ClientOrderId::from("O-20260305-120000-001-001-100");
1018 let encoded = encode_broker_id(&original, TEST_BROKER_ID);
1019
1020 let decoded = decode_client_order_id(&encoded, TEST_BROKER_ID).unwrap();
1021
1022 assert_eq!(decoded, original);
1023 }
1024
1025 #[rstest]
1026 fn test_decode_client_order_id_preserves_valid_legacy_id() {
1027 let decoded = decode_client_order_id("legacy-order-1", TEST_BROKER_ID).unwrap();
1028
1029 assert_eq!(decoded, ClientOrderId::from("legacy-order-1"));
1030 }
1031
1032 #[rstest]
1033 fn test_o_format_trader_overflow_sends_without_prefix() {
1034 let id_str = "O-20260131-174827-1024-001-1";
1037 let coid = ClientOrderId::from(id_str);
1038 let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
1039 assert_eq!(encoded, id_str);
1040 }
1041
1042 #[rstest]
1043 fn test_o_format_count_overflow_sends_without_prefix() {
1044 let id_str = "O-20260131-174827-001-001-1048576";
1047 let coid = ClientOrderId::from(id_str);
1048 let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
1049 assert_eq!(encoded, id_str);
1050 }
1051
1052 #[rstest]
1053 fn test_too_long_id_sends_without_prefix() {
1054 let id_str = "this-is-a-very-long-order-id-that-exceeds-everything";
1055 let coid = ClientOrderId::from(id_str);
1056 let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
1057
1058 assert_eq!(encoded, id_str);
1059 }
1060
1061 #[rstest]
1062 fn test_o_format_always_25_chars() {
1063 let test_cases = [
1064 "O-20200101-000000-000-000-0",
1065 "O-20260131-235959-999-999-4095",
1066 "O-20260305-120000-001-001-1048575",
1067 ];
1068
1069 for id_str in test_cases {
1070 let coid = ClientOrderId::from(id_str);
1071 let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
1072 assert_eq!(
1073 encoded.len(),
1074 25,
1075 "got {} for {id_str}: {encoded}",
1076 encoded.len()
1077 );
1078 }
1079 }
1080
1081 #[rstest]
1082 fn test_uuid_always_34_chars() {
1083 let id_str = "550e8400-e29b-41d4-a716-446655440000";
1084 let coid = ClientOrderId::from(id_str);
1085 let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
1086 assert_eq!(encoded.len(), 34, "got {}", encoded.len());
1087 }
1088
1089 #[rstest]
1090 fn test_broker_prefix_format() {
1091 let encoded = encode_broker_id(&ClientOrderId::from("test"), TEST_BROKER_ID);
1092 assert_eq!(encoded, "x-TD67BGP9-Rtest");
1093 }
1094
1095 #[rstest]
1096 fn test_encoded_chars_are_binance_valid() {
1097 let valid = |c: char| {
1098 c.is_ascii_alphanumeric() || c == '.' || c == ':' || c == '/' || c == '_' || c == '-'
1099 };
1100
1101 let ids = [
1102 "O-20260131-174827-001-001-1",
1103 "550e8400-e29b-41d4-a716-446655440000",
1104 "short-id",
1105 ];
1106
1107 for id_str in ids {
1108 let coid = ClientOrderId::from(id_str);
1109 let encoded = encode_broker_id(&coid, TEST_BROKER_ID);
1110 assert!(
1111 encoded.chars().all(valid),
1112 "'{encoded}' contains invalid Binance characters"
1113 );
1114 }
1115 }
1116
1117 #[rstest]
1118 fn test_civil_time_roundtrip() {
1119 let epoch = civil_to_epoch(2020, 1, 1, 0, 0, 0).unwrap();
1120 assert_eq!(epoch, O_FORMAT_EPOCH);
1121 let (y, m, d, h, mi, s) = epoch_to_civil(epoch).unwrap();
1122 assert_eq!((y, m, d, h, mi, s), (2020, 1, 1, 0, 0, 0));
1123 }
1124
1125 #[rstest]
1126 #[case("O-20260305-120000-001-001-1")]
1127 #[case("O-20260131-174827-001-001-1")]
1128 #[case("O-20260305-120000-001-001-1048575")]
1129 #[case("550e8400-e29b-41d4-a716-446655440000")]
1130 #[case("550e8400e29b41d4a716446655440000")]
1131 #[case("my-order-42")]
1132 #[case("short")]
1133 fn test_end_to_end_submit_and_receive(#[case] original_id: &str) {
1134 let broker_id = TEST_BROKER_ID;
1135 let client_order_id = ClientOrderId::from(original_id);
1136
1137 let encoded = encode_broker_id(&client_order_id, broker_id);
1139 assert!(encoded.len() <= 36, "encoded len {} > 36", encoded.len());
1140
1141 let decoded = decode_broker_id(&encoded, broker_id);
1143
1144 assert_eq!(decoded, original_id);
1146 assert_eq!(ClientOrderId::new(decoded), client_order_id);
1147 }
1148
1149 #[rstest]
1150 fn bench_encode_decode_timing() {
1151 let o_coid = ClientOrderId::from("O-20260305-120000-001-001-100");
1152 let uuid_coid = ClientOrderId::from("550e8400-e29b-41d4-a716-446655440000");
1153 let raw_coid = ClientOrderId::from("my-order-123");
1154
1155 let iterations = 100_000;
1156
1157 let start = std::time::Instant::now();
1158
1159 for _ in 0..iterations {
1160 black_box(encode_broker_id(black_box(&o_coid), TEST_BROKER_ID));
1161 }
1162 let encode_o = start.elapsed();
1163
1164 let o_encoded = encode_broker_id(&o_coid, TEST_BROKER_ID);
1165 let start = std::time::Instant::now();
1166
1167 for _ in 0..iterations {
1168 black_box(decode_broker_id(black_box(&o_encoded), TEST_BROKER_ID));
1169 }
1170 let decode_o = start.elapsed();
1171
1172 let start = std::time::Instant::now();
1173
1174 for _ in 0..iterations {
1175 black_box(encode_broker_id(black_box(&uuid_coid), TEST_BROKER_ID));
1176 }
1177 let encode_uuid = start.elapsed();
1178
1179 let uuid_encoded = encode_broker_id(&uuid_coid, TEST_BROKER_ID);
1180 let start = std::time::Instant::now();
1181
1182 for _ in 0..iterations {
1183 black_box(decode_broker_id(black_box(&uuid_encoded), TEST_BROKER_ID));
1184 }
1185 let decode_uuid = start.elapsed();
1186
1187 let start = std::time::Instant::now();
1188
1189 for _ in 0..iterations {
1190 black_box(encode_broker_id(black_box(&raw_coid), TEST_BROKER_ID));
1191 }
1192 let encode_raw = start.elapsed();
1193
1194 let raw_encoded = encode_broker_id(&raw_coid, TEST_BROKER_ID);
1195 let start = std::time::Instant::now();
1196
1197 for _ in 0..iterations {
1198 black_box(decode_broker_id(black_box(&raw_encoded), TEST_BROKER_ID));
1199 }
1200 let decode_raw = start.elapsed();
1201
1202 let passthrough = "O-20260305-120000-001-001-100";
1203 let start = std::time::Instant::now();
1204
1205 for _ in 0..iterations {
1206 black_box(decode_broker_id(black_box(passthrough), TEST_BROKER_ID));
1207 }
1208 let decode_pass = start.elapsed();
1209
1210 println!("\n--- Broker ID Encoder Performance ({iterations} iterations) ---");
1211 println!(
1212 "encode O-format: {:>8.1} ns/op",
1213 encode_o.as_nanos() as f64 / iterations as f64
1214 );
1215 println!(
1216 "decode O-format: {:>8.1} ns/op",
1217 decode_o.as_nanos() as f64 / iterations as f64
1218 );
1219 println!(
1220 "encode UUID: {:>8.1} ns/op",
1221 encode_uuid.as_nanos() as f64 / iterations as f64
1222 );
1223 println!(
1224 "decode UUID: {:>8.1} ns/op",
1225 decode_uuid.as_nanos() as f64 / iterations as f64
1226 );
1227 println!(
1228 "encode raw: {:>8.1} ns/op",
1229 encode_raw.as_nanos() as f64 / iterations as f64
1230 );
1231 println!(
1232 "decode raw: {:>8.1} ns/op",
1233 decode_raw.as_nanos() as f64 / iterations as f64
1234 );
1235 println!(
1236 "decode passthrough: {:>8.1} ns/op",
1237 decode_pass.as_nanos() as f64 / iterations as f64
1238 );
1239 }
1240}