onflow / flow-nft

The non-fungible token standard on the Flow blockchain
https://onflow.org
The Unlicense
465 stars 169 forks source link

IMPROVEMENT: NFT Metadata #9

Closed joshuahannan closed 2 years ago

joshuahannan commented 4 years ago

Issue To Be Solved

NFTs always have some sort of metadata associated with them. Historically, most of that metadata has been stored off-chain, but we would like to create a standard for metadata that allows all metadata to be stored on-chain so everything about the NFTs is truly decentralized.

This issue is meant for discussion about the possibilities of the solution. More documentation and examples will be added as we research and discuss more.

I am currently leading the charge on this, but I have a lot on my plate and don't know if I can give this issue the love it deserves, so if someone from the community wants to lead, I would love to speak with you!

Suggest A Solution

Context

The Avastars project is an interesting project on Ethereum that that we could potentially take inspiration from for our metadata.

rheaplex commented 3 years ago

@orodio ^ wrt DIDs.

jimmcnelis commented 3 years ago
  • This scheme would work incredibly well for something like Avastars, where the smart contract itself constructs the metadata (an image in that case), but once it's been constructed, the data doesn't need to live on-chain indefinitely. Any client software could confirm that an off-chain representation of the image matches what the smart contract generated, without the network needing to maintain the long term storage of that data.

the constructed Avastars don't live on-chain. they are rendered on-demand via a read call from small bits stored on-chain. so no one ever has to rely on off-chain storage. its literally the entire point of their existence. the more avastars minted, the more efficient the storage becomes. we did cap them for collectible purposes but had my vision been fully realized these assets would have been made available as a public good for anyone to use in a similar way, making the storage of the individual traits on chain even more efficient over time. i don't think avastars is a great example here. fwiw we do the same with json metadata - we render it on-demand via read calls. so we have 100% on-chain media and metadata, without having stored the individual media and metadata for any single token.

c0wfunk commented 3 years ago

I took a quick look at http://internft.org , apart from the first pages there is not much to see, the project looks abandoned.

Look at the list of working meetings, it is quite active.

https://github.com/interNFT/nft-rfc

https://github.com/interNFT/nft-rfc/blob/main/nft-rfc-002.md

https://blog.cosmos.network/progress-report-interchain-nft-metadata-standards-94770dfe3bb1

pizzarob commented 3 years ago

Great thread here. Would be awesome if we had an open source repo of JSON schemas for different media types, i.e. static image, video, audio, 3d, licenses, tickets and combinations of each. The NFT could reference which schema it adheres to. Then platforms could decide which schemas they want to support - there could even be an open source library of front end components that make it easy to translate these schemas into UI elements.

This could solve one part of the problem which is interoperability. The second part to solve is the storage part. As @ericelliott and @DanMacDonald mentioned a decentralized storage layer for metadata would be ideal. You could use something like Arweave and just reference Arweave URLs and that might work fine, but a storage layer that could be referenced directly from smart contracts would be the best solution. The problem with Arweave is that there is a disconnect between the NFTs and the assets.

There are two types of assets associated with the NFT

JSON Metadata I don't think this should be stored in the smart contract (Ethereum), or NFT resource (Flow) unless it was cost effective and flexible. So if it's not stored in the smart contract then the smart contract or NFT resource could state which schema it adheres to and point to an external resource. 0x cert created something like this years ago here's an overview and here's a link to their technical docs . I think they are onto something here.

Associated files (image, video, 3D file, PDF, etc) The issue is with the URL references. The smart contract references a JSON file and the JSON files references media files. This requires trust that someone isn't going to update the metadata of your $69 million Beeple to a Rick Roll. BUT I do think that metadata should be updatable by the project owner - people make mistakes that need to be fixed and it can open up use cases for dynamic NFTs. So maybe a change log of metadata would work which is publicly accessible. But we are lacking the technology to make this work and right now we are building around this limitation. We need a metadata layer that is accessible via the smart contract layer and can create a change log automatically - otherwise you will always have to trust that you won't get rick rolled.

ericelliott commented 3 years ago

@pizzarob There are some parts of the metadata which must exist on-chain for most media-focused NFTs (images, 3d objects, video, etc):

Currently, when you move metadata off-chain, you make it hard for smart contracts to react to that data. This is why a good integration between a blockchain and a storage layer would be quite useful.

pizzarob commented 3 years ago

@ericelliott 100% agree.

We should add royalties and splits to the NFT standard on Flow. That's important given that all current splits/royalties are platform specific and require a good amount of trust.

