1use std::sync::{
40 Arc,
41 atomic::{AtomicI64, Ordering},
42};
43
44use dashmap::DashMap;
45use thiserror::Error;
46
47pub const DEFAULT_SKIP_WINDOW: u32 = 16;
53
54#[derive(Debug, Error, PartialEq, Eq)]
56pub enum NonceError {
57 #[error("nonce manager not initialized for account={account_index}, api_key={api_key_index}")]
60 NotInitialized {
61 account_index: i64,
63 api_key_index: u8,
65 },
66 #[error(
68 "skip-window exhausted for account={account_index}, api_key={api_key_index}: outstanding={outstanding}, window={skip_window}"
69 )]
70 SkipWindowExhausted {
71 account_index: i64,
73 api_key_index: u8,
75 outstanding: u32,
77 skip_window: u32,
79 },
80 #[error(
83 "no outstanding nonce to roll back for account={account_index}, api_key={api_key_index}"
84 )]
85 NothingToRollBack {
86 account_index: i64,
88 api_key_index: u8,
90 },
91}
92
93#[derive(Debug)]
99pub struct NonceManager {
100 skip_window: u32,
101 states: DashMap<(i64, u8), Arc<AccountNonce>>,
102}
103
104impl NonceManager {
105 #[must_use]
107 pub fn new(skip_window: u32) -> Self {
108 Self {
109 skip_window,
110 states: DashMap::new(),
111 }
112 }
113
114 #[must_use]
116 pub fn skip_window(&self) -> u32 {
117 self.skip_window
118 }
119
120 pub fn refresh(&self, account_index: i64, api_key_index: u8, venue_next_nonce: i64) {
139 let entry = self
140 .states
141 .entry((account_index, api_key_index))
142 .or_insert_with(|| Arc::new(AccountNonce::new(venue_next_nonce - 1)));
143
144 entry
152 .baseline
153 .store(venue_next_nonce - 1, Ordering::Release);
154 entry
155 .last_issued
156 .store(venue_next_nonce - 1, Ordering::Release);
157 }
158
159 pub fn sync_from_venue(
180 &self,
181 account_index: i64,
182 api_key_index: u8,
183 venue_next_nonce: i64,
184 ) -> Result<(), NonceError> {
185 let state = self.state_for(account_index, api_key_index)?;
186 let applied = venue_next_nonce - 1;
187 state.last_issued.fetch_max(applied, Ordering::AcqRel);
188 state.baseline.fetch_max(applied, Ordering::AcqRel);
189 Ok(())
190 }
191
192 pub fn next_nonce(&self, account_index: i64, api_key_index: u8) -> Result<i64, NonceError> {
202 let state = self.state_for(account_index, api_key_index)?;
203
204 loop {
205 let last = state.last_issued.load(Ordering::Acquire);
206 let baseline = state.baseline.load(Ordering::Acquire);
207 let next = last.wrapping_add(1);
208 let outstanding = next.saturating_sub(baseline);
209
210 if outstanding > i64::from(self.skip_window) {
211 return Err(NonceError::SkipWindowExhausted {
212 account_index,
213 api_key_index,
214 outstanding: u32::try_from(outstanding).unwrap_or(u32::MAX),
215 skip_window: self.skip_window,
216 });
217 }
218
219 if state
220 .last_issued
221 .compare_exchange_weak(last, next, Ordering::AcqRel, Ordering::Acquire)
222 .is_ok()
223 {
224 return Ok(next);
225 }
226 }
227 }
228
229 pub fn ack_success(
244 &self,
245 account_index: i64,
246 api_key_index: u8,
247 nonce: i64,
248 ) -> Result<(), NonceError> {
249 let state = self.state_for(account_index, api_key_index)?;
250 state.baseline.fetch_max(nonce, Ordering::AcqRel);
251 Ok(())
252 }
253
254 pub fn ack_failure(&self, account_index: i64, api_key_index: u8) -> Result<i64, NonceError> {
267 let state = self.state_for(account_index, api_key_index)?;
268
269 loop {
270 let last = state.last_issued.load(Ordering::Acquire);
271 let baseline = state.baseline.load(Ordering::Acquire);
272
273 if last == baseline {
274 return Err(NonceError::NothingToRollBack {
275 account_index,
276 api_key_index,
277 });
278 }
279
280 let prev = last - 1;
281
282 if state
283 .last_issued
284 .compare_exchange_weak(last, prev, Ordering::AcqRel, Ordering::Acquire)
285 .is_ok()
286 {
287 return Ok(last);
288 }
289 }
290 }
291
292 pub fn ack_failure_if_latest(
308 &self,
309 account_index: i64,
310 api_key_index: u8,
311 nonce: i64,
312 ) -> Result<bool, NonceError> {
313 let state = self.state_for(account_index, api_key_index)?;
314
315 loop {
316 let last = state.last_issued.load(Ordering::Acquire);
317 let baseline = state.baseline.load(Ordering::Acquire);
318
319 if last != nonce || last <= baseline {
320 return Ok(false);
321 }
322
323 if state
324 .last_issued
325 .compare_exchange_weak(last, last - 1, Ordering::AcqRel, Ordering::Acquire)
326 .is_ok()
327 {
328 return Ok(true);
329 }
330 }
331 }
332
333 #[must_use]
335 pub fn last_issued(&self, account_index: i64, api_key_index: u8) -> Option<i64> {
336 self.states
337 .get(&(account_index, api_key_index))
338 .map(|s| s.last_issued.load(Ordering::Acquire))
339 }
340
341 #[must_use]
343 pub fn baseline(&self, account_index: i64, api_key_index: u8) -> Option<i64> {
344 self.states
345 .get(&(account_index, api_key_index))
346 .map(|s| s.baseline.load(Ordering::Acquire))
347 }
348
349 fn state_for(
353 &self,
354 account_index: i64,
355 api_key_index: u8,
356 ) -> Result<Arc<AccountNonce>, NonceError> {
357 let entry =
358 self.states
359 .get(&(account_index, api_key_index))
360 .ok_or(NonceError::NotInitialized {
361 account_index,
362 api_key_index,
363 })?;
364 let state = entry.value().clone();
365 drop(entry);
366 Ok(state)
367 }
368}
369
370impl Default for NonceManager {
371 fn default() -> Self {
372 Self::new(DEFAULT_SKIP_WINDOW)
373 }
374}
375
376#[derive(Debug)]
377struct AccountNonce {
378 last_issued: AtomicI64,
379 baseline: AtomicI64,
380}
381
382impl AccountNonce {
383 fn new(initial: i64) -> Self {
384 Self {
385 last_issued: AtomicI64::new(initial),
386 baseline: AtomicI64::new(initial),
387 }
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use std::{sync::Arc as StdArc, thread};
394
395 use proptest::prelude::*;
396 use rstest::rstest;
397
398 use super::*;
399
400 const ACCOUNT: i64 = 12345;
401 const API_KEY: u8 = 5;
402
403 #[rstest]
404 fn next_nonce_uninitialized_errors() {
405 let mgr = NonceManager::new(8);
406 let err = mgr.next_nonce(ACCOUNT, API_KEY).expect_err("must error");
407 assert_eq!(
408 err,
409 NonceError::NotInitialized {
410 account_index: ACCOUNT,
411 api_key_index: API_KEY
412 },
413 );
414 }
415
416 #[rstest]
417 fn ack_failure_uninitialized_errors() {
418 let mgr = NonceManager::new(8);
419 let err = mgr.ack_failure(ACCOUNT, API_KEY).expect_err("must error");
420 assert_eq!(
421 err,
422 NonceError::NotInitialized {
423 account_index: ACCOUNT,
424 api_key_index: API_KEY
425 },
426 );
427 }
428
429 #[rstest]
430 fn default_uses_default_skip_window() {
431 let mgr = NonceManager::default();
432 assert_eq!(
433 mgr.skip_window(),
434 DEFAULT_SKIP_WINDOW,
435 "Default impl must use DEFAULT_SKIP_WINDOW, was {}",
436 mgr.skip_window(),
437 );
438 }
439
440 #[rstest]
441 fn last_issued_and_baseline_return_none_for_absent_key() {
442 let mgr = NonceManager::new(8);
443 assert_eq!(
444 mgr.last_issued(ACCOUNT, API_KEY),
445 None,
446 "absent key must report no last_issued",
447 );
448 assert_eq!(
449 mgr.baseline(ACCOUNT, API_KEY),
450 None,
451 "absent key must report no baseline",
452 );
453 }
454
455 #[rstest]
456 fn baseline_pins_to_refresh_value_through_allocations() {
457 let mgr = NonceManager::new(8);
458 mgr.refresh(ACCOUNT, API_KEY, 42);
459 assert_eq!(
460 mgr.baseline(ACCOUNT, API_KEY),
461 Some(41),
462 "baseline must equal venue_next_nonce - 1 after refresh",
463 );
464
465 for _ in 0..3 {
466 mgr.next_nonce(ACCOUNT, API_KEY).unwrap();
467 }
468 assert_eq!(
469 mgr.baseline(ACCOUNT, API_KEY),
470 Some(41),
471 "baseline must not move when next_nonce advances last_issued",
472 );
473
474 mgr.refresh(ACCOUNT, API_KEY, 100);
475 assert_eq!(
476 mgr.baseline(ACCOUNT, API_KEY),
477 Some(99),
478 "subsequent refresh must reset baseline to new venue value - 1",
479 );
480 }
481
482 #[rstest]
483 fn refresh_then_next_nonce_starts_at_venue_value() {
484 let mgr = NonceManager::new(8);
485 mgr.refresh(ACCOUNT, API_KEY, 42);
486 let n = mgr.next_nonce(ACCOUNT, API_KEY).unwrap();
487 assert_eq!(n, 42, "first nonce must equal venue baseline, was {n}");
488 }
489
490 #[rstest]
491 fn next_nonce_is_monotonic_and_gap_free() {
492 let mgr = NonceManager::new(64);
493 mgr.refresh(ACCOUNT, API_KEY, 0);
494 let issued: Vec<i64> = (0..32)
495 .map(|_| mgr.next_nonce(ACCOUNT, API_KEY).unwrap())
496 .collect();
497 let expected: Vec<i64> = (0..32).collect();
498 assert_eq!(
499 issued, expected,
500 "nonces must be monotonic and gap-free, was {issued:?}",
501 );
502 }
503
504 #[rstest]
505 fn skip_window_caps_outstanding_allocations() {
506 let mgr = NonceManager::new(4);
507 mgr.refresh(ACCOUNT, API_KEY, 100);
508 for _ in 0..4 {
509 mgr.next_nonce(ACCOUNT, API_KEY).unwrap();
510 }
511 let err = mgr.next_nonce(ACCOUNT, API_KEY).expect_err("must error");
512 match err {
513 NonceError::SkipWindowExhausted {
514 outstanding,
515 skip_window,
516 ..
517 } => {
518 assert_eq!(skip_window, 4, "skip_window mismatch, was {skip_window}");
519 assert_eq!(outstanding, 5, "outstanding mismatch, was {outstanding}");
520 }
521 other => panic!("expected SkipWindowExhausted, was {other:?}"),
522 }
523 }
524
525 #[rstest]
526 fn ack_failure_rolls_back_most_recent_issuance() {
527 let mgr = NonceManager::new(8);
528 mgr.refresh(ACCOUNT, API_KEY, 0);
529 let issued = mgr.next_nonce(ACCOUNT, API_KEY).unwrap();
530 let rolled = mgr.ack_failure(ACCOUNT, API_KEY).unwrap();
531 assert_eq!(
532 rolled, issued,
533 "ack_failure must report rolled-back nonce, was {rolled}",
534 );
535 let reused = mgr.next_nonce(ACCOUNT, API_KEY).unwrap();
536 assert_eq!(
537 reused, issued,
538 "rolled-back nonce must be reissued, was {reused}"
539 );
540 }
541
542 #[rstest]
543 fn ack_failure_at_baseline_errors() {
544 let mgr = NonceManager::new(8);
545 mgr.refresh(ACCOUNT, API_KEY, 7);
546 let err = mgr.ack_failure(ACCOUNT, API_KEY).expect_err("must error");
547 assert_eq!(
548 err,
549 NonceError::NothingToRollBack {
550 account_index: ACCOUNT,
551 api_key_index: API_KEY
552 },
553 );
554 }
555
556 #[rstest]
557 fn ack_success_uninitialized_errors() {
558 let mgr = NonceManager::new(8);
559 let err = mgr
560 .ack_success(ACCOUNT, API_KEY, 5)
561 .expect_err("must error");
562 assert_eq!(
563 err,
564 NonceError::NotInitialized {
565 account_index: ACCOUNT,
566 api_key_index: API_KEY
567 },
568 );
569 }
570
571 #[rstest]
572 fn ack_success_advances_baseline_monotonically() {
573 let mgr = NonceManager::new(8);
574 mgr.refresh(ACCOUNT, API_KEY, 0);
575 for _ in 0..5 {
576 mgr.next_nonce(ACCOUNT, API_KEY).unwrap();
577 }
578
579 mgr.ack_success(ACCOUNT, API_KEY, 2).unwrap();
580 assert_eq!(
581 mgr.baseline(ACCOUNT, API_KEY),
582 Some(2),
583 "ack must advance baseline to the acked nonce",
584 );
585
586 mgr.ack_success(ACCOUNT, API_KEY, 0).unwrap();
587 assert_eq!(
588 mgr.baseline(ACCOUNT, API_KEY),
589 Some(2),
590 "lower ack must not retreat the baseline",
591 );
592
593 mgr.ack_success(ACCOUNT, API_KEY, 4).unwrap();
594 assert_eq!(mgr.baseline(ACCOUNT, API_KEY), Some(4));
595 assert_eq!(
596 mgr.last_issued(ACCOUNT, API_KEY),
597 Some(4),
598 "ack must not touch last_issued",
599 );
600 }
601
602 #[rstest]
603 fn ack_success_recovers_window_across_more_than_window_txs() {
604 let window = 16_u32;
605 let total = 40_i64;
606 let mgr = NonceManager::new(window);
607 mgr.refresh(ACCOUNT, API_KEY, 0);
608
609 let mut issued = Vec::with_capacity(total as usize);
610 for i in 0..total {
611 if i >= i64::from(window) {
612 mgr.ack_success(ACCOUNT, API_KEY, i - i64::from(window))
614 .unwrap();
615 }
616 issued.push(mgr.next_nonce(ACCOUNT, API_KEY).unwrap());
617 }
618
619 let expected: Vec<i64> = (0..total).collect();
620 assert_eq!(
621 issued, expected,
622 "interleaved acks must keep issuance contiguous past the window",
623 );
624 }
625
626 #[rstest]
627 fn sync_from_venue_uninitialized_errors() {
628 let mgr = NonceManager::new(8);
629 let err = mgr
630 .sync_from_venue(ACCOUNT, API_KEY, 5)
631 .expect_err("must error");
632 assert_eq!(
633 err,
634 NonceError::NotInitialized {
635 account_index: ACCOUNT,
636 api_key_index: API_KEY
637 },
638 );
639 }
640
641 #[rstest]
642 fn sync_from_venue_lifts_baseline_and_last_issued() {
643 let mgr = NonceManager::new(2);
644 mgr.refresh(ACCOUNT, API_KEY, 0);
645 for _ in 0..2 {
646 mgr.next_nonce(ACCOUNT, API_KEY).unwrap();
647 }
648 assert!(
649 mgr.next_nonce(ACCOUNT, API_KEY).is_err(),
650 "window must trip"
651 );
652
653 mgr.sync_from_venue(ACCOUNT, API_KEY, 2).unwrap();
655 assert_eq!(mgr.baseline(ACCOUNT, API_KEY), Some(1));
656 assert_eq!(
657 mgr.last_issued(ACCOUNT, API_KEY),
658 Some(1),
659 "venue sync must not retreat last_issued below issued nonces",
660 );
661 let n = mgr.next_nonce(ACCOUNT, API_KEY).unwrap();
662 assert_eq!(n, 2, "venue sync must re-arm allocation, was {n}");
663
664 mgr.sync_from_venue(ACCOUNT, API_KEY, 10).unwrap();
666 assert_eq!(mgr.baseline(ACCOUNT, API_KEY), Some(9));
667 assert_eq!(mgr.last_issued(ACCOUNT, API_KEY), Some(9));
668 let n = mgr.next_nonce(ACCOUNT, API_KEY).unwrap();
669 assert_eq!(n, 10, "allocation must resume at venue nonce, was {n}");
670 }
671
672 #[rstest]
673 fn sync_from_venue_never_moves_backwards() {
674 let mgr = NonceManager::new(8);
675 mgr.refresh(ACCOUNT, API_KEY, 100);
676 for _ in 0..2 {
677 mgr.next_nonce(ACCOUNT, API_KEY).unwrap();
678 }
679
680 mgr.sync_from_venue(ACCOUNT, API_KEY, 50).unwrap();
682 assert_eq!(mgr.baseline(ACCOUNT, API_KEY), Some(99));
683 assert_eq!(mgr.last_issued(ACCOUNT, API_KEY), Some(101));
684 let n = mgr.next_nonce(ACCOUNT, API_KEY).unwrap();
685 assert_eq!(n, 102, "stale venue read must not cause reissue, was {n}");
686 }
687
688 #[rstest]
689 fn ack_failure_if_latest_uninitialized_errors() {
690 let mgr = NonceManager::new(8);
691 let err = mgr
692 .ack_failure_if_latest(ACCOUNT, API_KEY, 5)
693 .expect_err("must error");
694 assert_eq!(
695 err,
696 NonceError::NotInitialized {
697 account_index: ACCOUNT,
698 api_key_index: API_KEY
699 },
700 );
701 }
702
703 #[rstest]
704 fn ack_failure_if_latest_rolls_back_latest_issuance() {
705 let mgr = NonceManager::new(8);
706 mgr.refresh(ACCOUNT, API_KEY, 0);
707 mgr.next_nonce(ACCOUNT, API_KEY).unwrap();
708 let latest = mgr.next_nonce(ACCOUNT, API_KEY).unwrap();
709
710 let rolled = mgr.ack_failure_if_latest(ACCOUNT, API_KEY, latest).unwrap();
711 assert!(rolled, "latest issuance must roll back");
712 let reused = mgr.next_nonce(ACCOUNT, API_KEY).unwrap();
713 assert_eq!(
714 reused, latest,
715 "rolled-back nonce must be reissued, was {reused}",
716 );
717 }
718
719 #[rstest]
720 fn ack_failure_if_latest_skips_with_newer_issuance() {
721 let mgr = NonceManager::new(8);
722 mgr.refresh(ACCOUNT, API_KEY, 0);
723 let older = mgr.next_nonce(ACCOUNT, API_KEY).unwrap();
724 let newer = mgr.next_nonce(ACCOUNT, API_KEY).unwrap();
725
726 let rolled = mgr.ack_failure_if_latest(ACCOUNT, API_KEY, older).unwrap();
727 assert!(!rolled, "non-latest nonce must not roll back");
728 assert_eq!(
729 mgr.last_issued(ACCOUNT, API_KEY),
730 Some(newer),
731 "skipped rollback must leave last_issued alone",
732 );
733 let next = mgr.next_nonce(ACCOUNT, API_KEY).unwrap();
734 assert_eq!(
735 next,
736 newer + 1,
737 "no nonce signed into an in-flight tx may be reissued, was {next}",
738 );
739 }
740
741 #[rstest]
742 fn ack_failure_if_latest_skips_when_baseline_caught_up() {
743 let mgr = NonceManager::new(8);
744 mgr.refresh(ACCOUNT, API_KEY, 0);
745 let nonce = mgr.next_nonce(ACCOUNT, API_KEY).unwrap();
746 mgr.ack_success(ACCOUNT, API_KEY, nonce).unwrap();
747
748 let rolled = mgr.ack_failure_if_latest(ACCOUNT, API_KEY, nonce).unwrap();
749 assert!(
750 !rolled,
751 "a nonce the venue already applied must not roll back",
752 );
753 assert_eq!(mgr.last_issued(ACCOUNT, API_KEY), Some(nonce));
754 }
755
756 #[rstest]
757 fn refresh_resets_after_skip_window_exhausted() {
758 let mgr = NonceManager::new(2);
759 mgr.refresh(ACCOUNT, API_KEY, 0);
760 for _ in 0..2 {
761 mgr.next_nonce(ACCOUNT, API_KEY).unwrap();
762 }
763 assert!(
764 mgr.next_nonce(ACCOUNT, API_KEY).is_err(),
765 "window must trip"
766 );
767 mgr.refresh(ACCOUNT, API_KEY, 5);
769 let n = mgr.next_nonce(ACCOUNT, API_KEY).unwrap();
770 assert_eq!(n, 5, "refresh must re-arm allocation, was {n}");
771 }
772
773 #[rstest]
774 fn distinct_keys_track_independent_state() {
775 let mgr = NonceManager::new(8);
776 mgr.refresh(ACCOUNT, 0, 0);
777 mgr.refresh(ACCOUNT, 1, 100);
778 let a = mgr.next_nonce(ACCOUNT, 0).unwrap();
779 let b = mgr.next_nonce(ACCOUNT, 1).unwrap();
780 assert_eq!(a, 0, "key 0 must start at 0, was {a}");
781 assert_eq!(b, 100, "key 1 must start at 100, was {b}");
782 }
783
784 #[rstest]
785 fn concurrent_callers_see_no_duplicate_or_gap() {
786 let mgr = StdArc::new(NonceManager::new(10_000));
787 mgr.refresh(ACCOUNT, API_KEY, 0);
788 let threads = 8;
789 let per_thread = 250;
790 let handles: Vec<_> = (0..threads)
791 .map(|_| {
792 let mgr = StdArc::clone(&mgr);
793
794 thread::spawn(move || -> Vec<i64> {
795 (0..per_thread)
796 .map(|_| mgr.next_nonce(ACCOUNT, API_KEY).unwrap())
797 .collect()
798 })
799 })
800 .collect();
801 let mut all = Vec::with_capacity(threads * per_thread);
802 for h in handles {
803 all.extend(h.join().unwrap());
804 }
805 all.sort_unstable();
806 let expected: Vec<i64> = (0..(threads as i64) * (per_thread as i64)).collect();
807 assert_eq!(
808 all, expected,
809 "concurrent issuance must cover [0, N) without gaps or duplicates",
810 );
811 }
812
813 #[rstest]
814 fn concurrent_allocation_with_interleaved_acks_is_gap_free() {
815 let threads = 4;
816 let per_thread = 200;
817 let mgr = StdArc::new(NonceManager::new(64));
819 mgr.refresh(ACCOUNT, API_KEY, 0);
820 let handles: Vec<_> = (0..threads)
821 .map(|_| {
822 let mgr = StdArc::clone(&mgr);
823
824 thread::spawn(move || -> Vec<i64> {
825 (0..per_thread)
826 .map(|_| {
827 let nonce = mgr.next_nonce(ACCOUNT, API_KEY).unwrap();
828 mgr.ack_success(ACCOUNT, API_KEY, nonce).unwrap();
829 nonce
830 })
831 .collect()
832 })
833 })
834 .collect();
835 let mut all = Vec::with_capacity(threads * per_thread);
836 for h in handles {
837 all.extend(h.join().unwrap());
838 }
839 all.sort_unstable();
840 let expected: Vec<i64> = (0..(threads as i64) * (per_thread as i64)).collect();
841 assert_eq!(
842 all, expected,
843 "concurrent issuance with acks must cover [0, N) without gaps or duplicates",
844 );
845 }
846
847 proptest! {
848 #[rstest]
851 fn prop_sequential_issuance_is_contiguous(
852 baseline in 0i64..1_000_000,
853 count in 1usize..256,
854 ) {
855 let mgr = NonceManager::new(u32::MAX);
856 mgr.refresh(ACCOUNT, API_KEY, baseline);
857 let issued: Vec<i64> = (0..count)
858 .map(|_| mgr.next_nonce(ACCOUNT, API_KEY).unwrap())
859 .collect();
860
861 for (i, &n) in issued.iter().enumerate() {
862 prop_assert_eq!(n, baseline + i as i64);
863 }
864 prop_assert_eq!(
865 mgr.last_issued(ACCOUNT, API_KEY),
866 Some(baseline + count as i64 - 1),
867 );
868 }
869
870 #[rstest]
873 fn prop_ack_failure_is_idempotent_round_trip(
874 baseline in 0i64..1_000_000,
875 advance in 1usize..32,
876 ) {
877 let mgr = NonceManager::new(u32::MAX);
878 mgr.refresh(ACCOUNT, API_KEY, baseline);
879 for _ in 0..advance - 1 {
880 mgr.next_nonce(ACCOUNT, API_KEY).unwrap();
881 }
882 let issued = mgr.next_nonce(ACCOUNT, API_KEY).unwrap();
883 let rolled = mgr.ack_failure(ACCOUNT, API_KEY).unwrap();
884 prop_assert_eq!(rolled, issued);
885 let reused = mgr.next_nonce(ACCOUNT, API_KEY).unwrap();
886 prop_assert_eq!(reused, issued);
887 }
888 }
889}