diff --git a/cmd/generate-bindings/solana/README.md b/cmd/generate-bindings/solana/README.md index 72f2ba62..2d9a5f41 100644 --- a/cmd/generate-bindings/solana/README.md +++ b/cmd/generate-bindings/solana/README.md @@ -41,7 +41,7 @@ generators emit: - pure account/event **decoders** (discriminator-checked) — there is no read/simulate capability, so these only decode bytes obtained elsewhere, - per-event **log-trigger bindings**: an `Filters` type, - `encodeSubkeys` (EQ comparers, OR across filter rows), and a typed + `encodeSubkeys` (EQ comparers; single-row input only), and a typed `logTriggerLog(filterName, filters, opts)` method whose output adapts the raw log into decoded event data (Go: `bindings.DecodedLog[T]`, TS: `SolanaDecodedLog`). `opts.cpi` targets Anchor `emit_cpi!` events. Only diff --git a/cmd/generate-bindings/solana/anchor-go/generator/cre.go b/cmd/generate-bindings/solana/anchor-go/generator/cre.go index a50c1e94..8c1dcf56 100644 --- a/cmd/generate-bindings/solana/anchor-go/generator/cre.go +++ b/cmd/generate-bindings/solana/anchor-go/generator/cre.go @@ -514,7 +514,8 @@ func creEventFiltersStruct(eventName string, filterFields []eventFilterField) Co st := Empty() st.Commentf("%sFilters holds optional filter values for %s log triggers.", exportedName, exportedName) st.Line() - st.Comment("Set a field to filter on that value (OR across filter rows). Leave nil for wildcard.") + st.Comment("Set fields within one row to AND those predicates. Multiple rows are OR alternatives, but current") + st.Comment("trigger configuration supports only a single row, so encoding rejects multi-row input.") st.Line() st.Type().Id(exportedName + "Filters").StructFunc(func(g *Group) { for _, field := range filterFields { @@ -532,6 +533,16 @@ func creEncodeSubkeysForEvent(eventName string, filterFields []eventFilterField) Params(Id("filters").Index().Id(exportedName+"Filters")). Params(Index().Op("*").Qual(PkgSolanaCre, "SubkeyConfig"), Error()). BlockFunc(func(block *Group) { + block.If(Len(Id("filters")).Op(">").Lit(1)).Block( + Return( + Nil(), + Qual("fmt", "Errorf").Call( + Lit("multiple filter rows are not supported for "+exportedName+"; provide a single filter row"), + ), + ), + ) + block.Line() + for _, field := range filterFields { block.Id(field.goName+"Comparers").Op(":=").Make( Index().Op("*").Qual(PkgSolanaCre, "ValueComparator"), Lit(0), diff --git a/cmd/generate-bindings/solana/bindings_test.go b/cmd/generate-bindings/solana/bindings_test.go index cafeaea7..480dfb30 100644 --- a/cmd/generate-bindings/solana/bindings_test.go +++ b/cmd/generate-bindings/solana/bindings_test.go @@ -190,17 +190,33 @@ func TestLogTrigger(t *testing.T) { testPubKey2, err := solana.NewRandomPrivateKey() require.NoError(t, err) caller2 := testPubKey2.PublicKey() + messageFilter := "match me" - filters := []datastorage.AccessLoggedFilters{ + multiRowFilters := []datastorage.AccessLoggedFilters{ {Caller: &testPubKey}, {Caller: &caller2}, } + _, err = ds.Codec.EncodeAccessLoggedSubkeys(multiRowFilters) + require.Error(t, err) + require.Contains(t, err.Error(), "multiple filter rows are not supported") + + filters := []datastorage.AccessLoggedFilters{ + {Caller: &testPubKey, Message: &messageFilter}, + } + subkeys, err := ds.Codec.EncodeAccessLoggedSubkeys(filters) require.NoError(t, err) - require.Len(t, subkeys, 1) - require.Equal(t, []string{"Caller"}, subkeys[0].Path) - require.Len(t, subkeys[0].Comparers, 2) + require.Len(t, subkeys, 2) + + paths := map[string]bool{} + for _, subkey := range subkeys { + require.Len(t, subkey.Path, 1) + require.Len(t, subkey.Comparers, 1) + paths[subkey.Path[0]] = true + } + require.True(t, paths["Caller"]) + require.True(t, paths["Message"]) trigger, err := ds.LogTriggerAccessLoggedLog( anyChainSelector, @@ -241,9 +257,7 @@ func TestLogTrigger(t *testing.T) { metadata := []byte{0xde, 0xad, 0xbe, 0xef} filters := []datastorage.DynamicEventFilters{ - {Key: &key}, - {Sender: &sender}, - {Metadata: &metadata}, + {Key: &key, Sender: &sender, Metadata: &metadata}, } subkeys, err := ds.Codec.EncodeDynamicEventSubkeys(filters) diff --git a/cmd/generate-bindings/solana/sourcecre.ts.tpl b/cmd/generate-bindings/solana/sourcecre.ts.tpl index 43add2a9..20630a7e 100644 --- a/cmd/generate-bindings/solana/sourcecre.ts.tpl +++ b/cmd/generate-bindings/solana/sourcecre.ts.tpl @@ -139,8 +139,9 @@ export const parseAnyEvent = (data: Uint8Array): {{range $i, $e := .Events}}{{if {{- range .Triggers}} /** - * Optional filter values for {{.Name}} log triggers. Set a field to filter on - * that value (OR across filter rows). Leave unset for wildcard. Only top-level + * Optional filter values for {{.Name}} log triggers. Set fields in one row to + * AND those predicates. Multiple rows are OR alternatives, but current trigger + * configuration supports only a single row. Leave unset for wildcard. Only top-level * scalar fields with supported subkey encodings are auto-filterable — nested * structs, vecs, arrays, bool, u128, and i128 need a manual SubkeyConfig. */ @@ -152,6 +153,9 @@ export type {{.Name}}Filters = { } export const encode{{.Name}}Subkeys = (filters: {{.Name}}Filters[]): SolanaSubkeyConfigJson[] => { + if (filters.length > 1) { + throw new Error('multiple filter rows are not supported for {{.Name}}; provide a single filter row') + } {{- range .FilterFields}} const {{.Name}}Comparers: SolanaValueComparatorJson[] = [] {{- end}} @@ -176,7 +180,12 @@ export const encode{{.Name}}Subkeys = (filters: {{.Name}}Filters[]): SolanaSubke {{- else}} export type {{.Name}}Filters = Record -export const encode{{.Name}}Subkeys = (_filters: {{.Name}}Filters[]): SolanaSubkeyConfigJson[] => [] +export const encode{{.Name}}Subkeys = (filters: {{.Name}}Filters[]): SolanaSubkeyConfigJson[] => { + if (filters.length > 1) { + throw new Error('multiple filter rows are not supported for {{.Name}}; provide a single filter row') + } + return [] +} {{- end}} {{- end}} {{- if or .Accounts .Events}} diff --git a/cmd/generate-bindings/solana/testdata/data_storage/triggers.go b/cmd/generate-bindings/solana/testdata/data_storage/triggers.go index a2acde60..a4fcf37f 100644 --- a/cmd/generate-bindings/solana/testdata/data_storage/triggers.go +++ b/cmd/generate-bindings/solana/testdata/data_storage/triggers.go @@ -23,13 +23,17 @@ var ( // Nested structs, vecs, arrays, bool, u128, and i128 require manual SubkeyConfig. // AccessLoggedFilters holds optional filter values for AccessLogged log triggers. -// Set a field to filter on that value (OR across filter rows). Leave nil for wildcard. +// Set fields within one row to AND those predicates. Multiple rows are OR alternatives, but current // trigger configuration supports only a single row, so encoding rejects multi-row input. type AccessLoggedFilters struct { Caller *solanago.PublicKey Message *string } func (c *Codec) EncodeAccessLoggedSubkeys(filters []AccessLoggedFilters) ([]*solana.SubkeyConfig, error) { + if len(filters) > 1 { + return nil, fmt.Errorf("multiple filter rows are not supported for AccessLogged; provide a single filter row") + } + CallerComparers := make([]*solana.ValueComparator, 0) MessageComparers := make([]*solana.ValueComparator, 0) @@ -115,7 +119,7 @@ func (c *DataStorage) LogTriggerAccessLoggedLog( } // DynamicEventFilters holds optional filter values for DynamicEvent log triggers. -// Set a field to filter on that value (OR across filter rows). Leave nil for wildcard. +// Set fields within one row to AND those predicates. Multiple rows are OR alternatives, but current // trigger configuration supports only a single row, so encoding rejects multi-row input. type DynamicEventFilters struct { Key *string Sender *string @@ -123,6 +127,10 @@ type DynamicEventFilters struct { } func (c *Codec) EncodeDynamicEventSubkeys(filters []DynamicEventFilters) ([]*solana.SubkeyConfig, error) { + if len(filters) > 1 { + return nil, fmt.Errorf("multiple filter rows are not supported for DynamicEvent; provide a single filter row") + } + KeyComparers := make([]*solana.ValueComparator, 0) SenderComparers := make([]*solana.ValueComparator, 0) MetadataComparers := make([]*solana.ValueComparator, 0) @@ -225,10 +233,14 @@ func (c *DataStorage) LogTriggerDynamicEventLog( } // NoFieldsFilters holds optional filter values for NoFields log triggers. -// Set a field to filter on that value (OR across filter rows). Leave nil for wildcard. +// Set fields within one row to AND those predicates. Multiple rows are OR alternatives, but current // trigger configuration supports only a single row, so encoding rejects multi-row input. type NoFieldsFilters struct{} func (c *Codec) EncodeNoFieldsSubkeys(filters []NoFieldsFilters) ([]*solana.SubkeyConfig, error) { + if len(filters) > 1 { + return nil, fmt.Errorf("multiple filter rows are not supported for NoFields; provide a single filter row") + } + subkeys := make([]*solana.SubkeyConfig, 0) return subkeys, nil } diff --git a/cmd/generate-bindings/solana/testdata/data_storage_ts/DataStorage.ts b/cmd/generate-bindings/solana/testdata/data_storage_ts/DataStorage.ts index e6338638..67ea791e 100644 --- a/cmd/generate-bindings/solana/testdata/data_storage_ts/DataStorage.ts +++ b/cmd/generate-bindings/solana/testdata/data_storage_ts/DataStorage.ts @@ -167,8 +167,9 @@ export const parseAnyEvent = (data: Uint8Array): AccessLogged | DynamicEvent | N } /** - * Optional filter values for AccessLogged log triggers. Set a field to filter on - * that value (OR across filter rows). Leave unset for wildcard. Only top-level + * Optional filter values for AccessLogged log triggers. Set fields in one row to + * AND those predicates. Multiple rows are OR alternatives, but current trigger + * configuration supports only a single row. Leave unset for wildcard. Only top-level * scalar fields with supported subkey encodings are auto-filterable — nested * structs, vecs, arrays, bool, u128, and i128 need a manual SubkeyConfig. */ @@ -178,6 +179,9 @@ export type AccessLoggedFilters = { } export const encodeAccessLoggedSubkeys = (filters: AccessLoggedFilters[]): SolanaSubkeyConfigJson[] => { + if (filters.length > 1) { + throw new Error('multiple filter rows are not supported for AccessLogged; provide a single filter row') + } const callerComparers: SolanaValueComparatorJson[] = [] const messageComparers: SolanaValueComparatorJson[] = [] for (const f of filters) { @@ -205,8 +209,9 @@ export const encodeAccessLoggedSubkeys = (filters: AccessLoggedFilters[]): Solan } /** - * Optional filter values for DynamicEvent log triggers. Set a field to filter on - * that value (OR across filter rows). Leave unset for wildcard. Only top-level + * Optional filter values for DynamicEvent log triggers. Set fields in one row to + * AND those predicates. Multiple rows are OR alternatives, but current trigger + * configuration supports only a single row. Leave unset for wildcard. Only top-level * scalar fields with supported subkey encodings are auto-filterable — nested * structs, vecs, arrays, bool, u128, and i128 need a manual SubkeyConfig. */ @@ -217,6 +222,9 @@ export type DynamicEventFilters = { } export const encodeDynamicEventSubkeys = (filters: DynamicEventFilters[]): SolanaSubkeyConfigJson[] => { + if (filters.length > 1) { + throw new Error('multiple filter rows are not supported for DynamicEvent; provide a single filter row') + } const keyComparers: SolanaValueComparatorJson[] = [] const senderComparers: SolanaValueComparatorJson[] = [] const metadataComparers: SolanaValueComparatorJson[] = [] @@ -254,14 +262,20 @@ export const encodeDynamicEventSubkeys = (filters: DynamicEventFilters[]): Solan } /** - * Optional filter values for NoFields log triggers. Set a field to filter on - * that value (OR across filter rows). Leave unset for wildcard. Only top-level + * Optional filter values for NoFields log triggers. Set fields in one row to + * AND those predicates. Multiple rows are OR alternatives, but current trigger + * configuration supports only a single row. Leave unset for wildcard. Only top-level * scalar fields with supported subkey encodings are auto-filterable — nested * structs, vecs, arrays, bool, u128, and i128 need a manual SubkeyConfig. */ export type NoFieldsFilters = Record -export const encodeNoFieldsSubkeys = (_filters: NoFieldsFilters[]): SolanaSubkeyConfigJson[] => [] +export const encodeNoFieldsSubkeys = (filters: NoFieldsFilters[]): SolanaSubkeyConfigJson[] => { + if (filters.length > 1) { + throw new Error('multiple filter rows are not supported for NoFields; provide a single filter row') + } + return [] +} export class DataStorage { readonly programId: Uint8Array diff --git a/cmd/generate-bindings/solana/tsbindgen_test.go b/cmd/generate-bindings/solana/tsbindgen_test.go index 6b1d2689..ed2420cd 100644 --- a/cmd/generate-bindings/solana/tsbindgen_test.go +++ b/cmd/generate-bindings/solana/tsbindgen_test.go @@ -158,6 +158,7 @@ func TestGenerateBindingsTS_LogTriggers(t *testing.T) { assert.Contains(t, source, "export type AccessLoggedFilters = {") assert.Contains(t, source, "caller?: Address | null") assert.Contains(t, source, "export const encodeAccessLoggedSubkeys = (filters: AccessLoggedFilters[]): SolanaSubkeyConfigJson[] =>") + assert.Contains(t, source, "multiple filter rows are not supported for AccessLogged; provide a single filter row") assert.Contains(t, source, "logTriggerAccessLoggedLog(") assert.Contains(t, source, "): Trigger> {") // Subkey paths use the Go bindings' PascalCase names. @@ -176,7 +177,8 @@ func TestGenerateBindingsTS_LogTriggers(t *testing.T) { assert.NotContains(t, source, "metadataArray?:") // An event with no filterable fields still gets a trigger with empty filters. assert.Contains(t, source, "export type NoFieldsFilters = Record") - assert.Contains(t, source, "export const encodeNoFieldsSubkeys = (_filters: NoFieldsFilters[]): SolanaSubkeyConfigJson[] => []") + assert.Contains(t, source, "export const encodeNoFieldsSubkeys = (filters: NoFieldsFilters[]): SolanaSubkeyConfigJson[] => {") + assert.Contains(t, source, "multiple filter rows are not supported for NoFields; provide a single filter row") assert.Contains(t, source, "logTriggerNoFieldsLog(") // An IDL without events must not emit trigger code or its imports.