Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Codegen subgraph
run: pnpm --filter @dealbot/subgraph codegen
Comment thread
dennis-tra marked this conversation as resolved.

- name: Run unit tests
run: pnpm test

Expand Down
21 changes: 21 additions & 0 deletions apps/subgraph/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# graph-cli outputs
build/
generated/

# Derived from @filoz/synapse-core/abis by scripts/sync-abis.mjs.
# Regenerated on every `pnpm run codegen`.
abis/

# Node dependencies
node_modules/

# Goldsky deploy artifacts
.goldsky/

# Test outputs
coverage/
tests/.bin/
tests/.latest.json

# Editor / OS
.DS_Store
50 changes: 50 additions & 0 deletions apps/subgraph/README.md
Comment thread
dennis-tra marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# @dealbot/subgraph

A dealbot-owned Graph Protocol subgraph.

## What it indexes

- **PDPVerifier** — dataset lifecycle, piece add/remove, proving periods.
- **FilecoinWarmStorageService (FWSS)** — payer/service-provider metadata, `withIPFSIndexing` flag, `ipfsRootCID` per piece, service/payment termination.

## Contract ABIs

`abis/*.json` is generated by `scripts/sync-abis.mjs` from the canonical
`@filoz/synapse-core/abis` exports (`pdp`, `fwss`). It runs automatically
before `pnpm codegen`, so the subgraph stays in lock-step with the SDK;
pulling in new contract changes is a synapse-core version bump.

## Local commands

```bash
# Refresh abis/ from @filoz/synapse-core (also runs on codegen)
pnpm sync-abis

# Typegen only (no WASM build)
pnpm codegen

# Full build for one network
pnpm build:mainnet
pnpm build:calibration

# Run matchstick tests
pnpm test
```

## Deploy
Comment thread
dennis-tra marked this conversation as resolved.

Requires `goldsky` CLI authenticated via `GOLDSKY_API_KEY`.

```bash
export VERSION=0.1.0
pnpm build:calibration
pnpm deploy:calibration

pnpm build:mainnet
pnpm deploy:mainnet
```

Goldsky slots (slugs TBD):

- `dealbot-mainnet/<version>` — mainnet
- `dealbot-calibration/<version>` — calibration
22 changes: 22 additions & 0 deletions apps/subgraph/networks.json
Comment thread
dennis-tra marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"filecoin": {
"PDPVerifier": {
"address": "0xBADd0B92C1c71d02E7d520f64c0876538fa2557F",
"startBlock": 5441432
},
"FilecoinWarmStorageService": {
"address": "0x8408502033C418E1bbC97cE9ac48E5528F371A9f",
"startBlock": 5459617
}
},
"filecoin-testnet": {
"PDPVerifier": {
"address": "0x85e366Cf9DD2c0aE37E963d9556F5f4718d6417C",
"startBlock": 3140755
},
"FilecoinWarmStorageService": {
"address": "0x02925630df557F957f70E112bA06e50965417CA0",
"startBlock": 3141276
}
}
}
24 changes: 24 additions & 0 deletions apps/subgraph/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@dealbot/subgraph",
"private": true,
"license": "(MIT OR Apache-2.0)",
"scripts": {
"sync-abis": "node scripts/sync-abis.mjs",
"codegen": "pnpm run sync-abis && graph codegen",
"build": "pnpm run build:mainnet",
"build:mainnet": "pnpm run codegen && graph build --network filecoin",
"build:calibration": "pnpm run codegen && graph build --network filecoin-testnet",
"deploy:mainnet": "goldsky subgraph deploy dealbot-mainnet/$VERSION --path ./build",
"deploy:calibration": "goldsky subgraph deploy dealbot-calibration/$VERSION --path ./build",
"test": "graph test"
},
"dependencies": {
"@filoz/synapse-core": "0.3.3",
"@graphprotocol/graph-cli": "0.98.1",
"@graphprotocol/graph-ts": "0.38.2"
},
"devDependencies": {
"assemblyscript": "0.19.23",
"matchstick-as": "0.6.0"
}
}
82 changes: 82 additions & 0 deletions apps/subgraph/schema.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
enum DataSetStatus {
EMPTY # Newly created dataset, no roots yet
READY # Dataset has roots and is ready for proving
PROVING # Proofs are currently expected for this dataset
DELETED # Dataset has been deleted on-chain
}

