package gateway 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) }