技术社区分享|Cadence NFT 新标准 MetadataViews 介绍
本文由Flow技术大使Caos撰写。
2022 年 1 月 5 日, Flow NFT 仓库合并了 metadata 的新标准,本次 metadata 标准是由 Versus 与 Find 创始人Bjarte S. Karlsen 和 DapperLabs 工程师 Joshua Hannan 共同在 Flip-Fest 活动中完成,经过了将近三个月的讨论和完善,最终合并至 Flow 官方的 NFT 标准库中,本文将从代码的细节介绍 MetadataView 相关的标准细节。
写在前面
本次标准升级是非强制升级的,不会影响 NonFungibleToken 合约,也不会影响原有部署的合约的业务逻辑
已经部署在主网的合约需要升级才能应用新的 Metadata 标准
升级所覆盖的资源会影响到 Collection 继承的类型,也会影响 NFT 资源
Metadata 标准定义了一套灵活的接口实现,可以支持任意类型或自定义的 Metadata 格式
支持一个 NFT 拥有多种 Metadata 类型,并提供统一的读取方式
Metadata 标准是用一个全新的合约来定义,想要实现标准的合约需要在 NonFungibleToken 之外引入新的名为 MetadataViews 的新合约。
MetadataViews 合约详解
MetadataViews 又两个接口定义和四个推荐的 Metadata struct 组成(后续可能还会增补):
Interface
Resolver —— 由 NFT 资源继承
ResolverCollection —— 由存储 NFT 集合继承
Struct
Display
File
HTTPFile
IPFSFile
■ MetadataViews.Resolver
// A Resolver provides access to a set of metadata views.
//
// A struct or resource (e.g. an NFT) can implement this interface
// to provide access to the views that it supports.
//
pub resource interface Resolver {
pub fun getViews(): [Type] // getViews 返回 NFT 所支持的不同类型的 metadata 数据
pub fun resolveView(_ view: Type): AnyStruct? // 根据具体的 View 类型获取到 NFT 所实现的 Struct
}
Resolver 需要由 NFT 或 NFT 的数据结构继承,并实现其中定义的两个函数
getViews —— 返回数组形式的 View 类型,也是 NFT 所支持的不同类型的 metadata 数据
resolveView —— 根据具体的类型,获得 NFT 的 struct 数据
// ExampleNFT
// 1. 比原有的实现增加了 MetadataViews.Resolver 接口,并实现了其定义的两个函数
pub resource NFT: NonFungibleToken.INFT, MetadataViews.Resolver {
pub let id: UInt64
// 2. 为实现 MetadataViews.Display 所添加的属性,如果需要实现其他类型的 metadata 需要添加相应的字段
pub let name: String
pub let description: String
pub let thumbnail: String
/* init function ... */
pub fun getViews(): [Type] {
return [
Type<MetadataViews.Display>() // 3. 可以定义多个 View 类型
]
}
pub fun resolveView(_ view: Type): AnyStruct? {
// 4. 根据 view 类型,返回不同的 struct 结构,当然我们也可以自己定义返回结构
switch view {
case Type<MetadataViews.Display>():
return MetadataViews.Display(
name: self.name,
description: self.description,
thumbnail: MetadataViews.HTTPFile(
url: self.thumbnail
)
)
}
return nil
}
}
注释一位置我们能看到需要让 NFT 资源实现 MetadataViews.Resolver 接口,实现了其定义的两个函数
注释二位置实现了接口需要 NFT 能够返回指定类型的 Struct ,所以也需要定义返回结构所需要的字段,所以 NFT 增加了 MetadataViews.Display name、description 和 thumbnail
注释三位置getViews 返回一个由类型组成的数组,这里只有一个 MetadataViews.Display 类型,当然我们也可以增加多个类型,比如 File、HTTPFile、IPFSFile,当然也要对应实现其对应的字段。
注释四位置resolveView 会根据传入的 View 类型返回不同的数据结构,如果没有,则直接返回空,所以在编写查询脚本的时候需要先获通过 getViews 获得 NFT 支持的 View 类型,然后根据其支持的类型,调用 resolveView 获得 metadata 数据结构。
■ MetadataViews.ResolverCollection
/ A ResolverCollection is a group of view resolvers index by ID.
//
pub resource interface ResolverCollection {
pub fun borrowViewResolver(id: UInt64): &{Resolver} // 通过 NFT id 获得 Resolver
pub fun getIDs(): [UInt64] // 获得集合中的 NFT ids
}
ResolverCollection 需要由 Collection 资源继承,并实现其中的两个函数:
borrowViewResolver —— 通过具体的 NFT id 获得其支持的 Resolver 可以是任意类型 AnyResource
getIDs —— 获得集合中的 NFT ids
// 1. 增加 MetadataViews.ResolverCollection 继承并实现其函数
pub resource Collection: ExampleNFTCollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection {
pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT}
/* init function and props */
// getIDs returns an array of the IDs that are in the collection
pub fun getIDs(): [UInt64] {
return self.ownedNFTs.keys
}
/* other function */
// 2. 这里能够返回任意类型的 MetadataViews.Resolver 实现,拥有 getViews 和 resolveView 的方法获得 NFT 的数据
pub fun borrowViewResolver(id: UInt64): &AnyResource{MetadataViews.Resolver} {
let nft = &self.ownedNFTs[id] as auth &NonFungibleToken.NFT
let exampleNFT = nft as! &ExampleNFT.NFT
return exampleNFT as &AnyResource{MetadataViews.Resolver}
}
destroy() {
destroy self.ownedNFTs
}
}
*注释一位置: NFT Collection 资源实现 MetadataViews.ResolverCollection 接口,实现了其定义的两个函数:我们一般在查询的时候也是通过获取到用户的 Collection 资源进行详情的查询,包括 getIDs 和获得 Resolver ,后面会在查询接口那里详述。
*注释二位置:borrowViewResolver 根据 NFT id 返回任意类型的 View Resolver 中的任意一种类型,其中包含 getViews 与 resolveView 方法
■ View Structs
MetadataViews.Display —— 基础的 metadata 实现,包含 NFT 基本信息
name
description
thumbnail —— 返回任意类型的 File 结构 —— 可以是 HTTPFile 或 IPFSFile
pub struct Display {
// The name of the object.
// This field will be displayed in lists and therefore should
// be short an concise.
pub let name: String
// A written description of the object.
// This field will be displayed in a detailed view of the object,
// so can be more verbose (e.g. a paragraph instead of a single line).
pub let description: String
// A small thumbnail representation of the object.
// This field should be a web-friendly file (i.e JPEG, PNG)
// that can be displayed in lists, link previews, etc.
pub let thumbnail: AnyStruct{File}
init(
name: String,
description: String,
thumbnail: AnyStruct{File}
) {
self.name = name
self.description = description
self.thumbnail = thumbnail
}
}
MetadataViews.File —— 基础 File 的结构接口,包含 uri 函数返回
// File is a generic interface that represents a file stored on or off chain.
// Files can be used to references images, videos and other media.
pub struct interface File {
pub fun uri(): String
}
MetadataViews.HTTPFile —— 通过 Http 协议访问的图像 URL 信息
包含 url 信息
// HTTPFile is a file that is accessible at an HTTP (or HTTPS) URL.
pub struct HTTPFile: File {
pub let url: String
init(url: String) {
self.url = url
}
pub fun uri(): String {
return self.url
}
}
MetadataViews.IPFSFile —— 兼容 IPFS 协议的文件存储,包含 cid 和 path 的返回
cid
path
// IPFSThumbnail returns a thumbnail image for an object
// stored as an image file in IPFS.
// IPFS images are referenced by their content identifier (CID)
// rather than a direct URI. A client application can use this CID
// to find and load the image via an IPFS gateway.
pub struct IPFSFile: File {
// CID is the content identifier for this IPFS file.
// Ref: <https://docs.ipfs.io/concepts/content-addressing/>
pub let cid: String
// Path is an optional path to the file resource in an IPFS directory.
// This field is only needed if the file is inside a directory.
// Ref: <https://docs.ipfs.io/concepts/file-systems/>
pub let path: String?
init(cid: String, path: String?) {
self.cid = cid
self.path = path
}
// This function returns the IPFS native URL for this file.
// Ref: <https://docs.ipfs.io/how-to/address-ipfs-on-web/#native-urls>
pub fun uri(): String {
if let path = self.path {
return "ipfs://".concat(self.cid).concat("/").concat(path)
}
return "ipfs://".concat(self.cid)
}
}
查询脚本
查询脚本需要进一步的对资源进行类型转换,才能获得基础的 metadata 信息,这里我们以 ExampleNFT 的查询脚本为例
import ExampleNFT from "../../contracts/ExampleNFT.cdc"
import MetadataViews from "./MetadataViews.cdc"
// 1. 定义 NFT 数据的读取结构,为脚本函数的返回值定义类型
pub struct NFT {
pub let name: String
pub let description: String
pub let thumbnail: String
pub let owner: Address
pub let type: String
init(
name: String,
description: String,
thumbnail: String,
owner: Address,
nftType: String,
) {
self.name = name
self.description = description
self.thumbnail = thumbnail
self.owner = owner
self.type = nftType
}
}
// 2. 脚本执行函数,返回之前定义的 NFT 结构,传入地址 + NFT id 参数获取 NFT 数据
pub fun main(address: Address, id: UInt64): NFT {
let account = getAccount(address)
// 借用用户账户下的 Collection 资源
let collection = account
.getCapability(ExampleNFT.CollectionPublicPath)
.borrow<&{ExampleNFT.ExampleNFTCollectionPublic}>()
?? panic("Could not borrow a reference to the collection")
// 通过 NFT id 获取到 NFT 资源
let nft = collection.borrowExampleNFT(id: id)!
// Get the basic display information for this NFT
// 3. 因为我们的 NFT 已经实现了 MetadataViews.Resolver 接口, 所以这里调用 resolveView 能够获取到具体的 View 类型
let view = nft.resolveView(Type<MetadataViews.Display>())!
// 将返回的 `AnyStruct` 转换成具体的 struct
let display = view as! MetadataViews.Display
// 补充 NFT 所需要的额外信息
let owner: Address = nft.owner!.address!
// 获得 NFT 类型
let nftType = nft.getType()
// 返回 NFT 结构
return NFT(
name: display.name,
description: display.description,
thumbnail: display.thumbnail.uri(),
owner: owner,
nftType: nftType.identifier,
)
}
*注释一位置:定义了一个 NFT 的数据结构,作为查询脚本的返回值
*注释二位置: 通过传入地址和 NFT id 作为参数,查询 NFT 的 metadata,这里需要我们提前知道该地址下所拥有的的 NFT IDs,通过 getIDs 就能获取到当前用户拥有的 NFT ids 然后查询
*注释三位置:因为我们的 NFT 已经实现了 MetadataViews.Resolver 接口, 所以这里调用 resolveView 能够获取到具体的 View 类型,在这之前也需要我们知道 NFT 具体实现了哪些 Struct,所以也需要我们通过 getViews 来获取。示例中因假设我们知道 NFT 实现了 MetadataViews.Display,所以使用了强制转换符号 ! 将 AnyStruct 转换成 view as! MetadataViews.Display
在实际使用的过程中,代码也许会更复杂一些,这里只是针对简单的 Example NFT 的实现进行针对性的查询,在整合的过程中还是需要具体的情况做具体的处理。
总结
新版的 Metadata 标准对 NFT 的影响很大,这也是为什么笔者决定特意写一篇介绍的原因,它可以帮助合约开发者、第三方的应用还有基础服务开发者按照统一的接口和规范实现 NFT 资产的数据查询。在以往没有标准的情况下,NFT 资产的多样性会导致查询复杂度提升,基于实现了 MetadataViews 标准的 NFT 合约,能够减轻基于 NFT 资产业务开发的复杂度,降低整合的成本,提高创新业务的开发效率,最重要的是能够让打开 NFT 资产可组合性的空间,类似于 ERC20 标准对 DeFi 的促进一样,Metadata 标准能够在资产可组合性上提供更多的可能。
更多细节可以参考Jacob和Bjarte S. Karlsen现场英文视频讲解。
视频链接:https://www.youtube.com/watch?v=rgJRTozG3cw&t=404s&ab_channel=JacobTucker
END
什么是Flow福洛链?
Flow福洛链是一个快速,去中心化,且对开发者友好的区块链,旨在为新一代游戏、娱乐应用程序提供动力的数字资产的基础。Flow是唯一一个由始至终为消费者提供出色体验的Layer-1区块链团队。其团队创造的dApp包括:CryptoKitties、Dapper Wallets、NBA Top shot。
CrytoKitties于2017年推出时便快速成为加密市场最受欢迎的dApp,因其成功而导致以太坊堵塞。在Flow上运营的NBA Top shot也已成为增长最快的dApp,在公开发布后的6个月创造了7亿美金销量。正因为Flow公链的可扩展性和消费者友好的体验,让这一切成为可能。目前有1000多个项目正在Flow链上筹备中,我们期待看到一个伟大的生态系统蓬勃发展。
关于Dapper Labs
Dapper Labs是一家位于加拿大的全球顶尖区块链服务商,在2017 年年底通过CryptoKitties收藏游戏成功进入⽤户视野,并且因为加密猫的爆⽕导致以太坊拥堵,从而推出Flow公链以及全新的开发语言—— Cadence,旨在吸引更多的开发者在Flow 上开发应⽤。
Flow的合作伙伴们:
我们欢迎越来越多的小伙伴加入Flow星球,为星球增添色彩!
Flow 官网:https://zh.onflow.org/
Flow 论坛: https://forum.onflow.org/
Flow Discord:
https://discord.com/invite/flow
Flow CN Telegram: https://t.me/flow_zh
Flow B站:https://space.bilibili.com/1002168058
Flow 微博:
https://weibo.com/7610419699
Flow CSDN:
https://blog.csdn.net/weixin_57551966?spm=1010.2135.3001.5343
扫码添加Flow官方账号微信号,加入Flow生态群
微信号 : FlowChainOfficial