Skip to content

Commit 9a65178

Browse files
MoonBoi9001claude
andcommitted
feat: on-chain acceptance fixes for DIPs recurring agreements
Three fixes that together enable end-to-end DIPs on-chain acceptance: 1. Pass SubgraphService (not HorizonStaking) to RewardsManager.getRewards in the allocation stakeUsageSummary call. 2. Decouple DIPs proposal acceptance from the 120s reconciliation loop into a dedicated 5s polling loop (startProposalAcceptanceLoop). The 300s RCA deadline left insufficient slack with the old 240s+ latency. 3. Log full revert context (reason, data, message, contract target) when on-chain acceptance fails, instead of just the parsed error (which was null for unknown custom errors). Test metadata version updated to 0 to match the Solidity enum IndexingAgreementVersion.V1 (first variant = 0). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ed88229 commit 9a65178

6 files changed

Lines changed: 150 additions & 232 deletions

File tree

packages/indexer-agent/src/agent.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -717,9 +717,8 @@ export class Agent {
717717
throw new Error('DipsManager is not available')
718718
}
719719

720-
await operator.dipsManager.acceptPendingProposals(
721-
activeAllocations,
722-
)
720+
// Proposal acceptance is handled by the dedicated fast loop
721+
// (startProposalAcceptanceLoop), not the reconciliation cycle.
723722

724723
this.logger.debug(
725724
`Matching agreement allocations for network ${network.specification.networkIdentifier}`,

packages/indexer-common/src/indexer-management/allocations.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ export class AllocationManager {
177177
this,
178178
this.pendingRcaModel,
179179
)
180+
this.dipsManager.startProposalAcceptanceLoop()
180181
}
181182
}
182183

