CIP Draft - Cardano "Smart NFTs"

Cardano “Smart NFTs”

Abstract

This CIP specifies a standard which allows on-chain Javascript NFTs to read the transaction history for a particular token (which could be the NFT itself, or any other token), or a particular address (to allow monitoring of smart contracts or Oracle endpoints).

Currently if an NFT creator wishes to change or otherwise “evolve” their NFT after minting, they must burn the token and re-mint. It would be very nice if the user were able to modify their NFT simply by sending it to themselves with some extra data contained in the new transaction metadata. This would allow implementation of something like a ROM+RAM concept, where you have the original immutable part of the NFT (in Cardano’s case represented by the original 721 key from the mint transaction), and you also have a mutable part – represented by any subsequent transaction metadata.

It would also be nice to be able to retrieve data that has been previously committed to the blockchain, separately to the NFT which wishes to access it. This would be useful for retrieving oracle data such as current Ada price quotes – as well as for allowing an NFT to import another NFT’s data – thus reducing code duplication in on-chain metadata (enabling us to do more within the same metadata size limits).

This functionality enables many exciting new possibilities with on-chain NFTs, so many, I’ve created a separate section at the end of this documentation with suggestions on things it could be used for.

Description:

Currently the NFT sites which support on-chain Javascript NFTs do so by creating a sandboxed <iframe> into which they inject the HTML from the NFT’s metadata. From within this sandbox it is not possible to bring-in arbitrary data from external sources – everything must be contained within the NFT, or explicitly bought into the sandbox via an API.

This proposal suggests an addition to the 721 metadata key from CIP 25, to enable an NFT to specify that it would like to receive a particular transaction history accessible to it from within the sandbox – thus defining it as a “Smart NFT”.

In tandem with the additional metadata, we also define a standard for the Javascript API which is provided to the NFT within the sandbox.

The Metadata

Minting metadata for Smart NFTs – based on the existing CIP 25 standard:

{
      "721": {
        "<policy_id>": {
          "<asset_name>": {
            "name": <string>,
    
            "image": <uri | array>,
            "mediaType": "image/<mime_sub_type>",
    
            "description": <string | array>,
    
            "files": [{
              "name": <string>,
              "mediaType": <mime_type>,
              "src": <uri | array>,
              <other_properties>
            }],
    
            "uses": {
	            "transactions": <string | array>,
	            "ipfs": <string: default "false">	    
	        }
          }
        },
        "version": "1.0"
      }
}

Here we have added the “uses” key – any future additions to the Smart NFT API can be implemented by adding additional keys here. For now, we just define “transactions”, which can be a string or an array, specifying the token fingerprints or addresses the NFT wishes to receive transaction history for. Optionally we also provide the ipfs option, which specifies whether the NFT wishes to retrieve files from IPFS.

We also define a special keyword “own” which can be used to monitor the NFT’s own transaction history. So if we wish to create an “evolvable” NFT, the “uses” key within the metadata would look like:

	"uses": {
		"transactions": "own"
	}

If you wanted to create an evolving NFT which monitored its own transaction history, as well as that of an external smart contract address, the metadata would look like this:

 	"uses": {
		"transactions": [
			"own",
			"addr1wywukn5q6lxsa5uymffh2esuk8s8fel7a0tna63rdntgrysv0f3ms"
		]			
	}

Finally, we also provide the option to receive the transaction history for a specific token other than the NFT itself (generally this is intended to enable import of Oracle data or control data from an external source – although monitoring an address transaction history could also be used for that).

When specifying an external token to monitor, you should do so via the token’s fingerprint as in this example, which also specifies that it will use the ipfs gateway:

 	"uses": {
		"transactions": [
			"own",			
			"asset1frc5wn889lcmj5y943mcn7tl8psk98mc480v3j"
		],
		"ipfs": "true"
	}

The Javascript API

When an on-chain javascript NFT is rendered which specifies any of the metadata options above, the website / dApp / wallet which creates the iframe sandbox, should inject the API defined here into
that <iframe> sandbox.

It is recommended that the API not be injected for every NFT – only the ones which specify the relevant metadata - this is an important step so that it’s clear which NFTs require this additional API, and also to
enable pre-loading and caching of the required data. We are aiming to expose only the specific data requested by the NFT in its metadata – in this CIP we are not providing a more general API for querying arbitrary data from the blockchain.

There is potentially a desire to provide a more open-ended interface to query arbitrary data from the blockchain – perhaps in the form of direct access to GraphQL – but that may follow in a later CIP – additional fields which could be added to the uses: {} metadata to enable the NFT to perform more complex queries on the blockchain.

Although an asynchronous API is specified – so the data could be retrieved at the time when the NFT actually requests it – it is expected that in most instances the site which renders the NFT would gather the relevant transaction logs in advance, and inject them into the <iframe> sandbox at the point where the sandbox is created, so that the data is immediately available to the NFT without having to
perform an HTTP request.