type Provider @entity(immutable: false) {
id: Bytes! # address
address: Bytes!
# Proving-period tracking is strictly bound to the FWSS contract.
totalFaultedPeriods: BigInt!
totalProvingPeriods: BigInt!
# Derived relationship
proofSets: [DataSet!]! @derivedFrom(field: "owner")
}

type DataSet @entity(immutable: false) {
id: Bytes! # setId
setId: BigInt!
owner: Provider!
# True iff the dataset has not been deleted (PDPVerifier.DataSetDeleted)
# and the FWSS service has not been terminated (FWSS.ServiceTerminated).
# Note: PDPPaymentTerminated does NOT affect this flag; clients compare
# pdpPaymentEndEpoch to current epoch themselves.
isActive: Boolean!
status: DataSetStatus!
# Block number of next deadline, 0 if not set. Dealbot filters on this
# to detect overdue proving periods.
nextDeadline: BigInt!
# Multiplier for proving period frequency, 0 if not set.
maxProvingPeriod: BigInt!
# Internal: flipped true by PossessionProven, reset false on each
# NextProvingPeriod. Drives the faultedPeriods counter on Provider;
# not directly queried by dealbot.
provenThisPeriod: Boolean!

# ---- FWSS fields (null / false for non-FWSS datasets) ----
# Populated by FWSS.DataSetCreated handler.
fwssPayer: Bytes # address of the payer
fwssServiceProvider: Bytes # address — may diverge from owner after transfers

# Derived from FWSS.DataSetCreated metadataKeys.
withIPFSIndexing: Boolean!

# Populated by FWSS.PDPPaymentTerminated handler.
# May be in the past (already terminated) or future (terminating).
# Does NOT flip isActive — clients compare to current epoch.
pdpPaymentEndEpoch: BigInt

# Block timestamp at which this DataSet was first created. Set once on
# the creating event (whichever of FWSS.DataSetCreated or
# PDPVerifier.DataSetCreated fires first) and never updated.
createdAt: BigInt!

# Derived relationship
roots: [Root!]! @derivedFrom(field: "proofSet")
}

type Root @entity(immutable: false) {
id: Bytes! # setId-rootId
setId: BigInt!
rootId: BigInt!
rawSize: BigInt!
cid: Bytes!
removed: Boolean!

# Populated by FWSS.PieceAdded handler (null for non-FWSS pieces).
ipfsRootCID: String

# Block timestamp at which this Root was first added via PiecesAdded.
# Set once on creation and never updated.
createdAt: BigInt!

# keccak256(id) computed once at insert time. Used by dealbot's anonymous
# retrieval check to draw a uniform-random piece via
# `orderBy: sampleKey, where: { sampleKey_gte: <random> }, first: 1`
# — independent of creation order, dataset age, and corpus size.
sampleKey: Bytes!

proofSet: DataSet!
}
27 changes: 27 additions & 0 deletions apps/subgraph/scripts/sync-abis.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env node
// Writes contract ABIs consumed by graph-cli from the canonical
// @filoz/synapse-core package into apps/subgraph/abis/*.json. Running this
// before `graph codegen` keeps the subgraph in lock-step with the source of
// truth; bumping the synapse-core version is all that's needed to pick up
// ABI changes.

import { writeFile, mkdir } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { fwss, pdp } from "@filoz/synapse-core/abis";

const here = dirname(fileURLToPath(import.meta.url));
const abisDir = join(here, "..", "abis");

const targets = [
{ file: "PDPVerifier.json", abi: pdp },
{ file: "FilecoinWarmStorageService.json", abi: fwss },
];

await mkdir(abisDir, { recursive: true });

