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