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