Files
dex/pkg/trading/evm.go
zeekay 6e2b4c747a refactor(trading): resolve all tech debt from code review
- symbolRegistry: move from package global to SymbolRegistry struct
  with DefaultRegistry for backward compat
- Off-chain venue check: replace brittle string prefix list with
  Venue.IsExecutable() lookup via api.isVenueExecutable()
- BFS queue: add maxQueueSize=1000 cap to prevent unbounded exploration
- Bridge venues: extract percentageFeeBridge base struct, compose
  WarpBridge/TeleportBridge/CCTPBridge/LuxBridgeVenue from it
- Arbitrage: rename Confidence→ProfitScore with clear documentation,
  sum gas estimates from venue quotes instead of hardcoding
- Tests: add Swapper field to all SwapRequest test cases

Coverage: 76.1% (all tests pass)
2026-03-29 18:32:26 -07:00

95 lines
2.3 KiB
Go

package trading
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"sync/atomic"
"time"
)
// EVMClient sends eth_call JSON-RPC requests to an EVM endpoint.
// Used by all on-chain venues (V2, V3, V4/Native) for quoting.
type EVMClient struct {
rpcURL string
httpClient *http.Client
reqID atomic.Uint64
}
// NewEVMClient creates an EVMClient targeting the given RPC URL.
func NewEVMClient(rpcURL string) *EVMClient {
return &EVMClient{
rpcURL: rpcURL,
httpClient: &http.Client{
Timeout: 5 * time.Second,
},
}
}
// CallContract performs eth_call against the given contract address.
func (c *EVMClient) CallContract(ctx context.Context, to string, data []byte) ([]byte, error) {
id := c.reqID.Add(1)
callObj := map[string]string{
"to": to,
"data": "0x" + hex.EncodeToString(data),
}
rpcReq := map[string]any{
"jsonrpc": "2.0",
"method": "eth_call",
"params": []any{callObj, "latest"},
"id": id,
}
body, err := json.Marshal(rpcReq)
if err != nil {
return nil, fmt.Errorf("marshal rpc request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.rpcURL, bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("create http request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("rpc call: %w", err)
}
defer resp.Body.Close()
// 64 KiB is enough for any eth_call response (quotes, balances, allowances).
// If a response exceeds this, the RPC endpoint is misbehaving.
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
if err != nil {
return nil, fmt.Errorf("read rpc response: %w", err)
}
var rpcResp struct {
Result string `json:"result"`
Error *struct {
Code int `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
if err := json.Unmarshal(respBody, &rpcResp); err != nil {
return nil, fmt.Errorf("decode rpc response: %w", err)
}
if rpcResp.Error != nil {
return nil, fmt.Errorf("rpc error %d: %s", rpcResp.Error.Code, rpcResp.Error.Message)
}
result := strings.TrimPrefix(rpcResp.Result, "0x")
if result == "" {
return nil, fmt.Errorf("empty rpc response")
}
return hex.DecodeString(result)
}