@@ -2314,7 +2315,7 @@ export class AllocationManager {
23142315
} else {
23152316
if (isHorizon) {
23162317
rewards = await this.network.contracts.RewardsManager.getRewards(
2317-
this.network.contracts.HorizonStaking.target,
2318+
this.network.contracts.SubgraphService.target,
23182319
action.allocationID,
23192320
)
23202321
} else {

packages/indexer-common/src/indexing-fees/__tests__/accept-proposals.test.ts

Lines changed: 64 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -66,36 +66,9 @@ function createMockProposal(
6666
}
6767
}
6868

69-
function createMockAllocation(
70-
deploymentBytes32: string = TEST_DEPLOYMENT_BYTES32,
71-
): Allocation {
72-
return {
73-
id: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
74-
status: AllocationStatus.ACTIVE,
75-
isLegacy: false,
76-
subgraphDeployment: {
77-
id: new SubgraphDeploymentID(deploymentBytes32),
78-
},
79-
indexer: '0x5555555555555555555555555555555555555555',
80-
allocatedTokens: 1000000000000000000n,
81-
createdAt: 0,
82-
createdAtEpoch: 100,
83-
createdAtBlockHash: '0x',
84-
closedAt: 0,
85-
closedAtEpoch: 0,
86-
closedAtEpochStartBlockHash: undefined,
87-
previousEpochStartBlockHash: undefined,
88-
closedAtBlockHash: '0x',
89-
poi: undefined,
90-
queryFeeRebates: 0n,
91-
queryFeesCollected: 0n,
92-
} as Allocation
93-
}
94-
9569
function createMockConsumer(proposals: DecodedRcaProposal[] = []) {
9670
return {
9771
getPendingProposals: jest.fn().mockResolvedValue(proposals),
98-
getPendingProposalsForDeployment: jest.fn().mockResolvedValue([]),
9972
markAccepted: jest.fn().mockResolvedValue(undefined),
10073
markRejected: jest.fn().mockResolvedValue(undefined),
10174
} as unknown as PendingRcaConsumer
@@ -135,9 +108,6 @@ function createMockNetwork() {
135108
EpochManager: {
136109
currentEpoch: jest.fn().mockResolvedValue(100n),
137110
},
138-
RewardsManager: {
139-
isDenied: jest.fn().mockResolvedValue(false),
140-
},
141111
},
142112
transactionManager: {
143113
executeTransaction: jest.fn(),
@@ -154,8 +124,7 @@ function createMockNetwork() {
154124
indexerOptions: {
155125
address: '0x5555555555555555555555555555555555555555',
156126
enableDips: true,
157-
dipsAllocationAmount: 0n,
158-
defaultAllocationAmount: 10000000000000000000n, // 10 GRT
127+
dipsAllocationAmount: 1000000000000000000n,
159128
},
160129
networkIdentifier: 'eip155:1337',
161130
},
@@ -198,8 +167,11 @@ describe('DipsManager.acceptPendingProposals', () => {
198167
deadline: BigInt(Math.floor(Date.now() / 1000) - 100),
199168
})
200169
const consumer = createMockConsumer([proposal])
201-
// After rejection, no other proposals for this deployment
202-
;(consumer.getPendingProposalsForDeployment as jest.Mock).mockResolvedValue([])
170+
// After rejection, getPendingProposals returns empty (no other proposals)
171+
consumer.getPendingProposals = jest
172+
.fn()
173+
.mockResolvedValueOnce([proposal]) // first call in acceptPendingProposals
174+
.mockResolvedValueOnce([]) // second call in cleanupDipsRule
203175
const mockRule = { id: 42 }
204176
const models = createMockModels()
205177
;(models.IndexingRule.findOne as jest.Mock).mockResolvedValue(mockRule)
@@ -218,10 +190,10 @@ describe('DipsManager.acceptPendingProposals', () => {
218190
})
219191
const otherProposal = createMockProposal({ id: 'proposal-2' })
220192
const consumer = createMockConsumer([proposal])
221-
// Another proposal exists for same deployment
222-
;(consumer.getPendingProposalsForDeployment as jest.Mock).mockResolvedValue([
223-
otherProposal,
224-
])
193+
consumer.getPendingProposals = jest
194+
.fn()
195+
.mockResolvedValueOnce([proposal])
196+
.mockResolvedValueOnce([otherProposal]) // another proposal for same deployment
225197
const models = createMockModels()
226198
const network = createMockNetwork()
227199
const dm = createDipsManager(network, models, consumer)
@@ -253,6 +225,32 @@ describe('DipsManager.acceptPendingProposals', () => {
253225
})
254226

255227
describe('with existing allocation', () => {
228+
function createMockAllocation(
229+
deploymentBytes32: string = TEST_DEPLOYMENT_BYTES32,
230+
): Allocation {
231+
return {
232+
id: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
233+
status: AllocationStatus.ACTIVE,
234+
isLegacy: false,
235+
subgraphDeployment: {
236+
id: new SubgraphDeploymentID(deploymentBytes32),
237+
},
238+
indexer: '0x5555555555555555555555555555555555555555',
239+
allocatedTokens: 1000000000000000000n,
240+
createdAt: 0,
241+
createdAtEpoch: 100,
242+
createdAtBlockHash: '0x',
243+
closedAt: 0,
244+
closedAtEpoch: 0,
245+
closedAtEpochStartBlockHash: undefined,
246+
previousEpochStartBlockHash: undefined,
247+
closedAtBlockHash: '0x',
248+
poi: undefined,
249+
queryFeeRebates: 0n,
250+
queryFeesCollected: 0n,
251+
} as Allocation
252+
}
253+
256254
test('accepts proposal on-chain and marks accepted', async () => {
257255
const proposal = createMockProposal()
258256
const allocation = createMockAllocation()
@@ -411,12 +409,39 @@ describe('DipsManager.acceptPendingProposals', () => {
411409
})
412410

413411
describe('error handling', () => {
412+
function createMockAllocation(): Allocation {
413+
return {
414+
id: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
415+
status: AllocationStatus.ACTIVE,
416+
isLegacy: false,
417+
subgraphDeployment: {
418+
id: new SubgraphDeploymentID(TEST_DEPLOYMENT_BYTES32),
419+
},
420+
indexer: '0x5555555555555555555555555555555555555555',
421+
allocatedTokens: 1000000000000000000n,
422+
createdAt: 0,
423+
createdAtEpoch: 100,
424+
createdAtBlockHash: '0x',
425+
closedAt: 0,
426+
closedAtEpoch: 0,
427+
closedAtEpochStartBlockHash: undefined,
428+
previousEpochStartBlockHash: undefined,
429+
closedAtBlockHash: '0x',
430+
poi: undefined,
431+
queryFeeRebates: 0n,
432+
queryFeesCollected: 0n,
433+
} as Allocation
434+
}
435+
414436
test('rejects proposal on deterministic CALL_EXCEPTION error', async () => {
415437
const proposal = createMockProposal()
416438
const allocation = createMockAllocation()
417439
const consumer = createMockConsumer([proposal])
418440
// After rejection, no remaining proposals for cleanup
419-
;(consumer.getPendingProposalsForDeployment as jest.Mock).mockResolvedValue([])
441+
consumer.getPendingProposals = jest
442+
.fn()
443+
.mockResolvedValueOnce([proposal])
444+
.mockResolvedValueOnce([])
420445
const models = createMockModels()
421446
const network = createMockNetwork()
422447
;(network.transactionManager.executeTransaction as jest.Mock).mockRejectedValue({
@@ -490,104 +515,4 @@ describe('DipsManager.acceptPendingProposals', () => {
490515
expect(consumer.markAccepted).toHaveBeenCalledWith('ok-1')
491516
})
492517
})
493-
494-
describe('allocation amount selection', () => {
495-
test('uses defaultAllocationAmount for rewarded (not denied) subgraph', async () => {
496-
const proposal = createMockProposal()
497-
const consumer = createMockConsumer([proposal])
498-
const models = createMockModels()
499-
const network = createMockNetwork()
500-
;(
501-
network.contracts.RewardsManager.isDenied as unknown as jest.Mock
502-
).mockResolvedValue(false)
503-
const mockReceipt = { hash: '0x', status: 1 }
504-
;(network.transactionManager.executeTransaction as jest.Mock).mockResolvedValue(
505-
mockReceipt,
506-
)
507-
508-
const dm = createDipsManager(network, models, consumer)
509-
510-
await dm.acceptPendingProposals([])
511-
512-
// Should check isDenied
513-
expect(network.contracts.RewardsManager.isDenied).toHaveBeenCalledWith(
514-
proposal.subgraphDeploymentId.bytes32,
515-
)
516-
// startService should be called (new allocation path)
517-
expect(
518-
network.contracts.SubgraphService.startService.populateTransaction,
519-
).toHaveBeenCalled()
520-
})
521-
522-
test('uses dipsAllocationAmount (0) for denied subgraph', async () => {
523-
const proposal = createMockProposal()
524-
const consumer = createMockConsumer([proposal])
525-
const models = createMockModels()
526-
const network = createMockNetwork()
527-
;(
528-
network.contracts.RewardsManager.isDenied as unknown as jest.Mock
529-
).mockResolvedValue(true)
530-
const mockReceipt = { hash: '0x', status: 1 }
531-
;(network.transactionManager.executeTransaction as jest.Mock).mockResolvedValue(
532-
mockReceipt,
533-
)
534-
535-
const dm = createDipsManager(network, models, consumer)
536-
537-
await dm.acceptPendingProposals([])
538-
539-
expect(network.contracts.RewardsManager.isDenied).toHaveBeenCalledWith(
540-
proposal.subgraphDeploymentId.bytes32,
541-
)
542-
expect(consumer.markAccepted).toHaveBeenCalledWith(proposal.id)
543-
})
544-
545-
test('uses per-deployment rule amount for rewarded subgraph with existing rule', async () => {
546-
const proposal = createMockProposal()
547-
const consumer = createMockConsumer([proposal])
548-
const models = createMockModels()
549-
const ruleAmount = 5000000000000000000n // 5 GRT
550-
;(models.IndexingRule.findOne as jest.Mock).mockResolvedValue({
551-
allocationAmount: ruleAmount.toString(),
552-
})
553-
const network = createMockNetwork()
554-
;(
555-
network.contracts.RewardsManager.isDenied as unknown as jest.Mock
556-
).mockResolvedValue(false)
557-
const mockReceipt = { hash: '0x', status: 1 }
558-
;(network.transactionManager.executeTransaction as jest.Mock).mockResolvedValue(
559-
mockReceipt,
560-
)
561-
562-
const dm = createDipsManager(network, models, consumer)
563-
564-
await dm.acceptPendingProposals([])
565-
566-
expect(consumer.markAccepted).toHaveBeenCalledWith(proposal.id)
567-
})
568-
569-
test('uses custom dipsAllocationAmount for denied subgraph with override', async () => {
570-
const proposal = createMockProposal()
571-
const consumer = createMockConsumer([proposal])
572-
const models = createMockModels()
573-
const network = createMockNetwork()
574-
// Override dipsAllocationAmount to non-zero
575-
;(
576-
network.specification.indexerOptions as { dipsAllocationAmount: bigint }
577-
).dipsAllocationAmount = 1000000000000000000n // 1 GRT
578-
;(
579-
network.contracts.RewardsManager.isDenied as unknown as jest.Mock
580-
).mockResolvedValue(true)
581-
const mockReceipt = { hash: '0x', status: 1 }
582-
;(network.transactionManager.executeTransaction as jest.Mock).mockResolvedValue(
583-
mockReceipt,
584-
)
585-
586-
const dm = createDipsManager(network, models, consumer)
587-
588-
await dm.acceptPendingProposals([])
589-
590-
expect(consumer.markAccepted).toHaveBeenCalledWith(proposal.id)
591-
})
592-
})
593518
})

packages/indexer-common/src/indexing-fees/__tests__/pending-rca-consumer.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ function encodeTestPayload(overrides?: {
4242
[
4343
{
4444
subgraphDeploymentId: TEST_DEPLOYMENT_BYTES32,
45-
version: 1n,
45+
version: 0n,
4646
terms: termsEncoded,
4747
},
4848
],

0 commit comments

Comments
 (0)