⚒️

如何利 Cadence 的资源模型扩展 NFT 资产

Tags
Tech
Cadence
Published
Jan 15, 2023 08:36 AM
Language
ZH
Glen Carrie@Unsplash
Glen Carrie@Unsplash

如何利 Cadence 的资源模型扩展 NFT 资产

Flowns 简介

Flowns 是基于 Flow 网络的去中心化域名协议, 2021 年 10 月 28 日上线主网, 平稳运行 400 多天, 注册量达到了 70k.
notion image

Cadence 资源模型

首先我们先了解一下 Cadence 的资源模型,在 Cadence 的设计中,对资源的简单约束规则如下:
  • 资源在同一时间只能存在于一个确切的位置
  • 资源无法被复制
  • 资源必须被明确的销毁
除此之外在 Flowns 开发过程中,我们发现了资源模型的新特性
  • 资源中可以嵌套资源 (正如 NFT 的 Collection)
  • 资源的移动会携带子资源移动 (就像快递盒子一样)
  • 资源可以存储在账户的 storage path 下或者是其他资源中
  • 资源通过特殊的类型值来排重
我们先看基于 Flow 账户的资源存储模型,以 FT 和 NFT 为例:
notion image
资产以资源的形式存在用户账户中,资产的移动代表着资源的移动,我们看 FT 和 NFT 资源在账户中存储的细节,和转账的图示
notion image
因此我们就需要账户在接收资源之前先在 Storage path 中初始化对应的资源才能接收资产,虽然资源模型有很多优点,但这种资源初始化的方式,限制了某些场景的应用,比如我们比较熟悉的 Airdrop, 或者一些合约操作内的对用户转账,账户中并没有 fallback 相关的函数,如果没有初始化资源,则会失败。

Flowns 基于资源模型做了哪些扩展

Flowns 在设计之初就是想将域名成为通用资产,在兼容 NFT 标准的同时,也实现了很多资源嵌套上的扩展,比如:
  • 子域名以内嵌 NFT 的方式存储在主域名中
  • FT 的通用收款 InBox
  • NFT 的通用收款 InBox
  • 用户自定义键值链上存储等
我们来看图示:
notion image
  • Subdomains
  • Vaults Mapping
  • Collections Mapping
这三个都是域名中的内嵌资源, 并且根据具体的使用, 实现了不同的权限控制: 域名的所有者可以创建和管理子域名, 可以提取域名中内嵌的 FT 和 NFT 资产到账户中.
下面我们看看实现的细节

子域名

在 Domains NFT 资源中定义了一个存储子域名的资源集合

// 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}
    access(self) let texts: {String: String}
}

// 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
    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
      )

      let oldSubdomain <- self.subdomains[nameHash] <- subdomain

      // ...
    }

    // Remove subdomain
    pub fun removeSubDomain(nameHash: String){
      // ..

      let oldToken <- self.subdomains.remove(key: nameHash) ?? panic("missing subdomain")

     // ..

    }

}


示例代码有所简化, 详细的实现参考创建子域名销毁子域名

内嵌 FT 和 NFT 资产 Inbox

Inbox 顾名思义就是可以接受任意类型的标准资产,如 FT 和 NFT 只要资产满足这两个标准中的一个,同时用户的账户下拥有一个 Flowns 域名 (.fn 或 .lilico) 都可以将域名资源作为一个通用收款资源来使用,就像是邮箱一样.
  // Domain resource
  pub resource NFT: DomainPublic, DomainPrivate, NonFungibleToken.INFT, MetadataViews.Resolver{
    // for user config key/value
    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}

    // ...

  }

在域名资源中定义了两个 String 为 key , 资源作为值的映射表,valuts 存储 FT collections 存储 NFT 资产。
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 yet
      if self.vaults[typeKey] == nil {
        self.vaults[typeKey] <-! from
      } else {
        // deposite vault if exist
        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 {

      let vaultRef = (&self.vaults[key] as &FungibleToken.Vault?)!
      let balance = vaultRef.balance
      var withdrawAmount = amount
      if amount == 0.0 {
        withdrawAmount = balance
      }
      return <- vaultRef.withdraw(amount: withdrawAmount)
    }
示例代码有所简化, 详细的实现代码在此
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)
    }
示例代码有所简化, 详细的实现代码在此

Transaction fallback

我们来看如何在转账资产时添加了基于 Flowns Inbox 的 fallback 的转账脚本:

// FT
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: Address

  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)
    }
  }
}

示例代码有所简化, 详细的实现在此

// NFT fallback
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)
    }
  }
}

示例代码有所简化, 详细的实现在此
在 Lilico 的钱包中已经实现了基于 Flowns Inbox 的资产转移逻辑,我们看到脚本中 <NFT> <Token> 的标志并不是 Cadence 的语法,实际上是将脚本作为模版替换成不同种类的 NFT 和 FT 资产来使用,这样就提高了用户的转账体验,同时也能实现类似于 Airdrop 的场景。
资源嵌套让 NFT 拥有了更多的扩展空间, 为游戏, 动态合成资产等应用提供了基础. Flowns 目前已经有比较大的用户基础, 如果对可组合 NFT 方向感兴趣的开发者, 欢迎基于 Flowns 的 Inbox 做更多的扩展和创新探索.