So if we tie the content hash to an NFT, or group of NFTs that means those NFT(s) become immutable. Which is probably fine given that I imagine the cost of NFT creation on Flow will be negligible. If you need to modify an NFT you need to create a new one. What are your thoughts there?

bjartek commented 3 years ago

This is how Versus adds royalities to our Art NFT.

https://github.com/versus-flow/auction-flow-contract/blob/master/contracts/Art.cdc#L59

And how it is created

https://github.com/versus-flow/auction-flow-contract/blob/master/transactions/setup/mint_art.cdc#L41

bluesign commented 3 years ago

@pizzarob I disagree, royalties and splits are actually not property of all NFTs. If you have royalty it is better to implement in your contract. Different NFT's can have different models: some have for the lifetime royalty, some only In first few sales.

I think we need to add only very simple stuff to standard, but also give extensibility to developers, in the mean time life of wallet developers hell. I think good wallet's for NFT's is one of the top priorities.

(when I say wallets, I mean all possible applications can show your collections, this can be even some website, or even some electronic appliance like picture frames)

I think something like this can help to all participants (users, wallet developers, chain, etc)

`

//In Standard
pub resource interface INFT{
    pub let id : UInt64
    pub let metadata: [NFTMeta]
}

pub resource interface NFTMeta {
    pub let contentType: String 
    pub let value: AnyStruct
    pub let alternateText: String
    pub let title: String
    pub let description: String
}`

value is AnyStruct, so it allows having unlimited depth of properties. So for example, if we think TopShot, you can have something like "Game" NFT Meta, inside that you can store a dictionary of {String: NFTMeta} , you can put there "score", "winner", "teams" etc. And wallets can show them as sub properties

With this wallet's can access and show all meta data. (if they don't implement some content type basically they can skip showing that, just show as text with getAlternateText )

Developers can extend their logic as much as they want.

pizzarob commented 3 years ago

@bluesign yeah I agree royalties/splits aren't required for every NFT, but there should be a standardized extension that's required for interoperable royalties. If we let developers implement royalties any way they see fit then we find ourselves in our current predicament where there is no interoperable decentralized mechanism for royalties. They are all platform specific. OpenSea, Rarible, Cargo, etc. all implement royalties differently and it's a problem for creators.

You may be onto something NFTMeta idea. It's nice it can be anything but again that makes interoperability difficult as there wouldn't be a standard schema which all platforms/wallets could implement.

bluesign commented 3 years ago

@pizzarob Sorry I think I don't understand the concern. I though you want to put royalty information to the NFT standard, but do you also want to put enforcement on the NFT standard too?

I don't think it will be technically possible in current situation.

kitobelix commented 3 years ago

Why not use UUIDs and keep the information off-chain using cloud-agnostic service networks and reward owners with (for example) a token? That way, I think it is possible to have multiple sources of the same metadata associated by their UUID and in that way decentralise the hosting of the information. Each marketplace or wallet provider could then choose the origin of the information and then hand over the UUID contained in the contract to obtain the information. We could then establish a standard of synchronisation between metadata providers, thus generating a certain level of competition that could - hypothetically - help to improve the system progressively.

The concept of rewards for hosting information is not yet clear to me, but I may soon have an idea. I'm working in a project for doing this possible, it's called Xave and will be online soon.

ericelliott commented 3 years ago

@bluesign I agree with @pizzarob that we need an interoperable standard for royalty payments.

ericelliott commented 3 years ago

Why not use UUIDs and keep the information off-chain

As I mentioned above, it's very useful to store some of the metadata on-chain for use in smart contract logic. As for UUIDs, IPFS uses a content-addressable multi-hash that can be referenced on-chain. I'm all for a standard of looking up metadata by multi-hash, regardless of where it's stored (IPFS, ARWEAVE, etc).

kitobelix commented 3 years ago

Both solutions are compatible and can coexist perfectly. I agree too.

bluesign commented 3 years ago

@ericelliott I understand you want to have some kind of royalty kick back on transactions, but I don't understand how it can be possible technically.

if transaction amount is 0, then there should be no cut. Then I can code a contract, when you deposit somewhere money, send you this NFT for free.

Saying "we need" and "it should be" is easy, but do you guys have any proposal to make this technically?

ericelliott commented 3 years ago

@bluesign The royalty on a payment of $0 is $0. We can't enforce payment on-chain, but when there is a payment, we can automate royalty payments and splits.

bjartek commented 3 years ago

One issue I see with royalty is what token should it be in?

