diff --git a/packages/subgraph/schema.graphql b/packages/subgraph/schema.graphql index 30a0d3e..a278e45 100644 --- a/packages/subgraph/schema.graphql +++ b/packages/subgraph/schema.graphql @@ -41,6 +41,14 @@ type ServiceProvider @entity(immutable: false) { "Service provider address" id: Bytes! + # Relationships + "Provisions created by this service provider" + provisions: [Provision!]! @derivedFrom(field: "serviceProvider") + "Delegation pools for this service provider" + delegationPools: [DelegationPool!]! @derivedFrom(field: "serviceProvider") + "Provision thaw requests for this service provider" + provisionThawRequests: [ProvisionThawRequest!]! @derivedFrom(field: "serviceProvider") + # Counts "Number of active provisions" countProvisions: Int! @@ -73,14 +81,6 @@ type ServiceProvider @entity(immutable: false) { "Tokens slashed from delegation pools" tokensSlashedFromDelegationPools: BigInt! - # Provisions - "Provisions created by this service provider" - provisions: [Provision!]! @derivedFrom(field: "serviceProvider") - - # Delegation pools - "Delegation pools for this service provider" - delegationPools: [DelegationPool!]! @derivedFrom(field: "serviceProvider") - # Metadata "Block number when entity was created" createdAtBlock: BigInt! @@ -101,6 +101,8 @@ type DataService @entity(immutable: false) { provisions: [Provision!]! @derivedFrom(field: "dataService") "Delegation pools for this data service" delegationPools: [DelegationPool!]! @derivedFrom(field: "dataService") + "Provision thaw requests for this data service" + provisionThawRequests: [ProvisionThawRequest!]! @derivedFrom(field: "dataService") # Counts "Active service providers with provisions to this data service" @@ -185,6 +187,8 @@ type Provision @entity(immutable: false) { serviceProvider: ServiceProvider! "Data service (verifier)" dataService: DataService! + "Thaw requests for this provision" + thawRequests: [ProvisionThawRequest!]! @derivedFrom(field: "provision") # Tokens "Tokens currently provisioned" @@ -216,3 +220,42 @@ type Provision @entity(immutable: false) { "Timestamp when entity was last updated" updatedAt: BigInt! } + +type ProvisionThawRequest @entity(immutable: false) { + "Thaw request ID (bytes32) emitted by ThawRequestCreated event" + id: Bytes! + + # Relationships + "Provision being thawed" + provision: Provision! + "Service provider" + serviceProvider: ServiceProvider! + "Data service (verifier)" + dataService: DataService! + + # State + "Shares being thawed" + shares: BigInt! + "Timestamp when thaw completes" + thawingUntil: BigInt! + "Thawing nonce at time of creation" + thawingNonce: BigInt! + "Tokens withdrawn (set on fulfillment, null while pending)" + tokensWithdrawn: BigInt + + # Status + "False if invalidated by slashing" + valid: Boolean! + "True when tokens have been withdrawn" + fulfilled: Boolean! + + # 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! +} diff --git a/packages/subgraph/src/entities/delegationPool.ts b/packages/subgraph/src/entities/delegationPool.ts index ae9cf33..01f1768 100644 --- a/packages/subgraph/src/entities/delegationPool.ts +++ b/packages/subgraph/src/entities/delegationPool.ts @@ -1,9 +1,10 @@ import { BigInt, Bytes, ethereum } from "@graphprotocol/graph-ts" import { DelegationPool } from "../../generated/schema" import { BIGINT_ZERO } from "../common/constants" +import { twoPartId } from "../common/ids" export function getDelegationPoolId(serviceProvider: Bytes, dataService: Bytes): Bytes { - return serviceProvider.concat(dataService) + return twoPartId(serviceProvider, dataService) } export class DelegationPoolResult { diff --git a/packages/subgraph/src/entities/provision.ts b/packages/subgraph/src/entities/provision.ts index 0c48deb..3dc038d 100644 --- a/packages/subgraph/src/entities/provision.ts +++ b/packages/subgraph/src/entities/provision.ts @@ -1,9 +1,10 @@ import { BigInt, Bytes, ethereum } from "@graphprotocol/graph-ts" import { Provision } from "../../generated/schema" import { BIGINT_ZERO } from "../common/constants" +import { twoPartId } from "../common/ids" export function getProvisionId(serviceProvider: Bytes, dataService: Bytes): Bytes { - return serviceProvider.concat(dataService) + return twoPartId(serviceProvider, dataService) } export class ProvisionResult { diff --git a/packages/subgraph/src/entities/provisionThawRequest.ts b/packages/subgraph/src/entities/provisionThawRequest.ts new file mode 100644 index 0000000..b3d05f3 --- /dev/null +++ b/packages/subgraph/src/entities/provisionThawRequest.ts @@ -0,0 +1,52 @@ +import { BigInt, Bytes, ethereum } from "@graphprotocol/graph-ts" +import { ProvisionThawRequest } from "../../generated/schema" +import { getProvisionId } from "./provision" + +export class ProvisionThawRequestResult { + entity: ProvisionThawRequest + isNew: boolean + + constructor(entity: ProvisionThawRequest, isNew: boolean) { + this.entity = entity + this.isNew = isNew + } +} + +export function getOrCreateProvisionThawRequest( + id: Bytes, + serviceProvider: Bytes, + dataService: Bytes, + blockNumber: BigInt, + timestamp: BigInt +): ProvisionThawRequestResult { + let entity = ProvisionThawRequest.load(id) + let isNew = entity == null + + if (entity == null) { + entity = new ProvisionThawRequest(id) + entity.provision = getProvisionId(serviceProvider, dataService) + entity.serviceProvider = serviceProvider + entity.dataService = dataService + entity.shares = BigInt.zero() + entity.thawingUntil = BigInt.zero() + entity.thawingNonce = BigInt.zero() + entity.tokensWithdrawn = null + entity.valid = true + entity.fulfilled = false + entity.createdAtBlock = blockNumber + entity.createdAt = timestamp + entity.updatedAtBlock = blockNumber + entity.updatedAt = timestamp + } + + return new ProvisionThawRequestResult(entity, isNew) +} + +export function saveProvisionThawRequest( + thawRequest: ProvisionThawRequest, + block: ethereum.Block +): void { + thawRequest.updatedAtBlock = block.number + thawRequest.updatedAt = block.timestamp + thawRequest.save() +} diff --git a/packages/subgraph/src/handlers/thawRequest.ts b/packages/subgraph/src/handlers/thawRequest.ts new file mode 100644 index 0000000..ebb1876 --- /dev/null +++ b/packages/subgraph/src/handlers/thawRequest.ts @@ -0,0 +1,72 @@ +import { Bytes, log } from "@graphprotocol/graph-ts" +import { + ThawRequestCreated, + ThawRequestFulfilled, +} from "../../generated/HorizonStaking/HorizonStaking" +import { ProvisionThawRequest } from "../../generated/schema" +import { + getOrCreateProvisionThawRequest, + saveProvisionThawRequest, +} from "../entities/provisionThawRequest" + +// ThawRequestType enum values from the contract +// 0 = Provision +// 1 = Delegation +const THAW_REQUEST_TYPE_PROVISION = 0 + +/** + * Handles ThawRequestCreated event. + * Creates a ProvisionThawRequest entity when a service provider initiates thawing. + * Only handles provision thaw requests (type 0), ignores delegation thaw requests. + */ +export function handleThawRequestCreated(event: ThawRequestCreated): void { + if (event.params.requestType != THAW_REQUEST_TYPE_PROVISION) { + return + } + + let serviceProviderBytes = Bytes.fromHexString( + event.params.serviceProvider.toHexString() + ) as Bytes + let dataServiceBytes = Bytes.fromHexString( + event.params.verifier.toHexString() + ) as Bytes + + let thawRequest = getOrCreateProvisionThawRequest( + event.params.thawRequestId, + serviceProviderBytes, + dataServiceBytes, + event.block.number, + event.block.timestamp + ) + + assert(thawRequest.isNew, "Thaw request already exists.") + thawRequest.entity.shares = event.params.shares + thawRequest.entity.thawingUntil = event.params.thawingUntil + thawRequest.entity.thawingNonce = event.params.nonce + + saveProvisionThawRequest(thawRequest.entity, event.block) +} + +/** + * Handles ThawRequestFulfilled event. + * Updates a ProvisionThawRequest entity when thawed tokens are withdrawn. + * Only handles provision thaw requests (type 0), ignores delegation thaw requests. + */ +export function handleThawRequestFulfilled(event: ThawRequestFulfilled): void { + if (event.params.requestType != THAW_REQUEST_TYPE_PROVISION) { + return + } + + // Load directly since ThawRequestFulfilled doesn't include serviceProvider/dataService + let thawRequest = ProvisionThawRequest.load(event.params.thawRequestId) + if (thawRequest == null) { + log.critical("Could not find thaw request: {}.", [event.params.thawRequestId.toHexString()]) + return + } + + thawRequest.tokensWithdrawn = event.params.tokens + thawRequest.valid = event.params.valid + thawRequest.fulfilled = true + + saveProvisionThawRequest(thawRequest, event.block) +} diff --git a/packages/subgraph/src/mapping.ts b/packages/subgraph/src/mapping.ts index 14591ae..eb46e8b 100644 --- a/packages/subgraph/src/mapping.ts +++ b/packages/subgraph/src/mapping.ts @@ -21,3 +21,7 @@ export { handleRebateCollected, handleAllocationClosed } from "./handlers/legacy" +export { + handleThawRequestCreated, + handleThawRequestFulfilled +} from "./handlers/thawRequest" diff --git a/packages/subgraph/subgraph.yaml b/packages/subgraph/subgraph.yaml index b028979..7f333da 100644 --- a/packages/subgraph/subgraph.yaml +++ b/packages/subgraph/subgraph.yaml @@ -18,10 +18,10 @@ dataSources: entities: - GraphNetwork - ServiceProvider + - DataService - Provision - DelegationPool - - Delegator - - Delegation + - ProvisionThawRequest abis: - name: HorizonStaking file: ./abis/HorizonStaking.json @@ -59,6 +59,11 @@ dataSources: handler: handleDelegatedTokensWithdrawn - event: DelegationSlashed(indexed address,indexed address,uint256) handler: handleDelegationSlashed + # Thaw request events + - event: ThawRequestCreated(indexed uint8,indexed address,indexed address,address,uint256,uint64,bytes32,uint256) + handler: handleThawRequestCreated + - event: ThawRequestFulfilled(indexed uint8,indexed bytes32,uint256,uint256,uint64,bool) + handler: handleThawRequestFulfilled # Legacy delegation reward events (HorizonStakingExtension) - event: RebateCollected(address,indexed address,indexed bytes32,indexed address,uint256,uint256,uint256,uint256,uint256,uint256,uint256) handler: handleRebateCollected diff --git a/packages/subgraph/tests/thawRequest.test.ts b/packages/subgraph/tests/thawRequest.test.ts new file mode 100644 index 0000000..082ce7b --- /dev/null +++ b/packages/subgraph/tests/thawRequest.test.ts @@ -0,0 +1,383 @@ +import { + describe, + test, + beforeEach, + clearStore, + assert, + newTypedMockEvent, +} from "matchstick-as" +import { Address, BigInt, Bytes, ethereum } from "@graphprotocol/graph-ts" +import { + ThawRequestCreated, + ThawRequestFulfilled, + HorizonStakeDeposited, + ProvisionCreated, +} from "../generated/HorizonStaking/HorizonStaking" +import { + handleThawRequestCreated, + handleThawRequestFulfilled, +} from "../src/handlers/thawRequest" +import { handleHorizonStakeDeposited } from "../src/handlers/staking" +import { handleProvisionCreated } from "../src/handlers/provision" +import { getProvisionId } from "../src/entities/provision" + +// Test addresses +const SP_ADDRESS = Address.fromString("0x1234567890123456789012345678901234567890") +const VERIFIER_ADDRESS = Address.fromString("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd") +const THAW_REQUEST_ID = Bytes.fromHexString("0x1111111111111111111111111111111111111111111111111111111111111111") +const THAW_REQUEST_ID_2 = Bytes.fromHexString("0x2222222222222222222222222222222222222222222222222222222222222222") + +// ThawRequestType enum values +const THAW_REQUEST_TYPE_PROVISION = 0 +const THAW_REQUEST_TYPE_DELEGATION = 1 + +// Helper to create stake deposit +function createStakeDepositedEvent(serviceProvider: Address, tokens: BigInt): HorizonStakeDeposited { + let event = newTypedMockEvent() + event.parameters = new Array() + event.parameters.push(new ethereum.EventParam("serviceProvider", ethereum.Value.fromAddress(serviceProvider))) + event.parameters.push(new ethereum.EventParam("tokens", ethereum.Value.fromUnsignedBigInt(tokens))) + event.block.number = BigInt.fromI32(100) + event.block.timestamp = BigInt.fromI32(1000) + return event +} + +// Helper to create provision +function createProvisionCreatedEvent( + serviceProvider: Address, + verifier: Address, + tokens: BigInt +): ProvisionCreated { + let event = newTypedMockEvent() + event.parameters = new Array() + event.parameters.push(new ethereum.EventParam("serviceProvider", ethereum.Value.fromAddress(serviceProvider))) + event.parameters.push(new ethereum.EventParam("verifier", ethereum.Value.fromAddress(verifier))) + event.parameters.push(new ethereum.EventParam("tokens", ethereum.Value.fromUnsignedBigInt(tokens))) + event.parameters.push(new ethereum.EventParam("maxVerifierCut", ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(100000)))) + event.parameters.push(new ethereum.EventParam("thawingPeriod", ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(2592000)))) + event.block.number = BigInt.fromI32(200) + event.block.timestamp = BigInt.fromI32(2000) + return event +} + +// Helper to create ThawRequestCreated event +function createThawRequestCreatedEvent( + requestType: i32, + serviceProvider: Address, + verifier: Address, + owner: Address, + shares: BigInt, + thawingUntil: BigInt, + thawRequestId: Bytes, + nonce: BigInt +): ThawRequestCreated { + let event = newTypedMockEvent() + event.parameters = new Array() + event.parameters.push(new ethereum.EventParam("requestType", ethereum.Value.fromI32(requestType))) + event.parameters.push(new ethereum.EventParam("serviceProvider", ethereum.Value.fromAddress(serviceProvider))) + event.parameters.push(new ethereum.EventParam("verifier", ethereum.Value.fromAddress(verifier))) + event.parameters.push(new ethereum.EventParam("owner", ethereum.Value.fromAddress(owner))) + event.parameters.push(new ethereum.EventParam("shares", ethereum.Value.fromUnsignedBigInt(shares))) + event.parameters.push(new ethereum.EventParam("thawingUntil", ethereum.Value.fromUnsignedBigInt(thawingUntil))) + event.parameters.push(new ethereum.EventParam("thawRequestId", ethereum.Value.fromBytes(thawRequestId))) + event.parameters.push(new ethereum.EventParam("nonce", ethereum.Value.fromUnsignedBigInt(nonce))) + event.block.number = BigInt.fromI32(300) + event.block.timestamp = BigInt.fromI32(3000) + return event +} + +// Helper to create ThawRequestFulfilled event +function createThawRequestFulfilledEvent( + requestType: i32, + thawRequestId: Bytes, + tokens: BigInt, + shares: BigInt, + thawingUntil: BigInt, + valid: boolean +): ThawRequestFulfilled { + let event = newTypedMockEvent() + event.parameters = new Array() + event.parameters.push(new ethereum.EventParam("requestType", ethereum.Value.fromI32(requestType))) + event.parameters.push(new ethereum.EventParam("thawRequestId", ethereum.Value.fromBytes(thawRequestId))) + event.parameters.push(new ethereum.EventParam("tokens", ethereum.Value.fromUnsignedBigInt(tokens))) + event.parameters.push(new ethereum.EventParam("shares", ethereum.Value.fromUnsignedBigInt(shares))) + event.parameters.push(new ethereum.EventParam("thawingUntil", ethereum.Value.fromUnsignedBigInt(thawingUntil))) + event.parameters.push(new ethereum.EventParam("valid", ethereum.Value.fromBoolean(valid))) + event.block.number = BigInt.fromI32(400) + event.block.timestamp = BigInt.fromI32(4000) + return event +} + +function setupServiceProviderAndProvision(): void { + // Deposit stake + let stakeTokens = BigInt.fromString("10000000000000000000000") // 10000 GRT + let depositEvent = createStakeDepositedEvent(SP_ADDRESS, stakeTokens) + handleHorizonStakeDeposited(depositEvent) + + // Create provision + let provisionTokens = BigInt.fromString("5000000000000000000000") // 5000 GRT + let provisionEvent = createProvisionCreatedEvent(SP_ADDRESS, VERIFIER_ADDRESS, provisionTokens) + handleProvisionCreated(provisionEvent) +} + +function getProvisionIdString(sp: Address, verifier: Address): string { + return getProvisionId(Bytes.fromHexString(sp.toHexString()), Bytes.fromHexString(verifier.toHexString())).toHexString() +} + +describe("ThawRequestCreated", () => { + beforeEach(() => { + clearStore() + }) + + test("creates ProvisionThawRequest entity for provision type", () => { + setupServiceProviderAndProvision() + + let shares = BigInt.fromString("1000000000000000000000") // 1000 shares + let thawingUntil = BigInt.fromI32(3000 + 2592000) // current time + 30 days + let nonce = BigInt.fromI32(1) + + let event = createThawRequestCreatedEvent( + THAW_REQUEST_TYPE_PROVISION, + SP_ADDRESS, + VERIFIER_ADDRESS, + SP_ADDRESS, // owner is SP for provision thaws + shares, + thawingUntil, + THAW_REQUEST_ID, + nonce + ) + handleThawRequestCreated(event) + + // Check entity was created + assert.entityCount("ProvisionThawRequest", 1) + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "shares", shares.toString()) + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "thawingUntil", thawingUntil.toString()) + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "thawingNonce", nonce.toString()) + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "valid", "true") + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "fulfilled", "false") + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "createdAtBlock", "300") + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "createdAt", "3000") + + // Check relationships + let provisionId = getProvisionIdString(SP_ADDRESS, VERIFIER_ADDRESS) + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "provision", provisionId) + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "serviceProvider", SP_ADDRESS.toHexString()) + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "dataService", VERIFIER_ADDRESS.toHexString()) + }) + + test("ignores delegation type thaw requests", () => { + setupServiceProviderAndProvision() + + let shares = BigInt.fromString("1000000000000000000000") + let thawingUntil = BigInt.fromI32(3000 + 2592000) + let nonce = BigInt.fromI32(1) + + let event = createThawRequestCreatedEvent( + THAW_REQUEST_TYPE_DELEGATION, // delegation type + SP_ADDRESS, + VERIFIER_ADDRESS, + Address.fromString("0x9999999999999999999999999999999999999999"), // delegator + shares, + thawingUntil, + THAW_REQUEST_ID, + nonce + ) + handleThawRequestCreated(event) + + // No entity should be created + assert.entityCount("ProvisionThawRequest", 0) + }) + + test("handles multiple thaw requests", () => { + setupServiceProviderAndProvision() + + // First thaw request + let event1 = createThawRequestCreatedEvent( + THAW_REQUEST_TYPE_PROVISION, + SP_ADDRESS, + VERIFIER_ADDRESS, + SP_ADDRESS, + BigInt.fromString("500000000000000000000"), + BigInt.fromI32(3000 + 2592000), + THAW_REQUEST_ID, + BigInt.fromI32(1) + ) + handleThawRequestCreated(event1) + + // Second thaw request + let event2 = createThawRequestCreatedEvent( + THAW_REQUEST_TYPE_PROVISION, + SP_ADDRESS, + VERIFIER_ADDRESS, + SP_ADDRESS, + BigInt.fromString("300000000000000000000"), + BigInt.fromI32(3500 + 2592000), + THAW_REQUEST_ID_2, + BigInt.fromI32(2) + ) + event2.block.number = BigInt.fromI32(350) + event2.block.timestamp = BigInt.fromI32(3500) + handleThawRequestCreated(event2) + + assert.entityCount("ProvisionThawRequest", 2) + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "thawingNonce", "1") + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID_2.toHexString(), "thawingNonce", "2") + }) +}) + +describe("ThawRequestFulfilled", () => { + beforeEach(() => { + clearStore() + }) + + test("marks thaw request as fulfilled with tokens", () => { + setupServiceProviderAndProvision() + + // Create thaw request + let shares = BigInt.fromString("1000000000000000000000") + let thawingUntil = BigInt.fromI32(3000 + 2592000) + let createEvent = createThawRequestCreatedEvent( + THAW_REQUEST_TYPE_PROVISION, + SP_ADDRESS, + VERIFIER_ADDRESS, + SP_ADDRESS, + shares, + thawingUntil, + THAW_REQUEST_ID, + BigInt.fromI32(1) + ) + handleThawRequestCreated(createEvent) + + // Fulfill thaw request + let tokensWithdrawn = BigInt.fromString("1000000000000000000000") // 1000 GRT + let fulfillEvent = createThawRequestFulfilledEvent( + THAW_REQUEST_TYPE_PROVISION, + THAW_REQUEST_ID, + tokensWithdrawn, + shares, + thawingUntil, + true // valid + ) + handleThawRequestFulfilled(fulfillEvent) + + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "fulfilled", "true") + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "valid", "true") + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "tokensWithdrawn", tokensWithdrawn.toString()) + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "updatedAtBlock", "400") + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "updatedAt", "4000") + }) + + test("marks slashed thaw request as invalid", () => { + setupServiceProviderAndProvision() + + // Create thaw request + let shares = BigInt.fromString("1000000000000000000000") + let thawingUntil = BigInt.fromI32(3000 + 2592000) + let createEvent = createThawRequestCreatedEvent( + THAW_REQUEST_TYPE_PROVISION, + SP_ADDRESS, + VERIFIER_ADDRESS, + SP_ADDRESS, + shares, + thawingUntil, + THAW_REQUEST_ID, + BigInt.fromI32(1) + ) + handleThawRequestCreated(createEvent) + + // Fulfill thaw request as invalid (slashed) + let tokensWithdrawn = BigInt.fromString("500000000000000000000") // Less than shares due to slashing + let fulfillEvent = createThawRequestFulfilledEvent( + THAW_REQUEST_TYPE_PROVISION, + THAW_REQUEST_ID, + tokensWithdrawn, + shares, + thawingUntil, + false // invalid due to slashing + ) + handleThawRequestFulfilled(fulfillEvent) + + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "fulfilled", "true") + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "valid", "false") + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "tokensWithdrawn", tokensWithdrawn.toString()) + }) + + test("ignores delegation type fulfillment", () => { + setupServiceProviderAndProvision() + + // Create provision thaw request + let createEvent = createThawRequestCreatedEvent( + THAW_REQUEST_TYPE_PROVISION, + SP_ADDRESS, + VERIFIER_ADDRESS, + SP_ADDRESS, + BigInt.fromString("1000000000000000000000"), + BigInt.fromI32(3000 + 2592000), + THAW_REQUEST_ID, + BigInt.fromI32(1) + ) + handleThawRequestCreated(createEvent) + + // Try to fulfill with delegation type (should be ignored) + let fulfillEvent = createThawRequestFulfilledEvent( + THAW_REQUEST_TYPE_DELEGATION, + THAW_REQUEST_ID, + BigInt.fromString("1000000000000000000000"), + BigInt.fromString("1000000000000000000000"), + BigInt.fromI32(3000 + 2592000), + true + ) + handleThawRequestFulfilled(fulfillEvent) + + // Should still be unfulfilled + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "fulfilled", "false") + }) +}) + +describe("Thaw request lifecycle", () => { + beforeEach(() => { + clearStore() + }) + + test("tracks thaw request from creation to fulfillment", () => { + setupServiceProviderAndProvision() + + let shares = BigInt.fromString("2000000000000000000000") // 2000 shares + let thawingUntil = BigInt.fromI32(3000 + 2592000) + + // 1. Create thaw request + let createEvent = createThawRequestCreatedEvent( + THAW_REQUEST_TYPE_PROVISION, + SP_ADDRESS, + VERIFIER_ADDRESS, + SP_ADDRESS, + shares, + thawingUntil, + THAW_REQUEST_ID, + BigInt.fromI32(1) + ) + handleThawRequestCreated(createEvent) + + // Verify initial state + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "fulfilled", "false") + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "valid", "true") + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "shares", shares.toString()) + + // 2. Fulfill thaw request + let tokensWithdrawn = BigInt.fromString("2000000000000000000000") // Full amount + let fulfillEvent = createThawRequestFulfilledEvent( + THAW_REQUEST_TYPE_PROVISION, + THAW_REQUEST_ID, + tokensWithdrawn, + shares, + thawingUntil, + true + ) + handleThawRequestFulfilled(fulfillEvent) + + // Verify final state + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "fulfilled", "true") + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "valid", "true") + assert.fieldEquals("ProvisionThawRequest", THAW_REQUEST_ID.toHexString(), "tokensWithdrawn", tokensWithdrawn.toString()) + }) +}) diff --git a/packages/tools/src/validation/internal.ts b/packages/tools/src/validation/internal.ts index 3106f0b..6dfb9e2 100644 --- a/packages/tools/src/validation/internal.ts +++ b/packages/tools/src/validation/internal.ts @@ -70,6 +70,14 @@ interface DelegationPool { tokensThawing: string } +interface ProvisionThawRequest { + id: string + provision: { id: string } + serviceProvider: { id: string } + dataService: { id: string } + fulfilled: boolean +} + // ============================================================================ // Queries // ============================================================================ @@ -134,6 +142,16 @@ const DELEGATION_POOLS_QUERY = `{ } }` +const PROVISION_THAW_REQUESTS_QUERY = `{ + provisionThawRequests(first: 1000) { + id + provision { id } + serviceProvider { id } + dataService { id } + fulfilled + } +}` + // ============================================================================ // Main // ============================================================================ @@ -146,12 +164,13 @@ async function main(): Promise { // Fetch all data console.log("=== Fetching subgraph data ===") - const [networkData, spData, dsData, provisionData, poolData] = await Promise.all([ + const [networkData, spData, dsData, provisionData, poolData, thawRequestData] = 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), + querySubgraph<{ provisionThawRequests: ProvisionThawRequest[] }>(subgraphUrl, PROVISION_THAW_REQUESTS_QUERY), ]) const graphNetwork = networkData.graphNetwork @@ -164,6 +183,7 @@ async function main(): Promise { const dataServices = dsData.dataServices const provisions = provisionData.provisions const pools = poolData.delegationPools + const thawRequests = thawRequestData.provisionThawRequests // Filter to only SPs with stake > 0 (matches countServiceProviders semantics) const stakedSPs = serviceProviders.filter((sp) => BigInt(sp.tokensStaked) > 0n) @@ -171,11 +191,16 @@ async function main(): Promise { // Filter to only pools with tokens > 0 (matches countDelegationPools semantics) const activePools = pools.filter((p) => BigInt(p.tokens) > 0n) + // Filter thaw requests by status + const pendingThawRequests = thawRequests.filter((t) => !t.fulfilled) + const fulfilledThawRequests = thawRequests.filter((t) => t.fulfilled) + 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(` ProvisionThawRequests: ${thawRequests.length} total, ${pendingThawRequests.length} pending, ${fulfilledThawRequests.length} fulfilled`) console.log("") // ============================================================================ @@ -375,6 +400,50 @@ async function main(): Promise { warnings += dsWarnings + // ============================================================================ + // ProvisionThawRequest Referential Integrity + // ============================================================================ + + console.log("=== ProvisionThawRequest Referential Integrity ===") + let trWarnings = 0 + + // Build lookup sets for fast existence checks + const provisionIds = new Set(provisions.map((p) => p.id)) + const spIds = new Set(serviceProviders.map((sp) => sp.id)) + const dsIds = new Set(dataServices.map((ds) => ds.id)) + + for (const tr of thawRequests) { + const issues: string[] = [] + + if (!provisionIds.has(tr.provision.id)) { + issues.push(`references non-existent Provision: ${tr.provision.id}`) + } + + if (!spIds.has(tr.serviceProvider.id)) { + issues.push(`references non-existent ServiceProvider: ${tr.serviceProvider.id}`) + } + + if (!dsIds.has(tr.dataService.id)) { + issues.push(`references non-existent DataService: ${tr.dataService.id}`) + } + + if (issues.length > 0) { + trWarnings++ + console.log(`WARNING: ${tr.id}`) + for (const issue of issues) { + console.log(` ${issue}`) + } + console.log("") + } + } + + if (trWarnings === 0) { + console.log("All ProvisionThawRequest references are valid!") + console.log("") + } + + warnings += trWarnings + // ============================================================================ // Summary // ============================================================================