Tutorial - Making a User Profile using Cadence
GM! Welcome, everyone, to this session on developing our very first User Profile using Cadence. I am Memxor, your neighborhood’s average nerdy developer, and I’m excited to guide you through this process.
To get started, we will be utilizing the powerful Flow Playground tool for developing our smart contract. It provides us with a convenient environment to write and test our Cadence code effectively.
Without further ado, let’s jump right into the action!
Some basics about Cadence
Before we dive into writing our User Profile smart contract, it’s essential to understand the access types in Cadence and how they relate to access modifiers in other programming languages, such as public, private, and protected.
In Cadence, access types control how resources and functions can be accessed within a contract. Let’s explore the different access types and their similarities to access modifiers in other languages:
Access(all):
This access type is similar to the public access modifier in other languages. It means that the resource or function can be accessed from any account, including both within and outside the contract.
Access(contract):
This access type is similar to the private access modifier in other languages. It means that the resource or function can only be accessed from within the same contract. Other contracts or accounts cannot directly access these resources or invoke these functions.
Access(self):
This access type is unique to Cadence and provides access within the same function. It is similar to the private access modifier, but with the distinction that it limits access to the function rather than the whole contract. Resources or functions marked with access(self) can only be accessed within the same function or contract, not by other accounts or contracts.
Understanding these access types will help us define the appropriate access modifiers for our User Profile smart contract. It ensures that our contract’s resources and functions are accessible in a controlled and secure manner.
That being said, Cadence also supports pub
and priv
. But we will continue with access().
Step 1 - Making the User Profile Contract
In this step, we will focus on writing the Cadence smart contract responsible for managing user profiles. The user profile will contain information about individuals in our application.
Let’s start by defining the structure of the contract and we will learn what it’s doing along the way.
access(all) contract Profile
{
access(contract) var totalUsersCount: UInt64;
access(all) var publicProfileStoragePath: PublicPath;
access(all) var storageProfileStoragePath: StoragePath;
init()
{
self.totalUsersCount = 0;
self.publicProfileStoragePath = /public/userProfile;
self.storageProfileStoragePath = /storage/userProfile;
}
}
access(all) contract Profile
: This line declares a contract namedProfile
with theaccess(all)
access type. It means that this contract can be accessed from any account, both within and outside the contract.access(contract) var totalUsersCount: UInt64;
: This line declares a variable namedtotalUsersCount
of typeUInt64
(an unsigned 64-bit integer). The variable has theaccess(contract)
access type, which means it can only be accessed within the same contract.access(all) var publicProfileStoragePath: PublicPath;
andaccess(all) var storageProfileStoragePath: StoragePath;
: These lines declare two variables,publicProfileStoragePath
andstorageProfileStoragePath
. Both variables have theaccess(all)
access type. These are the storage paths, if you’re not sure what they are please refer to the Beginner Candence Course.init()
: This is the initialization function for the contract. It is called when the contract is created. In this function, the following actions are performed:self.totalUsersCount = 0;
: Sets the initial value oftotalUsersCount
to 0. This variable will be used to keep track of the total number of users.self.publicProfileStoragePath = /public/userProfile;
andself.storageProfileStoragePath = /storage/userProfile;
: Assigns the paths to thepublicProfileStoragePath
andstorageProfileStoragePath
variables. These paths represent the storage locations where user profile information will be stored.
Feel free to copy and paste the code snippet and modify to your needs.
Now let’s go ahead abit and define the UserProfileInfo
struct that will be returned by getUserProfileInfo()
in our next step.
access(all) struct UserProfileInfo
{
access(all) let id: UInt64;
access(all) let name: String;
access(all) let address: String;
init(_ id: UInt64, _ name: String, _ address: String)
{
self.id = id;
self.name = name;
self.address = address;
}
}
access(all) struct UserProfileInfo
: This line declares a struct namedUserProfileInfo
with theaccess(all)
access type. A struct in Cadence is a composite data type that can contain multiple fields.access(all) let id: UInt64;
,access(all) let name: String;
,access(all) let address: String;
: These lines declare three properties of theUserProfileInfo
struct:id
,name
, andaddress
.init(_ id: UInt64, _ name: String, _ address: String)
: This is the initialization function for theUserProfileInfo
struct. It takes three parameters:id
of typeUInt64
,name
of typeString
, andaddress
of typeString
. The function initializes theid
,name
, andaddress
properties of theUserProfileInfo
struct with the provided parameter values.
Now, we will add our UserProfile
resource. If you’re not sure what resources are please refer to the Beginner Cadence Course.
access(all) resource UserProfile : IUserProfilePublic
{
access(all) let id: UInt64;
access(all) let address: String;
access(all) var name: String;
access(all) fun getUserProfileInfo(): UserProfileInfo
{
return UserProfileInfo(self.id, self.name, self.address);
}
access(all) fun updateUserName(_ name: String)
{
self.name = name;
}
init(_ id: UInt64, _ name: String, _ address: String)
{
self.id = id;
self.name = name;
self.address = address;
}
}
access(all) resource UserProfile : IUserProfilePublic
: This line declares a resource namedUserProfile
with theaccess(all)
access type. A resource in Cadence is a type of object that is owned by a single account and can be moved or consumed during transactions. It also implements theIUserProfilePublic
interface, which we will look at next, and why do we use it.access(all) let id: UInt64;
,access(all) let address: String;
,access(all) var name: String;
: These lines declare three properties of theUserProfile
resource:id
,address
, andname
. All three properties have theaccess(all)
access type.access(all) fun getUserProfileInfo(): UserProfileInfo
: This line declares a function namedgetUserProfileInfo
. The function returns a value of typeUserProfileInfo
, which we defined in the previous step.access(all) fun updateUserName(_ name: String)
: This line declares a function namedupdateUserName
that takes a parametername
of typeString
. The function has theaccess(all)
access type, making it accessible from any account. The function updates thename
property of theUserProfile
resource with the providedname
parameter value.init(_ id: UInt64, _ name: String, _ address: String)
: This is the initialization function for theUserProfile
resource. It takes three parameters:id
of typeUInt64
,name
of typeString
, andaddress
of typeString
. The function initializes theid
,name
, andaddress
properties of theUserProfile
resource with the provided parameter values.
Feel free to copy and paste the code snippet and modify to your needs.
Going ahead we will define the IUserProfilePublic
interface first, then we will talk about why we need it! If you’re not sure what interfaces are please chec out the Beginner Cadence Course
access(all) resource interface IUserProfilePublic
{
access(all) let id: UInt64;
access(all) let address: String;
access(all) var name: String;
access(all) fun getUserProfileInfo(): UserProfileInfo;
}
access(all) resource interface IUserProfilePublic
: This line declares a resource interface namedIUserProfilePublic
with theaccess(all)
access type. A resource interface in Cadence defines a set of properties and functions that can be implemented by a resource types.access(all) let id: UInt64;
,access(all) let address: String;
,access(all) var name: String;
: These lines declare three properties within theIUserProfilePublic
interface:id
,address
, andname
. Which is exactly same as what we defined in the resource.access(all) fun getUserProfileInfo(): UserProfileInfo;
: This line declares a function within theIUserProfilePublic
interface namedgetUserProfileInfo
. Which is again same as we defined in the resource.
You’ll notice that the fuction has no boby, that’s because the interface only defines it, and it must be implemented by the resource itself. Same goes with the variables we just define them in the interface and then initialize them in the resource.
Another thing you’ll notice is that the interface doesn’t contain the updateUserName
function. Which bring us to, why do we even need the interface? If you know a few things about Cadence you’ll also know that you can borrow a resource and do things with it, now imagine if someone had the full access to our resource including the updateUserName
function. Anyone would be able to borrow anyone’s resource and change its name, which is not ideal. So we make an interface that doesn’t have the updateUserName
function, and then give everyone the access to this interface. Which means anyone will be able to see our name, id and address. But only the owner will be able to change it. Isn’t that cool?
And finally, we will define a function in the contract that we will be able to call from outside the contract from a trasaction, that will help us to create a user profile.
access(all) fun createUserProfile(_ name: String, _ address: String): @UserProfile
{
let newUserProfile <- create UserProfile(self.totalUsersCount, name, address);
self.totalUsersCount = self.totalUsersCount + 1;
return <- newUserProfile;
}
access(all) fun createUserProfile(_ name: String, _ address: String): @UserProfile
: This line declares a function namedcreateUserProfile
. It takes two parameters:name
of typeString
andaddress
of typeString
. The function returns a reference to a resource of typeUserProfile
using the@UserProfile
syntax.@
in Cadence means a type, in C# it may look liketypeof(UserProfile)
.let newUserProfile <- create UserProfile(self.totalUsersCount, name, address);
: This line creates a new instance of theUserProfile
resource using thecreate
keyword. It initializes thenewUserProfile
variable with the newly created resource. Theself.totalUsersCount
parameter passed to theUserProfile
constructor represents the ID for the new user profile, which is obtained from thetotalUsersCount
property of the current contract.self.totalUsersCount = self.totalUsersCount + 1;
: This line increments thetotalUsersCount
property of the current contract by 1. This ensures that each newly created user profile receives a unique ID.return <- newUserProfile;
: This line returns the newly created user profile resource using the<-
arrow syntax. The caller of thecreateUserProfile
function will receive a reference to the createdUserProfile
resource.
All right! That’s our whole contract. Wasn’t that easy? I hope it was.
The whole contract in action might look something like this
access(all) contract Profile
{
access(contract) var totalUsersCount: UInt64;
access(all) var publicProfileStoragePath: PublicPath;
access(all) var storageProfileStoragePath: StoragePath;
access(all) resource interface IUserProfilePublic
{
access(all) let id: UInt64;
access(all) let address: String;
access(all) var name: String;
access(all) fun getUserProfileInfo(): UserProfileInfo;
}
access(all) resource UserProfile : IUserProfilePublic
{
access(all) let id: UInt64;
access(all) let address: String;
access(all) var name: String;
access(all) fun getUserProfileInfo(): UserProfileInfo
{
return UserProfileInfo(self.id, self.name, self.address);
}
access(all) fun updateUserName(_ name: String)
{
self.name = name;
}
init(_ id: UInt64, _ name: String, _ address: String)
{
self.id = id;
self.name = name;
self.address = address;
}
}
access(all) fun createUserProfile(_ name: String, _ address: String): @UserProfile
{
let newUserProfile <- create UserProfile(self.totalUsersCount, name, address);
self.totalUsersCount = self.totalUsersCount + 1;
return <- newUserProfile;
}
access(all) struct UserProfileInfo
{
access(all) let id: UInt64;
access(all) let name: String;
access(all) let address: String;
init(_ id: UInt64, _ name: String, _ address: String)
{
self.id = id;
self.name = name;
self.address = address;
}
}
init()
{
self.totalUsersCount = 0;
self.publicProfileStoragePath = /public/userProfile;
self.storageProfileStoragePath = /storage/userProfile;
}
}
Now, Let’s go ahead and deploy it to 0x01
account. Select 0x01
and then click on deploy. As shown in the image below!
Step 2 - Making our transactions
We will write 2 transactions, 1 for creating a userProfile and 1 for updating the name in a userProfile.
Creating a User Profile
import Profile from 0x01;
transaction(name: String)
{
prepare(acct: AuthAccount)
{
let newUserProfile <- Profile.createUserProfile(name, acct.address.toString());
acct.save(<- newUserProfile, to: Profile.storageProfileStoragePath);
acct.link<&Profile.UserProfile{Profile.IUserProfilePublic}>(Profile.publicProfileStoragePath, target: Profile.storageProfileStoragePath);
}
}
import Profile from 0x01;
: This line imports the contract namedProfile
from the address0x01
. It allows the transaction to use the functions and resources defined in theProfile
contract.transaction(name: String)
: This line declares a transaction that takes a parametername
of typeString
. A transaction in Cadence represents a sequence of operations that can modify the state of the blockchain.prepare(acct: AuthAccount)
: This line defines theprepare
block of the transaction. It takes anAuthAccount
argument namedacct
, which represents the authenticated account initiating the transaction.let newUserProfile <- Profile.createUserProfile(name, acct.address.toString());
: This line creates a new user profile by calling thecreateUserProfile
function from the importedProfile
contract. It passes thename
parameter and the string representation of the account’s address (acct.address.toString()
) as arguments. Then we use the<-
syntax to move the newly created resource into the variable.acct.save(<- newUserProfile, to: Profile.storageProfileStoragePath);
: This line saves the newly created user profile resource to the storage path specified byProfile.storageProfileStoragePath
. The<-
syntax is used again to move the resource from the variable to storage.acct.link<&Profile.UserProfile{Profile.IUserProfilePublic}>(Profile.publicProfileStoragePath, target: Profile.storageProfileStoragePath);
: This line establishes a link between the public storage pathProfile.publicProfileStoragePath
and the storage path where the user profile resource is storedProfile.storageProfileStoragePath
. Thelink
function is called on theacct
account object, specifying the type&Profile.UserProfile{Profile.IUserProfilePublic}
to indicate the resource interface type being linked. By doing this, we will be able to read the state of any User Profile from any Script.
Wanna give it a spin? Type you name in the box below and hit Send. As shown in the image below!
Updating the name in a User Profile
import Profile from 0x01;
transaction(name: String)
{
prepare(acct: AuthAccount)
{
let userInfo = acct.borrow<&Profile.UserProfile>(from: Profile.storageProfileStoragePath) ?? panic("Can't borrow the file from storage!");
userInfo.updateUserName(name);
}
}
import Profile from 0x01;
: This line imports the contract namedProfile
from the address0x01
. It allows the transaction to use the functions and resources defined in theProfile
contract.transaction(name: String)
: This line declares a transaction that takes a parametername
of typeString
.prepare(acct: AuthAccount)
: This line defines theprepare
block of the transaction. It takes anAuthAccount
argument namedacct
, which represents the authenticated account initiating the transaction.let userInfo = acct.borrow<&Profile.UserProfile>(from: Profile.storageProfileStoragePath) ?? panic("Can't borrow the file from storage!");
: This line borrows a reference to the user profile resource from storage. It uses theborrow
function on theacct
account object to borrow a reference to theUserProfile
resource. The&Profile.UserProfile
type parameter specifies the type of the resource being borrowed. Thefrom
keyword is used to indicate the storage path from which the resource is being borrowed. If the borrowing operation fails (i.e., the resource is not found), the??
operator is used for error handling, and the program panics with the error message “Can’t borrow the file from storage!“.userInfo.updateUserName(name);
: This line calls theupdateUserName
function on the borrowed referenceuserInfo
to update the user’s name with the providedname
parameter.
Exited? Let’s spin it!
Type the new name, and hit Send. As shown in the example below!
Step 3 - Making our Scripts
We are in the final stretch now! We will make a script that will be able to read from the storage and show us information stored there.
import Profile from 0x01;
pub fun main(add: Address): Profile.UserProfileInfo
{
let publicCap = getAccount(add).getCapability(Profile.publicProfileStoragePath).borrow<&Profile.UserProfile{Profile.IUserProfilePublic}>() ?? panic("Can't find public path!");
return publicCap.getUserProfileInfo();
}
import Profile from 0x01;
: This line imports the contract namedProfile
from the address0x01
.pub fun main(add: Address): Profile.UserProfileInfo
: This line declares a public function namedmain
. The function takes anadd
parameter of typeAddress
and returns a value of typeProfile.UserProfileInfo
.let publicCap = getAccount(add).getCapability(Profile.publicProfileStoragePath)
.borrow<&Profile.UserProfile{Profile.IUserProfilePublic}>()
?? panic("Can't find public path!");
: This line retrieves the capability for accessing the public storage path of the user profile. It first callsgetAccount(add)
to get the account object associated with the provided address. Then, it callsgetCapability(Profile.publicProfileStoragePath)
on that account object to obtain the capability for the specified public storage path. Theborrow
function is used to borrow a reference to theUserProfile
resource from the capability. The&Profile.UserProfile{Profile.IUserProfilePublic}
type parameter specifies that the borrowed reference should conform to theIUserProfilePublic
interface defined within theUserProfile
resource. If the borrowing operation fails (i.e., the resource is not found or does not conform to the interface), the??
operator is used for error handling, and the program panics with the error message “Can’t find public path!“.return publicCap.getUserProfileInfo();
: This line calls thegetUserProfileInfo
function on the borrowed referencepublicCap
to retrieve the user profile information. The function returns this information as the result of themain
function.
Fire it up! Put in the account address as 0x01
in the box and hit Execute. And you should be able to see the logs in the logs tab. As shown in the image below.
WOW! You did it! Congratulations! Give yourself a pat in the back and buy ourself something nice to eat (please stop with those Doritos)!