From 196bbdeba607300816920c66bd7cf0cc3f8bfe5e Mon Sep 17 00:00:00 2001 From: datluongductuan Date: Wed, 24 Sep 2025 17:19:45 +0700 Subject: [PATCH 1/4] feat: add BaseChainSender implementation and tests for sending raw transactions --- pkg/mev/base_chain_sender.go | 73 ++++++++++++++ pkg/mev/base_chain_sender_test.go | 148 +++++++++++++++++++++++++++++ pkg/mev/bundlesendertype_enumer.go | 12 ++- pkg/mev/pkg.go | 16 ++++ 4 files changed, 245 insertions(+), 4 deletions(-) create mode 100644 pkg/mev/base_chain_sender.go create mode 100644 pkg/mev/base_chain_sender_test.go diff --git a/pkg/mev/base_chain_sender.go b/pkg/mev/base_chain_sender.go new file mode 100644 index 0000000..d893d16 --- /dev/null +++ b/pkg/mev/base_chain_sender.go @@ -0,0 +1,73 @@ +package mev + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" +) + +type BaseChainSender struct { + c *http.Client + endpoint string + senderType BundleSenderType +} + +func NewBaseChainSender( + c *http.Client, + endpoint string, + senderType BundleSenderType, +) *BaseChainSender { + return &BaseChainSender{ + c: c, + endpoint: endpoint, + senderType: senderType, + } +} + +func (s *BaseChainSender) GetSenderType() BundleSenderType { + return s.senderType +} + +func (s *BaseChainSender) SendRawTransaction( + ctx context.Context, + tx *types.Transaction, +) (SendRawTransactionResponse, error) { + txBin, err := tx.MarshalBinary() + if err != nil { + return SendRawTransactionResponse{}, fmt.Errorf("marshal tx binary: %w", err) + } + + req := SendRequest{ + ID: SendBundleID, + JSONRPC: JSONRPC2, + Method: ETHSendRawTransaction, + Params: []any{hexutil.Encode(txBin)}, + } + + reqBody, err := json.Marshal(req) + if err != nil { + return SendRawTransactionResponse{}, fmt.Errorf("marshal json error: %w", err) + } + + httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, s.endpoint, bytes.NewBuffer(reqBody)) + if err != nil { + return SendRawTransactionResponse{}, fmt.Errorf("new http request error: %w", err) + } + + resp, err := doRequest[SendRawTransactionResponse](s.c, httpReq) + if err != nil { + return SendRawTransactionResponse{}, err + } + + if len(resp.Error.Messange) != 0 { + return SendRawTransactionResponse{}, fmt.Errorf("response error, code: [%d], message: [%s]", + resp.Error.Code, resp.Error.Messange) + } + + return resp, nil +} diff --git a/pkg/mev/base_chain_sender_test.go b/pkg/mev/base_chain_sender_test.go new file mode 100644 index 0000000..f3ea5ba --- /dev/null +++ b/pkg/mev/base_chain_sender_test.go @@ -0,0 +1,148 @@ +package mev_test + +import ( + "context" + "math/big" + "net/http" + "testing" + "time" + + "github.com/KyberNetwork/tradinglib/pkg/mev" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/stretchr/testify/require" +) + +func TestBaseChainSender_SendRawTransaction(t *testing.T) { + t.Skip("Skip by default - uncomment to run actual test against Base mainnet") + + // Create HTTP client + httpClient := &http.Client{Timeout: time.Second * 30} + + // Initialize BaseChainSender with Base mainnet RPC + sender := mev.NewBaseChainSender( + httpClient, + "https://mainnet.base.org", + mev.BundleSenderTypeBaseMainnet, + ) + + // Verify sender type + require.Equal(t, mev.BundleSenderTypeBaseMainnet, sender.GetSenderType()) + + // Create a test transaction (this is a dummy transaction that will likely fail) + // In a real scenario, you would use proper private key, nonce, gas price, etc. + privateKey, err := crypto.GenerateKey() + require.NoError(t, err) + + // Connect to Base mainnet to get current gas price and nonce + ethClient, err := ethclient.Dial("https://mainnet.base.org") + require.NoError(t, err) + + fromAddress := crypto.PubkeyToAddress(privateKey.PublicKey) + nonce, err := ethClient.PendingNonceAt(context.Background(), fromAddress) + require.NoError(t, err) + + gasPrice, err := ethClient.SuggestGasPrice(context.Background()) + require.NoError(t, err) + + // Create a simple ETH transfer transaction + toAddress := common.HexToAddress("0x0000000000000000000000000000000000000001") // Burn address + value := big.NewInt(1) // 1 wei + gasLimit := uint64(21000) // Standard ETH transfer gas + + // Get chain ID for Base mainnet (8453) + chainID, err := ethClient.ChainID(context.Background()) + require.NoError(t, err) + require.Equal(t, int64(8453), chainID.Int64()) // Base mainnet chain ID + + // Create transaction + tx := types.NewTransaction(nonce, toAddress, value, gasLimit, gasPrice, nil) + + // Sign transaction + signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey) + require.NoError(t, err) + + // Send transaction using BaseChainSender + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + resp, err := sender.SendRawTransaction(ctx, signedTx) + + // Log the response for debugging + t.Logf("Response: %+v", resp) + t.Logf("Error: %v", err) + + // The transaction will likely fail due to insufficient funds, but we should get a proper response + // We expect either a successful response with transaction hash or a proper error response + if err != nil { + // Check if it's a proper RPC error (not a network/parsing error) + t.Logf("Expected error due to insufficient funds or other RPC error: %v", err) + } else { + // If successful, verify response structure + require.Equal(t, "2.0", resp.Jsonrpc) + require.Equal(t, 1, resp.ID) + require.NotEmpty(t, resp.Result) + t.Logf("Transaction hash: %s", resp.Result) + } +} + +func TestBaseChainSender_SendRawTransaction_InvalidTx(t *testing.T) { + t.Skip("Skip by default - uncomment to run actual test against Base mainnet") + // Create HTTP client + httpClient := &http.Client{Timeout: time.Second * 10} + + // Initialize BaseChainSender with Base mainnet RPC + sender := mev.NewBaseChainSender( + httpClient, + "https://mainnet.base.org", + mev.BundleSenderTypeBaseMainnet, + ) + + // Create an invalid transaction (unsigned) + toAddress := common.HexToAddress("0x0000000000000000000000000000000000000001") + value := big.NewInt(1) + gasLimit := uint64(21000) + gasPrice := big.NewInt(1000000000) // 1 gwei + + // Create unsigned transaction + tx := types.NewTransaction(0, toAddress, value, gasLimit, gasPrice, nil) + + // Try to send unsigned transaction (should fail) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + resp, err := sender.SendRawTransaction(ctx, tx) + + // Should get an error due to invalid transaction + t.Logf("Response: %+v", resp) + t.Logf("Error: %v", err) + + // We expect an error here + require.Error(t, err) +} + +func TestBaseChainSender_Interface_Compliance(t *testing.T) { + // Test that BaseChainSender implements ISendRawTransaction interface + httpClient := &http.Client{Timeout: time.Second * 10} + sender := mev.NewBaseChainSender( + httpClient, + "https://mainnet.base.org", + mev.BundleSenderTypeBaseMainnet, + ) + + // Verify it implements the interface + var _ mev.ISendRawTransaction = sender +} + +func TestNewBaseChainSender(t *testing.T) { + httpClient := &http.Client{Timeout: time.Second * 10} + endpoint := "https://mainnet.base.org" + senderType := mev.BundleSenderTypeBaseMainnet + + sender := mev.NewBaseChainSender(httpClient, endpoint, senderType) + + require.NotNil(t, sender) + require.Equal(t, senderType, sender.GetSenderType()) +} diff --git a/pkg/mev/bundlesendertype_enumer.go b/pkg/mev/bundlesendertype_enumer.go index 0c234ae..791102d 100644 --- a/pkg/mev/bundlesendertype_enumer.go +++ b/pkg/mev/bundlesendertype_enumer.go @@ -7,11 +7,11 @@ import ( "strings" ) -const _BundleSenderTypeName = "BundleSenderTypeFlashbotBundleSenderTypeBeaverBundleSenderTypeRsyncBundleSenderTypeTitanBundleSenderTypeBloxrouteBundleSenderTypeAllBundleSenderTypeMevShareBundleSenderTypeBackrunPublicBundleSenderTypeMevBlockerBundleSenderTypeBlinkBundleSenderTypeMerkleBundleSenderTypeJetbldrBundleSenderTypePenguinBundleSenderTypeLokiBundleSenderTypeQuasarBundleSenderTypeBuilderNetBundleSenderTypeBTCS" +const _BundleSenderTypeName = "BundleSenderTypeFlashbotBundleSenderTypeBeaverBundleSenderTypeRsyncBundleSenderTypeTitanBundleSenderTypeBloxrouteBundleSenderTypeAllBundleSenderTypeMevShareBundleSenderTypeBackrunPublicBundleSenderTypeMevBlockerBundleSenderTypeBlinkBundleSenderTypeMerkleBundleSenderTypeJetbldrBundleSenderTypePenguinBundleSenderTypeLokiBundleSenderTypeQuasarBundleSenderTypeBuilderNetBundleSenderTypeBTCSBundleSenderTypeBaseMainnet" -var _BundleSenderTypeIndex = [...]uint16{0, 24, 46, 67, 88, 113, 132, 156, 185, 211, 232, 254, 277, 300, 320, 342, 368, 388} +var _BundleSenderTypeIndex = [...]uint16{0, 24, 46, 67, 88, 113, 132, 156, 185, 211, 232, 254, 277, 300, 320, 342, 368, 388, 415} -const _BundleSenderTypeLowerName = "bundlesendertypeflashbotbundlesendertypebeaverbundlesendertypersyncbundlesendertypetitanbundlesendertypebloxroutebundlesendertypeallbundlesendertypemevsharebundlesendertypebackrunpublicbundlesendertypemevblockerbundlesendertypeblinkbundlesendertypemerklebundlesendertypejetbldrbundlesendertypepenguinbundlesendertypelokibundlesendertypequasarbundlesendertypebuildernetbundlesendertypebtcs" +const _BundleSenderTypeLowerName = "bundlesendertypeflashbotbundlesendertypebeaverbundlesendertypersyncbundlesendertypetitanbundlesendertypebloxroutebundlesendertypeallbundlesendertypemevsharebundlesendertypebackrunpublicbundlesendertypemevblockerbundlesendertypeblinkbundlesendertypemerklebundlesendertypejetbldrbundlesendertypepenguinbundlesendertypelokibundlesendertypequasarbundlesendertypebuildernetbundlesendertypebtcsbundlesendertypebasemainnet" func (i BundleSenderType) String() string { i -= 1 @@ -42,9 +42,10 @@ func _BundleSenderTypeNoOp() { _ = x[BundleSenderTypeQuasar-(15)] _ = x[BundleSenderTypeBuilderNet-(16)] _ = x[BundleSenderTypeBTCS-(17)] + _ = x[BundleSenderTypeBaseMainnet-(18)] } -var _BundleSenderTypeValues = []BundleSenderType{BundleSenderTypeFlashbot, BundleSenderTypeBeaver, BundleSenderTypeRsync, BundleSenderTypeTitan, BundleSenderTypeBloxroute, BundleSenderTypeAll, BundleSenderTypeMevShare, BundleSenderTypeBackrunPublic, BundleSenderTypeMevBlocker, BundleSenderTypeBlink, BundleSenderTypeMerkle, BundleSenderTypeJetbldr, BundleSenderTypePenguin, BundleSenderTypeLoki, BundleSenderTypeQuasar, BundleSenderTypeBuilderNet, BundleSenderTypeBTCS} +var _BundleSenderTypeValues = []BundleSenderType{BundleSenderTypeFlashbot, BundleSenderTypeBeaver, BundleSenderTypeRsync, BundleSenderTypeTitan, BundleSenderTypeBloxroute, BundleSenderTypeAll, BundleSenderTypeMevShare, BundleSenderTypeBackrunPublic, BundleSenderTypeMevBlocker, BundleSenderTypeBlink, BundleSenderTypeMerkle, BundleSenderTypeJetbldr, BundleSenderTypePenguin, BundleSenderTypeLoki, BundleSenderTypeQuasar, BundleSenderTypeBuilderNet, BundleSenderTypeBTCS, BundleSenderTypeBaseMainnet} var _BundleSenderTypeNameToValueMap = map[string]BundleSenderType{ _BundleSenderTypeName[0:24]: BundleSenderTypeFlashbot, @@ -81,6 +82,8 @@ var _BundleSenderTypeNameToValueMap = map[string]BundleSenderType{ _BundleSenderTypeLowerName[342:368]: BundleSenderTypeBuilderNet, _BundleSenderTypeName[368:388]: BundleSenderTypeBTCS, _BundleSenderTypeLowerName[368:388]: BundleSenderTypeBTCS, + _BundleSenderTypeName[388:415]: BundleSenderTypeBaseMainnet, + _BundleSenderTypeLowerName[388:415]: BundleSenderTypeBaseMainnet, } var _BundleSenderTypeNames = []string{ @@ -101,6 +104,7 @@ var _BundleSenderTypeNames = []string{ _BundleSenderTypeName[320:342], _BundleSenderTypeName[342:368], _BundleSenderTypeName[368:388], + _BundleSenderTypeName[388:415], } // BundleSenderTypeString retrieves an enum value from the enum constants string name. diff --git a/pkg/mev/pkg.go b/pkg/mev/pkg.go index c7ea509..1668e52 100644 --- a/pkg/mev/pkg.go +++ b/pkg/mev/pkg.go @@ -38,6 +38,7 @@ const ( BundleSenderTypeQuasar BundleSenderTypeBuilderNet BundleSenderTypeBTCS + BundleSenderTypeBaseMainnet ) const ( @@ -58,10 +59,25 @@ const ( FlashbotGetUserStats = "flashbots_getUserStats" FlashbotGetUserStatsV2 = "flashbots_getUserStatsV2" TitanGetUserStats = "titan_getUserStats" + ETHSendRawTransaction = "eth_sendRawTransaction" MaxBlockFromTarget = 3 ) +type ISendRawTransaction interface { + SendRawTransaction( + ctx context.Context, + tx *types.Transaction, + ) (SendRawTransactionResponse, error) +} + +type SendRawTransactionResponse struct { + Jsonrpc string `json:"jsonrpc"` + ID int `json:"id"` + Result string `json:"result"` + Error ErrorResponse `json:"error,omitempty"` +} + type IBackrunSender interface { SendBackrunBundle( ctx context.Context, From 6b706c22aa90a9a069dbfbc923f9066f9c70b348 Mon Sep 17 00:00:00 2001 From: datluongductuan Date: Wed, 24 Sep 2025 17:26:36 +0700 Subject: [PATCH 2/4] test: skip BaseChainSender interface compliance tests by default --- pkg/mev/base_chain_sender_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/mev/base_chain_sender_test.go b/pkg/mev/base_chain_sender_test.go index f3ea5ba..481c49e 100644 --- a/pkg/mev/base_chain_sender_test.go +++ b/pkg/mev/base_chain_sender_test.go @@ -124,6 +124,7 @@ func TestBaseChainSender_SendRawTransaction_InvalidTx(t *testing.T) { } func TestBaseChainSender_Interface_Compliance(t *testing.T) { + t.Skip("Skip by default - uncomment to run actual test against Base mainnet") // Test that BaseChainSender implements ISendRawTransaction interface httpClient := &http.Client{Timeout: time.Second * 10} sender := mev.NewBaseChainSender( @@ -137,6 +138,7 @@ func TestBaseChainSender_Interface_Compliance(t *testing.T) { } func TestNewBaseChainSender(t *testing.T) { + t.Skip("Skip by default - uncomment to run actual test against Base mainnet") httpClient := &http.Client{Timeout: time.Second * 10} endpoint := "https://mainnet.base.org" senderType := mev.BundleSenderTypeBaseMainnet From 7a50b94dd98e4324f7266375a1b71effc6aabd21 Mon Sep 17 00:00:00 2001 From: datluongductuan Date: Wed, 24 Sep 2025 17:52:17 +0700 Subject: [PATCH 3/4] chore: add git configuration for Go private module in CI workflow --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff8cefd..c102cac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,8 @@ jobs: needs: - prepare steps: + - name: Add git config for Go private module + run: git config --global url."https://${{ secrets.GH_PAT }}:x-oauth-basic@github.com/".insteadOf https://github.com/ - name: Checkout uses: actions/checkout@v4 - name: Install Go From b1ed3db5de6b69ba6200b5dfc25f3fc026dfe773 Mon Sep 17 00:00:00 2001 From: datluongductuan Date: Wed, 24 Sep 2025 17:57:02 +0700 Subject: [PATCH 4/4] chore: add git configuration step for Go private module in CI workflow --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c102cac..3e27147 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,6 +63,8 @@ jobs: needs: - prepare steps: + - name: Add git config for Go private module + run: git config --global url."https://${{ secrets.GH_PAT }}:x-oauth-basic@github.com/".insteadOf https://github.com/ - name: Checkout uses: actions/checkout@v4 - name: Install Go