diff --git a/biome.json b/biome.json index eaabf16..7defb6b 100644 --- a/biome.json +++ b/biome.json @@ -10,6 +10,7 @@ }, "linter": { "enabled": true, + "ignore": ["contracts/councilhaus-subgraph/generated/**"], "rules": { "recommended": true, "suspicious": { diff --git a/contracts/councilhaus-subgraph/.gitignore b/contracts/councilhaus-subgraph/.gitignore new file mode 100644 index 0000000..364e724 --- /dev/null +++ b/contracts/councilhaus-subgraph/.gitignore @@ -0,0 +1,38 @@ +# Abis and subgraph.yaml +abis/ +subgraph.yaml + +# Graph CLI generated artifacts +build/ +generated/ + +# Dependency directories +node_modules/ +jspm_packages/ + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# dotenv environment variables file +.env + +# Testing +coverage +coverage.json + +# Typechain +typechain +typechain-types + +# Hardhat files +cache diff --git a/contracts/councilhaus-subgraph/config/optimism.json b/contracts/councilhaus-subgraph/config/optimism.json new file mode 100644 index 0000000..e72041c --- /dev/null +++ b/contracts/councilhaus-subgraph/config/optimism.json @@ -0,0 +1,5 @@ +{ + "network": "optimism", + "address": "0x8e01ebaa4f4d660294e0d1301fe46ed96f4c44eb", + "startBlock": 125496765 +} diff --git a/contracts/councilhaus-subgraph/docker-compose.yml b/contracts/councilhaus-subgraph/docker-compose.yml new file mode 100644 index 0000000..a008fc9 --- /dev/null +++ b/contracts/councilhaus-subgraph/docker-compose.yml @@ -0,0 +1,50 @@ +version: "3" +services: + graph-node: + image: graphprotocol/graph-node + ports: + - "8000:8000" + - "8001:8001" + - "8020:8020" + - "8030:8030" + - "8040:8040" + depends_on: + - ipfs + - postgres + extra_hosts: + - host.docker.internal:host-gateway + environment: + postgres_host: postgres + postgres_user: graph-node + postgres_pass: let-me-in + postgres_db: graph-node + ipfs: "ipfs:5001" + ethereum: "mainnet:http://host.docker.internal:8545" + GRAPH_LOG: info + ipfs: + image: ipfs/kubo:v0.17.0 + ports: + - "5001:5001" + volumes: + - ./data/ipfs:/data/ipfs + postgres: + image: postgres:14 + ports: + - "5432:5432" + command: + [ + "postgres", + "-cshared_preload_libraries=pg_stat_statements", + "-cmax_connections=200", + ] + environment: + POSTGRES_USER: graph-node + POSTGRES_PASSWORD: let-me-in + POSTGRES_DB: graph-node + # FIXME: remove this env. var. which we shouldn't need. Introduced by + # , maybe as a + # workaround for https://github.com/docker/for-mac/issues/6270? + PGDATA: "/var/lib/postgresql/data" + POSTGRES_INITDB_ARGS: "-E UTF8 --locale=C" + volumes: + - ./data/postgres:/var/lib/postgresql/data diff --git a/contracts/councilhaus-subgraph/networks.json b/contracts/councilhaus-subgraph/networks.json new file mode 100644 index 0000000..d84e9a0 --- /dev/null +++ b/contracts/councilhaus-subgraph/networks.json @@ -0,0 +1,8 @@ +{ + "optimism": { + "CouncilFactory": { + "address": "0x8e01ebaa4f4d660294e0d1301fe46ed96f4c44eb", + "startBlock": 125496765 + } + } +} diff --git a/contracts/councilhaus-subgraph/package.json b/contracts/councilhaus-subgraph/package.json new file mode 100644 index 0000000..2c81848 --- /dev/null +++ b/contracts/councilhaus-subgraph/package.json @@ -0,0 +1,22 @@ +{ + "name": "councilhaus-subgraph", + "license": "AGPL-3.0-only", + "scripts": { + "codegen": "graph codegen", + "build": "graph build", + "deploy": "graph deploy --node https://api.studio.thegraph.com/deploy/ councilhaus-optimism", + "prepare:optimism": "mustache config/optimism.json subgraph.template.yaml > subgraph.yaml", + "create-local": "graph create --node http://localhost:8020/ councilhaus-optimism", + "remove-local": "graph remove --node http://localhost:8020/ councilhaus-optimism", + "deploy-local": "graph deploy --node http://localhost:8020/ --ipfs http://localhost:5001 councilhaus-optimism", + "test": "graph test" + }, + "dependencies": { + "@graphprotocol/graph-cli": "0.82.0", + "@graphprotocol/graph-ts": "0.35.1" + }, + "devDependencies": { + "matchstick-as": "0.5.0", + "mustache": "^4.0.1" + } +} diff --git a/contracts/councilhaus-subgraph/schema.graphql b/contracts/councilhaus-subgraph/schema.graphql new file mode 100644 index 0000000..05f421d --- /dev/null +++ b/contracts/councilhaus-subgraph/schema.graphql @@ -0,0 +1,35 @@ +type Council @entity { + id: ID! + councilName: String! + councilSymbol: String! + distributionToken: Bytes! + councilMembers: [CouncilMember!]! @derivedFrom(field: "council") + grantees: [Grantee!]! @derivedFrom(field: "council") + allocations: [Allocation!]! @derivedFrom(field: "council") + createdAt: BigInt! +} + +type CouncilMember @entity { + id: ID! + account: Bytes! + votingPower: BigInt! + council: Council! + enabled: Boolean! +} + +type Grantee @entity { + id: ID! + name: String! + account: Bytes! + council: Council! + enabled: Boolean! +} + +type Allocation @entity { + id: ID! + council: Council! + councilMember: CouncilMember! + grantees: [Grantee!]! + amounts: [BigInt!]! + allocatedAt: BigInt! +} \ No newline at end of file diff --git a/contracts/councilhaus-subgraph/src/council-factory.ts b/contracts/councilhaus-subgraph/src/council-factory.ts new file mode 100644 index 0000000..8c508e0 --- /dev/null +++ b/contracts/councilhaus-subgraph/src/council-factory.ts @@ -0,0 +1,20 @@ +import type { CouncilCreated as CouncilCreatedEvent } from "../generated/CouncilFactory/CouncilFactory"; +import { Council } from "../generated/schema"; +import { Council as CouncilTemplate } from "../generated/templates"; +import { Council as CouncilContract } from "../generated/templates/Council/Council"; + +export function handleCouncilCreated(event: CouncilCreatedEvent): void { + const councilAddress = event.params.council; + const entity = new Council(event.params.council.toHex()); + + const councilContract = CouncilContract.bind(councilAddress); + + entity.councilName = councilContract.name(); + entity.councilSymbol = councilContract.symbol(); + entity.distributionToken = councilContract.distributionToken(); + entity.createdAt = event.block.timestamp; + + entity.save(); + + CouncilTemplate.create(councilAddress); +} diff --git a/contracts/councilhaus-subgraph/src/council.ts b/contracts/councilhaus-subgraph/src/council.ts new file mode 100644 index 0000000..b5bbd08 --- /dev/null +++ b/contracts/councilhaus-subgraph/src/council.ts @@ -0,0 +1,89 @@ +// biome-ignore lint/suspicious/noShadowRestrictedNames: x +import { BigInt } from "@graphprotocol/graph-ts"; +import { log } from "@graphprotocol/graph-ts"; +import { Allocation, CouncilMember, Grantee } from "../generated/schema"; +import type { + BudgetAllocated, + CouncilMemberAdded, + CouncilMemberRemoved, + GranteeAdded, + GranteeRemoved, +} from "../generated/templates/Council/Council"; + +// Handle council member added +export function handleCouncilMemberAdded(event: CouncilMemberAdded): void { + const councilMember = new CouncilMember(event.params.member.toHex()); + councilMember.account = event.params.member; + councilMember.votingPower = event.params.votingPower; + councilMember.council = event.address.toHex(); // Linking to the council + councilMember.enabled = true; + + councilMember.save(); +} + +// Handle council member removed +export function handleCouncilMemberRemoved(event: CouncilMemberRemoved): void { + const councilMemberId = event.params.member.toHex(); + const councilMember = CouncilMember.load(councilMemberId); + if (councilMember) { + councilMember.votingPower = new BigInt(0); + councilMember.enabled = false; + councilMember.save(); + } else { + log.warning("Council member not found, skipping removal", [ + councilMemberId, + ]); + } +} + +// Handle grantee added +export function handleGranteeAdded(event: GranteeAdded): void { + const grantee = new Grantee(event.params.grantee.toHex()); + grantee.name = event.params.name; + grantee.account = event.params.grantee; + grantee.council = event.address.toHex(); // Linking to the council + grantee.enabled = true; + + grantee.save(); +} + +// Handle grantee removed +export function handleGranteeRemoved(event: GranteeRemoved): void { + const grantee = Grantee.load(event.params.grantee.toHex()); + if (grantee) { + grantee.enabled = false; + grantee.save(); + } else { + log.warning("Grantee not found, skipping removal", [ + event.params.grantee.toHex(), + ]); + } +} + +// Handle budget allocated +export function handleBudgetAllocated(event: BudgetAllocated): void { + const allocation = new Allocation( + `${event.transaction.hash.toHex()}-${event.logIndex.toString()}`, + ); + + allocation.council = event.address.toHex(); + allocation.councilMember = event.params.member.toHex(); + allocation.allocatedAt = event.block.timestamp; + allocation.amounts = event.params.allocation.amounts; + + // Check all grantees are valid + const grantees: string[] = []; + const accounts = event.params.allocation.accounts; + for (let i = 0; i < accounts.length; i++) { + const grantee = Grantee.load(accounts[i].toHex()); + if (!grantee) { + log.warning("Not all grantees found, skipping allocation", [ + accounts[i].toHex(), + ]); + return; + } + grantees.push(grantee.id); + } + allocation.grantees = grantees; + allocation.save(); +} diff --git a/contracts/councilhaus-subgraph/subgraph.template.yaml b/contracts/councilhaus-subgraph/subgraph.template.yaml new file mode 100644 index 0000000..640d4a5 --- /dev/null +++ b/contracts/councilhaus-subgraph/subgraph.template.yaml @@ -0,0 +1,62 @@ +specVersion: 1.0.0 +description: Track councils, council members, grantees, and allocations created by the CouncilFactory. +repository: "https://github.com/BlossomLabs/councilhaus/tree/master/packages/councilhaus-subgraph" + +indexerHints: + prune: auto + +schema: + file: ./schema.graphql + +dataSources: + - kind: ethereum + name: CouncilFactory + network: {{ network }} + source: + address: "{{ address }}" + abi: CouncilFactory + startBlock: {{ startBlock }} + mapping: + kind: ethereum/events + apiVersion: 0.0.6 + language: wasm/assemblyscript + entities: + - Council + abis: + - name: CouncilFactory + file: ./abis/CouncilFactory.json + - name: Council + file: ./abis/Council.json + eventHandlers: + - event: CouncilCreated(address,address) + handler: handleCouncilCreated + file: ./src/council-factory.ts +templates: + - kind: ethereum/contract + name: Council + network: {{ network }} + source: + abi: Council + mapping: + kind: ethereum/events + apiVersion: 0.0.6 + language: wasm/assemblyscript + entities: + - CouncilMember + - Grantee + - Budget + abis: + - name: Council + file: ./abis/Council.json + eventHandlers: + - event: CouncilMemberAdded(address,uint256) + handler: handleCouncilMemberAdded + - event: CouncilMemberRemoved(address) + handler: handleCouncilMemberRemoved + - event: GranteeAdded(string,address) + handler: handleGranteeAdded + - event: GranteeRemoved(address) + handler: handleGranteeRemoved + - event: BudgetAllocated(address,(address[],uint128[])) + handler: handleBudgetAllocated + file: ./src/council.ts \ No newline at end of file diff --git a/contracts/councilhaus-subgraph/tsconfig.json b/contracts/councilhaus-subgraph/tsconfig.json new file mode 100644 index 0000000..4e86672 --- /dev/null +++ b/contracts/councilhaus-subgraph/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "@graphprotocol/graph-ts/types/tsconfig.base.json", + "include": ["src", "tests"] +} diff --git a/contracts/councilhaus/hardhat.config.ts b/contracts/councilhaus/hardhat.config.ts index f76ea7b..c4009ae 100644 --- a/contracts/councilhaus/hardhat.config.ts +++ b/contracts/councilhaus/hardhat.config.ts @@ -1,5 +1,6 @@ import type { HardhatUserConfig } from "hardhat/config"; import "@nomicfoundation/hardhat-toolbox-viem"; +import "hardhat-abi-exporter"; const config: HardhatUserConfig = { solidity: { @@ -36,6 +37,12 @@ const config: HardhatUserConfig = { sourcify: { enabled: false, }, + abiExporter: { + path: "../councilhaus-subgraph/abis", + runOnCompile: true, + clear: true, + flat: true, + }, }; export default config; diff --git a/contracts/councilhaus/package.json b/contracts/councilhaus/package.json index 9a50082..d4665d9 100644 --- a/contracts/councilhaus/package.json +++ b/contracts/councilhaus/package.json @@ -6,7 +6,8 @@ "hardhat": "^2.22.7" }, "dependencies": { - "@openzeppelin/contracts": "^5.0.2" + "@openzeppelin/contracts": "^5.0.2", + "hardhat-abi-exporter": "^2.10.1" }, "scripts": { "compile": "hardhat compile",