So lets say I mint something as a plattform for an artist. I register 2.5% and 5% royalty and link it to the respective FlowWallets.

What if some marketplace then sell this using FUSD or some other coin?

The artist might not have a wallet for this coin?

When FUSD arrives I think it would be best to just use that everywhere for transactions on NFTs.

ericelliott commented 3 years ago

@bjartek Split metadata usually does not need to include the token to pay with. One of the opportunities of standardized royalties is that the standard should provide simple defaults that work for most cases. If you need something different, you have a turing-complete smart contract programming language to come up with your own scheme.

So lets say I mint something as a plattform for an artist. I register 2.5% and 5% royalty and link it to the respective FlowWallets. What if some marketplace then sell this using FUSD or some other coin? The artist might not have a wallet for this coin?

Beneficiary wallets should use token standards like Flow FT tokens or ERC-20s. The beneficiary wallet must accept the standard Flow FT or ERC-20 tokens, respectively. If they don't, the marketplace should take responsibility to wrap the contract and automatically exchange for a compatible token. These concerns don't need to be part of the royalty spec, but could be addressed in a FAQ.

bjartek commented 3 years ago

But right now both the FlowToken and FUSD only accept that token. There is no wallet that accepts both AFAIK.

I am in the process of making a suggestion repo for this thread. I will just use FlowToken there since that is the only alternative right now.

I also want to be a bit pragmatic and not so theoretically since I think getting a suggestion out is important. There are more and more solutions on flow and a standard for NFT is more important then ever.

If you want your NFT in my example repo send me a ping.

Right now I have versus.Art, topshot.Moment, viv3.Evolution

bluesign commented 3 years ago

@ericelliott problem is when people have a way to evade royalty payment, they will choose that way. As long as I can make a contract, if it receives lets say X flow, then it will transfer NFT from A to B for free, then there is no point of having royalty at all.

From TopShot example, if people find a way to list with 0% market cut, they will use that, instead of TS website. Actually thats the reason for now you cannot even gift your NFT (without 100 conditions)

Problem is adding movement detection is only possible on collection level, stuffing stuff to collection, is not extendable.

@bjartek super, I am curious about your suggestions on this too, I think repo would be nice, feedback is usually very slow process otherwise.

ericelliott commented 3 years ago

@ericelliott problem is when people have a way to evade royalty payment, they will choose that way. As long as I can make a contract, if it receives lets say X flow, then it will transfer NFT from A to B for free, then there is no point of having royalty at all.

Some will bypass royalties, but most don't in practice. See every royalty-supporting NFT platform out there, for reference (notably, Rarible and OpenSea). People are more complex than "can we evade the royalty?" - turns out a large fraction of people actually want to reward the creator for making something they love.

If you made a contract that takes money then transfers for free, bypassing the royalty system, that contract is open source, on a blockchain, and eventually people are gonna call it out and shame every platform that uses it for stealing from artists.

Blockchain contracts are as much social contracts as they are money incentive contracts.

From TopShot example, if people find a way to list with 0% market cut, they will use that, instead of TS website. Actually thats the reason for now you cannot even gift your NFT (without 100 conditions)

Yep, some people do. Out of curiosity, do you happen to know what % of TS transactions happen OFF the TS website with gift transactions vs on the TS website with money transactions? I'm willing to bet it's relatively small compared to over-all volume.

But right now both the FlowToken and FUSD only accept that token. There is no wallet that accepts both AFAIK.

Yeah, there are lots of protocols that allow recipients to select the token they want payment in. As soon as Flow has a DEX, you could say "give me 5% of the payment, and I want it in FLOW". If the user pays in FUSD, you'd need the smart contract that accepts payment in FUSD to encapsulate a swap for FLOW. But the token metadata doesn't need to care about that.

bluesign commented 3 years ago

Main problem is people want "flow" to be the guardian.

You say "eventually people are gonna call it out and shame every platform that uses it for stealing from artists." but you want to put royalty in the standard. Why people not choose royalty respecting platforms now?

"Blockchain contracts are as much social contracts as they are money incentive contracts." I am sorry but this is totally wrong , when people will evade royalty, next thing you will see, people complaining on discord or in here, flow not enforcing royalty stuff. Then another group of people come, it says royalty is not flow's business to manage.

Putting a rule which you cannot enforce is worse than not putting a rule. You are being a party to conflict unnecessarily.

ericelliott commented 3 years ago

@bluesign Smart contract royalties are here to stay. They're already in most of the popular NFT platform smart contracts. If you don't like them, don't use them. Nobody is saying that every NFT needs a royalty system. What we are saying is for platforms that want to support them, we should have a standard to make them easier to integrate.

