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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/subgraph/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,8 @@ type Provision @entity(immutable: false) {
dataService: DataService!
"Thaw requests for this provision"
thawRequests: [ProvisionThawRequest!]! @derivedFrom(field: "provision")
"Fee cuts for delegators by payment type"
feeCuts: [ProvisionFeeCut!]! @derivedFrom(field: "provision")

# Tokens
"Tokens currently provisioned"
Expand Down Expand Up @@ -221,6 +223,27 @@ type Provision @entity(immutable: false) {
updatedAt: BigInt!
}

type ProvisionFeeCut @entity(immutable: false) {
"Composite ID: provision-paymentType"
id: Bytes!

# Relationships
"Provision this fee cut belongs to"
provision: Provision!

# State
"Payment type (maps to PaymentTypes enum: 0 = QueryFee, 1 = IndexingFee, 2 = IndexingReward, ...)"
paymentType: Int!
"Fee cut percentage (PPM)"
feeCut: BigInt!

# Metadata
"Block number when entity was last updated"
updatedAtBlock: BigInt!
"Timestamp when entity was last updated"
updatedAt: BigInt!
}

type ProvisionThawRequest @entity(immutable: false) {
"Thaw request ID (bytes32) emitted by ThawRequestCreated event"
id: Bytes!
Expand Down
48 changes: 48 additions & 0 deletions packages/subgraph/src/entities/provisionFeeCut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { BigInt, Bytes, ethereum } from "@graphprotocol/graph-ts"
import { ProvisionFeeCut } from "../../generated/schema"
import { BIGINT_ZERO } from "../common/constants"
import { getProvisionId } from "./provision"

export function getProvisionFeeCutId(serviceProvider: Bytes, dataService: Bytes, paymentType: i32): Bytes {
let provisionId = getProvisionId(serviceProvider, dataService)
return provisionId.concat(Bytes.fromI32(paymentType))
}

export class ProvisionFeeCutResult {
entity: ProvisionFeeCut
isNew: boolean

constructor(entity: ProvisionFeeCut, isNew: boolean) {
this.entity = entity
this.isNew = isNew
}
}

export function getOrCreateProvisionFeeCut(
serviceProvider: Bytes,
dataService: Bytes,
paymentType: i32,
blockNumber: BigInt,
timestamp: BigInt
): ProvisionFeeCutResult {
let id = getProvisionFeeCutId(serviceProvider, dataService, paymentType)
let entity = ProvisionFeeCut.load(id)
let isNew = entity == null

if (entity == null) {
entity = new ProvisionFeeCut(id)
entity.provision = getProvisionId(serviceProvider, dataService)
entity.paymentType = paymentType
entity.feeCut = BIGINT_ZERO
entity.updatedAtBlock = blockNumber
entity.updatedAt = timestamp
}

return new ProvisionFeeCutResult(entity, isNew)
}

export function saveProvisionFeeCut(feeCut: ProvisionFeeCut, block: ethereum.Block): void {
feeCut.updatedAtBlock = block.number
feeCut.updatedAt = block.timestamp
feeCut.save()
}
32 changes: 32 additions & 0 deletions packages/subgraph/src/handlers/feeCut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Bytes } from "@graphprotocol/graph-ts"
import { DelegationFeeCutSet } from "../../generated/HorizonStaking/HorizonStaking"
import {
getOrCreateProvisionFeeCut,
saveProvisionFeeCut,
} from "../entities/provisionFeeCut"

/**
* Handles DelegationFeeCutSet event.
* Creates or updates a ProvisionFeeCut entity when a service provider sets
* their fee cut percentage for a specific payment type on a provision.
*/
export function handleDelegationFeeCutSet(event: DelegationFeeCutSet): void {
let serviceProviderBytes = Bytes.fromHexString(
event.params.serviceProvider.toHexString()
) as Bytes
let dataServiceBytes = Bytes.fromHexString(
event.params.verifier.toHexString()
) as Bytes

let feeCut = getOrCreateProvisionFeeCut(
serviceProviderBytes,
dataServiceBytes,
event.params.paymentType,
event.block.number,
event.block.timestamp
)

feeCut.entity.feeCut = event.params.feeCut

saveProvisionFeeCut(feeCut.entity, event.block)
}
1 change: 1 addition & 0 deletions packages/subgraph/src/mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ export {
handleThawRequestCreated,
handleThawRequestFulfilled
} from "./handlers/thawRequest"
export { handleDelegationFeeCutSet } from "./handlers/feeCut"
3 changes: 3 additions & 0 deletions packages/subgraph/subgraph.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dataSources:
- Provision
- DelegationPool
- ProvisionThawRequest
- ProvisionFeeCut
abis:
- name: HorizonStaking
file: ./abis/HorizonStaking.json
Expand Down Expand Up @@ -59,6 +60,8 @@ dataSources:
handler: handleDelegatedTokensWithdrawn
- event: DelegationSlashed(indexed address,indexed address,uint256)
handler: handleDelegationSlashed
- event: DelegationFeeCutSet(indexed address,indexed address,indexed uint8,uint256)
handler: handleDelegationFeeCutSet
# Thaw request events
- event: ThawRequestCreated(indexed uint8,indexed address,indexed address,address,uint256,uint64,bytes32,uint256)
handler: handleThawRequestCreated
Expand Down
254 changes: 254 additions & 0 deletions packages/subgraph/tests/feeCut.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import {
describe,
test,
beforeEach,
clearStore,
assert,
newTypedMockEvent,
} from "matchstick-as"
import { Address, BigInt, Bytes, ethereum } from "@graphprotocol/graph-ts"
import {
DelegationFeeCutSet,
HorizonStakeDeposited,
ProvisionCreated,
} from "../generated/HorizonStaking/HorizonStaking"
import { handleDelegationFeeCutSet } from "../src/handlers/feeCut"
import { handleHorizonStakeDeposited } from "../src/handlers/staking"
import { handleProvisionCreated } from "../src/handlers/provision"
import { getProvisionFeeCutId } from "../src/entities/provisionFeeCut"
import { getProvisionId } from "../src/entities/provision"

// Test addresses
const SP_ADDRESS = Address.fromString("0x1234567890123456789012345678901234567890")
const VERIFIER_ADDRESS = Address.fromString("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd")

// Payment types (from IGraphPayments.PaymentTypes enum)
const PAYMENT_TYPE_QUERY_FEE = 0
const PAYMENT_TYPE_INDEXING_FEE = 1
const PAYMENT_TYPE_INDEXING_REWARD = 2

// Helper to create stake deposit
function createStakeDepositedEvent(serviceProvider: Address, tokens: BigInt): HorizonStakeDeposited {
let event = newTypedMockEvent<HorizonStakeDeposited>()
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<ProvisionCreated>()
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 DelegationFeeCutSet event
function createDelegationFeeCutSetEvent(
serviceProvider: Address,
verifier: Address,
paymentType: i32,
feeCut: BigInt
): DelegationFeeCutSet {
let event = newTypedMockEvent<DelegationFeeCutSet>()
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("paymentType", ethereum.Value.fromI32(paymentType)))
event.parameters.push(new ethereum.EventParam("feeCut", ethereum.Value.fromUnsignedBigInt(feeCut)))
event.block.number = BigInt.fromI32(300)
event.block.timestamp = BigInt.fromI32(3000)
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 getFeeCutIdString(sp: Address, verifier: Address, paymentType: i32): string {
return getProvisionFeeCutId(
Bytes.fromHexString(sp.toHexString()),
Bytes.fromHexString(verifier.toHexString()),
paymentType
).toHexString()
}

function getProvisionIdString(sp: Address, verifier: Address): string {
return getProvisionId(
Bytes.fromHexString(sp.toHexString()),
Bytes.fromHexString(verifier.toHexString())
).toHexString()
}

describe("DelegationFeeCutSet", () => {
beforeEach(() => {
clearStore()
})

test("creates ProvisionFeeCut entity for query fee type", () => {
setupServiceProviderAndProvision()

let feeCut = BigInt.fromI32(100000) // 10% in PPM
let event = createDelegationFeeCutSetEvent(
SP_ADDRESS,
VERIFIER_ADDRESS,
PAYMENT_TYPE_QUERY_FEE,
feeCut
)
handleDelegationFeeCutSet(event)

let entityId = getFeeCutIdString(SP_ADDRESS, VERIFIER_ADDRESS, PAYMENT_TYPE_QUERY_FEE)
let provisionId = getProvisionIdString(SP_ADDRESS, VERIFIER_ADDRESS)

assert.entityCount("ProvisionFeeCut", 1)
assert.fieldEquals("ProvisionFeeCut", entityId, "provision", provisionId)
assert.fieldEquals("ProvisionFeeCut", entityId, "paymentType", PAYMENT_TYPE_QUERY_FEE.toString())
assert.fieldEquals("ProvisionFeeCut", entityId, "feeCut", feeCut.toString())
assert.fieldEquals("ProvisionFeeCut", entityId, "updatedAtBlock", "300")
assert.fieldEquals("ProvisionFeeCut", entityId, "updatedAt", "3000")
})

test("creates separate entities for different payment types", () => {
setupServiceProviderAndProvision()

// Set query fee cut
let queryFeeCut = BigInt.fromI32(100000) // 10%
let queryEvent = createDelegationFeeCutSetEvent(
SP_ADDRESS,
VERIFIER_ADDRESS,
PAYMENT_TYPE_QUERY_FEE,
queryFeeCut
)
handleDelegationFeeCutSet(queryEvent)

// Set indexing fee cut
let indexingFeeCut = BigInt.fromI32(200000) // 20%
let indexingEvent = createDelegationFeeCutSetEvent(
SP_ADDRESS,
VERIFIER_ADDRESS,
PAYMENT_TYPE_INDEXING_FEE,
indexingFeeCut
)
indexingEvent.block.number = BigInt.fromI32(301)
indexingEvent.block.timestamp = BigInt.fromI32(3100)
handleDelegationFeeCutSet(indexingEvent)

// Set indexing reward cut
let rewardFeeCut = BigInt.fromI32(50000) // 5%
let rewardEvent = createDelegationFeeCutSetEvent(
SP_ADDRESS,
VERIFIER_ADDRESS,
PAYMENT_TYPE_INDEXING_REWARD,
rewardFeeCut
)
rewardEvent.block.number = BigInt.fromI32(302)
rewardEvent.block.timestamp = BigInt.fromI32(3200)
handleDelegationFeeCutSet(rewardEvent)

assert.entityCount("ProvisionFeeCut", 3)

let queryId = getFeeCutIdString(SP_ADDRESS, VERIFIER_ADDRESS, PAYMENT_TYPE_QUERY_FEE)
let indexingId = getFeeCutIdString(SP_ADDRESS, VERIFIER_ADDRESS, PAYMENT_TYPE_INDEXING_FEE)
let rewardId = getFeeCutIdString(SP_ADDRESS, VERIFIER_ADDRESS, PAYMENT_TYPE_INDEXING_REWARD)

assert.fieldEquals("ProvisionFeeCut", queryId, "paymentType", PAYMENT_TYPE_QUERY_FEE.toString())
assert.fieldEquals("ProvisionFeeCut", queryId, "feeCut", queryFeeCut.toString())

assert.fieldEquals("ProvisionFeeCut", indexingId, "paymentType", PAYMENT_TYPE_INDEXING_FEE.toString())
assert.fieldEquals("ProvisionFeeCut", indexingId, "feeCut", indexingFeeCut.toString())

assert.fieldEquals("ProvisionFeeCut", rewardId, "paymentType", PAYMENT_TYPE_INDEXING_REWARD.toString())
assert.fieldEquals("ProvisionFeeCut", rewardId, "feeCut", rewardFeeCut.toString())
})

test("updates existing fee cut for same payment type", () => {
setupServiceProviderAndProvision()

// Initial fee cut
let initialFeeCut = BigInt.fromI32(100000) // 10%
let event1 = createDelegationFeeCutSetEvent(
SP_ADDRESS,
VERIFIER_ADDRESS,
PAYMENT_TYPE_QUERY_FEE,
initialFeeCut
)
handleDelegationFeeCutSet(event1)

let entityId = getFeeCutIdString(SP_ADDRESS, VERIFIER_ADDRESS, PAYMENT_TYPE_QUERY_FEE)
assert.fieldEquals("ProvisionFeeCut", entityId, "feeCut", initialFeeCut.toString())
assert.fieldEquals("ProvisionFeeCut", entityId, "updatedAtBlock", "300")

// Update fee cut
let updatedFeeCut = BigInt.fromI32(150000) // 15%
let event2 = createDelegationFeeCutSetEvent(
SP_ADDRESS,
VERIFIER_ADDRESS,
PAYMENT_TYPE_QUERY_FEE,
updatedFeeCut
)
event2.block.number = BigInt.fromI32(400)
event2.block.timestamp = BigInt.fromI32(4000)
handleDelegationFeeCutSet(event2)

// Should still be 1 entity, but updated
assert.entityCount("ProvisionFeeCut", 1)
assert.fieldEquals("ProvisionFeeCut", entityId, "feeCut", updatedFeeCut.toString())
assert.fieldEquals("ProvisionFeeCut", entityId, "updatedAtBlock", "400")
assert.fieldEquals("ProvisionFeeCut", entityId, "updatedAt", "4000")
})

test("handles fee cut set to zero", () => {
setupServiceProviderAndProvision()

// Set fee cut to zero (100% goes to delegators)
let feeCut = BigInt.fromI32(0)
let event = createDelegationFeeCutSetEvent(
SP_ADDRESS,
VERIFIER_ADDRESS,
PAYMENT_TYPE_QUERY_FEE,
feeCut
)
handleDelegationFeeCutSet(event)

let entityId = getFeeCutIdString(SP_ADDRESS, VERIFIER_ADDRESS, PAYMENT_TYPE_QUERY_FEE)
assert.fieldEquals("ProvisionFeeCut", entityId, "feeCut", "0")
})

test("handles max fee cut (100%)", () => {
setupServiceProviderAndProvision()

// Set fee cut to 100% (1000000 PPM)
let feeCut = BigInt.fromI32(1000000) // 100% in PPM
let event = createDelegationFeeCutSetEvent(
SP_ADDRESS,
VERIFIER_ADDRESS,
PAYMENT_TYPE_QUERY_FEE,
feeCut
)
handleDelegationFeeCutSet(event)

let entityId = getFeeCutIdString(SP_ADDRESS, VERIFIER_ADDRESS, PAYMENT_TYPE_QUERY_FEE)
assert.fieldEquals("ProvisionFeeCut", entityId, "feeCut", feeCut.toString())
})
})
Loading
Loading