mirror of
https://github.com/luxfi/indexer.git
synced 2026-10-30 16:22:36 +00:00
The index could hold a block without its transactions. Before v1.5.9 a null receipt became an empty transaction, stored under the empty hash, and every later miss merged into that one row. The blocks stayed and counted their transactions, and nothing read them again, so Zoo mainnet holds 800 blocks and 2 of 811 transactions. - A transaction without a hash is refused with its block. - The block row is written last, after its transactions, logs and transfers. Rows at that height under another block hash are deleted. - indexBlock can be repeated. Transactions are replaced, and address counts and token balances move only for rows not already stored. - The audit deletes hashless rows, then reads again every block whose transaction rows do not match its tx_count or its hash. Totals are compared first, so a consistent index costs two range reads. A repair runs 256 heights per pass, with a pass after every head poll. - A node that answers eth_getBlockReceipts with MethodNotFound (zood) is read per transaction with eth_getTransactionReceipt, and never asked for block receipts again. A null receipt is still ErrNoBlock. Any other receipt error is retried instead of storing transactions without their results. - The transports return JSON-RPC errors as transport.RPCError with their code. - An empty index starts at StartBlock. An index holding only block 0 resumes at 1 instead of re-reading block 0 on every poll. Co-authored-by: Hanzo Dev <[email protected]>
169 lines
4.1 KiB
Go
169 lines
4.1 KiB
Go
// Copyright (c) 2025 Lux Industries Inc
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package transport
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
// HTTP implements Transport over standard JSON-RPC.
|
|
type HTTP struct {
|
|
endpoint string
|
|
client *http.Client
|
|
reqID atomic.Uint64
|
|
}
|
|
|
|
// NewHTTP creates an HTTP JSON-RPC transport.
|
|
func NewHTTP(endpoint string) *HTTP {
|
|
return &HTTP{
|
|
endpoint: endpoint,
|
|
client: &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Call makes a single JSON-RPC call.
|
|
func (h *HTTP) Call(ctx context.Context, method string, params ...any) (json.RawMessage, error) {
|
|
if len(params) == 0 {
|
|
params = []any{}
|
|
}
|
|
id := h.reqID.Add(1)
|
|
|
|
reqBody, err := json.Marshal(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"method": method,
|
|
"params": params,
|
|
"id": id,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal request: %w", err)
|
|
}
|
|
|
|
return h.doRequest(ctx, reqBody)
|
|
}
|
|
|
|
// BatchCall makes multiple JSON-RPC calls in a single HTTP request.
|
|
func (h *HTTP) BatchCall(ctx context.Context, calls []RPCCall) ([]json.RawMessage, error) {
|
|
if len(calls) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
batch := make([]map[string]any, len(calls))
|
|
for i, c := range calls {
|
|
params := c.Params
|
|
if len(params) == 0 {
|
|
params = []any{}
|
|
}
|
|
batch[i] = map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"method": c.Method,
|
|
"params": params,
|
|
"id": h.reqID.Add(1),
|
|
}
|
|
}
|
|
|
|
reqBody, err := json.Marshal(batch)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal batch request: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", h.endpoint, bytes.NewReader(reqBody))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := h.client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("http request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("http status %d from %s", resp.StatusCode, h.endpoint)
|
|
}
|
|
|
|
var results []struct {
|
|
Result json.RawMessage `json:"result"`
|
|
Error *RPCError `json:"error"`
|
|
ID uint64 `json:"id"`
|
|
}
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&results); err != nil {
|
|
return nil, fmt.Errorf("decode batch response: %w", err)
|
|
}
|
|
|
|
out := make([]json.RawMessage, len(results))
|
|
for i, r := range results {
|
|
if r.Error != nil {
|
|
return nil, fmt.Errorf("batch[%d]: %w", i, r.Error)
|
|
}
|
|
out[i] = r.Result
|
|
}
|
|
|
|
return out, nil
|
|
}
|
|
|
|
// Subscribe is not supported over HTTP. Returns an error.
|
|
func (h *HTTP) Subscribe(_ context.Context, _ string, _ ...any) (<-chan json.RawMessage, error) {
|
|
return nil, fmt.Errorf("subscribe not supported over HTTP transport")
|
|
}
|
|
|
|
// Close is a no-op for HTTP (connections are pooled by http.Client).
|
|
func (h *HTTP) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func (h *HTTP) doRequest(ctx context.Context, body []byte) (json.RawMessage, error) {
|
|
req, err := http.NewRequestWithContext(ctx, "POST", h.endpoint, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := h.client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("http request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("http status %d from %s", resp.StatusCode, h.endpoint)
|
|
}
|
|
|
|
var result struct {
|
|
Result json.RawMessage `json:"result"`
|
|
Error *RPCError `json:"error"`
|
|
}
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, fmt.Errorf("decode response: %w", err)
|
|
}
|
|
|
|
if result.Error != nil {
|
|
return nil, result.Error
|
|
}
|
|
|
|
return result.Result, nil
|
|
}
|
|
|
|
// RPCError is the error object a JSON-RPC server answers with. Its code is
|
|
// what tells "this node has no such method" (MethodNotFound) from a failure.
|
|
type RPCError struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
// MethodNotFound is the JSON-RPC code for a method the server does not serve.
|
|
const MethodNotFound = -32601
|
|
|
|
func (e *RPCError) Error() string { return fmt.Sprintf("rpc error %d: %s", e.Code, e.Message) }
|