
How to extend Cadence resource model
Flowns intro
Flowns is a decentralized domain name protocol on Flow. launched on the mainnet October 28 2021, has been running for more than 400 days, now we have 70k domains on the mainnet.

Cadence Resource model
First, let's take a look at the Cadence resource model. In the Cadence design, the simple rules for resources are as follows:
- resources can only exist in one place
- the resource can not be copied
- resources must be explicitly destroyed
In addition, in the process of Flowns development, we found new features of the resource model.
- Resources can be nested in resources (such as NFT's Collection)
- the movement of resources will carry the movement of sub-resources (just like an express box)
- Resources can be stored under the storage path of the account or in other resources
- Resources have a special type values as a identifier
Tthe resource storage model based on Flow account, taking FT and NFT as examples:

Assets are stored in user accounts in the form of resources. The movement of assets represents the movement of resources. Here is the details of the FT and NFT resources stored in the account, and the diagram of the transfer.

Therefore, we need the account to initialize the corresponding resources in Storage path before receiving assets. Although the resource model has many advantages, this method of resource initialization limits the application of some scenarios, such as Airdrop, which we are familiar with, or transfer money to users in some contract operations. There is no fallback function of flow account. If there is no initialization of resources, the contract will throw panic.
What extensions have been made based on the resource model
At the beginning of its design, Flowns wanted to make the domain name as a general asset. While it is compatible with the NFT standard, it also implements many methods on resource nesting, such as:
- subdomains are stored in the main domain name resource
- Universal collection InBox for FT
- Universal collection InBox for NFT
- user-defined key-value storage on chain, etc.

- Subdomains
- Vaults Mapping
- Collections Mapping
They are nested resources of domain, have different permissions depending on how they are used: the owner of the domain name can create and manage subdomains, the FT and NFT assets nested in the domain can be withdraw to owner’s account.
Subdomain
The collection of nesting subdomain names is defined in the Domains NFT resource
// Subdomain resource
pub resource Subdomain: SubdomainPublic, SubdomainPrivate {
pub let id: UInt64
pub let name: String
pub let nameHash: String
pub let parent: String
access(self) let addresses: {UInt64: String} // address record of subdomain
access(self) let texts: {String: String} // key-value storage of subdomain
}
// Domain resource
pub resource NFT: DomainPublic, DomainPrivate, NonFungibleToken.INFT, MetadataViews.Resolver{
pub let id: UInt64
pub let name: String
pub let nameHash: String
// mapping for store Subdomain resrouces. Key == name hash: 0x5d...50f98a Value == resource : @Subdomain
access(self) var subdomains: @{String: Subdomain}
// ...
// Create subdomain
pub fun createSubDomain(name: String){
// ...
let subdomain <- create Subdomain(
id: self.subdomainCount,
name: name,
nameHash: nameHash,
parent: self.getDomainName(),
parentNameHash: self.nameHash
)
// move the created Subdomain to Domain.subdomains collection
let oldSubdomain <- self.subdomains[nameHash] <- subdomain
// ...
}
// Remove subdomain
pub fun removeSubDomain(nameHash: String){
// ..
// move the subdomain from Domain.subdomains collection
let oldToken <- self.subdomains.remove(key: nameHash) ?? panic("missing subdomain")
// destroy the subdomain resource
destroy oldToken
}
}
The sample code is simplified, check the detail Create subdomain and Remove subdomain
The Inbox for FT and NFT
Inbox can accept any type of FT and NFT standard assets, users with a Flowns domain name (.fn or .lilico) under their account can use the domain name resource as a general collection, just like a mailbox.
// Domain resource
pub resource NFT: DomainPublic, DomainPrivate, NonFungibleToken.INFT, MetadataViews.Resolver{
// for user config key/value on-chain
access(self) let texts: {String: String}
// for user config addresses in Flow Ethereum Bitcoin etc...
access(self) let addresses: {UInt64: String}
// FT vaults mapping
access(self) var vaults: @{String: FungibleToken.Vault}
// NFT collections mapping
access(self) var collections: @{String: NonFungibleToken.Collection}
// ...
}
In the domain name resource, two Mapping are defined, Key is String, and the resource is value.
valuts
stores FT resources collections
stores NFT resources.FT Inbox
// deposit FT token to Flowns domain
pub fun depositVault(from: @FungibleToken.Vault, senderRef: &{FungibleToken.Receiver}?) {
// get ft resource type identifier
let typeKey = from.getType().identifier
// add type whitelist check
assert(FNSConfig.checkFTWhitelist(typeKey) == true, message: "FT type is not in inbox whitelist")
let amount = from.balance
let address = from.owner?.address
// add vault resource if not init before
if self.vaults[typeKey] == nil {
self.vaults[typeKey] <-! from
} else {
// deposit to the corresponding vault
let vault = (&self.vaults[typeKey] as &FungibleToken.Vault?)!
vault.deposit(from: <- from)
}
}
// withdraw FT from Flowns domain
pub fun withdrawVault(key: String, amount: UFix64): @FungibleToken.Vault {
// get Vault resource with type identifier key Eg: A.0ae53cb6e3f42a79.FlowToken.Vault
let vaultRef = (&self.vaults[key] as &FungibleToken.Vault?)!
let balance = vaultRef.balance
var withdrawAmount = amount
// withdraw all
if amount == 0.0 {
withdrawAmount = balance
}
// return withdraw Vault resource
return <- vaultRef.withdraw(amount: withdrawAmount)
}
The sample code is simplified, check the detail here
NFT Inbox
// add NFT collection to Flowns domain
pub fun addCollection(collection: @NonFungibleToken.Collection) {
// get collection type identifier
let typeKey = collection.getType().identifier
assert(FNSConfig.checkNFTWhitelist(typeKey) == true, message: "NFT type is not in inbox whitelist")
// avoid the domain resource recursion
if collection.isInstance(Type<@Domains.Collection>()) {
panic("Do not nest domain resource")
}
if self.collections[typeKey] == nil {
self.collections[typeKey] <-! collection
} else {
if collection.getIDs().length > 0 {
panic("collection not empty ")
}
destroy collection
}
}
// deposit NFT to Flowns domain
pub fun depositNFT(key: String, token: @NonFungibleToken.NFT, senderRef: &{NonFungibleToken.CollectionPublic}?) {
// ...
assert(FNSConfig.checkNFTWhitelist(key) == true, message: "NFT type is not in inbox whitelist")
let collectionRef = (&self.collections[key] as &NonFungibleToken.Collection?)!
collectionRef.deposit(token: <- token)
}
// withdraw NFT from Flowns domain
pub fun withdrawNFT(key: String, itemId: UInt64): @NonFungibleToken.NFT {
// ...
let collectionRef = (&self.collections[key] as &NonFungibleToken.Collection?)!
return <- collectionRef.withdraw(withdrawID: itemId)
}
The sample code is simplified, check the detail here
Transaction fallback
Here's how to add a fallback script for Inbox when transfer assets:
// FT fallback script
transaction(amount: UFix64, recipient: Address) {
// The Vault resource that holds the tokens that are being transfered
let senderRef: &{FungibleToken.Receiver}
let sentVault: @FungibleToken.Vault
let sender: Add·ress
prepare(signer: AuthAccount) {
// Get a reference to the signer's stored vault
let vaultRef = signer.borrow<&<Token>.Vault>(from: <Token>.VaultStoragePath)
?? panic("Could not borrow reference to the owner's Vault!")
self.senderRef = signer.getCapability(<Token>.ReceiverPublicPath)
.borrow<&{FungibleToken.Receiver}>()!
self.sender = vaultRef.owner!.address
// Withdraw tokens from the signer's stored vault
self.sentVault <- vaultRef.withdraw(amount: amount)
}
execute {
// Get the recipient's public account object
let recipientAccount = getAccount(recipient)
// Get a reference to the recipient's Receiver
let receiverRef = recipientAccount.getCapability(<Token>.ReceiverPublicPath)
.borrow<&{FungibleToken.Receiver}>()
// Inbox fallback
if receiverRef == nil {
let collectionCap = recipientAccount.getCapability<&{Domains.CollectionPublic}>(Domains.CollectionPublicPath)
let collection = collectionCap.borrow()!
var defaultDomain: &{Domains.DomainPublic}? = nil
let ids = collection.getIDs()
if ids.length == 0 {
panic("Recipient have no domain ")
}
defaultDomain = collection.borrowDomain(id: ids[0])!
// check defualt domain
for id in ids {
let domain = collection.borrowDomain(id: id)!
let isDefault = domain.getText(key: "isDefault")
if isDefault == "true" {
defaultDomain = domain
}
}
// Deposit the withdrawn tokens in the recipient's domain inbox
defaultDomain!.depositVault(from: <- self.sentVault, senderRef: self.senderRef)
} else {
// Deposit the withdrawn tokens in the recipient's receiver
receiverRef!.deposit(from: <- self.sentVault)
}
}
}
The sample code is simplified, check the detail here
// NFT fallback script
transaction(recipient: Address, withdrawID: UInt64) {
prepare(signer: AuthAccount) {
// get the recipients public account object
let recipient = getAccount(recipient)
// borrow a reference to the signer's NFT collection
let collectionRef = signer
.borrow<&NonFungibleToken.Collection>(from: <NFT>.CollectionStoragePath)
?? panic("Could not borrow a reference to the owner's collection")
let senderRef = signer
.getCapability(<NFT>.CollectionPublicPath)
.borrow<&{NonFungibleToken.CollectionPublic}>()
// borrow a public reference to the receivers collection
let recipientRef = recipient
.getCapability(<NFT>.CollectionPublicPath)
.borrow<&{<NFT>.CollectionPublic}>()
// Inbox fallback
if recipientRef == nil {
let collectionCap = recipient.getCapability<&{Domains.CollectionPublic}>(Domains.CollectionPublicPath)
let collection = collectionCap.borrow()!
var defaultDomain: &{Domains.DomainPublic}? = nil
let ids = collection.getIDs()
if ids.length == 0 {
panic("Recipient have no domain ")
}
defaultDomain = collection.borrowDomain(id: ids[0])!
// check defualt Flowns domain
for id in ids {
let domain = collection.borrowDomain(id: id)!
let isDefault = domain.getText(key: "isDefault")
if isDefault == "true" {
defaultDomain = domain
}
}
let typeKey = collectionRef.getType().identifier
let nft <- collectionRef.withdraw(withdrawID: withdrawID)
if defaultDomain!.checkCollection(key: typeKey) == false {
let collection <- <NFT>.createEmptyCollection()
defaultDomain!.addCollection(collection: <- collection)
}
// deposite NFT to Flowns domain
defaultDomain!.depositNFT(key: typeKey, token: <- nft, senderRef: senderRef )
} else {
// withdraw the NFT from the owner's collection
let nft <- collectionRef.withdraw(withdrawID: withdrawID)
// Deposit the NFT in the recipient's collection
recipientRef!.deposit(token: <-nft)
}
}
}
The sample code is simplified, check the detail here
The scripts above has been implemented in Lilico's wallet. We see that the flags of
< Token >
and< NFT >
in the script are placeholder, This improves the transfer experience of users and implements scenarios similar to Airdrop.Resource nesting allows NFT to have more expansion space, which provides the basis for applications such as games and dynamically synthesized assets.
Flowns already has a large user base, so developers interested in composable NFT directions are welcome to explore more extensions and innovations in Flowns Inbox.