From 7cc92fbde82df100ae43cce1daccfa611986805a Mon Sep 17 00:00:00 2001 From: ilija42 <57732589+ilija42@users.noreply.github.com> Date: Thu, 19 Dec 2024 19:50:51 +0100 Subject: [PATCH] Codec interface tests (#967) * Connect codec interface tests and refactor codec to interface like EVM one * progress * Fully implement Codec interface tests * Run codec tests in loop * Prettify codec and codec tests * Refactor codec nil encoding handling * Revert accidental changes to testIDL.json * Add sonar exclusion for codec test utils * Add sq exclusion for duplications in testutils, add decoder unit tests * Add encoder unit test * Fix lint and rename codec to solanacodec to avoid types name collision * Solana codec entry improvements * Fix Solana codec field casing * minor err messages improvements * Code improvements * Fix encoder unit tests * Fix sonar exclusions * lint * Reorder methods in Solana codec * Fix CR integration tests config * Revert TestNewIDLCodec_WithModifiers deletion * Add comments for codec entry includeDiscriminator option * Add discriminator value check in codec entry Decode * Reuse utils from interface tests for Solana codec interface tests * Fix comment * Fix comment * [Non-EVM-1062] Solana Codec events support, Hookup Fuzz tests and cleanup Codec init (#987) * Add events IDL parsing to codec * temp * Add a basic codec test for event IDL parsing * Cleanup Solana Codec init * Hookup Codec fuzz tests * delete an unnecessary comment * lint --------- Co-authored-by: Jonghyeon Park --- go.mod | 4 +- go.sum | 8 +- integration-tests/go.mod | 4 +- integration-tests/go.sum | 8 +- .../relayinterface/chain_components_test.go | 6 +- pkg/solana/codec/codec_entry.go | 192 ++++++ pkg/solana/codec/codec_test.go | 158 +++++ pkg/solana/codec/decoder.go | 35 ++ pkg/solana/codec/decoder_test.go | 90 +++ pkg/solana/codec/discriminator.go | 16 +- pkg/solana/codec/encoder.go | 35 ++ pkg/solana/codec/encoder_test.go | 103 ++++ pkg/solana/codec/parsed_types.go | 48 ++ pkg/solana/codec/solana.go | 241 +++++--- .../codec/testutils/eventItemTypeIDL.json | 73 +++ .../codec/testutils/itemArray1TypeIDL.json | 92 +++ .../codec/testutils/itemArray2TypeIDL.json | 92 +++ pkg/solana/codec/testutils/itemIDL.json | 77 +++ .../codec/testutils/itemSliceTypeIDL.json | 89 +++ pkg/solana/codec/testutils/nilTypeIDL.json | 12 + .../codec/testutils/sizeItemTypeIDL.json | 38 ++ pkg/solana/codec/testutils/types.go | 561 +++++++++++++++++- pkg/solana/codec/types.go | 24 + pkg/solana/config/chain_reader.go | 4 +- sonar-project.properties | 5 +- 25 files changed, 1918 insertions(+), 97 deletions(-) create mode 100644 pkg/solana/codec/codec_entry.go create mode 100644 pkg/solana/codec/codec_test.go create mode 100644 pkg/solana/codec/decoder.go create mode 100644 pkg/solana/codec/decoder_test.go create mode 100644 pkg/solana/codec/encoder.go create mode 100644 pkg/solana/codec/encoder_test.go create mode 100644 pkg/solana/codec/parsed_types.go create mode 100644 pkg/solana/codec/testutils/eventItemTypeIDL.json create mode 100644 pkg/solana/codec/testutils/itemArray1TypeIDL.json create mode 100644 pkg/solana/codec/testutils/itemArray2TypeIDL.json create mode 100644 pkg/solana/codec/testutils/itemIDL.json create mode 100644 pkg/solana/codec/testutils/itemSliceTypeIDL.json create mode 100644 pkg/solana/codec/testutils/nilTypeIDL.json create mode 100644 pkg/solana/codec/testutils/sizeItemTypeIDL.json create mode 100644 pkg/solana/codec/types.go diff --git a/go.mod b/go.mod index 22ff1dd69..8e6423815 100644 --- a/go.mod +++ b/go.mod @@ -20,8 +20,8 @@ require ( github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 - golang.org/x/sync v0.8.0 - golang.org/x/text v0.18.0 + golang.org/x/sync v0.10.0 + golang.org/x/text v0.21.0 ) require ( diff --git a/go.sum b/go.sum index a44777df8..a2d9c0b39 100644 --- a/go.sum +++ b/go.sum @@ -682,8 +682,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -746,8 +746,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index bde3745bc..78f6d6394 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -25,8 +25,8 @@ require ( github.com/stretchr/testify v1.9.0 github.com/testcontainers/testcontainers-go v0.34.0 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c - golang.org/x/sync v0.8.0 - golang.org/x/text v0.19.0 + golang.org/x/sync v0.10.0 + golang.org/x/text v0.21.0 gopkg.in/guregu/null.v4 v4.0.0 ) diff --git a/integration-tests/go.sum b/integration-tests/go.sum index cc45a70d9..19dd0b8a2 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1692,8 +1692,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1804,8 +1804,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/integration-tests/relayinterface/chain_components_test.go b/integration-tests/relayinterface/chain_components_test.go index 6af9c3444..9b7be2f0a 100644 --- a/integration-tests/relayinterface/chain_components_test.go +++ b/integration-tests/relayinterface/chain_components_test.go @@ -132,7 +132,7 @@ func (it *SolanaChainComponentsInterfaceTester[T]) Setup(t T) { Procedure: config.ChainReaderProcedure{ IDLAccount: "DataAccount", OutputModifications: codec.ModifiersConfig{ - &codec.PropertyExtractorConfig{FieldName: "U64value"}, + &codec.PropertyExtractorConfig{FieldName: "U64Value"}, }, }, }, @@ -142,7 +142,7 @@ func (it *SolanaChainComponentsInterfaceTester[T]) Setup(t T) { Procedure: config.ChainReaderProcedure{ IDLAccount: "DataAccount", OutputModifications: codec.ModifiersConfig{ - &codec.PropertyExtractorConfig{FieldName: "U64slice"}, + &codec.PropertyExtractorConfig{FieldName: "U64Slice"}, }, }, }, @@ -156,7 +156,7 @@ func (it *SolanaChainComponentsInterfaceTester[T]) Setup(t T) { Procedure: config.ChainReaderProcedure{ IDLAccount: "DataAccount", OutputModifications: codec.ModifiersConfig{ - &codec.PropertyExtractorConfig{FieldName: "U64value"}, + &codec.PropertyExtractorConfig{FieldName: "U64Value"}, }, }, }, diff --git a/pkg/solana/codec/codec_entry.go b/pkg/solana/codec/codec_entry.go new file mode 100644 index 000000000..bc42ae968 --- /dev/null +++ b/pkg/solana/codec/codec_entry.go @@ -0,0 +1,192 @@ +package codec + +import ( + "bytes" + "fmt" + "reflect" + + "github.com/smartcontractkit/chainlink-common/pkg/codec" + commonencodings "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" +) + +type Entry interface { + Encode(value any, into []byte) ([]byte, error) + Decode(encoded []byte) (any, []byte, error) + GetCodecType() commonencodings.TypeCodec + GetType() reflect.Type + Modifier() codec.Modifier + Size(numItems int) (int, error) + FixedSize() (int, error) +} + +type entry struct { + // TODO this might not be needed in the end, it was handy to make tests simpler + offchainName string + onchainName string + reflectType reflect.Type + typeCodec commonencodings.TypeCodec + mod codec.Modifier + // includeDiscriminator during Encode adds a discriminator to the encoded bytes under an assumption that the provided value didn't have a discriminator. + // During Decode includeDiscriminator removes discriminator from bytes under an assumption that the provided struct doesn't need a discriminator. + includeDiscriminator bool + discriminator Discriminator +} + +func NewAccountEntry(offchainName string, idlAccount IdlTypeDef, idlTypes IdlTypeDefSlice, includeDiscriminator bool, mod codec.Modifier, builder commonencodings.Builder) (Entry, error) { + _, accCodec, err := createCodecType(idlAccount, createRefs(idlTypes, builder), false) + if err != nil { + return nil, err + } + + return newEntry( + offchainName, + idlAccount.Name, + accCodec, + includeDiscriminator, + mod, + ), nil +} + +func NewInstructionArgsEntry(offChainName string, instructions IdlInstruction, idlTypes IdlTypeDefSlice, mod codec.Modifier, builder commonencodings.Builder) (Entry, error) { + _, instructionCodecArgs, err := asStruct(instructions.Args, createRefs(idlTypes, builder), instructions.Name, false, true) + if err != nil { + return nil, err + } + + return newEntry( + offChainName, + instructions.Name, + instructionCodecArgs, + // Instruction arguments don't need a discriminator by default + false, + mod, + ), nil +} + +func NewEventArgsEntry(offChainName string, event IdlEvent, idlTypes IdlTypeDefSlice, includeDiscriminator bool, mod codec.Modifier, builder commonencodings.Builder) (Entry, error) { + _, eventCodec, err := asStruct(eventFieldsToFields(event.Fields), createRefs(idlTypes, builder), event.Name, false, false) + if err != nil { + return nil, err + } + + return newEntry( + offChainName, + event.Name, + eventCodec, + includeDiscriminator, + mod, + ), nil +} + +func newEntry( + offchainName, onchainName string, + typeCodec commonencodings.TypeCodec, + includeDiscriminator bool, + mod codec.Modifier, +) Entry { + return &entry{ + offchainName: offchainName, + onchainName: onchainName, + reflectType: typeCodec.GetType(), + typeCodec: typeCodec, + mod: ensureModifier(mod), + includeDiscriminator: includeDiscriminator, + discriminator: *NewDiscriminator(onchainName), + } +} + +func createRefs(idlTypes IdlTypeDefSlice, builder commonencodings.Builder) *codecRefs { + return &codecRefs{ + builder: builder, + codecs: make(map[string]commonencodings.TypeCodec), + typeDefs: idlTypes, + dependencies: make(map[string][]string), + } +} + +func (e *entry) Encode(value any, into []byte) ([]byte, error) { + // Special handling for encoding a nil pointer to an empty struct. + t := e.reflectType + if value == nil { + if t.Kind() == reflect.Pointer { + elem := t.Elem() + if elem.Kind() == reflect.Struct && elem.NumField() == 0 { + return []byte{}, nil + } + } + return nil, fmt.Errorf("%w: cannot encode nil value for offchainName: %q, onchainName: %q", + commontypes.ErrInvalidType, e.offchainName, e.onchainName) + } + + encodedVal, err := e.typeCodec.Encode(value, into) + if err != nil { + return nil, err + } + + if e.includeDiscriminator { + var byt []byte + encodedDisc, err := e.discriminator.Encode(&e.discriminator.hashPrefix, byt) + if err != nil { + return nil, err + } + return append(encodedDisc, encodedVal...), nil + } + + return encodedVal, nil +} + +func (e *entry) Decode(encoded []byte) (any, []byte, error) { + if e.includeDiscriminator { + if len(encoded) < discriminatorLength { + return nil, nil, fmt.Errorf("%w: encoded data too short to contain discriminator for offchainName: %q, onchainName: %q", + commontypes.ErrInvalidType, e.offchainName, e.onchainName) + } + + if !bytes.Equal(e.discriminator.hashPrefix, encoded[:discriminatorLength]) { + return nil, nil, fmt.Errorf("%w: encoded data has a bad discriminator %v for offchainName: %q, onchainName: %q", + commontypes.ErrInvalidType, encoded[:discriminatorLength], e.offchainName, e.onchainName) + } + + encoded = encoded[discriminatorLength:] + } + return e.typeCodec.Decode(encoded) +} + +func (e *entry) GetCodecType() commonencodings.TypeCodec { + return e.typeCodec +} + +func (e *entry) GetType() reflect.Type { + return e.reflectType +} + +func (e *entry) Modifier() codec.Modifier { + return e.mod +} + +func (e *entry) Size(numItems int) (int, error) { + return e.typeCodec.Size(numItems) +} + +func (e *entry) FixedSize() (int, error) { + return e.typeCodec.FixedSize() +} + +func ensureModifier(mod codec.Modifier) codec.Modifier { + if mod == nil { + return codec.MultiModifier{} + } + return mod +} + +func eventFieldsToFields(evFields []IdlEventField) []IdlField { + var idlFields []IdlField + for _, evField := range evFields { + idlFields = append(idlFields, IdlField{ + Name: evField.Name, + Type: evField.Type, + }) + } + return idlFields +} diff --git a/pkg/solana/codec/codec_test.go b/pkg/solana/codec/codec_test.go new file mode 100644 index 000000000..8a9dedb45 --- /dev/null +++ b/pkg/solana/codec/codec_test.go @@ -0,0 +1,158 @@ +package codec_test + +import ( + "bytes" + _ "embed" + "slices" + "testing" + + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + ocr2types "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" + looptestutils "github.com/smartcontractkit/chainlink-common/pkg/loop/testutils" + clcommontypes "github.com/smartcontractkit/chainlink-common/pkg/types" + . "github.com/smartcontractkit/chainlink-common/pkg/types/interfacetests" //nolint common practice to import test mods with . + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec/testutils" +) + +const anyExtraValue = 3 + +func TestCodec(t *testing.T) { + tester := &codecInterfaceTester{} + RunCodecInterfaceTests(t, tester) + RunCodecInterfaceTests(t, looptestutils.WrapCodecTesterForLoop(tester)) + + t.Run("Events are encode-able and decode-able for a single item", func(t *testing.T) { + ctx := tests.Context(t) + item := CreateTestStruct[*testing.T](0, tester) + req := &EncodeRequest{TestStructs: []TestStruct{item}, TestOn: testutils.TestEventItem} + resp := tester.EncodeFields(t, req) + + codec := tester.GetCodec(t) + actualEncoding, err := codec.Encode(ctx, item, testutils.TestEventItem) + require.NoError(t, err) + assert.Equal(t, resp, actualEncoding) + + into := TestStruct{} + require.NoError(t, codec.Decode(ctx, actualEncoding, &into, testutils.TestEventItem)) + assert.Equal(t, item, into) + }) +} + +func FuzzCodec(f *testing.F) { + tester := &codecInterfaceTester{} + RunCodecInterfaceFuzzTests(f, tester) +} + +type codecInterfaceTester struct { + TestSelectionSupport +} + +func (it *codecInterfaceTester) Setup(_ *testing.T) {} + +func (it *codecInterfaceTester) GetAccountBytes(_ int) []byte { + // TODO solana base58 string can be of variable length, this value is always 44, but it should be able to handle any length 32-44 + pk := solana.PublicKeyFromBytes([]byte{220, 108, 195, 188, 166, 6, 163, 39, 197, 131, 44, 38, 154, 177, 232, 80, 141, 50, 7, 65, 28, 65, 182, 165, 57, 5, 176, 68, 46, 181, 58, 245}) + return pk.Bytes() +} + +func (it *codecInterfaceTester) GetAccountString(i int) string { + return solana.PublicKeyFromBytes(it.GetAccountBytes(i)).String() +} + +func (it *codecInterfaceTester) EncodeFields(t *testing.T, request *EncodeRequest) []byte { + if request.TestOn == TestItemType || request.TestOn == testutils.TestEventItem { + return encodeFieldsOnItem(t, request) + } + + return encodeFieldsOnSliceOrArray(t, request) +} + +func encodeFieldsOnItem(t *testing.T, request *EncodeRequest) ocr2types.Report { + buf := new(bytes.Buffer) + // The underlying TestItemAsAccount adds a discriminator by default while being Borsh encoded. + if err := testutils.EncodeRequestToTestItemAsAccount(request.TestStructs[0]).MarshalWithEncoder(bin.NewBorshEncoder(buf)); err != nil { + require.NoError(t, err) + } + return buf.Bytes() +} + +func encodeFieldsOnSliceOrArray(t *testing.T, request *EncodeRequest) []byte { + var toEncode interface{} + buf := new(bytes.Buffer) + switch request.TestOn { + case TestItemArray1Type: + toEncode = [1]testutils.TestItemAsArgs{testutils.EncodeRequestToTestItemAsArgs(request.TestStructs[0])} + case TestItemArray2Type: + toEncode = [2]testutils.TestItemAsArgs{testutils.EncodeRequestToTestItemAsArgs(request.TestStructs[0]), testutils.EncodeRequestToTestItemAsArgs(request.TestStructs[1])} + default: + // encode TestItemSliceType as instruction args (similar to accounts, but no discriminator) because accounts can't be just a vector + var itemSliceType []testutils.TestItemAsArgs + for _, req := range request.TestStructs { + itemSliceType = append(itemSliceType, testutils.EncodeRequestToTestItemAsArgs(req)) + } + toEncode = itemSliceType + } + + if err := bin.NewBorshEncoder(buf).Encode(toEncode); err != nil { + require.NoError(t, err) + } + return buf.Bytes() +} + +func (it *codecInterfaceTester) GetCodec(t *testing.T) clcommontypes.Codec { + codecConfig := codec.Config{Configs: map[string]codec.ChainConfig{}} + TestItem := CreateTestStruct[*testing.T](0, it) + for offChainName, v := range testutils.CodecDefs { + codecEntryCfg := codecConfig.Configs[offChainName] + codecEntryCfg.IDL = v.IDL + codecEntryCfg.Type = v.ItemType + codecEntryCfg.OnChainName = v.IDLTypeName + + if offChainName != NilType { + codecEntryCfg.ModifierConfigs = commoncodec.ModifiersConfig{ + &commoncodec.RenameModifierConfig{Fields: map[string]string{"NestedDynamicStruct.Inner.IntVal": "I"}}, + &commoncodec.RenameModifierConfig{Fields: map[string]string{"NestedStaticStruct.Inner.IntVal": "I"}}, + } + } + + if slices.Contains([]string{TestItemType, TestItemSliceType, TestItemArray1Type, TestItemArray2Type, testutils.TestItemWithConfigExtraType, testutils.TestEventItem}, offChainName) { + addressByteModifier := &commoncodec.AddressBytesToStringModifierConfig{ + Fields: []string{"AccountStruct.AccountStr"}, + Modifier: codec.SolanaAddressModifier{}, + } + codecEntryCfg.ModifierConfigs = append(codecEntryCfg.ModifierConfigs, addressByteModifier) + } + + if offChainName == testutils.TestItemWithConfigExtraType { + hardCode := &commoncodec.HardCodeModifierConfig{ + OnChainValues: map[string]any{ + "BigField": TestItem.BigField.String(), + "AccountStruct.Account": solana.PublicKeyFromBytes(TestItem.AccountStruct.Account), + }, + OffChainValues: map[string]any{"ExtraField": anyExtraValue}, + } + codecEntryCfg.ModifierConfigs = append(codecEntryCfg.ModifierConfigs, hardCode) + } + codecConfig.Configs[offChainName] = codecEntryCfg + } + + c, err := codec.NewCodec(codecConfig) + require.NoError(t, err) + + return c +} + +func (it *codecInterfaceTester) IncludeArrayEncodingSizeEnforcement() bool { + return true +} +func (it *codecInterfaceTester) Name() string { + return "Solana" +} diff --git a/pkg/solana/codec/decoder.go b/pkg/solana/codec/decoder.go new file mode 100644 index 000000000..242dbc44f --- /dev/null +++ b/pkg/solana/codec/decoder.go @@ -0,0 +1,35 @@ +package codec + +import ( + "context" + "fmt" + + "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" +) + +type Decoder struct { + definitions map[string]Entry + codecFromTypeCodec encodings.CodecFromTypeCodec +} + +var _ commontypes.Decoder = &Decoder{} + +func (d *Decoder) Decode(ctx context.Context, raw []byte, into any, itemType string) (err error) { + if d.codecFromTypeCodec == nil { + d.codecFromTypeCodec = make(encodings.CodecFromTypeCodec) + for k, v := range d.definitions { + d.codecFromTypeCodec[k] = v + } + } + + return d.codecFromTypeCodec.Decode(ctx, raw, into, itemType) +} + +func (d *Decoder) GetMaxDecodingSize(_ context.Context, n int, itemType string) (int, error) { + codecEntry, ok := d.definitions[itemType] + if !ok { + return 0, fmt.Errorf("%w: nil entry", commontypes.ErrInvalidType) + } + return codecEntry.GetCodecType().Size(n) +} diff --git a/pkg/solana/codec/decoder_test.go b/pkg/solana/codec/decoder_test.go new file mode 100644 index 000000000..ceea9644f --- /dev/null +++ b/pkg/solana/codec/decoder_test.go @@ -0,0 +1,90 @@ +package codec + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + commonencodings "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" +) + +type testErrDecodeEntry struct { + entry +} + +func (t *testErrDecodeEntry) Decode(_ []byte) (interface{}, []byte, error) { + return nil, nil, fmt.Errorf("decode error") +} + +type testErrDecodeRemainingBytes struct { + entry +} + +func (t *testErrDecodeRemainingBytes) Decode(_ []byte) (interface{}, []byte, error) { + return nil, []byte{1}, nil +} + +func TestDecoder_Decode_Errors(t *testing.T) { + var into interface{} + someType := "some-type" + t.Run("error when item type not found", func(t *testing.T) { + d := &Decoder{definitions: map[string]Entry{}} + d.definitions[someType] = &entry{} + + nonExistentType := "non-existent" + err := d.Decode(tests.Context(t), []byte{}, &into, nonExistentType) + require.ErrorIs(t, err, fmt.Errorf("%w: cannot find type %s", commontypes.ErrInvalidType, nonExistentType)) + }) + + t.Run("error when underlying entry decode fails", func(t *testing.T) { + d := &Decoder{definitions: map[string]Entry{}} + d.definitions[someType] = &testErrDecodeEntry{} + require.Error(t, d.Decode(tests.Context(t), []byte{}, &into, someType)) + }) + + t.Run("error when remaining bytes exist after decode", func(t *testing.T) { + d := &Decoder{definitions: map[string]Entry{}} + d.definitions[someType] = &testErrDecodeRemainingBytes{} + require.Error(t, d.Decode(tests.Context(t), []byte{}, &into, someType)) + }) +} + +type testErrGetMaxDecodingSize struct { + entry +} + +type testErrGetMaxDecodingSizeCodecType struct { + commonencodings.Empty +} + +func (t testErrGetMaxDecodingSizeCodecType) Size(_ int) (int, error) { + return 0, fmt.Errorf("error") +} + +func (t *testErrGetMaxDecodingSize) GetCodecType() commonencodings.TypeCodec { + return testErrGetMaxDecodingSizeCodecType{} +} + +func TestDecoder_GetMaxDecodingSize_Errors(t *testing.T) { + someType := "some-type" + + t.Run("error when entry for item type is missing", func(t *testing.T) { + d := &Decoder{definitions: map[string]Entry{}} + d.definitions[someType] = &entry{} + + nonExistentType := "non-existent" + _, err := d.GetMaxDecodingSize(tests.Context(t), 0, nonExistentType) + require.ErrorIs(t, err, fmt.Errorf("%w: cannot find type %s", commontypes.ErrInvalidType, nonExistentType)) + }) + + t.Run("error when underlying entry decode fails", func(t *testing.T) { + d := &Decoder{definitions: map[string]Entry{}} + d.definitions[someType] = &testErrGetMaxDecodingSize{} + + _, err := d.GetMaxDecodingSize(tests.Context(t), 0, someType) + require.Error(t, err) + }) +} diff --git a/pkg/solana/codec/discriminator.go b/pkg/solana/codec/discriminator.go index f712a3f68..9bc363ae7 100644 --- a/pkg/solana/codec/discriminator.go +++ b/pkg/solana/codec/discriminator.go @@ -12,16 +12,16 @@ import ( const discriminatorLength = 8 -func NewDiscriminator(name string) encodings.TypeCodec { +func NewDiscriminator(name string) *Discriminator { sum := sha256.Sum256([]byte("account:" + name)) - return &discriminator{hashPrefix: sum[:discriminatorLength]} + return &Discriminator{hashPrefix: sum[:discriminatorLength]} } -type discriminator struct { +type Discriminator struct { hashPrefix []byte } -func (d discriminator) Encode(value any, into []byte) ([]byte, error) { +func (d Discriminator) Encode(value any, into []byte) ([]byte, error) { if value == nil { return append(into, d.hashPrefix...), nil } @@ -44,7 +44,7 @@ func (d discriminator) Encode(value any, into []byte) ([]byte, error) { return append(into, *raw...), nil } -func (d discriminator) Decode(encoded []byte) (any, []byte, error) { +func (d Discriminator) Decode(encoded []byte) (any, []byte, error) { raw, remaining, err := encodings.SafeDecode(encoded, discriminatorLength, func(raw []byte) []byte { return raw }) if err != nil { return nil, nil, err @@ -57,15 +57,15 @@ func (d discriminator) Decode(encoded []byte) (any, []byte, error) { return &raw, remaining, nil } -func (d discriminator) GetType() reflect.Type { +func (d Discriminator) GetType() reflect.Type { // Pointer type so that nil can inject values and so that the NamedCodec won't wrap with no-nil pointer. return reflect.TypeOf(&[]byte{}) } -func (d discriminator) Size(_ int) (int, error) { +func (d Discriminator) Size(_ int) (int, error) { return discriminatorLength, nil } -func (d discriminator) FixedSize() (int, error) { +func (d Discriminator) FixedSize() (int, error) { return discriminatorLength, nil } diff --git a/pkg/solana/codec/encoder.go b/pkg/solana/codec/encoder.go new file mode 100644 index 000000000..409fb0013 --- /dev/null +++ b/pkg/solana/codec/encoder.go @@ -0,0 +1,35 @@ +package codec + +import ( + "context" + "fmt" + + "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" +) + +type Encoder struct { + definitions map[string]Entry + codecFromTypeCodec encodings.CodecFromTypeCodec +} + +var _ commontypes.Encoder = &Encoder{} + +func (e *Encoder) Encode(ctx context.Context, item any, itemType string) (res []byte, err error) { + if e.codecFromTypeCodec == nil { + e.codecFromTypeCodec = make(encodings.CodecFromTypeCodec) + for k, v := range e.definitions { + e.codecFromTypeCodec[k] = v + } + } + + return e.codecFromTypeCodec.Encode(ctx, item, itemType) +} + +func (e *Encoder) GetMaxEncodingSize(_ context.Context, n int, itemType string) (int, error) { + entry, ok := e.definitions[itemType] + if !ok { + return 0, fmt.Errorf("%w: nil entry", commontypes.ErrInvalidType) + } + return entry.GetCodecType().Size(n) +} diff --git a/pkg/solana/codec/encoder_test.go b/pkg/solana/codec/encoder_test.go new file mode 100644 index 000000000..fb098d884 --- /dev/null +++ b/pkg/solana/codec/encoder_test.go @@ -0,0 +1,103 @@ +package codec + +import ( + "fmt" + "reflect" + "testing" + + "github.com/stretchr/testify/require" + + commonencodings "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" +) + +type testErrEncodeEntry struct { + entry + codecType commonencodings.TypeCodec +} + +func (t *testErrEncodeEntry) Encode(_ interface{}, _ []byte) ([]byte, error) { + return nil, fmt.Errorf("encode error") +} + +func (t *testErrEncodeEntry) GetType() reflect.Type { + return commonencodings.Empty{}.GetType() +} + +type testErrEncodeTypeEntry struct { + entry + tCodec commonencodings.TypeCodec +} + +func (e *testErrEncodeTypeEntry) GetCodecType() commonencodings.TypeCodec { + return e.tCodec +} + +func TestEncoder_Encode_Errors(t *testing.T) { + someType := "some-type" + + t.Run("error when item type not found", func(t *testing.T) { + e := &Encoder{definitions: map[string]Entry{}} + _, err := e.Encode(tests.Context(t), nil, "non-existent-type") + require.Error(t, err) + require.ErrorIs(t, err, commontypes.ErrInvalidType) + require.Contains(t, err.Error(), "cannot find type non-existent-type") + }) + + t.Run("error when convert fails because of unexpected type", func(t *testing.T) { + e := &Encoder{ + definitions: map[string]Entry{ + someType: &testErrEncodeEntry{}, + }, + } + _, err := e.Encode(tests.Context(t), nil, someType) + require.Error(t, err) + }) + + t.Run("error when entry encode fails", func(t *testing.T) { + e := &Encoder{ + definitions: map[string]Entry{ + someType: &testErrEncodeEntry{codecType: commonencodings.Empty{}}, + }, + } + _, err := e.Encode(tests.Context(t), make(map[string]interface{}), someType) + require.ErrorContains(t, err, "encode error") + }) +} + +type testErrGetSize struct { + commonencodings.Empty + retType reflect.Type +} + +func (t testErrGetSize) GetType() reflect.Type { + return t.retType +} + +func (t testErrGetSize) Size(_ int) (int, error) { + return 0, fmt.Errorf("size error") +} + +func TestEncoder_GetMaxEncodingSize_Errors(t *testing.T) { + t.Run("error when entry for item type is missing", func(t *testing.T) { + e := &Encoder{definitions: map[string]Entry{}} + _, err := e.GetMaxEncodingSize(tests.Context(t), 10, "no-entry-type") + require.Error(t, err) + require.ErrorIs(t, err, commontypes.ErrInvalidType) + require.Contains(t, err.Error(), "nil entry") + }) + + t.Run("error when size calculation fails", func(t *testing.T) { + someType := "some-type" + e := &Encoder{ + definitions: map[string]Entry{ + someType: &testErrEncodeTypeEntry{tCodec: testErrGetSize{}}, + }, + } + + _, err := e.GetMaxEncodingSize(tests.Context(t), 0, someType) + require.Error(t, err) + require.Contains(t, err.Error(), "size error") + }) +} diff --git a/pkg/solana/codec/parsed_types.go b/pkg/solana/codec/parsed_types.go new file mode 100644 index 000000000..3144f6dd0 --- /dev/null +++ b/pkg/solana/codec/parsed_types.go @@ -0,0 +1,48 @@ +package codec + +import ( + "fmt" + "reflect" + + commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" +) + +type ParsedTypes struct { + EncoderDefs map[string]Entry + DecoderDefs map[string]Entry +} + +func (parsed *ParsedTypes) ToCodec() (commontypes.RemoteCodec, error) { + modByTypeName := map[string]commoncodec.Modifier{} + if err := AddEntries(parsed.EncoderDefs, modByTypeName); err != nil { + return nil, err + } + if err := AddEntries(parsed.DecoderDefs, modByTypeName); err != nil { + return nil, err + } + + mod, err := commoncodec.NewByItemTypeModifier(modByTypeName) + if err != nil { + return nil, err + } + underlying := &solanaCodec{ + Encoder: &Encoder{definitions: parsed.EncoderDefs}, + Decoder: &Decoder{definitions: parsed.DecoderDefs}, + ParsedTypes: parsed, + } + return commoncodec.NewModifierCodec(underlying, mod, DecoderHooks...) +} + +// AddEntries extracts the mods from entry and adds them to modByTypeName use with codec.NewByItemTypeModifier +// Since each input/output can have its own modifications, we need to keep track of them by type name +func AddEntries(defs map[string]Entry, modByTypeName map[string]commoncodec.Modifier) error { + for k, def := range defs { + modByTypeName[k] = def.Modifier() + _, err := def.Modifier().RetypeToOffChain(reflect.PointerTo(def.GetType()), k) + if err != nil { + return fmt.Errorf("%w: cannot retype %v: %w", commontypes.ErrInvalidConfig, k, err) + } + } + return nil +} diff --git a/pkg/solana/codec/solana.go b/pkg/solana/codec/solana.go index 71e2f7f06..19fe40d3e 100644 --- a/pkg/solana/codec/solana.go +++ b/pkg/solana/codec/solana.go @@ -20,58 +20,130 @@ Modifiers can be provided to assist in modifying property names, adding properti package codec import ( + "encoding/json" "fmt" "math" + "reflect" "github.com/go-viper/mapstructure/v2" "golang.org/x/text/cases" "golang.org/x/text/language" - "github.com/smartcontractkit/chainlink-common/pkg/codec" - "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings" - "github.com/smartcontractkit/chainlink-common/pkg/types" + commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" + commonencodings "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings" + "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings/binary" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" ) const ( DefaultHashBitLength = 32 - unknownIDLFormat = "%w: unknown IDL type def %s" + unknownIDLFormat = "%w: unknown IDL type def %q" ) +// DecoderHooks +// // BigIntHook allows *big.Int to be represented as any integer type or a string and to go back to them. // Useful for config, or if when a model may use a go type that isn't a *big.Int when Pack expects one. // Eg: int32 in a go struct from a plugin could require a *big.Int in Pack for int24, if it fits, we shouldn't care. // SliceToArrayVerifySizeHook verifies that slices have the correct size when converting to an array // EpochToTimeHook allows multiple conversions: time.Time -> int64; int64 -> time.Time; *big.Int -> time.Time; and more -var DecoderHooks = []mapstructure.DecodeHookFunc{codec.EpochToTimeHook, codec.BigIntHook, codec.SliceToArrayVerifySizeHook} +var DecoderHooks = []mapstructure.DecodeHookFunc{commoncodec.EpochToTimeHook, commoncodec.BigIntHook, commoncodec.SliceToArrayVerifySizeHook} -func NewNamedModifierCodec(original types.RemoteCodec, itemType string, modifier codec.Modifier) (types.RemoteCodec, error) { - mod, err := codec.NewByItemTypeModifier(map[string]codec.Modifier{itemType: modifier}) - if err != nil { - return nil, err +type solanaCodec struct { + *Encoder + *Decoder + *ParsedTypes +} + +// NewCodec creates a new [commontypes.RemoteCodec] for Solana. +func NewCodec(conf Config) (commontypes.RemoteCodec, error) { + parsed := &ParsedTypes{ + EncoderDefs: map[string]Entry{}, + DecoderDefs: map[string]Entry{}, } - modCodec, err := codec.NewModifierCodec(original, mod, DecoderHooks...) - if err != nil { - return nil, err + for offChainName, cfg := range conf.Configs { + var idl IDL + if err := json.Unmarshal([]byte(cfg.IDL), &idl); err != nil { + return nil, err + } + + mod, err := cfg.ModifierConfigs.ToModifier(DecoderHooks...) + if err != nil { + return nil, err + } + + definition, err := findDefinitionFromIDL(cfg.Type, cfg.OnChainName, idl) + if err != nil { + return nil, err + } + + var cEntry Entry + switch v := definition.(type) { + case IdlTypeDef: + cEntry, err = NewAccountEntry(offChainName, v, idl.Types, true, mod, binary.LittleEndian()) + case IdlInstruction: + cEntry, err = NewInstructionArgsEntry(offChainName, v, idl.Types, mod, binary.LittleEndian()) + case IdlEvent: + cEntry, err = NewEventArgsEntry(offChainName, v, idl.Types, true, mod, binary.LittleEndian()) + } + if err != nil { + return nil, fmt.Errorf("failed to create %q codec entry: %w", offChainName, err) + } + + parsed.EncoderDefs[offChainName] = cEntry + parsed.DecoderDefs[offChainName] = cEntry } - _, err = modCodec.CreateType(itemType, true) + return parsed.ToCodec() +} - return modCodec, err +func findDefinitionFromIDL(cfgType ChainConfigType, onChainName string, idl IDL) (interface{}, error) { + // not the most efficient way to do this, but these slices should always be very, very small + switch cfgType { + case ChainConfigTypeAccountDef: + for i := range idl.Accounts { + if idl.Accounts[i].Name == onChainName { + return idl.Accounts[i], nil + } + } + return nil, fmt.Errorf("failed to find account %q in IDL", onChainName) + + case ChainConfigTypeInstructionDef: + for i := range idl.Instructions { + if idl.Instructions[i].Name == onChainName { + return idl.Instructions[i], nil + } + } + return nil, fmt.Errorf("failed to find instruction %q in IDL", onChainName) + + case ChainConfigTypeEventDef: + for i := range idl.Events { + if idl.Events[i].Name == onChainName { + return idl.Events[i], nil + } + } + return nil, fmt.Errorf("failed to find event %q in IDL", onChainName) + } + return nil, fmt.Errorf("unknown type: %q", cfgType) +} + +// NewIDLAccountCodec is for Anchor custom types +func NewIDLAccountCodec(idl IDL, builder commonencodings.Builder) (commontypes.RemoteCodec, error) { + return newIDLCoded(idl, builder, idl.Accounts, true) } -func NewIDLInstructionsCodec(idl IDL, builder encodings.Builder) (types.RemoteCodec, error) { - typeCodecs := make(encodings.LenientCodecFromTypeCodec) - caser := cases.Title(language.English) +func NewIDLInstructionsCodec(idl IDL, builder commonencodings.Builder) (commontypes.RemoteCodec, error) { + typeCodecs := make(commonencodings.LenientCodecFromTypeCodec) refs := &codecRefs{ builder: builder, - codecs: make(map[string]encodings.TypeCodec), + codecs: make(map[string]commonencodings.TypeCodec), typeDefs: idl.Types, dependencies: make(map[string][]string), } for _, instruction := range idl.Instructions { - name, instCodec, err := asStruct(instruction.Args, refs, instruction.Name, caser, false) + name, instCodec, err := asStruct(instruction.Args, refs, instruction.Name, false, false) if err != nil { return nil, err } @@ -82,22 +154,54 @@ func NewIDLInstructionsCodec(idl IDL, builder encodings.Builder) (types.RemoteCo return typeCodecs, nil } -// NewIDLAccountCodec is for Anchor custom types -func NewIDLAccountCodec(idl IDL, builder encodings.Builder) (types.RemoteCodec, error) { - return newIDLCoded(idl, builder, idl.Accounts, true) +func NewNamedModifierCodec(original commontypes.RemoteCodec, itemType string, modifier commoncodec.Modifier) (commontypes.RemoteCodec, error) { + mod, err := commoncodec.NewByItemTypeModifier(map[string]commoncodec.Modifier{itemType: modifier}) + if err != nil { + return nil, err + } + + modCodec, err := commoncodec.NewModifierCodec(original, mod, DecoderHooks...) + if err != nil { + return nil, err + } + + _, err = modCodec.CreateType(itemType, true) + + return modCodec, err } -func NewIDLDefinedTypesCodec(idl IDL, builder encodings.Builder) (types.RemoteCodec, error) { +func NewIDLDefinedTypesCodec(idl IDL, builder commonencodings.Builder) (commontypes.RemoteCodec, error) { return newIDLCoded(idl, builder, idl.Types, false) } +func (s solanaCodec) CreateType(itemType string, forEncoding bool) (any, error) { + var itemTypes map[string]Entry + if forEncoding { + itemTypes = s.EncoderDefs + } else { + itemTypes = s.DecoderDefs + } + + def, ok := itemTypes[itemType] + if !ok { + return nil, fmt.Errorf("%w: cannot find type name %q", commontypes.ErrInvalidType, itemType) + } + + // we don't need double pointers, and they can also mess up reflection variable creation and mapstruct decode + if def.GetType().Kind() == reflect.Pointer { + return reflect.New(def.GetCodecType().GetType().Elem()).Interface(), nil + } + + return reflect.New(def.GetType()).Interface(), nil +} + func newIDLCoded( - idl IDL, builder encodings.Builder, from IdlTypeDefSlice, includeDiscriminator bool) (types.RemoteCodec, error) { - typeCodecs := make(encodings.LenientCodecFromTypeCodec) + idl IDL, builder commonencodings.Builder, from IdlTypeDefSlice, includeDiscriminator bool) (commontypes.RemoteCodec, error) { + typeCodecs := make(commonencodings.LenientCodecFromTypeCodec) refs := &codecRefs{ builder: builder, - codecs: make(map[string]encodings.TypeCodec), + codecs: make(map[string]commonencodings.TypeCodec), typeDefs: idl.Types, dependencies: make(map[string][]string), } @@ -105,11 +209,11 @@ func newIDLCoded( for _, def := range from { var ( name string - accCodec encodings.TypeCodec + accCodec commonencodings.TypeCodec err error ) - name, accCodec, err = createNamedCodec(def, refs, includeDiscriminator) + name, accCodec, err = createCodecType(def, refs, includeDiscriminator) if err != nil { return nil, err } @@ -121,32 +225,29 @@ func newIDLCoded( } type codecRefs struct { - builder encodings.Builder - codecs map[string]encodings.TypeCodec + builder commonencodings.Builder + codecs map[string]commonencodings.TypeCodec typeDefs IdlTypeDefSlice dependencies map[string][]string } -func createNamedCodec( +func createCodecType( def IdlTypeDef, refs *codecRefs, includeDiscriminator bool, -) (string, encodings.TypeCodec, error) { - caser := cases.Title(language.English) +) (string, commonencodings.TypeCodec, error) { name := def.Name - switch def.Type.Kind { case IdlTypeDefTyKindStruct: - return asStruct(*def.Type.Fields, refs, name, caser, includeDiscriminator) + return asStruct(*def.Type.Fields, refs, name, includeDiscriminator, false) case IdlTypeDefTyKindEnum: variants := def.Type.Variants if !variants.IsAllUint8() { - return name, nil, fmt.Errorf("%w: variants are not supported", types.ErrInvalidConfig) + return name, nil, fmt.Errorf("%w: variants are not supported", commontypes.ErrInvalidConfig) } - return name, refs.builder.Uint8(), nil default: - return name, nil, fmt.Errorf(unknownIDLFormat, types.ErrInvalidConfig, def.Type.Kind) + return name, nil, fmt.Errorf(unknownIDLFormat, commontypes.ErrInvalidConfig, def.Type.Kind) } } @@ -154,17 +255,18 @@ func asStruct( fields []IdlField, refs *codecRefs, name string, // name is the struct name and can be used in dependency checks - caser cases.Caser, includeDiscriminator bool, -) (string, encodings.TypeCodec, error) { + isInstructionArgs bool, +) (string, commonencodings.TypeCodec, error) { desLen := 0 if includeDiscriminator { desLen = 1 } - named := make([]encodings.NamedTypeCodec, len(fields)+desLen) + + named := make([]commonencodings.NamedTypeCodec, len(fields)+desLen) if includeDiscriminator { - named[0] = encodings.NamedTypeCodec{Name: "Discriminator" + name, Codec: NewDiscriminator(name)} + named[0] = commonencodings.NamedTypeCodec{Name: "Discriminator" + name, Codec: NewDiscriminator(name)} } for idx, field := range fields { @@ -175,10 +277,15 @@ func asStruct( return name, nil, err } - named[idx+desLen] = encodings.NamedTypeCodec{Name: caser.String(fieldName), Codec: typedCodec} + named[idx+desLen] = commonencodings.NamedTypeCodec{Name: cases.Title(language.English, cases.NoLower).String(fieldName), Codec: typedCodec} + } + + // accounts have to be in a struct, instruction args don't + if len(named) == 1 && isInstructionArgs { + return name, named[0].Codec, nil } - structCodec, err := encodings.NewStructCodec(named) + structCodec, err := commonencodings.NewStructCodec(named) if err != nil { return name, nil, err } @@ -186,7 +293,7 @@ func asStruct( return name, structCodec, nil } -func processFieldType(parentTypeName string, idlType IdlType, refs *codecRefs) (encodings.TypeCodec, error) { +func processFieldType(parentTypeName string, idlType IdlType, refs *codecRefs) (commonencodings.TypeCodec, error) { switch true { case idlType.IsString(): return getCodecByStringType(idlType.GetString(), refs.builder) @@ -201,13 +308,13 @@ func processFieldType(parentTypeName string, idlType IdlType, refs *codecRefs) ( case idlType.IsIdlTypeVec(): return asVec(parentTypeName, idlType.GetIdlTypeVec(), refs) default: - return nil, fmt.Errorf("%w: unknown IDL type def", types.ErrInvalidConfig) + return nil, fmt.Errorf("%w: unknown IDL type def", commontypes.ErrInvalidConfig) } } -func asDefined(parentTypeName string, definedName *IdlTypeDefined, refs *codecRefs) (encodings.TypeCodec, error) { +func asDefined(parentTypeName string, definedName *IdlTypeDefined, refs *codecRefs) (commonencodings.TypeCodec, error) { if definedName == nil { - return nil, fmt.Errorf("%w: defined type name should not be nil", types.ErrInvalidConfig) + return nil, fmt.Errorf("%w: defined type name should not be nil", commontypes.ErrInvalidConfig) } // already exists as a type in the typed codecs @@ -217,19 +324,19 @@ func asDefined(parentTypeName string, definedName *IdlTypeDefined, refs *codecRe // nextDef should not have a dependency on definedName if !validDependency(refs, parentTypeName, definedName.Defined) { - return nil, fmt.Errorf("%w: circular dependency detected on %s -> %s relation", types.ErrInvalidConfig, parentTypeName, definedName.Defined) + return nil, fmt.Errorf("%w: circular dependency detected on %q -> %q relation", commontypes.ErrInvalidConfig, parentTypeName, definedName.Defined) } // codec by defined type doesn't exist // process it using the provided typeDefs nextDef := refs.typeDefs.GetByName(definedName.Defined) if nextDef == nil { - return nil, fmt.Errorf("%w: IDL type does not exist for name %s", types.ErrInvalidConfig, definedName.Defined) + return nil, fmt.Errorf("%w: IDL type does not exist for name %q", commontypes.ErrInvalidConfig, definedName.Defined) } saveDependency(refs, parentTypeName, definedName.Defined) - newTypeName, newTypeCodec, err := createNamedCodec(*nextDef, refs, false) + newTypeName, newTypeCodec, err := createCodecType(*nextDef, refs, false) if err != nil { return nil, err } @@ -240,16 +347,16 @@ func asDefined(parentTypeName string, definedName *IdlTypeDefined, refs *codecRe return newTypeCodec, nil } -func asArray(parentTypeName string, idlArray *IdlTypeArray, refs *codecRefs) (encodings.TypeCodec, error) { +func asArray(parentTypeName string, idlArray *IdlTypeArray, refs *codecRefs) (commonencodings.TypeCodec, error) { codec, err := processFieldType(parentTypeName, idlArray.Thing, refs) if err != nil { return nil, err } - return encodings.NewArray(idlArray.Num, codec) + return commonencodings.NewArray(idlArray.Num, codec) } -func asVec(parentTypeName string, idlVec *IdlTypeVec, refs *codecRefs) (encodings.TypeCodec, error) { +func asVec(parentTypeName string, idlVec *IdlTypeVec, refs *codecRefs) (commonencodings.TypeCodec, error) { codec, err := processFieldType(parentTypeName, idlVec.Vec, refs) if err != nil { return nil, err @@ -260,10 +367,10 @@ func asVec(parentTypeName string, idlVec *IdlTypeVec, refs *codecRefs) (encoding return nil, err } - return encodings.NewSlice(codec, b) + return commonencodings.NewSlice(codec, b) } -func getCodecByStringType(curType IdlTypeAsString, builder encodings.Builder) (encodings.TypeCodec, error) { +func getCodecByStringType(curType IdlTypeAsString, builder commonencodings.Builder) (commonencodings.TypeCodec, error) { switch curType { case IdlTypeBool: return builder.Bool(), nil @@ -278,11 +385,11 @@ func getCodecByStringType(curType IdlTypeAsString, builder encodings.Builder) (e case IdlTypeBytes, IdlTypePublicKey, IdlTypeHash: return getByteCodecByStringType(curType, builder) default: - return nil, fmt.Errorf(unknownIDLFormat, types.ErrInvalidConfig, curType) + return nil, fmt.Errorf(unknownIDLFormat, commontypes.ErrInvalidConfig, curType) } } -func getIntCodecByStringType(curType IdlTypeAsString, builder encodings.Builder) (encodings.TypeCodec, error) { +func getIntCodecByStringType(curType IdlTypeAsString, builder commonencodings.Builder) (commonencodings.TypeCodec, error) { switch curType { case IdlTypeI8: return builder.Int8(), nil @@ -295,11 +402,11 @@ func getIntCodecByStringType(curType IdlTypeAsString, builder encodings.Builder) case IdlTypeI128: return builder.BigInt(16, true) default: - return nil, fmt.Errorf(unknownIDLFormat, types.ErrInvalidConfig, curType) + return nil, fmt.Errorf(unknownIDLFormat, commontypes.ErrInvalidConfig, curType) } } -func getUIntCodecByStringType(curType IdlTypeAsString, builder encodings.Builder) (encodings.TypeCodec, error) { +func getUIntCodecByStringType(curType IdlTypeAsString, builder commonencodings.Builder) (commonencodings.TypeCodec, error) { switch curType { case IdlTypeU8: return builder.Uint8(), nil @@ -312,22 +419,22 @@ func getUIntCodecByStringType(curType IdlTypeAsString, builder encodings.Builder case IdlTypeU128: return builder.BigInt(16, true) default: - return nil, fmt.Errorf(unknownIDLFormat, types.ErrInvalidConfig, curType) + return nil, fmt.Errorf(unknownIDLFormat, commontypes.ErrInvalidConfig, curType) } } -func getTimeCodecByStringType(curType IdlTypeAsString, builder encodings.Builder) (encodings.TypeCodec, error) { +func getTimeCodecByStringType(curType IdlTypeAsString, builder commonencodings.Builder) (commonencodings.TypeCodec, error) { switch curType { case IdlTypeUnixTimestamp: return builder.Int64(), nil case IdlTypeDuration: return NewDuration(builder), nil default: - return nil, fmt.Errorf(unknownIDLFormat, types.ErrInvalidConfig, curType) + return nil, fmt.Errorf(unknownIDLFormat, commontypes.ErrInvalidConfig, curType) } } -func getByteCodecByStringType(curType IdlTypeAsString, builder encodings.Builder) (encodings.TypeCodec, error) { +func getByteCodecByStringType(curType IdlTypeAsString, builder commonencodings.Builder) (commonencodings.TypeCodec, error) { switch curType { case IdlTypeBytes: b, err := builder.Int(4) @@ -335,11 +442,11 @@ func getByteCodecByStringType(curType IdlTypeAsString, builder encodings.Builder return nil, err } - return encodings.NewSlice(builder.Uint8(), b) + return commonencodings.NewSlice(builder.Uint8(), b) case IdlTypePublicKey, IdlTypeHash: - return encodings.NewArray(DefaultHashBitLength, builder.Uint8()) + return commonencodings.NewArray(DefaultHashBitLength, builder.Uint8()) default: - return nil, fmt.Errorf(unknownIDLFormat, types.ErrInvalidConfig, curType) + return nil, fmt.Errorf(unknownIDLFormat, commontypes.ErrInvalidConfig, curType) } } diff --git a/pkg/solana/codec/testutils/eventItemTypeIDL.json b/pkg/solana/codec/testutils/eventItemTypeIDL.json new file mode 100644 index 000000000..f98f27671 --- /dev/null +++ b/pkg/solana/codec/testutils/eventItemTypeIDL.json @@ -0,0 +1,73 @@ +{ + "version": "0.1.0", + "name": "test_item_event_type", + "instructions": [], + "events": [ + { + "name": "TestItem", + "fields": [ + { "name": "Field", "type": "i32" }, + { "name": "OracleId", "type": "u8" }, + { "name": "OracleIds", "type": { "array": ["u8", 32] } }, + { "name": "AccountStruct", "type": { "defined": "AccountStruct" } }, + { "name": "Accounts", "type": { "vec": "publicKey" } }, + { "name": "DifferentField", "type": "string" }, + { "name": "BigField", "type": "i128" }, + { "name": "NestedDynamicStruct", "type": { "defined": "NestedDynamic" } }, + { "name": "NestedStaticStruct", "type": { "defined": "NestedStatic" } } + ] + } + ], + "types": [ + { + "name": "AccountStruct", + "type": { + "kind": "struct", + "fields": [ + { "name": "Account", "type": "publicKey" }, + { "name": "AccountStr", "type": "publicKey" } + ] + } + }, + { + "name": "InnerDynamic", + "type": { + "kind": "struct", + "fields": [ + { "name": "IntVal", "type": "i64" }, + { "name": "S", "type": "string" } + ] + } + }, + { + "name": "NestedDynamic", + "type": { + "kind": "struct", + "fields": [ + { "name": "FixedBytes", "type": { "array": ["u8", 2] } }, + { "name": "Inner", "type": { "defined": "InnerDynamic" } } + ] + } + }, + { + "name": "InnerStatic", + "type": { + "kind": "struct", + "fields": [ + { "name": "IntVal", "type": "i64" }, + { "name": "A", "type": "publicKey" } + ] + } + }, + { + "name": "NestedStatic", + "type": { + "kind": "struct", + "fields": [ + { "name": "FixedBytes", "type": { "array": ["u8", 2] } }, + { "name": "Inner", "type": { "defined": "InnerStatic" } } + ] + } + } + ] +} diff --git a/pkg/solana/codec/testutils/itemArray1TypeIDL.json b/pkg/solana/codec/testutils/itemArray1TypeIDL.json new file mode 100644 index 000000000..8d061aed2 --- /dev/null +++ b/pkg/solana/codec/testutils/itemArray1TypeIDL.json @@ -0,0 +1,92 @@ +{ + "version": "0.1.0", + "name": "test_item_array1", + "instructions": [ + { + "name": "TestItemArray1Type", + "accounts": [], + "args": [ + { + "name": "TestItemArray1Type", + "type": { + "array": [ + { + "defined": "TestItem" + }, + 1 + ] + } + } + ] + } + ], + "types": [ + { + "name": "TestItem", + "type": { + "kind": "struct", + "fields": [ + { "name": "Field", "type": "i32" }, + { "name": "OracleId", "type": "u8" }, + { "name": "OracleIds", "type": { "array": ["u8", 32] } }, + { "name": "AccountStruct", "type": { "defined": "AccountStruct" } }, + { "name": "Accounts", "type": { "vec": "publicKey" } }, + { "name": "DifferentField", "type": "string" }, + { "name": "BigField", "type": "i128" }, + { "name": "NestedDynamicStruct", "type": { "defined": "NestedDynamic" } }, + { "name": "NestedStaticStruct", "type": { "defined": "NestedStatic" } } + ] + } + }, + { + "name": "AccountStruct", + "type": { + "kind": "struct", + "fields": [ + { "name": "Account", "type": "publicKey" }, + { "name": "AccountStr", "type": "publicKey" } + ] + } + }, + { + "name": "InnerDynamic", + "type": { + "kind": "struct", + "fields": [ + { "name": "IntVal", "type": "i64" }, + { "name": "S", "type": "string" } + ] + } + }, + { + "name": "NestedDynamic", + "type": { + "kind": "struct", + "fields": [ + { "name": "FixedBytes", "type": { "array": ["u8", 2] } }, + { "name": "Inner", "type": { "defined": "InnerDynamic" } } + ] + } + }, + { + "name": "InnerStatic", + "type": { + "kind": "struct", + "fields": [ + { "name": "IntVal", "type": "i64" }, + { "name": "A", "type": "publicKey" } + ] + } + }, + { + "name": "NestedStatic", + "type": { + "kind": "struct", + "fields": [ + { "name": "FixedBytes", "type": { "array": ["u8", 2] } }, + { "name": "Inner", "type": { "defined": "InnerStatic" } } + ] + } + } + ] +} \ No newline at end of file diff --git a/pkg/solana/codec/testutils/itemArray2TypeIDL.json b/pkg/solana/codec/testutils/itemArray2TypeIDL.json new file mode 100644 index 000000000..c20c55785 --- /dev/null +++ b/pkg/solana/codec/testutils/itemArray2TypeIDL.json @@ -0,0 +1,92 @@ +{ + "version": "0.1.0", + "name": "test_item_array2", + "instructions": [ + { + "name": "TestItemArray2Type", + "accounts": [], + "args": [ + { + "name": "TestItemArray2Type", + "type": { + "array": [ + { + "defined": "TestItem" + }, + 2 + ] + } + } + ] + } + ], + "types": [ + { + "name": "TestItem", + "type": { + "kind": "struct", + "fields": [ + { "name": "Field", "type": "i32" }, + { "name": "OracleId", "type": "u8" }, + { "name": "OracleIds", "type": { "array": ["u8", 32] } }, + { "name": "AccountStruct", "type": { "defined": "AccountStruct" } }, + { "name": "Accounts", "type": { "vec": "publicKey" } }, + { "name": "DifferentField", "type": "string" }, + { "name": "BigField", "type": "i128" }, + { "name": "NestedDynamicStruct", "type": { "defined": "NestedDynamic" } }, + { "name": "NestedStaticStruct", "type": { "defined": "NestedStatic" } } + ] + } + }, + { + "name": "AccountStruct", + "type": { + "kind": "struct", + "fields": [ + { "name": "Account", "type": "publicKey" }, + { "name": "AccountStr", "type": "publicKey" } + ] + } + }, + { + "name": "InnerDynamic", + "type": { + "kind": "struct", + "fields": [ + { "name": "IntVal", "type": "i64" }, + { "name": "S", "type": "string" } + ] + } + }, + { + "name": "NestedDynamic", + "type": { + "kind": "struct", + "fields": [ + { "name": "FixedBytes", "type": { "array": ["u8", 2] } }, + { "name": "Inner", "type": { "defined": "InnerDynamic" } } + ] + } + }, + { + "name": "InnerStatic", + "type": { + "kind": "struct", + "fields": [ + { "name": "IntVal", "type": "i64" }, + { "name": "A", "type": "publicKey" } + ] + } + }, + { + "name": "NestedStatic", + "type": { + "kind": "struct", + "fields": [ + { "name": "FixedBytes", "type": { "array": ["u8", 2] } }, + { "name": "Inner", "type": { "defined": "InnerStatic" } } + ] + } + } + ] +} diff --git a/pkg/solana/codec/testutils/itemIDL.json b/pkg/solana/codec/testutils/itemIDL.json new file mode 100644 index 000000000..ee2a719cc --- /dev/null +++ b/pkg/solana/codec/testutils/itemIDL.json @@ -0,0 +1,77 @@ +{ + "version": "0.1.0", + "name": "test_item_type", + "instructions": [ + ], + "accounts": [ + { + "name": "TestItem", + "type": { + "kind": "struct", + "fields": [ + { "name": "Field", "type": "i32" }, + { "name": "OracleId", "type": "u8" }, + { "name": "OracleIds", "type": { "array": ["u8", 32] } }, + { "name": "AccountStruct", "type": { "defined": "AccountStruct" } }, + { "name": "Accounts", "type": { "vec": "publicKey" } }, + { "name": "DifferentField", "type": "string" }, + { "name": "BigField", "type": "i128" }, + { "name": "NestedDynamicStruct", "type": { "defined": "NestedDynamic" } }, + { "name": "NestedStaticStruct", "type": { "defined": "NestedStatic" } } + ] + } + } + ], + "types": [ + { + "name": "AccountStruct", + "type": { + "kind": "struct", + "fields": [ + { "name": "Account", "type": "publicKey" }, + { "name": "AccountStr", "type": "publicKey" } + ] + } + }, + { + "name": "InnerDynamic", + "type": { + "kind": "struct", + "fields": [ + { "name": "IntVal", "type": "i64" }, + { "name": "S", "type": "string" } + ] + } + }, + { + "name": "NestedDynamic", + "type": { + "kind": "struct", + "fields": [ + { "name": "FixedBytes", "type": { "array": ["u8", 2] } }, + { "name": "Inner", "type": { "defined": "InnerDynamic" } } + ] + } + }, + { + "name": "InnerStatic", + "type": { + "kind": "struct", + "fields": [ + { "name": "IntVal", "type": "i64" }, + { "name": "A", "type": "publicKey" } + ] + } + }, + { + "name": "NestedStatic", + "type": { + "kind": "struct", + "fields": [ + { "name": "FixedBytes", "type": { "array": ["u8", 2] } }, + { "name": "Inner", "type": { "defined": "InnerStatic" } } + ] + } + } + ] +} diff --git a/pkg/solana/codec/testutils/itemSliceTypeIDL.json b/pkg/solana/codec/testutils/itemSliceTypeIDL.json new file mode 100644 index 000000000..491d3e12f --- /dev/null +++ b/pkg/solana/codec/testutils/itemSliceTypeIDL.json @@ -0,0 +1,89 @@ +{ + "version": "0.1.0", + "name": "test_item_slice_type", + "instructions": [ + { + "name": "TestItemSliceType", + "accounts": [], + "args": [ + { + "name": "TestItemSliceType", + "type": { + "vec": { + "defined": "TestItem" + } + } + } + ] + } + ], + "types": [ + { + "name": "TestItem", + "type": { + "kind": "struct", + "fields": [ + { "name": "Field", "type": "i32" }, + { "name": "OracleId", "type": "u8" }, + { "name": "OracleIds", "type": { "array": ["u8", 32] } }, + { "name": "AccountStruct", "type": { "defined": "AccountStruct" } }, + { "name": "Accounts", "type": { "vec": "publicKey" } }, + { "name": "DifferentField", "type": "string" }, + { "name": "BigField", "type": "i128" }, + { "name": "NestedDynamicStruct", "type": { "defined": "NestedDynamic" } }, + { "name": "NestedStaticStruct", "type": { "defined": "NestedStatic" } } + ] + } + }, + { + "name": "AccountStruct", + "type": { + "kind": "struct", + "fields": [ + { "name": "Account", "type": "publicKey" }, + { "name": "AccountStr", "type": "publicKey" } + ] + } + }, + { + "name": "InnerDynamic", + "type": { + "kind": "struct", + "fields": [ + { "name": "IntVal", "type": "i64" }, + { "name": "S", "type": "string" } + ] + } + }, + { + "name": "NestedDynamic", + "type": { + "kind": "struct", + "fields": [ + { "name": "FixedBytes", "type": { "array": ["u8", 2] } }, + { "name": "Inner", "type": { "defined": "InnerDynamic" } } + ] + } + }, + { + "name": "InnerStatic", + "type": { + "kind": "struct", + "fields": [ + { "name": "IntVal", "type": "i64" }, + { "name": "A", "type": "publicKey" } + ] + } + }, + { + "name": "NestedStatic", + "type": { + "kind": "struct", + "fields": [ + { "name": "FixedBytes", "type": { "array": ["u8", 2] } }, + { "name": "Inner", "type": { "defined": "InnerStatic" } } + ] + } + } + ] +} diff --git a/pkg/solana/codec/testutils/nilTypeIDL.json b/pkg/solana/codec/testutils/nilTypeIDL.json new file mode 100644 index 000000000..47b169428 --- /dev/null +++ b/pkg/solana/codec/testutils/nilTypeIDL.json @@ -0,0 +1,12 @@ +{ + "name": "NilType", + "accounts": [ + { + "name": "NilType", + "type": { + "kind": "struct", + "fields": [] + } + } + ] +} \ No newline at end of file diff --git a/pkg/solana/codec/testutils/sizeItemTypeIDL.json b/pkg/solana/codec/testutils/sizeItemTypeIDL.json new file mode 100644 index 000000000..fdb73115e --- /dev/null +++ b/pkg/solana/codec/testutils/sizeItemTypeIDL.json @@ -0,0 +1,38 @@ +{ + "version": "0.1.0", + "name": "item_for_size", + "instructions": [ + { + "name": "ProcessItemForSize", + "accounts": [ + { + "name": "ItemForSize", + "isMut": true, + "isSigner": false + } + ], + "args": [] + } + ], + "accounts": [ + { + "name": "ItemForSize", + "type": { + "kind": "struct", + "fields": [ + { + "name": "Stuff", + "type": { + "vec": "i128" + } + }, + { + "name": "OtherStuff", + "type": "i128" + } + ] + } + } + ], + "types": [] +} diff --git a/pkg/solana/codec/testutils/types.go b/pkg/solana/codec/testutils/types.go index 533e88b0b..3c52adb0f 100644 --- a/pkg/solana/codec/testutils/types.go +++ b/pkg/solana/codec/testutils/types.go @@ -2,10 +2,16 @@ package testutils import ( _ "embed" + "fmt" "math/big" "time" - ag_solana "github.com/gagliardetto/solana-go" + agbinary "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + + "github.com/smartcontractkit/chainlink-common/pkg/types/interfacetests" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" ) var ( @@ -45,9 +51,11 @@ var ( BasicVector: []string{"some string", "another string"}, TimeVal: 683_100_000, DurationVal: 42 * time.Second, - PublicKey: ag_solana.NewWallet().PublicKey(), + PublicKey: solana.NewWallet().PublicKey(), EnumVal: 0, } + TestItemWithConfigExtraType = "TestItemWithConfigExtra" + TestEventItem = "TestEventItem" ) type StructWithNestedStruct struct { @@ -59,7 +67,7 @@ type StructWithNestedStruct struct { BasicVector []string TimeVal int64 DurationVal time.Duration - PublicKey ag_solana.PublicKey + PublicKey solana.PublicKey EnumVal uint8 } @@ -86,3 +94,550 @@ var JSONIDLWithAllTypes string //go:embed circularDepIDL.json var CircularDepIDL string + +//go:embed itemIDL.json +var itemTypeJSONIDL string + +//go:embed eventItemTypeIDL.json +var eventItemTypeJSONIDL string + +//go:embed itemSliceTypeIDL.json +var itemSliceTypeJSONIDL string + +//go:embed itemArray1TypeIDL.json +var itemArray1TypeJSONIDL string + +//go:embed itemArray2TypeIDL.json +var itemArray2TypeJSONIDL string + +//go:embed nilTypeIDL.json +var nilTypeJSONIDL string + +type CodecDef struct { + IDL string + IDLTypeName string + ItemType codec.ChainConfigType +} + +// CodecDefs key is codec offchain type name +var CodecDefs = map[string]CodecDef{ + interfacetests.TestItemType: { + IDL: itemTypeJSONIDL, + IDLTypeName: interfacetests.TestItemType, + ItemType: codec.ChainConfigTypeAccountDef, + }, + interfacetests.TestItemSliceType: { + IDL: itemSliceTypeJSONIDL, + IDLTypeName: interfacetests.TestItemSliceType, + ItemType: codec.ChainConfigTypeInstructionDef, + }, + interfacetests.TestItemArray1Type: { + IDL: itemArray1TypeJSONIDL, + IDLTypeName: interfacetests.TestItemArray1Type, + ItemType: codec.ChainConfigTypeInstructionDef, + }, + interfacetests.TestItemArray2Type: { + IDL: itemArray2TypeJSONIDL, + IDLTypeName: interfacetests.TestItemArray2Type, + ItemType: codec.ChainConfigTypeInstructionDef, + }, + TestItemWithConfigExtraType: { + IDL: itemTypeJSONIDL, + IDLTypeName: interfacetests.TestItemType, + ItemType: codec.ChainConfigTypeAccountDef, + }, + interfacetests.NilType: { + IDL: nilTypeJSONIDL, + IDLTypeName: interfacetests.NilType, + ItemType: codec.ChainConfigTypeAccountDef, + }, + TestEventItem: { + IDL: eventItemTypeJSONIDL, + IDLTypeName: interfacetests.TestItemType, + ItemType: codec.ChainConfigTypeEventDef, + }, +} + +type TestItemAsAccount struct { + Field int32 + OracleID uint8 + OracleIDs [32]uint8 + AccountStruct AccountStruct + Accounts []solana.PublicKey + DifferentField string + BigField agbinary.Int128 + NestedDynamicStruct NestedDynamic + NestedStaticStruct NestedStatic +} + +var TestItemDiscriminator = [8]byte{148, 105, 105, 155, 26, 167, 212, 149} + +func (obj TestItemAsAccount) MarshalWithEncoder(encoder *agbinary.Encoder) (err error) { + // Write account discriminator: + err = encoder.WriteBytes(TestItemDiscriminator[:], false) + if err != nil { + return err + } + // Serialize `Field` param: + err = encoder.Encode(obj.Field) + if err != nil { + return err + } + // Serialize `OracleID` param: + err = encoder.Encode(obj.OracleID) + if err != nil { + return err + } + // Serialize `OracleIDs` param: + err = encoder.Encode(obj.OracleIDs) + if err != nil { + return err + } + // Serialize `AccountStruct` param: + err = encoder.Encode(obj.AccountStruct) + if err != nil { + return err + } + // Serialize `Accounts` param: + err = encoder.Encode(obj.Accounts) + if err != nil { + return err + } + // Serialize `DifferentField` param: + err = encoder.Encode(obj.DifferentField) + if err != nil { + return err + } + // Serialize `BigField` param: + err = encoder.Encode(obj.BigField) + if err != nil { + return err + } + // Serialize `NestedDynamicStruct` param: + err = encoder.Encode(obj.NestedDynamicStruct) + if err != nil { + return err + } + // Serialize `NestedStaticStruct` param: + err = encoder.Encode(obj.NestedStaticStruct) + if err != nil { + return err + } + return nil +} + +func (obj *TestItemAsAccount) UnmarshalWithDecoder(decoder *agbinary.Decoder) error { + // Read and check account discriminator: + { + discriminator, err := decoder.ReadTypeID() + if err != nil { + return err + } + if !discriminator.Equal(TestItemDiscriminator[:]) { + return fmt.Errorf( + "wrong discriminator: wanted %s, got %s", + "[148 105 105 155 26 167 212 149]", + fmt.Sprint(discriminator[:])) + } + } + // Deserialize `Field`: + err := decoder.Decode(&obj.Field) + if err != nil { + return err + } + // Deserialize `OracleID`: + err = decoder.Decode(&obj.OracleID) + if err != nil { + return err + } + // Deserialize `OracleIDs`: + err = decoder.Decode(&obj.OracleIDs) + if err != nil { + return err + } + // Deserialize `AccountStruct`: + err = decoder.Decode(&obj.AccountStruct) + if err != nil { + return err + } + // Deserialize `Accounts`: + err = decoder.Decode(&obj.Accounts) + if err != nil { + return err + } + // Deserialize `DifferentField`: + err = decoder.Decode(&obj.DifferentField) + if err != nil { + return err + } + // Deserialize `BigField`: + err = decoder.Decode(&obj.BigField) + if err != nil { + return err + } + // Deserialize `NestedDynamicStruct`: + err = decoder.Decode(&obj.NestedDynamicStruct) + if err != nil { + return err + } + // Deserialize `NestedStaticStruct`: + err = decoder.Decode(&obj.NestedStaticStruct) + if err != nil { + return err + } + return nil +} + +type TestItemAsArgs struct { + Field int32 + OracleID uint8 + OracleIDs [32]uint8 + AccountStruct AccountStruct + Accounts []solana.PublicKey + DifferentField string + BigField agbinary.Int128 + NestedDynamicStruct NestedDynamic + NestedStaticStruct NestedStatic +} + +func (obj TestItemAsArgs) MarshalWithEncoder(encoder *agbinary.Encoder) (err error) { + // Serialize `Field` param: + err = encoder.Encode(obj.Field) + if err != nil { + return err + } + // Serialize `OracleID` param: + err = encoder.Encode(obj.OracleID) + if err != nil { + return err + } + // Serialize `OracleIDs` param: + err = encoder.Encode(obj.OracleIDs) + if err != nil { + return err + } + // Serialize `AccountStruct` param: + err = encoder.Encode(obj.AccountStruct) + if err != nil { + return err + } + // Serialize `Accounts` param: + err = encoder.Encode(obj.Accounts) + if err != nil { + return err + } + // Serialize `DifferentField` param: + err = encoder.Encode(obj.DifferentField) + if err != nil { + return err + } + // Serialize `BigField` param: + err = encoder.Encode(obj.BigField) + if err != nil { + return err + } + // Serialize `NestedDynamicStruct` param: + err = encoder.Encode(obj.NestedDynamicStruct) + if err != nil { + return err + } + // Serialize `NestedStaticStruct` param: + err = encoder.Encode(obj.NestedStaticStruct) + if err != nil { + return err + } + return nil +} + +func (obj *TestItemAsArgs) UnmarshalWithDecoder(decoder *agbinary.Decoder) (err error) { + // Deserialize `Field`: + err = decoder.Decode(&obj.Field) + if err != nil { + return err + } + // Deserialize `OracleID`: + err = decoder.Decode(&obj.OracleID) + if err != nil { + return err + } + // Deserialize `OracleIDs`: + err = decoder.Decode(&obj.OracleIDs) + if err != nil { + return err + } + // Deserialize `AccountStruct`: + err = decoder.Decode(&obj.AccountStruct) + if err != nil { + return err + } + // Deserialize `Accounts`: + err = decoder.Decode(&obj.Accounts) + if err != nil { + return err + } + // Deserialize `DifferentField`: + err = decoder.Decode(&obj.DifferentField) + if err != nil { + return err + } + // Deserialize `BigField`: + err = decoder.Decode(&obj.BigField) + if err != nil { + return err + } + // Deserialize `NestedDynamicStruct`: + err = decoder.Decode(&obj.NestedDynamicStruct) + if err != nil { + return err + } + // Deserialize `NestedStaticStruct`: + err = decoder.Decode(&obj.NestedStaticStruct) + if err != nil { + return err + } + return nil +} + +type AccountStruct struct { + Account solana.PublicKey + AccountStr solana.PublicKey +} + +func (obj AccountStruct) MarshalWithEncoder(encoder *agbinary.Encoder) (err error) { + // Serialize `Account` param: + err = encoder.Encode(obj.Account) + if err != nil { + return err + } + // Serialize `AccountStr` param: + err = encoder.Encode(obj.AccountStr) + if err != nil { + return err + } + return nil +} + +func (obj *AccountStruct) UnmarshalWithDecoder(decoder *agbinary.Decoder) (err error) { + // Deserialize `Account`: + err = decoder.Decode(&obj.Account) + if err != nil { + return err + } + // Deserialize `AccountStr`: + err = decoder.Decode(&obj.AccountStr) + if err != nil { + return err + } + return nil +} + +type InnerDynamic struct { + IntVal int64 + S string +} + +func (obj InnerDynamic) MarshalWithEncoder(encoder *agbinary.Encoder) (err error) { + // Serialize `IntVal` param: + err = encoder.Encode(obj.IntVal) + if err != nil { + return err + } + // Serialize `S` param: + err = encoder.Encode(obj.S) + if err != nil { + return err + } + return nil +} + +func (obj *InnerDynamic) UnmarshalWithDecoder(decoder *agbinary.Decoder) (err error) { + // Deserialize `IntVal`: + err = decoder.Decode(&obj.IntVal) + if err != nil { + return err + } + // Deserialize `S`: + err = decoder.Decode(&obj.S) + if err != nil { + return err + } + return nil +} + +type NestedDynamic struct { + FixedBytes [2]uint8 + Inner InnerDynamic +} + +func (obj NestedDynamic) MarshalWithEncoder(encoder *agbinary.Encoder) (err error) { + // Serialize `FixedBytes` param: + err = encoder.Encode(obj.FixedBytes) + if err != nil { + return err + } + // Serialize `Inner` param: + err = encoder.Encode(obj.Inner) + if err != nil { + return err + } + return nil +} + +func (obj *NestedDynamic) UnmarshalWithDecoder(decoder *agbinary.Decoder) (err error) { + // Deserialize `FixedBytes`: + err = decoder.Decode(&obj.FixedBytes) + if err != nil { + return err + } + // Deserialize `Inner`: + err = decoder.Decode(&obj.Inner) + if err != nil { + return err + } + return nil +} + +type InnerStatic struct { + IntVal int64 + A solana.PublicKey +} + +func (obj InnerStatic) MarshalWithEncoder(encoder *agbinary.Encoder) (err error) { + // Serialize `IntVal` param: + err = encoder.Encode(obj.IntVal) + if err != nil { + return err + } + // Serialize `A` param: + err = encoder.Encode(obj.A) + if err != nil { + return err + } + return nil +} + +func (obj *InnerStatic) UnmarshalWithDecoder(decoder *agbinary.Decoder) (err error) { + // Deserialize `IntVal`: + err = decoder.Decode(&obj.IntVal) + if err != nil { + return err + } + // Deserialize `A`: + err = decoder.Decode(&obj.A) + if err != nil { + return err + } + return nil +} + +type NestedStatic struct { + FixedBytes [2]uint8 + Inner InnerStatic +} + +func (obj NestedStatic) MarshalWithEncoder(encoder *agbinary.Encoder) (err error) { + // Serialize `FixedBytes` param: + err = encoder.Encode(obj.FixedBytes) + if err != nil { + return err + } + // Serialize `Inner` param: + err = encoder.Encode(obj.Inner) + if err != nil { + return err + } + return nil +} + +func (obj *NestedStatic) UnmarshalWithDecoder(decoder *agbinary.Decoder) (err error) { + // Deserialize `FixedBytes`: + err = decoder.Decode(&obj.FixedBytes) + if err != nil { + return err + } + // Deserialize `Inner`: + err = decoder.Decode(&obj.Inner) + if err != nil { + return err + } + return nil +} + +func EncodeRequestToTestItemAsAccount(testStruct interfacetests.TestStruct) TestItemAsAccount { + return TestItemAsAccount{ + Field: *testStruct.Field, + OracleID: uint8(testStruct.OracleID), + OracleIDs: getOracleIDs(testStruct), + AccountStruct: getAccountStruct(testStruct), + Accounts: getAccounts(testStruct), + DifferentField: testStruct.DifferentField, + BigField: bigIntToBinInt128(testStruct.BigField), + NestedDynamicStruct: getNestedDynamic(testStruct), + NestedStaticStruct: getNestedStatic(testStruct), + } +} + +func EncodeRequestToTestItemAsArgs(testStruct interfacetests.TestStruct) TestItemAsArgs { + return TestItemAsArgs{ + Field: *testStruct.Field, + OracleID: uint8(testStruct.OracleID), + OracleIDs: getOracleIDs(testStruct), + AccountStruct: getAccountStruct(testStruct), + Accounts: getAccounts(testStruct), + DifferentField: testStruct.DifferentField, + BigField: bigIntToBinInt128(testStruct.BigField), + NestedDynamicStruct: getNestedDynamic(testStruct), + NestedStaticStruct: getNestedStatic(testStruct), + } +} + +func getOracleIDs(testStruct interfacetests.TestStruct) [32]byte { + var oracleIDs [32]byte + for i, v := range testStruct.OracleIDs { + oracleIDs[i] = byte(v) + } + return oracleIDs +} + +func getAccountStruct(testStruct interfacetests.TestStruct) AccountStruct { + k, _ := solana.PublicKeyFromBase58(testStruct.AccountStruct.AccountStr) + return AccountStruct{ + Account: solana.PublicKeyFromBytes(testStruct.AccountStruct.Account), + AccountStr: k, + } +} + +func getAccounts(testStruct interfacetests.TestStruct) []solana.PublicKey { + accs := make([]solana.PublicKey, len(testStruct.Accounts)) + for i, v := range testStruct.Accounts { + accs[i] = solana.PublicKeyFromBytes(v) + } + return accs +} + +func getNestedDynamic(testStruct interfacetests.TestStruct) NestedDynamic { + return NestedDynamic{ + FixedBytes: testStruct.NestedDynamicStruct.FixedBytes, + Inner: InnerDynamic{ + IntVal: int64(testStruct.NestedDynamicStruct.Inner.I), + S: testStruct.NestedDynamicStruct.Inner.S, + }, + } +} + +func getNestedStatic(testStruct interfacetests.TestStruct) NestedStatic { + return NestedStatic{ + FixedBytes: testStruct.NestedStaticStruct.FixedBytes, + Inner: InnerStatic{ + IntVal: int64(testStruct.NestedStaticStruct.Inner.I), + A: solana.PublicKeyFromBytes(testStruct.NestedStaticStruct.Inner.A), + }, + } +} + +func bigIntToBinInt128(val *big.Int) agbinary.Int128 { + return agbinary.Int128{ + Lo: val.Uint64(), + Hi: new(big.Int).Rsh(val, 64).Uint64(), + } +} diff --git a/pkg/solana/codec/types.go b/pkg/solana/codec/types.go new file mode 100644 index 000000000..e047b36ae --- /dev/null +++ b/pkg/solana/codec/types.go @@ -0,0 +1,24 @@ +package codec + +import commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" + +type ChainConfigType string + +const ( + ChainConfigTypeAccountDef ChainConfigType = "account" + ChainConfigTypeInstructionDef ChainConfigType = "instruction" + ChainConfigTypeEventDef ChainConfigType = "event" +) + +type Config struct { + // Configs key is the type's offChainName for the codec + Configs map[string]ChainConfig `json:"configs" toml:"configs"` +} + +type ChainConfig struct { + IDL string `json:"typeIdl" toml:"typeIdl"` + OnChainName string `json:"onChainName" toml:"onChainName"` + // Type can be Solana Account, Instruction args, or TODO Event + Type ChainConfigType `json:"type" toml:"type"` + ModifierConfigs commoncodec.ModifiersConfig `json:"modifierConfigs,omitempty" toml:"modifierConfigs,omitempty"` +} diff --git a/pkg/solana/config/chain_reader.go b/pkg/solana/config/chain_reader.go index dbe9ef4ab..4251624fe 100644 --- a/pkg/solana/config/chain_reader.go +++ b/pkg/solana/config/chain_reader.go @@ -7,7 +7,7 @@ import ( "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" - "github.com/smartcontractkit/chainlink-common/pkg/codec" + commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings" "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings/binary" "github.com/smartcontractkit/chainlink-common/pkg/types" @@ -82,7 +82,7 @@ type chainDataProcedureFields struct { IDLAccount string `json:"idlAccount,omitempty"` // OutputModifications provides modifiers to convert chain data format to custom // output formats. - OutputModifications codec.ModifiersConfig `json:"outputModifications,omitempty"` + OutputModifications commoncodec.ModifiersConfig `json:"outputModifications,omitempty"` // RPCOpts provides optional configurations for commitment, encoding, and data // slice offsets. RPCOpts *RPCOpts `json:"rpcOpts,omitempty"` diff --git a/sonar-project.properties b/sonar-project.properties index 0434465b5..bec9533fd 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -5,9 +5,10 @@ sonar.sources=. # Full exclusions from the static analysis sonar.exclusions=**/node_modules/**/*, **/contracts/artifacts/**/*, **/generated/**/*, **/docs/**/*, **/*.config.ts, **/*.config.js, **/*.txt, pkg/solana/codec/anchoridl.go # Coverage exclusions -sonar.coverage.exclusions=**/*.test.ts, **/*_test.go, **/contracts/tests/**/*, **/integration-tests/**/* +sonar.coverage.exclusions=**/*.test.ts, **/*_test.go, **/contracts/tests/**/*, **/integration-tests/**/*, **/pkg/solana/codec/testutils/**/* # Tests' root folder, inclusions (tests to check and count) and exclusions sonar.tests=. sonar.test.inclusions=**/*_test.go, **/contracts/tests/**/* -sonar.test.exclusions=**/integration-tests/*, **/gauntlet/* \ No newline at end of file +sonar.test.exclusions=**/integration-tests/*, **/gauntlet/* +sonar.cpd.exclusions=**/pkg/solana/codec/testutils/**/* \ No newline at end of file