Creating and publishing metadata
Contract metadata provides context and descriptive information about smart contracts to wallets, explorers, dApps, and other off-chain applications.
In most cases, smart contracts store their metadata off-chain and keep only a link to the metadata in their storage. Therefore, on-chain applications including contracts cannot access metadata. To store data off-chain in a decentralized way, many Tezos developers use IPFS.
The primary Tezos standard for metadata is TZIP-016 (Tezos Metadata Standard). SmartPy includes tools to help you create standard-compliant metadata and store it in IPFS.
Creating metadata
- sp.create_tzip16_metadata(name: str, description: str, version: str, license_name: str, license_details: str, interfaces: List[str], authors: List[str], homepage: str, source_uri: str) → dict[str, any]
Return a TZIP-016 compliant metadata dictionary. All arguments are optional.
Parameters:
name
: The name of the contract, which should identify its purpose.description
: A description of the contract.version
: The version of the contract. The version together with the name should be unique on the chain. For example:1.0.0
.license_name
: License name. See Debian guidelines.license_details
: Optional license details.interfaces
: A list of the TZIP interfaces supported by the contract.TZIP-016
is automatically added to this list. For example:["TZIP-012"]
.authors
: A list of the authors of the contract. For example:["SmartPy <https://smartpy.io/>"]
.homepage
: The homepage is for human consumption. It can be the location of the decentralized app website, a git repository, a SmartPy IDE link, where to submit issue tickets, a more elaborate description, etc.source_uri
: The URI of a document giving all details to rebuild the contract from SmartPy source in the formatipfs://<hash>
. For example,ipfs://QmaV5gQ6p9ND9pjc1BPD3dc8oyi8CWEDdueSmkmasiaWGA
. See Uploading and linking to source code.offchain_views
: The compiled off-chain views of the contract.error_map
: A dictionary that maps error codes to error messages. See contracts#c.get_error_map.
For example, this code creates metadata for a contract:
pythonimport smartpy as sp @sp.module def main(): class MyContract(sp.Contract): @sp.entrypoint def fail(self): raise "An error occurred" @sp.add_test() def test scenario = sp.test_scenario("MyTest", main) scenario.add_flag("exceptions", "metadata-id") contract = main.MyContract() metadata_dict = sp.create_tzip16_metadata( name="MyContract", version="1.0.0", license_name="CC0", description="This is a demo contract using SmartPy.", authors=["SmartPy <https://smartpy.io/>"], homepage="https://smartpy.io/ide?template=contract_metadata.py", # Uncomment if you want to upload the SmartPy source code of the # contract to IPFS and specify them in the metadata dictionary. # source_uri=sp.pin_on_ipfs(c1.get_source(), name = "Source code for my contract"), offchain_views=contract.get_offchain_views(), error_map=contract.get_error_map(), )
The TZIP-16 standard is flexible, which means that you can add any non-standard field with information as needed. For example, you can add a calendar URL to the metadata_dict
object in the previous example with this code: metadata_dict["calendar"] = "<url>.ics"
You can also create and import a metadata JSON file on your own with this code:
import json
encoded_metadata = json.dumps(metadata_dict)
Uploading metadata
To be available to off-chain applications, you must convert the metadata dictionary to JSON and upload it to off-chain hosting.
SmartPy provides a utility to help you upload and pin the JSON data on IPFS through the Pinata service.
INFO
Pinning data on IPFS ensures that content remains accessible and is not garbage collected. As long as at least one node pins the content, it remains available to the network. Moreover, any party in possession of the data can re-upload it to IPFS. Because the data's hash remains consistent, the content becomes instantly accessible via the original URI.
To upload your metadata to IPFS and ensure its continued availability, you can use the sp.pin_on_ipfs
function. This function uses the Pinata service, a popular IPFS pinning provider, to store your data on the decentralized web.
To use this function, you must create an account on Pinata and create an API key with the "pinJSONToIPFS" permission, which allows it to pin JSON data.
- sp.pin_on_ipfs(data: dict[str, any], api_key: str = None, secret_key: str = None, name: str = None) → str
Pin a JSON file to IPFS through Pinata.
Parameters:
data
: The metadata to pin, such as the data returned bysp.create_tzip16_metadata(...)
or bycontract.get_source()
.api_key
: The API key of your Pinata API keypair. You can also put the key in thePINATA_KEY
environment variable.secret_key
: The secret key of your Pinata API keypair. You can also put the secret key in thePINATA_SECRET
environment variable.name
: The name to give the file in Pinata.
For example:
python# Pin the source code on IPFS and retrieve the URI source_uri = sp.pin_on_ipfs(contract.get_source()) # Create a TZIP-16 metadata object including the URI to the source code metadata_dict = sp.create_tzip16_metadata(..., source_uri=source_uri) # Pin the metadata to IPFS and get the URI metadata_uri = sp.pin_on_ipfs(metadata_dict, name = "My contract metadata")
Now when you compile the contract, SmartPy pins the metadata to IPFS automatically.
TIP
You can set environment variables such as
PINATA_KEY
andPINATA_SECRET
in your operating system or within your development environment.
Linking to metadata
Contracts store a link to their metadata in a big map of type sp.big_map[sp.string, sp.bytes]
. This big map is stored in a variable named metadata
in the contract storage. This big map always contains the empty key ""
and its value is an encoded URI that points to a JSON document (the one created with sp.create_tzip16_metadata
).
While you can provide additional metadata directly in the contract's storage, the JSON document should be the main place to store metadata.
- sp.scenario_utils.metadata_of_url(url: str) → sp.big_map[sp.string, sp.bytes]
Create a big map with an empty key with the encoded URL for its value.
For example:
pythonmetadata_big_map = sp.scenario_utils.metadata_of_url( "ipfs://QmaV5gQ6p9ND9pjc1BPD3dc8oyi8CWEDdueSmkmasiaWGA") # Equivalent to metadata_big_map = sp.big_map({ "": sp.bytes("0x" + s.encode("utf-8").hex()) })
Then you can use this big map as the value of the
metadata
field in the contract storage.
Uploading and linking to source code
If you want to expose the source code of your contract, you can retrieve it with contract.get_source()
and pin it to IPFS as described in Uploading metadata. You can then include the resulting URI (starting with ipfs://
) in the contract metadata.
Here is an example of SmartPy source code uploaded to IPFS. It contains the following elements:
module_
: The module from which the contract was builtname
: The class nameparam
: The parameters given to the constructor after evaluationmodules
: All the modules imported in the scenarioflags
: The flags given to the compilation
Verifying source code
Anyone can verify that the source code indicated in the metadata corresponds to the Michelson code uploaded on the Tezos blockchain.
- sp.get_metadata(address: str, network = "mainnet") → dict
Fetches and returns the metadata dictionary for a given contract address and network from an explorer API like TZKT.
- sp.get_metadata_code(metadata: dict) → dict
Fetches and returns the code metadata for a contract's metadata through an IPFS endpoint, for example ipfs.io.
- sp.get_code(address: str, network: "mainnet") → str
Fetches and returns the Michelson code of a contract from an explorer API like TZKT.
- sp.check_validity(metadata: dict, code_metadata: dict, onchain_michelson: str) →
Checks that the code given in the metadata generates the same on-chain code. Raises
CodeMismatchException
if the codes do not match.INFO
The metadata dictionary is used to ensure that the version of SmartPy you are using is compatible with the version specified in the metadata. If you need to verify metadata for a contract created with a different version of SmartPy, you should run this function using that specific version of SmartPy.
For example, this code verifies the code of a SmartPy contract that was compiled with SmartPy version 0.19.0a2:
pythonimport smartpy as sp address = "KT1EQLe6AbouoX9RhFYHKeYQUAGAdGyFJXoi" metadata = sp.get_metadata(address, network="ghostnet") code_metadata = sp.get_metadata_code(metadata) onchain_michelson = sp.get_michelson_code(address, network="ghostnet") try: sp.check_validity(metadata, code_metadata, onchain_michelson) print("Metadata code is valid") except sp.CodeMismatchException as e: print(e) print("Generated michelson:\n", e.generated_michelson) print("Details of the first difference:", e.diff_details)