Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 133 additions & 27 deletions pkg/lumera/modules/tx/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package tx
import (
"context"
"fmt"
"strings"
"sync"

"github.com/LumeraProtocol/supernode/v2/pkg/lumera/modules/auth"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
Expand All @@ -17,6 +19,12 @@ type TxHelper struct {
authmod auth.Module
txmod Module
config *TxConfig

mu sync.Mutex

accountNumber uint64
nextSequence uint64
seqInit bool
}

// TxHelperConfig holds configuration for creating a TxHelper
Expand Down Expand Up @@ -67,35 +75,128 @@ func NewTxHelperWithDefaults(authmod auth.Module, txmod Module, chainID, keyName
return NewTxHelper(authmod, txmod, config)
}

// ExecuteTransaction is a convenience method that handles the complete transaction flow
// for a single message. It gets account info, creates the message, and processes the transaction.
func (h *TxHelper) ExecuteTransaction(ctx context.Context, msgCreator func(creator string) (types.Msg, error)) (*sdktx.BroadcastTxResponse, error) {
// Step 1: Get creator address from keyring
func (h *TxHelper) ExecuteTransaction(
ctx context.Context,
msgCreator func(creator string) (types.Msg, error),
) (*sdktx.BroadcastTxResponse, error) {

h.mu.Lock()
defer h.mu.Unlock()

// --- Step 1: Resolve creator address ---
key, err := h.config.Keyring.Key(h.config.KeyName)
if err != nil {
return nil, fmt.Errorf("failed to get key from keyring: %w", err)
}

addr, err := key.GetAddress()
if err != nil {
return nil, fmt.Errorf("failed to get address from key: %w", err)
return nil, fmt.Errorf("failed to get address: %w", err)
}
creator := addr.String()

// Step 2: Get account info
accInfoRes, err := h.authmod.AccountInfoByAddress(ctx, creator)
if err != nil {
return nil, fmt.Errorf("failed to get account info: %w", err)
// --- Step 2: Local sequence initialization (run once) ---
if !h.seqInit {
accInfoRes, err := h.authmod.AccountInfoByAddress(ctx, creator)
if err != nil {
return nil, fmt.Errorf("failed to fetch initial account info: %w", err)
}
if accInfoRes == nil || accInfoRes.Info == nil {
return nil, fmt.Errorf("empty account info response for creator %s", creator)
}

h.accountNumber = accInfoRes.Info.AccountNumber
h.nextSequence = accInfoRes.Info.Sequence
h.seqInit = true
}

// Step 3: Create the message using the provided creator function
// --- Step 3: Create message ---
msg, err := msgCreator(creator)
if err != nil {
return nil, fmt.Errorf("failed to create message: %w", err)
}

// Step 4: Process transaction
return h.ExecuteTransactionWithMsgs(ctx, []types.Msg{msg}, accInfoRes.Info)
// --- Step 4: Attempt tx (with 1 retry on sequence mismatch) ---
const maxAttempts = 2

for attempt := 1; attempt <= maxAttempts; attempt++ {

// Build a local accountInfo using in-memory sequence
localAcc := &authtypes.BaseAccount{
AccountNumber: h.accountNumber,
Sequence: h.nextSequence,
Address: creator,
}

// Run full tx flow
resp, err := h.ExecuteTransactionWithMsgs(ctx, []types.Msg{msg}, localAcc)
if err == nil {
// SUCCESS → bump local sequence and return
h.nextSequence++
return resp, nil
}

// Check if this is a sequence mismatch error
if !isSequenceMismatch(err) {
return resp, err // unrelated error → bail out (preserve response for debugging)
}

// If retry unavailable, bubble error
if attempt == maxAttempts {
return resp, fmt.Errorf("sequence mismatch after retry: %w", err)
}

// --- Retry logic: prefer expected sequence from the error ---
if expectedSeq, ok := parseExpectedSequence(err); ok {
h.nextSequence = expectedSeq
continue
}

// Fallback: resync from chain state.
accInfoRes, err2 := h.authmod.AccountInfoByAddress(ctx, creator)
if err2 != nil {
return resp, fmt.Errorf("failed to resync account info after mismatch: %w", err2)
}
if accInfoRes == nil || accInfoRes.Info == nil {
return resp, fmt.Errorf("empty account info response for creator %s after mismatch", creator)
}

h.accountNumber = accInfoRes.Info.AccountNumber
h.nextSequence = accInfoRes.Info.Sequence
}

return nil, fmt.Errorf("unreachable state in ExecuteTransaction")
}

func isSequenceMismatch(err error) bool {
if err == nil {
return false
}

msg := strings.ToLower(err.Error())

return strings.Contains(msg, "incorrect account sequence") ||
strings.Contains(msg, "account sequence mismatch") ||
strings.Contains(msg, "wrong sequence")
}

func parseExpectedSequence(err error) (uint64, bool) {
if err == nil {
return 0, false
}

msg := strings.ToLower(err.Error())
idx := strings.Index(msg, "expected ")
if idx == -1 {
return 0, false
}

var expected, got uint64
if _, scanErr := fmt.Sscanf(msg[idx:], "expected %d, got %d", &expected, &got); scanErr == nil {
return expected, true
}

return 0, false
}

// ExecuteTransactionWithMsgs processes a transaction with pre-created messages and account info
Expand Down Expand Up @@ -133,45 +234,50 @@ func (h *TxHelper) GetAccountInfo(ctx context.Context) (*authtypes.BaseAccount,
return accInfoRes.Info, nil
}

// UpdateConfig allows updating the transaction configuration
func (h *TxHelper) UpdateConfig(config *TxHelperConfig) {
// Merge provided fields with existing config to avoid zeroing defaults
h.mu.Lock()
defer h.mu.Unlock()

if h.config == nil {
h.config = &TxConfig{}
}

// ChainID
if config.ChainID != "" {
h.config.ChainID = config.ChainID
}
// Keyring
if config.Keyring != nil {
keyChanged := false

if config.Keyring != nil && config.Keyring != h.config.Keyring {
h.config.Keyring = config.Keyring
keyChanged = true
}
// KeyName
if config.KeyName != "" {
if config.KeyName != "" && config.KeyName != h.config.KeyName {
h.config.KeyName = config.KeyName
keyChanged = true
}

if config.ChainID != "" {
h.config.ChainID = config.ChainID
}
// GasLimit
if config.GasLimit != 0 {
h.config.GasLimit = config.GasLimit
}
// GasAdjustment
if config.GasAdjustment != 0 {
h.config.GasAdjustment = config.GasAdjustment
}
// GasPadding
if config.GasPadding != 0 {
h.config.GasPadding = config.GasPadding
}
// FeeDenom
if config.FeeDenom != "" {
h.config.FeeDenom = config.FeeDenom
}
// GasPrice
if config.GasPrice != "" {
h.config.GasPrice = config.GasPrice
}

// If key has changed, reset sequence tracking so we re-init on next tx
if keyChanged {
h.seqInit = false
h.accountNumber = 0
h.nextSequence = 0
}
}

// GetConfig returns the current transaction configuration
Expand Down
67 changes: 67 additions & 0 deletions pkg/lumera/modules/tx/helper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package tx

import (
"fmt"
"testing"
)

func TestIsSequenceMismatch(t *testing.T) {
t.Parallel()

tests := []struct {
name string
err error
want bool
}{
{
name: "grpc simulation mismatch",
err: fmt.Errorf(
"simulation failed: simulation error: rpc error: code = Unknown desc = account sequence mismatch, expected 7855, got 7854: incorrect account sequence [cosmos/cosmos-sdk@v0.53.0/x/auth/ante/sigverify.go:290] with gas used: '35369'",
),
want: true,
},
{
name: "broadcast raw_log mismatch",
err: fmt.Errorf(
"tx failed: code=32 codespace=sdk height=0 gas_wanted=0 gas_used=0 raw_log=account sequence mismatch, expected 10, got 9: incorrect account sequence",
),
want: true,
},
{
name: "unrelated expected/got",
err: fmt.Errorf("expected 5, got 4"),
want: false,
},
{
name: "nil",
err: nil,
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := isSequenceMismatch(tt.err); got != tt.want {
t.Fatalf("isSequenceMismatch() = %v, want %v", got, tt.want)
}
})
}
}

func TestParseExpectedSequence(t *testing.T) {
t.Parallel()

err := fmt.Errorf("account sequence mismatch, expected 7855, got 7854: incorrect account sequence")
if got, ok := parseExpectedSequence(err); !ok || got != 7855 {
t.Fatalf("parseExpectedSequence() = (%d, %v), want (7855, true)", got, ok)
}

if _, ok := parseExpectedSequence(fmt.Errorf("incorrect account sequence")); ok {
t.Fatalf("parseExpectedSequence() unexpectedly matched message without expected/got")
}

if _, ok := parseExpectedSequence(nil); ok {
t.Fatalf("parseExpectedSequence() unexpectedly matched nil error")
}
}