@@ -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 )
@@ -113,6 +133,17 @@ func main() {
113133 log .Fatalf ("Failed to get network ID: %v" , err )
114134 }
115135
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+
116147 flashblockErrors := 0
117148 baseErrors := 0
118149
@@ -201,23 +232,17 @@ func writeToFile(filename string, data []stats) error {
201232 return nil
202233}
203234
204- func timeTransaction (chainId * big.Int , privateKey * ecdsa.PrivateKey , fromAddress common.Address , toAddress common.Address , client * ethclient.Client , useSyncRPC bool , pollingIntervalMs int ) (stats , error ) {
205- // Use confirmed nonce to avoid conflicts with pending transactions
206- nonce , err := client .NonceAt (context .Background (), fromAddress , nil )
207- if err != nil {
208- return stats {}, fmt .Errorf ("unable to get nonce: %v" , err )
209- }
210-
235+ func createTx (chainId * big.Int , privateKey * ecdsa.PrivateKey , toAddress common.Address , client * ethclient.Client , nonce uint64 ) (* types.Transaction , error ) {
211236 gasPrice , err := client .SuggestGasPrice (context .Background ())
212237 if err != nil {
213- return stats {} , fmt .Errorf ("unable to get gas price: %v" , err )
238+ return nil , fmt .Errorf ("unable to get gas price: %v" , err )
214239 }
215240 gasLimit := uint64 (21000 )
216241 value := big .NewInt (100 )
217242
218243 tip , err := client .SuggestGasTipCap (context .Background ())
219244 if err != nil {
220- return stats {} , fmt .Errorf ("unable to get gas tip cap: %v" , err )
245+ return nil , fmt .Errorf ("unable to get gas tip cap: %v" , err )
221246 }
222247
223248 // Add 20% buffer to tip to ensure replacement transactions are accepted
@@ -240,7 +265,22 @@ func timeTransaction(chainId *big.Int, privateKey *ecdsa.PrivateKey, fromAddress
240265
241266 signedTx , err := types .SignTx (tx , types .NewPragueSigner (chainId ), privateKey )
242267 if err != nil {
243- return stats {}, fmt .Errorf ("unable to sign transaction: %v" , err )
268+ return nil , fmt .Errorf ("unable to sign transaction: %v" , err )
269+ }
270+
271+ return signedTx , nil
272+ }
273+
274+ func timeTransaction (chainId * big.Int , privateKey * ecdsa.PrivateKey , fromAddress common.Address , toAddress common.Address , client * ethclient.Client , useSyncRPC bool , pollingIntervalMs int ) (stats , error ) {
275+ // Use confirmed nonce to avoid conflicts with pending transactions
276+ nonce , err := client .NonceAt (context .Background (), fromAddress , nil )
277+ if err != nil {
278+ return stats {}, fmt .Errorf ("unable to get nonce: %v" , err )
279+ }
280+
281+ signedTx , err := createTx (chainId , privateKey , toAddress , client , nonce )
282+ if err != nil {
283+ return stats {}, fmt .Errorf ("unable to create transaction: %v" , err )
244284 }
245285
246286 if useSyncRPC {
@@ -305,3 +345,74 @@ func sendTransactionAsync(client *ethclient.Client, signedTx *types.Transaction,
305345
306346 return stats {}, fmt .Errorf ("failed to get transaction" )
307347}
348+
349+ func sendBundle (client * ethclient.Client , signedTxs []* types.Transaction , targetBlockNumber uint64 ) (string , error ) {
350+ // Convert transactions to raw transaction bytes and collect hashes
351+ var txsBytes [][]byte
352+ var txHashes []common.Hash
353+ for _ , tx := range signedTxs {
354+ rawTx , err := tx .MarshalBinary ()
355+ if err != nil {
356+ return "" , fmt .Errorf ("unable to marshal transaction: %v" , err )
357+ }
358+ txsBytes = append (txsBytes , rawTx )
359+ txHashes = append (txHashes , tx .Hash ())
360+ }
361+
362+ // Create bundle structure matching Base TIPS format
363+ bundle := Bundle {
364+ Txs : txsBytes ,
365+ BlockNumber : targetBlockNumber ,
366+ RevertingTxHashes : txHashes , // All transaction hashes must be in reverting_tx_hashes
367+ DroppingTxHashes : []common.Hash {}, // Empty array if no dropping txs
368+ }
369+
370+ // Send bundle via RPC call
371+ var bundleHash string
372+ err := client .Client ().CallContext (context .Background (), & bundleHash , "eth_sendBundle" , bundle )
373+ if err != nil {
374+ return "" , fmt .Errorf ("unable to send bundle: %v" , err )
375+ }
376+
377+ log .Printf ("Bundle sent successfully with hash: %s" , bundleHash )
378+ return bundleHash , nil
379+ }
380+
381+ func createAndSendBundle (chainId * big.Int , privateKey * ecdsa.PrivateKey , fromAddress common.Address , toAddress common.Address , client * ethclient.Client , numTxs int ) error {
382+ // Get current block number for targeting
383+ currentBlock , err := client .BlockNumber (context .Background ())
384+ if err != nil {
385+ return fmt .Errorf ("unable to get current block number: %v" , err )
386+ }
387+
388+ // Target the next block
389+ targetBlock := currentBlock + 1
390+
391+ // Get base nonce
392+ baseNonce , err := client .NonceAt (context .Background (), fromAddress , nil )
393+ if err != nil {
394+ return fmt .Errorf ("unable to get nonce: %v" , err )
395+ }
396+
397+ // Create multiple signed transactions for the bundle
398+ var signedTxs []* types.Transaction
399+ for i := 0 ; i < numTxs ; i ++ {
400+ nonce := baseNonce + uint64 (i ) // Sequential nonces
401+ signedTx , err := createTx (chainId , privateKey , toAddress , client , nonce )
402+ if err != nil {
403+ return fmt .Errorf ("unable to create transaction %d: %v" , i , err )
404+ }
405+
406+ signedTxs = append (signedTxs , signedTx )
407+ log .Printf ("Created transaction %d with nonce %d, hash: %s" , i , nonce , signedTx .Hash ().Hex ())
408+ }
409+
410+ // Send the bundle
411+ bundleHash , err := sendBundle (client , signedTxs , targetBlock )
412+ if err != nil {
413+ return fmt .Errorf ("failed to send bundle: %v" , err )
414+ }
415+
416+ log .Printf ("Bundle sent with hash: %s, targeting block: %d" , bundleHash , targetBlock )
417+ return nil
418+ }
0 commit comments