Skip to content
Open
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
15 changes: 15 additions & 0 deletions cmd/generate-bindings/solana/anchor-go/generator/cre.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ const (
PkgPbSdk = "github.com/smartcontractkit/chainlink-protos/cre/go/sdk"
PkgSolanaCre = "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/solana"
PkgBindings = "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/solana/bindings"
// maxGeneratedSubkeysPerEvent mirrors backend support in the Solana log poller.
maxGeneratedSubkeysPerEvent = 4
// maxGeneratedComparersPerSubkey limits per-subkey comparer fanout to match
// trigger registration guardrails.
maxGeneratedComparersPerSubkey = 4
)

// func (c *Codec) Decode<name>(data []byte) (*<name>, error) {
Expand Down Expand Up @@ -564,6 +569,16 @@ func creEncodeSubkeysForEvent(eventName string, filterFields []eventFilterField)
block.Line()
}

for _, field := range filterFields {
block.If(Len(Id(field.goName + "Comparers")).Op(">").Lit(maxGeneratedComparersPerSubkey)).Block(
Return(Nil(), Qual("fmt", "Errorf").Call(
Lit("too many comparers for subkey "+field.goName+": maximum supported is %d, got %d"),
Lit(maxGeneratedComparersPerSubkey),
Len(Id(field.goName+"Comparers")),
)),
)
}

