From e215d091d868ea7dca3f64208f444551174a9dfc Mon Sep 17 00:00:00 2001 From: Chance Santana-Wees Date: Thu, 16 Sep 2021 14:51:00 -0500 Subject: [PATCH 1/6] Outline of intended NFT Metadata functionality and structure --- Milestone 1/NFTStandard.cdc | 326 ++++++++++++++++++++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 Milestone 1/NFTStandard.cdc diff --git a/Milestone 1/NFTStandard.cdc b/Milestone 1/NFTStandard.cdc new file mode 100644 index 00000000..b2a48f5e --- /dev/null +++ b/Milestone 1/NFTStandard.cdc @@ -0,0 +1,326 @@ +/* + This project is addressing problem "New Standard: NFT metadata #16" + The below code is untested and is meant as an illustration of the concept. + + + MetaDataUtil is added as a new default contract. + + Metadata is instantiated in the form of structs at the time the NFT is minted. + Metadata is organized by string tags, and each metadata element may have multiple tags allowing NFTs to conform to multiple tag schemas. + Metadata elements cannot be added after the NFT is minted, but Mutable elements can allow data to be modifiable. + + MetaDataElements are wrappers around two possible types of structs, one of which is immutable and the other of which is Mutable. + Immutable elements can use default types or be defined in custom structs that implement ITaggedMetaData and IImmutableMetaData. + Defining a custom class allows the data elements to be immutable while allowing a developer to upgrade the contract that defines the struct to alter the tags. + Mutable elements store Capability pointers to IMetaDataProvider instances that can be stored anywhere. + Mutable elements can be used for metadata that can be modified, such as for leveling up a character. + Mutable elements can also be used for metadata that is shared between multiple NFTs, to minimize redundant data storage. + A Side-effect (benefit?) here is that if the instance is stored in a contract in the developers account, the developer will need to cover the metaData storage costs instead of the NFT holder. + PNG_RemoteDefaultImage is a default implementation that can be used to give an NFT a shared and/or updatable image. + RemotePNGProvider is an example of an implementation of IMetaDataProvider that allows multiple NFTs to share the same image via Capability reference. +*/ + +pub contract MetaDataUtil { + pub struct interface ITaggedMetaData { + pub fun getTags() : [String] + } + + pub struct interface IMetaDataProvider { + pub fun getData() : AnyStruct + pub fun getDataType() : String + } + + pub struct interface IImmutableMetaData { + pub let data: AnyStruct + pub let type: String + } + + pub struct interface IMutableMetaData { + pub let dataProvider: Capability<&AnyStruct{IMetaDataProvider}> + } + + pub struct MetaData { + pub let elements : [MetaDataElement] + + init(metaData : [MetaDataElement]?) { + self.elements = metaData ?? [] + } + + pub fun getMetaDataByTag(tag : String) : MetaDataElement? { + for element in self.elements { + if (element.hasTag(tag : tag)) { + return element + } + } + return nil + } + + pub fun conformsToSchema(schemaTags : [String]) : Bool { + var tags : [String] = [] + for element in self.elements { + tags.appendAll(element.getTags()) + } + for tag in schemaTags { + if(!tags.contains(tag)) { + return false + } + } + return true + } + } + + pub struct MetaDataElement { + pub let metaData : AnyStruct{ITaggedMetaData} + init (metadata : AnyStruct{ITaggedMetaData}) { + if (!metadata.isInstance(Type()) && !metadata.isInstance(Type())) { + panic("Invalid Metadata Type") + } + self.metaData = metadata + } + + pub fun getData() : AnyStruct { + let m1 = self.metaData as? AnyStruct{IMutableMetaData} + if(m1 != nil) { + return m1!.dataProvider.borrow()!.getData() + } + let m2 = self.metaData as? AnyStruct{IImmutableMetaData} + if(m2 != nil) { + return m2!.data + } + + panic("Invalid MetaData") + } + + pub fun getDataType() : String { + let m1 = self.metaData as? AnyStruct{IMutableMetaData} + if(m1 != nil) { + return m1!.dataProvider.borrow()!.getDataType() + } + let m2 = self.metaData as? AnyStruct{IImmutableMetaData} + if(m2 != nil) { + return m2!.type + } + + panic("Invalid MetaData") + } + + pub fun getTags() : [String] { + return self.metaData.getTags() + } + + pub fun hasTag(tag : String) : Bool { + return self.metaData.getTags().contains(tag) + } + + pub fun isMutable() : Bool { + return self.metaData.isInstance(Type()) + } + } + + pub struct ImmutablyTaggedString : IImmutableMetaData, ITaggedMetaData { + pub let data: AnyStruct + pub let type: String + pub let tags: [String] + + init(data : String, tags: [String]) { + self.data = data + self.type = "string" + self.tags = tags + } + + pub fun getTags() : [String] { + return self.tags + } + } + + pub struct DefaultName : IImmutableMetaData, ITaggedMetaData { + pub let data: AnyStruct + pub let type: String + + init(name : String) { + self.data = name + self.type = "string" + } + + pub fun getTags() : [String] { + return ["name", "title"] + } + } + + pub struct DefaultDescription : IImmutableMetaData, ITaggedMetaData { + pub let data: AnyStruct + pub let type: String + + init(description : String) { + self.data = description + self.type = "string" + } + + pub fun getTags() : [String] { + return ["description"] + } + } + + pub struct PNG_DefaultImage : IImmutableMetaData, ITaggedMetaData { + pub let data: AnyStruct + pub let type: String + + init(imgData : [UInt8]) { + self.data = imgData + self.type = "png" + } + + pub fun getTags() : [String] { + return ["image", "portrait"] + } + } + + pub struct PNG_RemoteDefaultImage : IMutableMetaData, ITaggedMetaData { + pub let dataProvider: Capability<&AnyStruct{IMetaDataProvider}> + + init(provider : Capability<&AnyStruct{IMetaDataProvider}>) { + self.dataProvider = provider + } + + pub fun getTags() : [String] { + return ["image", "portrait"] + } + } + + pub struct RemotePNGProvider : IMetaDataProvider { + access(self) var data: [UInt8] + access(self) var isStatic : Bool + + init (imgData : [UInt8], static : Bool) { + self.data = imgData + self.isStatic = static + } + + access(account) fun setImage(imgData : [UInt8]) { + if(self.isStatic) { + panic("Cannot Set Static Image") + } + self.data = imgData + } + + pub fun getData() : AnyStruct { + return self.data + } + + pub fun getDataType() : String { + return "png" + } + } +} + +/** + +## The Flow Non-Fungible Token standard +The only modification made is to add "pub let metadata: MetaDataUtil.MetaData?" +If the NonFungibleToken contract is updated, older NFTs will still be valid but cannot take advantage of the new standard without a wrapper. + +*/ + +// The main NFT contract interface. Other NFT contracts will +// import and implement this interface +// +pub contract interface NonFungibleToken { + + // The total number of tokens of this type in existence + pub var totalSupply: UInt64 + + // Event that emitted when the NFT contract is initialized + // + pub event ContractInitialized() + + // Event that is emitted when a token is withdrawn, + // indicating the owner of the collection that it was withdrawn from. + // + // If the collection is not in an account's storage, `from` will be `nil`. + // + pub event Withdraw(id: UInt64, from: Address?) + + // Event that emitted when a token is deposited to a collection. + // + // It indicates the owner of the collection that it was deposited to. + // + pub event Deposit(id: UInt64, to: Address?) + + // Interface that the NFTs have to conform to + // + pub resource interface INFT { + // The unique ID that each NFT has + pub let id: UInt64 + pub let metadata: MetaDataUtil.MetaData? + } + + // Requirement that all conforming NFT smart contracts have + // to define a resource called NFT that conforms to INFT + pub resource NFT: INFT { + pub let id: UInt64 + pub let metadata: MetaDataUtil.MetaData? + } + + // Interface to mediate withdraws from the Collection + // + pub resource interface Provider { + // withdraw removes an NFT from the collection and moves it to the caller + pub fun withdraw(withdrawID: UInt64): @NFT { + post { + result.id == withdrawID: "The ID of the withdrawn token must be the same as the requested ID" + } + } + } + + // Interface to mediate deposits to the Collection + // + pub resource interface Receiver { + + // deposit takes an NFT as an argument and adds it to the Collection + // + pub fun deposit(token: @NFT) + } + + // Interface that an account would commonly + // publish for their collection + pub resource interface CollectionPublic { + pub fun deposit(token: @NFT) + pub fun getIDs(): [UInt64] + pub fun borrowNFT(id: UInt64): &NFT + } + + // Requirement for the the concrete resource type + // to be declared in the implementing contract + // + pub resource Collection: Provider, Receiver, CollectionPublic { + + // Dictionary to hold the NFTs in the Collection + pub var ownedNFTs: @{UInt64: NFT} + + // withdraw removes an NFT from the collection and moves it to the caller + pub fun withdraw(withdrawID: UInt64): @NFT + + // deposit takes a NFT and adds it to the collections dictionary + // and adds the ID to the id array + pub fun deposit(token: @NFT) + + // getIDs returns an array of the IDs that are in the collection + pub fun getIDs(): [UInt64] + + // Returns a borrowed reference to an NFT in the collection + // so that the caller can read data and call methods from it + pub fun borrowNFT(id: UInt64): &NFT { + pre { + self.ownedNFTs[id] != nil: "NFT does not exist in the collection!" + } + } + } + + // createEmptyCollection creates an empty Collection + // and returns it to the caller so that they can own NFTs + pub fun createEmptyCollection(): @Collection { + post { + result.getIDs().length == 0: "The created collection must be empty!" + } + } +} \ No newline at end of file From 5f6cd3c04757e25b7709df69357c435319798416 Mon Sep 17 00:00:00 2001 From: srinjoyc Date: Thu, 16 Sep 2021 13:52:00 -0700 Subject: [PATCH 2/6] Rename Milestone 1/NFTStandard.cdc to submissions/issue-16/milestone-1/coelacant/NFTStandard.cdc --- .../issue-16/milestone-1/coelacant}/NFTStandard.cdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename {Milestone 1 => submissions/issue-16/milestone-1/coelacant}/NFTStandard.cdc (99%) diff --git a/Milestone 1/NFTStandard.cdc b/submissions/issue-16/milestone-1/coelacant/NFTStandard.cdc similarity index 99% rename from Milestone 1/NFTStandard.cdc rename to submissions/issue-16/milestone-1/coelacant/NFTStandard.cdc index b2a48f5e..278f59b1 100644 --- a/Milestone 1/NFTStandard.cdc +++ b/submissions/issue-16/milestone-1/coelacant/NFTStandard.cdc @@ -323,4 +323,4 @@ pub contract interface NonFungibleToken { result.getIDs().length == 0: "The created collection must be empty!" } } -} \ No newline at end of file +} From cf38127722f75ee867c331df84a1fe9e6ab514fb Mon Sep 17 00:00:00 2001 From: Chance Santana-Wees Date: Fri, 17 Sep 2021 13:30:26 -0500 Subject: [PATCH 3/6] merge --- .../{coelacant => Coelacanth}/NFTStandard.cdc | 0 .../20210917-nft-metadata-standard.md | 674 ++++++++++++++++++ .../milestone-2/Coelacanth/ExampleNFT.cdc | 144 ++++ .../milestone-2/Coelacanth/NFTStandard.cdc | 488 +++++++++++++ 4 files changed, 1306 insertions(+) rename submissions/issue-16/milestone-1/{coelacant => Coelacanth}/NFTStandard.cdc (100%) create mode 100644 submissions/issue-16/milestone-2/Coelacanth/20210917-nft-metadata-standard.md create mode 100644 submissions/issue-16/milestone-2/Coelacanth/ExampleNFT.cdc create mode 100644 submissions/issue-16/milestone-2/Coelacanth/NFTStandard.cdc diff --git a/submissions/issue-16/milestone-1/coelacant/NFTStandard.cdc b/submissions/issue-16/milestone-1/Coelacanth/NFTStandard.cdc similarity index 100% rename from submissions/issue-16/milestone-1/coelacant/NFTStandard.cdc rename to submissions/issue-16/milestone-1/Coelacanth/NFTStandard.cdc diff --git a/submissions/issue-16/milestone-2/Coelacanth/20210917-nft-metadata-standard.md b/submissions/issue-16/milestone-2/Coelacanth/20210917-nft-metadata-standard.md new file mode 100644 index 00000000..0976483e --- /dev/null +++ b/submissions/issue-16/milestone-2/Coelacanth/20210917-nft-metadata-standard.md @@ -0,0 +1,674 @@ +# Title of FLIP + +| Status | (Proposed / Rejected / Accepted / Implemented) | +:-------------- |:---------------------------------------------------- | +| **FLIP #** | [NNN](https://github.com/onflow/flow/pull/NNN) | +| **Author(s)** | Chance Santana-Wees (figs999@gmail.com) | +| **Updated** | 2021-09-16 | + +## Objective + +Flip-fest issue #16 "New Standard: NFT metadata" + +NFTs are more than just numbers and bytes — at their best, they are rich +representations of digital goods that people around the world can fall in +love with. The current Flow NFT interface, however, does not include a metadata +standard that allows these representations to flourish. NFTs should be able to +include structured data, images, videos and other types of data. What +modifications should be made to the NFT interface to support a variety of +metadata types? + +## Motivation + +In the NFT Metadata discussion thread, the primary concerns that contributors have brought up are the competing needs for a flexible but parsable schema, immutable and mutable metadata, and storage cost minimization. + +In order to address these concerns the following design choices were made: +* NFT Metadata Schema has been reduced to an update-able tag system, which allows NFTs to conform to multiple schemas and add conformance to new schemas as standards evolve over time. +* Immutable and Mutable metadata are both supported in this implementation. True immutability is ensured in Immutable implementations by defining access code in default contracts that cannot be modified by developers. +* Mutable metadata will utilize Capabilities to maintain pointers to external data, which allows multiple resources to point to the same large binary data (upload once for all that share that metadata) +* Usage of Capabilities in Mutable metadata elements allows the metadata data to be stored in the holder's account or in the developer's account, allowing devs the flexibility of subsidizing storage costs when using mutable metadata. +* Mutable type image metadata can be made "semi-immutable" by using the PNG_RemoteDefaultImage default implementation, allowing some ensurance of immutability while leveraging the capabilities of the Mutable metadata interface. + +## User Benefit + +A Metadata standard would allow off-chain systems to easily understand +how to parse and display NFT metadata. + +This would allow for exchanges and showcase clients to render the NFTs +pertinent data in an expected and consistent manner, regardless of the +metadata needs of the NFTs core project. + +## Design Proposal + +This Flip is for a new standard which adds client and contract parsable meta-data +to the default NFT interface. This is accomplished via a new default contract +"MetaDataUtils". + +```cadence +pub contract MetaDataUtil { + //Utilizing MIME types as existing standard, this will allow metadata to be more easily parsed by a browser + //Any non-conformant type should use a MIME type with the following syntax "application.cadence+T" where T is the Type + pub struct DataType { + //This should be an IANA MIME type + pub let MIME : String + //Is the data a link to a file on an external service (ipfs for instance) + pub let isLink : Bool + //if the data is compressed, this IAMA MIME type of the data inside the compressed bytes + pub let compressedMIME : String? + + init(MIME : String, isLink : Bool, compressedMIME : String?) { + self.MIME = MIME + self.isLink = isLink + self.compressedMIME = compressedMIME + } + } + + //Interface for Partial MetaDataElement implementation. + pub struct interface ITaggedMetaData { + pub fun getTags() : [String] + pub let id: UInt64 + } + + pub struct interface IMetaDataProvider { + pub fun getData(id: UInt64) : AnyStruct + pub fun getDataType(id: UInt64) : DataType + pub fun getTags(id: UInt64) : [String] + } + + pub struct interface IImmutableMetaData { + pub let data: AnyStruct + pub let type: DataType + } + + pub struct interface IMutableMetaData { + pub let dataProvider: Capability<&{IMetaDataProvider}> + } + + pub struct MetaData { + pub let elements : [MetaDataElement] + + init(metaData : [MetaDataElement]?) { + self.elements = metaData ?? [] + } + + pub fun getMetaDataByTag(tag : String) : [MetaDataElement] { + var taggedElements : [MetaDataElement] = [] + for element in self.elements { + if (element.hasTag(tag : tag)) { + taggedElements.append(element) + } + } + return taggedElements + } + + pub fun conformsToSchema(schemaTags : [String]) : Bool { + var tags : [String] = [] + for element in self.elements { + tags.appendAll(element.getTags()) + } + for tag in schemaTags { + if(!tags.contains(tag)) { + return false + } + } + return true + } + } + + //Struct wrapper around metadata implemenations. + //Because this struct is defined in default contract, users can rest assured that the code that accesses their metadata cannot be altered. + pub struct MetaDataElement { + pub let metaData : {ITaggedMetaData} + + init (metadata : {ITaggedMetaData}) { + //This forces metadata to conform to expected implementations of "ITaggedMetaData,IMutableMetaData" or "ITaggedMetaData,IImmutableMetaData" + if (!metadata.isInstance(Type<{IMutableMetaData}>()) && !metadata.isInstance(Type<{IImmutableMetaData}>())) { + panic("Invalid Metadata Type") + } + self.metaData = metadata + } + + pub fun getData() : AnyStruct { + let m1 = self.metaData as? {IMutableMetaData} + if(m1 != nil) { + return m1!.dataProvider.borrow()!.getData(id: self.metaData.id) + } + //Immutable data directly references storage via default contract code, ensuring it cannot be altered by contract upgrade + let m2 = self.metaData as? AnyStruct{IImmutableMetaData} + if(m2 != nil) { + return m2!.data + } + + //this cannot be reached + panic("Invalid MetaData") + } + + pub fun getDataType() : DataType { + let m1 = self.metaData as? {IMutableMetaData} + if(m1 != nil) { + return m1!.dataProvider.borrow()!.getDataType(id: self.metaData.id) + } + //Immutable data directly references storage via default contract code, ensuring it cannot be altered by contract upgrade + let m2 = self.metaData as? AnyStruct{IImmutableMetaData} + if(m2 != nil) { + return m2!.type + } + + //this cannot be reached + panic("Invalid MetaData") + } + + pub fun getTags() : [String] { + //ITaggedMetaData relies on function to return tags for both immutable and mutable. This allows contract upgrades to effect tags. + return self.metaData.getTags() + } + + pub fun hasTag(tag : String) : Bool { + return self.metaData.getTags().contains(tag) + } + + //This method can be checked to determine if the associated metadata is able to be altered by developers + pub fun isMutable() : Bool { + return self.metaData.isInstance(Type()) + } + } +} +``` + +In this standard: +* Every NFT possesses a MetaData property which acts as a wrapper around MetaDataElements +* MetaDataElements are wrapped custom structs that are created at the time the NFT is minted. +* MetaDataElements cannot be added after the NFT is minted, but Mutable elements can allow data to be modifiable. +* MetaDataElements are organized by String tags, and each metadata element may have multiple tags. This allows NFTs to conform to multiple tag schemas. +* Multiple MetaDataElements can share a tag. This is useful for broad catergories of metadata such as "equipment", but can also be used to provide multiple media types for the same tag to conform to varying schema requirements. + +The only modification to the existing NFT interface is the addition of the metadata property: +``` +// Interface that the NFTs have to conform to +// +pub resource interface INFT { + // The unique ID that each NFT has + pub let id: UInt64 + pub let metadata: MetaDataUtil.MetaData? +} + +// Requirement that all conforming NFT smart contracts have +// to define a resource called NFT that conforms to INFT +pub resource NFT: INFT { + pub let id: UInt64 + pub let metadata: MetaDataUtil.MetaData? +} +``` + +Further explanation of implementation: + +MetaDataElements are wrappers around two possible types of structs, one of which is immutable and the other of which is Mutable. +MetaDataElements possess 3 accessible functions that access stored properties +* The getData function can return an object of any AnyStruct type, and contains the actual metadata itself +* The getDataType function returns a DataType object that describes the MIME type of the object for reference by off-chain systems + * MIME type is used as it is a widely used standard that can already be understood by browsers + * DataTypes also contain isLink, which is true if the data element describes a link to externaly hosted data. + * compressedMIME is used to describe the internal data type if the data blob should be decompressed by the client + +Immutable elements can use default types or be defined in custom structs that implement ITaggedMetaData and IImmutableMetaData. +* Defining a custom class allows the data elements to be immutable while allowing a developer to upgrade the contract that defines the struct to alter the tags. + +Mutable elements store Capability pointers to IMetaDataProvider instances that can be stored anywhere. +* Mutable elements can be used for metadata that can be modified, such as for leveling up a character. +* Mutable elements can also be used for metadata that is shared between multiple NFTs, to minimize redundant data storage. + * A Side-effect (benefit?) here is that if the instance is stored in a contract in the developers account, the developer will need to cover the metaData storage costs instead of the NFT holder. + +Additionally, this standard proposes two more default contracts that include some common DataTypes Metadata implementations. + +MIME includes the most common datatypes for NFT metadata and can be used as reference for how developers can create their own. +The vast majority of generic client-facing metadata will conform to the TextPlain, HttpLink, and ImagePNG types. +``` +pub contract MIME { + pub let TextPlain : MetaDataUtil.DataType + pub let HttpLink : MetaDataUtil.DataType + pub let ImagePNG : MetaDataUtil.DataType + pub let Numeric : MetaDataUtil.DataType + pub let AnyStruct : MetaDataUtil.DataType + + init() { + self.TextPlain = MetaDataUtil.DataType("text/plain",false,nil) + self.HttpLink = MetaDataUtil.DataType("text/plain",true,nil) + self.ImagePNG = MetaDataUtil.DataType("image/png",false,nil) + //Not an official MIME type, but can be used for numeric data that will be returned in cadence object format + self.Numeric = MetaDataUtil.DataType("application/cadence+Number",false,nil) + //Not an official MIME type, but can be used for arbitrary data that will be returned in cadence object format + self.AnyStruct = MetaDataUtil.DataType("application/cadence+AnyStruct",false,nil) + } +} +``` + +CommonMetaDataElements contains example implementations that can be used for many common metadata elements. +The tags that this default contract returns for these common implementations should be modified by further Flips in the event that common schemas have changed as time passes. +The three most common metadata elements are Name, Description, and Image. These can all be added to an NFT by using common implementations. + +DefaultName and DefaultDescription are self explanatory. They allow a string to be added with the common tags of ["name", "title"] and ["description"]. + +Images can be added as PNGs to an NFT by using either the PNG_DefaultImage or PNG_RemoteDefaultImage implementations. + +PNG_DefaultImage stores the image locally as a unique copy of the bytes. This is good for unique NFTs that require immutability. +PNG_RemoteDefaultImage is a default implementation that can be used to give an NFT a shared and/or update-able image. +-RemotePNGProvider is an example of an implementation of IMetaDataProvider that allows multiple NFTs to share the same image via Capability reference. +``` +pub contract CommonMetaDataElements { + //This implementation allows the tags to also be immutable if required for some reason + pub struct ImmutablyTaggedData : MetaDataUtil.IImmutableMetaData, MetaDataUtil.ITaggedMetaData { + pub let data: AnyStruct + pub let type: MetaDataUtil.DataType + pub let tags: [String] + pub let id: UInt64 + + init(data : AnyStruct, type: MetaDataUtil.DataType, tags: [String]) { + self.data = data + self.type = type + self.tags = tags + self.id = 0 + } + + pub fun getTags() : [String] { + return self.tags + } + } + + //Default element implementation for a named NFT, most NFTs will need this + pub struct DefaultName : MetaDataUtil.IImmutableMetaData, MetaDataUtil.ITaggedMetaData { + pub let data: AnyStruct + pub let type: MetaDataUtil.DataType + pub let id: UInt64 + + init(name : String) { + self.data = name + self.type = MIME.TextPlain + self.id = 0 + } + + pub fun getTags() : [String] { + return ["name", "title"] + } + } + + //Default element implementation for an NFT with a text description, most NFTs will need this + pub struct DefaultDescription : MetaDataUtil.IImmutableMetaData, MetaDataUtil.ITaggedMetaData { + pub let data: AnyStruct + pub let type: MetaDataUtil.DataType + pub let id: UInt64 + + init(description : String) { + self.data = description + self.type = MIME.TextPlain + self.id = 0 + } + + pub fun getTags() : [String] { + return ["description"] + } + } + + //Default element implementation for an NFT with unique and immutable binary data that stores a png format image + pub struct PNG_DefaultImage : MetaDataUtil.IImmutableMetaData, MetaDataUtil.ITaggedMetaData { + pub let data: AnyStruct + pub let type: MetaDataUtil.DataType + pub let id: UInt64 + + init(imgData : [UInt8]) { + self.data = imgData + self.type = MIME.ImagePNG + self.id = 0 + } + + pub fun getTags() : [String] { + return ["image", "portrait"] + } + } + + pub struct UpdatableStats : MetaDataUtil.IMutableMetaData, MetaDataUtil.ITaggedMetaData { + pub let dataProvider: Capability<&AnyStruct{MetaDataUtil.IMetaDataProvider}> + pub let id: UInt64 + + init(id: UInt64, provider : Capability<&UpdatableStatsProvider{MetaDataUtil.IMetaDataProvider}>,) { + self.dataProvider = provider + self.id = id + } + + pub fun getTags() : [String] { + return self.dataProvider.borrow()!.getTags(id: self.id) + } + } + + //An instance of this struct is needed for any NFT that utilizes UpdatableStatistics. + //A single instance can be shared by multiple resources. + //As a statistic entry, type of data is assumed to be a number, a string, or a complex struct (Array/Dictionary/Custom) + //Data can be updated arbitrarily by the account that holds this struct in storage, so this should only be stored in developer controlled account + pub struct UpdatableStatsProvider : MetaDataUtil.IMetaDataProvider { + access(self) var data: {UInt64: AnyStruct} + access(self) var tags: {UInt64: [String]} + access(self) var type: {UInt64: MetaDataUtil.DataType} + + init () { + self.data = {} + self.tags = {} + self.type = {} + } + + pub fun addData (id: UInt64, data : AnyStruct, tags : [String]) { + if(self.data[id] != nil) { + panic("data already exists for id") + } + + self.data[id] = data + self.tags[id] = tags + self.type[id] = MIME.AnyStruct + + if(data as? Number != nil) { + self.type[id] = MIME.Numeric + } + else if(data as? String != nil) { + self.type[id] = MIME.TextPlain + } + } + + pub fun setData(id: UInt64, data : AnyStruct) { + if(data.getType() != self.data.getType()) { + if(data as? Number != nil) { + self.type[id] = MIME.Numeric + } + else if(data as? String != nil) { + self.type[id] = MIME.TextPlain + } + else { + self.type[id] = MIME.AnyStruct + } + } + + self.data[id] = data + } + + //Tags are not static and can be updated + pub fun setTags(id: UInt64, tags : [String]) { + self.tags[id] = tags + } + + pub fun getData(id: UInt64) : AnyStruct { + return self.data[id]! + } + + pub fun getDataType(id: UInt64) : MetaDataUtil.DataType { + return self.type[id]! + } + + pub fun getTags(id: UInt64) : [String] { + return self.tags[id]! + } + } + + //Default element implementation for an NFT with mutable or shared binary data that stores a png format image + pub struct PNG_RemoteDefaultImage : MetaDataUtil.IMutableMetaData, MetaDataUtil.ITaggedMetaData { + pub let dataProvider: Capability<&AnyStruct{MetaDataUtil.IMetaDataProvider}> + pub let id: UInt64 + + init(id: UInt64, provider : Capability<&CommonMetaDataElements.RemotePNGProvider{MetaDataUtil.IMetaDataProvider}>) { + self.dataProvider = provider + self.id = id + } + + pub fun getTags() : [String] { + return self.dataProvider.borrow()!.getTags(id: self.id) + } + } + + //An instance of this struct is needed for any NFT that utilizes PNG_RemoteDefaultImage. + //A single instance can be shared by multiple resources. + //This provider allows the data to be either mutable or semi-immutable + //Data can be updated arbitrarily by the account that holds this struct in storage, so this should only be stored in developer controlled account + //If needing to store the image in the storage of the resource holder, developers will need to make a custom provider. + pub struct RemotePNGProvider : MetaDataUtil.IMetaDataProvider { + access(self) var data: {UInt64: [UInt8]} + access(self) var isStatic: {UInt64: Bool} + + init() { + self.data = {} + self.isStatic = {} + } + + pub fun addImage (id: UInt64, imgData : [UInt8], static : Bool) { + if(self.data[id] != nil) { + panic("") + } + + self.data[id] = imgData + self.isStatic[id] = static + } + + pub fun setImage(id: UInt64, imgData : [UInt8]) { + if(self.isStatic[id]!) { + panic("Cannot Set Static Image") + } + self.data[id] = imgData + } + + pub fun getData(id: UInt64) : AnyStruct { + return self.data + } + + pub fun getDataType(id: UInt64) : MetaDataUtil.DataType { + return MIME.ImagePNG + } + + pub fun getTags(id: UInt64) : [String] { + return ["image", "portrait"] + } + } +} +``` + +### Drawbacks + +Implementing a Metadata standard of any kind will obsolete existing NFT projects, +which will force them to go through a re-minting process of some kind in order to +take advantage of the new functionality. + +### Performance Implications + +Metadata will of course impact storage size of NFTs. + +### Dependencies + +Cadence + +### Engineering Impact + +Minimal. Only adding new default contracts. + +### Tutorials and Examples + +The code shown in the Design Proposal includes a number of standard implementations of +meta-data types. + +Additional Example implementation code: + +``` +// This is an example implementation of a Flow Non-Fungible Token +// It is not part of the official standard but it assumed to be +// very similar to how many NFTs would implement the core functionality. + +import NonFungibleToken, MetaDataUtil, MIME, CommonMetaDataElements from 0x01 + +pub contract ExampleNFT: NonFungibleToken { + + pub var totalSupply: UInt64 + access(contract) let sharedImages : {String: UInt64} + access(contract) let remotePNGProvider : CommonMetaDataElements.RemotePNGProvider + + pub event ContractInitialized() + pub event Withdraw(id: UInt64, from: Address?) + pub event Deposit(id: UInt64, to: Address?) + + pub resource NFT: NonFungibleToken.INFT { + pub let id: UInt64 + pub let metadata : MetaDataUtil.MetaData? + + init(initID: UInt64, metadataElements : [MetaDataUtil.MetaDataElement]) { + self.id = initID + self.metadata = MetaDataUtil.MetaData(metadataElements) + } + } + + pub resource Collection: NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic { + // dictionary of NFT conforming tokens + // NFT is a resource type with an `UInt64` ID field + pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT} + + init () { + self.ownedNFTs <- {} + } + + // withdraw removes an NFT from the collection and moves it to the caller + pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT { + let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT") + + emit Withdraw(id: token.id, from: self.owner?.address) + + return <-token + } + + // deposit takes a NFT and adds it to the collections dictionary + // and adds the ID to the id array + pub fun deposit(token: @NonFungibleToken.NFT) { + let token <- token as! @ExampleNFT.NFT + + let id: UInt64 = token.id + + // add the new token to the dictionary which removes the old one + let oldToken <- self.ownedNFTs[id] <- token + + emit Deposit(id: id, to: self.owner?.address) + + destroy oldToken + } + + // getIDs returns an array of the IDs that are in the collection + pub fun getIDs(): [UInt64] { + return self.ownedNFTs.keys + } + + // borrowNFT gets a reference to an NFT in the collection + // so that the caller can read its metadata and call its methods + pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT { + return &self.ownedNFTs[id] as &NonFungibleToken.NFT + } + + destroy() { + destroy self.ownedNFTs + } + } + + // public function that anyone can call to create a new empty collection + pub fun createEmptyCollection(): @NonFungibleToken.Collection { + return <- create Collection() + } + + // Resource that an admin or something similar would own to be + // able to mint new NFTs + // + pub resource NFTMinter { + // mintNFT mints a new NFT with a new ID + // and deposit it in the recipients collection using their collection reference + pub fun mintNFT(recipient: &{NonFungibleToken.CollectionPublic}, name : String, description : String, imgBytes : [UInt8]) { + let capability = ExampleNFT.account.getCapability<&CommonMetaDataElements.RemotePNGProvider{MetaDataUtil.IMetaDataProvider}>(/public/ExampleNFTRemotePNGProvider) + let hash = String.encodeHex(imgBytes) //String.encodeHex(HashAlgorithm.SHA3_256.hash(imgBytes)) + + var providerID : UInt64? = ExampleNFT.sharedImages[hash] + if(providerID == nil) { + providerID = ExampleNFT.totalSupply + ExampleNFT.AddImage(id: providerID!, imgData: imgBytes, static: true) + } + + // create a new NFT + let elements : [MetaDataUtil.MetaDataElement] = [ + MetaDataUtil.MetaDataElement(metadata: CommonMetaDataElements.DefaultName(name: name)), + MetaDataUtil.MetaDataElement(metadata: CommonMetaDataElements.DefaultDescription(description: description)), + MetaDataUtil.MetaDataElement(metadata: CommonMetaDataElements.PNG_RemoteDefaultImage(id: providerID!, capability: capability)) + ] + + var newNFT <- create NFT(initID: ExampleNFT.totalSupply, metadataElements: elements) + + // deposit it in the recipient's account using their reference + recipient.deposit(token: <-newNFT) + + ExampleNFT.totalSupply = ExampleNFT.totalSupply + (1 as UInt64) + } + } + + access(contract) fun AddImage(id: UInt64, imgData: [UInt8], static: Bool) { + self.remotePNGProvider.addImage(id: id, imgData: imgData, static: static) + self.account.save(self.remotePNGProvider, to: /storage/ExampleNFTRemotePNGProvider) + } + + init() { + // Initialize the total supply + self.totalSupply = 0 + + // Initialize the shared image cache + self.sharedImages = {} + self.remotePNGProvider = CommonMetaDataElements.RemotePNGProvider() + self.account.save(self.remotePNGProvider, to: /storage/ExampleNFTRemotePNGProvider) + self.account.link<&CommonMetaDataElements.RemotePNGProvider{MetaDataUtil.IMetaDataProvider}>(/public/ExampleNFTRemotePNGProvider, target: /storage/ExampleNFTRemotePNGProvider) + + // Create a Collection resource and save it to storage + let collection <- create Collection() + self.account.save(<-collection, to: /storage/NFTCollection) + + // create a public capability for the collection + self.account.link<&{NonFungibleToken.CollectionPublic}>( + /public/NFTCollection, + target: /storage/NFTCollection + ) + + // Create a Minter resource and save it to storage + let minter <- create NFTMinter() + self.account.save(<-minter, to: /storage/NFTMinter) + + emit ContractInitialized() + } +} +``` + +### Compatibility + +Current NFT projects that implement the NonFungibleToken contract interface will +be made obsolete and will require a migration process to use NonFungibleToken2 if +they want to take advantage of standardized metadata. + +There is a risk that implementations of NonFungibleToken could be left out of +various future projects (such as exchanges) that rely on all assets implementing the +NonFungibleToken2 contract interface, if they do not migrate to new interface. + +### User Impact + +None + +## Related Issues + +The ability to mark functions as "locked" in cadence, so that if they are modified +a contract update will fail, would simplify the required implementation by allowing +both mutable and immutable metadata elements to inherit from the same interface. + +If contract update restrictions can be modified to allow for the addition of nil-able +properties, then this standard can be implemented as a contract upgrade of the existing +default NonFungibleToken contract, allowing backwards compatibility. + +## Prior Art + +The Flow community has been discussing an NFT metadata standard over the past several months, with representation from many of the NFT applications currently live on mainnet. + +## Questions and Discussion Topics + +Please help me elaborate on the Design Proposal by commenting on anything that is +in the code that is confusing, omitted, unexplained, etc. + +Seed this with open questions you require feedback on from the FLIP process. +What parts of the design still need to be defined? \ No newline at end of file diff --git a/submissions/issue-16/milestone-2/Coelacanth/ExampleNFT.cdc b/submissions/issue-16/milestone-2/Coelacanth/ExampleNFT.cdc new file mode 100644 index 00000000..75391a8f --- /dev/null +++ b/submissions/issue-16/milestone-2/Coelacanth/ExampleNFT.cdc @@ -0,0 +1,144 @@ +// This is an example implementation of a Flow Non-Fungible Token +// It is not part of the official standard but it assumed to be +// very similar to how many NFTs would implement the core functionality. + +import NonFungibleToken, MetaDataUtil, MIME, CommonMetaDataElements from 0x01 + +pub contract ExampleNFT: NonFungibleToken { + + pub var totalSupply: UInt64 + access(contract) let sharedImages : {String: UInt64} + access(contract) let remotePNGProvider : CommonMetaDataElements.RemotePNGProvider + + pub event ContractInitialized() + pub event Withdraw(id: UInt64, from: Address?) + pub event Deposit(id: UInt64, to: Address?) + + pub resource NFT: NonFungibleToken.INFT { + pub let id: UInt64 + pub let metadata : MetaDataUtil.MetaData? + + init(initID: UInt64, metadataElements : [MetaDataUtil.MetaDataElement]) { + self.id = initID + self.metadata = MetaDataUtil.MetaData(metadataElements) + } + } + + pub resource Collection: NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic { + // dictionary of NFT conforming tokens + // NFT is a resource type with an `UInt64` ID field + pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT} + + init () { + self.ownedNFTs <- {} + } + + // withdraw removes an NFT from the collection and moves it to the caller + pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT { + let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT") + + emit Withdraw(id: token.id, from: self.owner?.address) + + return <-token + } + + // deposit takes a NFT and adds it to the collections dictionary + // and adds the ID to the id array + pub fun deposit(token: @NonFungibleToken.NFT) { + let token <- token as! @ExampleNFT.NFT + + let id: UInt64 = token.id + + // add the new token to the dictionary which removes the old one + let oldToken <- self.ownedNFTs[id] <- token + + emit Deposit(id: id, to: self.owner?.address) + + destroy oldToken + } + + // getIDs returns an array of the IDs that are in the collection + pub fun getIDs(): [UInt64] { + return self.ownedNFTs.keys + } + + // borrowNFT gets a reference to an NFT in the collection + // so that the caller can read its metadata and call its methods + pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT { + return &self.ownedNFTs[id] as &NonFungibleToken.NFT + } + + destroy() { + destroy self.ownedNFTs + } + } + + // public function that anyone can call to create a new empty collection + pub fun createEmptyCollection(): @NonFungibleToken.Collection { + return <- create Collection() + } + + // Resource that an admin or something similar would own to be + // able to mint new NFTs + // + pub resource NFTMinter { + // mintNFT mints a new NFT with a new ID + // and deposit it in the recipients collection using their collection reference + pub fun mintNFT(recipient: &{NonFungibleToken.CollectionPublic}, name : String, description : String, imgBytes : [UInt8]) { + let capability = ExampleNFT.account.getCapability<&CommonMetaDataElements.RemotePNGProvider{MetaDataUtil.IMetaDataProvider}>(/public/ExampleNFTRemotePNGProvider) + let hash = String.encodeHex(imgBytes) //String.encodeHex(HashAlgorithm.SHA3_256.hash(imgBytes)) + + var providerID : UInt64? = ExampleNFT.sharedImages[hash] + if(providerID == nil) { + providerID = ExampleNFT.totalSupply + ExampleNFT.AddImage(id: providerID!, imgData: imgBytes, static: true) + } + + // create a new NFT + let elements : [MetaDataUtil.MetaDataElement] = [ + MetaDataUtil.MetaDataElement(metadata: CommonMetaDataElements.DefaultName(name: name)), + MetaDataUtil.MetaDataElement(metadata: CommonMetaDataElements.DefaultDescription(description: description)), + MetaDataUtil.MetaDataElement(metadata: CommonMetaDataElements.PNG_RemoteDefaultImage(id: providerID!, capability: capability)) + ] + + var newNFT <- create NFT(initID: ExampleNFT.totalSupply, metadataElements: elements) + + // deposit it in the recipient's account using their reference + recipient.deposit(token: <-newNFT) + + ExampleNFT.totalSupply = ExampleNFT.totalSupply + (1 as UInt64) + } + } + + access(contract) fun AddImage(id: UInt64, imgData: [UInt8], static: Bool) { + self.remotePNGProvider.addImage(id: id, imgData: imgData, static: static) + self.account.save(self.remotePNGProvider, to: /storage/ExampleNFTRemotePNGProvider) + } + + init() { + // Initialize the total supply + self.totalSupply = 0 + + // Initialize the shared image cache + self.sharedImages = {} + self.remotePNGProvider = CommonMetaDataElements.RemotePNGProvider() + self.account.save(self.remotePNGProvider, to: /storage/ExampleNFTRemotePNGProvider) + self.account.link<&CommonMetaDataElements.RemotePNGProvider{MetaDataUtil.IMetaDataProvider}>(/public/ExampleNFTRemotePNGProvider, target: /storage/ExampleNFTRemotePNGProvider) + + // Create a Collection resource and save it to storage + let collection <- create Collection() + self.account.save(<-collection, to: /storage/NFTCollection) + + // create a public capability for the collection + self.account.link<&{NonFungibleToken.CollectionPublic}>( + /public/NFTCollection, + target: /storage/NFTCollection + ) + + // Create a Minter resource and save it to storage + let minter <- create NFTMinter() + self.account.save(<-minter, to: /storage/NFTMinter) + + emit ContractInitialized() + } +} \ No newline at end of file diff --git a/submissions/issue-16/milestone-2/Coelacanth/NFTStandard.cdc b/submissions/issue-16/milestone-2/Coelacanth/NFTStandard.cdc new file mode 100644 index 00000000..6546f645 --- /dev/null +++ b/submissions/issue-16/milestone-2/Coelacanth/NFTStandard.cdc @@ -0,0 +1,488 @@ +/* + This project is addressing problem "New Standard: NFT metadata #16" + The below code is untested and is meant as an illustration of the concept. + + MetaDataUtil, MIME, and CommonMetaDataElements are added as a new default contracts. + + Metadata is instantiated in the form of structs at the time the NFT is minted. + Metadata is organized by string tags, and each metadata element may have multiple tags allowing NFTs to conform to multiple tag schemas. + Metadata elements cannot be added after the NFT is minted, but Mutable elements can allow data to be modifiable. + + MetaDataElements are wrappers around two possible types of structs, one of which is immutable and the other of which is Mutable. + MetaDataElements possess 3 accessible functions that access stored properties + The getData function can return an object of any AnyStruct type, and contains the actual metadata itself + The getDataType function returns a DataType object that describes the MIME type of the object for reference by off-chain systems + MIME type is used as it is a widely used standard that can already be understood by browsers + DataTypes also contain isLink, which is true if the data element describes a link to externaly hosted data. + compressedMIME is used to describe the internal data type if the data blob should be decompressed by the client + Immutable elements can use default types or be defined in custom structs that implement ITaggedMetaData and IImmutableMetaData. + Defining a custom class allows the data elements to be immutable while allowing a developer to upgrade the contract that defines the struct to alter the tags. + Mutable elements store Capability pointers to IMetaDataProvider instances that can be stored anywhere. + Mutable elements can be used for metadata that can be modified, such as for leveling up a character. + Mutable elements can also be used for metadata that is shared between multiple NFTs, to minimize redundant data storage. + A Side-effect (benefit?) here is that if the instance is stored in a contract in the developers account, the developer will need to cover the metaData storage costs instead of the NFT holder. + PNG_RemoteDefaultImage is a default implementation that can be used to give an NFT a shared and/or updatable image. + RemotePNGProvider is an example of an implementation of IMetaDataProvider that allows multiple NFTs to share the same image via Capability reference. + + MIME contains a number of commonly used DataTypes for convenience + CommonMetaDataElements contains default struct implementations that can be wrapped by MetaDataElements + More default struct implementations should be added for any other common use cases +*/ + +pub contract MetaDataUtil { + //Utilizing MIME types as existing standard, this will allow metadata to be more easily parsed by a browser + //Any non-conformant type should use a MIME type with the following syntax "application.cadence+T" where T is the Type + pub struct DataType { + //This should be an IANA MIME type + pub let MIME : String + //Is the data a link to a file on an external service (ipfs for instance) + pub let isLink : Bool + //if the data is compressed, this IAMA MIME type of the data inside the compressed bytes + pub let compressedMIME : String? + + init(MIME : String, isLink : Bool, compressedMIME : String?) { + self.MIME = MIME + self.isLink = isLink + self.compressedMIME = compressedMIME + } + } + + //Interface for Partial MetaDataElement implementation. + pub struct interface ITaggedMetaData { + pub fun getTags() : [String] + pub let id: UInt64 + } + + pub struct interface IMetaDataProvider { + pub fun getData(id: UInt64) : AnyStruct + pub fun getDataType(id: UInt64) : DataType + pub fun getTags(id: UInt64) : [String] + } + + pub struct interface IImmutableMetaData { + pub let data: AnyStruct + pub let type: DataType + } + + pub struct interface IMutableMetaData { + pub let dataProvider: Capability<&{IMetaDataProvider}> + } + + pub struct MetaData { + pub let elements : [MetaDataElement] + + init(metaData : [MetaDataElement]?) { + self.elements = metaData ?? [] + } + + pub fun getMetaDataByTag(tag : String) : [MetaDataElement] { + var taggedElements : [MetaDataElement] = [] + for element in self.elements { + if (element.hasTag(tag : tag)) { + taggedElements.append(element) + } + } + return taggedElements + } + + pub fun conformsToSchema(schemaTags : [String]) : Bool { + var tags : [String] = [] + for element in self.elements { + tags.appendAll(element.getTags()) + } + for tag in schemaTags { + if(!tags.contains(tag)) { + return false + } + } + return true + } + } + + //Struct wrapper around metadata implemenations. + //Because this struct is defined in default contract, users can rest assured that the code that accesses their metadata cannot be altered. + pub struct MetaDataElement { + pub let metaData : {ITaggedMetaData} + + init (metadata : {ITaggedMetaData}) { + //This forces metadata to conform to expected implementations of "ITaggedMetaData,IMutableMetaData" or "ITaggedMetaData,IImmutableMetaData" + if (!metadata.isInstance(Type<{IMutableMetaData}>()) && !metadata.isInstance(Type<{IImmutableMetaData}>())) { + panic("Invalid Metadata Type") + } + self.metaData = metadata + } + + pub fun getData() : AnyStruct { + let m1 = self.metaData as? {IMutableMetaData} + if(m1 != nil) { + return m1!.dataProvider.borrow()!.getData(id: self.metaData.id) + } + //Immutable data directly references storage via default contract code, ensuring it cannot be altered by contract upgrade + let m2 = self.metaData as? AnyStruct{IImmutableMetaData} + if(m2 != nil) { + return m2!.data + } + + //this cannot be reached + panic("Invalid MetaData") + } + + pub fun getDataType() : DataType { + let m1 = self.metaData as? {IMutableMetaData} + if(m1 != nil) { + return m1!.dataProvider.borrow()!.getDataType(id: self.metaData.id) + } + //Immutable data directly references storage via default contract code, ensuring it cannot be altered by contract upgrade + let m2 = self.metaData as? AnyStruct{IImmutableMetaData} + if(m2 != nil) { + return m2!.type + } + + //this cannot be reached + panic("Invalid MetaData") + } + + pub fun getTags() : [String] { + //ITaggedMetaData relies on function to return tags for both immutable and mutable. This allows contract upgrades to effect tags. + return self.metaData.getTags() + } + + pub fun hasTag(tag : String) : Bool { + return self.metaData.getTags().contains(tag) + } + + //This method can be checked to determine if the associated metadata is able to be altered by developers + pub fun isMutable() : Bool { + return self.metaData.isInstance(Type()) + } + } +} + +pub contract MIME { + pub let TextPlain : MetaDataUtil.DataType + pub let HttpLink : MetaDataUtil.DataType + pub let ImagePNG : MetaDataUtil.DataType + pub let Numeric : MetaDataUtil.DataType + pub let AnyStruct : MetaDataUtil.DataType + + init() { + self.TextPlain = MetaDataUtil.DataType("text/plain",false,nil) + self.HttpLink = MetaDataUtil.DataType("text/plain",true,nil) + self.ImagePNG = MetaDataUtil.DataType("image/png",false,nil) + //Not an official MIME type, but can be used for numeric data that will be returned in cadence object format + self.Numeric = MetaDataUtil.DataType("application/cadence+Number",false,nil) + //Not an official MIME type, but can be used for arbitrary data that will be returned in cadence object format + self.AnyStruct = MetaDataUtil.DataType("application/cadence+AnyStruct",false,nil) + } +} + +pub contract CommonMetaDataElements { + //This implementation allows the tags to also be immutable if required for some reason + pub struct ImmutablyTaggedData : MetaDataUtil.IImmutableMetaData, MetaDataUtil.ITaggedMetaData { + pub let data: AnyStruct + pub let type: MetaDataUtil.DataType + pub let tags: [String] + pub let id: UInt64 + + init(data : AnyStruct, type: MetaDataUtil.DataType, tags: [String]) { + self.data = data + self.type = type + self.tags = tags + self.id = 0 + } + + pub fun getTags() : [String] { + return self.tags + } + } + + //Default element implementation for a named NFT, most NFTs will need this + pub struct DefaultName : MetaDataUtil.IImmutableMetaData, MetaDataUtil.ITaggedMetaData { + pub let data: AnyStruct + pub let type: MetaDataUtil.DataType + pub let id: UInt64 + + init(name : String) { + self.data = name + self.type = MIME.TextPlain + self.id = 0 + } + + pub fun getTags() : [String] { + return ["name", "title"] + } + } + + //Default element implementation for an NFT with a text description, most NFTs will need this + pub struct DefaultDescription : MetaDataUtil.IImmutableMetaData, MetaDataUtil.ITaggedMetaData { + pub let data: AnyStruct + pub let type: MetaDataUtil.DataType + pub let id: UInt64 + + init(description : String) { + self.data = description + self.type = MIME.TextPlain + self.id = 0 + } + + pub fun getTags() : [String] { + return ["description"] + } + } + + //Default element implementation for an NFT with unique and immutable binary data that stores a png format image + pub struct PNG_DefaultImage : MetaDataUtil.IImmutableMetaData, MetaDataUtil.ITaggedMetaData { + pub let data: AnyStruct + pub let type: MetaDataUtil.DataType + pub let id: UInt64 + + init(imgData : [UInt8]) { + self.data = imgData + self.type = MIME.ImagePNG + self.id = 0 + } + + pub fun getTags() : [String] { + return ["image", "portrait"] + } + } + + pub struct UpdatableStats : MetaDataUtil.IMutableMetaData, MetaDataUtil.ITaggedMetaData { + pub let dataProvider: Capability<&AnyStruct{MetaDataUtil.IMetaDataProvider}> + pub let id: UInt64 + + init(id: UInt64, provider : Capability<&UpdatableStatsProvider{MetaDataUtil.IMetaDataProvider}>,) { + self.dataProvider = provider + self.id = id + } + + pub fun getTags() : [String] { + return self.dataProvider.borrow()!.getTags(id: self.id) + } + } + + //An instance of this struct is needed for any NFT that utilizes UpdatableStatistics. + //A single instance can be shared by multiple resources. + //As a statistic entry, type of data is assumed to be a number, a string, or a complex struct (Array/Dictionary/Custom) + //Data can be updated arbitrarily by the account that holds this struct in storage, so this should only be stored in developer controlled account + pub struct UpdatableStatsProvider : MetaDataUtil.IMetaDataProvider { + access(self) var data: {UInt64: AnyStruct} + access(self) var tags: {UInt64: [String]} + access(self) var type: {UInt64: MetaDataUtil.DataType} + + init () { + self.data = {} + self.tags = {} + self.type = {} + } + + pub fun addData (id: UInt64, data : AnyStruct, tags : [String]) { + if(self.data[id] != nil) { + panic("data already exists for id") + } + + self.data[id] = data + self.tags[id] = tags + self.type[id] = MIME.AnyStruct + + if(data as? Number != nil) { + self.type[id] = MIME.Numeric + } + else if(data as? String != nil) { + self.type[id] = MIME.TextPlain + } + } + + pub fun setData(id: UInt64, data : AnyStruct) { + if(data.getType() != self.data.getType()) { + if(data as? Number != nil) { + self.type[id] = MIME.Numeric + } + else if(data as? String != nil) { + self.type[id] = MIME.TextPlain + } + else { + self.type[id] = MIME.AnyStruct + } + } + + self.data[id] = data + } + + //Tags are not static and can be updated + pub fun setTags(id: UInt64, tags : [String]) { + self.tags[id] = tags + } + + pub fun getData(id: UInt64) : AnyStruct { + return self.data[id]! + } + + pub fun getDataType(id: UInt64) : MetaDataUtil.DataType { + return self.type[id]! + } + + pub fun getTags(id: UInt64) : [String] { + return self.tags[id]! + } + } + + //Default element implementation for an NFT with mutable or shared binary data that stores a png format image + pub struct PNG_RemoteDefaultImage : MetaDataUtil.IMutableMetaData, MetaDataUtil.ITaggedMetaData { + pub let dataProvider: Capability<&AnyStruct{MetaDataUtil.IMetaDataProvider}> + pub let id: UInt64 + + init(id: UInt64, provider : Capability<&CommonMetaDataElements.RemotePNGProvider{MetaDataUtil.IMetaDataProvider}>) { + self.dataProvider = provider + self.id = id + } + + pub fun getTags() : [String] { + return self.dataProvider.borrow()!.getTags(id: self.id) + } + } + + //An instance of this struct is needed for any NFT that utilizes PNG_RemoteDefaultImage. + //A single instance can be shared by multiple resources. + //This provider allows the data to be either mutable or semi-immutable + //Data can be updated arbitrarily by the account that holds this struct in storage, so this should only be stored in developer controlled account + //If needing to store the image in the storage of the resource holder, developers will need to make a custom provider. + pub struct RemotePNGProvider : MetaDataUtil.IMetaDataProvider { + access(self) var data: {UInt64: [UInt8]} + access(self) var isStatic: {UInt64: Bool} + + init() { + self.data = {} + self.isStatic = {} + } + + pub fun addImage (id: UInt64, imgData : [UInt8], static : Bool) { + if(self.data[id] != nil) { + panic("") + } + + self.data[id] = imgData + self.isStatic[id] = static + } + + pub fun setImage(id: UInt64, imgData : [UInt8]) { + if(self.isStatic[id]!) { + panic("Cannot Set Static Image") + } + self.data[id] = imgData + } + + pub fun getData(id: UInt64) : AnyStruct { + return self.data + } + + pub fun getDataType(id: UInt64) : MetaDataUtil.DataType { + return MIME.ImagePNG + } + + pub fun getTags(id: UInt64) : [String] { + return ["image", "portrait"] + } + } +} + +pub contract interface NonFungibleToken2 { + + // The total number of tokens of this type in existence + pub var totalSupply: UInt64 + + // Event that emitted when the NFT contract is initialized + // + pub event ContractInitialized() + + // Event that is emitted when a token is withdrawn, + // indicating the owner of the collection that it was withdrawn from. + // + // If the collection is not in an account's storage, `from` will be `nil`. + // + pub event Withdraw(id: UInt64, from: Address?) + + // Event that emitted when a token is deposited to a collection. + // + // It indicates the owner of the collection that it was deposited to. + // + pub event Deposit(id: UInt64, to: Address?) + + // Interface that the NFTs have to conform to + // + pub resource interface INFT { + // The unique ID that each NFT has + pub let id: UInt64 + pub let metadata: MetaDataUtil.MetaData? + } + + // Requirement that all conforming NFT smart contracts have + // to define a resource called NFT that conforms to INFT + pub resource NFT: INFT { + pub let id: UInt64 + pub let metadata: MetaDataUtil.MetaData? + } + + // Interface to mediate withdraws from the Collection + // + pub resource interface Provider { + // withdraw removes an NFT from the collection and moves it to the caller + pub fun withdraw(withdrawID: UInt64): @NFT { + post { + result.id == withdrawID: "The ID of the withdrawn token must be the same as the requested ID" + } + } + } + + // Interface to mediate deposits to the Collection + // + pub resource interface Receiver { + + // deposit takes an NFT as an argument and adds it to the Collection + // + pub fun deposit(token: @NFT) + } + + // Interface that an account would commonly + // publish for their collection + pub resource interface CollectionPublic { + pub fun deposit(token: @NFT) + pub fun getIDs(): [UInt64] + pub fun borrowNFT(id: UInt64): &NFT + } + + // Requirement for the the concrete resource type + // to be declared in the implementing contract + // + pub resource Collection: Provider, Receiver, CollectionPublic { + + // Dictionary to hold the NFTs in the Collection + pub var ownedNFTs: @{UInt64: NFT} + + // withdraw removes an NFT from the collection and moves it to the caller + pub fun withdraw(withdrawID: UInt64): @NFT + + // deposit takes a NFT and adds it to the collections dictionary + // and adds the ID to the id array + pub fun deposit(token: @NFT) + + // getIDs returns an array of the IDs that are in the collection + pub fun getIDs(): [UInt64] + + // Returns a borrowed reference to an NFT in the collection + // so that the caller can read data and call methods from it + pub fun borrowNFT(id: UInt64): &NFT { + pre { + self.ownedNFTs[id] != nil: "NFT does not exist in the collection!" + } + } + } + + // createEmptyCollection creates an empty Collection + // and returns it to the caller so that they can own NFTs + pub fun createEmptyCollection(): @Collection { + post { + result.getIDs().length == 0: "The created collection must be empty!" + } + } +} \ No newline at end of file From bb8748d2ca66dd652970109e05c3f72a5351f1ae Mon Sep 17 00:00:00 2001 From: Chance Santana-Wees Date: Fri, 17 Sep 2021 17:27:35 -0500 Subject: [PATCH 4/6] Fleshing out the Schema system further --- .../milestone-2/Coelacanth/NFTStandard.cdc | 148 ++++++++++++++++-- 1 file changed, 137 insertions(+), 11 deletions(-) diff --git a/submissions/issue-16/milestone-2/Coelacanth/NFTStandard.cdc b/submissions/issue-16/milestone-2/Coelacanth/NFTStandard.cdc index 6546f645..ae7f8a73 100644 --- a/submissions/issue-16/milestone-2/Coelacanth/NFTStandard.cdc +++ b/submissions/issue-16/milestone-2/Coelacanth/NFTStandard.cdc @@ -27,6 +27,12 @@ MIME contains a number of commonly used DataTypes for convenience CommonMetaDataElements contains default struct implementations that can be wrapped by MetaDataElements More default struct implementations should be added for any other common use cases + + Schema, SchemaElement, and SchemaRetrievalMode are data types primarily for off-chain accessing of MetaData, although it may be useful for certain on-chain functionality + MetaDataHolder.retrieveSchemaData can be used to retrieve solid copies of the NFTs metadata for use off-chain via script + SchemaRetrievalMode allows the caller to require varying degrees of schema compliance + If the Schema complies, MetaDataHolder.retrieveSchemaData returns a Schema object which contains the MetaData in its elements[?].schemaData properties + If compliance fails, the method returns nil */ pub contract MetaDataUtil { @@ -69,33 +75,124 @@ pub contract MetaDataUtil { } pub struct MetaData { - pub let elements : [MetaDataElement] + pub let data : AnyStruct + pub let mime : String + pub let tags : [String] + pub let mutable : Bool + pub let link : Bool + + init(data : AnyStruct, mime : String, tags : [String], mutable : Bool, link : Bool) { + self.data = data + self.mime = mime + self.tags = tags + self.mutable = mutable + self.link = link + } + } + + pub struct SchemaElement { + pub let requiredTags : [String] + pub let validMIMETypes : [String] + pub let schemaData : [MetaData] + init(requiredTags : [String], validMIMETypes : [String]) { + self.requiredTags = requiredTags + self.validMIMETypes = validMIMETypes + self.schemaData = [] + } + } + + pub struct Schema { + pub let elements : [SchemaElement] + init(elements : [SchemaElement]) { + self.elements = elements + } + } + + pub enum SchemaRetrievalMode : UInt8 { + //Schema always is returned, even if empty + pub case ALLOW_NONE + //Schema is returned if any MetaData is found + pub case REQUIRE_ANY + //Schema is returned only if all SchemaElements are found + pub case REQUIRE_ALL + //Schema is returned if any MetaData is found, but only if each SchemaElement has 1 or less matches + pub case SINGLE_DATA_ANY + //Schema is only returned if exactly one MetaData is found per SchemaElement + pub case SINGLE_DATA_ALL + } + + pub struct MetaDataHolder { + access(self) let elements : [MetaDataElement] init(metaData : [MetaDataElement]?) { self.elements = metaData ?? [] } - pub fun getMetaDataByTag(tag : String) : [MetaDataElement] { - var taggedElements : [MetaDataElement] = [] + pub fun getMetaDatasByTag(tag : String) : [MetaData] { + var taggedElements : [MetaData] = [] for element in self.elements { if (element.hasTag(tag : tag)) { - taggedElements.append(element) + taggedElements.append(element.toMetaData()) } } return taggedElements } - pub fun conformsToSchema(schemaTags : [String]) : Bool { - var tags : [String] = [] + pub fun getMetaDatasBySchemaElement(schemaElement: SchemaElement) : [MetaData] { + let matchingElements : [MetaData] = [] for element in self.elements { - tags.appendAll(element.getTags()) + if(element.hasAllTags(requiredTags: schemaElement.requiredTags) && element.conformsToTypeRequirements(validMIMETypes: schemaElement.validMIMETypes)) { + let dataType = element.getDataType() + matchingElements.append(MetaData(data: element.getData(), mime: dataType.MIME, tags: element.getTags(), mutable: element.isMutable(), link: dataType.isLink)) + } } - for tag in schemaTags { - if(!tags.contains(tag)) { - return false + + return matchingElements + } + + //When successfull, this method returns the schema object with the MetaData filled into the SchemeElements + //If the NFTs metadata does not conform to the schema with the chosen SchemaRetrievalMode, it returns nil + //Checking the return of this method to see if it is nil can be used to check if the NFT conforms to the required Schema + pub fun retrieveSchemaData(schema : Schema, retrievalMode : SchemaRetrievalMode) : Schema? { + var foundAll = true + var foundAny = false + var allSingle = true + for schemaElement in schema.elements { + let found = self.getMetaDatasBySchemaElement(schemaElement: schemaElement) + schemaElement.schemaData.appendAll(found) + + if(found.length > 0) { + foundAny = true + if(found.length > 1) { + allSingle = false + } + } else { + foundAll = false } } - return true + + switch retrievalMode { + case SchemaRetrievalMode.SINGLE_DATA_ALL: + if(!foundAll && !allSingle) { + return nil + } + case SchemaRetrievalMode.SINGLE_DATA_ANY: + if(!foundAny && !allSingle) { + return nil + } + case SchemaRetrievalMode.REQUIRE_ALL: + if(!foundAll) { + return nil + } + case SchemaRetrievalMode.REQUIRE_ANY: + if(!foundAll) { + return nil + } + default: + break + } + + return schema } } @@ -151,6 +248,35 @@ pub contract MetaDataUtil { return self.metaData.getTags().contains(tag) } + pub fun hasAllTags(requiredTags : [String]) : Bool { + let tags : [String] = self.metaData.getTags() + for tag in requiredTags { + if(!tags.contains(tag)) { + return false + } + } + + return true + } + + pub fun conformsToTypeRequirements(validMIMETypes : [String]) : Bool { + if(validMIMETypes.length == 0) { + return true + } + let mime = self.getDataType().MIME + for type in validMIMETypes { + if(type == mime) { + return true + } + } + return false + } + + pub fun toMetaData() : MetaData { + let dataType = self.getDataType() + return MetaData(data: self.getData(), mime: dataType.MIME, tags: self.getTags(), mutable: self.isMutable(), link: dataType.isLink) + } + //This method can be checked to determine if the associated metadata is able to be altered by developers pub fun isMutable() : Bool { return self.metaData.isInstance(Type()) From 6d3c55c17b7d00ee413b64d305b9c5e34af2cfe4 Mon Sep 17 00:00:00 2001 From: Chance Santana-Wees Date: Fri, 17 Sep 2021 17:46:30 -0500 Subject: [PATCH 5/6] Fixed issues with data type usage compressedMime replaced with innerMime so it can be used by linked data --- .../milestone-2/Coelacanth/NFTStandard.cdc | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/submissions/issue-16/milestone-2/Coelacanth/NFTStandard.cdc b/submissions/issue-16/milestone-2/Coelacanth/NFTStandard.cdc index ae7f8a73..d0b58303 100644 --- a/submissions/issue-16/milestone-2/Coelacanth/NFTStandard.cdc +++ b/submissions/issue-16/milestone-2/Coelacanth/NFTStandard.cdc @@ -14,7 +14,7 @@ The getDataType function returns a DataType object that describes the MIME type of the object for reference by off-chain systems MIME type is used as it is a widely used standard that can already be understood by browsers DataTypes also contain isLink, which is true if the data element describes a link to externaly hosted data. - compressedMIME is used to describe the internal data type if the data blob should be decompressed by the client + innerMIME is used to describe the final data type if the data blob should be decompressed or downloaded by the client Immutable elements can use default types or be defined in custom structs that implement ITaggedMetaData and IImmutableMetaData. Defining a custom class allows the data elements to be immutable while allowing a developer to upgrade the contract that defines the struct to alter the tags. Mutable elements store Capability pointers to IMetaDataProvider instances that can be stored anywhere. @@ -43,13 +43,13 @@ pub contract MetaDataUtil { pub let MIME : String //Is the data a link to a file on an external service (ipfs for instance) pub let isLink : Bool - //if the data is compressed, this IAMA MIME type of the data inside the compressed bytes - pub let compressedMIME : String? + //if the data is compressed or a link, this IAMA MIME type of the actual final data (after download/decompression) + pub let innerMIME : String? - init(MIME : String, isLink : Bool, compressedMIME : String?) { + init(MIME : String, isLink : Bool, innerMIME : String?) { self.MIME = MIME self.isLink = isLink - self.compressedMIME = compressedMIME + self.innerMIME = innerMIME } } @@ -74,19 +74,18 @@ pub contract MetaDataUtil { pub let dataProvider: Capability<&{IMetaDataProvider}> } + //Solid version of MetaDataHolder that can be passed around or returned in script call for use off-chain pub struct MetaData { pub let data : AnyStruct - pub let mime : String pub let tags : [String] + pub let type : DataType pub let mutable : Bool - pub let link : Bool - init(data : AnyStruct, mime : String, tags : [String], mutable : Bool, link : Bool) { + init(data : AnyStruct, type : DataType, tags : [String], mutable : Bool) { self.data = data - self.mime = mime + self.type = type self.tags = tags self.mutable = mutable - self.link = link } } @@ -143,7 +142,7 @@ pub contract MetaDataUtil { for element in self.elements { if(element.hasAllTags(requiredTags: schemaElement.requiredTags) && element.conformsToTypeRequirements(validMIMETypes: schemaElement.validMIMETypes)) { let dataType = element.getDataType() - matchingElements.append(MetaData(data: element.getData(), mime: dataType.MIME, tags: element.getTags(), mutable: element.isMutable(), link: dataType.isLink)) + matchingElements.append(MetaData(data: element.getData(), type: dataType, tags: element.getTags(), mutable: element.isMutable())) } } @@ -263,7 +262,8 @@ pub contract MetaDataUtil { if(validMIMETypes.length == 0) { return true } - let mime = self.getDataType().MIME + let dataType = self.getDataType() + let mime = dataType.innerMIME ?? dataType.MIME for type in validMIMETypes { if(type == mime) { return true @@ -274,7 +274,7 @@ pub contract MetaDataUtil { pub fun toMetaData() : MetaData { let dataType = self.getDataType() - return MetaData(data: self.getData(), mime: dataType.MIME, tags: self.getTags(), mutable: self.isMutable(), link: dataType.isLink) + return MetaData(data: self.getData(), type: dataType, tags: self.getTags(), mutable: self.isMutable()) } //This method can be checked to determine if the associated metadata is able to be altered by developers @@ -286,14 +286,14 @@ pub contract MetaDataUtil { pub contract MIME { pub let TextPlain : MetaDataUtil.DataType - pub let HttpLink : MetaDataUtil.DataType + pub let LinkedPNG : MetaDataUtil.DataType pub let ImagePNG : MetaDataUtil.DataType pub let Numeric : MetaDataUtil.DataType pub let AnyStruct : MetaDataUtil.DataType init() { self.TextPlain = MetaDataUtil.DataType("text/plain",false,nil) - self.HttpLink = MetaDataUtil.DataType("text/plain",true,nil) + self.LinkedPNG = MetaDataUtil.DataType("text/plain",true,"image/png") self.ImagePNG = MetaDataUtil.DataType("image/png",false,nil) //Not an official MIME type, but can be used for numeric data that will be returned in cadence object format self.Numeric = MetaDataUtil.DataType("application/cadence+Number",false,nil) From 8cd14050321e39f3f153c6502b8d2629a384ab87 Mon Sep 17 00:00:00 2001 From: Chance Santana-Wees Date: Fri, 17 Sep 2021 18:13:17 -0500 Subject: [PATCH 6/6] Update 20210917-nft-metadata-standard.md --- .../20210917-nft-metadata-standard.md | 256 ++++++++++++++---- 1 file changed, 204 insertions(+), 52 deletions(-) diff --git a/submissions/issue-16/milestone-2/Coelacanth/20210917-nft-metadata-standard.md b/submissions/issue-16/milestone-2/Coelacanth/20210917-nft-metadata-standard.md index 0976483e..d553d2fc 100644 --- a/submissions/issue-16/milestone-2/Coelacanth/20210917-nft-metadata-standard.md +++ b/submissions/issue-16/milestone-2/Coelacanth/20210917-nft-metadata-standard.md @@ -1,16 +1,18 @@ -# Title of FLIP +Comment Period throughout Flip-Fest (ends Oct 30) -| Status | (Proposed / Rejected / Accepted / Implemented) | +# Team Coelacanth's Flip-Fest NFT Standard + +| Status | Proposed | :-------------- |:---------------------------------------------------- | -| **FLIP #** | [NNN](https://github.com/onflow/flow/pull/NNN) | -| **Author(s)** | Chance Santana-Wees (figs999@gmail.com) | -| **Updated** | 2021-09-16 | +| **FLIP #** | [635](https://github.com/onflow/flow/pull/635) | +| **Author(s)** | Chance Santana-Wees (figs999@gmail.com) | +| **Updated** | 2021-09-17 | ## Objective Flip-fest issue #16 "New Standard: NFT metadata" -NFTs are more than just numbers and bytes — at their best, they are rich +NFTs are more than just numbers and bytes — at their best, they are rich representations of digital goods that people around the world can fall in love with. The current Flow NFT interface, however, does not include a metadata standard that allows these representations to flourish. NFTs should be able to @@ -44,7 +46,7 @@ This Flip is for a new standard which adds client and contract parsable meta-dat to the default NFT interface. This is accomplished via a new default contract "MetaDataUtils". -```cadence +``` pub contract MetaDataUtil { //Utilizing MIME types as existing standard, this will allow metadata to be more easily parsed by a browser //Any non-conformant type should use a MIME type with the following syntax "application.cadence+T" where T is the Type @@ -53,13 +55,13 @@ pub contract MetaDataUtil { pub let MIME : String //Is the data a link to a file on an external service (ipfs for instance) pub let isLink : Bool - //if the data is compressed, this IAMA MIME type of the data inside the compressed bytes - pub let compressedMIME : String? + //if the data is compressed or a link, this IAMA MIME type of the actual final data (after download/decompression) + pub let innerMIME : String? - init(MIME : String, isLink : Bool, compressedMIME : String?) { + init(MIME : String, isLink : Bool, innerMIME : String?) { self.MIME = MIME self.isLink = isLink - self.compressedMIME = compressedMIME + self.innerMIME = innerMIME } } @@ -84,34 +86,124 @@ pub contract MetaDataUtil { pub let dataProvider: Capability<&{IMetaDataProvider}> } + //Solid version of MetaDataHolder that can be passed around or returned in script call for use off-chain pub struct MetaData { - pub let elements : [MetaDataElement] + pub let data : AnyStruct + pub let tags : [String] + pub let type : DataType + pub let mutable : Bool + + init(data : AnyStruct, type : DataType, tags : [String], mutable : Bool) { + self.data = data + self.type = type + self.tags = tags + self.mutable = mutable + } + } + + pub struct SchemaElement { + pub let requiredTags : [String] + pub let validMIMETypes : [String] + pub let schemaData : [MetaData] + init(requiredTags : [String], validMIMETypes : [String]) { + self.requiredTags = requiredTags + self.validMIMETypes = validMIMETypes + self.schemaData = [] + } + } + + pub struct Schema { + pub let elements : [SchemaElement] + init(elements : [SchemaElement]) { + self.elements = elements + } + } + + pub enum SchemaRetrievalMode : UInt8 { + //Schema always is returned, even if empty + pub case ALLOW_NONE + //Schema is returned if any MetaData is found + pub case REQUIRE_ANY + //Schema is returned only if all SchemaElements are found + pub case REQUIRE_ALL + //Schema is returned if any MetaData is found, but only if each SchemaElement has 1 or less matches + pub case SINGLE_DATA_ANY + //Schema is only returned if exactly one MetaData is found per SchemaElement + pub case SINGLE_DATA_ALL + } + + pub struct MetaDataHolder { + access(self) let elements : [MetaDataElement] init(metaData : [MetaDataElement]?) { self.elements = metaData ?? [] } - pub fun getMetaDataByTag(tag : String) : [MetaDataElement] { - var taggedElements : [MetaDataElement] = [] + pub fun getMetaDatasByTag(tag : String) : [MetaData] { + var taggedElements : [MetaData] = [] for element in self.elements { if (element.hasTag(tag : tag)) { - taggedElements.append(element) + taggedElements.append(element.toMetaData()) } } return taggedElements } - pub fun conformsToSchema(schemaTags : [String]) : Bool { - var tags : [String] = [] + pub fun getMetaDatasBySchemaElement(schemaElement: SchemaElement) : [MetaData] { + let matchingElements : [MetaData] = [] for element in self.elements { - tags.appendAll(element.getTags()) + if(element.hasAllTags(requiredTags: schemaElement.requiredTags) && element.conformsToTypeRequirements(validMIMETypes: schemaElement.validMIMETypes)) { + let dataType = element.getDataType() + matchingElements.append(MetaData(data: element.getData(), type: dataType, tags: element.getTags(), mutable: element.isMutable())) + } } - for tag in schemaTags { - if(!tags.contains(tag)) { - return false + + return matchingElements + } + + //When successfull, this method returns the schema object with the MetaData filled into the SchemeElements + //If the NFTs metadata does not conform to the schema with the chosen SchemaRetrievalMode, it returns nil + //Checking the return of this method to see if it is nil can be used to check if the NFT conforms to the required Schema + pub fun retrieveSchemaData(schema : Schema, retrievalMode : SchemaRetrievalMode) : Schema? { + var foundAll = true + var foundAny = false + var allSingle = true + for schemaElement in schema.elements { + let found = self.getMetaDatasBySchemaElement(schemaElement: schemaElement) + schemaElement.schemaData.appendAll(found) + + if(found.length > 0) { + foundAny = true + if(found.length > 1) { + allSingle = false + } + } else { + foundAll = false } } - return true + + switch retrievalMode { + case SchemaRetrievalMode.SINGLE_DATA_ALL: + if(!foundAll && !allSingle) { + return nil + } + case SchemaRetrievalMode.SINGLE_DATA_ANY: + if(!foundAny && !allSingle) { + return nil + } + case SchemaRetrievalMode.REQUIRE_ALL: + if(!foundAll) { + return nil + } + case SchemaRetrievalMode.REQUIRE_ANY: + if(!foundAll) { + return nil + } + default: + break + } + + return schema } } @@ -167,6 +259,36 @@ pub contract MetaDataUtil { return self.metaData.getTags().contains(tag) } + pub fun hasAllTags(requiredTags : [String]) : Bool { + let tags : [String] = self.metaData.getTags() + for tag in requiredTags { + if(!tags.contains(tag)) { + return false + } + } + + return true + } + + pub fun conformsToTypeRequirements(validMIMETypes : [String]) : Bool { + if(validMIMETypes.length == 0) { + return true + } + let dataType = self.getDataType() + let mime = dataType.innerMIME ?? dataType.MIME + for type in validMIMETypes { + if(type == mime) { + return true + } + } + return false + } + + pub fun toMetaData() : MetaData { + let dataType = self.getDataType() + return MetaData(data: self.getData(), type: dataType, tags: self.getTags(), mutable: self.isMutable()) + } + //This method can be checked to determine if the associated metadata is able to be altered by developers pub fun isMutable() : Bool { return self.metaData.isInstance(Type()) @@ -175,13 +297,59 @@ pub contract MetaDataUtil { } ``` -In this standard: -* Every NFT possesses a MetaData property which acts as a wrapper around MetaDataElements +### General Overview +* Every NFT possesses a MetaDataHolder property which acts as a wrapper around MetaDataElements * MetaDataElements are wrapped custom structs that are created at the time the NFT is minted. * MetaDataElements cannot be added after the NFT is minted, but Mutable elements can allow data to be modifiable. * MetaDataElements are organized by String tags, and each metadata element may have multiple tags. This allows NFTs to conform to multiple tag schemas. * Multiple MetaDataElements can share a tag. This is useful for broad catergories of metadata such as "equipment", but can also be used to provide multiple media types for the same tag to conform to varying schema requirements. +### MetaDataElements +MetaDataElements are wrappers around two possible types of structs, one of which is immutable and the other of which is Mutable. +MetaDataElements possess 3 accessible functions that access stored properties +* The getData function can return an object of any AnyStruct type, and contains the actual metadata itself +* The getDataType function returns a DataType object that describes the MIME type of the object for reference by off-chain systems + * MIME type is used as it is a widely used standard that can already be understood by browsers + * DataTypes also contain isLink, which is true if the data element describes a link to externaly hosted data. + * innerMIME is used to describe the final data type if the data blob should be decompressed or downloaded by the client + +#### IImmutableElements +Immutable elements can use default types or be defined in custom structs that implement ITaggedMetaData and IImmutableMetaData. +* Defining a custom class allows the data elements to be immutable while allowing a developer to upgrade the contract that defines the struct to alter the tags. + +#### IMutableElements +Mutable elements store Capability pointers to IMetaDataProvider instances that can be stored anywhere. +* Mutable elements can be used for metadata that can be modified, such as for leveling up a character. +* Mutable elements can also be used for metadata that is shared between multiple NFTs, to minimize redundant data storage. + * A Side-effect (benefit?) here is that if the instance is stored in a contract in the developers account, the developer will need to cover the metaData storage costs instead of the NFT holder. + +### Schema System +Schema, SchemaElement, and SchemaRetrievalMode are data types primarily for off-chain accessing of MetaData, although it may be useful for certain on-chain functionality +* MetaDataHolder.retrieveSchemaData can be used to retrieve solid copies of the NFTs metadata for use off-chain via script +* If the Schema complies, MetaDataHolder.retrieveSchemaData returns a Schema object which contains the MetaData in its elements[?].schemaData properties +* If compliance fails, the method returns nil + +The schema system allows dApps to attempt to access the metadata of an NFT by providing a known standard. SchemaRetrievalMode allows the caller to require varying degrees of schema compliance. +Schemas can allow multiple valid MIME types for the data allowing NFTs to have flexibility in their data storage types for media files. +Schemas can require multiple tags per element, which can allow interesting functionality for certain applications, such as requiring a \["Equipment","Helmet"]. +Because MetaDataElements can possess multiple tags, it allows NFT developers to update their NFTs MetaData to conform to a variety of schemas with the same data. + + For instance, if DEX-A expects NFTs to comply with: + -tag: ["name"] + -tag: ["text"] + -tag: ["img"] + + while Dex-B expects NFTs to comply with: + -tag: ["title"] + -tag: ["description"] + -tag: ["portrait"] + + An NFT can comply with both by providing elements tagged: + ["name","title"] + ["text","description"] + ["img","portrait"] + +### Modifications to Standard The only modification to the existing NFT interface is the addition of the metadata property: ``` // Interface that the NFTs have to conform to @@ -200,23 +368,7 @@ pub resource NFT: INFT { } ``` -Further explanation of implementation: - -MetaDataElements are wrappers around two possible types of structs, one of which is immutable and the other of which is Mutable. -MetaDataElements possess 3 accessible functions that access stored properties -* The getData function can return an object of any AnyStruct type, and contains the actual metadata itself -* The getDataType function returns a DataType object that describes the MIME type of the object for reference by off-chain systems - * MIME type is used as it is a widely used standard that can already be understood by browsers - * DataTypes also contain isLink, which is true if the data element describes a link to externaly hosted data. - * compressedMIME is used to describe the internal data type if the data blob should be decompressed by the client - -Immutable elements can use default types or be defined in custom structs that implement ITaggedMetaData and IImmutableMetaData. -* Defining a custom class allows the data elements to be immutable while allowing a developer to upgrade the contract that defines the struct to alter the tags. - -Mutable elements store Capability pointers to IMetaDataProvider instances that can be stored anywhere. -* Mutable elements can be used for metadata that can be modified, such as for leveling up a character. -* Mutable elements can also be used for metadata that is shared between multiple NFTs, to minimize redundant data storage. - * A Side-effect (benefit?) here is that if the instance is stored in a contract in the developers account, the developer will need to cover the metaData storage costs instead of the NFT holder. +### Other Contracts Additionally, this standard proposes two more default contracts that include some common DataTypes Metadata implementations. @@ -225,14 +377,14 @@ The vast majority of generic client-facing metadata will conform to the TextPlai ``` pub contract MIME { pub let TextPlain : MetaDataUtil.DataType - pub let HttpLink : MetaDataUtil.DataType + pub let LinkedPNG : MetaDataUtil.DataType pub let ImagePNG : MetaDataUtil.DataType pub let Numeric : MetaDataUtil.DataType pub let AnyStruct : MetaDataUtil.DataType init() { self.TextPlain = MetaDataUtil.DataType("text/plain",false,nil) - self.HttpLink = MetaDataUtil.DataType("text/plain",true,nil) + self.LinkedPNG = MetaDataUtil.DataType("text/plain",true,"image/png") self.ImagePNG = MetaDataUtil.DataType("image/png",false,nil) //Not an official MIME type, but can be used for numeric data that will be returned in cadence object format self.Numeric = MetaDataUtil.DataType("application/cadence+Number",false,nil) @@ -465,25 +617,25 @@ pub contract CommonMetaDataElements { } ``` -### Drawbacks +## Drawbacks Implementing a Metadata standard of any kind will obsolete existing NFT projects, which will force them to go through a re-minting process of some kind in order to take advantage of the new functionality. -### Performance Implications +## Performance Implications -Metadata will of course impact storage size of NFTs. +Metadata will, of course, impact storage size of NFTs. -### Dependencies +## Dependencies Cadence -### Engineering Impact +## Engineering Impact Minimal. Only adding new default contracts. -### Tutorials and Examples +## Tutorials and Examples The code shown in the Design Proposal includes a number of standard implementations of meta-data types. @@ -637,7 +789,7 @@ pub contract ExampleNFT: NonFungibleToken { } ``` -### Compatibility +## Compatibility Current NFT projects that implement the NonFungibleToken contract interface will be made obsolete and will require a migration process to use NonFungibleToken2 if @@ -647,7 +799,7 @@ There is a risk that implementations of NonFungibleToken could be left out of various future projects (such as exchanges) that rely on all assets implementing the NonFungibleToken2 contract interface, if they do not migrate to new interface. -### User Impact +## User Impact None @@ -671,4 +823,4 @@ Please help me elaborate on the Design Proposal by commenting on anything that i in the code that is confusing, omitted, unexplained, etc. Seed this with open questions you require feedback on from the FLIP process. -What parts of the design still need to be defined? \ No newline at end of file +What parts of the design still need to be defined?