⚒️

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.