// Package engine implements the GraphQL query execution engine. // // Extracted from chains/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/dao" "github.com/luxfi/graph/resolvers/derivatives" "github.com/luxfi/graph/resolvers/dex" "github.com/luxfi/graph/resolvers/did" "github.com/luxfi/graph/resolvers/exchange" "github.com/luxfi/graph/resolvers/fhe" "github.com/luxfi/graph/resolvers/governance" "github.com/luxfi/graph/resolvers/identity" "github.com/luxfi/graph/resolvers/key" "github.com/luxfi/graph/resolvers/liquid" "github.com/luxfi/graph/resolvers/liquidity" "github.com/luxfi/graph/resolvers/liquidprotocol" "github.com/luxfi/graph/resolvers/mpc" "github.com/luxfi/graph/resolvers/oracle" "github.com/luxfi/graph/resolvers/platform" "github.com/luxfi/graph/resolvers/precompile" "github.com/luxfi/graph/resolvers/prediction" "github.com/luxfi/graph/resolvers/privacy" "github.com/luxfi/graph/resolvers/quantum" "github.com/luxfi/graph/resolvers/relay" "github.com/luxfi/graph/resolvers/securities" "github.com/luxfi/graph/resolvers/servicenode" "github.com/luxfi/graph/resolvers/treasury" "github.com/luxfi/graph/resolvers/utxo" "github.com/luxfi/graph/resolvers/zk" "github.com/luxfi/graph/storage" ) // Config controls query execution limits. type Config struct { MaxQueryDepth int `json:"maxQueryDepth" yaml:"max_query_depth"` MaxTopFields int `json:"maxTopFields" yaml:"max_top_fields"` MaxResultSize int `json:"maxResultSize" yaml:"max_result_size"` QueryTimeoutMs int `json:"queryTimeoutMs" yaml:"query_timeout_ms"` } // A page that asks for several tables at once is ordinary, not abusive: the // Explore transactions view alone requests 23. The cap exists to bound work per // request, so it sits with the other limits rather than as a literal at the // check. const defaultMaxTopFields = 64 func (e *Engine) maxTopFields() int { if e.config == nil || e.config.MaxTopFields <= 0 { return defaultMaxTopFields } return e.config.MaxTopFields } // 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, MaxTopFields: defaultMaxTopFields, 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 "utxo": e.registerChain(utxo.Register) return nil case "zk", "zchain": e.registerChain(zk.Register) return nil case "mpc", "mchain": e.registerChain(mpc.Register) return nil case "bridge", "bchain": e.registerChain(bridge.Register) return nil case "privacy": 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 "governance": e.registerChain(governance.Register) return nil case "dao": e.registerChain(dao.Register) return nil case "treasury": e.registerChain(treasury.Register) return nil case "liquid", "liquid-staking": e.registerChain(liquid.Register) return nil case "liquidity", "liquidity-protocol", "omnichain": e.registerChain(liquidity.Register) return nil case "liquid-protocol", "teleport", "liquid-vault": e.registerChain(liquidprotocol.Register) return nil case "did", "did-registry": e.registerChain(did.Register) return nil case "prediction", "prediction-market": e.registerChain(prediction.Register) return nil case "securities", "security-token": e.registerChain(securities.Register) return nil case "derivatives", "futures", "options": e.registerChain(derivatives.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(utxo.Register) e.registerChain(bridge.Register) e.registerChain(privacy.Register) e.registerChain(quantum.Register) e.registerChain(key.Register) e.registerChain(mpc.Register) e.registerChain(ai.Register) e.registerChain(identity.Register) e.registerChain(oracle.Register) e.registerChain(relay.Register) e.registerChain(zk.Register) e.registerChain(servicenode.Register) e.registerChain(precompile.Register) e.registerChain(governance.Register) e.registerChain(dao.Register) e.registerChain(treasury.Register) e.registerChain(liquid.Register) e.registerChain(did.Register) e.registerChain(prediction.Register) e.registerChain(securities.Register) e.registerChain(derivatives.Register) e.registerChain(liquidity.Register) e.registerChain(liquidprotocol.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 max := e.maxTopFields(); len(fields) > max { return &Response{Errors: []Error{{Message: fmt.Sprintf("too many top-level fields (%d), maximum %d", len(fields), max)}}} } 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 the top-level fields of a GraphQL query. // // A field ends where its own parts end — its arguments, then its selection set // — not at a newline. Whitespace between fields is whitespace: a document is // the same document pretty-printed or minified, and a client that sends one // line is entitled to the same answer as one that sends ten. // // Splitting on newlines instead made a single-line document parse as ONE field, // name taken up to the first paren, and every field after it was dropped // without an error. The explore page asks for its bundle, factory, tokens, // pools and day series in one document; minified, it would have been answered // with the bundle alone and no sign anything was missing. func parseTopFields(query string) ([]field, error) { 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 for i := 0; i < len(body); { if isSpace(body[i]) || body[i] == ',' { i++ continue } fieldStart := i i = skipSpace(body, scanName(body, i)) if i < len(body) && body[i] == ':' { // `alias: name` i = skipSpace(body, scanName(body, skipSpace(body, i+1))) } if i < len(body) && body[i] == '(' { closed := findMatchingParen(body, i) if closed <= i { return nil, fmt.Errorf("unclosed arguments") } i = skipSpace(body, closed+1) } if i < len(body) && body[i] == '{' { closed := findMatchingBrace(body, i) if closed <= i { return nil, fmt.Errorf("unclosed selection set") } i = closed + 1 } // Every pass must consume something. A scanner that can stand still on // an input it does not understand does not return a wrong answer — it // returns none, forever, holding the request open. This is a public // endpoint; the parser refuses the query instead. if i == fieldStart { return nil, fmt.Errorf("cannot parse field at offset %d", fieldStart) } if f, err := parseField(body[fieldStart:i]); err == nil && f.name != "" { fields = append(fields, f) } } if len(fields) == 0 { return nil, fmt.Errorf("no fields in query") } return fields, nil } func isSpace(c byte) bool { return c == ' ' || c == '\t' || c == '\n' || c == '\r' } // scanName advances past a field or alias name — everything up to the // punctuation that can follow one. func scanName(s string, i int) int { for i < len(s) && !isSpace(s[i]) && s[i] != '(' && s[i] != '{' && s[i] != ':' && s[i] != ',' { i++ } return i } func skipSpace(s string, i int) int { for i < len(s) && isSpace(s[i]) { i++ } return i } // findMatchingBrace finds the '}' closing the '{' at pos, respecting nesting // and quoted strings. func findMatchingBrace(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 } } } return -1 } 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 = ` g-node GraphQL
`