diff --git a/broadcast/broadcast.go b/broadcast/broadcast.go index 76df261..cf79089 100644 --- a/broadcast/broadcast.go +++ b/broadcast/broadcast.go @@ -3,6 +3,7 @@ package broadcast import ( "context" "fmt" + "sync" "time" coretypes "github.com/cometbft/cometbft/rpc/core/types" @@ -15,7 +16,13 @@ import ( banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" ) -// Add these at the top of the file +// Global variables for account locking and node assignment +var ( + accountLocks sync.Map // map[string]*sync.Mutex for account serialization + accountNodeMap sync.Map // map[string]string for account-to-node mapping + accountNodeMapMux sync.Mutex +) + type TimingMetrics struct { PrepStart time.Time SignStart time.Time @@ -61,14 +68,17 @@ func init() { banktypes.RegisterInterfaces(cdc.InterfaceRegistry()) } -// Transaction broadcasts the transaction bytes to the given RPC endpoint. -func Transaction(txBytes []byte, rpcEndpoint string) (*coretypes.ResultBroadcastTx, error) { - client, err := GetClient(rpcEndpoint) - if err != nil { - return nil, err +// assignNodeToAccount assigns a node to an account if not already assigned +func assignNodeToAccount(acctAddress, nodeURL string) string { + accountNodeMapMux.Lock() + defer accountNodeMapMux.Unlock() + + if assignedNode, ok := accountNodeMap.Load(acctAddress); ok { + return assignedNode.(string) } - return client.Transaction(txBytes) + accountNodeMap.Store(acctAddress, nodeURL) + return nodeURL } // Loop handles the main transaction broadcasting logic @@ -82,8 +92,23 @@ func Loop( responseCodes = make(map[uint32]int) sequence := txParams.Sequence + // Assign this account to the provided node if not already assigned + nodeURL := assignNodeToAccount(txParams.AcctAddress, txParams.NodeURL) + if nodeURL != txParams.NodeURL { + fmt.Printf("[POS-%d] Account %s reassigned to node %s (original: %s)\n", + position, txParams.AcctAddress, nodeURL, txParams.NodeURL) + } + txParams.NodeURL = nodeURL + + // Get or create lock for this account + lock, _ := accountLocks.LoadOrStore(txParams.AcctAddress, &sync.Mutex{}) + mutex := lock.(*sync.Mutex) + mutex.Lock() + defer mutex.Unlock() + // Log the start of broadcasting for this position - LogVisualizerDebug(fmt.Sprintf("Starting broadcasts for position %d (batchSize: %d)", position, batchSize)) + LogVisualizerDebug(fmt.Sprintf("Starting broadcasts for position %d (batchSize: %d) on node %s", + position, batchSize, txParams.NodeURL)) for i := 0; i < batchSize; i++ { currentSequence := sequence @@ -107,7 +132,7 @@ func Loop( // Update visualizer with failed tx UpdateVisualizerStats(0, 1, txLatency) - if resp != nil && resp.Code == 32 { + if resp != nil && resp.Code == 32 { // Sequence mismatch newSeq, success, newResp := handleSequenceMismatch(txParams, position, sequence, err) sequence = newSeq if success { @@ -117,8 +142,16 @@ func Loop( // Update visualizer with successful tx after sequence recovery UpdateVisualizerStats(1, 0, metrics.Complete.Sub(metrics.PrepStart)) } + // Record gas result even on failure for fee adjustment + GetGasStrategyManager().RecordTransactionResult( + txParams.NodeURL, success, 0, txParams.MsgType, txParams.MsgParams["calculated_gas_amount"].(uint64), err.Error()) continue } + + // Record failure for gas strategy adjustment + gasLimit, _ := txParams.MsgParams["calculated_gas_amount"].(uint64) + GetGasStrategyManager().RecordTransactionResult( + txParams.NodeURL, false, 0, txParams.MsgType, gasLimit, err.Error()) continue } @@ -127,6 +160,11 @@ func Loop( responseCodes[resp.Code]++ sequence++ + // Record success for gas strategy + gasLimit, _ := txParams.MsgParams["calculated_gas_amount"].(uint64) + GetGasStrategyManager().RecordTransactionResult( + txParams.NodeURL, true, 0, txParams.MsgType, gasLimit, "") + // Update visualizer with successful tx UpdateVisualizerStats(1, 0, txLatency) } diff --git a/broadcast/gas_strategy.go b/broadcast/gas_strategy.go index 35a3b7f..a1b722b 100644 --- a/broadcast/gas_strategy.go +++ b/broadcast/gas_strategy.go @@ -3,22 +3,24 @@ package broadcast import ( "context" "fmt" + "strings" "sync" "time" rpchttp "github.com/cometbft/cometbft/rpc/client/http" + "github.com/somatic-labs/meteorite/lib" "github.com/somatic-labs/meteorite/types" ) -// NodeGasCapabilities tracks whether a node accepts 0-gas transactions +// NodeGasCapabilities tracks gas-related capabilities for a node type NodeGasCapabilities struct { NodeURL string AcceptsZeroGas bool MinAcceptableGas int64 + MinGasPrice float64 // Minimum gas price required by this node LastChecked time.Time SuccessfulTxCount int FailedTxCount int - // Tracking gas usage per message type GasUsageByMsgType map[string]*GasUsageStats } @@ -27,7 +29,7 @@ type GasUsageStats struct { SuccessCount int64 TotalGasUsed int64 AverageGasUsed int64 - RecentGasValues []int64 // Store recent gas values for better averaging + RecentGasValues []int64 MaxGasUsed int64 MinGasUsed int64 FailedCount int64 @@ -53,11 +55,11 @@ func (g *GasStrategyManager) GetNodeCapabilities(nodeURL string) *NodeGasCapabil caps, exists := g.nodeCapabilities[nodeURL] if !exists { - // Return default capabilities if node hasn't been tested yet return &NodeGasCapabilities{ NodeURL: nodeURL, AcceptsZeroGas: false, MinAcceptableGas: 0, + MinGasPrice: 0.0, // Default to 0 until updated LastChecked: time.Time{}, GasUsageByMsgType: make(map[string]*GasUsageStats), } @@ -74,7 +76,7 @@ func (g *GasStrategyManager) UpdateNodeCapabilities(caps *NodeGasCapabilities) { } // RecordTransactionResult records the result of a transaction for a node -func (g *GasStrategyManager) RecordTransactionResult(nodeURL string, success bool, gasUsed int64, msgType string) { +func (g *GasStrategyManager) RecordTransactionResult(nodeURL string, success bool, gasUsed int64, msgType string, txGasLimit uint64, requiredFee string) { g.mutex.Lock() defer g.mutex.Unlock() @@ -84,36 +86,32 @@ func (g *GasStrategyManager) RecordTransactionResult(nodeURL string, success boo NodeURL: nodeURL, AcceptsZeroGas: false, MinAcceptableGas: gasUsed, + MinGasPrice: 0.0, LastChecked: time.Now(), GasUsageByMsgType: make(map[string]*GasUsageStats), } } - // Ensure GasUsageByMsgType is initialized if caps.GasUsageByMsgType == nil { caps.GasUsageByMsgType = make(map[string]*GasUsageStats) } - // Get or create stats for this message type stats, exists := caps.GasUsageByMsgType[msgType] if !exists { stats = &GasUsageStats{ MinGasUsed: gasUsed, MaxGasUsed: gasUsed, - RecentGasValues: make([]int64, 0, 10), // Keep last 10 gas values + RecentGasValues: make([]int64, 0, 10), } caps.GasUsageByMsgType[msgType] = stats } if success { caps.SuccessfulTxCount++ - - // Update gas usage statistics for this message type stats.SuccessCount++ stats.TotalGasUsed += gasUsed stats.AverageGasUsed = stats.TotalGasUsed / stats.SuccessCount - // Update min/max gas used if gasUsed < stats.MinGasUsed { stats.MinGasUsed = gasUsed } @@ -121,19 +119,15 @@ func (g *GasStrategyManager) RecordTransactionResult(nodeURL string, success boo stats.MaxGasUsed = gasUsed } - // Store recent gas values for better averaging (keep last 10) if len(stats.RecentGasValues) >= 10 { - stats.RecentGasValues = stats.RecentGasValues[1:] // Remove oldest + stats.RecentGasValues = stats.RecentGasValues[1:] } stats.RecentGasValues = append(stats.RecentGasValues, gasUsed) - // If successful with lower gas than we thought was required for this node, - // update our minimum gas estimate if gasUsed < caps.MinAcceptableGas || caps.MinAcceptableGas == 0 { caps.MinAcceptableGas = gasUsed } - // If a zero-gas transaction was successful, mark this node as accepting zero gas if gasUsed == 0 { caps.AcceptsZeroGas = true fmt.Printf("Node %s accepts zero-gas transactions! Optimizing future transactions.\n", nodeURL) @@ -141,6 +135,19 @@ func (g *GasStrategyManager) RecordTransactionResult(nodeURL string, success boo } else { caps.FailedTxCount++ stats.FailedCount++ + + // Update MinGasPrice if the failure is due to insufficient fees + if strings.Contains(requiredFee, "insufficient fee") { + requiredAmount, requiredDenom, err := lib.ExtractRequiredFee(requiredFee) + if err == nil && requiredDenom == "uatone" && txGasLimit > 0 { + requiredGasPrice := float64(requiredAmount) / float64(txGasLimit) + if requiredGasPrice > caps.MinGasPrice { + caps.MinGasPrice = requiredGasPrice + fmt.Printf("Updated MinGasPrice for node %s to %f based on required fee %d\n", + nodeURL, caps.MinGasPrice, requiredAmount) + } + } + } } caps.LastChecked = time.Now() @@ -154,21 +161,19 @@ func (g *GasStrategyManager) GetAverageGasForMsgType(nodeURL, msgType string) in caps, exists := g.nodeCapabilities[nodeURL] if !exists || caps.GasUsageByMsgType == nil { - return 0 // No data available + return 0 } stats, exists := caps.GasUsageByMsgType[msgType] if !exists || stats.SuccessCount == 0 { - return 0 // No data for this message type + return 0 } - // If we have recent values, calculate a weighted average if len(stats.RecentGasValues) > 0 { - // Calculate weighted average with more weight to recent values total := int64(0) weights := 0 for i, gas := range stats.RecentGasValues { - weight := i + 1 // More weight to more recent values + weight := i + 1 total += gas * int64(weight) weights += weight } @@ -179,17 +184,13 @@ func (g *GasStrategyManager) GetAverageGasForMsgType(nodeURL, msgType string) in } // GetRecommendedGasForMsgType returns the recommended gas for a message type -// It takes into account average usage, success rate, and applies a safety buffer func (g *GasStrategyManager) GetRecommendedGasForMsgType(nodeURL, msgType string, baseGas int64) int64 { avgGas := g.GetAverageGasForMsgType(nodeURL, msgType) - // If we have historical data, use it with a safety buffer if avgGas > 0 { - // Apply a 20% safety buffer to the average safetyBuffer := float64(1.2) recommendedGas := int64(float64(avgGas) * safetyBuffer) - // Always use at least the base gas if recommendedGas < baseGas { recommendedGas = baseGas } @@ -197,7 +198,6 @@ func (g *GasStrategyManager) GetRecommendedGasForMsgType(nodeURL, msgType string return recommendedGas } - // If no historical data, return the base gas return baseGas } @@ -207,25 +207,22 @@ func (g *GasStrategyManager) TestZeroGasTransaction(ctx context.Context, nodeURL defer g.mutex.Unlock() caps, exists := g.nodeCapabilities[nodeURL] - // If we already know this node or it was checked recently, don't test again if exists && time.Since(caps.LastChecked) < 1*time.Hour { return caps.AcceptsZeroGas } - // Create a new capability entry if it doesn't exist if !exists { caps = &NodeGasCapabilities{ NodeURL: nodeURL, AcceptsZeroGas: false, + MinGasPrice: 0.0, LastChecked: time.Now(), GasUsageByMsgType: make(map[string]*GasUsageStats), } } - // We'll test a zero-gas transaction to see if the node accepts it fmt.Printf("Testing if node %s accepts zero-gas transactions...\n", nodeURL) - // Connect to the node client, err := rpchttp.New(nodeURL, "/websocket") if err != nil { fmt.Printf("Failed to connect to node %s: %v\n", nodeURL, err) @@ -233,7 +230,6 @@ func (g *GasStrategyManager) TestZeroGasTransaction(ctx context.Context, nodeURL return false } - // Check if the node is up first _, err = client.Status(ctx) if err != nil { fmt.Printf("Node %s is unreachable: %v\n", nodeURL, err) @@ -241,12 +237,9 @@ func (g *GasStrategyManager) TestZeroGasTransaction(ctx context.Context, nodeURL return false } - // For now, we'll just set a flag to indicate that we should try a zero-gas transaction with this node - // The actual test will happen when we send a real transaction caps.LastChecked = time.Now() g.nodeCapabilities[nodeURL] = caps - - return false // Default to false until proven otherwise + return false // Default until proven } // DetermineOptimalGasForNode determines the optimal gas limit for a specific node @@ -257,16 +250,13 @@ func (g *GasStrategyManager) DetermineOptimalGasForNode( msgType string, canUseZeroGas bool, ) uint64 { - // Get node capabilities caps := g.GetNodeCapabilities(nodeURL) - // If this node accepts zero gas and we're allowed to use it, use zero gas if caps.AcceptsZeroGas && canUseZeroGas { fmt.Printf("Using zero gas for node %s which accepts zero-gas transactions\n", nodeURL) return 0 } - // Try to get a recommended gas amount based on historical data recommendedGas := g.GetRecommendedGasForMsgType(nodeURL, msgType, int64(baseGasLimit)) if recommendedGas > 0 { fmt.Printf("Using optimized gas amount for node %s and msg type %s: %d (based on historical data)\n", @@ -274,7 +264,6 @@ func (g *GasStrategyManager) DetermineOptimalGasForNode( return uint64(recommendedGas) } - // If we have a known minimum gas value for this node, use it if it's lower than the base gas if caps.MinAcceptableGas > 0 && uint64(caps.MinAcceptableGas) < baseGasLimit { gasLimit := uint64(caps.MinAcceptableGas) fmt.Printf("Using optimized gas amount for node %s: %d (down from %d)\n", @@ -282,11 +271,15 @@ func (g *GasStrategyManager) DetermineOptimalGasForNode( return gasLimit } - // Otherwise use the base gas limit return baseGasLimit } -// global instance of the gas strategy manager +// GetMinGasPrice returns the minimum gas price for a node +func (g *GasStrategyManager) GetMinGasPrice(nodeURL string) float64 { + caps := g.GetNodeCapabilities(nodeURL) + return caps.MinGasPrice +} + var ( globalGasManager *GasStrategyManager globalGasManagerOnce sync.Once diff --git a/broadcast/grpc.go b/broadcast/grpc.go index 00f3bfa..d980b23 100644 --- a/broadcast/grpc.go +++ b/broadcast/grpc.go @@ -36,7 +36,7 @@ func SendTransactionViaGRPC( txParams.Sequence = sequence // Build and sign the transaction - txBytes, err := BuildAndSignTransaction(ctx, txParams, sequence, encodingConfig) + txBytes, err := BuildAndSignTransaction(ctx, txParams, sequence, encodingConfig.TxConfig) if err != nil { return nil, "", fmt.Errorf("failed to build transaction: %w", err) } diff --git a/broadcast/rpc.go b/broadcast/rpc.go index 3aedf89..261b872 100644 --- a/broadcast/rpc.go +++ b/broadcast/rpc.go @@ -38,7 +38,7 @@ func SendTransactionViaRPC( txParams.Sequence = sequence // Build and sign the transaction - txBytes, err := BuildAndSignTransaction(ctx, txParams, sequence, encodingConfig) + txBytes, err := BuildAndSignTransaction(ctx, txParams, sequence, encodingConfig.TxConfig) if err != nil { return nil, "", fmt.Errorf("failed to build transaction: %w", err) } diff --git a/broadcast/transaction.go b/broadcast/transaction.go index 606b9f2..1be67bc 100644 --- a/broadcast/transaction.go +++ b/broadcast/transaction.go @@ -11,15 +11,17 @@ import ( "sync" "time" + bankmodule "github.com/somatic-labs/meteorite/modules/bank" types "github.com/somatic-labs/meteorite/types" sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/client" sdkclient "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/tx" sdk "github.com/cosmos/cosmos-sdk/types" signing "github.com/cosmos/cosmos-sdk/types/tx/signing" - xauthsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" ) @@ -273,7 +275,7 @@ func BuildTransaction(ctx context.Context, txParams *types.TxParams) ([]byte, er sigV2, err := tx.SignWithPrivKey( ctx, signing.SignMode_SIGN_MODE_DIRECT, - xauthsigning.SignerData{ + authsigning.SignerData{ ChainID: txParams.ChainID, AccountNumber: accNum, Sequence: accSeq, @@ -305,7 +307,7 @@ func BuildTransaction(ctx context.Context, txParams *types.TxParams) ([]byte, er // Schedule an async check to verify if the tx was processed by the node // We'll mark this node as accepting zero-gas transactions if the tx is successful gsm := GetGasStrategyManager() - gsm.RecordTransactionResult(txParams.NodeURL, true, 0, txParams.MsgType) + gsm.RecordTransactionResult(txParams.NodeURL, true, 0, txParams.MsgType, 0, txParams.Config.Denom) }() } @@ -591,219 +593,138 @@ func calculateInitialFee(gasLimit uint64, gasPrice int64) int64 { } // BuildAndSignTransaction builds and signs a transaction with proper error handling +// BuildAndSignTransaction builds and signs a transaction based on the provided parameters func BuildAndSignTransaction( ctx context.Context, txParams types.TransactionParams, sequence uint64, - _ interface{}, + encodingConfig client.TxConfig, ) ([]byte, error) { - // First, check if we have more up-to-date sequence info for this node from previous errors - nodeSettings := getNodeSettings(txParams.NodeURL) - nodeSettings.mutex.RLock() - if nodeSettings.LastSequence > sequence { - // Log that we're using the cached sequence from previous error responses - fmt.Printf("Using cached sequence %d instead of provided %d for node %s (from previous tx errors)\n", - nodeSettings.LastSequence, sequence, txParams.NodeURL) - sequence = nodeSettings.LastSequence - } - nodeSettings.mutex.RUnlock() - - // We need to ensure the passed-in sequence is used - txp := &types.TxParams{ - Config: txParams.Config, - NodeURL: txParams.NodeURL, - ChainID: txParams.ChainID, - PrivKey: txParams.PrivKey, - MsgType: txParams.MsgType, - MsgParams: txParams.MsgParams, - } + // Create a new TxBuilder + txBuilder := encodingConfig.NewTxBuilder() - // Pass distributor through MsgParams for multisend operations - if txParams.Distributor != nil && txParams.MsgType == "bank_multisend" { - if txp.MsgParams == nil { - txp.MsgParams = make(map[string]interface{}) + // Determine gas limit (use pre-calculated value for multisend) + gasLimit, ok := txParams.MsgParams["calculated_gas_amount"].(uint64) + if !ok { + gasLimit = uint64(200000) // Default fallback + if txParams.MsgType == "bank_multisend" { + gasLimit = 90150000 // Default for multisend with 3000 recipients } - txp.MsgParams["distributor"] = txParams.Distributor } - // Use ClientContext with correct sequence - clientCtx, err := GetClientContext(txParams.Config, txParams.NodeURL) + // Get gas price from config and adjust with node's minimum requirement + gasPrice, err := strconv.ParseFloat(txParams.Config.Gas.Price, 64) if err != nil { - return nil, fmt.Errorf("failed to get client context: %w", err) + return nil, fmt.Errorf("failed to parse gas price: %w", err) } - // Build and sign the transaction - msg, err := createMessage(txp) - if err != nil { - return nil, fmt.Errorf("failed to create message: %w", err) + // Adjust gas price based on node's minimum requirement + minGasPrice := GetGasStrategyManager().GetMinGasPrice(txParams.NodeURL) + if minGasPrice > gasPrice { + gasPrice = minGasPrice + fmt.Printf("Adjusted gas price to %f for node %s (min required: %f)\n", + gasPrice, txParams.NodeURL, minGasPrice) } - // Create a new TxBuilder - txBuilder := clientCtx.TxConfig.NewTxBuilder() + // Calculate fee + feeAmount := int64(gasPrice * float64(gasLimit)) + fee := sdk.NewCoins(sdk.NewInt64Coin(txParams.Config.Denom, feeAmount)) + txBuilder.SetFeeAmount(fee) + txBuilder.SetGasLimit(gasLimit) - // Set the message and other transaction parameters - if err := txBuilder.SetMsgs(msg); err != nil { - return nil, fmt.Errorf("failed to set messages: %w", err) - } + fmt.Printf("Setting transaction fee: %d%s (gas limit: %d, gas price: %f) for node %s\n", + feeAmount, txParams.Config.Denom, gasLimit, gasPrice, txParams.NodeURL) - // Check if there's a pre-calculated gas amount for multisend transactions - var gasLimit uint64 - if calculatedGas, ok := txp.MsgParams["calculated_gas_amount"].(uint64); ok && calculatedGas > 0 { - // Use the pre-calculated gas amount for multisend - gasLimit = calculatedGas - fmt.Printf("Using pre-calculated gas amount for multisend: %d\n", gasLimit) - } else { - // Estimate gas through simulation - simulatedGas, err := DetermineOptimalGas(ctx, clientCtx, tx.Factory{}, 1.3, msg) + // Build the message based on MsgType + var msg sdk.Msg + switch txParams.MsgType { + case "bank_send": + var memo string + msg, memo, err = bankmodule.CreateBankSendMsg(txParams.Config, txParams.MsgParams["from_address"].(string), convertMapToMsgParams(txParams.MsgParams)) if err != nil { - // Use default if simulation fails - gasLimit = uint64(txParams.Config.BaseGas) - fmt.Printf("Gas simulation failed, using default gas: %d\n", gasLimit) - } else { - gasLimit = simulatedGas + return nil, fmt.Errorf("failed to build send message: %w", err) } - } - - txBuilder.SetGasLimit(gasLimit) - - // Set fee - get the gas price from config - gasPrice := txParams.Config.Gas.Low - - // For zero gas price, check if we should use adaptive gas pricing - if gasPrice == 0 { - gsm := GetGasStrategyManager() - caps := gsm.GetNodeCapabilities(txParams.NodeURL) - if !caps.AcceptsZeroGas { - // Use low non-zero gas price if node doesn't support zero gas - gasPrice = 1 - fmt.Printf("Node %s may not support zero gas, using gas price: %d\n", - txParams.NodeURL, gasPrice) + if err := txBuilder.SetMsgs(msg); err != nil { + return nil, fmt.Errorf("failed to set send message: %w", err) } - } - - // Calculate initial fee amount - feeAmount := calculateInitialFee(gasLimit, gasPrice) - - // Important: Check if we have a stored minimum fee for this node and use it if higher - nodeSettings.mutex.RLock() - if minFee, exists := nodeSettings.MinimumFees[txParams.Config.Denom]; exists { - if uint64(feeAmount) < minFee { - fmt.Printf("Using node-specific minimum fee %d instead of calculated %d for %s\n", - minFee, feeAmount, txParams.NodeURL) - feeAmount = int64(minFee) + // Set memo if needed + if memo != "" && txParams.Config.Memo == "" { + txParams.Config.Memo = memo } - } - nodeSettings.mutex.RUnlock() - - // Apply a more aggressive minimum fee strategy to prevent "insufficient fees" errors - // For nodes that have previously returned fee errors, add a buffer - baseFeeThreshold := int64(200) - - // For large multisend transactions, ensure the fee is proportionally higher - if txParams.MsgType == "bank_multisend" && gasLimit > 100000 { - // Much higher minimum fee for large multisends - use gas-based calculation - minFee := int64(gasLimit) / 5000 // More aggressive scaling (changed from 10000) - if feeAmount < minFee { - feeAmount = minFee - fmt.Printf("Increasing multisend fee to %d based on gas usage\n", feeAmount) + case "bank_multisend": + distributor, ok := txParams.Distributor.(*bankmodule.MultiSendDistributor) + if !ok { + return nil, fmt.Errorf("invalid distributor for multisend") } - } else if gasPrice > 0 && feeAmount < baseFeeThreshold { - // For regular transactions, use higher minimum fee - feeAmount = baseFeeThreshold - fmt.Printf("Using minimum fee threshold of %d\n", feeAmount) - } - - // Apply additional node-specific fee buffer based on historical errors - // This helps prevent fee errors on chains that are sensitive to fee amounts - feeAmount = applyNodeFeeBuffer(txParams.NodeURL, txParams.Config.Denom, feeAmount) - - feeCoin := fmt.Sprintf("%d%s", feeAmount, txParams.Config.Denom) - fee, err := sdk.ParseCoinsNormalized(feeCoin) - if err != nil { - return nil, fmt.Errorf("failed to parse fee: %w", err) + var memo string + msg, memo, err = distributor.CreateDistributedMultiSendMsg( + txParams.MsgParams["from_address"].(string), + convertMapToMsgParams(txParams.MsgParams), + time.Now().UnixNano(), + ) + if err != nil { + return nil, fmt.Errorf("failed to build multisend message: %w", err) + } + if err := txBuilder.SetMsgs(msg); err != nil { + return nil, fmt.Errorf("failed to set multisend message: %w", err) + } + // Set memo if needed + if memo != "" && txParams.Config.Memo == "" { + txParams.Config.Memo = memo + } + default: + return nil, fmt.Errorf("unsupported message type: %s", txParams.MsgType) } - fmt.Printf("Setting transaction fee: %s (gas limit: %d, gas price: %d) for node %s\n", - fee.String(), gasLimit, gasPrice, txParams.NodeURL) - txBuilder.SetFeeAmount(fee) - - // Set memo if provided - txBuilder.SetMemo("") - - // Get account number - this is still needed, but we won't use its sequence - accNum := txParams.AccNum - fromAddress, _ := txParams.MsgParams["from_address"].(string) - - // We only need account number from GetAccountInfo, NOT the sequence - // The sequence we use should be from our tracking system, which considers mempool state - fetchedAccNum, stateSequence, err := GetAccountInfo(ctx, clientCtx, fromAddress) - if err != nil { - return nil, fmt.Errorf("failed to get account info: %w", err) + // Set memo and timeout height if provided + txBuilder.SetMemo(txParams.Config.Memo) + if txParams.Config.TimeoutHeight > 0 { + txBuilder.SetTimeoutHeight(uint64(txParams.Config.TimeoutHeight)) } - // Use the fetched account number - accNum = fetchedAccNum - - // Only log the state sequence for debugging, but don't use it directly - // This helps us understand discrepancies between state and our tracked sequences - fmt.Printf("Node %s reports state sequence %d, using tracked sequence %d\n", - txParams.NodeURL, stateSequence, sequence) - - // If we have no better information (first tx to this node), then use state sequence - if sequence == 0 { - sequence = stateSequence - fmt.Printf("First transaction to node %s, using state sequence: %d\n", txParams.NodeURL, sequence) - updateSequence(txParams.NodeURL, sequence) + // Use a simplified approach for signing that works with SDK v0.50.x + // Create an initial signature with no actual signature data + sigData := &signing.SingleSignatureData{ + SignMode: signing.SignMode_SIGN_MODE_DIRECT, + Signature: nil, } - // Set up signature - sigV2 := signing.SignatureV2{ + sig := signing.SignatureV2{ PubKey: txParams.PubKey, + Data: sigData, Sequence: sequence, - Data: &signing.SingleSignatureData{ - SignMode: signing.SignMode_SIGN_MODE_DIRECT, - }, } - if err := txBuilder.SetSignatures(sigV2); err != nil { - return nil, fmt.Errorf("failed to set signatures: %w", err) + // Set the initial signature + if err := txBuilder.SetSignatures(sig); err != nil { + return nil, fmt.Errorf("failed to set initial signature: %w", err) } - signerData := xauthsigning.SignerData{ - ChainID: txParams.ChainID, - AccountNumber: accNum, - Sequence: sequence, + // Get bytes to sign - manually encode the transaction + // First encode to get the bytes to sign + encodedTx, err := encodingConfig.TxEncoder()(txBuilder.GetTx()) + if err != nil { + return nil, fmt.Errorf("failed to encode transaction for signing: %w", err) } // Sign the transaction with the private key - sigV2, err = tx.SignWithPrivKey( - ctx, - signing.SignMode_SIGN_MODE_DIRECT, - signerData, - txBuilder, - txParams.PrivKey, - clientCtx.TxConfig, - sequence, - ) + signature, err := txParams.PrivKey.Sign(encodedTx) if err != nil { return nil, fmt.Errorf("failed to sign transaction: %w", err) } - // Set the signed signature - if err := txBuilder.SetSignatures(sigV2); err != nil { - return nil, fmt.Errorf("failed to set signatures: %w", err) + // Update the signature with the real signature + sigData.Signature = signature + if err := txBuilder.SetSignatures(sig); err != nil { + return nil, fmt.Errorf("failed to set final signature: %w", err) } // Encode the transaction - txBytes, err := clientCtx.TxConfig.TxEncoder()(txBuilder.GetTx()) + txBytes, err := encodingConfig.TxEncoder()(txBuilder.GetTx()) if err != nil { return nil, fmt.Errorf("failed to encode transaction: %w", err) } - // If successful, update our node sequence cache preemptively - // This helps avoid sequence errors on subsequent transactions to the same node - updateSequence(txParams.NodeURL, sequence+1) - return txBytes, nil } diff --git a/configurations/atomone/nodes.toml b/configurations/atomone/nodes.toml new file mode 100644 index 0000000..7d3cdd3 --- /dev/null +++ b/configurations/atomone/nodes.toml @@ -0,0 +1,43 @@ +# Meteorite configuration for AtomOne (atomone) +# Generated from the Cosmos Chain Registry + +chain = "atomone-1" +gas_per_byte = 100 +base_gas = 200000 +msg_type = "hybrid" +multisend = true +num_multisend = 3000 +positions = 50 +slip44 = 118 +denom = "uatone" +prefix = "atone" +broadcast_mode = "grpc" + +[gas] +low = 25 +precision = 3 + +[nodes] +rpc = [ + "https://atomone.rpc.nodeshub.online:443", + "https://atomone-rpc.allinbits.com:443", + "https://rpc.atomone-1.atomone.aviaone.com:443", + "https://rpc.atomone.citizenweb3.com:443", + "https://rpc-atone.vinjan.xyz", + "https://atomone.rpc.m.stavr.tech:443", + "https://atomone-rpc.publicnode.com:443", + "https://rpc-atomone.nodeist.net", + "https://atomone-mainnet-rpc.itrocket.net:443", + "https://community.nuxian-node.ch:6797/atomone/trpc", + "https://rpc-atomone.ecostake.com", + "https://atomone-rpc.cogwheel.zone", + "https://rpc-atomone-1.cros-nest.com:443", + "https://atomone_mainnet_rpc.chain.whenmoonwhenlambo.money", + "https://atomone-rpc.ibs.team:443", +] +api = "https://atomone-api.allinbits.com" +grpc = "atomone-grpc.allinbits.com:9090" + +[msg_params] +to_address = "" +amount = 1 diff --git a/configurations/atomone/peerdiscovery.log b/configurations/atomone/peerdiscovery.log new file mode 100644 index 0000000..e69de29 diff --git a/modes/registry/registry.go b/modes/registry/registry.go index d37fb5a..56e5b55 100644 --- a/modes/registry/registry.go +++ b/modes/registry/registry.go @@ -329,9 +329,11 @@ func runChainTest(selection *chainregistry.ChainSelection, configMap map[string] printAccountInformation(accounts, config) // Check and adjust balances if needed - if err := checkAndAdjustBalances(accounts, config); err != nil { - return fmt.Errorf("failed to handle balance adjustment: %v", err) - } + /* + if err := checkAndAdjustBalances(accounts, config); err != nil { + return fmt.Errorf("failed to handle balance adjustment: %v", err) + } + */ // Get chain ID chainID := config.Chain // Use the chain ID from the config