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: 1 addition & 2 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@
"Bash(npx tsx:*)",
"Bash(cast sig:*)",
"Bash(npx tsc:*)",
"Bash(npm run build:*)",
"Bash(npm run test:*)"
"Bash(pnpm exec tsc:*)"
]
}
}
40 changes: 40 additions & 0 deletions packages/subgraph/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Graph Horizon Network Subgraph

Indexes The Graph's Horizon protocol core contracts: `HorizonStaking`, `GraphPayments` and `PaymentsEscrow`.

The Graph Horizon Network subgraph is an **aggregate state subgraph** that tracks current state (balances, counts, parameters) for:

- **Service provider stake and provisions**: Token balances, provision parameters, and thawing state
- **Delegation pools**: Pool-level token balances, shares, and thawing tokens
- **Thaw requests**: Active deprovisioning requests
- **Payment collections and escrow**: Escrow balances and collector discovery
- **Operators**: Operator authorizations per service provider

Entities are updated in place as events occur. Data services and payment collectors are discovered dynamically via staking and escrow events, but are tracked generically without service-specific or collector-specific details.

## What this subgraph is NOT

- **Not historical**: This subgraph does not track event-by-event history. Individual stake deposits, slashes, or delegation details are not recorded.

- **Not data-service specific**: Data services are indexed but no service-specific parameters are tracked. Each data service (Subgraph Service, etc.) should create their own subgraph that suits their own needs.

- **Not collector specific**: Payment collectors are discovered but collector-specific logic like signer authorizations is not tracked. Collector-specific subgraphs should handle these details.

- **Not delegator-level**: Individual Delegator and Delegation entities are not included. Only pool-level aggregates (DelegationPool) are tracked.

## What data is indexed?

This subgraph begins indexing at **Horizon genesis**, not protocol genesis. The Graph protocol existed before the Horizon upgrade, so pre-existing state that is relevant such as stake or delegations needs to be backfilled to have a correct view of the network state.

At the Horizon genesis block, a one-time migration seeds entities by reading current contract state. The list of entities to migrate is generated by querying the legacy network subgraph at the Horizon genesis block (see `packages/tools`), these include:

- **Service providers**: Stake balances are read from the `HorizonStaking` contract for all service providers with `staked tokens > 0` at Graph Horizon genesis. All pre-Horizon activity (deposits, thaws, rewards, slashings) is aggregated into a single stake snapshot regardless of it's source: direct stake deposit, indexing rewards or query fee payments.

- **Delegation pools**: Pool state (tokens, shares, tokensThawing) is read from the contract for all service providers with `delegated tokens > 0` at Graph Horizon genesis.

### Important caveats

