Cadence NFT 新标准 MetadataViews 介绍
背景
2022 年 1 月 5 日, Flow NFT 仓库合并了 metadata 的新标准,本次 metadata 标准是由 Versus 与 Find 创始人Bjarte S. Karlsen 和 DapperLabs 工程师 Joshua Hannan 共同在 Flip-Fest 活动中完成,经过了将近三个月的讨论和完善,最终合并至 Flow 官方的 NFT 标准库中,关于 metadata 的草案详情可以看这里
写在前面
- 本次标准升级是非强制升级的,不会影响
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.Displayname、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 标准能够在资产可组合性上提供更多的可能。
注:原有合约的升级会有一些技术难度,甚至需要对合约和查询脚本进行重写,也需要对用户已有的资源进行迁移初始化,虽有一定的成本,但会提升整体 NFT 资产在未来的采用率,如果已有的项目不进行升级也不会对现有的合约造成任何影响。
2022-01-22