From b646ad016baf39e1db841831a00aa3f2b845f5ac Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Fri, 7 Aug 2026 19:57:35 +0300 Subject: [PATCH 1/2] fix(dips): build the POI reference from the deployment chain Collections took a recent block number from the protocol chain and looked up its hash on the deployment's own network, so an agreement indexing any other chain asked for a block that chain never produced and could never collect. The POI now uses the deployment's indexed head. --- .../__tests__/collect-agreements.test.ts | 48 +++++++++++++++++++ .../src/indexing-fees/__tests__/dips.test.ts | 30 ++++++++++-- .../indexer-common/src/indexing-fees/dips.ts | 21 +++++--- 3 files changed, 87 insertions(+), 12 deletions(-) diff --git a/packages/indexer-common/src/indexing-fees/__tests__/collect-agreements.test.ts b/packages/indexer-common/src/indexing-fees/__tests__/collect-agreements.test.ts index dada6f29e..7231320bf 100644 --- a/packages/indexer-common/src/indexing-fees/__tests__/collect-agreements.test.ts +++ b/packages/indexer-common/src/indexing-fees/__tests__/collect-agreements.test.ts @@ -41,6 +41,16 @@ const mockGraphNode = { proofOfIndexing: jest.fn(), blockHashFromNumber: jest.fn(), subgraphFeatures: jest.fn().mockResolvedValue({ network: 'mainnet' }), + indexingStatus: jest.fn().mockResolvedValue([ + { + chains: [ + { + network: 'mainnet', + latestBlock: { number: '500', hash: '0x' + '00'.repeat(32) }, + }, + ], + }, + ]), } as any const mockNetwork = { @@ -141,6 +151,44 @@ describe('DipsManager.collectAgreementPayments', () => { expect(mockExecuteTransaction).toHaveBeenCalledTimes(1) }) + test("builds the POI reference from the deployment chain's indexed head, not the protocol chain", async () => { + mockQuery.mockResolvedValueOnce({ + data: { indexingAgreements: [makeReadyAgreement()] }, + }) + + // Protocol chain (mock provider) sits at block 1000; the deployment indexes + // a chain whose indexed head is 500. The POI must reference the latter. + mockGraphNode.entityCount.mockResolvedValueOnce([500]) + mockGraphNode.blockHashFromNumber.mockResolvedValueOnce('0x' + 'ab'.repeat(32)) + mockGraphNode.proofOfIndexing.mockResolvedValueOnce('0x' + 'cd'.repeat(32)) + mockExecuteTransaction.mockResolvedValueOnce({ hash: '0xtxhash', status: 1 }) + + const dm = createDipsManager() + await dm.collectAgreementPayments() + + expect(mockGraphNode.blockHashFromNumber).toHaveBeenCalledWith('mainnet', 490) + expect(mockGraphNode.proofOfIndexing).toHaveBeenCalledWith( + expect.anything(), + { number: 490, hash: '0x' + 'ab'.repeat(32) }, + expect.anything(), + ) + }) + + test('does not attempt collection when the deployment has no indexed block', async () => { + mockQuery.mockResolvedValueOnce({ + data: { indexingAgreements: [makeReadyAgreement()] }, + }) + + mockGraphNode.entityCount.mockResolvedValueOnce([500]) + mockGraphNode.indexingStatus.mockResolvedValueOnce([{ chains: [] }]) + + const dm = createDipsManager() + await dm.collectAgreementPayments() + + expect(mockExecuteTransaction).not.toHaveBeenCalled() + expect(logger.warn).toHaveBeenCalled() + }) + test('updates tracker after successful collection', async () => { mockQuery .mockResolvedValueOnce({ data: { indexingAgreements: [makeReadyAgreement()] } }) diff --git a/packages/indexer-common/src/indexing-fees/__tests__/dips.test.ts b/packages/indexer-common/src/indexing-fees/__tests__/dips.test.ts index 3c4871de7..d03d18c97 100644 --- a/packages/indexer-common/src/indexing-fees/__tests__/dips.test.ts +++ b/packages/indexer-common/src/indexing-fees/__tests__/dips.test.ts @@ -910,7 +910,11 @@ describe('DipsManager', () => { // Mock block number and graph node methods for collect network.networkProvider.getBlockNumber = jest.fn().mockResolvedValue(100) graphNode.entityCount = jest.fn().mockResolvedValue([250000]) - graphNode.subgraphFeatures = jest.fn().mockResolvedValue({ network: 'mainnet' }) + graphNode.indexingStatus = jest.fn().mockResolvedValue([ + { + chains: [{ network: 'mainnet', latestBlock: { number: '90', hash: '0xh' } }], + }, + ]) graphNode.blockHashFromNumber = jest.fn().mockResolvedValue('0xblockhash') graphNode.proofOfIndexing = jest .fn() @@ -940,7 +944,11 @@ describe('DipsManager', () => { network.networkProvider.getBlockNumber = jest.fn().mockResolvedValue(100) graphNode.entityCount = jest.fn().mockResolvedValue([250000]) - graphNode.subgraphFeatures = jest.fn().mockResolvedValue({ network: 'mainnet' }) + graphNode.indexingStatus = jest.fn().mockResolvedValue([ + { + chains: [{ network: 'mainnet', latestBlock: { number: '90', hash: '0xh' } }], + }, + ]) graphNode.blockHashFromNumber = jest.fn().mockResolvedValue('0xblockhash') graphNode.proofOfIndexing = jest .fn() @@ -968,7 +976,11 @@ describe('DipsManager', () => { // Mock block number and graph node methods network.networkProvider.getBlockNumber = jest.fn().mockResolvedValue(100) graphNode.entityCount = jest.fn().mockResolvedValue([250000]) - graphNode.subgraphFeatures = jest.fn().mockResolvedValue({ network: 'mainnet' }) + graphNode.indexingStatus = jest.fn().mockResolvedValue([ + { + chains: [{ network: 'mainnet', latestBlock: { number: '90', hash: '0xh' } }], + }, + ]) graphNode.blockHashFromNumber = jest.fn().mockResolvedValue('0xblockhash') graphNode.proofOfIndexing = jest .fn() @@ -995,7 +1007,11 @@ describe('DipsManager', () => { network.networkProvider.getBlockNumber = jest.fn().mockResolvedValue(100) graphNode.entityCount = jest.fn().mockResolvedValue([250000]) - graphNode.subgraphFeatures = jest.fn().mockResolvedValue({ network: 'mainnet' }) + graphNode.indexingStatus = jest.fn().mockResolvedValue([ + { + chains: [{ network: 'mainnet', latestBlock: { number: '90', hash: '0xh' } }], + }, + ]) graphNode.blockHashFromNumber = jest.fn().mockResolvedValue('0xblockhash') graphNode.proofOfIndexing = jest .fn() @@ -1021,7 +1037,11 @@ describe('DipsManager', () => { network.networkProvider.getBlockNumber = jest.fn().mockResolvedValue(100) graphNode.entityCount = jest.fn().mockResolvedValue([250000]) - graphNode.subgraphFeatures = jest.fn().mockResolvedValue({ network: 'mainnet' }) + graphNode.indexingStatus = jest.fn().mockResolvedValue([ + { + chains: [{ network: 'mainnet', latestBlock: { number: '90', hash: '0xh' } }], + }, + ]) graphNode.blockHashFromNumber = jest.fn().mockResolvedValue('0xblockhash') graphNode.proofOfIndexing = jest .fn() diff --git a/packages/indexer-common/src/indexing-fees/dips.ts b/packages/indexer-common/src/indexing-fees/dips.ts index ed5dafe97..f5a9b96c3 100644 --- a/packages/indexer-common/src/indexing-fees/dips.ts +++ b/packages/indexer-common/src/indexing-fees/dips.ts @@ -795,8 +795,7 @@ export class DipsManager { // Step 1: Best-effort final collection BEFORE cancelling. try { - const blockNumber = await this.network.networkProvider.getBlockNumber() - await this.tryCollectAgreement(agreement, blockNumber, logger) + await this.tryCollectAgreement(agreement, logger) logger.info('Final collection succeeded before cancel') } catch (err) { const errorMsg = err instanceof Error ? err.message : String(err) @@ -939,7 +938,7 @@ export class DipsManager { for (const agreement of readyAgreements) { try { - const result = await this.tryCollectAgreement(agreement, blockNumber, logger) + const result = await this.tryCollectAgreement(agreement, logger) if (result === 'collected') { this.collectionTracker.updateAfterCollection(agreement.id, nowSeconds) this.cleanupFinishedAgreement(agreement, nowSeconds, logger) @@ -995,16 +994,24 @@ export class DipsManager { private async tryCollectAgreement( agreement: SubgraphIndexingAgreement, - blockNumber: number, logger: Logger, ): Promise<'collected' | 'paused' | 'unauthorized'> { const deploymentId = new SubgraphDeploymentID(agreement.subgraphDeploymentId) const entityCounts = await this.graphNode.entityCount([deploymentId]) const entities = entityCounts[0] - const recentBlock = blockNumber - RECENT_BLOCK_OFFSET - const { network: networkAlias } = await this.graphNode.subgraphFeatures(deploymentId) - const blockHash = await this.graphNode.blockHashFromNumber(networkAlias!, recentBlock) + // The POI block must belong to the chain the deployment indexes, which need + // not be the protocol chain, so the deployment's indexed head is the base. + const [status] = await this.graphNode.indexingStatus([deploymentId]) + const chain = status?.chains?.[0] + if (!chain?.network || !chain?.latestBlock) { + throw new Error( + `Deployment ${deploymentId.ipfsHash} has no indexed block to reference a POI against`, + ) + } + const networkAlias = chain.network + const recentBlock = Math.max(Number(chain.latestBlock.number) - RECENT_BLOCK_OFFSET, 0) + const blockHash = await this.graphNode.blockHashFromNumber(networkAlias, recentBlock) const poi = await this.graphNode.proofOfIndexing( deploymentId, { number: recentBlock, hash: blockHash }, From 7533b4fda05f5c00720153f39f290455a690b115 Mon Sep 17 00:00:00 2001 From: MoonBoi9001 Date: Fri, 7 Aug 2026 20:15:31 +0300 Subject: [PATCH 2/2] style(dips): apply prettier formatting to the POI reference change --- packages/indexer-common/src/indexing-fees/dips.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/indexer-common/src/indexing-fees/dips.ts b/packages/indexer-common/src/indexing-fees/dips.ts index f5a9b96c3..c7e8bc819 100644 --- a/packages/indexer-common/src/indexing-fees/dips.ts +++ b/packages/indexer-common/src/indexing-fees/dips.ts @@ -1010,7 +1010,10 @@ export class DipsManager { ) } const networkAlias = chain.network - const recentBlock = Math.max(Number(chain.latestBlock.number) - RECENT_BLOCK_OFFSET, 0) + const recentBlock = Math.max( + Number(chain.latestBlock.number) - RECENT_BLOCK_OFFSET, + 0, + ) const blockHash = await this.graphNode.blockHashFromNumber(networkAlias, recentBlock) const poi = await this.graphNode.proofOfIndexing( deploymentId,