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..c7e8bc819 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,27 @@ 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 },