Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0a4405a
migrate initial signatures for views + use view walk in eventInfoForE…
cjonas9 Sep 4, 2026
6715f38
move events view matcher + v1 filter compiler from rpcv2 into shared/…
cjonas9 Sep 4, 2026
97c5c82
wire updated handler to shared matcher
cjonas9 Sep 4, 2026
bc894ed
adapt migration_test's GetEvents callback to the view scan signature
cjonas9 Sep 8, 2026
4d421dd
use rpcv1 views fns in rpcv2
cjonas9 Sep 10, 2026
7495ec6
fix comments referencing old function names
cjonas9 Sep 14, 2026
682dd35
restore parity with SDK on diagnostic-typed events
cjonas9 Sep 14, 2026
798fd04
render eventInfo in view scanner callback fn
cjonas9 Sep 14, 2026
cdb4987
fix number of clauses referenced in comment
cjonas9 Sep 14, 2026
0ab2d63
update test to account for sorted keys
cjonas9 Sep 14, 2026
62ad52e
use sql.RawBytes in getEvents to avoid needless event data copying
cjonas9 Sep 14, 2026
d719cd7
move event match test to store + add test
cjonas9 Sep 14, 2026
513ad32
make error messages in shared events file backend neutral
cjonas9 Sep 14, 2026
de7a7b4
fix stale comments
cjonas9 Sep 14, 2026
3b9a453
minor shared renderer optimizations
cjonas9 Sep 14, 2026
ac9ca1b
change handler to check length to see if match found
cjonas9 Sep 14, 2026
387ed77
get event fields in one pass per level
cjonas9 Sep 15, 2026
609ff54
fix scan function error handling
cjonas9 Sep 15, 2026
719aba2
revert to contract/system event type fold
cjonas9 Sep 15, 2026
d7630df
revert event match machinery relocation
cjonas9 Sep 15, 2026
b2bffae
error on an event type outside the enum instead of rendering an empty…
cjonas9 Sep 17, 2026
af88dd9
fix loan doc: only the event view is borrowed, txHash is a copy
cjonas9 Sep 17, 2026
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
111 changes: 111 additions & 0 deletions cmd/stellar-rpc/internal/methods/event_filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package methods

import (
"bytes"
"fmt"
"slices"

protocol "github.com/stellar/go-stellar-sdk/protocols/rpc"
"github.com/stellar/go-stellar-sdk/strkey"
"github.com/stellar/go-stellar-sdk/xdr"
)

// eventFilters is a validated GetEventsRequest.Filters with comparison values in wire form,
// matched against a view's raw bytes the way GetEventsRequest.Matches matches a decoded event.
// Empty matches every event.
type eventFilters []eventFilter

// eventFilter is one protocol.EventFilter; an empty field matches everything.
type eventFilter struct {
types []xdr.ContractEventType
contractIDs [][]byte
topics []topicFilter
}

// topicFilter is one protocol.TopicFilter: a nil value is "*", atLeast is a trailing "**".
type topicFilter struct {
values [][]byte
atLeast bool
}

func compileFilters(in []protocol.EventFilter) (eventFilters, error) {
out := make(eventFilters, 0, len(in))
for _, f := range in {
c := eventFilter{}
for _, name := range f.EventType.Keys() {
typ, ok := protocol.GetEventTypeXDRFromEventType()[name]
if !ok { // Valid admits only contract and system
return nil, fmt.Errorf("unsupported event type %q", name)
}
c.types = append(c.types, typ)
}
for _, id := range f.ContractIDs {
raw, err := strkey.Decode(strkey.VersionByteContract, id)
if err != nil { // unreachable: Valid decoded it already
return nil, fmt.Errorf("invalid contract ID: %v", id)
}
c.contractIDs = append(c.contractIDs, raw)
}
for _, tf := range f.Topics {
t, err := compileTopicFilter(tf)
if err != nil {
return nil, err
}
c.topics = append(c.topics, t)
}
out = append(out, c)
}
return out, nil
}

func compileTopicFilter(tf protocol.TopicFilter) (topicFilter, error) {
var t topicFilter
if n := len(tf); n > 0 && tf[n-1].Wildcard != nil && *tf[n-1].Wildcard == protocol.WildCardZeroOrMore {
tf, t.atLeast = tf[:n-1], true
}
t.values = make([][]byte, len(tf))
for i, s := range tf {
if s.ScVal == nil { // "*", or a segment Valid would have rejected
continue
}
raw, err := s.ScVal.MarshalBinary()
if err != nil {
return topicFilter{}, fmt.Errorf("failed to marshal segment: %w", err)
}
t.values[i] = raw
}
return t, nil
}

// matchHeader reports whether some filter accepts the type and contract id. It never rejects
// an event match would accept, so a false answer skips reading the topics.
func (fs eventFilters) matchHeader(h eventHead) bool {
return len(fs) == 0 || slices.ContainsFunc(fs, func(f eventFilter) bool { return f.matchHeader(h) })
}

