From 7fa1a735acdf6e091232c19533e32b77169015ae Mon Sep 17 00:00:00 2001 From: Awbrey Hughlett Date: Wed, 19 Feb 2025 12:24:48 -0600 Subject: [PATCH 1/8] add bytes to string modifier for solana contracts --- pkg/codec/byte_string_modifier.go | 36 +++++++++++++++++++++++++++++++ pkg/codec/config.go | 20 +++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/pkg/codec/byte_string_modifier.go b/pkg/codec/byte_string_modifier.go index b6b6f6524..64befec39 100644 --- a/pkg/codec/byte_string_modifier.go +++ b/pkg/codec/byte_string_modifier.go @@ -1,6 +1,7 @@ package codec import ( + "bytes" "fmt" "reflect" @@ -66,6 +67,41 @@ func NewPathTraverseAddressBytesToStringModifier( return m } +func NewConstrainedLengthBytesToStringModifier( + fields []string, + maxLen int, +) Modifier { + return NewPathTraverseAddressBytesToStringModifier(fields, &constrainedLengthBytesToStringModifier{maxLen: maxLen}, false) +} + +func NewPathTraverseConstrainedLengthBytesToStringModifier( + fields []string, + maxLen int, + enablePathTraverse bool, +) Modifier { + return NewPathTraverseAddressBytesToStringModifier(fields, &constrainedLengthBytesToStringModifier{maxLen: maxLen}, enablePathTraverse) +} + +type constrainedLengthBytesToStringModifier struct { + maxLen int +} + +func (m constrainedLengthBytesToStringModifier) EncodeAddress(bts []byte) (string, error) { + return string(bytes.Trim(bts, "\x00")), nil +} + +func (m constrainedLengthBytesToStringModifier) DecodeAddress(str string) ([]byte, error) { + output := make([]byte, m.maxLen) + + copy(output, []byte(str)[:]) + + return output, nil +} + +func (m constrainedLengthBytesToStringModifier) Length() int { + return m.maxLen +} + type bytesToStringModifier struct { // Injected modifier that contains chain-specific logic modifier AddressModifier diff --git a/pkg/codec/config.go b/pkg/codec/config.go index c8248aa0e..8632f4018 100644 --- a/pkg/codec/config.go +++ b/pkg/codec/config.go @@ -59,6 +59,8 @@ func (m *ModifiersConfig) UnmarshalJSON(data []byte) error { (*m)[i] = &PropertyExtractorConfig{} case ModifierAddressToString: (*m)[i] = &AddressBytesToStringModifierConfig{} + case ModifierBytesToString: + (*m)[i] = &ConstrainedBytesToStringModifierConfig{} case ModifierWrapper: (*m)[i] = &WrapperModifierConfig{} case ModifierPreCodec: @@ -103,6 +105,7 @@ const ( ModifierEpochToTime ModifierType = "epoch to time" ModifierExtractProperty ModifierType = "extract property" ModifierAddressToString ModifierType = "address to string" + ModifierBytesToString ModifierType = "constrained bytes to string" ModifierWrapper ModifierType = "wrapper" ) @@ -368,6 +371,23 @@ func (c *AddressBytesToStringModifierConfig) MarshalJSON() ([]byte, error) { }) } +type ConstrainedBytesToStringModifierConfig struct { + Fields []string + MaxLen int + EnablePathTraverse bool +} + +func (c *ConstrainedBytesToStringModifierConfig) ToModifier(_ ...mapstructure.DecodeHookFunc) (Modifier, error) { + return NewPathTraverseConstrainedLengthBytesToStringModifier(c.Fields, c.MaxLen, c.EnablePathTraverse), nil +} + +func (c *ConstrainedBytesToStringModifierConfig) MarshalJSON() ([]byte, error) { + return json.Marshal(&modifierMarshaller[ConstrainedBytesToStringModifierConfig]{ + Type: ModifierBytesToString, + T: c, + }) +} + // WrapperModifierConfig replaces each field based on cfg map keys with a struct containing one field with the value of the original field which has is named based on map values. // Wrapper modifier does not maintain the original pointers. // Wrapper modifier config shouldn't edit fields that affect each other since the results are not deterministic. From 4128048b9b9976689861a68da98f486b04de82b3 Mon Sep 17 00:00:00 2001 From: Awbrey Hughlett Date: Thu, 20 Feb 2025 10:13:12 -0600 Subject: [PATCH 2/8] property extractor path traversal --- pkg/codec/config.go | 5 +- pkg/codec/modifier_base.go | 13 ++ pkg/codec/property_extractor.go | 177 +++++++++++++++++++++------ pkg/codec/property_extractor_test.go | 17 +++ 4 files changed, 170 insertions(+), 42 deletions(-) diff --git a/pkg/codec/config.go b/pkg/codec/config.go index 8632f4018..9b006cf59 100644 --- a/pkg/codec/config.go +++ b/pkg/codec/config.go @@ -337,11 +337,12 @@ func (e *EpochToTimeModifierConfig) MarshalJSON() ([]byte, error) { } type PropertyExtractorConfig struct { - FieldName string + FieldName string + EnablePathTraverse bool } func (c *PropertyExtractorConfig) ToModifier(_ ...mapstructure.DecodeHookFunc) (Modifier, error) { - return NewPropertyExtractor(upperFirstCharacter(c.FieldName)), nil + return NewPathTraversePropertyExtractor(upperFirstCharacter(c.FieldName), c.EnablePathTraverse), nil } func (c *PropertyExtractorConfig) MarshalJSON() ([]byte, error) { diff --git a/pkg/codec/modifier_base.go b/pkg/codec/modifier_base.go index d1d939f03..52a91bfa4 100644 --- a/pkg/codec/modifier_base.go +++ b/pkg/codec/modifier_base.go @@ -422,6 +422,19 @@ func valueForPath(from reflect.Value, itemType string) (any, error) { } return valueForPath(field, tail) + case reflect.Map: + head, tail := ItemTyper(itemType).Next() + + field := from.MapIndex(reflect.ValueOf(head)) + if !field.IsValid() { + return nil, fmt.Errorf("%w: field not found for path %s and itemType %s", types.ErrInvalidType, from, itemType) + } + + if tail == "" { + return field.Interface(), nil + } + + return valueForPath(reflect.ValueOf(field.Interface()), tail) default: return nil, fmt.Errorf("%w: cannot extract a field from kind %s", types.ErrInvalidType, from.Kind()) } diff --git a/pkg/codec/property_extractor.go b/pkg/codec/property_extractor.go index a5e7c38c9..0a6d9c9ef 100644 --- a/pkg/codec/property_extractor.go +++ b/pkg/codec/property_extractor.go @@ -14,19 +14,25 @@ import ( // This modifier is lossy, as TransformToOffchain will discard unwanted struct properties and // return a single element. Calling TransformToOnchain will result in unset properties. func NewPropertyExtractor(fieldName string) Modifier { - m := &propertyExtractor{ - onToOffChainType: map[reflect.Type]reflect.Type{}, - offToOnChainType: map[reflect.Type]reflect.Type{}, - fieldName: fieldName, - } + return NewPathTraversePropertyExtractor(fieldName, false) +} - return m +func NewPathTraversePropertyExtractor(fieldName string, enablePathTraverse bool) Modifier { + return &propertyExtractor{ + onToOffChainType: map[reflect.Type]reflect.Type{}, + offToOnChainType: map[reflect.Type]reflect.Type{}, + fieldName: fieldName, + enablePathTraverse: enablePathTraverse, + } } type propertyExtractor struct { - onToOffChainType map[reflect.Type]reflect.Type - offToOnChainType map[reflect.Type]reflect.Type - fieldName string + fieldName string + enablePathTraverse bool + onToOffChainType map[reflect.Type]reflect.Type + offToOnChainType map[reflect.Type]reflect.Type + onChainStructType reflect.Type + offChainStructType reflect.Type } func (e *propertyExtractor) RetypeToOffChain(onChainType reflect.Type, itemType string) (reflect.Type, error) { @@ -34,57 +40,142 @@ func (e *propertyExtractor) RetypeToOffChain(onChainType reflect.Type, itemType return nil, fmt.Errorf("%w: field name required for extraction", types.ErrInvalidConfig) } - if cached, ok := e.onToOffChainType[onChainType]; ok { + // path traverse allows an item type of Struct.FieldA.NestedField to isolate modifiers + // associated with the nested field `NestedField`. + if !e.enablePathTraverse { + itemType = "" + } + + // if itemType is empty, store the type mappings + // if itemType is not empty, assume a sub-field property is expected to be extracted + onChainStructType := onChainType + if itemType != "" { + onChainStructType = e.onChainStructType + } + + if cached, ok := e.onToOffChainType[onChainStructType]; ok { return cached, nil } + var ( + offChainType reflect.Type + err error + ) + switch onChainType.Kind() { case reflect.Pointer: - elm, err := e.RetypeToOffChain(onChainType.Elem(), "") - if err != nil { + var elm reflect.Type + + if elm, err = e.RetypeToOffChain(onChainStructType.Elem(), ""); err != nil { return nil, err } - ptr := reflect.PointerTo(elm) - e.onToOffChainType[onChainType] = ptr - e.offToOnChainType[ptr] = onChainType - - return ptr, nil + offChainType = reflect.PointerTo(elm) case reflect.Slice: - elm, err := e.RetypeToOffChain(onChainType.Elem(), "") - if err != nil { + var elm reflect.Type + + if elm, err = e.RetypeToOffChain(onChainStructType.Elem(), ""); err != nil { return nil, err } - sliceType := reflect.SliceOf(elm) - e.onToOffChainType[onChainType] = sliceType - e.offToOnChainType[sliceType] = onChainType - - return sliceType, nil + offChainType = reflect.SliceOf(elm) case reflect.Array: - elm, err := e.RetypeToOffChain(onChainType.Elem(), "") - if err != nil { + var elm reflect.Type + + if elm, err = e.RetypeToOffChain(onChainStructType.Elem(), ""); err != nil { return nil, err } - arrayType := reflect.ArrayOf(onChainType.Len(), elm) - e.onToOffChainType[onChainType] = arrayType - e.offToOnChainType[arrayType] = onChainType - - return arrayType, nil + offChainType = reflect.ArrayOf(onChainStructType.Len(), elm) case reflect.Struct: - return e.getPropTypeFromStruct(onChainType) + if offChainType, err = e.getPropTypeFromStruct(onChainStructType); err != nil { + return nil, err + } default: + // if the types don't match, it means we are attempting to traverse the main struct + if onChainType != e.onChainStructType { + return onChainType, nil + } + return nil, fmt.Errorf("%w: cannot retype the kind %v", types.ErrInvalidType, onChainType.Kind()) } + + e.onToOffChainType[onChainStructType] = offChainType + e.offToOnChainType[offChainType] = onChainStructType + + if e.onChainStructType == nil { + e.onChainStructType = onChainType + e.offChainStructType = offChainType + } + + return typeForPath(offChainType, itemType) } -func (e *propertyExtractor) TransformToOnChain(offChainValue any, _ string) (any, error) { - return extractOrExpandWithMaps(offChainValue, e.offToOnChainType, e.fieldName, expandWithMapsHelper) +func (e *propertyExtractor) TransformToOnChain(offChainValue any, itemType string) (any, error) { + offChainValue, itemType, err := e.selectType(offChainValue, e.offChainStructType, itemType) + if err != nil { + return nil, err + } + + modified, err := extractOrExpandWithMaps(offChainValue, e.offToOnChainType, e.fieldName, expandWithMapsHelper) + if err != nil { + return nil, err + } + + if itemType != "" { + // add the field name because the offChainType was nested into a new struct + itemType = fmt.Sprintf("%s.%s", e.fieldName, itemType) + + return valueForPath(reflect.ValueOf(modified), itemType) + } + + return modified, nil +} + +func (e *propertyExtractor) TransformToOffChain(onChainValue any, itemType string) (any, error) { + onChainValue, itemType, err := e.selectType(onChainValue, e.onChainStructType, itemType) + if err != nil { + return nil, err + } + + modified, err := extractOrExpandWithMaps(onChainValue, e.onToOffChainType, e.fieldName, extractWithMapsHelper) + if err != nil { + return nil, err + } + + if itemType != "" { + // remove the head from the itemType because a field was extracted + _, tail := ItemTyper(itemType).Next() + + return valueForPath(reflect.ValueOf(modified), tail) + } + + return modified, nil } -func (e *propertyExtractor) TransformToOffChain(onChainValue any, _ string) (any, error) { - return extractOrExpandWithMaps(onChainValue, e.onToOffChainType, e.fieldName, extractWithMapsHelper) +func (e *propertyExtractor) selectType(inputValue any, savedType reflect.Type, itemType string) (any, string, error) { + // set itemType to an ignore value if path traversal is not enabled + if !e.enablePathTraverse { + return inputValue, "", nil + } + + // the offChainValue might be a subfield value; get the true offChainStruct type already stored and set the value + baseStructValue := inputValue + + // path traversal is expected, but offChainValue is the value of a field, not the actual struct + // create a new struct from the stored offChainStruct with the provided value applied and all other fields set to + // their zero value. + if itemType != "" { + into := reflect.New(savedType) + + if err := applyValueForPath(into, reflect.ValueOf(inputValue), itemType); err != nil { + return nil, itemType, err + } + + baseStructValue = reflect.Indirect(into).Interface() + } + + return baseStructValue, itemType, nil } func (e *propertyExtractor) getPropTypeFromStruct(onChainType reflect.Type) (reflect.Type, error) { @@ -110,9 +201,6 @@ func (e *propertyExtractor) getPropTypeFromStruct(onChainType reflect.Type) (ref return nil, fmt.Errorf("%w: field not found in on-chain type %s", types.ErrInvalidType, e.fieldName) } - e.onToOffChainType[onChainType] = field.Type - e.offToOnChainType[field.Type] = onChainType - return field.Type, nil } @@ -186,9 +274,18 @@ func extractWithMapsHelper(rItem reflect.Value, toType reflect.Type, field strin case reflect.Pointer: elm := rItem.Elem() if elm.Kind() == reflect.Struct { - tmp, err := extractElement(rItem.Interface(), field) + var ( + tmp reflect.Value + err error + ) + + if tmp, err = extractElement(rItem.Interface(), field); err != nil { + return rItem, err + } + result := reflect.New(toType.Elem()) err = mapstructure.Decode(tmp.Interface(), result.Interface()) + return result, err } diff --git a/pkg/codec/property_extractor_test.go b/pkg/codec/property_extractor_test.go index 6f58a5b81..8a1381425 100644 --- a/pkg/codec/property_extractor_test.go +++ b/pkg/codec/property_extractor_test.go @@ -28,6 +28,7 @@ func TestPropertyExtractor(t *testing.T) { extractor := codec.NewPropertyExtractor("A") invalidExtractor := codec.NewPropertyExtractor("A.B") nestedExtractor := codec.NewPropertyExtractor("B.B") + pathTraverseExt := codec.NewPathTraversePropertyExtractor("B", true) t.Run("RetypeToOffChain sets the type for offchain to the onchain property", func(t *testing.T) { offChainType, err := extractor.RetypeToOffChain(reflect.TypeOf(nestedTestStruct{}), "") @@ -246,4 +247,20 @@ func TestPropertyExtractor(t *testing.T) { assert.Equal(t, expectedLossy, lossyOnChain) }) + + t.Run("TransformToOnChain and TransformToOffChain works for path traversal", func(t *testing.T) { + _, err := pathTraverseExt.RetypeToOffChain(reflect.PointerTo(onChainType), "") + require.NoError(t, err) + + offChainValue, err := pathTraverseExt.TransformToOffChain(int64(42), "B.B") + require.NoError(t, err) + + expectedVal := int64(42) + require.Equal(t, expectedVal, offChainValue) + + lossyOnChain, err := pathTraverseExt.TransformToOnChain(int64(42), "B") + require.NoError(t, err) + + assert.Equal(t, int64(42), lossyOnChain) + }) } From ff01dc04903f10f32a0fd0e64246cdba867364a5 Mon Sep 17 00:00:00 2001 From: Awbrey Hughlett Date: Thu, 20 Feb 2025 22:42:41 -0600 Subject: [PATCH 3/8] update modifiers and value extraction --- pkg/codec/modifier_base.go | 47 +----------------- pkg/codec/precodec.go | 4 +- pkg/codec/property_extractor.go | 2 +- pkg/codec/utils.go | 86 +++++++++++++++++++++++++++++++++ pkg/codec/utils_test.go | 43 +++++++++++++++++ 5 files changed, 133 insertions(+), 49 deletions(-) diff --git a/pkg/codec/modifier_base.go b/pkg/codec/modifier_base.go index 52a91bfa4..e9ef19999 100644 --- a/pkg/codec/modifier_base.go +++ b/pkg/codec/modifier_base.go @@ -208,7 +208,7 @@ func (m *modifierBase[T]) selectType(inputValue any, savedType reflect.Type, ite if itemType != "" { into := reflect.New(savedType) - if err := applyValueForPath(into, reflect.ValueOf(inputValue), itemType); err != nil { + if err := SetValueAtPath(into, reflect.ValueOf(inputValue), itemType); err != nil { return nil, itemType, err } @@ -440,51 +440,6 @@ func valueForPath(from reflect.Value, itemType string) (any, error) { } } -func applyValueForPath(vInto, vField reflect.Value, itemType string) error { - switch vInto.Kind() { - case reflect.Pointer: - if !vInto.Elem().IsValid() { - into := reflect.New(vInto.Type().Elem()) - - vInto.Set(into) - } - - err := applyValueForPath(vInto.Elem(), vField, itemType) - if err != nil { - return err - } - - return nil - case reflect.Array, reflect.Slice: - return fmt.Errorf("%w: cannot set a field from an array or slice", types.ErrInvalidType) - case reflect.Struct: - head, tail := ItemTyper(itemType).Next() - - field := vInto.FieldByName(head) - if !field.IsValid() { - return fmt.Errorf("%w: invalid field for type %s and name %s", types.ErrInvalidType, vInto, head) - } - - if tail == "" { - if field.Type() != vField.Type() { - return fmt.Errorf("%w: value type mismatch for field %s", types.ErrInvalidType, head) - } - - if !field.CanSet() { - return fmt.Errorf("%w: cannot set field %s", types.ErrInvalidType, head) - } - - field.Set(vField) - - return nil - } - - return applyValueForPath(field, vField, tail) - default: - return fmt.Errorf("%w: cannot set a field from kind %s", types.ErrInvalidType, vInto.Kind()) - } -} - type PathMappingError struct { Err error Path string diff --git a/pkg/codec/precodec.go b/pkg/codec/precodec.go index e0a5b5139..6b56b1563 100644 --- a/pkg/codec/precodec.go +++ b/pkg/codec/precodec.go @@ -88,7 +88,7 @@ func (pc *preCodec) TransformToOffChain(onChainValue any, itemType string) (any, if itemType != "" { into := reflect.New(pc.onChainStructType) - if err := applyValueForPath(into, reflect.ValueOf(onChainValue), itemType); err != nil { + if err := SetValueAtPath(into, reflect.ValueOf(onChainValue), itemType); err != nil { return nil, err } @@ -152,7 +152,7 @@ func (pc *preCodec) TransformToOnChain(offChainValue any, itemType string) (any, if itemType != "" { into := reflect.New(pc.offChainStructType) - if err := applyValueForPath(into, reflect.ValueOf(offChainValue), itemType); err != nil { + if err := SetValueAtPath(into, reflect.ValueOf(offChainValue), itemType); err != nil { return nil, err } diff --git a/pkg/codec/property_extractor.go b/pkg/codec/property_extractor.go index 0a6d9c9ef..1a324ccde 100644 --- a/pkg/codec/property_extractor.go +++ b/pkg/codec/property_extractor.go @@ -168,7 +168,7 @@ func (e *propertyExtractor) selectType(inputValue any, savedType reflect.Type, i if itemType != "" { into := reflect.New(savedType) - if err := applyValueForPath(into, reflect.ValueOf(inputValue), itemType); err != nil { + if err := SetValueAtPath(into, reflect.ValueOf(inputValue), itemType); err != nil { return nil, itemType, err } diff --git a/pkg/codec/utils.go b/pkg/codec/utils.go index bb42c2de0..29917d79b 100644 --- a/pkg/codec/utils.go +++ b/pkg/codec/utils.go @@ -370,3 +370,89 @@ func addr(value reflect.Value) reflect.Value { tmp.Elem().Set(value) return tmp } + +func SetValueAtPath(vInto, vField reflect.Value, itemType string) error { + switch vInto.Kind() { + case reflect.Pointer: + if !vInto.Elem().IsValid() { + into := reflect.New(vInto.Type().Elem()) + + vInto.Set(into) + } + + err := SetValueAtPath(vInto.Elem(), vField, itemType) + if err != nil { + return err + } + + return nil + case reflect.Array, reflect.Slice: + return fmt.Errorf("%w: cannot set a field from an array or slice", types.ErrInvalidType) + case reflect.Struct: + head, tail := ItemTyper(itemType).Next() + + field := vInto.FieldByName(head) + if !field.IsValid() { + return fmt.Errorf("%w: invalid field for type %s and name %s", types.ErrInvalidType, vInto, head) + } + + if tail == "" { + if err := applyValue(field, vField); err != nil { + return fmt.Errorf("%w: %w for field %s", types.ErrInvalidType, err, head) + } + + return nil + } + + return SetValueAtPath(field, vField, tail) + default: + return fmt.Errorf("%w: cannot set a field from kind %s", types.ErrInvalidType, vInto.Kind()) + } +} + +func applyValue(vInto, vField reflect.Value) error { + if typeWithoutPtr(vInto.Type()) != typeWithoutPtr(vField.Type()) { + return fmt.Errorf("value type mismatch for field") + } + + switch vInto.Kind() { + case reflect.Ptr: + switch vField.Kind() { + case reflect.Ptr: + if vInto.CanSet() { + vInto.Set(vField) + + return nil + } + + if !vInto.Elem().IsValid() { + return fmt.Errorf("value to set is unaddressable") + } + + if vField.IsNil() { + vField = reflect.New(vField.Type().Elem()) + } + + vInto.Elem().Set(vField.Elem()) + default: + if !vInto.Elem().IsValid() { + vInto.Set(reflect.New(vInto.Type().Elem())) + } + + vInto.Elem().Set(vField) + } + default: + return fmt.Errorf("input must be a pointer to set value") + } + + return nil +} + +func typeWithoutPtr(val reflect.Type) reflect.Type { + switch val.Kind() { + case reflect.Ptr: + return typeWithoutPtr(val.Elem()) + default: + return val + } +} diff --git a/pkg/codec/utils_test.go b/pkg/codec/utils_test.go index dcf6bbc2c..92075c093 100644 --- a/pkg/codec/utils_test.go +++ b/pkg/codec/utils_test.go @@ -328,3 +328,46 @@ func TestEpochToTimeHook(t *testing.T) { require.Equal(t, expected, output) }) } + +func TestSetValueAtPath(t *testing.T) { + t.Parallel() + + t.Run("works for basic structs", func(t *testing.T) { + t.Parallel() + + type basicStruct struct { + A *int + B string + } + + into := reflect.New(reflect.TypeOf(&basicStruct{})) + + require.NoError(t, SetValueAtPath(into, reflect.ValueOf(int(42)), "A")) + + output := into.Elem().Interface() + + assert.Equal(t, *output.(*basicStruct).A, int(42)) + }) + + t.Run("works for structs with nested structs", func(t *testing.T) { + t.Parallel() + + type nested struct { + X *int + Y *big.Int + } + + type nestableStruct struct { + A nested + B string + } + + into := reflect.New(reflect.TypeOf(nestableStruct{})) + + require.NoError(t, SetValueAtPath(into, reflect.ValueOf(big.NewInt(42)), "A.Y")) + + output := into.Interface() + + assert.Equal(t, output.(*nestableStruct).A.Y.String(), big.NewInt(42).String()) + }) +} From 0b3dc531827fb420ed7f5008044ee67ccab44308 Mon Sep 17 00:00:00 2001 From: Awbrey Hughlett Date: Fri, 21 Feb 2025 08:57:01 -0600 Subject: [PATCH 4/8] fix bug on settable struct --- pkg/codec/utils.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pkg/codec/utils.go b/pkg/codec/utils.go index 29917d79b..f7a3ef9ec 100644 --- a/pkg/codec/utils.go +++ b/pkg/codec/utils.go @@ -442,7 +442,18 @@ func applyValue(vInto, vField reflect.Value) error { vInto.Elem().Set(vField) } default: - return fmt.Errorf("input must be a pointer to set value") + if vInto.CanSet() { + switch vField.Kind() { + case reflect.Ptr: + vInto.Set(vField.Elem()) + default: + vInto.Set(vField) + } + + return nil + } + + return fmt.Errorf("value is not settable") } return nil From 762f880546ee33953726c8b233ead25e6f999c75 Mon Sep 17 00:00:00 2001 From: Awbrey Hughlett Date: Fri, 21 Feb 2025 13:38:50 -0600 Subject: [PATCH 5/8] export value util functions for values at path --- pkg/codec/byte_string_modifier.go | 4 +- pkg/codec/epoch_to_time.go | 4 +- pkg/codec/hard_coder.go | 4 +- pkg/codec/modifier_base.go | 46 ------------------ pkg/codec/precodec.go | 4 +- pkg/codec/property_extractor.go | 4 +- pkg/codec/utils.go | 63 +++++++++++++++++++++++++ pkg/codec/utils_test.go | 78 +++++++++++++++++++++++++++++++ pkg/codec/wrapper.go | 4 +- 9 files changed, 153 insertions(+), 58 deletions(-) diff --git a/pkg/codec/byte_string_modifier.go b/pkg/codec/byte_string_modifier.go index 64befec39..6d5f6871d 100644 --- a/pkg/codec/byte_string_modifier.go +++ b/pkg/codec/byte_string_modifier.go @@ -146,7 +146,7 @@ func (m *bytesToStringModifier) TransformToOnChain(offChainValue any, itemType s } if itemType != "" { - return valueForPath(reflect.ValueOf(modified), itemType) + return ValueForPath(reflect.ValueOf(modified), itemType) } return modified, nil @@ -168,7 +168,7 @@ func (m *bytesToStringModifier) TransformToOffChain(onChainValue any, itemType s } if itemType != "" { - return valueForPath(reflect.ValueOf(modified), itemType) + return ValueForPath(reflect.ValueOf(modified), itemType) } return modified, nil diff --git a/pkg/codec/epoch_to_time.go b/pkg/codec/epoch_to_time.go index 15ea07403..3f5795dc4 100644 --- a/pkg/codec/epoch_to_time.go +++ b/pkg/codec/epoch_to_time.go @@ -58,7 +58,7 @@ func (m *timeToUnixModifier) TransformToOnChain(offChainValue any, itemType stri } if itemType != "" { - return valueForPath(reflect.ValueOf(modified), itemType) + return ValueForPath(reflect.ValueOf(modified), itemType) } return modified, nil @@ -77,7 +77,7 @@ func (m *timeToUnixModifier) TransformToOffChain(onChainValue any, itemType stri } if itemType != "" { - return valueForPath(reflect.ValueOf(modified), itemType) + return ValueForPath(reflect.ValueOf(modified), itemType) } return modified, nil diff --git a/pkg/codec/hard_coder.go b/pkg/codec/hard_coder.go index a224568af..f009bf3c6 100644 --- a/pkg/codec/hard_coder.go +++ b/pkg/codec/hard_coder.go @@ -114,7 +114,7 @@ func (m *onChainHardCoder) TransformToOnChain(offChainValue any, itemType string } if itemType != "" { - return valueForPath(reflect.ValueOf(modified), itemType) + return ValueForPath(reflect.ValueOf(modified), itemType) } return modified, nil @@ -143,7 +143,7 @@ func (m *onChainHardCoder) TransformToOffChain(onChainValue any, itemType string } if itemType != "" { - return valueForPath(reflect.ValueOf(modified), itemType) + return ValueForPath(reflect.ValueOf(modified), itemType) } return modified, nil diff --git a/pkg/codec/modifier_base.go b/pkg/codec/modifier_base.go index e9ef19999..5b58ded8b 100644 --- a/pkg/codec/modifier_base.go +++ b/pkg/codec/modifier_base.go @@ -394,52 +394,6 @@ func typeForPath(from reflect.Type, itemType string) (reflect.Type, error) { } } -func valueForPath(from reflect.Value, itemType string) (any, error) { - if itemType == "" { - return from.Interface(), nil - } - - switch from.Kind() { - case reflect.Pointer: - elem, err := valueForPath(from.Elem(), itemType) - if err != nil { - return nil, err - } - - return elem, nil - case reflect.Array, reflect.Slice: - return nil, fmt.Errorf("%w: cannot extract a field from an array or slice", types.ErrInvalidType) - case reflect.Struct: - head, tail := ItemTyper(itemType).Next() - - field := from.FieldByName(head) - if !field.IsValid() { - return nil, fmt.Errorf("%w: field not found for path %s and itemType %s", types.ErrInvalidType, from, itemType) - } - - if tail == "" { - return field.Interface(), nil - } - - return valueForPath(field, tail) - case reflect.Map: - head, tail := ItemTyper(itemType).Next() - - field := from.MapIndex(reflect.ValueOf(head)) - if !field.IsValid() { - return nil, fmt.Errorf("%w: field not found for path %s and itemType %s", types.ErrInvalidType, from, itemType) - } - - if tail == "" { - return field.Interface(), nil - } - - return valueForPath(reflect.ValueOf(field.Interface()), tail) - default: - return nil, fmt.Errorf("%w: cannot extract a field from kind %s", types.ErrInvalidType, from.Kind()) - } -} - type PathMappingError struct { Err error Path string diff --git a/pkg/codec/precodec.go b/pkg/codec/precodec.go index 6b56b1563..0285b335a 100644 --- a/pkg/codec/precodec.go +++ b/pkg/codec/precodec.go @@ -101,7 +101,7 @@ func (pc *preCodec) TransformToOffChain(onChainValue any, itemType string) (any, } if itemType != "" { - return valueForPath(reflect.ValueOf(modified), itemType) + return ValueForPath(reflect.ValueOf(modified), itemType) } return modified, nil @@ -165,7 +165,7 @@ func (pc *preCodec) TransformToOnChain(offChainValue any, itemType string) (any, } if itemType != "" { - return valueForPath(reflect.ValueOf(modified), itemType) + return ValueForPath(reflect.ValueOf(modified), itemType) } return modified, nil diff --git a/pkg/codec/property_extractor.go b/pkg/codec/property_extractor.go index 1a324ccde..af3d06064 100644 --- a/pkg/codec/property_extractor.go +++ b/pkg/codec/property_extractor.go @@ -126,7 +126,7 @@ func (e *propertyExtractor) TransformToOnChain(offChainValue any, itemType strin // add the field name because the offChainType was nested into a new struct itemType = fmt.Sprintf("%s.%s", e.fieldName, itemType) - return valueForPath(reflect.ValueOf(modified), itemType) + return ValueForPath(reflect.ValueOf(modified), itemType) } return modified, nil @@ -147,7 +147,7 @@ func (e *propertyExtractor) TransformToOffChain(onChainValue any, itemType strin // remove the head from the itemType because a field was extracted _, tail := ItemTyper(itemType).Next() - return valueForPath(reflect.ValueOf(modified), tail) + return ValueForPath(reflect.ValueOf(modified), tail) } return modified, nil diff --git a/pkg/codec/utils.go b/pkg/codec/utils.go index f7a3ef9ec..f829a09e4 100644 --- a/pkg/codec/utils.go +++ b/pkg/codec/utils.go @@ -371,6 +371,52 @@ func addr(value reflect.Value) reflect.Value { return tmp } +func ValueForPath(from reflect.Value, itemType string) (any, error) { + if itemType == "" { + return from.Interface(), nil + } + + switch from.Kind() { + case reflect.Pointer: + elem, err := ValueForPath(from.Elem(), itemType) + if err != nil { + return nil, err + } + + return elem, nil + case reflect.Array, reflect.Slice: + return nil, fmt.Errorf("%w: cannot extract a field from an array or slice", types.ErrInvalidType) + case reflect.Struct: + head, tail := ItemTyper(itemType).Next() + + field := from.FieldByName(head) + if !field.IsValid() { + return nil, fmt.Errorf("%w: field not found for path %s and itemType %s", types.ErrInvalidType, from, itemType) + } + + if tail == "" { + return field.Interface(), nil + } + + return ValueForPath(field, tail) + case reflect.Map: + head, tail := ItemTyper(itemType).Next() + + field := from.MapIndex(reflect.ValueOf(head)) + if !field.IsValid() { + return nil, fmt.Errorf("%w: field not found for path %s and itemType %s", types.ErrInvalidType, from, itemType) + } + + if tail == "" { + return field.Interface(), nil + } + + return ValueForPath(reflect.ValueOf(field.Interface()), tail) + default: + return nil, fmt.Errorf("%w: cannot extract a field from kind %s", types.ErrInvalidType, from.Kind()) + } +} + func SetValueAtPath(vInto, vField reflect.Value, itemType string) error { switch vInto.Kind() { case reflect.Pointer: @@ -404,6 +450,23 @@ func SetValueAtPath(vInto, vField reflect.Value, itemType string) error { return nil } + return SetValueAtPath(field, vField, tail) + case reflect.Map: + head, tail := ItemTyper(itemType).Next() + + field := vInto.MapIndex(reflect.ValueOf(head)) + if !field.IsValid() { + return fmt.Errorf("%w: field not found for itemType %s", types.ErrInvalidType, itemType) + } + + if tail == "" { + if err := applyValue(field, vField); err != nil { + return fmt.Errorf("%w: %w for field %s", types.ErrInvalidType, err, head) + } + + return nil + } + return SetValueAtPath(field, vField, tail) default: return fmt.Errorf("%w: cannot set a field from kind %s", types.ErrInvalidType, vInto.Kind()) diff --git a/pkg/codec/utils_test.go b/pkg/codec/utils_test.go index 92075c093..382c1f7b8 100644 --- a/pkg/codec/utils_test.go +++ b/pkg/codec/utils_test.go @@ -329,6 +329,84 @@ func TestEpochToTimeHook(t *testing.T) { }) } +func TestValueForPath(t *testing.T) { + t.Parallel() + + type nestableStruct struct { + X *big.Int + Y string + Z map[string]any + } + + type baseStruct struct { + A *int + B nestableStruct + C []bool + } + + t.Run("can extract nested field value", func(t *testing.T) { + t.Parallel() + + expected := "test" + str := baseStruct{B: nestableStruct{Y: expected}} + value, err := ValueForPath(reflect.ValueOf(str), "B.Y") + + require.NoError(t, err) + assert.Equal(t, expected, value) + + value, err = ValueForPath(reflect.ValueOf(&str), "B.Y") + + require.NoError(t, err) + assert.Equal(t, expected, value) + }) + + t.Run("can extract nested field value with pointer", func(t *testing.T) { + t.Parallel() + + expected := big.NewInt(42) + str := baseStruct{B: nestableStruct{X: expected}} + value, err := ValueForPath(reflect.ValueOf(str), "B.X") + + require.NoError(t, err) + assert.Equal(t, expected, value) + + value, err = ValueForPath(reflect.ValueOf(&str), "B.X") + + require.NoError(t, err) + assert.Equal(t, expected, value) + }) + + t.Run("can extract nested field value from map", func(t *testing.T) { + t.Parallel() + + expected := "test" + str := baseStruct{B: nestableStruct{Z: map[string]any{"Field": expected}}} + value, err := ValueForPath(reflect.ValueOf(str), "B.Z.Field") + + require.NoError(t, err) + assert.Equal(t, expected, value) + }) + + t.Run("returns error for arrays", func(t *testing.T) { + t.Parallel() + + str := baseStruct{C: []bool{true, false}} + _, err := ValueForPath(reflect.ValueOf(str), "C.[0]") + + require.ErrorIs(t, err, types.ErrInvalidType) + }) + + t.Run("returns error for field not found", func(t *testing.T) { + t.Parallel() + + expected := "test" + str := baseStruct{B: nestableStruct{Y: expected}} + _, err := ValueForPath(reflect.ValueOf(str), "B.D") + + require.ErrorIs(t, err, types.ErrInvalidType) + }) +} + func TestSetValueAtPath(t *testing.T) { t.Parallel() diff --git a/pkg/codec/wrapper.go b/pkg/codec/wrapper.go index fb6ad0a3c..3badffe28 100644 --- a/pkg/codec/wrapper.go +++ b/pkg/codec/wrapper.go @@ -47,7 +47,7 @@ func (m *wrapperModifier) TransformToOnChain(offChainValue any, itemType string) } if itemType != "" { - return valueForPath(reflect.ValueOf(modified), itemType) + return ValueForPath(reflect.ValueOf(modified), itemType) } return modified, nil @@ -65,7 +65,7 @@ func (m *wrapperModifier) TransformToOffChain(onChainValue any, itemType string) } if itemType != "" { - return valueForPath(reflect.ValueOf(modified), itemType) + return ValueForPath(reflect.ValueOf(modified), itemType) } return modified, nil From 9851a2dc8b4ebe71207f55d40e942ae3caaf42d0 Mon Sep 17 00:00:00 2001 From: Awbrey Hughlett Date: Mon, 24 Feb 2025 11:13:44 -0600 Subject: [PATCH 6/8] add example to address modifier --- pkg/codec/byte_string_modifier.go | 66 +++++++++++++++++++++++++++++++ pkg/codec/example_test.go | 21 ++++++++++ 2 files changed, 87 insertions(+) diff --git a/pkg/codec/byte_string_modifier.go b/pkg/codec/byte_string_modifier.go index 6d5f6871d..e79a55530 100644 --- a/pkg/codec/byte_string_modifier.go +++ b/pkg/codec/byte_string_modifier.go @@ -2,12 +2,78 @@ package codec import ( "bytes" + "crypto/rand" + "encoding/base64" + "errors" "fmt" "reflect" "github.com/smartcontractkit/chainlink-common/pkg/types" ) +type ExampleAddressModifier struct{} + +func (m *ExampleAddressModifier) EncodeAddress(bts []byte) (string, error) { + if len(bts) > 32 { + return "", errors.New("upexpected address byte length") + } + + normalized := make([]byte, 32) + + // apply byts as big endian + copy(normalized[:], bts[:]) + + return base64.StdEncoding.EncodeToString(normalized), nil +} + +func (m *ExampleAddressModifier) DecodeAddress(str string) ([]byte, error) { + decodedBytes, err := base64.StdEncoding.DecodeString(str) + if err != nil { + return nil, err + } + + if len(decodedBytes) != 32 { + return nil, errors.New("unexpected address byte length") + } + + return decodedBytes, nil +} + +func (m *ExampleAddressModifier) Length() int { + return 32 +} + +func ExampleAddressBytesToStringModifier() { + type onChainNested struct { + X []byte + } + + type onChain struct { + A [32]byte + B onChainNested + } + + encoder := &ExampleAddressModifier{} + mod := NewPathTraverseAddressBytesToStringModifier([]string{"B.X"}, encoder, true) + + // call RetypeToOffChain first with empty itemType to set base types + offChainType, _ := mod.RetypeToOffChain(reflect.TypeOf(&onChain{}), "") + + fmt.Println("offChainType:") + fmt.Println(offChainType) + // offChainType: + // struct { A: string; B: struct { X: string } } + + // calls to transform can transform the entire struct or nested fields specified by itemType + onChainAddress := [32]byte{} + _, _ = rand.Read(onChainAddress[:]) + + offChainAddress, _ := mod.TransformToOffChain(onChainAddress, "A") + + // the onChainAddress value is modified to the offChainType + fmt.Println(offChainAddress) +} + // AddressModifier defines the interface for encoding, decoding, and handling addresses. // This interface allows for chain-specific logic to be injected into the modifier without // modifying the common repository. diff --git a/pkg/codec/example_test.go b/pkg/codec/example_test.go index 96a6fa032..207ebdd5a 100644 --- a/pkg/codec/example_test.go +++ b/pkg/codec/example_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "math" + "math/big" "time" "github.com/smartcontractkit/chainlink-common/pkg/codec" @@ -53,6 +54,26 @@ func (ExampleStructJSONCodec) CreateType(_ string, _ bool) (any, error) { return &OnChainStruct{}, nil } +type ExampleOffChainNestedTestStruct struct { + X *int + Y *big.Int + Z map[string]any +} + +type ExampleOffChainTestStruct struct { + A string + B ExampleOffChainNestedTestStruct + C []byte +} + +type ExampleOnChainNestedTestStruct struct{} + +type ExampleOnChainTestStruct struct { + A []byte + B ExampleOnChainNestedTestStruct + C [32]byte +} + type OnChainStruct struct { Aa int64 Bb string From a7e554683ca835f77fd0cf73cef4953dc282e888 Mon Sep 17 00:00:00 2001 From: Awbrey Hughlett Date: Mon, 24 Feb 2025 11:59:12 -0600 Subject: [PATCH 7/8] move and rename examples for CI to pass --- .../encodings/binary/example_bigendian_test.go} | 6 +++--- pkg/services/service_example_simple_test.go | 2 +- pkg/services/service_example_state_test.go | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename pkg/{types/example_codec_test.go => codec/encodings/binary/example_bigendian_test.go} (91%) diff --git a/pkg/types/example_codec_test.go b/pkg/codec/encodings/binary/example_bigendian_test.go similarity index 91% rename from pkg/types/example_codec_test.go rename to pkg/codec/encodings/binary/example_bigendian_test.go index 94dec8155..0debf449f 100644 --- a/pkg/types/example_codec_test.go +++ b/pkg/codec/encodings/binary/example_bigendian_test.go @@ -1,4 +1,4 @@ -package types_test +package binary_test import ( "context" @@ -9,8 +9,8 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings/binary" ) -// ExampleCodec provides a minimal example of constructing and using a codec. -func ExampleCodec() { +// ExampleBigEndian provides a minimal example of constructing and using a codec. +func ExampleBigEndian() { ctx := context.Background() typeCodec, _ := binary.BigEndian().BigInt(32, true) diff --git a/pkg/services/service_example_simple_test.go b/pkg/services/service_example_simple_test.go index 5f50d5af7..fffb9c5dd 100644 --- a/pkg/services/service_example_simple_test.go +++ b/pkg/services/service_example_simple_test.go @@ -20,7 +20,7 @@ func NewSimple(lggr logger.Logger) *simple { // The name makes sense for the test even though there's no ExampleService struct // nolint -func ExampleNewService() { +func ExampleService() { lggr, err := Logger() if err != nil { fmt.Println("Failed to create logger:", err) diff --git a/pkg/services/service_example_state_test.go b/pkg/services/service_example_state_test.go index d875c4be7..ab0c36b18 100644 --- a/pkg/services/service_example_state_test.go +++ b/pkg/services/service_example_state_test.go @@ -59,7 +59,7 @@ func (f *stateMachine) run() { } } -func ExampleService() { +func ExampleStateMachine() { lggr, err := Logger() if err != nil { fmt.Println("Failed to create logger:", err) From a6a6a4f6512bc643221ad0512855217125dc35a2 Mon Sep 17 00:00:00 2001 From: Awbrey Hughlett Date: Mon, 24 Feb 2025 13:08:26 -0600 Subject: [PATCH 8/8] move example to test file --- pkg/codec/byte_string_modifier.go | 32 ------------------------- pkg/codec/byte_string_modifier_test.go | 33 ++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 32 deletions(-) diff --git a/pkg/codec/byte_string_modifier.go b/pkg/codec/byte_string_modifier.go index e79a55530..a0e69535e 100644 --- a/pkg/codec/byte_string_modifier.go +++ b/pkg/codec/byte_string_modifier.go @@ -2,7 +2,6 @@ package codec import ( "bytes" - "crypto/rand" "encoding/base64" "errors" "fmt" @@ -43,37 +42,6 @@ func (m *ExampleAddressModifier) Length() int { return 32 } -func ExampleAddressBytesToStringModifier() { - type onChainNested struct { - X []byte - } - - type onChain struct { - A [32]byte - B onChainNested - } - - encoder := &ExampleAddressModifier{} - mod := NewPathTraverseAddressBytesToStringModifier([]string{"B.X"}, encoder, true) - - // call RetypeToOffChain first with empty itemType to set base types - offChainType, _ := mod.RetypeToOffChain(reflect.TypeOf(&onChain{}), "") - - fmt.Println("offChainType:") - fmt.Println(offChainType) - // offChainType: - // struct { A: string; B: struct { X: string } } - - // calls to transform can transform the entire struct or nested fields specified by itemType - onChainAddress := [32]byte{} - _, _ = rand.Read(onChainAddress[:]) - - offChainAddress, _ := mod.TransformToOffChain(onChainAddress, "A") - - // the onChainAddress value is modified to the offChainType - fmt.Println(offChainAddress) -} - // AddressModifier defines the interface for encoding, decoding, and handling addresses. // This interface allows for chain-specific logic to be injected into the modifier without // modifying the common repository. diff --git a/pkg/codec/byte_string_modifier_test.go b/pkg/codec/byte_string_modifier_test.go index f6fdafed0..3fdcef423 100644 --- a/pkg/codec/byte_string_modifier_test.go +++ b/pkg/codec/byte_string_modifier_test.go @@ -1,8 +1,10 @@ package codec_test import ( + "crypto/rand" "encoding/hex" "errors" + "fmt" "reflect" "testing" @@ -339,3 +341,34 @@ func TestAddressBytesToString(t *testing.T) { assert.Contains(t, err.Error(), "cannot convert bytes for field T") }) } + +func ExampleNewPathTraverseAddressBytesToStringModifier() { + type onChainNested struct { + X []byte + } + + type onChain struct { + A [32]byte + B onChainNested + } + + encoder := &codec.ExampleAddressModifier{} + mod := codec.NewPathTraverseAddressBytesToStringModifier([]string{"B.X"}, encoder, true) + + // call RetypeToOffChain first with empty itemType to set base types + offChainType, _ := mod.RetypeToOffChain(reflect.TypeOf(&onChain{}), "") + + fmt.Println("offChainType:") + fmt.Println(offChainType) + // offChainType: + // struct { A: string; B: struct { X: string } } + + // calls to transform can transform the entire struct or nested fields specified by itemType + onChainAddress := [32]byte{} + _, _ = rand.Read(onChainAddress[:]) + + offChainAddress, _ := mod.TransformToOffChain(onChainAddress, "A") + + // the onChainAddress value is modified to the offChainType + fmt.Println(offChainAddress) +}