Skip to content

Commit 1c4022c

Browse files
authored
Merge pull request #6 from base/work-with-ingress
feat: support eth_sendBundle
2 parents 0cf51ec + 2c2745a commit 1c4022c

File tree

2 files changed

+125
-9
lines changed

2 files changed

+125
-9
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: 122 additions & 9 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)
@@ -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

Comments
 (0)