diff --git a/runtime/interpreter/interpreter.go b/runtime/interpreter/interpreter.go index 7a61e17f32..56c2da647f 100644 --- a/runtime/interpreter/interpreter.go +++ b/runtime/interpreter/interpreter.go @@ -4018,6 +4018,50 @@ func (interpreter *Interpreter) authAccountBorrowFunction(addressValue AddressVa ) } +func (interpreter *Interpreter) authAccountCheckFunction(addressValue AddressValue) *HostFunctionValue { + + // Converted addresses can be cached and don't have to be recomputed on each function invocation + address := addressValue.ToAddress() + + return NewHostFunctionValue( + interpreter, + sema.AuthAccountTypeCheckFunctionType, + func(invocation Invocation) Value { + interpreter := invocation.Interpreter + + path, ok := invocation.Arguments[0].(PathValue) + if !ok { + panic(errors.NewUnreachableError()) + } + + domain := path.Domain.Identifier() + identifier := path.Identifier + + storageMapKey := StringStorageMapKey(identifier) + + value := interpreter.ReadStored(address, domain, storageMapKey) + + if value == nil { + return FalseValue + } + + // If there is value stored for the given path, + // check that it satisfies the type given as the type argument. + + typeParameterPair := invocation.TypeParameterTypes.Oldest() + if typeParameterPair == nil { + panic(errors.NewUnreachableError()) + } + + ty := typeParameterPair.Value + + valueStaticType := value.StaticType(interpreter) + + return AsBoolValue(interpreter.IsSubTypeOfSemaType(valueStaticType, ty)) + }, + ) +} + func (interpreter *Interpreter) authAccountLinkFunction(addressValue AddressValue) *HostFunctionValue { // Converted addresses can be cached and don't have to be recomputed on each function invocation diff --git a/runtime/interpreter/value_account.go b/runtime/interpreter/value_account.go index 5448a966fc..fb7d520c90 100644 --- a/runtime/interpreter/value_account.go +++ b/runtime/interpreter/value_account.go @@ -72,6 +72,7 @@ func NewAuthAccountValue( var copyFunction *HostFunctionValue var saveFunction *HostFunctionValue var borrowFunction *HostFunctionValue + var checkFunction *HostFunctionValue var linkFunction *HostFunctionValue var linkAccountFunction *HostFunctionValue var unlinkFunction *HostFunctionValue @@ -188,6 +189,12 @@ func NewAuthAccountValue( } return borrowFunction + case sema.AuthAccountTypeCheckFunctionName: + if checkFunction == nil { + checkFunction = inter.authAccountCheckFunction(address) + } + return checkFunction + case sema.AuthAccountTypeLinkFunctionName: if linkFunction == nil { linkFunction = inter.authAccountLinkFunction(address) diff --git a/runtime/sema/authaccount.cdc b/runtime/sema/authaccount.cdc index 0fa60a2232..8617ee2ab6 100644 --- a/runtime/sema/authaccount.cdc +++ b/runtime/sema/authaccount.cdc @@ -109,6 +109,14 @@ pub struct AuthAccount { /// The path must be a storage path, i.e., only the domain `storage` is allowed pub fun borrow(from: StoragePath): T? + /// Returns true if the object in account storage under the given path satisfies the given type, + /// i.e. could be borrowed using the given type. + /// + /// The given type must not necessarily be exactly the same as the type of the borrowed object. + /// + /// The path must be a storage path, i.e., only the domain `storage` is allowed. + pub fun check(from: StoragePath): Bool + /// **DEPRECATED**: Instead, use `capabilities.storage.issue`, and `capabilities.publish` if the path is public. /// /// Creates a capability at the given public or private path, diff --git a/runtime/sema/authaccount.gen.go b/runtime/sema/authaccount.gen.go index 1e4fe2f827..3425d5deeb 100644 --- a/runtime/sema/authaccount.gen.go +++ b/runtime/sema/authaccount.gen.go @@ -363,6 +363,37 @@ The given type must not necessarily be exactly the same as the type of the borro The path must be a storage path, i.e., only the domain ` + "`storage`" + ` is allowed ` +const AuthAccountTypeCheckFunctionName = "check" + +var AuthAccountTypeCheckFunctionTypeParameterT = &TypeParameter{ + Name: "T", + TypeBound: AnyType, +} + +var AuthAccountTypeCheckFunctionType = &FunctionType{ + TypeParameters: []*TypeParameter{ + AuthAccountTypeCheckFunctionTypeParameterT, + }, + Parameters: []Parameter{ + { + Identifier: "from", + TypeAnnotation: NewTypeAnnotation(StoragePathType), + }, + }, + ReturnTypeAnnotation: NewTypeAnnotation( + BoolType, + ), +} + +const AuthAccountTypeCheckFunctionDocString = ` +Returns true if the object in account storage under the given path satisfies the given type, +i.e. could be borrowed using the given type. + +The given type must not necessarily be exactly the same as the type of the borrowed object. + +The path must be a storage path, i.e., only the domain ` + "`storage`" + ` is allowed. +` + const AuthAccountTypeLinkFunctionName = "link" var AuthAccountTypeLinkFunctionTypeParameterT = &TypeParameter{ @@ -1960,6 +1991,13 @@ func init() { AuthAccountTypeBorrowFunctionType, AuthAccountTypeBorrowFunctionDocString, ), + NewUnmeteredFunctionMember( + AuthAccountType, + ast.AccessPublic, + AuthAccountTypeCheckFunctionName, + AuthAccountTypeCheckFunctionType, + AuthAccountTypeCheckFunctionDocString, + ), NewUnmeteredFunctionMember( AuthAccountType, ast.AccessPublic, diff --git a/runtime/tests/interpreter/account_test.go b/runtime/tests/interpreter/account_test.go index 35530c8172..64fec7448a 100644 --- a/runtime/tests/interpreter/account_test.go +++ b/runtime/tests/interpreter/account_test.go @@ -644,6 +644,10 @@ func TestInterpretAuthAccount_borrow(t *testing.T) { account.save(<-r, to: /storage/r) } + fun checkR(): Bool { + return account.check<@R>(from: /storage/r) + } + fun borrowR(): &R? { return account.borrow<&R>(from: /storage/r) } @@ -652,10 +656,18 @@ func TestInterpretAuthAccount_borrow(t *testing.T) { return account.borrow<&R>(from: /storage/r)!.foo } + fun checkR2(): Bool { + return account.check<@R2>(from: /storage/r) + } + fun borrowR2(): &R2? { return account.borrow<&R2>(from: /storage/r) } + fun checkR2WithInvalidPath(): Bool { + return account.check<@R2>(from: /storage/wrongpath) + } + fun changeAfterBorrow(): Int { let ref = account.borrow<&R>(from: /storage/r)! @@ -680,7 +692,15 @@ func TestInterpretAuthAccount_borrow(t *testing.T) { t.Run("borrow R ", func(t *testing.T) { - // first borrow + // first check & borrow + checkRes, err := inter.Invoke("checkR") + require.NoError(t, err) + AssertValuesEqual( + t, + inter, + interpreter.AsBoolValue(true), + checkRes, + ) value, err := inter.Invoke("borrowR") require.NoError(t, err) @@ -711,7 +731,15 @@ func TestInterpretAuthAccount_borrow(t *testing.T) { // TODO: should fail, i.e. return nil - // second borrow + // second check & borrow + checkRes, err = inter.Invoke("checkR") + require.NoError(t, err) + AssertValuesEqual( + t, + inter, + interpreter.AsBoolValue(true), + checkRes, + ) value, err = inter.Invoke("borrowR") require.NoError(t, err) @@ -727,8 +755,16 @@ func TestInterpretAuthAccount_borrow(t *testing.T) { }) t.Run("borrow R2", func(t *testing.T) { + checkRes, err := inter.Invoke("checkR2") + require.NoError(t, err) + AssertValuesEqual( + t, + inter, + interpreter.AsBoolValue(false), + checkRes, + ) - _, err := inter.Invoke("borrowR2") + _, err = inter.Invoke("borrowR2") RequireError(t, err) require.ErrorAs(t, err, &interpreter.ForceCastTypeMismatchError{}) @@ -744,6 +780,17 @@ func TestInterpretAuthAccount_borrow(t *testing.T) { require.ErrorAs(t, err, &interpreter.DereferenceError{}) }) + + t.Run("check R2 with wrong path", func(t *testing.T) { + checkRes, err := inter.Invoke("checkR2WithInvalidPath") + require.NoError(t, err) + AssertValuesEqual( + t, + inter, + interpreter.AsBoolValue(false), + checkRes, + ) + }) }) t.Run("struct", func(t *testing.T) { @@ -778,6 +825,10 @@ func TestInterpretAuthAccount_borrow(t *testing.T) { account.save(s, to: /storage/s) } + fun checkS(): Bool { + return account.check(from: /storage/s) + } + fun borrowS(): &S? { return account.borrow<&S>(from: /storage/s) } @@ -785,8 +836,12 @@ func TestInterpretAuthAccount_borrow(t *testing.T) { fun foo(): Int { return account.borrow<&S>(from: /storage/s)!.foo } - - fun borrowS2(): &S2? { + + fun checkS2(): Bool { + return account.check(from: /storage/s) + } + + fun borrowS2(): &S2? { return account.borrow<&S2>(from: /storage/s) } @@ -821,7 +876,15 @@ func TestInterpretAuthAccount_borrow(t *testing.T) { t.Run("borrow S", func(t *testing.T) { - // first borrow + // first check & borrow + checkRes, err := inter.Invoke("checkS") + require.NoError(t, err) + AssertValuesEqual( + t, + inter, + interpreter.AsBoolValue(true), + checkRes, + ) value, err := inter.Invoke("borrowS") require.NoError(t, err) @@ -852,7 +915,15 @@ func TestInterpretAuthAccount_borrow(t *testing.T) { // TODO: should fail, i.e. return nil - // second borrow + // second check & borrow + checkRes, err = inter.Invoke("checkS") + require.NoError(t, err) + AssertValuesEqual( + t, + inter, + interpreter.AsBoolValue(true), + checkRes, + ) value, err = inter.Invoke("borrowS") require.NoError(t, err) @@ -868,6 +939,14 @@ func TestInterpretAuthAccount_borrow(t *testing.T) { }) t.Run("borrow S2", func(t *testing.T) { + checkRes, err := inter.Invoke("checkS2") + require.NoError(t, err) + AssertValuesEqual( + t, + inter, + interpreter.AsBoolValue(false), + checkRes, + ) _, err = inter.Invoke("borrowS2") RequireError(t, err)