Skip to content
This repository was archived by the owner on Oct 20, 2024. It is now read-only.

Commit 702086e

Browse files
authored
feat: Broadcaster Middleware (metachris#19)
* broadcaster impl * update README to include example for broadcast bundle
1 parent f234223 commit 702086e

File tree

4 files changed

+238
-0
lines changed

4 files changed

+238
-0
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,29 @@ if err != nil {
6565
fmt.Printf("%+v\n", result)
6666
```
6767

68+
#### Send a transaction bundle to a list of Builder endpoints with `eth_sendBundle` (full example [/examples/broadcastbundle]):
69+
70+
```go
71+
urls := []string{
72+
"https://relay.flashbots.net",
73+
// Refer to https://www.mev.to/builders for builder endpoints
74+
}
75+
rpc := flashbotsrpc.NewBuilderBroadcastRPC(urls)
76+
77+
sendBundleArgs := flashbotsrpc.FlashbotsSendBundleRequest{
78+
Txs: []string{"YOUR_RAW_TX"},
79+
BlockNumber: fmt.Sprintf("0x%x", 13281018),
80+
}
81+
82+
results := rpc.BroadcastBundle(privateKey, sendBundleArgs)
83+
for _, result := range results {
84+
if result.Err != nil {
85+
log.Fatal(result.Err)
86+
}
87+
fmt.Printf("%+v\n", result.BundleResponse)
88+
}
89+
```
90+
6891
#### More examples
6992

7093
You can find example code in the [`/examples/` directory](https://github.com/metachris/flashbotsrpc/tree/master/examples).

examples/broadcastbundle/main.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/ethereum/go-ethereum/crypto"
8+
"github.com/metachris/flashbotsrpc"
9+
)
10+
11+
var privateKey, _ = crypto.GenerateKey() // creating a new private key for testing. you probably want to use an existing key.
12+
// var privateKey, _ = crypto.HexToECDSA("YOUR_PRIVATE_KEY")
13+
14+
func main() {
15+
urls := []string{
16+
"https://relay.flashbots.net",
17+
"https://rpc.titanbuilder.xyz",
18+
"https://builder0x69.io",
19+
"https://rpc.beaverbuild.org",
20+
"https://rsync-builder.xyz",
21+
"https://api.blocknative.com/v1/auction",
22+
// "https://mev.api.blxrbdn.com", # Authentication required
23+
"https://eth-builder.com",
24+
"https://builder.gmbit.co/rpc",
25+
"https://buildai.net",
26+
"https://rpc.payload.de",
27+
"https://rpc.lightspeedbuilder.info",
28+
"https://rpc.nfactorial.xyz",
29+
}
30+
31+
rpc := flashbotsrpc.NewBuilderBroadcastRPC(urls)
32+
rpc.Debug = true
33+
34+
sendBundleArgs := flashbotsrpc.FlashbotsSendBundleRequest{
35+
Txs: []string{"YOUR_RAW_TX"},
36+
BlockNumber: fmt.Sprintf("0x%x", 13281018),
37+
}
38+
39+
results := rpc.BroadcastBundle(privateKey, sendBundleArgs)
40+
for _, result := range results {
41+
if result.Err != nil {
42+
if errors.Is(result.Err, flashbotsrpc.ErrRelayErrorResponse) {
43+
// ErrRelayErrorResponse means it's a standard Flashbots relay error response, so probably a user error, rather than JSON or network error
44+
fmt.Println(result.Err.Error())
45+
} else {
46+
fmt.Printf("error: %+v\n", result.Err)
47+
}
48+
return
49+
}
50+
51+
// Print result
52+
fmt.Printf("%+v\n", result.BundleResponse)
53+
}
54+
}

flashbotsrpc.go

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import (
66
"encoding/json"
77
"fmt"
88
"io"
9+
"io/ioutil"
910
"log"
1011
"math/big"
1112
"net/http"
1213
"os"
14+
"sync"
1315
"time"
1416

1517
"github.com/ethereum/go-ethereum/accounts"
@@ -730,3 +732,157 @@ func (rpc *FlashbotsRPC) FlashbotsCancelPrivateTransaction(privKey *ecdsa.Privat
730732
err = json.Unmarshal(rawMsg, &cancelled)
731733
return cancelled, err
732734
}
735+
736+
type BuilderBroadcastRPC struct {
737+
urls []string
738+
client httpClient
739+
log logger
740+
Debug bool
741+
Headers map[string]string // Additional headers to send with the request
742+
Timeout time.Duration
743+
}
744+
745+
// NewBuilderBroadcastRPC create broadcaster rpc client with given url
746+
func NewBuilderBroadcastRPC(urls []string, options ...func(rpc *BuilderBroadcastRPC)) *BuilderBroadcastRPC {
747+
rpc := &BuilderBroadcastRPC{
748+
urls: urls,
749+
log: log.New(os.Stderr, "", log.LstdFlags),
750+
Headers: make(map[string]string),
751+
Timeout: 30 * time.Second,
752+
}
753+
for _, option := range options {
754+
option(rpc)
755+
}
756+
rpc.client = &http.Client{
757+
Timeout: rpc.Timeout,
758+
}
759+
return rpc
760+
}
761+
762+
// https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint/#eth_sendbundle
763+
func (broadcaster *BuilderBroadcastRPC) BroadcastBundle(privKey *ecdsa.PrivateKey, param FlashbotsSendBundleRequest) []BuilderBroadcastResponse {
764+
requestResponses := broadcaster.broadcastRequest("eth_sendBundle", privKey, param)
765+
766+
responses := []BuilderBroadcastResponse{}
767+
768+
for _, requestResponse := range requestResponses {
769+
if requestResponse.Err != nil {
770+
responses = append(responses, BuilderBroadcastResponse{Err: requestResponse.Err})
771+
}
772+
fbResponse := FlashbotsSendBundleResponse{}
773+
err := json.Unmarshal(requestResponse.Msg, &fbResponse)
774+
responses = append(responses, BuilderBroadcastResponse{BundleResponse: fbResponse, Err: err})
775+
}
776+
777+
return responses
778+
}
779+
780+
type broadcastRequestResponse struct {
781+
Msg json.RawMessage
782+
Err error
783+
}
784+
785+
func (broadcaster *BuilderBroadcastRPC) broadcastRequest(method string, privKey *ecdsa.PrivateKey, params ...interface{}) []broadcastRequestResponse {
786+
request := rpcRequest{
787+
ID: 1,
788+
JSONRPC: "2.0",
789+
Method: method,
790+
Params: params,
791+
}
792+
793+
body, err := json.Marshal(request)
794+
if err != nil {
795+
responseArr := []broadcastRequestResponse{{Msg: nil, Err: err}}
796+
return responseArr
797+
}
798+
799+
hashedBody := crypto.Keccak256Hash([]byte(body)).Hex()
800+
sig, err := crypto.Sign(accounts.TextHash([]byte(hashedBody)), privKey)
801+
if err != nil {
802+
responseArr := []broadcastRequestResponse{{Msg: nil, Err: err}}
803+
return responseArr
804+
}
805+
806+
signature := crypto.PubkeyToAddress(privKey.PublicKey).Hex() + ":" + hexutil.Encode(sig)
807+
808+
var wg sync.WaitGroup
809+
responseCh := make(chan []byte)
810+
811+
// Iterate over the URLs and send requests concurrently
812+
for _, url := range broadcaster.urls {
813+
wg.Add(1)
814+
815+
go func(url string) {
816+
defer wg.Done()
817+
818+
// Create a new HTTP GET request
819+
req, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
820+
if err != nil {
821+
return
822+
}
823+
824+
req.Header.Add("Content-Type", "application/json")
825+
req.Header.Add("Accept", "application/json")
826+
req.Header.Add("X-Flashbots-Signature", signature)
827+
for k, v := range broadcaster.Headers {
828+
req.Header.Add(k, v)
829+
}
830+
831+
response, err := broadcaster.client.Do(req)
832+
if response != nil {
833+
defer response.Body.Close()
834+
}
835+
if err != nil {
836+
return
837+
}
838+
839+
// Read the response body
840+
body, err := ioutil.ReadAll(response.Body)
841+
if err != nil {
842+
return
843+
}
844+
845+
// Send the response body through the channel
846+
responseCh <- body
847+
848+
if broadcaster.Debug {
849+
broadcaster.log.Println(fmt.Sprintf("%s\nRequest: %s\nSignature: %s\nResponse: %s\n", method, body, signature, string(body)))
850+
}
851+
852+
}(url)
853+
}
854+
855+
go func() {
856+
wg.Wait()
857+
close(responseCh)
858+
}()
859+
860+
responses := []broadcastRequestResponse{}
861+
for data := range responseCh {
862+
// On error, response looks like this instead of JSON-RPC: {"error":"block param must be a hex int"}
863+
errorResp := new(RelayErrorResponse)
864+
if err := json.Unmarshal(data, errorResp); err == nil && errorResp.Error != "" {
865+
// relay returned an error
866+
responseArr := []broadcastRequestResponse{{Msg: nil, Err: fmt.Errorf("%w: %s", ErrRelayErrorResponse, errorResp.Error)}}
867+
return responseArr
868+
}
869+
870+
resp := new(rpcResponse)
871+
if err := json.Unmarshal(data, resp); err != nil {
872+
responseArr := []broadcastRequestResponse{{Msg: nil, Err: err}}
873+
return responseArr
874+
}
875+
876+
if resp.Error != nil {
877+
responseArr := []broadcastRequestResponse{{Msg: nil, Err: fmt.Errorf("%w: %s", ErrRelayErrorResponse, (*resp).Error.Message)}}
878+
return responseArr
879+
}
880+
881+
responses = append(responses, broadcastRequestResponse{
882+
Msg: resp.Result,
883+
Err: nil,
884+
})
885+
}
886+
887+
return responses
888+
}

types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,11 @@ type FlashbotsSendBundleResponse struct {
410410
BundleHash string `json:"bundleHash"`
411411
}
412412

413+
type BuilderBroadcastResponse struct {
414+
BundleResponse FlashbotsSendBundleResponse `json:"bundleResponse"`
415+
Err error `json:"err"`
416+
}
417+
413418
// sendPrivateTransaction
414419
type FlashbotsSendPrivateTransactionRequest struct {
415420
Tx string `json:"tx"`

0 commit comments

Comments
 (0)