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).