cardano.getTransactions( string ) : Promise

The argument to this function should be either an address, token fingerprint or the keyword “own”. It must match one of the ones specified via the new metadata mechanism detailed above.

This function will return a list of transaction hashes and metadata relating to the specified address or token. The list will be ordered by date with the newest transaction first, and will match the following format:

 {
	"transactions": [
		{ 
			"txHash":  "1507d1b15e5bd3c7827f1f0575eb0fdc3b26d69af0296a260c12d5c0c78239e0",
			"metadata": <raw metadata from blockchain>
		},
		<more transactions here>
	],
	"fetched": "2022-05-01T22:39:03.369Z"
}

For simplicity, we do not include anything other than the txHash and the metadata – since any other relevant details about the transaction can always be encoded into the metadata, there is no need to
over-complicate by including other transaction data like inputs, outputs or the date of the transaction etc. That is left for a potential future extension of the API to include more full GraphQL support.

cardano.getIPFSFile( string hash ) : Promise

If the NFT specifies ipfs = true in its “uses” metadata, the NFT renderer sandbox should provide this function to retrieve an IPFS file by hash – the implementation may wish to pre-cache any IPFS URLs
which are mentioned in the files array of the NFT metadata, but this function is expected to be used on arbitrary hashes – this is to enable NFT creators to enter new IPFS hashes into the NFT via the
above defined getTransactions() interface.

The return from this function should be the raw data directly from ipfs.

Use Cases

Various types of evolving NFT are possible in this manner – without re-minting an NFT, the user is able to sign a specially crafted transaction which simply sends the NFT to another UTXO at their wallet. You could use this for simple things like customizing the hair colour of a PFP NFT, or complete rewrites of the NFT’s appearance – as in the case of an egg that hatches.

By monitoring an external address or token, it’s also possible to have these events triggered externally without the owner of the token having to do anything – this way, all the eggs could hatch at once, triggered by the creator rather than the owner. The creator would simply have their NFT monitor a particular token in the creator’s own wallet, and when they wanted their eggs to hatch, they would submit one new transaction to the blockchain, and all the NFTs that are monitoring for it would immediately update and receive the new metadata.

This allows for things like control tokens – a creator may choose to mint some special tokens within their policy which all other tokens would monitor – the holder of these special tokens would be able to effect changes to the whole project, simply by submitting a specially crafted transaction involving this special control token that they own.

Take a random example – you could have a project of 9,999 monkey NFTs, and one monkey god NFT – the monkey NFTs themselves are all individual, but they read their current “dancing” state from the special monkey god control NFT. If monkey god wants monkeys to dance, all he has to do is submit one transaction with some special metadata – this is then picked up by every other NFT, and they all begin dancing in unison. You could have multiple different control tokens controlling different aspects of the NFT. I’m sure the NFT community will think of some fun things to do with this.

The ability to import another NFT also means we can greatly increase the amount of actual program data that’s available to an on-chain NFT, by simply having it spread over more individual tokens. On-chain creators will be able to register their Javascript libraries onto the blockchain as NFTs themselves, and then import them into their creations. This in itself will greatly expand what’s possible with Cardano NFTs without requiring IPFS, but the IPFS mechanism is provided to enable external code and data to be loaded from an immutable source outside Cardano.

In the future this API will likely expand to include more integration with on-chain oracles, so that the same data feeds that are available to smart contracts are also accessible within Smart NFTs. This will enable NFTs to respond to changing market conditions, maybe even changing weather conditions.
With appropriate oracle data, you could make an NFT of a pump that visually activates when Cardano price pumps (or any other favourite digital asset).

16 Likes

This looks great Kieran!
Say you wanted to add accessories to your NFT…
Could this be used to check if another token is in the same wallet as this NFT but was sent to a different address generated with the same key?

also for clarity, if “src” is an array, could this be a raw HTML file (+ JavaScript etc) (as opposed to a url/dataurl)?

thanks

1 Like

IPFS/IPNS have been the classic pinning mechanism for off-chain data but that appears to be changing … Arweave is becoming popular for permanent storage of images and data related to NFT.

1 Like

This is basically the behaviour I am currently trying to implement using burn and remint. Is this posted anywhere else like the cip github where it can gain traction?

1 Like

Why is this needed exactly and why not just providing this API to all JS NFT’s without the need of extending the 721 metadata?

Tis explained in the CIP - firstly to enable sites to identify which NFTs require this extra API (so any site which chooses not to implement the API can know which NFTs are expected not to render correctly) - also so sites like pool.pm can flag the NFT with “this is a Smart NFT” - similar to how they currently flag IPFS and on-chain NFTs
Secondly this is to allow pre-caching of the needed data for speed reasons - metadata is used to specify exactly which data should be pre-loaded, and then it is inserted statically into the sandbox for both performance and security reasons.

