Skip to content

Commit ff67ef8

Browse files
feat: add BaseChainSender implementation and tests for sending raw tr… (#173)
* feat: add BaseChainSender implementation and tests for sending raw transactions * test: skip BaseChainSender interface compliance tests by default * chore: add git configuration for Go private module in CI workflow * chore: add git configuration step for Go private module in CI workflow * feat: update BundleSenderType to include L2 and add L2ChainSender implementation with tests * fix: change BundleSenderType from BaseMainnet to L2 in layer2_sender_test.go
1 parent 670c855 commit ff67ef8

File tree

5 files changed

+251
-4
lines changed

5 files changed

+251
-4
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ jobs:
2929
needs:
3030
- prepare
3131
steps:
32+
- name: Add git config for Go private module
33+
run: git config --global url."https://${{ secrets.GH_PAT }}:[email protected]/".insteadOf https://github.com/
3234
- name: Checkout
3335
uses: actions/checkout@v4
3436
- name: Install Go
@@ -61,6 +63,8 @@ jobs:
6163
needs:
6264
- prepare
6365
steps:
66+
- name: Add git config for Go private module
67+
run: git config --global url."https://${{ secrets.GH_PAT }}:[email protected]/".insteadOf https://github.com/
6468
- name: Checkout
6569
uses: actions/checkout@v4
6670
- name: Install Go

pkg/mev/bundlesendertype_enumer.go

Lines changed: 8 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/mev/layer2_sender.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package mev
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"net/http"
9+
10+
"github.com/ethereum/go-ethereum/common/hexutil"
11+
"github.com/ethereum/go-ethereum/core/types"
12+
)
13+
14+
type L2Sender struct {
15+
c *http.Client
16+
endpoint string
17+
senderType BundleSenderType
18+
}
19+
20+
func NewL2ChainSender(
21+
c *http.Client,
22+
endpoint string,
23+
senderType BundleSenderType,
24+
) *L2Sender {
25+
return &L2Sender{
26+
c: c,
27+
endpoint: endpoint,
28+
senderType: senderType,
29+
}
30+
}
31+
32+
func (s *L2Sender) GetSenderType() BundleSenderType {
33+
return s.senderType
34+
}
35+
36+
func (s *L2Sender) SendRawTransaction(
37+
ctx context.Context,
38+
tx *types.Transaction,
39+
) (SendRawTransactionResponse, error) {
40+
txBin, err := tx.MarshalBinary()
41+
if err != nil {
42+
return SendRawTransactionResponse{}, fmt.Errorf("marshal tx binary: %w", err)
43+
}
44+
45+
req := SendRequest{
46+
ID: SendBundleID,
47+
JSONRPC: JSONRPC2,
48+
Method: ETHSendRawTransaction,
49+
Params: []any{hexutil.Encode(txBin)},
50+
}
51+
52+
reqBody, err := json.Marshal(req)
53+
if err != nil {
54+
return SendRawTransactionResponse{}, fmt.Errorf("marshal json error: %w", err)
55+
}
56+
57+
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, s.endpoint, bytes.NewBuffer(reqBody))
58+
if err != nil {
59+
return SendRawTransactionResponse{}, fmt.Errorf("new http request error: %w", err)
60+
}
61+
62+
resp, err := doRequest[SendRawTransactionResponse](s.c, httpReq)
63+
if err != nil {
64+
return SendRawTransactionResponse{}, err
65+
}
66+
67+
if len(resp.Error.Messange) != 0 {
68+
return SendRawTransactionResponse{}, fmt.Errorf("response error, code: [%d], message: [%s]",
69+
resp.Error.Code, resp.Error.Messange)
70+
}
71+
72+
return resp, nil
73+
}