rheaplex commented 3 years ago

@rafaelmorado @jimwheaton yes this one. 😺

slavakurilyak commented 3 years ago

The problem with most NFT marketplaces today is permanence which leads to link rot. If the underlying assets to NFTs are modified or deleted, NFT collectors are left in the dark.

Storing metadata and underlying assets onchain is expensive and uncommon, especially for @ethereum. Decentralized providers offer relief. @IPFS is commonly used by the Ethereum community.

What's the issue with IPFS for metadata storage? Assets stored using IPFS rely on the goodwill of the people storing it. IPFS acts more as a map telling you where a specific piece of data resides, but does not incentivize anyone for actually storing such data.

What's an alternative to IPFS? @ArweaveTeam ensures permanent storage of the asset by incentivizing the storers through an upfront endowment payment.

P.S. I'm not affiliated with IPFS or Arweave teams.

bjartek commented 3 years ago

Versus has experimented with #SODA, the O is relevant here and stands for onChainData.

There is currently two issues that is worth mentioning here. If you are uploading large files split it up into sections and append them to a storage, then read from storage in a seperate transaction. The size limit on mainnet is 1.5Mb.

There is also a limit on script return size and that is 8mb.

Storing data on flow is viable but not for big files.

bmann commented 3 years ago

@slavakurilyak

What's the issue with IPFS for metadata storage? Assets stored using IPFS rely on the goodwill of the people storing it. IPFS acts more as a map telling you where a specific piece of data resides, but does not incentivize anyone for actually storing such data.

This is the same issue with literally anything on the web, but much worse -- not paying for a DNS name, not paying for hosting, the same link resolving to a completely different piece of content, etc. etc. etc.

Any collection of bits will always resolve to the same IPFS hash, and anyone can cooperatively help host it. I don't know of any other system that allows this. (BitTorrent, kind of, but it really is for hosting downloads, not "live files" that IPFS can do)

This means you can stick the resource on a USB key, and if you put it back online 10 years later, that resource's address will once again be shared across the network.

It also means that the owner of the NFT can "host" it by just keeping a copy, which they most likely want to do.

IPFS doesn't include "economic surety" (in a good way). You can add economic surety from any number of different ways, from ones based on fiat (pay a service provider regular dollars to run IPFS hosting) to crypto (a smart contract, Filecoin, etc. etc.)

bjartek commented 3 years ago

My current thoughts on this is to make it what in my eyes is really simple.

  1. Default implementations for methods are added to interfaces in flow.
  2. the NFT standard gets some aditional methods

The adtional methods can be like getName(): String : return the name of the NFT getDescription(): String : return a longer description about the NFT getSchemas(): [String] : return a list of schemas getData(schema: String) : AnyStruct : return data for the given schema

You could even argue that getDescription could just be removed and you could use description as a schema that could be returned.

These simple changes will make NFTS so much more usable since you can then borrow any NFT, find its name and schemas and get the data for them.

franckdpt commented 3 years ago

Hi there,

NFT assets storage is a very big deal. Works like Arweave or IPFS are the first step to fully decentralized media storage, and we can't wait to reach it. First, we need to educate people about this issue. Too many think that their NFTs are fully owned by them, while the assets can disappear at any moment.

With my brother, we built https://artforall.io to evaluate how NFTs are decentralized by auditing the storage methods. We really want non-technical users to understand if they really own NFT assets. So we grade the decentralization and persistence level.

Feedback is welcome!

m4nuC commented 3 years ago

@iWhited I don't want to clutter this thread but very cool project 👏

bjartek commented 3 years ago

In the following branch https://github.com/bjartek/flow-nfmt/tree/default-implementations I have taken the following PR to cadence https://github.com/onflow/cadence/pull/1076 and impemented the defaults methods for

getName
getSchemas
resolveSchemas

It is a bit of a hassle if anybody else wants to get this up and running so I can print the output of the scenario below

Deploying 7 contracts for accounts: emulator-account

NonFungibleToken -> 0xf8d6e0586b0a20c7 (4584d32d3916f92b6ab94daa6544d11958e418c6c1f7a6c9877c4aabb11146e2)

Content -> 0xf8d6e0586b0a20c7 (bd8056217eb105c9aab3001e90da35b7db91cbcc70add42b41de15d9fd6265a9)

Evolution -> 0xf8d6e0586b0a20c7 (2f2805baba71ecd885e54579a64efe5db5d5da097cf37699375b0eea88b09e1f)

