Follow-up after the API surface for bounty matching landed in #75. The bounties table is empty until the indexer learns to ingest the chain's bounty events; this issue tracks that work.
Dependencies
- Blocked on ligate-chain v0.4.0 shipping. The chain side (PRs #531, #532, #533) is in main but unreleased; devnet-2 is still on v0.3.1 + chain_hash
eec077f4… which has no bounty module. Once v0.4.0 cuts, the chain emits Bounty/BountyPosted, BountyClaimed, BountyDisputed, DisputeResolved, BountyExpired events.
Scope
1. Event payload types in ligate-api-types
Add typed structs mirroring the chain's bounty::Event<S> payloads:
BountyBountyPostedEvent { bounty_posted: { bounty_id, poster, pool } }
BountyBountyClaimedEvent { bounty_claimed: { bounty_id, attestation_id, payout, attester } }
BountyBountyDisputedEvent { bounty_disputed: { bounty_id, attestation_id, disputer, bond } }
BountyDisputeResolvedEvent { dispute_resolved: { bounty_id, attestation_id, decision, bond_recipient } }
BountyBountyExpiredEvent { bounty_expired: { bounty_id, refunded_to_poster } }
Same naming convention as the attestation events: outer wrapper struct + #[serde(rename)] on the inner field. Capture the real wire format from a devnet-3 (or whatever chain id ships at v0.4) /v1/ledger/events sample before locking the field names.
2. Event key constants in indexer::parser
const KEY_BOUNTY_POSTED: &str = "Bounty/BountyPosted";
const KEY_BOUNTY_CLAIMED: &str = "Bounty/BountyClaimed";
const KEY_BOUNTY_DISPUTED: &str = "Bounty/BountyDisputed";
const KEY_BOUNTY_RESOLVED: &str = "Bounty/DisputeResolved";
const KEY_BOUNTY_EXPIRED: &str = "Bounty/BountyExpired";
3. IndexerTx enum variants + parser branches
Add IndexerPostBounty, IndexerClaimBounty, IndexerDisputeAttestation, IndexerResolveDispute, IndexerCancelBounty variants to IndexerTx. Add parser branches in classify_events() (after the Attestation passes, before the Bank fallback) so a bounty tx's semantic event wins over its fee transfer event.
4. Chain RPC enrichment
Critical detail: the chain's BountyPosted event only carries (bounty_id, poster, pool). To populate acceptance / expiry_da_height / dispute_window_blocks / per_attestation_nano columns, the indexer needs to fetch from the chain's GET /v1/modules/bounty/bounties/{id} after seeing each BountyPosted event.
Pattern matches the existing schema-detail fetch the indexer already does. Add a chain_client.fetch_bounty(id) helper that returns the full BountyState, call it in db::write_bounty_posted().
5. DB write functions in indexer::db
write_bounty_posted(tx, event_details, full_state) — INSERT into bounties with status='open'
write_bounty_claimed(tx, event_details) — UPDATE escrow_remaining_nano + claim_count + last_claim_at_slot; transition to 'exhausted' when escrow < per_attestation
write_bounty_disputed(tx, event_details) — no bounties-table change at v0; dispute state lives in chain only (could add disputes table mirror later)
write_dispute_resolved(tx, event_details) — same; v0 noop on bounties table
write_bounty_expired(tx, event_details) — UPDATE status to 'expired' (or 'cancelled' depending on which path triggered)
6. Tests
- Indexer e2e: feed a fixture event stream through ingest, assert bounties table rows match expected lifecycle.
- API: existing query function should now return populated matches when the indexer has ingested events.
Acceptance
- All 5 bounty events parsed + persisted
- Test fixture covers post → claim → disputed → resolved → expired lifecycle
- The
GET /v1/bounties/matching/{address} endpoint surfaces matching bounties on devnet (whatever chain id v0.4 cuts to)
- CHANGELOG entry under
[Unreleased] documenting the new types + tables
Follow-up after the API surface for bounty matching landed in #75. The bounties table is empty until the indexer learns to ingest the chain's bounty events; this issue tracks that work.
Dependencies
eec077f4…which has no bounty module. Once v0.4.0 cuts, the chain emitsBounty/BountyPosted,BountyClaimed,BountyDisputed,DisputeResolved,BountyExpiredevents.Scope
1. Event payload types in
ligate-api-typesAdd typed structs mirroring the chain's
bounty::Event<S>payloads:BountyBountyPostedEvent { bounty_posted: { bounty_id, poster, pool } }BountyBountyClaimedEvent { bounty_claimed: { bounty_id, attestation_id, payout, attester } }BountyBountyDisputedEvent { bounty_disputed: { bounty_id, attestation_id, disputer, bond } }BountyDisputeResolvedEvent { dispute_resolved: { bounty_id, attestation_id, decision, bond_recipient } }BountyBountyExpiredEvent { bounty_expired: { bounty_id, refunded_to_poster } }Same naming convention as the attestation events: outer wrapper struct +
#[serde(rename)]on the inner field. Capture the real wire format from a devnet-3 (or whatever chain id ships at v0.4)/v1/ledger/eventssample before locking the field names.2. Event key constants in
indexer::parser3.
IndexerTxenum variants + parser branchesAdd
IndexerPostBounty,IndexerClaimBounty,IndexerDisputeAttestation,IndexerResolveDispute,IndexerCancelBountyvariants toIndexerTx. Add parser branches inclassify_events()(after the Attestation passes, before the Bank fallback) so a bounty tx's semantic event wins over its fee transfer event.4. Chain RPC enrichment
Critical detail: the chain's
BountyPostedevent only carries(bounty_id, poster, pool). To populateacceptance/expiry_da_height/dispute_window_blocks/per_attestation_nanocolumns, the indexer needs to fetch from the chain'sGET /v1/modules/bounty/bounties/{id}after seeing eachBountyPostedevent.Pattern matches the existing schema-detail fetch the indexer already does. Add a
chain_client.fetch_bounty(id)helper that returns the fullBountyState, call it indb::write_bounty_posted().5. DB write functions in
indexer::dbwrite_bounty_posted(tx, event_details, full_state)— INSERT into bounties with status='open'write_bounty_claimed(tx, event_details)— UPDATE escrow_remaining_nano + claim_count + last_claim_at_slot; transition to 'exhausted' when escrow < per_attestationwrite_bounty_disputed(tx, event_details)— no bounties-table change at v0; dispute state lives in chain only (could adddisputestable mirror later)write_dispute_resolved(tx, event_details)— same; v0 noop on bounties tablewrite_bounty_expired(tx, event_details)— UPDATE status to 'expired' (or 'cancelled' depending on which path triggered)6. Tests
Acceptance
GET /v1/bounties/matching/{address}endpoint surfaces matching bounties on devnet (whatever chain id v0.4 cuts to)[Unreleased]documenting the new types + tables