// match reports whether some filter accepts the whole event.
func (fs eventFilters) match(h eventHead, topics [][]byte) bool {
return len(fs) == 0 || slices.ContainsFunc(fs, func(f eventFilter) bool {
return f.matchHeader(h) && (len(f.topics) == 0 ||
slices.ContainsFunc(f.topics, func(t topicFilter) bool { return t.matches(topics) }))
})
}

func (f eventFilter) matchHeader(h eventHead) bool {
return (len(f.types) == 0 || slices.Contains(f.types, h.typ)) &&
(len(f.contractIDs) == 0 || slices.ContainsFunc(f.contractIDs, func(id []byte) bool {
return bytes.Equal(id, h.cid)
}))
}

func (t topicFilter) matches(topics [][]byte) bool {
if len(topics) < len(t.values) || (!t.atLeast && len(topics) != len(t.values)) {
return false
}
for i, want := range t.values {
if want != nil && !bytes.Equal(topics[i], want) {
return false
}
}
return true
}
78 changes: 78 additions & 0 deletions cmd/stellar-rpc/internal/methods/event_filter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package methods

import (
"bytes"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

protocol "github.com/stellar/go-stellar-sdk/protocols/rpc"
"github.com/stellar/go-stellar-sdk/strkey"
"github.com/stellar/go-stellar-sdk/xdr"
)

// TestEventFilters pins the port of GetEventsRequest.Matches: filters OR, fields AND,
// "*" is any one topic, a trailing "**" relaxes the arity, and matchHeader never rejects
// an event match accepts.
func TestEventFilters(t *testing.T) {
sym := func(s string) xdr.ScVal { return xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: (*xdr.ScSymbol)(&s)} }
raw := func(v xdr.ScVal) []byte { b, err := v.MarshalBinary(); require.NoError(t, err); return b }
seg := func(s string) protocol.SegmentFilter { v := sym(s); return protocol.SegmentFilter{ScVal: &v} }
wild := func(w string) protocol.SegmentFilter { return protocol.SegmentFilter{Wildcard: &w} }
cidA, cidB := bytes.Repeat([]byte{0xAA}, 32), bytes.Repeat([]byte{0xBB}, 32)
strkeyA := strkey.MustEncode(strkey.VersionByteContract, cidA)
strkeyB := strkey.MustEncode(strkey.VersionByteContract, cidB)
topics := func(tf ...protocol.SegmentFilter) []protocol.TopicFilter { return []protocol.TopicFilter{tf} }
contract := protocol.EventTypeSet{protocol.EventTypeContract: nil}

xferAlice := [][]byte{raw(sym("xfer")), raw(sym("alice"))}
headA := eventHead{typ: xdr.ContractEventTypeContract, cid: cidA}
headB := eventHead{typ: xdr.ContractEventTypeSystem, cid: cidB}

for name, tc := range map[string]struct {
in []protocol.EventFilter
head eventHead
topics [][]byte
want bool
}{
"no filters match everything": {nil, headB, nil, true},
"contract id": {[]protocol.EventFilter{{ContractIDs: []string{strkeyA}}}, headA, nil, true},
"contract id mismatch": {[]protocol.EventFilter{{ContractIDs: []string{strkeyA}}}, headB, nil, false},
"type": {[]protocol.EventFilter{{EventType: contract}}, headB, nil, false},
"fields AND within a filter": {
[]protocol.EventFilter{{
EventType: contract, ContractIDs: []string{strkeyA},
Topics: topics(seg("xfer"), wild("*")),
}}, headA, xferAlice, true,
},
"filters OR": {
[]protocol.EventFilter{
{ContractIDs: []string{strkeyA}, Topics: topics(seg("mint"))},
{Topics: topics(seg("xfer"), seg("alice"))},
}, headA, xferAlice, true,
},
"header and topics must come from one filter": {
[]protocol.EventFilter{
{ContractIDs: []string{strkeyA}, Topics: topics(seg("mint"))},
{ContractIDs: []string{strkeyB}, Topics: topics(seg("xfer"), wild("**"))},
},
headA, xferAlice, false,
},
"exact arity": {[]protocol.EventFilter{{Topics: topics(seg("xfer"))}}, headA, xferAlice, false},
"trailing ** relaxes": {[]protocol.EventFilter{{Topics: topics(seg("xfer"), wild("**"))}}, headA, xferAlice, true},
"** needs the prefix": {
[]protocol.EventFilter{{Topics: topics(seg("a"), seg("b"), seg("c"), wild("**"))}}, headA, xferAlice, false,
},
} {
t.Run(name, func(t *testing.T) {
fs, err := compileFilters(tc.in)
require.NoError(t, err)
got := fs.match(tc.head, tc.topics)
assert.Equal(t, tc.want, got)
if got {
assert.True(t, fs.matchHeader(tc.head), "matchHeader must not reject a matching event")
}
})
}
}
Loading
Loading