block.Id("subkeys").Op(":=").Make(Index().Op("*").Qual(PkgSolanaCre, "SubkeyConfig"), Lit(0))
for _, field := range filterFields {
block.If(Len(Id(field.goName + "Comparers")).Op(">").Lit(0)).Block(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ func (g *Generator) genfile_triggers() (*OutputFile, error) {
}

filterFields := getEventFilterFields(fields)
if len(filterFields) > maxGeneratedSubkeysPerEvent {
return nil, fmt.Errorf("event %s has %d auto-filterable indexed fields; maximum supported is %d", event.Name, len(filterFields), maxGeneratedSubkeysPerEvent)
}

file.Add(creEventFiltersStruct(event.Name, filterFields))
file.Line()
Expand Down
35 changes: 35 additions & 0 deletions cmd/generate-bindings/solana/bindings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,41 @@ func TestLogTrigger(t *testing.T) {
require.Equal(t, mockLog, decodedLog.Log)
})

t.Run("AccessLogged subkey comparer limit", func(t *testing.T) {
pk1, err := solana.NewRandomPrivateKey()
require.NoError(t, err)
pk2, err := solana.NewRandomPrivateKey()
require.NoError(t, err)
pk3, err := solana.NewRandomPrivateKey()
require.NoError(t, err)
pk4, err := solana.NewRandomPrivateKey()
require.NoError(t, err)
pk5, err := solana.NewRandomPrivateKey()
require.NoError(t, err)

caller1 := pk1.PublicKey()
caller2 := pk2.PublicKey()
caller3 := pk3.PublicKey()
caller4 := pk4.PublicKey()
caller5 := pk5.PublicKey()

filters := []datastorage.AccessLoggedFilters{
{Caller: &caller1},
{Caller: &caller2},
{Caller: &caller3},
{Caller: &caller4},
{Caller: &caller5},
}

_, err = ds.Codec.EncodeAccessLoggedSubkeys(filters)
require.Error(t, err)
require.Contains(t, err.Error(), "too many comparers for subkey Caller")

_, err = ds.LogTriggerAccessLoggedLog(anyChainSelector, "too-many-comparers", filters, nil)
require.Error(t, err)
require.Contains(t, err.Error(), "too many comparers for subkey Caller")
})

t.Run("DynamicEvent scalar filters exclude nested and vec fields", func(t *testing.T) {
key := "lookup-key"
sender := "alice"
Expand Down
26 changes: 26 additions & 0 deletions cmd/generate-bindings/solana/gen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,29 @@ func TestGenerateBindings_MissingAddress(t *testing.T) {
_, statErr := os.Stat(filepath.Join(outDir, "program_id.go"))
assert.True(t, os.IsNotExist(statErr), "program_id.go should not be generated when IDL has no address")
}

func TestGenerateBindings_RejectsMoreThanFourIndexedFields(t *testing.T) {
idl := `{
"address": "ECL8142j2YQAvs9R9geSsRnkVH2wLEi7soJCRyJ74cfL",
"metadata": {"name": "over_indexed", "version": "0.1.0", "spec": "0.1.0"},
"instructions": [
{"name": "on_report", "discriminator": [214,173,18,221,173,148,151,208], "accounts": [], "args": []}
],
"accounts": [],
"events": [{"name": "OverIndexed", "discriminator": [1,2,3,4,5,6,7,8]}],
"errors": [],
"types": [{"name": "OverIndexed", "type": {"kind": "struct", "fields": [
{"name": "a", "type": "u8"},
{"name": "b", "type": "u16"},
{"name": "c", "type": "u32"},
{"name": "d", "type": "string"},
{"name": "e", "type": "bytes"}
]}}]
}`
idlPath := filepath.Join(t.TempDir(), "over_indexed.json")
require.NoError(t, os.WriteFile(idlPath, []byte(idl), 0o600))

err := solana.GenerateBindings(idlPath, "over_indexed", t.TempDir())
require.Error(t, err)
assert.Contains(t, err.Error(), "maximum supported is 4")
}
3 changes: 3 additions & 0 deletions cmd/generate-bindings/solana/sourcecre.ts.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,9 @@ export const encode{{.Name}}Subkeys = (filters: {{.Name}}Filters[]): SolanaSubke
}
const subkeys: SolanaSubkeyConfigJson[] = []
{{- range .FilterFields}}
if ({{.Name}}Comparers.length > 4) {
throw new Error(`too many comparers for subkey {{.PathName}}: maximum supported is 4, got ${ {{.Name}}Comparers.length }`)
}
if ({{.Name}}Comparers.length > 0) {
subkeys.push({ path: ['{{.PathName}}'], comparers: {{.Name}}Comparers })
}
Expand Down
15 changes: 15 additions & 0 deletions cmd/generate-bindings/solana/testdata/data_storage/triggers.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,15 @@ export const encodeAccessLoggedSubkeys = (filters: AccessLoggedFilters[]): Solan
}
}
const subkeys: SolanaSubkeyConfigJson[] = []
if (callerComparers.length > 4) {
throw new Error(`too many comparers for subkey Caller: maximum supported is 4, got ${ callerComparers.length }`)
}
if (callerComparers.length > 0) {
subkeys.push({ path: ['Caller'], comparers: callerComparers })
}
if (messageComparers.length > 4) {
throw new Error(`too many comparers for subkey Message: maximum supported is 4, got ${ messageComparers.length }`)
}
if (messageComparers.length > 0) {
subkeys.push({ path: ['Message'], comparers: messageComparers })
}
Expand Down Expand Up @@ -241,12 +247,21 @@ export const encodeDynamicEventSubkeys = (filters: DynamicEventFilters[]): Solan
}
}
const subkeys: SolanaSubkeyConfigJson[] = []
if (keyComparers.length > 4) {
throw new Error(`too many comparers for subkey Key: maximum supported is 4, got ${ keyComparers.length }`)
}
if (keyComparers.length > 0) {
subkeys.push({ path: ['Key'], comparers: keyComparers })
}
if (senderComparers.length > 4) {
throw new Error(`too many comparers for subkey Sender: maximum supported is 4, got ${ senderComparers.length }`)
}
if (senderComparers.length > 0) {
subkeys.push({ path: ['Sender'], comparers: senderComparers })
}
if (metadataComparers.length > 4) {
throw new Error(`too many comparers for subkey Metadata: maximum supported is 4, got ${ metadataComparers.length }`)
}
if (metadataComparers.length > 0) {
subkeys.push({ path: ['Metadata'], comparers: metadataComparers })
}
Expand Down
6 changes: 6 additions & 0 deletions cmd/generate-bindings/solana/tsbindgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ var (
tsSolanaMockTemplate = template.Must(template.New("mockcontract.ts").Parse(tsSolanaMockTpl))
)

const maxGeneratedSubkeysPerEvent = 4

