UNPKG

create-eth

Version:
381 lines (303 loc) 14.5 kB
--- name: subgraph description: "Integrate The Graph subgraph into a Scaffold-ETH 2 project for indexing blockchain events. Use when the user wants to: index contract events with The Graph, add a subgraph, query onchain data with GraphQL, set up a local graph node, or deploy a subgraph to Subgraph Studio." --- # The Graph Subgraph Integration for Scaffold-ETH 2 ## Prerequisites Check if `./packages/nextjs/scaffold.config.ts` exists directly in the current working directory (do not search subdirectories). If it doesn't exist, this is not a Scaffold-ETH 2 project. Follow the instructions at https://docs.scaffoldeth.io/SKILL.md to scaffold it first. If it exists, continue directly with building. ## Overview [The Graph](https://thegraph.com/) is a decentralized indexing protocol for querying blockchain data via GraphQL. A **subgraph** defines which contract events to index, how to transform them, and exposes the indexed data through a GraphQL API. This skill adds a subgraph workspace to SE-2, with a local Graph Node (via Docker) for development and deployment to [Subgraph Studio](https://thegraph.com/studio/) for production. For The Graph's full API reference, see the [official docs](https://thegraph.com/docs/). This skill focuses on the SE-2 integration — the workspace structure, the ABI copy bridge, and local development workflow. ## Dependencies & Scripts ### Subgraph package (`packages/subgraph/`) Create `packages/subgraph/package.json`: ```json { "name": "@se-2/subgraph", "version": "0.0.1", "type": "module", "scripts": { "abi-copy": "tsx scripts/abi_copy.ts", "codegen": "graph codegen", "build": "graph build", "graph": "graph", "deploy": "graph deploy --node https://api.studio.thegraph.com/deploy/ your-contract", "create-local": "graph create --node http://localhost:8020/ scaffold-eth/your-contract", "remove-local": "graph remove --node http://localhost:8020/ scaffold-eth/your-contract", "deploy-local": "graph deploy --node http://localhost:8020/ --ipfs http://localhost:5001 scaffold-eth/your-contract", "local-ship": "yarn abi-copy && yarn codegen && yarn build --network localhost && yarn deploy-local", "test": "graph test -d", "run-node": "cd graph-node && docker compose up", "stop-node": "cd graph-node && docker compose down", "clean-node": "rm -rf graph-node/data/" }, "dependencies": { "@graphprotocol/graph-cli": "^0.98.0", "@graphprotocol/graph-ts": "^0.38.0", "tsx": "^4.0.0", "typescript": "^5.7.0" }, "devDependencies": { "@types/chalk": "^2.2.0", "@types/node": "^20.11.0", "matchstick-as": "~0.6.0" } } ``` ### NextJS package additions For querying the subgraph from the frontend via Graph Client: ```json { "scripts": { "client": "graphclient build" }, "dependencies": { "graphql": "^16.8.0" }, "devDependencies": { "@graphprotocol/client-cli": "^3.0.0" } } ``` ### Root package.json scripts ```json { "graph": "yarn workspace @se-2/subgraph graph", "graphclient:build": "yarn workspace @se-2/nextjs client", "subgraph:abi-copy": "yarn workspace @se-2/subgraph abi-copy", "subgraph:build": "yarn workspace @se-2/subgraph build", "subgraph:clean-node": "yarn workspace @se-2/subgraph clean-node", "subgraph:codegen": "yarn workspace @se-2/subgraph codegen", "subgraph:create-local": "yarn workspace @se-2/subgraph create-local", "subgraph:local-ship": "yarn workspace @se-2/subgraph local-ship", "subgraph:run-node": "yarn workspace @se-2/subgraph run-node", "subgraph:stop-node": "yarn workspace @se-2/subgraph stop-node", "subgraph:test": "yarn workspace @se-2/subgraph test -d" } ``` ## Docker Setup (Local Graph Node) The Graph requires three services: a Graph Node, IPFS, and PostgreSQL. Create `packages/subgraph/graph-node/docker-compose.yml` with these three services: - **graph-node**: `graphprotocol/graph-node:v0.41.1` — ports 8000 (GraphQL), 8001, 8020 (admin), 8030, 8040. Set `ethereum: "localhost:http://host.docker.internal:8545"` to connect to the local chain. Add `extra_hosts: ["host.docker.internal:host-gateway"]`. - **ipfs**: `ipfs/kubo:v0.39.0` (not the legacy `ipfs/go-ipfs`) — port 5001, volume `./data/ipfs:/data/ipfs` - **postgres**: `postgres` — port 5432, volume `./data/postgres:/var/lib/postgresql/data`. Credentials: user `graph-node`, password `let-me-in`, db `graph-node`. **Must set `POSTGRES_INITDB_ARGS: "--locale=C --encoding=UTF8"`** — graph-node requires the C locale and will panic on startup otherwise. The graph-node environment also needs: `postgres_host: postgres`, `postgres_user/pass/db`, `ipfs: "ipfs:5001"`, `GRAPH_LOG: info`. ## Subgraph Configuration ### Subgraph manifest (`subgraph.yaml`) The manifest defines what to index. Adapt this to the project's actual contracts: ```yaml # packages/subgraph/subgraph.yaml specVersion: 0.0.4 description: Your subgraph description schema: file: ./src/schema.graphql dataSources: - kind: ethereum/contract name: YourContract network: localhost source: abi: YourContract address: "0x5FbDB2315678afecb367f032d93F642f64180aa3" mapping: kind: ethereum/events apiVersion: 0.0.6 language: wasm/assemblyscript entities: - Greeting - Sender abis: - name: YourContract file: ./abis/localhost_YourContract.json eventHandlers: - event: GreetingChange(indexed address,string,bool,uint256) handler: handleGreetingChange file: ./src/mapping.ts ``` **Key fields to update per project:** - `name` — must match the contract name in `deployedContracts.ts` - `address` — auto-updated by `abi-copy` script for localhost - `eventHandlers` — must match the exact Solidity event signatures (parameter names don't matter, types and order do) - `entities` — must match what's defined in `schema.graphql` ### GraphQL schema (`src/schema.graphql`) Define entities that represent your indexed data. Each entity maps to a table in the Graph Node's Postgres: ```graphql # packages/subgraph/src/schema.graphql type Greeting @entity(immutable: true) { id: ID! sender: Sender! greeting: String! premium: Boolean value: BigInt createdAt: BigInt! transactionHash: String! } type Sender @entity(immutable: false) { id: ID! address: Bytes! greetings: [Greeting!] @derivedFrom(field: "sender") createdAt: BigInt! greetingCount: BigInt! } ``` ### AssemblyScript mappings (`src/mapping.ts`) Mappings transform raw event data into entities. They're written in [AssemblyScript](https://www.assemblyscript.org/) (a TypeScript subset that compiles to WASM): ```typescript // packages/subgraph/src/mapping.ts import { BigInt } from "@graphprotocol/graph-ts"; import { GreetingChange } from "../generated/YourContract/YourContract"; import { Greeting, Sender } from "../generated/schema"; export function handleGreetingChange(event: GreetingChange): void { const senderString = event.params.greetingSetter.toHexString(); let sender = Sender.load(senderString); if (sender === null) { sender = new Sender(senderString); sender.address = event.params.greetingSetter; sender.createdAt = event.block.timestamp; sender.greetingCount = BigInt.fromI32(1); } else { sender.greetingCount = sender.greetingCount.plus(BigInt.fromI32(1)); } const greeting = new Greeting( event.transaction.hash.toHex() + "-" + event.logIndex.toString(), ); greeting.greeting = event.params.newGreeting; greeting.sender = senderString; greeting.premium = event.params.premium; greeting.value = event.params.value; greeting.createdAt = event.block.timestamp; greeting.transactionHash = event.transaction.hash.toHex(); greeting.save(); sender.save(); } ``` AssemblyScript compiles to WASM — no closures, no `Array.map/filter/reduce`, no `console.log`. Use `@graphprotocol/graph-ts` utilities for logging (`log.info()`). ## ABI Copy Bridge The `abi-copy` script bridges SE-2's deployment output to the subgraph. It reads `packages/nextjs/contracts/deployedContracts.ts`, extracts ABIs and addresses for chain ID 31337 (localhost), and writes them to `packages/subgraph/abis/` and `networks.json`. Create `packages/subgraph/scripts/abi_copy.ts` — this script parses the deployedContracts file, extracts contract data, and publishes it: ```typescript // packages/subgraph/scripts/abi_copy.ts import * as fs from "fs"; import type { Abi } from "viem"; const DEPLOYED_CONTRACTS_FILE = "../nextjs/contracts/deployedContracts.ts"; const GRAPH_DIR = "./"; function publishContract( contractName: string, contractObject: { address: string; abi: Abi }, networkName: string, ) { const graphConfigPath = `${GRAPH_DIR}/networks.json`; let graphConfig = fs.existsSync(graphConfigPath) ? JSON.parse(fs.readFileSync(graphConfigPath, "utf8")) : {}; if (!graphConfig[networkName]) graphConfig[networkName] = {}; graphConfig[networkName][contractName] = { address: contractObject.address }; fs.writeFileSync(graphConfigPath, JSON.stringify(graphConfig, null, 2)); if (!fs.existsSync(`${GRAPH_DIR}/abis`)) fs.mkdirSync(`${GRAPH_DIR}/abis`); fs.writeFileSync( `${GRAPH_DIR}/abis/${networkName}_${contractName}.json`, JSON.stringify(contractObject.abi, null, 2), ); } async function main() { const fileContent = fs.readFileSync(DEPLOYED_CONTRACTS_FILE, "utf8"); const match = fileContent.match( /const deployedContracts = ({[^;]+}) as const;/s, ); if (!match?.[1]) throw new Error("Failed to find deployedContracts"); // Parse the TS object literal as JSON (add quotes around keys, remove trailing commas) let json = match[1] .replace(/(\w+)(?=\s*:)/g, '"$1"') .replace(/,(?=\s*[}\]])/g, ""); const contracts = JSON.parse(json); const localContracts = contracts[31337]; if (!localContracts) { console.error("No contracts for local network."); return; } for (const name in localContracts) { publishContract(name, localContracts[name], "localhost"); } console.log("Published contracts to subgraph package."); } main().catch((e) => { console.error(e); process.exit(1); }); ``` ## Graph Client (Frontend Queries) [Graph Client](https://github.com/graphprotocol/graph-client) provides a typed GraphQL client with features like client-side composition and automatic pagination. ### Configuration ```yaml # packages/nextjs/.graphclientrc.yml sources: - name: YourContract handler: graphql: endpoint: http://localhost:8000/subgraphs/name/scaffold-eth/your-contract documents: - ./graphql/GetGreetings.gql ``` ### GraphQL queries ```graphql # packages/nextjs/graphql/GetGreetings.gql query GetGreetings { greetings(first: 25, orderBy: createdAt, orderDirection: desc) { id greeting premium value createdAt sender { address greetingCount } } } ``` ### Using in components After running `yarn graphclient:build`, import the generated client. Use TanStack Query (already available in SE-2) for data fetching: ```tsx "use client"; import { useQuery } from "@tanstack/react-query"; import { GetGreetingsDocument, execute } from "~~/.graphclient"; async function fetchGreetings() { const result = await execute(GetGreetingsDocument, {}); return result.data?.greetings ?? []; } const GreetingsTable = () => { const { data: greetings = [], isLoading, error, } = useQuery({ queryKey: ["subgraph-greetings"], queryFn: fetchGreetings, }); // Render data... }; ``` > **`~~/.graphclient`** is the generated runtime artifact. It only exists after `yarn graphclient:build`. The `.graphclient/` directory should NOT be committed — it's generated from `.graphclientrc.yml` and the GQL files. ## Gotchas & Common Pitfalls **Docker must be running.** The local Graph Node, IPFS, and Postgres all run in Docker. If Docker isn't running, `yarn subgraph:run-node` will fail. **`yarn deploy` must run before `yarn subgraph:abi-copy`.** The ABI copy script reads from `deployedContracts.ts` which is generated by the deploy step. If you haven't deployed, there's nothing to copy. **`local-ship` does everything in one command.** It runs `abi-copy` → `codegen` → `build` → `deploy-local` sequentially. Use this instead of running each step manually. **`create-local` only needs to run once.** It registers the subgraph name with the local Graph Node. Running it again will error with "subgraph already exists." Only re-run after `clean-node`. **Linux users need `--hostname 0.0.0.0`.** The default Hardhat/Anvil config binds to `127.0.0.1`, which Docker can't reach. Add `--hostname 0.0.0.0` (Hardhat) or `--host 0.0.0.0` (Anvil) to the chain command. You may also need `sudo ufw allow 8545/tcp`. **Graph Client artifacts must be regenerated after schema changes.** Run `yarn graphclient:build` whenever you change the GraphQL schema or queries. The frontend imports from `~~/.graphclient` which contains generated types. **Port conflicts with other services.** The Graph Node stack uses ports 5001 (IPFS), 5432 (Postgres), 8000 (GraphQL), 8020 (admin). If you're also running the drizzle-neon extension (which uses port 5432 for its own Postgres), you'll have a conflict. Change one of the Postgres ports. ## How to Test 1. `yarn chain` — start local blockchain 2. `yarn deploy` — deploy contracts (generates `deployedContracts.ts`) 3. `yarn subgraph:run-node` — start Docker Graph Node (keep this terminal open) 4. `yarn subgraph:create-local` — register subgraph (once only) 5. `yarn subgraph:local-ship` — copies ABIs, generates types, builds, and deploys 6. Visit `http://localhost:8000/subgraphs/name/scaffold-eth/your-contract/graphql` — test GraphQL queries 7. `yarn graphclient:build` — generate frontend client artifacts 8. `yarn start` — visit the subgraph page to see indexed data 9. `yarn subgraph:test` — run Matchstick unit tests ### Deploying to Subgraph Studio 1. Update `subgraph.yaml`: change `network` from `localhost` to target network (e.g., `sepolia`), add deployed `address` and `startBlock` 2. Create a subgraph on [Subgraph Studio](https://thegraph.com/studio/) 3. `yarn graph auth --studio <DEPLOY_KEY>` 4. `yarn graph deploy --studio <SUBGRAPH_SLUG>` 5. Update the Graph Client endpoint in `.graphclientrc.yml` to point to the Studio URL For the full list of [supported networks](https://thegraph.com/docs/networks), check The Graph docs.