TopShot -> 0xf8d6e0586b0a20c7 (5358941e3ca5a19864b0e2d5fde798235ee7cbf767f08eeae22c6c7b41a25212)

ChainmonstersRewards -> 0xf8d6e0586b0a20c7 (51cc1b83e1fa6dc8958a34266d4dbe201d881fc443c0844f7fc8d9850d74381a)

NFTMetadata -> 0xf8d6e0586b0a20c7 (b27e1741bc2cec60ea0dbbeaf6ba75d05d86934b74b7e18920e8b49d3da26a74)

Art -> 0xf8d6e0586b0a20c7 (fdeea5f82f1ec9cb3b83907c56dd6c558da11081bfd140c8267811eb21cd3ccf)

✨ All contracts deployed successfully
[emulator-account]

Ensuring account with name 'emulator-account' is present
Account is present
Transaction ID: 0d2df12272e4c31f289b1062ba8bc42caaf09b45a3953b65c0b7e907d5bb4123
👌 Transaction nfmt successfully applied

2021/08/01 23:35:45 EVENTS
2021/08/01 23:35:45 ======
2021/08/01 23:35:45 {
    "name": "A.f8d6e0586b0a20c7.Art.Created",
    "time": "2021-08-01T23:35:45.484467+02:00",
    "fields": {
        "id": "0",
        "metadata": {
            "artist": "test creator",
            "artistAddress": "0xf8d6e0586b0a20c7",
            "description": "This is a test",
            "edition": "1",
            "maxEdition": "1",
            "name": "Test art",
            "type": "png"
        }
    }
}
2021/08/01 23:35:45 {
    "name": "A.f8d6e0586b0a20c7.Art.Deposit",
    "time": "2021-08-01T23:35:45.484545+02:00",
    "fields": {
        "id": "0",
        "to": "0xf8d6e0586b0a20c7"
    }
}
2021/08/01 23:35:45 ======
⭐ Script run from path ./scripts/art.cdc

A.f8d6e0586b0a20c7.Art.Metadata(name: "Test art", artist: "test creator", artistAddress: 0xf8d6e0586b0a20c7, description: "This is a test", type: "png", edition: 1, maxEdition: 1)
⭐ Script run from path ./scripts/creativework.cdc

A.f8d6e0586b0a20c7.NFTMetadata.CreativeWork(artist: "test creator", name: "Test art", description: "This is a test", type: "png")
⭐ Script run from path ./scripts/editioned.cdc

A.f8d6e0586b0a20c7.NFTMetadata.Editioned(edition: 1, maxEdition: 1)
⭐ Script run from path ./scripts/royalty.cdc

A.f8d6e0586b0a20c7.NFTMetadata.Royalties(royalty: [A.f8d6e0586b0a20c7.NFTMetadata.Royalty(wallet: Capability<&AnyResource{A.ee82856bf20e2aa6.FungibleToken.Receiver}>(address: 0xf8d6e0586b0a20c7, path: /public/flowTokenReceiver), cut: 0.05000000), A.f8d6e0586b0a20c7.NFTMetadata.Royalty(wallet: Capability<&AnyResource{A.ee82856bf20e2aa6.FungibleToken.Receiver}>(address: 0xf8d6e0586b0a20c7, path: /public/flowTokenReceiver), cut: 0.02500000)])

As you can see in the transcript above TopShot/Evolution/ChainMonstersRewards are all deployed successfully even though the NonFungibleToken interface in this branch has the new methods mentioned above with a default implementation in them.

For the versus contracts I have implemented various schemas and even created a seperate contract NFTMetadata that contian shared structs that can be used for metadata.

Comments are appreciated. Hope we can drive this forward with concrete examples and not just talk.

aturX commented 3 years ago

Why not use UUIDs and keep the information off-chain

As I mentioned above, it's very useful to store some of the metadata on-chain for use in smart contract logic. As for UUIDs, IPFS uses a content-addressable multi-hash that can be referenced on-chain. I'm all for a standard of looking up metadata by multi-hash, regardless of where it's stored (IPFS, ARWEAVE, etc).

I found Ceramic Network (formerly 3Box), which I think is a very potential solution for DID mapping between different blockchains . It has supported a number of mainstream public chains.Therefore, I would like to discuss here whether Flow can also integrate Provider according to standard CAIP. Also, can this be used as an NFT storage solution? Anybody interested in that? @ericelliott @joshuahannan @oed

add doc: https://developers.ceramic.network/reference/javascript/blockchain/

