@@ -27,6 +27,18 @@ type stats struct {
2727 InclusionDelay time.Duration
2828}
2929
30+ type Bundle struct {
31+ Txs [][]byte `json:"txs"` // Raw transaction bytes
32+ BlockNumber uint64 `json:"blockNumber"` // Target block number
33+ FlashblockNumberMin * uint64 `json:"flashblockNumberMin,omitempty"` // Optional: minimum flashblock number
34+ FlashblockNumberMax * uint64 `json:"flashblockNumberMax,omitempty"` // Optional: maximum flashblock number
35+ MinTimestamp * uint64 `json:"minTimestamp,omitempty"` // Optional: minimum timestamp
36+ MaxTimestamp * uint64 `json:"maxTimestamp,omitempty"` // Optional: maximum timestamp
37+ RevertingTxHashes []common.Hash `json:"revertingTxHashes"` // Transaction hashes that can revert
38+ ReplacementUuid * string `json:"replacementUuid,omitempty"` // Optional: replacement UUID
39+ DroppingTxHashes []common.Hash `json:"droppingTxHashes"` // Transaction hashes to drop
40+ }
41+
3042func main () {
3143 err := godotenv .Load ()
3244 if err != nil {
@@ -65,6 +77,7 @@ func main() {
6577
6678 sendTxnSync := os .Getenv ("SEND_TXN_SYNC" ) == "true"
6779 runStandardTransactionSending := os .Getenv ("RUN_STANDARD_TRANSACTION_SENDING" ) != "false"
80+ runBundleTest := os .Getenv ("RUN_BUNDLE_TEST" ) == "true"
6881
6982 pollingIntervalMs := 100
7083 if pollingEnv := os .Getenv ("POLLING_INTERVAL_MS" ); pollingEnv != "" {
@@ -82,6 +95,13 @@ func main() {
8295 }
8396 }
8497
98+ bundleSize := 3
99+ if bundleSizeEnv := os .Getenv ("BUNDLE_SIZE" ); bundleSizeEnv != "" {
100+ if parsed , err := strconv .Atoi (bundleSizeEnv ); err == nil {
101+ bundleSize = parsed
102+ }
103+ }
104+
85105 flashblocksClient , err := ethclient .Dial (flashblocksUrl )
86106 if err != nil {
87107 log .Fatalf ("Failed to connect to the Ethereum client: %v" , err )
@@ -108,10 +128,22 @@ func main() {
108128 var baseTimings []stats
109129
110130 chainId , err := baseClient .NetworkID (context .Background ())
131+ log .Printf ("Chain ID: %v" , chainId )
111132 if err != nil {
112133 log .Fatalf ("Failed to get network ID: %v" , err )
113134 }
114135
136+ // Bundle testing
137+ if runBundleTest {
138+ log .Printf ("Starting bundle test with %d transactions per bundle" , bundleSize )
139+ err = createAndSendBundle (chainId , privateKey , fromAddress , toAddress , flashblocksClient , bundleSize )
140+ if err != nil {
141+ log .Printf ("Failed to send bundle: %v" , err )
142+ } else {
143+ log .Printf ("Bundle test completed successfully" )
144+ }
145+ }
146+
115147 flashblockErrors := 0
116148 baseErrors := 0
117149
@@ -200,22 +232,17 @@ func writeToFile(filename string, data []stats) error {
200232 return nil
201233}
202234
203- func timeTransaction (chainId * big.Int , privateKey * ecdsa.PrivateKey , fromAddress common.Address , toAddress common.Address , client * ethclient.Client , useSyncRPC bool , pollingIntervalMs int ) (stats , error ) {
204- nonce , err := client .PendingNonceAt (context .Background (), fromAddress )
205- if err != nil {
206- return stats {}, fmt .Errorf ("unable to get nonce: %v" , err )
207- }
208-
235+ func createTx (chainId * big.Int , privateKey * ecdsa.PrivateKey , toAddress common.Address , client * ethclient.Client , nonce uint64 ) (* types.Transaction , error ) {
209236 gasPrice , err := client .SuggestGasPrice (context .Background ())
210237 if err != nil {
211- return stats {} , fmt .Errorf ("unable to get gas price: %v" , err )
238+ return nil , fmt .Errorf ("unable to get gas price: %v" , err )
212239 }
213240 gasLimit := uint64 (21000 )
214241 value := big .NewInt (100 )
215242
216243 tip , err := client .SuggestGasTipCap (context .Background ())
217244 if err != nil {
218- return stats {} , fmt .Errorf ("unable to get gas price : %v" , err )
245+ return nil , fmt .Errorf ("unable to get gas tip cap : %v" , err )
219246 }
220247
221248 tx := types .NewTx (& types.DynamicFeeTx {
@@ -231,7 +258,22 @@ func timeTransaction(chainId *big.Int, privateKey *ecdsa.PrivateKey, fromAddress
231258
232259 signedTx , err := types .SignTx (tx , types .NewPragueSigner (chainId ), privateKey )
233260 if err != nil {
234- return stats {}, fmt .Errorf ("unable to sign transaction: %v" , err )
261+ return nil , fmt .Errorf ("unable to sign transaction: %v" , err )
262+ }
263+
264+ return signedTx , nil
265+ }
266+
267+ func timeTransaction (chainId * big.Int , privateKey * ecdsa.PrivateKey , fromAddress common.Address , toAddress common.Address , client * ethclient.Client , useSyncRPC bool , pollingIntervalMs int ) (stats , error ) {
268+ // Use pending nonce to avoid conflicts with pending transactions
269+ nonce , err := client .PendingNonceAt (context .Background (), fromAddress )
270+ if err != nil {
271+ return stats {}, fmt .Errorf ("unable to get nonce: %v" , err )
272+ }
273+
274+ signedTx , err := createTx (chainId , privateKey , toAddress , client , nonce )
275+ if err != nil {
276+ return stats {}, fmt .Errorf ("unable to create transaction: %v" , err )
235277 }
236278
237279 if useSyncRPC {
@@ -296,3 +338,74 @@ func sendTransactionAsync(client *ethclient.Client, signedTx *types.Transaction,
296338
297339 return stats {}, fmt .Errorf ("failed to get transaction" )
298340}
341+
342+ func sendBundle (client * ethclient.Client , signedTxs []* types.Transaction , targetBlockNumber uint64 ) (string , error ) {
343+ // Convert transactions to raw transaction bytes and collect hashes
344+ var txsBytes [][]byte
345+ var txHashes []common.Hash
346+ for _ , tx := range signedTxs {
347+ rawTx , err := tx .MarshalBinary ()
348+ if err != nil {
349+ return "" , fmt .Errorf ("unable to marshal transaction: %v" , err )
350+ }
351+ txsBytes = append (txsBytes , rawTx )
352+ txHashes = append (txHashes , tx .Hash ())
353+ }
354+
355+ // Create bundle structure matching Base TIPS format
356+ bundle := Bundle {
357+ Txs : txsBytes ,
358+ BlockNumber : targetBlockNumber ,
359+ RevertingTxHashes : txHashes , // All transaction hashes must be in reverting_tx_hashes
360+ DroppingTxHashes : []common.Hash {}, // Empty array if no dropping txs
361+ }
362+
363+ // Send bundle via RPC call
364+ var bundleHash string
365+ err := client .Client ().CallContext (context .Background (), & bundleHash , "eth_sendBundle" , bundle )
366+ if err != nil {
367+ return "" , fmt .Errorf ("unable to send bundle: %v" , err )
368+ }
369+
370+ log .Printf ("Bundle sent successfully with hash: %s" , bundleHash )
371+ return bundleHash , nil
372+ }
373+
374+ func createAndSendBundle (chainId * big.Int , privateKey * ecdsa.PrivateKey , fromAddress common.Address , toAddress common.Address , client * ethclient.Client , numTxs int ) error {
375+ // Get current block number for targeting
376+ currentBlock , err := client .BlockNumber (context .Background ())
377+ if err != nil {
378+ return fmt .Errorf ("unable to get current block number: %v" , err )
379+ }
380+
381+ // Target the next block
382+ targetBlock := currentBlock + 1
383+
384+ // Get base nonce
385+ baseNonce , err := client .PendingNonceAt (context .Background (), fromAddress )
386+ if err != nil {
387+ return fmt .Errorf ("unable to get nonce: %v" , err )
388+ }
389+
390+ // Create multiple signed transactions for the bundle
391+ var signedTxs []* types.Transaction
392+ for i := 0 ; i < numTxs ; i ++ {
393+ nonce := baseNonce + uint64 (i ) // Sequential nonces
394+ signedTx , err := createTx (chainId , privateKey , toAddress , client , nonce )
395+ if err != nil {
396+ return fmt .Errorf ("unable to create transaction %d: %v" , i , err )
397+ }
398+
399+ signedTxs = append (signedTxs , signedTx )
400+ log .Printf ("Created transaction %d with nonce %d, hash: %s" , i , nonce , signedTx .Hash ().Hex ())
401+ }
402+
403+ // Send the bundle
404+ bundleHash , err := sendBundle (client , signedTxs , targetBlock )
405+ if err != nil {
406+ return fmt .Errorf ("failed to send bundle: %v" , err )
407+ }
408+
409+ log .Printf ("Bundle sent with hash: %s, targeting block: %d" , bundleHash , targetBlock )
410+ return nil
411+ }
0 commit comments