Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { createLogger, Logger } from '@graphprotocol/common-ts'
import { AllocationManager } from '../allocations'
import { IndexingStatusCode } from '../types'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
declare const __LOG_LEVEL__: any

const indexer = '0x0000000000000000000000000000000000000001'
const allocationID = '0x000000000000000000000000000000000000000a'

const buildAllocationManager = (isOverAllocated: boolean) => {
const subgraphService = {
isOverAllocated: jest.fn().mockResolvedValue(isOverAllocated),
interface: { encodeFunctionData: jest.fn((name: string) => name) },
multicall: {
populateTransaction: jest.fn(async (calls: string[]) => ({ data: calls })),
},
collect: {
populateTransaction: jest.fn(async () => ({ data: 'collect-only' })),
},
}
const network = { contracts: { SubgraphService: subgraphService } }
const manager = new AllocationManager(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
undefined as any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
undefined as any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
undefined as any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
network as any,
)
return { manager, subgraphService }
}

describe('populateUnallocateTransaction', () => {
let logger: Logger

beforeAll(() => {
logger = createLogger({
name: 'unallocate-test',
async: false,
level: __LOG_LEVEL__ ?? 'error',
})
})

// An over-allocated collect resizes the allocation to zero rather than closing it,
// so stopService is still needed to close it.
it.each([false, true])(
'multicalls collect and stopService when isOverAllocated is %s',
async (isOverAllocated) => {
const { manager, subgraphService } = buildAllocationManager(isOverAllocated)

const tx = await manager.populateUnallocateTransaction(logger, {
allocationID,
poi: {
poi: '0x' + '11'.repeat(32),
publicPOI: '0x' + '22'.repeat(32),
blockNumber: 1,
indexingStatus: IndexingStatusCode.Healthy,
},
isLegacy: false,
indexer,
actionID: 1,
protocolNetwork: 'eip155:42161',
})

expect(subgraphService.multicall.populateTransaction).toHaveBeenCalledWith([
'collect',
'stopService',
])
expect(subgraphService.collect.populateTransaction).not.toHaveBeenCalled()
expect(tx.data).toEqual(['collect', 'stopService'])
},
)
})
24 changes: 2 additions & 22 deletions packages/indexer-common/src/indexer-management/allocations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1221,28 +1221,8 @@ export class AllocationManager {
}
}

// Horizon: Need to collect indexing rewards and stop service
// Check if indexer is over-allocated - if so, collect() will auto-close the allocation
// and we should NOT call stopService to avoid "AllocationClosed" revert
const isOverAllocated = await this.network.contracts.SubgraphService.isOverAllocated(
params.indexer,
)

logger.debug('Checking over-allocation status for unallocate', {
allocationID: params.allocationID,
isOverAllocated,
})

if (isOverAllocated) {
// Reuse populatePresentPOITransaction - collect will auto-close the allocation
logger.info(
'Indexer is over-allocated, using collect-only transaction (allocation will auto-close)',
{ allocationID: params.allocationID },
)
return await this.populatePresentPOITransaction(logger, params)
}

// Normal path: multicall collect + stopService
// Horizon: collect indexing rewards and stop service. When over-allocated, collect
// resizes the allocation to zero instead of closing it, so stopService is still needed.
const collectData = encodeCollectData(params.allocationID, params.poi)
const collectCallData =
this.network.contracts.SubgraphService.interface.encodeFunctionData('collect', [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -827,85 +827,57 @@ async function closeHorizonAllocation(
throw indexerError(IndexerErrorCode.IE065, 'Allocation has already been closed')
}

// Check if indexer is over-allocated - if so, collect() will auto-close the allocation
// and we should NOT call stopService to avoid "AllocationClosed" revert
const isOverAllocated = await contracts.SubgraphService.isOverAllocated(address)

logger.debug('Checking over-allocation status for close allocation', {
allocationId: allocation.id,
isOverAllocated,
})

// When over-allocated, collect resizes the allocation to zero instead of closing it,
// so stopService is still needed.
const collectData = encodeCollectData(allocation.id, poiData)
const collectCallData = contracts.SubgraphService.interface.encodeFunctionData(
'collect',
[address, PaymentTypes.IndexingRewards, collectData],
)
const closeAllocationData = encodeStopServiceData(allocation.id)
const stopServiceCallData = contracts.SubgraphService.interface.encodeFunctionData(
'stopService',
[address, closeAllocationData],
)

let receipt: TransactionReceipt | 'paused' | 'unauthorized'
let rewardsAssigned: bigint

if (isOverAllocated) {
// Reuse shared collect logic - allocation will auto-close
logger.info(
'Indexer is over-allocated, using collect-only transaction (allocation will auto-close)',
{ allocationId: allocation.id },
)
const result = await executeCollectTransaction(
network,
allocation.id,
collectData,
logger,
)
receipt = result.receipt
rewardsAssigned = result.rewardsCollected
} else {
// Normal path: multicall collect + stopService
const collectCallData = contracts.SubgraphService.interface.encodeFunctionData(
'collect',
[address, PaymentTypes.IndexingRewards, collectData],
)
const closeAllocationData = encodeStopServiceData(allocation.id)
const stopServiceCallData = contracts.SubgraphService.interface.encodeFunctionData(
'stopService',
[address, closeAllocationData],
)
const receipt = await transactionManager.executeTransaction(
async () =>
contracts.SubgraphService.multicall.estimateGas([
collectCallData,
stopServiceCallData,
]),
async (gasLimit) =>
contracts.SubgraphService.multicall([collectCallData, stopServiceCallData], {
gasLimit,
}),
logger,
)

receipt = await transactionManager.executeTransaction(
async () =>
contracts.SubgraphService.multicall.estimateGas([
collectCallData,
stopServiceCallData,
]),
async (gasLimit) =>
contracts.SubgraphService.multicall([collectCallData, stopServiceCallData], {
gasLimit,
}),
logger,
if (receipt === 'paused' || receipt === 'unauthorized') {
throw indexerError(
IndexerErrorCode.IE062,
`Allocation '${allocation.id}' could not be closed: ${receipt}`,
)
}

if (receipt === 'paused' || receipt === 'unauthorized') {
throw indexerError(
IndexerErrorCode.IE062,
`Allocation '${allocation.id}' could not be closed: ${receipt}`,
)
}
const collectEventLogs = transactionManager.findEvent(
'ServicePaymentCollected',
contracts.SubgraphService.interface,
'serviceProvider',
address,
receipt,
logger,
)

const collectEventLogs = transactionManager.findEvent(
'ServicePaymentCollected',
contracts.SubgraphService.interface,
'serviceProvider',
address,
receipt,
logger,
if (!collectEventLogs) {
throw indexerError(
IndexerErrorCode.IE015,
`Collecting indexing rewards for allocation '${allocation.id}' failed`,
)

if (!collectEventLogs) {
throw indexerError(
IndexerErrorCode.IE015,
`Collecting indexing rewards for allocation '${allocation.id}' failed`,
)
}

rewardsAssigned = collectEventLogs.tokens ?? 0n
}

const rewardsAssigned: bigint = collectEventLogs.tokens ?? 0n

if (rewardsAssigned === 0n) {
logger.warn('No rewards were distributed upon closing the allocation')
}
Expand Down
Loading