Resource Contract Tutorial
An introduction to resources, capabilities, and account storage in Cadence
Overview
https://play.onflow.org/a26ec672-14c8-42cd-85ad-1409a8a23a84
The tutorial will ask you to take various actions to interact with this code.
This tutorial builds on the previous Hello World
tutorial.
Before beginning this tutorial, you should understand :
This tutorial will build on your understanding of accounts and how to interact with them by introducing resources. Resources are one of the Cadence's defining features. In Cadence, resources are a composite type like a struct or a class, but with some special rules.
Resources are useful when you want to model direct ownership. Traditional structs or classes from other conventional programming languages are not an ideal way to represent ownership because they can be copied. This means a coding error can easily result in creating multiple copies of the same asset, which breaks the scarcity requirements needed for these assets to have real value. We have to consider loss and theft at the scale of a house, a car, or a bank account with millions of dollars, or a horse. Resources, in turn, solve this problem by making creation, destruction, and movement of assets explicit.
In this tutorial, you will:
- Deploy a contract that declares a resource
- Save the resource into the account storage
- Interact with the resource we created using a transaction
Implementing a Contract with Resources
To interact with resources, you'll learn a few important concepts:
- Using the create keyword
- The move operator
<-
- The Account Storage API
Let's start by looking at how to create a resource with the create
keyword and the move operator <-
.
You use the create
keyword used to initialize a resource.
Resources must be created before you can use them.
The move operator <-
is used to move a resource into a variable.
You cannot use the assignment operator =
with resources, so when you initialize a resource you will need to use the move operator <-
.
Open the Account 0x01
tab with file named HelloWorldResource.cdc
.
HelloWorldResource.cdc
should contain the following code:
pub contract HelloWorld {
// Declare a resource that only includes one function.
pub resource HelloAsset {
// A transaction can call this function to get the "Hello, World!"
// message from the resource.
pub fun hello(): String {
return "Hello, World!"
}
}
// We're going to use the built-in create function to create a new instance
// of the HelloAsset resource
pub fun createHelloAsset(): @HelloAsset {
return <-create HelloAsset()
}
init() {
log("Hello Asset")
}
}
Deploy this code to account 0x01
using the Deploy
button.
We start by declaring a new HelloWorld
contract in account 0x01
, inside this new HelloWorld
contract we:
- Declare the resource
HelloAsset
with public scopepub
- Declare the function
hello()
insideHelloAsset
with public scopepub
- Declare the function
createHelloAsset()
which willcreate
aHelloAsset
resource - The
createHelloAsset()
function will use the the move operator (<-
) to return the resource
This is another example of what we can do with a contract. Cadence can declare type definitions within deployed contracts. A type definition is simply a description of how a particular set of data is organized. It isn't a copy of that data on its own. Any account can import these definitions and use them to create an object that follows that definition or to interact with other objects of those types.
This contract that we just deployed declares a definition for the HelloAsset
resource and a function to create the resource.
Let's walk through this contract in more detail, starting with the resource. Resources are one of the most important things that Cadence introduces to the smart contract design experience:
pub resource HelloAsset {
pub fun hello(): String {
return "Hello, World!"
}
}
Resources
The key difference between a resource and a struct or class is the access scope for resources:
- Each instance of a resource can only exist in exactly one location and cannot be copied. Here, location refers to account storage, a temporary variable in a function, a storage field in a contract, etc.
- Resources must be explicitly moved from one location to another when accessed.
- Resources also cannot go out of scope at the end of function execution. They must be explicitly stored somewhere or destroyed.
These characteristics make it impossible to accidentally lose a resource from a coding mistake.
init() {
// ...
All composite types like contracts, resources,
and structs can have an optional init()
function that only runs when the object is initially created.
Cadence requires that all fields must be explicitly initialized,
so if the object has any fields,
this function has to be used to initialize them.
Contracts also have read and write access to the storage of the account that they are deployed to by using the built-in
self.account
object.
This is an AuthAccount
object
that gives them access to many different functions to interact with the private storage of the account.
This contract's init
function is simple, it logs the phrase "Hello Asset"
to the console.
A resource can only be created in the scope that it is defined in.
This prevents anyone from being able to create arbitrary amounts of resource objects that others have defined.
The Move Operator (<-
)
In this example, we declared a function that can create HelloAsset
resources:
pub fun createHelloAsset(): @HelloAsset {
return <-create HelloAsset()
}
The @
symbol specifies that it is a resource, of the type HelloAsset
which we defined in the contract.
This function uses the move operator to create a resource of type HelloAsset
and return it
To create a new resource object, we use the create
keyword
Here we use the <-
symbol. This is the move operator.
The move operator <-
replaces the assignment operator =
in assignments that involve resources.
To make the assignment of resources explicit, the move operator <-
must be used when:
- the resource is the initial value of a constant or variable,
- the resource is moved to a different variable in an assignment,
- the resource is moved to a function as an argument
- the resource is returned from a function.
When a resource is moved, the old location is invalidated, and the object moves into the context of the new location.
So if I have a resource in the variable first_resource
, like so:
// Note the `@` symbol to specify that it is a resource
var first_resource: @AnyResource <- create AnyResource()
and I want to assign it to a new variable, second_resource
,
after I do the assignment, first_resource
is invalid because the underlying resource has been moved to the new variable.
var second_resource <- first_resource
// first_resource is now invalid. Nothing can be done with it
Regular assignments of resources are not allowed because assignments only copy the value.
Resources can only exist in one location at a time, so movement must be explicitly shown in the code by using the move operator <-
.
Create Hello Transaction
Now we're going to use a transaction to that calls the createHelloAsset()
function
and saves a HelloAsset
resource to the account storage.
Open the transaction named Create Hello
.
Create Hello
should contain the following code:
// Transaction1.cdc
// This transaction calls the createHelloAsset() function from the contract
// to create a resource, then saves the resource in account storage using the "save" method.
import HelloWorld from 0x01
transaction {
prepare(acct: AuthAccount) {
// Here we create a resource and move it to the variable newHello,
// then we save it in the account storage
let newHello <- HelloWorld.createHelloAsset()
acct.save(<-newHello, to: /storage/HelloAssetTutorial)
}
// In execute, we log a string to confirm that the transaction executed successfully.
execute {
log("Saved Hello Resource to account.")
}
}
Here's what this transaction does:
- Import the
HelloWorld
definitions from account0x01
- Uses the
createHelloAsset()
function to create a resource and move it tonewHello
save
the created resource in the account storage of the account that deployed this contract, at the path/storage/HelloAssetTutorial
log
the textHelloAsset created and stored
to the console.
This is our first transaction using the prepare
phase!
The prepare
phase is the only place that has access to the signing accounts'
private AuthAccount
object.
AuthAccount
objects have many different methods that are used to interact with account storage.
You can see the documentation for all of these in the account section of the language reference.
In this tutorial, we'll be using AuthAccount
methods to save and load from /storage/
.
The prepare
phase can also create /private/
and /public/
links to the objects in /storage/
,
called capabilities (more on these later).
By not allowing the execute phase to access account storage, we can statically verify which assets and areas of the signers' storage a given transaction can modify. Browser wallets and applications that submit transactions for users can use this to show what a transaction could alter, giving users information about transactions that wallets will be executing for them, and confidence that they aren't getting fed a malicious or dangerous transaction from an app or wallet.
Let's go over the transaction in more detail.
To create a HelloAsset
resource, we accessed the function createHelloAsset()
from our contract, and moved the
resource it created to the variable newHello
.
let newHello <- HelloWorld.createHelloAsset()
Next, we save the resource to the account storage.
We use the account storage API to interact with the account storage in Flow.
To save the resource, we'll be using the
save()
method from the account storage API to store the resource in the account at the path /storage/HelloAssetTutorial
.
acct.save(<-newHello, to: /storage/HelloAssetTutorial)
The first parameter to save
is the object that is being stored,
and the to
parameter is the path that the object is being stored at.
The path must be a storage path, so only the domain /storage/
is allowed as the to
parameter.
If there is already an object stored under the given path, the program aborts. Remember, the Cadence type system ensures that a resource can never be accidentally lost. When moving a resource to a field, into an array, into a dictionary, or into storage, there is the possibility that the location already contains a resource. Cadence forces the developer to handle the case of an existing resource so that it is not accidentally lost through an overwrite.
It is also very important when choosing the name of your paths to pick an identifier that is very specific and unique to your project. Currently, account storage paths are global, so there is a chance that projects could use the same storage paths, which could cause path conflicts! This could be a headache for you, so choose unique path names to avoid this problem.
Finally, in the execute phase we log the phrase "Saved Hello Resource to account."
to the console.
log("Saved Hello Resource to account.")
Select account 0x01
as the only signer. Click the Send
button to submit
the transaction.
You should see something like this:
"Saved Hello Resource to account."
You can also try removing the line of code that saves newHello
to storage.
You should see an error for `newHello` that says `loss of resource`. This means that you are not handling the resource properly. If you ever see this error in any of your programs, it means there is a resource somewhere that is not being explicitly stored or destroyed, meaning the program is invalid.
Add the line back to make the contract check properly.
In this case, this is the first time we have saved anything with the selected account,
so we know that the storage spot at /storage/HelloAssetTutorial
is empty.
In real applications, we would likely perform necessary checks and actions with the location path we are storing in
to make sure we don't abort a transaction because of an accidental overwrite.
Now that you have executed the transaction, account 0x01
should have the newly created HelloWorld.HelloAsset
resource stored in its storage.
Load Hello Transaction
Now we're going to use a transaction to call the hello()
method from the HelloAsset
resource.
Open the transaction named Load Hello
.
Load Hello
should contain the following code:
import HelloWorld from 0x01
// This transaction calls the "hello" method on the HelloAsset object
// that is stored in the account's storage by removing that object
// from storage, calling the method, and then putting it back in storage
transaction {
prepare(acct: AuthAccount) {
// Load the resource from storage, specifying the type to load it as
// and the path where it is stored
let helloResource <- acct.load<@HelloWorld.HelloAsset>(from: /storage/HelloAssetTutorial)
// We use optional chaining (?) because the value in storage
// may or may not exist, and thus is considered optional.
log(helloResource?.hello())
// Put the resource back in storage at the same spot
// We use the force-unwrap operator `!` to get the value
// out of the optional. It aborts if the optional is nil
acct.save(<-helloResource!, to: /storage/HelloAssetTutorial)
}
}
Here's what this transaction does:
- Import the
HelloWorld
definitions from account0x01
- Moves the
HelloAsset
object from storage tohelloResource
with the move operator and theload
function from the account storage API - Calls the
hello()
function of theHelloAsset
resource stored inhelloResource
and logs the result - Saves the resource in the account that we originally moved it from at the path
/storage/HelloAssetTutorial
We're going to be using the prepare
phase again to load the resource because it
has access to the signing accounts' private AuthAccount
object.
Let's go over the transaction in more detail.
To remove an object from storage, we use the load
method from the account storage API
let helloResource <- acct.load<@HelloWorld.HelloAsset>(from: /storage/HelloAssetTutorial)
If no object of the specified type is stored under the given path, the function returns nil
.
(This is an Optional,
a special type of data that we will cover later)
If the object at the given path is not of the specified type, Cadence will throw an error and the transaction will fail.
If there is an object of the specified type at the path, the function returns that object and the account storage will no longer contain an object under the given path.
The type parameter for the object type to load is contained in <>
.
A type argument for the parameter must be provided explicitly, which is @HelloWorld.HelloAsset
here.
(Note the @
symbol to specify that it is a resource)
The path from
must be a storage path, so only the domain /storage/
is allowed.
Next, we call the hello()
function and log the output.
log(helloResource?.hello())
We use ?
because the values in the storage are returned as optionals.
Optionals are values that can represent the absence of a value.
Optionals have two cases: either there is a value of the specified type, or there is nothing (nil
).
An optional type is declared using the ?
suffix.
let newResource: HelloAsset? // could either have a value of type `HelloAsset`
// or it could have a value of `nil`
Optionals allow developers to account for nil
cases more gracefully.
Here, we explicitly have to account for the possibility that the helloResource
object we got with load
is nil
(because load
will return nil
if there is nothing there to load).
Using ?
"unwraps" the optional, meaning that it gets the value if it is there, before calling hello
,
but only if the value isn't nil
. If the value is nil, the ?
returns nil
.
Because ?
is used when calling the hello
function, the function call only happens if the stored value is not nil
.
In this case, the result of the hello
function will be returned as an optional.
However, if the stored value was nil
, the function call would not occur and the result is nil
.
Next, we use save
again to put the object back in storage in the same spot:
acct.save(<-helloResource!, to: /storage/HelloAssetTutorial)
Remember, helloResource
is still an optional, so we have to handle the possibility that it is nil
.
Here, we use the force-unwrap operator (!
). This operator gets the value in the optional if it contains a value,
and aborts the entire transaction if the object is nil
.
It is a more risky way of dealing with optionals, but if your program is ever in a state where a value being nil
would defeat the purpose of the whole transaction, then the force-unwrap operator might be a good choice to deal with that.
Refer to Optionals In Cadence to learn more about optionals and how they are used.
Select account 0x01
as the only signer. Click the Send
button to submit
the transaction.
You should see something like this:
"Hello, World!"
Reviewing the Resource Contract
This tutorial covered an introduction to resources in Cadence, using the account storage API and interacting with resources using transactions.
You implemented a smart contract that is accessible in all scopes.
The smart contract had a resource declared that implemented a function called hello()
that returns the string "Hello, World!"
and declared a function that can create a resource.
Next, you deployed this contract in an account and implemented a transaction to create the resource in the smart contract
and save it in the account 0x01
by using it as the signer for this transaction.
Finally, you used a transaction to move the HelloAsset
resource from account storage, call the hello
method,
and return it to the account storage.
Now that you have completed the tutorial, you have the basic knowledge to write a simple Cadence program that can:
- Implement a resource in a smart contract
- Save, move, and load resources using the account storage API and the move operator
<-
- Use the
prepare
phase of a transaction to load resources from account storage
Feel free to modify the smart contract to create different resources, experiment with the available account storage API, and write new transactions and scripts that execute different functions from your smart contract. Have a look at the resource reference page to find out more about what you can do with resources.
You're on the right track to building more complex applications with Cadence, now is a great time to check out the Cadence Best Practices document and Anti-patterns document as your applications become more complex.