diff --git a/encoding/json/decode.go b/encoding/json/decode.go index fbbc268f2a..ac62ea620a 100644 --- a/encoding/json/decode.go +++ b/encoding/json/decode.go @@ -215,6 +215,8 @@ func (d *Decoder) decodeJSON(v any) cadence.Value { return d.decodeEvent(valueJSON) case contractTypeStr: return d.decodeContract(valueJSON) + case linkTypeStr: + return d.decodeLink(valueJSON) case pathTypeStr: return d.decodePath(valueJSON) case typeTypeStr: @@ -821,6 +823,29 @@ func (d *Decoder) decodeEnum(valueJSON any) cadence.Enum { )) } +func (d *Decoder) decodeLink(valueJSON any) cadence.PathLink { + obj := toObject(valueJSON) + + targetPath, ok := d.decodeJSON(obj.Get(targetPathKey)).(cadence.Path) + if !ok { + panic(errors.NewDefaultUserError("invalid link: missing or invalid target path")) + } + + borrowType := obj.GetString(borrowTypeKey) + + common.UseMemory(d.gauge, common.MemoryUsage{ + Kind: common.MemoryKindRawString, + // no need to add 1 to account for empty string: string is metered in Link struct + Amount: uint64(len(borrowType)), + }) + + return cadence.NewMeteredLink( + d.gauge, + targetPath, + borrowType, + ) +} + func (d *Decoder) decodePath(valueJSON any) cadence.Path { obj := toObject(valueJSON) diff --git a/encoding/json/encode.go b/encoding/json/encode.go index bc3a5ce01c..ddd9f73bae 100644 --- a/encoding/json/encode.go +++ b/encoding/json/encode.go @@ -126,6 +126,11 @@ type jsonCompositeField struct { Name string `json:"name"` } +type jsonPathLinkValue struct { + TargetPath jsonValue `json:"targetPath"` + BorrowType string `json:"borrowType"` +} + type jsonPathValue struct { Domain string `json:"domain"` Identifier string `json:"identifier"` @@ -238,6 +243,7 @@ const ( resourceTypeStr = "Resource" eventTypeStr = "Event" contractTypeStr = "Contract" + linkTypeStr = "Link" pathTypeStr = "Path" typeTypeStr = "Type" capabilityTypeStr = "Capability" @@ -313,6 +319,8 @@ func Prepare(v cadence.Value) jsonValue { return prepareEvent(x) case cadence.Contract: return prepareContract(x) + case cadence.PathLink: + return prepareLink(x) case cadence.Path: return preparePath(x) case cadence.TypeValue: @@ -592,6 +600,16 @@ func prepareComposite(kind, id string, fieldTypes []cadence.Field, fields []cade } } +func prepareLink(x cadence.PathLink) jsonValue { + return jsonValueObject{ + Type: linkTypeStr, + Value: jsonPathLinkValue{ + TargetPath: preparePath(x.TargetPath), + BorrowType: x.BorrowType, + }, + } +} + func preparePath(x cadence.Path) jsonValue { return jsonValueObject{ Type: pathTypeStr, diff --git a/encoding/json/encoding_test.go b/encoding/json/encoding_test.go index 534d55b954..b8d231eac7 100644 --- a/encoding/json/encoding_test.go +++ b/encoding/json/encoding_test.go @@ -1645,6 +1645,35 @@ func TestEncodeContract(t *testing.T) { testAllEncodeAndDecode(t, simpleContract, resourceContract) } +func TestEncodeLink(t *testing.T) { + + t.Parallel() + + testEncodeAndDecode( + t, + cadence.NewPathLink( + cadence.NewPath("storage", "foo"), + "Bar", + ), + // language=json + ` + { + "type": "Link", + "value": { + "targetPath": { + "type": "Path", + "value": { + "domain": "storage", + "identifier": "foo" + } + }, + "borrowType": "Bar" + } + } + `, + ) +} + func TestEncodeSimpleTypes(t *testing.T) { t.Parallel() diff --git a/runtime/convertValues.go b/runtime/convertValues.go index ecdf78428f..e5393c31d1 100644 --- a/runtime/convertValues.go +++ b/runtime/convertValues.go @@ -211,6 +211,8 @@ func exportValueWithInterpreter( ) case interpreter.AddressValue: return cadence.NewMeteredAddress(inter, v), nil + case interpreter.PathLinkValue: + return exportPathLinkValue(v, inter), nil case interpreter.PathValue: return exportPathValue(inter, v), nil case interpreter.TypeValue: @@ -583,6 +585,12 @@ func exportDictionaryValue( return dictionary.WithType(exportType), err } +func exportPathLinkValue(v interpreter.PathLinkValue, inter *interpreter.Interpreter) cadence.PathLink { + path := exportPathValue(inter, v.TargetPath) + ty := string(inter.MustConvertStaticToSemaType(v.Type).ID()) + return cadence.NewMeteredLink(inter, path, ty) +} + func exportPathValue(gauge common.MemoryGauge, v interpreter.PathValue) cadence.Path { domain := v.Domain.Identifier() common.UseMemory(gauge, common.MemoryUsage{ @@ -802,6 +810,8 @@ func (i valueImporter) importValue(value cadence.Value, expectedType sema.Type) return nil, errors.NewDefaultUserError("cannot import contract") case cadence.Function: return nil, errors.NewDefaultUserError("cannot import function") + case cadence.PathLink: + return nil, errors.NewDefaultUserError("cannot import link") default: // This means the implementation has unhandled types. // Hence, return an internal error diff --git a/runtime/convertValues_test.go b/runtime/convertValues_test.go index eb112a1d4e..86d7f18978 100644 --- a/runtime/convertValues_test.go +++ b/runtime/convertValues_test.go @@ -817,6 +817,17 @@ func TestImportValue(t *testing.T) { Identifier: "foo", }, }, + { + label: "Link (invalid)", + value: cadence.PathLink{ + TargetPath: cadence.Path{ + Domain: "storage", + Identifier: "test", + }, + BorrowType: "Int", + }, + expected: nil, + }, { label: "Capability (invalid)", value: cadence.StorageCapability{ @@ -2087,6 +2098,91 @@ func TestExportStorageCapabilityValue(t *testing.T) { }) } +func TestExportPathLinkValue(t *testing.T) { + + t.Parallel() + + t.Run("Int", func(t *testing.T) { + + link := interpreter.PathLinkValue{ + TargetPath: interpreter.PathValue{ + Domain: common.PathDomainStorage, + Identifier: "foo", + }, + Type: interpreter.PrimitiveStaticTypeInt, + } + + actual, err := exportValueWithInterpreter( + link, + newTestInterpreter(t), + interpreter.EmptyLocationRange, + seenReferences{}, + ) + require.NoError(t, err) + + expected := cadence.PathLink{ + TargetPath: cadence.Path{ + Domain: "storage", + Identifier: "foo", + }, + BorrowType: "Int", + } + + assert.Equal(t, expected, actual) + }) + + t.Run("Struct", func(t *testing.T) { + + const code = ` + struct S {} + ` + program, err := parser.ParseProgram(nil, []byte(code), parser.Config{}) + require.NoError(t, err) + + checker, err := sema.NewChecker( + program, + TestLocation, + nil, + &sema.Config{ + AccessCheckMode: sema.AccessCheckModeNotSpecifiedUnrestricted, + }, + ) + require.NoError(t, err) + + err = checker.Check() + require.NoError(t, err) + + inter := newTestInterpreter(t) + inter.Program = interpreter.ProgramFromChecker(checker) + + capability := interpreter.PathLinkValue{ + TargetPath: interpreter.PathValue{ + Domain: common.PathDomainStorage, + Identifier: "foo", + }, + Type: interpreter.NewCompositeStaticTypeComputeTypeID(inter, TestLocation, "S"), + } + + actual, err := exportValueWithInterpreter( + capability, + inter, + interpreter.EmptyLocationRange, + seenReferences{}, + ) + require.NoError(t, err) + + expected := cadence.PathLink{ + TargetPath: cadence.Path{ + Domain: "storage", + Identifier: "foo", + }, + BorrowType: "S.test.S", + } + + assert.Equal(t, expected, actual) + }) +} + func TestExportCompositeValueWithFunctionValueField(t *testing.T) { t.Parallel() diff --git a/values.go b/values.go index d43c4ee420..325ed0ee17 100644 --- a/values.go +++ b/values.go @@ -1815,6 +1815,49 @@ func (v Contract) String() string { return formatComposite(v.ContractType.ID(), v.ContractType.Fields, v.Fields) } +// PathLink + +type PathLink struct { + TargetPath Path + // TODO: a future version might want to export the whole type + BorrowType string +} + +var _ Value = PathLink{} + +func NewPathLink(targetPath Path, borrowType string) PathLink { + return PathLink{ + TargetPath: targetPath, + BorrowType: borrowType, + } +} + +func NewMeteredLink(gauge common.MemoryGauge, targetPath Path, borrowType string) PathLink { + common.UseMemory(gauge, common.CadencePathLinkValueMemoryUsage) + return NewPathLink(targetPath, borrowType) +} + +func (PathLink) isValue() {} + +func (v PathLink) Type() Type { + return nil +} + +func (v PathLink) MeteredType(_ common.MemoryGauge) Type { + return v.Type() +} + +func (v PathLink) ToGoValue() any { + return nil +} + +func (v PathLink) String() string { + return format.PathLink( + v.BorrowType, + v.TargetPath.String(), + ) +} + // Path type Path struct { diff --git a/values_test.go b/values_test.go index bcd9d8d466..f762524123 100644 --- a/values_test.go +++ b/values_test.go @@ -235,6 +235,16 @@ func TestStringer(t *testing.T) { }), expected: "S.test.FooContract(y: \"bar\")", }, + "Link": { + value: NewPathLink( + Path{ + Domain: "storage", + Identifier: "foo", + }, + "Int", + ), + expected: "PathLink(/storage/foo)", + }, "Path": { value: Path{ Domain: "storage", @@ -627,6 +637,10 @@ func TestGetType(t *testing.T) { reflect.TypeOf(Struct{}): {}, } + typelessTypes := map[reflect.Type]struct{}{ + reflect.TypeOf(PathLink{}): {}, + } + var valueInterface Value valueInterfaceType := reflect.TypeOf(&valueInterface).Elem() @@ -648,6 +662,10 @@ func TestGetType(t *testing.T) { continue } + if _, ok := typelessTypes[valueType]; ok { + continue + } + valueInstance := reflect.New(valueType) value := valueInstance.Elem().Interface().(Value)