⚒️

如何利 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 做更多的扩展和创新探索.