diff --git a/.github/workflows/cre-system-tests.yaml b/.github/workflows/cre-system-tests.yaml index b96a27a9d66..5f06144d00c 100644 --- a/.github/workflows/cre-system-tests.yaml +++ b/.github/workflows/cre-system-tests.yaml @@ -108,10 +108,13 @@ jobs: "Test_CRE_V2_Aptos_Suite": [ {"topology":"workflow-gateway-aptos","configs":"configs/workflow-gateway-don-aptos.toml"} ], - "Test_CRE_V2_Solana_Suite": [ + "Test_CRE_V2_Solana_Write": [ {"topology":"workflow","configs":"configs/workflow-don-solana.toml"} ], - "Test_CRE_V2_Solana_Read_Accounts": [ + "Test_CRE_V2_Solana_LogTrigger": [ + {"topology":"workflow","configs":"configs/workflow-don-solana.toml"} + ], + "Test_CRE_V2_Solana_Read_Accounts": [ {"topology":"workflow","configs":"configs/workflow-don-solana.toml"} ], "Test_CRE_V2_Sharding": [ diff --git a/system-tests/tests/smoke/cre/cre_suite_test.go b/system-tests/tests/smoke/cre/cre_suite_test.go index a1a0779adb2..394a44201c3 100644 --- a/system-tests/tests/smoke/cre/cre_suite_test.go +++ b/system-tests/tests/smoke/cre/cre_suite_test.go @@ -219,13 +219,19 @@ func runEVMReadBucket(t *testing.T, bucket evm_config.ReadBucket) { const solanaConfigPath = "/configs/workflow-don-solana.toml" -func Test_CRE_V2_Solana_Suite(t *testing.T) { +func Test_CRE_V2_Solana_Write(t *testing.T) { testEnv := t_helpers.SetupTestEnvironmentWithConfig(t, t_helpers.GetTestConfig(t, solanaConfigPath)) t.Run("Solana Write", func(t *testing.T) { ExecuteSolanaWriteTest(t, testEnv) }) - t.Run("[v2] Solana LogTrigger", func(t *testing.T) { +} + +func Test_CRE_V2_Solana_LogTrigger(t *testing.T) { + testEnv := t_helpers.SetupTestEnvironmentWithConfig(t, t_helpers.GetTestConfig(t, solanaConfigPath)) + t.Run("Solana LogTrigger", func(t *testing.T) { ExecuteSolanaLogTriggerTest(t, testEnv) + }) + t.Run("Solana LogTrigger CPI", func(t *testing.T) { ExecuteSolanaLogTriggerCPITest(t, testEnv) }) } diff --git a/system-tests/tests/smoke/cre/solana/sollogtrigger/config/config.go b/system-tests/tests/smoke/cre/solana/sollogtrigger/config/config.go index cf037de3fa6..1afdcbdc552 100644 --- a/system-tests/tests/smoke/cre/solana/sollogtrigger/config/config.go +++ b/system-tests/tests/smoke/cre/solana/sollogtrigger/config/config.go @@ -6,7 +6,6 @@ type Config struct { LogReadTestProgramID solanago.PublicKey ExpectedU64Value uint64 ExpectedStrVal string `yaml:"expected_str_val"` - ContractIdlJSON string `yaml:"contract_idl_json"` // CPILogTrigger registers the CPI log trigger handler when true, otherwise the direct log trigger handler. CPILogTrigger bool `yaml:"cpi_log_trigger"` } diff --git a/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/constants.go b/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/constants.go new file mode 100644 index 00000000000..7cb9ea0ffeb --- /dev/null +++ b/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/constants.go @@ -0,0 +1,4 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains constants. + +package log_read_test diff --git a/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/constructor.go b/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/constructor.go new file mode 100644 index 00000000000..595076604cf --- /dev/null +++ b/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/constructor.go @@ -0,0 +1,83 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains the constructor for the program. + +package log_read_test + +import ( + "bytes" + "encoding/binary" + sdk "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" + solana "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/solana" + bindings "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/solana/bindings" + cre "github.com/smartcontractkit/cre-sdk-go/cre" +) + +var IDL = "{\"address\":\"J1zQwrBNBngz26jRPNWsUSZMHJwBwpkoDitXRV95LdK4\",\"metadata\":{\"name\":\"log_read_test\",\"version\":\"0.1.0\",\"spec\":\"0.1.0\",\"description\":\"Created with Anchor\"},\"instructions\":[{\"name\":\"create_log\",\"discriminator\":[215,95,248,114,153,204,208,48],\"accounts\":[{\"name\":\"authority\",\"signer\":true},{\"name\":\"system_program\"}],\"args\":[{\"name\":\"value\",\"type\":\"u64\"}]},{\"name\":\"create_log_cpi\",\"discriminator\":[101,207,236,250,214,98,173,146],\"accounts\":[{\"name\":\"authority\",\"signer\":true},{\"name\":\"system_program\"},{\"name\":\"event_authority\"},{\"name\":\"program\"}],\"args\":[{\"name\":\"value\",\"type\":\"u64\"}]},{\"name\":\"create_truncated_log\",\"discriminator\":[133,74,116,132,80,11,241,64],\"accounts\":[{\"name\":\"authority\",\"signer\":true},{\"name\":\"system_program\"}],\"args\":[{\"name\":\"value\",\"type\":\"u64\"}]}],\"events\":[{\"name\":\"TestEvent\",\"discriminator\":[28,52,39,105,8,210,91,9]}],\"types\":[{\"name\":\"TestEvent\",\"type\":{\"kind\":\"struct\",\"fields\":[{\"name\":\"str_val\",\"type\":\"string\"},{\"name\":\"u64_value\",\"type\":\"u64\"}]}}]}" + +type LogReadTest struct { + client *solana.Client + Codec LogReadTestCodec +} + +type Codec struct{} + +func NewLogReadTest(client *solana.Client) (*LogReadTest, error) { + return &LogReadTest{ + Codec: &Codec{}, + client: client, + }, nil +} + +// EncodeBorshVecU32 returns Anchor/Borsh encoding of a Vec whose elements are opaque byte payloads. // Each [][]byte element must already be fully serialized for one Vec item on the wire. // Layout: little-endian u32 length followed by concatenated element payloads. +func EncodeBorshVecU32(elements [][]byte) ([]byte, error) { + buf := bytes.NewBuffer(nil) + if err := binary.Write(buf, binary.LittleEndian, uint32(len(elements))); err != nil { + return nil, err + } + for _, elem := range elements { + _, err := buf.Write(elem) + if err != nil { + return nil, err + } + } + return buf.Bytes(), nil +} + +// WriteReportFromBorshEncodedVec publishes through the CRE signer using a forwarder payload built from // Borsh Vec semantics (EncodeBorshVecU32). Compose each elementPayload for your program (e.g. one encoded struct per row). // Pass computeConfig = nil to use the host default Solana compute budget. +func (c *LogReadTest) WriteReportFromBorshEncodedVec(runtime cre.Runtime, elementPayloads [][]byte, remainingAccounts []*solana.AccountMeta, computeConfig *solana.ComputeConfig) cre.Promise[*solana.WriteReportReply] { + payload, err := EncodeBorshVecU32(elementPayloads) + if err != nil { + return cre.PromiseFromResult[*solana.WriteReportReply](nil, err) + } + + encodedAccountList := bindings.CalculateAccountsHash(remainingAccounts) + fwdReport := bindings.ForwarderReport{ + AccountHash: encodedAccountList, + Payload: payload, + } + encodedFwdReport, err := fwdReport.Marshal() + if err != nil { + return cre.PromiseFromResult[*solana.WriteReportReply](nil, err) + } + + promise := runtime.GenerateReport(&sdk.ReportRequest{ + EncodedPayload: encodedFwdReport, + EncoderName: "solana", + HashingAlgo: "keccak256", + SigningAlgo: "ecdsa", + }) + + return cre.ThenPromise(promise, func(report *cre.Report) cre.Promise[*solana.WriteReportReply] { + return c.client.WriteReport(runtime, &solana.WriteCreReportRequest{ + ComputeConfig: computeConfig, + Receiver: ProgramID.Bytes(), + RemainingAccounts: remainingAccounts, + Report: report, + }) + }) +} + +type LogReadTestCodec interface { + EncodeTestEventStruct(in TestEvent) ([]byte, error) + EncodeTestEventSubkeys(filters []TestEventFilters) ([]*solana.SubkeyConfig, error) +} diff --git a/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/discriminators.go b/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/discriminators.go new file mode 100644 index 00000000000..23e93ecc2af --- /dev/null +++ b/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/discriminators.go @@ -0,0 +1,21 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains the discriminators for accounts and events defined in the IDL. + +package log_read_test + +import binary "github.com/gagliardetto/binary" + +// Account discriminators +var () + +// Event discriminators +var ( + Event_TestEvent = binary.TypeID{28, 52, 39, 105, 8, 210, 91, 9} +) + +// Instruction discriminators +var ( + Instruction_CreateLog = binary.TypeID{215, 95, 248, 114, 153, 204, 208, 48} + Instruction_CreateLogCpi = binary.TypeID{101, 207, 236, 250, 214, 98, 173, 146} + Instruction_CreateTruncatedLog = binary.TypeID{133, 74, 116, 132, 80, 11, 241, 64} +) diff --git a/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/errors.go b/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/errors.go new file mode 100644 index 00000000000..de8e428d516 --- /dev/null +++ b/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/errors.go @@ -0,0 +1,4 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains errors. + +package log_read_test diff --git a/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/events.go b/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/events.go new file mode 100644 index 00000000000..c76cbb96977 --- /dev/null +++ b/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/events.go @@ -0,0 +1,45 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains parsers for the events defined in the IDL. + +package log_read_test + +import ( + "fmt" + binary "github.com/gagliardetto/binary" +) + +func ParseAnyEvent(eventData []byte) (any, error) { + decoder := binary.NewBorshDecoder(eventData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek event discriminator: %w", err) + } + switch discriminator { + case Event_TestEvent: + value := new(TestEvent) + err := value.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event as TestEvent: %w", err) + } + return value, nil + default: + return nil, fmt.Errorf("unknown discriminator: %s", binary.FormatDiscriminator(discriminator)) + } +} + +func ParseEvent_TestEvent(eventData []byte) (*TestEvent, error) { + decoder := binary.NewBorshDecoder(eventData) + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return nil, fmt.Errorf("failed to peek discriminator: %w", err) + } + if discriminator != Event_TestEvent { + return nil, fmt.Errorf("expected discriminator %v, got %s", Event_TestEvent, binary.FormatDiscriminator(discriminator)) + } + event := new(TestEvent) + err = event.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal event of type TestEvent: %w", err) + } + return event, nil +} diff --git a/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/fetchers.go b/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/fetchers.go new file mode 100644 index 00000000000..5c2afff0d6e --- /dev/null +++ b/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/fetchers.go @@ -0,0 +1,4 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains fetcher functions. + +package log_read_test diff --git a/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/instructions.go b/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/instructions.go new file mode 100644 index 00000000000..46660578ada --- /dev/null +++ b/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/instructions.go @@ -0,0 +1,592 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains instructions and instruction parsers. + +package log_read_test + +import ( + "bytes" + "fmt" + errors "github.com/gagliardetto/anchor-go/errors" + binary "github.com/gagliardetto/binary" + solanago "github.com/gagliardetto/solana-go" +) + +// Builds a "create_log" instruction. +func NewCreateLogInstruction( + // Params: + valueParam uint64, + + // Accounts: + authorityAccount solanago.PublicKey, + systemProgramAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_CreateLog[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } + { + // Serialize `valueParam`: + err = enc__.Encode(valueParam) + if err != nil { + return nil, errors.NewField("valueParam", err) + } + } + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "authority": Read-only, Signer, Required + accounts__.Append(solanago.NewAccountMeta(authorityAccount, false, true)) + // Account 1 "system_program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(systemProgramAccount, false, false)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil +} + +// Builds a "create_log_cpi" instruction. +func NewCreateLogCpiInstruction( + // Params: + valueParam uint64, + + // Accounts: + authorityAccount solanago.PublicKey, + systemProgramAccount solanago.PublicKey, + eventAuthorityAccount solanago.PublicKey, + programAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_CreateLogCpi[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } + { + // Serialize `valueParam`: + err = enc__.Encode(valueParam) + if err != nil { + return nil, errors.NewField("valueParam", err) + } + } + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "authority": Read-only, Signer, Required + accounts__.Append(solanago.NewAccountMeta(authorityAccount, false, true)) + // Account 1 "system_program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(systemProgramAccount, false, false)) + // Account 2 "event_authority": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(eventAuthorityAccount, false, false)) + // Account 3 "program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(programAccount, false, false)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil +} + +// Builds a "create_truncated_log" instruction. +func NewCreateTruncatedLogInstruction( + // Params: + valueParam uint64, + + // Accounts: + authorityAccount solanago.PublicKey, + systemProgramAccount solanago.PublicKey, +) (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_CreateTruncatedLog[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } + { + // Serialize `valueParam`: + err = enc__.Encode(valueParam) + if err != nil { + return nil, errors.NewField("valueParam", err) + } + } + accounts__ := solanago.AccountMetaSlice{} + + // Add the accounts to the instruction. + { + // Account 0 "authority": Read-only, Signer, Required + accounts__.Append(solanago.NewAccountMeta(authorityAccount, false, true)) + // Account 1 "system_program": Read-only, Non-signer, Required + accounts__.Append(solanago.NewAccountMeta(systemProgramAccount, false, false)) + } + + // Create the instruction. + return solanago.NewInstruction( + ProgramID, + accounts__, + buf__.Bytes(), + ), nil +} + +type CreateLogInstruction struct { + Value uint64 `json:"value"` + + // Accounts: + Authority solanago.PublicKey `json:"authority"` + AuthoritySigner bool `json:"authority_signer"` + SystemProgram solanago.PublicKey `json:"system_program"` +} + +func (obj *CreateLogInstruction) GetDiscriminator() []byte { + return Instruction_CreateLog[:] +} + +// UnmarshalWithDecoder unmarshals the CreateLogInstruction from Borsh-encoded bytes prefixed with its discriminator. +func (obj *CreateLogInstruction) UnmarshalWithDecoder(decoder *binary.Decoder) error { + var err error + // Read the discriminator and check it against the expected value: + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return fmt.Errorf("failed to read instruction discriminator for %s: %w", "CreateLogInstruction", err) + } + if discriminator != Instruction_CreateLog { + return fmt.Errorf("instruction discriminator mismatch for %s: expected %s, got %s", "CreateLogInstruction", Instruction_CreateLog, discriminator) + } + // Deserialize `Value`: + err = decoder.Decode(&obj.Value) + if err != nil { + return err + } + return nil +} + +func (obj *CreateLogInstruction) UnmarshalAccountIndices(buf []byte) ([]uint8, error) { + // UnmarshalAccountIndices decodes account indices from Borsh-encoded bytes + decoder := binary.NewBorshDecoder(buf) + indices := make([]uint8, 0) + index := uint8(0) + var err error + // Decode from authority account index + index = uint8(0) + err = decoder.Decode(&index) + if err != nil { + return nil, fmt.Errorf("failed to decode %s account index: %w", "authority", err) + } + indices = append(indices, index) + // Decode from system_program account index + index = uint8(0) + err = decoder.Decode(&index) + if err != nil { + return nil, fmt.Errorf("failed to decode %s account index: %w", "system_program", err) + } + indices = append(indices, index) + return indices, nil +} + +func (obj *CreateLogInstruction) PopulateFromAccountIndices(indices []uint8, accountKeys []solanago.PublicKey) error { + // PopulateFromAccountIndices sets account public keys from indices and account keys array + if len(indices) != 2 { + return fmt.Errorf("mismatch between expected accounts (%d) and provided indices (%d)", 2, len(indices)) + } + indexOffset := 0 + // Set authority account from index + if indices[indexOffset] >= uint8(len(accountKeys)) { + return fmt.Errorf("account index %d for %s is out of bounds (max: %d)", indices[indexOffset], "authority", len(accountKeys)-1) + } + obj.Authority = accountKeys[indices[indexOffset]] + indexOffset++ + // Set system_program account from index + if indices[indexOffset] >= uint8(len(accountKeys)) { + return fmt.Errorf("account index %d for %s is out of bounds (max: %d)", indices[indexOffset], "system_program", len(accountKeys)-1) + } + obj.SystemProgram = accountKeys[indices[indexOffset]] + indexOffset++ + return nil +} + +func (obj *CreateLogInstruction) GetAccountKeys() []solanago.PublicKey { + keys := make([]solanago.PublicKey, 0) + keys = append(keys, obj.Authority) + keys = append(keys, obj.SystemProgram) + return keys +} + +// Unmarshal unmarshals the CreateLogInstruction from Borsh-encoded bytes prefixed with the discriminator. +func (obj *CreateLogInstruction) Unmarshal(buf []byte) error { + var err error + err = obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling CreateLogInstruction: %w", err) + } + return nil +} + +// UnmarshalCreateLogInstruction unmarshals the instruction from Borsh-encoded bytes prefixed with the discriminator. +func UnmarshalCreateLogInstruction(buf []byte) (*CreateLogInstruction, error) { + obj := new(CreateLogInstruction) + var err error + err = obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +type CreateLogCpiInstruction struct { + Value uint64 `json:"value"` + + // Accounts: + Authority solanago.PublicKey `json:"authority"` + AuthoritySigner bool `json:"authority_signer"` + SystemProgram solanago.PublicKey `json:"system_program"` + EventAuthority solanago.PublicKey `json:"event_authority"` + Program solanago.PublicKey `json:"program"` +} + +func (obj *CreateLogCpiInstruction) GetDiscriminator() []byte { + return Instruction_CreateLogCpi[:] +} + +// UnmarshalWithDecoder unmarshals the CreateLogCpiInstruction from Borsh-encoded bytes prefixed with its discriminator. +func (obj *CreateLogCpiInstruction) UnmarshalWithDecoder(decoder *binary.Decoder) error { + var err error + // Read the discriminator and check it against the expected value: + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return fmt.Errorf("failed to read instruction discriminator for %s: %w", "CreateLogCpiInstruction", err) + } + if discriminator != Instruction_CreateLogCpi { + return fmt.Errorf("instruction discriminator mismatch for %s: expected %s, got %s", "CreateLogCpiInstruction", Instruction_CreateLogCpi, discriminator) + } + // Deserialize `Value`: + err = decoder.Decode(&obj.Value) + if err != nil { + return err + } + return nil +} + +func (obj *CreateLogCpiInstruction) UnmarshalAccountIndices(buf []byte) ([]uint8, error) { + // UnmarshalAccountIndices decodes account indices from Borsh-encoded bytes + decoder := binary.NewBorshDecoder(buf) + indices := make([]uint8, 0) + index := uint8(0) + var err error + // Decode from authority account index + index = uint8(0) + err = decoder.Decode(&index) + if err != nil { + return nil, fmt.Errorf("failed to decode %s account index: %w", "authority", err) + } + indices = append(indices, index) + // Decode from system_program account index + index = uint8(0) + err = decoder.Decode(&index) + if err != nil { + return nil, fmt.Errorf("failed to decode %s account index: %w", "system_program", err) + } + indices = append(indices, index) + // Decode from event_authority account index + index = uint8(0) + err = decoder.Decode(&index) + if err != nil { + return nil, fmt.Errorf("failed to decode %s account index: %w", "event_authority", err) + } + indices = append(indices, index) + // Decode from program account index + index = uint8(0) + err = decoder.Decode(&index) + if err != nil { + return nil, fmt.Errorf("failed to decode %s account index: %w", "program", err) + } + indices = append(indices, index) + return indices, nil +} + +func (obj *CreateLogCpiInstruction) PopulateFromAccountIndices(indices []uint8, accountKeys []solanago.PublicKey) error { + // PopulateFromAccountIndices sets account public keys from indices and account keys array + if len(indices) != 4 { + return fmt.Errorf("mismatch between expected accounts (%d) and provided indices (%d)", 4, len(indices)) + } + indexOffset := 0 + // Set authority account from index + if indices[indexOffset] >= uint8(len(accountKeys)) { + return fmt.Errorf("account index %d for %s is out of bounds (max: %d)", indices[indexOffset], "authority", len(accountKeys)-1) + } + obj.Authority = accountKeys[indices[indexOffset]] + indexOffset++ + // Set system_program account from index + if indices[indexOffset] >= uint8(len(accountKeys)) { + return fmt.Errorf("account index %d for %s is out of bounds (max: %d)", indices[indexOffset], "system_program", len(accountKeys)-1) + } + obj.SystemProgram = accountKeys[indices[indexOffset]] + indexOffset++ + // Set event_authority account from index + if indices[indexOffset] >= uint8(len(accountKeys)) { + return fmt.Errorf("account index %d for %s is out of bounds (max: %d)", indices[indexOffset], "event_authority", len(accountKeys)-1) + } + obj.EventAuthority = accountKeys[indices[indexOffset]] + indexOffset++ + // Set program account from index + if indices[indexOffset] >= uint8(len(accountKeys)) { + return fmt.Errorf("account index %d for %s is out of bounds (max: %d)", indices[indexOffset], "program", len(accountKeys)-1) + } + obj.Program = accountKeys[indices[indexOffset]] + indexOffset++ + return nil +} + +func (obj *CreateLogCpiInstruction) GetAccountKeys() []solanago.PublicKey { + keys := make([]solanago.PublicKey, 0) + keys = append(keys, obj.Authority) + keys = append(keys, obj.SystemProgram) + keys = append(keys, obj.EventAuthority) + keys = append(keys, obj.Program) + return keys +} + +// Unmarshal unmarshals the CreateLogCpiInstruction from Borsh-encoded bytes prefixed with the discriminator. +func (obj *CreateLogCpiInstruction) Unmarshal(buf []byte) error { + var err error + err = obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling CreateLogCpiInstruction: %w", err) + } + return nil +} + +// UnmarshalCreateLogCpiInstruction unmarshals the instruction from Borsh-encoded bytes prefixed with the discriminator. +func UnmarshalCreateLogCpiInstruction(buf []byte) (*CreateLogCpiInstruction, error) { + obj := new(CreateLogCpiInstruction) + var err error + err = obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +type CreateTruncatedLogInstruction struct { + Value uint64 `json:"value"` + + // Accounts: + Authority solanago.PublicKey `json:"authority"` + AuthoritySigner bool `json:"authority_signer"` + SystemProgram solanago.PublicKey `json:"system_program"` +} + +func (obj *CreateTruncatedLogInstruction) GetDiscriminator() []byte { + return Instruction_CreateTruncatedLog[:] +} + +// UnmarshalWithDecoder unmarshals the CreateTruncatedLogInstruction from Borsh-encoded bytes prefixed with its discriminator. +func (obj *CreateTruncatedLogInstruction) UnmarshalWithDecoder(decoder *binary.Decoder) error { + var err error + // Read the discriminator and check it against the expected value: + discriminator, err := decoder.ReadDiscriminator() + if err != nil { + return fmt.Errorf("failed to read instruction discriminator for %s: %w", "CreateTruncatedLogInstruction", err) + } + if discriminator != Instruction_CreateTruncatedLog { + return fmt.Errorf("instruction discriminator mismatch for %s: expected %s, got %s", "CreateTruncatedLogInstruction", Instruction_CreateTruncatedLog, discriminator) + } + // Deserialize `Value`: + err = decoder.Decode(&obj.Value) + if err != nil { + return err + } + return nil +} + +func (obj *CreateTruncatedLogInstruction) UnmarshalAccountIndices(buf []byte) ([]uint8, error) { + // UnmarshalAccountIndices decodes account indices from Borsh-encoded bytes + decoder := binary.NewBorshDecoder(buf) + indices := make([]uint8, 0) + index := uint8(0) + var err error + // Decode from authority account index + index = uint8(0) + err = decoder.Decode(&index) + if err != nil { + return nil, fmt.Errorf("failed to decode %s account index: %w", "authority", err) + } + indices = append(indices, index) + // Decode from system_program account index + index = uint8(0) + err = decoder.Decode(&index) + if err != nil { + return nil, fmt.Errorf("failed to decode %s account index: %w", "system_program", err) + } + indices = append(indices, index) + return indices, nil +} + +func (obj *CreateTruncatedLogInstruction) PopulateFromAccountIndices(indices []uint8, accountKeys []solanago.PublicKey) error { + // PopulateFromAccountIndices sets account public keys from indices and account keys array + if len(indices) != 2 { + return fmt.Errorf("mismatch between expected accounts (%d) and provided indices (%d)", 2, len(indices)) + } + indexOffset := 0 + // Set authority account from index + if indices[indexOffset] >= uint8(len(accountKeys)) { + return fmt.Errorf("account index %d for %s is out of bounds (max: %d)", indices[indexOffset], "authority", len(accountKeys)-1) + } + obj.Authority = accountKeys[indices[indexOffset]] + indexOffset++ + // Set system_program account from index + if indices[indexOffset] >= uint8(len(accountKeys)) { + return fmt.Errorf("account index %d for %s is out of bounds (max: %d)", indices[indexOffset], "system_program", len(accountKeys)-1) + } + obj.SystemProgram = accountKeys[indices[indexOffset]] + indexOffset++ + return nil +} + +func (obj *CreateTruncatedLogInstruction) GetAccountKeys() []solanago.PublicKey { + keys := make([]solanago.PublicKey, 0) + keys = append(keys, obj.Authority) + keys = append(keys, obj.SystemProgram) + return keys +} + +// Unmarshal unmarshals the CreateTruncatedLogInstruction from Borsh-encoded bytes prefixed with the discriminator. +func (obj *CreateTruncatedLogInstruction) Unmarshal(buf []byte) error { + var err error + err = obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling CreateTruncatedLogInstruction: %w", err) + } + return nil +} + +// UnmarshalCreateTruncatedLogInstruction unmarshals the instruction from Borsh-encoded bytes prefixed with the discriminator. +func UnmarshalCreateTruncatedLogInstruction(buf []byte) (*CreateTruncatedLogInstruction, error) { + obj := new(CreateTruncatedLogInstruction) + var err error + err = obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +// Instruction interface defines common methods for all instruction types +type Instruction interface { + GetDiscriminator() []byte + + UnmarshalWithDecoder(decoder *binary.Decoder) error + + UnmarshalAccountIndices(buf []byte) ([]uint8, error) + + PopulateFromAccountIndices(indices []uint8, accountKeys []solanago.PublicKey) error + + GetAccountKeys() []solanago.PublicKey +} + +// ParseInstruction parses instruction data and optionally populates accounts +// If accountIndicesData is nil or empty, accounts will not be populated +func ParseInstruction(instructionData []byte, accountIndicesData []byte, accountKeys []solanago.PublicKey) (Instruction, error) { + // Validate inputs + if len(instructionData) < 8 { + return nil, fmt.Errorf("instruction data too short: expected at least 8 bytes, got %d", len(instructionData)) + } + // Extract discriminator (TypeID for consistent equality with generated constants) + discriminator := binary.TypeIDFromBytes(instructionData[0:8]) + // Parse based on discriminator + switch discriminator { + case Instruction_CreateLog: + instruction := new(CreateLogInstruction) + decoder := binary.NewBorshDecoder(instructionData) + err := instruction.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal instruction as CreateLogInstruction: %w", err) + } + if accountIndicesData != nil && len(accountIndicesData) > 0 { + indices, err := instruction.UnmarshalAccountIndices(accountIndicesData) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal account indices: %w", err) + } + err = instruction.PopulateFromAccountIndices(indices, accountKeys) + if err != nil { + return nil, fmt.Errorf("failed to populate accounts: %w", err) + } + } + return instruction, nil + case Instruction_CreateLogCpi: + instruction := new(CreateLogCpiInstruction) + decoder := binary.NewBorshDecoder(instructionData) + err := instruction.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal instruction as CreateLogCpiInstruction: %w", err) + } + if accountIndicesData != nil && len(accountIndicesData) > 0 { + indices, err := instruction.UnmarshalAccountIndices(accountIndicesData) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal account indices: %w", err) + } + err = instruction.PopulateFromAccountIndices(indices, accountKeys) + if err != nil { + return nil, fmt.Errorf("failed to populate accounts: %w", err) + } + } + return instruction, nil + case Instruction_CreateTruncatedLog: + instruction := new(CreateTruncatedLogInstruction) + decoder := binary.NewBorshDecoder(instructionData) + err := instruction.UnmarshalWithDecoder(decoder) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal instruction as CreateTruncatedLogInstruction: %w", err) + } + if accountIndicesData != nil && len(accountIndicesData) > 0 { + indices, err := instruction.UnmarshalAccountIndices(accountIndicesData) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal account indices: %w", err) + } + err = instruction.PopulateFromAccountIndices(indices, accountKeys) + if err != nil { + return nil, fmt.Errorf("failed to populate accounts: %w", err) + } + } + return instruction, nil + default: + return nil, fmt.Errorf("unknown instruction discriminator: %s", binary.FormatDiscriminator(discriminator)) + } +} + +// ParseInstructionTyped parses instruction data and returns a specific instruction type // T must implement the Instruction interface +func ParseInstructionTyped[T Instruction](instructionData []byte, accountIndicesData []byte, accountKeys []solanago.PublicKey) (T, error) { + instruction, err := ParseInstruction(instructionData, accountIndicesData, accountKeys) + if err != nil { + return *new(T), err + } + typed, ok := instruction.(T) + if !ok { + return *new(T), fmt.Errorf("instruction is not of expected type") + } + return typed, nil +} + +// ParseInstructionWithoutAccounts parses instruction data without account information +func ParseInstructionWithoutAccounts(instructionData []byte) (Instruction, error) { + return ParseInstruction(instructionData, nil, []solanago.PublicKey{}) +} + +// ParseInstructionWithAccounts parses instruction data with account information +func ParseInstructionWithAccounts(instructionData []byte, accountIndicesData []byte, accountKeys []solanago.PublicKey) (Instruction, error) { + return ParseInstruction(instructionData, accountIndicesData, accountKeys) +} diff --git a/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/program_id.go b/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/program_id.go new file mode 100644 index 00000000000..62beffb5e2d --- /dev/null +++ b/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/program_id.go @@ -0,0 +1,8 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains the program ID. + +package log_read_test + +import solanago "github.com/gagliardetto/solana-go" + +var ProgramID = solanago.MustPublicKeyFromBase58("J1zQwrBNBngz26jRPNWsUSZMHJwBwpkoDitXRV95LdK4") diff --git a/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/tests_test.go b/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/tests_test.go new file mode 100644 index 00000000000..115811e52d0 --- /dev/null +++ b/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/tests_test.go @@ -0,0 +1,4 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains tests. + +package log_read_test diff --git a/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/triggers.go b/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/triggers.go new file mode 100644 index 00000000000..e5ff4d95215 --- /dev/null +++ b/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/triggers.go @@ -0,0 +1,115 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains log trigger bindings for events defined in the IDL. + +package log_read_test + +import ( + "fmt" + + solana "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/solana" + bindings "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/solana/bindings" + cre "github.com/smartcontractkit/cre-sdk-go/cre" +) + +var ( + _ = solana.FilterLogTriggerRequest{} + _ = bindings.LogTriggerOptions{} + _ = bindings.DecodedLog[struct{}]{} +) + +// Events +// The Filters struct should be used as a filter for log triggers. +// Only top-level scalar fields with supported subkey encodings are auto-filterable. +// Nested structs, vecs, arrays, bool, u128, and i128 require manual SubkeyConfig. + +// TestEventFilters holds optional filter values for TestEvent log triggers. +// Set a field to filter on that value (OR across filter rows). Leave nil for wildcard. +type TestEventFilters struct { + StrVal *string + U64Value *uint64 +} + +func (c *Codec) EncodeTestEventSubkeys(filters []TestEventFilters) ([]*solana.SubkeyConfig, error) { + StrValComparers := make([]*solana.ValueComparator, 0) + U64ValueComparers := make([]*solana.ValueComparator, 0) + + for _, f := range filters { + if f.StrVal != nil { + val, err := bindings.PrepareSubkeyValue(*f.StrVal) + if err != nil { + return nil, fmt.Errorf("failed to encode subkey value for StrVal: %w", err) + } + StrValComparers = append(StrValComparers, &solana.ValueComparator{ + Operator: solana.ComparisonOperator_COMPARISON_OPERATOR_EQ, + Value: val, + }) + } + if f.U64Value != nil { + val, err := bindings.PrepareSubkeyValue(*f.U64Value) + if err != nil { + return nil, fmt.Errorf("failed to encode subkey value for U64Value: %w", err) + } + U64ValueComparers = append(U64ValueComparers, &solana.ValueComparator{ + Operator: solana.ComparisonOperator_COMPARISON_OPERATOR_EQ, + Value: val, + }) + } + } + + subkeys := make([]*solana.SubkeyConfig, 0) + if len(StrValComparers) > 0 { + subkeys = append(subkeys, &solana.SubkeyConfig{ + Comparers: StrValComparers, + Path: []string{"StrVal"}, + }) + } + if len(U64ValueComparers) > 0 { + subkeys = append(subkeys, &solana.SubkeyConfig{ + Comparers: U64ValueComparers, + Path: []string{"U64Value"}, + }) + } + return subkeys, nil +} + +// TestEventTrigger wraps the raw log trigger and provides decoded TestEvent data. +type TestEventTrigger struct { + cre.Trigger[*solana.Log, *solana.Log] +} + +// Adapt decodes the log into TestEvent event data. +func (t *TestEventTrigger) Adapt(l *solana.Log) (*bindings.DecodedLog[TestEvent], error) { + decoded, err := ParseEvent_TestEvent(l.GetData()) + if err != nil { + return nil, fmt.Errorf("failed to decode TestEvent log: %w", err) + } + return &bindings.DecodedLog[TestEvent]{ + Data: *decoded, + Log: l, + }, nil +} + +// LogTriggerTestEventLog registers a typed log trigger for TestEvent events. +func (c *LogReadTest) LogTriggerTestEventLog( + chainSelector uint64, + filterName string, + filters []TestEventFilters, + opts *bindings.LogTriggerOptions, +) (cre.Trigger[*solana.Log, *bindings.DecodedLog[TestEvent]], error) { + subkeys, err := c.Codec.EncodeTestEventSubkeys(filters) + if err != nil { + return nil, fmt.Errorf("failed to encode subkeys for TestEvent: %w", err) + } + req := &solana.FilterLogTriggerRequest{ + Address: ProgramID.Bytes(), + ContractIdlJson: []byte(IDL), + EventName: "TestEvent", + Name: filterName, + Subkeys: subkeys, + } + if opts != nil && opts.CPI { + req.CpiFilterConfig = bindings.AnchorCPILogTriggerConfig(ProgramID.Bytes()) + } + rawTrigger := solana.LogTrigger(chainSelector, req) + return &TestEventTrigger{Trigger: rawTrigger}, nil +} diff --git a/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/types.go b/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/types.go new file mode 100644 index 00000000000..b715c07948f --- /dev/null +++ b/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test/types.go @@ -0,0 +1,136 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. +// This file contains parsers for the types defined in the IDL. + +package log_read_test + +import ( + "bytes" + "fmt" + errors "github.com/gagliardetto/anchor-go/errors" + binary "github.com/gagliardetto/binary" + sdk "github.com/smartcontractkit/chainlink-protos/cre/go/sdk" + solana "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/solana" + bindings "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/solana/bindings" + cre "github.com/smartcontractkit/cre-sdk-go/cre" +) + +type TestEvent struct { + StrVal string `json:"str_val"` + U64Value uint64 `json:"u64_value"` +} + +func (obj TestEvent) MarshalWithEncoder(encoder *binary.Encoder) (err error) { + // Serialize `StrVal`: + err = encoder.Encode(obj.StrVal) + if err != nil { + return errors.NewField("StrVal", err) + } + // Serialize `U64Value`: + err = encoder.Encode(obj.U64Value) + if err != nil { + return errors.NewField("U64Value", err) + } + return nil +} + +func (obj TestEvent) Marshal() ([]byte, error) { + buf := bytes.NewBuffer(nil) + encoder := binary.NewBorshEncoder(buf) + err := obj.MarshalWithEncoder(encoder) + if err != nil { + return nil, fmt.Errorf("error while encoding TestEvent: %w", err) + } + return buf.Bytes(), nil +} + +func (obj *TestEvent) UnmarshalWithDecoder(decoder *binary.Decoder) (err error) { + // Deserialize `StrVal`: + err = decoder.Decode(&obj.StrVal) + if err != nil { + return errors.NewField("StrVal", err) + } + // Deserialize `U64Value`: + err = decoder.Decode(&obj.U64Value) + if err != nil { + return errors.NewField("U64Value", err) + } + return nil +} + +func (obj *TestEvent) Unmarshal(buf []byte) error { + err := obj.UnmarshalWithDecoder(binary.NewBorshDecoder(buf)) + if err != nil { + return fmt.Errorf("error while unmarshaling TestEvent: %w", err) + } + return nil +} + +func UnmarshalTestEvent(buf []byte) (*TestEvent, error) { + obj := new(TestEvent) + err := obj.Unmarshal(buf) + if err != nil { + return nil, err + } + return obj, nil +} + +func (c *Codec) EncodeTestEventStruct(in TestEvent) ([]byte, error) { + return in.Marshal() +} + +// WriteReportFromTestEvent encodes the input struct, hashes the provided accounts, // generates a signed report, and submits it via WriteReport. // // remainingAccounts must follow the keystone-forwarder account layout: // - Index 0: forwarderState – the forwarder program's state account. // - Index 1: forwarderAuthority – PDA derived from seeds // ["forwarder", forwarderState, receiverProgram] under the forwarder program ID. // - Index 2+: receiver-specific accounts required by the target program. // // The full slice is hashed (via CalculateAccountsHash) into the report and forwarded // as WriteCreReportRequest.RemainingAccounts. The on-chain forwarder strips indices 0 and 1 // before CPI-ing into the receiver, so they must be present and correctly ordered. +func (c *LogReadTest) WriteReportFromTestEvent( + runtime cre.Runtime, + input TestEvent, + remainingAccounts []*solana.AccountMeta, + computeConfig *solana.ComputeConfig, +) cre.Promise[*solana.WriteReportReply] { + encodedInput, err := c.Codec.EncodeTestEventStruct(input) + if err != nil { + return cre.PromiseFromResult[*solana.WriteReportReply](nil, err) + } + + encodedAccountList := bindings.CalculateAccountsHash(remainingAccounts) + + fwdReport := bindings.ForwarderReport{ + AccountHash: encodedAccountList, + Payload: encodedInput, + } + encodedFwdReport, err := fwdReport.Marshal() + if err != nil { + return cre.PromiseFromResult[*solana.WriteReportReply](nil, err) + } + + promise := runtime.GenerateReport(&sdk.ReportRequest{ + EncodedPayload: encodedFwdReport, + EncoderName: "solana", + HashingAlgo: "keccak256", + SigningAlgo: "ecdsa", + }) + + return cre.ThenPromise(promise, func(report *cre.Report) cre.Promise[*solana.WriteReportReply] { + return c.client.WriteReport(runtime, &solana.WriteCreReportRequest{ + ComputeConfig: computeConfig, + Receiver: ProgramID.Bytes(), + RemainingAccounts: remainingAccounts, + Report: report, + }) + }) +} + +func (c *LogReadTest) WriteReportFromTestEvents( + runtime cre.Runtime, + inputs []TestEvent, + remainingAccounts []*solana.AccountMeta, + computeConfig *solana.ComputeConfig, +) cre.Promise[*solana.WriteReportReply] { + elements := make([][]byte, len(inputs)) + for i, input := range inputs { + encoded, err := c.Codec.EncodeTestEventStruct(input) + if err != nil { + return cre.PromiseFromResult[*solana.WriteReportReply](nil, err) + } + elements[i] = encoded + } + return c.WriteReportFromBorshEncodedVec(runtime, elements, remainingAccounts, computeConfig) +} diff --git a/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/idl/log_read_test.json b/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/idl/log_read_test.json new file mode 100644 index 00000000000..3597a6debd4 --- /dev/null +++ b/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/idl/log_read_test.json @@ -0,0 +1,134 @@ +{ + "address": "J1zQwrBNBngz26jRPNWsUSZMHJwBwpkoDitXRV95LdK4", + "metadata": { + "name": "log_read_test", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "instructions": [ + { + "name": "create_log", + "discriminator": [ + 215, + 95, + 248, + 114, + 153, + 204, + 208, + 48 + ], + "accounts": [ + { + "name": "authority", + "signer": true + }, + { + "name": "system_program" + } + ], + "args": [ + { + "name": "value", + "type": "u64" + } + ] + }, + { + "name": "create_log_cpi", + "discriminator": [ + 101, + 207, + 236, + 250, + 214, + 98, + 173, + 146 + ], + "accounts": [ + { + "name": "authority", + "signer": true + }, + { + "name": "system_program" + }, + { + "name": "event_authority" + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "value", + "type": "u64" + } + ] + }, + { + "name": "create_truncated_log", + "discriminator": [ + 133, + 74, + 116, + 132, + 80, + 11, + 241, + 64 + ], + "accounts": [ + { + "name": "authority", + "signer": true + }, + { + "name": "system_program" + } + ], + "args": [ + { + "name": "value", + "type": "u64" + } + ] + } + ], + "events": [ + { + "name": "TestEvent", + "discriminator": [ + 28, + 52, + 39, + 105, + 8, + 210, + 91, + 9 + ] + } + ], + "types": [ + { + "name": "TestEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "str_val", + "type": "string" + }, + { + "name": "u64_value", + "type": "u64" + } + ] + } + } + ] +} diff --git a/system-tests/tests/smoke/cre/solana/sollogtrigger/go.mod b/system-tests/tests/smoke/cre/solana/sollogtrigger/go.mod index c7a08ae0d65..29ff14339d2 100644 --- a/system-tests/tests/smoke/cre/solana/sollogtrigger/go.mod +++ b/system-tests/tests/smoke/cre/solana/sollogtrigger/go.mod @@ -3,10 +3,13 @@ module github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/solana go 1.26.4 require ( + github.com/gagliardetto/anchor-go v1.0.0 + github.com/gagliardetto/binary v0.8.0 github.com/gagliardetto/solana-go v1.14.0 github.com/smartcontractkit/chain-selectors v1.0.100 - github.com/smartcontractkit/cre-sdk-go v1.9.0-capdev.1.0.20260526211007-043b75d05749 - github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/solana v0.1.1-0.20260526211007-043b75d05749 + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260609153034-c8423a41ef9a + github.com/smartcontractkit/cre-sdk-go v1.11.0 + github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/solana v0.1.1-0.20260612142557-01f4db8d7d47 gopkg.in/yaml.v3 v3.0.1 ) @@ -18,7 +21,6 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/ethereum/go-ethereum v1.17.2 // indirect github.com/fatih/color v1.18.0 // indirect - github.com/gagliardetto/binary v0.8.0 // indirect github.com/gagliardetto/treeout v0.1.4 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/holiman/uint256 v1.3.2 // indirect @@ -34,7 +36,6 @@ require ( github.com/mr-tron/base58 v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/shopspring/decimal v1.4.0 // indirect - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260521154055-6c3fbf9a2435 // indirect github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect github.com/stretchr/testify v1.11.1 // indirect go.mongodb.org/mongo-driver v1.17.2 // indirect diff --git a/system-tests/tests/smoke/cre/solana/sollogtrigger/go.sum b/system-tests/tests/smoke/cre/solana/sollogtrigger/go.sum index 1b657782954..1341192d02e 100644 --- a/system-tests/tests/smoke/cre/solana/sollogtrigger/go.sum +++ b/system-tests/tests/smoke/cre/solana/sollogtrigger/go.sum @@ -19,6 +19,8 @@ github.com/ethereum/go-ethereum v1.17.2 h1:ag6geu0kn8Hv5FLKTpH+Hm2DHD+iuFtuqKxEu github.com/ethereum/go-ethereum v1.17.2/go.mod h1:KHcRXfGOUfUmKg51IhQ0IowiqZ6PqZf08CMtk0g5K1o= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/gagliardetto/anchor-go v1.0.0 h1:YNt9I/9NOrNzz5uuzfzByAcbp39Ft07w63iPqC/wi34= +github.com/gagliardetto/anchor-go v1.0.0/go.mod h1:X6c9bx9JnmwNiyy8hmV5pAsq1c/zzPvkdzeq9/qmlCg= github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg= github.com/gagliardetto/binary v0.8.0/go.mod h1:2tfj51g5o9dnvsc+fL3Jxr22MuWzYXwx9wEoN0XQ7/c= github.com/gagliardetto/solana-go v1.14.0 h1:3WfAi70jOOjAJ0deFMjdhFYlLXATF4tOQXsDNWJtOLw= @@ -71,12 +73,12 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/smartcontractkit/chain-selectors v1.0.100 h1:wpiSpmI/eFjY+wx/nPr5VuNF4hki0prIBMKEaQWn3g4= github.com/smartcontractkit/chain-selectors v1.0.100/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260521154055-6c3fbf9a2435 h1:Nq7qaDBmHj+fvupSN2vOmn72iUGcpqiTquiLuaAXXIY= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260521154055-6c3fbf9a2435/go.mod h1:vTFHTCbLui4Vn8fTmAadfE3rdnvfrDwOmMujmW857D0= -github.com/smartcontractkit/cre-sdk-go v1.9.0-capdev.1.0.20260526211007-043b75d05749 h1:1LLdmJdorKziQF1cYil445rpf4qFy9ShoYj1Q3/QTow= -github.com/smartcontractkit/cre-sdk-go v1.9.0-capdev.1.0.20260526211007-043b75d05749/go.mod h1:6hAl5/hIfy4xUeevNvNf4vywdQ1pClGj4nbdMge4VRM= -github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/solana v0.1.1-0.20260526211007-043b75d05749 h1:dQzWAW9pD2ojkO1nLL8T6hZUnq4uzUjJFxbVomU/ows= -github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/solana v0.1.1-0.20260526211007-043b75d05749/go.mod h1:kmq3r51oAMSK0UG33Bw72uFJI6MmQqBASupUhSyvvwc= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260609153034-c8423a41ef9a h1:alnfvQgCKPFqsfijZQnr6Sbus2GT5YZ9BT/KzVrbszE= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260609153034-c8423a41ef9a/go.mod h1:vTFHTCbLui4Vn8fTmAadfE3rdnvfrDwOmMujmW857D0= +github.com/smartcontractkit/cre-sdk-go v1.11.0 h1:E3MG0j8O9qDv6lDz71HPD3/WRKh/PX2/hfxO1+9YL2w= +github.com/smartcontractkit/cre-sdk-go v1.11.0/go.mod h1:8SDE/e+eDAFpbRjRyKnIalUkQk9BcNbo2aLnda9BM48= +github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/solana v0.1.1-0.20260612142557-01f4db8d7d47 h1:mJBsvYx40drsXCOqPnDoGCgnpEzboOyO6SRL6u4C1Ts= +github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/solana v0.1.1-0.20260612142557-01f4db8d7d47/go.mod h1:3Oja7Gviyrb8dtld04krKCsTQ41RP/3hpCmHXjZf31I= github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 h1:RN5mrigyirb8anBEtdjtHFIufXdacyTi6i4KBfeNXeo= github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/system-tests/tests/smoke/cre/solana/sollogtrigger/main.go b/system-tests/tests/smoke/cre/solana/sollogtrigger/main.go index 1eaf1161de8..5cdfcb7cb8f 100644 --- a/system-tests/tests/smoke/cre/solana/sollogtrigger/main.go +++ b/system-tests/tests/smoke/cre/solana/sollogtrigger/main.go @@ -3,48 +3,24 @@ package main import ( - "encoding/binary" "fmt" "log/slog" chain_selectors "github.com/smartcontractkit/chain-selectors" "github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/solana/sollogtrigger/config" + "github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/solana/sollogtrigger/contracts/solana/src/generated/log_read_test" "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/solana" + solanabindings "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/solana/bindings" "github.com/smartcontractkit/cre-sdk-go/cre" "github.com/smartcontractkit/cre-sdk-go/cre/wasm" "gopkg.in/yaml.v3" ) -const testEventDiscriminatorLen = 8 - -type testEvent struct { - StrVal string - U64Value uint64 -} - -func decodeTestEvent(data []byte) (testEvent, error) { - if len(data) < testEventDiscriminatorLen+4+8 { - return testEvent{}, fmt.Errorf("event data too short: %d bytes", len(data)) - } - - payload := data[testEventDiscriminatorLen:] - strLen := binary.LittleEndian.Uint32(payload[:4]) - strEnd := 4 + int(strLen) - if strLen == 0 || strEnd+8 > len(payload) { - return testEvent{}, fmt.Errorf("invalid TestEvent payload length") - } - - return testEvent{ - StrVal: string(payload[4:strEnd]), - U64Value: binary.LittleEndian.Uint64(payload[strEnd : strEnd+8]), - }, nil -} - -func formatTestEvent(evt testEvent) string { +func formatTestEvent(evt log_read_test.TestEvent) string { return fmt.Sprintf("str_val=%s u64_value=%d", evt.StrVal, evt.U64Value) } -func validateTestEvent(cfg config.Config, evt testEvent) error { +func validateTestEvent(cfg config.Config, evt log_read_test.TestEvent) error { if evt.StrVal != cfg.ExpectedStrVal { return fmt.Errorf("unexpected str_val: got %q want %q", evt.StrVal, cfg.ExpectedStrVal) } @@ -57,63 +33,51 @@ func validateTestEvent(cfg config.Config, evt testEvent) error { func RunSolLogTriggerWorkflow(cfg config.Config, logger *slog.Logger, secretsProvider cre.SecretsProvider) (cre.Workflow[config.Config], error) { logger.Info("RunSolLogTriggerWorkflow called") - if cfg.ContractIdlJSON == "" { - return nil, fmt.Errorf("contract_idl_json is required in workflow config") - } - eventIdlJson := []byte(cfg.ContractIdlJSON) - - expectedValueBytes := make([]byte, 8) - binary.BigEndian.PutUint64(expectedValueBytes, cfg.ExpectedU64Value) - - filterLogTriggerRequest := &solana.FilterLogTriggerRequest{ - Name: "test-event-filter", - Address: cfg.LogReadTestProgramID[:], - EventName: "TestEvent", - ContractIdlJson: eventIdlJson, - Subkeys: []*solana.SubkeyConfig{ - {Path: []string{"U64Value"}, Comparers: []*solana.ValueComparator{ - {Value: expectedValueBytes, Operator: solana.ComparisonOperator_COMPARISON_OPERATOR_EQ}, - }}, - }, - } + chainSelector := chain_selectors.TEST_22222222222222222222222222222222222222222222.Selector + client := &solana.Client{ChainSelector: chainSelector} - filterLogTriggerRequestCPI := &solana.FilterLogTriggerRequest{ - Name: "test-cpi-event-filter", - Address: cfg.LogReadTestProgramID[:], - EventName: "TestEvent", - ContractIdlJson: eventIdlJson, - CpiFilterConfig: &solana.CPIFilterConfig{ - DestAddress: cfg.LogReadTestProgramID[:], - MethodName: []byte("anchor:event"), - }, + logReadTest, err := log_read_test.NewLogReadTest(client) + if err != nil { + return nil, fmt.Errorf("create log_read_test bindings: %w", err) } - chainSelector := chain_selectors.TEST_22222222222222222222222222222222222222222222.Selector - if cfg.CPILogTrigger { - return cre.Workflow[config.Config]{ - cre.Handler( - solana.LogTrigger(chainSelector, filterLogTriggerRequestCPI), - onLogTriggerCPI, - ), - }, nil + filters := []log_read_test.TestEventFilters{ + {U64Value: &cfg.ExpectedU64Value}, } + return createLogTrigger(logReadTest, chainSelector, filters, cfg.CPILogTrigger) +} + +func createLogTrigger(logReadTest *log_read_test.LogReadTest, chainSelector uint64, filters []log_read_test.TestEventFilters, cpi bool) (cre.Workflow[config.Config], error) { + opts := &solanabindings.LogTriggerOptions{CPI: cpi} + name := "test-event-filter" + function := onLogTrigger + if cpi { + name = "test-cpi-event-filter" + function = onLogTriggerCPI + } + trigger, err := logReadTest.LogTriggerTestEventLog( + chainSelector, + name, + filters, + opts, + ) + if err != nil { + return nil, fmt.Errorf("failed to register log trigger: %w", err) + } return cre.Workflow[config.Config]{ - cre.Handler( - solana.LogTrigger(chainSelector, filterLogTriggerRequest), - onLogTrigger, - ), + cre.Handler(trigger, function), }, nil } -func onLogTrigger(cfg config.Config, runtime cre.Runtime, payload *solana.Log) (string, error) { +func onLogTrigger(cfg config.Config, runtime cre.Runtime, payload *solanabindings.DecodedLog[log_read_test.TestEvent]) (string, error) { return handleTestEventLog(cfg, runtime, payload, "TestEvent received: ", "Log trigger received event at block %d", ) } -func onLogTriggerCPI(cfg config.Config, runtime cre.Runtime, payload *solana.Log) (string, error) { +func onLogTriggerCPI(cfg config.Config, runtime cre.Runtime, payload *solanabindings.DecodedLog[log_read_test.TestEvent]) (string, error) { return handleTestEventLog(cfg, runtime, payload, "TestEvent CPI received: ", "CPI log trigger received event at block %d", @@ -123,19 +87,15 @@ func onLogTriggerCPI(cfg config.Config, runtime cre.Runtime, payload *solana.Log func handleTestEventLog( cfg config.Config, runtime cre.Runtime, - payload *solana.Log, + payload *solanabindings.DecodedLog[log_read_test.TestEvent], logPrefix string, resultFmt string, ) (string, error) { - evt, err := decodeTestEvent(payload.Data) - if err != nil { - return "", fmt.Errorf("decode TestEvent: %w", err) - } - if err := validateTestEvent(cfg, evt); err != nil { + if err := validateTestEvent(cfg, payload.Data); err != nil { return "", err } - decoded := formatTestEvent(evt) + decoded := formatTestEvent(payload.Data) runtime.Logger().Info(logPrefix+decoded, "blockNumber", payload.BlockNumber, "txHash", fmt.Sprintf("%x", payload.TxHash), diff --git a/system-tests/tests/smoke/cre/solana_capability_test.go b/system-tests/tests/smoke/cre/solana_capability_test.go index 67fbb1de712..419cf4a47d3 100644 --- a/system-tests/tests/smoke/cre/solana_capability_test.go +++ b/system-tests/tests/smoke/cre/solana_capability_test.go @@ -35,7 +35,6 @@ import ( "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains/evm" "github.com/smartcontractkit/chainlink/system-tests/lib/cre/environment/blockchains/solana" sollogtrigger_config "github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/solana/sollogtrigger/config" - sollogtrigger_idl "github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/solana/sollogtrigger/idl" "github.com/smartcontractkit/chainlink/system-tests/tests/smoke/cre/solana/solwrite/config" t_helpers "github.com/smartcontractkit/chainlink/system-tests/tests/test-helpers" "github.com/smartcontractkit/chainlink/system-tests/tests/test-helpers/configuration" @@ -432,7 +431,6 @@ func ExecuteSolanaLogTriggerTest(t *testing.T, tenv *configuration.TestEnvironme workflowConfig.LogReadTestProgramID = logReadTestProgramID workflowConfig.ExpectedU64Value = expectedU64Value workflowConfig.ExpectedStrVal = expectedStrVal - workflowConfig.ContractIdlJSON = string(sollogtrigger_idl.LogReadTest) workflowConfig.CPILogTrigger = false const workflowFileLocation = "./solana/sollogtrigger/main.go" @@ -601,7 +599,6 @@ func ExecuteSolanaLogTriggerCPITest(t *testing.T, tenv *configuration.TestEnviro workflowConfig.LogReadTestProgramID = logReadTestProgramID workflowConfig.ExpectedU64Value = expectedU64Value workflowConfig.ExpectedStrVal = expectedStrVal - workflowConfig.ContractIdlJSON = string(sollogtrigger_idl.LogReadTest) workflowConfig.CPILogTrigger = true const workflowFileLocation = "./solana/sollogtrigger/main.go"