diff --git a/encoding/ccf/ccf_test.go b/encoding/ccf/ccf_test.go index 86dd2b71e..1ca57b943 100644 --- a/encoding/ccf/ccf_test.go +++ b/encoding/ccf/ccf_test.go @@ -2582,7 +2582,10 @@ func TestDecodeWord128Invalid(t *testing.T) { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, }) require.Error(t, err) - assert.Equal(t, "ccf: failed to decode: failed to decode Word128: cbor: cannot decode CBOR tag type to big.Int", err.Error()) + assert.ErrorContains(t, + err, + "failed to decode Word128: cbor: cannot decode CBOR tag type to big.Int", + ) } } @@ -2682,7 +2685,10 @@ func TestDecodeWord256Invalid(t *testing.T) { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, }) require.Error(t, err) - assert.Equal(t, "ccf: failed to decode: failed to decode Word256: cbor: cannot decode CBOR tag type to big.Int", err.Error()) + assert.ErrorContains(t, + err, + "failed to decode Word256: cbor: cannot decode CBOR tag type to big.Int", + ) } } @@ -10921,7 +10927,10 @@ func TestEncodeType(t *testing.T) { _, err := ccf.Decode(nil, encodedData) require.Error(t, err) - assert.Equal(t, "ccf: failed to decode: unexpected empty intersection type", err.Error()) + assert.ErrorContains(t, + err, + "unexpected empty intersection type", + ) }) @@ -10952,7 +10961,10 @@ func TestEncodeType(t *testing.T) { _, err := ccf.Decode(nil, encodedData) require.Error(t, err) - assert.Equal(t, "ccf: failed to decode: unexpected empty intersection type", err.Error()) + assert.ErrorContains(t, + err, + "unexpected empty intersection type", + ) }) t.Run("with static intersection type", func(t *testing.T) { @@ -12816,7 +12828,10 @@ func TestDecodeInvalidType(t *testing.T) { for _, dm := range decModes { _, err := dm.Decode(nil, encodedData) require.Error(t, err) - assert.Equal(t, "ccf: failed to decode: invalid type ID for built-in: ``", err.Error()) + assert.ErrorContains(t, + err, + "invalid type ID for built-in: ``", + ) } }) @@ -12874,7 +12889,10 @@ func TestDecodeInvalidType(t *testing.T) { for _, dm := range decModes { _, err := dm.Decode(nil, encodedData) require.Error(t, err) - assert.Equal(t, "ccf: failed to decode: invalid type ID `I`: invalid identifier location type ID: missing location", err.Error()) + assert.ErrorContains(t, + err, + "invalid type ID `I`: invalid identifier location type ID: missing location", + ) } }) @@ -12932,7 +12950,10 @@ func TestDecodeInvalidType(t *testing.T) { for _, dm := range decModes { _, err := dm.Decode(nil, encodedData) require.Error(t, err) - assert.Equal(t, "ccf: failed to decode: invalid type ID for built-in: `N.PublicKey`", err.Error()) + assert.ErrorContains(t, + err, + "invalid type ID for built-in: `N.PublicKey`", + ) } }) } diff --git a/encoding/ccf/decode.go b/encoding/ccf/decode.go index 35db6dcef..39d8eae54 100644 --- a/encoding/ccf/decode.go +++ b/encoding/ccf/decode.go @@ -591,7 +591,7 @@ func (d *Decoder) decodeValue(t cadence.Type, types *cadenceTypeByCCFTypeID) (ca default: err := decodeCBORTagWithKnownNumber(d.dec, CBORTagTypeAndValue) if err != nil { - return nil, fmt.Errorf("unexpected encoded value of Cadence type %s (%T): %s", t.ID(), t, err.Error()) + return nil, fmt.Errorf("unexpected encoded value of Cadence type %s (%T): %w", t.ID(), t, err) } // Decode ccf-type-and-value-message. @@ -1538,7 +1538,7 @@ func (d *Decoder) decodeCapability(typ *cadence.CapabilityType, types *cadenceTy if nextType == cbor.TagType { err := decodeCBORTagWithKnownNumber(d.dec, CBORTagTypeAndValue) if err != nil { - return nil, fmt.Errorf("unexpected encoded value of Cadence type %s (%T): %s", typ.ID(), typ, err.Error()) + return nil, fmt.Errorf("unexpected encoded value of Cadence type %s (%T): %w", typ.ID(), typ, err) } // Decode ccf-type-and-value-message. diff --git a/encoding/json/decode.go b/encoding/json/decode.go index ae897f15c..2619a516c 100644 --- a/encoding/json/decode.go +++ b/encoding/json/decode.go @@ -22,9 +22,11 @@ import ( "bytes" "encoding/hex" "encoding/json" + "fmt" "io" "math/big" "strconv" + "strings" "unicode/utf8" _ "unsafe" @@ -35,6 +37,26 @@ import ( "github.com/onflow/cadence/sema" ) +type pathElement interface { + Append(w io.Writer) +} + +type indexPathElement int + +var _ pathElement = indexPathElement(0) + +func (e indexPathElement) Append(w io.Writer) { + _, _ = fmt.Fprintf(w, "[%d]", int(e)) +} + +type propertyPathElement string + +var _ pathElement = propertyPathElement("") + +func (e propertyPathElement) Append(w io.Writer) { + _, _ = fmt.Fprintf(w, ".%s", e) +} + // A Decoder decodes JSON-encoded representations of Cadence values. type Decoder struct { dec *json.Decoder @@ -44,6 +66,7 @@ type Decoder struct { allowUnstructuredStaticTypes bool // backwardsCompatible controls if the decoder can decode old versions of the JSON encoding backwardsCompatible bool + pathContext []pathElement } type Option func(*Decoder) @@ -90,8 +113,9 @@ func Decode(gauge common.MemoryGauge, b []byte, options ...Option) (cadence.Valu // given io.Reader. func NewDecoder(gauge common.MemoryGauge, r io.Reader) *Decoder { return &Decoder{ - dec: json.NewDecoder(r), - gauge: gauge, + dec: json.NewDecoder(r), + gauge: gauge, + pathContext: make([]pathElement, 0, 8), } } @@ -116,11 +140,18 @@ func (d *Decoder) Decode() (value cadence.Value, err error) { panic(r) } - err = errors.NewDefaultUserError("failed to decode JSON-Cadence value: %w", panicErr) + format := "failed to decode JSON-Cadence value: %w" + + path := d.getPathString() + if path != "" { + format += fmt.Sprintf(" (at %s)", path) + } + + err = errors.NewDefaultUserError(format, panicErr) } }() - value = d.DecodeJSON(jsonMap) + value = d.decodeValue(jsonMap) return value, nil } @@ -160,10 +191,32 @@ const ( stepKey = "step" ) -func (d *Decoder) DecodeJSON(v any) cadence.Value { +func (d *Decoder) pushPath(element pathElement) { + d.pathContext = append(d.pathContext, element) +} + +func (d *Decoder) popPath() { + if len(d.pathContext) > 0 { + d.pathContext = d.pathContext[:len(d.pathContext)-1] + } +} + +func (d *Decoder) getPathString() string { + if len(d.pathContext) == 0 { + return "" + } + + var builder strings.Builder + for _, element := range d.pathContext { + element.Append(&builder) + } + return builder.String() +} + +func (d *Decoder) decodeValue(v any) cadence.Value { obj := toObject(v) - typeStr := obj.GetString(typeKey) + typeStr := get(d, obj, typeKey, toString) // void is a special case, does not have "value" field if typeStr == voidTypeStr { @@ -172,103 +225,107 @@ func (d *Decoder) DecodeJSON(v any) cadence.Value { // object should only contain two keys: "type", "value" if len(obj) != 2 { - panic(errors.NewDefaultUserError("expected JSON object with keys `%s` and `%s`", typeKey, valueKey)) - } - - valueJSON := obj.Get(valueKey) - - switch typeStr { - case optionalTypeStr: - return d.decodeOptional(valueJSON) - case boolTypeStr: - return d.decodeBool(valueJSON) - case characterTypeStr: - return d.decodeCharacter(valueJSON) - case stringTypeStr: - return d.decodeString(valueJSON) - case addressTypeStr: - return d.decodeAddress(valueJSON) - case intTypeStr: - return d.decodeInt(valueJSON) - case int8TypeStr: - return d.decodeInt8(valueJSON) - case int16TypeStr: - return d.decodeInt16(valueJSON) - case int32TypeStr: - return d.decodeInt32(valueJSON) - case int64TypeStr: - return d.decodeInt64(valueJSON) - case int128TypeStr: - return d.decodeInt128(valueJSON) - case int256TypeStr: - return d.decodeInt256(valueJSON) - case uintTypeStr: - return d.decodeUInt(valueJSON) - case uint8TypeStr: - return d.decodeUInt8(valueJSON) - case uint16TypeStr: - return d.decodeUInt16(valueJSON) - case uint32TypeStr: - return d.decodeUInt32(valueJSON) - case uint64TypeStr: - return d.decodeUInt64(valueJSON) - case uint128TypeStr: - return d.decodeUInt128(valueJSON) - case uint256TypeStr: - return d.decodeUInt256(valueJSON) - case word8TypeStr: - return d.decodeWord8(valueJSON) - case word16TypeStr: - return d.decodeWord16(valueJSON) - case word32TypeStr: - return d.decodeWord32(valueJSON) - case word64TypeStr: - return d.decodeWord64(valueJSON) - case word128TypeStr: - return d.decodeWord128(valueJSON) - case word256TypeStr: - return d.decodeWord256(valueJSON) - case fix64TypeStr: - return d.decodeFix64(valueJSON) - case fix128TypeStr: - return d.decodeFix128(valueJSON) - case ufix64TypeStr: - return d.decodeUFix64(valueJSON) - case ufix128TypeStr: - return d.decodeUFix128(valueJSON) - case arrayTypeStr: - return d.decodeArray(valueJSON) - case dictionaryTypeStr: - return d.decodeDictionary(valueJSON) - case resourceTypeStr: - return d.decodeResource(valueJSON) - case structTypeStr: - return d.decodeStruct(valueJSON) - case eventTypeStr: - return d.decodeEvent(valueJSON) - case contractTypeStr: - return d.decodeContract(valueJSON) - case inclusiveRangeTypeStr: - return d.decodeInclusiveRange(valueJSON) - case pathTypeStr: - return d.decodePath(valueJSON) - case typeTypeStr: - return d.decodeTypeValue(valueJSON) - case capabilityTypeStr: - return d.decodeCapability(valueJSON) - case enumTypeStr: - return d.decodeEnum(valueJSON) - case functionTypeStr: - return d.decodeFunction(valueJSON) - } - - panic(errors.NewDefaultUserError("invalid type: %s", typeStr)) + panic(errors.NewDefaultUserError( + "expected JSON object with keys `%s` and `%s`", + typeKey, + valueKey, + )) + } + + return get(d, obj, valueKey, func(valueJSON any) cadence.Value { + switch typeStr { + case optionalTypeStr: + return d.decodeOptional(valueJSON) + case boolTypeStr: + return d.decodeBool(valueJSON) + case characterTypeStr: + return d.decodeCharacter(valueJSON) + case stringTypeStr: + return d.decodeString(valueJSON) + case addressTypeStr: + return d.decodeAddress(valueJSON) + case intTypeStr: + return d.decodeInt(valueJSON) + case int8TypeStr: + return d.decodeInt8(valueJSON) + case int16TypeStr: + return d.decodeInt16(valueJSON) + case int32TypeStr: + return d.decodeInt32(valueJSON) + case int64TypeStr: + return d.decodeInt64(valueJSON) + case int128TypeStr: + return d.decodeInt128(valueJSON) + case int256TypeStr: + return d.decodeInt256(valueJSON) + case uintTypeStr: + return d.decodeUInt(valueJSON) + case uint8TypeStr: + return d.decodeUInt8(valueJSON) + case uint16TypeStr: + return d.decodeUInt16(valueJSON) + case uint32TypeStr: + return d.decodeUInt32(valueJSON) + case uint64TypeStr: + return d.decodeUInt64(valueJSON) + case uint128TypeStr: + return d.decodeUInt128(valueJSON) + case uint256TypeStr: + return d.decodeUInt256(valueJSON) + case word8TypeStr: + return d.decodeWord8(valueJSON) + case word16TypeStr: + return d.decodeWord16(valueJSON) + case word32TypeStr: + return d.decodeWord32(valueJSON) + case word64TypeStr: + return d.decodeWord64(valueJSON) + case word128TypeStr: + return d.decodeWord128(valueJSON) + case word256TypeStr: + return d.decodeWord256(valueJSON) + case fix64TypeStr: + return d.decodeFix64(valueJSON) + case fix128TypeStr: + return d.decodeFix128(valueJSON) + case ufix64TypeStr: + return d.decodeUFix64(valueJSON) + case ufix128TypeStr: + return d.decodeUFix128(valueJSON) + case arrayTypeStr: + return d.decodeArray(valueJSON) + case dictionaryTypeStr: + return d.decodeDictionary(valueJSON) + case resourceTypeStr: + return d.decodeResource(valueJSON) + case structTypeStr: + return d.decodeStruct(valueJSON) + case eventTypeStr: + return d.decodeEvent(valueJSON) + case contractTypeStr: + return d.decodeContract(valueJSON) + case inclusiveRangeTypeStr: + return d.decodeInclusiveRange(valueJSON) + case pathTypeStr: + return d.decodePath(valueJSON) + case typeTypeStr: + return d.decodeTypeValue(valueJSON) + case capabilityTypeStr: + return d.decodeCapability(valueJSON) + case enumTypeStr: + return d.decodeEnum(valueJSON) + case functionTypeStr: + return d.decodeFunction(valueJSON) + } + + panic(errors.NewDefaultUserError("invalid type: %s", typeStr)) + }) } func (d *Decoder) decodeVoid(m map[string]any) cadence.Void { // object should not contain fields other than "type" if len(m) != 1 { - panic(errors.NewDefaultUserError("invalid additional fields in void value")) + panic(errors.NewDefaultUserError("invalid additional fields in Void value")) } return cadence.NewMeteredVoid(d.gauge) @@ -279,7 +336,7 @@ func (d *Decoder) decodeOptional(valueJSON any) cadence.Optional { return cadence.NewMeteredOptional(d.gauge, nil) } - return cadence.NewMeteredOptional(d.gauge, d.DecodeJSON(valueJSON)) + return cadence.NewMeteredOptional(d.gauge, d.decodeValue(valueJSON)) } func (d *Decoder) decodeBool(valueJSON any) cadence.Bool { @@ -692,9 +749,13 @@ func (d *Decoder) decodeArray(valueJSON any) cadence.Array { len(v), func() ([]cadence.Value, error) { values := make([]cadence.Value, len(v)) + for i, val := range v { - values[i] = d.DecodeJSON(val) + d.pushPath(indexPathElement(i)) + values[i] = d.decodeValue(val) + d.popPath() } + return values, nil }, ) @@ -715,7 +776,9 @@ func (d *Decoder) decodeDictionary(valueJSON any) cadence.Dictionary { pairs := make([]cadence.KeyValuePair, len(v)) for i, val := range v { + d.pushPath(indexPathElement(i)) pairs[i] = d.decodeKeyValuePair(val) + d.popPath() } return pairs, nil @@ -732,8 +795,8 @@ func (d *Decoder) decodeDictionary(valueJSON any) cadence.Dictionary { func (d *Decoder) decodeKeyValuePair(valueJSON any) cadence.KeyValuePair { obj := toObject(valueJSON) - key := obj.GetValue(d, keyKey) - value := obj.GetValue(d, valueKey) + key := get(d, obj, keyKey, d.decodeValue) + value := get(d, obj, valueKey, d.decodeValue) return cadence.NewMeteredKeyValuePair( d.gauge, @@ -742,19 +805,35 @@ func (d *Decoder) decodeKeyValuePair(valueJSON any) cadence.KeyValuePair { ) } -type composite struct { +type compositeTypeID struct { + typeID string location common.Location qualifiedIdentifier string - fieldValues []cadence.Value - fieldTypes []cadence.Field +} + +type compositeFields struct { + fieldValues []cadence.Value + fieldTypes []cadence.Field +} + +type composite struct { + compositeTypeID + compositeFields } func (d *Decoder) decodeComposite(valueJSON any) composite { obj := toObject(valueJSON) - typeID := obj.GetString(idKey) - location, qualifiedIdentifier, err := common.DecodeTypeID(d.gauge, typeID) + return composite{ + compositeTypeID: get(d, obj, idKey, d.decodeCompositeTypeID), + compositeFields: get(d, obj, fieldsKey, d.decodeCompositeFields), + } +} + +func (d *Decoder) decodeCompositeTypeID(valueJSON any) compositeTypeID { + typeID := toString(valueJSON) + location, qualifiedIdentifier, err := common.DecodeTypeID(d.gauge, typeID) if err != nil { panic(errors.NewDefaultUserError("invalid type ID `%s`: %w", typeID, err)) } else if location == nil && sema.NativeCompositeTypes[typeID] == nil { @@ -764,7 +843,15 @@ func (d *Decoder) decodeComposite(valueJSON any) composite { panic(errors.NewDefaultUserError("invalid type ID for built-in: `%s`", typeID)) } - fields := obj.GetSlice(fieldsKey) + return compositeTypeID{ + typeID: typeID, + location: location, + qualifiedIdentifier: qualifiedIdentifier, + } +} + +func (d *Decoder) decodeCompositeFields(valueJSON any) compositeFields { + fields := toSlice(valueJSON) common.UseMemory(d.gauge, common.MemoryUsage{ Kind: common.MemoryKindCadenceField, @@ -775,25 +862,25 @@ func (d *Decoder) decodeComposite(valueJSON any) composite { fieldTypes := make([]cadence.Field, len(fields)) for i, field := range fields { + d.pushPath(indexPathElement(i)) value, fieldType := d.decodeCompositeField(field) + d.popPath() fieldValues[i] = value fieldTypes[i] = fieldType } - return composite{ - location: location, - qualifiedIdentifier: qualifiedIdentifier, - fieldValues: fieldValues, - fieldTypes: fieldTypes, + return compositeFields{ + fieldValues: fieldValues, + fieldTypes: fieldTypes, } } func (d *Decoder) decodeCompositeField(valueJSON any) (cadence.Value, cadence.Field) { obj := toObject(valueJSON) - name := obj.GetString(nameKey) - value := obj.GetValue(d, valueKey) + name := get(d, obj, nameKey, toString) + value := get(d, obj, valueKey, d.decodeValue) // Unmetered because decodeCompositeField is metered in decodeComposite and called nowhere else // Type is still metered. @@ -812,7 +899,6 @@ func (d *Decoder) decodeStruct(valueJSON any) cadence.Struct { return comp.fieldValues, nil }, ) - if err != nil { panic(errors.NewDefaultUserError("invalid struct: %w", err)) } @@ -836,10 +922,10 @@ func (d *Decoder) decodeResource(valueJSON any) cadence.Resource { return comp.fieldValues, nil }, ) - if err != nil { panic(errors.NewDefaultUserError("invalid resource: %w", err)) } + return resource.WithType(cadence.NewMeteredResourceType( d.gauge, comp.location, @@ -859,7 +945,6 @@ func (d *Decoder) decodeEvent(valueJSON any) cadence.Event { return comp.fieldValues, nil }, ) - if err != nil { panic(errors.NewDefaultUserError("invalid event: %w", err)) } @@ -925,9 +1010,9 @@ func (d *Decoder) decodeEnum(valueJSON any) cadence.Enum { func (d *Decoder) decodeInclusiveRange(valueJSON any) *cadence.InclusiveRange { obj := toObject(valueJSON) - start := obj.GetValue(d, startKey) - end := obj.GetValue(d, endKey) - step := obj.GetValue(d, stepKey) + start := get(d, obj, startKey, d.decodeValue) + end := get(d, obj, endKey, d.decodeValue) + step := get(d, obj, stepKey, d.decodeValue) value := cadence.NewMeteredInclusiveRange( d.gauge, @@ -945,9 +1030,11 @@ func (d *Decoder) decodeInclusiveRange(valueJSON any) *cadence.InclusiveRange { func (d *Decoder) decodePath(valueJSON any) cadence.Path { obj := toObject(valueJSON) - domain := common.PathDomainFromIdentifier(obj.GetString(domainKey)) + domain := get(d, obj, domainKey, func(valueJSON any) common.PathDomain { + return common.PathDomainFromIdentifier(toString(valueJSON)) + }) - identifier := obj.GetString(identifierKey) + identifier := get(d, obj, identifierKey, toString) common.UseMemory(d.gauge, common.NewRawStringMemoryUsage(len(identifier))) path, err := cadence.NewMeteredPath( @@ -964,10 +1051,14 @@ func (d *Decoder) decodePath(valueJSON any) cadence.Path { func (d *Decoder) decodeFunction(valueJSON any) cadence.Function { obj := toObject(valueJSON) - functionType, ok := d.decodeType(obj.Get(functionTypeKey), typeDecodingResults{}).(*cadence.FunctionType) - if !ok { - panic(errors.NewDefaultUserError("invalid function: invalid function type")) - } + functionType := get(d, obj, functionTypeKey, func(valueJSON any) *cadence.FunctionType { + functionType, ok := d.decodeType(valueJSON, typeDecodingResults{}).(*cadence.FunctionType) + if !ok { + panic(errors.NewDefaultUserError("invalid function: invalid function type")) + } + + return functionType + }) return cadence.NewMeteredFunction( d.gauge, @@ -977,28 +1068,39 @@ func (d *Decoder) decodeFunction(valueJSON any) cadence.Function { func (d *Decoder) decodeTypeParameter(valueJSON any, results typeDecodingResults) cadence.TypeParameter { obj := toObject(valueJSON) + + name := get(d, obj, nameKey, toString) + // Unmetered because decodeTypeParameter is metered in decodeTypeParameters and called nowhere else - typeBoundObj, ok := obj[typeBoundKey] + // TODO: getOpt var typeBound cadence.Type + typeBoundObj, ok := obj[typeBoundKey] if ok { + d.pushPath(propertyPathElement(typeBoundKey)) typeBound = d.decodeType(typeBoundObj, results) + d.popPath() } return cadence.NewTypeParameter( - toString(obj.Get(nameKey)), + name, typeBound, ) } -func (d *Decoder) decodeTypeParameters(typeParams []any, results typeDecodingResults) []cadence.TypeParameter { +func (d *Decoder) decodeTypeParameters(valueJSON any, results typeDecodingResults) []cadence.TypeParameter { + typeParams := toSlice(valueJSON) + common.UseMemory(d.gauge, common.MemoryUsage{ Kind: common.MemoryKindCadenceTypeParameter, Amount: uint64(len(typeParams)), }) - typeParameters := make([]cadence.TypeParameter, 0, len(typeParams)) - for _, param := range typeParams { - typeParameters = append(typeParameters, d.decodeTypeParameter(param, results)) + typeParameters := make([]cadence.TypeParameter, len(typeParams)) + + for i, param := range typeParams { + d.pushPath(indexPathElement(i)) + typeParameters[i] = d.decodeTypeParameter(param, results) + d.popPath() } return typeParameters @@ -1006,38 +1108,69 @@ func (d *Decoder) decodeTypeParameters(typeParams []any, results typeDecodingRes func (d *Decoder) decodeParameter(valueJSON any, results typeDecodingResults) cadence.Parameter { obj := toObject(valueJSON) + + label := get(d, obj, labelKey, toString) + id := get(d, obj, idKey, toString) + ty := get(d, obj, typeKey, func(valueJSON any) cadence.Type { + return d.decodeType(valueJSON, results) + }) + // Unmetered because decodeParameter is metered in decodeParameters and called nowhere else return cadence.NewParameter( - toString(obj.Get(labelKey)), - toString(obj.Get(idKey)), - d.decodeType(obj.Get(typeKey), results), + label, + id, + ty, ) } -func (d *Decoder) decodeParameters(params []any, results typeDecodingResults) []cadence.Parameter { +func (d *Decoder) decodeParameters(valueJSON any, results typeDecodingResults) []cadence.Parameter { + params := toSlice(valueJSON) + common.UseMemory(d.gauge, common.MemoryUsage{ Kind: common.MemoryKindCadenceParameter, Amount: uint64(len(params)), }) - parameters := make([]cadence.Parameter, 0, len(params)) - for _, param := range params { - parameters = append(parameters, d.decodeParameter(param, results)) + parameters := make([]cadence.Parameter, len(params)) + + for i, param := range params { + d.pushPath(indexPathElement(i)) + parameters[i] = d.decodeParameter(param, results) + d.popPath() } return parameters } -func (d *Decoder) decodeFieldTypes(fs []any, results typeDecodingResults) []cadence.Field { +func (d *Decoder) decodeInitializers(valueJSON any, results typeDecodingResults) [][]cadence.Parameter { + initializers := toSlice(valueJSON) + + // Unmetered because this is created as an array of nil arrays, not Parameter structs + inits := make([][]cadence.Parameter, len(initializers)) + + for i, params := range initializers { + d.pushPath(indexPathElement(i)) + inits[i] = d.decodeParameters(params, results) + d.popPath() + } + + return inits +} + +func (d *Decoder) decodeFieldTypes(valueJSON any, results typeDecodingResults) []cadence.Field { + fs := toSlice(valueJSON) + common.UseMemory(d.gauge, common.MemoryUsage{ Kind: common.MemoryKindCadenceField, Amount: uint64(len(fs)), }) - fields := make([]cadence.Field, 0, len(fs)) + fields := make([]cadence.Field, len(fs)) - for _, field := range fs { - fields = append(fields, d.decodeFieldType(field, results)) + for i, field := range fs { + d.pushPath(indexPathElement(i)) + fields[i] = d.decodeFieldType(field, results) + d.popPath() } return fields @@ -1045,11 +1178,14 @@ func (d *Decoder) decodeFieldTypes(fs []any, results typeDecodingResults) []cade func (d *Decoder) decodeFieldType(valueJSON any, results typeDecodingResults) cadence.Field { obj := toObject(valueJSON) + + id := get(d, obj, idKey, toString) + ty := get(d, obj, typeKey, func(valueJSON any) cadence.Type { + return d.decodeType(valueJSON, results) + }) + // Unmetered because decodeFieldType is metered in decodeFieldTypes and called nowhere else - return cadence.NewField( - toString(obj.Get(idKey)), - d.decodeType(obj.Get(typeKey), results), - ) + return cadence.NewField(id, ty) } func (d *Decoder) decodePurity(purity any) cadence.FunctionPurity { @@ -1060,14 +1196,32 @@ func (d *Decoder) decodePurity(purity any) cadence.FunctionPurity { return cadence.FunctionPurityImpure } -func (d *Decoder) decodeFunctionType(typeParametersValue, parametersValue, returnValue any, purity any, results typeDecodingResults) cadence.Type { +func (d *Decoder) decodeFunctionType(obj jsonObject, results typeDecodingResults) cadence.Type { + // TODO: getOpt + functionPurity := cadence.FunctionPurityImpure + purity, ok := obj[purityKey] + if ok { + d.pushPath(propertyPathElement(purityKey)) + functionPurity = d.decodePurity(purity) + d.popPath() + } + + // TODO: getOpt var typeParameters []cadence.TypeParameter + typeParametersValue := obj[typeParametersKey] if typeParametersValue != nil { - typeParameters = d.decodeTypeParameters(toSlice(typeParametersValue), results) + d.pushPath(propertyPathElement(typeParametersKey)) + typeParameters = d.decodeTypeParameters(typeParametersValue, results) + d.popPath() } - parameters := d.decodeParameters(toSlice(parametersValue), results) - returnType := d.decodeType(returnValue, results) - functionPurity := d.decodePurity(purity) + + parameters := get(d, obj, parametersKey, func(valueJSON any) []cadence.Parameter { + return d.decodeParameters(valueJSON, results) + }) + + returnType := get(d, obj, returnKey, func(valueJSON any) cadence.Type { + return d.decodeType(valueJSON, results) + }) return cadence.NewMeteredFunctionType( d.gauge, @@ -1080,62 +1234,90 @@ func (d *Decoder) decodeFunctionType(typeParametersValue, parametersValue, retur func (d *Decoder) decodeAuthorization(authorizationJSON any) cadence.Authorization { obj := toObject(authorizationJSON) - kind := obj.Get(kindKey) + + kind := get(d, obj, kindKey, toString) switch kind { case "Unauthorized": return cadence.UnauthorizedAccess + case "EntitlementMapAuthorization": - entitlements := toSlice(obj.Get(entitlementsKey)) - m := toString(toObject(entitlements[0]).Get("typeID")) - return cadence.NewEntitlementMapAuthorization(d.gauge, common.TypeID(m)) + return d.decodeEntitlementMapAuthorization(obj) + case "EntitlementConjunctionSet": - var typeIDs []common.TypeID - entitlements := toSlice(obj.Get(entitlementsKey)) - for _, entitlement := range entitlements { - id := toString(toObject(entitlement).Get("typeID")) - typeIDs = append(typeIDs, common.TypeID(id)) - } - return cadence.NewEntitlementSetAuthorization(d.gauge, typeIDs, cadence.Conjunction) + return d.decodeEntitlementConjunctionSetAuthorization(obj) + case "EntitlementDisjunctionSet": - var typeIDs []common.TypeID - entitlements := toSlice(obj.Get(entitlementsKey)) - for _, entitlement := range entitlements { - id := toString(toObject(entitlement).Get("typeID")) - typeIDs = append(typeIDs, common.TypeID(id)) - } - return cadence.NewEntitlementSetAuthorization(d.gauge, typeIDs, cadence.Disjunction) + return d.decodeEntitlementDisjunctionSetAuthorization(obj) } panic(errors.NewDefaultUserError("invalid kind in authorization: %s", kind)) } +func (d *Decoder) decodeEntitlementMapAuthorization(obj jsonObject) cadence.Authorization { + typeIDs := get(d, obj, entitlementsKey, d.decodeEntitlementTypeIDs) + if len(typeIDs) != 1 { + panic(errors.NewDefaultUserError( + "invalid entitlement map authorization: exactly one entitlement type ID expected", + )) + } + + return cadence.NewEntitlementMapAuthorization(d.gauge, typeIDs[0]) +} + +func (d *Decoder) decodeEntitlementConjunctionSetAuthorization(obj jsonObject) cadence.Authorization { + typeIDs := get(d, obj, entitlementsKey, d.decodeEntitlementTypeIDs) + + return cadence.NewEntitlementSetAuthorization( + d.gauge, + typeIDs, + cadence.Conjunction, + ) +} + +func (d *Decoder) decodeEntitlementDisjunctionSetAuthorization(obj jsonObject) cadence.Authorization { + typeIDs := get(d, obj, entitlementsKey, d.decodeEntitlementTypeIDs) + + return cadence.NewEntitlementSetAuthorization( + d.gauge, + typeIDs, + cadence.Disjunction, + ) +} + +func (d *Decoder) decodeEntitlementTypeIDs(valueJSON any) []common.TypeID { + entitlements := toSlice(valueJSON) + + typeIDs := make([]common.TypeID, len(entitlements)) + + for i, entitlement := range entitlements { + d.pushPath(indexPathElement(i)) + typeIDs[i] = d.decodeEntitlementTypeID(entitlement) + d.popPath() + } + + return typeIDs +} + +func (d *Decoder) decodeEntitlementTypeID(valueJSON any) common.TypeID { + obj := toObject(valueJSON) + id := get(d, obj, typeIDKey, toString) + return common.TypeID(id) +} + //go:linkname setCompositeTypeFields github.com/onflow/cadence.setCompositeTypeFields func setCompositeTypeFields(cadence.CompositeType, []cadence.Field) //go:linkname setInterfaceTypeFields github.com/onflow/cadence.setInterfaceTypeFields func setInterfaceTypeFields(cadence.InterfaceType, []cadence.Field) -func (d *Decoder) decodeNominalType( - obj jsonObject, - kind, typeID string, - fs, initializers []any, - results typeDecodingResults, -) cadence.Type { +func (d *Decoder) decodeNominalType(obj jsonObject, kind string, results typeDecodingResults) cadence.Type { - // Unmetered because this is created as an array of nil arrays, not Parameter structs - inits := make([][]cadence.Parameter, 0, len(initializers)) - for _, params := range initializers { - inits = append( - inits, - d.decodeParameters(toSlice(params), results), - ) - } + inits := get(d, obj, initializersKey, func(valueJSON any) [][]cadence.Parameter { + return d.decodeInitializers(valueJSON, results) + }) - location, qualifiedIdentifier, err := common.DecodeTypeID(d.gauge, typeID) - if err != nil { - panic(errors.NewDefaultUserError("invalid type ID in nominal type: %w", err)) - } + compositeTypeID := get(d, obj, typeIDKey, d.decodeCompositeTypeID) var result cadence.Type var interfaceType cadence.InterfaceType @@ -1145,93 +1327,119 @@ func (d *Decoder) decodeNominalType( case "Struct": compositeType = cadence.NewMeteredStructType( d.gauge, - location, - qualifiedIdentifier, + compositeTypeID.location, + compositeTypeID.qualifiedIdentifier, nil, inits, ) result = compositeType + case "Resource": compositeType = cadence.NewMeteredResourceType( d.gauge, - location, - qualifiedIdentifier, + compositeTypeID.location, + compositeTypeID.qualifiedIdentifier, nil, inits, ) result = compositeType + case "Event": + if len(inits) != 1 { + panic(errors.NewDefaultUserError( + "invalid event: exactly one initializer expected, got %d", + len(inits), + )) + } + compositeType = cadence.NewMeteredEventType( d.gauge, - location, - qualifiedIdentifier, + compositeTypeID.location, + compositeTypeID.qualifiedIdentifier, nil, inits[0], ) result = compositeType + case "Contract": compositeType = cadence.NewMeteredContractType( d.gauge, - location, - qualifiedIdentifier, + compositeTypeID.location, + compositeTypeID.qualifiedIdentifier, nil, inits, ) result = compositeType + case "StructInterface": interfaceType = cadence.NewMeteredStructInterfaceType( d.gauge, - location, - qualifiedIdentifier, + compositeTypeID.location, + compositeTypeID.qualifiedIdentifier, nil, inits, ) result = interfaceType + case "ResourceInterface": interfaceType = cadence.NewMeteredResourceInterfaceType( d.gauge, - location, - qualifiedIdentifier, + compositeTypeID.location, + compositeTypeID.qualifiedIdentifier, nil, inits, ) result = interfaceType + case "ContractInterface": interfaceType = cadence.NewMeteredContractInterfaceType( d.gauge, - location, - qualifiedIdentifier, + compositeTypeID.location, + compositeTypeID.qualifiedIdentifier, nil, inits, ) result = interfaceType + case "Enum": + rawType := get(d, obj, typeKey, func(valueJSON any) cadence.Type { + return d.decodeType(valueJSON, results) + }) + compositeType = cadence.NewMeteredEnumType( d.gauge, - location, - qualifiedIdentifier, - d.decodeType(obj.Get(typeKey), results), + compositeTypeID.location, + compositeTypeID.qualifiedIdentifier, + rawType, nil, inits, ) result = compositeType + case "Attachment": + baseType := get(d, obj, typeKey, func(valueJSON any) cadence.Type { + return d.decodeType(valueJSON, results) + }) + compositeType = cadence.NewMeteredAttachmentType( d.gauge, - location, - qualifiedIdentifier, - d.decodeType(obj.Get(typeKey), results), + compositeTypeID.location, + compositeTypeID.qualifiedIdentifier, + baseType, nil, inits, ) result = compositeType + default: panic(errors.NewDefaultUserError("invalid kind: %s", kind)) } - results[typeID] = result + results[compositeTypeID.typeID] = result - fields := d.decodeFieldTypes(fs, results) + fields := get(d, obj, fieldsKey, func(valueJSON any) []cadence.Field { + return d.decodeFieldTypes(valueJSON, results) + }) switch { case compositeType != nil: @@ -1244,13 +1452,12 @@ func (d *Decoder) decodeNominalType( } func (d *Decoder) decodeIntersectionType( - intersectionValue []any, + obj jsonObject, results typeDecodingResults, ) cadence.Type { - types := make([]cadence.Type, 0, len(intersectionValue)) - for _, typ := range intersectionValue { - types = append(types, d.decodeType(typ, results)) - } + types := get(d, obj, intersectionTypesKey, func(valueJSON any) []cadence.Type { + return d.decodeTypes(valueJSON, results) + }) return cadence.NewMeteredIntersectionType( d.gauge, @@ -1258,6 +1465,20 @@ func (d *Decoder) decodeIntersectionType( ) } +func (d *Decoder) decodeTypes(valueJSON any, results typeDecodingResults) []cadence.Type { + v := toSlice(valueJSON) + + types := make([]cadence.Type, len(v)) + + for i, typ := range v { + d.pushPath(indexPathElement(i)) + types[i] = d.decodeType(typ, results) + d.popPath() + } + + return types +} + type typeDecodingResults map[string]cadence.Type var simpleTypes = func() map[string]cadence.Type { @@ -1306,164 +1527,204 @@ func (d *Decoder) decodeType(valueJSON any, results typeDecodingResults) cadence } obj := toObject(valueJSON) - kindValue := toString(obj.Get(kindKey)) + kindValue := get(d, obj, kindKey, toString) switch kindValue { case "Function": - typeParametersValue := obj[typeParametersKey] - parametersValue := obj.Get(parametersKey) - returnValue := obj.Get(returnKey) - purity, hasPurity := obj[purityKey] - if !hasPurity { - purity = "impure" - } - return d.decodeFunctionType(typeParametersValue, parametersValue, returnValue, purity, results) + return d.decodeFunctionType(obj, results) + case "Intersection": - intersectionValue := obj.Get(intersectionTypesKey) - return d.decodeIntersectionType( - toSlice(intersectionValue), - results, - ) + return d.decodeIntersectionType(obj, results) + case "Optional": - return cadence.NewMeteredOptionalType( - d.gauge, - d.decodeType(obj.Get(typeKey), results), - ) + return d.decodeOptionalType(obj, results) + case "Restriction": - // Backwards-compatibility for format (String):Int", "return": { "kind": "Int" @@ -2693,20 +2693,29 @@ func TestEncodeType(t *testing.T) { TypeParameters: []cadence.TypeParameter{}, }, }, - `{"type":"Type","value":{"staticType": - { - "kind" : "Function", - "purity": "view", - "typeID": "view fun(String):Int", - "return" : {"kind" : "Int"}, - "typeParameters": [], - "parameters" : [ - {"label" : "qux", "id" : "baz", "type": {"kind" : "String"}} - ]} - } - }`, + // language=json + ` + { + "type": "Type", + "value": { + "staticType": { + "kind" : "Function", + "purity": "view", + "typeID": "view fun(String):Int", + "return" : {"kind": "Int"}, + "typeParameters": [], + "parameters": [ + { + "label": "qux", + "id": "baz", + "type": {"kind" : "String"} + } + ] + } + } + } + `, ) - }) t.Run("with static function, without type parameters (decode only)", func(t *testing.T) { @@ -2750,16 +2759,26 @@ func TestEncodeType(t *testing.T) { t.Run("with implicit purity", func(t *testing.T) { - encodedValue := `{"type":"Type","value":{"staticType": - { - "kind" : "Function", - "return" : {"kind" : "Int"}, - "typeParameters": [], - "parameters" : [ - {"label" : "qux", "id" : "baz", "type": {"kind" : "String"}} - ]} - } - }` + //language=json + encodedValue := ` + { + "type": "Type", + "value": { + "staticType": { + "kind" : "Function", + "return": {"kind" : "Int"}, + "typeParameters": [], + "parameters": [ + { + "label": "qux", + "id" : "baz", + "type": {"kind": "String"} + } + ] + } + } + } + ` value := cadence.TypeValue{ StaticType: &cadence.FunctionType{ @@ -2994,16 +3013,16 @@ func TestDecodeCapability(t *testing.T) { t, // language=json ` - { - "type": "Capability", - "value": { - "borrowType": { - "kind": "Int" - }, - "address": "0x0000000102030405", - "id": "6" - } - } + { + "type": "Capability", + "value": { + "borrowType": { + "kind": "Int" + }, + "address": "0x0000000102030405", + "id": "6" + } + } `, cadence.NewCapability( 6, @@ -3021,23 +3040,23 @@ func TestDecodeCapability(t *testing.T) { t, // language=json ` - { - "type": "Capability", - "value": { - "path": { - "type": "Path", - "value": { - "domain": "public", - "identifier": "foo" - } - }, - "borrowType": { - "kind": "Int" - }, - "address": "0x0000000102030405" - } - } - `, + { + "type": "Capability", + "value": { + "path": { + "type": "Path", + "value": { + "domain": "public", + "identifier": "foo" + } + }, + "borrowType": { + "kind": "Int" + }, + "address": "0x0000000102030405" + } + } + `, cadence.NewDeprecatedPathCapability( //nolint:staticcheck cadence.BytesToAddress([]byte{1, 2, 3, 4, 5}), cadence.Path{ @@ -3055,23 +3074,23 @@ func TestDecodeCapability(t *testing.T) { _, err := Decode(nil, []byte( ` - { - "type": "Capability", - "value": { - "path": { - "type": "Path", - "value": { - "domain": "public", - "identifier": "foo" - } - }, - "borrowType": { - "kind": "Int" - }, - "address": "0x0000000102030405" - } - } - `, + { + "type": "Capability", + "value": { + "path": { + "type": "Path", + "value": { + "domain": "public", + "identifier": "foo" + } + }, + "borrowType": { + "kind": "Int" + }, + "address": "0x0000000102030405" + } + } + `, )) require.Error(t, err) @@ -3324,7 +3343,7 @@ func TestDecodeDeprecatedTypes(t *testing.T) { "type": { "kind": "Int" }, - "authorized": true + "authorized": true } } } @@ -3345,19 +3364,19 @@ func TestDecodeDeprecatedTypes(t *testing.T) { // Decode with error if reference is not supported _, err := Decode(nil, []byte(` - { - "type": "Type", - "value": { - "staticType": { - "kind": "Reference", - "type": { - "kind": "Int" - }, - "authorized": true - } - } - } - `)) + { + "type": "Type", + "value": { + "staticType": { + "kind": "Reference", + "type": { + "kind": "Int" + }, + "authorized": tru + } + } + } + `)) require.Error(t, err) }) @@ -3700,7 +3719,10 @@ func TestDecodeInvalidType(t *testing.T) { ` _, err := Decode(nil, []byte(encodedValue)) require.Error(t, err) - assert.Equal(t, "failed to decode JSON-Cadence value: invalid type ID for built-in: ``", err.Error()) + assert.ErrorContains(t, + err, + "invalid type ID for built-in: `` (at .value.id)", + ) }) t.Run("invalid type ID", func(t *testing.T) { @@ -3718,7 +3740,10 @@ func TestDecodeInvalidType(t *testing.T) { ` _, err := Decode(nil, []byte(encodedValue)) require.Error(t, err) - assert.Equal(t, "failed to decode JSON-Cadence value: invalid type ID `I`: invalid identifier location type ID: missing location", err.Error()) + assert.ErrorContains(t, + err, + "invalid type ID `I`: invalid identifier location type ID: missing location (at .value.id)", + ) }) t.Run("unknown location prefix", func(t *testing.T) { @@ -3736,7 +3761,10 @@ func TestDecodeInvalidType(t *testing.T) { ` _, err := Decode(nil, []byte(encodedValue)) require.Error(t, err) - assert.Equal(t, "failed to decode JSON-Cadence value: invalid type ID for built-in: `N.PublicKey`", err.Error()) + assert.ErrorContains(t, + err, + "invalid type ID for built-in: `N.PublicKey` (at .value.id)", + ) }) } @@ -4097,3 +4125,159 @@ func TestSimpleTypes(t *testing.T) { test(cadenceType, semaType) } } + +func TestDecodeErrorContext(t *testing.T) { + t.Parallel() + + t.Run("missing type at root", func(t *testing.T) { + t.Parallel() + + msg := `{"value": "test"}` + _, err := Decode(nil, []byte(msg)) + require.Error(t, err) + require.ErrorContains(t, err, "missing property: type") + }) + + t.Run("missing type in array element", func(t *testing.T) { + t.Parallel() + + //language=json + msg := `{ + "type": "Array", + "value": [ + { + "value": "test" + } + ] + }` + _, err := Decode(nil, []byte(msg)) + require.Error(t, err) + require.ErrorContains(t, err, "missing property: type") + require.ErrorContains(t, err, "at .value[0]") + }) + + t.Run("missing type in nested array element", func(t *testing.T) { + t.Parallel() + + //language=json + msg := ` + { + "type": "Array", + "value": [ + { + "type": "Array", + "value": [ + { + "value": "test" + } + ] + } + ] + } + ` + _, err := Decode(nil, []byte(msg)) + require.Error(t, err) + require.ErrorContains(t, err, "missing property: type") + require.ErrorContains(t, err, "at .value[0].value[0]") + }) + + t.Run("missing type in struct field", func(t *testing.T) { + t.Parallel() + + //language=json + msg := ` + { + "type": "Struct", + "value": { + "id": "S.test.MyStruct", + "fields": [ + { + "name": "myField", + "value": { + "value": "test" + } + } + ] + } + } + ` + _, err := Decode(nil, []byte(msg)) + require.Error(t, err) + require.ErrorContains(t, err, "missing property: type") + require.ErrorContains(t, err, "at .value.fields[0].value") + }) + + t.Run("missing kind in type", func(t *testing.T) { + t.Parallel() + + //language=json + msg := ` + { + "type": "Type", + "value": { + "staticType": { + } + } + } + ` + _, err := Decode(nil, []byte(msg)) + require.Error(t, err) + require.ErrorContains(t, err, "missing property: kind") + require.ErrorContains(t, err, "at .value.staticType") + }) + + t.Run("missing value in array element field", func(t *testing.T) { + t.Parallel() + + //language=json + msg := ` + { + "type": "Array", + "value": [ + { + "type": "Struct", + "value": { + "id": "S.test.MyStruct", + "fields": [ + { + "name": "nested" + } + ] + } + } + ] + } + ` + _, err := Decode(nil, []byte(msg)) + require.Error(t, err) + require.ErrorContains(t, err, "missing property: value") + require.ErrorContains(t, err, "at .value[0].value.fields[0]") + }) + + t.Run("missing value in dictionary key-value pair", func(t *testing.T) { + t.Parallel() + + //language=json + msg := ` + { + "type": "Dictionary", + "value": [ + { + "key": { + "type": "String", + "value": "a" + }, + "value": { + "type": "Int" + } + } + ] + } + ` + _, err := Decode(nil, []byte(msg)) + require.Error(t, err) + require.ErrorContains(t, err, "expected JSON object with keys `type` and `value`") + require.ErrorContains(t, err, "at .value[0].value") + }) + +}