for (const { file, abi } of targets) {
const outPath = join(abisDir, file);
await writeFile(outPath, `${JSON.stringify(abi, null, 2)}\n`);
console.log(`wrote ${outPath} (${abi.length} entries)`);
}
91 changes: 91 additions & 0 deletions apps/subgraph/src/fwss.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { BigInt, log } from "@graphprotocol/graph-ts";
import {
DataSetCreated as DataSetCreatedEvent,
DataSetServiceProviderChanged as DataSetServiceProviderChangedEvent,
PDPPaymentTerminated as PDPPaymentTerminatedEvent,
PieceAdded as PieceAddedEvent,
ServiceTerminated as ServiceTerminatedEvent,
} from "../generated/FilecoinWarmStorageService/FilecoinWarmStorageService";
import { DataSet, Root } from "../generated/schema";
import { arrayContains, extractMetadataValue, getProofSetEntityId, getRootEntityId } from "./helpers";
import { DataSetStatus } from "./types";

// ---- Handlers -------------------------------------------------------------

export function handleFwssDataSetCreated(event: DataSetCreatedEvent): void {
const id = getProofSetEntityId(event.params.dataSetId);
// FWSS.DataSetCreated fires BEFORE PDPVerifier's own DataSetCreated event
// (see PDPVerifier._createDataSet: listener callback runs first, THEN
// `emit DataSetCreated`). If no entity exists yet, create a stub with
// required defaults; pdp-verifier.handleDataSetCreated will run later in
// the same block and fill in PDPVerifier-level fields. Since handlers run
// sequentially and atomically within a block, no GraphQL query can observe
// that intermediate state.
let ds = DataSet.load(id);
if (ds == null) {
ds = new DataSet(id);
ds.setId = event.params.dataSetId;
// PDPVerifier-level non-null defaults; handleDataSetCreated will overwrite.
ds.owner = event.params.serviceProvider;
ds.isActive = true;
ds.status = DataSetStatus.EMPTY;
ds.nextDeadline = BigInt.zero();
ds.maxProvingPeriod = BigInt.zero();
ds.provenThisPeriod = false;
ds.createdAt = event.block.timestamp;
}

ds.fwssPayer = event.params.payer;
ds.fwssServiceProvider = event.params.serviceProvider;
ds.withIPFSIndexing = arrayContains(event.params.metadataKeys, "withIPFSIndexing");
ds.save();
}

export function handleFwssPieceAdded(event: PieceAddedEvent): void {
const root = Root.load(getRootEntityId(event.params.dataSetId, event.params.pieceId));
if (root == null) {
log.warning("FWSS PieceAdded for unknown root {}-{}", [
event.params.dataSetId.toString(),
event.params.pieceId.toString(),
]);
return;
}

root.ipfsRootCID = extractMetadataValue(event.params.keys, event.params.values, "ipfsRootCID");
root.save();
}

export function handleFwssServiceTerminated(event: ServiceTerminatedEvent): void {
const ds = DataSet.load(getProofSetEntityId(event.params.dataSetId));
if (ds == null) {
log.warning("FWSS ServiceTerminated for unknown dataSet {}", [event.params.dataSetId.toString()]);
return;
}

ds.isActive = false;
ds.save();
}

export function handleFwssPdpPaymentTerminated(event: PDPPaymentTerminatedEvent): void {
const ds = DataSet.load(getProofSetEntityId(event.params.dataSetId));
if (ds == null) {
log.warning("FWSS PDPPaymentTerminated for unknown dataSet {}", [event.params.dataSetId.toString()]);
return;
}

ds.pdpPaymentEndEpoch = event.params.endEpoch;
ds.save();
}

export function handleFwssDataSetServiceProviderChanged(event: DataSetServiceProviderChangedEvent): void {
const ds = DataSet.load(getProofSetEntityId(event.params.dataSetId));
if (ds == null) {
log.warning("FWSS DataSetServiceProviderChanged for unknown dataSet {}", [
event.params.dataSetId.toString(),
]);
return;
}

ds.fwssServiceProvider = event.params.newServiceProvider;
ds.save();
}
Loading