CAIPs: https://github.com/ChainAgnostic/CAIPs

Ceramic: https://ceramic.network/

3box: https://3boxlabs.com/

ericelliott commented 3 years ago

I'm very interested in checking out Ceramic for this. How close is it to production ready?

aturX commented 3 years ago

I'm very interested in checking out Ceramic for this. How close is it to production ready?

Its main network is already online, and mature product cases are currently being built on it.Similar to https://boardroom.info/

oed commented 3 years ago

Hey, was asked to weigh in here since Ceramic was mentioned! I'm not going to try to prescribe what the right thing for the Flow community to do for your NFT standard is, but can provide some context as what would make sense from our point of view and features that Ceramic provides.

Stroring some kind of content identifier, e.g. CID, or StreamID (used by Ceramic), is a good way to go about things since data can be stored much more cheaply on networks such as Filecoin, Sia, or Arweave.

Ceramic StreamIDs allows the content to be a mutable object (where all changes are notarized and auditable). The changes are signed by a DID.

We've also been working on the NFT DID Method which allows an NFT to be used as an identity where the current owner of the NFT can make signatures and decrypt data. This is useful if you want some content to only be modifiable by the current NFT owner.

Furthermore, Ceramic can enforce data in streams to conform to a certain json-schema, see our documentation

bjartek commented 3 years ago

I just expanded on my poc even more to add suport for web3 like urls, that is a url into a nft for a user, in a collection with a given scheme. For more information read https://github.com/bjartek/flow-nfmt/tree/default-implementations#support-for-web3-like-urls

hadv commented 3 years ago

what do you think about linking to a decentralize storage like bluzelle?

https://bluzelle.com/

bjartek commented 3 years ago

https://www.jottacloud.com/s/110aa1a7d6fbad74458bf8a002b4c39d153 A little video showing off this proposal.

bjartek commented 3 years ago

This was discussed extensivly at the latest office hours and I gave a demo almost like the one i linked above. I was asked to write up some more about this so I did here: https://gist.github.com/bjartek/be7b63825609f58e837e8ef345ca315a

This is far from done though. Feel free to comment here or right in the gist.

briandilley commented 3 years ago

I really like the work that @bjartek has done so far. I have a few comments:

So to me it seems like to define a standard for metadata on NFTs the task at hand is:

And it seems like @bjartek may have already started on a few of these things.

briandilley commented 3 years ago

For the sake of options, I'd like to throw our approach into the mix (below). It's a fairly basic system - but flexible enough to support most things. What's interesting is that what we've done would actually work within @bjartek 's proposal by exposing a schema like 0x012345abcdef.NFTMetaDataExample.MetadataCapable|metadataCapable

import NonFungibleToken from 0xNONFUNGIBLETOKEN

pub contract NFTMetaDataExample: NonFungibleToken {

   ... standard NFT stuff here ...

   pub interface MetadataCapable {
       pub let metadata: {String: NFTMetaDataExample.MetadataField}
   }

    // The immutable data for an NFT, this is the actual NFT
    //
    pub resource NFT: NonFungibleToken.INFT, NFTMetaDataExample.MetadataCapable {
        pub let id: UInt64
        pub let metadata: {String: NFTMetaDataExample.MetadataField}

        init(metadata: {String: NFTMetaDataExample.MetadataField}) {
            self.id = ...
            self.metadata = metadata
        }
    }

    // =====================================
    // Metadata
    // =====================================

    // The type of a meta data field
    //
    pub enum MetadataFieldType: UInt8 {
        pub case STRING
        pub case NUMBER
        pub case BOOLEAN
        pub case DATE
        pub case DATE_TIME
        pub case URL
        pub case URL_WITH_HASH
        pub case GEO_POINT
    }

    // a meta data field of variable type
    //
    pub struct MetadataField {
        pub let type: MetadataFieldType
        pub let value: AnyStruct

        init(_ type: MetadataFieldType, _ value: AnyStruct) {
            self.type = type
            self.value = value
        }

        pub fun getStringValue(): String? {
            if self.type != MetadataFieldType.STRING {
                return nil
            }
            return self.value as? String
        }

        pub fun getNumberValue(): String? {
            if self.type != MetadataFieldType.NUMBER {
                return nil
            }
            return self.value as? String
        }

        pub fun getBooleanValue(): Bool? {
            if self.type != MetadataFieldType.BOOLEAN {
                return nil
            }
            return self.value as? Bool
        }

        pub fun getURLValue(): String? {
            if self.type != MetadataFieldType.URL {
                return nil
            }
            return self.value as? String
        }

        pub fun getDateValue(): String? {
            if self.type != MetadataFieldType.DATE {
                return nil
            }
            return self.value as? String
        }

        pub fun getDateTimeValue(): String? {
            if self.type != MetadataFieldType.DATE_TIME {
                return nil
            }
            return self.value as? String
        }

        pub fun getURLWithHashValue(): URLWithHash? {
            if self.type != MetadataFieldType.URL_WITH_HASH {
                return nil
            }
            return self.value as? URLWithHash
        }

        pub fun getGeoPointValue(): GeoPoint? {
            if self.type != MetadataFieldType.GEO_POINT {
                return nil
            }
            return self.value as? GeoPoint
        }
    }

    // A url with a hash of the contents found at the url
    //
    pub struct URLWithHash {
        pub let url: String
        pub let hash: String?
        pub let hashAlgo: String?
        init(_ url: String, _ hash: String, _ hashAlgo: String?) {
            self.url = url
            self.hash = hash
            self.hashAlgo = hashAlgo
        }
    }

    // A geo point without any specific projection
    //
    pub struct GeoPoint {
        pub let lat: UFix64
        pub let lng: UFix64
        init(_ lat: UFix64, _ lng: UFix64) {
            self.lat = lat
            self.lng = lng
        }
    }
}
bjartek commented 3 years ago

