Skip to content
Closed
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
121 changes: 94 additions & 27 deletions pkg/lumera/modules/tx/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package tx
import (
"context"
"fmt"
"strings"

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

accountNumber uint64
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TxHelper now maintains internal state (accountNumber, nextSequence), which makes it not thread-safe. While action_msg currently handles synchronization externally, this makes TxHelper dangerous to use in other contexts.

Consider adding a sync.Mutex to TxHelper and locking in ExecuteTransaction (and other state-modifying methods) to ensure thread safety.

Fix it with Roo Code or mention @roomote and request a fix.

nextSequence uint64
seqInit bool
}

// TxHelperConfig holds configuration for creating a TxHelper
Expand Down Expand Up @@ -67,35 +72,95 @@ 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) {

// --- 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)
}

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 {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking only err == nil is insufficient because a transaction might fail CheckTx (e.g. insufficient fees) and return a successful response object but with a non-zero Code. In such cases, the sequence is not incremented on-chain.

Incrementing nextSequence here will cause a drift, forcing the next transaction to fail with a sequence mismatch and trigger a retry. You should also check resp.TxResponse.Code == 0.

		if err == nil {
			if resp != nil && resp.TxResponse != nil && resp.TxResponse.Code == 0 {
				// SUCCESS → bump local sequence
				h.nextSequence++
			}
			return resp, nil
		}

Fix it with Roo Code or mention @roomote and request a fix.

// SUCCESS → bump local sequence and return
h.nextSequence++
return resp, nil
}

// Check if this is a sequence mismatch error
if !isSequenceMismatch(err) {
return nil, err // unrelated error → bail out
}

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

// --- Retry logic: resync from chain ---
accInfoRes, err2 := h.authmod.AccountInfoByAddress(ctx, creator)
if err2 != nil {
return nil, fmt.Errorf("failed to resync account info after mismatch: %w", err2)
}

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 := err.Error()

return strings.Contains(msg, "incorrect account sequence") ||
strings.Contains(msg, "account sequence mismatch") ||
(strings.Contains(msg, "expected") && strings.Contains(msg, "got"))

}

// ExecuteTransactionWithMsgs processes a transaction with pre-created messages and account info
Expand Down Expand Up @@ -133,45 +198,47 @@ 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
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
Loading