mirror of
https://github.com/luxfi/graph.git
synced 2026-09-24 00:26:07 +00:00
242 lines
7.0 KiB
Go
242 lines
7.0 KiB
Go
// 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)
|
|
}
|