Skip to content

Commit

Permalink
(feat): Add PreCodec modifier (#961)
Browse files Browse the repository at this point in the history
  • Loading branch information
justinkaseman authored Dec 13, 2024
1 parent 6a43e61 commit dbebc0f
Show file tree
Hide file tree
Showing 3 changed files with 573 additions and 0 deletions.
67 changes: 67 additions & 0 deletions pkg/codec/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
// - epoch to time -> [EpochToTimeModifierConfig]
// - address to string -> [AddressBytesToStringModifierConfig]
// - field wrapper -> [WrapperModifierConfig]
// - precodec -> [PrecodecModifierConfig]
type ModifiersConfig []ModifierConfig

func (m *ModifiersConfig) UnmarshalJSON(data []byte) error {
Expand Down Expand Up @@ -58,6 +59,8 @@ func (m *ModifiersConfig) UnmarshalJSON(data []byte) error {
(*m)[i] = &AddressBytesToStringModifierConfig{}
case ModifierWrapper:
(*m)[i] = &WrapperModifierConfig{}
case ModifierPreCodec:
(*m)[i] = &PreCodecModifierConfig{}
default:
return fmt.Errorf("%w: unknown modifier type: %s", types.ErrInvalidConfig, mType)
}
Expand All @@ -84,6 +87,7 @@ func (m *ModifiersConfig) ToModifier(onChainHooks ...mapstructure.DecodeHookFunc
type ModifierType string

const (
ModifierPreCodec ModifierType = "precodec"
ModifierRename ModifierType = "rename"
ModifierDrop ModifierType = "drop"
ModifierHardCode ModifierType = "hard code"
Expand Down Expand Up @@ -199,6 +203,69 @@ func (h *HardCodeModifierConfig) MarshalJSON() ([]byte, error) {
})
}

// PreCodec creates a modifier that will transform data using a preliminary encoding/decoding step.
// 'Off-chain' values will be overwritten with the encoded data as a byte array.
// 'On-chain' values will be typed using the optimistic types from the codec.
// This is useful when wanting to move the data as generic bytes.
//
// Example:
//
// Based on this input struct:
// type example struct {
// A []B
// }
//
// type B struct {
// C string
// D string
// }
//
// And the fields config defined as:
// {"A": "string C, string D"}
//
// The codec config gives a map of strings (the values from fields config map) to implementation for encoding/decoding
//
// RemoteCodec {
// func (types.TypeProvider) CreateType(itemType string, forEncoding bool) (any, error)
// func (types.Decoder) Decode(ctx context.Context, raw []byte, into any, itemType string) error
// func (types.Encoder) Encode(ctx context.Context, item any, itemType string) ([]byte, error)
// func (types.Decoder) GetMaxDecodingSize(ctx context.Context, n int, itemType string) (int, error)
// func (types.Encoder) GetMaxEncodingSize(ctx context.Context, n int, itemType string) (int, error)
// }
//
// {"string C, string D": RemoteCodec}
//
// Result:
// type example struct {
// A [][]bytes
// }
//
// Where []bytes are the encoded input struct B
type PreCodecModifierConfig struct {
// A map of a path of properties to encoding scheme.
// If the path leads to an array, encoding will occur on every entry.
//
// Example: "a.b" -> "uint256 Value"
Fields map[string]string
// Codecs is skipped in JSON serialization, it will be injected later.
// The map should be keyed using the value from "Fields" to a corresponding Codec that can encode/decode for it
// This allows encoding and decoding implementations to be handled outside of the modifier.
//
// Example: "uint256 Value" -> a chain specific encoder for "uint256 Value"
Codecs map[string]types.RemoteCodec `json:"-"`
}

func (c *PreCodecModifierConfig) ToModifier(_ ...mapstructure.DecodeHookFunc) (Modifier, error) {
return NewPreCodec(c.Fields, c.Codecs)
}

func (c *PreCodecModifierConfig) MarshalJSON() ([]byte, error) {
return json.Marshal(&modifierMarshaller[PreCodecModifierConfig]{
Type: ModifierPreCodec,
T: c,
})
}

// EpochToTimeModifierConfig is used to convert epoch seconds as uint64 fields on-chain to time.Time
type EpochToTimeModifierConfig struct {
Fields []string
Expand Down
113 changes: 113 additions & 0 deletions pkg/codec/precodec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package codec

import (
"context"
"fmt"
"reflect"

"github.com/go-viper/mapstructure/v2"
"github.com/smartcontractkit/chainlink-common/pkg/types"
)

// PreCodec creates a modifier that will run a preliminary encoding/decoding step.
// This is useful when wanting to move nested data as generic bytes.
func NewPreCodec(fields map[string]string, codecs map[string]types.RemoteCodec) (Modifier, error) {
m := &preCodec{
modifierBase: modifierBase[string]{
fields: fields,
onToOffChainType: map[reflect.Type]reflect.Type{},
offToOnChainType: map[reflect.Type]reflect.Type{},
},
codecs: codecs,
}

// validate that there is a codec for each unique type definition
for _, typeDef := range fields {
if _, ok := m.codecs[typeDef]; ok {
continue
}
return nil, fmt.Errorf("codec not supplied for: %s", typeDef)
}

m.modifyFieldForInput = func(_ string, field *reflect.StructField, _ string, typeDef string) error {
if field.Type != reflect.SliceOf(reflect.TypeFor[uint8]()) {
return fmt.Errorf("can only decode []byte from on-chain: %s", field.Type)
}

codec, ok := m.codecs[typeDef]
if !ok || codec == nil {
return fmt.Errorf("codec not found for type definition: '%s'", typeDef)
}

newType, err := codec.CreateType("", false)
if err != nil {
return err
}
field.Type = reflect.TypeOf(newType)

return nil
}

return m, nil
}

type preCodec struct {
modifierBase[string]
codecs map[string]types.RemoteCodec
}

func (pc *preCodec) TransformToOffChain(onChainValue any, _ string) (any, error) {
allHooks := make([]mapstructure.DecodeHookFunc, 1)
allHooks[0] = hardCodeManyHook

return transformWithMaps(onChainValue, pc.onToOffChainType, pc.fields, pc.decodeFieldMapAction, allHooks...)
}

func (pc *preCodec) decodeFieldMapAction(extractMap map[string]any, key string, typeDef string) error {
_, exists := extractMap[key]
if !exists {
return fmt.Errorf("field %s does not exist", key)
}

codec, ok := pc.codecs[typeDef]
if !ok || codec == nil {
return fmt.Errorf("codec not found for type definition: '%s'", typeDef)
}

to, err := codec.CreateType("", false)
if err != nil {
return err
}
err = codec.Decode(context.Background(), extractMap[key].([]byte), &to, "")
if err != nil {
return err
}
extractMap[key] = to
return nil
}

func (pc *preCodec) TransformToOnChain(offChainValue any, _ string) (any, error) {
allHooks := make([]mapstructure.DecodeHookFunc, 1)
allHooks[0] = hardCodeManyHook

return transformWithMaps(offChainValue, pc.offToOnChainType, pc.fields, pc.encodeFieldMapAction, allHooks...)
}

func (pc *preCodec) encodeFieldMapAction(extractMap map[string]any, key string, typeDef string) error {
_, exists := extractMap[key]
if !exists {
return fmt.Errorf("field %s does not exist", key)
}

codec, ok := pc.codecs[typeDef]
if !ok || codec == nil {
return fmt.Errorf("codec not found for type definition: '%s'", typeDef)
}

encoded, err := codec.Encode(context.Background(), extractMap[key], "")
if err != nil {
return err
}
extractMap[key] = encoded
return nil
}
Loading

0 comments on commit dbebc0f

Please sign in to comment.