179 lines
5.2 KiB
Go
179 lines
5.2 KiB
Go
package track1
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// RPCGateway handles RPC passthrough with caching
|
|
type RPCGateway struct {
|
|
rpcURL string
|
|
httpClient *http.Client
|
|
cache Cache
|
|
rateLimit RateLimiter
|
|
}
|
|
|
|
// Cache interface for caching RPC responses
|
|
type Cache interface {
|
|
Get(key string) ([]byte, error)
|
|
Set(key string, value []byte, ttl time.Duration) error
|
|
}
|
|
|
|
// RateLimiter interface for rate limiting
|
|
type RateLimiter interface {
|
|
Allow(key string) bool
|
|
}
|
|
|
|
// NewRPCGateway creates a new RPC gateway
|
|
func NewRPCGateway(rpcURL string, cache Cache, rateLimit RateLimiter) *RPCGateway {
|
|
return &RPCGateway{
|
|
rpcURL: rpcURL,
|
|
httpClient: &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
},
|
|
cache: cache,
|
|
rateLimit: rateLimit,
|
|
}
|
|
}
|
|
|
|
// RPCRequest represents a JSON-RPC request
|
|
type RPCRequest struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
Method string `json:"method"`
|
|
Params []interface{} `json:"params"`
|
|
ID int `json:"id"`
|
|
}
|
|
|
|
// RPCResponse represents a JSON-RPC response
|
|
type RPCResponse struct {
|
|
JSONRPC string `json:"jsonrpc"`
|
|
Result interface{} `json:"result,omitempty"`
|
|
Error *RPCError `json:"error,omitempty"`
|
|
ID int `json:"id"`
|
|
}
|
|
|
|
// RPCError represents an RPC error
|
|
type RPCError struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
Data interface{} `json:"data,omitempty"`
|
|
}
|
|
|
|
// Call makes an RPC call with caching and rate limiting
|
|
func (g *RPCGateway) Call(ctx context.Context, method string, params []interface{}, cacheKey string, cacheTTL time.Duration) (*RPCResponse, error) {
|
|
// Check cache first
|
|
if cacheKey != "" {
|
|
if cached, err := g.cache.Get(cacheKey); err == nil {
|
|
var response RPCResponse
|
|
if err := json.Unmarshal(cached, &response); err == nil {
|
|
return &response, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check rate limit
|
|
if !g.rateLimit.Allow("rpc") {
|
|
return nil, fmt.Errorf("rate limit exceeded")
|
|
}
|
|
|
|
// Make RPC call
|
|
req := RPCRequest{
|
|
JSONRPC: "2.0",
|
|
Method: method,
|
|
Params: params,
|
|
ID: 1,
|
|
}
|
|
|
|
reqBody, err := json.Marshal(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
|
}
|
|
|
|
httpReq, err := http.NewRequestWithContext(ctx, "POST", g.rpcURL, bytes.NewBuffer(reqBody))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
httpReq.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := g.httpClient.Do(httpReq)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("RPC call failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("RPC returned status %d", resp.StatusCode)
|
|
}
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
|
}
|
|
|
|
var rpcResp RPCResponse
|
|
if err := json.Unmarshal(body, &rpcResp); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
|
}
|
|
|
|
if rpcResp.Error != nil {
|
|
return nil, fmt.Errorf("RPC error: %s (code: %d)", rpcResp.Error.Message, rpcResp.Error.Code)
|
|
}
|
|
|
|
// Cache response if cache key provided
|
|
if cacheKey != "" && rpcResp.Result != nil {
|
|
if cacheData, err := json.Marshal(rpcResp); err == nil {
|
|
g.cache.Set(cacheKey, cacheData, cacheTTL)
|
|
}
|
|
}
|
|
|
|
return &rpcResp, nil
|
|
}
|
|
|
|
// GetBlockByNumber gets a block by number
|
|
func (g *RPCGateway) GetBlockByNumber(ctx context.Context, blockNumber string, includeTxs bool) (*RPCResponse, error) {
|
|
cacheKey := fmt.Sprintf("block:%s:%v", blockNumber, includeTxs)
|
|
return g.Call(ctx, "eth_getBlockByNumber", []interface{}{blockNumber, includeTxs}, cacheKey, 10*time.Second)
|
|
}
|
|
|
|
// GetBlockByHash gets a block by hash
|
|
func (g *RPCGateway) GetBlockByHash(ctx context.Context, blockHash string, includeTxs bool) (*RPCResponse, error) {
|
|
cacheKey := fmt.Sprintf("block_hash:%s:%v", blockHash, includeTxs)
|
|
return g.Call(ctx, "eth_getBlockByHash", []interface{}{blockHash, includeTxs}, cacheKey, 10*time.Second)
|
|
}
|
|
|
|
// GetTransactionByHash gets a transaction by hash
|
|
func (g *RPCGateway) GetTransactionByHash(ctx context.Context, txHash string) (*RPCResponse, error) {
|
|
cacheKey := fmt.Sprintf("tx:%s", txHash)
|
|
return g.Call(ctx, "eth_getTransactionByHash", []interface{}{txHash}, cacheKey, 30*time.Second)
|
|
}
|
|
|
|
// GetBalance gets an address balance
|
|
func (g *RPCGateway) GetBalance(ctx context.Context, address string, blockNumber string) (*RPCResponse, error) {
|
|
if blockNumber == "" {
|
|
blockNumber = "latest"
|
|
}
|
|
cacheKey := fmt.Sprintf("balance:%s:%s", address, blockNumber)
|
|
return g.Call(ctx, "eth_getBalance", []interface{}{address, blockNumber}, cacheKey, 10*time.Second)
|
|
}
|
|
|
|
// GetBlockNumber gets the latest block number
|
|
func (g *RPCGateway) GetBlockNumber(ctx context.Context) (*RPCResponse, error) {
|
|
return g.Call(ctx, "eth_blockNumber", []interface{}{}, "block_number", 5*time.Second)
|
|
}
|
|
|
|
// GetTransactionCount gets transaction count for an address
|
|
func (g *RPCGateway) GetTransactionCount(ctx context.Context, address string, blockNumber string) (*RPCResponse, error) {
|
|
if blockNumber == "" {
|
|
blockNumber = "latest"
|
|
}
|
|
cacheKey := fmt.Sprintf("tx_count:%s:%s", address, blockNumber)
|
|
return g.Call(ctx, "eth_getTransactionCount", []interface{}{address, blockNumber}, cacheKey, 10*time.Second)
|
|
}
|
|
|