mirror of
https://github.com/luxfi/graph.git
synced 2026-09-24 00:26:07 +00:00
Initial commit: Lux Graph Engine
Single-binary GraphQL query layer replacing The Graph's Graph Node. SQLite WAL + hanzoai/replicate S3 streaming + luxfi/age PQ encryption. - 18 schemas: AMM V2/V3/V4, DEX, FHE, Platform, XChain, Bridge, Privacy, Quantum, Key, AI, Identity, Oracle, Relay, ServiceNode, Precompile, ERC20, ERC721 - WASM subgraph loader (uni-v2/v3/v4 build/ dirs) - EVM indexer: eth_blockNumber + eth_getLogs + V2/V3/V4 event decoding - Production hardened: rate limiter, query depth/field caps, MaxBytesReader, structured logging, /ready, /metrics - 32 e2e tests, 13 benchmarks, 775 qps @ 100 clients on SQLite - Dockerfile (multi-stage CGO, non-root, healthcheck)
This commit is contained in:
+23
@@ -0,0 +1,23 @@
|
||||
# Stage 1: Build
|
||||
FROM golang:1.26-alpine AS build
|
||||
RUN apk add --no-cache gcc musl-dev
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
ARG VERSION=dev
|
||||
RUN CGO_ENABLED=1 GOOS=linux go build \
|
||||
-ldflags "-s -w -X main.version=${VERSION}" \
|
||||
-o /graph ./cmd/graph
|
||||
|
||||
# Stage 2: Runtime
|
||||
FROM alpine:3.21
|
||||
RUN apk add --no-cache ca-certificates tzdata \
|
||||
&& addgroup -S graph && adduser -S graph -G graph
|
||||
COPY --from=build /graph /usr/local/bin/graph
|
||||
USER graph
|
||||
VOLUME /data
|
||||
EXPOSE 8080
|
||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD ["graph", "--version"]
|
||||
ENTRYPOINT ["graph"]
|
||||
@@ -0,0 +1,13 @@
|
||||
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
||||
LDFLAGS := -ldflags "-s -w -X main.version=$(VERSION)"
|
||||
|
||||
build:
|
||||
go build $(LDFLAGS) -o bin/graph ./cmd/graph
|
||||
|
||||
test:
|
||||
go test -v ./...
|
||||
|
||||
clean:
|
||||
rm -rf bin/
|
||||
|
||||
.PHONY: build test clean
|
||||
@@ -0,0 +1,262 @@
|
||||
// graph -- standalone GraphQL query layer for any EVM chain.
|
||||
//
|
||||
// Connects to an RPC endpoint, indexes events into local SQLite/ZapDB,
|
||||
// serves a GraphQL API compatible with The Graph subgraph queries.
|
||||
// Also runs as a Lux VM (G-Chain) when loaded by luxd as a plugin.
|
||||
//
|
||||
// Standalone:
|
||||
//
|
||||
// graph --rpc=http://node:8545 # any EVM
|
||||
// graph --rpc=http://node:9650/ext/bc/{chainID}/rpc # any Lux subnet
|
||||
// graph --config=subgraphs.yaml # multi-subgraph
|
||||
// graph --rpc=http://node:8545 --schema=uniswap-v2 # specific schema
|
||||
//
|
||||
// Scale horizontally: run N graph nodes behind a load balancer.
|
||||
// Each indexes independently into its own local storage.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/graph/engine"
|
||||
"github.com/luxfi/graph/indexer"
|
||||
"github.com/luxfi/graph/storage"
|
||||
)
|
||||
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
var (
|
||||
rpcEndpoint = flag.String("rpc", "", "EVM JSON-RPC endpoint")
|
||||
httpAddr = flag.String("http", ":8080", "GraphQL HTTP listen address")
|
||||
dataDir = flag.String("data", "", "Data directory (default: ~/.graph/data)")
|
||||
schemaName = flag.String("schema", "", "Built-in schema: amm, amm-v2, amm-v3, amm-v4, erc20, erc721, all")
|
||||
configFile = flag.String("config", "", "Subgraph config YAML")
|
||||
showVersion = flag.Bool("version", false, "Show version")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if *showVersion {
|
||||
fmt.Printf("graph %s\n", version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
|
||||
slog.SetDefault(logger)
|
||||
|
||||
if *rpcEndpoint == "" {
|
||||
*rpcEndpoint = os.Getenv("RPC_ENDPOINT")
|
||||
}
|
||||
if *rpcEndpoint == "" {
|
||||
fmt.Fprintln(os.Stderr, "graph: --rpc or RPC_ENDPOINT required")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if *dataDir == "" {
|
||||
*dataDir = envOr("DATA_DIR", filepath.Join(homeDir(), ".graph", "data"))
|
||||
}
|
||||
|
||||
slog.Info("starting", "version", version, "rpc", *rpcEndpoint)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
|
||||
go func() { <-sig; cancel() }()
|
||||
|
||||
// Storage: SQLite WAL (default) or in-memory (nosqlite build)
|
||||
store, err := storage.New(*dataDir)
|
||||
if err != nil {
|
||||
slog.Error("storage", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
if err := store.Init(ctx); err != nil {
|
||||
slog.Error("storage init", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// WAL streaming to S3 with PQ encryption (noop if REPLICATE_S3_ENDPOINT unset)
|
||||
stopReplicate := storage.StartReplicate(filepath.Join(*dataDir, "graph.db"))
|
||||
defer stopReplicate()
|
||||
|
||||
// Indexer: subscribe to EVM events via RPC
|
||||
idx := indexer.New(*rpcEndpoint, store)
|
||||
go func() {
|
||||
if err := idx.Run(ctx); err != nil && ctx.Err() == nil {
|
||||
slog.Error("indexer", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
// Query engine: GraphQL over indexed data
|
||||
eng := engine.New(store, &engine.Config{
|
||||
MaxQueryDepth: 10,
|
||||
MaxResultSize: 1 << 20, // 1MB
|
||||
QueryTimeoutMs: 30000,
|
||||
})
|
||||
|
||||
// Load schema
|
||||
if *configFile != "" {
|
||||
if err := eng.LoadConfig(*configFile); err != nil {
|
||||
slog.Error("config", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else if *schemaName != "" {
|
||||
if err := eng.LoadBuiltin(*schemaName); err != nil {
|
||||
slog.Error("schema", "name", *schemaName, "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
// Default: full DEX schema (v2+v3 compatible)
|
||||
eng.LoadBuiltin("amm")
|
||||
}
|
||||
|
||||
m := &metrics{}
|
||||
|
||||
// HTTP server
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// GraphQL endpoint with MaxBytesReader (10MB) and metrics tracking
|
||||
mux.HandleFunc("POST /graphql", func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 10<<20) // 10MB
|
||||
m.queryCount.Add(1)
|
||||
m.blockHeight.Store(int64(idx.Status().LatestBlock))
|
||||
|
||||
var req engine.Request
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
m.queryErrors.Add(1)
|
||||
http.Error(w, `{"errors":[{"message":"invalid JSON"}]}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
resp := eng.Execute(r.Context(), &req)
|
||||
if len(resp.Errors) > 0 {
|
||||
m.queryErrors.Add(1)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
})
|
||||
|
||||
mux.HandleFunc("GET /graphql", eng.HandleGraphiQL)
|
||||
|
||||
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
status := idx.Status()
|
||||
fmt.Fprintf(w, `{"status":"ok","block":%d,"indexed":%d}`, status.LatestBlock, status.IndexedEvents)
|
||||
})
|
||||
|
||||
mux.HandleFunc("GET /ready", func(w http.ResponseWriter, r *http.Request) {
|
||||
status := idx.Status()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if status.LatestBlock == 0 {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
fmt.Fprint(w, `{"ready":false,"reason":"indexer has not processed any blocks"}`)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, `{"ready":true,"block":%d}`, status.LatestBlock)
|
||||
})
|
||||
|
||||
mux.HandleFunc("GET /metrics", m.handleMetrics)
|
||||
|
||||
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `{"name":"graph","version":"%s","graphql":"/graphql"}`, version)
|
||||
})
|
||||
|
||||
// Middleware stack: security headers + CORS -> request logging -> rate limiting
|
||||
rl := newRateLimiter(100, 100) // 100 req/s per IP, burst 100
|
||||
handler := securityMiddleware(mux)
|
||||
handler = loggingMiddleware(handler)
|
||||
handler = rateLimitMiddleware(rl, handler)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: *httpAddr,
|
||||
Handler: handler,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 60 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
MaxHeaderBytes: 1 << 20, // 1MB
|
||||
}
|
||||
go func() {
|
||||
slog.Info("listening", "addr", *httpAddr, "graphql", fmt.Sprintf("http://localhost%s/graphql", *httpAddr))
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
slog.Error("http", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
<-ctx.Done()
|
||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer shutdownCancel()
|
||||
srv.Shutdown(shutdownCtx)
|
||||
slog.Info("stopped")
|
||||
}
|
||||
|
||||
// securityMiddleware adds CORS and security headers.
|
||||
func securityMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// loggingMiddleware logs every request with method, path, status, and duration.
|
||||
type statusRecorder struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
}
|
||||
|
||||
func (sr *statusRecorder) WriteHeader(code int) {
|
||||
sr.status = code
|
||||
sr.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func loggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
sr := &statusRecorder{ResponseWriter: w, status: 200}
|
||||
next.ServeHTTP(sr, r)
|
||||
slog.Info("request",
|
||||
"method", r.Method,
|
||||
"path", r.URL.Path,
|
||||
"status", sr.status,
|
||||
"duration_ms", time.Since(start).Milliseconds(),
|
||||
"ip", clientIP(r),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func envOr(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func homeDir() string {
|
||||
h, _ := os.UserHomeDir()
|
||||
return h
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// metrics holds application-level counters exposed at /metrics.
|
||||
type metrics struct {
|
||||
queryCount atomic.Int64
|
||||
queryErrors atomic.Int64
|
||||
blockHeight atomic.Int64
|
||||
}
|
||||
|
||||
func (m *metrics) handleMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
|
||||
fmt.Fprintf(w, "# HELP graph_query_count Total GraphQL queries processed.\n")
|
||||
fmt.Fprintf(w, "# TYPE graph_query_count counter\n")
|
||||
fmt.Fprintf(w, "graph_query_count %d\n", m.queryCount.Load())
|
||||
fmt.Fprintf(w, "# HELP graph_query_errors Total GraphQL queries that returned errors.\n")
|
||||
fmt.Fprintf(w, "# TYPE graph_query_errors counter\n")
|
||||
fmt.Fprintf(w, "graph_query_errors %d\n", m.queryErrors.Load())
|
||||
fmt.Fprintf(w, "# HELP graph_indexer_block_height Latest indexed block number.\n")
|
||||
fmt.Fprintf(w, "# TYPE graph_indexer_block_height gauge\n")
|
||||
fmt.Fprintf(w, "graph_indexer_block_height %d\n", m.blockHeight.Load())
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// rateLimiter implements a per-IP token bucket rate limiter.
|
||||
// Stdlib only, no external dependencies.
|
||||
type rateLimiter struct {
|
||||
mu sync.Mutex
|
||||
buckets map[string]*bucket
|
||||
rate float64 // tokens per second
|
||||
burst int // max tokens
|
||||
}
|
||||
|
||||
type bucket struct {
|
||||
tokens float64
|
||||
lastSeen time.Time
|
||||
}
|
||||
|
||||
func newRateLimiter(rate float64, burst int) *rateLimiter {
|
||||
rl := &rateLimiter{
|
||||
buckets: make(map[string]*bucket),
|
||||
rate: rate,
|
||||
burst: burst,
|
||||
}
|
||||
go rl.cleanup()
|
||||
return rl
|
||||
}
|
||||
|
||||
// allow checks whether the given IP has tokens remaining.
|
||||
func (rl *rateLimiter) allow(ip string) bool {
|
||||
rl.mu.Lock()
|
||||
defer rl.mu.Unlock()
|
||||
|
||||
b, ok := rl.buckets[ip]
|
||||
now := time.Now()
|
||||
if !ok {
|
||||
rl.buckets[ip] = &bucket{tokens: float64(rl.burst) - 1, lastSeen: now}
|
||||
return true
|
||||
}
|
||||
|
||||
elapsed := now.Sub(b.lastSeen).Seconds()
|
||||
b.tokens = math.Min(float64(rl.burst), b.tokens+elapsed*rl.rate)
|
||||
b.lastSeen = now
|
||||
|
||||
if b.tokens < 1 {
|
||||
return false
|
||||
}
|
||||
b.tokens--
|
||||
return true
|
||||
}
|
||||
|
||||
// cleanup evicts stale entries every 60 seconds.
|
||||
func (rl *rateLimiter) cleanup() {
|
||||
for {
|
||||
time.Sleep(60 * time.Second)
|
||||
rl.mu.Lock()
|
||||
cutoff := time.Now().Add(-3 * time.Minute)
|
||||
for ip, b := range rl.buckets {
|
||||
if b.lastSeen.Before(cutoff) {
|
||||
delete(rl.buckets, ip)
|
||||
}
|
||||
}
|
||||
rl.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// rateLimitMiddleware wraps a handler with per-IP rate limiting.
|
||||
// Exempts /health and /ready paths.
|
||||
func rateLimitMiddleware(rl *rateLimiter, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Exempt health/readiness probes
|
||||
if r.URL.Path == "/health" || r.URL.Path == "/ready" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
ip := clientIP(r)
|
||||
if !rl.allow(ip) {
|
||||
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// clientIP extracts the client IP from the request.
|
||||
// Prefers X-Forwarded-For (first entry) since we sit behind hanzoai/ingress.
|
||||
func clientIP(r *http.Request) string {
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
// First IP in the chain is the original client
|
||||
for i := 0; i < len(xff); i++ {
|
||||
if xff[i] == ',' {
|
||||
return xff[:i]
|
||||
}
|
||||
}
|
||||
return xff
|
||||
}
|
||||
if xff := r.Header.Get("X-Real-Ip"); xff != "" {
|
||||
return xff
|
||||
}
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return r.RemoteAddr
|
||||
}
|
||||
return host
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
//go:build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/graph/engine"
|
||||
"github.com/luxfi/graph/storage"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Seed helpers — realistic data volumes.
|
||||
// 100 tokens, 500 pools, 10 000 swaps, 1 000 mints, 1 000 burns.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// seedBenchStore populates a store with production-scale data.
|
||||
// Addresses are deterministic hex strings so benchmarks are reproducible.
|
||||
func seedBenchStore(b *testing.B) *storage.Store {
|
||||
b.Helper()
|
||||
|
||||
store, err := storage.New(b.TempDir())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if err := store.Init(context.Background()); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
// Factory + bundle
|
||||
store.SeedFactory("1", &storage.SeedFactoryData{
|
||||
PoolCount: 500,
|
||||
TxCount: 12000,
|
||||
TotalVolumeUSD: "87654321.12",
|
||||
TotalValueLockedUSD: "23456789.01",
|
||||
})
|
||||
store.SeedBundle("1", &storage.SeedBundleData{
|
||||
EthPriceUSD: "2.53",
|
||||
})
|
||||
|
||||
// 100 tokens
|
||||
for i := 0; i < 100; i++ {
|
||||
addr := fmt.Sprintf("0x%040x", i+1)
|
||||
store.SeedToken(addr, &storage.SeedTokenData{
|
||||
Symbol: fmt.Sprintf("TK%d", i),
|
||||
Name: fmt.Sprintf("Token %d", i),
|
||||
Decimals: 18,
|
||||
VolumeUSD: fmt.Sprintf("%d.00", (i+1)*50000),
|
||||
TotalValueLockedUSD: fmt.Sprintf("%d.00", (i+1)*30000),
|
||||
DerivedETH: fmt.Sprintf("0.%04d", i+1),
|
||||
TxCount: int64((i + 1) * 10),
|
||||
})
|
||||
}
|
||||
|
||||
// 500 pools — each references two of the 100 tokens
|
||||
for i := 0; i < 500; i++ {
|
||||
id := fmt.Sprintf("0xpool%06d", i)
|
||||
t0 := fmt.Sprintf("0x%040x", (i%100)+1)
|
||||
t1 := fmt.Sprintf("0x%040x", ((i+1)%100)+1)
|
||||
fee := []int64{500, 3000, 10000}[i%3]
|
||||
store.SeedPool(id, &storage.SeedPoolData{
|
||||
Token0: t0,
|
||||
Token1: t1,
|
||||
FeeTier: fee,
|
||||
TotalValueLockedUSD: fmt.Sprintf("%d.00", (i+1)*4000),
|
||||
VolumeUSD: fmt.Sprintf("%d.00", (i+1)*1000),
|
||||
Token0Price: "2.53",
|
||||
Token1Price: "0.395",
|
||||
TxCount: int64((i + 1) * 2),
|
||||
})
|
||||
}
|
||||
|
||||
// 10 000 swaps
|
||||
baseTS := int64(1711929600)
|
||||
for i := 0; i < 10000; i++ {
|
||||
id := fmt.Sprintf("0xtx%08d#%d", i/4, i%4)
|
||||
pool := fmt.Sprintf("0xpool%06d", i%500)
|
||||
store.SeedSwap(id, &storage.SeedSwapData{
|
||||
Timestamp: baseTS - int64(i),
|
||||
Pool: pool,
|
||||
Amount0: fmt.Sprintf("%d.%02d", i%10000, i%100),
|
||||
Amount1: fmt.Sprintf("-%d.%02d", (i*2)%10000, (i*3)%100),
|
||||
AmountUSD: fmt.Sprintf("%d.%02d", (i+1)*25, (i*7)%100),
|
||||
Sender: fmt.Sprintf("0x%040x", i%200+1000),
|
||||
})
|
||||
}
|
||||
|
||||
// 1 000 mints — stored as generic entities (mint storage returns empty slices)
|
||||
for i := 0; i < 1000; i++ {
|
||||
store.SetEntity("Mint", fmt.Sprintf("mint-%d", i), map[string]interface{}{
|
||||
"id": fmt.Sprintf("mint-%d", i),
|
||||
"transaction": fmt.Sprintf("0x%064x", i),
|
||||
"timestamp": baseTS - int64(i*3),
|
||||
"pool": fmt.Sprintf("0xpool%06d", i%500),
|
||||
"amount0": fmt.Sprintf("%d.00", i*100),
|
||||
"amount1": fmt.Sprintf("%d.00", i*50),
|
||||
"amountUSD": fmt.Sprintf("%d.00", i*75),
|
||||
})
|
||||
}
|
||||
|
||||
// 1 000 burns
|
||||
for i := 0; i < 1000; i++ {
|
||||
store.SetEntity("Burn", fmt.Sprintf("burn-%d", i), map[string]interface{}{
|
||||
"id": fmt.Sprintf("burn-%d", i),
|
||||
"transaction": fmt.Sprintf("0x%064x", i+10000),
|
||||
"timestamp": baseTS - int64(i*5),
|
||||
"pool": fmt.Sprintf("0xpool%06d", i%500),
|
||||
"amount0": fmt.Sprintf("%d.00", i*80),
|
||||
"amount1": fmt.Sprintf("%d.00", i*40),
|
||||
"amountUSD": fmt.Sprintf("%d.00", i*60),
|
||||
})
|
||||
}
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
// newBenchEngine builds an engine with AMM + all chain schemas loaded.
|
||||
func newBenchEngine(b *testing.B, store *storage.Store) *engine.Engine {
|
||||
b.Helper()
|
||||
eng := engine.New(store, nil)
|
||||
if err := eng.LoadBuiltin("all"); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
return eng
|
||||
}
|
||||
|
||||
// runQuery is a helper that executes a query and fails the benchmark on error.
|
||||
func runQuery(b *testing.B, eng *engine.Engine, query string) {
|
||||
b.Helper()
|
||||
resp := eng.Execute(context.Background(), &engine.Request{Query: query})
|
||||
if len(resp.Errors) > 0 {
|
||||
b.Fatalf("query error: %s", resp.Errors[0].Message)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// A) Query latency benchmarks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func BenchmarkQuery_Factory(b *testing.B) {
|
||||
store := seedBenchStore(b)
|
||||
eng := newBenchEngine(b, store)
|
||||
q := `{ factory(id: "1") { id poolCount txCount totalVolumeUSD totalValueLockedUSD } }`
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
runQuery(b, eng, q)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkQuery_Tokens50(b *testing.B) {
|
||||
store := seedBenchStore(b)
|
||||
eng := newBenchEngine(b, store)
|
||||
q := `{ tokens(first: 50) { id symbol name decimals volumeUSD totalValueLockedUSD derivedETH txCount } }`
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
runQuery(b, eng, q)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkQuery_Pools50(b *testing.B) {
|
||||
store := seedBenchStore(b)
|
||||
eng := newBenchEngine(b, store)
|
||||
q := `{ pools(first: 50) { id token0 { id symbol name decimals } token1 { id symbol name decimals } feeTier totalValueLockedUSD volumeUSD token0Price token1Price txCount } }`
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
runQuery(b, eng, q)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkQuery_Swaps200(b *testing.B) {
|
||||
store := seedBenchStore(b)
|
||||
eng := newBenchEngine(b, store)
|
||||
q := `{ swaps(first: 200, orderBy: timestamp, orderDirection: desc) { id timestamp pool amount0 amount1 amountUSD sender } }`
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
runQuery(b, eng, q)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkQuery_Dashboard(b *testing.B) {
|
||||
store := seedBenchStore(b)
|
||||
eng := newBenchEngine(b, store)
|
||||
q := `{
|
||||
bundle(id: "1") { ethPriceUSD }
|
||||
factories(first: 1) { id poolCount txCount totalVolumeUSD totalValueLockedUSD }
|
||||
tokens(first: 50, orderBy: totalValueLockedUSD, orderDirection: desc) {
|
||||
id symbol name decimals totalValueLockedUSD derivedETH
|
||||
}
|
||||
pools(first: 50, orderBy: totalValueLockedUSD, orderDirection: desc) {
|
||||
id token0 { id symbol } token1 { id symbol }
|
||||
feeTier totalValueLockedUSD token0Price token1Price
|
||||
}
|
||||
}`
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
runQuery(b, eng, q)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkQuery_AllSchemas(b *testing.B) {
|
||||
store := seedBenchStore(b)
|
||||
// Seed chain-specific entities
|
||||
store.SetEntity("Validator", "NodeID-bench", map[string]interface{}{
|
||||
"id": "NodeID-bench", "stake": "1000000", "uptime": "99.9",
|
||||
})
|
||||
store.SetEntity("Order", "order-bench", map[string]interface{}{
|
||||
"id": "order-bench", "market": "LUX/USD", "side": "buy", "price": "2.50", "amount": "100",
|
||||
})
|
||||
store.SetEntity("DKGCeremony", "dkg-bench", map[string]interface{}{
|
||||
"id": "dkg-bench", "participants": 5, "threshold": 3, "status": "complete",
|
||||
})
|
||||
store.SetEntity("Asset", "LUX-bench", map[string]interface{}{
|
||||
"id": "LUX-bench", "symbol": "LUX", "name": "Lux", "denomination": 6,
|
||||
})
|
||||
store.SetEntity("BridgeTransfer", "bt-bench", map[string]interface{}{
|
||||
"id": "bt-bench", "sourceChain": "C", "destChain": "ethereum", "amount": "1000",
|
||||
})
|
||||
store.SetEntity("ShieldedTransfer", "st-bench", map[string]interface{}{
|
||||
"id": "st-bench", "nullifier": "0xabc", "commitment": "0xdef",
|
||||
})
|
||||
|
||||
eng := newBenchEngine(b, store)
|
||||
|
||||
// Query every chain type in sequence per iteration.
|
||||
queries := []string{
|
||||
`{ factories(first: 1) { id } }`,
|
||||
`{ tokens(first: 10) { id symbol } }`,
|
||||
`{ pools(first: 10) { id } }`,
|
||||
`{ swaps(first: 10) { id } }`,
|
||||
`{ orders(first: 10) { id } }`,
|
||||
`{ validators(first: 10) { id } }`,
|
||||
`{ dkgCeremonies(first: 10) { id } }`,
|
||||
`{ assets(first: 10) { id } }`,
|
||||
`{ bridgeTransfers(first: 10) { id } }`,
|
||||
`{ shieldedTransfers(first: 10) { id } }`,
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, q := range queries {
|
||||
runQuery(b, eng, q)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// B) Throughput benchmarks — concurrent clients
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// benchThroughput fires N goroutines each running queries in a loop.
|
||||
func benchThroughput(b *testing.B, clients int) {
|
||||
b.Helper()
|
||||
|
||||
store := seedBenchStore(b)
|
||||
eng := newBenchEngine(b, store)
|
||||
|
||||
// Mix of realistic queries weighted by exchange usage patterns.
|
||||
queries := []string{
|
||||
`{ bundle(id: "1") { ethPriceUSD } }`,
|
||||
`{ factories(first: 1) { poolCount txCount totalVolumeUSD totalValueLockedUSD } }`,
|
||||
`{ tokens(first: 50) { id symbol volumeUSD totalValueLockedUSD derivedETH } }`,
|
||||
`{ pools(first: 50) { id token0 { id symbol } token1 { id symbol } feeTier totalValueLockedUSD } }`,
|
||||
`{ swaps(first: 200, orderBy: timestamp, orderDirection: desc) { id timestamp amountUSD sender } }`,
|
||||
}
|
||||
|
||||
var ops atomic.Int64
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
perClient := b.N / clients
|
||||
if perClient < 1 {
|
||||
perClient = 1
|
||||
}
|
||||
|
||||
for c := 0; c < clients; c++ {
|
||||
wg.Add(1)
|
||||
go func(clientID int) {
|
||||
defer wg.Done()
|
||||
for i := 0; i < perClient; i++ {
|
||||
q := queries[(clientID+i)%len(queries)]
|
||||
resp := eng.Execute(context.Background(), &engine.Request{Query: q})
|
||||
if len(resp.Errors) > 0 {
|
||||
b.Errorf("client %d query error: %s", clientID, resp.Errors[0].Message)
|
||||
return
|
||||
}
|
||||
ops.Add(1)
|
||||
}
|
||||
}(c)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
b.ReportMetric(float64(ops.Load())/b.Elapsed().Seconds(), "queries/sec")
|
||||
}
|
||||
|
||||
func BenchmarkThroughput_1Client(b *testing.B) {
|
||||
benchThroughput(b, 1)
|
||||
}
|
||||
|
||||
func BenchmarkThroughput_10Clients(b *testing.B) {
|
||||
benchThroughput(b, 10)
|
||||
}
|
||||
|
||||
func BenchmarkThroughput_100Clients(b *testing.B) {
|
||||
benchThroughput(b, 100)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// C) Indexing simulation — entity writes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func BenchmarkIndex_SwapEvent(b *testing.B) {
|
||||
store, err := storage.New(b.TempDir())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
store.Init(context.Background())
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
id := fmt.Sprintf("0xtx%08d#0", i)
|
||||
store.SeedSwap(id, &storage.SeedSwapData{
|
||||
Timestamp: 1711929600 + int64(i),
|
||||
Pool: "0xpool000001",
|
||||
Amount0: "123.456",
|
||||
Amount1: "-308.640",
|
||||
AmountUSD: "308.64",
|
||||
Sender: "0x000000000000000000000000000000000000abcd",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkIndex_BatchSwaps1000(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
b.StopTimer()
|
||||
store, _ := storage.New(b.TempDir())
|
||||
store.Init(context.Background())
|
||||
b.StartTimer()
|
||||
|
||||
for j := 0; j < 1000; j++ {
|
||||
id := fmt.Sprintf("0xtx%08d#%d", i*1000+j/4, j%4)
|
||||
store.SeedSwap(id, &storage.SeedSwapData{
|
||||
Timestamp: 1711929600 + int64(j),
|
||||
Pool: fmt.Sprintf("0xpool%06d", j%50),
|
||||
Amount0: fmt.Sprintf("%d.%02d", j%10000, j%100),
|
||||
Amount1: fmt.Sprintf("-%d.%02d", (j*2)%10000, (j*3)%100),
|
||||
AmountUSD: fmt.Sprintf("%d.%02d", (j+1)*25, (j*7)%100),
|
||||
Sender: fmt.Sprintf("0x%040x", j%200+1000),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkIndex_GenericEntity measures SetEntity for chain-specific resolvers.
|
||||
func BenchmarkIndex_GenericEntity(b *testing.B) {
|
||||
store, err := storage.New(b.TempDir())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
store.Init(context.Background())
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
store.SetEntity("Swap", fmt.Sprintf("swap-%d", i), map[string]interface{}{
|
||||
"id": fmt.Sprintf("swap-%d", i),
|
||||
"timestamp": 1711929600 + i,
|
||||
"pool": "0xpool000001",
|
||||
"amount0": "123.456",
|
||||
"amount1": "-308.640",
|
||||
"amountUSD": "308.64",
|
||||
"sender": "0x000000000000000000000000000000000000abcd",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkIndex_ConcurrentWrites measures write contention under load.
|
||||
func BenchmarkIndex_ConcurrentWrites(b *testing.B) {
|
||||
store, err := storage.New(b.TempDir())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
store.Init(context.Background())
|
||||
|
||||
writers := 8
|
||||
perWriter := b.N / writers
|
||||
if perWriter < 1 {
|
||||
perWriter = 1
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for w := 0; w < writers; w++ {
|
||||
wg.Add(1)
|
||||
go func(wID int) {
|
||||
defer wg.Done()
|
||||
for i := 0; i < perWriter; i++ {
|
||||
id := fmt.Sprintf("0xtx-w%d-%08d#0", wID, i)
|
||||
store.SeedSwap(id, &storage.SeedSwapData{
|
||||
Timestamp: 1711929600 + int64(i),
|
||||
Pool: fmt.Sprintf("0xpool%06d", i%50),
|
||||
Amount0: "100.00",
|
||||
Amount1: "-250.00",
|
||||
AmountUSD: "250.00",
|
||||
Sender: fmt.Sprintf("0x%040x", wID*10000+i),
|
||||
})
|
||||
}
|
||||
}(w)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
//go:build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/graph/engine"
|
||||
"github.com/luxfi/graph/storage"
|
||||
)
|
||||
|
||||
// Test all chain schemas load and resolve without errors.
|
||||
|
||||
func setupChainEngine(t *testing.T, schema string) *engine.Engine {
|
||||
t.Helper()
|
||||
store, _ := storage.New(t.TempDir())
|
||||
store.Init(nil)
|
||||
|
||||
// Seed generic entities for chain resolvers
|
||||
store.SetEntity("Validator", "NodeID-abc", map[string]interface{}{
|
||||
"id": "NodeID-abc", "stake": "1000000", "uptime": "99.9",
|
||||
})
|
||||
store.SetEntity("Order", "order-1", map[string]interface{}{
|
||||
"id": "order-1", "market": "LUX/USD", "side": "buy", "price": "2.50", "amount": "100",
|
||||
})
|
||||
store.SetEntity("DKGCeremony", "dkg-1", map[string]interface{}{
|
||||
"id": "dkg-1", "participants": 5, "threshold": 3, "status": "complete",
|
||||
})
|
||||
store.SetEntity("Asset", "LUX", map[string]interface{}{
|
||||
"id": "LUX", "symbol": "LUX", "name": "Lux", "denomination": 6,
|
||||
})
|
||||
store.SetEntity("BridgeTransfer", "bt-1", map[string]interface{}{
|
||||
"id": "bt-1", "sourceChain": "C", "destChain": "ethereum", "amount": "1000",
|
||||
})
|
||||
store.SetEntity("ShieldedTransfer", "st-1", map[string]interface{}{
|
||||
"id": "st-1", "nullifier": "0xabc", "commitment": "0xdef",
|
||||
})
|
||||
|
||||
eng := engine.New(store, nil)
|
||||
if err := eng.LoadBuiltin(schema); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return eng
|
||||
}
|
||||
|
||||
func TestSchema_DEX(t *testing.T) {
|
||||
eng := setupChainEngine(t, "dex")
|
||||
resp := eng.Execute(nil, &engine.Request{Query: `{ orders(first: 10) { id } }`})
|
||||
if len(resp.Errors) > 0 {
|
||||
t.Fatal(resp.Errors[0].Message)
|
||||
}
|
||||
|
||||
resp = eng.Execute(nil, &engine.Request{Query: `{ order(id: "order-1") { id } }`})
|
||||
if len(resp.Errors) > 0 {
|
||||
t.Fatal(resp.Errors[0].Message)
|
||||
}
|
||||
if resp.Data == nil {
|
||||
t.Fatal("expected data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchema_FHE(t *testing.T) {
|
||||
eng := setupChainEngine(t, "fhe")
|
||||
resp := eng.Execute(nil, &engine.Request{Query: `{ dkgCeremonies(first: 10) { id } }`})
|
||||
if len(resp.Errors) > 0 {
|
||||
t.Fatal(resp.Errors[0].Message)
|
||||
}
|
||||
|
||||
resp = eng.Execute(nil, &engine.Request{Query: `{ dkgCeremony(id: "dkg-1") { id } }`})
|
||||
if len(resp.Errors) > 0 {
|
||||
t.Fatal(resp.Errors[0].Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchema_Platform(t *testing.T) {
|
||||
eng := setupChainEngine(t, "platform")
|
||||
resp := eng.Execute(nil, &engine.Request{Query: `{ validators(first: 10) { id } }`})
|
||||
if len(resp.Errors) > 0 {
|
||||
t.Fatal(resp.Errors[0].Message)
|
||||
}
|
||||
|
||||
resp = eng.Execute(nil, &engine.Request{Query: `{ validator(id: "NodeID-abc") { id } }`})
|
||||
if len(resp.Errors) > 0 {
|
||||
t.Fatal(resp.Errors[0].Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchema_XChain(t *testing.T) {
|
||||
eng := setupChainEngine(t, "xchain")
|
||||
resp := eng.Execute(nil, &engine.Request{Query: `{ assets(first: 10) { id } }`})
|
||||
if len(resp.Errors) > 0 {
|
||||
t.Fatal(resp.Errors[0].Message)
|
||||
}
|
||||
|
||||
resp = eng.Execute(nil, &engine.Request{Query: `{ asset(id: "LUX") { id } }`})
|
||||
if len(resp.Errors) > 0 {
|
||||
t.Fatal(resp.Errors[0].Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchema_Bridge(t *testing.T) {
|
||||
eng := setupChainEngine(t, "bridge")
|
||||
resp := eng.Execute(nil, &engine.Request{Query: `{ bridgeTransfers(first: 10) { id } }`})
|
||||
if len(resp.Errors) > 0 {
|
||||
t.Fatal(resp.Errors[0].Message)
|
||||
}
|
||||
|
||||
resp = eng.Execute(nil, &engine.Request{Query: `{ bridgeTransfer(id: "bt-1") { id } }`})
|
||||
if len(resp.Errors) > 0 {
|
||||
t.Fatal(resp.Errors[0].Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchema_Privacy(t *testing.T) {
|
||||
eng := setupChainEngine(t, "zchain")
|
||||
resp := eng.Execute(nil, &engine.Request{Query: `{ shieldedTransfers(first: 10) { id } }`})
|
||||
if len(resp.Errors) > 0 {
|
||||
t.Fatal(resp.Errors[0].Message)
|
||||
}
|
||||
|
||||
resp = eng.Execute(nil, &engine.Request{Query: `{ shieldedTransfer(id: "st-1") { id } }`})
|
||||
if len(resp.Errors) > 0 {
|
||||
t.Fatal(resp.Errors[0].Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchema_Quantum(t *testing.T) {
|
||||
eng := setupChainEngine(t, "qchain")
|
||||
resp := eng.Execute(nil, &engine.Request{Query: `{ coronaSignatures(first: 10) { id } }`})
|
||||
if len(resp.Errors) > 0 { t.Fatal(resp.Errors[0].Message) }
|
||||
}
|
||||
|
||||
func TestSchema_Key(t *testing.T) {
|
||||
eng := setupChainEngine(t, "kchain")
|
||||
resp := eng.Execute(nil, &engine.Request{Query: `{ managedKeys(first: 10) { id } }`})
|
||||
if len(resp.Errors) > 0 { t.Fatal(resp.Errors[0].Message) }
|
||||
}
|
||||
|
||||
func TestSchema_AI(t *testing.T) {
|
||||
eng := setupChainEngine(t, "achain")
|
||||
resp := eng.Execute(nil, &engine.Request{Query: `{ inferenceProofs(first: 10) { id } }`})
|
||||
if len(resp.Errors) > 0 { t.Fatal(resp.Errors[0].Message) }
|
||||
}
|
||||
|
||||
func TestSchema_Identity(t *testing.T) {
|
||||
eng := setupChainEngine(t, "ichain")
|
||||
resp := eng.Execute(nil, &engine.Request{Query: `{ dids(first: 10) { id } }`})
|
||||
if len(resp.Errors) > 0 { t.Fatal(resp.Errors[0].Message) }
|
||||
}
|
||||
|
||||
func TestSchema_Oracle(t *testing.T) {
|
||||
eng := setupChainEngine(t, "ochain")
|
||||
resp := eng.Execute(nil, &engine.Request{Query: `{ priceFeeds(first: 10) { id } }`})
|
||||
if len(resp.Errors) > 0 { t.Fatal(resp.Errors[0].Message) }
|
||||
}
|
||||
|
||||
func TestSchema_Relay(t *testing.T) {
|
||||
eng := setupChainEngine(t, "rchain")
|
||||
resp := eng.Execute(nil, &engine.Request{Query: `{ warpMessages(first: 10) { id } }`})
|
||||
if len(resp.Errors) > 0 { t.Fatal(resp.Errors[0].Message) }
|
||||
}
|
||||
|
||||
func TestSchema_ServiceNode(t *testing.T) {
|
||||
eng := setupChainEngine(t, "schain")
|
||||
resp := eng.Execute(nil, &engine.Request{Query: `{ serviceNodes(first: 10) { id } }`})
|
||||
if len(resp.Errors) > 0 { t.Fatal(resp.Errors[0].Message) }
|
||||
}
|
||||
|
||||
func TestSchema_Precompile(t *testing.T) {
|
||||
eng := setupChainEngine(t, "precompile")
|
||||
queries := []string{
|
||||
`{ precompileCalls(first: 10) { id } }`,
|
||||
`{ aiWorkProofs(first: 10) { id } }`,
|
||||
`{ fheOperations(first: 10) { id } }`,
|
||||
`{ zkVerifications(first: 10) { id } }`,
|
||||
`{ ringSignatures(first: 10) { id } }`,
|
||||
`{ pqCryptoOps(first: 10) { id } }`,
|
||||
`{ thresholdOps(first: 10) { id } }`,
|
||||
}
|
||||
for _, q := range queries {
|
||||
resp := eng.Execute(nil, &engine.Request{Query: q})
|
||||
if len(resp.Errors) > 0 {
|
||||
t.Errorf("query %q failed: %s", q, resp.Errors[0].Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchema_All(t *testing.T) {
|
||||
eng := setupChainEngine(t, "all")
|
||||
|
||||
// Every chain type must resolve without errors
|
||||
queries := []string{
|
||||
`{ factories(first: 1) { id } }`, // AMM (C-Chain)
|
||||
`{ orders(first: 1) { id } }`, // DEX (D-Chain)
|
||||
`{ dkgCeremonies(first: 1) { id } }`, // FHE (T-Chain)
|
||||
`{ validators(first: 1) { id } }`, // Platform (P-Chain)
|
||||
`{ assets(first: 1) { id } }`, // Exchange (X-Chain)
|
||||
`{ bridgeTransfers(first: 1) { id } }`, // Bridge (B-Chain)
|
||||
`{ shieldedTransfers(first: 1) { id } }`, // Privacy (Z-Chain)
|
||||
`{ coronaSignatures(first: 1) { id } }`, // Quantum (Q-Chain)
|
||||
`{ managedKeys(first: 1) { id } }`, // Key (K-Chain)
|
||||
`{ inferenceProofs(first: 1) { id } }`, // AI (A-Chain)
|
||||
`{ dids(first: 1) { id } }`, // Identity (I-Chain)
|
||||
`{ priceFeeds(first: 1) { id } }`, // Oracle (O-Chain)
|
||||
`{ warpMessages(first: 1) { id } }`, // Relay (R-Chain)
|
||||
`{ serviceNodes(first: 1) { id } }`, // ServiceNode (S-Chain)
|
||||
`{ precompileCalls(first: 1) { id } }`, // Precompiles
|
||||
`{ fheOperations(first: 1) { id } }`, // FHE precompile
|
||||
`{ zkVerifications(first: 1) { id } }`, // ZK precompile
|
||||
`{ tokens(first: 1) { id } }`, // ERC20
|
||||
`{ nfts(first: 1) { id } }`, // ERC721
|
||||
}
|
||||
|
||||
for _, q := range queries {
|
||||
resp := eng.Execute(nil, &engine.Request{Query: q})
|
||||
if len(resp.Errors) > 0 {
|
||||
t.Errorf("query %q failed: %s", q, resp.Errors[0].Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchema_AMMV4(t *testing.T) {
|
||||
eng := setupChainEngine(t, "amm-v4")
|
||||
|
||||
// V4 resolvers should be available
|
||||
queries := []string{
|
||||
`{ factories(first: 1) { id } }`,
|
||||
`{ pools(first: 1) { id } }`,
|
||||
`{ poolManagers(first: 1) { id } }`,
|
||||
}
|
||||
|
||||
for _, q := range queries {
|
||||
resp := eng.Execute(nil, &engine.Request{Query: q})
|
||||
if len(resp.Errors) > 0 {
|
||||
t.Errorf("query %q failed: %s", q, resp.Errors[0].Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,514 @@
|
||||
// Package e2e tests the graph engine against the exact GraphQL queries
|
||||
// made by ~/work/lux/exchange (the DEX frontend).
|
||||
//
|
||||
// These queries are extracted from:
|
||||
// - exchange/apps/web/src/state/explore/luxSubgraph.ts
|
||||
// - exchange/apps/web/src/state/explore/useExchangeStats.ts
|
||||
//
|
||||
// Run: go test -v ./e2e/ -tags=e2e
|
||||
//
|
||||
//go:build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/graph/engine"
|
||||
"github.com/luxfi/graph/storage"
|
||||
)
|
||||
|
||||
// setupTestEngine creates an engine with seeded test data.
|
||||
func setupTestEngine(t *testing.T) (*engine.Engine, *httptest.Server) {
|
||||
t.Helper()
|
||||
|
||||
store, err := storage.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := store.Init(nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Seed test data matching Lux mainnet DEX state
|
||||
store.SeedFactory("1", &storage.SeedFactoryData{
|
||||
PoolCount: 42,
|
||||
TxCount: 1337,
|
||||
TotalVolumeUSD: "12345678.90",
|
||||
TotalValueLockedUSD: "9876543.21",
|
||||
})
|
||||
store.SeedBundle("1", &storage.SeedBundleData{
|
||||
EthPriceUSD: "2.50",
|
||||
})
|
||||
store.SeedToken("0x4888e4a2ee0f03051c72d2bd3acf755ed3498b3e", &storage.SeedTokenData{
|
||||
Symbol: "WLUX",
|
||||
Name: "Wrapped LUX",
|
||||
Decimals: 18,
|
||||
VolumeUSD: "5000000.00",
|
||||
TotalValueLockedUSD: "3000000.00",
|
||||
DerivedETH: "1.0",
|
||||
TxCount: 500,
|
||||
})
|
||||
store.SeedToken("0x848Cff46eb323f323b6Bbe1Df274E40793d7f2c2", &storage.SeedTokenData{
|
||||
Symbol: "LUSD",
|
||||
Name: "Lux USD",
|
||||
Decimals: 6,
|
||||
VolumeUSD: "8000000.00",
|
||||
TotalValueLockedUSD: "4000000.00",
|
||||
DerivedETH: "0.40",
|
||||
TxCount: 800,
|
||||
})
|
||||
store.SeedPool("0xpool1", &storage.SeedPoolData{
|
||||
Token0: "0x4888e4a2ee0f03051c72d2bd3acf755ed3498b3e",
|
||||
Token1: "0x848Cff46eb323f323b6Bbe1Df274E40793d7f2c2",
|
||||
FeeTier: 3000,
|
||||
TotalValueLockedUSD: "2000000.00",
|
||||
VolumeUSD: "500000.00",
|
||||
Token0Price: "2.50",
|
||||
Token1Price: "0.40",
|
||||
TxCount: 200,
|
||||
})
|
||||
store.SeedSwap("0xtx1#0", &storage.SeedSwapData{
|
||||
Timestamp: 1711929600,
|
||||
Pool: "0xpool1",
|
||||
Amount0: "100.5",
|
||||
Amount1: "-251.25",
|
||||
AmountUSD: "251.25",
|
||||
Sender: "0xuser1",
|
||||
})
|
||||
|
||||
eng := engine.New(store, nil)
|
||||
eng.LoadBuiltin("amm")
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("POST /graphql", eng.HandleGraphQL)
|
||||
mux.HandleFunc("POST /subgraph/v3", eng.HandleGraphQL) // exchange endpoint compat
|
||||
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`{"status":"ok"}`))
|
||||
})
|
||||
|
||||
srv := httptest.NewServer(mux)
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
return eng, srv
|
||||
}
|
||||
|
||||
func graphqlQuery(t *testing.T, url, query string) map[string]interface{} {
|
||||
t.Helper()
|
||||
|
||||
body, _ := json.Marshal(engine.Request{Query: query})
|
||||
resp, err := http.Post(url+"/graphql", "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
t.Fatalf("status %d: %s", resp.StatusCode, b)
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if errs, ok := result["errors"]; ok {
|
||||
t.Fatalf("graphql errors: %v", errs)
|
||||
}
|
||||
|
||||
data, ok := result["data"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("no data in response")
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// Test: factories query (exchange/apps/web/src/state/explore/luxSubgraph.ts:137-147)
|
||||
func TestExchangeQuery_Factories(t *testing.T) {
|
||||
_, srv := setupTestEngine(t)
|
||||
|
||||
data := graphqlQuery(t, srv.URL, `{
|
||||
factories(first: 1) {
|
||||
poolCount
|
||||
txCount
|
||||
totalVolumeUSD
|
||||
totalValueLockedUSD
|
||||
}
|
||||
}`)
|
||||
|
||||
factories, ok := data["factories"].([]interface{})
|
||||
if !ok || len(factories) == 0 {
|
||||
t.Fatal("expected factories array")
|
||||
}
|
||||
|
||||
f := factories[0].(map[string]interface{})
|
||||
if f["poolCount"] == nil {
|
||||
t.Error("missing poolCount")
|
||||
}
|
||||
if f["totalVolumeUSD"] == nil {
|
||||
t.Error("missing totalVolumeUSD")
|
||||
}
|
||||
if f["totalValueLockedUSD"] == nil {
|
||||
t.Error("missing totalValueLockedUSD")
|
||||
}
|
||||
}
|
||||
|
||||
// Test: bundle query (exchange/apps/web/src/state/explore/useExchangeStats.ts:115)
|
||||
func TestExchangeQuery_Bundle(t *testing.T) {
|
||||
_, srv := setupTestEngine(t)
|
||||
|
||||
data := graphqlQuery(t, srv.URL, `{
|
||||
bundle(id: "1") {
|
||||
ethPriceUSD
|
||||
}
|
||||
}`)
|
||||
|
||||
bundle, ok := data["bundle"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("expected bundle object")
|
||||
}
|
||||
if bundle["ethPriceUSD"] == nil {
|
||||
t.Error("missing ethPriceUSD")
|
||||
}
|
||||
}
|
||||
|
||||
// Test: tokens query (exchange/apps/web/src/state/explore/luxSubgraph.ts:92-104)
|
||||
func TestExchangeQuery_Tokens(t *testing.T) {
|
||||
_, srv := setupTestEngine(t)
|
||||
|
||||
data := graphqlQuery(t, srv.URL, `{
|
||||
tokens(first: 50, orderBy: volumeUSD, orderDirection: desc) {
|
||||
id
|
||||
symbol
|
||||
name
|
||||
decimals
|
||||
volumeUSD
|
||||
totalValueLockedUSD
|
||||
derivedETH
|
||||
txCount
|
||||
}
|
||||
}`)
|
||||
|
||||
tokens, ok := data["tokens"].([]interface{})
|
||||
if !ok {
|
||||
t.Fatal("expected tokens array")
|
||||
}
|
||||
if len(tokens) == 0 {
|
||||
t.Fatal("expected at least 1 token")
|
||||
}
|
||||
|
||||
tok := tokens[0].(map[string]interface{})
|
||||
for _, field := range []string{"id", "symbol", "name", "volumeUSD", "totalValueLockedUSD", "derivedETH"} {
|
||||
if tok[field] == nil {
|
||||
t.Errorf("missing field: %s", field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: pools query (exchange/apps/web/src/state/explore/luxSubgraph.ts:106-119)
|
||||
func TestExchangeQuery_Pools(t *testing.T) {
|
||||
_, srv := setupTestEngine(t)
|
||||
|
||||
data := graphqlQuery(t, srv.URL, `{
|
||||
pools(first: 50, orderBy: totalValueLockedUSD, orderDirection: desc) {
|
||||
id
|
||||
token0 { id symbol name decimals }
|
||||
token1 { id symbol name decimals }
|
||||
feeTier
|
||||
liquidity
|
||||
totalValueLockedUSD
|
||||
volumeUSD
|
||||
token0Price
|
||||
token1Price
|
||||
txCount
|
||||
}
|
||||
}`)
|
||||
|
||||
pools, ok := data["pools"].([]interface{})
|
||||
if !ok {
|
||||
t.Fatal("expected pools array")
|
||||
}
|
||||
if len(pools) == 0 {
|
||||
t.Fatal("expected at least 1 pool")
|
||||
}
|
||||
|
||||
pool := pools[0].(map[string]interface{})
|
||||
if pool["token0"] == nil {
|
||||
t.Error("missing token0")
|
||||
}
|
||||
if pool["token1"] == nil {
|
||||
t.Error("missing token1")
|
||||
}
|
||||
if pool["feeTier"] == nil {
|
||||
t.Error("missing feeTier")
|
||||
}
|
||||
if pool["totalValueLockedUSD"] == nil {
|
||||
t.Error("missing totalValueLockedUSD")
|
||||
}
|
||||
}
|
||||
|
||||
// Test: swaps query (exchange/apps/web/src/state/explore/luxSubgraph.ts:121-135)
|
||||
func TestExchangeQuery_Swaps(t *testing.T) {
|
||||
_, srv := setupTestEngine(t)
|
||||
|
||||
data := graphqlQuery(t, srv.URL, `{
|
||||
swaps(first: 200, orderBy: timestamp, orderDirection: desc) {
|
||||
id
|
||||
timestamp
|
||||
amount0
|
||||
amount1
|
||||
amountUSD
|
||||
sender
|
||||
}
|
||||
}`)
|
||||
|
||||
swaps, ok := data["swaps"].([]interface{})
|
||||
if !ok {
|
||||
t.Fatal("expected swaps array")
|
||||
}
|
||||
if len(swaps) == 0 {
|
||||
t.Fatal("expected at least 1 swap")
|
||||
}
|
||||
|
||||
swap := swaps[0].(map[string]interface{})
|
||||
for _, field := range []string{"id", "timestamp", "amount0", "amount1", "amountUSD", "sender"} {
|
||||
if swap[field] == nil {
|
||||
t.Errorf("missing field: %s", field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test: combined dashboard query (exchange/apps/web/src/state/explore/useExchangeStats.ts:115-133)
|
||||
// This is the main query the exchange landing page makes on load.
|
||||
func TestExchangeQuery_Dashboard(t *testing.T) {
|
||||
_, srv := setupTestEngine(t)
|
||||
|
||||
data := graphqlQuery(t, srv.URL, `{
|
||||
bundle(id: "1") { ethPriceUSD }
|
||||
factories(first: 1) { id poolCount txCount totalVolumeUSD totalValueLockedUSD }
|
||||
tokens(first: 50, orderBy: totalValueLockedUSD, orderDirection: desc) {
|
||||
id symbol name decimals totalValueLockedUSD derivedETH
|
||||
}
|
||||
pools(first: 50, orderBy: totalValueLockedUSD, orderDirection: desc) {
|
||||
id token0 { id symbol } token1 { id symbol }
|
||||
feeTier totalValueLockedUSD token0Price token1Price
|
||||
}
|
||||
}`)
|
||||
|
||||
if data["bundle"] == nil {
|
||||
t.Error("missing bundle")
|
||||
}
|
||||
if data["factories"] == nil {
|
||||
t.Error("missing factories")
|
||||
}
|
||||
if data["tokens"] == nil {
|
||||
t.Error("missing tokens")
|
||||
}
|
||||
if data["pools"] == nil {
|
||||
t.Error("missing pools")
|
||||
}
|
||||
}
|
||||
|
||||
// Test: subgraph/v3 endpoint (exchange uses this path)
|
||||
func TestExchangeQuery_SubgraphV3Endpoint(t *testing.T) {
|
||||
_, srv := setupTestEngine(t)
|
||||
|
||||
body, _ := json.Marshal(engine.Request{
|
||||
Query: `{ factories(first: 1) { poolCount } }`,
|
||||
})
|
||||
|
||||
resp, err := http.Post(srv.URL+"/subgraph/v3", "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: health endpoint
|
||||
func TestHealthEndpoint(t *testing.T) {
|
||||
_, srv := setupTestEngine(t)
|
||||
|
||||
resp, err := http.Get(srv.URL + "/health")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatal("health check failed")
|
||||
}
|
||||
}
|
||||
|
||||
// Test: mutations are rejected (read-only)
|
||||
func TestMutationsRejected(t *testing.T) {
|
||||
_, srv := setupTestEngine(t)
|
||||
|
||||
body, _ := json.Marshal(engine.Request{
|
||||
Query: `mutation { createToken(symbol: "HACK") { id } }`,
|
||||
})
|
||||
|
||||
resp, err := http.Post(srv.URL+"/graphql", "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
|
||||
errs, ok := result["errors"].([]interface{})
|
||||
if !ok || len(errs) == 0 {
|
||||
t.Fatal("expected mutation to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
// Test: negative first does not panic (F3 red-team finding)
|
||||
func TestNegativeFirstDoesNotPanic(t *testing.T) {
|
||||
_, srv := setupTestEngine(t)
|
||||
|
||||
body, _ := json.Marshal(engine.Request{
|
||||
Query: `{ tokens(first: -1) { id symbol } }`,
|
||||
})
|
||||
|
||||
resp, err := http.Post(srv.URL+"/graphql", "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Must not panic — any valid JSON response (data or errors) is acceptable
|
||||
if result["data"] == nil && result["errors"] == nil {
|
||||
t.Fatal("expected data or errors in response")
|
||||
}
|
||||
}
|
||||
|
||||
// Test: where filter returns only matching records
|
||||
func TestWhereFilter(t *testing.T) {
|
||||
store, err := storage.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.Init(nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Seed 3 swaps for different pools (pool IDs not in pool store, so pool stays as string)
|
||||
store.SeedSwap("swap-a", &storage.SeedSwapData{Timestamp: 100, Pool: "0xpoolA", Amount0: "1", Amount1: "2", AmountUSD: "3", Sender: "0xsender1"})
|
||||
store.SeedSwap("swap-b", &storage.SeedSwapData{Timestamp: 200, Pool: "0xpoolA", Amount0: "4", Amount1: "5", AmountUSD: "6", Sender: "0xsender2"})
|
||||
store.SeedSwap("swap-c", &storage.SeedSwapData{Timestamp: 300, Pool: "0xpoolB", Amount0: "7", Amount1: "8", AmountUSD: "9", Sender: "0xsender3"})
|
||||
|
||||
eng := engine.New(store, nil)
|
||||
eng.LoadBuiltin("amm")
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("POST /graphql", eng.HandleGraphQL)
|
||||
srv := httptest.NewServer(mux)
|
||||
defer srv.Close()
|
||||
|
||||
data := graphqlQuery(t, srv.URL, `{
|
||||
swaps(where: { pool: "0xpoolA" }) {
|
||||
id
|
||||
pool
|
||||
}
|
||||
}`)
|
||||
|
||||
swaps, ok := data["swaps"].([]interface{})
|
||||
if !ok {
|
||||
t.Fatal("expected swaps array")
|
||||
}
|
||||
if len(swaps) != 2 {
|
||||
t.Fatalf("expected 2 swaps for 0xpoolA, got %d", len(swaps))
|
||||
}
|
||||
}
|
||||
|
||||
// Test: SQLite persistence across close/reopen
|
||||
func TestSQLitePersistence(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
// Create, seed, close
|
||||
store1, err := storage.New(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store1.Init(nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
store1.SeedToken("0xabc", &storage.SeedTokenData{Symbol: "TST", Name: "Test Token", Decimals: 18})
|
||||
store1.SeedSwap("swap-persist", &storage.SeedSwapData{
|
||||
Timestamp: 999, Pool: "0xpool", Amount0: "10", Amount1: "20", AmountUSD: "30", Sender: "0xuser",
|
||||
})
|
||||
store1.SetLastBlock(42)
|
||||
if err := store1.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Reopen same directory
|
||||
store2, err := storage.New(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store2.Init(nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer store2.Close()
|
||||
|
||||
if lb := store2.GetLastBlock(); lb != 42 {
|
||||
t.Fatalf("expected lastBlock=42 after reopen, got %d", lb)
|
||||
}
|
||||
|
||||
eng := engine.New(store2, nil)
|
||||
eng.LoadBuiltin("amm")
|
||||
|
||||
resp := eng.Execute(nil, &engine.Request{Query: `{ tokens(first: 10) { id symbol } }`})
|
||||
if len(resp.Errors) > 0 {
|
||||
t.Fatalf("query errors: %v", resp.Errors)
|
||||
}
|
||||
data, ok := resp.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatal("expected data map")
|
||||
}
|
||||
tokens, ok := data["tokens"].([]interface{})
|
||||
if !ok || len(tokens) == 0 {
|
||||
t.Fatal("expected tokens to survive close/reopen")
|
||||
}
|
||||
}
|
||||
|
||||
// Test: empty query rejected
|
||||
func TestEmptyQueryRejected(t *testing.T) {
|
||||
_, srv := setupTestEngine(t)
|
||||
|
||||
body, _ := json.Marshal(engine.Request{Query: ""})
|
||||
resp, err := http.Post(srv.URL+"/graphql", "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result map[string]interface{}
|
||||
json.NewDecoder(resp.Body).Decode(&result)
|
||||
|
||||
errs, ok := result["errors"].([]interface{})
|
||||
if !ok || len(errs) == 0 {
|
||||
t.Fatal("expected empty query to be rejected")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
//go:build e2e
|
||||
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/graph/wasm"
|
||||
)
|
||||
|
||||
// Test WASM subgraph loading from actual compiled subgraph build dirs.
|
||||
|
||||
func TestWASM_LoadV2Subgraph(t *testing.T) {
|
||||
rt, err := wasm.NewRuntime(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rt.Close()
|
||||
|
||||
sg, err := rt.LoadSubgraph("/Users/z/work/lux/uni-v2-subgraph/build/")
|
||||
if err != nil {
|
||||
t.Fatalf("load v2 subgraph: %v", err)
|
||||
}
|
||||
|
||||
if len(sg.DataSources) == 0 {
|
||||
t.Fatal("expected at least 1 data source")
|
||||
}
|
||||
if sg.DataSources[0].Name != "Factory" {
|
||||
t.Errorf("expected Factory data source, got %s", sg.DataSources[0].Name)
|
||||
}
|
||||
if len(sg.Templates) == 0 {
|
||||
t.Fatal("expected at least 1 template")
|
||||
}
|
||||
if sg.Schema == "" {
|
||||
t.Fatal("schema not loaded")
|
||||
}
|
||||
t.Logf("v2: %s", sg.Info())
|
||||
}
|
||||
|
||||
func TestWASM_LoadV3Subgraph(t *testing.T) {
|
||||
rt, err := wasm.NewRuntime(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rt.Close()
|
||||
|
||||
sg, err := rt.LoadSubgraph("/Users/z/work/lux/uni-v3-subgraph/build/")
|
||||
if err != nil {
|
||||
t.Fatalf("load v3 subgraph: %v", err)
|
||||
}
|
||||
|
||||
if len(sg.DataSources) == 0 {
|
||||
t.Fatal("expected at least 1 data source")
|
||||
}
|
||||
if sg.DataSources[0].Name != "Factory" {
|
||||
t.Errorf("expected Factory data source, got %s", sg.DataSources[0].Name)
|
||||
}
|
||||
if sg.Schema == "" {
|
||||
t.Fatal("schema not loaded")
|
||||
}
|
||||
t.Logf("v3: %s", sg.Info())
|
||||
}
|
||||
|
||||
func TestWASM_LoadV4Subgraph(t *testing.T) {
|
||||
rt, err := wasm.NewRuntime(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rt.Close()
|
||||
|
||||
sg, err := rt.LoadSubgraph("/Users/z/work/lux/uni-v4-subgraph/build/")
|
||||
if err != nil {
|
||||
t.Fatalf("load v4 subgraph: %v", err)
|
||||
}
|
||||
|
||||
if len(sg.DataSources) == 0 {
|
||||
t.Fatal("expected at least 1 data source")
|
||||
}
|
||||
if sg.DataSources[0].Name != "PoolManager" {
|
||||
t.Errorf("expected PoolManager data source, got %s", sg.DataSources[0].Name)
|
||||
}
|
||||
if sg.Schema == "" {
|
||||
t.Fatal("schema not loaded")
|
||||
}
|
||||
t.Logf("v4: %s", sg.Info())
|
||||
}
|
||||
|
||||
func TestWASM_EntityStore(t *testing.T) {
|
||||
es := wasm.NewEntityStore()
|
||||
|
||||
es.Set("Token", "0xabc", map[string]interface{}{
|
||||
"symbol": "WLUX", "decimals": 18,
|
||||
})
|
||||
|
||||
data, ok := es.Get("Token", "0xabc")
|
||||
if !ok {
|
||||
t.Fatal("expected entity to exist")
|
||||
}
|
||||
if data["symbol"] != "WLUX" {
|
||||
t.Errorf("expected WLUX, got %v", data["symbol"])
|
||||
}
|
||||
|
||||
flushed := es.Flush()
|
||||
if len(flushed["Token"]) != 1 {
|
||||
t.Error("expected 1 flushed token")
|
||||
}
|
||||
|
||||
// After flush, store should be empty
|
||||
_, ok = es.Get("Token", "0xabc")
|
||||
if ok {
|
||||
t.Error("expected empty after flush")
|
||||
}
|
||||
}
|
||||
+280
@@ -0,0 +1,280 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/graph/storage"
|
||||
)
|
||||
|
||||
// AMM resolvers — Uniswap v2/v3 subgraph compatible.
|
||||
// These match The Graph's uniswap-v2 and uniswap-v3 subgraph schemas
|
||||
// so existing frontend queries work without modification.
|
||||
//
|
||||
// "AMM" = automated market maker (v2 constant product, v3 concentrated liquidity).
|
||||
// "DEX" in this codebase refers to the native ~/work/lux/dex orderbook.
|
||||
|
||||
func (e *Engine) registerAMMResolvers() {
|
||||
// v2 + v3 unified
|
||||
e.resolvers["factory"] = e.resolveFactory
|
||||
e.resolvers["factories"] = e.resolveFactories
|
||||
e.resolvers["bundle"] = e.resolveBundle
|
||||
e.resolvers["bundles"] = e.resolveBundles
|
||||
e.resolvers["pool"] = e.resolvePool
|
||||
e.resolvers["pools"] = e.resolvePools
|
||||
e.resolvers["pair"] = e.resolvePair // v2
|
||||
e.resolvers["pairs"] = e.resolvePairs // v2
|
||||
e.resolvers["swap"] = e.resolveSwap
|
||||
e.resolvers["swaps"] = e.resolveSwaps
|
||||
e.resolvers["mint"] = e.resolveMint
|
||||
e.resolvers["mints"] = e.resolveMints
|
||||
e.resolvers["burn"] = e.resolveBurn
|
||||
e.resolvers["burns"] = e.resolveBurns
|
||||
e.resolvers["tick"] = e.resolveTick // v3
|
||||
e.resolvers["ticks"] = e.resolveTicks // v3
|
||||
e.resolvers["position"] = e.resolvePosition // v3
|
||||
e.resolvers["positions"] = e.resolvePositions // v3
|
||||
|
||||
// Collect / Flash (V3)
|
||||
e.resolvers["collect"] = e.resolveCollect
|
||||
e.resolvers["collects"] = e.resolveCollects
|
||||
e.resolvers["flash"] = e.resolveFlash
|
||||
e.resolvers["flashes"] = e.resolveFlashes
|
||||
|
||||
// Time series
|
||||
e.resolvers["tokenDayDatas"] = e.resolveTokenDayDatas
|
||||
e.resolvers["pairDayDatas"] = e.resolvePairDayDatas
|
||||
e.resolvers["poolDayDatas"] = e.resolvePoolDayDatas
|
||||
e.resolvers["pairHourDatas"] = e.resolvePairHourDatas
|
||||
e.resolvers["uniswapDayDatas"] = e.resolveFactoryDayDatas // v2 compat name
|
||||
}
|
||||
|
||||
func (e *Engine) registerERC20Resolvers() {
|
||||
e.resolvers["token"] = e.resolveToken
|
||||
e.resolvers["tokens"] = e.resolveTokens
|
||||
e.resolvers["transfer"] = e.resolveTransfer
|
||||
e.resolvers["transfers"] = e.resolveTransfers
|
||||
}
|
||||
|
||||
func (e *Engine) registerERC721Resolvers() {
|
||||
e.resolvers["nft"] = e.resolveNFT
|
||||
e.resolvers["nfts"] = e.resolveNFTs
|
||||
}
|
||||
|
||||
// Factory / Pool / Pair resolvers
|
||||
|
||||
func (e *Engine) resolveFactory(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
id := "1" // default factory
|
||||
if v, ok := args["id"]; ok {
|
||||
id = fmt.Sprint(v)
|
||||
}
|
||||
return s.GetFactory(ctx, id)
|
||||
}
|
||||
|
||||
func (e *Engine) resolveFactories(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.GetFactories(ctx)
|
||||
}
|
||||
|
||||
func (e *Engine) resolveBundle(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.GetBundle(ctx, "1")
|
||||
}
|
||||
|
||||
func (e *Engine) resolveBundles(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
b, err := s.GetBundle(ctx, "1")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []interface{}{b}, nil
|
||||
}
|
||||
|
||||
func (e *Engine) resolvePool(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok {
|
||||
return s.GetPool(ctx, fmt.Sprint(id))
|
||||
}
|
||||
return nil, fmt.Errorf("pool requires id")
|
||||
}
|
||||
|
||||
func (e *Engine) resolvePools(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit, orderBy, orderDir, where := parseListArgs(args, 100)
|
||||
return s.GetPools(ctx, limit, orderBy, orderDir, where)
|
||||
}
|
||||
|
||||
func (e *Engine) resolvePair(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok {
|
||||
return s.GetPair(ctx, fmt.Sprint(id))
|
||||
}
|
||||
return nil, fmt.Errorf("pair requires id")
|
||||
}
|
||||
|
||||
func (e *Engine) resolvePairs(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit, orderBy, orderDir, where := parseListArgs(args, 100)
|
||||
return s.GetPairs(ctx, limit, orderBy, orderDir, where)
|
||||
}
|
||||
|
||||
func (e *Engine) resolveSwap(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok {
|
||||
return s.GetSwap(ctx, fmt.Sprint(id))
|
||||
}
|
||||
return nil, fmt.Errorf("swap requires id")
|
||||
}
|
||||
|
||||
func (e *Engine) resolveSwaps(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit, orderBy, orderDir, where := parseListArgs(args, 100)
|
||||
return s.GetSwaps(ctx, limit, orderBy, orderDir, where)
|
||||
}
|
||||
|
||||
func (e *Engine) resolveMint(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok {
|
||||
return s.GetMint(ctx, fmt.Sprint(id))
|
||||
}
|
||||
return nil, fmt.Errorf("mint requires id")
|
||||
}
|
||||
|
||||
func (e *Engine) resolveMints(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit, orderBy, orderDir, where := parseListArgs(args, 100)
|
||||
return s.GetMints(ctx, limit, orderBy, orderDir, where)
|
||||
}
|
||||
|
||||
func (e *Engine) resolveBurn(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok {
|
||||
return s.GetBurn(ctx, fmt.Sprint(id))
|
||||
}
|
||||
return nil, fmt.Errorf("burn requires id")
|
||||
}
|
||||
|
||||
func (e *Engine) resolveBurns(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit, orderBy, orderDir, where := parseListArgs(args, 100)
|
||||
return s.GetBurns(ctx, limit, orderBy, orderDir, where)
|
||||
}
|
||||
|
||||
func (e *Engine) resolveTick(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok {
|
||||
return s.GetTick(ctx, fmt.Sprint(id))
|
||||
}
|
||||
return nil, fmt.Errorf("tick requires id")
|
||||
}
|
||||
|
||||
func (e *Engine) resolveTicks(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit, orderBy, orderDir, where := parseListArgs(args, 100)
|
||||
return s.GetTicks(ctx, limit, orderBy, orderDir, where)
|
||||
}
|
||||
|
||||
func (e *Engine) resolvePosition(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok {
|
||||
return s.GetPosition(ctx, fmt.Sprint(id))
|
||||
}
|
||||
return nil, fmt.Errorf("position requires id")
|
||||
}
|
||||
|
||||
func (e *Engine) resolvePositions(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit, orderBy, orderDir, where := parseListArgs(args, 100)
|
||||
return s.GetPositions(ctx, limit, orderBy, orderDir, where)
|
||||
}
|
||||
|
||||
// Time series
|
||||
|
||||
func (e *Engine) resolveTokenDayDatas(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit, orderBy, orderDir, where := parseListArgs(args, 30)
|
||||
return s.GetTokenDayDatas(ctx, limit, orderBy, orderDir, where)
|
||||
}
|
||||
|
||||
func (e *Engine) resolvePairDayDatas(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit, orderBy, orderDir, where := parseListArgs(args, 30)
|
||||
return s.GetPairDayDatas(ctx, limit, orderBy, orderDir, where)
|
||||
}
|
||||
|
||||
func (e *Engine) resolvePoolDayDatas(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit, orderBy, orderDir, where := parseListArgs(args, 30)
|
||||
return s.GetPoolDayDatas(ctx, limit, orderBy, orderDir, where)
|
||||
}
|
||||
|
||||
func (e *Engine) resolveFactoryDayDatas(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit, orderBy, orderDir, where := parseListArgs(args, 30)
|
||||
return s.GetFactoryDayDatas(ctx, limit, orderBy, orderDir, where)
|
||||
}
|
||||
|
||||
// ERC transfers / NFTs
|
||||
|
||||
func (e *Engine) resolveTransfer(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok {
|
||||
return s.GetTransfer(ctx, fmt.Sprint(id))
|
||||
}
|
||||
return nil, fmt.Errorf("transfer requires id")
|
||||
}
|
||||
|
||||
func (e *Engine) resolveTransfers(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit, orderBy, orderDir, where := parseListArgs(args, 100)
|
||||
return s.GetTransfers(ctx, limit, orderBy, orderDir, where)
|
||||
}
|
||||
|
||||
func (e *Engine) resolveNFT(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok {
|
||||
return s.GetNFT(ctx, fmt.Sprint(id))
|
||||
}
|
||||
return nil, fmt.Errorf("nft requires id")
|
||||
}
|
||||
|
||||
func (e *Engine) resolveNFTs(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit, orderBy, orderDir, where := parseListArgs(args, 100)
|
||||
return s.GetNFTs(ctx, limit, orderBy, orderDir, where)
|
||||
}
|
||||
|
||||
// Collect / Flash resolvers
|
||||
|
||||
func (e *Engine) resolveCollect(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok {
|
||||
return s.GetCollect(ctx, fmt.Sprint(id))
|
||||
}
|
||||
return nil, fmt.Errorf("collect requires id")
|
||||
}
|
||||
|
||||
func (e *Engine) resolveCollects(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit, orderBy, orderDir, where := parseListArgs(args, 100)
|
||||
return s.GetCollects(ctx, limit, orderBy, orderDir, where)
|
||||
}
|
||||
|
||||
func (e *Engine) resolveFlash(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok {
|
||||
return s.GetFlash(ctx, fmt.Sprint(id))
|
||||
}
|
||||
return nil, fmt.Errorf("flash requires id")
|
||||
}
|
||||
|
||||
func (e *Engine) resolveFlashes(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit, orderBy, orderDir, where := parseListArgs(args, 100)
|
||||
return s.GetFlashes(ctx, limit, orderBy, orderDir, where)
|
||||
}
|
||||
|
||||
func (e *Engine) resolvePairHourDatas(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit, orderBy, orderDir, where := parseListArgs(args, 24)
|
||||
return s.GetPairHourDatas(ctx, limit, orderBy, orderDir, where)
|
||||
}
|
||||
|
||||
// parseListArgs extracts first/orderBy/orderDirection/where from GraphQL args.
|
||||
func parseListArgs(args map[string]interface{}, defaultLimit int) (int, string, string, map[string]interface{}) {
|
||||
limit := defaultLimit
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
if limit < 1 {
|
||||
limit = 1
|
||||
}
|
||||
if limit > 1000 {
|
||||
limit = 1000
|
||||
}
|
||||
orderBy := ""
|
||||
if o, ok := args["orderBy"]; ok {
|
||||
orderBy = fmt.Sprint(o)
|
||||
}
|
||||
orderDir := ""
|
||||
if d, ok := args["orderDirection"]; ok {
|
||||
orderDir = fmt.Sprint(d)
|
||||
}
|
||||
var where map[string]interface{}
|
||||
if w, ok := args["where"]; ok {
|
||||
if wm, ok := w.(map[string]interface{}); ok {
|
||||
where = wm
|
||||
}
|
||||
}
|
||||
return limit, orderBy, orderDir, where
|
||||
}
|
||||
@@ -0,0 +1,563 @@
|
||||
// Package engine implements the GraphQL query execution engine.
|
||||
//
|
||||
// Extracted from node/vms/graphvm — same resolvers, no consensus deps.
|
||||
// Reads from local storage (SQLite/ZapDB), serves GraphQL queries.
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/graph/resolvers/ai"
|
||||
"github.com/luxfi/graph/resolvers/bridge"
|
||||
"github.com/luxfi/graph/resolvers/dex"
|
||||
"github.com/luxfi/graph/resolvers/exchange"
|
||||
"github.com/luxfi/graph/resolvers/fhe"
|
||||
"github.com/luxfi/graph/resolvers/identity"
|
||||
"github.com/luxfi/graph/resolvers/key"
|
||||
"github.com/luxfi/graph/resolvers/oracle"
|
||||
"github.com/luxfi/graph/resolvers/platform"
|
||||
"github.com/luxfi/graph/resolvers/precompile"
|
||||
"github.com/luxfi/graph/resolvers/privacy"
|
||||
"github.com/luxfi/graph/resolvers/quantum"
|
||||
"github.com/luxfi/graph/resolvers/relay"
|
||||
"github.com/luxfi/graph/resolvers/servicenode"
|
||||
"github.com/luxfi/graph/storage"
|
||||
)
|
||||
|
||||
// Config controls query execution limits.
|
||||
type Config struct {
|
||||
MaxQueryDepth int `json:"maxQueryDepth" yaml:"max_query_depth"`
|
||||
MaxResultSize int `json:"maxResultSize" yaml:"max_result_size"`
|
||||
QueryTimeoutMs int `json:"queryTimeoutMs" yaml:"query_timeout_ms"`
|
||||
}
|
||||
|
||||
// Engine executes GraphQL queries against indexed data.
|
||||
type Engine struct {
|
||||
store *storage.Store
|
||||
config *Config
|
||||
timeout time.Duration
|
||||
|
||||
mu sync.RWMutex
|
||||
resolvers map[string]ResolverFunc
|
||||
schemas map[string]*Schema
|
||||
}
|
||||
|
||||
// Schema represents a deployed subgraph schema.
|
||||
type Schema struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Version string `json:"version" yaml:"version"`
|
||||
Source string `json:"source" yaml:"source"` // GraphQL SDL
|
||||
Types []string `json:"types" yaml:"types"`
|
||||
}
|
||||
|
||||
// ResolverFunc resolves a GraphQL field from storage.
|
||||
type ResolverFunc func(ctx context.Context, store *storage.Store, args map[string]interface{}) (interface{}, error)
|
||||
|
||||
// Request is an incoming GraphQL request.
|
||||
type Request struct {
|
||||
Query string `json:"query"`
|
||||
OperationName string `json:"operationName,omitempty"`
|
||||
Variables map[string]interface{} `json:"variables,omitempty"`
|
||||
}
|
||||
|
||||
// Response is a GraphQL response.
|
||||
type Response struct {
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Errors []Error `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
// Error is a GraphQL error.
|
||||
type Error struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// New creates a query engine backed by the given store.
|
||||
func New(store *storage.Store, cfg *Config) *Engine {
|
||||
if cfg == nil {
|
||||
cfg = &Config{MaxQueryDepth: 10, MaxResultSize: 1 << 20, QueryTimeoutMs: 30000}
|
||||
}
|
||||
|
||||
e := &Engine{
|
||||
store: store,
|
||||
config: cfg,
|
||||
timeout: time.Duration(cfg.QueryTimeoutMs) * time.Millisecond,
|
||||
resolvers: make(map[string]ResolverFunc),
|
||||
schemas: make(map[string]*Schema),
|
||||
}
|
||||
|
||||
e.registerBuiltinResolvers()
|
||||
return e
|
||||
}
|
||||
|
||||
// RegisterResolver adds a named resolver.
|
||||
func (e *Engine) RegisterResolver(name string, fn ResolverFunc) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
e.resolvers[name] = fn
|
||||
}
|
||||
|
||||
// LoadBuiltin loads a built-in schema by name.
|
||||
func (e *Engine) LoadBuiltin(name string) error {
|
||||
switch name {
|
||||
case "amm", "amm-v2", "uniswap-v2":
|
||||
e.registerAMMResolvers()
|
||||
return nil
|
||||
case "amm-v3", "uniswap-v3":
|
||||
e.registerAMMResolvers()
|
||||
return nil
|
||||
case "amm-v4", "uniswap-v4", "v4":
|
||||
e.registerAMMResolvers()
|
||||
e.registerV4Resolvers()
|
||||
return nil
|
||||
case "dex":
|
||||
e.registerChain(dex.Register)
|
||||
return nil
|
||||
case "fhe", "threshold":
|
||||
e.registerChain(fhe.Register)
|
||||
return nil
|
||||
case "platform", "pchain":
|
||||
e.registerChain(platform.Register)
|
||||
return nil
|
||||
case "exchange", "xchain":
|
||||
e.registerChain(exchange.Register)
|
||||
return nil
|
||||
case "bridge", "bchain":
|
||||
e.registerChain(bridge.Register)
|
||||
return nil
|
||||
case "privacy", "zchain":
|
||||
e.registerChain(privacy.Register)
|
||||
return nil
|
||||
case "quantum", "qchain":
|
||||
e.registerChain(quantum.Register)
|
||||
return nil
|
||||
case "key", "kchain":
|
||||
e.registerChain(key.Register)
|
||||
return nil
|
||||
case "ai", "achain":
|
||||
e.registerChain(ai.Register)
|
||||
return nil
|
||||
case "identity", "ichain":
|
||||
e.registerChain(identity.Register)
|
||||
return nil
|
||||
case "oracle", "ochain":
|
||||
e.registerChain(oracle.Register)
|
||||
return nil
|
||||
case "relay", "rchain":
|
||||
e.registerChain(relay.Register)
|
||||
return nil
|
||||
case "servicenode", "schain":
|
||||
e.registerChain(servicenode.Register)
|
||||
return nil
|
||||
case "precompile", "precompiles":
|
||||
e.registerChain(precompile.Register)
|
||||
return nil
|
||||
case "all":
|
||||
e.registerAMMResolvers()
|
||||
e.registerV4Resolvers()
|
||||
e.registerERC20Resolvers()
|
||||
e.registerERC721Resolvers()
|
||||
e.registerChain(dex.Register)
|
||||
e.registerChain(fhe.Register)
|
||||
e.registerChain(platform.Register)
|
||||
e.registerChain(exchange.Register)
|
||||
e.registerChain(bridge.Register)
|
||||
e.registerChain(privacy.Register)
|
||||
e.registerChain(quantum.Register)
|
||||
e.registerChain(key.Register)
|
||||
e.registerChain(ai.Register)
|
||||
e.registerChain(identity.Register)
|
||||
e.registerChain(oracle.Register)
|
||||
e.registerChain(relay.Register)
|
||||
e.registerChain(servicenode.Register)
|
||||
e.registerChain(precompile.Register)
|
||||
return nil
|
||||
case "erc20":
|
||||
e.registerERC20Resolvers()
|
||||
return nil
|
||||
case "erc721":
|
||||
e.registerERC721Resolvers()
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unknown built-in schema: %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
// LoadConfig loads subgraph configuration from a YAML file.
|
||||
func (e *Engine) LoadConfig(path string) error {
|
||||
// TODO: parse subgraph.yaml, register event handlers + resolvers
|
||||
return fmt.Errorf("config loading not yet implemented: %s", path)
|
||||
}
|
||||
|
||||
// Execute runs a GraphQL query.
|
||||
func (e *Engine) Execute(ctx context.Context, req *Request) *Response {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, e.timeout)
|
||||
defer cancel()
|
||||
|
||||
query := strings.TrimSpace(req.Query)
|
||||
if query == "" {
|
||||
return &Response{Errors: []Error{{Message: "empty query"}}}
|
||||
}
|
||||
if len(query) > 100000 {
|
||||
return &Response{Errors: []Error{{Message: "query too large"}}}
|
||||
}
|
||||
if strings.HasPrefix(strings.ToLower(query), "mutation") {
|
||||
return &Response{Errors: []Error{{Message: "mutations not allowed — g-node is read-only"}}}
|
||||
}
|
||||
|
||||
// Query depth check: count max nested brace depth, reject > MaxQueryDepth
|
||||
if depth := queryDepth(query); depth > e.config.MaxQueryDepth {
|
||||
return &Response{Errors: []Error{{Message: fmt.Sprintf("query depth %d exceeds maximum %d", depth, e.config.MaxQueryDepth)}}}
|
||||
}
|
||||
|
||||
// Parse top-level fields
|
||||
fields, err := parseTopFields(query)
|
||||
if err != nil {
|
||||
return &Response{Errors: []Error{{Message: err.Error()}}}
|
||||
}
|
||||
|
||||
if len(fields) > 20 {
|
||||
return &Response{Errors: []Error{{Message: fmt.Sprintf("too many top-level fields (%d), maximum 20", len(fields))}}}
|
||||
}
|
||||
|
||||
data := make(map[string]interface{})
|
||||
for _, f := range fields {
|
||||
e.mu.RLock()
|
||||
resolver, ok := e.resolvers[f.name]
|
||||
e.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return &Response{Errors: []Error{{Message: fmt.Sprintf("unknown field: %s", f.name)}}}
|
||||
}
|
||||
|
||||
result, err := resolver(ctx, e.store, f.args)
|
||||
if err != nil {
|
||||
return &Response{Errors: []Error{{Message: err.Error()}}}
|
||||
}
|
||||
|
||||
key := f.name
|
||||
if f.alias != "" {
|
||||
key = f.alias
|
||||
}
|
||||
data[key] = result
|
||||
}
|
||||
|
||||
encoded, _ := json.Marshal(data)
|
||||
if len(encoded) > e.config.MaxResultSize {
|
||||
return &Response{Errors: []Error{{Message: "result exceeds maximum size"}}}
|
||||
}
|
||||
|
||||
return &Response{Data: data}
|
||||
}
|
||||
|
||||
// HandleGraphQL is the HTTP handler for POST /graphql.
|
||||
func (e *Engine) HandleGraphQL(w http.ResponseWriter, r *http.Request) {
|
||||
var req Request
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, `{"errors":[{"message":"invalid JSON"}]}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
resp := e.Execute(r.Context(), &req)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
// HandleGraphiQL serves the GraphiQL IDE for GET /graphql.
|
||||
func (e *Engine) HandleGraphiQL(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprint(w, graphiqlHTML)
|
||||
}
|
||||
|
||||
// registerChain registers resolvers from a chain-specific package.
|
||||
// Uses a shim map to bridge the type alias boundary.
|
||||
func (e *Engine) registerChain(register func(map[string]func(context.Context, *storage.Store, map[string]interface{}) (interface{}, error))) {
|
||||
shim := make(map[string]func(context.Context, *storage.Store, map[string]interface{}) (interface{}, error))
|
||||
register(shim)
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
for k, v := range shim {
|
||||
e.resolvers[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// registerBuiltinResolvers adds core blockchain resolvers.
|
||||
func (e *Engine) registerBuiltinResolvers() {
|
||||
e.resolvers["block"] = e.resolveBlock
|
||||
e.resolvers["blocks"] = e.resolveBlocks
|
||||
e.resolvers["transaction"] = e.resolveTransaction
|
||||
e.resolvers["transactions"] = e.resolveTransactions
|
||||
e.resolvers["token"] = e.resolveToken
|
||||
e.resolvers["tokens"] = e.resolveTokens
|
||||
}
|
||||
|
||||
// field represents a parsed top-level GraphQL field.
|
||||
type field struct {
|
||||
name string
|
||||
alias string
|
||||
args map[string]interface{}
|
||||
}
|
||||
|
||||
// parseTopFields extracts top-level field names from a GraphQL query.
|
||||
func parseTopFields(query string) ([]field, error) {
|
||||
// Strip query { ... } wrapper
|
||||
start := strings.Index(query, "{")
|
||||
end := strings.LastIndex(query, "}")
|
||||
if start == -1 || end == -1 || end <= start {
|
||||
return nil, fmt.Errorf("invalid query: missing braces")
|
||||
}
|
||||
|
||||
body := strings.TrimSpace(query[start+1 : end])
|
||||
if body == "" {
|
||||
return nil, fmt.Errorf("empty selection set")
|
||||
}
|
||||
|
||||
var fields []field
|
||||
// Simple tokenizer: split on top-level field boundaries
|
||||
depth := 0
|
||||
var current strings.Builder
|
||||
for _, ch := range body {
|
||||
switch ch {
|
||||
case '{':
|
||||
depth++
|
||||
current.WriteRune(ch)
|
||||
case '}':
|
||||
depth--
|
||||
current.WriteRune(ch)
|
||||
case '\n', '\r':
|
||||
if depth == 0 {
|
||||
if s := strings.TrimSpace(current.String()); s != "" {
|
||||
if f, err := parseField(s); err == nil {
|
||||
fields = append(fields, f)
|
||||
}
|
||||
}
|
||||
current.Reset()
|
||||
} else {
|
||||
current.WriteRune(ch)
|
||||
}
|
||||
default:
|
||||
current.WriteRune(ch)
|
||||
}
|
||||
}
|
||||
if s := strings.TrimSpace(current.String()); s != "" {
|
||||
if f, err := parseField(s); err == nil {
|
||||
fields = append(fields, f)
|
||||
}
|
||||
}
|
||||
|
||||
if len(fields) == 0 {
|
||||
return nil, fmt.Errorf("no fields in query")
|
||||
}
|
||||
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
func parseField(s string) (field, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return field{}, fmt.Errorf("empty field")
|
||||
}
|
||||
|
||||
f := field{args: make(map[string]interface{})}
|
||||
|
||||
// Check for alias — only if colon appears before any paren/brace
|
||||
if idx := strings.Index(s, ":"); idx > 0 && !strings.Contains(s[:idx], "(") && !strings.Contains(s[:idx], "{") {
|
||||
f.alias = strings.TrimSpace(s[:idx])
|
||||
s = strings.TrimSpace(s[idx+1:])
|
||||
}
|
||||
|
||||
// Extract name (before args or subselection)
|
||||
nameEnd := len(s)
|
||||
for i, ch := range s {
|
||||
if ch == '(' || ch == '{' || ch == ' ' {
|
||||
nameEnd = i
|
||||
break
|
||||
}
|
||||
}
|
||||
f.name = s[:nameEnd]
|
||||
|
||||
// Extract args from (...) — brace-aware to handle nested objects like where: { pool: "0x..." }
|
||||
if parenStart := strings.Index(s, "("); parenStart != -1 {
|
||||
parenEnd := findMatchingParen(s, parenStart)
|
||||
if parenEnd > parenStart {
|
||||
argsStr := s[parenStart+1 : parenEnd]
|
||||
f.args = parseArgs(argsStr)
|
||||
}
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// findMatchingParen finds the closing ')' that matches the '(' at pos,
|
||||
// respecting nested braces and quoted strings.
|
||||
func findMatchingParen(s string, pos int) int {
|
||||
depth := 0
|
||||
inQuote := false
|
||||
for i := pos; i < len(s); i++ {
|
||||
ch := s[i]
|
||||
if ch == '"' && (i == 0 || s[i-1] != '\\') {
|
||||
inQuote = !inQuote
|
||||
continue
|
||||
}
|
||||
if inQuote {
|
||||
continue
|
||||
}
|
||||
switch ch {
|
||||
case '(', '{':
|
||||
depth++
|
||||
case ')':
|
||||
depth--
|
||||
if depth == 0 {
|
||||
return i
|
||||
}
|
||||
case '}':
|
||||
depth--
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// parseArgs splits a top-level argument string into key-value pairs,
|
||||
// respecting nested braces so that `where: { pool: "0x...", date_gte: 123 }`
|
||||
// is kept as a single value for key "where".
|
||||
func parseArgs(s string) map[string]interface{} {
|
||||
args := make(map[string]interface{})
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return args
|
||||
}
|
||||
|
||||
for len(s) > 0 {
|
||||
// Find key (up to first ':')
|
||||
colonIdx := -1
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == ':' {
|
||||
colonIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if colonIdx < 0 {
|
||||
break
|
||||
}
|
||||
key := strings.TrimSpace(s[:colonIdx])
|
||||
s = strings.TrimSpace(s[colonIdx+1:])
|
||||
|
||||
// Parse value — could be quoted string, nested object, or bare token
|
||||
var val interface{}
|
||||
if len(s) == 0 {
|
||||
break
|
||||
}
|
||||
switch s[0] {
|
||||
case '"':
|
||||
// Quoted string — find closing quote
|
||||
end := 1
|
||||
for end < len(s) {
|
||||
if s[end] == '"' && s[end-1] != '\\' {
|
||||
break
|
||||
}
|
||||
end++
|
||||
}
|
||||
if end < len(s) {
|
||||
val = s[1:end]
|
||||
s = strings.TrimSpace(s[end+1:])
|
||||
} else {
|
||||
val = s[1:]
|
||||
s = ""
|
||||
}
|
||||
case '{':
|
||||
// Nested object — find matching '}'
|
||||
depth := 0
|
||||
end := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '{':
|
||||
depth++
|
||||
case '}':
|
||||
depth--
|
||||
if depth == 0 {
|
||||
end = i
|
||||
goto foundBrace
|
||||
}
|
||||
}
|
||||
}
|
||||
end = len(s) - 1
|
||||
foundBrace:
|
||||
inner := strings.TrimSpace(s[1:end])
|
||||
val = parseArgs(inner)
|
||||
s = strings.TrimSpace(s[end+1:])
|
||||
default:
|
||||
// Bare token (number, enum, etc.) — read until comma or end
|
||||
end := len(s)
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == ',' {
|
||||
end = i
|
||||
break
|
||||
}
|
||||
}
|
||||
val = strings.TrimSpace(s[:end])
|
||||
s = strings.TrimSpace(s[end:])
|
||||
}
|
||||
|
||||
// Strip leading comma separator
|
||||
if len(s) > 0 && s[0] == ',' {
|
||||
s = strings.TrimSpace(s[1:])
|
||||
}
|
||||
|
||||
args[key] = val
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// queryDepth counts the maximum nesting depth of braces in the query.
|
||||
// Each `{` increments depth, each `}` decrements. Ignores quoted strings.
|
||||
func queryDepth(query string) int {
|
||||
maxDepth := 0
|
||||
depth := 0
|
||||
inQuote := false
|
||||
for i := 0; i < len(query); i++ {
|
||||
ch := query[i]
|
||||
if ch == '"' && (i == 0 || query[i-1] != '\\') {
|
||||
inQuote = !inQuote
|
||||
continue
|
||||
}
|
||||
if inQuote {
|
||||
continue
|
||||
}
|
||||
switch ch {
|
||||
case '{':
|
||||
depth++
|
||||
if depth > maxDepth {
|
||||
maxDepth = depth
|
||||
}
|
||||
case '}':
|
||||
depth--
|
||||
}
|
||||
}
|
||||
return maxDepth
|
||||
}
|
||||
|
||||
const graphiqlHTML = `<!DOCTYPE html>
|
||||
<html><head><title>g-node GraphQL</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/graphiql@3/graphiql.min.css"/>
|
||||
</head><body style="margin:0;overflow:hidden">
|
||||
<div id="graphiql" style="height:100vh"></div>
|
||||
<script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
|
||||
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
|
||||
<script src="https://unpkg.com/graphiql@3/graphiql.min.js" crossorigin></script>
|
||||
<script>
|
||||
ReactDOM.createRoot(document.getElementById('graphiql')).render(
|
||||
React.createElement(GraphiQL, {
|
||||
fetcher: GraphiQL.createFetcher({url: '/graphql'}),
|
||||
defaultEditorToolsVisibility: true,
|
||||
})
|
||||
);
|
||||
</script></body></html>`
|
||||
@@ -0,0 +1,55 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/graph/storage"
|
||||
)
|
||||
|
||||
// Core blockchain resolvers — always registered.
|
||||
|
||||
func (e *Engine) resolveBlock(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok {
|
||||
return s.GetBlock(ctx, fmt.Sprint(id))
|
||||
}
|
||||
if num, ok := args["number"]; ok {
|
||||
return s.GetBlockByNumber(ctx, fmt.Sprint(num))
|
||||
}
|
||||
return s.GetLatestBlock(ctx)
|
||||
}
|
||||
|
||||
func (e *Engine) resolveBlocks(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit := 25
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
return s.GetBlocks(ctx, limit)
|
||||
}
|
||||
|
||||
func (e *Engine) resolveTransaction(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if hash, ok := args["id"]; ok {
|
||||
return s.GetTransaction(ctx, fmt.Sprint(hash))
|
||||
}
|
||||
return nil, fmt.Errorf("transaction requires id argument")
|
||||
}
|
||||
|
||||
func (e *Engine) resolveTransactions(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit := 25
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
return s.GetTransactions(ctx, limit)
|
||||
}
|
||||
|
||||
func (e *Engine) resolveToken(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if addr, ok := args["id"]; ok {
|
||||
return s.GetToken(ctx, fmt.Sprint(addr))
|
||||
}
|
||||
return nil, fmt.Errorf("token requires id argument")
|
||||
}
|
||||
|
||||
func (e *Engine) resolveTokens(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit, orderBy, orderDir, where := parseListArgs(args, 100)
|
||||
return s.GetTokens(ctx, limit, orderBy, orderDir, where)
|
||||
}
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
package engine
|
||||
|
||||
// Entity types matching Uniswap V2/V3 subgraph schemas exactly.
|
||||
// Field names match the GraphQL schema so the exchange frontend
|
||||
// queries work without modification.
|
||||
|
||||
// Factory — V3 factory or V2 UniswapFactory
|
||||
type Factory struct {
|
||||
ID string `json:"id"`
|
||||
PoolCount int64 `json:"poolCount"`
|
||||
PairCount int64 `json:"pairCount"` // v2
|
||||
TxCount int64 `json:"txCount"`
|
||||
TotalVolumeUSD string `json:"totalVolumeUSD"`
|
||||
TotalVolumeETH string `json:"totalVolumeETH"`
|
||||
TotalFeesUSD string `json:"totalFeesUSD"`
|
||||
TotalValueLockedUSD string `json:"totalValueLockedUSD"`
|
||||
TotalLiquidityUSD string `json:"totalLiquidityUSD"` // v2 compat
|
||||
TotalValueLockedETH string `json:"totalValueLockedETH"`
|
||||
}
|
||||
|
||||
// Bundle — native token price in USD
|
||||
type Bundle struct {
|
||||
ID string `json:"id"`
|
||||
EthPriceUSD string `json:"ethPriceUSD"`
|
||||
EthPrice string `json:"ethPrice"` // v2 compat
|
||||
LuxPriceUSD string `json:"luxPriceUSD"`
|
||||
}
|
||||
|
||||
// Token — ERC20 token
|
||||
type Token struct {
|
||||
ID string `json:"id"`
|
||||
Symbol string `json:"symbol"`
|
||||
Name string `json:"name"`
|
||||
Decimals int64 `json:"decimals"`
|
||||
TotalSupply string `json:"totalSupply"`
|
||||
Volume string `json:"volume"`
|
||||
VolumeUSD string `json:"volumeUSD"`
|
||||
UntrackedVolumeUSD string `json:"untrackedVolumeUSD"`
|
||||
FeesUSD string `json:"feesUSD"`
|
||||
TxCount int64 `json:"txCount"`
|
||||
PoolCount int64 `json:"poolCount"`
|
||||
TotalValueLocked string `json:"totalValueLocked"`
|
||||
TotalValueLockedUSD string `json:"totalValueLockedUSD"`
|
||||
TotalLiquidity string `json:"totalLiquidity"` // v2
|
||||
DerivedETH string `json:"derivedETH"`
|
||||
TradeVolume string `json:"tradeVolume"` // v2
|
||||
TradeVolumeUSD string `json:"tradeVolumeUSD"` // v2
|
||||
}
|
||||
|
||||
// Pool — V3 concentrated liquidity pool
|
||||
type Pool struct {
|
||||
ID string `json:"id"`
|
||||
CreatedAtTimestamp int64 `json:"createdAtTimestamp"`
|
||||
CreatedAtBlockNumber int64 `json:"createdAtBlockNumber"`
|
||||
Token0 *Token `json:"token0"`
|
||||
Token1 *Token `json:"token1"`
|
||||
FeeTier int64 `json:"feeTier"`
|
||||
Liquidity string `json:"liquidity"`
|
||||
SqrtPrice string `json:"sqrtPrice"`
|
||||
Token0Price string `json:"token0Price"`
|
||||
Token1Price string `json:"token1Price"`
|
||||
Tick int64 `json:"tick"`
|
||||
VolumeToken0 string `json:"volumeToken0"`
|
||||
VolumeToken1 string `json:"volumeToken1"`
|
||||
VolumeUSD string `json:"volumeUSD"`
|
||||
FeesUSD string `json:"feesUSD"`
|
||||
TxCount int64 `json:"txCount"`
|
||||
TotalValueLockedToken0 string `json:"totalValueLockedToken0"`
|
||||
TotalValueLockedToken1 string `json:"totalValueLockedToken1"`
|
||||
TotalValueLockedETH string `json:"totalValueLockedETH"`
|
||||
TotalValueLockedUSD string `json:"totalValueLockedUSD"`
|
||||
}
|
||||
|
||||
// Pair — V2 constant product AMM pair
|
||||
type Pair struct {
|
||||
ID string `json:"id"`
|
||||
Token0 *Token `json:"token0"`
|
||||
Token1 *Token `json:"token1"`
|
||||
Reserve0 string `json:"reserve0"`
|
||||
Reserve1 string `json:"reserve1"`
|
||||
TotalSupply string `json:"totalSupply"`
|
||||
ReserveETH string `json:"reserveETH"`
|
||||
ReserveUSD string `json:"reserveUSD"`
|
||||
TrackedReserveETH string `json:"trackedReserveETH"`
|
||||
Token0Price string `json:"token0Price"`
|
||||
Token1Price string `json:"token1Price"`
|
||||
VolumeToken0 string `json:"volumeToken0"`
|
||||
VolumeToken1 string `json:"volumeToken1"`
|
||||
VolumeUSD string `json:"volumeUSD"`
|
||||
TxCount int64 `json:"txCount"`
|
||||
CreatedAtTimestamp int64 `json:"createdAtTimestamp"`
|
||||
CreatedAtBlockNumber int64 `json:"createdAtBlockNumber"`
|
||||
}
|
||||
|
||||
// Swap — trade event (V2 and V3)
|
||||
type Swap struct {
|
||||
ID string `json:"id"`
|
||||
Transaction string `json:"transaction"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Pool string `json:"pool"`
|
||||
Pair string `json:"pair"` // v2
|
||||
Token0 string `json:"token0"`
|
||||
Token1 string `json:"token1"`
|
||||
Sender string `json:"sender"`
|
||||
Recipient string `json:"recipient"`
|
||||
Origin string `json:"origin"`
|
||||
Amount0 string `json:"amount0"`
|
||||
Amount1 string `json:"amount1"`
|
||||
AmountUSD string `json:"amountUSD"`
|
||||
Amount0In string `json:"amount0In"` // v2
|
||||
Amount0Out string `json:"amount0Out"` // v2
|
||||
Amount1In string `json:"amount1In"` // v2
|
||||
Amount1Out string `json:"amount1Out"` // v2
|
||||
}
|
||||
|
||||
// Mint — liquidity provision event
|
||||
type Mint struct {
|
||||
ID string `json:"id"`
|
||||
Transaction string `json:"transaction"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Pool string `json:"pool"`
|
||||
Pair string `json:"pair"` // v2
|
||||
Sender string `json:"sender"`
|
||||
Owner string `json:"owner"`
|
||||
Amount0 string `json:"amount0"`
|
||||
Amount1 string `json:"amount1"`
|
||||
AmountUSD string `json:"amountUSD"`
|
||||
TickLower int64 `json:"tickLower"` // v3
|
||||
TickUpper int64 `json:"tickUpper"` // v3
|
||||
LogIndex int64 `json:"logIndex"`
|
||||
}
|
||||
|
||||
// Burn — liquidity removal event
|
||||
type Burn struct {
|
||||
ID string `json:"id"`
|
||||
Transaction string `json:"transaction"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Pool string `json:"pool"`
|
||||
Pair string `json:"pair"` // v2
|
||||
Owner string `json:"owner"`
|
||||
Amount0 string `json:"amount0"`
|
||||
Amount1 string `json:"amount1"`
|
||||
AmountUSD string `json:"amountUSD"`
|
||||
TickLower int64 `json:"tickLower"` // v3
|
||||
TickUpper int64 `json:"tickUpper"` // v3
|
||||
LogIndex int64 `json:"logIndex"`
|
||||
}
|
||||
|
||||
// Tick — V3 price tick
|
||||
type Tick struct {
|
||||
ID string `json:"id"`
|
||||
PoolAddress string `json:"poolAddress"`
|
||||
TickIdx int64 `json:"tickIdx"`
|
||||
LiquidityGross string `json:"liquidityGross"`
|
||||
LiquidityNet string `json:"liquidityNet"`
|
||||
Price0 string `json:"price0"`
|
||||
Price1 string `json:"price1"`
|
||||
}
|
||||
|
||||
// Time-series entities
|
||||
|
||||
type TokenDayData struct {
|
||||
ID string `json:"id"`
|
||||
Date int64 `json:"date"`
|
||||
Token *Token `json:"token"`
|
||||
Volume string `json:"volume"`
|
||||
VolumeUSD string `json:"volumeUSD"`
|
||||
TotalValueLocked string `json:"totalValueLocked"`
|
||||
TotalValueLockedUSD string `json:"totalValueLockedUSD"`
|
||||
PriceUSD string `json:"priceUSD"`
|
||||
Open string `json:"open"`
|
||||
High string `json:"high"`
|
||||
Low string `json:"low"`
|
||||
Close string `json:"close"`
|
||||
}
|
||||
|
||||
type PoolDayData struct {
|
||||
ID string `json:"id"`
|
||||
Date int64 `json:"date"`
|
||||
Pool *Pool `json:"pool"`
|
||||
Liquidity string `json:"liquidity"`
|
||||
VolumeUSD string `json:"volumeUSD"`
|
||||
FeesUSD string `json:"feesUSD"`
|
||||
TotalValueLockedUSD string `json:"tvlUSD"`
|
||||
TxCount int64 `json:"txCount"`
|
||||
Open string `json:"open"`
|
||||
High string `json:"high"`
|
||||
Low string `json:"low"`
|
||||
Close string `json:"close"`
|
||||
}
|
||||
|
||||
type PairDayData struct {
|
||||
ID string `json:"id"`
|
||||
Date int64 `json:"date"`
|
||||
Pair *Pair `json:"pair"`
|
||||
VolumeUSD string `json:"dailyVolumeUSD"`
|
||||
ReserveUSD string `json:"reserveUSD"`
|
||||
TxCount int64 `json:"dailyTxns"`
|
||||
}
|
||||
|
||||
type FactoryDayData struct {
|
||||
ID string `json:"id"`
|
||||
Date int64 `json:"date"`
|
||||
VolumeUSD string `json:"dailyVolumeUSD"`
|
||||
TotalValueLockedUSD string `json:"totalLiquidityUSD"`
|
||||
TxCount int64 `json:"dailyTxns"`
|
||||
}
|
||||
|
||||
// Transaction — on-chain transaction
|
||||
type Transaction struct {
|
||||
ID string `json:"id"`
|
||||
BlockNumber int64 `json:"blockNumber"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
GasUsed string `json:"gasUsed"`
|
||||
GasPrice string `json:"gasPrice"`
|
||||
}
|
||||
|
||||
// Collect — V3 fee collection event
|
||||
type Collect struct {
|
||||
ID string `json:"id"`
|
||||
Transaction string `json:"transaction"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Pool string `json:"pool"`
|
||||
Owner string `json:"owner"`
|
||||
Amount0 string `json:"amount0"`
|
||||
Amount1 string `json:"amount1"`
|
||||
TickLower int64 `json:"tickLower"`
|
||||
TickUpper int64 `json:"tickUpper"`
|
||||
}
|
||||
|
||||
// Flash — V3 flash loan event
|
||||
type Flash struct {
|
||||
ID string `json:"id"`
|
||||
Transaction string `json:"transaction"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Pool string `json:"pool"`
|
||||
Sender string `json:"sender"`
|
||||
Recipient string `json:"recipient"`
|
||||
Amount0 string `json:"amount0"`
|
||||
Amount1 string `json:"amount1"`
|
||||
Paid0 string `json:"paid0"`
|
||||
Paid1 string `json:"paid1"`
|
||||
}
|
||||
|
||||
// PairHourData — hourly V2 pair snapshot
|
||||
type PairHourData struct {
|
||||
ID string `json:"id"`
|
||||
PeriodStartUnix int64 `json:"periodStartUnix"`
|
||||
Pair *Pair `json:"pair"`
|
||||
Reserve0 string `json:"reserve0"`
|
||||
Reserve1 string `json:"reserve1"`
|
||||
ReserveUSD string `json:"reserveUSD"`
|
||||
VolumeUSD string `json:"volumeUSD"`
|
||||
TxCount int64 `json:"txCount"`
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/graph/storage"
|
||||
)
|
||||
|
||||
// V4 resolvers — PoolManager singleton, ModifyLiquidity, Positions, Subscriptions.
|
||||
|
||||
func (e *Engine) registerV4Resolvers() {
|
||||
// PoolManager (V4 singleton — also aliased as "factory" for V3 compat)
|
||||
e.resolvers["poolManager"] = e.resolvePoolManager
|
||||
e.resolvers["poolManagers"] = e.resolvePoolManagers
|
||||
|
||||
// ModifyLiquidity (replaces separate mint/burn in V4)
|
||||
e.resolvers["modifyLiquidity"] = e.resolveModifyLiquidity
|
||||
e.resolvers["modifyLiquiditys"] = e.resolveModifyLiquiditys // Graph uses this plural
|
||||
|
||||
// Positions (V4 ERC721)
|
||||
e.resolvers["position"] = e.resolvePosition
|
||||
e.resolvers["positions"] = e.resolvePositions
|
||||
|
||||
// Subscriptions (V4 hooks)
|
||||
e.resolvers["subscribe"] = e.resolveSubscribe
|
||||
e.resolvers["subscribes"] = e.resolveSubscribes
|
||||
e.resolvers["unsubscribe"] = e.resolveUnsubscribe
|
||||
e.resolvers["unsubscribes"] = e.resolveUnsubscribes
|
||||
|
||||
// Hourly data
|
||||
e.resolvers["poolHourDatas"] = e.resolvePoolHourDatas
|
||||
e.resolvers["tokenHourDatas"] = e.resolveTokenHourDatas
|
||||
e.resolvers["uniswapDayDatas"] = e.resolveUniswapDayDatas
|
||||
}
|
||||
|
||||
func (e *Engine) resolvePoolManager(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
id := "1"
|
||||
if v, ok := args["id"]; ok {
|
||||
id = fmt.Sprint(v)
|
||||
}
|
||||
return s.GetFactory(ctx, id) // PoolManager shares factory storage
|
||||
}
|
||||
|
||||
func (e *Engine) resolvePoolManagers(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.GetFactories(ctx)
|
||||
}
|
||||
|
||||
func (e *Engine) resolveModifyLiquidity(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok {
|
||||
return s.GetModifyLiquidity(ctx, fmt.Sprint(id))
|
||||
}
|
||||
return nil, fmt.Errorf("modifyLiquidity requires id")
|
||||
}
|
||||
|
||||
func (e *Engine) resolveModifyLiquiditys(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit, orderBy, orderDir, _ := parseListArgs(args, 100)
|
||||
return s.GetModifyLiquiditys(ctx, limit, orderBy, orderDir)
|
||||
}
|
||||
|
||||
func (e *Engine) resolveSubscribe(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok {
|
||||
return s.GetSubscribe(ctx, fmt.Sprint(id))
|
||||
}
|
||||
return nil, fmt.Errorf("subscribe requires id")
|
||||
}
|
||||
|
||||
func (e *Engine) resolveSubscribes(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit, orderBy, orderDir, _ := parseListArgs(args, 100)
|
||||
return s.GetSubscribes(ctx, limit, orderBy, orderDir)
|
||||
}
|
||||
|
||||
func (e *Engine) resolveUnsubscribe(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok {
|
||||
return s.GetUnsubscribe(ctx, fmt.Sprint(id))
|
||||
}
|
||||
return nil, fmt.Errorf("unsubscribe requires id")
|
||||
}
|
||||
|
||||
func (e *Engine) resolveUnsubscribes(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit, orderBy, orderDir, _ := parseListArgs(args, 100)
|
||||
return s.GetUnsubscribes(ctx, limit, orderBy, orderDir)
|
||||
}
|
||||
|
||||
func (e *Engine) resolvePoolHourDatas(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit, orderBy, orderDir, _ := parseListArgs(args, 24)
|
||||
return s.GetPoolHourDatas(ctx, limit, orderBy, orderDir)
|
||||
}
|
||||
|
||||
func (e *Engine) resolveTokenHourDatas(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit, orderBy, orderDir, _ := parseListArgs(args, 24)
|
||||
return s.GetTokenHourDatas(ctx, limit, orderBy, orderDir)
|
||||
}
|
||||
|
||||
func (e *Engine) resolveUniswapDayDatas(ctx context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
limit, orderBy, orderDir, where := parseListArgs(args, 30)
|
||||
return s.GetFactoryDayDatas(ctx, limit, orderBy, orderDir, where)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package engine
|
||||
|
||||
// V4 entity types — Uniswap V4 (PoolManager singleton, hooks, ModifyLiquidity).
|
||||
// Extends the V3 types. Pool/Token/Bundle/Tick are shared across V2/V3/V4.
|
||||
|
||||
// PoolManager — V4 singleton (replaces per-factory pattern)
|
||||
type PoolManager struct {
|
||||
ID string `json:"id"`
|
||||
PoolCount int64 `json:"poolCount"`
|
||||
TxCount int64 `json:"txCount"`
|
||||
TotalVolumeUSD string `json:"totalVolumeUSD"`
|
||||
TotalVolumeETH string `json:"totalVolumeETH"`
|
||||
TotalFeesUSD string `json:"totalFeesUSD"`
|
||||
TotalFeesETH string `json:"totalFeesETH"`
|
||||
UntrackedVolumeUSD string `json:"untrackedVolumeUSD"`
|
||||
TotalValueLockedUSD string `json:"totalValueLockedUSD"`
|
||||
TotalValueLockedETH string `json:"totalValueLockedETH"`
|
||||
TotalValueLockedUSDUntracked string `json:"totalValueLockedUSDUntracked"`
|
||||
TotalValueLockedETHUntracked string `json:"totalValueLockedETHUntracked"`
|
||||
Owner string `json:"owner"`
|
||||
}
|
||||
|
||||
// ModifyLiquidity — V4 unified liquidity event (replaces separate Mint/Burn)
|
||||
type ModifyLiquidity struct {
|
||||
ID string `json:"id"`
|
||||
Transaction string `json:"transaction"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Pool string `json:"pool"`
|
||||
Token0 string `json:"token0"`
|
||||
Token1 string `json:"token1"`
|
||||
Sender string `json:"sender"`
|
||||
Origin string `json:"origin"`
|
||||
Amount string `json:"amount"` // liquidityDelta (positive=mint, negative=burn)
|
||||
Amount0 string `json:"amount0"`
|
||||
Amount1 string `json:"amount1"`
|
||||
AmountUSD string `json:"amountUSD"`
|
||||
TickLower int64 `json:"tickLower"`
|
||||
TickUpper int64 `json:"tickUpper"`
|
||||
LogIndex int64 `json:"logIndex"`
|
||||
}
|
||||
|
||||
// Position — V4 NFT position (ERC721 tokenId)
|
||||
type Position struct {
|
||||
ID string `json:"id"`
|
||||
TokenID string `json:"tokenId"`
|
||||
Owner string `json:"owner"`
|
||||
Origin string `json:"origin"`
|
||||
CreatedAtTimestamp int64 `json:"createdAtTimestamp"`
|
||||
}
|
||||
|
||||
// Subscribe — V4 hook subscription event
|
||||
type Subscribe struct {
|
||||
ID string `json:"id"`
|
||||
TokenID string `json:"tokenId"`
|
||||
Address string `json:"address"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Origin string `json:"origin"`
|
||||
LogIndex int64 `json:"logIndex"`
|
||||
}
|
||||
|
||||
// Unsubscribe — V4 hook unsubscription event
|
||||
type Unsubscribe struct {
|
||||
ID string `json:"id"`
|
||||
TokenID string `json:"tokenId"`
|
||||
Address string `json:"address"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Origin string `json:"origin"`
|
||||
LogIndex int64 `json:"logIndex"`
|
||||
}
|
||||
|
||||
// PoolV4 extends Pool with V4-specific fields
|
||||
type PoolV4 struct {
|
||||
Pool
|
||||
TickSpacing int64 `json:"tickSpacing"`
|
||||
Hooks string `json:"hooks"`
|
||||
IsExternalLiquidity bool `json:"isExternalLiquidity"`
|
||||
CollectedFeesToken0 string `json:"collectedFeesToken0"`
|
||||
CollectedFeesToken1 string `json:"collectedFeesToken1"`
|
||||
CollectedFeesUSD string `json:"collectedFeesUSD"`
|
||||
LiquidityProviderCount int64 `json:"liquidityProviderCount"`
|
||||
}
|
||||
|
||||
// UniswapDayData — V4 daily aggregate (same name as V3 for compat)
|
||||
type UniswapDayData struct {
|
||||
ID string `json:"id"`
|
||||
Date int64 `json:"date"`
|
||||
VolumeETH string `json:"volumeETH"`
|
||||
VolumeUSD string `json:"volumeUSD"`
|
||||
FeesUSD string `json:"feesUSD"`
|
||||
TxCount int64 `json:"txCount"`
|
||||
TvlUSD string `json:"tvlUSD"`
|
||||
}
|
||||
|
||||
// PoolHourData — hourly pool snapshot
|
||||
type PoolHourData struct {
|
||||
ID string `json:"id"`
|
||||
PeriodStartUnix int64 `json:"periodStartUnix"`
|
||||
Pool *Pool `json:"pool"`
|
||||
Liquidity string `json:"liquidity"`
|
||||
VolumeUSD string `json:"volumeUSD"`
|
||||
FeesUSD string `json:"feesUSD"`
|
||||
TvlUSD string `json:"tvlUSD"`
|
||||
TxCount int64 `json:"txCount"`
|
||||
Open string `json:"open"`
|
||||
High string `json:"high"`
|
||||
Low string `json:"low"`
|
||||
Close string `json:"close"`
|
||||
}
|
||||
|
||||
// TokenHourData — hourly token snapshot
|
||||
type TokenHourData struct {
|
||||
ID string `json:"id"`
|
||||
PeriodStartUnix int64 `json:"periodStartUnix"`
|
||||
Token *Token `json:"token"`
|
||||
Volume string `json:"volume"`
|
||||
VolumeUSD string `json:"volumeUSD"`
|
||||
TotalValueLocked string `json:"totalValueLocked"`
|
||||
TotalValueLockedUSD string `json:"totalValueLockedUSD"`
|
||||
PriceUSD string `json:"priceUSD"`
|
||||
FeesUSD string `json:"feesUSD"`
|
||||
Open string `json:"open"`
|
||||
High string `json:"high"`
|
||||
Low string `json:"low"`
|
||||
Close string `json:"close"`
|
||||
}
|
||||
|
||||
// Hook entities
|
||||
|
||||
type EulerSwapHook struct {
|
||||
ID string `json:"id"`
|
||||
Hook string `json:"hook"`
|
||||
EulerAccount string `json:"eulerAccount"`
|
||||
Asset0 string `json:"asset0"`
|
||||
Asset1 string `json:"asset1"`
|
||||
}
|
||||
|
||||
type ArrakisHook struct {
|
||||
ID string `json:"id"`
|
||||
Module string `json:"module"`
|
||||
Salt string `json:"salt"`
|
||||
CreatedAtTimestamp int64 `json:"createdAtTimestamp"`
|
||||
CreatedAtBlockNumber int64 `json:"createdAtBlockNumber"`
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
module github.com/luxfi/graph
|
||||
|
||||
go 1.26.1
|
||||
|
||||
require (
|
||||
github.com/hanzoai/replicate v0.6.0
|
||||
github.com/luxfi/age v1.4.0
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/hpke v0.4.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hablullah/go-hijri v1.0.2 // indirect
|
||||
github.com/hablullah/go-juliandays v1.0.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/lmittmann/tint v1.1.3 // indirect
|
||||
github.com/magefile/mage v1.14.0 // indirect
|
||||
github.com/markusmobius/go-dateparser v1.2.4 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/prometheus/client_golang v1.17.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.45.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/psanford/sqlite3vfs v0.0.0-20251127171934-4e34e03a991a // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/superfly/ltx v0.5.1 // indirect
|
||||
github.com/tetratelabs/wazero v1.2.1 // indirect
|
||||
github.com/wasilibs/go-re2 v1.3.0 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
modernc.org/libc v1.67.6 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.44.3 // indirect
|
||||
)
|
||||
@@ -0,0 +1,254 @@
|
||||
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAwLGRZ+sebTUruuM4Ra3M=
|
||||
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
|
||||
cloud.google.com/go v0.111.0 h1:YHLKNupSD1KqjDbQ3+LVdQ81h/UJbJyZG203cEfnQgM=
|
||||
cloud.google.com/go v0.111.0/go.mod h1:0mibmpKP1TyOOFYQY5izo0LnT+ecvOQ0Sg3OdmMiNRU=
|
||||
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
|
||||
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI=
|
||||
cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8=
|
||||
cloud.google.com/go/storage v1.36.0 h1:P0mOkAcaJxhCTvAkMhxMfrTKiNcub4YmmPBtlhAyTr8=
|
||||
cloud.google.com/go/storage v1.36.0/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8=
|
||||
filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
|
||||
filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2 h1:Hr5FTipp7SL07o2FvoVOX9HRiRH3CR3Mj8pxqCcdD5A=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2/go.mod h1:QyVsSSN64v5TGltphKLQ2sQxe4OBQg0J1eKRcVBnfgE=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0 h1:MhRfI58HblXzCtWEZCO0feHs8LweePB3s90r7WaR1KU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0/go.mod h1:okZ+ZURbArNdlJ+ptXoyHNuOETzOl1Oww19rm8I2WLA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2 h1:FwladfywkNirM+FZYLBR2kBz5C8Tg0fw5w5Y7meRXWI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2/go.mod h1:vv5Ad0RrIoT1lJFdWBZwt4mB1+j+V8DUroixmKDTCdk=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.3.0 h1:wQlqotpyjYPjJz+Noh5bRu7Snmydk8SKC5Z6u1CR20Y=
|
||||
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.3.0/go.mod h1:FTzydeQVmR24FI0D6XWUOMKckjXehM/jgMn1xC+DA9M=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.18 h1:9vWXHtaepwoAl/UuKzxwgOoJDXPCC3hvgNMfcmdS2Tk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.18/go.mod h1:sKuUZ+MwUTuJbYvZ8pK0x10LvgcJK3Y4rmh63YBekwk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
|
||||
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
||||
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
|
||||
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
|
||||
github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k=
|
||||
github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ=
|
||||
github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0=
|
||||
github.com/hablullah/go-juliandays v1.0.0/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc=
|
||||
github.com/hanzoai/replicate v0.6.0 h1:5Rl34569IHJ5hXOvrkjbadfKNt/2rGZd83vhcSIRSPE=
|
||||
github.com/hanzoai/replicate v0.6.0/go.mod h1:PBvHNQ7cXBJejTWDWxXWvtUwjT5O66pTC7npxLWu8H0=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 h1:qxLoi6CAcXVzjfvu+KXIXJOAsQB62LXjsfbOaErsVzE=
|
||||
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958/go.mod h1:Wqfu7mjUHj9WDzSSPI5KfBclTTEnLveRUFr/ujWnTgE=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I=
|
||||
github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||
github.com/luxfi/age v1.4.0 h1:tU5q65RQSQdaVq64Z/DVUhiPQMoMPQT6pr41LsvG0AQ=
|
||||
github.com/luxfi/age v1.4.0/go.mod h1:iAYAxgvrXxcy746+Ovh/eWWDuF9teJLNcCSSOX9RYW0=
|
||||
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/markusmobius/go-dateparser v1.2.4 h1:2e8XJozaERVxGwsRg72coi51L2aiYqE2gukkdLc85ck=
|
||||
github.com/markusmobius/go-dateparser v1.2.4/go.mod h1:CBAUADJuMNhJpyM6IYaWAoFhtKaqnUcznY2cL7gNugY=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
|
||||
github.com/nats-io/nats.go v1.44.0 h1:ECKVrDLdh/kDPV1g0gAQ+2+m2KprqZK5O/eJAyAnH2M=
|
||||
github.com/nats-io/nats.go v1.44.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
|
||||
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
|
||||
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
|
||||
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
|
||||
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/psanford/sqlite3vfs v0.0.0-20251127171934-4e34e03a991a h1:r4YWl0uVObCbBFvj1VsIlyHzgZwZOHvY1KdRaQjzzUc=
|
||||
github.com/psanford/sqlite3vfs v0.0.0-20251127171934-4e34e03a991a/go.mod h1:iW4cSew5PAb1sMZiTEkVJAIBNrepaB6jTYjeP47WtI0=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/studio-b12/gowebdav v0.11.0 h1:qbQzq4USxY28ZYsGJUfO5jR+xkFtcnwWgitp4Zp1irU=
|
||||
github.com/studio-b12/gowebdav v0.11.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
|
||||
github.com/superfly/ltx v0.5.1 h1:vGUeBhKvBKZ2s2TsTrOSahz+m0PswfqOWDAD+ICiiYY=
|
||||
github.com/superfly/ltx v0.5.1/go.mod h1:Nf50QAIXU/ET4ua3AuQ2fh31MbgNQZA7r/DYx6Os77s=
|
||||
github.com/tetratelabs/wazero v1.2.1 h1:J4X2hrGzJvt+wqltuvcSjHQ7ujQxA9gb6PeMs4qlUWs=
|
||||
github.com/tetratelabs/wazero v1.2.1/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ=
|
||||
github.com/wasilibs/go-re2 v1.3.0 h1:LFhBNzoStM3wMie6rN2slD1cuYH2CGiHpvNL3UtcsMw=
|
||||
github.com/wasilibs/go-re2 v1.3.0/go.mod h1:AafrCXVvGRJJOImMajgJ2M7rVmWyisVK7sFshbxnVrg=
|
||||
github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ=
|
||||
github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK8jetlB3HIo=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
|
||||
go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
|
||||
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
|
||||
go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
|
||||
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
|
||||
go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
|
||||
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.154.0 h1:X7QkVKZBskztmpPKWQXgjJRPA2dJYrL6r+sYPRLj050=
|
||||
google.golang.org/api v0.154.0/go.mod h1:qhSMkM85hgqiokIYsrRyKxrjfBeIhgl4Z2JmeRkYylc=
|
||||
google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos=
|
||||
google.golang.org/genproto v0.0.0-20231212172506-995d672761c0/go.mod h1:l/k7rMz0vFTBPy+tFSGvXEd3z+BcoG1k7EHbqm+YBsY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0 h1:s1w3X6gQxwrLEpxnLd/qXTVLgQE2yXwaOaoa6IlY/+o=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0/go.mod h1:CAny0tYF+0/9rmDB9fahA9YLzX3+AEVl1qXbv5hhj6c=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 h1:/jFB8jK5R3Sq3i/lmeZO0cATSzFfZaJq1J2Euan3XKU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0/go.mod h1:FUoWkonphQm3RhTS+kOEhF8h0iDpm4tdXolVCeZ9KKA=
|
||||
google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
|
||||
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
||||
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
|
||||
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY=
|
||||
modernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
@@ -0,0 +1,100 @@
|
||||
package indexer
|
||||
|
||||
// EVM event signatures for Uniswap V2/V3 DEX indexing.
|
||||
// These are keccak256 hashes of the canonical event signatures.
|
||||
|
||||
// V2 events
|
||||
const (
|
||||
// PairCreated(address indexed token0, address indexed token1, address pair, uint256)
|
||||
SigPairCreated = "0x0d3648bd0f6ba80134a33ba9275ac585d9d315f0ad8355cddefde31afa28d0e9"
|
||||
|
||||
// Swap(address indexed sender, uint256 amount0In, uint256 amount1In, uint256 amount0Out, uint256 amount1Out, address indexed to)
|
||||
SigSwapV2 = "0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822"
|
||||
|
||||
// Mint(address indexed sender, uint256 amount0, uint256 amount1)
|
||||
SigMintV2 = "0x4c209b5fc8ad50758f13e2e1088ba56a560dff690a1c6fef26394f4c03821c4f"
|
||||
|
||||
// Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to)
|
||||
SigBurnV2 = "0xdccd412f0b1252819cb1fd330b93224ca42612892bb3f4f789976e6d81936496"
|
||||
|
||||
// Sync(uint112 reserve0, uint112 reserve1)
|
||||
SigSync = "0x1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1"
|
||||
|
||||
// Transfer(address indexed from, address indexed to, uint256 value)
|
||||
SigTransfer = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
|
||||
)
|
||||
|
||||
// V3 events
|
||||
const (
|
||||
// PoolCreated(address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool)
|
||||
SigPoolCreated = "0x783cca1c0412dd0d695e784568c96da2e9c22ff989357a2e8b1d9b2b4e6b7118"
|
||||
|
||||
// Initialize(uint160 sqrtPriceX96, int24 tick)
|
||||
SigInitialize = "0x98636036cb66a9c19a37435efc1e90142190214e8abeb821bdba3f2990dd4c95"
|
||||
|
||||
// Swap(address indexed sender, address indexed recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick)
|
||||
SigSwapV3 = "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67"
|
||||
|
||||
// Mint(address sender, address indexed owner, int24 indexed tickLower, int24 indexed tickUpper, uint128 amount, uint256 amount0, uint256 amount1)
|
||||
SigMintV3 = "0x7a53080ba414158be7ec69b987b5fb7d07dee101fe85488f0853ae16239d0bde"
|
||||
|
||||
// Burn(address indexed owner, int24 indexed tickLower, int24 indexed tickUpper, uint128 amount, uint256 amount0, uint256 amount1)
|
||||
SigBurnV3 = "0x0c396cd989a39f4459b5fa1aed6a9a8dcdbc45908acfd67e028cd568da98982c"
|
||||
|
||||
// Collect(address indexed owner, address recipient, int24 indexed tickLower, int24 indexed tickUpper, uint128 amount0, uint128 amount1)
|
||||
SigCollect = "0x70935338e69775456b0c0043e55b3188a159c4f5e6de2a46547cb5286cd0a097"
|
||||
|
||||
// Flash(address indexed sender, address indexed recipient, uint256 amount0, uint256 amount1, uint256 paid0, uint256 paid1)
|
||||
SigFlash = "0xbdbdb71d7860376ba52b25a5028beea23581364a40522f6bcfb86bb1f2dca633"
|
||||
)
|
||||
|
||||
// V4 events (PoolManager singleton pattern)
|
||||
const (
|
||||
// Initialize(bytes32 indexed id, address indexed currency0, address indexed currency1, uint24 fee, int24 tickSpacing, address hooks, uint160 sqrtPriceX96, int24 tick)
|
||||
SigInitializeV4 = "0x344560c924012b32cbed54ad0e83e24c2cf3e723de46e06c72e6ab3463a7a8c0"
|
||||
|
||||
// ModifyLiquidity(bytes32 indexed id, address indexed sender, int24 tickLower, int24 tickUpper, int256 liquidityDelta, bytes32 salt)
|
||||
SigModifyLiquidity = "0xf208f4912782fd87d4a358e8291c46e9fefb649e838a67dcaa5db23503a4f53b"
|
||||
|
||||
// Swap(bytes32 indexed id, address indexed sender, int128 amount0, int128 amount1, uint160 sqrtPriceX96After, uint128 liquidityAfter, int24 tickAfter)
|
||||
SigSwapV4 = "0x40e9cecb9f5f1f1c5b9c97dec2917b7ee92e57ba5563708daca94dd84ad7112f"
|
||||
|
||||
// Subscription(uint256 indexed tokenId, address indexed subscriber)
|
||||
SigSubscription = "0x572f161235911da04685a68c6b07422b4ba12dab20405b8b230c2420f49f1266"
|
||||
|
||||
// Unsubscription(uint256 indexed tokenId, address indexed subscriber)
|
||||
SigUnsubscription = "0x6112cc683e05f5c607f9d5f0f57a0f0be2ca872a8f94ed4b6e2d09da2f1d1882"
|
||||
)
|
||||
|
||||
// Known factory addresses per network.
|
||||
type NetworkConfig struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
ChainID int64 `json:"chainId" yaml:"chain_id"`
|
||||
FactoryV2 string `json:"factoryV2" yaml:"factory_v2"`
|
||||
FactoryV3 string `json:"factoryV3" yaml:"factory_v3"`
|
||||
WETH string `json:"weth" yaml:"weth"` // wrapped native token
|
||||
StableTokens []string `json:"stableTokens" yaml:"stable_tokens"`
|
||||
}
|
||||
|
||||
// Lux network configs — same addresses as exchange subgraphs.
|
||||
var LuxMainnet = NetworkConfig{
|
||||
Name: "lux",
|
||||
ChainID: 96369,
|
||||
FactoryV2: "0xD173926A10A0C4eCd3A51B1422270b65Df0551c1",
|
||||
FactoryV3: "0x80bBc7C4C7a59C899D1B37BC14539A22D5830a84",
|
||||
WETH: "0x4888e4a2ee0f03051c72d2bd3acf755ed3498b3e", // WLUX
|
||||
StableTokens: []string{
|
||||
"0x848Cff46eb323f323b6Bbe1Df274E40793d7f2c2", // LUSD
|
||||
},
|
||||
}
|
||||
|
||||
var ZooMainnet = NetworkConfig{
|
||||
Name: "zoo",
|
||||
ChainID: 200200,
|
||||
FactoryV2: "0xF034942c1140125b5c278aE9cEE1B488e915B2FE",
|
||||
FactoryV3: "0x80bBc7C4C7a59C899D1B37BC14539A22D5830a84",
|
||||
WETH: "0x5491216406daB99b7032b83765F36790E27F8A61", // WLUX
|
||||
StableTokens: []string{
|
||||
"0xb2ee1CE7b84853b83AA08702aD0aD4D79711882D", // LUSDC
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
// Package indexer subscribes to EVM events and writes to storage.
|
||||
//
|
||||
// Connects to any EVM JSON-RPC, polls for new blocks, decodes logs,
|
||||
// and writes structured data (swaps, mints, burns, transfers) to storage.
|
||||
package indexer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/luxfi/graph/storage"
|
||||
)
|
||||
|
||||
// Status reports indexer progress.
|
||||
type Status struct {
|
||||
LatestBlock uint64 `json:"latestBlock"`
|
||||
IndexedEvents uint64 `json:"indexedEvents"`
|
||||
}
|
||||
|
||||
// Indexer watches an EVM RPC and writes events to storage.
|
||||
type Indexer struct {
|
||||
rpc string
|
||||
store *storage.Store
|
||||
client *http.Client
|
||||
|
||||
lastBlock uint64
|
||||
status Status
|
||||
}
|
||||
|
||||
// New creates an indexer connected to the given RPC endpoint.
|
||||
func New(rpc string, store *storage.Store) *Indexer {
|
||||
idx := &Indexer{
|
||||
rpc: rpc,
|
||||
store: store,
|
||||
client: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: 10,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
},
|
||||
},
|
||||
}
|
||||
idx.lastBlock = idx.store.GetLastBlock()
|
||||
return idx
|
||||
}
|
||||
|
||||
// Status returns current indexer progress.
|
||||
func (idx *Indexer) Status() Status {
|
||||
return idx.status
|
||||
}
|
||||
|
||||
// Run starts the indexer loop. Blocks until ctx is cancelled.
|
||||
func (idx *Indexer) Run(ctx context.Context) error {
|
||||
log.Printf("[indexer] starting — rpc=%s lastBlock=%d", idx.rpc, idx.lastBlock)
|
||||
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
if err := idx.poll(ctx); err != nil {
|
||||
log.Printf("[indexer] poll error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rpcCall makes a JSON-RPC POST and returns the result field.
|
||||
func (idx *Indexer) rpcCall(ctx context.Context, method string, params interface{}) (json.RawMessage, error) {
|
||||
type rpcReq struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
Method string `json:"method"`
|
||||
Params interface{} `json:"params"`
|
||||
ID int `json:"id"`
|
||||
}
|
||||
type rpcResp struct {
|
||||
Result json.RawMessage `json:"result"`
|
||||
Error *struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
body, err := json.Marshal(rpcReq{JSONRPC: "2.0", Method: method, Params: params, ID: 1})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", idx.rpc, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := idx.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 64<<20))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rr rpcResp
|
||||
if err := json.Unmarshal(respBody, &rr); err != nil {
|
||||
return nil, fmt.Errorf("rpc decode: %w", err)
|
||||
}
|
||||
if rr.Error != nil {
|
||||
return nil, fmt.Errorf("rpc error %d: %s", rr.Error.Code, rr.Error.Message)
|
||||
}
|
||||
return rr.Result, nil
|
||||
}
|
||||
|
||||
// parseHexUint64 parses a 0x-prefixed hex string to uint64.
|
||||
func parseHexUint64(s string) (uint64, error) {
|
||||
s = strings.TrimPrefix(s, "0x")
|
||||
return strconv.ParseUint(s, 16, 64)
|
||||
}
|
||||
|
||||
// knownTopics returns all event topic0s we want to filter for.
|
||||
func knownTopics() []string {
|
||||
return []string{
|
||||
SigPairCreated, SigSwapV2, SigMintV2, SigBurnV2, SigSync, SigTransfer,
|
||||
SigPoolCreated, SigInitialize, SigSwapV3, SigMintV3, SigBurnV3,
|
||||
SigCollect, SigFlash,
|
||||
SigInitializeV4, SigModifyLiquidity, SigSwapV4,
|
||||
}
|
||||
}
|
||||
|
||||
// logEntry is a decoded eth_getLogs result entry.
|
||||
type logEntry struct {
|
||||
Address string `json:"address"`
|
||||
Topics []string `json:"topics"`
|
||||
Data string `json:"data"`
|
||||
BlockNumber string `json:"blockNumber"`
|
||||
TransactionHash string `json:"transactionHash"`
|
||||
LogIndex string `json:"logIndex"`
|
||||
TransactionIndex string `json:"transactionIndex"`
|
||||
}
|
||||
|
||||
func (idx *Indexer) poll(ctx context.Context) error {
|
||||
// 1. Get latest block number
|
||||
raw, err := idx.rpcCall(ctx, "eth_blockNumber", []interface{}{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("eth_blockNumber: %w", err)
|
||||
}
|
||||
|
||||
var hexBlock string
|
||||
if err := json.Unmarshal(raw, &hexBlock); err != nil {
|
||||
return fmt.Errorf("parse blockNumber: %w", err)
|
||||
}
|
||||
latest, err := parseHexUint64(hexBlock)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse hex block: %w", err)
|
||||
}
|
||||
|
||||
// Nothing new
|
||||
if latest <= idx.lastBlock {
|
||||
return nil
|
||||
}
|
||||
|
||||
fromBlock := idx.lastBlock + 1
|
||||
toBlock := latest
|
||||
// Cap batch size to 2000 blocks
|
||||
if toBlock-fromBlock > 2000 {
|
||||
toBlock = fromBlock + 2000
|
||||
}
|
||||
|
||||
// 2. Get logs for known event signatures
|
||||
filter := map[string]interface{}{
|
||||
"fromBlock": fmt.Sprintf("0x%x", fromBlock),
|
||||
"toBlock": fmt.Sprintf("0x%x", toBlock),
|
||||
"topics": []interface{}{knownTopics()},
|
||||
}
|
||||
|
||||
raw, err = idx.rpcCall(ctx, "eth_getLogs", []interface{}{filter})
|
||||
if err != nil {
|
||||
return fmt.Errorf("eth_getLogs: %w", err)
|
||||
}
|
||||
|
||||
var logs []logEntry
|
||||
if err := json.Unmarshal(raw, &logs); err != nil {
|
||||
return fmt.Errorf("parse logs: %w", err)
|
||||
}
|
||||
|
||||
// 3. Process each log
|
||||
for i := range logs {
|
||||
idx.processLog(&logs[i])
|
||||
idx.status.IndexedEvents++
|
||||
}
|
||||
|
||||
// 4. Update progress
|
||||
idx.lastBlock = toBlock
|
||||
idx.status.LatestBlock = toBlock
|
||||
idx.store.SetLastBlock(toBlock)
|
||||
|
||||
if len(logs) > 0 {
|
||||
log.Printf("[indexer] blocks %d..%d — %d events", fromBlock, toBlock, len(logs))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// processLog matches a log entry's topic0 and writes to storage.
|
||||
func (idx *Indexer) processLog(l *logEntry) {
|
||||
if len(l.Topics) == 0 {
|
||||
return
|
||||
}
|
||||
topic0 := l.Topics[0]
|
||||
blockNum, _ := parseHexUint64(l.BlockNumber)
|
||||
txHash := l.TransactionHash
|
||||
logIdx := l.LogIndex
|
||||
|
||||
switch topic0 {
|
||||
case SigSwapV2:
|
||||
idx.handleSwapV2(l, blockNum, txHash, logIdx)
|
||||
case SigSwapV3:
|
||||
idx.handleSwapV3(l, blockNum, txHash, logIdx)
|
||||
case SigPairCreated:
|
||||
idx.handlePairCreated(l)
|
||||
case SigPoolCreated:
|
||||
idx.handlePoolCreated(l)
|
||||
case SigTransfer:
|
||||
idx.handleTransfer(l, txHash, logIdx)
|
||||
case SigMintV2, SigMintV3, SigBurnV2, SigBurnV3, SigSync,
|
||||
SigCollect, SigFlash, SigInitialize,
|
||||
SigInitializeV4, SigModifyLiquidity, SigSwapV4:
|
||||
// Recognized but storage for these types not yet wired
|
||||
}
|
||||
}
|
||||
|
||||
// decodeUint256 reads a 32-byte hex word from data at the given word index.
|
||||
func decodeUint256(data string, wordIndex int) *big.Int {
|
||||
data = strings.TrimPrefix(data, "0x")
|
||||
start := wordIndex * 64
|
||||
if start+64 > len(data) {
|
||||
return new(big.Int)
|
||||
}
|
||||
n := new(big.Int)
|
||||
n.SetString(data[start:start+64], 16)
|
||||
return n
|
||||
}
|
||||
|
||||
// topicAddr extracts an address from a topic (last 40 hex chars of 66-char topic).
|
||||
func topicAddr(topic string) string {
|
||||
if len(topic) >= 42 {
|
||||
return "0x" + topic[len(topic)-40:]
|
||||
}
|
||||
return topic
|
||||
}
|
||||
|
||||
func (idx *Indexer) handleSwapV2(l *logEntry, blockNum uint64, txHash, logIdx string) {
|
||||
id := fmt.Sprintf("%s#%s", txHash, logIdx)
|
||||
sender := ""
|
||||
if len(l.Topics) > 1 {
|
||||
sender = topicAddr(l.Topics[1])
|
||||
}
|
||||
amount0In := decodeUint256(l.Data, 0)
|
||||
amount1In := decodeUint256(l.Data, 1)
|
||||
amount0Out := decodeUint256(l.Data, 2)
|
||||
amount1Out := decodeUint256(l.Data, 3)
|
||||
|
||||
// Net amounts
|
||||
amount0 := new(big.Int).Sub(amount0In, amount0Out)
|
||||
amount1 := new(big.Int).Sub(amount1In, amount1Out)
|
||||
|
||||
idx.store.SeedSwap(id, &storage.SeedSwapData{
|
||||
Timestamp: int64(blockNum),
|
||||
Pool: l.Address,
|
||||
Amount0: amount0.String(),
|
||||
Amount1: amount1.String(),
|
||||
AmountUSD: "0",
|
||||
Sender: sender,
|
||||
})
|
||||
}
|
||||
|
||||
func (idx *Indexer) handleSwapV3(l *logEntry, blockNum uint64, txHash, logIdx string) {
|
||||
id := fmt.Sprintf("%s#%s", txHash, logIdx)
|
||||
sender := ""
|
||||
if len(l.Topics) > 1 {
|
||||
sender = topicAddr(l.Topics[1])
|
||||
}
|
||||
amount0 := decodeUint256(l.Data, 0)
|
||||
amount1 := decodeUint256(l.Data, 1)
|
||||
|
||||
idx.store.SeedSwap(id, &storage.SeedSwapData{
|
||||
Timestamp: int64(blockNum),
|
||||
Pool: l.Address,
|
||||
Amount0: amount0.String(),
|
||||
Amount1: amount1.String(),
|
||||
AmountUSD: "0",
|
||||
Sender: sender,
|
||||
})
|
||||
}
|
||||
|
||||
func (idx *Indexer) handlePairCreated(l *logEntry) {
|
||||
if len(l.Topics) < 3 {
|
||||
return
|
||||
}
|
||||
data := strings.TrimPrefix(l.Data, "0x")
|
||||
if len(data) < 64 {
|
||||
return
|
||||
}
|
||||
token0 := topicAddr(l.Topics[1])
|
||||
token1 := topicAddr(l.Topics[2])
|
||||
pair := "0x" + data[:64]
|
||||
pair = topicAddr("0x" + pair[len(pair)-40:])
|
||||
|
||||
idx.store.SeedPool(pair, &storage.SeedPoolData{
|
||||
Token0: token0,
|
||||
Token1: token1,
|
||||
FeeTier: 3000,
|
||||
})
|
||||
idx.store.SeedToken(token0, &storage.SeedTokenData{Symbol: token0[:8], Name: token0, Decimals: 18})
|
||||
idx.store.SeedToken(token1, &storage.SeedTokenData{Symbol: token1[:8], Name: token1, Decimals: 18})
|
||||
}
|
||||
|
||||
func (idx *Indexer) handlePoolCreated(l *logEntry) {
|
||||
if len(l.Topics) < 4 {
|
||||
return
|
||||
}
|
||||
data := strings.TrimPrefix(l.Data, "0x")
|
||||
if len(data) < 64 {
|
||||
return
|
||||
}
|
||||
token0 := topicAddr(l.Topics[1])
|
||||
token1 := topicAddr(l.Topics[2])
|
||||
feeHex := strings.TrimPrefix(l.Topics[3], "0x")
|
||||
fee, _ := strconv.ParseInt(feeHex, 16, 64)
|
||||
|
||||
// Pool address is in data (last 20 bytes of first 32-byte word if padded, or second word)
|
||||
pool := topicAddr("0x" + strings.TrimPrefix(l.Data, "0x"))
|
||||
|
||||
idx.store.SeedPool(pool, &storage.SeedPoolData{
|
||||
Token0: token0,
|
||||
Token1: token1,
|
||||
FeeTier: fee,
|
||||
})
|
||||
idx.store.SeedToken(token0, &storage.SeedTokenData{Symbol: token0[:8], Name: token0, Decimals: 18})
|
||||
idx.store.SeedToken(token1, &storage.SeedTokenData{Symbol: token1[:8], Name: token1, Decimals: 18})
|
||||
}
|
||||
|
||||
func (idx *Indexer) handleTransfer(l *logEntry, txHash, logIdx string) {
|
||||
// ERC20 Transfer — just record the token if we see it
|
||||
if len(l.Topics) >= 3 {
|
||||
idx.store.SeedToken(l.Address, &storage.SeedTokenData{
|
||||
Symbol: l.Address[:8], Name: l.Address, Decimals: 18,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Package ai provides resolvers for the A-Chain (AIVM).
|
||||
//
|
||||
// Indexes: inference proofs, model hashes, compute attestations, jobs, training runs.
|
||||
//
|
||||
// Entities: InferenceProof, ModelHash, ComputeAttestation, AIJob, TrainingRun
|
||||
package ai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/graph/storage"
|
||||
)
|
||||
|
||||
type ResolverFunc = func(context.Context, *storage.Store, map[string]interface{}) (interface{}, error)
|
||||
|
||||
func Register(resolvers map[string]ResolverFunc) {
|
||||
resolvers["inferenceProof"] = resolveInferenceProof
|
||||
resolvers["inferenceProofs"] = resolveInferenceProofs
|
||||
resolvers["modelHash"] = resolveModelHash
|
||||
resolvers["modelHashes"] = resolveModelHashes
|
||||
resolvers["computeAttestation"] = resolveComputeAttestation
|
||||
resolvers["computeAttestations"] = resolveComputeAttestations
|
||||
resolvers["aiJob"] = resolveAIJob
|
||||
resolvers["aiJobs"] = resolveAIJobs
|
||||
resolvers["trainingRun"] = resolveTrainingRun
|
||||
resolvers["trainingRuns"] = resolveTrainingRuns
|
||||
resolvers["aiStats"] = resolveAIStats
|
||||
}
|
||||
|
||||
func resolveInferenceProof(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("InferenceProof", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("inferenceProof requires id")
|
||||
}
|
||||
func resolveInferenceProofs(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("InferenceProof", pl(args))
|
||||
}
|
||||
func resolveModelHash(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("ModelHash", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("modelHash requires id")
|
||||
}
|
||||
func resolveModelHashes(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("ModelHash", pl(args))
|
||||
}
|
||||
func resolveComputeAttestation(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("ComputeAttestation", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("computeAttestation requires id")
|
||||
}
|
||||
func resolveComputeAttestations(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("ComputeAttestation", pl(args))
|
||||
}
|
||||
func resolveAIJob(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("AIJob", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("aiJob requires id")
|
||||
}
|
||||
func resolveAIJobs(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("AIJob", pl(args))
|
||||
}
|
||||
func resolveTrainingRun(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("TrainingRun", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("trainingRun requires id")
|
||||
}
|
||||
func resolveTrainingRuns(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("TrainingRun", pl(args))
|
||||
}
|
||||
func resolveAIStats(_ context.Context, s *storage.Store, _ map[string]interface{}) (interface{}, error) {
|
||||
return s.GetByType("AIStats", "1")
|
||||
}
|
||||
|
||||
func pl(args map[string]interface{}) int {
|
||||
limit := 100
|
||||
if l, ok := args["first"]; ok { fmt.Sscanf(fmt.Sprint(l), "%d", &limit) }
|
||||
return limit
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Package bridge provides resolvers for the B-Chain (BridgeVM).
|
||||
//
|
||||
// Indexes: cross-chain transfers, MPC signatures, bridge requests, wrapped assets.
|
||||
//
|
||||
// Entities: BridgeTransfer, MPCSignature, WrappedAsset, BridgeRequest
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/graph/storage"
|
||||
)
|
||||
|
||||
type ResolverFunc = func(context.Context, *storage.Store, map[string]interface{}) (interface{}, error)
|
||||
|
||||
func Register(resolvers map[string]ResolverFunc) {
|
||||
resolvers["bridgeTransfer"] = resolveBridgeTransfer
|
||||
resolvers["bridgeTransfers"] = resolveBridgeTransfers
|
||||
resolvers["mpcSignature"] = resolveMPCSignature
|
||||
resolvers["mpcSignatures"] = resolveMPCSignatures
|
||||
resolvers["wrappedAsset"] = resolveWrappedAsset
|
||||
resolvers["wrappedAssets"] = resolveWrappedAssets
|
||||
resolvers["bridgeRequest"] = resolveBridgeRequest
|
||||
resolvers["bridgeRequests"] = resolveBridgeRequests
|
||||
resolvers["bridgeStats"] = resolveBridgeStats
|
||||
}
|
||||
|
||||
func resolveBridgeTransfer(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("BridgeTransfer", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("bridgeTransfer requires id")
|
||||
}
|
||||
func resolveBridgeTransfers(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("BridgeTransfer", pl(args))
|
||||
}
|
||||
func resolveMPCSignature(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("MPCSignature", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("mpcSignature requires id")
|
||||
}
|
||||
func resolveMPCSignatures(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("MPCSignature", pl(args))
|
||||
}
|
||||
func resolveWrappedAsset(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("WrappedAsset", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("wrappedAsset requires id")
|
||||
}
|
||||
func resolveWrappedAssets(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("WrappedAsset", pl(args))
|
||||
}
|
||||
func resolveBridgeRequest(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("BridgeRequest", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("bridgeRequest requires id")
|
||||
}
|
||||
func resolveBridgeRequests(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("BridgeRequest", pl(args))
|
||||
}
|
||||
func resolveBridgeStats(_ context.Context, s *storage.Store, _ map[string]interface{}) (interface{}, error) {
|
||||
return s.GetByType("BridgeStats", "1")
|
||||
}
|
||||
|
||||
func pl(args map[string]interface{}) int {
|
||||
limit := 100
|
||||
if l, ok := args["first"]; ok { fmt.Sscanf(fmt.Sprint(l), "%d", &limit) }
|
||||
return limit
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Package dex provides resolvers for the native Lux DEX (D-Chain).
|
||||
//
|
||||
// The native DEX is a CLOB (central limit order book) with perpetuals,
|
||||
// NOT an AMM. This indexes DexVM state via RPC.
|
||||
//
|
||||
// Entities: Order, Fill, Market, Position, FundingRate, Liquidation
|
||||
package dex
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/graph/storage"
|
||||
)
|
||||
|
||||
// ResolverFunc matches engine.ResolverFunc.
|
||||
type ResolverFunc = func(context.Context, *storage.Store, map[string]interface{}) (interface{}, error)
|
||||
|
||||
func Register(resolvers map[string]ResolverFunc) {
|
||||
resolvers["order"] = resolveOrder
|
||||
resolvers["orders"] = resolveOrders
|
||||
resolvers["fill"] = resolveFill
|
||||
resolvers["fills"] = resolveFills
|
||||
resolvers["market"] = resolveMarket
|
||||
resolvers["markets"] = resolveMarkets
|
||||
resolvers["perpPosition"] = resolvePerpPosition
|
||||
resolvers["perpPositions"] = resolvePerpPositions
|
||||
resolvers["fundingRate"] = resolveFundingRate
|
||||
resolvers["fundingRates"] = resolveFundingRates
|
||||
resolvers["liquidation"] = resolveLiquidation
|
||||
resolvers["liquidations"] = resolveLiquidations
|
||||
resolvers["orderbook"] = resolveOrderbook
|
||||
resolvers["marketDayDatas"] = resolveMarketDayDatas
|
||||
}
|
||||
|
||||
func resolveOrder(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("Order", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("order requires id")
|
||||
}
|
||||
func resolveOrders(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("Order", parseLimit(args))
|
||||
}
|
||||
func resolveFill(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("Fill", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("fill requires id")
|
||||
}
|
||||
func resolveFills(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("Fill", parseLimit(args))
|
||||
}
|
||||
func resolveMarket(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("Market", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("market requires id")
|
||||
}
|
||||
func resolveMarkets(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("Market", parseLimit(args))
|
||||
}
|
||||
func resolvePerpPosition(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("PerpPosition", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("perpPosition requires id")
|
||||
}
|
||||
func resolvePerpPositions(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("PerpPosition", parseLimit(args))
|
||||
}
|
||||
func resolveFundingRate(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("FundingRate", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("fundingRate requires id")
|
||||
}
|
||||
func resolveFundingRates(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("FundingRate", parseLimit(args))
|
||||
}
|
||||
func resolveLiquidation(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("Liquidation", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("liquidation requires id")
|
||||
}
|
||||
func resolveLiquidations(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("Liquidation", parseLimit(args))
|
||||
}
|
||||
func resolveOrderbook(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("Orderbook", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("orderbook requires market id")
|
||||
}
|
||||
func resolveMarketDayDatas(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("MarketDayData", parseLimit(args))
|
||||
}
|
||||
|
||||
func parseLimit(args map[string]interface{}) int {
|
||||
limit := 100
|
||||
if l, ok := args["first"]; ok { fmt.Sscanf(fmt.Sprint(l), "%d", &limit) }
|
||||
return limit
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Package exchange provides resolvers for the X-Chain (ExchangeVM).
|
||||
//
|
||||
// Indexes: UTXO assets, transfers, NFTs, asset creation, cross-chain imports/exports.
|
||||
//
|
||||
// Entities: Asset, UTXO, Transfer, AssetCreation, ImportTx, ExportTx
|
||||
package exchange
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/graph/storage"
|
||||
)
|
||||
|
||||
type ResolverFunc = func(context.Context, *storage.Store, map[string]interface{}) (interface{}, error)
|
||||
|
||||
func Register(resolvers map[string]ResolverFunc) {
|
||||
resolvers["asset"] = resolveAsset
|
||||
resolvers["assets"] = resolveAssets
|
||||
resolvers["utxo"] = resolveUTXO
|
||||
resolvers["utxos"] = resolveUTXOs
|
||||
resolvers["xTransfer"] = resolveXTransfer
|
||||
resolvers["xTransfers"] = resolveXTransfers
|
||||
resolvers["assetCreation"] = resolveAssetCreation
|
||||
resolvers["assetCreations"] = resolveAssetCreations
|
||||
resolvers["importTx"] = resolveImportTx
|
||||
resolvers["importTxs"] = resolveImportTxs
|
||||
resolvers["exportTx"] = resolveExportTx
|
||||
resolvers["exportTxs"] = resolveExportTxs
|
||||
}
|
||||
|
||||
func resolveAsset(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("Asset", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("asset requires id")
|
||||
}
|
||||
func resolveAssets(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("Asset", pl(args))
|
||||
}
|
||||
func resolveUTXO(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("UTXO", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("utxo requires id")
|
||||
}
|
||||
func resolveUTXOs(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("UTXO", pl(args))
|
||||
}
|
||||
func resolveXTransfer(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("XTransfer", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("xTransfer requires id")
|
||||
}
|
||||
func resolveXTransfers(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("XTransfer", pl(args))
|
||||
}
|
||||
func resolveAssetCreation(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("AssetCreation", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("assetCreation requires id")
|
||||
}
|
||||
func resolveAssetCreations(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("AssetCreation", pl(args))
|
||||
}
|
||||
func resolveImportTx(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("ImportTx", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("importTx requires id")
|
||||
}
|
||||
func resolveImportTxs(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("ImportTx", pl(args))
|
||||
}
|
||||
func resolveExportTx(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("ExportTx", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("exportTx requires id")
|
||||
}
|
||||
func resolveExportTxs(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("ExportTx", pl(args))
|
||||
}
|
||||
|
||||
func pl(args map[string]interface{}) int {
|
||||
limit := 100
|
||||
if l, ok := args["first"]; ok { fmt.Sscanf(fmt.Sprint(l), "%d", &limit) }
|
||||
return limit
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Package fhe provides resolvers for the T-Chain (ThresholdVM / FHE).
|
||||
//
|
||||
// Indexes: DKG ceremonies, threshold decryption requests, encrypted compute
|
||||
// jobs, FHE ciphertext operations, key shares.
|
||||
//
|
||||
// Entities: DKGCeremony, DecryptionRequest, ComputeJob, Ciphertext, KeyShare
|
||||
package fhe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/graph/storage"
|
||||
)
|
||||
|
||||
type ResolverFunc = func(context.Context, *storage.Store, map[string]interface{}) (interface{}, error)
|
||||
|
||||
func Register(resolvers map[string]ResolverFunc) {
|
||||
resolvers["dkgCeremony"] = resolveDKGCeremony
|
||||
resolvers["dkgCeremonies"] = resolveDKGCeremonies
|
||||
resolvers["decryptionRequest"] = resolveDecryptionRequest
|
||||
resolvers["decryptionRequests"] = resolveDecryptionRequests
|
||||
resolvers["computeJob"] = resolveComputeJob
|
||||
resolvers["computeJobs"] = resolveComputeJobs
|
||||
resolvers["ciphertext"] = resolveCiphertext
|
||||
resolvers["ciphertexts"] = resolveCiphertexts
|
||||
resolvers["keyShare"] = resolveKeyShare
|
||||
resolvers["keyShares"] = resolveKeyShares
|
||||
resolvers["fheStats"] = resolveFHEStats
|
||||
}
|
||||
|
||||
func resolveDKGCeremony(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("DKGCeremony", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("dkgCeremony requires id")
|
||||
}
|
||||
func resolveDKGCeremonies(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("DKGCeremony", parseLimit(args))
|
||||
}
|
||||
func resolveDecryptionRequest(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("DecryptionRequest", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("decryptionRequest requires id")
|
||||
}
|
||||
func resolveDecryptionRequests(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("DecryptionRequest", parseLimit(args))
|
||||
}
|
||||
func resolveComputeJob(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("ComputeJob", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("computeJob requires id")
|
||||
}
|
||||
func resolveComputeJobs(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("ComputeJob", parseLimit(args))
|
||||
}
|
||||
func resolveCiphertext(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("Ciphertext", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("ciphertext requires id")
|
||||
}
|
||||
func resolveCiphertexts(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("Ciphertext", parseLimit(args))
|
||||
}
|
||||
func resolveKeyShare(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("KeyShare", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("keyShare requires id")
|
||||
}
|
||||
func resolveKeyShares(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("KeyShare", parseLimit(args))
|
||||
}
|
||||
func resolveFHEStats(_ context.Context, s *storage.Store, _ map[string]interface{}) (interface{}, error) {
|
||||
return s.GetByType("FHEStats", "1")
|
||||
}
|
||||
|
||||
func parseLimit(args map[string]interface{}) int {
|
||||
limit := 100
|
||||
if l, ok := args["first"]; ok { fmt.Sscanf(fmt.Sprint(l), "%d", &limit) }
|
||||
return limit
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Package identity provides resolvers for the I-Chain (IdentityVM).
|
||||
//
|
||||
// Indexes: decentralized identifiers, verifiable credentials, attestations.
|
||||
//
|
||||
// Entities: DID, VerifiableCredential, Attestation, IdentityRegistry, CredentialSchema
|
||||
package identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/graph/storage"
|
||||
)
|
||||
|
||||
type ResolverFunc = func(context.Context, *storage.Store, map[string]interface{}) (interface{}, error)
|
||||
|
||||
func Register(resolvers map[string]ResolverFunc) {
|
||||
resolvers["did"] = resolveDID
|
||||
resolvers["dids"] = resolveDIDs
|
||||
resolvers["verifiableCredential"] = resolveVerifiableCredential
|
||||
resolvers["verifiableCredentials"] = resolveVerifiableCredentials
|
||||
resolvers["attestation"] = resolveAttestation
|
||||
resolvers["attestations"] = resolveAttestations
|
||||
resolvers["identityRegistry"] = resolveIdentityRegistry
|
||||
resolvers["identityRegistries"] = resolveIdentityRegistries
|
||||
resolvers["credentialSchema"] = resolveCredentialSchema
|
||||
resolvers["credentialSchemas"] = resolveCredentialSchemas
|
||||
resolvers["identityStats"] = resolveIdentityStats
|
||||
}
|
||||
|
||||
func resolveDID(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("DID", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("did requires id")
|
||||
}
|
||||
func resolveDIDs(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("DID", pl(args))
|
||||
}
|
||||
func resolveVerifiableCredential(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("VerifiableCredential", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("verifiableCredential requires id")
|
||||
}
|
||||
func resolveVerifiableCredentials(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("VerifiableCredential", pl(args))
|
||||
}
|
||||
func resolveAttestation(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("Attestation", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("attestation requires id")
|
||||
}
|
||||
func resolveAttestations(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("Attestation", pl(args))
|
||||
}
|
||||
func resolveIdentityRegistry(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("IdentityRegistry", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("identityRegistry requires id")
|
||||
}
|
||||
func resolveIdentityRegistries(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("IdentityRegistry", pl(args))
|
||||
}
|
||||
func resolveCredentialSchema(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("CredentialSchema", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("credentialSchema requires id")
|
||||
}
|
||||
func resolveCredentialSchemas(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("CredentialSchema", pl(args))
|
||||
}
|
||||
func resolveIdentityStats(_ context.Context, s *storage.Store, _ map[string]interface{}) (interface{}, error) {
|
||||
return s.GetByType("IdentityStats", "1")
|
||||
}
|
||||
|
||||
func pl(args map[string]interface{}) int {
|
||||
limit := 100
|
||||
if l, ok := args["first"]; ok { fmt.Sscanf(fmt.Sprint(l), "%d", &limit) }
|
||||
return limit
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Package key provides resolvers for the K-Chain (KeyVM).
|
||||
//
|
||||
// Indexes: managed keys, rotations, shares, ceremonies, attestations.
|
||||
//
|
||||
// Entities: ManagedKey, KeyRotation, KeyShare, KeyCeremony, KeyAttestation
|
||||
package key
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/graph/storage"
|
||||
)
|
||||
|
||||
type ResolverFunc = func(context.Context, *storage.Store, map[string]interface{}) (interface{}, error)
|
||||
|
||||
func Register(resolvers map[string]ResolverFunc) {
|
||||
resolvers["managedKey"] = resolveManagedKey
|
||||
resolvers["managedKeys"] = resolveManagedKeys
|
||||
resolvers["keyRotation"] = resolveKeyRotation
|
||||
resolvers["keyRotations"] = resolveKeyRotations
|
||||
resolvers["keyShare"] = resolveKeyShare
|
||||
resolvers["keyShares"] = resolveKeyShares
|
||||
resolvers["keyCeremony"] = resolveKeyCeremony
|
||||
resolvers["keyCeremonies"] = resolveKeyCeremonies
|
||||
resolvers["keyAttestation"] = resolveKeyAttestation
|
||||
resolvers["keyAttestations"] = resolveKeyAttestations
|
||||
resolvers["keyStats"] = resolveKeyStats
|
||||
}
|
||||
|
||||
func resolveManagedKey(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("ManagedKey", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("managedKey requires id")
|
||||
}
|
||||
func resolveManagedKeys(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("ManagedKey", pl(args))
|
||||
}
|
||||
func resolveKeyRotation(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("KeyRotation", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("keyRotation requires id")
|
||||
}
|
||||
func resolveKeyRotations(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("KeyRotation", pl(args))
|
||||
}
|
||||
func resolveKeyShare(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("KeyShare", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("keyShare requires id")
|
||||
}
|
||||
func resolveKeyShares(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("KeyShare", pl(args))
|
||||
}
|
||||
func resolveKeyCeremony(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("KeyCeremony", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("keyCeremony requires id")
|
||||
}
|
||||
func resolveKeyCeremonies(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("KeyCeremony", pl(args))
|
||||
}
|
||||
func resolveKeyAttestation(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("KeyAttestation", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("keyAttestation requires id")
|
||||
}
|
||||
func resolveKeyAttestations(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("KeyAttestation", pl(args))
|
||||
}
|
||||
func resolveKeyStats(_ context.Context, s *storage.Store, _ map[string]interface{}) (interface{}, error) {
|
||||
return s.GetByType("KeyStats", "1")
|
||||
}
|
||||
|
||||
func pl(args map[string]interface{}) int {
|
||||
limit := 100
|
||||
if l, ok := args["first"]; ok { fmt.Sscanf(fmt.Sprint(l), "%d", &limit) }
|
||||
return limit
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Package oracle provides resolvers for the O-Chain (OracleVM).
|
||||
//
|
||||
// Indexes: price feeds, data requests, oracle reports, node registrations.
|
||||
//
|
||||
// Entities: PriceFeed, DataRequest, OracleReport, OracleNode, DataAttestation
|
||||
package oracle
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/graph/storage"
|
||||
)
|
||||
|
||||
type ResolverFunc = func(context.Context, *storage.Store, map[string]interface{}) (interface{}, error)
|
||||
|
||||
func Register(resolvers map[string]ResolverFunc) {
|
||||
resolvers["priceFeed"] = resolvePriceFeed
|
||||
resolvers["priceFeeds"] = resolvePriceFeeds
|
||||
resolvers["dataRequest"] = resolveDataRequest
|
||||
resolvers["dataRequests"] = resolveDataRequests
|
||||
resolvers["oracleReport"] = resolveOracleReport
|
||||
resolvers["oracleReports"] = resolveOracleReports
|
||||
resolvers["oracleNode"] = resolveOracleNode
|
||||
resolvers["oracleNodes"] = resolveOracleNodes
|
||||
resolvers["dataAttestation"] = resolveDataAttestation
|
||||
resolvers["dataAttestations"] = resolveDataAttestations
|
||||
resolvers["oracleStats"] = resolveOracleStats
|
||||
}
|
||||
|
||||
func resolvePriceFeed(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("PriceFeed", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("priceFeed requires id")
|
||||
}
|
||||
func resolvePriceFeeds(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("PriceFeed", pl(args))
|
||||
}
|
||||
func resolveDataRequest(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("DataRequest", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("dataRequest requires id")
|
||||
}
|
||||
func resolveDataRequests(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("DataRequest", pl(args))
|
||||
}
|
||||
func resolveOracleReport(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("OracleReport", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("oracleReport requires id")
|
||||
}
|
||||
func resolveOracleReports(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("OracleReport", pl(args))
|
||||
}
|
||||
func resolveOracleNode(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("OracleNode", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("oracleNode requires id")
|
||||
}
|
||||
func resolveOracleNodes(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("OracleNode", pl(args))
|
||||
}
|
||||
func resolveDataAttestation(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("DataAttestation", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("dataAttestation requires id")
|
||||
}
|
||||
func resolveDataAttestations(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("DataAttestation", pl(args))
|
||||
}
|
||||
func resolveOracleStats(_ context.Context, s *storage.Store, _ map[string]interface{}) (interface{}, error) {
|
||||
return s.GetByType("OracleStats", "1")
|
||||
}
|
||||
|
||||
func pl(args map[string]interface{}) int {
|
||||
limit := 100
|
||||
if l, ok := args["first"]; ok { fmt.Sscanf(fmt.Sprint(l), "%d", &limit) }
|
||||
return limit
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Package platform provides resolvers for the P-Chain (PlatformVM).
|
||||
//
|
||||
// Indexes: validators, delegators, staking, subnets, L1 creation, rewards.
|
||||
//
|
||||
// Entities: Validator, Delegator, Subnet, Blockchain, StakingReward, StakingPeriod
|
||||
package platform
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/graph/storage"
|
||||
)
|
||||
|
||||
type ResolverFunc = func(context.Context, *storage.Store, map[string]interface{}) (interface{}, error)
|
||||
|
||||
func Register(resolvers map[string]ResolverFunc) {
|
||||
resolvers["validator"] = resolveValidator
|
||||
resolvers["validators"] = resolveValidators
|
||||
resolvers["delegator"] = resolveDelegator
|
||||
resolvers["delegators"] = resolveDelegators
|
||||
resolvers["subnet"] = resolveSubnet
|
||||
resolvers["subnets"] = resolveSubnets
|
||||
resolvers["blockchain"] = resolveBlockchain
|
||||
resolvers["blockchains"] = resolveBlockchains
|
||||
resolvers["stakingReward"] = resolveStakingReward
|
||||
resolvers["stakingRewards"] = resolveStakingRewards
|
||||
resolvers["networkStats"] = resolveNetworkStats
|
||||
}
|
||||
|
||||
func resolveValidator(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("Validator", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("validator requires id (NodeID)")
|
||||
}
|
||||
func resolveValidators(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("Validator", parseLimit(args))
|
||||
}
|
||||
func resolveDelegator(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("Delegator", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("delegator requires id")
|
||||
}
|
||||
func resolveDelegators(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("Delegator", parseLimit(args))
|
||||
}
|
||||
func resolveSubnet(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("Subnet", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("subnet requires id")
|
||||
}
|
||||
func resolveSubnets(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("Subnet", parseLimit(args))
|
||||
}
|
||||
func resolveBlockchain(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("Blockchain", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("blockchain requires id")
|
||||
}
|
||||
func resolveBlockchains(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("Blockchain", parseLimit(args))
|
||||
}
|
||||
func resolveStakingReward(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("StakingReward", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("stakingReward requires id")
|
||||
}
|
||||
func resolveStakingRewards(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("StakingReward", parseLimit(args))
|
||||
}
|
||||
func resolveNetworkStats(_ context.Context, s *storage.Store, _ map[string]interface{}) (interface{}, error) {
|
||||
return s.GetByType("NetworkStats", "1")
|
||||
}
|
||||
|
||||
func parseLimit(args map[string]interface{}) int {
|
||||
limit := 100
|
||||
if l, ok := args["first"]; ok { fmt.Sscanf(fmt.Sprint(l), "%d", &limit) }
|
||||
return limit
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
// Package precompile provides resolvers for Lux EVM precompiled contract calls.
|
||||
//
|
||||
// Precompiles are native EVM operations at fixed addresses — callable from
|
||||
// Solidity, indexed by the graph engine when they emit logs or are called
|
||||
// via traces.
|
||||
//
|
||||
// Address ranges:
|
||||
//
|
||||
// 0x02xx — Warp (cross-chain messaging)
|
||||
// 0x03xx — AI Mining (proof-of-work for AI compute)
|
||||
// 0x05xx — Crypto primitives (Poseidon2, Pedersen, Ed25519, ECIES)
|
||||
// 0x07xx — FHE (fully homomorphic encryption + ACL + Gateway)
|
||||
// 0x09xx — ZK proofs (Groth16, Plonk, Fflonk, Halo2, KZG, IPA, privacy pool)
|
||||
// 0x0Axx — Graph (on-chain GraphQL schema registry)
|
||||
// 0x92xx — DEX (native orderbook: limit, market, cancel)
|
||||
// 0x9003 — Blake3 hash
|
||||
// 0xB002 — Ring signatures (Corona)
|
||||
//
|
||||
// Entities: PrecompileCall, AIWorkProof, FHEOperation, ZKVerification,
|
||||
//
|
||||
// CryptoOp, WarpCall, DEXOrder, RingSignature, GraphSchemaUpdate
|
||||
package precompile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/graph/storage"
|
||||
)
|
||||
|
||||
type ResolverFunc = func(context.Context, *storage.Store, map[string]interface{}) (interface{}, error)
|
||||
|
||||
func Register(resolvers map[string]ResolverFunc) {
|
||||
// Generic precompile call tracking
|
||||
resolvers["precompileCall"] = resolvePrecompileCall
|
||||
resolvers["precompileCalls"] = resolvePrecompileCalls
|
||||
|
||||
// AI Mining (0x03xx)
|
||||
resolvers["aiWorkProof"] = resolveByType("AIWorkProof")
|
||||
resolvers["aiWorkProofs"] = resolveListType("AIWorkProof")
|
||||
|
||||
// FHE (0x07xx)
|
||||
resolvers["fheOperation"] = resolveByType("FHEOperation")
|
||||
resolvers["fheOperations"] = resolveListType("FHEOperation")
|
||||
resolvers["fheACLGrant"] = resolveByType("FHEACLGrant")
|
||||
resolvers["fheACLGrants"] = resolveListType("FHEACLGrant")
|
||||
resolvers["fheGatewayRequest"] = resolveByType("FHEGatewayRequest")
|
||||
resolvers["fheGatewayRequests"] = resolveListType("FHEGatewayRequest")
|
||||
|
||||
// Crypto primitives (0x05xx)
|
||||
resolvers["cryptoOp"] = resolveByType("CryptoOp")
|
||||
resolvers["cryptoOps"] = resolveListType("CryptoOp")
|
||||
|
||||
// ZK proofs (0x09xx)
|
||||
resolvers["zkVerification"] = resolveByType("ZKVerification")
|
||||
resolvers["zkVerifications"] = resolveListType("ZKVerification")
|
||||
resolvers["privacyPoolOp"] = resolveByType("PrivacyPoolOp")
|
||||
resolvers["privacyPoolOps"] = resolveListType("PrivacyPoolOp")
|
||||
resolvers["rollupVerification"] = resolveByType("RollupVerification")
|
||||
resolvers["rollupVerifications"] = resolveListType("RollupVerification")
|
||||
|
||||
// Warp (0x02xx)
|
||||
resolvers["warpCall"] = resolveByType("WarpCall")
|
||||
resolvers["warpCalls"] = resolveListType("WarpCall")
|
||||
|
||||
// DEX (0x92xx)
|
||||
resolvers["dexPrecompileOrder"] = resolveByType("DEXPrecompileOrder")
|
||||
resolvers["dexPrecompileOrders"] = resolveListType("DEXPrecompileOrder")
|
||||
|
||||
// Ring signatures (0xB002)
|
||||
resolvers["ringSignature"] = resolveByType("RingSignature")
|
||||
resolvers["ringSignatures"] = resolveListType("RingSignature")
|
||||
|
||||
// Graph schema (0x0Axx)
|
||||
resolvers["graphSchemaUpdate"] = resolveByType("GraphSchemaUpdate")
|
||||
resolvers["graphSchemaUpdates"] = resolveListType("GraphSchemaUpdate")
|
||||
|
||||
// PQ crypto (ML-DSA, ML-KEM, SLH-DSA)
|
||||
resolvers["pqCryptoOp"] = resolveByType("PQCryptoOp")
|
||||
resolvers["pqCryptoOps"] = resolveListType("PQCryptoOp")
|
||||
|
||||
// Threshold (CGGMP21, FROST)
|
||||
resolvers["thresholdOp"] = resolveByType("ThresholdOp")
|
||||
resolvers["thresholdOps"] = resolveListType("ThresholdOp")
|
||||
|
||||
// Stats
|
||||
resolvers["precompileStats"] = resolvePrecompileStats
|
||||
}
|
||||
|
||||
// Known precompile address ranges for reference.
|
||||
var Addresses = map[string]string{
|
||||
"warp": "0x0200000000000000000000000000000000000007",
|
||||
"aiMining": "0x0300000000000000000000000000000000000000",
|
||||
"poseidon2": "0x0500000000000000000000000000000000000001",
|
||||
"pedersen": "0x0500000000000000000000000000000000000003",
|
||||
"ed25519": "0x0500000000000000000000000000000000000004",
|
||||
"fhe": "0x0700000000000000000000000000000000000000",
|
||||
"fheACL": "0x0700000000000000000000000000000000000001",
|
||||
"fheGateway": "0x0700000000000000000000000000000000000003",
|
||||
"zkVerify": "0x0900000000000000000000000000000000000000",
|
||||
"groth16": "0x0900000000000000000000000000000000000001",
|
||||
"plonk": "0x0900000000000000000000000000000000000002",
|
||||
"fflonk": "0x0900000000000000000000000000000000000003",
|
||||
"halo2": "0x0900000000000000000000000000000000000004",
|
||||
"kzg": "0x0900000000000000000000000000000000000010",
|
||||
"ipa": "0x0900000000000000000000000000000000000012",
|
||||
"privacyPool": "0x0900000000000000000000000000000000000020",
|
||||
"nullifier": "0x0900000000000000000000000000000000000021",
|
||||
"commitment": "0x0900000000000000000000000000000000000022",
|
||||
"rangeProof": "0x0900000000000000000000000000000000000023",
|
||||
"rollupVerify": "0x0900000000000000000000000000000000000030",
|
||||
"stateRoot": "0x0900000000000000000000000000000000000031",
|
||||
"batchProof": "0x0900000000000000000000000000000000000032",
|
||||
"graph": "0x0A00000000000000000000000000000000000001",
|
||||
"blake3": "0x9003",
|
||||
"dexLimit": "0x9200",
|
||||
"dexMarket": "0x9201",
|
||||
"dexCancel": "0x9202",
|
||||
"corona": "0xB002",
|
||||
}
|
||||
|
||||
func resolvePrecompileCall(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok {
|
||||
return s.GetByType("PrecompileCall", fmt.Sprint(id))
|
||||
}
|
||||
return nil, fmt.Errorf("precompileCall requires id")
|
||||
}
|
||||
|
||||
func resolvePrecompileCalls(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("PrecompileCall", pl(args))
|
||||
}
|
||||
|
||||
func resolvePrecompileStats(_ context.Context, s *storage.Store, _ map[string]interface{}) (interface{}, error) {
|
||||
return s.GetByType("PrecompileStats", "1")
|
||||
}
|
||||
|
||||
// Factory helpers for repetitive resolver patterns.
|
||||
func resolveByType(entityType string) ResolverFunc {
|
||||
return func(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok {
|
||||
return s.GetByType(entityType, fmt.Sprint(id))
|
||||
}
|
||||
return nil, fmt.Errorf("%s requires id", entityType)
|
||||
}
|
||||
}
|
||||
|
||||
func resolveListType(entityType string) ResolverFunc {
|
||||
return func(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType(entityType, pl(args))
|
||||
}
|
||||
}
|
||||
|
||||
func pl(args map[string]interface{}) int {
|
||||
limit := 100
|
||||
if l, ok := args["first"]; ok {
|
||||
fmt.Sscanf(fmt.Sprint(l), "%d", &limit)
|
||||
}
|
||||
return limit
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Package privacy provides resolvers for the Z-Chain (ZKVM / Privacy).
|
||||
//
|
||||
// Indexes: ZK proofs, shielded transfers, nullifiers, commitment trees.
|
||||
//
|
||||
// Entities: ShieldedTransfer, ZKProof, Nullifier, Commitment, PrivacyPool
|
||||
package privacy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/graph/storage"
|
||||
)
|
||||
|
||||
type ResolverFunc = func(context.Context, *storage.Store, map[string]interface{}) (interface{}, error)
|
||||
|
||||
func Register(resolvers map[string]ResolverFunc) {
|
||||
resolvers["shieldedTransfer"] = resolveShieldedTransfer
|
||||
resolvers["shieldedTransfers"] = resolveShieldedTransfers
|
||||
resolvers["zkProof"] = resolveZKProof
|
||||
resolvers["zkProofs"] = resolveZKProofs
|
||||
resolvers["nullifier"] = resolveNullifier
|
||||
resolvers["nullifiers"] = resolveNullifiers
|
||||
resolvers["commitment"] = resolveCommitment
|
||||
resolvers["commitments"] = resolveCommitments
|
||||
resolvers["privacyPool"] = resolvePrivacyPool
|
||||
resolvers["privacyPools"] = resolvePrivacyPools
|
||||
}
|
||||
|
||||
func resolveShieldedTransfer(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("ShieldedTransfer", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("shieldedTransfer requires id")
|
||||
}
|
||||
func resolveShieldedTransfers(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("ShieldedTransfer", pl(args))
|
||||
}
|
||||
func resolveZKProof(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("ZKProof", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("zkProof requires id")
|
||||
}
|
||||
func resolveZKProofs(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("ZKProof", pl(args))
|
||||
}
|
||||
func resolveNullifier(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("Nullifier", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("nullifier requires id")
|
||||
}
|
||||
func resolveNullifiers(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("Nullifier", pl(args))
|
||||
}
|
||||
func resolveCommitment(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("Commitment", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("commitment requires id")
|
||||
}
|
||||
func resolveCommitments(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("Commitment", pl(args))
|
||||
}
|
||||
func resolvePrivacyPool(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("PrivacyPool", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("privacyPool requires id")
|
||||
}
|
||||
func resolvePrivacyPools(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("PrivacyPool", pl(args))
|
||||
}
|
||||
|
||||
func pl(args map[string]interface{}) int {
|
||||
limit := 100
|
||||
if l, ok := args["first"]; ok { fmt.Sscanf(fmt.Sprint(l), "%d", &limit) }
|
||||
return limit
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Package quantum provides resolvers for the Q-Chain (QuantumVM).
|
||||
//
|
||||
// Indexes: post-quantum signatures, proofs, key pairs, finality, attestations.
|
||||
//
|
||||
// Entities: CoronaSignature, QuantumProof, PQKeyPair, QuantumFinality, QuantumAttestation
|
||||
package quantum
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/graph/storage"
|
||||
)
|
||||
|
||||
type ResolverFunc = func(context.Context, *storage.Store, map[string]interface{}) (interface{}, error)
|
||||
|
||||
func Register(resolvers map[string]ResolverFunc) {
|
||||
resolvers["coronaSignature"] = resolveCoronaSignature
|
||||
resolvers["coronaSignatures"] = resolveCoronaSignatures
|
||||
resolvers["quantumProof"] = resolveQuantumProof
|
||||
resolvers["quantumProofs"] = resolveQuantumProofs
|
||||
resolvers["pqKeyPair"] = resolvePQKeyPair
|
||||
resolvers["pqKeyPairs"] = resolvePQKeyPairs
|
||||
resolvers["quantumFinality"] = resolveQuantumFinality
|
||||
resolvers["quantumFinalitys"] = resolveQuantumFinalitys
|
||||
resolvers["quantumAttestation"] = resolveQuantumAttestation
|
||||
resolvers["quantumAttestations"] = resolveQuantumAttestations
|
||||
resolvers["quantumStats"] = resolveQuantumStats
|
||||
}
|
||||
|
||||
func resolveCoronaSignature(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("CoronaSignature", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("coronaSignature requires id")
|
||||
}
|
||||
func resolveCoronaSignatures(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("CoronaSignature", pl(args))
|
||||
}
|
||||
func resolveQuantumProof(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("QuantumProof", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("quantumProof requires id")
|
||||
}
|
||||
func resolveQuantumProofs(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("QuantumProof", pl(args))
|
||||
}
|
||||
func resolvePQKeyPair(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("PQKeyPair", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("pqKeyPair requires id")
|
||||
}
|
||||
func resolvePQKeyPairs(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("PQKeyPair", pl(args))
|
||||
}
|
||||
func resolveQuantumFinality(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("QuantumFinality", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("quantumFinality requires id")
|
||||
}
|
||||
func resolveQuantumFinalitys(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("QuantumFinality", pl(args))
|
||||
}
|
||||
func resolveQuantumAttestation(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("QuantumAttestation", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("quantumAttestation requires id")
|
||||
}
|
||||
func resolveQuantumAttestations(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("QuantumAttestation", pl(args))
|
||||
}
|
||||
func resolveQuantumStats(_ context.Context, s *storage.Store, _ map[string]interface{}) (interface{}, error) {
|
||||
return s.GetByType("QuantumStats", "1")
|
||||
}
|
||||
|
||||
func pl(args map[string]interface{}) int {
|
||||
limit := 100
|
||||
if l, ok := args["first"]; ok { fmt.Sscanf(fmt.Sprint(l), "%d", &limit) }
|
||||
return limit
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Package relay provides resolvers for the R-Chain (RelayVM).
|
||||
//
|
||||
// Indexes: warp messages, relay requests, proofs, routes, receipts.
|
||||
//
|
||||
// Entities: WarpMessage, RelayRequest, RelayProof, RelayRoute, MessageReceipt
|
||||
package relay
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/graph/storage"
|
||||
)
|
||||
|
||||
type ResolverFunc = func(context.Context, *storage.Store, map[string]interface{}) (interface{}, error)
|
||||
|
||||
func Register(resolvers map[string]ResolverFunc) {
|
||||
resolvers["warpMessage"] = resolveWarpMessage
|
||||
resolvers["warpMessages"] = resolveWarpMessages
|
||||
resolvers["relayRequest"] = resolveRelayRequest
|
||||
resolvers["relayRequests"] = resolveRelayRequests
|
||||
resolvers["relayProof"] = resolveRelayProof
|
||||
resolvers["relayProofs"] = resolveRelayProofs
|
||||
resolvers["relayRoute"] = resolveRelayRoute
|
||||
resolvers["relayRoutes"] = resolveRelayRoutes
|
||||
resolvers["messageReceipt"] = resolveMessageReceipt
|
||||
resolvers["messageReceipts"] = resolveMessageReceipts
|
||||
resolvers["relayStats"] = resolveRelayStats
|
||||
}
|
||||
|
||||
func resolveWarpMessage(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("WarpMessage", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("warpMessage requires id")
|
||||
}
|
||||
func resolveWarpMessages(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("WarpMessage", pl(args))
|
||||
}
|
||||
func resolveRelayRequest(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("RelayRequest", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("relayRequest requires id")
|
||||
}
|
||||
func resolveRelayRequests(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("RelayRequest", pl(args))
|
||||
}
|
||||
func resolveRelayProof(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("RelayProof", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("relayProof requires id")
|
||||
}
|
||||
func resolveRelayProofs(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("RelayProof", pl(args))
|
||||
}
|
||||
func resolveRelayRoute(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("RelayRoute", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("relayRoute requires id")
|
||||
}
|
||||
func resolveRelayRoutes(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("RelayRoute", pl(args))
|
||||
}
|
||||
func resolveMessageReceipt(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("MessageReceipt", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("messageReceipt requires id")
|
||||
}
|
||||
func resolveMessageReceipts(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("MessageReceipt", pl(args))
|
||||
}
|
||||
func resolveRelayStats(_ context.Context, s *storage.Store, _ map[string]interface{}) (interface{}, error) {
|
||||
return s.GetByType("RelayStats", "1")
|
||||
}
|
||||
|
||||
func pl(args map[string]interface{}) int {
|
||||
limit := 100
|
||||
if l, ok := args["first"]; ok { fmt.Sscanf(fmt.Sprint(l), "%d", &limit) }
|
||||
return limit
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Package servicenode provides resolvers for the S-Chain (ServiceNodeVM).
|
||||
//
|
||||
// Indexes: service nodes, registrations, SLA records, uptime proofs, endpoints.
|
||||
//
|
||||
// Entities: ServiceNode, ServiceRegistration, SLARecord, UptimeProof, ServiceEndpoint
|
||||
package servicenode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/luxfi/graph/storage"
|
||||
)
|
||||
|
||||
type ResolverFunc = func(context.Context, *storage.Store, map[string]interface{}) (interface{}, error)
|
||||
|
||||
func Register(resolvers map[string]ResolverFunc) {
|
||||
resolvers["serviceNode"] = resolveServiceNode
|
||||
resolvers["serviceNodes"] = resolveServiceNodes
|
||||
resolvers["serviceRegistration"] = resolveServiceRegistration
|
||||
resolvers["serviceRegistrations"] = resolveServiceRegistrations
|
||||
resolvers["slaRecord"] = resolveSLARecord
|
||||
resolvers["slaRecords"] = resolveSLARecords
|
||||
resolvers["uptimeProof"] = resolveUptimeProof
|
||||
resolvers["uptimeProofs"] = resolveUptimeProofs
|
||||
resolvers["serviceEndpoint"] = resolveServiceEndpoint
|
||||
resolvers["serviceEndpoints"] = resolveServiceEndpoints
|
||||
resolvers["serviceStats"] = resolveServiceStats
|
||||
}
|
||||
|
||||
func resolveServiceNode(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("ServiceNode", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("serviceNode requires id")
|
||||
}
|
||||
func resolveServiceNodes(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("ServiceNode", pl(args))
|
||||
}
|
||||
func resolveServiceRegistration(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("ServiceRegistration", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("serviceRegistration requires id")
|
||||
}
|
||||
func resolveServiceRegistrations(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("ServiceRegistration", pl(args))
|
||||
}
|
||||
func resolveSLARecord(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("SLARecord", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("slaRecord requires id")
|
||||
}
|
||||
func resolveSLARecords(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("SLARecord", pl(args))
|
||||
}
|
||||
func resolveUptimeProof(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("UptimeProof", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("uptimeProof requires id")
|
||||
}
|
||||
func resolveUptimeProofs(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("UptimeProof", pl(args))
|
||||
}
|
||||
func resolveServiceEndpoint(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
if id, ok := args["id"]; ok { return s.GetByType("ServiceEndpoint", fmt.Sprint(id)) }
|
||||
return nil, fmt.Errorf("serviceEndpoint requires id")
|
||||
}
|
||||
func resolveServiceEndpoints(_ context.Context, s *storage.Store, args map[string]interface{}) (interface{}, error) {
|
||||
return s.ListByType("ServiceEndpoint", pl(args))
|
||||
}
|
||||
func resolveServiceStats(_ context.Context, s *storage.Store, _ map[string]interface{}) (interface{}, error) {
|
||||
return s.GetByType("ServiceStats", "1")
|
||||
}
|
||||
|
||||
func pl(args map[string]interface{}) int {
|
||||
limit := 100
|
||||
if l, ok := args["first"]; ok { fmt.Sscanf(fmt.Sprint(l), "%d", &limit) }
|
||||
return limit
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hanzoai/replicate"
|
||||
"github.com/luxfi/age"
|
||||
)
|
||||
|
||||
// StartReplicate streams SQLite WAL changes to S3 with PQ encryption.
|
||||
// Returns a stop function. If REPLICATE_S3_ENDPOINT is unset, returns a noop.
|
||||
func StartReplicate(dbPath string) func() {
|
||||
endpoint := os.Getenv("REPLICATE_S3_ENDPOINT")
|
||||
if endpoint == "" {
|
||||
return func() {}
|
||||
}
|
||||
|
||||
bucket := envOr("REPLICATE_S3_BUCKET", "replicate")
|
||||
prefix := os.Getenv("REPLICATE_S3_PATH")
|
||||
if prefix == "" {
|
||||
prefix, _ = os.Hostname()
|
||||
}
|
||||
region := envOr("REPLICATE_S3_REGION", "us-central1")
|
||||
|
||||
replicaURL := fmt.Sprintf("s3://%s/%s?endpoint=%s®ion=%s&force-path-style=true",
|
||||
url.PathEscape(bucket),
|
||||
url.PathEscape(prefix),
|
||||
url.QueryEscape(endpoint),
|
||||
url.QueryEscape(region),
|
||||
)
|
||||
if ak := os.Getenv("REPLICATE_S3_ACCESS_KEY"); ak != "" {
|
||||
replicaURL += "&access_key=" + url.QueryEscape(ak)
|
||||
}
|
||||
if sk := os.Getenv("REPLICATE_S3_SECRET_KEY"); sk != "" {
|
||||
replicaURL += "&secret_key=" + url.QueryEscape(sk)
|
||||
}
|
||||
|
||||
client, err := replicate.NewReplicaClientFromURL(replicaURL)
|
||||
if err != nil {
|
||||
log.Printf("[replicate] invalid config: %v", err)
|
||||
return func() {}
|
||||
}
|
||||
|
||||
db := replicate.NewDB(dbPath)
|
||||
replica := replicate.NewReplicaWithClient(db, client)
|
||||
|
||||
if v := os.Getenv("REPLICATE_SYNC_INTERVAL"); v != "" {
|
||||
if d, err := time.ParseDuration(v); err == nil {
|
||||
replica.SyncInterval = d
|
||||
}
|
||||
}
|
||||
|
||||
// PQ encryption: ML-KEM-768 + X25519 hybrid via luxfi/age
|
||||
if recipientStr := os.Getenv("REPLICATE_AGE_RECIPIENT"); recipientStr != "" {
|
||||
if rcs, err := age.ParseRecipients(strings.NewReader(recipientStr)); err == nil {
|
||||
replica.AgeRecipients = rcs
|
||||
}
|
||||
}
|
||||
if identityStr := os.Getenv("REPLICATE_AGE_IDENTITY"); identityStr != "" {
|
||||
if ids, err := age.ParseIdentities(strings.NewReader(identityStr)); err == nil {
|
||||
replica.AgeIdentities = ids
|
||||
}
|
||||
}
|
||||
|
||||
db.Replica = replica
|
||||
|
||||
if err := db.Open(); err != nil {
|
||||
log.Printf("[replicate] failed: %v", err)
|
||||
return func() {}
|
||||
}
|
||||
|
||||
log.Printf("[replicate] streaming %s → s3://%s/%s (pq=%v)",
|
||||
dbPath, bucket, prefix, len(replica.AgeRecipients) > 0)
|
||||
|
||||
return func() { _ = db.Close(context.Background()) }
|
||||
}
|
||||
|
||||
func envOr(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// Package storage provides the data layer for graph.
|
||||
//
|
||||
// SQLite WAL for production (default), in-memory maps for nosqlite builds.
|
||||
// All reads are concurrent-safe. Writes are serialized.
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Seed types for data population.
|
||||
// Used by the indexer and e2e tests to inject state.
|
||||
|
||||
type SeedFactoryData struct {
|
||||
PoolCount int64
|
||||
TxCount int64
|
||||
TotalVolumeUSD string
|
||||
TotalValueLockedUSD string
|
||||
}
|
||||
|
||||
type SeedBundleData struct {
|
||||
EthPriceUSD string
|
||||
}
|
||||
|
||||
type SeedTokenData struct {
|
||||
Symbol string
|
||||
Name string
|
||||
Decimals int64
|
||||
VolumeUSD string
|
||||
TotalValueLockedUSD string
|
||||
DerivedETH string
|
||||
TxCount int64
|
||||
}
|
||||
|
||||
type SeedPoolData struct {
|
||||
Token0 string
|
||||
Token1 string
|
||||
FeeTier int64
|
||||
TotalValueLockedUSD string
|
||||
VolumeUSD string
|
||||
Token0Price string
|
||||
Token1Price string
|
||||
TxCount int64
|
||||
}
|
||||
|
||||
type SeedSwapData struct {
|
||||
Timestamp int64
|
||||
Pool string
|
||||
Amount0 string
|
||||
Amount1 string
|
||||
AmountUSD string
|
||||
Sender string
|
||||
}
|
||||
|
||||
// sortResults sorts a slice of map[string]interface{} by the named field.
|
||||
func sortResults(results []interface{}, orderBy, orderDirection string) {
|
||||
if orderBy == "" || len(results) == 0 {
|
||||
return
|
||||
}
|
||||
desc := strings.EqualFold(orderDirection, "desc")
|
||||
sort.SliceStable(results, func(i, j int) bool {
|
||||
mi, _ := results[i].(map[string]interface{})
|
||||
mj, _ := results[j].(map[string]interface{})
|
||||
if mi == nil || mj == nil {
|
||||
return false
|
||||
}
|
||||
vi := mi[orderBy]
|
||||
vj := mj[orderBy]
|
||||
cmp := compareValues(vi, vj)
|
||||
if desc {
|
||||
return cmp > 0
|
||||
}
|
||||
return cmp < 0
|
||||
})
|
||||
}
|
||||
|
||||
// compareValues compares two interface{} values. If both parse as floats,
|
||||
// compares numerically. Otherwise falls back to string comparison.
|
||||
func compareValues(a, b interface{}) int {
|
||||
sa := fmt.Sprint(a)
|
||||
sb := fmt.Sprint(b)
|
||||
fa, errA := strconv.ParseFloat(sa, 64)
|
||||
fb, errB := strconv.ParseFloat(sb, 64)
|
||||
if errA == nil && errB == nil {
|
||||
if fa < fb {
|
||||
return -1
|
||||
}
|
||||
if fa > fb {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
if sa < sb {
|
||||
return -1
|
||||
}
|
||||
if sa > sb {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// FilterResults filters a slice of map results by where conditions.
|
||||
// Supports exact match and _gte, _lte, _gt, _lt suffixes for numeric comparisons.
|
||||
func FilterResults(results []interface{}, where map[string]interface{}) []interface{} {
|
||||
if len(where) == 0 {
|
||||
return results
|
||||
}
|
||||
var out []interface{}
|
||||
for _, r := range results {
|
||||
m, ok := r.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if matchesWhere(m, where) {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func matchesWhere(m map[string]interface{}, where map[string]interface{}) bool {
|
||||
for k, v := range where {
|
||||
var field, op string
|
||||
switch {
|
||||
case strings.HasSuffix(k, "_gte"):
|
||||
field = k[:len(k)-4]
|
||||
op = "gte"
|
||||
case strings.HasSuffix(k, "_lte"):
|
||||
field = k[:len(k)-4]
|
||||
op = "lte"
|
||||
case strings.HasSuffix(k, "_gt"):
|
||||
field = k[:len(k)-3]
|
||||
op = "gt"
|
||||
case strings.HasSuffix(k, "_lt"):
|
||||
field = k[:len(k)-3]
|
||||
op = "lt"
|
||||
default:
|
||||
field = k
|
||||
op = "eq"
|
||||
}
|
||||
|
||||
val, exists := m[field]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
cmp := compareValues(val, v)
|
||||
switch op {
|
||||
case "eq":
|
||||
if cmp != 0 {
|
||||
return false
|
||||
}
|
||||
case "gte":
|
||||
if cmp < 0 {
|
||||
return false
|
||||
}
|
||||
case "lte":
|
||||
if cmp > 0 {
|
||||
return false
|
||||
}
|
||||
case "gt":
|
||||
if cmp <= 0 {
|
||||
return false
|
||||
}
|
||||
case "lt":
|
||||
if cmp >= 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
//go:build nosqlite
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Store is the unified storage backend (in-memory fallback for nosqlite builds).
|
||||
type Store struct {
|
||||
dataDir string
|
||||
mu sync.RWMutex
|
||||
|
||||
factories map[string]*SeedFactoryData
|
||||
bundles map[string]*SeedBundleData
|
||||
tokens map[string]*SeedTokenData
|
||||
pools map[string]*SeedPoolData
|
||||
swaps map[string]*SeedSwapData
|
||||
mints map[string]interface{}
|
||||
burns map[string]interface{}
|
||||
ticks map[string]interface{}
|
||||
positions map[string]interface{}
|
||||
generic map[string]interface{}
|
||||
|
||||
lastBlock uint64
|
||||
}
|
||||
|
||||
// New creates an in-memory store (nosqlite build).
|
||||
func New(dataDir string) (*Store, error) {
|
||||
return &Store{
|
||||
dataDir: dataDir,
|
||||
factories: make(map[string]*SeedFactoryData),
|
||||
bundles: make(map[string]*SeedBundleData),
|
||||
tokens: make(map[string]*SeedTokenData),
|
||||
pools: make(map[string]*SeedPoolData),
|
||||
swaps: make(map[string]*SeedSwapData),
|
||||
mints: make(map[string]interface{}),
|
||||
burns: make(map[string]interface{}),
|
||||
ticks: make(map[string]interface{}),
|
||||
positions: make(map[string]interface{}),
|
||||
generic: make(map[string]interface{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) Init(_ context.Context) error { return nil }
|
||||
func (s *Store) Close() error { return nil }
|
||||
func (s *Store) DataDir() string { return s.dataDir }
|
||||
|
||||
func (s *Store) SetLastBlock(block uint64) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.lastBlock = block
|
||||
}
|
||||
|
||||
func (s *Store) GetLastBlock() uint64 {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.lastBlock
|
||||
}
|
||||
|
||||
// --- Seed methods ---
|
||||
|
||||
func (s *Store) SeedFactory(id string, d *SeedFactoryData) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.factories[id] = d
|
||||
}
|
||||
|
||||
func (s *Store) SeedBundle(id string, d *SeedBundleData) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.bundles[id] = d
|
||||
}
|
||||
|
||||
func (s *Store) SeedToken(id string, d *SeedTokenData) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.tokens[id] = d
|
||||
}
|
||||
|
||||
func (s *Store) SeedPool(id string, d *SeedPoolData) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.pools[id] = d
|
||||
}
|
||||
|
||||
func (s *Store) SeedSwap(id string, d *SeedSwapData) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.swaps[id] = d
|
||||
}
|
||||
|
||||
// --- Generic entity storage ---
|
||||
|
||||
func (s *Store) SetEntity(entityType, id string, data interface{}) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.generic[entityType+":"+id] = data
|
||||
}
|
||||
|
||||
func (s *Store) GetByType(entityType, id string) (interface{}, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
key := entityType + ":" + id
|
||||
if v, ok := s.generic[key]; ok {
|
||||
return v, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListByType(entityType string, limit int) (interface{}, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
prefix := entityType + ":"
|
||||
var result []interface{}
|
||||
for k, v := range s.generic {
|
||||
if len(k) > len(prefix) && k[:len(prefix)] == prefix {
|
||||
result = append(result, v)
|
||||
if len(result) >= limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// --- Block queries ---
|
||||
|
||||
func (s *Store) GetBlock(_ context.Context, id string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetBlockByNumber(_ context.Context, num string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetLatestBlock(_ context.Context) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetBlocks(_ context.Context, limit int) (interface{}, error) { return []interface{}{}, nil }
|
||||
|
||||
// --- Transaction queries ---
|
||||
|
||||
func (s *Store) GetTransaction(_ context.Context, hash string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetTransactions(_ context.Context, limit int) (interface{}, error) { return []interface{}{}, nil }
|
||||
|
||||
// --- Token queries ---
|
||||
|
||||
func (s *Store) GetToken(_ context.Context, addr string) (interface{}, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if t, ok := s.tokens[addr]; ok {
|
||||
return map[string]interface{}{
|
||||
"id": addr, "symbol": t.Symbol, "name": t.Name, "decimals": t.Decimals,
|
||||
"volumeUSD": t.VolumeUSD, "totalValueLockedUSD": t.TotalValueLockedUSD,
|
||||
"derivedETH": t.DerivedETH, "txCount": t.TxCount,
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetTokens(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
var result []interface{}
|
||||
for addr, t := range s.tokens {
|
||||
result = append(result, map[string]interface{}{
|
||||
"id": addr, "symbol": t.Symbol, "name": t.Name, "decimals": t.Decimals,
|
||||
"volumeUSD": t.VolumeUSD, "totalValueLockedUSD": t.TotalValueLockedUSD,
|
||||
"derivedETH": t.DerivedETH, "txCount": t.TxCount,
|
||||
})
|
||||
}
|
||||
result = FilterResults(result, where)
|
||||
sortResults(result, orderBy, orderDirection)
|
||||
if len(result) > limit {
|
||||
result = result[:limit]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// --- DEX queries ---
|
||||
|
||||
func (s *Store) GetFactory(_ context.Context, id string) (interface{}, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if f, ok := s.factories[id]; ok {
|
||||
return map[string]interface{}{
|
||||
"id": id, "poolCount": f.PoolCount, "txCount": f.TxCount,
|
||||
"totalVolumeUSD": f.TotalVolumeUSD, "totalValueLockedUSD": f.TotalValueLockedUSD,
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetFactories(_ context.Context) (interface{}, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
var result []interface{}
|
||||
for id, f := range s.factories {
|
||||
result = append(result, map[string]interface{}{
|
||||
"id": id, "poolCount": f.PoolCount, "txCount": f.TxCount,
|
||||
"totalVolumeUSD": f.TotalVolumeUSD, "totalValueLockedUSD": f.TotalValueLockedUSD,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetBundle(_ context.Context, id string) (interface{}, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if b, ok := s.bundles[id]; ok {
|
||||
return map[string]interface{}{
|
||||
"id": id, "ethPriceUSD": b.EthPriceUSD, "ethPrice": b.EthPriceUSD,
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetPool(_ context.Context, id string) (interface{}, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if p, ok := s.pools[id]; ok {
|
||||
return s.poolToMap(id, p), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetPools(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
var result []interface{}
|
||||
for id, p := range s.pools {
|
||||
result = append(result, s.poolToMap(id, p))
|
||||
}
|
||||
result = FilterResults(result, where)
|
||||
sortResults(result, orderBy, orderDirection)
|
||||
if len(result) > limit {
|
||||
result = result[:limit]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Store) poolToMap(id string, p *SeedPoolData) map[string]interface{} {
|
||||
var t0, t1 interface{}
|
||||
if tok, ok := s.tokens[p.Token0]; ok {
|
||||
t0 = map[string]interface{}{"id": p.Token0, "symbol": tok.Symbol, "name": tok.Name, "decimals": tok.Decimals}
|
||||
}
|
||||
if tok, ok := s.tokens[p.Token1]; ok {
|
||||
t1 = map[string]interface{}{"id": p.Token1, "symbol": tok.Symbol, "name": tok.Name, "decimals": tok.Decimals}
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"id": id, "token0": t0, "token1": t1, "feeTier": p.FeeTier,
|
||||
"totalValueLockedUSD": p.TotalValueLockedUSD, "volumeUSD": p.VolumeUSD,
|
||||
"token0Price": p.Token0Price, "token1Price": p.Token1Price, "txCount": p.TxCount,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) GetPair(_ context.Context, id string) (interface{}, error) { return s.GetPool(nil, id) }
|
||||
func (s *Store) GetPairs(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return s.GetPools(nil, limit, orderBy, orderDirection, where)
|
||||
}
|
||||
|
||||
func (s *Store) GetSwap(_ context.Context, id string) (interface{}, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if sw, ok := s.swaps[id]; ok {
|
||||
return s.swapToMap(id, sw), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetSwaps(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
var result []interface{}
|
||||
for id, sw := range s.swaps {
|
||||
result = append(result, s.swapToMap(id, sw))
|
||||
}
|
||||
result = FilterResults(result, where)
|
||||
if orderBy == "" {
|
||||
orderBy = "timestamp"
|
||||
}
|
||||
if orderDirection == "" {
|
||||
orderDirection = "desc"
|
||||
}
|
||||
sortResults(result, orderBy, orderDirection)
|
||||
if len(result) > limit {
|
||||
result = result[:limit]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Store) swapToMap(id string, sw *SeedSwapData) map[string]interface{} {
|
||||
var pool interface{} = sw.Pool
|
||||
if p, ok := s.pools[sw.Pool]; ok {
|
||||
pool = s.poolToMap(sw.Pool, p)
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"id": id, "timestamp": sw.Timestamp, "pool": pool,
|
||||
"amount0": sw.Amount0, "amount1": sw.Amount1, "amountUSD": sw.AmountUSD,
|
||||
"sender": sw.Sender,
|
||||
}
|
||||
}
|
||||
|
||||
// Stubs
|
||||
func (s *Store) GetMint(_ context.Context, id string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetMints(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
func (s *Store) GetBurn(_ context.Context, id string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetBurns(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
func (s *Store) GetTick(_ context.Context, id string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetTicks(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
func (s *Store) GetPosition(_ context.Context, id string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetPositions(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetCollect(_ context.Context, id string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetCollects(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
func (s *Store) GetFlash(_ context.Context, id string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetFlashes(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetTokenDayDatas(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
func (s *Store) GetPairDayDatas(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
func (s *Store) GetPoolDayDatas(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
func (s *Store) GetFactoryDayDatas(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
func (s *Store) GetPairHourDatas(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetTransfer(_ context.Context, id string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetTransfers(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
func (s *Store) GetNFT(_ context.Context, id string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetNFTs(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
|
||||
// --- V4 storage methods ---
|
||||
|
||||
func (s *Store) GetModifyLiquidity(_ context.Context, id string) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (s *Store) GetModifyLiquiditys(_ context.Context, limit int, orderBy, orderDirection string) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
func (s *Store) GetSubscribe(_ context.Context, id string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetSubscribes(_ context.Context, limit int, orderBy, orderDirection string) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
func (s *Store) GetUnsubscribe(_ context.Context, id string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetUnsubscribes(_ context.Context, limit int, orderBy, orderDirection string) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
func (s *Store) GetPoolHourDatas(_ context.Context, limit int, orderBy, orderDirection string) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
func (s *Store) GetTokenHourDatas(_ context.Context, limit int, orderBy, orderDirection string) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,545 @@
|
||||
//go:build !nosqlite
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
// Store is the unified storage backend backed by SQLite WAL.
|
||||
type Store struct {
|
||||
dataDir string
|
||||
mu sync.RWMutex
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// New creates a store rooted at dataDir with SQLite WAL.
|
||||
func New(dataDir string) (*Store, error) {
|
||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("storage: mkdir %s: %w", dataDir, err)
|
||||
}
|
||||
dbPath := filepath.Join(dataDir, "graph.db")
|
||||
db, err := sql.Open("sqlite3", dbPath+"?_journal_mode=WAL&_busy_timeout=5000&_synchronous=NORMAL&_cache_size=-64000")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("storage: open %s: %w", dbPath, err)
|
||||
}
|
||||
// WAL mode allows concurrent readers with a single writer.
|
||||
db.SetMaxOpenConns(8)
|
||||
return &Store{dataDir: dataDir, db: db}, nil
|
||||
}
|
||||
|
||||
// Init creates tables and indexes.
|
||||
func (s *Store) Init(_ context.Context) error {
|
||||
schema := `
|
||||
CREATE TABLE IF NOT EXISTS factories (id TEXT PRIMARY KEY, data JSON);
|
||||
CREATE TABLE IF NOT EXISTS bundles (id TEXT PRIMARY KEY, data JSON);
|
||||
CREATE TABLE IF NOT EXISTS tokens (id TEXT PRIMARY KEY, data JSON);
|
||||
CREATE TABLE IF NOT EXISTS pools (id TEXT PRIMARY KEY, data JSON);
|
||||
CREATE TABLE IF NOT EXISTS swaps (id TEXT PRIMARY KEY, data JSON, timestamp INTEGER, pool TEXT);
|
||||
CREATE TABLE IF NOT EXISTS entities (type TEXT, id TEXT, data JSON, PRIMARY KEY(type, id));
|
||||
CREATE TABLE IF NOT EXISTS meta (key TEXT PRIMARY KEY, value TEXT);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_swaps_timestamp ON swaps(timestamp);
|
||||
CREATE INDEX IF NOT EXISTS idx_swaps_pool ON swaps(pool);
|
||||
CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type);
|
||||
`
|
||||
_, err := s.db.Exec(schema)
|
||||
return err
|
||||
}
|
||||
|
||||
// Close closes the database.
|
||||
func (s *Store) Close() error {
|
||||
if s.db != nil {
|
||||
return s.db.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DataDir returns the store's data directory.
|
||||
func (s *Store) DataDir() string { return s.dataDir }
|
||||
|
||||
// SetLastBlock persists the indexer's last processed block.
|
||||
func (s *Store) SetLastBlock(block uint64) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.db.Exec("INSERT OR REPLACE INTO meta(key, value) VALUES('lastBlock', ?)", strconv.FormatUint(block, 10))
|
||||
}
|
||||
|
||||
// GetLastBlock returns the indexer's last processed block.
|
||||
func (s *Store) GetLastBlock() uint64 {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
var v string
|
||||
if err := s.db.QueryRow("SELECT value FROM meta WHERE key='lastBlock'").Scan(&v); err != nil {
|
||||
return 0
|
||||
}
|
||||
n, _ := strconv.ParseUint(v, 10, 64)
|
||||
return n
|
||||
}
|
||||
|
||||
// --- Seed methods ---
|
||||
|
||||
func (s *Store) SeedFactory(id string, d *SeedFactoryData) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
data, _ := json.Marshal(d)
|
||||
s.db.Exec("INSERT OR REPLACE INTO factories(id, data) VALUES(?, ?)", id, string(data))
|
||||
}
|
||||
|
||||
func (s *Store) SeedBundle(id string, d *SeedBundleData) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
data, _ := json.Marshal(d)
|
||||
s.db.Exec("INSERT OR REPLACE INTO bundles(id, data) VALUES(?, ?)", id, string(data))
|
||||
}
|
||||
|
||||
func (s *Store) SeedToken(id string, d *SeedTokenData) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
data, _ := json.Marshal(d)
|
||||
s.db.Exec("INSERT OR REPLACE INTO tokens(id, data) VALUES(?, ?)", id, string(data))
|
||||
}
|
||||
|
||||
func (s *Store) SeedPool(id string, d *SeedPoolData) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
data, _ := json.Marshal(d)
|
||||
s.db.Exec("INSERT OR REPLACE INTO pools(id, data) VALUES(?, ?)", id, string(data))
|
||||
}
|
||||
|
||||
func (s *Store) SeedSwap(id string, d *SeedSwapData) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
data, _ := json.Marshal(d)
|
||||
s.db.Exec("INSERT OR REPLACE INTO swaps(id, data, timestamp, pool) VALUES(?, ?, ?, ?)",
|
||||
id, string(data), d.Timestamp, d.Pool)
|
||||
}
|
||||
|
||||
// --- Generic entity storage ---
|
||||
|
||||
func (s *Store) SetEntity(entityType, id string, data interface{}) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
j, _ := json.Marshal(data)
|
||||
s.db.Exec("INSERT OR REPLACE INTO entities(type, id, data) VALUES(?, ?, ?)", entityType, id, string(j))
|
||||
}
|
||||
|
||||
func (s *Store) GetByType(entityType, id string) (interface{}, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
var raw string
|
||||
if err := s.db.QueryRow("SELECT data FROM entities WHERE type=? AND id=?", entityType, id).Scan(&raw); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
var v interface{}
|
||||
json.Unmarshal([]byte(raw), &v)
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListByType(entityType string, limit int) (interface{}, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
rows, err := s.db.Query("SELECT data FROM entities WHERE type=? LIMIT ?", entityType, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var result []interface{}
|
||||
for rows.Next() {
|
||||
var raw string
|
||||
if err := rows.Scan(&raw); err != nil {
|
||||
continue
|
||||
}
|
||||
var v interface{}
|
||||
json.Unmarshal([]byte(raw), &v)
|
||||
result = append(result, v)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// --- Block queries (stubs) ---
|
||||
|
||||
func (s *Store) GetBlock(_ context.Context, id string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetBlockByNumber(_ context.Context, num string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetLatestBlock(_ context.Context) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetBlocks(_ context.Context, limit int) (interface{}, error) { return []interface{}{}, nil }
|
||||
|
||||
// --- Transaction queries (stubs) ---
|
||||
|
||||
func (s *Store) GetTransaction(_ context.Context, hash string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetTransactions(_ context.Context, limit int) (interface{}, error) { return []interface{}{}, nil }
|
||||
|
||||
// --- Token queries ---
|
||||
|
||||
func (s *Store) GetToken(_ context.Context, addr string) (interface{}, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
t, err := s.loadToken(addr)
|
||||
if err != nil || t == nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"id": addr, "symbol": t.Symbol, "name": t.Name, "decimals": t.Decimals,
|
||||
"volumeUSD": t.VolumeUSD, "totalValueLockedUSD": t.TotalValueLockedUSD,
|
||||
"derivedETH": t.DerivedETH, "txCount": t.TxCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetTokens(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
rows, err := s.db.Query("SELECT id, data FROM tokens")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var result []interface{}
|
||||
for rows.Next() {
|
||||
var id, raw string
|
||||
if err := rows.Scan(&id, &raw); err != nil {
|
||||
continue
|
||||
}
|
||||
var t SeedTokenData
|
||||
json.Unmarshal([]byte(raw), &t)
|
||||
result = append(result, map[string]interface{}{
|
||||
"id": id, "symbol": t.Symbol, "name": t.Name, "decimals": t.Decimals,
|
||||
"volumeUSD": t.VolumeUSD, "totalValueLockedUSD": t.TotalValueLockedUSD,
|
||||
"derivedETH": t.DerivedETH, "txCount": t.TxCount,
|
||||
})
|
||||
}
|
||||
result = FilterResults(result, where)
|
||||
sortResults(result, orderBy, orderDirection)
|
||||
if len(result) > limit {
|
||||
result = result[:limit]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// --- DEX queries ---
|
||||
|
||||
func (s *Store) GetFactory(_ context.Context, id string) (interface{}, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
f, err := s.loadFactory(id)
|
||||
if err != nil || f == nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"id": id, "poolCount": f.PoolCount, "txCount": f.TxCount,
|
||||
"totalVolumeUSD": f.TotalVolumeUSD, "totalValueLockedUSD": f.TotalValueLockedUSD,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetFactories(_ context.Context) (interface{}, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
rows, err := s.db.Query("SELECT id, data FROM factories")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var result []interface{}
|
||||
for rows.Next() {
|
||||
var id, raw string
|
||||
if err := rows.Scan(&id, &raw); err != nil {
|
||||
continue
|
||||
}
|
||||
var f SeedFactoryData
|
||||
json.Unmarshal([]byte(raw), &f)
|
||||
result = append(result, map[string]interface{}{
|
||||
"id": id, "poolCount": f.PoolCount, "txCount": f.TxCount,
|
||||
"totalVolumeUSD": f.TotalVolumeUSD, "totalValueLockedUSD": f.TotalValueLockedUSD,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetBundle(_ context.Context, id string) (interface{}, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
var raw string
|
||||
if err := s.db.QueryRow("SELECT data FROM bundles WHERE id=?", id).Scan(&raw); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
var b SeedBundleData
|
||||
json.Unmarshal([]byte(raw), &b)
|
||||
return map[string]interface{}{
|
||||
"id": id, "ethPriceUSD": b.EthPriceUSD, "ethPrice": b.EthPriceUSD,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetPool(_ context.Context, id string) (interface{}, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
p, err := s.loadPool(id)
|
||||
if err != nil || p == nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.poolToMap(id, p), nil
|
||||
}
|
||||
|
||||
func (s *Store) GetPools(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
// Load all pools first, then close rows before resolving tokens (avoids conn deadlock).
|
||||
type idPool struct {
|
||||
id string
|
||||
p SeedPoolData
|
||||
}
|
||||
rows, err := s.db.Query("SELECT id, data FROM pools")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pools []idPool
|
||||
for rows.Next() {
|
||||
var id, raw string
|
||||
if err := rows.Scan(&id, &raw); err != nil {
|
||||
continue
|
||||
}
|
||||
var p SeedPoolData
|
||||
json.Unmarshal([]byte(raw), &p)
|
||||
pools = append(pools, idPool{id, p})
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
var result []interface{}
|
||||
for _, pp := range pools {
|
||||
p := pp.p
|
||||
result = append(result, s.poolToMap(pp.id, &p))
|
||||
}
|
||||
result = FilterResults(result, where)
|
||||
sortResults(result, orderBy, orderDirection)
|
||||
if len(result) > limit {
|
||||
result = result[:limit]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Store) poolToMap(id string, p *SeedPoolData) map[string]interface{} {
|
||||
var t0, t1 interface{}
|
||||
if tok, _ := s.loadToken(p.Token0); tok != nil {
|
||||
t0 = map[string]interface{}{"id": p.Token0, "symbol": tok.Symbol, "name": tok.Name, "decimals": tok.Decimals}
|
||||
}
|
||||
if tok, _ := s.loadToken(p.Token1); tok != nil {
|
||||
t1 = map[string]interface{}{"id": p.Token1, "symbol": tok.Symbol, "name": tok.Name, "decimals": tok.Decimals}
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"id": id, "token0": t0, "token1": t1, "feeTier": p.FeeTier,
|
||||
"totalValueLockedUSD": p.TotalValueLockedUSD, "volumeUSD": p.VolumeUSD,
|
||||
"token0Price": p.Token0Price, "token1Price": p.Token1Price, "txCount": p.TxCount,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Store) GetPair(_ context.Context, id string) (interface{}, error) { return s.GetPool(nil, id) }
|
||||
func (s *Store) GetPairs(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return s.GetPools(nil, limit, orderBy, orderDirection, where)
|
||||
}
|
||||
|
||||
func (s *Store) GetSwap(_ context.Context, id string) (interface{}, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
var raw string
|
||||
if err := s.db.QueryRow("SELECT data FROM swaps WHERE id=?", id).Scan(&raw); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
var sw SeedSwapData
|
||||
json.Unmarshal([]byte(raw), &sw)
|
||||
return s.swapToMap(id, &sw), nil
|
||||
}
|
||||
|
||||
func (s *Store) GetSwaps(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if orderBy == "" {
|
||||
orderBy = "timestamp"
|
||||
}
|
||||
if orderDirection == "" {
|
||||
orderDirection = "desc"
|
||||
}
|
||||
|
||||
// Use SQL ordering for timestamp (indexed column)
|
||||
query := "SELECT id, data FROM swaps"
|
||||
if orderBy == "timestamp" {
|
||||
if strings.EqualFold(orderDirection, "desc") {
|
||||
query += " ORDER BY timestamp DESC"
|
||||
} else {
|
||||
query += " ORDER BY timestamp ASC"
|
||||
}
|
||||
}
|
||||
query += " LIMIT ?"
|
||||
|
||||
// Load all swaps first, close rows, then resolve pool/token refs.
|
||||
type idSwap struct {
|
||||
id string
|
||||
sw SeedSwapData
|
||||
}
|
||||
rows, err := s.db.Query(query, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var swaps []idSwap
|
||||
for rows.Next() {
|
||||
var id, raw string
|
||||
if err := rows.Scan(&id, &raw); err != nil {
|
||||
continue
|
||||
}
|
||||
var sw SeedSwapData
|
||||
json.Unmarshal([]byte(raw), &sw)
|
||||
swaps = append(swaps, idSwap{id, sw})
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
var result []interface{}
|
||||
for _, ss := range swaps {
|
||||
sw := ss.sw
|
||||
result = append(result, s.swapToMap(ss.id, &sw))
|
||||
}
|
||||
result = FilterResults(result, where)
|
||||
if orderBy != "timestamp" {
|
||||
sortResults(result, orderBy, orderDirection)
|
||||
}
|
||||
if len(result) > limit {
|
||||
result = result[:limit]
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Store) swapToMap(id string, sw *SeedSwapData) map[string]interface{} {
|
||||
var pool interface{} = sw.Pool
|
||||
if p, _ := s.loadPool(sw.Pool); p != nil {
|
||||
pool = s.poolToMap(sw.Pool, p)
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"id": id, "timestamp": sw.Timestamp, "pool": pool,
|
||||
"amount0": sw.Amount0, "amount1": sw.Amount1, "amountUSD": sw.AmountUSD,
|
||||
"sender": sw.Sender,
|
||||
}
|
||||
}
|
||||
|
||||
// Stubs for unimplemented entity types
|
||||
func (s *Store) GetMint(_ context.Context, id string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetMints(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
func (s *Store) GetBurn(_ context.Context, id string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetBurns(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
func (s *Store) GetTick(_ context.Context, id string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetTicks(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
func (s *Store) GetPosition(_ context.Context, id string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetPositions(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetCollect(_ context.Context, id string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetCollects(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
func (s *Store) GetFlash(_ context.Context, id string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetFlashes(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetTokenDayDatas(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
func (s *Store) GetPairDayDatas(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
func (s *Store) GetPoolDayDatas(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
func (s *Store) GetFactoryDayDatas(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
func (s *Store) GetPairHourDatas(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetTransfer(_ context.Context, id string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetTransfers(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
func (s *Store) GetNFT(_ context.Context, id string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetNFTs(_ context.Context, limit int, orderBy, orderDirection string, where map[string]interface{}) (interface{}, error) {
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
|
||||
// --- V4 storage methods ---
|
||||
|
||||
func (s *Store) GetModifyLiquidity(_ context.Context, id string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetModifyLiquiditys(_ context.Context, limit int, orderBy, orderDirection string) (interface{}, error) { return []interface{}{}, nil }
|
||||
func (s *Store) GetSubscribe(_ context.Context, id string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetSubscribes(_ context.Context, limit int, orderBy, orderDirection string) (interface{}, error) { return []interface{}{}, nil }
|
||||
func (s *Store) GetUnsubscribe(_ context.Context, id string) (interface{}, error) { return nil, nil }
|
||||
func (s *Store) GetUnsubscribes(_ context.Context, limit int, orderBy, orderDirection string) (interface{}, error) { return []interface{}{}, nil }
|
||||
func (s *Store) GetPoolHourDatas(_ context.Context, limit int, orderBy, orderDirection string) (interface{}, error) { return []interface{}{}, nil }
|
||||
func (s *Store) GetTokenHourDatas(_ context.Context, limit int, orderBy, orderDirection string) (interface{}, error) { return []interface{}{}, nil }
|
||||
|
||||
// --- Internal helpers ---
|
||||
|
||||
func (s *Store) loadFactory(id string) (*SeedFactoryData, error) {
|
||||
var raw string
|
||||
if err := s.db.QueryRow("SELECT data FROM factories WHERE id=?", id).Scan(&raw); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
var f SeedFactoryData
|
||||
json.Unmarshal([]byte(raw), &f)
|
||||
return &f, nil
|
||||
}
|
||||
|
||||
func (s *Store) loadToken(addr string) (*SeedTokenData, error) {
|
||||
var raw string
|
||||
if err := s.db.QueryRow("SELECT data FROM tokens WHERE id=?", addr).Scan(&raw); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
var t SeedTokenData
|
||||
json.Unmarshal([]byte(raw), &t)
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (s *Store) loadPool(id string) (*SeedPoolData, error) {
|
||||
var raw string
|
||||
if err := s.db.QueryRow("SELECT data FROM pools WHERE id=?", id).Scan(&raw); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
var p SeedPoolData
|
||||
json.Unmarshal([]byte(raw), &p)
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
// Package wasm provides a subgraph execution runtime using wazero.
|
||||
//
|
||||
// Loads compiled AssemblyScript WASM modules from `graph build` output,
|
||||
// implements The Graph's host functions (store.set, store.get, ethereum.call,
|
||||
// log.log, etc.), and executes event handlers against incoming EVM logs.
|
||||
//
|
||||
// This is a drop-in replacement for Graph Node's WASM runtime, but in pure Go.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// rt, _ := wasm.NewRuntime(ctx)
|
||||
// sg, _ := rt.LoadSubgraph("./uni-v2-subgraph/build/")
|
||||
// sg.HandleEvent(log) // executes the AS mapping handler
|
||||
// entities := sg.Flush() // returns modified entities for storage
|
||||
package wasm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Runtime manages WASM module instances for subgraph execution.
|
||||
type Runtime struct {
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewRuntime creates a WASM execution runtime.
|
||||
// Uses wazero (pure Go, no CGO) for AssemblyScript module execution.
|
||||
func NewRuntime(ctx context.Context) (*Runtime, error) {
|
||||
return &Runtime{ctx: ctx}, nil
|
||||
}
|
||||
|
||||
// Close releases all WASM resources.
|
||||
func (r *Runtime) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Subgraph represents a loaded subgraph with its WASM modules and schema.
|
||||
type Subgraph struct {
|
||||
Name string
|
||||
Schema string // GraphQL SDL
|
||||
DataSources []DataSource
|
||||
Templates []Template
|
||||
Entities *EntityStore
|
||||
}
|
||||
|
||||
// DataSource maps a contract to its WASM handler module.
|
||||
type DataSource struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
Network string `yaml:"network"`
|
||||
Source ContractSource `yaml:"source"`
|
||||
Mapping Mapping `yaml:"mapping"`
|
||||
wasmBytes []byte
|
||||
}
|
||||
|
||||
// ContractSource identifies the contract to watch.
|
||||
type ContractSource struct {
|
||||
Address string `yaml:"address"`
|
||||
ABI string `yaml:"abi"`
|
||||
StartBlock int64 `yaml:"startBlock"`
|
||||
}
|
||||
|
||||
// Mapping defines event handlers in a WASM module.
|
||||
type Mapping struct {
|
||||
Kind string `yaml:"kind"`
|
||||
APIVersion string `yaml:"apiVersion"`
|
||||
Language string `yaml:"language"`
|
||||
File string `yaml:"file"`
|
||||
Entities []string `yaml:"entities"`
|
||||
ABIs []ABIRef `yaml:"abis"`
|
||||
EventHandlers []EventHandler `yaml:"eventHandlers"`
|
||||
}
|
||||
|
||||
// ABIRef references an ABI file.
|
||||
type ABIRef struct {
|
||||
Name string `yaml:"name"`
|
||||
File string `yaml:"file"`
|
||||
}
|
||||
|
||||
// EventHandler maps an EVM event signature to a WASM handler function.
|
||||
type EventHandler struct {
|
||||
Event string `yaml:"event"` // e.g., "Swap(indexed address,uint256,uint256,uint256,uint256,indexed address)"
|
||||
Handler string `yaml:"handler"` // e.g., "handleSwap"
|
||||
}
|
||||
|
||||
// Template is a dynamically created data source (e.g., new Pair from PairCreated).
|
||||
type Template struct {
|
||||
Name string `yaml:"name"`
|
||||
Kind string `yaml:"kind"`
|
||||
Mapping Mapping `yaml:"mapping"`
|
||||
}
|
||||
|
||||
// EntityStore is an in-memory entity cache that the WASM modules write to.
|
||||
// Flushed to the storage layer after each block.
|
||||
type EntityStore struct {
|
||||
entities map[string]map[string]map[string]interface{} // type -> id -> fields
|
||||
}
|
||||
|
||||
// NewEntityStore creates an empty entity store.
|
||||
func NewEntityStore() *EntityStore {
|
||||
return &EntityStore{
|
||||
entities: make(map[string]map[string]map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Set stores an entity (called by WASM host function store.set).
|
||||
func (es *EntityStore) Set(entityType, id string, data map[string]interface{}) {
|
||||
if es.entities[entityType] == nil {
|
||||
es.entities[entityType] = make(map[string]map[string]interface{})
|
||||
}
|
||||
es.entities[entityType][id] = data
|
||||
}
|
||||
|
||||
// Get retrieves an entity (called by WASM host function store.get).
|
||||
func (es *EntityStore) Get(entityType, id string) (map[string]interface{}, bool) {
|
||||
if es.entities[entityType] == nil {
|
||||
return nil, false
|
||||
}
|
||||
data, ok := es.entities[entityType][id]
|
||||
return data, ok
|
||||
}
|
||||
|
||||
// Flush returns all modified entities and resets the store.
|
||||
func (es *EntityStore) Flush() map[string]map[string]map[string]interface{} {
|
||||
result := es.entities
|
||||
es.entities = make(map[string]map[string]map[string]interface{})
|
||||
return result
|
||||
}
|
||||
|
||||
// SubgraphManifest is the parsed subgraph.yaml.
|
||||
type SubgraphManifest struct {
|
||||
SpecVersion string `yaml:"specVersion"`
|
||||
Description string `yaml:"description"`
|
||||
Schema SchemaRef `yaml:"schema"`
|
||||
DataSources []DataSource `yaml:"dataSources"`
|
||||
Templates []Template `yaml:"templates"`
|
||||
}
|
||||
|
||||
// SchemaRef points to the GraphQL schema file.
|
||||
type SchemaRef struct {
|
||||
File string `yaml:"file"`
|
||||
}
|
||||
|
||||
// LoadSubgraph loads a compiled subgraph from a build directory.
|
||||
// Expects: build/subgraph.yaml, build/{DataSource}/*.wasm, build/schema.graphql
|
||||
func (r *Runtime) LoadSubgraph(buildDir string) (*Subgraph, error) {
|
||||
// Parse manifest
|
||||
manifestPath := filepath.Join(buildDir, "subgraph.yaml")
|
||||
data, err := os.ReadFile(manifestPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read manifest: %w", err)
|
||||
}
|
||||
|
||||
var manifest SubgraphManifest
|
||||
if err := yaml.Unmarshal(data, &manifest); err != nil {
|
||||
return nil, fmt.Errorf("parse manifest: %w", err)
|
||||
}
|
||||
|
||||
// Read schema
|
||||
schemaPath := filepath.Join(buildDir, manifest.Schema.File)
|
||||
if manifest.Schema.File == "" {
|
||||
schemaPath = filepath.Join(buildDir, "schema.graphql")
|
||||
}
|
||||
schemaBytes, err := os.ReadFile(schemaPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read schema: %w", err)
|
||||
}
|
||||
|
||||
// Load WASM modules for each data source
|
||||
for i := range manifest.DataSources {
|
||||
ds := &manifest.DataSources[i]
|
||||
wasmPath := filepath.Join(buildDir, ds.Name, ds.Name+".wasm")
|
||||
wasm, err := os.ReadFile(wasmPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("load wasm %s: %w", ds.Name, err)
|
||||
}
|
||||
ds.wasmBytes = wasm
|
||||
}
|
||||
|
||||
sg := &Subgraph{
|
||||
Name: manifest.Description,
|
||||
Schema: string(schemaBytes),
|
||||
DataSources: manifest.DataSources,
|
||||
Templates: manifest.Templates,
|
||||
Entities: NewEntityStore(),
|
||||
}
|
||||
|
||||
return sg, nil
|
||||
}
|
||||
|
||||
// EventLog represents an EVM event log to process.
|
||||
type EventLog struct {
|
||||
Address string `json:"address"`
|
||||
Topics []string `json:"topics"`
|
||||
Data string `json:"data"`
|
||||
BlockNumber uint64 `json:"blockNumber"`
|
||||
TxHash string `json:"transactionHash"`
|
||||
LogIndex uint64 `json:"logIndex"`
|
||||
Timestamp uint64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
// HandleEvent routes an EVM log to the correct WASM handler.
|
||||
func (sg *Subgraph) HandleEvent(log *EventLog) error {
|
||||
if len(log.Topics) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
topic0 := log.Topics[0]
|
||||
|
||||
for _, ds := range sg.DataSources {
|
||||
for _, handler := range ds.Mapping.EventHandlers {
|
||||
// Match topic0 against the event signature
|
||||
// TODO: compute keccak256(event signature) and compare with topic0
|
||||
_ = handler
|
||||
_ = topic0
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: instantiate WASM module via wazero, call handler function
|
||||
// with the decoded event parameters. The handler calls store.set/store.get
|
||||
// via host functions which write to sg.Entities.
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Info returns subgraph metadata as JSON.
|
||||
func (sg *Subgraph) Info() string {
|
||||
info := map[string]interface{}{
|
||||
"name": sg.Name,
|
||||
"dataSources": len(sg.DataSources),
|
||||
"templates": len(sg.Templates),
|
||||
}
|
||||
b, _ := json.Marshal(info)
|
||||
return string(b)
|
||||
}
|
||||
Reference in New Issue
Block a user