diff --git a/packages/subgraph/schema.graphql b/packages/subgraph/schema.graphql index 12526b9..30a0d3e 100644 --- a/packages/subgraph/schema.graphql +++ b/packages/subgraph/schema.graphql @@ -5,6 +5,8 @@ type GraphNetwork @entity(immutable: false) { # Counts "Active service providers" countServiceProviders: Int! + "Active data services (verifiers)" + countDataServices: Int! "Active provisions" countProvisions: Int! "Active delegation pools" @@ -90,15 +92,66 @@ type ServiceProvider @entity(immutable: false) { updatedAt: BigInt! } +type DataService @entity(immutable: false) { + "Data service (verifier) contract address" + id: Bytes! + + # Relationships + "Provisions for this data service" + provisions: [Provision!]! @derivedFrom(field: "dataService") + "Delegation pools for this data service" + delegationPools: [DelegationPool!]! @derivedFrom(field: "dataService") + + # Counts + "Active service providers with provisions to this data service" + countServiceProviders: Int! + "Active provisions" + countProvisions: Int! + "Active delegation pools" + countDelegationPools: Int! + "Provision slash events" + countProvisionSlashEvents: Int! + "Delegation pool slash events" + countDelegationPoolSlashEvents: Int! + + # Tokens + "Total tokens provisioned to this data service" + tokensProvisioned: BigInt! + "Total tokens delegated to service providers for this data service" + tokensDelegated: BigInt! + "Tokens currently thawing from provisions" + tokensThawingFromProvisions: BigInt! + "Tokens currently thawing from delegation pools" + tokensThawingFromDelegationPools: BigInt! + + # Slashing + "Total tokens slashed" + tokensSlashed: BigInt! + "Tokens slashed from provisions" + tokensSlashedFromProvisions: BigInt! + "Tokens slashed from delegation pools" + tokensSlashedFromDelegationPools: BigInt! + + # Metadata + "Block number when entity was created" + createdAtBlock: BigInt! + "Timestamp when entity was created" + createdAt: BigInt! + "Block number when entity was last updated" + updatedAtBlock: BigInt! + "Timestamp when entity was last updated" + updatedAt: BigInt! +} + type DelegationPool @entity(immutable: false) { - "Composite ID: serviceProvider-verifier" + "Composite ID: serviceProvider-dataService" id: Bytes! # Relationships "Service provider that owns this pool" serviceProvider: ServiceProvider! - "Verifier address (data service)" - verifier: Bytes! + "Data service (verifier)" + dataService: DataService! # Pool state "Total tokens in the pool" @@ -124,14 +177,14 @@ type DelegationPool @entity(immutable: false) { } type Provision @entity(immutable: false) { - "Composite ID: serviceProvider-verifier" + "Composite ID: serviceProvider-dataService" id: Bytes! # Relationships "Service provider that created this provision" serviceProvider: ServiceProvider! - "Verifier address (data service)" - verifier: Bytes! + "Data service (verifier)" + dataService: DataService! # Tokens "Tokens currently provisioned" diff --git a/packages/subgraph/src/entities/dataService.ts b/packages/subgraph/src/entities/dataService.ts new file mode 100644 index 0000000..84c3240 --- /dev/null +++ b/packages/subgraph/src/entities/dataService.ts @@ -0,0 +1,58 @@ +import { BigInt, Bytes, ethereum } from "@graphprotocol/graph-ts" +import { DataService } from "../../generated/schema" +import { BIGINT_ZERO } from "../common/constants" + +export class DataServiceResult { + entity: DataService + isNew: boolean + + constructor(entity: DataService, isNew: boolean) { + this.entity = entity + this.isNew = isNew + } +} + +export function getOrCreateDataService( + id: Bytes, + blockNumber: BigInt, + timestamp: BigInt +): DataServiceResult { + let entity = DataService.load(id) + let isNew = entity == null + + if (entity == null) { + entity = new DataService(id) + + // Counts + entity.countServiceProviders = 0 + entity.countProvisions = 0 + entity.countDelegationPools = 0 + entity.countProvisionSlashEvents = 0 + entity.countDelegationPoolSlashEvents = 0 + + // Tokens + entity.tokensProvisioned = BIGINT_ZERO + entity.tokensDelegated = BIGINT_ZERO + entity.tokensThawingFromProvisions = BIGINT_ZERO + entity.tokensThawingFromDelegationPools = 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 + entity.updatedAt = timestamp + } + + return new DataServiceResult(entity, isNew) +} + +export function saveDataService(ds: DataService, block: ethereum.Block): void { + ds.updatedAtBlock = block.number + ds.updatedAt = block.timestamp + ds.save() +} diff --git a/packages/subgraph/src/entities/delegationPool.ts b/packages/subgraph/src/entities/delegationPool.ts index b21a726..ae9cf33 100644 --- a/packages/subgraph/src/entities/delegationPool.ts +++ b/packages/subgraph/src/entities/delegationPool.ts @@ -2,8 +2,8 @@ import { BigInt, Bytes, ethereum } from "@graphprotocol/graph-ts" import { DelegationPool } from "../../generated/schema" import { BIGINT_ZERO } from "../common/constants" -export function getDelegationPoolId(serviceProvider: Bytes, verifier: Bytes): Bytes { - return serviceProvider.concat(verifier) +export function getDelegationPoolId(serviceProvider: Bytes, dataService: Bytes): Bytes { + return serviceProvider.concat(dataService) } export class DelegationPoolResult { @@ -23,18 +23,18 @@ export class DelegationPoolResult { */ export function getOrCreateDelegationPool( serviceProvider: Bytes, - verifier: Bytes, + dataService: Bytes, blockNumber: BigInt, timestamp: BigInt ): DelegationPoolResult { - let id = getDelegationPoolId(serviceProvider, verifier) + let id = getDelegationPoolId(serviceProvider, dataService) let entity = DelegationPool.load(id) let isNew = entity == null if (entity == null) { entity = new DelegationPool(id) entity.serviceProvider = serviceProvider - entity.verifier = verifier + entity.dataService = dataService entity.tokens = BIGINT_ZERO entity.shares = BIGINT_ZERO entity.tokensThawing = BIGINT_ZERO diff --git a/packages/subgraph/src/entities/graphNetwork.ts b/packages/subgraph/src/entities/graphNetwork.ts index 49a05b9..c940994 100644 --- a/packages/subgraph/src/entities/graphNetwork.ts +++ b/packages/subgraph/src/entities/graphNetwork.ts @@ -8,6 +8,7 @@ export function getOrCreateGraphNetwork(): GraphNetwork { // Counts entity.countServiceProviders = 0 + entity.countDataServices = 0 entity.countProvisions = 0 entity.countDelegationPools = 0 entity.countProvisionSlashEvents = 0 diff --git a/packages/subgraph/src/entities/provision.ts b/packages/subgraph/src/entities/provision.ts index 1b248a6..0c48deb 100644 --- a/packages/subgraph/src/entities/provision.ts +++ b/packages/subgraph/src/entities/provision.ts @@ -2,8 +2,8 @@ import { BigInt, Bytes, ethereum } from "@graphprotocol/graph-ts" import { Provision } from "../../generated/schema" import { BIGINT_ZERO } from "../common/constants" -export function getProvisionId(serviceProvider: Bytes, verifier: Bytes): Bytes { - return serviceProvider.concat(verifier) +export function getProvisionId(serviceProvider: Bytes, dataService: Bytes): Bytes { + return serviceProvider.concat(dataService) } export class ProvisionResult { @@ -18,18 +18,18 @@ export class ProvisionResult { export function getOrCreateProvision( serviceProvider: Bytes, - verifier: Bytes, + dataService: Bytes, blockNumber: BigInt, timestamp: BigInt ): ProvisionResult { - let id = getProvisionId(serviceProvider, verifier) + let id = getProvisionId(serviceProvider, dataService) let entity = Provision.load(id) let isNew = entity == null if (entity == null) { entity = new Provision(id) entity.serviceProvider = serviceProvider - entity.verifier = verifier + entity.dataService = dataService entity.tokens = BIGINT_ZERO entity.tokensThawing = BIGINT_ZERO entity.maxVerifierCut = BIGINT_ZERO diff --git a/packages/subgraph/src/handlers/delegation.ts b/packages/subgraph/src/handlers/delegation.ts index 9956e1e..136728a 100644 --- a/packages/subgraph/src/handlers/delegation.ts +++ b/packages/subgraph/src/handlers/delegation.ts @@ -8,6 +8,7 @@ import { } from "../../generated/HorizonStaking/HorizonStaking" import { getOrCreateGraphNetwork, saveGraphNetwork } from "../entities/graphNetwork" import { getOrCreateServiceProvider, saveServiceProvider } from "../entities/serviceProvider" +import { getOrCreateDataService, saveDataService } from "../entities/dataService" import { getOrCreateDelegationPool, saveDelegationPool } from "../entities/delegationPool" import { BIGINT_ZERO } from "../common/constants" @@ -35,6 +36,15 @@ export function handleTokensDelegated(event: TokensDelegated): void { pool.entity.shares = pool.entity.shares.plus(shares) saveDelegationPool(pool.entity, event.block) + // Update DataService + let dataService = getOrCreateDataService(verifierBytes, event.block.number, event.block.timestamp) + assert(!dataService.isNew, "Data service does not exist.") + dataService.entity.tokensDelegated = dataService.entity.tokensDelegated.plus(tokens) + if (pool.isNew) { + dataService.entity.countDelegationPools += 1 + } + saveDataService(dataService.entity, event.block) + // Update ServiceProvider let serviceProvider = getOrCreateServiceProvider(serviceProviderBytes, event.block.number, event.block.timestamp) assert(!serviceProvider.isNew, "Service provider does not exist.") @@ -76,6 +86,12 @@ export function handleTokensUndelegated(event: TokensUndelegated): void { pool.entity.tokensThawing = pool.entity.tokensThawing.plus(tokens) saveDelegationPool(pool.entity, event.block) + // Update DataService + let dataService = getOrCreateDataService(verifierBytes, event.block.number, event.block.timestamp) + assert(!dataService.isNew, "Data service does not exist.") + dataService.entity.tokensThawingFromDelegationPools = dataService.entity.tokensThawingFromDelegationPools.plus(tokens) + saveDataService(dataService.entity, event.block) + // Update ServiceProvider let serviceProvider = getOrCreateServiceProvider(serviceProviderBytes, event.block.number, event.block.timestamp) assert(!serviceProvider.isNew, "Service provider does not exist.") @@ -114,6 +130,19 @@ export function handleDelegatedTokensWithdrawn(event: DelegatedTokensWithdrawn): pool.entity.tokensThawing = pool.entity.tokensThawing.minus(tokens) saveDelegationPool(pool.entity, event.block) + // Update DataService + let dataService = getOrCreateDataService(verifierBytes, event.block.number, event.block.timestamp) + assert(!dataService.isNew, "Data service does not exist.") + assert(dataService.entity.tokensThawingFromDelegationPools >= tokens, "Withdraw tokens exceed data service tokens thawing.") + dataService.entity.tokensThawingFromDelegationPools = dataService.entity.tokensThawingFromDelegationPools.minus(tokens) + assert(dataService.entity.tokensDelegated >= tokens, "Withdraw tokens exceed data service tokens delegated.") + dataService.entity.tokensDelegated = dataService.entity.tokensDelegated.minus(tokens) + if (pool.entity.tokens.equals(BIGINT_ZERO)) { + assert(dataService.entity.countDelegationPools > 0, "Data service delegation pool count is zero.") + dataService.entity.countDelegationPools -= 1 + } + saveDataService(dataService.entity, event.block) + // Update ServiceProvider let serviceProvider = getOrCreateServiceProvider(serviceProviderBytes, event.block.number, event.block.timestamp) assert(!serviceProvider.isNew, "Service provider does not exist.") @@ -160,6 +189,16 @@ export function handleDelegationSlashed(event: DelegationSlashed): void { pool.entity.tokens = pool.entity.tokens.minus(tokens) saveDelegationPool(pool.entity, event.block) + // Update DataService + let dataService = getOrCreateDataService(verifierBytes, event.block.number, event.block.timestamp) + assert(!dataService.isNew, "Data service does not exist.") + assert(dataService.entity.tokensDelegated >= tokens, "Slash tokens exceed data service tokens delegated.") + dataService.entity.tokensDelegated = dataService.entity.tokensDelegated.minus(tokens) + dataService.entity.countDelegationPoolSlashEvents += 1 + dataService.entity.tokensSlashed = dataService.entity.tokensSlashed.plus(tokens) + dataService.entity.tokensSlashedFromDelegationPools = dataService.entity.tokensSlashedFromDelegationPools.plus(tokens) + saveDataService(dataService.entity, event.block) + // Update ServiceProvider let serviceProvider = getOrCreateServiceProvider(serviceProviderBytes, event.block.number, event.block.timestamp) assert(!serviceProvider.isNew, "Service provider does not exist.") @@ -203,6 +242,12 @@ export function handleTokensToDelegationPoolAdded(event: TokensToDelegationPoolA pool.entity.tokens = pool.entity.tokens.plus(tokens) saveDelegationPool(pool.entity, event.block) + // Update DataService + let dataService = getOrCreateDataService(verifierBytes, event.block.number, event.block.timestamp) + assert(!dataService.isNew, "Data service does not exist.") + dataService.entity.tokensDelegated = dataService.entity.tokensDelegated.plus(tokens) + saveDataService(dataService.entity, event.block) + // Update ServiceProvider let serviceProvider = getOrCreateServiceProvider(serviceProviderBytes, event.block.number, event.block.timestamp) assert(!serviceProvider.isNew, "Service provider does not exist.") diff --git a/packages/subgraph/src/handlers/legacy.ts b/packages/subgraph/src/handlers/legacy.ts index aad464b..43bfdec 100644 --- a/packages/subgraph/src/handlers/legacy.ts +++ b/packages/subgraph/src/handlers/legacy.ts @@ -5,6 +5,7 @@ import { } from "../../generated/HorizonStaking/HorizonStaking" import { getOrCreateGraphNetwork, saveGraphNetwork } from "../entities/graphNetwork" import { getOrCreateServiceProvider, saveServiceProvider } from "../entities/serviceProvider" +import { getOrCreateDataService, saveDataService } from "../entities/dataService" import { getOrCreateDelegationPool, saveDelegationPool } from "../entities/delegationPool" import { config } from "../config" @@ -33,12 +34,12 @@ export function handleRebateCollected(event: RebateCollected): void { } let indexerBytes = Bytes.fromHexString(indexer.toHexString()) as Bytes - let verifier = Bytes.fromHexString(config.subgraphServiceAddress.toHexString()) as Bytes + let dataServiceBytes = Bytes.fromHexString(config.subgraphServiceAddress.toHexString()) as Bytes // Update legacy DelegationPool let pool = getOrCreateDelegationPool( indexerBytes, - verifier, + dataServiceBytes, event.block.number, event.block.timestamp ) @@ -49,11 +50,15 @@ export function handleRebateCollected(event: RebateCollected): void { pool.entity.tokens = pool.entity.tokens.plus(delegationRewards) saveDelegationPool(pool.entity, event.block) + // Update DataService + let dataService = getOrCreateDataService(dataServiceBytes, event.block.number, event.block.timestamp) + assert(!dataService.isNew, "Data service does not exist.") + dataService.entity.tokensDelegated = dataService.entity.tokensDelegated.plus(delegationRewards) + saveDataService(dataService.entity, event.block) + // Update ServiceProvider let serviceProvider = getOrCreateServiceProvider(indexerBytes, event.block.number, event.block.timestamp) - if (serviceProvider.isNew) { - return - } + assert(!serviceProvider.isNew, "Service provider does not exist.") serviceProvider.entity.tokensDelegated = serviceProvider.entity.tokensDelegated.plus(delegationRewards) saveServiceProvider(serviceProvider.entity, event.block) @@ -115,12 +120,12 @@ export function handleAllocationClosed(event: AllocationClosed): void { return } - let verifier = Bytes.fromHexString(config.subgraphServiceAddress.toHexString()) as Bytes + let dataServiceBytes = Bytes.fromHexString(config.subgraphServiceAddress.toHexString()) as Bytes // Get the legacy DelegationPool to read the indexer's reward cut let pool = getOrCreateDelegationPool( indexerBytes, - verifier, + dataServiceBytes, event.block.number, event.block.timestamp ) @@ -144,12 +149,17 @@ export function handleAllocationClosed(event: AllocationClosed): void { pool.entity.tokens = pool.entity.tokens.plus(delegationRewards) saveDelegationPool(pool.entity, event.block) + // Update DataService + let dataService = getOrCreateDataService(dataServiceBytes, event.block.number, event.block.timestamp) + assert(!dataService.isNew, "Data service does not exist.") + dataService.entity.tokensDelegated = dataService.entity.tokensDelegated.plus(delegationRewards) + saveDataService(dataService.entity, event.block) + // Update ServiceProvider let serviceProvider = getOrCreateServiceProvider(indexerBytes, event.block.number, event.block.timestamp) - if (!serviceProvider.isNew) { - serviceProvider.entity.tokensDelegated = serviceProvider.entity.tokensDelegated.plus(delegationRewards) - saveServiceProvider(serviceProvider.entity, event.block) - } + assert(!serviceProvider.isNew, "Service provider does not exist.") + serviceProvider.entity.tokensDelegated = serviceProvider.entity.tokensDelegated.plus(delegationRewards) + saveServiceProvider(serviceProvider.entity, event.block) // Update GraphNetwork let graphNetwork = getOrCreateGraphNetwork() diff --git a/packages/subgraph/src/handlers/migration.ts b/packages/subgraph/src/handlers/migration.ts index 62486ef..6ae8607 100644 --- a/packages/subgraph/src/handlers/migration.ts +++ b/packages/subgraph/src/handlers/migration.ts @@ -2,6 +2,7 @@ import { ethereum, Address, log, Bytes } from "@graphprotocol/graph-ts" import { HorizonStaking } from "../../generated/HorizonStaking/HorizonStaking" import { getOrCreateGraphNetwork, saveGraphNetwork } from "../entities/graphNetwork" import { getOrCreateServiceProvider, saveServiceProvider } from "../entities/serviceProvider" +import { getOrCreateDataService, saveDataService } from "../entities/dataService" import { getOrCreateDelegationPool, saveDelegationPool } from "../entities/delegationPool" import { config } from "../config" import { NetworkConfig } from "../config/types" @@ -113,7 +114,12 @@ export function migrateDelegationPools(block: ethereum.Block, networkConfig: Net let stakingContract = HorizonStaking.bind(networkConfig.horizonStakingAddress) let verifier = networkConfig.subgraphServiceAddress + let verifierBytes = Bytes.fromHexString(verifier.toHexString()) as Bytes let graphNetwork = getOrCreateGraphNetwork() + let dataService = getOrCreateDataService(verifierBytes, block.number, block.timestamp) + if (dataService.isNew) { + graphNetwork.countDataServices += 1 + } let indexerAddresses = networkConfig.delegatedIndexerAddresses @@ -148,7 +154,6 @@ export function migrateDelegationPools(block: ethereum.Block, networkConfig: Net let poolTokens = poolData[0] let indexerBytes = Bytes.fromHexString(indexerAddress.toHexString()) as Bytes - let verifierBytes = Bytes.fromHexString(verifier.toHexString()) as Bytes // Create delegation pool let pool = getOrCreateDelegationPool(indexerBytes, verifierBytes, block.number, block.timestamp) @@ -169,10 +174,15 @@ export function migrateDelegationPools(block: ethereum.Block, networkConfig: Net serviceProvider.entity.tokensDelegated = serviceProvider.entity.tokensDelegated.plus(poolTokens) saveServiceProvider(serviceProvider.entity, block) + // Update DataService aggregates + dataService.entity.countDelegationPools += 1 + dataService.entity.tokensDelegated = dataService.entity.tokensDelegated.plus(poolTokens) + graphNetwork.countDelegationPools += 1 graphNetwork.tokensDelegated = graphNetwork.tokensDelegated.plus(poolTokens) } } + saveDataService(dataService.entity, block) saveGraphNetwork(graphNetwork) } diff --git a/packages/subgraph/src/handlers/provision.ts b/packages/subgraph/src/handlers/provision.ts index c9c41c6..6e59d41 100644 --- a/packages/subgraph/src/handlers/provision.ts +++ b/packages/subgraph/src/handlers/provision.ts @@ -1,3 +1,4 @@ +import { Bytes } from "@graphprotocol/graph-ts" import { ProvisionCreated, ProvisionIncreased, @@ -9,21 +10,29 @@ import { } from "../../generated/HorizonStaking/HorizonStaking" import { getOrCreateGraphNetwork, saveGraphNetwork } from "../entities/graphNetwork" import { getOrCreateServiceProvider, saveServiceProvider } from "../entities/serviceProvider" +import { getOrCreateDataService, saveDataService } from "../entities/dataService" import { getOrCreateProvision, saveProvision } from "../entities/provision" /** * Emitted when a service provider creates a new provision to a verifier. */ export function handleProvisionCreated(event: ProvisionCreated): void { + let verifierBytes = Bytes.fromHexString(event.params.verifier.toHexString()) as Bytes + let graphNetwork = getOrCreateGraphNetwork() let serviceProvider = getOrCreateServiceProvider( event.params.serviceProvider, event.block.number, event.block.timestamp ) + let dataService = getOrCreateDataService( + verifierBytes, + event.block.number, + event.block.timestamp + ) let provision = getOrCreateProvision( event.params.serviceProvider, - event.params.verifier, + verifierBytes, event.block.number, event.block.timestamp ) @@ -37,6 +46,12 @@ export function handleProvisionCreated(event: ProvisionCreated): void { provision.entity.thawingPeriodPending = event.params.thawingPeriod saveProvision(provision.entity, event.block) + // DataService + dataService.entity.countServiceProviders += 1 + dataService.entity.countProvisions += 1 + dataService.entity.tokensProvisioned = dataService.entity.tokensProvisioned.plus(event.params.tokens) + saveDataService(dataService.entity, event.block) + // ServiceProvider assert(!serviceProvider.isNew, "Service provider does not exist.") serviceProvider.entity.countProvisions += 1 @@ -46,6 +61,9 @@ export function handleProvisionCreated(event: ProvisionCreated): void { saveServiceProvider(serviceProvider.entity, event.block) // GraphNetwork + if (dataService.isNew) { + graphNetwork.countDataServices += 1 + } graphNetwork.countProvisions += 1 graphNetwork.tokensProvisioned = graphNetwork.tokensProvisioned.plus(event.params.tokens) saveGraphNetwork(graphNetwork) @@ -55,15 +73,22 @@ export function handleProvisionCreated(event: ProvisionCreated): void { * Emitted when tokens are added to an existing provision. */ export function handleProvisionIncreased(event: ProvisionIncreased): void { + let verifierBytes = Bytes.fromHexString(event.params.verifier.toHexString()) as Bytes + let graphNetwork = getOrCreateGraphNetwork() let serviceProvider = getOrCreateServiceProvider( event.params.serviceProvider, event.block.number, event.block.timestamp ) + let dataService = getOrCreateDataService( + verifierBytes, + event.block.number, + event.block.timestamp + ) let provision = getOrCreateProvision( event.params.serviceProvider, - event.params.verifier, + verifierBytes, event.block.number, event.block.timestamp ) @@ -73,6 +98,11 @@ export function handleProvisionIncreased(event: ProvisionIncreased): void { provision.entity.tokens = provision.entity.tokens.plus(event.params.tokens) saveProvision(provision.entity, event.block) + // DataService + assert(!dataService.isNew, "Data service does not exist.") + dataService.entity.tokensProvisioned = dataService.entity.tokensProvisioned.plus(event.params.tokens) + saveDataService(dataService.entity, event.block) + // ServiceProvider assert(!serviceProvider.isNew, "Service provider does not exist.") serviceProvider.entity.tokensProvisioned = serviceProvider.entity.tokensProvisioned.plus(event.params.tokens) @@ -90,15 +120,22 @@ export function handleProvisionIncreased(event: ProvisionIncreased): void { * Note: Thawing tokens are still considered "provisioned". */ export function handleProvisionThawed(event: ProvisionThawed): void { + let verifierBytes = Bytes.fromHexString(event.params.verifier.toHexString()) as Bytes + let graphNetwork = getOrCreateGraphNetwork() let serviceProvider = getOrCreateServiceProvider( event.params.serviceProvider, event.block.number, event.block.timestamp ) + let dataService = getOrCreateDataService( + verifierBytes, + event.block.number, + event.block.timestamp + ) let provision = getOrCreateProvision( event.params.serviceProvider, - event.params.verifier, + verifierBytes, event.block.number, event.block.timestamp ) @@ -108,6 +145,11 @@ export function handleProvisionThawed(event: ProvisionThawed): void { provision.entity.tokensThawing = provision.entity.tokensThawing.plus(event.params.tokens) saveProvision(provision.entity, event.block) + // DataService + assert(!dataService.isNew, "Data service does not exist.") + dataService.entity.tokensThawingFromProvisions = dataService.entity.tokensThawingFromProvisions.plus(event.params.tokens) + saveDataService(dataService.entity, event.block) + // ServiceProvider assert(!serviceProvider.isNew, "Service provider does not exist.") serviceProvider.entity.tokensThawing = serviceProvider.entity.tokensThawing.plus(event.params.tokens) @@ -122,15 +164,22 @@ export function handleProvisionThawed(event: ProvisionThawed): void { * Emitted when thawed tokens are removed from a provision (after thawing period completes). */ export function handleTokensDeprovisioned(event: TokensDeprovisioned): void { + let verifierBytes = Bytes.fromHexString(event.params.verifier.toHexString()) as Bytes + let graphNetwork = getOrCreateGraphNetwork() let serviceProvider = getOrCreateServiceProvider( event.params.serviceProvider, event.block.number, event.block.timestamp ) + let dataService = getOrCreateDataService( + verifierBytes, + event.block.number, + event.block.timestamp + ) let provision = getOrCreateProvision( event.params.serviceProvider, - event.params.verifier, + verifierBytes, event.block.number, event.block.timestamp ) @@ -143,6 +192,14 @@ export function handleTokensDeprovisioned(event: TokensDeprovisioned): void { provision.entity.tokensThawing = provision.entity.tokensThawing.minus(event.params.tokens) saveProvision(provision.entity, event.block) + // DataService + assert(!dataService.isNew, "Data service does not exist.") + assert(dataService.entity.tokensThawingFromProvisions >= event.params.tokens, "Deprovision exceeds data service tokens thawing.") + dataService.entity.tokensThawingFromProvisions = dataService.entity.tokensThawingFromProvisions.minus(event.params.tokens) + assert(dataService.entity.tokensProvisioned >= event.params.tokens, "Deprovision exceeds data service tokens provisioned.") + dataService.entity.tokensProvisioned = dataService.entity.tokensProvisioned.minus(event.params.tokens) + saveDataService(dataService.entity, event.block) + // ServiceProvider assert(!serviceProvider.isNew, "Service provider does not exist.") assert(serviceProvider.entity.tokensThawing >= event.params.tokens, "Deprovision exceeds service provider tokens thawing.") @@ -165,15 +222,22 @@ export function handleTokensDeprovisioned(event: TokensDeprovisioned): void { * Emitted when a provision is slashed by the verifier. */ export function handleProvisionSlashed(event: ProvisionSlashed): void { + let verifierBytes = Bytes.fromHexString(event.params.verifier.toHexString()) as Bytes + let graphNetwork = getOrCreateGraphNetwork() let serviceProvider = getOrCreateServiceProvider( event.params.serviceProvider, event.block.number, event.block.timestamp ) + let dataService = getOrCreateDataService( + verifierBytes, + event.block.number, + event.block.timestamp + ) let provision = getOrCreateProvision( event.params.serviceProvider, - event.params.verifier, + verifierBytes, event.block.number, event.block.timestamp ) @@ -184,6 +248,15 @@ export function handleProvisionSlashed(event: ProvisionSlashed): void { provision.entity.tokens = provision.entity.tokens.minus(event.params.tokens) saveProvision(provision.entity, event.block) + // DataService + assert(!dataService.isNew, "Data service does not exist.") + assert(dataService.entity.tokensProvisioned >= event.params.tokens, "Slash exceeds data service tokens provisioned.") + dataService.entity.tokensProvisioned = dataService.entity.tokensProvisioned.minus(event.params.tokens) + dataService.entity.countProvisionSlashEvents += 1 + dataService.entity.tokensSlashed = dataService.entity.tokensSlashed.plus(event.params.tokens) + dataService.entity.tokensSlashedFromProvisions = dataService.entity.tokensSlashedFromProvisions.plus(event.params.tokens) + saveDataService(dataService.entity, event.block) + // ServiceProvider assert(!serviceProvider.isNew, "Service provider does not exist.") assert(serviceProvider.entity.tokensStaked >= event.params.tokens, "Slash exceeds service provider tokens staked.") @@ -212,9 +285,11 @@ export function handleProvisionSlashed(event: ProvisionSlashed): void { * Emitted when new provision parameters are staged (pending acceptance). */ export function handleProvisionParametersStaged(event: ProvisionParametersStaged): void { + let verifierBytes = Bytes.fromHexString(event.params.verifier.toHexString()) as Bytes + let provision = getOrCreateProvision( event.params.serviceProvider, - event.params.verifier, + verifierBytes, event.block.number, event.block.timestamp ) @@ -231,9 +306,11 @@ export function handleProvisionParametersStaged(event: ProvisionParametersStaged * Emitted when staged provision parameters are accepted. */ export function handleProvisionParametersSet(event: ProvisionParametersSet): void { + let verifierBytes = Bytes.fromHexString(event.params.verifier.toHexString()) as Bytes + let provision = getOrCreateProvision( event.params.serviceProvider, - event.params.verifier, + verifierBytes, event.block.number, event.block.timestamp ) diff --git a/packages/subgraph/tests/delegation.test.ts b/packages/subgraph/tests/delegation.test.ts index 137f1a5..4d60141 100644 --- a/packages/subgraph/tests/delegation.test.ts +++ b/packages/subgraph/tests/delegation.test.ts @@ -23,8 +23,9 @@ import { handleDelegationSlashed } from "../src/handlers/delegation" import { handleHorizonStakeDeposited } from "../src/handlers/staking" -import { GRAPH_NETWORK_ID } from "../src/common/constants" +import { GRAPH_NETWORK_ID, BIGINT_ZERO } from "../src/common/constants" import { getDelegationPoolId } from "../src/entities/delegationPool" +import { DataService, GraphNetwork } from "../generated/schema" // Test addresses const SP_ADDRESS = Address.fromString("0x1234567890123456789012345678901234567890") @@ -137,6 +138,36 @@ function getDelegationPoolIdString(sp: Address, verifier: Address): string { return getDelegationPoolId(Bytes.fromHexString(sp.toHexString()), Bytes.fromHexString(verifier.toHexString())).toHexString() } +// Helper to set up DataService entity (normally created via ProvisionCreated) +function setupDataService(verifier: Address): void { + let id = Bytes.fromHexString(verifier.toHexString()) + let ds = new DataService(id) + ds.countServiceProviders = 0 + ds.countProvisions = 0 + ds.countDelegationPools = 0 + ds.countProvisionSlashEvents = 0 + ds.countDelegationPoolSlashEvents = 0 + ds.tokensProvisioned = BIGINT_ZERO + ds.tokensDelegated = BIGINT_ZERO + ds.tokensThawingFromProvisions = BIGINT_ZERO + ds.tokensThawingFromDelegationPools = BIGINT_ZERO + ds.tokensSlashed = BIGINT_ZERO + ds.tokensSlashedFromProvisions = BIGINT_ZERO + ds.tokensSlashedFromDelegationPools = BIGINT_ZERO + ds.createdAtBlock = BigInt.fromI32(1) + ds.createdAt = BigInt.fromI32(100) + ds.updatedAtBlock = BigInt.fromI32(1) + ds.updatedAt = BigInt.fromI32(100) + ds.save() + + // Update GraphNetwork countDataServices + let graphNetwork = GraphNetwork.load(GRAPH_NETWORK_ID) + if (graphNetwork != null) { + graphNetwork.countDataServices += 1 + graphNetwork.save() + } +} + describe("TokensToDelegationPoolAdded", () => { beforeEach(() => { clearStore() @@ -147,6 +178,7 @@ describe("TokensToDelegationPoolAdded", () => { let stakeTokens = BigInt.fromString("10000000000000000000000") let depositEvent = createStakeDepositedEvent(SP_ADDRESS, stakeTokens) handleHorizonStakeDeposited(depositEvent) + setupDataService(VERIFIER_ADDRESS) let delegatedTokens = BigInt.fromString("1000000000000000000000") // 1000 GRT let shares = BigInt.fromString("1000000000000000000000") @@ -184,6 +216,7 @@ describe("TokensDelegated", () => { let stakeTokens = BigInt.fromString("10000000000000000000000") // 10000 GRT let depositEvent = createStakeDepositedEvent(SP_ADDRESS, stakeTokens) handleHorizonStakeDeposited(depositEvent) + setupDataService(VERIFIER_ADDRESS) // Delegate tokens let delegatedTokens = BigInt.fromString("1000000000000000000000") // 1000 GRT @@ -214,6 +247,7 @@ describe("TokensDelegated", () => { let stakeTokens = BigInt.fromString("10000000000000000000000") let depositEvent = createStakeDepositedEvent(SP_ADDRESS, stakeTokens) handleHorizonStakeDeposited(depositEvent) + setupDataService(VERIFIER_ADDRESS) // First delegation let tokens1 = BigInt.fromString("1000000000000000000000") // 1000 GRT @@ -255,6 +289,7 @@ describe("TokensUndelegated", () => { let stakeTokens = BigInt.fromString("10000000000000000000000") let depositEvent = createStakeDepositedEvent(SP_ADDRESS, stakeTokens) handleHorizonStakeDeposited(depositEvent) + setupDataService(VERIFIER_ADDRESS) let delegatedTokens = BigInt.fromString("1000000000000000000000") let shares = BigInt.fromString("1000000000000000000000") @@ -291,6 +326,7 @@ describe("DelegatedTokensWithdrawn", () => { let stakeTokens = BigInt.fromString("10000000000000000000000") let depositEvent = createStakeDepositedEvent(SP_ADDRESS, stakeTokens) handleHorizonStakeDeposited(depositEvent) + setupDataService(VERIFIER_ADDRESS) let delegatedTokens = BigInt.fromString("1000000000000000000000") let shares = BigInt.fromString("1000000000000000000000") @@ -335,6 +371,7 @@ describe("DelegationSlashed", () => { let stakeTokens = BigInt.fromString("10000000000000000000000") let depositEvent = createStakeDepositedEvent(SP_ADDRESS, stakeTokens) handleHorizonStakeDeposited(depositEvent) + setupDataService(VERIFIER_ADDRESS) let delegatedTokens = BigInt.fromString("1000000000000000000000") let shares = BigInt.fromString("1000000000000000000000") @@ -371,6 +408,7 @@ describe("Service Provider counter behavior", () => { let stakeTokens = BigInt.fromString("5000000000000000000000") let depositEvent = createStakeDepositedEvent(SP_ADDRESS, stakeTokens) handleHorizonStakeDeposited(depositEvent) + setupDataService(VERIFIER_ADDRESS) // Then add delegation let delegatedTokens = BigInt.fromString("1000000000000000000000") @@ -398,6 +436,7 @@ describe("Service Provider counter behavior", () => { let stakeTokens = BigInt.fromString("5000000000000000000000") let depositEvent = createStakeDepositedEvent(SP_ADDRESS, stakeTokens) handleHorizonStakeDeposited(depositEvent) + setupDataService(VERIFIER_ADDRESS) assert.fieldEquals("GraphNetwork", GRAPH_NETWORK_ID.toHexString(), "countServiceProviders", "1") @@ -422,6 +461,7 @@ describe("Delegation lifecycle", () => { let stakeTokens = BigInt.fromString("10000000000000000000000") // 10000 GRT let depositEvent = createStakeDepositedEvent(SP_ADDRESS, stakeTokens) handleHorizonStakeDeposited(depositEvent) + setupDataService(VERIFIER_ADDRESS) assert.fieldEquals("ServiceProvider", SP_ADDRESS.toHexString(), "tokensDelegated", "0") diff --git a/packages/subgraph/tests/legacy.test.ts b/packages/subgraph/tests/legacy.test.ts new file mode 100644 index 0000000..4400ee2 --- /dev/null +++ b/packages/subgraph/tests/legacy.test.ts @@ -0,0 +1,419 @@ +import { + describe, + test, + beforeEach, + clearStore, + assert, + newTypedMockEvent, +} from "matchstick-as" +import { Address, BigInt, Bytes, ethereum } from "@graphprotocol/graph-ts" +import { + RebateCollected, + AllocationClosed, +} from "../generated/HorizonStaking/HorizonStaking" +import { + handleRebateCollected, + handleAllocationClosed, +} from "../src/handlers/legacy" +import { GRAPH_NETWORK_ID, BIGINT_ZERO } from "../src/common/constants" +import { getDelegationPoolId } from "../src/entities/delegationPool" +import { config } from "../src/config" +import { DelegationPool, ServiceProvider, GraphNetwork, DataService } from "../generated/schema" + +// Test addresses +const INDEXER_ADDRESS = Address.fromString("0x1111111111111111111111111111111111111111") +const ALLOCATION_ID = Address.fromString("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") +const ASSET_HOLDER = Address.fromString("0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") +const SUBGRAPH_DEPLOYMENT_ID = Bytes.fromHexString("0x1234567890123456789012345678901234567890123456789012345678901234") + +// HorizonRewardsAssigned event topic +const HORIZON_REWARDS_ASSIGNED_TOPIC = Bytes.fromHexString( + "0xa111914d7f2ea8beca61d12f1a1f38c5533de5f1823c3936422df4404ac2ec68" +) + +// Helper to set up a DataService entity +function setupDataService(verifier: Address): void { + let id = Bytes.fromHexString(verifier.toHexString()) + let ds = new DataService(id) + ds.countServiceProviders = 0 + ds.countProvisions = 0 + ds.countDelegationPools = 1 + ds.countProvisionSlashEvents = 0 + ds.countDelegationPoolSlashEvents = 0 + ds.tokensProvisioned = BIGINT_ZERO + ds.tokensDelegated = BIGINT_ZERO + ds.tokensThawingFromProvisions = BIGINT_ZERO + ds.tokensThawingFromDelegationPools = BIGINT_ZERO + ds.tokensSlashed = BIGINT_ZERO + ds.tokensSlashedFromProvisions = BIGINT_ZERO + ds.tokensSlashedFromDelegationPools = BIGINT_ZERO + ds.createdAtBlock = BigInt.fromI32(1) + ds.createdAt = BigInt.fromI32(100) + ds.updatedAtBlock = BigInt.fromI32(1) + ds.updatedAt = BigInt.fromI32(100) + ds.save() +} + +// Helper to set up a DelegationPool entity +function setupDelegationPool( + indexer: Address, + tokens: BigInt, + legacyIndexingRewardCut: i32 = 823076 +): void { + let dataService = config.subgraphServiceAddress + let poolId = getDelegationPoolId( + Bytes.fromHexString(indexer.toHexString()), + Bytes.fromHexString(dataService.toHexString()) + ) + + let pool = new DelegationPool(poolId) + pool.serviceProvider = Bytes.fromHexString(indexer.toHexString()) + pool.dataService = Bytes.fromHexString(dataService.toHexString()) + pool.tokens = tokens + pool.shares = tokens // 1:1 for simplicity + pool.tokensThawing = BigInt.zero() + pool.legacyIndexingRewardCut = legacyIndexingRewardCut + pool.createdAtBlock = BigInt.fromI32(1) + pool.createdAt = BigInt.fromI32(1000) + pool.updatedAtBlock = BigInt.fromI32(1) + pool.updatedAt = BigInt.fromI32(1000) + pool.save() +} + +// Helper to set up a ServiceProvider entity +function setupServiceProvider(address: Address, tokensDelegated: BigInt): void { + let sp = new ServiceProvider(Bytes.fromHexString(address.toHexString())) + // Counts + sp.countProvisions = 0 + sp.countProvisionSlashEvents = 0 + sp.countDelegationPoolSlashEvents = 0 + // Stake + sp.tokensStaked = BigInt.fromI32(1000) + sp.tokensProvisioned = BigInt.zero() + sp.tokensIdle = BigInt.fromI32(1000) + sp.tokensThawing = BigInt.zero() + // Delegation + sp.tokensDelegated = tokensDelegated + sp.tokensDelegatedThawing = BigInt.zero() + // Slashing + sp.tokensSlashed = BigInt.zero() + sp.tokensSlashedFromProvisions = BigInt.zero() + sp.tokensSlashedFromDelegationPools = BigInt.zero() + // Metadata + sp.createdAtBlock = BigInt.fromI32(1) + sp.createdAt = BigInt.fromI32(1000) + sp.updatedAtBlock = BigInt.fromI32(1) + sp.updatedAt = BigInt.fromI32(1000) + sp.save() +} + +// Helper to set up GraphNetwork entity +function setupGraphNetwork(tokensDelegated: BigInt): void { + let network = new GraphNetwork(GRAPH_NETWORK_ID) + // Counts + network.countServiceProviders = 1 + network.countDataServices = 1 + network.countProvisions = 0 + network.countDelegationPools = 1 + network.countProvisionSlashEvents = 0 + network.countDelegationPoolSlashEvents = 0 + // Stake aggregates + network.tokensStaked = BigInt.zero() + network.tokensProvisioned = BigInt.zero() + network.tokensDelegated = tokensDelegated + network.tokensThawingFromProvisions = BigInt.zero() + network.tokensThawingFromDelegationPools = BigInt.zero() + // Slashing aggregates + network.tokensSlashed = BigInt.zero() + network.tokensSlashedFromProvisions = BigInt.zero() + network.tokensSlashedFromDelegationPools = BigInt.zero() + network.save() +} + +// Helper to create RebateCollected event +function createRebateCollectedEvent( + indexer: Address, + delegationRewards: BigInt +): RebateCollected { + let event = newTypedMockEvent() + event.parameters = new Array() + event.parameters.push(new ethereum.EventParam("assetHolder", ethereum.Value.fromAddress(ASSET_HOLDER))) + event.parameters.push(new ethereum.EventParam("indexer", ethereum.Value.fromAddress(indexer))) + event.parameters.push(new ethereum.EventParam("subgraphDeploymentID", ethereum.Value.fromBytes(SUBGRAPH_DEPLOYMENT_ID))) + event.parameters.push(new ethereum.EventParam("allocationID", ethereum.Value.fromAddress(ALLOCATION_ID))) + event.parameters.push(new ethereum.EventParam("epoch", ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(100)))) + event.parameters.push(new ethereum.EventParam("tokens", ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(10000)))) + event.parameters.push(new ethereum.EventParam("protocolTax", ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(100)))) + event.parameters.push(new ethereum.EventParam("curationFees", ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(200)))) + event.parameters.push(new ethereum.EventParam("queryFees", ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(9700)))) + event.parameters.push(new ethereum.EventParam("queryRebates", ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(8000)))) + event.parameters.push(new ethereum.EventParam("delegationRewards", ethereum.Value.fromUnsignedBigInt(delegationRewards))) + event.block.number = BigInt.fromI32(200) + event.block.timestamp = BigInt.fromI32(2000) + return event +} + +describe("handleRebateCollected", () => { + beforeEach(() => { + clearStore() + }) + + test("updates DelegationPool tokens", () => { + let initialPoolTokens = BigInt.fromString("5000000000000000000000") // 5000 GRT + let delegationRewards = BigInt.fromString("100000000000000000000") // 100 GRT + + setupDataService(config.subgraphServiceAddress) + setupDelegationPool(INDEXER_ADDRESS, initialPoolTokens) + setupServiceProvider(INDEXER_ADDRESS, initialPoolTokens) + setupGraphNetwork(initialPoolTokens) + + let event = createRebateCollectedEvent(INDEXER_ADDRESS, delegationRewards) + handleRebateCollected(event) + + let dataService = config.subgraphServiceAddress + let poolId = getDelegationPoolId( + Bytes.fromHexString(INDEXER_ADDRESS.toHexString()), + Bytes.fromHexString(dataService.toHexString()) + ) + + let expectedTokens = initialPoolTokens.plus(delegationRewards) + assert.fieldEquals("DelegationPool", poolId.toHexString(), "tokens", expectedTokens.toString()) + }) + + test("updates ServiceProvider tokensDelegated", () => { + let initialDelegated = BigInt.fromString("5000000000000000000000") + let delegationRewards = BigInt.fromString("100000000000000000000") + + setupDataService(config.subgraphServiceAddress) + setupDelegationPool(INDEXER_ADDRESS, initialDelegated) + setupServiceProvider(INDEXER_ADDRESS, initialDelegated) + setupGraphNetwork(initialDelegated) + + let event = createRebateCollectedEvent(INDEXER_ADDRESS, delegationRewards) + handleRebateCollected(event) + + let expectedDelegated = initialDelegated.plus(delegationRewards) + assert.fieldEquals( + "ServiceProvider", + INDEXER_ADDRESS.toHexString(), + "tokensDelegated", + expectedDelegated.toString() + ) + }) + + test("updates GraphNetwork tokensDelegated", () => { + let initialDelegated = BigInt.fromString("5000000000000000000000") + let delegationRewards = BigInt.fromString("100000000000000000000") + + setupDataService(config.subgraphServiceAddress) + setupDelegationPool(INDEXER_ADDRESS, initialDelegated) + setupServiceProvider(INDEXER_ADDRESS, initialDelegated) + setupGraphNetwork(initialDelegated) + + let event = createRebateCollectedEvent(INDEXER_ADDRESS, delegationRewards) + handleRebateCollected(event) + + let expectedDelegated = initialDelegated.plus(delegationRewards) + assert.fieldEquals( + "GraphNetwork", + GRAPH_NETWORK_ID.toHexString(), + "tokensDelegated", + expectedDelegated.toString() + ) + }) + + test("skips if delegationRewards is zero", () => { + let initialPoolTokens = BigInt.fromString("5000000000000000000000") + + setupDataService(config.subgraphServiceAddress) + setupDelegationPool(INDEXER_ADDRESS, initialPoolTokens) + setupServiceProvider(INDEXER_ADDRESS, initialPoolTokens) + setupGraphNetwork(initialPoolTokens) + + let event = createRebateCollectedEvent(INDEXER_ADDRESS, BigInt.zero()) + handleRebateCollected(event) + + // Pool tokens should remain unchanged + let dataService = config.subgraphServiceAddress + let poolId = getDelegationPoolId( + Bytes.fromHexString(INDEXER_ADDRESS.toHexString()), + Bytes.fromHexString(dataService.toHexString()) + ) + assert.fieldEquals("DelegationPool", poolId.toHexString(), "tokens", initialPoolTokens.toString()) + }) + + test("skips if DelegationPool does not exist", () => { + // Don't set up pool - only SP and network + setupDataService(config.subgraphServiceAddress) + setupServiceProvider(INDEXER_ADDRESS, BigInt.zero()) + setupGraphNetwork(BigInt.zero()) + + let delegationRewards = BigInt.fromString("100000000000000000000") + let event = createRebateCollectedEvent(INDEXER_ADDRESS, delegationRewards) + + // Should not throw - just skip + handleRebateCollected(event) + + // GraphNetwork should not be updated + assert.fieldEquals("GraphNetwork", GRAPH_NETWORK_ID.toHexString(), "tokensDelegated", "0") + }) +}) + +// Helper to create a mock Log for HorizonRewardsAssigned event +function createHorizonRewardsAssignedLog( + indexer: Address, + allocationID: Address, + rewards: BigInt +): ethereum.Log { + // Pad addresses to 32 bytes for indexed topics + let indexerTopic = Bytes.fromHexString("0x000000000000000000000000" + indexer.toHexString().slice(2)) + let allocationTopic = Bytes.fromHexString("0x000000000000000000000000" + allocationID.toHexString().slice(2)) + + // Encode rewards as data + let encodedRewards = ethereum.encode(ethereum.Value.fromUnsignedBigInt(rewards))! + + return new ethereum.Log( + Address.zero(), // address (RewardsManager) + [HORIZON_REWARDS_ASSIGNED_TOPIC, indexerTopic, allocationTopic], // topics + Bytes.fromUint8Array(encodedRewards), // data + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000000"), // blockHash + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000001"), // blockNumber + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000000"), // transactionHash + BigInt.fromI32(0), // transactionIndex + BigInt.fromI32(0), // logIndex + BigInt.fromI32(0), // transactionLogIndex + "mined", // logType + null // removed + ) +} + +// Helper to create AllocationClosed event with receipt +function createAllocationClosedEventWithReceipt( + indexer: Address, + allocationID: Address, + rewards: BigInt +): AllocationClosed { + let event = newTypedMockEvent() + event.parameters = new Array() + event.parameters.push(new ethereum.EventParam("indexer", ethereum.Value.fromAddress(indexer))) + event.parameters.push(new ethereum.EventParam("subgraphDeploymentID", ethereum.Value.fromBytes(SUBGRAPH_DEPLOYMENT_ID))) + event.parameters.push(new ethereum.EventParam("epoch", ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(100)))) + event.parameters.push(new ethereum.EventParam("tokens", ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(10000)))) + event.parameters.push(new ethereum.EventParam("allocationID", ethereum.Value.fromAddress(allocationID))) + event.parameters.push(new ethereum.EventParam("sender", ethereum.Value.fromAddress(indexer))) + event.parameters.push(new ethereum.EventParam("poi", ethereum.Value.fromBytes(Bytes.fromHexString("0x1234567890123456789012345678901234567890123456789012345678901234")))) + event.parameters.push(new ethereum.EventParam("isPublic", ethereum.Value.fromBoolean(false))) + event.block.number = BigInt.fromI32(200) + event.block.timestamp = BigInt.fromI32(2000) + + // Create receipt with HorizonRewardsAssigned log + let log = createHorizonRewardsAssignedLog(indexer, allocationID, rewards) + event.receipt = new ethereum.TransactionReceipt( + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000000"), // transactionHash + BigInt.fromI32(0), // transactionIndex + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000000"), // blockHash + BigInt.fromI32(200), // blockNumber + BigInt.fromI32(100000), // cumulativeGasUsed + BigInt.fromI32(50000), // gasUsed + Address.zero(), // contractAddress + [log], // logs + BigInt.fromI32(1), // status + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000000"), // root + Bytes.fromHexString("0x0000000000000000000000000000000000000000000000000000000000000000") // logsBloom + ) + + return event +} + +describe("handleAllocationClosed", () => { + beforeEach(() => { + clearStore() + }) + + test("calculates delegation rewards using legacyIndexingRewardCut", () => { + // Set up pool with 50% cut (500000 PPM) - indexer keeps 50%, delegators get 50% + let initialPoolTokens = BigInt.fromString("5000000000000000000000") // 5000 GRT + let indexerCut = 500000 // 50% + + setupDataService(config.subgraphServiceAddress) + setupDelegationPool(INDEXER_ADDRESS, initialPoolTokens, indexerCut) + setupServiceProvider(INDEXER_ADDRESS, initialPoolTokens) + setupGraphNetwork(initialPoolTokens) + + // Total rewards = 1000 GRT + let totalRewards = BigInt.fromString("1000000000000000000000") + // Expected delegation rewards = 1000 * (1 - 0.5) = 500 GRT + let expectedDelegationRewards = BigInt.fromString("500000000000000000000") + + let event = createAllocationClosedEventWithReceipt(INDEXER_ADDRESS, ALLOCATION_ID, totalRewards) + handleAllocationClosed(event) + + let dataService = config.subgraphServiceAddress + let poolId = getDelegationPoolId( + Bytes.fromHexString(INDEXER_ADDRESS.toHexString()), + Bytes.fromHexString(dataService.toHexString()) + ) + + let expectedPoolTokens = initialPoolTokens.plus(expectedDelegationRewards) + assert.fieldEquals("DelegationPool", poolId.toHexString(), "tokens", expectedPoolTokens.toString()) + }) + + test("updates ServiceProvider and GraphNetwork tokensDelegated", () => { + let initialDelegated = BigInt.fromString("5000000000000000000000") + let indexerCut = 800000 // 80% - delegators get 20% + + setupDataService(config.subgraphServiceAddress) + setupDelegationPool(INDEXER_ADDRESS, initialDelegated, indexerCut) + setupServiceProvider(INDEXER_ADDRESS, initialDelegated) + setupGraphNetwork(initialDelegated) + + // Total rewards = 1000 GRT, delegation rewards = 200 GRT (20%) + let totalRewards = BigInt.fromString("1000000000000000000000") + let expectedDelegationRewards = BigInt.fromString("200000000000000000000") + + let event = createAllocationClosedEventWithReceipt(INDEXER_ADDRESS, ALLOCATION_ID, totalRewards) + handleAllocationClosed(event) + + let expectedDelegated = initialDelegated.plus(expectedDelegationRewards) + assert.fieldEquals("ServiceProvider", INDEXER_ADDRESS.toHexString(), "tokensDelegated", expectedDelegated.toString()) + assert.fieldEquals("GraphNetwork", GRAPH_NETWORK_ID.toHexString(), "tokensDelegated", expectedDelegated.toString()) + }) + + test("skips if DelegationPool does not exist", () => { + // Don't set up pool + setupDataService(config.subgraphServiceAddress) + setupServiceProvider(INDEXER_ADDRESS, BigInt.zero()) + setupGraphNetwork(BigInt.zero()) + + let totalRewards = BigInt.fromString("1000000000000000000000") + let event = createAllocationClosedEventWithReceipt(INDEXER_ADDRESS, ALLOCATION_ID, totalRewards) + + // Should not throw + handleAllocationClosed(event) + + // GraphNetwork should not be updated + assert.fieldEquals("GraphNetwork", GRAPH_NETWORK_ID.toHexString(), "tokensDelegated", "0") + }) + + test("skips if 100% indexer cut (no delegation rewards)", () => { + let initialPoolTokens = BigInt.fromString("5000000000000000000000") + let indexerCut = 1000000 // 100% - delegators get 0% + + setupDataService(config.subgraphServiceAddress) + setupDelegationPool(INDEXER_ADDRESS, initialPoolTokens, indexerCut) + setupServiceProvider(INDEXER_ADDRESS, initialPoolTokens) + setupGraphNetwork(initialPoolTokens) + + let totalRewards = BigInt.fromString("1000000000000000000000") + let event = createAllocationClosedEventWithReceipt(INDEXER_ADDRESS, ALLOCATION_ID, totalRewards) + handleAllocationClosed(event) + + // Pool should not be updated (delegation rewards = 0) + let dataService = config.subgraphServiceAddress + let poolId = getDelegationPoolId( + Bytes.fromHexString(INDEXER_ADDRESS.toHexString()), + Bytes.fromHexString(dataService.toHexString()) + ) + assert.fieldEquals("DelegationPool", poolId.toHexString(), "tokens", initialPoolTokens.toString()) + }) +}) diff --git a/packages/subgraph/tests/staking.test.ts b/packages/subgraph/tests/staking.test.ts index 8d3a731..8ad9561 100644 --- a/packages/subgraph/tests/staking.test.ts +++ b/packages/subgraph/tests/staking.test.ts @@ -10,7 +10,8 @@ import { Address, BigInt, Bytes, ethereum } from "@graphprotocol/graph-ts" import { HorizonStakeDeposited, HorizonStakeWithdrawn, TokensDelegated } from "../generated/HorizonStaking/HorizonStaking" import { handleHorizonStakeDeposited, handleHorizonStakeWithdrawn } from "../src/handlers/staking" import { handleTokensDelegated } from "../src/handlers/delegation" -import { GRAPH_NETWORK_ID } from "../src/common/constants" +import { GRAPH_NETWORK_ID, BIGINT_ZERO } from "../src/common/constants" +import { DataService, GraphNetwork } from "../generated/schema" // Test addresses const SP_ADDRESS = Address.fromString("0x1234567890123456789012345678901234567890") @@ -61,6 +62,36 @@ function createTokensDelegatedEvent( return event } +// Helper to set up DataService entity (normally created via ProvisionCreated) +function setupDataService(verifier: Address): void { + let id = Bytes.fromHexString(verifier.toHexString()) + let ds = new DataService(id) + ds.countServiceProviders = 0 + ds.countProvisions = 0 + ds.countDelegationPools = 0 + ds.countProvisionSlashEvents = 0 + ds.countDelegationPoolSlashEvents = 0 + ds.tokensProvisioned = BIGINT_ZERO + ds.tokensDelegated = BIGINT_ZERO + ds.tokensThawingFromProvisions = BIGINT_ZERO + ds.tokensThawingFromDelegationPools = BIGINT_ZERO + ds.tokensSlashed = BIGINT_ZERO + ds.tokensSlashedFromProvisions = BIGINT_ZERO + ds.tokensSlashedFromDelegationPools = BIGINT_ZERO + ds.createdAtBlock = BigInt.fromI32(1) + ds.createdAt = BigInt.fromI32(100) + ds.updatedAtBlock = BigInt.fromI32(1) + ds.updatedAt = BigInt.fromI32(100) + ds.save() + + // Update GraphNetwork countDataServices + let graphNetwork = GraphNetwork.load(GRAPH_NETWORK_ID) + if (graphNetwork != null) { + graphNetwork.countDataServices += 1 + graphNetwork.save() + } +} + describe("HorizonStakeDeposited", () => { beforeEach(() => { clearStore() @@ -192,6 +223,7 @@ describe("HorizonStakeWithdrawn", () => { // First deposit stake let depositEvent = createStakeDepositedEvent(SP_ADDRESS, stake) handleHorizonStakeDeposited(depositEvent) + setupDataService(VERIFIER_ADDRESS) // Then receive delegation let delegatedTokens = BigInt.fromString("500000000000000000000") // 500 GRT diff --git a/packages/tools/src/validation/internal.ts b/packages/tools/src/validation/internal.ts index 49724e2..3106f0b 100644 --- a/packages/tools/src/validation/internal.ts +++ b/packages/tools/src/validation/internal.ts @@ -23,6 +23,7 @@ import { interface GraphNetwork { id: string countServiceProviders: number + countDataServices: number countProvisions: number countDelegationPools: number tokensStaked: string @@ -42,9 +43,21 @@ interface ServiceProvider { tokensIdle: string } +interface DataService { + id: string + countServiceProviders: number + countProvisions: number + countDelegationPools: number + tokensProvisioned: string + tokensDelegated: string + tokensThawingFromProvisions: string + tokensThawingFromDelegationPools: string +} + interface Provision { id: string serviceProvider: { id: string } + dataService: { id: string } tokens: string tokensThawing: string } @@ -52,6 +65,7 @@ interface Provision { interface DelegationPool { id: string serviceProvider: { id: string } + dataService: { id: string } tokens: string tokensThawing: string } @@ -64,6 +78,7 @@ const GRAPH_NETWORK_QUERY = `{ graphNetwork(id: "0x01000000") { id countServiceProviders + countDataServices countProvisions countDelegationPools tokensStaked @@ -86,10 +101,24 @@ const SERVICE_PROVIDERS_QUERY = `{ } }` +const DATA_SERVICES_QUERY = `{ + dataServices(first: 1000) { + id + countServiceProviders + countProvisions + countDelegationPools + tokensProvisioned + tokensDelegated + tokensThawingFromProvisions + tokensThawingFromDelegationPools + } +}` + const PROVISIONS_QUERY = `{ provisions(first: 1000) { id serviceProvider { id } + dataService { id } tokens tokensThawing } @@ -99,6 +128,7 @@ const DELEGATION_POOLS_QUERY = `{ delegationPools(first: 1000) { id serviceProvider { id } + dataService { id } tokens tokensThawing } @@ -116,9 +146,10 @@ async function main(): Promise { // Fetch all data console.log("=== Fetching subgraph data ===") - const [networkData, spData, provisionData, poolData] = await Promise.all([ + const [networkData, spData, dsData, provisionData, poolData] = await Promise.all([ querySubgraph<{ graphNetwork: GraphNetwork }>(subgraphUrl, GRAPH_NETWORK_QUERY), querySubgraph<{ serviceProviders: ServiceProvider[] }>(subgraphUrl, SERVICE_PROVIDERS_QUERY), + querySubgraph<{ dataServices: DataService[] }>(subgraphUrl, DATA_SERVICES_QUERY), querySubgraph<{ provisions: Provision[] }>(subgraphUrl, PROVISIONS_QUERY), querySubgraph<{ delegationPools: DelegationPool[] }>(subgraphUrl, DELEGATION_POOLS_QUERY), ]) @@ -130,6 +161,7 @@ async function main(): Promise { } const serviceProviders = spData.serviceProviders + const dataServices = dsData.dataServices const provisions = provisionData.provisions const pools = poolData.delegationPools @@ -141,6 +173,7 @@ async function main(): Promise { console.log(` GraphNetwork: found`) console.log(` ServiceProviders: ${serviceProviders.length} total, ${stakedSPs.length} with stake`) + console.log(` DataServices: ${dataServices.length}`) console.log(` Provisions: ${provisions.length}`) console.log(` DelegationPools: ${pools.length} total, ${activePools.length} with tokens`) console.log("") @@ -155,6 +188,10 @@ async function main(): Promise { warnings++ } + if (!validateCount("DataServices", dataServices.length, graphNetwork.countDataServices)) { + warnings++ + } + if (!validateCount("Provisions", provisions.length, graphNetwork.countProvisions)) { warnings++ } @@ -265,6 +302,79 @@ async function main(): Promise { warnings += spWarnings + // ============================================================================ + // DataService Aggregate Validations + // ============================================================================ + + console.log("=== DataService Aggregate Validations ===") + let dsWarnings = 0 + + for (const ds of dataServices) { + const dsProvisions = provisions.filter((p) => p.dataService.id === ds.id) + const dsPools = pools.filter((p) => p.dataService.id === ds.id) + const dsActivePools = dsPools.filter((p) => BigInt(p.tokens) > 0n) + + // Count unique service providers with provisions to this data service + const uniqueSPs = new Set(dsProvisions.map((p) => p.serviceProvider.id)) + + const issues: string[] = [] + + // countServiceProviders should equal unique SPs with provisions + if (ds.countServiceProviders !== uniqueSPs.size) { + issues.push(`countServiceProviders: DS=${ds.countServiceProviders}, actual=${uniqueSPs.size}`) + } + + // countProvisions should equal number of provisions + if (ds.countProvisions !== dsProvisions.length) { + issues.push(`countProvisions: DS=${ds.countProvisions}, actual=${dsProvisions.length}`) + } + + // countDelegationPools should equal number of active pools + if (ds.countDelegationPools !== dsActivePools.length) { + issues.push(`countDelegationPools: DS=${ds.countDelegationPools}, actual=${dsActivePools.length}`) + } + + // tokensProvisioned should equal sum of provision tokens + const provisionedSum = dsProvisions.reduce((sum, p) => sum + BigInt(p.tokens), 0n) + if (BigInt(ds.tokensProvisioned) !== provisionedSum) { + issues.push(`tokensProvisioned: DS=${formatGRT(BigInt(ds.tokensProvisioned))}, sum=${formatGRT(provisionedSum)}`) + } + + // tokensThawingFromProvisions should equal sum of provision tokensThawing + const provisionThawingSum = dsProvisions.reduce((sum, p) => sum + BigInt(p.tokensThawing), 0n) + if (BigInt(ds.tokensThawingFromProvisions) !== provisionThawingSum) { + issues.push(`tokensThawingFromProvisions: DS=${formatGRT(BigInt(ds.tokensThawingFromProvisions))}, sum=${formatGRT(provisionThawingSum)}`) + } + + // tokensDelegated should equal sum of pool tokens + const delegatedSum = dsPools.reduce((sum, p) => sum + BigInt(p.tokens), 0n) + if (BigInt(ds.tokensDelegated) !== delegatedSum) { + issues.push(`tokensDelegated: DS=${formatGRT(BigInt(ds.tokensDelegated))}, sum=${formatGRT(delegatedSum)}`) + } + + // tokensThawingFromDelegationPools should equal sum of pool tokensThawing + const poolThawingSum = dsPools.reduce((sum, p) => sum + BigInt(p.tokensThawing), 0n) + if (BigInt(ds.tokensThawingFromDelegationPools) !== poolThawingSum) { + issues.push(`tokensThawingFromDelegationPools: DS=${formatGRT(BigInt(ds.tokensThawingFromDelegationPools))}, sum=${formatGRT(poolThawingSum)}`) + } + + if (issues.length > 0) { + dsWarnings++ + console.log(`WARNING: ${ds.id}`) + for (const issue of issues) { + console.log(` ${issue}`) + } + console.log("") + } + } + + if (dsWarnings === 0) { + console.log("All DataService aggregates match!") + console.log("") + } + + warnings += dsWarnings + // ============================================================================ // Summary // ============================================================================ diff --git a/packages/tools/src/validation/onchain/delegations.ts b/packages/tools/src/validation/onchain/delegations.ts index d7cb049..2e1dc8b 100644 --- a/packages/tools/src/validation/onchain/delegations.ts +++ b/packages/tools/src/validation/onchain/delegations.ts @@ -19,7 +19,7 @@ import { interface DelegationPool { id: string serviceProvider: { id: string } - verifier: string + dataService: { id: string } tokens: string shares: string tokensThawing: string @@ -36,7 +36,7 @@ async function main(): Promise { `{ delegationPools(first: 1000, orderBy: tokens, orderDirection: desc) { id serviceProvider { id } - verifier + dataService { id } tokens shares tokensThawing @@ -53,7 +53,7 @@ async function main(): Promise { let matches = 0 for (const pool of pools) { - const onChain = await getDelegationPool(pool.serviceProvider.id, pool.verifier) + const onChain = await getDelegationPool(pool.serviceProvider.id, pool.dataService.id) const fields = [ compareField("tokens", BigInt(pool.tokens), onChain.tokens, true), @@ -64,7 +64,7 @@ async function main(): Promise { const fieldMismatches = fields.filter((f) => !f.match) if (fieldMismatches.length > 0) { mismatches++ - console.log(`MISMATCH: ${pool.serviceProvider.id} -> ${pool.verifier}`) + console.log(`MISMATCH: ${pool.serviceProvider.id} -> ${pool.dataService.id}`) for (const m of fieldMismatches) { console.log(m.message) } diff --git a/packages/tools/src/validation/onchain/provisions.ts b/packages/tools/src/validation/onchain/provisions.ts index c5f058b..fe86c2c 100644 --- a/packages/tools/src/validation/onchain/provisions.ts +++ b/packages/tools/src/validation/onchain/provisions.ts @@ -19,7 +19,7 @@ import { interface Provision { id: string serviceProvider: { id: string } - verifier: string + dataService: { id: string } tokens: string tokensThawing: string maxVerifierCut: string @@ -39,7 +39,7 @@ async function main(): Promise { `{ provisions(first: 1000, orderBy: tokens, orderDirection: desc) { id serviceProvider { id } - verifier + dataService { id } tokens tokensThawing maxVerifierCut @@ -59,7 +59,7 @@ async function main(): Promise { let matches = 0 for (const provision of provisions) { - const onChain = await getProvision(provision.serviceProvider.id, provision.verifier) + const onChain = await getProvision(provision.serviceProvider.id, provision.dataService.id) const fields = [ compareField("tokens", BigInt(provision.tokens), onChain.tokens, true), @@ -73,7 +73,7 @@ async function main(): Promise { const fieldMismatches = fields.filter((f) => !f.match) if (fieldMismatches.length > 0) { mismatches++ - console.log(`MISMATCH: ${provision.serviceProvider.id} -> ${provision.verifier}`) + console.log(`MISMATCH: ${provision.serviceProvider.id} -> ${provision.dataService.id}`) for (const m of fieldMismatches) { console.log(m.message) } diff --git a/packages/tools/src/validation/onchain/service-providers.ts b/packages/tools/src/validation/onchain/service-providers.ts index b5b6cc9..41a5600 100644 --- a/packages/tools/src/validation/onchain/service-providers.ts +++ b/packages/tools/src/validation/onchain/service-providers.ts @@ -35,13 +35,13 @@ interface ServiceProvider { interface Provision { id: string serviceProvider: { id: string } - verifier: string + dataService: { id: string } } interface DelegationPool { id: string serviceProvider: { id: string } - verifier: string + dataService: { id: string } } async function main(): Promise { @@ -64,11 +64,11 @@ async function main(): Promise { ), querySubgraph<{ provisions: Provision[] }>( subgraphUrl, - `{ provisions(first: 1000) { id serviceProvider { id } verifier } }` + `{ provisions(first: 1000) { id serviceProvider { id } dataService { id } } }` ), querySubgraph<{ delegationPools: DelegationPool[] }>( subgraphUrl, - `{ delegationPools(first: 1000) { id serviceProvider { id } verifier } }` + `{ delegationPools(first: 1000) { id serviceProvider { id } dataService { id } } }` ), ]) @@ -113,10 +113,10 @@ async function main(): Promise { // Build multicall batch: 1 SP call + N provision calls + M pool calls const calls: string[] = [encodeGetServiceProvider(sp.id)] for (const provision of spProvisions) { - calls.push(encodeGetProvision(sp.id, provision.verifier)) + calls.push(encodeGetProvision(sp.id, provision.dataService.id)) } for (const pool of spPools) { - calls.push(encodeGetDelegationPool(sp.id, pool.verifier)) + calls.push(encodeGetDelegationPool(sp.id, pool.dataService.id)) } // Execute single multicall for this SP