Skip to content
Merged
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
60 changes: 60 additions & 0 deletions event/drep.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2025 Blink Labs Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package event

import "github.com/blinklabs-io/gouroboros/ledger"

const (
DRepCertificateTypeRegistration = "Registration"
DRepCertificateTypeUpdate = "Update"
DRepCertificateTypeDeregistration = "Deregistration"
)

const (
DRepRegistrationEventType = "chainsync.drep.registration"
DRepUpdateEventType = "chainsync.drep.update"
DRepDeregistrationEventType = "chainsync.drep.deregistration"
)

// DRepCertificateEvent represents a single DRep certificate event
type DRepCertificateEvent struct {
BlockHash string `json:"blockHash"`
Certificate DRepCertificateData `json:"certificate"`
}

// NewDRepCertificateEvent creates a new DRepCertificateEvent
func NewDRepCertificateEvent(
block ledger.Block,
cert DRepCertificateData,
) DRepCertificateEvent {
return DRepCertificateEvent{
BlockHash: block.Hash().String(),
Certificate: cert,
}
}

// DRepEventType returns the event type for the given certificate type
func DRepEventType(certType string) (string, bool) {
switch certType {
case DRepCertificateTypeRegistration:
return DRepRegistrationEventType, true
case DRepCertificateTypeUpdate:
return DRepUpdateEventType, true
case DRepCertificateTypeDeregistration:
return DRepDeregistrationEventType, true
default:
return "", false
}
}
112 changes: 73 additions & 39 deletions event/governance.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,46 +412,11 @@ func extractGovernanceCertificates(tx ledger.Transaction) (
committee []CommitteeCertificateData,
) {
for _, cert := range tx.Certificates() {
switch c := cert.(type) {
// DRep certificates
case *lcommon.RegistrationDrepCertificate:
data := DRepCertificateData{
CertificateType: "Registration",
DRepHash: hex.EncodeToString(c.DrepCredential.Hash().Bytes()),
DRepId: c.DrepCredential.Hash().Bech32("drep"),
Deposit: c.Amount,
}
// Certificate anchors are pointer types (*GovAnchor), so we check for nil
if c.Anchor != nil {
data.Anchor = AnchorData{
Url: c.Anchor.Url,
DataHash: hex.EncodeToString(c.Anchor.DataHash[:]),
}
}
if data, ok := extractDRepCertificate(cert); ok {
drep = append(drep, data)

case *lcommon.DeregistrationDrepCertificate:
drep = append(drep, DRepCertificateData{
CertificateType: "Deregistration",
DRepHash: hex.EncodeToString(c.DrepCredential.Hash().Bytes()),
DRepId: c.DrepCredential.Hash().Bech32("drep"),
Deposit: c.Amount,
})

case *lcommon.UpdateDrepCertificate:
data := DRepCertificateData{
CertificateType: "Update",
DRepHash: hex.EncodeToString(c.DrepCredential.Hash().Bytes()),
DRepId: c.DrepCredential.Hash().Bech32("drep"),
}
if c.Anchor != nil {
data.Anchor = AnchorData{
Url: c.Anchor.Url,
DataHash: hex.EncodeToString(c.Anchor.DataHash[:]),
}
}
drep = append(drep, data)

continue
}
switch c := cert.(type) {
// Vote delegation certificates
case *lcommon.VoteDelegationCertificate:
voteDel = append(voteDel, extractVoteDelegation("VoteDelegation", c.StakeCredential, c.Drep, 0))
Expand Down Expand Up @@ -494,6 +459,75 @@ func extractGovernanceCertificates(tx ledger.Transaction) (
return
}

// ExtractDRepCertificates extracts DRep certificates from a transaction
func ExtractDRepCertificates(tx ledger.Transaction) []DRepCertificateData {
if len(tx.Certificates()) == 0 {
return nil
}

var result []DRepCertificateData
for _, cert := range tx.Certificates() {
if data, ok := extractDRepCertificate(cert); ok {
result = append(result, data)
}
}
if len(result) == 0 {
return nil
}
return result
}

func drepBech32Prefix(cred lcommon.Credential) string {
if cred.CredType == 1 {
return "drep_script"
}
return "drep"
}

func extractDRepCertificate(cert ledger.Certificate) (DRepCertificateData, bool) {
switch c := cert.(type) {
case *lcommon.RegistrationDrepCertificate:
data := DRepCertificateData{
CertificateType: DRepCertificateTypeRegistration,
DRepHash: hex.EncodeToString(c.DrepCredential.Hash().Bytes()),
DRepId: c.DrepCredential.Hash().Bech32(drepBech32Prefix(c.DrepCredential)),
Deposit: c.Amount,
}
// Certificate anchors are pointer types (*GovAnchor), so we check for nil
if c.Anchor != nil {
data.Anchor = AnchorData{
Url: c.Anchor.Url,
DataHash: hex.EncodeToString(c.Anchor.DataHash[:]),
}
}
return data, true

case *lcommon.DeregistrationDrepCertificate:
return DRepCertificateData{
CertificateType: DRepCertificateTypeDeregistration,
DRepHash: hex.EncodeToString(c.DrepCredential.Hash().Bytes()),
DRepId: c.DrepCredential.Hash().Bech32(drepBech32Prefix(c.DrepCredential)),
Deposit: c.Amount,
}, true

case *lcommon.UpdateDrepCertificate:
data := DRepCertificateData{
CertificateType: DRepCertificateTypeUpdate,
DRepHash: hex.EncodeToString(c.DrepCredential.Hash().Bytes()),
DRepId: c.DrepCredential.Hash().Bech32(drepBech32Prefix(c.DrepCredential)),
}
if c.Anchor != nil {
data.Anchor = AnchorData{
Url: c.Anchor.Url,
DataHash: hex.EncodeToString(c.Anchor.DataHash[:]),
}
}
return data, true
}

return DRepCertificateData{}, false
}

// Helper functions

func getGovActionType(action lcommon.GovAction) string {
Expand Down
19 changes: 19 additions & 0 deletions filter/cardano/cardano.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ func (c *Cardano) filterEvent(evt event.Event) bool {
return c.filterTransactionEvent(v)
case event.GovernanceEvent:
return c.filterGovernanceEvent(v)
case event.DRepCertificateEvent:
return c.filterDRepCertificateEvent(v)
default:
// Pass through events we don't filter
return true
Expand Down Expand Up @@ -161,6 +163,23 @@ func (c *Cardano) filterTransactionEvent(te event.TransactionEvent) bool {
return true
}

// filterDRepCertificateEvent checks DRep filter for DRep certificate events
func (c *Cardano) filterDRepCertificateEvent(de event.DRepCertificateEvent) bool {
if !c.filterSet.hasDRepFilter {
return true
}

if _, exists := c.filterSet.dreps.hexDRepIds[de.Certificate.DRepHash]; exists {
return true
}

if _, exists := c.filterSet.dreps.bech32DRepIds[de.Certificate.DRepId]; exists {
return true
}

return false
}

// matchAddressFilter checks if transaction matches address filters
func (c *Cardano) matchAddressFilter(te event.TransactionEvent) bool {
// Include resolved inputs as outputs for matching
Expand Down
59 changes: 59 additions & 0 deletions filter/cardano/cardano_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,65 @@ func TestFilterByDRepIdGovernanceEvent(t *testing.T) {
})
}

func TestFilterByDRepIdDRepCertificateEvent(t *testing.T) {
// 28 bytes = 56 hex chars for Blake2b224
drepHex := "abcd1234567890abcdef1234567890abcdef1234567890abcdef1234"

t.Run("matches DRep certificate event", func(t *testing.T) {
cs := New(WithDRepIds([]string{drepHex}))

evt := event.Event{
Payload: event.DRepCertificateEvent{
Certificate: event.DRepCertificateData{
CertificateType: event.DRepCertificateTypeRegistration,
DRepHash: drepHex,
DRepId: "drep1...",
},
},
}

err := cs.Start()
assert.NoError(t, err)
defer cs.Stop()

cs.InputChan() <- evt

select {
case filteredEvt := <-cs.OutputChan():
assert.Equal(t, evt, filteredEvt)
case <-time.After(1 * time.Second):
t.Error("Expected event to pass filter but it didn't")
}
})

t.Run("does not match when DRep not in filter", func(t *testing.T) {
cs := New(WithDRepIds([]string{drepHex}))

evt := event.Event{
Payload: event.DRepCertificateEvent{
Certificate: event.DRepCertificateData{
CertificateType: event.DRepCertificateTypeRegistration,
DRepHash: "deadbeef",
DRepId: "drep1notmatched",
},
},
}

err := cs.Start()
assert.NoError(t, err)
defer cs.Stop()

cs.InputChan() <- evt

select {
case <-cs.OutputChan():
t.Error("Expected event to be filtered out but it passed through")
case <-time.After(100 * time.Millisecond):
// Expected no event
}
})
}

func TestWithDRepIds(t *testing.T) {
t.Run("stores bech32 drep ID and computes hex", func(t *testing.T) {
// Create a valid drep bech32 ID from known hex (28 bytes = 56 hex chars)
Expand Down
42 changes: 42 additions & 0 deletions input/chainsync/chainsync.go
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,27 @@ func (c *ChainSync) handleRollForward(
)
tmpEvents = append(tmpEvents, govEvt)
}
// Emit DRep certificate events
if drepCerts := event.ExtractDRepCertificates(transaction); len(drepCerts) > 0 {
drepCtx := event.NewGovernanceContext(
block,
transaction,
//nolint:gosec // t is bounds-checked above
uint32(t),
c.networkMagic,
)
for _, cert := range drepCerts {
if evtType, ok := event.DRepEventType(cert.CertificateType); ok {
drepEvt := event.New(
evtType,
time.Now(),
drepCtx,
event.NewDRepCertificateEvent(block, cert),
)
tmpEvents = append(tmpEvents, drepEvt)
}
}
}
}
updateTip := ochainsync.Tip{
Point: ocommon.Point{
Expand Down Expand Up @@ -618,6 +639,27 @@ func (c *ChainSync) handleBlockFetchBlock(
)
c.eventChan <- govEvt
}
// Emit DRep certificate events
if drepCerts := event.ExtractDRepCertificates(transaction); len(drepCerts) > 0 {
drepCtx := event.NewGovernanceContext(
block,
transaction,
//nolint:gosec // t is bounds-checked above
uint32(t),
c.networkMagic,
)
for _, cert := range drepCerts {
if evtType, ok := event.DRepEventType(cert.CertificateType); ok {
drepEvt := event.New(
evtType,
time.Now(),
drepCtx,
event.NewDRepCertificateEvent(block, cert),
)
c.eventChan <- drepEvt
}
}
}
}
c.updateStatus(
block.SlotNumber(),
Expand Down