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:
2026-04-11 13:47:05 -07:00
commit de2e43e606
39 changed files with 6448 additions and 0 deletions
+23
View File
@@ -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"]
+13
View File
@@ -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
Executable
BIN
View File
Binary file not shown.
+262
View File
@@ -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
}
+27
View File
@@ -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())
}
+112
View File
@@ -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
}
+432
View File
@@ -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()
}
+238
View File
@@ -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)
}
}
}
+514
View File
@@ -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")
}
}
+114
View File
@@ -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
View File
@@ -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
}
+563
View File
@@ -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>`
+55
View File
@@ -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
View File
@@ -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"`
}
+98
View File
@@ -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)
}
+143
View File
@@ -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"`
}
+49
View File
@@ -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
)
+254
View File
@@ -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=
+100
View File
@@ -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
},
}
+364
View File
@@ -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,
})
}
}
+74
View File
@@ -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
}
+65
View File
@@ -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
}
+90
View File
@@ -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
}
+79
View File
@@ -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
}
+75
View File
@@ -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
}
+74
View File
@@ -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
}
+74
View File
@@ -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
}
+74
View File
@@ -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
}
+74
View File
@@ -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
}
+159
View File
@@ -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
}
+70
View File
@@ -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
}
+74
View File
@@ -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
}
+74
View File
@@ -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
}
+74
View File
@@ -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
}
+89
View File
@@ -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&region=%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
}
+175
View File
@@ -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
}
+372
View File
@@ -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
}
+545
View File
@@ -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
View File
@@ -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)
}