⚒️

How to extend Cadence resource model

Tags
Cadence
Tech
Published
Jan 15, 2023 08:39 AM
Language
EN
Ivan Diaz@Unsplash
Ivan Diaz@Unsplash

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.
notion image

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:
notion image
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.
notion image
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.
notion image
  • 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.