1. **Migrated data**: As described before, the migration strategy "flattens" pre-Horizon history into snapshots. Cumulative fields that represent current state include aggregated pre-Horizon values, but event-driven fields (e.g., query fees collected) only track activity from Horizon onwards. For example:
- `tokensStaked` includes stake deposits/thaws made before/after Horizon. This is expected. However it also aggregates all rewards and query fees that were direclty deposited into the service provider's stake before Horizon, that stake is also counted towards `tokensStaked` as it is the "starting stake" at Horizon genesis.
- `tokensCollected` tracks payments (indexing rewards and query fees) only after Horizon. Pre-horizon data for collections is not available in this subgraph.
2. **Legacy allocations**: During Horizon's transition period legacy allocations were allowed to exist but forced to be eventually closed. To simplify the subgraph this is not being tracked. The consequence is that stake balances in the subgraph will be incorrect for service providers untill all their legacy allocations are closed down. For most service providers this is ~30 days after Horizon genesis (Dec 9th 2025).
38 changes: 38 additions & 0 deletions packages/subgraph/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ type GraphNetwork @entity(immutable: false) {
countProvisions: Int!
"Active delegation pools"
countDelegationPools: Int!
"Provision slash events"
countProvisionSlashEvents: Int!
"Delegation pool slash events"
countDelegationPoolSlashEvents: Int!

# Stake aggregates
"Total tokens staked by service providers"
Expand All @@ -17,21 +21,55 @@ type GraphNetwork @entity(immutable: false) {
tokensProvisioned: BigInt!
"Total tokens delegated to service providers"
tokensDelegated: BigInt!
"Total tokens currently thawing from provisions"
tokensThawingFromProvisions: BigInt!
"Total tokens currently thawing from delegation pools"
tokensThawingFromDelegationPools: BigInt!

# Slashing aggregates
"Total tokens slashed"
tokensSlashed: BigInt!
"Total tokens slashed from provisions"
tokensSlashedFromProvisions: BigInt!
"Total tokens slashed from delegation pools"
tokensSlashedFromDelegationPools: BigInt!
}

type ServiceProvider @entity(immutable: false) {
"Service provider address"
id: Bytes!

# Counts
"Number of active provisions"
countProvisions: Int!
"Provision slash events"
countProvisionSlashEvents: Int!
"Delegation pool slash events"
countDelegationPoolSlashEvents: Int!

# Stake
"Tokens staked by the service provider"
tokensStaked: BigInt!
"Tokens provisioned to data services"
tokensProvisioned: BigInt!
"Tokens that are not locked in provisions"
tokensIdle: BigInt!
"Tokens currently thawing from provisions"
tokensThawing: BigInt!

# Delegation
"Tokens delegated to this service provider"
tokensDelegated: BigInt!
"Tokens currently thawing from delegation pools"
tokensDelegatedThawing: BigInt!

# Slashing
"Total tokens slashed"
tokensSlashed: BigInt!
"Tokens slashed from provisions"
tokensSlashedFromProvisions: BigInt!
"Tokens slashed from delegation pools"
tokensSlashedFromDelegationPools: BigInt!

# Provisions
"Provisions created by this service provider"
Expand Down
13 changes: 13 additions & 0 deletions packages/subgraph/src/entities/graphNetwork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,25 @@ export function getOrCreateGraphNetwork(): GraphNetwork {
let entity = GraphNetwork.load(GRAPH_NETWORK_ID)
if (entity == null) {
entity = new GraphNetwork(GRAPH_NETWORK_ID)

// Counts
entity.countServiceProviders = 0
entity.countProvisions = 0
entity.countDelegationPools = 0
entity.countProvisionSlashEvents = 0
entity.countDelegationPoolSlashEvents = 0

// Stake aggregates
entity.tokensStaked = BIGINT_ZERO
entity.tokensProvisioned = BIGINT_ZERO
entity.tokensDelegated = BIGINT_ZERO
entity.tokensThawingFromProvisions = BIGINT_ZERO
entity.tokensThawingFromDelegationPools = BIGINT_ZERO

// Slashing aggregates
entity.tokensSlashed = BIGINT_ZERO
entity.tokensSlashedFromProvisions = BIGINT_ZERO
entity.tokensSlashedFromDelegationPools = BIGINT_ZERO
}
return entity
}
Expand Down
18 changes: 18 additions & 0 deletions packages/subgraph/src/entities/serviceProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,28 @@ export function getOrCreateServiceProvider(

if (entity == null) {
entity = new ServiceProvider(id)

// Counts
entity.countProvisions = 0
entity.countProvisionSlashEvents = 0
entity.countDelegationPoolSlashEvents = 0

// Stake
entity.tokensStaked = BIGINT_ZERO
entity.tokensProvisioned = BIGINT_ZERO
entity.tokensIdle = BIGINT_ZERO
entity.tokensThawing = BIGINT_ZERO

// Delegation
entity.tokensDelegated = BIGINT_ZERO
entity.tokensDelegatedThawing = BIGINT_ZERO

// Slashing
entity.tokensSlashed = BIGINT_ZERO
entity.tokensSlashedFromProvisions = BIGINT_ZERO
entity.tokensSlashedFromDelegationPools = BIGINT_ZERO

// Metadata
entity.createdAtBlock = blockNumber
entity.createdAt = timestamp
entity.updatedAtBlock = blockNumber
Expand Down
37 changes: 35 additions & 2 deletions packages/subgraph/src/handlers/delegation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import { getOrCreateGraphNetwork, saveGraphNetwork } from "../entities/graphNetwork"
import { getOrCreateServiceProvider, saveServiceProvider } from "../entities/serviceProvider"
import { getOrCreateDelegationPool, saveDelegationPool } from "../entities/delegationPool"
import { BIGINT_ZERO } from "../common/constants"

/**
* Handles TokensDelegated event.
Expand Down Expand Up @@ -36,6 +37,7 @@ export function handleTokensDelegated(event: TokensDelegated): void {

// Update ServiceProvider
let serviceProvider = getOrCreateServiceProvider(serviceProviderBytes, event.block.number, event.block.timestamp)
assert(!serviceProvider.isNew, "Service provider does not exist.")
serviceProvider.entity.tokensDelegated = serviceProvider.entity.tokensDelegated.plus(tokens)
saveServiceProvider(serviceProvider.entity, event.block)

Expand All @@ -62,17 +64,28 @@ export function handleTokensUndelegated(event: TokensUndelegated): void {
let verifierBytes = Bytes.fromHexString(verifier.toHexString()) as Bytes

// Update DelegationPool - shares burned, tokens start thawing
// Note: sharesThawing not tracked here - it's a calculated value in the contract
// that differs from the delegation shares being burned
let pool = getOrCreateDelegationPool(
serviceProviderBytes,
verifierBytes,
event.block.number,
event.block.timestamp
)
assert(!pool.isNew, "Delegation pool does not exist.")
assert(pool.entity.shares >= shares, "Undelegated shares exceed pool shares.")
pool.entity.shares = pool.entity.shares.minus(shares)
pool.entity.tokensThawing = pool.entity.tokensThawing.plus(tokens)
saveDelegationPool(pool.entity, event.block)

// Update ServiceProvider
let serviceProvider = getOrCreateServiceProvider(serviceProviderBytes, event.block.number, event.block.timestamp)
assert(!serviceProvider.isNew, "Service provider does not exist.")
serviceProvider.entity.tokensDelegatedThawing = serviceProvider.entity.tokensDelegatedThawing.plus(tokens)
saveServiceProvider(serviceProvider.entity, event.block)

// Update GraphNetwork
let graphNetwork = getOrCreateGraphNetwork()
graphNetwork.tokensThawingFromDelegationPools = graphNetwork.tokensThawingFromDelegationPools.plus(tokens)
saveGraphNetwork(graphNetwork)
}

/**
Expand All @@ -94,6 +107,7 @@ export function handleDelegatedTokensWithdrawn(event: DelegatedTokensWithdrawn):
event.block.number,
event.block.timestamp
)
assert(!pool.isNew, "Delegation pool does not exist.")
assert(pool.entity.tokens >= tokens, "Withdraw tokens exceed pool tokens.")
pool.entity.tokens = pool.entity.tokens.minus(tokens)
assert(pool.entity.tokensThawing >= tokens, "Withdraw tokens exceed pool thawing tokens.")
Expand All @@ -102,14 +116,23 @@ export function handleDelegatedTokensWithdrawn(event: DelegatedTokensWithdrawn):

// Update ServiceProvider
let serviceProvider = getOrCreateServiceProvider(serviceProviderBytes, event.block.number, event.block.timestamp)
assert(!serviceProvider.isNew, "Service provider does not exist.")
assert(serviceProvider.entity.tokensDelegatedThawing >= tokens, "Withdraw tokens exceed service provider delegated tokens thawing.")
serviceProvider.entity.tokensDelegatedThawing = serviceProvider.entity.tokensDelegatedThawing.minus(tokens)
assert(serviceProvider.entity.tokensDelegated >= tokens, "Withdraw tokens exceed service provider delegated tokens.")
serviceProvider.entity.tokensDelegated = serviceProvider.entity.tokensDelegated.minus(tokens)
saveServiceProvider(serviceProvider.entity, event.block)

// Update GraphNetwork
let graphNetwork = getOrCreateGraphNetwork()
assert(graphNetwork.tokensThawingFromDelegationPools >= tokens, "Withdraw tokens exceed network tokens thawing from delegation pools.")
graphNetwork.tokensThawingFromDelegationPools = graphNetwork.tokensThawingFromDelegationPools.minus(tokens)
assert(graphNetwork.tokensDelegated >= tokens, "Withdraw tokens exceed network tokens delegated.")
graphNetwork.tokensDelegated = graphNetwork.tokensDelegated.minus(tokens)
if (pool.entity.tokens.equals(BIGINT_ZERO)) {
assert(graphNetwork.countDelegationPools > 0, "Delegation pool count is zero.")
graphNetwork.countDelegationPools -= 1
}
saveGraphNetwork(graphNetwork)
}

Expand All @@ -132,20 +155,28 @@ export function handleDelegationSlashed(event: DelegationSlashed): void {
event.block.number,
event.block.timestamp
)
assert(!pool.isNew, "Delegation pool does not exist.")
assert(pool.entity.tokens >= tokens, "Slash tokens exceed pool tokens.")
pool.entity.tokens = pool.entity.tokens.minus(tokens)
saveDelegationPool(pool.entity, event.block)

// Update ServiceProvider
let serviceProvider = getOrCreateServiceProvider(serviceProviderBytes, event.block.number, event.block.timestamp)
assert(!serviceProvider.isNew, "Service provider does not exist.")
assert(serviceProvider.entity.tokensDelegated >= tokens, "Slash tokens exceed service provider delegated tokens.")
serviceProvider.entity.tokensDelegated = serviceProvider.entity.tokensDelegated.minus(tokens)
serviceProvider.entity.countDelegationPoolSlashEvents += 1
serviceProvider.entity.tokensSlashed = serviceProvider.entity.tokensSlashed.plus(tokens)
serviceProvider.entity.tokensSlashedFromDelegationPools = serviceProvider.entity.tokensSlashedFromDelegationPools.plus(tokens)
saveServiceProvider(serviceProvider.entity, event.block)

// Update GraphNetwork
let graphNetwork = getOrCreateGraphNetwork()
assert(graphNetwork.tokensDelegated >= tokens, "Slash tokens exceed network tokens delegated.")
graphNetwork.tokensDelegated = graphNetwork.tokensDelegated.minus(tokens)
graphNetwork.countDelegationPoolSlashEvents += 1
graphNetwork.tokensSlashed = graphNetwork.tokensSlashed.plus(tokens)
graphNetwork.tokensSlashedFromDelegationPools = graphNetwork.tokensSlashedFromDelegationPools.plus(tokens)
saveGraphNetwork(graphNetwork)
}

Expand All @@ -168,11 +199,13 @@ export function handleTokensToDelegationPoolAdded(event: TokensToDelegationPoolA
event.block.number,
event.block.timestamp
)
assert(!pool.isNew, "Delegation pool does not exist.")
pool.entity.tokens = pool.entity.tokens.plus(tokens)
saveDelegationPool(pool.entity, event.block)

// Update ServiceProvider
let serviceProvider = getOrCreateServiceProvider(serviceProviderBytes, event.block.number, event.block.timestamp)
assert(!serviceProvider.isNew, "Service provider does not exist.")
serviceProvider.entity.tokensDelegated = serviceProvider.entity.tokensDelegated.plus(tokens)
saveServiceProvider(serviceProvider.entity, event.block)

Expand Down
4 changes: 0 additions & 4 deletions packages/subgraph/src/handlers/migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,6 @@ export function migrateDelegationPools(block: ethereum.Block, networkConfig: Net
serviceProvider.entity.tokensDelegated = serviceProvider.entity.tokensDelegated.plus(poolTokens)
saveServiceProvider(serviceProvider.entity, block)

// Update graph network
if (serviceProvider.isNew) {
graphNetwork.countServiceProviders += 1
}
graphNetwork.countDelegationPools += 1
graphNetwork.tokensDelegated = graphNetwork.tokensDelegated.plus(poolTokens)
}
Expand Down
28 changes: 27 additions & 1 deletion packages/subgraph/src/handlers/provision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export function handleProvisionCreated(event: ProvisionCreated): void {

// ServiceProvider
assert(!serviceProvider.isNew, "Service provider does not exist.")
serviceProvider.entity.countProvisions += 1
serviceProvider.entity.tokensProvisioned = serviceProvider.entity.tokensProvisioned.plus(event.params.tokens)
assert(serviceProvider.entity.tokensStaked >= serviceProvider.entity.tokensProvisioned, "Provisioned tokens exceed staked tokens.")
serviceProvider.entity.tokensIdle = serviceProvider.entity.tokensStaked.minus(serviceProvider.entity.tokensProvisioned)
Expand Down Expand Up @@ -86,9 +87,15 @@ export function handleProvisionIncreased(event: ProvisionIncreased): void {

/**
* Emitted when tokens begin thawing from a provision.
* Note: Thawing tokens are still considered "provisioned" .
* Note: Thawing tokens are still considered "provisioned".
*/
export function handleProvisionThawed(event: ProvisionThawed): void {
let graphNetwork = getOrCreateGraphNetwork()
let serviceProvider = getOrCreateServiceProvider(
event.params.serviceProvider,
event.block.number,
event.block.timestamp
)
let provision = getOrCreateProvision(
event.params.serviceProvider,
event.params.verifier,
Expand All @@ -102,6 +109,15 @@ export function handleProvisionThawed(event: ProvisionThawed): void {
provision.entity.tokens = provision.entity.tokens.minus(event.params.tokens)
provision.entity.tokensThawing = provision.entity.tokensThawing.plus(event.params.tokens)
saveProvision(provision.entity, event.block)

// ServiceProvider
assert(!serviceProvider.isNew, "Service provider does not exist.")
serviceProvider.entity.tokensThawing = serviceProvider.entity.tokensThawing.plus(event.params.tokens)
saveServiceProvider(serviceProvider.entity, event.block)

// GraphNetwork
graphNetwork.tokensThawingFromProvisions = graphNetwork.tokensThawingFromProvisions.plus(event.params.tokens)
saveGraphNetwork(graphNetwork)
}

/**
Expand Down Expand Up @@ -129,13 +145,17 @@ export function handleTokensDeprovisioned(event: TokensDeprovisioned): void {

// ServiceProvider
assert(!serviceProvider.isNew, "Service provider does not exist.")
assert(serviceProvider.entity.tokensThawing >= event.params.tokens, "Deprovision exceeds service provider tokens thawing.")
serviceProvider.entity.tokensThawing = serviceProvider.entity.tokensThawing.minus(event.params.tokens)
assert(serviceProvider.entity.tokensProvisioned >= event.params.tokens, "Deprovision exceeds service provider tokens provisioned.")
serviceProvider.entity.tokensProvisioned = serviceProvider.entity.tokensProvisioned.minus(event.params.tokens)
assert(serviceProvider.entity.tokensStaked >= serviceProvider.entity.tokensProvisioned, "Provisioned tokens exceed staked tokens.")
serviceProvider.entity.tokensIdle = serviceProvider.entity.tokensStaked.minus(serviceProvider.entity.tokensProvisioned)
saveServiceProvider(serviceProvider.entity, event.block)

// GraphNetwork
assert(graphNetwork.tokensThawingFromProvisions >= event.params.tokens, "Deprovision exceeds network tokens thawing from provisions.")
graphNetwork.tokensThawingFromProvisions = graphNetwork.tokensThawingFromProvisions.minus(event.params.tokens)
assert(graphNetwork.tokensProvisioned >= event.params.tokens, "Deprovision exceeds network tokens provisioned.")
graphNetwork.tokensProvisioned = graphNetwork.tokensProvisioned.minus(event.params.tokens)
saveGraphNetwork(graphNetwork)
Expand Down Expand Up @@ -172,13 +192,19 @@ export function handleProvisionSlashed(event: ProvisionSlashed): void {
serviceProvider.entity.tokensProvisioned = serviceProvider.entity.tokensProvisioned.minus(event.params.tokens)
assert(serviceProvider.entity.tokensStaked >= serviceProvider.entity.tokensProvisioned, "Provisioned tokens exceed staked tokens.")
serviceProvider.entity.tokensIdle = serviceProvider.entity.tokensStaked.minus(serviceProvider.entity.tokensProvisioned)
serviceProvider.entity.countProvisionSlashEvents += 1
serviceProvider.entity.tokensSlashed = serviceProvider.entity.tokensSlashed.plus(event.params.tokens)
serviceProvider.entity.tokensSlashedFromProvisions = serviceProvider.entity.tokensSlashedFromProvisions.plus(event.params.tokens)
saveServiceProvider(serviceProvider.entity, event.block)

// GraphNetwork
assert(graphNetwork.tokensStaked >= event.params.tokens, "Slash exceeds network tokens staked.")
assert(graphNetwork.tokensProvisioned >= event.params.tokens, "Slash exceeds network tokens provisioned.")
graphNetwork.tokensStaked = graphNetwork.tokensStaked.minus(event.params.tokens)
graphNetwork.tokensProvisioned = graphNetwork.tokensProvisioned.minus(event.params.tokens)
graphNetwork.countProvisionSlashEvents += 1
graphNetwork.tokensSlashed = graphNetwork.tokensSlashed.plus(event.params.tokens)
graphNetwork.tokensSlashedFromProvisions = graphNetwork.tokensSlashedFromProvisions.plus(event.params.tokens)
saveGraphNetwork(graphNetwork)
}

Expand Down
Loading
Loading