
如何利 Cadence 的资源模型扩展 NFT 资产
Flowns 简介
Flowns 是基于 Flow 网络的去中心化域名协议, 2021 年 10 月 28 日上线主网, 平稳运行 400 多天, 注册量达到了 70k.

Cadence 资源模型
首先我们先了解一下 Cadence 的资源模型,在 Cadence 的设计中,对资源的简单约束规则如下:
- 资源在同一时间只能存在于一个确切的位置
- 资源无法被复制
- 资源必须被明确的销毁
除此之外在 Flowns 开发过程中,我们发现了资源模型的新特性
- 资源中可以嵌套资源 (正如 NFT 的 Collection)
- 资源的移动会携带子资源移动 (就像快递盒子一样)
- 资源可以存储在账户的 storage path 下或者是其他资源中
- 资源通过特殊的类型值来排重
我们先看基于 Flow 账户的资源存储模型,以 FT 和 NFT 为例:

资产以资源的形式存在用户账户中,资产的移动代表着资源的移动,我们看 FT 和 NFT 资源在账户中存储的细节,和转账的图示

因此我们就需要账户在接收资源之前先在 Storage path 中初始化对应的资源才能接收资产,虽然资源模型有很多优点,但这种资源初始化的方式,限制了某些场景的应用,比如我们比较熟悉的 Airdrop, 或者一些合约操作内的对用户转账,账户中并没有 fallback 相关的函数,如果没有初始化资源,则会失败。
Flowns 基于资源模型做了哪些扩展
Flowns 在设计之初就是想将域名成为通用资产,在兼容 NFT 标准的同时,也实现了很多资源嵌套上的扩展,比如:
- 子域名以内嵌 NFT 的方式存储在主域名中
- FT 的通用收款 InBox
- NFT 的通用收款 InBox
- 用户自定义键值链上存储等
我们来看图示:

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