Skip to content

Commit

Permalink
add bytes to string modifier for solana contracts (#1040)
Browse files Browse the repository at this point in the history
* add bytes to string modifier for solana contracts

* property extractor path traversal

* update modifiers and value extraction

* fix bug on settable struct

* export value util functions for values at path

* add example to address modifier

* move and rename examples for CI to pass

* move example to test file
  • Loading branch information
EasterTheBunny authored Feb 24, 2025
1 parent b3b5e4e commit dc2073f
Show file tree
Hide file tree
Showing 16 changed files with 600 additions and 138 deletions.
74 changes: 72 additions & 2 deletions pkg/codec/byte_string_modifier.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,47 @@
package codec

import (
"bytes"
"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
}

// 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.
Expand Down Expand Up @@ -66,6 +101,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
Expand Down Expand Up @@ -110,7 +180,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
Expand All @@ -132,7 +202,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
Expand Down
33 changes: 33 additions & 0 deletions pkg/codec/byte_string_modifier_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package codec_test

import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"reflect"
"testing"

Expand Down Expand Up @@ -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)
}
25 changes: 23 additions & 2 deletions pkg/codec/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"
)

Expand Down Expand Up @@ -334,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) {
Expand Down Expand Up @@ -368,6 +372,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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package types_test
package binary_test

import (
"context"
Expand All @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions pkg/codec/epoch_to_time.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
21 changes: 21 additions & 0 deletions pkg/codec/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"math"
"math/big"
"time"

"github.com/smartcontractkit/chainlink-common/pkg/codec"
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions pkg/codec/hard_coder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
80 changes: 1 addition & 79 deletions pkg/codec/modifier_base.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -394,84 +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)
default:
return nil, fmt.Errorf("%w: cannot extract a field from kind %s", types.ErrInvalidType, from.Kind())
}
}

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
Expand Down
Loading

0 comments on commit dc2073f

Please sign in to comment.