400 error sending JSON bundle to Flashbots relay, using Golang and HTTP requests

Running into an error sending a JSON object to the primary flashbots relay. Typically, the only response I’m getting, is either 400: Bad Request, or 400: Could not parse body as JSON. I only run into this problem while I attempt to send a contract call. Sending a request for user information resolves just fine, with the same format as before. See below:

func EncodeFunction(contractData string, functionCall Function) ([]byte, error) {
// Takes a function's signature, it's arguments. and returns it as an encoded bytearray, without the 0x prefix.

contractABI, err := abi.JSON(strings.NewReader(contractData))
if err != nil {
	return []byte{}, err
}
data, err := contractABI.Pack(functionCall.FunctionName, functionCall.ArgSlice...)
if err != nil {
	return []byte{}, err
}
/*
	var dataString string
	dataString += "0x"
	for _, b := range data {
		dataString += fmt.Sprintf("%02x", b)
	}
*/
return data, nil


}



func CreateSignedTx(toAddress common.Address, data []byte, privateKey *ecdsa.PrivateKey, endpoint string) (string, error) {
// Create and return a single signed raw transaction, as a string.
var err error

publicKey := privateKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
	return "", err
}
fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
client, err := ethclient.Dial(endpoint)
if err != nil {
	return "", err
}

gasPrice, err := client.SuggestGasPrice(context.Background())
if err != nil {
	return "", err
}
nonce, err := client.PendingNonceAt(context.Background(), fromAddress)
if err != nil {
	return "", err
}
tx := types.NewTransaction(
	nonce, //	nonce
	toAddress,
	big.NewInt(0), // value
	uint64(21000), // gasLimit, may need to be raised.
	gasPrice,
	data,
)
chainID, err := client.NetworkID(context.Background())
if err != nil {
	return "", err
}
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
if err != nil {
	return "", err
}
ts := types.Transactions{signedTx}
b := new(bytes.Buffer)
ts.EncodeIndex(0, b)
rawTxBytes := b.Bytes()
rawTxHex := hex.EncodeToString(rawTxBytes)

return "0x" + rawTxHex, nil


}



func constructPayload(bundle Bundle, privateKey *ecdsa.PrivateKey) (*http.Request, error) {
marshaledBundle, err := json.Marshal(bundle)
if err != nil {
	return &http.Request{}, err
}

req, err := http.NewRequest("POST", config.FlashbotsMainnetEndpoint, bytes.NewBuffer(marshaledBundle))
if err != nil {
	return &http.Request{}, err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")

bundleString := string(marshaledBundle)
hashedBundle := crypto.Keccak256Hash([]byte(bundleString)).Hex()
sig, err := crypto.Sign(accounts.TextHash([]byte(hashedBundle)), privateKey)
if err != nil {
	return &http.Request{}, err
}
signature := crypto.PubkeyToAddress(privateKey.PublicKey).Hex() + ":" + hexutil.Encode(sig)
req.Header.Add("X-Flashbots-Signature", signature)

return req, nil


}



func submitBundle(reqPayload *http.Request) (string, error) {

httpclient := &http.Client{}
resp, err := httpclient.Do(reqPayload)
if err != nil {
	return "", err
}
if resp.StatusCode != 200 {
	respBody, _ := io.ReadAll(resp.Body)
	errStr := fmt.Sprintf("Error from relay. Status: %s. Body: %s", resp.Status, respBody)
	return "", errors.New(errStr)
} else if resp.StatusCode == 200 {
	respBody, _ := io.ReadAll(resp.Body)
	return fmt.Sprintf("Success. Result: %s", respBody), nil
}
defer resp.Body.Close()
return "", nil


}



func SimulateBundle(rawTxes []string, privateKey *ecdsa.PrivateKey, endpoint string) error {
client, err := ethclient.Dial(endpoint)
if err != nil {
	return err
}
blockHead, err := client.HeaderByNumber(context.Background(), nil)
if err != nil {
	return err
}
// ts := time.Now().Unix()
/*
	params := map[string]interface{}{
		"txs":              rawTxes,
		"blockNumber":      fmt.Sprintf("0x%x", blockHead.Number.Uint64()),
		"stateBlockNumber": "latest",
		//"timestamp":        int(ts),
	}

	bundle := map[string]interface{}{
		"jsonrpc": "2.0",
		"id":      1,
		"method":  "eth_callBundle",
		"params":  params,
	}*/

params := Params{
	Transactions:     rawTxes,
	BlockNumber:      fmt.Sprintf("0x%x", blockHead.Number.Uint64()),
	StateBlockNumber: "latest",
}

bundle := Bundle{
	Json:   "2.0",
	Id:     1,
	Method: "eth_callBundle",
	Params: params,
}

payload, err := constructPayload(bundle, privateKey)
if err != nil {
	return err
}
res, err := submitBundle(payload)
if err != nil {
	return err
}
fmt.Printf(res, "\n")
return nil

}

The classes I’m using should have the correct JSON formatting, and checking in a debugger, they appear to line up correctly.

type Function struct {
FunctionName string        //Name of the function.
ArgSlice     []interface{} //A list of the arguments, in order, which the relevant function takes.


}



type Bundle struct {
Json   string `json:"jsonrpc"`
Id     int    `json:"id"`
Method string `json:"method"`
Params Params `json:"params"`


}



type Params struct {
GenericInput     []interface{} `json:" ,omitempty"`
Transactions     []string      `json:"txs ,omitempty"`               // List of signed raw transactions.
BlockNumber      string        `json:"blockNumber ,omitempty"`       // Hex-encoded block number for which the bundle is valid.
MinTimestamp     int           `json:"minTimestamp ,omitempty"`      // OPTIONAL Minimum timestamp this bundle can be considered valid, in seconds since Unix epoch
MaxTimestamp     int           `json:"maxTimestamp ,omitempty"`      // OPTIONAL Maximum timestamp this bundle can be considered valid, in seconds since Unix epoch
RevertTxHash     []string      `json:"revertingTxHashes ,omitempty"` // OPTIONAL A list of transactions which are allowed to revert in the bundle.
BundleUUID       string        `json:"replacementUUID ,omitempty"`   // OPTIONAL A UUID which can be used to cancel/replace the bundle.
StateBlockNumber string        `json:"stateBlockNumber ,omitempty"`
Timestamp        int           `json:"timestamp ,omitempty"`


}

What, exactly, am I missing here?

To answer my own question: The JSON object was not formatted correctly. “params” expects an ‘array’ of tuples. This can be done by creating a map, or a separate object, and appending that object’s fields onto Bundle’s Params class.

2 Likes