Skip to content

Commit 31df5c2

Browse files
committed
feat: add example gRPC client with convenient wrappers for bundle simulation, submission, batch storage reads, pending transactions, and contract calls, demonstrating low-latency API usage patterns for high-frequency trading
1 parent f4f2213 commit 31df5c2

File tree

1 file changed

+272
-0
lines changed

1 file changed

+272
-0
lines changed

api/grpc/example_client.go

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
// Copyright 2024 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
// Package grpc provides an example client for the low-latency gRPC trading API.
18+
// This example demonstrates how to:
19+
// - Connect to the gRPC server
20+
// - Submit transaction bundles
21+
// - Simulate bundle execution
22+
// - Perform batch storage reads
23+
// - Subscribe to pending transactions
24+
package grpc
25+
26+
import (
27+
"context"
28+
"fmt"
29+
"math/big"
30+
"time"
31+
32+
"github.com/ethereum/go-ethereum/common"
33+
"github.com/ethereum/go-ethereum/core/types"
34+
"google.golang.org/grpc"
35+
"google.golang.org/grpc/credentials/insecure"
36+
)
37+
38+
// Client wraps the gRPC TraderService client with convenient methods.
39+
type Client struct {
40+
conn *grpc.ClientConn
41+
client TraderServiceClient
42+
}
43+
44+
// NewClient creates a new gRPC client connected to the specified host and port.
45+
func NewClient(host string, port int) (*Client, error) {
46+
addr := fmt.Sprintf("%s:%d", host, port)
47+
conn, err := grpc.NewClient(addr,
48+
grpc.WithTransportCredentials(insecure.NewCredentials()),
49+
grpc.WithDefaultCallOptions(
50+
grpc.MaxCallRecvMsgSize(100*1024*1024), // 100MB
51+
grpc.MaxCallSendMsgSize(100*1024*1024), // 100MB
52+
),
53+
)
54+
if err != nil {
55+
return nil, fmt.Errorf("failed to connect to %s: %w", addr, err)
56+
}
57+
58+
return &Client{
59+
conn: conn,
60+
client: NewTraderServiceClient(conn),
61+
}, nil
62+
}
63+
64+
// Close closes the gRPC connection.
65+
func (c *Client) Close() error {
66+
return c.conn.Close()
67+
}
68+
69+
// SimulateBundle simulates a bundle of transactions and returns the results.
70+
// This is useful for testing bundle profitability before submission.
71+
func (c *Client) SimulateBundle(ctx context.Context, txs []*types.Transaction, opts *BundleOptions) (*SimulateBundleResponse, error) {
72+
// Encode transactions
73+
encodedTxs := make([][]byte, len(txs))
74+
for i, tx := range txs {
75+
encoded, err := tx.MarshalBinary()
76+
if err != nil {
77+
return nil, fmt.Errorf("failed to encode transaction %d: %w", i, err)
78+
}
79+
encodedTxs[i] = encoded
80+
}
81+
82+
req := &SimulateBundleRequest{
83+
Transactions: encodedTxs,
84+
}
85+
86+
if opts != nil {
87+
req.MinTimestamp = opts.MinTimestamp
88+
req.MaxTimestamp = opts.MaxTimestamp
89+
req.TargetBlock = opts.TargetBlock
90+
req.RevertingTxs = opts.RevertingTxIndices
91+
}
92+
93+
return c.client.SimulateBundle(ctx, req)
94+
}
95+
96+
// SubmitBundle submits a bundle for inclusion in future blocks.
97+
func (c *Client) SubmitBundle(ctx context.Context, txs []*types.Transaction, opts *BundleOptions) (common.Hash, error) {
98+
// Encode transactions
99+
encodedTxs := make([][]byte, len(txs))
100+
for i, tx := range txs {
101+
encoded, err := tx.MarshalBinary()
102+
if err != nil {
103+
return common.Hash{}, fmt.Errorf("failed to encode transaction %d: %w", i, err)
104+
}
105+
encodedTxs[i] = encoded
106+
}
107+
108+
req := &SubmitBundleRequest{
109+
Transactions: encodedTxs,
110+
}
111+
112+
if opts != nil {
113+
req.MinTimestamp = opts.MinTimestamp
114+
req.MaxTimestamp = opts.MaxTimestamp
115+
req.TargetBlock = opts.TargetBlock
116+
req.RevertingTxs = opts.RevertingTxIndices
117+
}
118+
119+
resp, err := c.client.SubmitBundle(ctx, req)
120+
if err != nil {
121+
return common.Hash{}, err
122+
}
123+
124+
return common.BytesToHash(resp.BundleHash), nil
125+
}
126+
127+
// GetStorageBatch retrieves multiple storage slots in a single call.
128+
// This is significantly faster than multiple eth_getStorageAt JSON-RPC calls.
129+
func (c *Client) GetStorageBatch(ctx context.Context, contract common.Address, slots []common.Hash, blockNum *uint64) ([]common.Hash, error) {
130+
encodedSlots := make([][]byte, len(slots))
131+
for i, slot := range slots {
132+
encodedSlots[i] = slot.Bytes()
133+
}
134+
135+
req := &GetStorageBatchRequest{
136+
Contract: contract.Bytes(),
137+
Slots: encodedSlots,
138+
BlockNumber: blockNum,
139+
}
140+
141+
resp, err := c.client.GetStorageBatch(ctx, req)
142+
if err != nil {
143+
return nil, err
144+
}
145+
146+
values := make([]common.Hash, len(resp.Values))
147+
for i, val := range resp.Values {
148+
values[i] = common.BytesToHash(val)
149+
}
150+
151+
return values, nil
152+
}
153+
154+
// GetPendingTransactions retrieves currently pending transactions.
155+
func (c *Client) GetPendingTransactions(ctx context.Context, minGasPrice *uint64) ([]*types.Transaction, error) {
156+
req := &GetPendingTransactionsRequest{
157+
MinGasPrice: minGasPrice,
158+
}
159+
160+
resp, err := c.client.GetPendingTransactions(ctx, req)
161+
if err != nil {
162+
return nil, err
163+
}
164+
165+
txs := make([]*types.Transaction, 0, len(resp.Transactions))
166+
for i, encoded := range resp.Transactions {
167+
tx := new(types.Transaction)
168+
if err := tx.UnmarshalBinary(encoded); err != nil {
169+
return nil, fmt.Errorf("failed to decode transaction %d: %w", i, err)
170+
}
171+
txs = append(txs, tx)
172+
}
173+
174+
return txs, nil
175+
}
176+
177+
// CallContract executes a contract call.
178+
func (c *Client) CallContract(ctx context.Context, msg *CallMessage, blockNum *uint64) (*CallContractResponse, error) {
179+
req := &CallContractRequest{
180+
From: msg.From.Bytes(),
181+
To: msg.To.Bytes(),
182+
Data: msg.Data,
183+
BlockNumber: blockNum,
184+
}
185+
186+
if msg.Gas != nil {
187+
req.Gas = msg.Gas
188+
}
189+
if msg.GasPrice != nil {
190+
req.GasPrice = msg.GasPrice
191+
}
192+
if msg.Value != nil {
193+
req.Value = msg.Value.Bytes()
194+
}
195+
196+
return c.client.CallContract(ctx, req)
197+
}
198+
199+
// BundleOptions contains optional parameters for bundle submission and simulation.
200+
type BundleOptions struct {
201+
MinTimestamp *uint64
202+
MaxTimestamp *uint64
203+
TargetBlock *uint64
204+
RevertingTxIndices []int32
205+
}
206+
207+
// CallMessage contains parameters for contract calls.
208+
type CallMessage struct {
209+
From common.Address
210+
To common.Address
211+
Data []byte
212+
Gas *uint64
213+
GasPrice *uint64
214+
Value *big.Int
215+
}
216+
217+
// ExampleUsage demonstrates typical usage patterns for high-frequency trading.
218+
func ExampleUsage() error {
219+
// Connect to gRPC server
220+
client, err := NewClient("localhost", 9090)
221+
if err != nil {
222+
return fmt.Errorf("failed to create client: %w", err)
223+
}
224+
defer client.Close()
225+
226+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
227+
defer cancel()
228+
229+
// Example 1: Batch storage read (e.g., reading Uniswap pool reserves)
230+
poolAddress := common.HexToAddress("0x...")
231+
slot0 := common.HexToHash("0x0") // slot0 contains sqrtPriceX96 and tick
232+
slot1 := common.HexToHash("0x1") // Other pool data
233+
234+
values, err := client.GetStorageBatch(ctx, poolAddress, []common.Hash{slot0, slot1}, nil)
235+
if err != nil {
236+
return fmt.Errorf("failed to get storage: %w", err)
237+
}
238+
fmt.Printf("Pool state: %v\n", values)
239+
240+
// Example 2: Simulate a bundle before submission
241+
// (assuming you have prepared transactions)
242+
var txs []*types.Transaction // ... your transactions
243+
244+
simResult, err := client.SimulateBundle(ctx, txs, &BundleOptions{
245+
TargetBlock: func() *uint64 { b := uint64(12345678); return &b }(),
246+
})
247+
if err != nil {
248+
return fmt.Errorf("failed to simulate bundle: %w", err)
249+
}
250+
251+
if !simResult.Success {
252+
fmt.Printf("Bundle simulation failed at tx %d: %s\n", simResult.FailedTxIndex, simResult.FailedTxError)
253+
return nil
254+
}
255+
256+
profit := new(big.Int).SetBytes(simResult.Profit)
257+
fmt.Printf("Bundle profit: %s wei, gas used: %d\n", profit.String(), simResult.GasUsed)
258+
259+
// Example 3: Submit bundle if profitable
260+
if profit.Sign() > 0 {
261+
bundleHash, err := client.SubmitBundle(ctx, txs, &BundleOptions{
262+
TargetBlock: func() *uint64 { b := uint64(12345678); return &b }(),
263+
})
264+
if err != nil {
265+
return fmt.Errorf("failed to submit bundle: %w", err)
266+
}
267+
fmt.Printf("Bundle submitted: %s\n", bundleHash.Hex())
268+
}
269+
270+
return nil
271+
}
272+

0 commit comments

Comments
 (0)