Skip to content

Commit

Permalink
Handle broken Flashbots errors
Browse files Browse the repository at this point in the history
  • Loading branch information
dvush committed Oct 23, 2024
1 parent b6a6f38 commit 2e8a556
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 14 deletions.
76 changes: 62 additions & 14 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"reflect"
"strconv"
)

const (
jsonrpcVersion = "2.0"
// This error code is set in the error reponse when interacting with Flashbots RPC that has broken error response
// see docs for RPCClientOpts.RejectBrokenFlashbotsErrors
FlashbotsBrokenErrorResponseCode = -32088
)

// RPCClient sends JSON-RPC requests over HTTP to the provided JSON-RPC backend.
Expand Down Expand Up @@ -250,12 +254,13 @@ func (e *HTTPError) Error() string {
}

type rpcClient struct {
endpoint string
httpClient *http.Client
customHeaders map[string]string
allowUnknownFields bool
defaultRequestID int
signer *RequestSigner
endpoint string
httpClient *http.Client
customHeaders map[string]string
allowUnknownFields bool
defaultRequestID int
signer *RequestSigner
rejectBrokenFlashbotsErrors bool
}

// RPCClientOpts can be provided to NewClientWithOpts() to change configuration of RPCClient.
Expand All @@ -271,8 +276,12 @@ type RPCClientOpts struct {
AllowUnknownFields bool
DefaultRequestID int

// if Signer is nil we don't sign the request
// If Signer is set requset body will be signed and signature will be set in the X-Flashbots-Signature header
Signer *RequestSigner
// if true client will return error when server responds with errors like {"error": "text"}
// otherwise this response will be converted to equivalent {"error": {"message": "text", "code": FlashbotsBrokenErrorResponseCode}}
// Bad errors are always rejected for batch requests
RejectBrokenFlashbotsErrors bool
}

// RPCResponses is of type []*RPCResponse.
Expand Down Expand Up @@ -353,6 +362,7 @@ func NewClientWithOpts(endpoint string, opts *RPCClientOpts) RPCClient {

rpcClient.defaultRequestID = opts.DefaultRequestID
rpcClient.signer = opts.Signer
rpcClient.rejectBrokenFlashbotsErrors = opts.RejectBrokenFlashbotsErrors

return rpcClient
}
Expand Down Expand Up @@ -452,16 +462,50 @@ func (client *rpcClient) doCall(ctx context.Context, RPCRequest *RPCRequest) (*R
}
defer httpResponse.Body.Close()

var rpcResponse *RPCResponse
decoder := json.NewDecoder(httpResponse.Body)
if !client.allowUnknownFields {
decoder.DisallowUnknownFields()
body, err := io.ReadAll(httpResponse.Body)
if err != nil {
return nil, fmt.Errorf("rpc call %v() on %v: %w", RPCRequest.Method, httpRequest.URL.Redacted(), err)
}

decodeJSONBody := func(v any) error {
decoder := json.NewDecoder(bytes.NewReader(body))
if !client.allowUnknownFields {
decoder.DisallowUnknownFields()
}
decoder.UseNumber()
return decoder.Decode(v)
}

var (
rpcResponse *RPCResponse
brokenErrorResponseHandled bool
)
err = decodeJSONBody(&rpcResponse)

// try parse broken Flashbots error
if err != nil && !client.rejectBrokenFlashbotsErrors {
var brokenErrorResponse *brokenFlashbostErrorResponse
// if we have error here we just ingore it and the code below will work with the original error
newErr := decodeJSONBody(&brokenErrorResponse)
if newErr == nil {
rpcResponse = &RPCResponse{
JSONRPC: jsonrpcVersion,
Result: nil,
Error: &RPCError{
Code: FlashbotsBrokenErrorResponseCode,
Message: brokenErrorResponse.Error,
Data: nil,
},
ID: RPCRequest.ID,
}
brokenErrorResponseHandled = true
err = nil
}
}
decoder.UseNumber()
err = decoder.Decode(&rpcResponse)

// parsing error
if err != nil {

// if we have some http error, return it
if httpResponse.StatusCode >= 400 {
return nil, &HTTPError{
Expand All @@ -485,7 +529,7 @@ func (client *rpcClient) doCall(ctx context.Context, RPCRequest *RPCRequest) (*R
}

// if we have a response body, but also a http error situation, return both
if httpResponse.StatusCode >= 400 {
if !brokenErrorResponseHandled && httpResponse.StatusCode >= 400 {
if rpcResponse.Error != nil {
return rpcResponse, &HTTPError{
Code: httpResponse.StatusCode,
Expand Down Expand Up @@ -699,3 +743,7 @@ func (RPCResponse *RPCResponse) GetObject(toType interface{}) error {

return nil
}

type brokenFlashbostErrorResponse struct {
Error string `json:"error,omitempty"`
}
36 changes: 36 additions & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,42 @@ func TestUnsignedRequest(t *testing.T) {
check.Equal("", header)
}

func TestCallFlashbots(t *testing.T) {
check := assert.New(t)
signer, _ := RandomSigner()
rpcClient := NewClientWithOpts("https://relay.flashbots.net", &RPCClientOpts{
Signer: signer,
})

res, err := rpcClient.Call(context.Background(), "eth_sendBundle", struct{}{})
check.Nil(err)
check.NotNil(res)
check.NotNil(res.Error)
check.Equal("unable to parse body as JSON", res.Error.Message)
check.Equal(FlashbotsBrokenErrorResponseCode, res.Error.Code)
}

func TestBrokenFlashbotsErrorResponse(t *testing.T) {
oldStatusCode := httpStatusCode
oldResponseBody := responseBody
defer func() {
httpStatusCode = oldStatusCode
responseBody = oldResponseBody
}()

check := assert.New(t)
rpcClient := NewClient(httpServer.URL)

responseBody = `{"error":"unknown method: something"}`
httpStatusCode = 400
res, err := rpcClient.Call(context.Background(), "something", 1, 2, 3)
<-requestChan
check.Nil(err)
check.Nil(res.Result)
check.Equal(FlashbotsBrokenErrorResponseCode, res.Error.Code)
check.Equal("unknown method: something", res.Error.Message)
}

type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Expand Down

0 comments on commit 2e8a556

Please sign in to comment.