From 9b138f50ab2b56e08badd904a0924ee0475f8598 Mon Sep 17 00:00:00 2001 From: Vladimir Shchukin Date: Tue, 7 Apr 2026 07:53:44 -0400 Subject: [PATCH] add descriminator for zero arg instructions --- .../anchor-go/generator/instructions.go | 53 ++++++------------- .../anchor-go/generator/instructions_test.go | 48 +++++++++++++++++ cmd/generate-bindings/solana/bindings_test.go | 39 ++++++++++++++ .../testdata/data_storage/instructions.go | 30 +++++++++-- 4 files changed, 129 insertions(+), 41 deletions(-) create mode 100644 cmd/generate-bindings/solana/anchor-go/generator/instructions_test.go diff --git a/cmd/generate-bindings/solana/anchor-go/generator/instructions.go b/cmd/generate-bindings/solana/anchor-go/generator/instructions.go index 2f8123ba..43191feb 100644 --- a/cmd/generate-bindings/solana/anchor-go/generator/instructions.go +++ b/cmd/generate-bindings/solana/anchor-go/generator/instructions.go @@ -99,40 +99,21 @@ func (g *Generator) gen_instructions() (*OutputFile, error) { returnsCode.Qual(PkgSolanaGo, "Instruction") returnsCode.Error() }).BlockFunc(func(body *Group) { - if len(instruction.Args) > 0 { - body.Id("buf__").Op(":=").New(Qual("bytes", "Buffer")) - body.Id("enc__").Op(":=").Qual(PkgBinary, "NewBorshEncoder").Call(Id("buf__")) + body.Id("buf__").Op(":=").New(Qual("bytes", "Buffer")) + body.Id("enc__").Op(":=").Qual(PkgBinary, "NewBorshEncoder").Call(Id("buf__")) - { - // write the discriminator - body.Line().Comment("Encode the instruction discriminator.") - discriminatorName := FormatInstructionDiscriminatorName(instruction.Name) - body.Err().Op(":=").Id("enc__").Dot("WriteBytes").Call(Id(discriminatorName).Index(Op(":")), False()) - body.If(Err().Op("!=").Nil()).Block( - Return( - Nil(), - Qual("fmt", "Errorf").Call(Lit("failed to write instruction discriminator: %w"), Err()), - ), - ) - } - // for _, param := range instruction.Args { - // paramName := formatParamName(param.Name) - // isComplexEnum(param.Ty) - - // body.Line().Commentf("Encode the parameter: %s", paramName) - // body.Block( - // Err().Op(":=").Id("enc__").Dot("Encode").Call(Id(paramName)), - // If(Err().Op("!=").Nil()).Block( - // Return( - // Nil(), - // Qual(PkgAnchorGoErrors, "NewField").Call( - // Lit(paramName), - // Err(), - // ), - // ), - // ), - // ) - // } + { + body.Line().Comment("Encode the instruction discriminator.") + discriminatorName := FormatInstructionDiscriminatorName(instruction.Name) + body.Err().Op(":=").Id("enc__").Dot("WriteBytes").Call(Id(discriminatorName).Index(Op(":")), False()) + body.If(Err().Op("!=").Nil()).Block( + Return( + Nil(), + Qual("fmt", "Errorf").Call(Lit("failed to write instruction discriminator: %w"), Err()), + ), + ) + } + if len(instruction.Args) > 0 { checkNil := true body.BlockFunc(func(g *Group) { gen_marshal_DefinedFieldsNamed( @@ -223,11 +204,7 @@ func (g *Generator) gen_instructions() (*OutputFile, error) { ListMultiline(func(gg *Group) { gg.Id("ProgramID") gg.Id("accounts__") - if len(instruction.Args) > 0 { - gg.Id("buf__").Dot("Bytes").Call() - } else { - gg.Nil() // No arguments to encode. - } + gg.Id("buf__").Dot("Bytes").Call() }), ) }, diff --git a/cmd/generate-bindings/solana/anchor-go/generator/instructions_test.go b/cmd/generate-bindings/solana/anchor-go/generator/instructions_test.go new file mode 100644 index 00000000..05e5fc74 --- /dev/null +++ b/cmd/generate-bindings/solana/anchor-go/generator/instructions_test.go @@ -0,0 +1,48 @@ +//nolint:all // Forked from anchor-go generator, maintaining original code structure +package generator + +import ( + "strings" + "testing" + + "github.com/gagliardetto/anchor-go/idl" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenInstructionsZeroArgsAlwaysWritesDiscriminator(t *testing.T) { + idlData := &idl.Idl{ + Instructions: []idl.IdlInstruction{ + { + Name: "ping", + Discriminator: idl.IdlDiscriminator{1, 2, 3, 4, 5, 6, 7, 8}, + Args: []idl.IdlField{}, + }, + }, + } + + gen := &Generator{ + idl: idlData, + options: &GeneratorOptions{Package: "test"}, + } + + outputFile, err := gen.gen_instructions() + require.NoError(t, err) + require.NotNil(t, outputFile) + + code := outputFile.File.GoString() + + assert.Contains(t, code, "NewBorshEncoder", + "zero-arg instruction builder must allocate an encoder for the discriminator") + assert.Contains(t, code, "WriteBytes", + "zero-arg instruction builder must write the discriminator bytes") + assert.NotContains(t, code, "nil, // No arguments to encode", + "zero-arg instruction must not pass nil as instruction data") + + // The generated code must reference buf__.Bytes() so the discriminator is sent. + builderIdx := strings.Index(code, "func NewPingInstruction") + require.Greater(t, builderIdx, 0, "expected to find NewPingInstruction in generated code") + builderSnippet := code[builderIdx:] + assert.Contains(t, builderSnippet, "buf__.Bytes()", + "zero-arg instruction must pass buf__.Bytes() (containing the discriminator) as instruction data") +} diff --git a/cmd/generate-bindings/solana/bindings_test.go b/cmd/generate-bindings/solana/bindings_test.go index bff2ad1f..942e515b 100644 --- a/cmd/generate-bindings/solana/bindings_test.go +++ b/cmd/generate-bindings/solana/bindings_test.go @@ -123,6 +123,45 @@ func TestWriteReportMethods(t *testing.T) { }, response), "Response should match expected WriteReportReply") } +func TestZeroArgInstructionRoundTrip(t *testing.T) { + zeroArgInstructions := []struct { + name string + buildFn func() (solana.Instruction, error) + expectedTyp datastorage.Instruction + }{ + { + name: "get_multiple_reserves", + buildFn: datastorage.NewGetMultipleReservesInstruction, + expectedTyp: &datastorage.GetMultipleReservesInstruction{}, + }, + { + name: "get_reserves", + buildFn: datastorage.NewGetReservesInstruction, + expectedTyp: &datastorage.GetReservesInstruction{}, + }, + { + name: "get_tuple_reserves", + buildFn: datastorage.NewGetTupleReservesInstruction, + expectedTyp: &datastorage.GetTupleReservesInstruction{}, + }, + } + + for _, tc := range zeroArgInstructions { + t.Run(tc.name, func(t *testing.T) { + ix, err := tc.buildFn() + require.NoError(t, err, "building instruction should succeed") + + data, err := ix.Data() + require.NoError(t, err) + require.Len(t, data, 8, "zero-arg instruction data must be exactly the 8-byte discriminator") + + parsed, err := datastorage.ParseInstructionWithoutAccounts(data) + require.NoError(t, err, "ParseInstruction must accept the discriminator-only data produced by the builder") + require.IsType(t, tc.expectedTyp, parsed) + }) + } +} + func TestEncodeStruct(t *testing.T) { client := &solanasdk.Client{ChainSelector: anyChainSelector} ds, err := datastorage.NewDataStorage(client) diff --git a/cmd/generate-bindings/solana/testdata/data_storage/instructions.go b/cmd/generate-bindings/solana/testdata/data_storage/instructions.go index 2bed748e..1676a5e1 100644 --- a/cmd/generate-bindings/solana/testdata/data_storage/instructions.go +++ b/cmd/generate-bindings/solana/testdata/data_storage/instructions.go @@ -13,37 +13,61 @@ import ( // Builds a "get_multiple_reserves" instruction. func NewGetMultipleReservesInstruction() (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_GetMultipleReserves[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } accounts__ := solanago.AccountMetaSlice{} // Create the instruction. return solanago.NewInstruction( ProgramID, accounts__, - nil, + buf__.Bytes(), ), nil } // Builds a "get_reserves" instruction. func NewGetReservesInstruction() (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_GetReserves[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } accounts__ := solanago.AccountMetaSlice{} // Create the instruction. return solanago.NewInstruction( ProgramID, accounts__, - nil, + buf__.Bytes(), ), nil } // Builds a "get_tuple_reserves" instruction. func NewGetTupleReservesInstruction() (solanago.Instruction, error) { + buf__ := new(bytes.Buffer) + enc__ := binary.NewBorshEncoder(buf__) + + // Encode the instruction discriminator. + err := enc__.WriteBytes(Instruction_GetTupleReserves[:], false) + if err != nil { + return nil, fmt.Errorf("failed to write instruction discriminator: %w", err) + } accounts__ := solanago.AccountMetaSlice{} // Create the instruction. return solanago.NewInstruction( ProgramID, accounts__, - nil, + buf__.Bytes(), ), nil }