diff --git a/bindings/bind/common.go b/bindings/bind/common.go index 3efabec1..56b4da3c 100644 --- a/bindings/bind/common.go +++ b/bindings/bind/common.go @@ -12,6 +12,7 @@ import ( "github.com/block-vision/sui-go-sdk/transaction" bindutils "github.com/smartcontractkit/chainlink-sui/bindings/utils" + "github.com/smartcontractkit/chainlink-sui/codec" "github.com/smartcontractkit/chainlink-sui/relayer/client" ) @@ -32,10 +33,10 @@ type transactionObjectChangesClient interface { GetTransactionChangedObjects(ctx context.Context, digest string) ([]*suirpcv2.ChangedObject, error) } -type Object struct { - Id string - InitialSharedVersion *uint64 -} +// Object is an alias for codec.Object for backward compatibility. +// +// Deprecated: use codec.Object directly. +type Object = codec.Object type EmptyMoveStructWitness struct{} diff --git a/bindings/bind/json_decoder.go b/bindings/bind/json_decoder.go index 5edf4558..4117d71e 100644 --- a/bindings/bind/json_decoder.go +++ b/bindings/bind/json_decoder.go @@ -1,47 +1,24 @@ package bind import ( - "encoding/json" - "errors" "fmt" "math/big" "reflect" "strings" "github.com/mitchellh/mapstructure" + + "github.com/smartcontractkit/chainlink-sui/codec" ) // DecodeJSONReturn decodes gRPC/JSON Move return values into the provided target. +// +// Deprecated: use codec.DecodeJSONReturn directly. func DecodeJSONReturn(data any, target any) error { - if target == nil { - return errors.New("target cannot be nil") - } - - if raw, ok := data.(json.RawMessage); ok { - var intermediate any - if err := json.Unmarshal(raw, &intermediate); err != nil { - return fmt.Errorf("json unmarshal failed: %w", err) - } - - return DecodeJSONReturn(intermediate, target) - } - - if reflect.TypeOf(data) == reflect.TypeOf(target).Elem() { - reflect.ValueOf(target).Elem().Set(reflect.ValueOf(data)) - return nil - } - - targetType := reflect.TypeOf(target).Elem() - - bigPtrT := reflect.TypeOf((*big.Int)(nil)) - bigValT := bigPtrT.Elem() - if targetType == bigValT || targetType == bigPtrT { - return decodeBigInt(data, target) - } - - return decodeWithMapstructure(data, target) + return codec.DecodeJSONReturn(data, target) } +// decodeBigInt decodes a string into a big.Int target. func decodeBigInt(data any, target any) error { str, ok := data.(string) if !ok { @@ -67,6 +44,7 @@ func decodeBigInt(data any, target any) error { return nil } +// decodeWithMapstructure decodes using mapstructure with custom hooks. func decodeWithMapstructure(data any, target any) error { config := &mapstructure.DecoderConfig{ DecodeHook: mapstructure.ComposeDecodeHookFunc( diff --git a/bindings/bind/utils.go b/bindings/bind/utils.go index f2aa7143..eba9c667 100644 --- a/bindings/bind/utils.go +++ b/bindings/bind/utils.go @@ -1,59 +1,25 @@ package bind import ( - "encoding/hex" "fmt" - "strings" - "unicode" "github.com/block-vision/sui-go-sdk/models" - "github.com/block-vision/sui-go-sdk/utils" + + "github.com/smartcontractkit/chainlink-sui/codec" ) // IsSuiAddress returns true if addr is a valid Sui address/ObjectID. -// It is an improvement over the sui-go-sdk's IsValidSuiAddress function. +// +// Deprecated: use codec.IsSuiAddress directly. func IsSuiAddress(addr string) bool { - if !(strings.HasPrefix(addr, "0x") || strings.HasPrefix(addr, "0X")) { - return false - } - - h := addr[2:] - - // 1..64 hex chars - if len(h) == 0 || len(h) > 64 { - return false - } - - // hex only - for _, r := range h { - if !isHexRune(r) { - return false - } - } - - // hex.DecodeString requires even length; allow odd by left-padding one '0' - if len(h)%2 == 1 { - h = "0" + h - } - - b, err := hex.DecodeString(h) - - if err != nil { - return false - } - - // must be ≤32 bytes (32 bytes == 64 hex chars) - return len(b) <= 32 + return codec.IsSuiAddress(addr) } -// ToSuiAddress normalizes and validates a Sui address +// ToSuiAddress normalizes and validates a Sui address. +// +// Deprecated: use codec.ToSuiAddress directly. func ToSuiAddress(address string) (string, error) { - normalized := utils.NormalizeSuiAddress(address) - if !IsSuiAddress(string(normalized)) { - return "", fmt.Errorf("invalid sui address: %s", address) - } - - return string(normalized), nil + return codec.ToSuiAddress(address) } func GetFailedTxError(tx *models.SuiTransactionBlockResponse) error { @@ -63,9 +29,3 @@ func GetFailedTxError(tx *models.SuiTransactionBlockResponse) error { return fmt.Errorf("transaction failed with error: %s", tx.Effects.Status.Error) } - -func isHexRune(r rune) bool { - return unicode.IsDigit(r) || - ('a' <= r && r <= 'f') || - ('A' <= r && r <= 'F') -} diff --git a/codec/bcs_converter.go b/codec/bcs_converter.go new file mode 100644 index 00000000..6566fcd3 --- /dev/null +++ b/codec/bcs_converter.go @@ -0,0 +1,241 @@ +package codec + +import ( + "fmt" + "strconv" + + aptosBCS "github.com/aptos-labs/aptos-go-sdk/bcs" +) + +// BCSPrimitiveHandler defines a function that reads a primitive type from a BCS deserializer +type BCSPrimitiveHandler func(*aptosBCS.Deserializer) (any, error) + +// BCSVectorHandler defines a function that reads a vector type from a BCS deserializer +type BCSVectorHandler func(*aptosBCS.Deserializer, uint64) (any, error) + +// BCSTypeConverter provides a registry-based approach to converting BCS types to JSON-compatible values +type BCSTypeConverter struct { + primitiveHandlers map[string]BCSPrimitiveHandler + vectorHandlers map[string]BCSVectorHandler +} + +// NewBCSTypeConverter creates a new BCS type converter with all standard Sui types registered +func NewBCSTypeConverter() *BCSTypeConverter { + c := &BCSTypeConverter{ + primitiveHandlers: make(map[string]BCSPrimitiveHandler), + vectorHandlers: make(map[string]BCSVectorHandler), + } + + // Register primitive type handlers + c.registerPrimitiveHandlers() + c.registerVectorHandlers() + + return c +} + +// registerPrimitiveHandlers registers all standard Sui primitive type handlers +func (c *BCSTypeConverter) registerPrimitiveHandlers() { + // U8 - return as-is + c.RegisterPrimitive("U8", func(d *aptosBCS.Deserializer) (any, error) { + return d.U8(), nil + }) + c.RegisterPrimitive("u8", func(d *aptosBCS.Deserializer) (any, error) { + return d.U8(), nil + }) + + // U16 - return as-is + c.RegisterPrimitive("U16", func(d *aptosBCS.Deserializer) (any, error) { + return d.U16(), nil + }) + c.RegisterPrimitive("u16", func(d *aptosBCS.Deserializer) (any, error) { + return d.U16(), nil + }) + + // U32 - return as-is + c.RegisterPrimitive("U32", func(d *aptosBCS.Deserializer) (any, error) { + return d.U32(), nil + }) + c.RegisterPrimitive("u32", func(d *aptosBCS.Deserializer) (any, error) { + return d.U32(), nil + }) + + // U64 - return as string for JSON compatibility + c.RegisterPrimitive("U64", func(d *aptosBCS.Deserializer) (any, error) { + value := d.U64() + return strconv.FormatUint(value, base10), nil + }) + c.RegisterPrimitive("u64", func(d *aptosBCS.Deserializer) (any, error) { + value := d.U64() + return strconv.FormatUint(value, base10), nil + }) + + // U128 - return as string for JSON compatibility + c.RegisterPrimitive("U128", func(d *aptosBCS.Deserializer) (any, error) { + value := d.U128() + return value.String(), nil + }) + c.RegisterPrimitive("u128", func(d *aptosBCS.Deserializer) (any, error) { + value := d.U128() + return value.String(), nil + }) + + // U256 - return as string for JSON compatibility + c.RegisterPrimitive("U256", func(d *aptosBCS.Deserializer) (any, error) { + value := d.U256() + return value.String(), nil + }) + c.RegisterPrimitive("u256", func(d *aptosBCS.Deserializer) (any, error) { + value := d.U256() + return value.String(), nil + }) + + // Bool - return as-is + c.RegisterPrimitive("Bool", func(d *aptosBCS.Deserializer) (any, error) { + return d.Bool(), nil + }) + c.RegisterPrimitive("bool", func(d *aptosBCS.Deserializer) (any, error) { + return d.Bool(), nil + }) + + // Address - return as byte array + c.RegisterPrimitive("Address", func(d *aptosBCS.Deserializer) (any, error) { + addressBytesLen := 32 + return d.ReadFixedBytes(addressBytesLen), nil + }) + c.RegisterPrimitive("address", func(d *aptosBCS.Deserializer) (any, error) { + addressBytesLen := 32 + return d.ReadFixedBytes(addressBytesLen), nil + }) +} + +// registerVectorHandlers registers standard vector type handlers +func (c *BCSTypeConverter) registerVectorHandlers() { + // Vector - read as byte array + c.RegisterVector("U8", func(d *aptosBCS.Deserializer, length uint64) (any, error) { + bytes := make([]byte, length) + for i := range length { + bytes[i] = d.U8() + } + return bytes, nil + }) + c.RegisterVector("u8", func(d *aptosBCS.Deserializer, length uint64) (any, error) { + bytes := make([]byte, length) + for i := range length { + bytes[i] = d.U8() + } + return bytes, nil + }) + + // Vector - read as uint64 array + c.RegisterVector("U64", func(d *aptosBCS.Deserializer, length uint64) (any, error) { + uint64s := make([]uint64, length) + for i := range length { + uint64s[i] = d.U64() + } + return uint64s, nil + }) + c.RegisterVector("u64", func(d *aptosBCS.Deserializer, length uint64) (any, error) { + uint64s := make([]uint64, length) + for i := range length { + uint64s[i] = d.U64() + } + return uint64s, nil + }) + + // Vector
- read as address array + c.RegisterVector("Address", func(d *aptosBCS.Deserializer, length uint64) (any, error) { + addresses := make([]any, length) + for i := range length { + addressBytesLen := 32 + addresses[i] = d.ReadFixedBytes(addressBytesLen) + } + return addresses, nil + }) + c.RegisterVector("address", func(d *aptosBCS.Deserializer, length uint64) (any, error) { + addresses := make([]any, length) + for i := range length { + addressBytesLen := 32 + addresses[i] = d.ReadFixedBytes(addressBytesLen) + } + return addresses, nil + }) +} + +// RegisterPrimitive registers a handler for a primitive type +func (c *BCSTypeConverter) RegisterPrimitive(typeName string, handler BCSPrimitiveHandler) { + c.primitiveHandlers[typeName] = handler +} + +// RegisterVector registers a handler for a vector type +func (c *BCSTypeConverter) RegisterVector(elementType string, handler BCSVectorHandler) { + c.vectorHandlers[elementType] = handler +} + +// DecodePrimitive decodes a primitive type using the registered handler +func (c *BCSTypeConverter) DecodePrimitive(deserializer *aptosBCS.Deserializer, primitiveType string) (any, error) { + handler, ok := c.primitiveHandlers[primitiveType] + if !ok { + return nil, fmt.Errorf("unsupported primitive type: %s", primitiveType) + } + return handler(deserializer) +} + +// DecodeVector decodes a vector type using the registered handler +func (c *BCSTypeConverter) DecodeVector(deserializer *aptosBCS.Deserializer, elementType string) (any, error) { + length := deserializer.Uleb128() + + handler, ok := c.vectorHandlers[elementType] + if !ok { + // Fall back to generic primitive vector handling + primitiveHandler, primitiveOk := c.primitiveHandlers[elementType] + if !primitiveOk { + return nil, fmt.Errorf("unsupported vector element type: %s", elementType) + } + + // Generic vector of primitives + result := make([]any, length) + for i := range length { + value, err := primitiveHandler(deserializer) + if err != nil { + return nil, fmt.Errorf("failed to decode vector element at index %d: %w", i, err) + } + result[i] = value + } + return result, nil + } + + return handler(deserializer, uint64(length)) +} + +// HasPrimitiveHandler checks if a primitive type handler is registered +func (c *BCSTypeConverter) HasPrimitiveHandler(typeName string) bool { + _, ok := c.primitiveHandlers[typeName] + return ok +} + +// HasVectorHandler checks if a vector type handler is registered +func (c *BCSTypeConverter) HasVectorHandler(elementType string) bool { + _, ok := c.vectorHandlers[elementType] + return ok +} + +// Global BCS converter instance for package-wide use (lazy initialization) +var defaultBCSConverter *BCSTypeConverter + +// getDefaultBCSConverter returns the global BCS converter, initializing it if necessary +func getDefaultBCSConverter() *BCSTypeConverter { + if defaultBCSConverter == nil { + defaultBCSConverter = NewBCSTypeConverter() + } + return defaultBCSConverter +} + +// DecodeBCSPrimitive decodes a BCS primitive type using the default converter +func DecodeBCSPrimitive(deserializer *aptosBCS.Deserializer, primitiveType string) (any, error) { + return getDefaultBCSConverter().DecodePrimitive(deserializer, primitiveType) +} + +// DecodeBCSVector decodes a BCS vector type using the default converter +func DecodeBCSVector(deserializer *aptosBCS.Deserializer, elementType string) (any, error) { + return getDefaultBCSConverter().DecodeVector(deserializer, elementType) +} diff --git a/relayer/codec/bcs_converter_test.go b/codec/bcs_converter_test.go similarity index 100% rename from relayer/codec/bcs_converter_test.go rename to codec/bcs_converter_test.go diff --git a/codec/decoder.go b/codec/decoder.go new file mode 100644 index 00000000..d3c19167 --- /dev/null +++ b/codec/decoder.go @@ -0,0 +1,631 @@ +package codec + +import ( + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "math" + "reflect" + "strconv" + "strings" + + aptosBCS "github.com/aptos-labs/aptos-go-sdk/bcs" + "github.com/block-vision/sui-go-sdk/models" + +) + +const ( + // Bit and byte constants + byteSize = 8 + uint8Bits = 8 + uint64Bits = 64 + bits128 = 128 + bits256 = 256 + + // Number bases + base10 = 10 + base16 = 16 + base2 = 2 + + // Response parsing constants + maxByteValue = 255 + + // proofWireBytes is the fixed on-wire size of each merkle proof element. + proofWireBytes = 32 + + // tokenTransferMinWireBytes is the minimum BCS size of Any2SuiTokenTransfer: + // uleb128(0) source pool + 32 dest token + 4 dest gas + uleb128(0) extra data + 32 amount. + tokenTransferMinWireBytes = 70 +) + +// DecodeSuiJsonValue decodes Sui JSON response data into the provided target. +func DecodeSuiJsonValue(data any, target any) error { + return DecodeJSONReturn(data, target) +} + +// ConvertBase64StringsToHex walks arbitrary JSON-like structures and converts any +// base64-encoded strings into 0x-prefixed hex strings, preserving []byte slices. +func ConvertBase64StringsToHex(data any) any { + switch v := data.(type) { + case nil: + return nil + case string: + // check if the string is entirely numeric + if _, err := strconv.ParseUint(v, 10, 64); err == nil { + return v + } + + decoded, err := base64.StdEncoding.DecodeString(v) + if err == nil && len(decoded) > 0 { + return "0x" + hex.EncodeToString(decoded) + } + return v + case json.RawMessage: + var inner any + if err := json.Unmarshal(v, &inner); err != nil { + return v + } + return ConvertBase64StringsToHex(inner) + case map[string]any: + result := make(map[string]any, len(v)) + for key, value := range v { + result[key] = ConvertBase64StringsToHex(value) + } + return result + case []any: + result := make([]any, len(v)) + for i, value := range v { + result[i] = ConvertBase64StringsToHex(value) + } + return result + default: + rv := reflect.ValueOf(data) + if rv.Kind() == reflect.Slice { + // Preserve []byte and other byte slices as-is + if rv.Type().Elem().Kind() == reflect.Uint8 { + return data + } + + result := make([]any, rv.Len()) + for i := 0; i < rv.Len(); i++ { + result[i] = ConvertBase64StringsToHex(rv.Index(i).Interface()) + } + return result + } + + if rv.Kind() == reflect.Map && rv.Type().Key().Kind() == reflect.String { + result := make(map[string]any, rv.Len()) + iter := rv.MapRange() + for iter.Next() { + result[iter.Key().String()] = ConvertBase64StringsToHex(iter.Value().Interface()) + } + return result + } + } + + return data +} + +// DecodeSuiStructToJSON decodes a Sui struct into a JSON object +// using the normalized struct and the result +func DecodeSuiStructToJSON(normalizedStructs map[string]any, identifier string, bcsDecoder *aptosBCS.Deserializer) (map[string]any, error) { + jsonResult := make(map[string]any) + + normalizedStruct, ok := normalizedStructs[identifier].(map[string]any) + if !ok { + return nil, fmt.Errorf("struct with identifier '%s' not found in normalized structs", identifier) + } + + fields, ok := normalizedStruct["fields"].([]any) + if !ok { + return nil, fmt.Errorf("fields not found for struct '%s'", identifier) + } + + for _, field := range fields { + fieldMap, ok := field.(map[string]any) + if !ok { + continue + } + + fieldName, ok := fieldMap["name"].(string) + if !ok { + continue + } + + fieldType := fieldMap["type"] + + // Handle different field types based on the new format + switch v := fieldType.(type) { + case string: + // Primitive types like "U64", "Bool", "Address" + value, err := getDefaultBCSConverter().DecodePrimitive(bcsDecoder, v) + if err != nil { + return nil, fmt.Errorf("failed to decode primitive field %s: %w", fieldName, err) + } + jsonResult[fieldName] = value + + case map[string]any: + if vectorType, exists := v["Vector"]; exists { + // Vector type + decodedVector, err := decodeVectorField(bcsDecoder, vectorType, normalizedStructs) + if err != nil { + return nil, fmt.Errorf("failed to decode vector field %s: %w", fieldName, err) + } + jsonResult[fieldName] = decodedVector + } else if structType, exists := v["Struct"]; exists { + // Struct type + structMap, ok := structType.(map[string]any) + if !ok { + return nil, fmt.Errorf("invalid struct type for field %s", fieldName) + } + structName, ok := structMap["name"].(string) + if !ok { + return nil, fmt.Errorf("struct name not found for field %s", fieldName) + } + + // Special case for String struct - it's a primitive type in Sui + if structName == "String" { + jsonResult[fieldName] = bcsDecoder.ReadString() + } else { + inner, err := DecodeSuiStructToJSON(normalizedStructs, structName, bcsDecoder) + if err != nil { + return nil, fmt.Errorf("failed to decode struct field %s: %w", fieldName, err) + } + jsonResult[fieldName] = inner + } + } + } + } + + return jsonResult, nil +} + +func decodeVectorField(bcsDecoder *aptosBCS.Deserializer, vectorType any, normalizedStructs map[string]any) (any, error) { + // Read the length of the vector first + vectorLength := bcsDecoder.Uleb128() + + switch v := vectorType.(type) { + case string: + // Try to use the BCS converter for registered vector types + if getDefaultBCSConverter().HasVectorHandler(v) { + // Use the registered vector handler which will handle the length internally + // We need to "rewind" by putting the length back since the handler expects to read it + // Actually, the handler receives the length as a parameter, so we're good + handler, _ := getDefaultBCSConverter().vectorHandlers[v] + return handler(bcsDecoder, uint64(vectorLength)) + } + + // Fall back to generic primitive vector handling + if getDefaultBCSConverter().HasPrimitiveHandler(v) { + primitiveVector := make([]any, vectorLength) + for i := range vectorLength { + value, err := getDefaultBCSConverter().DecodePrimitive(bcsDecoder, v) + if err != nil { + return nil, fmt.Errorf("failed to decode primitive vector element at index %d: %w", i, err) + } + primitiveVector[i] = value + } + return primitiveVector, nil + } + + return nil, fmt.Errorf("unsupported vector element type: %s", v) + + case map[string]any: + if innerVectorType, exists := v["Vector"]; exists { + // This is vector> - recursively decode each inner vector + outerVector := make([]any, vectorLength) + for i := range vectorLength { + innerResult, err := decodeVectorField(bcsDecoder, innerVectorType, normalizedStructs) + if err != nil { + return nil, fmt.Errorf("failed to decode inner vector at index %d: %w", i, err) + } + outerVector[i] = innerResult + } + + return outerVector, nil + } else if structType, exists := v["Struct"]; exists { + // This is vector - decode each struct + structVector := make([]any, vectorLength) + structMap, ok := structType.(map[string]any) + if !ok { + return nil, fmt.Errorf("invalid struct type in vector") + } + structName, ok := structMap["name"].(string) + if !ok { + return nil, fmt.Errorf("struct name not found in vector element") + } + + // this is a special case where strings are defined as a struct in Sui normalized module structs definition + if structName == "String" { + vecOfStrings := make([]any, vectorLength) + for i := range vectorLength { + vecOfStrings[i] = bcsDecoder.ReadString() + } + + return vecOfStrings, nil + } + + for i := range vectorLength { + structResult, err := DecodeSuiStructToJSON(normalizedStructs, structName, bcsDecoder) + if err != nil { + return nil, fmt.Errorf("failed to decode struct at index %d: %w", i, err) + } + structVector[i] = structResult + } + + return structVector, nil + } + } + + return nil, fmt.Errorf("unsupported vector type: %v", vectorType) +} + +func DecodeSuiPrimative(bcsDecoder *aptosBCS.Deserializer, primativeType string) (any, error) { + // Try to decode as primitive using the BCS converter registry + converter := getDefaultBCSConverter() + if converter.HasPrimitiveHandler(primativeType) { + return converter.DecodePrimitive(bcsDecoder, primativeType) + } + + // Handle vector types + if strings.HasPrefix(primativeType, "vector<") && strings.HasSuffix(primativeType, ">") { + innerType := strings.TrimSuffix(strings.TrimPrefix(primativeType, "vector<"), ">") + + // Handle simple vector types + if converter.HasVectorHandler(innerType) || converter.HasPrimitiveHandler(innerType) { + return decodeVectorField(bcsDecoder, innerType, nil) + } + + // Handle nested vector types (e.g., vector>) + if innerType == "vector" || innerType == "vector" { + return decodeVectorField(bcsDecoder, map[string]any{"Vector": "U8"}, nil) + } + } + + return nil, fmt.Errorf("unsupported BCS primitive type: %s", primativeType) +} + +// DecodeVectorOfStructs decodes a vector of structs from BCS bytes +// vectorType should be in format "vector<0xpackage::module::StructName>" +func DecodeVectorOfStructs(bcsDecoder *aptosBCS.Deserializer, vectorType string, normalizedStructs map[string]any) (any, error) { + // Check if it's actually a vector type + if !strings.HasPrefix(vectorType, "vector<") || !strings.HasSuffix(vectorType, ">") { + return nil, fmt.Errorf("not a vector type: %s", vectorType) + } + + // Extract inner type + innerType := strings.TrimSuffix(strings.TrimPrefix(vectorType, "vector<"), ">") + + // Check if inner type is a struct (has 3 parts when split by ::) + structParts := strings.Split(innerType, "::") + if len(structParts) != 3 { + return nil, fmt.Errorf("inner type is not a struct: %s", innerType) + } + + structName := structParts[2] + + // Create vector type definition compatible with decodeVectorField + vectorTypedef := map[string]any{ + "Struct": map[string]any{ + "name": structName, + }, + } + + return decodeVectorField(bcsDecoder, vectorTypedef, normalizedStructs) +} + +// numericToBytes converts a number to byte slice (little-endian) +// Used by type_converters.go +func numericToBytes(num uint64) []byte { + bytes := make([]byte, uint64Bits/uint8Bits) + for i := range uint8Bits { + bytes[i] = byte(num >> (i * uint8Bits)) + } + // Remove trailing zeros + for len(bytes) > 1 && bytes[len(bytes)-1] == 0 { + bytes = bytes[:len(bytes)-1] + } + + return bytes +} + +// AnySliceToBytes converts slice of interface{} to byte slice +func AnySliceToBytes(src []any) ([]byte, error) { + dst := make([]byte, len(src)) + for i, v := range src { + //nolint:exhaustive + switch x := v.(type) { + case uint8: + dst[i] = x + case int: + if x < 0 || x > maxByteValue { + return nil, fmt.Errorf("element %d: int %d out of byte range", i, x) + } + dst[i] = byte(x) + case uint: + if x > maxByteValue { + return nil, fmt.Errorf("element %d: uint %d out of byte range", i, x) + } + dst[i] = byte(x) + case float64: + if x > maxByteValue { + return nil, fmt.Errorf("element %d: float64 %f out of byte range", i, x) + } + dst[i] = byte(x) + default: + return nil, fmt.Errorf("element %d: unsupported type %T", i, v) + } + } + + return dst, nil +} + +// handleSingleFieldStruct processes structs with single fields +// This is kept here for backward compatibility but the main implementation is in type_converters.go +func handleSingleFieldStruct(t reflect.Type, data any, decodeFn func(any, any) error) (any, error) { + field := t.Field(0) + newStructVal := reflect.New(t).Elem() + fieldPtr := newStructVal.Field(0).Addr().Interface() + + if err := decodeFn(data, fieldPtr); err != nil { + return nil, fmt.Errorf("failed decoding for single-field struct %v field %s (%v): %w", + t, field.Name, field.Type, err) + } + + return newStructVal.Interface(), nil +} + +// Overflow checking functions +func overflowFloat(t reflect.Type, x float64) bool { + //nolint:exhaustive + switch t.Kind() { + case reflect.Float32: + return overflowFloat32(x) + case reflect.Float64: + return false + default: + panic("reflect: OverflowFloat of non-float type " + t.String()) + } +} + +func overflowFloat32(x float64) bool { + if x < 0 { + x = -x + } + + return math.MaxFloat32 < x && x <= math.MaxFloat64 +} + +func overflowInt(t reflect.Type, x int64) bool { + //nolint:exhaustive + switch t.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + bitSize := t.Size() * uint8Bits + trunc := (x << (uint64Bits - bitSize)) >> (uint64Bits - bitSize) + + return x != trunc + default: + panic("reflect: OverflowInt of non-int type " + t.String()) + } +} + +func overflowUint(t reflect.Type, x uint64) bool { + //nolint:exhaustive + switch t.Kind() { + case reflect.Uint, reflect.Uintptr, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + bitSize := t.Size() * uint8Bits + trunc := (x << (uint64Bits - bitSize)) >> (uint64Bits - bitSize) + + return x != trunc + default: + panic("reflect: OverflowUint of non-uint type " + t.String()) + } +} + +func DeserializeExecutionReport(data []byte) (*ExecutionReport, error) { + deserializer := aptosBCS.NewDeserializer(data) + + // 1. Read source_chain_selector (u64) + sourceChainSelector := deserializer.U64() + if err := deserializer.Error(); err != nil { + return nil, fmt.Errorf("failed to deserialize sourceChainSelector: %w", err) + } + + // 2. Read message header + messageID := make([]byte, 32) + deserializer.ReadFixedBytesInto(messageID) + if err := deserializer.Error(); err != nil { + return nil, fmt.Errorf("failed to deserialize messageID: %w", err) + } + + headerSourceChain := deserializer.U64() + if err := deserializer.Error(); err != nil { + return nil, fmt.Errorf("failed to deserialize headerSourceChain: %w", err) + } + + destChainSelector := deserializer.U64() + if err := deserializer.Error(); err != nil { + return nil, fmt.Errorf("failed to deserialize destChainSelector: %w", err) + } + + sequenceNumber := deserializer.U64() + if err := deserializer.Error(); err != nil { + return nil, fmt.Errorf("failed to deserialize sequenceNumber: %w", err) + } + + nonce := deserializer.U64() + if err := deserializer.Error(); err != nil { + return nil, fmt.Errorf("failed to deserialize nonce: %w", err) + } + + if sourceChainSelector != headerSourceChain { + return nil, fmt.Errorf("source chain selector mismatch: %d != %d", sourceChainSelector, headerSourceChain) + } + + header := RampMessageHeader{ + MessageID: messageID, + SourceChainSelector: headerSourceChain, + DestChainSelector: destChainSelector, + SequenceNumber: sequenceNumber, + Nonce: nonce, + } + + // 3. Read sender (vector) + sender := deserializer.ReadBytes() + if err := deserializer.Error(); err != nil { + return nil, fmt.Errorf("failed to deserialize sender: %w", err) + } + + // 4. Read data (vector) + msgData := deserializer.ReadBytes() + if err := deserializer.Error(); err != nil { + return nil, fmt.Errorf("failed to deserialize data: %w", err) + } + + // 5. Read receiver (address) + receiver := deserializer.ReadFixedBytes(32) + if err := deserializer.Error(); err != nil { + return nil, fmt.Errorf("failed to deserialize receiver: %w", err) + } + + // 6. Read gas_limit (u256) + gasLimit := deserializer.U256() + if err := deserializer.Error(); err != nil { + return nil, fmt.Errorf("failed to deserialize gas_limit: %w", err) + } + + tokenReceiver := [32]byte{} + deserializer.ReadFixedBytesInto(tokenReceiver[:]) + if err := deserializer.Error(); err != nil { + return nil, fmt.Errorf("failed to deserialize tokenReceiver: %w", err) + } + + // 7. Read token_amounts vector + tokenAmountsLen := deserializer.Uleb128() + if err := deserializer.Error(); err != nil { + return nil, fmt.Errorf("failed to deserialize token_amounts length: %w", err) + } + remaining := deserializer.Remaining() + if remaining < 0 || uint64(tokenAmountsLen)*tokenTransferMinWireBytes > uint64(remaining) { + return nil, fmt.Errorf("failed to deserialize execution report: token_amounts length %d exceeds remaining %d bytes", tokenAmountsLen, remaining) + } + tokenAmounts := make([]Any2SuiTokenTransfer, tokenAmountsLen) + + for i := range tokenAmountsLen { + sourcePoolAddr := deserializer.ReadBytes() + if err := deserializer.Error(); err != nil { + return nil, fmt.Errorf("failed to deserialize sourcePoolAddr: %w", err) + } + + destToken := deserializer.ReadFixedBytes(32) + if err := deserializer.Error(); err != nil { + return nil, fmt.Errorf("failed to deserialize destToken: %w", err) + } + + destGas := deserializer.U32() + if err := deserializer.Error(); err != nil { + return nil, fmt.Errorf("failed to deserialize destGas: %w", err) + } + extraData := deserializer.ReadBytes() + if err := deserializer.Error(); err != nil { + return nil, fmt.Errorf("failed to deserialize extraData: %w", err) + } + amount := deserializer.U256() + if err := deserializer.Error(); err != nil { + return nil, fmt.Errorf("failed to deserialize amount: %w", err) + } + + tokenAmounts[i] = Any2SuiTokenTransfer{ + SourcePoolAddress: sourcePoolAddr, + DestTokenAddress: models.SuiAddress(hex.EncodeToString(destToken)), + DestGasAmount: destGas, + ExtraData: extraData, + Amount: &amount, + } + } + + message := Any2SuiRampMessage{ + Header: header, + Sender: sender, + Data: msgData, + Receiver: models.SuiAddress(hex.EncodeToString(receiver)), + GasLimit: &gasLimit, + TokenReceiver: models.SuiAddressBytes(tokenReceiver), + TokenAmounts: tokenAmounts, + } + + // 8. Read offchain_token_data (vector>) + offchainDataLen := deserializer.Uleb128() + if err := deserializer.Error(); err != nil { + return nil, fmt.Errorf("failed to deserialize offchain_token_data length: %w", err) + } + if int(offchainDataLen) > deserializer.Remaining() { + return nil, fmt.Errorf("failed to deserialize execution report: offchain_token_data length %d exceeds remaining %d bytes", offchainDataLen, deserializer.Remaining()) + } + offchainData := make([][]byte, offchainDataLen) + + for i := range offchainDataLen { + offchainData[i] = deserializer.ReadBytes() + if err := deserializer.Error(); err != nil { + return nil, fmt.Errorf("failed to deserialize offchainData at index %d: %w", i, err) + } + } + + // 9. Read proofs (vector>) + proofsLen := deserializer.Uleb128() + if err := deserializer.Error(); err != nil { + return nil, fmt.Errorf("failed to deserialize proofs length: %w", err) + } + remaining = deserializer.Remaining() + if remaining < 0 || uint64(proofsLen)*proofWireBytes > uint64(remaining) { + return nil, fmt.Errorf("failed to deserialize execution report: proofs length %d exceeds remaining %d bytes", proofsLen, remaining) + } + proofs := make([][]byte, proofsLen) + + for i := range proofsLen { + proofs[i] = deserializer.ReadFixedBytes(proofWireBytes) + if err := deserializer.Error(); err != nil { + return nil, fmt.Errorf("failed to deserialize proof at index %d: %w", i, err) + } + } + + if err := deserializer.Error(); err != nil { + return nil, fmt.Errorf("failed to deserialize execution report: %w", err) + } + + return &ExecutionReport{ + SourceChainSelector: sourceChainSelector, + Message: message, + OffchainTokenData: offchainData, + Proofs: proofs, + }, nil +} + +// UnwrapBCSPureBytes decodes a BCS-encoded pure input value stored on-chain. +// Pure vector arguments are stored with a ULEB128 length prefix. +func UnwrapBCSPureBytes(pure []byte) ([]byte, error) { + if len(pure) == 0 { + return nil, errors.New("pure bytes are empty") + } + + deserializer := aptosBCS.NewDeserializer(pure) + unwrapped := deserializer.ReadBytes() + if err := deserializer.Error(); err != nil { + return nil, fmt.Errorf("failed to unwrap pure bytes: %w", err) + } + + return unwrapped, nil +} + +// DeserializeExecutionReportFromPure deserializes an execution report from a +// BCS-encoded pure input containing vector. +func DeserializeExecutionReportFromPure(pure []byte) (*ExecutionReport, error) { + reportBytes, err := UnwrapBCSPureBytes(pure) + if err != nil { + return nil, err + } + + return DeserializeExecutionReport(reportBytes) +} diff --git a/relayer/codec/decoder_test.go b/codec/decoder_test.go similarity index 99% rename from relayer/codec/decoder_test.go rename to codec/decoder_test.go index d31d2483..eead6744 100644 --- a/relayer/codec/decoder_test.go +++ b/codec/decoder_test.go @@ -18,8 +18,6 @@ import ( aptosBCS "github.com/aptos-labs/aptos-go-sdk/bcs" - "github.com/smartcontractkit/chainlink-sui/shared" - "github.com/stretchr/testify/require" ) @@ -895,12 +893,12 @@ func TestDecodeBase64(t *testing.T) { testData := []byte("Hello, World!") encoded := base64.StdEncoding.EncodeToString(testData) - decoded, err := shared.DecodeBase64(encoded) + decoded, err := base64.StdEncoding.DecodeString(encoded) require.NoError(t, err) require.Equal(t, testData, decoded) // Test invalid base64 - _, err = shared.DecodeBase64("invalid_base64!!!") + _, err = base64.StdEncoding.DecodeString("invalid_base64!!!") require.Error(t, err) } diff --git a/codec/encoder.go b/codec/encoder.go new file mode 100644 index 00000000..ac520d46 --- /dev/null +++ b/codec/encoder.go @@ -0,0 +1,367 @@ +package codec + +import ( + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "math/big" + "reflect" + "strconv" + "strings" + +) + +const ( + // Type bounds + maxUint8 = 255 + maxUint16 = 65535 + maxUint32 = 4294967295 +) + +// Safe conversion functions to avoid lint issues +func safeUint8(val uint64) (uint8, error) { + if val > maxUint8 { + return 0, fmt.Errorf("value %d exceeds uint8 maximum", val) + } + + return uint8(val), nil +} + +func safeUint16(val uint64) (uint16, error) { + if val > maxUint16 { + return 0, fmt.Errorf("value %d exceeds uint16 maximum", val) + } + + return uint16(val), nil +} + +func safeUint32(val uint64) (uint32, error) { + if val > maxUint32 { + return 0, fmt.Errorf("value %d exceeds uint32 maximum", val) + } + + return uint32(val), nil +} + +func EncodeToSuiValue(typeName string, value any) (any, error) { + // Normalize fully-qualified Sui string type to logical "string" + if strings.Contains(typeName, "::string::String") { + typeName = "string" + } + + switch typeName { + case "address": + return encodeAddress(value) + case "object_id": + return encodeObject(value) + case "u8", "u16", "u32", "u64", "u128", "u256": + return encodeUint(typeName, value) + case "bool": + return encodeBool(value) + case "string": + return encodeString(value) + default: + if strings.HasPrefix(typeName, "0x") && strings.Contains(typeName, "::") { + // TODO: need to use go-bsc to encode this. Reference https://github.com/fardream/go-bcs/blob/main/bcs/encode_test.go + return nil, errors.New("struct types are not supported") + } + if strings.HasPrefix(typeName, "vector<") && strings.HasSuffix(typeName, ">") { + return encodeVector(typeName, value) + } + + return nil, fmt.Errorf("unsupported type: %s", typeName) + } +} + +func encodeObject(value any) (Object, error) { + switch v := value.(type) { + case Object: + return Object{Id: v.Id}, nil + default: + return Object{}, fmt.Errorf("cannot convert %T to object", value) + } +} + +func encodeAddress(value any) (string, error) { + switch v := value.(type) { + case string: + // Ensure it's a valid Sui address format + if !strings.HasPrefix(v, "0x") { + v = "0x" + v + } + + normalizedAddress, err := ToSuiAddress(v) + if err != nil { + return "", fmt.Errorf("failed to convert address to Sui address: %w", err) + } + + return normalizedAddress, nil + case []byte: + stringAddr := "0x" + hex.EncodeToString(v) + + normalizedAddress, err := ToSuiAddress(stringAddr) + if err != nil { + return "", fmt.Errorf("failed to convert address to Sui address: %w", err) + } + + return normalizedAddress, nil + default: + return "", fmt.Errorf("cannot convert %T to address", value) + } +} + +func encodeUint(typeName string, value any) (any, error) { + // First convert to a common intermediate type + var baseValue uint64 + var bigIntValue *big.Int + + switch v := value.(type) { + case int: + if v < 0 { + return nil, fmt.Errorf("cannot convert negative int %d to %s", v, typeName) + } + baseValue = uint64(v) + case int64: + if v < 0 { + return nil, fmt.Errorf("cannot convert negative int %d to %s", v, typeName) + } + baseValue = uint64(v) + case uint: + baseValue = uint64(v) + case uint64: + baseValue = v + case uint32: + baseValue = uint64(v) + case uint16: + baseValue = uint64(v) + case uint8: + baseValue = uint64(v) + case float64: + if v < 0 { + return nil, fmt.Errorf("cannot convert negative float %f to %s", v, typeName) + } + baseValue = uint64(v) + case json.Number: + // Handle JSON numbers properly + numStr := v.String() + if typeName == "u128" || typeName == "u256" { + bigIntValue = new(big.Int) + if _, ok := bigIntValue.SetString(numStr, base10); !ok { + return nil, fmt.Errorf("cannot convert json.Number %s to %s", v, typeName) + } + } else if strings.Contains(numStr, ".") { + f, err := v.Float64() + if err != nil { + return nil, fmt.Errorf("cannot convert json.Number %s to %s: %w", v, typeName, err) + } + baseValue = uint64(f) + } else { + // json parsing int64 for selector was going out of range + i, err := strconv.ParseUint(numStr, base10, 64) + if err != nil { + return nil, fmt.Errorf("cannot convert json.Number %s to %s: %w", v, typeName, err) + } + //nolint:gosec + // we assume safe conversion without negative numbers + baseValue = uint64(i) + } + case string: + // Handle big numbers that might come as strings + if typeName == "u128" || typeName == "u256" { + bigIntValue = new(big.Int) + _, ok := bigIntValue.SetString(v, base10) + if !ok { + return nil, fmt.Errorf("cannot convert string %s to %s", v, typeName) + } + } else { + i, err := strconv.ParseUint(v, base10, 64) + if err != nil { + return nil, fmt.Errorf("cannot convert string %s to %s: %w", v, typeName, err) + } + baseValue = i + } + case *big.Int: + bigIntValue = v + default: + return nil, fmt.Errorf("cannot convert %T to %s", value, typeName) + } + + // Now convert to the appropriate type based on typeName + switch typeName { + case "u8": + if bigIntValue != nil { + if !bigIntValue.IsUint64() || bigIntValue.Uint64() > maxUint8 { + return nil, fmt.Errorf("value %s too large for u8", bigIntValue.String()) + } + // Safe conversion after bounds check + return safeUint8(bigIntValue.Uint64()) + } + if baseValue > maxUint8 { + return nil, fmt.Errorf("value %d too large for u8", baseValue) + } + + return safeUint8(baseValue) + case "u16": + if bigIntValue != nil { + if !bigIntValue.IsUint64() || bigIntValue.Uint64() > maxUint16 { + return nil, fmt.Errorf("value %s too large for u16", bigIntValue.String()) + } + // Safe conversion after bounds check + return safeUint16(bigIntValue.Uint64()) + } + if baseValue > maxUint16 { + return nil, fmt.Errorf("value %d too large for u16", baseValue) + } + + return safeUint16(baseValue) + case "u32": + if bigIntValue != nil { + if !bigIntValue.IsUint64() || bigIntValue.Uint64() > maxUint32 { + return nil, fmt.Errorf("value %s too large for u32", bigIntValue.String()) + } + // Safe conversion after bounds check + return safeUint32(bigIntValue.Uint64()) + } + if baseValue > maxUint32 { + return nil, fmt.Errorf("value %d too large for u32", baseValue) + } + + return safeUint32(baseValue) + case "u64": + if bigIntValue != nil { + if !bigIntValue.IsUint64() { + return nil, fmt.Errorf("value %s too large for u64", bigIntValue.String()) + } + + return bigIntValue.Uint64(), nil + } + + return baseValue, nil + case "u128": + if bigIntValue != nil { + // Validate it fits in u128 (2^128 - 1) + maxVal := new(big.Int) + maxVal.Exp(big.NewInt(base2), big.NewInt(bits128), nil) + maxVal.Sub(maxVal, big.NewInt(1)) + if bigIntValue.Cmp(maxVal) > 0 { + return nil, fmt.Errorf("value %s too large for u128", bigIntValue.String()) + } + + return bigIntValue.String(), nil + } + + return strconv.FormatUint(baseValue, base10), nil + case "u256": + if bigIntValue != nil { + // Validate it fits in u256 (2^256 - 1) + maxVal := new(big.Int) + maxVal.Exp(big.NewInt(base2), big.NewInt(bits256), nil) + maxVal.Sub(maxVal, big.NewInt(1)) + if bigIntValue.Cmp(maxVal) > 0 { + return nil, fmt.Errorf("value %s too large for u256", bigIntValue.String()) + } + + return bigIntValue.String(), nil + } + + return strconv.FormatUint(baseValue, base10), nil + default: + return nil, fmt.Errorf("unsupported uint type: %s", typeName) + } +} + +func encodeBool(value any) (bool, error) { + switch v := value.(type) { + case bool: + return v, nil + case string: + b, err := strconv.ParseBool(v) + if err != nil { + return false, fmt.Errorf("cannot convert string %s to bool: %w", v, err) + } + + return b, nil + case int: + return v != 0, nil + case float64: + return v != 0, nil + default: + return false, fmt.Errorf("cannot convert %T to bool", value) + } +} + +func encodeString(value any) (string, error) { + switch v := value.(type) { + case string: + return v, nil + case []byte: + return string(v), nil + default: + return "", fmt.Errorf("cannot convert %T to string", value) + } +} + +func encodeVector(typeName string, value any) ([]any, error) { + // Extract the inner type, e.g., "vector" -> "string" + if !strings.HasPrefix(typeName, "vector<") || !strings.HasSuffix(typeName, ">") { + return nil, fmt.Errorf("invalid vector type: %s", typeName) + } + innerType := typeName[len("vector<") : len(typeName)-1] + + if value == nil { + return nil, fmt.Errorf("nil value cannot be encoded in encodeVector") + } + rv := reflect.ValueOf(value) + + if !rv.IsValid() { + return nil, fmt.Errorf("invalid reflect value for vector type %s in encodeVector", typeName) + } + kind := rv.Kind() + + // Use reflection to ensure 'value' is a slice or array + if kind != reflect.Slice && kind != reflect.Array { + return nil, fmt.Errorf("expected a slice/array for vector type %s, got %T", typeName, value) + } + + // Special handling for vector (byte arrays) + if innerType == "u8" { + // Handle []any from JSON unmarshaling + if interfaceSlice, ok := value.([]any); ok { + bytes := make([]byte, len(interfaceSlice)) + for i, item := range interfaceSlice { + if num, ok := item.(float64); ok { + if num < 0 || num > maxUint8 { + return nil, fmt.Errorf("invalid byte value at index %d: %f", i, num) + } + bytes[i] = byte(num) + } else if num, ok := item.(int); ok { + if num < 0 || num > maxUint8 { + return nil, fmt.Errorf("invalid byte value at index %d: %d", i, num) + } + bytes[i] = byte(num) + } else { + return nil, fmt.Errorf("invalid byte value at index %d: %T", i, item) + } + } + + return []any{bytes}, nil + } + // Handle []byte directly + if bytes, ok := value.([]byte); ok { + return []any{bytes}, nil + } + } + + encodedElements := make([]any, 0, rv.Len()) + for i := range rv.Len() { + elem := rv.Index(i).Interface() + encodedElem, err := EncodeToSuiValue(innerType, elem) + if err != nil { + return nil, fmt.Errorf("failed to encode element at index %d: %w", i, err) + } + encodedElements = append(encodedElements, encodedElem) + } + + return encodedElements, nil +} diff --git a/codec/go.mod b/codec/go.mod new file mode 100644 index 00000000..dd092425 --- /dev/null +++ b/codec/go.mod @@ -0,0 +1,54 @@ +module github.com/smartcontractkit/chainlink-sui/codec + +go 1.26.2 + +require ( + github.com/aptos-labs/aptos-go-sdk v1.13.0 + github.com/block-vision/sui-go-sdk v1.2.1 + github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260428085939-5c70de12dbfc + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260506120607-7f10be016c89 + github.com/stretchr/testify v1.11.1 +) + +require ( + filippo.io/edwards25519 v1.1.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/coder/websocket v1.8.14 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/ethereum/go-ethereum v1.17.3 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hasura/go-graphql-client v0.15.1 // indirect + github.com/hdevalence/ed25519consensus v0.2.0 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/pelletier/go-toml/v2 v2.3.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/smartcontractkit/chain-selectors v1.0.102 // indirect + github.com/smartcontractkit/libocr v0.0.0-20260304194147-a03701e2c02e // indirect + github.com/stretchr/objx v0.5.3 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.2.0 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.28.0 // indirect + golang.org/x/crypto v0.52.0 // indirect + golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/time v0.15.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/grpc v1.81.1 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/codec/go.sum b/codec/go.sum new file mode 100644 index 00000000..ff68b765 --- /dev/null +++ b/codec/go.sum @@ -0,0 +1,130 @@ +filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= +filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/aptos-labs/aptos-go-sdk v1.13.0 h1:epv7K/tIbAEO2RfogwGacICBig8rrigJj24fDsy6KTg= +github.com/aptos-labs/aptos-go-sdk v1.13.0/go.mod h1:FTgKp0RLfEefllCdkCj0jPU14xWk11yA7SFVfCDLUj8= +github.com/block-vision/sui-go-sdk v1.2.1 h1:uwvGbzfcrS4SsIaakclYxy0qgEF1XWIUtTYWXB4PoAw= +github.com/block-vision/sui-go-sdk v1.2.1/go.mod h1:t8mWASwfyv+EyqHGO9ZrcDiCJWGOFEXqq50TMJ8GQco= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= +github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI= +github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0= +github.com/cucumber/godog v0.15.1 h1:rb/6oHDdvVZKS66hrhpjFQFHjthFSrQBCOI1LwshNTI= +github.com/cucumber/godog v0.15.1/go.mod h1:qju+SQDewOljHuq9NSM66s0xEhogx0q30flfxL4WUk8= +github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI= +github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/ethereum/go-ethereum v1.17.3 h1:Ev/sQHH+UdKZHWjuVzhu2pxhi/sXaPZl23Q+Q5LDd4Q= +github.com/ethereum/go-ethereum v1.17.3/go.mod h1:f2EhRwqewIZkGoQekywI2Y2RZAMTSavLNkD9qItFy1A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-memdb v1.3.5 h1:b3taDMxCBCBVgyRrS1AZVHO14ubMYZB++QpNhBg+Nyo= +github.com/hashicorp/go-memdb v1.3.5/go.mod h1:8IVKKBkVe+fxFgdFOYxzQQNjz+sWCyHCdIC/+5+Vy1Y= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hasura/go-graphql-client v0.15.1 h1:mCb5I+8Bk3FU3GKWvf/zDXkTh7FbGlqJmP3oisBdnN8= +github.com/hasura/go-graphql-client v0.15.1/go.mod h1:jfSZtBER3or+88Q9vFhWHiFMPppfYILRyl+0zsgPIIw= +github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= +github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 h1:BpfhmLKZf+SjVanKKhCgf3bg+511DmU9eDQTen7LLbY= +github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= +github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/smartcontractkit/chain-selectors v1.0.102 h1:qYP4+72HfvogCHR5ymwRFee36WH77514ZBj299SVCBA= +github.com/smartcontractkit/chain-selectors v1.0.102/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260428085939-5c70de12dbfc h1:Um9FBcf0JNSFuGbxgccDG1vM3cNrMGy0SdJ7r6VbX0o= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260428085939-5c70de12dbfc/go.mod h1:zfE2R7887kiwXkGTHKPe5NBgwhFwIC3pnA2uAxrbvig= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260506120607-7f10be016c89 h1:5z3LQ27MJmhiaeqp9S2TzbF5Wm4GGvUKAYOtE9AauR8= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260506120607-7f10be016c89/go.mod h1:G2AII0QmWzXx8Ag9IKnGN3h/gwwNnhHUOCviJievdvo= +github.com/smartcontractkit/libocr v0.0.0-20260304194147-a03701e2c02e h1:poXTj5cFVM6XfC4HICIDYkDVc/A6OYB0eeID0wU2JQE= +github.com/smartcontractkit/libocr v0.0.0-20260304194147-a03701e2c02e/go.mod h1:PLdNK6GlqfxIWXzziPkU7dCAVlVFeYkyyW7AQY0R+4Q= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= +github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= +golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/codec/json_decoder.go b/codec/json_decoder.go new file mode 100644 index 00000000..b6312d05 --- /dev/null +++ b/codec/json_decoder.go @@ -0,0 +1,94 @@ +package codec + +import ( + "encoding/json" + "errors" + "fmt" + "math/big" + "reflect" + "strings" + + "github.com/mitchellh/mapstructure" +) + +// DecodeJSONReturn decodes gRPC/JSON Move return values into the provided target. +func DecodeJSONReturn(data any, target any) error { + if target == nil { + return errors.New("target cannot be nil") + } + + if raw, ok := data.(json.RawMessage); ok { + var intermediate any + if err := json.Unmarshal(raw, &intermediate); err != nil { + return fmt.Errorf("json unmarshal failed: %w", err) + } + + return DecodeJSONReturn(intermediate, target) + } + + if reflect.TypeOf(data) == reflect.TypeOf(target).Elem() { + reflect.ValueOf(target).Elem().Set(reflect.ValueOf(data)) + return nil + } + + targetType := reflect.TypeOf(target).Elem() + + bigPtrT := reflect.TypeOf((*big.Int)(nil)) + bigValT := bigPtrT.Elem() + if targetType == bigValT || targetType == bigPtrT { + return decodeBigInt(data, target) + } + + return decodeWithMapstructure(data, target) +} + +func decodeBigInt(data any, target any) error { + str, ok := data.(string) + if !ok { + return fmt.Errorf("big.Int decode: expected string, got %T", data) + } + + bi, success := new(big.Int).SetString(str, 10) + if !success { + return fmt.Errorf("big.Int decode: invalid number %q", str) + } + + targetValue := reflect.ValueOf(target).Elem() + targetType := targetValue.Type() + bigPtrT := reflect.TypeOf((*big.Int)(nil)) + bigValT := bigPtrT.Elem() + + if targetType == bigValT { + targetValue.Set(reflect.ValueOf(*bi)) + } else { + targetValue.Set(reflect.ValueOf(bi)) + } + + return nil +} + +func decodeWithMapstructure(data any, target any) error { + config := &mapstructure.DecoderConfig{ + DecodeHook: mapstructure.ComposeDecodeHookFunc( + UnifiedTypeConverterHook, + mapstructure.StringToTimeDurationHookFunc(), + ), + Result: target, + WeaklyTypedInput: true, + TagName: "json", + MatchName: fuzzyFieldMatcher, + } + + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return fmt.Errorf("failed to create decoder: %w", err) + } + + return decoder.Decode(data) +} + +func fuzzyFieldMatcher(mapKey, fieldName string) bool { + mk := strings.ReplaceAll(mapKey, "_", "") + fn := strings.ReplaceAll(fieldName, "_", "") + return strings.EqualFold(mk, fn) +} diff --git a/codec/loop/loop_reader.go b/codec/loop/loop_reader.go new file mode 100644 index 00000000..d925d560 --- /dev/null +++ b/codec/loop/loop_reader.go @@ -0,0 +1,191 @@ +package loop + +import ( + "context" + "encoding/json" + "fmt" + "reflect" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-common/pkg/types/query" + "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives" + + "github.com/smartcontractkit/chainlink-aptos/relayer/chainreader/loop" + + "github.com/smartcontractkit/chainlink-sui/codec" +) + +const ( + READ_COMPONENTS_COUNT = 3 +) + +func NewLoopChainReader(log logger.Logger, reader types.ContractReader) types.ContractReader { + lrLogger := logger.Named(log, "LoopChainReader") + return &loopChainReader{logger: lrLogger, reader: reader, moduleAddresses: map[string]string{}} +} + +type loopChainReader struct { + services.Service + types.UnimplementedContractReader + logger logger.Logger + reader types.ContractReader + moduleAddresses map[string]string +} + +func (s *loopChainReader) Name() string { + return s.reader.Name() +} + +func (s *loopChainReader) Ready() error { + return s.reader.Ready() +} + +func (s *loopChainReader) HealthReport() map[string]error { + return s.reader.HealthReport() +} + +func (s *loopChainReader) Start(ctx context.Context) error { + return s.reader.Start(ctx) +} + +func (s *loopChainReader) Close() error { + return s.reader.Close() +} + +func (s *loopChainReader) GetLatestValue(ctx context.Context, readIdentifier string, confidenceLevel primitives.ConfidenceLevel, params, returnVal any) error { + convertedResult := []byte{} + + s.logger.Debugw("GetLatestValue params before serialization over LOOP", "params", params) + + jsonParamBytes, err := json.Marshal(params) + if err != nil { + return fmt.Errorf("failed to marshal params: %+w", err) + } + + err = s.reader.GetLatestValue(ctx, readIdentifier, confidenceLevel, &jsonParamBytes, &convertedResult) + if err != nil { + return fmt.Errorf("failed to call GetLatestValue over LOOP: %w", err) + } + + err = s.decodeGLVReturnValue(readIdentifier, convertedResult, returnVal) + if err != nil { + return fmt.Errorf("failed to decode GetLatestValue return value: %w", err) + } + + return nil +} + +func (s *loopChainReader) BatchGetLatestValues(ctx context.Context, request types.BatchGetLatestValuesRequest) (types.BatchGetLatestValuesResult, error) { + convertedRequest := types.BatchGetLatestValuesRequest{} + for contract, requestBatch := range request { + convertedBatch := []types.BatchRead{} + for _, read := range requestBatch { + jsonParamBytes, err := json.Marshal(read.Params) + if err != nil { + return nil, fmt.Errorf("failed to marshal params: %+w", err) + } + convertedBatch = append(convertedBatch, types.BatchRead{ + ReadName: read.ReadName, + Params: jsonParamBytes, + ReturnVal: &[]byte{}, + }) + } + convertedRequest[contract] = convertedBatch + } + + result, err := s.reader.BatchGetLatestValues(ctx, convertedRequest) + if err != nil { + return nil, err + } + + convertedResult := types.BatchGetLatestValuesResult{} + for contract, resultBatch := range result { + requestBatch := request[contract] + convertedBatch := []types.BatchReadResult{} + for i, result := range resultBatch { + read := requestBatch[i] + resultValue, resultError := result.GetResult() + convertedResult := types.BatchReadResult{ReadName: result.ReadName} + if resultError == nil { + resultPointer := resultValue.(*[]byte) + err := s.decodeGLVReturnValue(result.ReadName, *resultPointer, read.ReturnVal) + if err != nil { + resultError = fmt.Errorf("failed to decode BatchGetLatestValue return value: %w", err) + } + } + convertedResult.SetResult(read.ReturnVal, resultError) + convertedBatch = append(convertedBatch, convertedResult) + } + convertedResult[contract] = convertedBatch + } + + s.logger.Debugw("BatchGetLatestValues result", "result", convertedResult) + + return convertedResult, nil +} + +func (s *loopChainReader) QueryKey(ctx context.Context, contract types.BoundContract, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType any) ([]types.Sequence, error) { + convertedExpressions, err := loop.SerializeExpressions(filter.Expressions) + if err != nil { + return nil, fmt.Errorf("failed to serialize QueryKey expressions: %w", err) + } + + s.logger.Debugw("QueryKey expressions after serialization over LOOP", "expressions", convertedExpressions) + + convertedFilter := query.KeyFilter{ + Key: filter.Key, + Expressions: convertedExpressions, + } + + sequences, err := s.reader.QueryKey(ctx, contract, convertedFilter, limitAndSort, &[]byte{}) + if err != nil { + return nil, fmt.Errorf("failed to call QueryKey over LOOP: %w", err) + } + + for i, sequence := range sequences { + jsonBytes := sequence.Data.(*[]byte) + jsonData := map[string]any{} + err := json.Unmarshal(*jsonBytes, &jsonData) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal LOOP sourced JSON event data (`%s`): %w", string(*jsonBytes), err) + } + + eventData := reflect.New(reflect.TypeOf(sequenceDataType).Elem()).Interface() + err = codec.DecodeSuiJsonValue(jsonData, eventData) + if err != nil { + return nil, fmt.Errorf("failed to decode LOOP sourced event data (`%s`) into a Sui value: %+w", string(*jsonBytes), err) + } + + sequences[i].Data = eventData + } + + return sequences, nil +} + +func (s *loopChainReader) Bind(ctx context.Context, bindings []types.BoundContract) error { + return s.reader.Bind(ctx, bindings) +} + +func (s *loopChainReader) Unbind(ctx context.Context, bindings []types.BoundContract) error { + // we ignore unbind errors, because if the LOOP plugin restarted, the binding would not exist. + _ = s.reader.Unbind(ctx, bindings) + + return nil +} + +func (s *loopChainReader) decodeGLVReturnValue(label string, jsonBytes []byte, returnVal any) error { + var result any + err := json.Unmarshal(jsonBytes, &result) + if err != nil { + return fmt.Errorf("failed to unmarshal %s GetLatestValue result (`%s`): %w", label, string(jsonBytes), err) + } + + err = codec.DecodeSuiJsonValue(result, returnVal) + if err != nil { + return fmt.Errorf("failed to decode %s GetLatestValue JSON value (`%s`) to %T: %w", label, string(jsonBytes), returnVal, err) + } + + return nil +} diff --git a/codec/loop/loop_reader_test.go b/codec/loop/loop_reader_test.go new file mode 100644 index 00000000..37b89f54 --- /dev/null +++ b/codec/loop/loop_reader_test.go @@ -0,0 +1,429 @@ +//go:build unit + +package loop + +import ( + "context" + "encoding/json" + "errors" + "testing" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-common/pkg/types/query" + "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +// mockContractReader is a mock implementation of types.ContractReader for testing. +// It simulates the LOOP boundary behavior where params and return values are []byte. +type mockContractReader struct { + mock.Mock + types.UnimplementedContractReader +} + +func (m *mockContractReader) Name() string { + args := m.Called() + return args.String(0) +} + +func (m *mockContractReader) GetLatestValue(ctx context.Context, readIdentifier string, confidenceLevel primitives.ConfidenceLevel, params, returnVal any) error { + args := m.Called(ctx, readIdentifier, confidenceLevel, params, returnVal) + + switch len(args) { + case 0: + return nil + case 1: + ret := args.Get(0) + if ret == nil { + return nil + } + if err, ok := ret.(error); ok { + return err + } + if rv, ok := returnVal.(*[]byte); ok { + *rv = ret.([]byte) + } + return nil + default: + if ret := args.Get(0); ret != nil { + if rv, ok := returnVal.(*[]byte); ok { + *rv = ret.([]byte) + } + } + return args.Error(1) + } +} + +func (m *mockContractReader) BatchGetLatestValues(ctx context.Context, request types.BatchGetLatestValuesRequest) (types.BatchGetLatestValuesResult, error) { + args := m.Called(ctx, request) + return args.Get(0).(types.BatchGetLatestValuesResult), args.Error(1) +} + +func (m *mockContractReader) QueryKey(ctx context.Context, contract types.BoundContract, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType any) ([]types.Sequence, error) { + args := m.Called(ctx, contract, filter, limitAndSort, sequenceDataType) + + // Simulate LOOP boundary: sequenceDataType is *[]byte, we return mock data + if ret := args.Get(0); ret != nil { + return ret.([]types.Sequence), nil + } + + return nil, args.Error(1) +} + +func (m *mockContractReader) Bind(ctx context.Context, bindings []types.BoundContract) error { + args := m.Called(ctx, bindings) + return args.Error(0) +} + +func (m *mockContractReader) Unbind(ctx context.Context, bindings []types.BoundContract) error { + args := m.Called(ctx, bindings) + return args.Error(0) +} + +func (m *mockContractReader) Start(ctx context.Context) error { + args := m.Called(ctx) + return args.Error(0) +} + +func (m *mockContractReader) Close() error { + args := m.Called() + return args.Error(0) +} + +func (m *mockContractReader) Ready() error { + args := m.Called() + return args.Error(0) +} + +func (m *mockContractReader) HealthReport() map[string]error { + args := m.Called() + return args.Get(0).(map[string]error) +} + +// TestLoopChainReader_GetLatestValue_VerifiesLOOPBoundary tests that the LoopChainReader +// properly serializes params to []byte and deserializes results using DecodeSuiJsonValue. +func TestLoopChainReader_GetLatestValue_VerifiesLOOPBoundary(t *testing.T) { + t.Parallel() + log := logger.Test(t) + + mockReader := new(mockContractReader) + loopReader := NewLoopChainReader(log, mockReader) + + ctx := context.Background() + readIdentifier := "test-contract-echo_u64" + params := map[string]any{"val": uint64(42)} + + // Expected JSON params after serialization + expectedParams, err := json.Marshal(params) + require.NoError(t, err) + + // Mock return value (simulating what comes back from the LOOP boundary) + mockReturn := json.RawMessage("42") + + // Set up expectation: GetLatestValue receives []byte params + mockReader.On("GetLatestValue", + ctx, + readIdentifier, + primitives.Finalized, + mock.AnythingOfType("*[]uint8"), // params as *[]byte + mock.AnythingOfType("*[]uint8"), // returnVal as *[]byte + ).Run(func(args mock.Arguments) { + // Verify params were serialized correctly + paramBytes := args.Get(3).(*[]byte) + require.JSONEq(t, string(expectedParams), string(*paramBytes)) + + // Simulate setting the return value (as would happen across LOOP boundary) + returnVal := args.Get(4).(*[]byte) + *returnVal = []byte(mockReturn) + }).Return(nil) + + var result uint64 + err = loopReader.GetLatestValue(ctx, readIdentifier, primitives.Finalized, params, &result) + + require.NoError(t, err) + require.Equal(t, uint64(42), result) + mockReader.AssertExpectations(t) +} + +// TestLoopChainReader_GetLatestValue_DecodesComplexTypes tests decoding of complex +// return types using the Sui JSON decoder. +func TestLoopChainReader_GetLatestValue_DecodesComplexTypes(t *testing.T) { + t.Parallel() + log := logger.Test(t) + + mockReader := new(mockContractReader) + loopReader := NewLoopChainReader(log, mockReader) + + ctx := context.Background() + + tests := []struct { + name string + mockResponse string + expectedResult any + targetType any + }{ + { + name: "uint64", + mockResponse: `100`, + expectedResult: uint64(100), + targetType: new(uint64), + }, + { + name: "string", + mockResponse: `"hello"`, + expectedResult: "hello", + targetType: new(string), + }, + { + name: "struct", + mockResponse: `{"first": 10, "second": 20}`, + expectedResult: map[string]any{"first": float64(10), "second": float64(20)}, + targetType: new(map[string]any), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockReader.ExpectedCalls = nil + + mockReader.On("GetLatestValue", + ctx, + "test-read", + primitives.Finalized, + mock.AnythingOfType("*[]uint8"), + mock.AnythingOfType("*[]uint8"), + ).Run(func(args mock.Arguments) { + returnVal := args.Get(4).(*[]byte) + *returnVal = []byte(tt.mockResponse) + }).Return(nil) + + switch v := tt.targetType.(type) { + case *uint64: + err := loopReader.GetLatestValue(ctx, "test-read", primitives.Finalized, map[string]any{}, v) + require.NoError(t, err) + require.Equal(t, tt.expectedResult, *v) + case *string: + err := loopReader.GetLatestValue(ctx, "test-read", primitives.Finalized, map[string]any{}, v) + require.NoError(t, err) + require.Equal(t, tt.expectedResult, *v) + case *map[string]any: + err := loopReader.GetLatestValue(ctx, "test-read", primitives.Finalized, map[string]any{}, v) + require.NoError(t, err) + require.Equal(t, tt.expectedResult, *v) + } + + mockReader.AssertExpectations(t) + }) + } +} + +// TestLoopChainReader_QueryKey_VerifiesExpressionSerialization tests that QueryKey +// properly serializes expressions for the LOOP boundary. +func TestLoopChainReader_QueryKey_VerifiesExpressionSerialization(t *testing.T) { + t.Parallel() + t.Skip("TODO: implement when QueryKey expression serialization is better understood") + + log := logger.Test(t) + mockReader := new(mockContractReader) + loopReader := NewLoopChainReader(log, mockReader) + + ctx := context.Background() + contract := types.BoundContract{Name: "test", Address: "0x123"} + filter := query.KeyFilter{ + Key: "test_event", + Expressions: []query.Expression{ + // Add expression here + }, + } + limitAndSort := query.LimitAndSort{ + Limit: query.CountLimit(10), + } + + // Mock event data as would come back from LOOP + mockEventData := []byte(`{"value": 123}`) + mockReader.On("QueryKey", + ctx, + contract, + mock.Anything, // filter with serialized expressions + limitAndSort, + mock.AnythingOfType("*[]uint8"), + ).Return([]types.Sequence{ + { + Data: &mockEventData, + }, + }, nil) + + type TestEvent struct { + Value uint64 `json:"value"` + } + + var event TestEvent + sequences, err := loopReader.QueryKey(ctx, contract, filter, limitAndSort, &event) + + require.NoError(t, err) + require.Len(t, sequences, 1) + require.Equal(t, uint64(123), event.Value) +} + +// TestLoopChainReader_ProxiesCalls tests that all non-transforming calls are proxied +// directly to the underlying reader. +func TestLoopChainReader_ProxiesCalls(t *testing.T) { + t.Parallel() + log := logger.Test(t) + mockReader := new(mockContractReader) + loopReader := NewLoopChainReader(log, mockReader) + + ctx := context.Background() + + t.Run("Name", func(t *testing.T) { + mockReader.On("Name").Return("test-reader") + name := loopReader.Name() + require.Equal(t, "test-reader", name) + mockReader.AssertExpectations(t) + }) + + t.Run("Ready", func(t *testing.T) { + mockReader.On("Ready").Return(nil) + err := loopReader.Ready() + require.NoError(t, err) + mockReader.AssertExpectations(t) + }) + + t.Run("HealthReport", func(t *testing.T) { + report := map[string]error{"test": nil} + mockReader.On("HealthReport").Return(report) + result := loopReader.HealthReport() + require.Equal(t, report, result) + mockReader.AssertExpectations(t) + }) + + t.Run("Start", func(t *testing.T) { + mockReader.On("Start", ctx).Return(nil) + err := loopReader.Start(ctx) + require.NoError(t, err) + mockReader.AssertExpectations(t) + }) + + t.Run("Close", func(t *testing.T) { + mockReader.On("Close").Return(nil) + err := loopReader.Close() + require.NoError(t, err) + mockReader.AssertExpectations(t) + }) + + t.Run("Bind", func(t *testing.T) { + bindings := []types.BoundContract{{Name: "test", Address: "0x123"}} + mockReader.On("Bind", ctx, bindings).Return(nil) + err := loopReader.Bind(ctx, bindings) + require.NoError(t, err) + mockReader.AssertExpectations(t) + }) + + t.Run("Unbind", func(t *testing.T) { + bindings := []types.BoundContract{{Name: "test", Address: "0x123"}} + mockReader.On("Unbind", ctx, bindings).Return(nil) + err := loopReader.Unbind(ctx, bindings) + require.NoError(t, err) + mockReader.AssertExpectations(t) + }) +} + +// TestLoopChainReader_BatchGetLatestValues_VerifiesLOOPBoundary tests that +// BatchGetLatestValues properly serializes all requests and deserializes responses. +func TestLoopChainReader_BatchGetLatestValues_VerifiesLOOPBoundary(t *testing.T) { + t.Parallel() + log := logger.Test(t) + mockReader := new(mockContractReader) + loopReader := NewLoopChainReader(log, mockReader) + + ctx := context.Background() + + request := types.BatchGetLatestValuesRequest{ + types.BoundContract{Name: "contract1", Address: "0x1"}: { + {ReadName: "read1", Params: map[string]any{"key": "value1"}}, + }, + } + + mockResponse := types.BatchGetLatestValuesResult{ + types.BoundContract{Name: "contract1", Address: "0x1"}: { + {ReadName: "read1"}, + }, + } + + // The mock will receive converted requests with []byte params + mockReader.On("BatchGetLatestValues", ctx, mock.Anything).Run(func(args mock.Arguments) { + // Verify the request was converted to use []byte params + convertedRequest := args.Get(1).(types.BatchGetLatestValuesRequest) + for _, reads := range convertedRequest { + for _, read := range reads { + // Params should be []byte after serialization + _, ok := read.Params.([]byte) + require.True(t, ok, "params should be []byte after LOOP serialization") + } + } + }).Return(mockResponse, nil) + + result, err := loopReader.BatchGetLatestValues(ctx, request) + + require.NoError(t, err) + require.NotNil(t, result) + mockReader.AssertExpectations(t) +} + +// TestLoopChainReader_GetLatestValue_PropagateErrors tests that errors from the +// underlying reader are properly propagated. +func TestLoopChainReader_GetLatestValue_PropagateErrors(t *testing.T) { + t.Parallel() + log := logger.Test(t) + mockReader := new(mockContractReader) + loopReader := NewLoopChainReader(log, mockReader) + + ctx := context.Background() + expectedErr := errors.New("underlying reader error") + + mockReader.On("GetLatestValue", + ctx, + "test-read", + primitives.Finalized, + mock.AnythingOfType("*[]uint8"), + mock.AnythingOfType("*[]uint8"), + ).Return(expectedErr) + + var result uint64 + err := loopReader.GetLatestValue(ctx, "test-read", primitives.Finalized, map[string]any{}, &result) + + require.Error(t, err) + require.ErrorIs(t, err, expectedErr) + mockReader.AssertExpectations(t) +} + +// TestLoopChainReader_DecodeErrors tests that decode errors are wrapped properly. +func TestLoopChainReader_DecodeErrors(t *testing.T) { + t.Parallel() + log := logger.Test(t) + mockReader := new(mockContractReader) + loopReader := NewLoopChainReader(log, mockReader) + + ctx := context.Background() + + // Return invalid JSON that can't be decoded into the target type + mockReader.On("GetLatestValue", + ctx, + "test-read", + primitives.Finalized, + mock.AnythingOfType("*[]uint8"), + mock.AnythingOfType("*[]uint8"), + ).Run(func(args mock.Arguments) { + returnVal := args.Get(4).(*[]byte) + *returnVal = []byte(`invalid json`) + }).Return(nil) + + var result uint64 + err := loopReader.GetLatestValue(ctx, "test-read", primitives.Finalized, map[string]any{}, &result) + + require.Error(t, err) + require.Contains(t, err.Error(), "failed to decode") + mockReader.AssertExpectations(t) +} diff --git a/codec/type_converters.go b/codec/type_converters.go new file mode 100644 index 00000000..c330c21d --- /dev/null +++ b/codec/type_converters.go @@ -0,0 +1,775 @@ +package codec + +import ( + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "math/big" + "reflect" + "strconv" + "strings" +) + +// TypeConversionFunc defines a function that converts data from one type to another +type TypeConversionFunc func(from reflect.Type, to reflect.Type, data any) (any, error) + +// TypeConverter provides a registry approach to type conversions for mapstructure +type TypeConverter struct { + converters map[string]TypeConversionFunc +} + +// NewTypeConverter creates a new type converter with all standard conversions registered +func NewTypeConverter() *TypeConverter { + tc := &TypeConverter{ + converters: make(map[string]TypeConversionFunc), + } + + tc.registerStandardConverters() + return tc +} + +// registerStandardConverters registers all standard type conversion handlers +func (tc *TypeConverter) registerStandardConverters() { + // Hex string conversions + tc.RegisterConverter("hex_string_to_string", tc.hexToString) + tc.RegisterConverter("hex_string_to_bytes", tc.hexToBytes) + tc.RegisterConverter("hex_string_to_uint", tc.hexToUint) + tc.RegisterConverter("hex_string_to_int", tc.hexToInt) + tc.RegisterConverter("hex_string_to_bigint", tc.hexToBigInt) + tc.RegisterConverter("hex_string_to_array", tc.hexToArray) + + // Base64 string conversions + tc.RegisterConverter("base64_string_to_bytes", tc.base64ToBytes) + + // Numeric string conversions + tc.RegisterConverter("string_to_int", tc.stringToInt) + tc.RegisterConverter("string_to_uint", tc.stringToUint) + tc.RegisterConverter("string_to_float", tc.stringToFloat) + tc.RegisterConverter("string_to_bytes", tc.stringToBytes) + tc.RegisterConverter("string_to_bigint", tc.stringToBigInt) + + // Boolean conversions + tc.RegisterConverter("bool_to_int", tc.boolToInt) + tc.RegisterConverter("bool_to_uint", tc.boolToUint) + tc.RegisterConverter("bool_to_bigint", tc.boolToBigInt) + + // Array/Slice conversions + tc.RegisterConverter("slice_to_slice", tc.sliceToSlice) + tc.RegisterConverter("slice_to_hex_string", tc.sliceToHexString) + + // Float64 conversions (JSON unmarshals numbers as float64) + tc.RegisterConverter("float64_to_uint", tc.float64ToUint) + tc.RegisterConverter("float64_to_int", tc.float64ToInt) + + // json.Number conversions + tc.RegisterConverter("json_number_to_uint", tc.jsonNumberToUint) + tc.RegisterConverter("json_number_to_int", tc.jsonNumberToInt) + + // Bytes to numeric conversions (for BCS decoding) + tc.RegisterConverter("bytes_to_uint", tc.bytesToUint) + tc.RegisterConverter("slice_any_to_uint", tc.sliceAnyToUint) +} + +// RegisterConverter registers a conversion function with a unique key +func (tc *TypeConverter) RegisterConverter(key string, fn TypeConversionFunc) { + tc.converters[key] = fn +} + +// Convert attempts to convert data using registered converters +func (tc *TypeConverter) Convert(from reflect.Type, to reflect.Type, data any) (any, error) { + // Try float64 conversions (JSON unmarshals numbers as float64) + if from.Kind() == reflect.Float64 { + result, err, handled := tc.handleFloat64(data.(float64), to) + if handled { + return result, err + } + } + + // Try json.Number conversions + if _, ok := data.(json.Number); ok { + result, err, handled := tc.handleJSONNumber(data.(json.Number), to) + if handled { + return result, err + } + } + + // Try hex string conversions + if from.Kind() == reflect.String { + if str, ok := data.(string); ok && strings.HasPrefix(str, "0x") { + result, err, handled := tc.handleHexString(str, to, data) + if handled { + return result, err + } + } + } + + // Try numeric string conversions (before base64, as numeric strings can be valid base64) + if from.Kind() == reflect.String { + result, err, handled := tc.handleNumericString(data.(string), to) + if handled { + return result, err + } + } + + // Try base64 conversions (fallback for non-numeric strings to []byte) + if from.Kind() == reflect.String && (to.Kind() == reflect.Slice || to.Kind() == reflect.Array) && to.Elem().Kind() == reflect.Uint8 { + result, err, handled := tc.handleBase64String(data.(string), to) + if handled { + return result, err + } + } + + // Try boolean conversions + if from.Kind() == reflect.Bool { + result, err, handled := tc.handleBoolean(data.(bool), to) + if handled { + return result, err + } + } + + // Try bytes/slice to numeric conversions + if from.Kind() == reflect.Slice || from.Kind() == reflect.Array { + // Handle []byte or []any to numeric + if isNumericTarget(to) { + result, err, handled := tc.handleBytesToNumeric(from, to, data) + if handled { + return result, err + } + } + + // Handle slice to slice conversions + if to.Kind() == reflect.Slice { + result, err, handled := tc.handleSlice(from, to, data) + if handled { + return result, err + } + } + + // Handle slice to hex string conversions + if to.Kind() == reflect.String { + result, err := tc.sliceToHexString(from, to, data) + if err == nil { + return result, err + } + } + } + + // No conversion found, return data as-is + return data, nil +} + +// handleHexString handles hex string conversions +func (tc *TypeConverter) handleHexString(str string, to reflect.Type, data any) (result any, err error, handled bool) { + hexStr := strings.TrimPrefix(str, "0x") + + switch to.Kind() { + case reflect.String: + if fn, ok := tc.converters["hex_string_to_string"]; ok { + result, err = fn(reflect.TypeOf(str), to, hexStr) + return result, err, true + } + case reflect.Slice: + if to.Elem().Kind() == reflect.Uint8 { + if fn, ok := tc.converters["hex_string_to_bytes"]; ok { + result, err = fn(reflect.TypeOf(str), to, hexStr) + return result, err, true + } + } + case reflect.Array: + if to.Elem().Kind() == reflect.Uint8 { + if fn, ok := tc.converters["hex_string_to_array"]; ok { + result, err = fn(reflect.TypeOf(str), to, hexStr) + return result, err, true + } + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if fn, ok := tc.converters["hex_string_to_uint"]; ok { + result, err = fn(reflect.TypeOf(str), to, hexStr) + return result, err, true + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if fn, ok := tc.converters["hex_string_to_int"]; ok { + result, err = fn(reflect.TypeOf(str), to, hexStr) + return result, err, true + } + case reflect.Ptr: + if to == reflect.TypeOf((*big.Int)(nil)) { + if fn, ok := tc.converters["hex_string_to_bigint"]; ok { + result, err = fn(reflect.TypeOf(str), to, hexStr) + return result, err, true + } + } + case reflect.Interface: + return "0x" + hexStr, nil, true + } + + return nil, nil, false +} + +// handleBase64String handles base64 string conversions +func (tc *TypeConverter) handleBase64String(str string, to reflect.Type) (result any, err error, handled bool) { + if fn, ok := tc.converters["base64_string_to_bytes"]; ok { + result, err = fn(reflect.TypeOf(str), to, str) + // If base64ToBytes returns the original string unchanged, it means + // base64 decoding failed, so we should let default handling try + if resultStr, isStr := result.(string); isStr && resultStr == str { + return nil, nil, false + } + return result, err, true + } + return nil, nil, false +} + +// handleNumericString handles numeric string conversions +func (tc *TypeConverter) handleNumericString(str string, to reflect.Type) (result any, err error, handled bool) { + switch to.Kind() { + case reflect.String: + return str, nil, true + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if fn, ok := tc.converters["string_to_int"]; ok { + result, err = fn(reflect.TypeOf(str), to, str) + return result, err, true + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if fn, ok := tc.converters["string_to_uint"]; ok { + result, err = fn(reflect.TypeOf(str), to, str) + return result, err, true + } + case reflect.Float32, reflect.Float64: + if fn, ok := tc.converters["string_to_float"]; ok { + result, err = fn(reflect.TypeOf(str), to, str) + return result, err, true + } + case reflect.Slice: + if to.Elem().Kind() == reflect.Uint8 { + if fn, ok := tc.converters["string_to_bytes"]; ok { + result, err = fn(reflect.TypeOf(str), to, str) + // If stringToBytes returns the original string unchanged, it means + // it's not a numeric string, so we should let other handlers try + if resultStr, isStr := result.(string); isStr && resultStr == str { + return nil, nil, false + } + return result, err, true + } + } + case reflect.Ptr: + if to == reflect.TypeOf((*big.Int)(nil)) { + if fn, ok := tc.converters["string_to_bigint"]; ok { + result, err = fn(reflect.TypeOf(str), to, str) + return result, err, true + } + } + } + + return nil, nil, false +} + +// handleBoolean handles boolean conversions +func (tc *TypeConverter) handleBoolean(boolValue bool, to reflect.Type) (result any, err error, handled bool) { + switch to.Kind() { + case reflect.Bool: + return boolValue, nil, true + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if fn, ok := tc.converters["bool_to_int"]; ok { + result, err = fn(reflect.TypeOf(boolValue), to, boolValue) + return result, err, true + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if fn, ok := tc.converters["bool_to_uint"]; ok { + result, err = fn(reflect.TypeOf(boolValue), to, boolValue) + return result, err, true + } + case reflect.Ptr: + if to == reflect.TypeOf((*big.Int)(nil)) { + if fn, ok := tc.converters["bool_to_bigint"]; ok { + result, err = fn(reflect.TypeOf(boolValue), to, boolValue) + return result, err, true + } + } + } + + return nil, nil, false +} + +// handleSlice handles array/slice conversions +func (tc *TypeConverter) handleSlice(from, to reflect.Type, data any) (result any, err error, handled bool) { + if fn, ok := tc.converters["slice_to_slice"]; ok { + result, err = fn(from, to, data) + return result, err, true + } + return nil, nil, false +} + +// Conversion function implementations + +func (tc *TypeConverter) hexToString(from, to reflect.Type, data any) (any, error) { + hexStr, ok := data.(string) + if !ok { + return data, nil + } + return hexStr, nil +} + +func (tc *TypeConverter) hexToBytes(from, to reflect.Type, data any) (any, error) { + hexStr, ok := data.(string) + if !ok { + return data, nil + } + + if hexStr == "" { + return []uint8{}, nil + } + + if len(hexStr)%2 == 1 { + hexStr = "0" + hexStr + } + + return hex.DecodeString(hexStr) +} + +func (tc *TypeConverter) hexToUint(from, to reflect.Type, data any) (any, error) { + hexStr, ok := data.(string) + if !ok { + return data, nil + } + + return strconv.ParseUint(hexStr, base16, uint64Bits) +} + +func (tc *TypeConverter) hexToInt(from, to reflect.Type, data any) (any, error) { + hexStr, ok := data.(string) + if !ok { + return data, nil + } + + val, err := strconv.ParseInt(hexStr, base16, uint64Bits) + if err != nil { + return nil, fmt.Errorf("failed to parse hex to int: %w", err) + } + + return reflect.ValueOf(val).Convert(to).Interface(), nil +} + +func (tc *TypeConverter) hexToBigInt(from, to reflect.Type, data any) (any, error) { + hexStr, ok := data.(string) + if !ok { + return data, nil + } + + bi := new(big.Int) + bi.SetString(hexStr, base16) + return bi, nil +} + +func (tc *TypeConverter) hexToArray(from, to reflect.Type, data any) (any, error) { + hexStr, ok := data.(string) + if !ok { + return data, nil + } + + bytes, err := tc.hexToBytes(from, reflect.SliceOf(to.Elem()), hexStr) + if err != nil { + return nil, fmt.Errorf("failed to decode hex string %q: %w", hexStr, err) + } + + byteSlice := bytes.([]byte) + out := make([]uint8, to.Len()) + + // Re-enable this check once we can guarantee that responses are always the same length as the target array length. + // Disabling for now to avoid breaking changes to core. + // if len(byteSlice) != to.Len() { + // return nil, fmt.Errorf("hex to array: byte slice length %d is not equal to output array length %d", len(byteSlice), to.Len()) + // } + + copy(out, byteSlice) + + return out, nil +} + +func (tc *TypeConverter) sliceToHexString(from, to reflect.Type, data any) (any, error) { + bytes, err := AnySliceToBytes(data.([]any)) + if err != nil { + return nil, err + } + + return "0x" + hex.EncodeToString(bytes), nil +} + +func (tc *TypeConverter) base64ToBytes(from, to reflect.Type, data any) (any, error) { + str, ok := data.(string) + if !ok { + return data, nil + } + + bytes, err := base64.StdEncoding.DecodeString(str) + if err == nil { + return bytes, nil + } + + // Base64 decoding failed - return string unchanged so handler knows to pass to default + return str, nil +} + +func (tc *TypeConverter) stringToInt(from, to reflect.Type, data any) (any, error) { + str, ok := data.(string) + if !ok { + return data, nil + } + + val, err := strconv.ParseInt(str, base10, uint64Bits) + if err != nil { + return nil, fmt.Errorf("failed to parse string to int: %w", err) + } + + if overflowInt(to, val) { + return nil, fmt.Errorf("value %d overflows %v", val, to) + } + + return reflect.ValueOf(val).Convert(to).Interface(), nil +} + +func (tc *TypeConverter) stringToUint(from, to reflect.Type, data any) (any, error) { + str, ok := data.(string) + if !ok { + return data, nil + } + + val, err := strconv.ParseUint(str, base10, uint64Bits) + if err != nil { + return nil, fmt.Errorf("failed to parse string to uint: %w", err) + } + + if overflowUint(to, val) { + return nil, fmt.Errorf("value %d overflows %v", val, to) + } + + return reflect.ValueOf(val).Convert(to).Interface(), nil +} + +func (tc *TypeConverter) stringToFloat(from, to reflect.Type, data any) (any, error) { + str, ok := data.(string) + if !ok { + return data, nil + } + + val, err := strconv.ParseFloat(str, uint64Bits) + if err != nil { + return nil, fmt.Errorf("failed to parse string to float: %w", err) + } + + if overflowFloat(to, val) { + return nil, fmt.Errorf("value %f overflows %v", val, to) + } + + return reflect.ValueOf(val).Convert(to).Interface(), nil +} + +func (tc *TypeConverter) stringToBytes(from, to reflect.Type, data any) (any, error) { + str, ok := data.(string) + if !ok { + return data, nil + } + + // Try numeric string first (convert to little-endian bytes) + if num, err := strconv.ParseUint(str, base10, uint64Bits); err == nil { + return numericToBytes(num), nil + } + + // Not a numeric string - return unchanged so other handlers (base64) can try + return str, nil +} + +func (tc *TypeConverter) stringToBigInt(from, to reflect.Type, data any) (any, error) { + str, ok := data.(string) + if !ok { + return data, nil + } + + result := new(big.Int) + _, success := result.SetString(str, base10) + if !success { + return nil, fmt.Errorf("cannot parse string %s as big.Int", str) + } + + return result, nil +} + +func (tc *TypeConverter) boolToInt(from, to reflect.Type, data any) (any, error) { + boolValue, ok := data.(bool) + if !ok { + return data, nil + } + + if boolValue { + return reflect.ValueOf(1).Convert(to).Interface(), nil + } + + return reflect.ValueOf(0).Convert(to).Interface(), nil +} + +func (tc *TypeConverter) boolToUint(from, to reflect.Type, data any) (any, error) { + boolValue, ok := data.(bool) + if !ok { + return data, nil + } + + if boolValue { + return reflect.ValueOf(1).Convert(to).Interface(), nil + } + + return reflect.ValueOf(0).Convert(to).Interface(), nil +} + +func (tc *TypeConverter) boolToBigInt(from, to reflect.Type, data any) (any, error) { + boolValue, ok := data.(bool) + if !ok { + return data, nil + } + + if boolValue { + return big.NewInt(1), nil + } + + return big.NewInt(0), nil +} + +func (tc *TypeConverter) sliceToSlice(from, to reflect.Type, data any) (any, error) { + sourceSlice := reflect.ValueOf(data) + targetSlice := reflect.MakeSlice(to, sourceSlice.Len(), sourceSlice.Cap()) + + for i := range sourceSlice.Len() { + sourceElem := sourceSlice.Index(i).Interface() + targetElem := reflect.New(to.Elem()).Interface() + + if err := DecodeSuiJsonValue(sourceElem, targetElem); err != nil { + return nil, fmt.Errorf("failed to decode array element at index %d: %w", i, err) + } + + targetSlice.Index(i).Set(reflect.ValueOf(targetElem).Elem()) + } + + return targetSlice.Interface(), nil +} + +// float64ToUint converts float64 to uint types (JSON unmarshals numbers as float64) +func (tc *TypeConverter) float64ToUint(from, to reflect.Type, data any) (any, error) { + floatVal, ok := data.(float64) + if !ok { + return data, nil + } + + uintVal := uint64(floatVal) + if overflowUint(to, uintVal) { + return nil, fmt.Errorf("value %d overflows %v", uintVal, to) + } + + return reflect.ValueOf(uintVal).Convert(to).Interface(), nil +} + +// float64ToInt converts float64 to int types +func (tc *TypeConverter) float64ToInt(from, to reflect.Type, data any) (any, error) { + floatVal, ok := data.(float64) + if !ok { + return data, nil + } + + intVal := int64(floatVal) + if overflowInt(to, intVal) { + return nil, fmt.Errorf("value %d overflows %v", intVal, to) + } + + return reflect.ValueOf(intVal).Convert(to).Interface(), nil +} + +// jsonNumberToUint converts json.Number to uint types +func (tc *TypeConverter) jsonNumberToUint(from, to reflect.Type, data any) (any, error) { + jsonNum, ok := data.(json.Number) + if !ok { + return data, nil + } + + intVal, err := jsonNum.Int64() + if err != nil { + return nil, fmt.Errorf("failed to parse JSON number: %w", err) + } + + if intVal < 0 { + return nil, fmt.Errorf("cannot convert negative value %d to uint", intVal) + } + + uintVal := uint64(intVal) + if overflowUint(to, uintVal) { + return nil, fmt.Errorf("value %d overflows %v", uintVal, to) + } + + return reflect.ValueOf(uintVal).Convert(to).Interface(), nil +} + +// jsonNumberToInt converts json.Number to int types +func (tc *TypeConverter) jsonNumberToInt(from, to reflect.Type, data any) (any, error) { + jsonNum, ok := data.(json.Number) + if !ok { + return data, nil + } + + intVal, err := jsonNum.Int64() + if err != nil { + return nil, fmt.Errorf("failed to parse JSON number: %w", err) + } + + if overflowInt(to, intVal) { + return nil, fmt.Errorf("value %d overflows %v", intVal, to) + } + + return reflect.ValueOf(intVal).Convert(to).Interface(), nil +} + +// bytesToUint converts []byte to uint types (little-endian) +func (tc *TypeConverter) bytesToUint(from, to reflect.Type, data any) (any, error) { + bytes, ok := data.([]byte) + if !ok { + return data, nil + } + + if len(bytes) == 0 { + return nil, fmt.Errorf("empty byte array cannot be converted to numeric value") + } + + var result uint64 + // Process bytes in little-endian order + for i := 0; i < len(bytes) && i < 8; i++ { + result |= uint64(bytes[i]) << (8 * i) + } + + if overflowUint(to, result) { + return nil, fmt.Errorf("value %d overflows %v", result, to) + } + + return reflect.ValueOf(result).Convert(to).Interface(), nil +} + +// sliceAnyToUint converts []any to uint types (converts to bytes first) +func (tc *TypeConverter) sliceAnyToUint(from, to reflect.Type, data any) (any, error) { + anySlice, ok := data.([]any) + if !ok { + return data, nil + } + + bytes, err := AnySliceToBytes(anySlice) + if err != nil { + return nil, fmt.Errorf("failed to convert slice to bytes: %w", err) + } + + return tc.bytesToUint(reflect.TypeOf(bytes), to, bytes) +} + +// handleFloat64 handles float64 conversions +func (tc *TypeConverter) handleFloat64(floatVal float64, to reflect.Type) (result any, err error, handled bool) { + switch to.Kind() { + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if fn, ok := tc.converters["float64_to_uint"]; ok { + result, err = fn(reflect.TypeOf(floatVal), to, floatVal) + return result, err, true + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if fn, ok := tc.converters["float64_to_int"]; ok { + result, err = fn(reflect.TypeOf(floatVal), to, floatVal) + return result, err, true + } + } + + return nil, nil, false +} + +// handleJSONNumber handles json.Number conversions +func (tc *TypeConverter) handleJSONNumber(jsonNum json.Number, to reflect.Type) (result any, err error, handled bool) { + switch to.Kind() { + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if fn, ok := tc.converters["json_number_to_uint"]; ok { + result, err = fn(reflect.TypeOf(jsonNum), to, jsonNum) + return result, err, true + } + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if fn, ok := tc.converters["json_number_to_int"]; ok { + result, err = fn(reflect.TypeOf(jsonNum), to, jsonNum) + return result, err, true + } + } + + return nil, nil, false +} + +// handleBytesToNumeric handles conversions from []byte or []any to numeric types +func (tc *TypeConverter) handleBytesToNumeric(from, to reflect.Type, data any) (result any, err error, handled bool) { + if from.Kind() == reflect.Slice && from.Elem().Kind() == reflect.Uint8 { + // []byte to numeric + if fn, ok := tc.converters["bytes_to_uint"]; ok { + result, err = fn(from, to, data) + return result, err, true + } + } + + if from.Kind() == reflect.Slice && from.Elem().Kind() == reflect.Interface { + // []any to numeric + if fn, ok := tc.converters["slice_any_to_uint"]; ok { + result, err = fn(from, to, data) + return result, err, true + } + } + + return nil, nil, false +} + +// isNumericTarget checks if the target type is a numeric type +func isNumericTarget(to reflect.Type) bool { + switch to.Kind() { + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return true + } + return false +} + +// Global type converter instance for package-wide use (lazy initialization) +var defaultTypeConverter *TypeConverter + +// getDefaultTypeConverter returns the global type converter, initializing it if necessary +func getDefaultTypeConverter() *TypeConverter { + if defaultTypeConverter == nil { + defaultTypeConverter = NewTypeConverter() + } + return defaultTypeConverter +} + +// UnifiedTypeConverterHook is a mapstructure hook that uses the type converter registry +func UnifiedTypeConverterHook(from, to reflect.Type, data any) (any, error) { + // Skip if types are the same + if from == to { + return data, nil + } + + // Handle single-field struct case only for primitive source types + // (string, bool, numeric types) to avoid interfering with map-to-struct decoding + if to.Kind() == reflect.Struct && to.NumField() == 1 { + switch from.Kind() { + case reflect.String, reflect.Bool, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64, + reflect.Slice, reflect.Array: + return handleSingleFieldStruct(to, data, DecodeSuiJsonValue) + } + } + + // Use the global converter + return getDefaultTypeConverter().Convert(from, to, data) +} + +func BytesToAnySlice(b []byte) []any { + result := make([]any, len(b)) + for i, v := range b { + result[i] = v + } + return result +} diff --git a/relayer/codec/type_converters_test.go b/codec/type_converters_test.go similarity index 100% rename from relayer/codec/type_converters_test.go rename to codec/type_converters_test.go diff --git a/codec/types.go b/codec/types.go new file mode 100644 index 00000000..ba2c7a4b --- /dev/null +++ b/codec/types.go @@ -0,0 +1,229 @@ +package codec + +import ( + "encoding/hex" + "errors" + "fmt" + "math/big" + "strings" + "unicode" + + "github.com/block-vision/sui-go-sdk/models" +) + +var AccountZero = make([]byte, 32) + +// Object represents a Sui object with its ID and optional initial shared version. +type Object struct { + Id string + InitialSharedVersion *uint64 +} + +// IsSuiAddress returns true if addr is a valid Sui address/ObjectID. +// It is an improvement over the sui-go-sdk's IsValidSuiAddress function. +func IsSuiAddress(addr string) bool { + if !(strings.HasPrefix(addr, "0x") || strings.HasPrefix(addr, "0X")) { + return false + } + + h := addr[2:] + + // 1..64 hex chars + if len(h) == 0 || len(h) > 64 { + return false + } + + // hex only + for _, r := range h { + if !isHexRune(r) { + return false + } + } + + // hex.DecodeString requires even length; allow odd by left-padding one '0' + if len(h)%2 == 1 { + h = "0" + h + } + + b, err := hex.DecodeString(h) + if err != nil { + return false + } + + // must be ≤32 bytes (32 bytes == 64 hex chars) + return len(b) <= 32 +} + +// ToSuiAddress normalizes and validates a Sui address. +// It pads the address to 64 hex characters (32 bytes) with leading zeros. +func ToSuiAddress(address string) (string, error) { + normalized := NormalizeSuiAddress(address) + if !IsSuiAddress(string(normalized)) { + return "", fmt.Errorf("invalid sui address: %s", address) + } + + return string(normalized), nil +} + +// NormalizeSuiAddress normalizes a Sui address to 64 hex characters. +func NormalizeSuiAddress(address string) string { + if !strings.HasPrefix(address, "0x") && !strings.HasPrefix(address, "0X") { + address = "0x" + address + } + + // Remove 0x prefix for processing + h := address[2:] + + // Pad with leading zeros to 64 characters (32 bytes) + if len(h) < 64 { + h = strings.Repeat("0", 64-len(h)) + h + } + + // Convert to lowercase + h = strings.ToLower(h) + + return "0x" + h +} + +func isHexRune(r rune) bool { + return unicode.IsDigit(r) || (r >= 'a' && r <= 'f') || (r >= 'A' && r <= 'F') +} + +type PTBCommandDependency struct { + CommandIndex uint16 + ResultIndex *uint16 +} + +// PointerTag defines the structured format for pointer tags used in chain reader. +// Pointer tags specify how to derive object IDs from pointer objects stored on-chain. +type PointerTag struct { + // Module name containing the pointer object (e.g. "state_object", "offramp", "counter") + Module string `json:"module"` + // PointerName is the object type to search for (e.g. "CCIPObjectRefPointer", "OffRampStatePointer") + PointerName string `json:"pointerName"` + // FieldName is OPTIONAL and NOT USED by the implementation. The parent field name is automatically + // looked up from the global common.PointerConfigs registry based on the PointerName. + // This field exists for backward compatibility or future implementations to override static code but is currently ignored. + FieldName string `json:"fieldName,omitempty"` + // DerivationKey is the key used to derive the child object ID from the parent object ID (e.g. "CCIPObjectRef", "CCIP_OWNABLE") + DerivationKey string `json:"derivationKey"` + // PackageID is the package ID for the Pointer object if it differs from the calling contract's package ID + // This is used for cross-package pointer dependencies (e.g. offramp package depending on CCIP package CCIPObjectRef) + // If empty, the calling contract's package ID is used + PackageID string `json:"packageId,omitempty"` +} + +func (p PointerTag) Validate() error { + if p.Module == "" { + return errors.New("PointerTag.Module is required") + } + if p.PointerName == "" { + return errors.New("PointerTag.PointerName is required") + } + // FieldName is optional - it's looked up from common.PointerConfigs + if p.DerivationKey == "" { + return errors.New("PointerTag.DerivationKey is required") + } + return nil +} + +// SuiFunctionParam defines a parameter for a Sui function call +type SuiFunctionParam struct { + // Name of the parameter + Name string + // PointerTag (optional) specify how to derive object IDs from pointer objects stored on-chain. + PointerTag *PointerTag + // Type of the parameter (e.g., "u64", "String", "vector", "ptb_dependency") + Type string + // IsMutable specifies if the object is mutable or not (optional - defaults to true) + IsMutable *bool + // IsGeneric specifies if the parameter is a generic argument + GenericType *string + // Whether the parameter is required + Required bool + // Default value to use if not provided + DefaultValue any + // Result from a previous PTB Command (optional). It is used for expressive construction of PTB commands + PTBDependency *PTBCommandDependency + // GenericDependency maps to internal helpers for fetching an unknown generic type required by the parameter + GenericDependency *string +} + +type SuiPTBCommandType string + +const ( + SuiPTBCommandMoveCall SuiPTBCommandType = "move_call" + SuiPTBCommandPublish SuiPTBCommandType = "publish" + SuiPTBCommandTransfer SuiPTBCommandType = "transfer" +) + +// OCRConfigSet event data +type ConfigSet struct { + OcrPluginType byte + ConfigDigest []byte + Signers [][]byte + // this is a list of addresses, we can treat them as strings + Transmitters []string + BigF byte +} + +// SourceChainConfigSet event data +type SourceChainConfigSet struct { + SourceChainSelector uint64 + SourceChainConfig SourceChainConfig +} + +// SourceChainConfig event data +type SourceChainConfig struct { + Router string + IsEnabled bool + MinSeqNr uint64 + IsRMNVerificationDisabled bool + OnRamp []byte +} + +// ExecutionReport event data +type ExecutionReport struct { + SourceChainSelector uint64 + Message Any2SuiRampMessage + OffchainTokenData [][]byte + Proofs [][]byte +} + +// RampMessageHeader event data +type RampMessageHeader struct { + MessageID []byte + SourceChainSelector uint64 + DestChainSelector uint64 + SequenceNumber uint64 + Nonce uint64 +} + +// Any2SuiTokenTransfer event data +type Any2SuiTokenTransfer struct { + SourcePoolAddress []byte + DestTokenAddress models.SuiAddress + DestGasAmount uint32 + ExtraData []byte + Amount *big.Int +} + +// Any2SuiRampMessage event data +type Any2SuiRampMessage struct { + Header RampMessageHeader + Sender []byte + Data []byte + Receiver models.SuiAddress + GasLimit *big.Int + TokenReceiver models.SuiAddressBytes + TokenAmounts []Any2SuiTokenTransfer +} + +// ExecutionStateChanged event data +type ExecutionStateChanged struct { + SourceChainSelector uint64 `json:"source_chain_selector"` + SequenceNumber uint64 `json:"sequence_number"` + MessageId []byte `json:"message_id"` + MessageHash []byte `json:"message_hash"` + State byte `json:"state"` +} diff --git a/deployment/go.mod b/deployment/go.mod index a8c1840c..5076477d 100644 --- a/deployment/go.mod +++ b/deployment/go.mod @@ -7,6 +7,8 @@ replace github.com/fbsobreira/gotron-sdk => github.com/smartcontractkit/chainlin replace github.com/smartcontractkit/chainlink-sui => ../ +replace github.com/smartcontractkit/chainlink-sui/codec => ../codec + require ( github.com/Masterminds/semver/v3 v3.5.0 github.com/aptos-labs/aptos-go-sdk v1.13.0 @@ -139,6 +141,7 @@ require ( github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9 // indirect + github.com/smartcontractkit/chainlink-sui/codec v0.0.0-20260702152904-78d359a411ad // indirect github.com/smartcontractkit/chainlink-ton v1.0.5-0.20260514223130-48bc90aca745 // indirect github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20250908203554-5bd9d2fe9513 // indirect github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad // indirect diff --git a/go.md b/go.md index 4dfab678..c2a06ec7 100644 --- a/go.md +++ b/go.md @@ -37,9 +37,11 @@ flowchart LR click chainlink-protos/storage-service href "https://github.com/smartcontractkit/chainlink-protos" chainlink-protos/workflows/go click chainlink-protos/workflows/go href "https://github.com/smartcontractkit/chainlink-protos" - chainlink-sui --> chainlink-aptos chainlink-sui --> chainlink-ccip + chainlink-sui --> chainlink-sui/codec click chainlink-sui href "https://github.com/smartcontractkit/chainlink-sui" + chainlink-sui/codec --> chainlink-aptos + click chainlink-sui/codec href "https://github.com/smartcontractkit/chainlink-sui" freeport click freeport href "https://github.com/smartcontractkit/freeport" go-sumtype2 @@ -66,8 +68,14 @@ flowchart LR end click chainlink-protos-repo href "https://github.com/smartcontractkit/chainlink-protos" + subgraph chainlink-sui-repo[chainlink-sui] + chainlink-sui + chainlink-sui/codec + end + click chainlink-sui-repo href "https://github.com/smartcontractkit/chainlink-sui" + classDef outline stroke-dasharray:6,fill:none; - class chainlink-common-repo,chainlink-protos-repo outline + class chainlink-common-repo,chainlink-protos-repo,chainlink-sui-repo outline ``` ## All modules ```mermaid @@ -189,9 +197,11 @@ flowchart LR click chainlink-protos/svr href "https://github.com/smartcontractkit/chainlink-protos" chainlink-protos/workflows/go click chainlink-protos/workflows/go href "https://github.com/smartcontractkit/chainlink-protos" - chainlink-sui --> chainlink-aptos chainlink-sui --> chainlink-ccip + chainlink-sui --> chainlink-sui/codec click chainlink-sui href "https://github.com/smartcontractkit/chainlink-sui" + chainlink-sui/codec --> chainlink-aptos + click chainlink-sui/codec href "https://github.com/smartcontractkit/chainlink-sui" chainlink-sui/deployment --> chainlink-ccip/deployment click chainlink-sui/deployment href "https://github.com/smartcontractkit/chainlink-sui" chainlink-sui/integration-tests --> chainlink-sui/deployment @@ -304,6 +314,7 @@ flowchart LR subgraph chainlink-sui-repo[chainlink-sui] chainlink-sui + chainlink-sui/codec chainlink-sui/deployment chainlink-sui/integration-tests end diff --git a/go.mod b/go.mod index 3c38b9bf..a23359fb 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/smartcontractkit/chainlink-aptos v0.0.0-20260428085939-5c70de12dbfc github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260624154507-ea7ff77a0ddb github.com/smartcontractkit/chainlink-common v0.11.2-0.20260506120607-7f10be016c89 + github.com/smartcontractkit/chainlink-sui/codec v0.0.0-20260702152904-78d359a411ad github.com/stretchr/testify v1.11.1 github.com/test-go/testify v1.1.4 go.opentelemetry.io/otel v1.44.0 @@ -33,6 +34,8 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) +replace github.com/smartcontractkit/chainlink-sui/codec => ./codec + require ( filippo.io/edwards25519 v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.5.0 // indirect diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 27c59c67..23b60e08 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -4,6 +4,8 @@ go 1.26.2 replace github.com/smartcontractkit/chainlink-sui => ../ +replace github.com/smartcontractkit/chainlink-sui/codec => ../codec + replace github.com/smartcontractkit/chainlink-sui/deployment => ../deployment require ( @@ -141,6 +143,7 @@ require ( github.com/smartcontractkit/chainlink-protos/job-distributor v0.19.0 // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9 // indirect + github.com/smartcontractkit/chainlink-sui/codec v0.0.0-20260702152904-78d359a411ad // indirect github.com/smartcontractkit/chainlink-ton v1.0.5-0.20260514223130-48bc90aca745 // indirect github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20251014143056-a0c6328c91e9 // indirect github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad // indirect diff --git a/relayer/chainreader/loop/loop_reader.go b/relayer/chainreader/loop/loop_reader.go index 97eedc9d..48ac7b61 100644 --- a/relayer/chainreader/loop/loop_reader.go +++ b/relayer/chainreader/loop/loop_reader.go @@ -1,191 +1,18 @@ package loop import ( - "context" - "encoding/json" - "fmt" - "reflect" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/types" - "github.com/smartcontractkit/chainlink-common/pkg/types/query" - "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives" - - "github.com/smartcontractkit/chainlink-aptos/relayer/chainreader/loop" - "github.com/smartcontractkit/chainlink-sui/relayer/codec" + "github.com/smartcontractkit/chainlink-sui/codec/loop" ) -const ( - READ_COMPONENTS_COUNT = 3 -) +//go:fix inline +const ReadComponentsCount = loop.READ_COMPONENTS_COUNT +// Deprecated: use loop.NewLoopChainReader. +// +//go:fix inline func NewLoopChainReader(log logger.Logger, reader types.ContractReader) types.ContractReader { - lrLogger := logger.Named(log, "LoopChainReader") - return &loopChainReader{logger: lrLogger, reader: reader, moduleAddresses: map[string]string{}} -} - -type loopChainReader struct { - services.Service - types.UnimplementedContractReader - logger logger.Logger - reader types.ContractReader - moduleAddresses map[string]string -} - -func (s *loopChainReader) Name() string { - return s.reader.Name() -} - -func (s *loopChainReader) Ready() error { - return s.reader.Ready() -} - -func (s *loopChainReader) HealthReport() map[string]error { - return s.reader.HealthReport() -} - -func (s *loopChainReader) Start(ctx context.Context) error { - return s.reader.Start(ctx) -} - -func (s *loopChainReader) Close() error { - return s.reader.Close() -} - -func (s *loopChainReader) GetLatestValue(ctx context.Context, readIdentifier string, confidenceLevel primitives.ConfidenceLevel, params, returnVal any) error { - convertedResult := []byte{} - - s.logger.Debugw("GetLatestValue params before serialization over LOOP", "params", params) - - jsonParamBytes, err := json.Marshal(params) - if err != nil { - return fmt.Errorf("failed to marshal params: %+w", err) - } - - err = s.reader.GetLatestValue(ctx, readIdentifier, confidenceLevel, &jsonParamBytes, &convertedResult) - if err != nil { - return fmt.Errorf("failed to call GetLatestValue over LOOP: %w", err) - } - - err = s.decodeGLVReturnValue(readIdentifier, convertedResult, returnVal) - if err != nil { - return fmt.Errorf("failed to decode GetLatestValue return value: %w", err) - } - - return nil -} - -func (s *loopChainReader) BatchGetLatestValues(ctx context.Context, request types.BatchGetLatestValuesRequest) (types.BatchGetLatestValuesResult, error) { - convertedRequest := types.BatchGetLatestValuesRequest{} - for contract, requestBatch := range request { - convertedBatch := []types.BatchRead{} - for _, read := range requestBatch { - jsonParamBytes, err := json.Marshal(read.Params) - if err != nil { - return nil, fmt.Errorf("failed to marshal params: %+w", err) - } - convertedBatch = append(convertedBatch, types.BatchRead{ - ReadName: read.ReadName, - Params: jsonParamBytes, - ReturnVal: &[]byte{}, - }) - } - convertedRequest[contract] = convertedBatch - } - - result, err := s.reader.BatchGetLatestValues(ctx, convertedRequest) - if err != nil { - return nil, err - } - - convertedResult := types.BatchGetLatestValuesResult{} - for contract, resultBatch := range result { - requestBatch := request[contract] - convertedBatch := []types.BatchReadResult{} - for i, result := range resultBatch { - read := requestBatch[i] - resultValue, resultError := result.GetResult() - convertedResult := types.BatchReadResult{ReadName: result.ReadName} - if resultError == nil { - resultPointer := resultValue.(*[]byte) - err := s.decodeGLVReturnValue(result.ReadName, *resultPointer, read.ReturnVal) - if err != nil { - resultError = fmt.Errorf("failed to decode BatchGetLatestValue return value: %w", err) - } - } - convertedResult.SetResult(read.ReturnVal, resultError) - convertedBatch = append(convertedBatch, convertedResult) - } - convertedResult[contract] = convertedBatch - } - - s.logger.Debugw("BatchGetLatestValues result", "result", convertedResult) - - return convertedResult, nil -} - -func (s *loopChainReader) QueryKey(ctx context.Context, contract types.BoundContract, filter query.KeyFilter, limitAndSort query.LimitAndSort, sequenceDataType any) ([]types.Sequence, error) { - convertedExpressions, err := loop.SerializeExpressions(filter.Expressions) - if err != nil { - return nil, fmt.Errorf("failed to serialize QueryKey expressions: %w", err) - } - - s.logger.Debugw("QueryKey expressions after serialization over LOOP", "expressions", convertedExpressions) - - convertedFilter := query.KeyFilter{ - Key: filter.Key, - Expressions: convertedExpressions, - } - - sequences, err := s.reader.QueryKey(ctx, contract, convertedFilter, limitAndSort, &[]byte{}) - if err != nil { - return nil, fmt.Errorf("failed to call QueryKey over LOOP: %w", err) - } - - for i, sequence := range sequences { - jsonBytes := sequence.Data.(*[]byte) - jsonData := map[string]any{} - err := json.Unmarshal(*jsonBytes, &jsonData) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal LOOP sourced JSON event data (`%s`): %w", string(*jsonBytes), err) - } - - eventData := reflect.New(reflect.TypeOf(sequenceDataType).Elem()).Interface() - err = codec.DecodeSuiJsonValue(jsonData, eventData) - if err != nil { - return nil, fmt.Errorf("failed to decode LOOP sourced event data (`%s`) into a Sui value: %+w", string(*jsonBytes), err) - } - - sequences[i].Data = eventData - } - - return sequences, nil -} - -func (s *loopChainReader) Bind(ctx context.Context, bindings []types.BoundContract) error { - return s.reader.Bind(ctx, bindings) -} - -func (s *loopChainReader) Unbind(ctx context.Context, bindings []types.BoundContract) error { - // we ignore unbind errors, because if the LOOP plugin restarted, the binding would not exist. - _ = s.reader.Unbind(ctx, bindings) - - return nil -} - -func (s *loopChainReader) decodeGLVReturnValue(label string, jsonBytes []byte, returnVal any) error { - var result any - err := json.Unmarshal(jsonBytes, &result) - if err != nil { - return fmt.Errorf("failed to unmarshal %s GetLatestValue result (`%s`): %w", label, string(jsonBytes), err) - } - - err = codec.DecodeSuiJsonValue(result, returnVal) - if err != nil { - return fmt.Errorf("failed to decode %s GetLatestValue JSON value (`%s`) to %T: %w", label, string(jsonBytes), returnVal, err) - } - - return nil + return loop.NewLoopChainReader(log, reader) } diff --git a/relayer/chainreader/loop/loop_reader_test.go b/relayer/chainreader/loop/loop_reader_test.go index d6152e32..449571bf 100644 --- a/relayer/chainreader/loop/loop_reader_test.go +++ b/relayer/chainreader/loop/loop_reader_test.go @@ -1,6 +1,6 @@ //go:build integration -package loop +package loop_test import ( "context" @@ -30,6 +30,8 @@ import ( "github.com/smartcontractkit/chainlink-sui/relayer/client" "github.com/smartcontractkit/chainlink-sui/relayer/codec" "github.com/smartcontractkit/chainlink-sui/relayer/testutils" + + codecLoop "github.com/smartcontractkit/chainlink-sui/codec/loop" ) //nolint:paralleltest @@ -299,7 +301,7 @@ func runLoopChainReaderEchoTest(t *testing.T, log logger.Logger, rpcUrl string) require.NoError(t, err) // Wrap the base chain reader with loop chain reader - loopReader := NewLoopChainReader(log, chainReader) + loopReader := codecLoop.NewLoopChainReader(log, chainReader) // Bind the contracts err = loopReader.Bind(context.Background(), []types.BoundContract{echoBinding, counterBinding}) @@ -579,7 +581,7 @@ func runLoopChainReaderEchoTest(t *testing.T, log logger.Logger, rpcUrl string) // then compares ConfigInfo.ConfigDigest against the DON's digest. // // IMPORTANT: the core node's Sui LOOP reader (suiloop.loopChainReader.decodeGLVReturnValue) does NOT - // plain json.Unmarshal into the typed target. It json.Unmarshals the bytes into a generic map and then + // plain json.Unmarshal into the typed target. It json.Unmarshals the LOOP bytes into a generic map and then // runs codec.DecodeSuiJsonValue (mapstructure with a fuzzy, underscore-insensitive field matcher and // hex/base64 type-conversion hooks). That decoder happily maps the Sui node's snake_case keys and // decodes the base64 digest / 0x-hex transmitters correctly. The one field it can NOT map on its own is diff --git a/relayer/codec/bcs_converter.go b/relayer/codec/bcs_converter.go index 6566fcd3..de01aeca 100644 --- a/relayer/codec/bcs_converter.go +++ b/relayer/codec/bcs_converter.go @@ -1,241 +1,43 @@ package codec import ( - "fmt" - "strconv" - aptosBCS "github.com/aptos-labs/aptos-go-sdk/bcs" + + "github.com/smartcontractkit/chainlink-sui/codec" ) -// BCSPrimitiveHandler defines a function that reads a primitive type from a BCS deserializer -type BCSPrimitiveHandler func(*aptosBCS.Deserializer) (any, error) +// Deprecated: use codec.BCSPrimitiveHandler +// +//go:fix inline +type BCSPrimitiveHandler = codec.BCSPrimitiveHandler -// BCSVectorHandler defines a function that reads a vector type from a BCS deserializer -type BCSVectorHandler func(*aptosBCS.Deserializer, uint64) (any, error) +// Deprecated: use codec.BCSVectorHandler +// +//go:fix inline +type BCSVectorHandler = codec.BCSVectorHandler -// BCSTypeConverter provides a registry-based approach to converting BCS types to JSON-compatible values -type BCSTypeConverter struct { - primitiveHandlers map[string]BCSPrimitiveHandler - vectorHandlers map[string]BCSVectorHandler -} +// Deprecated: use codec.BCSTypeConverter +// +//go:fix inline +type BCSTypeConverter = codec.BCSTypeConverter -// NewBCSTypeConverter creates a new BCS type converter with all standard Sui types registered +// Deprecated: use codec.NewBCSTypeConverter +// +//go:fix inline func NewBCSTypeConverter() *BCSTypeConverter { - c := &BCSTypeConverter{ - primitiveHandlers: make(map[string]BCSPrimitiveHandler), - vectorHandlers: make(map[string]BCSVectorHandler), - } - - // Register primitive type handlers - c.registerPrimitiveHandlers() - c.registerVectorHandlers() - - return c -} - -// registerPrimitiveHandlers registers all standard Sui primitive type handlers -func (c *BCSTypeConverter) registerPrimitiveHandlers() { - // U8 - return as-is - c.RegisterPrimitive("U8", func(d *aptosBCS.Deserializer) (any, error) { - return d.U8(), nil - }) - c.RegisterPrimitive("u8", func(d *aptosBCS.Deserializer) (any, error) { - return d.U8(), nil - }) - - // U16 - return as-is - c.RegisterPrimitive("U16", func(d *aptosBCS.Deserializer) (any, error) { - return d.U16(), nil - }) - c.RegisterPrimitive("u16", func(d *aptosBCS.Deserializer) (any, error) { - return d.U16(), nil - }) - - // U32 - return as-is - c.RegisterPrimitive("U32", func(d *aptosBCS.Deserializer) (any, error) { - return d.U32(), nil - }) - c.RegisterPrimitive("u32", func(d *aptosBCS.Deserializer) (any, error) { - return d.U32(), nil - }) - - // U64 - return as string for JSON compatibility - c.RegisterPrimitive("U64", func(d *aptosBCS.Deserializer) (any, error) { - value := d.U64() - return strconv.FormatUint(value, base10), nil - }) - c.RegisterPrimitive("u64", func(d *aptosBCS.Deserializer) (any, error) { - value := d.U64() - return strconv.FormatUint(value, base10), nil - }) - - // U128 - return as string for JSON compatibility - c.RegisterPrimitive("U128", func(d *aptosBCS.Deserializer) (any, error) { - value := d.U128() - return value.String(), nil - }) - c.RegisterPrimitive("u128", func(d *aptosBCS.Deserializer) (any, error) { - value := d.U128() - return value.String(), nil - }) - - // U256 - return as string for JSON compatibility - c.RegisterPrimitive("U256", func(d *aptosBCS.Deserializer) (any, error) { - value := d.U256() - return value.String(), nil - }) - c.RegisterPrimitive("u256", func(d *aptosBCS.Deserializer) (any, error) { - value := d.U256() - return value.String(), nil - }) - - // Bool - return as-is - c.RegisterPrimitive("Bool", func(d *aptosBCS.Deserializer) (any, error) { - return d.Bool(), nil - }) - c.RegisterPrimitive("bool", func(d *aptosBCS.Deserializer) (any, error) { - return d.Bool(), nil - }) - - // Address - return as byte array - c.RegisterPrimitive("Address", func(d *aptosBCS.Deserializer) (any, error) { - addressBytesLen := 32 - return d.ReadFixedBytes(addressBytesLen), nil - }) - c.RegisterPrimitive("address", func(d *aptosBCS.Deserializer) (any, error) { - addressBytesLen := 32 - return d.ReadFixedBytes(addressBytesLen), nil - }) -} - -// registerVectorHandlers registers standard vector type handlers -func (c *BCSTypeConverter) registerVectorHandlers() { - // Vector - read as byte array - c.RegisterVector("U8", func(d *aptosBCS.Deserializer, length uint64) (any, error) { - bytes := make([]byte, length) - for i := range length { - bytes[i] = d.U8() - } - return bytes, nil - }) - c.RegisterVector("u8", func(d *aptosBCS.Deserializer, length uint64) (any, error) { - bytes := make([]byte, length) - for i := range length { - bytes[i] = d.U8() - } - return bytes, nil - }) - - // Vector - read as uint64 array - c.RegisterVector("U64", func(d *aptosBCS.Deserializer, length uint64) (any, error) { - uint64s := make([]uint64, length) - for i := range length { - uint64s[i] = d.U64() - } - return uint64s, nil - }) - c.RegisterVector("u64", func(d *aptosBCS.Deserializer, length uint64) (any, error) { - uint64s := make([]uint64, length) - for i := range length { - uint64s[i] = d.U64() - } - return uint64s, nil - }) - - // Vector
- read as address array - c.RegisterVector("Address", func(d *aptosBCS.Deserializer, length uint64) (any, error) { - addresses := make([]any, length) - for i := range length { - addressBytesLen := 32 - addresses[i] = d.ReadFixedBytes(addressBytesLen) - } - return addresses, nil - }) - c.RegisterVector("address", func(d *aptosBCS.Deserializer, length uint64) (any, error) { - addresses := make([]any, length) - for i := range length { - addressBytesLen := 32 - addresses[i] = d.ReadFixedBytes(addressBytesLen) - } - return addresses, nil - }) -} - -// RegisterPrimitive registers a handler for a primitive type -func (c *BCSTypeConverter) RegisterPrimitive(typeName string, handler BCSPrimitiveHandler) { - c.primitiveHandlers[typeName] = handler -} - -// RegisterVector registers a handler for a vector type -func (c *BCSTypeConverter) RegisterVector(elementType string, handler BCSVectorHandler) { - c.vectorHandlers[elementType] = handler -} - -// DecodePrimitive decodes a primitive type using the registered handler -func (c *BCSTypeConverter) DecodePrimitive(deserializer *aptosBCS.Deserializer, primitiveType string) (any, error) { - handler, ok := c.primitiveHandlers[primitiveType] - if !ok { - return nil, fmt.Errorf("unsupported primitive type: %s", primitiveType) - } - return handler(deserializer) -} - -// DecodeVector decodes a vector type using the registered handler -func (c *BCSTypeConverter) DecodeVector(deserializer *aptosBCS.Deserializer, elementType string) (any, error) { - length := deserializer.Uleb128() - - handler, ok := c.vectorHandlers[elementType] - if !ok { - // Fall back to generic primitive vector handling - primitiveHandler, primitiveOk := c.primitiveHandlers[elementType] - if !primitiveOk { - return nil, fmt.Errorf("unsupported vector element type: %s", elementType) - } - - // Generic vector of primitives - result := make([]any, length) - for i := range length { - value, err := primitiveHandler(deserializer) - if err != nil { - return nil, fmt.Errorf("failed to decode vector element at index %d: %w", i, err) - } - result[i] = value - } - return result, nil - } - - return handler(deserializer, uint64(length)) -} - -// HasPrimitiveHandler checks if a primitive type handler is registered -func (c *BCSTypeConverter) HasPrimitiveHandler(typeName string) bool { - _, ok := c.primitiveHandlers[typeName] - return ok -} - -// HasVectorHandler checks if a vector type handler is registered -func (c *BCSTypeConverter) HasVectorHandler(elementType string) bool { - _, ok := c.vectorHandlers[elementType] - return ok -} - -// Global BCS converter instance for package-wide use (lazy initialization) -var defaultBCSConverter *BCSTypeConverter - -// getDefaultBCSConverter returns the global BCS converter, initializing it if necessary -func getDefaultBCSConverter() *BCSTypeConverter { - if defaultBCSConverter == nil { - defaultBCSConverter = NewBCSTypeConverter() - } - return defaultBCSConverter + return codec.NewBCSTypeConverter() } -// DecodeBCSPrimitive decodes a BCS primitive type using the default converter +// Deprecated: codec.DecodeBCSPrimitive +// +//go:fix inline func DecodeBCSPrimitive(deserializer *aptosBCS.Deserializer, primitiveType string) (any, error) { - return getDefaultBCSConverter().DecodePrimitive(deserializer, primitiveType) + return codec.DecodeBCSPrimitive(deserializer, primitiveType) } -// DecodeBCSVector decodes a BCS vector type using the default converter +// Deprecated: codec.DecodeBCSVector +// +//go:fix inline func DecodeBCSVector(deserializer *aptosBCS.Deserializer, elementType string) (any, error) { - return getDefaultBCSConverter().DecodeVector(deserializer, elementType) + return codec.DecodeBCSVector(deserializer, elementType) } diff --git a/relayer/codec/decoder.go b/relayer/codec/decoder.go index 10f6cd09..68653360 100644 --- a/relayer/codec/decoder.go +++ b/relayer/codec/decoder.go @@ -1,632 +1,71 @@ package codec import ( - "encoding/base64" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "math" - "reflect" - "strconv" - "strings" - aptosBCS "github.com/aptos-labs/aptos-go-sdk/bcs" - "github.com/block-vision/sui-go-sdk/models" - "github.com/smartcontractkit/chainlink-sui/bindings/bind" + "github.com/smartcontractkit/chainlink-sui/codec" ) -const ( - // Bit and byte constants - byteSize = 8 - uint8Bits = 8 - uint64Bits = 64 - bits128 = 128 - bits256 = 256 - - // Number bases - base10 = 10 - base16 = 16 - base2 = 2 - - // Response parsing constants - maxByteValue = 255 - - // proofWireBytes is the fixed on-wire size of each merkle proof element. - proofWireBytes = 32 - - // tokenTransferMinWireBytes is the minimum BCS size of Any2SuiTokenTransfer: - // uleb128(0) source pool + 32 dest token + 4 dest gas + uleb128(0) extra data + 32 amount. - tokenTransferMinWireBytes = 70 -) -// DecodeSuiJsonValue decodes Sui JSON response data into the provided target. +// Deprecated: use codec.DecodeSuiJsonValue +// +//go:fix inline func DecodeSuiJsonValue(data any, target any) error { - return bind.DecodeJSONReturn(data, target) + return codec.DecodeSuiJsonValue(data, target) } -// ConvertBase64StringsToHex walks arbitrary JSON-like structures and converts any -// base64-encoded strings into 0x-prefixed hex strings, preserving []byte slices. +// Deprecated: use codec.ConvertBase64StringsToHex +// +//go:fix inline func ConvertBase64StringsToHex(data any) any { - switch v := data.(type) { - case nil: - return nil - case string: - // check if the string is entirely numeric - if _, err := strconv.ParseUint(v, 10, 64); err == nil { - return v - } - - decoded, err := base64.StdEncoding.DecodeString(v) - if err == nil && len(decoded) > 0 { - return "0x" + hex.EncodeToString(decoded) - } - return v - case json.RawMessage: - var inner any - if err := json.Unmarshal(v, &inner); err != nil { - return v - } - return ConvertBase64StringsToHex(inner) - case map[string]any: - result := make(map[string]any, len(v)) - for key, value := range v { - result[key] = ConvertBase64StringsToHex(value) - } - return result - case []any: - result := make([]any, len(v)) - for i, value := range v { - result[i] = ConvertBase64StringsToHex(value) - } - return result - default: - rv := reflect.ValueOf(data) - if rv.Kind() == reflect.Slice { - // Preserve []byte and other byte slices as-is - if rv.Type().Elem().Kind() == reflect.Uint8 { - return data - } - - result := make([]any, rv.Len()) - for i := 0; i < rv.Len(); i++ { - result[i] = ConvertBase64StringsToHex(rv.Index(i).Interface()) - } - return result - } - - if rv.Kind() == reflect.Map && rv.Type().Key().Kind() == reflect.String { - result := make(map[string]any, rv.Len()) - iter := rv.MapRange() - for iter.Next() { - result[iter.Key().String()] = ConvertBase64StringsToHex(iter.Value().Interface()) - } - return result - } - } - - return data + return codec.ConvertBase64StringsToHex(data) } -// DecodeSuiStructToJSON decodes a Sui struct into a JSON object -// using the normalized struct and the result +// Deprecated: use codec.DecodeSuiStructToJSON +// +//go:fix inline func DecodeSuiStructToJSON(normalizedStructs map[string]any, identifier string, bcsDecoder *aptosBCS.Deserializer) (map[string]any, error) { - jsonResult := make(map[string]any) - - normalizedStruct, ok := normalizedStructs[identifier].(map[string]any) - if !ok { - return nil, fmt.Errorf("struct with identifier '%s' not found in normalized structs", identifier) - } - - fields, ok := normalizedStruct["fields"].([]any) - if !ok { - return nil, fmt.Errorf("fields not found for struct '%s'", identifier) - } - - for _, field := range fields { - fieldMap, ok := field.(map[string]any) - if !ok { - continue - } - - fieldName, ok := fieldMap["name"].(string) - if !ok { - continue - } - - fieldType := fieldMap["type"] - - // Handle different field types based on the new format - switch v := fieldType.(type) { - case string: - // Primitive types like "U64", "Bool", "Address" - value, err := getDefaultBCSConverter().DecodePrimitive(bcsDecoder, v) - if err != nil { - return nil, fmt.Errorf("failed to decode primitive field %s: %w", fieldName, err) - } - jsonResult[fieldName] = value - - case map[string]any: - if vectorType, exists := v["Vector"]; exists { - // Vector type - decodedVector, err := decodeVectorField(bcsDecoder, vectorType, normalizedStructs) - if err != nil { - return nil, fmt.Errorf("failed to decode vector field %s: %w", fieldName, err) - } - jsonResult[fieldName] = decodedVector - } else if structType, exists := v["Struct"]; exists { - // Struct type - structMap, ok := structType.(map[string]any) - if !ok { - return nil, fmt.Errorf("invalid struct type for field %s", fieldName) - } - structName, ok := structMap["name"].(string) - if !ok { - return nil, fmt.Errorf("struct name not found for field %s", fieldName) - } - - // Special case for String struct - it's a primitive type in Sui - if structName == "String" { - jsonResult[fieldName] = bcsDecoder.ReadString() - } else { - inner, err := DecodeSuiStructToJSON(normalizedStructs, structName, bcsDecoder) - if err != nil { - return nil, fmt.Errorf("failed to decode struct field %s: %w", fieldName, err) - } - jsonResult[fieldName] = inner - } - } - } - } - - return jsonResult, nil -} - -func decodeVectorField(bcsDecoder *aptosBCS.Deserializer, vectorType any, normalizedStructs map[string]any) (any, error) { - // Read the length of the vector first - vectorLength := bcsDecoder.Uleb128() - - switch v := vectorType.(type) { - case string: - // Try to use the BCS converter for registered vector types - if getDefaultBCSConverter().HasVectorHandler(v) { - // Use the registered vector handler which will handle the length internally - // We need to "rewind" by putting the length back since the handler expects to read it - // Actually, the handler receives the length as a parameter, so we're good - handler, _ := getDefaultBCSConverter().vectorHandlers[v] - return handler(bcsDecoder, uint64(vectorLength)) - } - - // Fall back to generic primitive vector handling - if getDefaultBCSConverter().HasPrimitiveHandler(v) { - primitiveVector := make([]any, vectorLength) - for i := range vectorLength { - value, err := getDefaultBCSConverter().DecodePrimitive(bcsDecoder, v) - if err != nil { - return nil, fmt.Errorf("failed to decode primitive vector element at index %d: %w", i, err) - } - primitiveVector[i] = value - } - return primitiveVector, nil - } - - return nil, fmt.Errorf("unsupported vector element type: %s", v) - - case map[string]any: - if innerVectorType, exists := v["Vector"]; exists { - // This is vector> - recursively decode each inner vector - outerVector := make([]any, vectorLength) - for i := range vectorLength { - innerResult, err := decodeVectorField(bcsDecoder, innerVectorType, normalizedStructs) - if err != nil { - return nil, fmt.Errorf("failed to decode inner vector at index %d: %w", i, err) - } - outerVector[i] = innerResult - } - - return outerVector, nil - } else if structType, exists := v["Struct"]; exists { - // This is vector - decode each struct - structVector := make([]any, vectorLength) - structMap, ok := structType.(map[string]any) - if !ok { - return nil, fmt.Errorf("invalid struct type in vector") - } - structName, ok := structMap["name"].(string) - if !ok { - return nil, fmt.Errorf("struct name not found in vector element") - } - - // this is a special case where strings are defined as a struct in Sui normalized module structs definition - if structName == "String" { - vecOfStrings := make([]any, vectorLength) - for i := range vectorLength { - vecOfStrings[i] = bcsDecoder.ReadString() - } - - return vecOfStrings, nil - } - - for i := range vectorLength { - structResult, err := DecodeSuiStructToJSON(normalizedStructs, structName, bcsDecoder) - if err != nil { - return nil, fmt.Errorf("failed to decode struct at index %d: %w", i, err) - } - structVector[i] = structResult - } - - return structVector, nil - } - } - - return nil, fmt.Errorf("unsupported vector type: %v", vectorType) + return codec.DecodeSuiStructToJSON(normalizedStructs, identifier, bcsDecoder) } +// Deprecated: use codec.DecodeSuiPrimative +// +//go:fix inline func DecodeSuiPrimative(bcsDecoder *aptosBCS.Deserializer, primativeType string) (any, error) { - // Try to decode as primitive using the BCS converter registry - converter := getDefaultBCSConverter() - if converter.HasPrimitiveHandler(primativeType) { - return converter.DecodePrimitive(bcsDecoder, primativeType) - } - - // Handle vector types - if strings.HasPrefix(primativeType, "vector<") && strings.HasSuffix(primativeType, ">") { - innerType := strings.TrimSuffix(strings.TrimPrefix(primativeType, "vector<"), ">") - - // Handle simple vector types - if converter.HasVectorHandler(innerType) || converter.HasPrimitiveHandler(innerType) { - return decodeVectorField(bcsDecoder, innerType, nil) - } - - // Handle nested vector types (e.g., vector>) - if innerType == "vector" || innerType == "vector" { - return decodeVectorField(bcsDecoder, map[string]any{"Vector": "U8"}, nil) - } - } - - return nil, fmt.Errorf("unsupported BCS primitive type: %s", primativeType) + return codec.DecodeSuiPrimative(bcsDecoder, primativeType) } -// DecodeVectorOfStructs decodes a vector of structs from BCS bytes -// vectorType should be in format "vector<0xpackage::module::StructName>" +// Deprecated: use codec.DecodeVectorOfStructs +// +//go:fix inline func DecodeVectorOfStructs(bcsDecoder *aptosBCS.Deserializer, vectorType string, normalizedStructs map[string]any) (any, error) { - // Check if it's actually a vector type - if !strings.HasPrefix(vectorType, "vector<") || !strings.HasSuffix(vectorType, ">") { - return nil, fmt.Errorf("not a vector type: %s", vectorType) - } - - // Extract inner type - innerType := strings.TrimSuffix(strings.TrimPrefix(vectorType, "vector<"), ">") - - // Check if inner type is a struct (has 3 parts when split by ::) - structParts := strings.Split(innerType, "::") - if len(structParts) != 3 { - return nil, fmt.Errorf("inner type is not a struct: %s", innerType) - } - - structName := structParts[2] - - // Create vector type definition compatible with decodeVectorField - vectorTypedef := map[string]any{ - "Struct": map[string]any{ - "name": structName, - }, - } - - return decodeVectorField(bcsDecoder, vectorTypedef, normalizedStructs) + return codec.DecodeVectorOfStructs(bcsDecoder, vectorType, normalizedStructs) } -// numericToBytes converts a number to byte slice (little-endian) -// Used by type_converters.go -func numericToBytes(num uint64) []byte { - bytes := make([]byte, uint64Bits/uint8Bits) - for i := range uint8Bits { - bytes[i] = byte(num >> (i * uint8Bits)) - } - // Remove trailing zeros - for len(bytes) > 1 && bytes[len(bytes)-1] == 0 { - bytes = bytes[:len(bytes)-1] - } - - return bytes -} - -// AnySliceToBytes converts slice of interface{} to byte slice +// Deprecated: use codec.AnySliceToBytes +// +//go:fix inline func AnySliceToBytes(src []any) ([]byte, error) { - dst := make([]byte, len(src)) - for i, v := range src { - //nolint:exhaustive - switch x := v.(type) { - case uint8: - dst[i] = x - case int: - if x < 0 || x > maxByteValue { - return nil, fmt.Errorf("element %d: int %d out of byte range", i, x) - } - dst[i] = byte(x) - case uint: - if x > maxByteValue { - return nil, fmt.Errorf("element %d: uint %d out of byte range", i, x) - } - dst[i] = byte(x) - case float64: - if x > maxByteValue { - return nil, fmt.Errorf("element %d: float64 %f out of byte range", i, x) - } - dst[i] = byte(x) - default: - return nil, fmt.Errorf("element %d: unsupported type %T", i, v) - } - } - - return dst, nil -} - -// handleSingleFieldStruct processes structs with single fields -// This is kept here for backward compatibility but the main implementation is in type_converters.go -func handleSingleFieldStruct(t reflect.Type, data any, decodeFn func(any, any) error) (any, error) { - field := t.Field(0) - newStructVal := reflect.New(t).Elem() - fieldPtr := newStructVal.Field(0).Addr().Interface() - - if err := decodeFn(data, fieldPtr); err != nil { - return nil, fmt.Errorf("failed decoding for single-field struct %v field %s (%v): %w", - t, field.Name, field.Type, err) - } - - return newStructVal.Interface(), nil -} - -// Overflow checking functions -func overflowFloat(t reflect.Type, x float64) bool { - //nolint:exhaustive - switch t.Kind() { - case reflect.Float32: - return overflowFloat32(x) - case reflect.Float64: - return false - default: - panic("reflect: OverflowFloat of non-float type " + t.String()) - } -} - -func overflowFloat32(x float64) bool { - if x < 0 { - x = -x - } - - return math.MaxFloat32 < x && x <= math.MaxFloat64 -} - -func overflowInt(t reflect.Type, x int64) bool { - //nolint:exhaustive - switch t.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - bitSize := t.Size() * uint8Bits - trunc := (x << (uint64Bits - bitSize)) >> (uint64Bits - bitSize) - - return x != trunc - default: - panic("reflect: OverflowInt of non-int type " + t.String()) - } -} - -func overflowUint(t reflect.Type, x uint64) bool { - //nolint:exhaustive - switch t.Kind() { - case reflect.Uint, reflect.Uintptr, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - bitSize := t.Size() * uint8Bits - trunc := (x << (uint64Bits - bitSize)) >> (uint64Bits - bitSize) - - return x != trunc - default: - panic("reflect: OverflowUint of non-uint type " + t.String()) - } + return codec.AnySliceToBytes(src) } +// Deprecated: use codec.DeserializeExecutionReport +// +//go:fix inline func DeserializeExecutionReport(data []byte) (*ExecutionReport, error) { - deserializer := aptosBCS.NewDeserializer(data) - - // 1. Read source_chain_selector (u64) - sourceChainSelector := deserializer.U64() - if err := deserializer.Error(); err != nil { - return nil, fmt.Errorf("failed to deserialize sourceChainSelector: %w", err) - } - - // 2. Read message header - messageID := make([]byte, 32) - deserializer.ReadFixedBytesInto(messageID) - if err := deserializer.Error(); err != nil { - return nil, fmt.Errorf("failed to deserialize messageID: %w", err) - } - - headerSourceChain := deserializer.U64() - if err := deserializer.Error(); err != nil { - return nil, fmt.Errorf("failed to deserialize headerSourceChain: %w", err) - } - - destChainSelector := deserializer.U64() - if err := deserializer.Error(); err != nil { - return nil, fmt.Errorf("failed to deserialize destChainSelector: %w", err) - } - - sequenceNumber := deserializer.U64() - if err := deserializer.Error(); err != nil { - return nil, fmt.Errorf("failed to deserialize sequenceNumber: %w", err) - } - - nonce := deserializer.U64() - if err := deserializer.Error(); err != nil { - return nil, fmt.Errorf("failed to deserialize nonce: %w", err) - } - - if sourceChainSelector != headerSourceChain { - return nil, fmt.Errorf("source chain selector mismatch: %d != %d", sourceChainSelector, headerSourceChain) - } - - header := RampMessageHeader{ - MessageID: messageID, - SourceChainSelector: headerSourceChain, - DestChainSelector: destChainSelector, - SequenceNumber: sequenceNumber, - Nonce: nonce, - } - - // 3. Read sender (vector) - sender := deserializer.ReadBytes() - if err := deserializer.Error(); err != nil { - return nil, fmt.Errorf("failed to deserialize sender: %w", err) - } - - // 4. Read data (vector) - msgData := deserializer.ReadBytes() - if err := deserializer.Error(); err != nil { - return nil, fmt.Errorf("failed to deserialize data: %w", err) - } - - // 5. Read receiver (address) - receiver := deserializer.ReadFixedBytes(32) - if err := deserializer.Error(); err != nil { - return nil, fmt.Errorf("failed to deserialize receiver: %w", err) - } - - // 6. Read gas_limit (u256) - gasLimit := deserializer.U256() - if err := deserializer.Error(); err != nil { - return nil, fmt.Errorf("failed to deserialize gas_limit: %w", err) - } - - tokenReceiver := [32]byte{} - deserializer.ReadFixedBytesInto(tokenReceiver[:]) - if err := deserializer.Error(); err != nil { - return nil, fmt.Errorf("failed to deserialize tokenReceiver: %w", err) - } - - // 7. Read token_amounts vector - tokenAmountsLen := deserializer.Uleb128() - if err := deserializer.Error(); err != nil { - return nil, fmt.Errorf("failed to deserialize token_amounts length: %w", err) - } - remaining := deserializer.Remaining() - if remaining < 0 || uint64(tokenAmountsLen)*tokenTransferMinWireBytes > uint64(remaining) { - return nil, fmt.Errorf("failed to deserialize execution report: token_amounts length %d exceeds remaining %d bytes", tokenAmountsLen, remaining) - } - tokenAmounts := make([]Any2SuiTokenTransfer, tokenAmountsLen) - - for i := range tokenAmountsLen { - sourcePoolAddr := deserializer.ReadBytes() - if err := deserializer.Error(); err != nil { - return nil, fmt.Errorf("failed to deserialize sourcePoolAddr: %w", err) - } - - destToken := deserializer.ReadFixedBytes(32) - if err := deserializer.Error(); err != nil { - return nil, fmt.Errorf("failed to deserialize destToken: %w", err) - } - - destGas := deserializer.U32() - if err := deserializer.Error(); err != nil { - return nil, fmt.Errorf("failed to deserialize destGas: %w", err) - } - extraData := deserializer.ReadBytes() - if err := deserializer.Error(); err != nil { - return nil, fmt.Errorf("failed to deserialize extraData: %w", err) - } - amount := deserializer.U256() - if err := deserializer.Error(); err != nil { - return nil, fmt.Errorf("failed to deserialize amount: %w", err) - } - - tokenAmounts[i] = Any2SuiTokenTransfer{ - SourcePoolAddress: sourcePoolAddr, - DestTokenAddress: models.SuiAddress(hex.EncodeToString(destToken)), - DestGasAmount: destGas, - ExtraData: extraData, - Amount: &amount, - } - } - - message := Any2SuiRampMessage{ - Header: header, - Sender: sender, - Data: msgData, - Receiver: models.SuiAddress(hex.EncodeToString(receiver)), - GasLimit: &gasLimit, - TokenReceiver: models.SuiAddressBytes(tokenReceiver), - TokenAmounts: tokenAmounts, - } - - // 8. Read offchain_token_data (vector>) - offchainDataLen := deserializer.Uleb128() - if err := deserializer.Error(); err != nil { - return nil, fmt.Errorf("failed to deserialize offchain_token_data length: %w", err) - } - if int(offchainDataLen) > deserializer.Remaining() { - return nil, fmt.Errorf("failed to deserialize execution report: offchain_token_data length %d exceeds remaining %d bytes", offchainDataLen, deserializer.Remaining()) - } - offchainData := make([][]byte, offchainDataLen) - - for i := range offchainDataLen { - offchainData[i] = deserializer.ReadBytes() - if err := deserializer.Error(); err != nil { - return nil, fmt.Errorf("failed to deserialize offchainData at index %d: %w", i, err) - } - } - - // 9. Read proofs (vector>) - proofsLen := deserializer.Uleb128() - if err := deserializer.Error(); err != nil { - return nil, fmt.Errorf("failed to deserialize proofs length: %w", err) - } - remaining = deserializer.Remaining() - if remaining < 0 || uint64(proofsLen)*proofWireBytes > uint64(remaining) { - return nil, fmt.Errorf("failed to deserialize execution report: proofs length %d exceeds remaining %d bytes", proofsLen, remaining) - } - proofs := make([][]byte, proofsLen) - - for i := range proofsLen { - proofs[i] = deserializer.ReadFixedBytes(proofWireBytes) - if err := deserializer.Error(); err != nil { - return nil, fmt.Errorf("failed to deserialize proof at index %d: %w", i, err) - } - } - - if err := deserializer.Error(); err != nil { - return nil, fmt.Errorf("failed to deserialize execution report: %w", err) - } - - return &ExecutionReport{ - SourceChainSelector: sourceChainSelector, - Message: message, - OffchainTokenData: offchainData, - Proofs: proofs, - }, nil + return codec.DeserializeExecutionReport(data) } -// UnwrapBCSPureBytes decodes a BCS-encoded pure input value stored on-chain. -// Pure vector arguments are stored with a ULEB128 length prefix. +// Deprecated: use codec.UnwrapBCSPureBytes +// +//go:fix inline func UnwrapBCSPureBytes(pure []byte) ([]byte, error) { - if len(pure) == 0 { - return nil, errors.New("pure bytes are empty") - } - - deserializer := aptosBCS.NewDeserializer(pure) - unwrapped := deserializer.ReadBytes() - if err := deserializer.Error(); err != nil { - return nil, fmt.Errorf("failed to unwrap pure bytes: %w", err) - } - - return unwrapped, nil + return codec.UnwrapBCSPureBytes(pure) } -// DeserializeExecutionReportFromPure deserializes an execution report from a -// BCS-encoded pure input containing vector. +// Deprecated: use DeserializeExecutionReportFromPure +// +//go:fix inline func DeserializeExecutionReportFromPure(pure []byte) (*ExecutionReport, error) { - reportBytes, err := UnwrapBCSPureBytes(pure) - if err != nil { - return nil, err - } - - return DeserializeExecutionReport(reportBytes) + return codec.DeserializeExecutionReportFromPure(pure) } diff --git a/relayer/codec/encoder.go b/relayer/codec/encoder.go index cdae8770..86940d74 100644 --- a/relayer/codec/encoder.go +++ b/relayer/codec/encoder.go @@ -1,368 +1,10 @@ package codec -import ( - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "math/big" - "reflect" - "strconv" - "strings" - - "github.com/smartcontractkit/chainlink-sui/bindings/bind" -) - -const ( - // Type bounds - maxUint8 = 255 - maxUint16 = 65535 - maxUint32 = 4294967295 -) - -// Safe conversion functions to avoid lint issues -func safeUint8(val uint64) (uint8, error) { - if val > maxUint8 { - return 0, fmt.Errorf("value %d exceeds uint8 maximum", val) - } - - return uint8(val), nil -} - -func safeUint16(val uint64) (uint16, error) { - if val > maxUint16 { - return 0, fmt.Errorf("value %d exceeds uint16 maximum", val) - } - - return uint16(val), nil -} - -func safeUint32(val uint64) (uint32, error) { - if val > maxUint32 { - return 0, fmt.Errorf("value %d exceeds uint32 maximum", val) - } - - return uint32(val), nil -} +import "github.com/smartcontractkit/chainlink-sui/codec" +// Deprecated: use codec.EncodeToSuiValue +// +//go:fix inline func EncodeToSuiValue(typeName string, value any) (any, error) { - // Normalize fully-qualified Sui string type to logical "string" - if strings.Contains(typeName, "::string::String") { - typeName = "string" - } - - switch typeName { - case "address": - return encodeAddress(value) - case "object_id": - return encodeObject(value) - case "u8", "u16", "u32", "u64", "u128", "u256": - return encodeUint(typeName, value) - case "bool": - return encodeBool(value) - case "string": - return encodeString(value) - default: - if strings.HasPrefix(typeName, "0x") && strings.Contains(typeName, "::") { - // TODO: need to use go-bsc to encode this. Reference https://github.com/fardream/go-bcs/blob/main/bcs/encode_test.go - return nil, errors.New("struct types are not supported") - } - if strings.HasPrefix(typeName, "vector<") && strings.HasSuffix(typeName, ">") { - return encodeVector(typeName, value) - } - - return nil, fmt.Errorf("unsupported type: %s", typeName) - } -} - -func encodeObject(value any) (bind.Object, error) { - switch v := value.(type) { - case bind.Object: - return bind.Object{Id: v.Id}, nil - default: - return bind.Object{}, fmt.Errorf("cannot convert %T to object", value) - } -} - -func encodeAddress(value any) (string, error) { - switch v := value.(type) { - case string: - // Ensure it's a valid Sui address format - if !strings.HasPrefix(v, "0x") { - v = "0x" + v - } - - normalizedAddress, err := bind.ToSuiAddress(v) - if err != nil { - return "", fmt.Errorf("failed to convert address to Sui address: %w", err) - } - - return normalizedAddress, nil - case []byte: - stringAddr := "0x" + hex.EncodeToString(v) - - normalizedAddress, err := bind.ToSuiAddress(stringAddr) - if err != nil { - return "", fmt.Errorf("failed to convert address to Sui address: %w", err) - } - - return normalizedAddress, nil - default: - return "", fmt.Errorf("cannot convert %T to address", value) - } -} - -func encodeUint(typeName string, value any) (any, error) { - // First convert to a common intermediate type - var baseValue uint64 - var bigIntValue *big.Int - - switch v := value.(type) { - case int: - if v < 0 { - return nil, fmt.Errorf("cannot convert negative int %d to %s", v, typeName) - } - baseValue = uint64(v) - case int64: - if v < 0 { - return nil, fmt.Errorf("cannot convert negative int %d to %s", v, typeName) - } - baseValue = uint64(v) - case uint: - baseValue = uint64(v) - case uint64: - baseValue = v - case uint32: - baseValue = uint64(v) - case uint16: - baseValue = uint64(v) - case uint8: - baseValue = uint64(v) - case float64: - if v < 0 { - return nil, fmt.Errorf("cannot convert negative float %f to %s", v, typeName) - } - baseValue = uint64(v) - case json.Number: - // Handle JSON numbers properly - numStr := v.String() - if typeName == "u128" || typeName == "u256" { - bigIntValue = new(big.Int) - if _, ok := bigIntValue.SetString(numStr, base10); !ok { - return nil, fmt.Errorf("cannot convert json.Number %s to %s", v, typeName) - } - } else if strings.Contains(numStr, ".") { - f, err := v.Float64() - if err != nil { - return nil, fmt.Errorf("cannot convert json.Number %s to %s: %w", v, typeName, err) - } - baseValue = uint64(f) - } else { - // json parsing int64 for selector was going out of range - i, err := strconv.ParseUint(numStr, base10, 64) - if err != nil { - return nil, fmt.Errorf("cannot convert json.Number %s to %s: %w", v, typeName, err) - } - //nolint:gosec - // we assume safe conversion without negative numbers - baseValue = uint64(i) - } - case string: - // Handle big numbers that might come as strings - if typeName == "u128" || typeName == "u256" { - bigIntValue = new(big.Int) - _, ok := bigIntValue.SetString(v, base10) - if !ok { - return nil, fmt.Errorf("cannot convert string %s to %s", v, typeName) - } - } else { - i, err := strconv.ParseUint(v, base10, 64) - if err != nil { - return nil, fmt.Errorf("cannot convert string %s to %s: %w", v, typeName, err) - } - baseValue = i - } - case *big.Int: - bigIntValue = v - default: - return nil, fmt.Errorf("cannot convert %T to %s", value, typeName) - } - - // Now convert to the appropriate type based on typeName - switch typeName { - case "u8": - if bigIntValue != nil { - if !bigIntValue.IsUint64() || bigIntValue.Uint64() > maxUint8 { - return nil, fmt.Errorf("value %s too large for u8", bigIntValue.String()) - } - // Safe conversion after bounds check - return safeUint8(bigIntValue.Uint64()) - } - if baseValue > maxUint8 { - return nil, fmt.Errorf("value %d too large for u8", baseValue) - } - - return safeUint8(baseValue) - case "u16": - if bigIntValue != nil { - if !bigIntValue.IsUint64() || bigIntValue.Uint64() > maxUint16 { - return nil, fmt.Errorf("value %s too large for u16", bigIntValue.String()) - } - // Safe conversion after bounds check - return safeUint16(bigIntValue.Uint64()) - } - if baseValue > maxUint16 { - return nil, fmt.Errorf("value %d too large for u16", baseValue) - } - - return safeUint16(baseValue) - case "u32": - if bigIntValue != nil { - if !bigIntValue.IsUint64() || bigIntValue.Uint64() > maxUint32 { - return nil, fmt.Errorf("value %s too large for u32", bigIntValue.String()) - } - // Safe conversion after bounds check - return safeUint32(bigIntValue.Uint64()) - } - if baseValue > maxUint32 { - return nil, fmt.Errorf("value %d too large for u32", baseValue) - } - - return safeUint32(baseValue) - case "u64": - if bigIntValue != nil { - if !bigIntValue.IsUint64() { - return nil, fmt.Errorf("value %s too large for u64", bigIntValue.String()) - } - - return bigIntValue.Uint64(), nil - } - - return baseValue, nil - case "u128": - if bigIntValue != nil { - // Validate it fits in u128 (2^128 - 1) - maxVal := new(big.Int) - maxVal.Exp(big.NewInt(base2), big.NewInt(bits128), nil) - maxVal.Sub(maxVal, big.NewInt(1)) - if bigIntValue.Cmp(maxVal) > 0 { - return nil, fmt.Errorf("value %s too large for u128", bigIntValue.String()) - } - - return bigIntValue.String(), nil - } - - return strconv.FormatUint(baseValue, base10), nil - case "u256": - if bigIntValue != nil { - // Validate it fits in u256 (2^256 - 1) - maxVal := new(big.Int) - maxVal.Exp(big.NewInt(base2), big.NewInt(bits256), nil) - maxVal.Sub(maxVal, big.NewInt(1)) - if bigIntValue.Cmp(maxVal) > 0 { - return nil, fmt.Errorf("value %s too large for u256", bigIntValue.String()) - } - - return bigIntValue.String(), nil - } - - return strconv.FormatUint(baseValue, base10), nil - default: - return nil, fmt.Errorf("unsupported uint type: %s", typeName) - } -} - -func encodeBool(value any) (bool, error) { - switch v := value.(type) { - case bool: - return v, nil - case string: - b, err := strconv.ParseBool(v) - if err != nil { - return false, fmt.Errorf("cannot convert string %s to bool: %w", v, err) - } - - return b, nil - case int: - return v != 0, nil - case float64: - return v != 0, nil - default: - return false, fmt.Errorf("cannot convert %T to bool", value) - } -} - -func encodeString(value any) (string, error) { - switch v := value.(type) { - case string: - return v, nil - case []byte: - return string(v), nil - default: - return "", fmt.Errorf("cannot convert %T to string", value) - } -} - -func encodeVector(typeName string, value any) ([]any, error) { - // Extract the inner type, e.g., "vector" -> "string" - if !strings.HasPrefix(typeName, "vector<") || !strings.HasSuffix(typeName, ">") { - return nil, fmt.Errorf("invalid vector type: %s", typeName) - } - innerType := typeName[len("vector<") : len(typeName)-1] - - if value == nil { - return nil, fmt.Errorf("nil value cannot be encoded in encodeVector") - } - rv := reflect.ValueOf(value) - - if !rv.IsValid() { - return nil, fmt.Errorf("invalid reflect value for vector type %s in encodeVector", typeName) - } - kind := rv.Kind() - - // Use reflection to ensure 'value' is a slice or array - if kind != reflect.Slice && kind != reflect.Array { - return nil, fmt.Errorf("expected a slice/array for vector type %s, got %T", typeName, value) - } - - // Special handling for vector (byte arrays) - if innerType == "u8" { - // Handle []any from JSON unmarshaling - if interfaceSlice, ok := value.([]any); ok { - bytes := make([]byte, len(interfaceSlice)) - for i, item := range interfaceSlice { - if num, ok := item.(float64); ok { - if num < 0 || num > maxUint8 { - return nil, fmt.Errorf("invalid byte value at index %d: %f", i, num) - } - bytes[i] = byte(num) - } else if num, ok := item.(int); ok { - if num < 0 || num > maxUint8 { - return nil, fmt.Errorf("invalid byte value at index %d: %d", i, num) - } - bytes[i] = byte(num) - } else { - return nil, fmt.Errorf("invalid byte value at index %d: %T", i, item) - } - } - - return []any{bytes}, nil - } - // Handle []byte directly - if bytes, ok := value.([]byte); ok { - return []any{bytes}, nil - } - } - - encodedElements := make([]any, 0, rv.Len()) - for i := range rv.Len() { - elem := rv.Index(i).Interface() - encodedElem, err := EncodeToSuiValue(innerType, elem) - if err != nil { - return nil, fmt.Errorf("failed to encode element at index %d: %w", i, err) - } - encodedElements = append(encodedElements, encodedElem) - } - - return encodedElements, nil + return codec.EncodeToSuiValue(typeName, value) } diff --git a/relayer/codec/type_converters.go b/relayer/codec/type_converters.go index c330c21d..b9c9d319 100644 --- a/relayer/codec/type_converters.go +++ b/relayer/codec/type_converters.go @@ -1,775 +1,38 @@ package codec import ( - "encoding/base64" - "encoding/hex" - "encoding/json" - "fmt" - "math/big" "reflect" - "strconv" - "strings" + + "github.com/smartcontractkit/chainlink-sui/codec" ) -// TypeConversionFunc defines a function that converts data from one type to another -type TypeConversionFunc func(from reflect.Type, to reflect.Type, data any) (any, error) +// Deprecated: use codec.TypeConversionFunc +// +//go:fix inline +type TypeConversionFunc = codec.TypeConversionFunc -// TypeConverter provides a registry approach to type conversions for mapstructure -type TypeConverter struct { - converters map[string]TypeConversionFunc -} +// Deprecated: use codec.TypeConverter +// +//go:fix inline +type TypeConverter = codec.TypeConverter -// NewTypeConverter creates a new type converter with all standard conversions registered +// Deprecated: use codec.NewTypeConverter +// +//go:fix inline func NewTypeConverter() *TypeConverter { - tc := &TypeConverter{ - converters: make(map[string]TypeConversionFunc), - } - - tc.registerStandardConverters() - return tc -} - -// registerStandardConverters registers all standard type conversion handlers -func (tc *TypeConverter) registerStandardConverters() { - // Hex string conversions - tc.RegisterConverter("hex_string_to_string", tc.hexToString) - tc.RegisterConverter("hex_string_to_bytes", tc.hexToBytes) - tc.RegisterConverter("hex_string_to_uint", tc.hexToUint) - tc.RegisterConverter("hex_string_to_int", tc.hexToInt) - tc.RegisterConverter("hex_string_to_bigint", tc.hexToBigInt) - tc.RegisterConverter("hex_string_to_array", tc.hexToArray) - - // Base64 string conversions - tc.RegisterConverter("base64_string_to_bytes", tc.base64ToBytes) - - // Numeric string conversions - tc.RegisterConverter("string_to_int", tc.stringToInt) - tc.RegisterConverter("string_to_uint", tc.stringToUint) - tc.RegisterConverter("string_to_float", tc.stringToFloat) - tc.RegisterConverter("string_to_bytes", tc.stringToBytes) - tc.RegisterConverter("string_to_bigint", tc.stringToBigInt) - - // Boolean conversions - tc.RegisterConverter("bool_to_int", tc.boolToInt) - tc.RegisterConverter("bool_to_uint", tc.boolToUint) - tc.RegisterConverter("bool_to_bigint", tc.boolToBigInt) - - // Array/Slice conversions - tc.RegisterConverter("slice_to_slice", tc.sliceToSlice) - tc.RegisterConverter("slice_to_hex_string", tc.sliceToHexString) - - // Float64 conversions (JSON unmarshals numbers as float64) - tc.RegisterConverter("float64_to_uint", tc.float64ToUint) - tc.RegisterConverter("float64_to_int", tc.float64ToInt) - - // json.Number conversions - tc.RegisterConverter("json_number_to_uint", tc.jsonNumberToUint) - tc.RegisterConverter("json_number_to_int", tc.jsonNumberToInt) - - // Bytes to numeric conversions (for BCS decoding) - tc.RegisterConverter("bytes_to_uint", tc.bytesToUint) - tc.RegisterConverter("slice_any_to_uint", tc.sliceAnyToUint) -} - -// RegisterConverter registers a conversion function with a unique key -func (tc *TypeConverter) RegisterConverter(key string, fn TypeConversionFunc) { - tc.converters[key] = fn -} - -// Convert attempts to convert data using registered converters -func (tc *TypeConverter) Convert(from reflect.Type, to reflect.Type, data any) (any, error) { - // Try float64 conversions (JSON unmarshals numbers as float64) - if from.Kind() == reflect.Float64 { - result, err, handled := tc.handleFloat64(data.(float64), to) - if handled { - return result, err - } - } - - // Try json.Number conversions - if _, ok := data.(json.Number); ok { - result, err, handled := tc.handleJSONNumber(data.(json.Number), to) - if handled { - return result, err - } - } - - // Try hex string conversions - if from.Kind() == reflect.String { - if str, ok := data.(string); ok && strings.HasPrefix(str, "0x") { - result, err, handled := tc.handleHexString(str, to, data) - if handled { - return result, err - } - } - } - - // Try numeric string conversions (before base64, as numeric strings can be valid base64) - if from.Kind() == reflect.String { - result, err, handled := tc.handleNumericString(data.(string), to) - if handled { - return result, err - } - } - - // Try base64 conversions (fallback for non-numeric strings to []byte) - if from.Kind() == reflect.String && (to.Kind() == reflect.Slice || to.Kind() == reflect.Array) && to.Elem().Kind() == reflect.Uint8 { - result, err, handled := tc.handleBase64String(data.(string), to) - if handled { - return result, err - } - } - - // Try boolean conversions - if from.Kind() == reflect.Bool { - result, err, handled := tc.handleBoolean(data.(bool), to) - if handled { - return result, err - } - } - - // Try bytes/slice to numeric conversions - if from.Kind() == reflect.Slice || from.Kind() == reflect.Array { - // Handle []byte or []any to numeric - if isNumericTarget(to) { - result, err, handled := tc.handleBytesToNumeric(from, to, data) - if handled { - return result, err - } - } - - // Handle slice to slice conversions - if to.Kind() == reflect.Slice { - result, err, handled := tc.handleSlice(from, to, data) - if handled { - return result, err - } - } - - // Handle slice to hex string conversions - if to.Kind() == reflect.String { - result, err := tc.sliceToHexString(from, to, data) - if err == nil { - return result, err - } - } - } - - // No conversion found, return data as-is - return data, nil -} - -// handleHexString handles hex string conversions -func (tc *TypeConverter) handleHexString(str string, to reflect.Type, data any) (result any, err error, handled bool) { - hexStr := strings.TrimPrefix(str, "0x") - - switch to.Kind() { - case reflect.String: - if fn, ok := tc.converters["hex_string_to_string"]; ok { - result, err = fn(reflect.TypeOf(str), to, hexStr) - return result, err, true - } - case reflect.Slice: - if to.Elem().Kind() == reflect.Uint8 { - if fn, ok := tc.converters["hex_string_to_bytes"]; ok { - result, err = fn(reflect.TypeOf(str), to, hexStr) - return result, err, true - } - } - case reflect.Array: - if to.Elem().Kind() == reflect.Uint8 { - if fn, ok := tc.converters["hex_string_to_array"]; ok { - result, err = fn(reflect.TypeOf(str), to, hexStr) - return result, err, true - } - } - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - if fn, ok := tc.converters["hex_string_to_uint"]; ok { - result, err = fn(reflect.TypeOf(str), to, hexStr) - return result, err, true - } - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if fn, ok := tc.converters["hex_string_to_int"]; ok { - result, err = fn(reflect.TypeOf(str), to, hexStr) - return result, err, true - } - case reflect.Ptr: - if to == reflect.TypeOf((*big.Int)(nil)) { - if fn, ok := tc.converters["hex_string_to_bigint"]; ok { - result, err = fn(reflect.TypeOf(str), to, hexStr) - return result, err, true - } - } - case reflect.Interface: - return "0x" + hexStr, nil, true - } - - return nil, nil, false -} - -// handleBase64String handles base64 string conversions -func (tc *TypeConverter) handleBase64String(str string, to reflect.Type) (result any, err error, handled bool) { - if fn, ok := tc.converters["base64_string_to_bytes"]; ok { - result, err = fn(reflect.TypeOf(str), to, str) - // If base64ToBytes returns the original string unchanged, it means - // base64 decoding failed, so we should let default handling try - if resultStr, isStr := result.(string); isStr && resultStr == str { - return nil, nil, false - } - return result, err, true - } - return nil, nil, false -} - -// handleNumericString handles numeric string conversions -func (tc *TypeConverter) handleNumericString(str string, to reflect.Type) (result any, err error, handled bool) { - switch to.Kind() { - case reflect.String: - return str, nil, true - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if fn, ok := tc.converters["string_to_int"]; ok { - result, err = fn(reflect.TypeOf(str), to, str) - return result, err, true - } - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - if fn, ok := tc.converters["string_to_uint"]; ok { - result, err = fn(reflect.TypeOf(str), to, str) - return result, err, true - } - case reflect.Float32, reflect.Float64: - if fn, ok := tc.converters["string_to_float"]; ok { - result, err = fn(reflect.TypeOf(str), to, str) - return result, err, true - } - case reflect.Slice: - if to.Elem().Kind() == reflect.Uint8 { - if fn, ok := tc.converters["string_to_bytes"]; ok { - result, err = fn(reflect.TypeOf(str), to, str) - // If stringToBytes returns the original string unchanged, it means - // it's not a numeric string, so we should let other handlers try - if resultStr, isStr := result.(string); isStr && resultStr == str { - return nil, nil, false - } - return result, err, true - } - } - case reflect.Ptr: - if to == reflect.TypeOf((*big.Int)(nil)) { - if fn, ok := tc.converters["string_to_bigint"]; ok { - result, err = fn(reflect.TypeOf(str), to, str) - return result, err, true - } - } - } - - return nil, nil, false -} - -// handleBoolean handles boolean conversions -func (tc *TypeConverter) handleBoolean(boolValue bool, to reflect.Type) (result any, err error, handled bool) { - switch to.Kind() { - case reflect.Bool: - return boolValue, nil, true - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if fn, ok := tc.converters["bool_to_int"]; ok { - result, err = fn(reflect.TypeOf(boolValue), to, boolValue) - return result, err, true - } - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - if fn, ok := tc.converters["bool_to_uint"]; ok { - result, err = fn(reflect.TypeOf(boolValue), to, boolValue) - return result, err, true - } - case reflect.Ptr: - if to == reflect.TypeOf((*big.Int)(nil)) { - if fn, ok := tc.converters["bool_to_bigint"]; ok { - result, err = fn(reflect.TypeOf(boolValue), to, boolValue) - return result, err, true - } - } - } - - return nil, nil, false -} - -// handleSlice handles array/slice conversions -func (tc *TypeConverter) handleSlice(from, to reflect.Type, data any) (result any, err error, handled bool) { - if fn, ok := tc.converters["slice_to_slice"]; ok { - result, err = fn(from, to, data) - return result, err, true - } - return nil, nil, false -} - -// Conversion function implementations - -func (tc *TypeConverter) hexToString(from, to reflect.Type, data any) (any, error) { - hexStr, ok := data.(string) - if !ok { - return data, nil - } - return hexStr, nil -} - -func (tc *TypeConverter) hexToBytes(from, to reflect.Type, data any) (any, error) { - hexStr, ok := data.(string) - if !ok { - return data, nil - } - - if hexStr == "" { - return []uint8{}, nil - } - - if len(hexStr)%2 == 1 { - hexStr = "0" + hexStr - } - - return hex.DecodeString(hexStr) -} - -func (tc *TypeConverter) hexToUint(from, to reflect.Type, data any) (any, error) { - hexStr, ok := data.(string) - if !ok { - return data, nil - } - - return strconv.ParseUint(hexStr, base16, uint64Bits) -} - -func (tc *TypeConverter) hexToInt(from, to reflect.Type, data any) (any, error) { - hexStr, ok := data.(string) - if !ok { - return data, nil - } - - val, err := strconv.ParseInt(hexStr, base16, uint64Bits) - if err != nil { - return nil, fmt.Errorf("failed to parse hex to int: %w", err) - } - - return reflect.ValueOf(val).Convert(to).Interface(), nil -} - -func (tc *TypeConverter) hexToBigInt(from, to reflect.Type, data any) (any, error) { - hexStr, ok := data.(string) - if !ok { - return data, nil - } - - bi := new(big.Int) - bi.SetString(hexStr, base16) - return bi, nil -} - -func (tc *TypeConverter) hexToArray(from, to reflect.Type, data any) (any, error) { - hexStr, ok := data.(string) - if !ok { - return data, nil - } - - bytes, err := tc.hexToBytes(from, reflect.SliceOf(to.Elem()), hexStr) - if err != nil { - return nil, fmt.Errorf("failed to decode hex string %q: %w", hexStr, err) - } - - byteSlice := bytes.([]byte) - out := make([]uint8, to.Len()) - - // Re-enable this check once we can guarantee that responses are always the same length as the target array length. - // Disabling for now to avoid breaking changes to core. - // if len(byteSlice) != to.Len() { - // return nil, fmt.Errorf("hex to array: byte slice length %d is not equal to output array length %d", len(byteSlice), to.Len()) - // } - - copy(out, byteSlice) - - return out, nil -} - -func (tc *TypeConverter) sliceToHexString(from, to reflect.Type, data any) (any, error) { - bytes, err := AnySliceToBytes(data.([]any)) - if err != nil { - return nil, err - } - - return "0x" + hex.EncodeToString(bytes), nil -} - -func (tc *TypeConverter) base64ToBytes(from, to reflect.Type, data any) (any, error) { - str, ok := data.(string) - if !ok { - return data, nil - } - - bytes, err := base64.StdEncoding.DecodeString(str) - if err == nil { - return bytes, nil - } - - // Base64 decoding failed - return string unchanged so handler knows to pass to default - return str, nil -} - -func (tc *TypeConverter) stringToInt(from, to reflect.Type, data any) (any, error) { - str, ok := data.(string) - if !ok { - return data, nil - } - - val, err := strconv.ParseInt(str, base10, uint64Bits) - if err != nil { - return nil, fmt.Errorf("failed to parse string to int: %w", err) - } - - if overflowInt(to, val) { - return nil, fmt.Errorf("value %d overflows %v", val, to) - } - - return reflect.ValueOf(val).Convert(to).Interface(), nil -} - -func (tc *TypeConverter) stringToUint(from, to reflect.Type, data any) (any, error) { - str, ok := data.(string) - if !ok { - return data, nil - } - - val, err := strconv.ParseUint(str, base10, uint64Bits) - if err != nil { - return nil, fmt.Errorf("failed to parse string to uint: %w", err) - } - - if overflowUint(to, val) { - return nil, fmt.Errorf("value %d overflows %v", val, to) - } - - return reflect.ValueOf(val).Convert(to).Interface(), nil -} - -func (tc *TypeConverter) stringToFloat(from, to reflect.Type, data any) (any, error) { - str, ok := data.(string) - if !ok { - return data, nil - } - - val, err := strconv.ParseFloat(str, uint64Bits) - if err != nil { - return nil, fmt.Errorf("failed to parse string to float: %w", err) - } - - if overflowFloat(to, val) { - return nil, fmt.Errorf("value %f overflows %v", val, to) - } - - return reflect.ValueOf(val).Convert(to).Interface(), nil -} - -func (tc *TypeConverter) stringToBytes(from, to reflect.Type, data any) (any, error) { - str, ok := data.(string) - if !ok { - return data, nil - } - - // Try numeric string first (convert to little-endian bytes) - if num, err := strconv.ParseUint(str, base10, uint64Bits); err == nil { - return numericToBytes(num), nil - } - - // Not a numeric string - return unchanged so other handlers (base64) can try - return str, nil -} - -func (tc *TypeConverter) stringToBigInt(from, to reflect.Type, data any) (any, error) { - str, ok := data.(string) - if !ok { - return data, nil - } - - result := new(big.Int) - _, success := result.SetString(str, base10) - if !success { - return nil, fmt.Errorf("cannot parse string %s as big.Int", str) - } - - return result, nil -} - -func (tc *TypeConverter) boolToInt(from, to reflect.Type, data any) (any, error) { - boolValue, ok := data.(bool) - if !ok { - return data, nil - } - - if boolValue { - return reflect.ValueOf(1).Convert(to).Interface(), nil - } - - return reflect.ValueOf(0).Convert(to).Interface(), nil -} - -func (tc *TypeConverter) boolToUint(from, to reflect.Type, data any) (any, error) { - boolValue, ok := data.(bool) - if !ok { - return data, nil - } - - if boolValue { - return reflect.ValueOf(1).Convert(to).Interface(), nil - } - - return reflect.ValueOf(0).Convert(to).Interface(), nil -} - -func (tc *TypeConverter) boolToBigInt(from, to reflect.Type, data any) (any, error) { - boolValue, ok := data.(bool) - if !ok { - return data, nil - } - - if boolValue { - return big.NewInt(1), nil - } - - return big.NewInt(0), nil -} - -func (tc *TypeConverter) sliceToSlice(from, to reflect.Type, data any) (any, error) { - sourceSlice := reflect.ValueOf(data) - targetSlice := reflect.MakeSlice(to, sourceSlice.Len(), sourceSlice.Cap()) - - for i := range sourceSlice.Len() { - sourceElem := sourceSlice.Index(i).Interface() - targetElem := reflect.New(to.Elem()).Interface() - - if err := DecodeSuiJsonValue(sourceElem, targetElem); err != nil { - return nil, fmt.Errorf("failed to decode array element at index %d: %w", i, err) - } - - targetSlice.Index(i).Set(reflect.ValueOf(targetElem).Elem()) - } - - return targetSlice.Interface(), nil -} - -// float64ToUint converts float64 to uint types (JSON unmarshals numbers as float64) -func (tc *TypeConverter) float64ToUint(from, to reflect.Type, data any) (any, error) { - floatVal, ok := data.(float64) - if !ok { - return data, nil - } - - uintVal := uint64(floatVal) - if overflowUint(to, uintVal) { - return nil, fmt.Errorf("value %d overflows %v", uintVal, to) - } - - return reflect.ValueOf(uintVal).Convert(to).Interface(), nil + return codec.NewTypeConverter() } -// float64ToInt converts float64 to int types -func (tc *TypeConverter) float64ToInt(from, to reflect.Type, data any) (any, error) { - floatVal, ok := data.(float64) - if !ok { - return data, nil - } - - intVal := int64(floatVal) - if overflowInt(to, intVal) { - return nil, fmt.Errorf("value %d overflows %v", intVal, to) - } - - return reflect.ValueOf(intVal).Convert(to).Interface(), nil -} - -// jsonNumberToUint converts json.Number to uint types -func (tc *TypeConverter) jsonNumberToUint(from, to reflect.Type, data any) (any, error) { - jsonNum, ok := data.(json.Number) - if !ok { - return data, nil - } - - intVal, err := jsonNum.Int64() - if err != nil { - return nil, fmt.Errorf("failed to parse JSON number: %w", err) - } - - if intVal < 0 { - return nil, fmt.Errorf("cannot convert negative value %d to uint", intVal) - } - - uintVal := uint64(intVal) - if overflowUint(to, uintVal) { - return nil, fmt.Errorf("value %d overflows %v", uintVal, to) - } - - return reflect.ValueOf(uintVal).Convert(to).Interface(), nil -} - -// jsonNumberToInt converts json.Number to int types -func (tc *TypeConverter) jsonNumberToInt(from, to reflect.Type, data any) (any, error) { - jsonNum, ok := data.(json.Number) - if !ok { - return data, nil - } - - intVal, err := jsonNum.Int64() - if err != nil { - return nil, fmt.Errorf("failed to parse JSON number: %w", err) - } - - if overflowInt(to, intVal) { - return nil, fmt.Errorf("value %d overflows %v", intVal, to) - } - - return reflect.ValueOf(intVal).Convert(to).Interface(), nil -} - -// bytesToUint converts []byte to uint types (little-endian) -func (tc *TypeConverter) bytesToUint(from, to reflect.Type, data any) (any, error) { - bytes, ok := data.([]byte) - if !ok { - return data, nil - } - - if len(bytes) == 0 { - return nil, fmt.Errorf("empty byte array cannot be converted to numeric value") - } - - var result uint64 - // Process bytes in little-endian order - for i := 0; i < len(bytes) && i < 8; i++ { - result |= uint64(bytes[i]) << (8 * i) - } - - if overflowUint(to, result) { - return nil, fmt.Errorf("value %d overflows %v", result, to) - } - - return reflect.ValueOf(result).Convert(to).Interface(), nil -} - -// sliceAnyToUint converts []any to uint types (converts to bytes first) -func (tc *TypeConverter) sliceAnyToUint(from, to reflect.Type, data any) (any, error) { - anySlice, ok := data.([]any) - if !ok { - return data, nil - } - - bytes, err := AnySliceToBytes(anySlice) - if err != nil { - return nil, fmt.Errorf("failed to convert slice to bytes: %w", err) - } - - return tc.bytesToUint(reflect.TypeOf(bytes), to, bytes) -} - -// handleFloat64 handles float64 conversions -func (tc *TypeConverter) handleFloat64(floatVal float64, to reflect.Type) (result any, err error, handled bool) { - switch to.Kind() { - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - if fn, ok := tc.converters["float64_to_uint"]; ok { - result, err = fn(reflect.TypeOf(floatVal), to, floatVal) - return result, err, true - } - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if fn, ok := tc.converters["float64_to_int"]; ok { - result, err = fn(reflect.TypeOf(floatVal), to, floatVal) - return result, err, true - } - } - - return nil, nil, false -} - -// handleJSONNumber handles json.Number conversions -func (tc *TypeConverter) handleJSONNumber(jsonNum json.Number, to reflect.Type) (result any, err error, handled bool) { - switch to.Kind() { - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - if fn, ok := tc.converters["json_number_to_uint"]; ok { - result, err = fn(reflect.TypeOf(jsonNum), to, jsonNum) - return result, err, true - } - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if fn, ok := tc.converters["json_number_to_int"]; ok { - result, err = fn(reflect.TypeOf(jsonNum), to, jsonNum) - return result, err, true - } - } - - return nil, nil, false -} - -// handleBytesToNumeric handles conversions from []byte or []any to numeric types -func (tc *TypeConverter) handleBytesToNumeric(from, to reflect.Type, data any) (result any, err error, handled bool) { - if from.Kind() == reflect.Slice && from.Elem().Kind() == reflect.Uint8 { - // []byte to numeric - if fn, ok := tc.converters["bytes_to_uint"]; ok { - result, err = fn(from, to, data) - return result, err, true - } - } - - if from.Kind() == reflect.Slice && from.Elem().Kind() == reflect.Interface { - // []any to numeric - if fn, ok := tc.converters["slice_any_to_uint"]; ok { - result, err = fn(from, to, data) - return result, err, true - } - } - - return nil, nil, false -} - -// isNumericTarget checks if the target type is a numeric type -func isNumericTarget(to reflect.Type) bool { - switch to.Kind() { - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, - reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return true - } - return false -} - -// Global type converter instance for package-wide use (lazy initialization) -var defaultTypeConverter *TypeConverter - -// getDefaultTypeConverter returns the global type converter, initializing it if necessary -func getDefaultTypeConverter() *TypeConverter { - if defaultTypeConverter == nil { - defaultTypeConverter = NewTypeConverter() - } - return defaultTypeConverter -} - -// UnifiedTypeConverterHook is a mapstructure hook that uses the type converter registry +// Deprecated: use codec.UnifiedTypeConverterHook +// +//go:fix inline func UnifiedTypeConverterHook(from, to reflect.Type, data any) (any, error) { - // Skip if types are the same - if from == to { - return data, nil - } - - // Handle single-field struct case only for primitive source types - // (string, bool, numeric types) to avoid interfering with map-to-struct decoding - if to.Kind() == reflect.Struct && to.NumField() == 1 { - switch from.Kind() { - case reflect.String, reflect.Bool, - reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, - reflect.Float32, reflect.Float64, - reflect.Slice, reflect.Array: - return handleSingleFieldStruct(to, data, DecodeSuiJsonValue) - } - } - - // Use the global converter - return getDefaultTypeConverter().Convert(from, to, data) + return codec.UnifiedTypeConverterHook(from, to, data) } +// Deprecated: use codec.BytesToAnySlice +// +//go:fix inline func BytesToAnySlice(b []byte) []any { - result := make([]any, len(b)) - for i, v := range b { - result[i] = v - } - return result + return codec.BytesToAnySlice(b) } diff --git a/relayer/codec/types.go b/relayer/codec/types.go index 22d8d560..8a47ecff 100644 --- a/relayer/codec/types.go +++ b/relayer/codec/types.go @@ -1,149 +1,48 @@ package codec -import ( - "errors" - "math/big" +import "github.com/smartcontractkit/chainlink-sui/codec" - "github.com/block-vision/sui-go-sdk/models" -) - -var AccountZero = make([]byte, 32) - -type PTBCommandDependency struct { - CommandIndex uint16 - ResultIndex *uint16 -} +//go:fix inline +var AccountZero = codec.AccountZero -// PointerTag defines the structured format for pointer tags used in chain reader. -// Pointer tags specify how to derive object IDs from pointer objects stored on-chain. -type PointerTag struct { - // Module name containing the pointer object (e.g. "state_object", "offramp", "counter") - Module string `json:"module"` - // PointerName is the object type to search for (e.g. "CCIPObjectRefPointer", "OffRampStatePointer") - PointerName string `json:"pointerName"` - // FieldName is OPTIONAL and NOT USED by the implementation. The parent field name is automatically - // looked up from the global common.PointerConfigs registry based on the PointerName. - // This field exists for backward compatibility or future implementations to override static code but is currently ignored. - FieldName string `json:"fieldName,omitempty"` - // DerivationKey is the key used to derive the child object ID from the parent object ID (e.g. "CCIPObjectRef", "CCIP_OWNABLE") - DerivationKey string `json:"derivationKey"` - // PackageID is the package ID for the Pointer object if it differs from the calling contract's package ID - // This is used for cross-package pointer dependencies (e.g. offramp package depending on CCIP package CCIPObjectRef) - // If empty, the calling contract's package ID is used - PackageID string `json:"packageId,omitempty"` -} +//go:fix inline +type PTBCommandDependency = codec.PTBCommandDependency -func (p PointerTag) Validate() error { - if p.Module == "" { - return errors.New("PointerTag.Module is required") - } - if p.PointerName == "" { - return errors.New("PointerTag.Pointer is required") - } - // FieldName is optional - it's looked up from common.PointerConfigs - if p.DerivationKey == "" { - return errors.New("PointerTag.DerivationKey is required") - } - return nil -} +//go:fix inline +type PointerTag = codec.PointerTag -// SuiFunctionParam defines a parameter for a Sui function call -type SuiFunctionParam struct { - // Name of the parameter - Name string - // PointerTag (optional) specify how to derive object IDs from pointer objects stored on-chain. - PointerTag *PointerTag - // Type of the parameter (e.g., "u64", "String", "vector", "ptb_dependency") - Type string - // IsMutable specifies if the object is mutable or not (optional - defaults to true) - IsMutable *bool - // IsGeneric specifies if the parameter is a generic argument - GenericType *string - // Whether the parameter is required - Required bool - // Default value to use if not provided - DefaultValue any - // Result from a previous PTB Command (optional). It is used for expressive construction of PTB commands - PTBDependency *PTBCommandDependency - // GenericDependency maps to internal helpers for fetching an unknown generic type required by the parameter - GenericDependency *string -} +//go:fix inline +type SuiFunctionParam = codec.SuiFunctionParam -type SuiPTBCommandType string +//go:fix inline +type SuiPTBCommandType = codec.SuiPTBCommandType const ( - SuiPTBCommandMoveCall SuiPTBCommandType = "move_call" - SuiPTBCommandPublish SuiPTBCommandType = "publish" - SuiPTBCommandTransfer SuiPTBCommandType = "transfer" + SuiPTBCommandMoveCall = codec.SuiPTBCommandMoveCall + SuiPTBCommandPublish = codec.SuiPTBCommandPublish + SuiPTBCommandTransfer = codec.SuiPTBCommandTransfer ) -// OCRConfigSet event data -type ConfigSet struct { - OcrPluginType byte - ConfigDigest []byte - Signers [][]byte - // this is a list of addresses, we can treat them as strings - Transmitters []string - BigF byte -} +//go:fix inline +type ConfigSet = codec.ConfigSet -// SourceChainConfigSet event data -type SourceChainConfigSet struct { - SourceChainSelector uint64 - SourceChainConfig SourceChainConfig -} +//go:fix inline +type SourceChainConfigSet = codec.SourceChainConfigSet -// SourceChainConfig event data -type SourceChainConfig struct { - Router string - IsEnabled bool - MinSeqNr uint64 - IsRMNVerificationDisabled bool - OnRamp []byte -} +//go:fix inline +type SourceChainConfig = codec.SourceChainConfig -// ExecutionReport event data -type ExecutionReport struct { - SourceChainSelector uint64 - Message Any2SuiRampMessage - OffchainTokenData [][]byte - Proofs [][]byte -} +//go:fix inline +type ExecutionReport = codec.ExecutionReport -// RampMessageHeader event data -type RampMessageHeader struct { - MessageID []byte - SourceChainSelector uint64 - DestChainSelector uint64 - SequenceNumber uint64 - Nonce uint64 -} +//go:fix inline +type RampMessageHeader = codec.RampMessageHeader -// Any2SuiTokenTransfer event data -type Any2SuiTokenTransfer struct { - SourcePoolAddress []byte - DestTokenAddress models.SuiAddress - DestGasAmount uint32 - ExtraData []byte - Amount *big.Int -} +//go:fix inline +type Any2SuiTokenTransfer = codec.Any2SuiTokenTransfer -// Any2SuiRampMessage event data -type Any2SuiRampMessage struct { - Header RampMessageHeader - Sender []byte - Data []byte - Receiver models.SuiAddress - GasLimit *big.Int - TokenReceiver models.SuiAddressBytes - TokenAmounts []Any2SuiTokenTransfer -} +//go:fix inline +type Any2SuiRampMessage = codec.Any2SuiRampMessage -// ExecutionStateChanged event data -type ExecutionStateChanged struct { - SourceChainSelector uint64 `json:"source_chain_selector"` - SequenceNumber uint64 `json:"sequence_number"` - MessageId []byte `json:"message_id"` - MessageHash []byte `json:"message_hash"` - State byte `json:"state"` -} +//go:fix inline +type ExecutionStateChanged = codec.ExecutionStateChanged diff --git a/scripts/go.mod b/scripts/go.mod index 0101c1d6..1215a36c 100644 --- a/scripts/go.mod +++ b/scripts/go.mod @@ -6,6 +6,8 @@ replace github.com/fbsobreira/gotron-sdk => github.com/smartcontractkit/chainlin replace github.com/smartcontractkit/chainlink-sui => ../ +replace github.com/smartcontractkit/chainlink-sui/codec => ../codec + replace github.com/smartcontractkit/chainlink-sui/deployment => ../deployment require github.com/smartcontractkit/chainlink-sui/deployment v0.0.0-00010101000000-000000000000 @@ -136,6 +138,7 @@ require ( github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9 // indirect github.com/smartcontractkit/chainlink-sui v0.0.0 // indirect + github.com/smartcontractkit/chainlink-sui/codec v0.0.0-20260702152904-78d359a411ad // indirect github.com/smartcontractkit/chainlink-ton v1.0.5-0.20260514223130-48bc90aca745 // indirect github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20251014143056-a0c6328c91e9 // indirect github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad // indirect