diff --git a/command/loadtest/load_test_run.go b/command/loadtest/load_test_run.go index 179bdcd8b6..ba30bd3faa 100644 --- a/command/loadtest/load_test_run.go +++ b/command/loadtest/load_test_run.go @@ -105,6 +105,13 @@ func setFlags(cmd *cobra.Command) { "waits for tx pool to empty before collecting results", ) + cmd.Flags().IntVar( + ¶ms.batchSize, + batchSizeFlag, + 1, + "size of a batch of transactions to send to rpc node", + ) + _ = cmd.MarkFlagRequired(mnemonicFlag) _ = cmd.MarkFlagRequired(loadTestTypeFlag) } @@ -124,6 +131,7 @@ func runCommand(cmd *cobra.Command, _ []string) { TxPoolTimeout: params.txPoolTimeout, VUs: params.vus, TxsPerUser: params.txsPerUser, + BatchSize: params.batchSize, DynamicTxs: params.dynamicTxs, ResultsToJSON: params.toJSON, WaitForTxPoolToEmpty: params.waitForTxPoolToEmpty, diff --git a/command/loadtest/params.go b/command/loadtest/params.go index 4f49cd6a15..ad18abf320 100644 --- a/command/loadtest/params.go +++ b/command/loadtest/params.go @@ -18,6 +18,7 @@ const ( vusFlag = "vus" txsPerUserFlag = "txs-per-user" dynamicTxsFlag = "dynamic" + batchSizeFlag = "batch-size" saveToJSONFlag = "to-json" waitForTxPoolToEmptyFlag = "wait-txpool" @@ -29,6 +30,7 @@ var ( errUnsupportedLoadTestType = errors.New("unsupported load test type") errInvalidVUs = errors.New("vus must be greater than 0") errInvalidTxsPerUser = errors.New("txs-per-user must be greater than 0") + errInvalidBatchSize = errors.New("batch-size must be greater than 0 and less or equal to txs-per-user") ) type loadTestParams struct { @@ -42,6 +44,7 @@ type loadTestParams struct { vus int txsPerUser int + batchSize int dynamicTxs bool toJSON bool @@ -69,5 +72,9 @@ func (ltp *loadTestParams) validateFlags() error { return errInvalidTxsPerUser } + if ltp.batchSize < 1 || ltp.batchSize > ltp.txsPerUser { + return errInvalidBatchSize + } + return nil } diff --git a/loadtest/runner/base_load_test_runner.go b/loadtest/runner/base_load_test_runner.go index 05d8a8b696..885e562cf7 100644 --- a/loadtest/runner/base_load_test_runner.go +++ b/loadtest/runner/base_load_test_runner.go @@ -2,6 +2,7 @@ package runner import ( "context" + "encoding/hex" "encoding/json" "fmt" "math" @@ -48,6 +49,8 @@ type BaseLoadTestRunner struct { resultsCollectedCh chan *stats done chan error + + batchSender *TransactionBatchSender } // NewBaseLoadTestRunner creates a new instance of BaseLoadTestRunner with the provided LoadTestConfig. @@ -82,6 +85,7 @@ func NewBaseLoadTestRunner(cfg LoadTestConfig) (*BaseLoadTestRunner, error) { client: client, resultsCollectedCh: make(chan *stats), done: make(chan error), + batchSender: newTransactionBatchSender(cfg.JSONRPCUrl), }, nil } @@ -667,8 +671,8 @@ func (r *BaseLoadTestRunner) saveResultsToJSONFile( // The transaction hashes are appended to the allTxnHashes slice. // Finally, the function prints the time taken to send the transactions // and returns the transaction hashes and nil error. -func (r *BaseLoadTestRunner) sendTransactions( - sendFn func(*account, *big.Int, *progressbar.ProgressBar) ([]types.Hash, []error, error)) ([]types.Hash, error) { +func (r *BaseLoadTestRunner) sendTransactions(createTxnFn func(*account, *feeData, *big.Int) *types.Transaction, +) ([]types.Hash, error) { fmt.Println("=============================================================") chainID, err := r.client.ChainID() @@ -691,6 +695,11 @@ func (r *BaseLoadTestRunner) sendTransactions( g, ctx := errgroup.WithContext(context.Background()) + sendFn := r.sendTransactionsForUser + if r.cfg.BatchSize > 1 { + sendFn = r.sendTransactionsForUserInBatches + } + for _, vu := range r.vus { vu := vu @@ -700,7 +709,7 @@ func (r *BaseLoadTestRunner) sendTransactions( return ctx.Err() default: - txnHashes, sendErrors, err := sendFn(vu, chainID, bar) + txnHashes, sendErrors, err := sendFn(vu, chainID, bar, createTxnFn) if err != nil { return err } @@ -728,6 +737,126 @@ func (r *BaseLoadTestRunner) sendTransactions( return allTxnHashes, nil } +// sendTransactionsForUser sends ERC20 token transactions for a given user account. +// It takes an account pointer and a chainID as input parameters. +// It returns a slice of transaction hashes and an error if any. +func (r *BaseLoadTestRunner) sendTransactionsForUser(account *account, chainID *big.Int, + bar *progressbar.ProgressBar, createTxnFn func(*account, *feeData, *big.Int) *types.Transaction, +) ([]types.Hash, []error, error) { + txRelayer, err := txrelayer.NewTxRelayer( + txrelayer.WithClient(r.client), + txrelayer.WithChainID(chainID), + txrelayer.WithCollectTxnHashes(), + txrelayer.WithNoWaiting(), + txrelayer.WithEstimateGasFallback(), + txrelayer.WithoutNonceGet(), + ) + if err != nil { + return nil, nil, err + } + + feeData, err := getFeeData(r.client, r.cfg.DynamicTxs) + if err != nil { + return nil, nil, err + } + + sendErrs := make([]error, 0) + checkFeeDataNum := r.cfg.TxsPerUser / 5 + + for i := 0; i < r.cfg.TxsPerUser; i++ { + if i%checkFeeDataNum == 0 { + feeData, err = getFeeData(r.client, r.cfg.DynamicTxs) + if err != nil { + return nil, nil, err + } + } + + _, err = txRelayer.SendTransaction(createTxnFn(account, feeData, chainID), account.key) + if err != nil { + sendErrs = append(sendErrs, err) + } + + account.nonce++ + _ = bar.Add(1) + } + + return txRelayer.GetTxnHashes(), sendErrs, nil +} + +// sendTransactionsForUserInBatches sends user transactions in batches to the rpc node +func (r *BaseLoadTestRunner) sendTransactionsForUserInBatches(account *account, chainID *big.Int, + bar *progressbar.ProgressBar, createTxnFn func(*account, *feeData, *big.Int) *types.Transaction, +) ([]types.Hash, []error, error) { + signer := crypto.NewLondonSigner(chainID.Uint64()) + + numOfBatches := int(math.Ceil(float64(r.cfg.TxsPerUser) / float64(r.cfg.BatchSize))) + txHashes := make([]types.Hash, 0, r.cfg.TxsPerUser) + sendErrs := make([]error, 0) + totalTxs := 0 + + var gas uint64 + + feeData, err := getFeeData(r.client, r.cfg.DynamicTxs) + if err != nil { + return nil, nil, err + } + + txnExample := createTxnFn(account, feeData, chainID) + if txnExample.Gas() == 0 { + // estimate gas initially + gasLimit, err := r.client.EstimateGas(txrelayer.ConvertTxnToCallMsg(txnExample)) + if err != nil { + gasLimit = txrelayer.DefaultGasLimit + } + + gas = gasLimit * 2 // double it just in case + } else { + gas = txnExample.Gas() + } + + for i := 0; i < numOfBatches; i++ { + batchTxs := make([]string, 0, r.cfg.BatchSize) + + feeData, err := getFeeData(r.client, r.cfg.DynamicTxs) + if err != nil { + return nil, nil, err + } + + for j := 0; j < r.cfg.BatchSize; j++ { + if totalTxs >= r.cfg.TxsPerUser { + break + } + + txn := createTxnFn(account, feeData, chainID) + txn.SetGas(gas) + + signedTxn, err := signer.SignTxWithCallback(txn, + func(hash types.Hash) (sig []byte, err error) { + return account.key.Sign(hash.Bytes()) + }) + if err != nil { + sendErrs = append(sendErrs, err) + + continue + } + + batchTxs = append(batchTxs, "0x"+hex.EncodeToString(signedTxn.MarshalRLP())) + account.nonce++ + totalTxs++ + } + + hashes, err := r.batchSender.SendBatch(batchTxs) + if err != nil { + return nil, nil, err + } + + txHashes = append(txHashes, hashes...) + _ = bar.Add(len(batchTxs)) + } + + return txHashes, sendErrs, nil +} + // getFeeData retrieves fee data based on the provided JSON-RPC Ethereum client and dynamicTxs flag. // If dynamicTxs is true, it calculates the gasTipCap and gasFeeCap based on the MaxPriorityFeePerGas, // FeeHistory, and BaseFee values obtained from the client. If dynamicTxs is false, it calculates the diff --git a/loadtest/runner/eoa_runner.go b/loadtest/runner/eoa_runner.go index 2ae0bc1cc7..7868847eb6 100644 --- a/loadtest/runner/eoa_runner.go +++ b/loadtest/runner/eoa_runner.go @@ -4,9 +4,7 @@ import ( "fmt" "math/big" - "github.com/0xPolygon/polygon-edge/txrelayer" "github.com/0xPolygon/polygon-edge/types" - "github.com/schollz/progressbar/v3" "github.com/umbracle/ethgo" ) @@ -50,7 +48,7 @@ func (e *EOARunner) Run() error { go e.waitForReceiptsParallel() go e.calculateResultsParallel() - _, err := e.sendTransactions() + _, err := e.sendTransactions(e.createEOATransaction) if err != nil { return err } @@ -58,7 +56,7 @@ func (e *EOARunner) Run() error { return <-e.done } - txHashes, err := e.sendTransactions() + txHashes, err := e.sendTransactions(e.createEOATransaction) if err != nil { return err } @@ -70,75 +68,28 @@ func (e *EOARunner) Run() error { return e.calculateResults(e.waitForReceipts(txHashes)) } -// sendTransactions sends transactions for the load test. -func (e *EOARunner) sendTransactions() ([]types.Hash, error) { - return e.BaseLoadTestRunner.sendTransactions(e.sendTransactionsForUser) -} - -// sendTransactionsForUser sends multiple transactions for a user account on a specific chain. -// It uses the provided client and chain ID to send transactions using either dynamic or legacy fee models. -// For each transaction, it increments the account's nonce and returns the transaction hashes. -// If an error occurs during the transaction sending process, it returns the error. -func (e *EOARunner) sendTransactionsForUser(account *account, chainID *big.Int, - bar *progressbar.ProgressBar) ([]types.Hash, []error, error) { - txRelayer, err := txrelayer.NewTxRelayer( - txrelayer.WithClient(e.client), - txrelayer.WithChainID(chainID), - txrelayer.WithCollectTxnHashes(), - txrelayer.WithNoWaiting(), - txrelayer.WithoutNonceGet(), - ) - if err != nil { - return nil, nil, err - } - - feeData, err := getFeeData(e.client, e.cfg.DynamicTxs) - if err != nil { - return nil, nil, err - } - - sendErrs := make([]error, 0) - checkFeeDataNum := e.cfg.TxsPerUser / 5 - - for i := 0; i < e.cfg.TxsPerUser; i++ { - var err error - - if i%checkFeeDataNum == 0 { - feeData, err = getFeeData(e.client, e.cfg.DynamicTxs) - if err != nil { - return nil, nil, err - } - } - - if e.cfg.DynamicTxs { - _, err = txRelayer.SendTransaction(types.NewTx(types.NewDynamicFeeTx( - types.WithNonce(account.nonce), - types.WithTo(&receiverAddr), - types.WithValue(ethgo.Gwei(1)), - types.WithGas(21000), - types.WithFrom(account.key.Address()), - types.WithGasFeeCap(feeData.gasFeeCap), - types.WithGasTipCap(feeData.gasTipCap), - types.WithChainID(chainID), - )), account.key) - } else { - _, err = txRelayer.SendTransaction(types.NewTx(types.NewLegacyTx( - types.WithNonce(account.nonce), - types.WithTo(&receiverAddr), - types.WithValue(ethgo.Gwei(1)), - types.WithGas(21000), - types.WithGasPrice(feeData.gasPrice), - types.WithFrom(account.key.Address()), - )), account.key) - } - - if err != nil { - sendErrs = append(sendErrs, err) - } - - account.nonce++ - _ = bar.Add(1) +// createEOATransaction creates an EOA transaction +func (e *EOARunner) createEOATransaction(account *account, feeData *feeData, + chainID *big.Int) *types.Transaction { + if e.cfg.DynamicTxs { + return types.NewTx(types.NewDynamicFeeTx( + types.WithNonce(account.nonce), + types.WithTo(&receiverAddr), + types.WithValue(ethgo.Gwei(1)), + types.WithGas(21000), + types.WithFrom(account.key.Address()), + types.WithGasFeeCap(feeData.gasFeeCap), + types.WithGasTipCap(feeData.gasTipCap), + types.WithChainID(chainID), + )) } - return txRelayer.GetTxnHashes(), sendErrs, nil + return types.NewTx(types.NewLegacyTx( + types.WithNonce(account.nonce), + types.WithTo(&receiverAddr), + types.WithValue(ethgo.Gwei(1)), + types.WithGas(21000), + types.WithGasPrice(feeData.gasPrice), + types.WithFrom(account.key.Address()), + )) } diff --git a/loadtest/runner/erc20_runner.go b/loadtest/runner/erc20_runner.go index e178d6ec5e..783b0fabed 100644 --- a/loadtest/runner/erc20_runner.go +++ b/loadtest/runner/erc20_runner.go @@ -21,6 +21,7 @@ type ERC20Runner struct { erc20Token types.Address erc20TokenArtifact *contracts.Artifact + txInput []byte } // NewERC20Runner creates a new ERC20Runner instance with the given LoadTestConfig. @@ -68,7 +69,7 @@ func (e *ERC20Runner) Run() error { go e.waitForReceiptsParallel() go e.calculateResultsParallel() - _, err := e.sendTransactions() + _, err := e.sendTransactions(e.createERC20Transaction) if err != nil { return err } @@ -76,7 +77,7 @@ func (e *ERC20Runner) Run() error { return <-e.done } - txHashes, err := e.sendTransactions() + txHashes, err := e.sendTransactions(e.createERC20Transaction) if err != nil { return err } @@ -136,6 +137,16 @@ func (e *ERC20Runner) deployERC20Token() error { e.erc20Token = types.Address(receipt.ContractAddress) e.erc20TokenArtifact = artifact + input, err = e.erc20TokenArtifact.Abi.Methods["transfer"].Encode(map[string]interface{}{ + "receiver": receiverAddr, + "numTokens": big.NewInt(1), + }) + if err != nil { + return err + } + + e.txInput = input + fmt.Printf("Deploying ERC20 token took %s\n", time.Since(start)) return nil @@ -219,79 +230,26 @@ func (e *ERC20Runner) mintERC20TokenToVUs() error { return nil } -// sendTransactions sends transactions for the load test. -func (e *ERC20Runner) sendTransactions() ([]types.Hash, error) { - return e.BaseLoadTestRunner.sendTransactions(e.sendTransactionsForUser) -} - -// sendTransactionsForUser sends ERC20 token transactions for a given user account. -// It takes an account pointer and a chainID as input parameters. -// It returns a slice of transaction hashes and an error if any. -func (e *ERC20Runner) sendTransactionsForUser(account *account, chainID *big.Int, - bar *progressbar.ProgressBar) ([]types.Hash, []error, error) { - txRelayer, err := txrelayer.NewTxRelayer( - txrelayer.WithClient(e.client), - txrelayer.WithChainID(chainID), - txrelayer.WithCollectTxnHashes(), - txrelayer.WithNoWaiting(), - txrelayer.WithEstimateGasFallback(), - txrelayer.WithoutNonceGet(), - ) - if err != nil { - return nil, nil, err - } - - feeData, err := getFeeData(e.client, e.cfg.DynamicTxs) - if err != nil { - return nil, nil, err - } - - sendErrs := make([]error, 0) - checkFeeDataNum := e.cfg.TxsPerUser / 5 - - for i := 0; i < e.cfg.TxsPerUser; i++ { - input, err := e.erc20TokenArtifact.Abi.Methods["transfer"].Encode(map[string]interface{}{ - "receiver": receiverAddr, - "numTokens": big.NewInt(1), - }) - if err != nil { - return nil, nil, err - } - - if i%checkFeeDataNum == 0 { - feeData, err = getFeeData(e.client, e.cfg.DynamicTxs) - if err != nil { - return nil, nil, err - } - } - - if e.cfg.DynamicTxs { - _, err = txRelayer.SendTransaction(types.NewTx(types.NewDynamicFeeTx( - types.WithNonce(account.nonce), - types.WithTo(&e.erc20Token), - types.WithFrom(account.key.Address()), - types.WithGasFeeCap(feeData.gasFeeCap), - types.WithGasTipCap(feeData.gasTipCap), - types.WithChainID(chainID), - types.WithInput(input), - )), account.key) - } else { - _, err = txRelayer.SendTransaction(types.NewTx(types.NewLegacyTx( - types.WithNonce(account.nonce), - types.WithTo(&e.erc20Token), - types.WithGasPrice(feeData.gasPrice), - types.WithFrom(account.key.Address()), - types.WithInput(input), - )), account.key) - } - - if err != nil { - sendErrs = append(sendErrs, err) - } - - account.nonce++ - _ = bar.Add(1) - } - - return txRelayer.GetTxnHashes(), sendErrs, nil +// createERC20Transaction creates an ERC20 transaction +func (e *ERC20Runner) createERC20Transaction(account *account, feeData *feeData, + chainID *big.Int) *types.Transaction { + if e.cfg.DynamicTxs { + return types.NewTx(types.NewDynamicFeeTx( + types.WithNonce(account.nonce), + types.WithTo(&e.erc20Token), + types.WithFrom(account.key.Address()), + types.WithGasFeeCap(feeData.gasFeeCap), + types.WithGasTipCap(feeData.gasTipCap), + types.WithChainID(chainID), + types.WithInput(e.txInput), + )) + } + + return types.NewTx(types.NewLegacyTx( + types.WithNonce(account.nonce), + types.WithTo(&e.erc20Token), + types.WithGasPrice(feeData.gasPrice), + types.WithFrom(account.key.Address()), + types.WithInput(e.txInput), + )) } diff --git a/loadtest/runner/erc721_runner.go b/loadtest/runner/erc721_runner.go index d36eb0134a..16f9fac911 100644 --- a/loadtest/runner/erc721_runner.go +++ b/loadtest/runner/erc721_runner.go @@ -9,7 +9,6 @@ import ( "github.com/0xPolygon/polygon-edge/contracts" "github.com/0xPolygon/polygon-edge/txrelayer" "github.com/0xPolygon/polygon-edge/types" - "github.com/schollz/progressbar/v3" ) const nftURL = "https://really-valuable-nft-page.io" @@ -20,6 +19,7 @@ type ERC721Runner struct { erc721Token types.Address erc721TokenArtifact *contracts.Artifact + txInput []byte } // NewERC721Runner creates a new ERC721Runner instance with the given LoadTestConfig. @@ -62,7 +62,7 @@ func (e *ERC721Runner) Run() error { go e.waitForReceiptsParallel() go e.calculateResultsParallel() - _, err := e.sendTransactions() + _, err := e.sendTransactions(e.createERC721Transaction) if err != nil { return err } @@ -70,7 +70,7 @@ func (e *ERC721Runner) Run() error { return <-e.done } - txHashes, err := e.sendTransactions() + txHashes, err := e.sendTransactions(e.createERC721Transaction) if err != nil { return err } @@ -129,81 +129,38 @@ func (e *ERC721Runner) deployERC21Token() error { e.erc721Token = types.Address(receipt.ContractAddress) e.erc721TokenArtifact = artifact - fmt.Printf("Deploying ERC721 token took %s\n", time.Since(start)) - - return nil -} - -// sendTransactions sends transactions for the load test. -func (e *ERC721Runner) sendTransactions() ([]types.Hash, error) { - return e.BaseLoadTestRunner.sendTransactions(e.sendTransactionsForUser) -} - -// sendTransactionsForUser sends ERC20 token transactions for a given user account. -// It takes an account pointer and a chainID as input parameters. -// It returns a slice of transaction hashes and an error if any. -func (e *ERC721Runner) sendTransactionsForUser(account *account, chainID *big.Int, - bar *progressbar.ProgressBar) ([]types.Hash, []error, error) { - txRelayer, err := txrelayer.NewTxRelayer( - txrelayer.WithClient(e.client), - txrelayer.WithChainID(chainID), - txrelayer.WithCollectTxnHashes(), - txrelayer.WithNoWaiting(), - txrelayer.WithEstimateGasFallback(), - txrelayer.WithoutNonceGet(), - ) - if err != nil { - return nil, nil, err - } - - feeData, err := getFeeData(e.client, e.cfg.DynamicTxs) + input, err = e.erc721TokenArtifact.Abi.Methods["createNFT"].Encode(map[string]interface{}{"tokenURI": nftURL}) if err != nil { - return nil, nil, err + return err } - sendErrs := make([]error, 0) - checkFeeDataNum := e.cfg.TxsPerUser / 5 + e.txInput = input - for i := 0; i < e.cfg.TxsPerUser; i++ { - input, err := e.erc721TokenArtifact.Abi.Methods["createNFT"].Encode(map[string]interface{}{"tokenURI": nftURL}) - if err != nil { - return nil, nil, err - } - - if i%checkFeeDataNum == 0 { - feeData, err = getFeeData(e.client, e.cfg.DynamicTxs) - if err != nil { - return nil, nil, err - } - } - - if e.cfg.DynamicTxs { - _, err = txRelayer.SendTransaction(types.NewTx(types.NewDynamicFeeTx( - types.WithNonce(account.nonce), - types.WithTo(&e.erc721Token), - types.WithFrom(account.key.Address()), - types.WithGasFeeCap(feeData.gasFeeCap), - types.WithGasTipCap(feeData.gasTipCap), - types.WithChainID(chainID), - types.WithInput(input), - )), account.key) - } else { - _, err = txRelayer.SendTransaction(types.NewTx(types.NewLegacyTx( - types.WithNonce(account.nonce), - types.WithTo(&e.erc721Token), - types.WithGasPrice(feeData.gasPrice), - types.WithFrom(account.key.Address()), - types.WithInput(input), - )), account.key) - } + fmt.Printf("Deploying ERC721 token took %s\n", time.Since(start)) - if err != nil { - sendErrs = append(sendErrs, err) - } + return nil +} - account.nonce++ - _ = bar.Add(1) +// createERC721Transaction creates an ERC721 transaction +func (e *ERC721Runner) createERC721Transaction(account *account, feeData *feeData, + chainID *big.Int) *types.Transaction { + if e.cfg.DynamicTxs { + return types.NewTx(types.NewDynamicFeeTx( + types.WithNonce(account.nonce), + types.WithTo(&e.erc721Token), + types.WithFrom(account.key.Address()), + types.WithGasFeeCap(feeData.gasFeeCap), + types.WithGasTipCap(feeData.gasTipCap), + types.WithChainID(chainID), + types.WithInput(e.txInput), + )) } - return txRelayer.GetTxnHashes(), sendErrs, nil + return types.NewTx(types.NewLegacyTx( + types.WithNonce(account.nonce), + types.WithTo(&e.erc721Token), + types.WithGasPrice(feeData.gasPrice), + types.WithFrom(account.key.Address()), + types.WithInput(e.txInput), + )) } diff --git a/loadtest/runner/load_test_runner.go b/loadtest/runner/load_test_runner.go index 5acecd7c3b..a3e3fcec53 100644 --- a/loadtest/runner/load_test_runner.go +++ b/loadtest/runner/load_test_runner.go @@ -55,6 +55,7 @@ type LoadTestConfig struct { VUs int // VUs is the number of virtual users. TxsPerUser int // TxsPerUser is the number of transactions per user. + BatchSize int // BatchSize is the number of transactions to send in a single batch. DynamicTxs bool // DynamicTxs indicates whether the load test should generate dynamic transactions. ResultsToJSON bool // ResultsToJSON indicates whether the results should be written in JSON format. diff --git a/loadtest/runner/transaction_batch_sender.go b/loadtest/runner/transaction_batch_sender.go new file mode 100644 index 0000000000..84dcb83361 --- /dev/null +++ b/loadtest/runner/transaction_batch_sender.go @@ -0,0 +1,94 @@ +package runner + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/0xPolygon/polygon-edge/types" + "github.com/umbracle/ethgo/jsonrpc/codec" + "github.com/valyala/fasthttp" +) + +// TransactionBatchSender is an http transport for sending transactions in a batch +type TransactionBatchSender struct { + addr string + client *fasthttp.Client +} + +// newTransactionBatchSender creates a new TransactionBatchSender instance with the given address +func newTransactionBatchSender(addr string) *TransactionBatchSender { + return &TransactionBatchSender{ + addr: addr, + client: &fasthttp.Client{ + DialDualStack: true, + }, + } +} + +// SendBatch implements sends transactions in a batch +func (h *TransactionBatchSender) SendBatch(params []string) ([]types.Hash, error) { + if len(params) == 0 { + return nil, nil + } + + var requests = make([]codec.Request, 0, len(params)) + + for i, param := range params { + request := codec.Request{ + JsonRPC: "2.0", + Method: "eth_sendRawTransaction", + ID: uint64(i), + } + + data, err := json.Marshal([]string{param}) + if err != nil { + return nil, err + } + + request.Params = data + requests = append(requests, request) + } + + raw, err := json.Marshal(requests) + if err != nil { + return nil, err + } + + req := fasthttp.AcquireRequest() + res := fasthttp.AcquireResponse() + + defer fasthttp.ReleaseRequest(req) + defer fasthttp.ReleaseResponse(res) + + req.SetRequestURI(h.addr) + req.Header.SetMethod("POST") + req.Header.SetContentType("application/json") + req.SetBody(raw) + + if err := h.client.Do(req, res); err != nil { + return nil, err + } + + if sc := res.StatusCode(); sc != fasthttp.StatusOK { + return nil, fmt.Errorf("status code is %d. response = %s", sc, string(res.Body())) + } + + // Decode json-rpc response + var responses []*codec.Response + if err := json.Unmarshal(res.Body(), &responses); err != nil { + return nil, err + } + + txHashes := make([]types.Hash, 0, len(responses)) + + for _, response := range responses { + if response.Error != nil { + return nil, fmt.Errorf("error: %w", response.Error) + } + + txHashes = append(txHashes, types.StringToHash(strings.Trim(string(response.Result), "\""))) + } + + return txHashes, nil +}