diff --git a/go.mod b/go.mod index 73056a8..a6d8ea5 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.21 require ( github.com/ethereum/go-ethereum v1.13.1 + github.com/google/uuid v1.3.0 github.com/shopspring/decimal v1.3.1 github.com/sourcegraph/conc v0.3.0 github.com/stretchr/testify v1.8.1 diff --git a/pkg/mev/bloxroute_submit_bundle.go b/pkg/mev/blxr_bundle_sender.go similarity index 88% rename from pkg/mev/bloxroute_submit_bundle.go rename to pkg/mev/blxr_bundle_sender.go index 204074e..fd02bf6 100644 --- a/pkg/mev/bloxroute_submit_bundle.go +++ b/pkg/mev/blxr_bundle_sender.go @@ -9,9 +9,7 @@ import ( "net/http" "strconv" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" ) type BlxrBuilder string @@ -51,9 +49,15 @@ func NewBloxrouteClient( } func (s *BloxrouteClient) SendBundle( - ctx context.Context, blockNumber uint64, txs ...*types.Transaction, + ctx context.Context, + uuid *string, + blockNumber uint64, + txs ...*types.Transaction, ) (SendBundleResponse, error) { p := new(BLXRSubmitBundleParams).SetBlockNumber(blockNumber).SetTransactions(txs...) + if uuid != nil { + p.SetUUID(*uuid) + } mevBuilders := make(map[BlxrBuilder]string) for _, b := range s.enabledBuilders { @@ -96,6 +100,17 @@ func (s *BloxrouteClient) SendBundle( return SendBundleResponse(resp), nil } +func (s *BloxrouteClient) CancelBundle( + ctx context.Context, bundleUUID string, +) error { + _, err := s.SendBundle(ctx, &bundleUUID, 0) + if err != nil { + return fmt.Errorf("cancel by send bundle error: %w", err) + } + + return nil +} + type BLXRSubmitBundleRequest struct { ID string `json:"id,omitempty"` Method string `json:"method,omitempty"` @@ -113,6 +128,10 @@ type BLXRSubmitBundleParams struct { } func (p *BLXRSubmitBundleParams) SetTransactions(txs ...*types.Transaction) *BLXRSubmitBundleParams { + if len(txs) == 0 { + return p + } + transactions := make([]string, 0, len(txs)) for _, tx := range txs { transactions = append(transactions, "0x"+txToRlp(tx)) @@ -124,11 +143,21 @@ func (p *BLXRSubmitBundleParams) SetTransactions(txs ...*types.Transaction) *BLX } func (p *BLXRSubmitBundleParams) SetBlockNumber(block uint64) *BLXRSubmitBundleParams { + if block == 0 { + return p + } + p.BlockNumber = fmt.Sprintf("0x%x", block) return p } +func (p *BLXRSubmitBundleParams) SetUUID(uuid string) *BLXRSubmitBundleParams { + p.UUID = uuid + + return p +} + type BLXRSubmitBundleResponse struct { Jsonrpc string `json:"jsonrpc,omitempty"` ID int `json:"id,string,omitempty"` @@ -156,12 +185,10 @@ func bloxrouteSignFlashbot(key *ecdsa.PrivateKey, p *BLXRSubmitBundleParams) (st return "", fmt.Errorf("marshal json error: %w", err) } - signature, err := signRequest(key, reqBody) + signature, err := requestSignature(key, reqBody) if err != nil { return "", fmt.Errorf("sign request error: %w", err) } - sig := fmt.Sprintf("%s:%s", crypto.PubkeyToAddress(key.PublicKey), hexutil.Encode(signature)) - - return sig, nil + return signature, nil } diff --git a/pkg/mev/send_bundle.go b/pkg/mev/bundle_sender.go similarity index 53% rename from pkg/mev/send_bundle.go rename to pkg/mev/bundle_sender.go index 593cc5b..1397075 100644 --- a/pkg/mev/send_bundle.go +++ b/pkg/mev/bundle_sender.go @@ -17,22 +17,32 @@ import ( // Client https://beaverbuild.org/docs.html; https://rsync-builder.xyz/docs; // https://docs.flashbots.net/flashbots-auction/advanced/rpc-endpoint#eth_sendbundle type Client struct { - c *http.Client - endpoint string - flashbotKey *ecdsa.PrivateKey + c *http.Client + endpoint string + flashbotKey *ecdsa.PrivateKey + cancelBySendBundle bool } // NewClient set the flashbotKey to nil will skip adding the signature header. -func NewClient(c *http.Client, endpoint string, flashbotKey *ecdsa.PrivateKey) *Client { +func NewClient( + c *http.Client, + endpoint string, + flashbotKey *ecdsa.PrivateKey, + cancelBySendBundle bool, +) *Client { return &Client{ - c: c, - endpoint: endpoint, - flashbotKey: flashbotKey, + c: c, + endpoint: endpoint, + flashbotKey: flashbotKey, + cancelBySendBundle: cancelBySendBundle, } } func (s *Client) SendBundle( - ctx context.Context, blockNumber uint64, txs ...*types.Transaction, + ctx context.Context, + uuid *string, + blockNumber uint64, + txs ...*types.Transaction, ) (SendBundleResponse, error) { req := SendBundleRequest{ ID: SendBundleID, @@ -40,6 +50,9 @@ func (s *Client) SendBundle( Method: ETHSendBundleMethod, } p := new(SendBundleParams).SetBlockNumber(blockNumber).SetTransactions(txs...) + if uuid != nil { + p.SetUUID(*uuid) + } req.Params = append(req.Params, p) reqBody, err := json.Marshal(req) @@ -49,14 +62,11 @@ func (s *Client) SendBundle( var headers [][2]string if s.flashbotKey != nil { - signature, err := signRequest(s.flashbotKey, reqBody) + signature, err := requestSignature(s.flashbotKey, reqBody) if err != nil { return SendBundleResponse{}, fmt.Errorf("sign flashbot request error: %w", err) } - flashbotSig := fmt.Sprintf("%s:%s", - crypto.PubkeyToAddress(s.flashbotKey.PublicKey), hexutil.Encode(signature)) - - headers = append(headers, [2]string{"X-Flashbots-Signature", flashbotSig}) + headers = append(headers, [2]string{"X-Flashbots-Signature", signature}) } httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, s.endpoint, bytes.NewBuffer(reqBody)) @@ -77,14 +87,67 @@ func (s *Client) SendBundle( return resp, nil } -func signRequest(key *ecdsa.PrivateKey, body []byte) ([]byte, error) { +func (s *Client) CancelBundle( + ctx context.Context, bundleUUID string, +) error { + if s.cancelBySendBundle { + if _, err := s.SendBundle(ctx, &bundleUUID, 0); err != nil { + return fmt.Errorf("cancel by send bundle error: %w", err) + } + return nil + } + + // build request + p := CancelBundleParams{ + ReplacementUUID: bundleUUID, + } + req := SendBundleRequest{ + ID: SendBundleID, + JSONRPC: JSONRPC2, + Method: ETHCancelBundleMethod, + Params: []any{p}, + } + reqBody, err := json.Marshal(req) + if err != nil { + return fmt.Errorf("marshal json error: %w", err) + } + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, s.endpoint, bytes.NewBuffer(reqBody)) + if err != nil { + return fmt.Errorf("new http request error: %w", err) + } + + var headers [][2]string + if s.flashbotKey != nil { + signature, err := requestSignature(s.flashbotKey, reqBody) + if err != nil { + return fmt.Errorf("sign flashbot request error: %w", err) + } + + headers = append(headers, [2]string{"X-Flashbots-Signature", signature}) + } + + // do + resp, err := doRequest[SendBundleResponse](s.c, httpReq, headers...) + if err != nil { + return err + } + + // check + if len(resp.Error.Messange) != 0 { + return fmt.Errorf("response error, code: [%d], message: [%s]", resp.Error.Code, resp.Error.Messange) + } + + return nil +} + +func requestSignature(key *ecdsa.PrivateKey, body []byte) (string, error) { hashed := crypto.Keccak256Hash(body).Hex() signature, err := crypto.Sign(accounts.TextHash([]byte(hashed)), key) if err != nil { - return nil, fmt.Errorf("sign crypto error: %w", err) + return "", fmt.Errorf("sign crypto error: %w", err) } - return signature, nil + return fmt.Sprintf("%s:%s", crypto.PubkeyToAddress(key.PublicKey), hexutil.Encode(signature)), nil } type SendBundleRequest struct { @@ -105,9 +168,17 @@ type SendBundleParams struct { MaxTimestamp *uint64 `json:"maxTimestamp,omitempty"` // (Optional) Array[String], A list of tx hashes that are allowed to revert RevertingTxs *[]string `json:"revertingTxHashes,omitempty"` + // (Optional) String, UUID that can be used to cancel/replace this bundle + ReplacementUUID string `json:"ReplacementUuid,omitempty"` + // (Optional) String, UUID that can be used to cancel/replace this bundle (For beaverbuild) + UUID string `json:"uuid,omitempty"` } func (p *SendBundleParams) SetTransactions(txs ...*types.Transaction) *SendBundleParams { + if len(txs) == 0 { + return p + } + transactions := make([]string, 0, len(txs)) for _, tx := range txs { transactions = append(transactions, "0x"+txToRlp(tx)) @@ -119,7 +190,22 @@ func (p *SendBundleParams) SetTransactions(txs ...*types.Transaction) *SendBundl } func (p *SendBundleParams) SetBlockNumber(block uint64) *SendBundleParams { + if block == 0 { + return p + } + p.BlockNumber = fmt.Sprintf("0x%x", block) return p } + +func (p *SendBundleParams) SetUUID(uuid string) *SendBundleParams { + p.ReplacementUUID = uuid + p.UUID = uuid + + return p +} + +type CancelBundleParams struct { + ReplacementUUID string `json:"replacementUuid"` +} diff --git a/pkg/mev/send_bundle_test.go b/pkg/mev/bundle_sender_test.go similarity index 89% rename from pkg/mev/send_bundle_test.go rename to pkg/mev/bundle_sender_test.go index bbc0ce9..a099185 100644 --- a/pkg/mev/send_bundle_test.go +++ b/pkg/mev/bundle_sender_test.go @@ -12,6 +12,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethclient" + "github.com/google/uuid" "github.com/stretchr/testify/require" ) @@ -56,11 +57,14 @@ func TestSendBundle(t *testing.T) { t.Log("new tx", signedTx.Hash().String()) - sender := mev.NewClient(client, endpoint, privateKey) - resp, err := sender.SendBundle(ctx, blockNumber+12, signedTx) - require.NoError(t, err) // sepolia: code: [-32000], message: [internal server error] + uuid := uuid.NewString() + sender := mev.NewClient(client, endpoint, privateKey, false) + resp, err := sender.SendBundle(ctx, &uuid, blockNumber+12, signedTx) + require.NoError(t, err) // sepolia: code: [-32000], message: [internal server error] t.Log("send bundle response", resp) + + require.NoError(t, sender.CancelBundle(ctx, uuid)) } func TestUnmarshal(t *testing.T) { diff --git a/pkg/mev/pkg.go b/pkg/mev/pkg.go index 8b42f2e..2fcc083 100644 --- a/pkg/mev/pkg.go +++ b/pkg/mev/pkg.go @@ -17,10 +17,19 @@ const ( SendBundleID = 1 BloxrouteSubmitBundleMethod = "blxr_submit_bundle" ETHSendBundleMethod = "eth_sendBundle" + ETHCancelBundleMethod = "eth_cancelBundle" ) type IBundleSender interface { - SendBundle(ctx context.Context, blockNumber uint64, tx ...*types.Transaction) (SendBundleResponse, error) + SendBundle( + ctx context.Context, + uuid *string, + blockNumber uint64, + tx ...*types.Transaction, + ) (SendBundleResponse, error) + CancelBundle( + ctx context.Context, bundleUUID string, + ) error } var (