The old Cadence capability system is being deprecated. Namely, link
and getCapability
are not what you should use anymore to create capabilities.
This tutorial will walk through the new mechanism: Capability Controllers (“Cap Cons”).
Example Overview
In order to showcase the new Capability Controllers system, let’s create at an example contract so we can compare how you would create, manage, and get the old vs. new capabilities side by side.
pub contract HelloWorld {
pub resource interface IGreeting {
pub var greeting: String
}
pub resource Greeting: IGreeting {
pub var greeting: String
pub fun changeGreeting(newGreeting: String) {
self.greeting = newGreeting
}
init(greeting: String) {
self.greeting = greeting
}
}
pub fun createGreeting(greeting: String): @Greeting {
return <- create Greeting(greeting: greeting)
}
}
The idea is that we have a Greeting
resource. We want the public to be able to read the greeting
variable, but only the owner of the resource should be able to call the changeGreeting
function.
Old vs. New: Creating a Public Capability
First we want to store the greeting resource in a user’s storage, and then allow the public to read the greeting variable.
Here is the old way of doing that:
import HelloWorld from 0x01
transaction(greeting: String) {
prepare(signer: AuthAccount) {
// store the resource
signer.save(<- HelloWorld.createGreeting(greeting: greeting), to: /storage/MyGreeting)
// create a public capability restricted to `IGreeting` that is linked to a storage path
signer.link<&HelloWorld.Greeting{HelloWorld.IGreeting}>(/public/MyGreeting, target: /storage/MyGreeting)
}
}
Here is the new way of doing that:
import HelloWorld from 0x01
transaction(greeting: String) {
prepare(signer: AuthAccount) {
// store the resource
signer.save(<- HelloWorld.createGreeting(greeting: greeting), to: /storage/MyGreeting)
// create a storage capability, previously known as a "private capability"
let cap = signer.capabilities.storage.issue<&HelloWorld.Greeting{HelloWorld.IGreeting}>(/storage/MyGreeting)
// publish the storage ("private") capability to a public path
signer.capabilities.publish(cap, /public/MyGreeting)
}
}
As you can see, instead of just linking a certain type to the public from a storage path, it is now broken up into 2 steps:
- Create a new storage capability (previously known as a “private capability”) first
- Issue that storage capability to a public path
Old vs. New: Getting a Public Capability
Now let’s walk through how you would actually get the capability from a user’s account.
Here is the old way of doing that:
import HelloWorld from 0x01
pub fun main(user: Address): String {
let greetingCap: Capability<&HelloWorld.Greeting{HelloWorld.IGreeting}> = getAccount(user).getCapability<&HelloWorld.Greeting{HelloWorld.IGreeting}>(/public/MyGreeting)
let greetingRef: &HelloWorld.Greeting{HelloWorld.IGreeting} = greetingCap.borrow() ?? panic("This public capability does not exist.")
return greetingRef.greeting
}
Here is the new way of doing that:
import HelloWorld from 0x01
pub fun main(user: Address): String {
// Note how the get function returns an optional now.
let greetingCap: Capability<&HelloWorld.Greeting{HelloWorld.IGreeting}> = getAccount(user).capabilities.get<&HelloWorld.Greeting{HelloWorld.IGreeting}>(/public/MyGreeting)
?? panic("This public capability does not exist.")
let greetingRef: &HelloWorld.Greeting{HelloWorld.IGreeting} = greetingCap.borrow()!
return greetingRef.greeting
}
OR, you can use a more simplified version using the new borrow
conveniance function:
import HelloWorld from 0x01
pub fun main(user: Address): String {
let greetingRef: &HelloWorld.Greeting{HelloWorld.IGreeting} = getAccount(user).capabilities.borrow<&HelloWorld.Greeting{HelloWorld.IGreeting}>(/public/MyGreeting)
?? panic("This public capability does not exist.")
return greetingRef.greeting
}
Old vs. New: Removing a Public Capability
Here is the old way of doing that:
import HelloWorld from 0x01
transaction(greeting: String) {
prepare(signer: AuthAccount) {
signer.unlink(/public/MyGreeting)
}
}
Here is the new way of doing that:
import HelloWorld from 0x01
transaction(greeting: String) {
prepare(signer: AuthAccount) {
signer.capabilities.unpublish(/public/MyGreeting)
}
}
Old vs. New: Creating & Getting a Storage (“Private”) Capability
As you’ve seen above, public capabilities are pretty similar. It is just different syntax.
However, private capabilities are very different now.
Here is the old way of creating a private capability:
import HelloWorld from 0x01
transaction(greeting: String) {
prepare(signer: AuthAccount) {
// store the resource
signer.save(<- HelloWorld.createGreeting(greeting: greeting), to: /storage/MyGreeting)
// create a private capability that is linked to a storage path
signer.link<&HelloWorld.Greeting>(/private/MyGreeting, target: /storage/MyGreeting)
// then, at a later point (potentially in a different transaction), you would borrow it and distribute it like this:
let privateCap: Capability<&HelloWorld.Greeting> = signer.getCapability<&HelloWorld.Greeting>(/private/MyGreeting)
// ... and you would then move it around as you please
}
}
Here is the new way of creating a private capability:
import HelloWorld from 0x01
transaction(greeting: String) {
prepare(signer: AuthAccount) {
// store the resource
signer.save(<- HelloWorld.createGreeting(greeting: greeting), to: /storage/MyGreeting)
// create a storage capability, previously known as a "private capability"
let privateCap = signer.capabilities.storage.issue<&HelloWorld.Greeting>(/storage/MyGreeting)
// ... and you would then move it around as you please
}
}
What’s really important to see here is that in the old system, you would just create a private capability one time and then be able to get that one capability whenever you’d like. In the new system, every time you want to get the storage (“private”) capability, you have to make a new one. There is no way to go back and get an old private capability you created.
The downside here is that you waste a lot of storage by having to fetch a private capability, because you’re not really fetching it, you’re making a new one. The benefit is that you can now individually control the private capabilities you’re giving out. So unlike the old way where unlinking a private capability takes away control from everyone who has it, in the new way you can individually unlink certain capabilities.
Also note that private paths do not exist anymore. You are instead just issuing storage capabilities on a certain storage path. Because of this, you need a way to view all the capabilities you’ve created for a certain storage path so you can individually manage them. Here is how you’d do that:
import HelloWorld from 0x01
pub fun main(user: Address, storagePath: StoragePath): [&AuthAccount.StorageCapabilityController] {
let authAccount: AuthAccount = getAuthAccount(user)
return authAccount.capabilities.storage.getControllers(forPath: storagePath)
}
Here is what the &AuthAccount.StorageCapabilityController
type looks like:
pub struct StorageCapabilityController {
/// An arbitrary "tag" for the controller.
/// For example, it could be used to describe the purpose of the capability.
/// Empty by default.
pub var tag: String
/// The type of the controlled capability, i.e. the T in `Capability<T>`.
pub let borrowType: Type
/// The identifier of the controlled capability.
/// All copies of a capability have the same ID.
pub let capabilityID: UInt64
/// Delete this capability controller,
/// and disable the controlled capability and its copies.
///
/// The controller will be deleted from storage,
/// but the controlled capability and its copies remain.
///
/// Once this function returns, the controller is no longer usable,
/// all further operations on the controller will panic.
///
/// Borrowing from the controlled capability or its copies will return nil.
///
pub fun delete()
/// Returns the targeted storage path of the controlled capability.
pub fun target(): StoragePath
/// Retarget the controlled capability to the given storage path.
/// The path may be different or the same as the current path.
pub fun retarget(_ target: StoragePath)
}
The most important thing to note here is the capabilityID
. Each capability now has an ID that you need to know to manage it.
Old vs. New: Removing a Storage (“Private”) Capability
Here is the old way of doing that:
import HelloWorld from 0x01
transaction(greeting: String) {
prepare(signer: AuthAccount) {
signer.unlink(/private/MyGreeting)
}
}
Here is the new way of doing that:
import HelloWorld from 0x01
transaction(greeting: String, capabilityID: UInt64) {
prepare(signer: AuthAccount) {
let capController: &AuthAccount.StorageCapabilityController = signer.capabilities.storage.getController(byCapabilityID: capabilityID)
?? panic("This Cap Con does not exist.")
capController.delete()
}
}
In the new way, you must know the capabilityID
. Unfortunately this means that you as the developer will have to keep track of which ID belongs to which people to know how which one to unlink. Remember, you can also view all the capability IDs using the code in the above section.
Conclusion
There are other new features included in the new CapCon system, but I wanted this to outline the exact differences between the old Capabilities and new CapCon api. I hope this helped!
Also, if you’d like to view the official proposal for Cap Cons, go here.
Till next time ~ Jacob Tucker