Very nice @briandilley. I think this is a testament to the power and simplicity or my proposal.

Regarding your comment on Content-Negotiation. You really have to involve hypermedia/REST lvl 4 on RMM if you can compare it. That is you should have a MediaType for your content not just application/json. However REST is a overloaded/vague term, people interpet it differently. That is one of the reasons why I went with 'schema' and not contentType.

briandilley commented 3 years ago

Just wanted to leave this here, more information on the given proposal.

dete commented 3 years ago

There's some great discussion here! Thanks to everyone who's putting thought into this. Special call outs to @bjartek and @briandilley for putting together concrete proposals. 💥

I had always assumed that a simple "string->string" dictionary was the way to go, but I have to admit that Bjartek's idea that we should use native Cadence types makes quite a bit of sense to me. It certainly makes it much more powerful to interact with the metadata from within Cadence, and we know that any libraries that interact with Flow will need to have SOME way to inspect Cadence structs.

So then I wonder if we should lean all the way into that: Why not have the interface look more like:

pub fun availableMetadata() : [Type]
pub fun getMetadata(_ type: Type): AnyStruct

Then, anyone who wants to define a new metadata schema just needs to define a new Cadence type that is published on-chain. For example, we might have a struct BasicMetadata that is included as part of the new NFT standard:

struct BasicMetadata {
   pub let name: String
   pub let description: String
}

And we could have a "HostedImage" struct:

struct HostedImage {
   pub let imageURI: String
}

And a "IPFSImage" struct:

struct IPFSImage {
  pub let ipfsHash: String
}

The counter-argument would be "What if someone needs two bits of metadata with the same type?" Well, my counter-counter-argument would be: "If that ever comes up, and I can't think of any examples of that might, we can define a new metadata type that includes an array or dictionary of the old metadata type."

Curious to get folks reactions!

briandilley commented 3 years ago

@dete - nice to see the big dog chiming in!

A few notes:

You'll notice i use the word "View" below, it's synonymous with "Schema" as in @bjartek's original proposal, but we've both agreed that "View" is more appropriate as this standard is more about looking at the data enclosed within an NFT in different ways.

In our original thinking your example of pub fun availableMetadata() : [Type] would be covered by pub fun getViews(): {String: Type}. The reason we return a dictionary is so that we can support multiple views of the same type. For instance, an NFT for a piece of music might have a Person view for each of the artists, writers, etc. They would be keyed as such by the NFT in the dictionary returned by the getViews() method. This prevents from having to publish composite structs to bring those things together for simple things.

Now after typing this all out, I would almost prefer my above example to be a composite struct like so:

struct Song {
   pub let name: String
   pub let artists: [Person]
   pub let writers: [Person]
   ... etc ...
}

So maybe the naming part is not necessary?

Either way, I think that along with this standard there needs to be an Account with contracts deployed for some of the obvious Views like the ones that you mentioned.

Thinking out loud, if we got rid of the names it would look something like this:

        pub fun getViews(): [Type]
        pub fun resolveView(_ type: Type): AnyStruct?

and perhaps (for convenience):

        pub fun supportsView(_ type: Type): Bool
