diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000..534c2d6 --- /dev/null +++ b/.cursorignore @@ -0,0 +1,3 @@ +# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) +emails/ +configurations/ diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..6416961 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,23 @@ +// Hardhat .cursorrules + +// HTMX and Go best practices +const htmxGoBestPractices = [ +"follow practices suited for cosmos-sdk v0.50.x", +"follow practices suited for ibc-go v8.x.x" +]; + +// Folder structure +const folderStructure = ` + main.go +lib/ + +modules/ +go.mod +go.sum +`; + +// Additional instructions +const additionalInstructions = ` +1. We are building a high speed test framework for mainnets +2. We should aim for this application to be precise and performant +`; \ No newline at end of file diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml new file mode 100644 index 0000000..42d8071 --- /dev/null +++ b/.github/workflows/clippy.yml @@ -0,0 +1,29 @@ +# .github/workflows/clippy.yml +name: Clippy + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + clippy: + name: Run Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: clippy + - name: Run Clippy + uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features --manifest-path contracts/statefilestore/Cargo.toml -- -D warnings diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..5ab0a91 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,21 @@ +on: + push: + branches: + - main + pull_request: + branches: + - main +name: Test +jobs: + test: + strategy: + matrix: + go-version: [1.23.x] + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go-version }} + - run: go test ./... diff --git a/.gitignore b/.gitignore index 6612f8b..d166f1f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +target/ **seedphrase -**meteorite \ No newline at end of file +**meteorite diff --git a/README.md b/README.md index e403551..becbb64 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,14 @@ This will initiate the testing suite with your specified configurations. ## Important Notes -- **Responsible Use**: meteorite is designed for use on test networks and should be used responsibly. Ensure you have proper authorization before testing on any network you do not own or operate. +- **Responsible Use**: meteorite is designed for use on mainnets. The tokens you own grant you the right to make any valid transaction. Like any user of any chain, meteorite can only make valid transactions. + * Test your favorite mainnet, yourself, and make sure that you can't get Luna'd + * Test your favorite mainnet, yourself, and make sure that you can't get Levana'd + +Over $70,001,400,000 has been lost to the class of issues named p2p storms. + +If you're investing, test the chain. It's free if you tweet about using meteorite. + - **Valid Transactions Only**: The tool operates within the bounds of valid transactions explicitly supported by the chains it tests. - **Reporting Issues**: For questions about meteorite's capabilities or to report potential security issues, please contact the project maintainers through the appropriate channels listed in this repository. diff --git a/broadcast/broadcast.go b/broadcast/broadcast.go index e87f2e9..3562103 100644 --- a/broadcast/broadcast.go +++ b/broadcast/broadcast.go @@ -1,20 +1,58 @@ package broadcast import ( - "context" "fmt" - "log" + "time" - cometrpc "github.com/cometbft/cometbft/rpc/client/http" coretypes "github.com/cometbft/cometbft/rpc/core/types" - tmtypes "github.com/cometbft/cometbft/types" transfertypes "github.com/cosmos/ibc-go/v8/modules/apps/transfer/types" + "github.com/somatic-labs/meteorite/lib" + types "github.com/somatic-labs/meteorite/types" "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" ) +// Add these at the top of the file +type TimingMetrics struct { + PrepStart time.Time + SignStart time.Time + BroadStart time.Time + Complete time.Time + Position int +} + +func (t *TimingMetrics) LogTiming(sequence uint64, success bool, err error) { + prepTime := t.SignStart.Sub(t.PrepStart) + signTime := t.BroadStart.Sub(t.SignStart) + broadcastTime := t.Complete.Sub(t.BroadStart) + totalTime := t.Complete.Sub(t.PrepStart) + + status := "SUCCESS" + if !success { + status = "FAILED" + } + + fmt.Printf("[POS-%d] %s Transaction %s: seq=%d prep=%v sign=%v broadcast=%v total=%v%s\n", + t.Position, + time.Now().Format("15:04:05.000"), + status, + sequence, + prepTime, + signTime, + broadcastTime, + totalTime, + formatError(err)) +} + +func formatError(err error) string { + if err != nil { + return fmt.Sprintf(" error=\"%v\"", err) + } + return "" +} + var cdc = codec.NewProtoCodec(codectypes.NewInterfaceRegistry()) func init() { @@ -24,24 +62,88 @@ func init() { // Transaction broadcasts the transaction bytes to the given RPC endpoint. func Transaction(txBytes []byte, rpcEndpoint string) (*coretypes.ResultBroadcastTx, error) { - cmtCli, err := cometrpc.New(rpcEndpoint, "/websocket") + client, err := GetClient(rpcEndpoint) if err != nil { - log.Fatal(err) + return nil, err } - t := tmtypes.Tx(txBytes) + return client.Transaction(txBytes) +} - ctx := context.Background() - res, err := cmtCli.BroadcastTxSync(ctx, t) - if err != nil { - fmt.Println("Error at broadcast:", err) - return nil, err +// Loop handles the main transaction broadcasting logic +func Loop( + txParams types.TransactionParams, + batchSize int, + position int, +) (successfulTxns, failedTxns int, responseCodes map[uint32]int, updatedSequence uint64) { + successfulTxns = 0 + failedTxns = 0 + responseCodes = make(map[uint32]int) + sequence := txParams.Sequence + + for i := 0; i < batchSize; i++ { + currentSequence := sequence + metrics := &TimingMetrics{ + PrepStart: time.Now(), + Position: position, + } + + metrics.SignStart = time.Now() + metrics.BroadStart = time.Now() + resp, _, err := SendTransactionViaRPC(txParams, currentSequence) + metrics.Complete = time.Now() + + if err != nil { + metrics.LogTiming(currentSequence, false, err) + failedTxns++ + + if resp != nil && resp.Code == 32 { + newSeq, success, newResp := handleSequenceMismatch(txParams, position, sequence, err) + sequence = newSeq + if success { + successfulTxns++ + responseCodes[newResp.Code]++ + } + continue + } + continue + } + + metrics.LogTiming(currentSequence, true, nil) + successfulTxns++ + responseCodes[resp.Code]++ + sequence++ + } + + updatedSequence = sequence + return successfulTxns, failedTxns, responseCodes, updatedSequence +} + +// handleSequenceMismatch handles the case where a transaction fails due to sequence mismatch +func handleSequenceMismatch(txParams types.TransactionParams, position int, sequence uint64, err error) (uint64, bool, *coretypes.ResultBroadcastTx) { + expectedSeq, parseErr := lib.ExtractExpectedSequence(err.Error()) + if parseErr != nil { + fmt.Printf("[POS-%d] Failed to parse expected sequence: %v\n", position, parseErr) + return sequence, false, nil } - if res.Code != 0 { - // Return an error containing the code and log message - return res, fmt.Errorf("broadcast error code %d: %s", res.Code, res.Log) + fmt.Printf("[POS-%d] Set sequence to expected value %d due to mismatch\n", position, expectedSeq) + + metrics := &TimingMetrics{ + PrepStart: time.Now(), + Position: position, + } + + metrics.SignStart = time.Now() + metrics.BroadStart = time.Now() + resp, _, err := SendTransactionViaRPC(txParams, expectedSeq) + metrics.Complete = time.Now() + + if err != nil { + metrics.LogTiming(expectedSeq, false, err) + return expectedSeq, false, nil } - return res, nil + metrics.LogTiming(expectedSeq, true, nil) + return expectedSeq + 1, true, resp } diff --git a/broadcast/client.go b/broadcast/client.go new file mode 100644 index 0000000..fb04f17 --- /dev/null +++ b/broadcast/client.go @@ -0,0 +1,68 @@ +package broadcast + +import ( + "context" + "fmt" + "sync" + "time" + + cometrpc "github.com/cometbft/cometbft/rpc/client/http" + coretypes "github.com/cometbft/cometbft/rpc/core/types" + tmtypes "github.com/cometbft/cometbft/types" +) + +type Client struct { + client *cometrpc.HTTP +} + +var ( + clients = make(map[string]*Client) + clientsMux sync.RWMutex +) + +func GetClient(rpcEndpoint string) (*Client, error) { + clientsMux.RLock() + if client, exists := clients[rpcEndpoint]; exists { + clientsMux.RUnlock() + return client, nil + } + clientsMux.RUnlock() + + // If client doesn't exist, acquire write lock and create it + clientsMux.Lock() + defer clientsMux.Unlock() + + // Double-check after acquiring write lock + if client, exists := clients[rpcEndpoint]; exists { + return client, nil + } + + // Create new client + cmtCli, err := cometrpc.New(rpcEndpoint, "/websocket") + if err != nil { + return nil, err + } + + client := &Client{ + client: cmtCli, + } + clients[rpcEndpoint] = client + return client, nil +} + +func (b *Client) Transaction(txBytes []byte) (*coretypes.ResultBroadcastTx, error) { + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + t := tmtypes.Tx(txBytes) + res, err := b.client.BroadcastTxSync(ctx, t) + if err != nil { + return nil, err + } + + if res.Code != 0 { + return res, fmt.Errorf("broadcast error code %d: %s", res.Code, res.Log) + } + + return res, nil +} diff --git a/broadcast/grpc.go b/broadcast/grpc.go index 7c3f68a..a8ba297 100644 --- a/broadcast/grpc.go +++ b/broadcast/grpc.go @@ -5,25 +5,10 @@ import ( "fmt" "github.com/cosmos/ibc-go/modules/apps/callbacks/testing/simapp/params" - "github.com/cosmos/ibc-go/v8/modules/apps/transfer" - ibc "github.com/cosmos/ibc-go/v8/modules/core" client "github.com/somatic-labs/meteorite/client" - meteoritebank "github.com/somatic-labs/meteorite/modules/bank" - meteoriteibc "github.com/somatic-labs/meteorite/modules/ibc" - wasm "github.com/somatic-labs/meteorite/modules/wasm" types "github.com/somatic-labs/meteorite/types" - sdkmath "cosmossdk.io/math" - - "github.com/cosmos/cosmos-sdk/client/tx" - "github.com/cosmos/cosmos-sdk/std" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/tx/signing" - authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" - "github.com/cosmos/cosmos-sdk/x/bank" - "github.com/cosmos/cosmos-sdk/x/gov" - - wasmd "github.com/CosmWasm/wasmd/x/wasm" ) func SendTransactionViaGRPC( @@ -35,110 +20,8 @@ func SendTransactionViaGRPC( encodingConfig := params.MakeTestEncodingConfig() encodingConfig.Codec = cdc - // Register necessary interfaces - transferModule := transfer.AppModuleBasic{} - ibcModule := ibc.AppModuleBasic{} - bankModule := bank.AppModuleBasic{} - wasmModule := wasmd.AppModuleBasic{} - govModule := gov.AppModuleBasic{} - - ibcModule.RegisterInterfaces(encodingConfig.InterfaceRegistry) - transferModule.RegisterInterfaces(encodingConfig.InterfaceRegistry) - bankModule.RegisterInterfaces(encodingConfig.InterfaceRegistry) - wasmModule.RegisterInterfaces(encodingConfig.InterfaceRegistry) - govModule.RegisterInterfaces(encodingConfig.InterfaceRegistry) - std.RegisterInterfaces(encodingConfig.InterfaceRegistry) - - // Create a new TxBuilder. - txBuilder := encodingConfig.TxConfig.NewTxBuilder() - - var msg sdk.Msg - var memo string - - // Construct the message based on the message type - switch txParams.MsgType { - case "ibc_transfer": - var err error - msg, memo, err = meteoriteibc.CreateIBCTransferMsg(txParams.Config, txParams.AcctAddress, txParams.MsgParams) - if err != nil { - return nil, "", err - } - case "bank_send": - var err error - msg, memo, err = meteoritebank.CreateBankSendMsg(txParams.Config, txParams.AcctAddress, txParams.MsgParams) - if err != nil { - return nil, "", err - } - case "store_code": - var err error - msg, memo, err = wasm.CreateStoreCodeMsg(txParams.Config, txParams.AcctAddress, txParams.MsgParams) - if err != nil { - return nil, "", err - } - case "instantiate_contract": - var err error - msg, memo, err = wasm.CreateInstantiateContractMsg(txParams.Config, txParams.AcctAddress, txParams.MsgParams) - if err != nil { - return nil, "", err - } - default: - return nil, "", fmt.Errorf("unsupported message type: %s", txParams.MsgType) - } - - // Set the message and other transaction parameters - if err := txBuilder.SetMsgs(msg); err != nil { - return nil, "", err - } - - // Estimate gas limit - txSize := len(msg.String()) - gasLimit := uint64((int64(txSize) * txParams.Config.Bytes) + txParams.Config.BaseGas) - txBuilder.SetGasLimit(gasLimit) - - // Calculate fee - gasPrice := sdk.NewDecCoinFromDec(txParams.Config.Denom, sdkmath.LegacyNewDecWithPrec(txParams.Config.Gas.Low, txParams.Config.Gas.Precision)) - feeAmount := gasPrice.Amount.MulInt64(int64(gasLimit)).RoundInt() - feeCoin := sdk.NewCoin(txParams.Config.Denom, feeAmount) - txBuilder.SetFeeAmount(sdk.NewCoins(feeCoin)) - - // Set memo and timeout height - txBuilder.SetMemo(memo) - txBuilder.SetTimeoutHeight(0) - - // Set up signature - sigV2 := signing.SignatureV2{ - PubKey: txParams.PubKey, - Sequence: sequence, - Data: &signing.SingleSignatureData{ - SignMode: signing.SignMode_SIGN_MODE_DIRECT, - }, - } - - if err := txBuilder.SetSignatures(sigV2); err != nil { - return nil, "", err - } - - signerData := authsigning.SignerData{ - ChainID: txParams.ChainID, - AccountNumber: txParams.AccNum, - Sequence: sequence, - } - - // Sign the transaction - if _, err := tx.SignWithPrivKey( - ctx, - signing.SignMode_SIGN_MODE_DIRECT, - signerData, - txBuilder, - txParams.PrivKey, - encodingConfig.TxConfig, - sequence, - ); err != nil { - return nil, "", err - } - - // Encode the transaction - txBytes, err := encodingConfig.TxConfig.TxEncoder()(txBuilder.GetTx()) + // Build and sign the transaction + txBytes, err := BuildAndSignTransaction(ctx, txParams, sequence, encodingConfig) if err != nil { return nil, "", err } diff --git a/broadcast/rpc.go b/broadcast/rpc.go index 0d04dd6..3a1a660 100644 --- a/broadcast/rpc.go +++ b/broadcast/rpc.go @@ -6,146 +6,23 @@ import ( coretypes "github.com/cometbft/cometbft/rpc/core/types" "github.com/cosmos/ibc-go/modules/apps/callbacks/testing/simapp/params" - "github.com/cosmos/ibc-go/v8/modules/apps/transfer" - ibc "github.com/cosmos/ibc-go/v8/modules/core" - meteoritebank "github.com/somatic-labs/meteorite/modules/bank" - meteoriteibc "github.com/somatic-labs/meteorite/modules/ibc" - wasm "github.com/somatic-labs/meteorite/modules/wasm" types "github.com/somatic-labs/meteorite/types" - - sdkmath "cosmossdk.io/math" - - "github.com/cosmos/cosmos-sdk/client/tx" - "github.com/cosmos/cosmos-sdk/std" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/tx/signing" - authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" - "github.com/cosmos/cosmos-sdk/x/bank" - "github.com/cosmos/cosmos-sdk/x/gov" - - wasmd "github.com/CosmWasm/wasmd/x/wasm" ) // SendTransactionViaRPC sends a transaction using the provided TransactionParams and sequence number. -func SendTransactionViaRPC(txParams types.TransactionParams, sequence uint64) (response *coretypes.ResultBroadcastTx, txbody string, err error) { +func SendTransactionViaRPC(txParams types.TransactionParams, sequence uint64) (*coretypes.ResultBroadcastTx, string, error) { encodingConfig := params.MakeTestEncodingConfig() encodingConfig.Codec = cdc - // Register IBC and other necessary types - transferModule := transfer.AppModuleBasic{} - ibcModule := ibc.AppModuleBasic{} - bankModule := bank.AppModuleBasic{} - wasmModule := wasmd.AppModuleBasic{} - govModule := gov.AppModuleBasic{} - - ibcModule.RegisterInterfaces(encodingConfig.InterfaceRegistry) - transferModule.RegisterInterfaces(encodingConfig.InterfaceRegistry) - bankModule.RegisterInterfaces(encodingConfig.InterfaceRegistry) - wasmModule.RegisterInterfaces(encodingConfig.InterfaceRegistry) - govModule.RegisterInterfaces(encodingConfig.InterfaceRegistry) - std.RegisterInterfaces(encodingConfig.InterfaceRegistry) - - // Create a new TxBuilder. - txBuilder := encodingConfig.TxConfig.NewTxBuilder() - - var msg sdk.Msg - var memo string // Declare a variable to hold the memo - - switch txParams.MsgType { - case "ibc_transfer": - msg, memo, err = meteoriteibc.CreateIBCTransferMsg(txParams.Config, txParams.AcctAddress, txParams.MsgParams) - if err != nil { - return nil, "", err - } - case "bank_send": - msg, memo, err = meteoritebank.CreateBankSendMsg(txParams.Config, txParams.AcctAddress, txParams.MsgParams) - if err != nil { - return nil, "", err - } - case "store_code": - msg, memo, err = wasm.CreateStoreCodeMsg(txParams.Config, txParams.AcctAddress, txParams.MsgParams) - if err != nil { - return nil, "", err - } - case "instantiate_contract": - msg, memo, err = wasm.CreateInstantiateContractMsg(txParams.Config, txParams.AcctAddress, txParams.MsgParams) - if err != nil { - return nil, "", err - } - default: - return nil, "", fmt.Errorf("unsupported message type: %s", txParams.MsgType) - } - - // Set messages - err = txBuilder.SetMsgs(msg) - if err != nil { - return nil, "", err - } - - // Estimate gas limit based on transaction size - txSize := len(msg.String()) - gasLimit := uint64((int64(txSize) * txParams.Config.Bytes) + txParams.Config.BaseGas) - txBuilder.SetGasLimit(gasLimit) - - // Calculate fee based on gas limit and a fixed gas price - gasPrice := sdk.NewDecCoinFromDec(txParams.Config.Denom, sdkmath.LegacyNewDecWithPrec(txParams.Config.Gas.Low, txParams.Config.Gas.Precision)) - feeAmount := gasPrice.Amount.MulInt64(int64(gasLimit)).RoundInt() - feecoin := sdk.NewCoin(txParams.Config.Denom, feeAmount) - txBuilder.SetFeeAmount(sdk.NewCoins(feecoin)) - - // Set the memo (either random for bank_send or as per IBC transfer) - txBuilder.SetMemo(memo) - txBuilder.SetTimeoutHeight(0) - - // First round: gather all the signer infos using the "set empty signature" hack - sigV2 := signing.SignatureV2{ - PubKey: txParams.PubKey, - Sequence: sequence, - Data: &signing.SingleSignatureData{ - SignMode: signing.SignMode(encodingConfig.TxConfig.SignModeHandler().DefaultMode()), - }, - } - - err = txBuilder.SetSignatures(sigV2) - if err != nil { - fmt.Println("Error setting signatures") - return nil, "", err - } - - signerData := authsigning.SignerData{ - ChainID: txParams.ChainID, - AccountNumber: txParams.AccNum, - Sequence: sequence, - } - ctx := context.Background() - signed, err := tx.SignWithPrivKey( - ctx, - signing.SignMode(encodingConfig.TxConfig.SignModeHandler().DefaultMode()), - signerData, - txBuilder, - txParams.PrivKey, - encodingConfig.TxConfig, - sequence, - ) - if err != nil { - fmt.Println("Couldn't sign") - return nil, "", err - } - - err = txBuilder.SetSignatures(signed) - if err != nil { - return nil, "", err - } - - // Generate the encoded transaction bytes - txBytes, err := encodingConfig.TxConfig.TxEncoder()(txBuilder.GetTx()) + // Build and sign the transaction + txBytes, err := BuildAndSignTransaction(ctx, txParams, sequence, encodingConfig) if err != nil { - fmt.Println(err) return nil, "", err } + // Broadcast the transaction via RPC resp, err := Transaction(txBytes, txParams.NodeURL) if err != nil { return resp, string(txBytes), fmt.Errorf("failed to broadcast transaction: %w", err) diff --git a/broadcast/transaction.go b/broadcast/transaction.go new file mode 100644 index 0000000..89b12ea --- /dev/null +++ b/broadcast/transaction.go @@ -0,0 +1,142 @@ +package broadcast + +import ( + "context" + "fmt" + + "github.com/cosmos/ibc-go/modules/apps/callbacks/testing/simapp/params" + "github.com/cosmos/ibc-go/v8/modules/apps/transfer" + ibc "github.com/cosmos/ibc-go/v8/modules/core" + meteoritebank "github.com/somatic-labs/meteorite/modules/bank" + meteoriteibc "github.com/somatic-labs/meteorite/modules/ibc" + wasm "github.com/somatic-labs/meteorite/modules/wasm" + "github.com/somatic-labs/meteorite/types" + + sdkmath "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/client/tx" + "github.com/cosmos/cosmos-sdk/std" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/gov" + + wasmd "github.com/CosmWasm/wasmd/x/wasm" +) + +func BuildAndSignTransaction( + ctx context.Context, + txParams types.TransactionParams, + sequence uint64, + encodingConfig params.EncodingConfig, +) ([]byte, error) { + // Register necessary interfaces + transferModule := transfer.AppModuleBasic{} + ibcModule := ibc.AppModuleBasic{} + bankModule := bank.AppModuleBasic{} + wasmModule := wasmd.AppModuleBasic{} + govModule := gov.AppModuleBasic{} + + ibcModule.RegisterInterfaces(encodingConfig.InterfaceRegistry) + transferModule.RegisterInterfaces(encodingConfig.InterfaceRegistry) + bankModule.RegisterInterfaces(encodingConfig.InterfaceRegistry) + wasmModule.RegisterInterfaces(encodingConfig.InterfaceRegistry) + govModule.RegisterInterfaces(encodingConfig.InterfaceRegistry) + std.RegisterInterfaces(encodingConfig.InterfaceRegistry) + + // Create a new TxBuilder + txBuilder := encodingConfig.TxConfig.NewTxBuilder() + + var msg sdk.Msg + var memo string + + // Construct the message based on the message type + var err error + switch txParams.MsgType { + case "ibc_transfer": + msg, memo, err = meteoriteibc.CreateIBCTransferMsg(txParams.Config, txParams.AcctAddress, txParams.MsgParams) + case "bank_send": + msg, memo, err = meteoritebank.CreateBankSendMsg(txParams.Config, txParams.AcctAddress, txParams.MsgParams) + case "store_code": + msg, memo, err = wasm.CreateStoreCodeMsg(txParams.Config, txParams.AcctAddress, txParams.MsgParams) + case "instantiate_contract": + msg, memo, err = wasm.CreateInstantiateContractMsg(txParams.Config, txParams.AcctAddress, txParams.MsgParams) + default: + return nil, fmt.Errorf("unsupported message type: %s", txParams.MsgType) + } + if err != nil { + return nil, err + } + + // Set the message and other transaction parameters + if err := txBuilder.SetMsgs(msg); err != nil { + return nil, err + } + + // Estimate gas limit + txSize := len(msg.String()) + gasLimit := uint64(int64(txSize)*txParams.Config.GasPerByte + txParams.Config.BaseGas) + txBuilder.SetGasLimit(gasLimit) + + // Calculate fee + gasPrice := sdk.NewDecCoinFromDec( + txParams.Config.Denom, + sdkmath.LegacyNewDecWithPrec(txParams.Config.Gas.Low, txParams.Config.Gas.Precision), + ) + feeAmount := gasPrice.Amount.MulInt64(int64(gasLimit)).RoundInt() + feeCoin := sdk.NewCoin(txParams.Config.Denom, feeAmount) + txBuilder.SetFeeAmount(sdk.NewCoins(feeCoin)) + + // Set memo and timeout height + txBuilder.SetMemo(memo) + txBuilder.SetTimeoutHeight(0) + + // Set up signature + sigV2 := signing.SignatureV2{ + PubKey: txParams.PubKey, + Sequence: sequence, + Data: &signing.SingleSignatureData{ + SignMode: signing.SignMode_SIGN_MODE_DIRECT, + }, + } + + if err := txBuilder.SetSignatures(sigV2); err != nil { + return nil, err + } + + signerData := authsigning.SignerData{ + ChainID: txParams.ChainID, + AccountNumber: txParams.AccNum, + Sequence: sequence, + } + + fmt.Println("signerData", signerData) + + // Sign the transaction with the private key + sigV2, err = tx.SignWithPrivKey( + ctx, + signing.SignMode_SIGN_MODE_DIRECT, + signerData, + txBuilder, + txParams.PrivKey, + encodingConfig.TxConfig, + sequence, + ) + if err != nil { + return nil, err + } + + // Set the signed signature back to the txBuilder + if err := txBuilder.SetSignatures(sigV2); err != nil { + return nil, err + } + + // Encode the transaction + txBytes, err := encodingConfig.TxConfig.TxEncoder()(txBuilder.GetTx()) + if err != nil { + return nil, err + } + + return txBytes, nil +} diff --git a/configurations/v50/banksend.toml b/configurations/v50/banksend.toml new file mode 100644 index 0000000..eb5e47f --- /dev/null +++ b/configurations/v50/banksend.toml @@ -0,0 +1,32 @@ +# configurations/v50/testiepoo.toml + +chain = "mantra" +channel = "channel-1" +denom = "uom" +prefix = "mantra" +gas_per_byte = 100 +base_gas = 175000 +ibc_memo = "Your IBC Memo" +memo = "Just checking on this. And making sure that we have all 256 bytes. Because why not. If we gonna storm it might as well be with all the bytes." +ibc_memo_repeat = 10 +rand_min = 15000 +rand_max = 15000 +revision_number = 4 +timeout_height = 21720608 +slip44 = 118 + +msg_type = "bank_send" + +[msg_params] +amount = 1 +to_address = "mantra1uqrar205hjv4s8832kwj8e6xhwvk4x0eqml043" + +[gas] +low = 1 +precision = 2 + +[nodes] +rpc = ["http://127.0.0.1:26657"] +api = "https://api.dukong.mantrachain.dev:443" + +# rpc = ["http://127.0.0.1:26657", "https://rpc.dukong.mantrachain.dev:443"] diff --git a/contracts/statefilestore/src/contract.rs b/contracts/statefilestore/src/contract.rs index 5130c28..ee350f6 100644 --- a/contracts/statefilestore/src/contract.rs +++ b/contracts/statefilestore/src/contract.rs @@ -42,7 +42,9 @@ pub fn execute_store_file(deps: DepsMut, data: Binary) -> StdResult { } // Compress the data - let compressed_data = encode_all(&data, 3)?; // Compression level 3 (balance between speed and compression ratio) + let compressed_data = encode_all(data.as_slice(), 3).map_err(|err| { + cosmwasm_std::StdError::generic_err(format!("Compression error: {}", err)) + })?; // Compression level 3 // Compute SHA256 hash of the compressed data let mut hasher = Sha256::new(); @@ -61,7 +63,8 @@ pub fn execute_store_file(deps: DepsMut, data: Binary) -> StdResult { let cid_string = format!("b{}", cid_base32.to_lowercase()); // Store compressed file data in storage, keyed by SHA256 hash - FILES.save(deps.storage, &sha256_hex, &compressed_data)?; + // Convert compressed_data to Binary and save it + FILES.save(deps.storage, &sha256_hex, &compressed_data.clone().into())?; let res = Response::new() .add_attribute("method", "execute_store_file") diff --git a/go.mod b/go.mod index f5e7c93..368bdf8 100644 --- a/go.mod +++ b/go.mod @@ -91,7 +91,7 @@ require ( github.com/cosmos/btcutil v1.0.5 // indirect github.com/cosmos/cosmos-db v1.0.2 // indirect github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect - github.com/cosmos/go-bip39 v1.0.0 // indirect + github.com/cosmos/go-bip39 v1.0.0 github.com/cosmos/gogogateway v1.2.0 // indirect github.com/cosmos/gogoproto v1.7.0 // indirect github.com/cosmos/iavl v1.2.0 // indirect diff --git a/lib/lib.go b/lib/lib.go index c23b1e5..7960dd0 100644 --- a/lib/lib.go +++ b/lib/lib.go @@ -5,22 +5,27 @@ import ( "crypto/rand" "encoding/hex" "encoding/json" + "errors" + "fmt" "io" "log" "math/big" "net" "net/http" "strconv" + "strings" "time" "github.com/BurntSushi/toml" - "github.com/somatic-labs/meteorite/types" + types "github.com/somatic-labs/meteorite/types" + + sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" ) -var client = &http.Client{ - Timeout: 1 * time.Second, // Adjusted timeout to 10 seconds +var httpClient = &http.Client{ + Timeout: 10 * time.Second, // Adjusted timeout to 10 seconds Transport: &http.Transport{ MaxIdleConns: 100, // Increased maximum idle connections MaxIdleConnsPerHost: 10, // Increased maximum idle connections per host @@ -29,33 +34,29 @@ var client = &http.Client{ }, } -func GetAccountInfo(address string, config types.Config) (uint64, uint64) { +func GetAccountInfo(address string, config types.Config) (seqint, accnum uint64, err error) { resp, err := HTTPGet(config.Nodes.API + "/cosmos/auth/v1beta1/accounts/" + address) if err != nil { - log.Printf("Failed to get initial sequence: %v", err) - return 0, 0 + return 0, 0, fmt.Errorf("failed to get initial sequence: %v", err) } var accountRes types.AccountResult err = json.Unmarshal(resp, &accountRes) if err != nil { - log.Printf("Failed to unmarshal account result: %v", err) - return 0, 0 + return 0, 0, fmt.Errorf("failed to unmarshal account result: %v", err) } - seqint, err := strconv.ParseInt(accountRes.Account.Sequence, 10, 64) + seqint, err = strconv.ParseUint(accountRes.Account.Sequence, 10, 64) if err != nil { - log.Printf("Failed to convert sequence to int: %v", err) - return 0, 0 + return 0, 0, fmt.Errorf("failed to convert sequence to int: %v", err) } - accnum, err := strconv.ParseInt(accountRes.Account.AccountNumber, 10, 64) + accnum, err = strconv.ParseUint(accountRes.Account.AccountNumber, 10, 64) if err != nil { - log.Printf("Failed to convert account number to int: %v", err) - return 0, 0 + return 0, 0, fmt.Errorf("failed to convert account number to int: %v", err) } - return uint64(seqint), uint64(accnum) + return seqint, accnum, nil } func GetChainID(nodeURL string) (string, error) { @@ -84,7 +85,7 @@ func HTTPGet(url string) ([]byte, error) { return nil, err } - resp, err := client.Do(req) + resp, err := httpClient.Do(req) if err != nil { netErr, ok := err.(net.Error) if ok && netErr.Timeout() { @@ -157,3 +158,114 @@ func GenerateRandomAccount() (sdk.AccAddress, error) { return accAddress, nil } + +func GetBalances(accounts []types.Account, config types.Config) (map[string]sdkmath.Int, error) { + balances := make(map[string]sdkmath.Int) + for _, account := range accounts { + balance, err := GetAccountBalance(account.Address, config) + if err != nil { + return nil, err + } + balances[account.Address] = balance + } + return balances, nil +} + +func GetAccountBalance(address string, config types.Config) (sdkmath.Int, error) { + resp, err := HTTPGet(config.Nodes.API + "/cosmos/bank/v1beta1/balances/" + address) + if err != nil { + return sdkmath.ZeroInt(), err + } + + var balanceRes types.BalanceResult + err = json.Unmarshal(resp, &balanceRes) + if err != nil { + return sdkmath.ZeroInt(), err + } + + for _, coin := range balanceRes.Balances { + if coin.Denom == config.Denom { + amount, ok := sdkmath.NewIntFromString(coin.Amount) + if !ok { + return sdkmath.ZeroInt(), errors.New("invalid coin amount") + } + return amount, nil + } + } + + // If no balance found for the denom, return zero balance + return sdkmath.ZeroInt(), fmt.Errorf("denomination %s not found in account balances", config.Denom) +} + +func CheckBalancesWithinThreshold(balances map[string]sdkmath.Int, threshold float64) bool { + if len(balances) == 0 { + return false + } + + var minBalance, maxBalance sdkmath.Int + first := true + + for _, balance := range balances { + if first { + minBalance = balance + maxBalance = balance + first = false + continue + } + + if balance.LT(minBalance) { + minBalance = balance + } + if balance.GT(maxBalance) { + maxBalance = balance + } + } + + // Skip check if all balances are below minimum threshold + minThreshold := sdkmath.NewInt(1000000) // 1 token assuming 6 decimals + if maxBalance.LT(minThreshold) { + return true + } + + // Calculate the difference as a percentage of the max balance + if maxBalance.IsZero() { + return minBalance.IsZero() + } + + diff := maxBalance.Sub(minBalance) + diffFloat := float64(diff.Int64()) + maxFloat := float64(maxBalance.Int64()) + + percentage := diffFloat / maxFloat + return percentage <= threshold +} + +// Function to extract the expected sequence number from the error message +func ExtractExpectedSequence(errMsg string) (uint64, error) { + // Parse the error message to extract the expected sequence number + // Example error message: + // "account sequence mismatch, expected 42, got 41: incorrect account sequence" + if !strings.Contains(errMsg, "account sequence mismatch") { + return 0, fmt.Errorf("unexpected error message format: %s", errMsg) + } + + index := strings.Index(errMsg, "expected ") + if index == -1 { + return 0, errors.New("expected sequence not found in error message") + } + + start := index + len("expected ") + rest := errMsg[start:] + parts := strings.SplitN(rest, ",", 2) + if len(parts) < 1 { + return 0, errors.New("failed to split expected sequence from error message") + } + + expectedSeqStr := strings.TrimSpace(parts[0]) + expectedSeq, err := strconv.ParseUint(expectedSeqStr, 10, 64) + if err != nil { + return 0, fmt.Errorf("failed to parse expected sequence number: %v", err) + } + + return expectedSeq, nil +} diff --git a/lib/privkey.go b/lib/privkey.go index 36cd0a3..9914cb1 100644 --- a/lib/privkey.go +++ b/lib/privkey.go @@ -10,30 +10,16 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -func GetPrivKey(config types.Config, mnemonic []byte) (cryptotypes.PrivKey, cryptotypes.PubKey, string) { - sdkConfig := sdk.GetConfig() - sdkConfig.SetBech32PrefixForAccount(config.Prefix, config.Prefix+"pub") - sdkConfig.SetBech32PrefixForValidator(config.Prefix+"valoper", config.Prefix+"valoperpub") - sdkConfig.SetBech32PrefixForConsensusNode(config.Prefix+"valcons", config.Prefix+"valconspub") - sdkConfig.Seal() - // Generate a Bip32 HD wallet for the mnemonic and a user supplied password - // create master key and derive first key for keyring - stringmem := string(mnemonic) - +func GetPrivKey(config types.Config, mnemonic []byte, position uint32) (cryptotypes.PrivKey, cryptotypes.PubKey, string) { algo := hd.Secp256k1 - // Derive the first key for keyring - // NOTE: this function had a bug, it was set to 118, then to 330. - // it is now configurable in the config file, to prevent this problem - derivedPriv, err := algo.Derive()(stringmem, "", fmt.Sprintf("m/44'/%d'/0'/0/0", config.Slip44)) + hdPath := fmt.Sprintf("m/44'/%d'/0'/0/%d", config.Slip44, position) + derivedPriv, err := algo.Derive()(string(mnemonic), "", hdPath) if err != nil { panic(err) } privKey := algo.Generate()(derivedPriv) - - // Create master private key from - pubKey := privKey.PubKey() addressbytes := sdk.AccAddress(pubKey.Address().Bytes()) @@ -42,7 +28,7 @@ func GetPrivKey(config types.Config, mnemonic []byte) (cryptotypes.PrivKey, cryp panic(err) } - fmt.Println("Address Ought to be", address) + fmt.Println("Derived Address at position", position, ":", address) return privKey, pubKey, address } diff --git a/main.go b/main.go index 367ad58..af1048a 100644 --- a/main.go +++ b/main.go @@ -1,18 +1,23 @@ package main import ( + "context" "errors" "fmt" "log" "os" - "strconv" - "strings" + "sync" "time" "github.com/BurntSushi/toml" "github.com/somatic-labs/meteorite/broadcast" + "github.com/somatic-labs/meteorite/client" "github.com/somatic-labs/meteorite/lib" "github.com/somatic-labs/meteorite/types" + + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" ) const ( @@ -30,169 +35,390 @@ func main() { if err != nil { log.Fatalf("Failed to read seed phrase: %v", err) } - privKey, pubKey, acctAddress := lib.GetPrivKey(config, mnemonic) - nodes := lib.LoadNodes() - if len(nodes) == 0 { - log.Fatal("No nodes available to send transactions") + // Set Bech32 prefixes and seal the configuration once + sdkConfig := sdk.GetConfig() + sdkConfig.SetBech32PrefixForAccount(config.Prefix, config.Prefix+"pub") + sdkConfig.SetBech32PrefixForValidator(config.Prefix+"valoper", config.Prefix+"valoperpub") + sdkConfig.SetBech32PrefixForConsensusNode(config.Prefix+"valcons", config.Prefix+"valconspub") + sdkConfig.Seal() + + positions := config.Positions + const MaxPositions = 100 // Adjust based on requirements + if positions <= 0 || positions > MaxPositions { + log.Fatalf("Number of positions must be between 1 and %d, got: %d", MaxPositions, positions) + } + fmt.Println("Positions", positions) + + var accounts []types.Account + for i := 0; i < int(positions); i++ { + position := uint32(i) + privKey, pubKey, acctAddress := lib.GetPrivKey(config, mnemonic, position) + if privKey == nil || pubKey == nil || len(acctAddress) == 0 { + log.Fatalf("Failed to generate keys for position %d", position) + } + accounts = append(accounts, types.Account{ + PrivKey: privKey, + PubKey: pubKey, + Address: acctAddress, + Position: position, + }) } - nodeURL := nodes[0] // Use only the first node - if nodeURL == "" { - log.Fatal("Node URL is empty. Please verify the nodes configuration.") + // **Print addresses and positions at startup** + fmt.Println("Addresses and Positions:") + for _, acct := range accounts { + fmt.Printf("Position %d: Address: %s\n", acct.Position, acct.Address) } - chainID, err := lib.GetChainID(nodeURL) + + // Get balances and ensure they are within 10% of each other + balances, err := lib.GetBalances(accounts, config) if err != nil { - log.Fatalf("Failed to get chain ID: %v", err) + log.Fatalf("Failed to get balances: %v", err) } - msgParams := config.MsgParams + // Print addresses and balances + fmt.Println("Wallets and Balances:") + for _, acct := range accounts { + balance, err := lib.GetAccountBalance(acct.Address, config) + if err != nil { + log.Printf("Failed to get balance for %s: %v", acct.Address, err) + continue + } + fmt.Printf("Position %d: Address: %s, Balance: %s %s\n", acct.Position, acct.Address, balance.String(), config.Denom) + } - // Get the account info - _, accNum := lib.GetAccountInfo(acctAddress, config) - if err != nil { - log.Fatalf("Failed to get account info: %v", err) + fmt.Println("balances", balances) + + if !lib.CheckBalancesWithinThreshold(balances, 0.10) { + fmt.Println("Account balances are not within 10% of each other. Adjusting balances...") + if err := handleBalanceAdjustment(accounts, balances, config); err != nil { + log.Fatalf("Failed to handle balance adjustment: %v", err) + } } - sequence := uint64(1) // Start from sequence number 1 + nodeURL := config.Nodes.RPC[0] // Use the first node - // Create a TransactionParams struct - txParams := types.TransactionParams{ - Config: config, - NodeURL: nodeURL, - ChainID: chainID, - Sequence: sequence, - AccNum: accNum, - PrivKey: privKey, - PubKey: pubKey, - AcctAddress: acctAddress, - MsgType: config.MsgType, - MsgParams: msgParams, + chainID, err := lib.GetChainID(nodeURL) + if err != nil { + log.Fatalf("Failed to get chain ID: %v", err) } - // ctx := context.Background() + msgParams := config.MsgParams - // _, err := client.NewGRPCClient(config.Nodes.GRPC) + // Initialize gRPC client + // grpcClient, err := client.NewGRPCClient(config.Nodes.GRPC) // if err != nil { // log.Fatalf("Failed to create gRPC client: %v", err) // } - // Call the broadcast loop - successfulTxns, failedTxns, responseCodes, _ := broadcastLoop(txParams, BatchSize) + var wg sync.WaitGroup + for _, account := range accounts { + wg.Add(1) + go func(acct types.Account) { + defer wg.Done() - // After the loop - fmt.Println("Successful transactions:", successfulTxns) - fmt.Println("Failed transactions:", failedTxns) - totalTxns := successfulTxns + failedTxns - fmt.Println("Response code breakdown:") - for code, count := range responseCodes { - percentage := float64(count) / float64(totalTxns) * 100 - fmt.Printf("Code %d: %d (%.2f%%)\n", code, count, percentage) + // Get account info + sequence, accNum, err := lib.GetAccountInfo(acct.Address, config) + if err != nil { + log.Printf("Failed to get account info for %s: %v", acct.Address, err) + return + } + + txParams := types.TransactionParams{ + Config: config, + NodeURL: nodeURL, + ChainID: chainID, + Sequence: sequence, + AccNum: accNum, + PrivKey: acct.PrivKey, + PubKey: acct.PubKey, + AcctAddress: acct.Address, + MsgType: config.MsgType, + MsgParams: msgParams, + } + + // Broadcast transactions + successfulTxns, failedTxns, responseCodes, _ := broadcast.Loop(txParams, BatchSize, int(acct.Position)) + + fmt.Printf("Account %s: Successful transactions: %d, Failed transactions: %d\n", acct.Address, successfulTxns, failedTxns) + fmt.Println("Response code breakdown:") + for code, count := range responseCodes { + percentage := float64(count) / float64(successfulTxns+failedTxns) * 100 + fmt.Printf("Code %d: %d (%.2f%%)\n", code, count, percentage) + } + }(account) } + + wg.Wait() } -// broadcastLoop handles the main transaction broadcasting logic -func broadcastLoop( - txParams types.TransactionParams, - batchSize int, -) (successfulTxns, failedTxns int, responseCodes map[uint32]int, updatedSequence uint64) { - successfulTxns = 0 - failedTxns = 0 - responseCodes = make(map[uint32]int) - sequence := txParams.Sequence - - for i := 0; i < batchSize; i++ { - currentSequence := sequence - - fmt.Println("FROM LOOP, currentSequence", currentSequence) - fmt.Println("FROM LOOP, accNum", txParams.AccNum) - fmt.Println("FROM LOOP, chainID", txParams.ChainID) - - start := time.Now() - resp, _, err := broadcast.SendTransactionViaRPC( - txParams, - currentSequence, - ) - elapsed := time.Since(start) - - fmt.Println("FROM MAIN, err", err) - fmt.Println("FROM MAIN, resp", resp.Code) - - if err == nil { - fmt.Printf("%s Transaction succeeded, sequence: %d, time: %v\n", - time.Now().Format("15:04:05"), currentSequence, elapsed) - successfulTxns++ - responseCodes[resp.Code]++ - sequence++ // Increment sequence for next transaction - continue +// adjustBalances transfers funds between accounts to balance their balances within the threshold +func adjustBalances(accounts []types.Account, balances map[string]sdkmath.Int, config types.Config) error { + if len(accounts) == 0 { + return errors.New("no accounts provided for balance adjustment") + } + + // Calculate the total balance + totalBalance := sdkmath.ZeroInt() + for _, balance := range balances { + totalBalance = totalBalance.Add(balance) + } + fmt.Printf("Total Balance across all accounts: %s %s\n", totalBalance.String(), config.Denom) + + if totalBalance.IsZero() { + return errors.New("total balance is zero, nothing to adjust") + } + + numAccounts := sdkmath.NewInt(int64(len(accounts))) + averageBalance := totalBalance.Quo(numAccounts) + fmt.Printf("Number of Accounts: %d, Average Balance per account: %s %s\n", numAccounts.Int64(), averageBalance.String(), config.Denom) + + // Define minimum transfer amount to avoid dust transfers + minTransfer := sdkmath.NewInt(1000000) // Adjust based on your token's decimal places + fmt.Printf("Minimum Transfer Amount to avoid dust: %s %s\n", minTransfer.String(), config.Denom) + + // Create a slice to track balances that need to send or receive funds + type balanceAdjustment struct { + Account types.Account + Amount sdkmath.Int // Positive if needs to receive, negative if needs to send + } + var adjustments []balanceAdjustment + + threshold := averageBalance.MulRaw(10).QuoRaw(100) // threshold = averageBalance * 10 / 100 + fmt.Printf("Balance Threshold for adjustments (10%% of average balance): %s %s\n", threshold.String(), config.Denom) + + for _, acct := range accounts { + currentBalance := balances[acct.Address] + difference := averageBalance.Sub(currentBalance) + + fmt.Printf("Account %s - Current Balance: %s %s, Difference from average: %s %s\n", + acct.Address, currentBalance.String(), config.Denom, difference.String(), config.Denom) + + // Only consider adjustments exceeding the threshold and minimum transfer amount + if difference.Abs().GT(threshold) && difference.Abs().GT(minTransfer) { + adjustments = append(adjustments, balanceAdjustment{ + Account: acct, + Amount: difference, + }) + fmt.Printf("-> Account %s requires adjustment of %s %s\n", acct.Address, difference.String(), config.Denom) + } else { + fmt.Printf("-> Account %s is within balance threshold, no adjustment needed\n", acct.Address) } + } - fmt.Printf("%s Error: %v\n", time.Now().Format("15:04:05.000"), err) - fmt.Println("FROM MAIN, resp.Code", resp.Code) + // Separate adjustments into senders (negative amounts) and receivers (positive amounts) + var senders, receivers []balanceAdjustment + for _, adj := range adjustments { + if adj.Amount.IsNegative() { + // Check if the account has enough balance to send + accountBalance := balances[adj.Account.Address] + fmt.Printf("Sender Account %s - Balance: %s %s, Surplus: %s %s\n", + adj.Account.Address, accountBalance.String(), config.Denom, adj.Amount.Abs().String(), config.Denom) + + if accountBalance.GT(sdkmath.ZeroInt()) { + senders = append(senders, adj) + } else { + fmt.Printf("-> Account %s has zero balance, cannot send funds.\n", adj.Account.Address) + } + } else if adj.Amount.IsPositive() { + fmt.Printf("Receiver Account %s - Needs: %s %s\n", + adj.Account.Address, adj.Amount.String(), config.Denom) + receivers = append(receivers, adj) + } + } - if resp.Code == 32 { - // Extract the expected sequence number from the error message - expectedSeq, parseErr := extractExpectedSequence(err.Error()) - if parseErr != nil { - fmt.Printf("%s Failed to parse expected sequence: %v\n", time.Now().Format("15:04:05.000"), parseErr) - failedTxns++ + // Perform transfers from senders to receivers + for _, sender := range senders { + // The total amount the sender needs to transfer (their surplus) + amountToSend := sender.Amount.Abs() + fmt.Printf("\nStarting transfers from Sender Account %s - Total Surplus to send: %s %s\n", + sender.Account.Address, amountToSend.String(), config.Denom) + + // Iterate over the receivers who need funds + for i := range receivers { + receiver := &receivers[i] + + // Check if the receiver still needs funds + if receiver.Amount.GT(sdkmath.ZeroInt()) { + // Determine the amount to transfer: + // It's the minimum of what the sender can send and what the receiver needs + transferAmount := sdkmath.MinInt(amountToSend, receiver.Amount) + + fmt.Printf("Transferring %s %s from %s to %s\n", + transferAmount.String(), config.Denom, sender.Account.Address, receiver.Account.Address) + + // Transfer funds from the sender to the receiver + err := TransferFunds(sender.Account, receiver.Account.Address, transferAmount, config) + if err != nil { + return fmt.Errorf("failed to transfer funds from %s to %s: %v", + sender.Account.Address, receiver.Account.Address, err) + } + + fmt.Printf("-> Successfully transferred %s %s from %s to %s\n", + transferAmount.String(), config.Denom, sender.Account.Address, receiver.Account.Address) + + // Update the sender's remaining amount to send + amountToSend = amountToSend.Sub(transferAmount) + fmt.Printf("Sender %s remaining surplus to send: %s %s\n", + sender.Account.Address, amountToSend.String(), config.Denom) + + // Update the receiver's remaining amount to receive + receiver.Amount = receiver.Amount.Sub(transferAmount) + fmt.Printf("Receiver %s remaining amount needed: %s %s\n", + receiver.Account.Address, receiver.Amount.String(), config.Denom) + + // If the sender has sent all their surplus, move to the next sender + if amountToSend.IsZero() { + fmt.Printf("Sender %s has sent all surplus funds.\n", sender.Account.Address) + break + } + } else { + fmt.Printf("Receiver %s no longer needs funds.\n", receiver.Account.Address) + } + } + } + + fmt.Println("\nBalance adjustment complete.") + return nil +} + +func TransferFunds(sender types.Account, receiverAddress string, amount sdkmath.Int, config types.Config) error { + fmt.Printf("\n=== Starting Transfer ===\n") + fmt.Printf("Sender Address: %s\n", sender.Address) + fmt.Printf("Receiver Address: %s\n", receiverAddress) + fmt.Printf("Amount: %s %s\n", amount.String(), config.Denom) + + if sender.PrivKey == nil { + return errors.New("sender private key is nil") + } + if sender.PubKey == nil { + return errors.New("sender public key is nil") + } + + // Get the sender's account info + sequence, accnum, err := lib.GetAccountInfo(sender.Address, config) + if err != nil { + return fmt.Errorf("failed to get account info for sender %s: %v", sender.Address, err) + } + + nodeURL := config.Nodes.RPC[0] + + grpcClient, err := client.NewGRPCClient(config.Nodes.GRPC) + if err != nil { + return fmt.Errorf("failed to create gRPC client: %v", err) + } + + txParams := types.TransactionParams{ + Config: config, + NodeURL: nodeURL, + ChainID: config.Chain, + Sequence: sequence, + AccNum: accnum, + PrivKey: sender.PrivKey, + PubKey: sender.PubKey, + AcctAddress: sender.Address, + MsgType: "bank_send", + MsgParams: types.MsgParams{ + FromAddress: sender.Address, + ToAddress: receiverAddress, + Amount: amount.Int64(), + Denom: config.Denom, + }, + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + maxRetries := 3 + for attempt := 0; attempt < maxRetries; attempt++ { + fmt.Printf("Attempt %d to send transaction with sequence %d\n", attempt+1, sequence) + + resp, _, err := broadcast.SendTransactionViaGRPC(ctx, txParams, sequence, grpcClient) + if err != nil { + fmt.Printf("Transaction failed: %v\n", err) + + // Check if the error is a sequence mismatch error (code 32) + if resp != nil && resp.Code == 32 { + expectedSeq, parseErr := lib.ExtractExpectedSequence(resp.RawLog) + if parseErr != nil { + return fmt.Errorf("failed to parse expected sequence: %v", parseErr) + } + + // Update sequence and retry + sequence = expectedSeq + txParams.Sequence = sequence + fmt.Printf("Sequence mismatch detected. Updating sequence to %d and retrying...\n", sequence) continue } - sequence = expectedSeq - fmt.Printf("%s Set sequence to expected value %d due to mismatch\n", - time.Now().Format("15:04:05"), sequence) + return fmt.Errorf("failed to send transaction: %v", err) + } - // Re-send the transaction with the correct sequence - start = time.Now() - resp, _, err = broadcast.SendTransactionViaRPC( - txParams, - sequence, - ) - elapsed = time.Since(start) + if resp.Code != 0 { + fmt.Printf("Transaction failed with code %d: %s\n", resp.Code, resp.RawLog) - if err != nil { - fmt.Printf("%s Error after adjusting sequence: %v\n", time.Now().Format("15:04:05.000"), err) - failedTxns++ + // Check for sequence mismatch error + if resp.Code == 32 { + expectedSeq, parseErr := lib.ExtractExpectedSequence(resp.RawLog) + if parseErr != nil { + return fmt.Errorf("failed to parse expected sequence: %v", parseErr) + } + + // Update sequence and retry + sequence = expectedSeq + txParams.Sequence = sequence + fmt.Printf("Sequence mismatch detected. Updating sequence to %d and retrying...\n", sequence) continue } - fmt.Printf("%s Transaction succeeded after adjusting sequence, sequence: %d, time: %v\n", - time.Now().Format("15:04:05"), sequence, elapsed) - successfulTxns++ - responseCodes[resp.Code]++ - sequence++ // Increment sequence for next transaction - continue + return fmt.Errorf("transaction failed with code %d: %s", resp.Code, resp.RawLog) } - failedTxns++ + fmt.Printf("-> Successfully transferred %s %s from %s to %s\n", + amount.String(), config.Denom, sender.Address, receiverAddress) + return nil } - updatedSequence = sequence - return successfulTxns, failedTxns, responseCodes, updatedSequence + + return fmt.Errorf("failed to send transaction after %d attempts", maxRetries) } -// Function to extract the expected sequence number from the error message -func extractExpectedSequence(errMsg string) (uint64, error) { - // Parse the error message to extract the expected sequence number - // Example error message: - // "account sequence mismatch, expected 42, got 41: incorrect account sequence" - index := strings.Index(errMsg, "expected ") - if index == -1 { - return 0, errors.New("expected sequence not found in error message") +// Add this new function +func handleBalanceAdjustment(accounts []types.Account, balances map[string]sdkmath.Int, config types.Config) error { + if err := adjustBalances(accounts, balances, config); err != nil { + return fmt.Errorf("failed to adjust balances: %v", err) } - start := index + len("expected ") - rest := errMsg[start:] - parts := strings.SplitN(rest, ",", 2) - if len(parts) < 1 { - return 0, errors.New("failed to split expected sequence from error message") + balances, err := lib.GetBalances(accounts, config) + if err != nil { + return fmt.Errorf("failed to get balances after adjustment: %v", err) } - expectedSeqStr := strings.TrimSpace(parts[0]) - expectedSeq, err := strconv.ParseUint(expectedSeqStr, 10, 64) - if err != nil { - return 0, fmt.Errorf("failed to parse expected sequence number: %v", err) + if !shouldProceedWithBalances(balances) { + return errors.New("account balances are still not within threshold after adjustment") + } + + return nil +} + +func shouldProceedWithBalances(balances map[string]sdkmath.Int) bool { + if lib.CheckBalancesWithinThreshold(balances, 0.15) { + fmt.Println("Balances successfully adjusted within acceptable range") + return true + } + + var maxBalance sdkmath.Int + for _, balance := range balances { + if balance.GT(maxBalance) { + maxBalance = balance + } + } + + minSignificantBalance := sdkmath.NewInt(1000000) + if maxBalance.LT(minSignificantBalance) { + fmt.Println("Remaining balance differences are below minimum threshold, proceeding") + return true } - return expectedSeq, nil + return false } diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..d473c44 --- /dev/null +++ b/main_test.go @@ -0,0 +1,391 @@ +package main + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/cosmos/go-bip39" + "github.com/cosmos/ibc-go/modules/apps/callbacks/testing/simapp/params" + "github.com/somatic-labs/meteorite/broadcast" + "github.com/somatic-labs/meteorite/lib" + "github.com/somatic-labs/meteorite/types" + + sdkmath "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func TestExtractExpectedSequence(t *testing.T) { + tests := []struct { + name string + errMsg string + want uint64 + wantErr bool + }{ + { + name: "valid error message", + errMsg: "account sequence mismatch, expected 42, got 41: incorrect account sequence", + want: 42, + wantErr: false, + }, + { + name: "missing expected keyword", + errMsg: "account sequence mismatch, sequence 42, got 41", + want: 0, + wantErr: true, + }, + { + name: "invalid sequence number", + errMsg: "account sequence mismatch, expected abc, got 41", + want: 0, + wantErr: true, + }, + { + name: "empty error message", + errMsg: "", + want: 0, + wantErr: true, + }, + { + name: "large sequence number", + errMsg: "account sequence mismatch, expected 18446744073709551615, got 41", + want: 18446744073709551615, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := lib.ExtractExpectedSequence(tt.errMsg) + if (err != nil) != tt.wantErr { + t.Errorf("extractExpectedSequence() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("extractExpectedSequence() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestTransferFunds(t *testing.T) { + // Generate a random mnemonic + entropy, err := bip39.NewEntropy(256) + if err != nil { + t.Fatalf("Failed to generate entropy: %v", err) + } + mnemonic, err := bip39.NewMnemonic(entropy) + if err != nil { + t.Fatalf("Failed to generate mnemonic: %v", err) + } + + // Create key from mnemonic + seed := bip39.NewSeed(mnemonic, "") + master, ch := hd.ComputeMastersFromSeed(seed) + path := hd.NewFundraiserParams(0, sdk.CoinType, 0).String() + privKey, err := hd.DerivePrivateKeyForPath(master, ch, path) + if err != nil { + t.Fatalf("Failed to derive private key: %v", err) + } + + secp256k1PrivKey := &secp256k1.PrivKey{Key: privKey} + pubKey := secp256k1PrivKey.PubKey() + + tests := []struct { + name string + sender types.Account + receiver string + amount sdkmath.Int + config types.Config + expectedError string + }{ + { + name: "invalid prefix", + sender: types.Account{ + PrivKey: secp256k1PrivKey, + PubKey: pubKey, + Address: "cosmos1uqrar205hjv4s8832kwj8e6xhwvk4x0eqml043", + Position: 0, + }, + receiver: "cosmos1paefpxvjvmmq03gvsfjzwut0zap7z5nq8r99sf", + amount: sdkmath.NewInt(3123890412), + config: types.Config{ + Chain: "cosmoshub-4", + Prefix: "cosmos", + Denom: "uatom", + Nodes: types.NodesConfig{ + RPC: []string{"http://127.0.0.1:26657"}, + API: "http://localhost:1317", + GRPC: "localhost:9090", + }, + }, + expectedError: "failed to get account info", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := TransferFunds(tt.sender, tt.receiver, tt.amount, tt.config) + if err == nil { + t.Error("expected error but got none") + return + } + if !strings.Contains(err.Error(), tt.expectedError) { + t.Errorf("expected error containing %q, got %q", tt.expectedError, err.Error()) + } + }) + } +} + +func TestAdjustBalancesWithSeedPhrase(t *testing.T) { + // Create a temporary seed phrase file + mnemonic := []byte("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about") + tmpfile, err := os.CreateTemp("", "seedphrase") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tmpfile.Name()) + + if _, err := tmpfile.Write(mnemonic); err != nil { + t.Fatalf("Failed to write to temp file: %v", err) + } + if err := tmpfile.Close(); err != nil { + t.Fatalf("Failed to close temp file: %v", err) + } + + // Set up test config + config := types.Config{ + Chain: "test-chain", + Prefix: "cosmos", + Denom: "uatom", + Slip44: 118, + Nodes: types.NodesConfig{ + RPC: []string{"http://localhost:26657"}, + API: "http://localhost:1317", + }, + } + + // Create test accounts from seed phrase + var accounts []types.Account + for i := 0; i < 3; i++ { // Create 3 test accounts + position := uint32(i) + privKey, pubKey, acctAddress := lib.GetPrivKey(config, mnemonic, position) + accounts = append(accounts, types.Account{ + PrivKey: privKey, + PubKey: pubKey, + Address: acctAddress, + Position: position, + }) + } + + // Set up mock balances where only the 0th position is funded + balances := map[string]sdkmath.Int{ + accounts[0].Address: sdkmath.NewInt(1000000), + accounts[1].Address: sdkmath.ZeroInt(), + accounts[2].Address: sdkmath.ZeroInt(), + } + + // Create a test server to mock the API responses + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + // Mock successful transaction response + fmt.Fprintln(w, `{"height":"1","txhash":"hash","code":0}`) + })) + defer ts.Close() + config.Nodes.API = ts.URL + config.Nodes.RPC = []string{ts.URL} + + // Run adjustBalances + err = adjustBalances(accounts, balances, config) + if err != nil { + t.Errorf("adjustBalances() error = %v", err) + } + + // Verify that balances were attempted to be adjusted + // Note: In a real scenario, you'd want to verify the actual balance changes, + // but since we're using a mock server, we're just verifying the function ran without error +} + +func TestAdjustBalances(t *testing.T) { + tests := []struct { + name string + accounts []types.Account + balances map[string]sdkmath.Int + config types.Config + wantErr bool + }{ + { + name: "empty accounts list", + accounts: []types.Account{}, + balances: map[string]sdkmath.Int{}, + config: types.Config{ + Denom: "uom", + }, + wantErr: true, + }, + { + name: "zero total balance", + accounts: []types.Account{ + {Address: "cosmos1test1"}, + {Address: "cosmos1test2"}, + }, + balances: map[string]sdkmath.Int{ + "cosmos1test1": sdkmath.ZeroInt(), + "cosmos1test2": sdkmath.ZeroInt(), + }, + config: types.Config{ + Denom: "uom", + }, + wantErr: true, + }, + { + name: "uneven balances need adjustment", + accounts: []types.Account{ + {Address: "cosmos1test1"}, + {Address: "cosmos1test2"}, + }, + balances: map[string]sdkmath.Int{ + "cosmos1test1": sdkmath.NewInt(1000000), + "cosmos1test2": sdkmath.NewInt(0), + }, + config: types.Config{ + Denom: "uom", + Nodes: types.NodesConfig{ + RPC: []string{"http://localhost:26657"}, + API: "http://localhost:1317", + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := adjustBalances(tt.accounts, tt.balances, tt.config) + if (err != nil) != tt.wantErr { + t.Errorf("adjustBalances() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestBuildAndSignTransaction(t *testing.T) { + tests := []struct { + name string + txParams types.TransactionParams + sequence uint64 + wantErr bool + errorMatch string + }{ + { + name: "invalid message type", + txParams: types.TransactionParams{ + MsgType: "invalid_type", + Config: types.Config{ + Denom: "uatom", + }, + }, + sequence: 0, + wantErr: true, + errorMatch: "unsupported message type", + }, + { + name: "missing private key", + txParams: types.TransactionParams{ + MsgType: "bank_send", + Config: types.Config{ + Denom: "uatom", + }, + PrivKey: nil, + }, + sequence: 0, + wantErr: true, + errorMatch: "invalid from address: empty address string is not allowed", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + encodingConfig := params.MakeTestEncodingConfig() + _, err := broadcast.BuildAndSignTransaction(ctx, tt.txParams, tt.sequence, encodingConfig) + if (err != nil) != tt.wantErr { + t.Errorf("BuildAndSignTransaction() error = %v, wantErr %v", err, tt.wantErr) + return + } + if err != nil && !strings.Contains(err.Error(), tt.errorMatch) { + t.Errorf("BuildAndSignTransaction() error = %v, want error containing %v", err, tt.errorMatch) + } + }) + } +} + +func TestGetAccountInfo(t *testing.T) { + // Create a test server to mock the API responses + tests := []struct { + name string + address string + mockResp string + wantSeq uint64 + wantAccNum uint64 + wantErr bool + }{ + { + name: "valid response", + address: "cosmos1test1", + mockResp: `{ + "account": { + "sequence": "42", + "account_number": "26" + } + }`, + wantSeq: 42, + wantAccNum: 26, + wantErr: false, + }, + { + name: "invalid sequence", + address: "cosmos1test2", + mockResp: `{ + "account": { + "sequence": "invalid", + "account_number": "26" + } + }`, + wantSeq: 0, + wantAccNum: 0, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + fmt.Fprintln(w, tt.mockResp) + })) + defer ts.Close() + + config := types.Config{ + Nodes: types.NodesConfig{ + API: ts.URL, + }, + } + + seq, accNum, err := lib.GetAccountInfo(tt.address, config) + if (err != nil) != tt.wantErr { + t.Errorf("GetAccountInfo() error = %v, wantErr %v", err, tt.wantErr) + return + } + if seq != tt.wantSeq || accNum != tt.wantAccNum { + t.Errorf("GetAccountInfo() = (%v, %v), want (%v, %v)", + seq, accNum, tt.wantSeq, tt.wantAccNum) + } + }) + } +} diff --git a/nodes.toml b/nodes.toml index bc34fc7..0f12087 100644 --- a/nodes.toml +++ b/nodes.toml @@ -1,11 +1,11 @@ # configurations/contract_store_spam/nodes.toml -chain = "mantra" +chain = "mantra-canary-net-1" channel = "channel-1" denom = "uom" prefix = "mantra" gas_per_byte = 100 -base_gas = 160000 +base_gas = 200000 ibc_memo = "Contract store spam test" memo = "Storing compiled contract with randomized memo" ibc_memo_repeat = 10 @@ -14,6 +14,7 @@ rand_max = 30000 revision_number = 4 timeout_height = 21720608 slip44 = 118 +positions = 50 # Number of positions to use from the seed phrase broadcast_mode = "grpc" # or "rpc" @@ -24,6 +25,7 @@ msg_type = "bank_send" # if this field is left blank as "", meteorite will send to random accounts endlessly to_address = "" +amount = 1 #[msg_params] @@ -44,5 +46,5 @@ precision = 3 [nodes] rpc = ["http://127.0.0.1:26657"] -api = "https://api.canary.mantrachain.dev:443" +api = "http://localhost:1317" grpc = "localhost:9090" diff --git a/types/types.go b/types/types.go index b3e752f..1b00810 100644 --- a/types/types.go +++ b/types/types.go @@ -1,5 +1,7 @@ package types +import cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + type Header struct { Height string `json:"height"` } @@ -135,10 +137,13 @@ type Config struct { Gas GasConfig `toml:"gas"` Nodes NodesConfig `toml:"nodes"` BroadcastMode string `toml:"broadcast_mode"` + Positions uint `toml:"positions"` } type MsgParams struct { + FromAddress string `toml:"from_address"` Amount int64 `toml:"amount"` + Denom string `toml:"denom"` Receiver string `toml:"receiver"` ToAddress string `toml:"to_address"` WasmFile string `toml:"wasm_file"` @@ -176,3 +181,28 @@ type NodeStatusResult struct { type NodeStatusResponse struct { Result NodeStatusResult `json:"result"` } + +type Account struct { + PrivKey cryptotypes.PrivKey + PubKey cryptotypes.PubKey + Address string + Position uint32 +} + +// BalanceResult represents the response from the bank balances query. +type BalanceResult struct { + Balances []Coin `json:"balances"` + Pagination Pagination `json:"pagination"` +} + +// Coin represents a token balance. +type Coin struct { + Denom string `json:"denom"` + Amount string `json:"amount"` +} + +// Pagination holds pagination information. +type Pagination struct { + NextKey string `json:"next_key"` + Total string `json:"total"` +}