pkg/mev/layer2_sender_test.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package mev_test
2+
3+
import (
4+
"context"
5+
"math/big"
6+
"net/http"
7+
"testing"
8+
"time"
9+
10+
"github.com/KyberNetwork/tradinglib/pkg/mev"
11+
"github.com/ethereum/go-ethereum/common"
12+
"github.com/ethereum/go-ethereum/core/types"
13+
"github.com/ethereum/go-ethereum/crypto"
14+
"github.com/ethereum/go-ethereum/ethclient"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
func TestBaseChainSender_SendRawTransaction(t *testing.T) {
19+
t.Skip("Skip by default - uncomment to run actual test against Base mainnet")
20+
21+
// Create HTTP client
22+
httpClient := &http.Client{Timeout: time.Second * 30}
23+
24+
// Initialize BaseChainSender with Base mainnet RPC
25+
sender := mev.NewL2ChainSender(
26+
httpClient,
27+
"https://mainnet.base.org",
28+
mev.BundleSenderTypeL2,
29+
)
30+
31+
// Verify sender type
32+
require.Equal(t, mev.BundleSenderTypeL2, sender.GetSenderType())
33+
34+
// Create a test transaction (this is a dummy transaction that will likely fail)
35+
// In a real scenario, you would use proper private key, nonce, gas price, etc.
36+
privateKey, err := crypto.GenerateKey()
37+
require.NoError(t, err)
38+
39+
// Connect to Base mainnet to get current gas price and nonce
40+
ethClient, err := ethclient.Dial("https://mainnet.base.org")
41+
require.NoError(t, err)
42+
43+
fromAddress := crypto.PubkeyToAddress(privateKey.PublicKey)
44+
nonce, err := ethClient.PendingNonceAt(context.Background(), fromAddress)
45+
require.NoError(t, err)
46+
47+
gasPrice, err := ethClient.SuggestGasPrice(context.Background())
48+
require.NoError(t, err)
49+
50+
// Create a simple ETH transfer transaction
51+
toAddress := common.HexToAddress("0x0000000000000000000000000000000000000001") // Burn address
52+
value := big.NewInt(1) // 1 wei
53+
gasLimit := uint64(21000) // Standard ETH transfer gas
54+
55+
// Get chain ID for Base mainnet (8453)
56+
chainID, err := ethClient.ChainID(context.Background())
57+
require.NoError(t, err)
58+
require.Equal(t, int64(8453), chainID.Int64()) // Base mainnet chain ID
59+
60+
// Create transaction
61+
tx := types.NewTransaction(nonce, toAddress, value, gasLimit, gasPrice, nil)
62+
63+
// Sign transaction
64+
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
65+
require.NoError(t, err)
66+
67+
// Send transaction using BaseChainSender
68+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
69+
defer cancel()
70+
71+
resp, err := sender.SendRawTransaction(ctx, signedTx)
72+
73+
// Log the response for debugging
74+
t.Logf("Response: %+v", resp)
75+
t.Logf("Error: %v", err)
76+
77+
// The transaction will likely fail due to insufficient funds, but we should get a proper response
78+
// We expect either a successful response with transaction hash or a proper error response
79+
if err != nil {
80+
// Check if it's a proper RPC error (not a network/parsing error)
81+
t.Logf("Expected error due to insufficient funds or other RPC error: %v", err)
82+
} else {
83+
// If successful, verify response structure
84+
require.Equal(t, "2.0", resp.Jsonrpc)
85+
require.Equal(t, 1, resp.ID)
86+
require.NotEmpty(t, resp.Result)
87+
t.Logf("Transaction hash: %s", resp.Result)
88+
}
89+
}
90+
91+
func TestBaseChainSender_SendRawTransaction_InvalidTx(t *testing.T) {
92+
t.Skip("Skip by default - uncomment to run actual test against Base mainnet")
93+
// Create HTTP client
94+
httpClient := &http.Client{Timeout: time.Second * 10}
95+
96+
// Initialize BaseChainSender with Base mainnet RPC
97+
sender := mev.NewL2ChainSender(
98+
httpClient,
99+
"https://mainnet.base.org",
100+
mev.BundleSenderTypeL2,
101+
)
102+
103+
// Create an invalid transaction (unsigned)
104+
toAddress := common.HexToAddress("0x0000000000000000000000000000000000000001")
105+
value := big.NewInt(1)
106+
gasLimit := uint64(21000)
107+
gasPrice := big.NewInt(1000000000) // 1 gwei
108+
109+
// Create unsigned transaction
110+
tx := types.NewTransaction(0, toAddress, value, gasLimit, gasPrice, nil)
111+
112+
// Try to send unsigned transaction (should fail)
113+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
114+
defer cancel()
115+
116+
resp, err := sender.SendRawTransaction(ctx, tx)
117+
118+
// Should get an error due to invalid transaction
119+
t.Logf("Response: %+v", resp)
120+
t.Logf("Error: %v", err)
121+
122+
// We expect an error here
123+
require.Error(t, err)
124+
}
125+
126+
func TestBaseChainSender_Interface_Compliance(t *testing.T) {
127+
t.Skip("Skip by default - uncomment to run actual test against Base mainnet")
128+
// Test that BaseChainSender implements ISendRawTransaction interface
129+
httpClient := &http.Client{Timeout: time.Second * 10}
130+
sender := mev.NewL2ChainSender(
131+
httpClient,
132+
"https://mainnet.base.org",
133+
mev.BundleSenderTypeL2,
134+
)
135+
136+
// Verify it implements the interface
137+
var _ mev.ISendRawTransaction = sender
138+
}
139+
140+
func TestNewBaseChainSender(t *testing.T) {
141+
t.Skip("Skip by default - uncomment to run actual test against Base mainnet")
142+
httpClient := &http.Client{Timeout: time.Second * 10}
143+
endpoint := "https://mainnet.base.org"
144+
senderType := mev.BundleSenderTypeL2
145+
146+
sender := mev.NewL2ChainSender(httpClient, endpoint, senderType)
147+
148+
require.NotNil(t, sender)
149+
require.Equal(t, senderType, sender.GetSenderType())
150+
}

pkg/mev/pkg.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const (
3838
BundleSenderTypeQuasar
3939
BundleSenderTypeBuilderNet
4040
BundleSenderTypeBTCS
41+
BundleSenderTypeL2
4142
)
4243

4344
const (
@@ -58,10 +59,25 @@ const (
5859
FlashbotGetUserStats = "flashbots_getUserStats"
5960
FlashbotGetUserStatsV2 = "flashbots_getUserStatsV2"
6061
TitanGetUserStats = "titan_getUserStats"
62+
ETHSendRawTransaction = "eth_sendRawTransaction"
6163

6264
MaxBlockFromTarget = 3
6365
)
6466

67+
type ISendRawTransaction interface {
68+
SendRawTransaction(
69+
ctx context.Context,
70+
tx *types.Transaction,
71+
) (SendRawTransactionResponse, error)
72+
}
73+
74+
type SendRawTransactionResponse struct {
75+
Jsonrpc string `json:"jsonrpc"`
76+
ID int `json:"id"`
77+
Result string `json:"result"`
78+
Error ErrorResponse `json:"error,omitempty"`
79+
}
80+
6581
type IBackrunSender interface {
6682
SendBackrunBundle(
6783
ctx context.Context,

0 commit comments

Comments
 (0)