diff --git a/integration-tests/relayinterface/chain_components_test.go b/integration-tests/relayinterface/chain_components_test.go index 4bfb7ffbf..e2d76f90f 100644 --- a/integration-tests/relayinterface/chain_components_test.go +++ b/integration-tests/relayinterface/chain_components_test.go @@ -799,19 +799,19 @@ func (it *SolanaChainComponentsInterfaceTester[T]) buildContractReaderConfig(t T ResponseAddressHardCoder: &commoncodec.HardCodeModifierConfig{ // placeholder values, whatever is put as value gets replaced with a solana pub key anyway OffChainValues: map[string]any{ - "SharedAddress": solana.PublicKey{}, - "AddressToShare": solana.PublicKey{}, - }, - }, - OutputModifications: commoncodec.ModifiersConfig{ - &commoncodec.HardCodeModifierConfig{ - OffChainValues: map[string]any{"U": "", "V": false}, + "SharedAddress": "", + "AddressToShare": "", }, }, } multiReadDef := readWithAddressHardCodedIntoResponseDef multiReadDef.ResponseAddressHardCoder = nil + multiReadDef.OutputModifications = commoncodec.ModifiersConfig{ + &commoncodec.HardCodeModifierConfig{ + OffChainValues: map[string]any{"U": "", "V": false}, + }, + } multiReadDef.MultiReader = &config.MultiReader{ Reads: []config.ReadDefinition{{ ChainSpecificName: "MultiRead2", @@ -820,14 +820,31 @@ func (it *SolanaChainComponentsInterfaceTester[T]) buildContractReaderConfig(t T }}, } + idl := mustUnmarshalIDL(t, string(it.Helper.GetPrimaryIDL(t))) + idl.Accounts = append(idl.Accounts, codec.IdlTypeDef{ + Name: "USDPerToken", + Type: codec.IdlTypeDefTy{ + Kind: codec.IdlTypeDefTyKindStruct, + Fields: &codec.IdlTypeDefStruct{ + { + Name: "tokenPrices", + Type: codec.IdlType{ + AsIdlTypeVec: &codec.IdlTypeVec{Vec: codec.IdlType{AsIdlTypeDefined: &codec.IdlTypeDefined{Defined: "TimestampedPackedU224"}}}, + }, + }, + }, + }, + }) + return config.ContractReader{ Namespaces: map[string]config.ChainContractReader{ AnyContractName: { - IDL: mustUnmarshalIDL(t, string(it.Helper.GetPrimaryIDL(t))), + IDL: idl, Reads: map[string]config.ReadDefinition{ ReadWithAddressHardCodedIntoResponse: readWithAddressHardCodedIntoResponseDef, GetTokenPrices: { - ChainSpecificName: "BillingTokenConfigWrapper", + ChainSpecificName: "USDPerToken", + ReadType: config.Account, PDADefinition: codec.PDATypeDef{ Prefix: []byte("fee_billing_token_config"), Seeds: []codec.PDASeed{ @@ -842,17 +859,8 @@ func (it *SolanaChainComponentsInterfaceTester[T]) buildContractReaderConfig(t T }, }, OutputModifications: commoncodec.ModifiersConfig{ - &commoncodec.DropModifierConfig{ - Fields: []string{"Config"}, - }, - &commoncodec.HardCodeModifierConfig{ - OffChainValues: map[string]any{ - "Response": make([]TimestampedUnixBig, 1000), - }, - }, - &commoncodec.PropertyExtractorConfig{FieldName: "Response"}, + &commoncodec.PropertyExtractorConfig{FieldName: "TokenPrices"}, }, - ReadType: config.Account, }, MultiRead: multiReadDef, MultiReadWithParamsReuse: { diff --git a/pkg/solana/chainreader/chain_reader.go b/pkg/solana/chainreader/chain_reader.go index b9b0b0991..38d859206 100644 --- a/pkg/solana/chainreader/chain_reader.go +++ b/pkg/solana/chainreader/chain_reader.go @@ -187,7 +187,10 @@ func (s *ContractReaderService) GetLatestValue(ctx context.Context, readIdentifi // TODO this is a temporary edge case - NONEVM-1320 if values.reads[0].readName == GetTokenPrices { - return s.handleGetTokenPricesGetLatestValue(ctx, params, values, returnVal) + if err := s.handleGetTokenPricesGetLatestValue(ctx, params, values, returnVal); err != nil { + return fmt.Errorf("failed to read contract: %q, account: %q err: %w", values.contract, values.reads[0].readName, err) + } + return nil } batch := []call{ @@ -635,7 +638,14 @@ func (s *ContractReaderService) handleGetTokenPricesGetLatestValue( params any, values readValues, returnVal any, -) error { +) (err error) { + // shouldn't happen, but just to be sure + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic recovered: %v", r) + } + }() + pdaAddresses, err := s.getPDAsForGetTokenPrices(params, values) if err != nil { return err @@ -643,64 +653,69 @@ func (s *ContractReaderService) handleGetTokenPricesGetLatestValue( data, err := s.client.GetMultipleAccountData(ctx, pdaAddresses...) if err != nil { - return fmt.Errorf( - "for contract %q read %q: failed to get multiple account data: %w", - values.contract, values.reads[0].readName, err, - ) + return err } - // -------------- Fill out the returnVal slice with data -------------- - // can't typecast returnVal so we have to use reflection here - - // Ensure `returnVal` is a pointer to a slice we can populate. returnSliceVal := reflect.ValueOf(returnVal) - if returnSliceVal.Kind() == reflect.Ptr { + if returnSliceVal.Kind() != reflect.Ptr { + return fmt.Errorf("expected <**[]*struct { Value *big.Int; Timestamp *int64 } Value>, got %q", returnSliceVal.String()) + } + returnSliceVal = returnSliceVal.Elem() + + returnSliceValType := returnSliceVal.Type() + if returnSliceValType.Kind() != reflect.Ptr { + return fmt.Errorf("expected <*[]*struct { Value *big.Int; Timestamp *int64 } Value>, got %q", returnSliceValType.String()) + } + + sliceType := returnSliceValType.Elem() + if sliceType.Kind() != reflect.Slice { + return fmt.Errorf("expected []*struct { Value *big.Int; Timestamp *int64 }, got %q", sliceType.String()) + } + + if returnSliceVal.IsNil() { + // init a slice + sliceVal := reflect.MakeSlice(sliceType, 0, 0) + + // create a pointer to that slice to match what slicePtr + slicePtr := reflect.New(sliceType) + slicePtr.Elem().Set(sliceVal) + + returnSliceVal.Set(slicePtr) returnSliceVal = returnSliceVal.Elem() - if returnSliceVal.Kind() == reflect.Ptr { - returnSliceVal = returnSliceVal.Elem() - } } - if returnSliceVal.Kind() != reflect.Slice { - return fmt.Errorf( - "for contract %q read %q: expected `returnVal` to be a slice, got %s", - values.contract, values.reads[0].readName, returnSliceVal.Kind(), - ) + + pointerType := sliceType.Elem() + if pointerType.Kind() != reflect.Ptr { + return fmt.Errorf("expected *struct { Value *big.Int; Timestamp *int64 }, got %q", pointerType.String()) + } + + underlyingStruct := pointerType.Elem() + if underlyingStruct.Kind() != reflect.Struct { + return fmt.Errorf("expected struct { Value *big.Int; Timestamp *int64 }, got %q", underlyingStruct.String()) } - elemType := returnSliceVal.Type().Elem() for _, d := range data { var wrapper fee_quoter.BillingTokenConfigWrapper if err = wrapper.UnmarshalWithDecoder(bin.NewBorshDecoder(d)); err != nil { - return fmt.Errorf( - "for contract %q read %q: failed to unmarshal account data: %w", - values.contract, values.reads[0].readName, err, - ) + return err } - newElem := reflect.New(elemType).Elem() - + newElemPtr := reflect.New(underlyingStruct) + newElem := newElemPtr.Elem() valueField := newElem.FieldByName("Value") if !valueField.IsValid() { - return fmt.Errorf( - "for contract %q read %q: struct type missing `Value` field", - values.contract, values.reads[0].readName, - ) + return fmt.Errorf("field `Value` missing from %q", newElem.String()) } - valueField.Set(reflect.ValueOf(big.NewInt(0).SetBytes(wrapper.Config.UsdPerToken.Value[:]))) + valueField.Set(reflect.ValueOf(big.NewInt(0).SetBytes(wrapper.Config.UsdPerToken.Value[:]))) timestampField := newElem.FieldByName("Timestamp") if !timestampField.IsValid() { - return fmt.Errorf( - "for contract %q read %q: struct type missing `Timestamp` field", - values.contract, values.reads[0].readName, - ) + return fmt.Errorf("field `Timestamp` missing from %q", newElem.String()) } - // nolint:gosec // G115: integer overflow conversion int64 -> uint32 - timestampField.Set(reflect.ValueOf(uint32(wrapper.Config.UsdPerToken.Timestamp))) - - returnSliceVal.Set(reflect.Append(returnSliceVal, newElem)) + timestampField.Set(reflect.ValueOf(&wrapper.Config.UsdPerToken.Timestamp)) + returnSliceVal.Set(reflect.Append(returnSliceVal, newElemPtr)) } return nil diff --git a/pkg/solana/codec/anchoridl.go b/pkg/solana/codec/anchoridl.go index ceac3fb3b..5bdb38b2f 100644 --- a/pkg/solana/codec/anchoridl.go +++ b/pkg/solana/codec/anchoridl.go @@ -272,7 +272,7 @@ func (env *IdlType) UnmarshalJSON(data []byte) error { if err := utilz.TranscodeJSON(temp, &target); err != nil { return err } - env.asIdlTypeDefined = &target + env.AsIdlTypeDefined = &target } if got, ok := v["array"]; ok { if _, ok := got.([]interface{}); !ok { @@ -303,7 +303,7 @@ type IdlType struct { AsString IdlTypeAsString AsIdlTypeVec *IdlTypeVec asIdlTypeOption *IdlTypeOption - asIdlTypeDefined *IdlTypeDefined + AsIdlTypeDefined *IdlTypeDefined AsIdlTypeArray *IdlTypeArray } @@ -323,7 +323,7 @@ func (env *IdlType) IsIdlTypeOption() bool { return env.asIdlTypeOption != nil } func (env *IdlType) IsIdlTypeDefined() bool { - return env.asIdlTypeDefined != nil + return env.AsIdlTypeDefined != nil } func (env *IdlType) IsArray() bool { return env.AsIdlTypeArray != nil @@ -340,7 +340,7 @@ func (env *IdlType) GetIdlTypeOption() *IdlTypeOption { return env.asIdlTypeOption } func (env *IdlType) GetIdlTypeDefined() *IdlTypeDefined { - return env.asIdlTypeDefined + return env.AsIdlTypeDefined } func (env *IdlType) GetArray() *IdlTypeArray { return env.AsIdlTypeArray diff --git a/pkg/solana/codec/solana.go b/pkg/solana/codec/solana.go index 912847fd9..b16e66c44 100644 --- a/pkg/solana/codec/solana.go +++ b/pkg/solana/codec/solana.go @@ -350,6 +350,17 @@ func asDefined(parentTypeName string, definedName *IdlTypeDefined, refs *codecRe } func asArray(parentTypeName string, idlArray *IdlTypeArray, refs *codecRefs) (commonencodings.TypeCodec, error) { + if idlArray == nil { + return nil, fmt.Errorf("%w: field type cannot be nil", commontypes.ErrInvalidConfig) + } + + // better to implement bytes to big int codec modifiers, but this works fine + if idlArray.Num == 28 && idlArray.Thing.AsString == IdlTypeU8 { + // nolint:gosec + // G115: integer overflow conversion int -> uint + return binary.BigEndian().BigInt(uint(idlArray.Num), false) + } + codec, err := processFieldType(parentTypeName, idlArray.Thing, refs) if err != nil { return nil, err