// jsReservedWords are identifiers that cannot be used as TypeScript/JavaScript
// names. Field names that collide get an underscore suffix; type/class names
// that collide are rejected (they come from IDL type names, which are expected
Expand Down Expand Up @@ -363,6 +365,10 @@ func buildTriggerDef(
})
}

if len(trigger.FilterFields) > maxGeneratedSubkeysPerEvent {
return nil, fmt.Errorf("event %q has %d auto-filterable indexed fields; maximum supported is %d", eventName, len(trigger.FilterFields), maxGeneratedSubkeysPerEvent)
}

return trigger, nil
}

Expand Down
34 changes: 30 additions & 4 deletions cmd/generate-bindings/solana/tsbindgen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@
assert.Contains(t, source, "): Trigger<SolanaLog, SolanaDecodedLog<AccessLogged>> {")
// Subkey paths use the Go bindings' PascalCase names.
assert.Contains(t, source, "subkeys.push({ path: ['Caller'], comparers: callerComparers })")
assert.Contains(t, source, "too many comparers for subkey Caller: maximum supported is 4")
// Pubkey filter values are converted from base58 before encoding.
assert.Contains(t, source, "value: bytesToBase64(solanaAddressToBytes(f.caller))")
// The compact IDL is embedded once as base64 and sent as contractIdlJson.
Expand Down Expand Up @@ -211,7 +212,6 @@
{"name": "maybe_tag", "type": {"option": "string"}},
{"name": "flag", "type": "bool"},
{"name": "huge", "type": "u128"},
{"name": "blob", "type": "bytes"},
{"name": "list", "type": {"vec": "u8"}},
{"name": "fixed", "type": {"array": ["u8", 32]}}
]}}]
Expand All @@ -231,7 +231,6 @@
assert.Contains(t, source, "small?: number | null")
assert.Contains(t, source, "amount?: bigint | null")
assert.Contains(t, source, "ratio?: number | null")
assert.Contains(t, source, "blob?: Uint8Array | null")
// Option is unwrapped for the filter value type.
assert.Contains(t, source, "maybeTag?: string | null")
assert.Contains(t, source, "value: bytesToBase64(prepareSubkeyValue(f.amount))")
Expand Down Expand Up @@ -296,11 +295,38 @@
]}}]`,
wantErr: "both map to",
},
{
name: "more than four indexed event fields",
typesJSON: `[{"name": "OverIndexed", "type": {"kind": "struct", "fields": [
{"name": "a", "type": "u8"},
{"name": "b", "type": "u16"},
{"name": "c", "type": "u32"},
{"name": "d", "type": "string"},
{"name": "e", "type": "bytes"}
]}},
{"name": "Unused", "type": {"kind": "struct", "fields": []}}]`,
wantErr: "maximum supported is 4",
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
idlPath := writeTestIdl(t, tc.typesJSON)
t.Run(tc.name, func(t *testing.T) {

Check failure on line 313 in cmd/generate-bindings/solana/tsbindgen_test.go

View workflow job for this annotation

GitHub Actions / ci-lint

File is not properly formatted (gci)
idlPath := writeTestIdl(t, tc.typesJSON)
if tc.name == "more than four indexed event fields" {
idlWithEvent := `{
"address": "ECL8142j2YQAvs9R9geSsRnkVH2wLEi7soJCRyJ74cfL",
"metadata": {"name": "fail_loud", "version": "0.1.0", "spec": "0.1.0"},
"instructions": [
{"name": "on_report", "discriminator": [214,173,18,221,173,148,151,208], "accounts": [], "args": []}
],
"accounts": [],
"events": [{"name": "OverIndexed", "discriminator": [1,2,3,4,5,6,7,8]}],
"errors": [],
"types": ` + tc.typesJSON + `
}`
idlPath = filepath.Join(t.TempDir(), "over_indexed.json")
require.NoError(t, os.WriteFile(idlPath, []byte(idlWithEvent), 0o600))
}
_, err := GenerateBindingsTS(idlPath, "fail_loud", t.TempDir())
require.Error(t, err)
assert.Contains(t, err.Error(), tc.wantErr)
Expand Down
Loading