@@ -151,8 +151,8 @@ func TestDirectSubmitterRetriesSequenceMismatch(t *testing.T) {
151151 if result .Height != 77 {
152152 t .Fatalf ("height = %d, want 77" , result .Height )
153153 }
154- if client .accountCalls != 1 {
155- t .Fatalf ("account calls = %d, want 1 " , client .accountCalls )
154+ if client .accountCalls != 2 {
155+ t .Fatalf ("account calls = %d, want 2 " , client .accountCalls )
156156 }
157157 if client .broadcastCalls != 2 {
158158 t .Fatalf ("broadcast calls = %d, want 2" , client .broadcastCalls )
@@ -345,6 +345,7 @@ func TestDirectSubmitterRecoversAfterRestartWithPendingSequences(t *testing.T) {
345345
346346 signer := mustSigner (t )
347347 client := newSequenceRecoveryAppClient (signer .Address (), 7 , 11 , 16 )
348+ client .accountSequenceQueue = []uint64 {11 , 16 }
348349
349350 // Simulate a fresh process with no local sequence cache while the mempool
350351 // still holds earlier pending transactions.
@@ -365,8 +366,8 @@ func TestDirectSubmitterRecoversAfterRestartWithPendingSequences(t *testing.T) {
365366 if result .Height != 116 {
366367 t .Fatalf ("height = %d, want 116" , result .Height )
367368 }
368- if client .accountCalls != 1 {
369- t .Fatalf ("account calls = %d, want 1 " , client .accountCalls )
369+ if client .accountCalls != 2 {
370+ t .Fatalf ("account calls = %d, want 2 " , client .accountCalls )
370371 }
371372 if ! slices .Equal (client .attemptSequences , []uint64 {11 , 16 }) {
372373 t .Fatalf ("attempt sequences = %v, want [11 16]" , client .attemptSequences )
@@ -382,6 +383,7 @@ func TestDirectSubmitterRecoversWhenCachedSequenceFallsBehindExternalWriter(t *t
382383 signer := mustSigner (t )
383384 client := newSequenceRecoveryAppClient (signer .Address (), 7 , 16 , 16 )
384385 client .afterSuccessNext = []uint64 {19 }
386+ client .accountSequenceQueue = []uint64 {16 , 19 }
385387
386388 submitter , err := NewDirectSubmitter (client , signer , DirectConfig {
387389 ChainID : "mocha-4" ,
@@ -408,8 +410,8 @@ func TestDirectSubmitterRecoversWhenCachedSequenceFallsBehindExternalWriter(t *t
408410 if second .Height != 119 {
409411 t .Fatalf ("second height = %d, want 119" , second .Height )
410412 }
411- if client .accountCalls != 1 {
412- t .Fatalf ("account calls = %d, want 1 " , client .accountCalls )
413+ if client .accountCalls != 2 {
414+ t .Fatalf ("account calls = %d, want 2 " , client .accountCalls )
413415 }
414416 if ! slices .Equal (client .attemptSequences , []uint64 {16 , 17 , 19 }) {
415417 t .Fatalf ("attempt sequences = %v, want [16 17 19]" , client .attemptSequences )
@@ -454,6 +456,125 @@ func TestDirectSubmitterReconcilesPersistedPendingSequenceBeforeBroadcast(t *tes
454456 }
455457}
456458
459+ func TestDirectSubmitterMismatchDetectedByABCICode (t * testing.T ) {
460+ t .Parallel ()
461+
462+ signer := mustSigner (t )
463+ // RawLog contains no parseable sequence — detection must come from Code 32 alone.
464+ client := & fakeAppClient {
465+ accountInfos : []* AccountInfo {
466+ {Address : signer .Address (), AccountNumber : 7 , Sequence : 1 },
467+ {Address : signer .Address (), AccountNumber : 7 , Sequence : 2 },
468+ },
469+ broadcastStatuses : []* TxStatus {
470+ {Code : 32 , RawLog : "wrong sequence" },
471+ {Hash : "ABCDEF" },
472+ },
473+ getTxStatuses : []* TxStatus {{Hash : "ABCDEF" , Height : 88 }},
474+ }
475+
476+ submitter , err := NewDirectSubmitter (client , signer , DirectConfig {
477+ ChainID : "mocha-4" ,
478+ GasPrice : 0.002 ,
479+ ConfirmationTimeout : 100 * time .Millisecond ,
480+ })
481+ if err != nil {
482+ t .Fatalf ("NewDirectSubmitter: %v" , err )
483+ }
484+ submitter .pollInterval = time .Millisecond
485+
486+ result , err := submitter .Submit (context .Background (), testRequest ())
487+ if err != nil {
488+ t .Fatalf ("Submit: %v" , err )
489+ }
490+ if result .Height != 88 {
491+ t .Fatalf ("height = %d, want 88" , result .Height )
492+ }
493+ if client .broadcastCalls != 2 {
494+ t .Fatalf ("broadcast calls = %d, want 2" , client .broadcastCalls )
495+ }
496+ if client .accountCalls != 2 {
497+ t .Fatalf ("account calls = %d, want 2" , client .accountCalls )
498+ }
499+ }
500+
501+ func TestDirectSubmitterMismatchReQueryFails (t * testing.T ) {
502+ t .Parallel ()
503+
504+ signer := mustSigner (t )
505+ reQueryErr := errors .New ("network unreachable" )
506+ client := & fakeAppClient {
507+ accountInfos : []* AccountInfo {
508+ {Address : signer .Address (), AccountNumber : 7 , Sequence : 1 },
509+ },
510+ accountErrs : []error {nil , reQueryErr },
511+ broadcastStatuses : []* TxStatus {
512+ {Code : 32 , RawLog : "account sequence mismatch, expected 2, got 1" },
513+ },
514+ }
515+
516+ submitter , err := NewDirectSubmitter (client , signer , DirectConfig {
517+ ChainID : "mocha-4" ,
518+ GasPrice : 0.002 ,
519+ ConfirmationTimeout : 100 * time .Millisecond ,
520+ })
521+ if err != nil {
522+ t .Fatalf ("NewDirectSubmitter: %v" , err )
523+ }
524+ submitter .pollInterval = time .Millisecond
525+
526+ _ , err = submitter .Submit (context .Background (), testRequest ())
527+ if err == nil {
528+ t .Fatal ("expected error, got nil" )
529+ }
530+ if ! errors .Is (err , reQueryErr ) {
531+ t .Fatalf ("expected re-query error in chain, got: %v" , err )
532+ }
533+ }
534+
535+ func TestDirectSubmitterExhaustsAllRetries (t * testing.T ) {
536+ t .Parallel ()
537+
538+ signer := mustSigner (t )
539+ client := & fakeAppClient {
540+ accountInfos : []* AccountInfo {
541+ {Address : signer .Address (), AccountNumber : 7 , Sequence : 1 },
542+ {Address : signer .Address (), AccountNumber : 7 , Sequence : 2 },
543+ {Address : signer .Address (), AccountNumber : 7 , Sequence : 3 },
544+ {Address : signer .Address (), AccountNumber : 7 , Sequence : 4 },
545+ },
546+ broadcastStatuses : []* TxStatus {
547+ {Code : 32 , RawLog : "account sequence mismatch" },
548+ {Code : 32 , RawLog : "account sequence mismatch" },
549+ {Code : 32 , RawLog : "account sequence mismatch" },
550+ },
551+ }
552+
553+ submitter , err := NewDirectSubmitter (client , signer , DirectConfig {
554+ ChainID : "mocha-4" ,
555+ GasPrice : 0.002 ,
556+ ConfirmationTimeout : 100 * time .Millisecond ,
557+ })
558+ if err != nil {
559+ t .Fatalf ("NewDirectSubmitter: %v" , err )
560+ }
561+ submitter .pollInterval = time .Millisecond
562+
563+ _ , err = submitter .Submit (context .Background (), testRequest ())
564+ if err == nil {
565+ t .Fatal ("expected error after exhausting retries, got nil" )
566+ }
567+ if ! errors .Is (err , errSequenceMismatch ) {
568+ t .Fatalf ("expected errSequenceMismatch in chain, got: %v" , err )
569+ }
570+ if client .broadcastCalls != 3 {
571+ t .Fatalf ("broadcast calls = %d, want 3" , client .broadcastCalls )
572+ }
573+ if client .accountCalls != 4 {
574+ t .Fatalf ("account calls = %d, want 4" , client .accountCalls )
575+ }
576+ }
577+
457578func TestDirectSubmitterRejectsWhenMaxInFlightExceeded (t * testing.T ) {
458579 t .Parallel ()
459580
@@ -679,11 +800,12 @@ func decodeInnerTx(raw []byte) ([]byte, error) {
679800}
680801
681802type sequenceRecoveryAppClient struct {
682- address string
683- accountNumber uint64
684- committedSequence uint64
685- nextAvailable uint64
686- afterSuccessNext []uint64
803+ address string
804+ accountNumber uint64
805+ committedSequence uint64
806+ nextAvailable uint64
807+ afterSuccessNext []uint64
808+ accountSequenceQueue []uint64
687809
688810 mu sync.Mutex
689811 accountCalls int
@@ -707,10 +829,15 @@ func (c *sequenceRecoveryAppClient) AccountInfo(_ context.Context, _ string) (*A
707829 defer c .mu .Unlock ()
708830
709831 c .accountCalls ++
832+ seq := c .committedSequence
833+ if len (c .accountSequenceQueue ) > 0 {
834+ seq = c .accountSequenceQueue [0 ]
835+ c .accountSequenceQueue = c .accountSequenceQueue [1 :]
836+ }
710837 return & AccountInfo {
711838 Address : c .address ,
712839 AccountNumber : c .accountNumber ,
713- Sequence : c . committedSequence ,
840+ Sequence : seq ,
714841 }, nil
715842}
716843
0 commit comments