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 namedProfilewith 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 namedtotalUsersCountof 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,publicProfileStoragePathandstorageProfileStoragePath. 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 oftotalUsersCountto 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 thepublicProfileStoragePathandstorageProfileStoragePathvariables. 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 namedUserProfileInfowith 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 theUserProfileInfostruct:id,name, andaddress.init(_ id: UInt64, _ name: String, _ address: String): This is the initialization function for theUserProfileInfostruct. It takes three parameters:idof typeUInt64,nameof typeString, andaddressof typeString. The function initializes theid,name, andaddressproperties of theUserProfileInfostruct 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 namedUserProfilewith 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 theIUserProfilePublicinterface, 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 theUserProfileresource: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 namedupdateUserNamethat takes a parameternameof typeString. The function has theaccess(all)access type, making it accessible from any account. The function updates thenameproperty of theUserProfileresource with the providednameparameter value.init(_ id: UInt64, _ name: String, _ address: String): This is the initialization function for theUserProfileresource. It takes three parameters:idof typeUInt64,nameof typeString, andaddressof typeString. The function initializes theid,name, andaddressproperties of theUserProfileresource 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 namedIUserProfilePublicwith 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 theIUserProfilePublicinterface: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 theIUserProfilePublicinterface 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:nameof typeStringandaddressof typeString. The function returns a reference to a resource of typeUserProfileusing the@UserProfilesyntax.@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 theUserProfileresource using thecreatekeyword. It initializes thenewUserProfilevariable with the newly created resource. Theself.totalUsersCountparameter passed to theUserProfileconstructor represents the ID for the new user profile, which is obtained from thetotalUsersCountproperty of the current contract.self.totalUsersCount = self.totalUsersCount + 1;: This line increments thetotalUsersCountproperty 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 thecreateUserProfilefunction will receive a reference to the createdUserProfileresource.
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 namedProfilefrom the address0x01. It allows the transaction to use the functions and resources defined in theProfilecontract.transaction(name: String): This line declares a transaction that takes a parameternameof typeString. A transaction in Cadence represents a sequence of operations that can modify the state of the blockchain.prepare(acct: AuthAccount): This line defines theprepareblock of the transaction. It takes anAuthAccountargument 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 thecreateUserProfilefunction from the importedProfilecontract. It passes thenameparameter 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.publicProfileStoragePathand the storage path where the user profile resource is storedProfile.storageProfileStoragePath. Thelinkfunction is called on theacctaccount 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 namedProfilefrom the address0x01. It allows the transaction to use the functions and resources defined in theProfilecontract.transaction(name: String): This line declares a transaction that takes a parameternameof typeString.prepare(acct: AuthAccount): This line defines theprepareblock of the transaction. It takes anAuthAccountargument 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 theborrowfunction on theacctaccount object to borrow a reference to theUserProfileresource. The&Profile.UserProfiletype parameter specifies the type of the resource being borrowed. Thefromkeyword 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 theupdateUserNamefunction on the borrowed referenceuserInfoto update the user’s name with the providednameparameter.
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 namedProfilefrom the address0x01.pub fun main(add: Address): Profile.UserProfileInfo: This line declares a public function namedmain. The function takes anaddparameter of typeAddressand 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. Theborrowfunction is used to borrow a reference to theUserProfileresource from the capability. The&Profile.UserProfile{Profile.IUserProfilePublic}type parameter specifies that the borrowed reference should conform to theIUserProfilePublicinterface defined within theUserProfileresource. 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 thegetUserProfileInfofunction on the borrowed referencepublicCapto retrieve the user profile information. The function returns this information as the result of themainfunction.
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)!