I understand some people may also want a more general interface to query the blockchain from within an NFT’s sandbox, but this isn’t that. I may write a separate CIP for that functionality at a later date, unless someone else beats me to it.

I think there are a few projects out there using burn-and-remint at the moment - that was one of my primary motivations for developing this specification - I’m following the recommended process for submitting CIPs - requesting feedback and comments on this forum, then once it’s ready, I will submit a pull request to the CIP repository for formal inclusion.

It’s worth mentioning that I will also provide the reference implementation at Artifct.app, as well as a mint for an example NFT that follows this standard, so you will be able to test the implementation at our site fairly soon.

This is a good point, and the standard should support any future off-chain data storage mechanisms as well - not be specific to IPFS. I will modify the specification so it applies more generally to any off-chain storage.

There is not currently a function to get the list of tokens held by an address, but I absolutely see the use-case for this, so I’ll add one :slight_smile: Re-draft incoming…

1 Like

off-chain, side-chain, etc. Arweave, Filecoin, et al are blockchains in their own right. To generalize there would need to be a data reference that provides enough context to determine the platform in use and the more specific usage. At which point using Cardano transactions becomes just another source of platform dependencies. If the values require objects or more complexity than array of strings then a platform meta data server to register integrations might be necessary …

"uses": {
    "transactions": [], // tx id values(s)
    "ipfs": [], // uri hash values(s)
    "arweave": [], // etc
    "plutus": [], // etc
}

will check that when it’s ready :slight_smile: I am in Discord for artifct. I will be glad to test this out when it’s ready. I’m hoping to mint an nft and then apply another nft or token to create a new one.

1 Like

Doesn’t this break the permanence of the NFT?

Without this, It seems that we could guarantee that the NFT works forever (assuming JS/Web APIs backward compatibility), because referenced transactions and metadata cannot be removed, but being able to use arbitrary IPFS files that may not be available anymore would break this property, which seems a shame for on-chain NFTs.

1 Like

I do like the idea of defining the extra data the NFT will use up front, so that the NFT itself doesn’t need to use any apis and can remain more “standalone” then.

I’ve certainly had conversations with other on chain devs about the ability to reference data from other transactions on the blockchain, both for code reuse and changing NFT behaviour based on blockchain state, so I think this would definitely see use if it were implemented.

2 Likes

Yes, you make a very good point - I think the solution here is to require any imported file to be mentioned in the original NFT’s metadata files array - this also solves the problem of the suggested solution being ipfs-specifc too, as we’ll just support any file referenced in the files array no matter where it’s located.

I’ll add a requirement that files in the original mint transaction metadata must contain a unique ID - which can then be used to retrieve them within the sandbox, and I’ll replace the getIPFSFile() function with a more general getFile() function which just gets the file by the ID specified in the metadata.
This does limit you from adding any new large blobs, but I don’t think this is necessarily too limiting, you could still add SVGs on-chain if necessary.

Further down the road, we could add a mechanism to import new ipfs hashes, in a way that will show up in the transaction history and thus be preserve-able.

Aren’t files on IPFS supposed to be permanent? Not being available anymore (because no one holds that specific file in its cache anymore) can be said about regular NFT images too, no?

The possibility that nobody holds them anymore is quite the contrary of permanent.

Technologically, IPFS is not so much different from the file-sharing services of the early 2000s. Try today, how permanent they were.

If the data are on-chain, in a data: URI or in some newly defined structure, they will be there as long as the Cardano network is running and some node has the history.

3 Likes

Most NFT images aren’t stored that way… I was comparing the NFT images now stored on IPFS and the one that would be used by that function.

Yes, then you are right. They are both far from permanent. But then again, who knows how permanent Cardano itself will be?

1 Like

Now you’re becoming philosophical. :sweat_smile:

1 Like

As far as it relates to this standard, we just want a way to ensure all the ipfs hashes that are needed to render an NFT are “knowable”, so that if someone so wishes, they can ensure they stay live.
The problem with allowing arbitrary hashes is that you could add new hashes after the original mint in a non-standard way which was only known to the particular NFT in question - thus obscuring those hashes from someone wishing to preserve the NFT by pinning it. By forcing all hashes to be in the files array, you guarantee that they’re accessible to someone who wants to know them.

It is understood that there’s definitely a demand for fully-on-chain with no ipfs or other external storage - this is another reason to require that Smart NFTs specify which API functions they intend to use in the initial metadata - so that those which make absolutely no use of any off-chain data, can be indicated as such, as this is a desirable feature of an NFT for many.

I’ve had lots of great feedback on this initial draft here and on Twitter, so I’m going to do a significant revision this weekend in response to the feedback, then we’ll be one step closer to an an accepted and implemented standard :slight_smile:

3 Likes