From 357ad9c5bd8ee811bac8eac0cb79232837a4713f Mon Sep 17 00:00:00 2001 From: Shane Date: Tue, 19 Aug 2025 15:09:11 -0400 Subject: [PATCH] feat: add eth_sendPrivateTransaction method --- go.mod | 3 +- openrpc.json | 77 +++++++++++++++++++++++++++++++++ server/request_handler.go | 4 +- server/request_processor.go | 8 +++- server/request_sendprivatetx.go | 37 ++++++++++++++++ tests/e2e_test.go | 16 +++++++ 6 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 openrpc.json create mode 100644 server/request_sendprivatetx.go diff --git a/go.mod b/go.mod index f3baa9e..24b98f9 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.9.0 golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -44,8 +45,6 @@ require ( github.com/valyala/fastrand v1.1.0 // indirect github.com/valyala/histogram v1.2.0 // indirect golang.org/x/sync v0.10.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/tmplfunc v0.0.3 // indirect ) diff --git a/openrpc.json b/openrpc.json new file mode 100644 index 0000000..c5da91d --- /dev/null +++ b/openrpc.json @@ -0,0 +1,77 @@ +{ + "openrpc": "1.3.2", + "info": { + "title": "Protect API", + "description": "APIs that Protect API Serves without proxying upstream to a provider", + "version": "1.0.0" + }, + "methods": [ + { + "name": "eth_sendPrivateTransaction", + "description": "Send a single private transaction. Private transactions are sent directly to validators and not included in the public mempool.", + "params": [ + { + "name": "TransactionString", + "description": "The raw signed transaction", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "MaxBlockNumber", + "description": "Highest block number that the block can be included in", + "schema": { + "type": "string", + "format": "^0x(0|[1-9a-f][0-9a-f]*)$" + } + }, + { + "name": "Preferences", + "description": "Transaction preferences", + "schema": { + "type": "object", + "properties": { + "fast": { + "type": "boolean" + } + } + } + } + ], + "result": { + "name": "TransactionHash", + "schema": { + "title": "32 byte hex value", + "type": "string", + "pattern": "^0x[0-9a-f]{64}$" + } + }, + "examples": [ + { + "name": "sendPrivateTransactionExample", + "params": [ + { + "name": "TransactionString", + "value": "0x02f87205158459682f00850c51e9727a82520894477db63b8e73aea96f201c3c4f5e8fbfcdd18b5c87038d7ea4c6800080c080a0312d6375e578ab953a41456d4be583a46dced97a9e38f349bb8e7b63d14cfc1ea001daea40332180670568a1864e6d3f910b194903128d4a0a5c499597f3f6ff40" + }, + { + "name": "MaxBlockNumber", + "value": "0x2540be400" + }, + { + "name": "Preferences", + "value": { + "fast": true + } + } + ], + "result": { + "name": "ResultExample", + "value": "0xb1770efb14906e509893b6190359658208ae64d0c56e22f748a1b0869885559e" + } + } + ] + } + ] +} diff --git a/server/request_handler.go b/server/request_handler.go index 05c8b4d..afc12c4 100644 --- a/server/request_handler.go +++ b/server/request_handler.go @@ -158,10 +158,10 @@ func (r *RpcRequestHandler) process() { // processRequest handles single request func (r *RpcRequestHandler) processRequest(client RPCProxyClient, jsonReq *types.JsonRpcRequest, origin, referer string, isWhitehatBundleCollection bool, whitehatBundleId string, urlParams URLParameters, reqURL string, body []byte) { var entry *database.EthSendRawTxEntry - if jsonReq.Method == "eth_sendRawTransaction" { + if jsonReq.Method == "eth_sendRawTransaction" || jsonReq.Method == "eth_sendPrivateTransaction" { entry = r.requestRecord.AddEthSendRawTxEntry(uuid.New()) // log the full url for debugging - r.logger.Info("[processRequest] eth_sendRawTransaction request URL", "url", reqURL) + r.logger.Info("[processRequest] ", jsonReq.Method, " request URL", "url", reqURL) } // Handle single request rpcReq := NewRpcRequest(r.logger, client, jsonReq, r.relaySigningKey, r.relayUrl, origin, referer, isWhitehatBundleCollection, whitehatBundleId, entry, urlParams, r.chainID, r.rpcCache, r.defaultEthClient) diff --git a/server/request_processor.go b/server/request_processor.go index 5d06a38..58e0829 100644 --- a/server/request_processor.go +++ b/server/request_processor.go @@ -48,6 +48,7 @@ type RpcRequest struct { chainID []byte rpcCache *application.RpcCache flashbotsSigningAddress string + maxBlockNumberOverride uint64 } func NewRpcRequest( @@ -110,6 +111,9 @@ func (r *RpcRequest) ProcessRequest() *types.JsonRpcResponse { case r.jsonReq.Method == "eth_sendRawTransaction": r.ethSendRawTxEntry.WhiteHatBundleId = r.whitehatBundleId r.handle_sendRawTransaction() + case r.jsonReq.Method == "eth_sendPrivateTransaction": + r.ethSendRawTxEntry.WhiteHatBundleId = r.whitehatBundleId + r.handle_sendPrivateTransaction() case r.jsonReq.Method == "eth_getTransactionCount" && r.intercept_signed_eth_getTransactionCount(): case r.jsonReq.Method == "eth_getTransactionCount" && r.intercept_mm_eth_getTransactionCount(): // intercept if MM needs to show an error to user case r.jsonReq.Method == "eth_call" && r.intercept_eth_call_to_FlashRPC_Contract(): // intercept if Flashbots isRPC contract @@ -320,7 +324,9 @@ func (r *RpcRequest) sendTxToRelay() { sendPrivateTxArgs.Preferences.Privacy.AuctionTimeout = r.urlParams.auctionTimeout } - if r.urlParams.blockRange > 0 { + if r.maxBlockNumberOverride > 0 { + sendPrivateTxArgs.MaxBlockNumber = r.maxBlockNumberOverride + } else if r.urlParams.blockRange > 0 { bn, err := r.defaultEthClient.BlockNumber(context.Background()) if err != nil { r.logger.Error("[sendTxToRelay] BlockNumber failed", "error", err) diff --git a/server/request_sendprivatetx.go b/server/request_sendprivatetx.go new file mode 100644 index 0000000..877afed --- /dev/null +++ b/server/request_sendprivatetx.go @@ -0,0 +1,37 @@ +package server + +import ( + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/flashbots/rpc-endpoint/types" +) + +func (r *RpcRequest) handle_sendPrivateTransaction() { + if len(r.jsonReq.Params) > 1 { + m, ok := r.jsonReq.Params[1].(string) + if !ok { + r.writeRpcError("MaxBlockNumber must be a string", types.JsonRpcParseError) + return + } + max, err := hexutil.DecodeUint64(m) + if err != nil { + r.writeRpcError("MaxBlockNumber must be a valid hexadecimal string", types.JsonRpcParseError) + return + } + r.maxBlockNumberOverride = max + } + if len(r.jsonReq.Params) > 2 { + f, ok := r.jsonReq.Params[2].(map[string]interface{}) + if !ok { + r.writeRpcError("Preferences must be an object", types.JsonRpcParseError) + return + } + fast, ok := f["fast"].(bool) + if !ok { + r.writeRpcError("Preferences fast must be a boolean", types.JsonRpcParseError) + return + } + r.urlParams.pref.Fast = fast + } + + r.handle_sendRawTransaction() +} diff --git a/tests/e2e_test.go b/tests/e2e_test.go index 6089045..0e1f054 100644 --- a/tests/e2e_test.go +++ b/tests/e2e_test.go @@ -617,6 +617,22 @@ func TestWhitehatBundleCollectionGetBalance(t *testing.T) { require.Equal(t, "0x56bc75e2d63100000", val) } +func TestSendPrivateTransaction(t *testing.T) { + testServerSetupWithMockStore() + + req := types.NewJsonRpcRequest(1, "eth_sendPrivateTransaction", []interface{}{ + testutils.TestTx_BundleFailedTooManyTimes_RawTx, + }) + r1 := testutils.SendRpcAndParseResponseOrFailNowAllowRpcError(t, req) + require.Nil(t, r1.Error) + + require.Equal(t, "eth_sendPrivateTransaction", testutils.MockBackendLastJsonRpcRequest.Method) + + var res string + json.Unmarshal(r1.Result, &res) + require.Equal(t, testutils.TestTx_BundleFailedTooManyTimes_Hash, res) +} + func Test_StoreRequests(t *testing.T) { // Store setup memStore := database.NewMemStore()