Skip to content

Commit c675700

Browse files
authored
Merge pull request #7 from base/bundle
feat: be able to send `Bundle`s
2 parents b129715 + edf89d0 commit c675700

File tree

2 files changed

+124
-10
lines changed

2 files changed

+124
-10
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@ POLLING_INTERVAL_MS=100
44
FLASHBLOCKS_URL=https://sepolia-preconf.base.org
55
BASE_URL=https://sepolia.base.org
66
REGION=texas
7+
NUMBER_OF_TRANSACTIONS=100
8+
RUN_BUNDLE_TEST=true
9+
BUNDLE_SIZE=3

main.go

Lines changed: 121 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
3042
func 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

Comments
 (0)