Skip to content

Commit

Permalink
feat: Broadcaster Middleware (#19)
Browse files Browse the repository at this point in the history
* broadcaster impl

* update README to include example for broadcast bundle
  • Loading branch information
TitanBuilder authored May 24, 2023
1 parent f234223 commit 702086e
Show file tree
Hide file tree
Showing 4 changed files with 238 additions and 0 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,29 @@ if err != nil {
fmt.Printf("%+v\n", result)
```

#### Send a transaction bundle to a list of Builder endpoints with `eth_sendBundle` (full example [/examples/broadcastbundle]):

```go
urls := []string{
"https://relay.flashbots.net",
// Refer to https://www.mev.to/builders for builder endpoints
}
rpc := flashbotsrpc.NewBuilderBroadcastRPC(urls)

sendBundleArgs := flashbotsrpc.FlashbotsSendBundleRequest{
Txs: []string{"YOUR_RAW_TX"},
BlockNumber: fmt.Sprintf("0x%x", 13281018),
}

results := rpc.BroadcastBundle(privateKey, sendBundleArgs)
for _, result := range results {
if result.Err != nil {
log.Fatal(result.Err)
}
fmt.Printf("%+v\n", result.BundleResponse)
}
```

#### More examples

You can find example code in the [`/examples/` directory](https://github.com/metachris/flashbotsrpc/tree/master/examples).
54 changes: 54 additions & 0 deletions examples/broadcastbundle/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package main

import (
"errors"
"fmt"

"github.com/ethereum/go-ethereum/crypto"
"github.com/metachris/flashbotsrpc"
)

var privateKey, _ = crypto.GenerateKey() // creating a new private key for testing. you probably want to use an existing key.
// var privateKey, _ = crypto.HexToECDSA("YOUR_PRIVATE_KEY")

func main() {
urls := []string{
"https://relay.flashbots.net",
"https://rpc.titanbuilder.xyz",
"https://builder0x69.io",
"https://rpc.beaverbuild.org",
"https://rsync-builder.xyz",
"https://api.blocknative.com/v1/auction",
// "https://mev.api.blxrbdn.com", # Authentication required
"https://eth-builder.com",
"https://builder.gmbit.co/rpc",
"https://buildai.net",
"https://rpc.payload.de",
"https://rpc.lightspeedbuilder.info",
"https://rpc.nfactorial.xyz",
}

rpc := flashbotsrpc.NewBuilderBroadcastRPC(urls)
rpc.Debug = true

sendBundleArgs := flashbotsrpc.FlashbotsSendBundleRequest{
Txs: []string{"YOUR_RAW_TX"},
BlockNumber: fmt.Sprintf("0x%x", 13281018),
}

results := rpc.BroadcastBundle(privateKey, sendBundleArgs)
for _, result := range results {
if result.Err != nil {
if errors.Is(result.Err, flashbotsrpc.ErrRelayErrorResponse) {
// ErrRelayErrorResponse means it's a standard Flashbots relay error response, so probably a user error, rather than JSON or network error
fmt.Println(result.Err.Error())
} else {
fmt.Printf("error: %+v\n", result.Err)
}
return
}

// Print result
fmt.Printf("%+v\n", result.BundleResponse)
}
}
156 changes: 156 additions & 0 deletions flashbotsrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"math/big"
"net/http"
"os"
"sync"
"time"

"github.com/ethereum/go-ethereum/accounts"
Expand Down Expand Up @@ -730,3 +732,157 @@ func (rpc *FlashbotsRPC) FlashbotsCancelPrivateTransaction(privKey *ecdsa.Privat
err = json.Unmarshal(rawMsg, &cancelled)
return cancelled, err
}

type BuilderBroadcastRPC struct {
urls []string
client httpClient
log logger
Debug bool
Headers map[string]string // Additional headers to send with the request
Timeout time.Duration
}

// NewBuilderBroadcastRPC create broadcaster rpc client with given url
func NewBuilderBroadcastRPC(urls []string, options ...func(rpc *BuilderBroadcastRPC)) *BuilderBroadcastRPC {
rpc := &BuilderBroadcastRPC{
urls: urls,
log: log.New(os.Stderr, "", log.LstdFlags),
Headers: make(map[string]string),
Timeout: 30 * time.Second,
}
for _, option := range options {
option(rpc)
}
rpc.client = &http.Client{
Timeout: rpc.Timeout,
}
return rpc
}

// https://docs.flashbots.net/flashbots-auction/searchers/advanced/rpc-endpoint/#eth_sendbundle
func (broadcaster *BuilderBroadcastRPC) BroadcastBundle(privKey *ecdsa.PrivateKey, param FlashbotsSendBundleRequest) []BuilderBroadcastResponse {
requestResponses := broadcaster.broadcastRequest("eth_sendBundle", privKey, param)

responses := []BuilderBroadcastResponse{}

for _, requestResponse := range requestResponses {
if requestResponse.Err != nil {
responses = append(responses, BuilderBroadcastResponse{Err: requestResponse.Err})
}
fbResponse := FlashbotsSendBundleResponse{}
err := json.Unmarshal(requestResponse.Msg, &fbResponse)
responses = append(responses, BuilderBroadcastResponse{BundleResponse: fbResponse, Err: err})
}

return responses
}

type broadcastRequestResponse struct {
Msg json.RawMessage
Err error
}

func (broadcaster *BuilderBroadcastRPC) broadcastRequest(method string, privKey *ecdsa.PrivateKey, params ...interface{}) []broadcastRequestResponse {
request := rpcRequest{
ID: 1,
JSONRPC: "2.0",
Method: method,
Params: params,
}

body, err := json.Marshal(request)
if err != nil {
responseArr := []broadcastRequestResponse{{Msg: nil, Err: err}}
return responseArr
}

hashedBody := crypto.Keccak256Hash([]byte(body)).Hex()
sig, err := crypto.Sign(accounts.TextHash([]byte(hashedBody)), privKey)
if err != nil {
responseArr := []broadcastRequestResponse{{Msg: nil, Err: err}}
return responseArr
}

signature := crypto.PubkeyToAddress(privKey.PublicKey).Hex() + ":" + hexutil.Encode(sig)

var wg sync.WaitGroup
responseCh := make(chan []byte)

// Iterate over the URLs and send requests concurrently
for _, url := range broadcaster.urls {
wg.Add(1)

go func(url string) {
defer wg.Done()

// Create a new HTTP GET request
req, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
if err != nil {
return
}

req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
req.Header.Add("X-Flashbots-Signature", signature)
for k, v := range broadcaster.Headers {
req.Header.Add(k, v)
}

response, err := broadcaster.client.Do(req)
if response != nil {
defer response.Body.Close()
}
if err != nil {
return
}

// Read the response body
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return
}

// Send the response body through the channel
responseCh <- body

if broadcaster.Debug {
broadcaster.log.Println(fmt.Sprintf("%s\nRequest: %s\nSignature: %s\nResponse: %s\n", method, body, signature, string(body)))
}

}(url)
}

go func() {
wg.Wait()
close(responseCh)
}()

responses := []broadcastRequestResponse{}
for data := range responseCh {
// On error, response looks like this instead of JSON-RPC: {"error":"block param must be a hex int"}
errorResp := new(RelayErrorResponse)
if err := json.Unmarshal(data, errorResp); err == nil && errorResp.Error != "" {
// relay returned an error
responseArr := []broadcastRequestResponse{{Msg: nil, Err: fmt.Errorf("%w: %s", ErrRelayErrorResponse, errorResp.Error)}}
return responseArr
}

resp := new(rpcResponse)
if err := json.Unmarshal(data, resp); err != nil {
responseArr := []broadcastRequestResponse{{Msg: nil, Err: err}}
return responseArr
}

if resp.Error != nil {
responseArr := []broadcastRequestResponse{{Msg: nil, Err: fmt.Errorf("%w: %s", ErrRelayErrorResponse, (*resp).Error.Message)}}
return responseArr
}

responses = append(responses, broadcastRequestResponse{
Msg: resp.Result,
Err: nil,
})
}

return responses
}
5 changes: 5 additions & 0 deletions types.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,11 @@ type FlashbotsSendBundleResponse struct {
BundleHash string `json:"bundleHash"`
}

type BuilderBroadcastResponse struct {
BundleResponse FlashbotsSendBundleResponse `json:"bundleResponse"`
Err error `json:"err"`
}

// sendPrivateTransaction
type FlashbotsSendPrivateTransactionRequest struct {
Tx string `json:"tx"`
Expand Down

0 comments on commit 702086e

Please sign in to comment.