bluesign commented 3 years ago

Hey @dete,

I had a similar comment on the issue recently on the discord (about struct usage on metadata)

I think @bjartek and @briandilley are talking about different a layer than what we are thinking. In my opinion, they are not proposing something about underlying storage but more about the presentation.

In NFT I can even store all metadata as a single line | separated string, but as long as I parse this data into structs with appropriate View types it should be ok.

Although I see this can be a powerful pattern, I am not sure if it is necessary. As we store stuff with struct, we get a free presentation, and the only problem there is duplicate (or overlapping) presentation causing extra storage.

briandilley commented 3 years ago

@bluesign our proposal really has nothing to do with storage, you need not to store anything to accommodate the proposed methods. In fact in my implementation I build the structs on the fly. Also, the storage costs (if there were any) would be on the owner of the nft contract not the owners of the nfts themselves. This proposal is purely about presenting the NFT data in different "shapes' as defined by the structs that are used.

It seems to me that @dete understands what we're proposing. The only hiccup is the naming, which I'm kind of leaning back on being more useful than if it weren't there. The main reason being the requirement to publish many structs just to support multiple of the same type for different pieces of data, like in my music and person example from earlier.

bluesign commented 3 years ago

@briandilley what you said is exactly my point, your proposal is different layer than NFT metadata storage, hence the confusion (as for myself got confused, as I guess @dete was in similar spot)

There are 2 parts of the problem:

In my opinion storage and presentation if can be together it is much better. ( I don't think there is a benefit separating them in this case tbh, please correct me if there are some benefits )

Your proposal look very similar to :

Dictionary.Keys ( getViews ) 
Dictionary.Get ( resolveView )
Dictionary.HasKey ( supportVIew )

so why not use Dictionary instead. ( I mean as underlying storage )

Multiple metadata with same type can be solved by defining a metadata interface like below: (so we can name metadata items)

struct interface MetaStruct{
pub var tag : String
}

Now we can use metadata in NFT as: {Type: [MetaStruct]} (where MetaStruct will be something like @dete proposed: HostedImage, BasicMetaData etc)

So when we arrive this point, I think functions are getting little obsolete too. (but I think this is a discussion for later)

Only problem I see as I stated before, duplicate meta storage.

Let's say you have 3 competing metas (from different ecosystems):

An NFT want to implement those may need to add 3 meta instead of 1. (with functions instead of dictionary, you can use helper functions to convert)

But I am not sure if this will be problem, if we cover most basic structures in the standard contract.

bjartek commented 3 years ago

Thanksf or your kind words @dete. The way I see it mine and @briandilley's suggestion has several advantages over what you propose.

  1. Our solution can solve all the issues you suggest. The only different is that you have to give the View/Metadata a string name in adition to the type. This can serve as documentation and to show intent and it bring value to the simples examples IMHO.

  2. There will be a lot of wrapper types if we do not support multiple Views/Metadata with the same type. Lets take the Song example. What if i suddenly want a nother field in that metadata? I have to add a new type in my own contract for it and suddenly there is two Song structs that contracts have to know about and implement. If we support multiple Persons with our solution we could easily use the string tag to say "bob|bandmember", "elvis|singer" or something like that to distinguish the Types.

  3. If we allow multiple entries with the same type it will be a lot easier to write generic code that maps Metadata/View -> React component. However if somebody suddenly uses Song that has Person and Car that has Person aso you have to unrwap things and then it becomes much harder. In fact i feel that the hirarchy of the Metadata/Schemas should be pretty flat just for this reason.

  4. Supporting things like Shared data between NFTs and state that is Mixed in after creation by an owner is hard if you do not allow multiple instances of the same type. Lets say I want to sign something i bough from @briandilley. I can do that, but then later if @bluesign buy it from me he cannot sign it since I have already signed it. (He cannot access the struct that i added since he is not me)

For me these 4 reasons are the most important ones but it is just of tip of the iceberg. The original solution should stand, but how we name things can change.

The name of "Schema/View/Metadata/Data/Whatever/Thing/Addon/Mixin/State" is not imporant to me. However if we use the Metadata term what makes it "meta" compared to "data". Some of the schemas added to an NFT might actually be the data that is the NFT.

Personally i vote for View. "Get all of the Views of this NFT of this type" sounds good imho.

bjartek commented 3 years ago

@bluesign I think the standard should be open to storing things and resolving things the way the NFT would like. If you already have an existing NFT like versus we already store things in a certain way. So i can just convert the versus way to this new standard and we will support it.