Minting an NFT on Cardano with Daedalus and MacOS
Today we are going to mint a NFT on Cardano using Daedalus and a Mac
[UPDATED] With new input from El Gato Loco - 4-22-2021
One of the things I was unable to get working previously was the token locking via the policy. After chatting with El Gato Loco on the Dev Discord, he gave me a great way to accomplish that piece I was struggling with.
Thank you El Gato Loco for dropping the missing knowledge on me!!
With the new knowledge I created a new NFT called D4T401
Let's Start
Things needed:
- Daedalus [Testnet version]
- BlockFrost.io account [free version for now]
- Art files [jpg, png, something]
- Testnet ADA
Here are the steps we will be taking today:
- (1) Create Art
- (2) Upload to IPFS
- (3) Mint the Token and attach meta data
- (4) Send Token to an address in Daedalus Testnet app
Previous Post for minting tokens:
Please go through this post to gain more knowledge on minting native tokens:
https://deafmice.com/minting-native-tokens-on-cardano-testnet/
Step 0 - Setup some environmental variables
Mainly for the testnet we need a Cardano node provided by the Daedalus client.
So we will use the following:
# By default Daedalus installs in the users Library
# We need a var for the socket in order to talk to the chain nicely
export CARDANO_NODE_SOCKET_PATH=~/Library/Application\ Support/Daedalus\ Testnet/cardano-node.socket
# We want to make sure we are on the testnet
export TESTNET_ID=1097911063
# We create a nice alias so that we can be lazy in our typing
alias cardano-cli="/Applications/Daedalus\ Testnet.app/Contents/MacOS/cardano-cli"
Note: If you had a Linux box you'd just need to find the cardano-node.socket
and cardano-cli
in your system and update the vars accordingly.
Note: I was able to get the testnet ID by looking at the config's published by IOHK:
https://hydra.iohk.io/build/5822084/download/1/index.html
Click the button by "shelleyGenesis" and search for "networkMagic" near the bottom of the JSON.
# As of this writing the JSON returned the following and that is what we use under TESTNET_ID
"networkMagic": 1097911063,
++++++++++++++++++++++++++++++++++++++++++++++
Step 1 - Create Art
Time to get creative.
For this example I will be using the very cool D4T4 Logo, but I will add some hipster glitching with PhotoMosh as it seems like the NFT community likes that sort of thing.
OK, we have an image, let's make it permanent on the web with IPFS.
++++++++++++++++++++++++++++++++++++++++++++++
Step 2 - Upload to IPFS
The InterPlanetary File System (IPFS) is a protocol and peer-to-peer network for storing and sharing data in a distributed file system. IPFS uses content-addressing to uniquely identify each file in a global namespace connecting all computing devices.[4]
https://en.wikipedia.org/wiki/InterPlanetary_File_System
Instead of setting up a node and pinning images from my home machine I will use a service provided by https://blockfrost.io/.
First things first, create an account and get your Project ID.
export PROJECT_ID=YOUR_FANCY_PROJECT_ID
Upload the image to IPFS
Upload the glitched.gif
image to IPFS via Blockfrost.io with curl.
curl "https://ipfs.blockfrost.io/api/v0/ipfs/add" \
-X POST \
-H "project_id: $PROJECT_ID" \
-F "file=@./glitched.gif"
This will take a few seconds to upload depending on internet speed and then return something like the following:
{"name":"glitched.gif","ipfs_hash":"QmV6m1SyHjc4Km16Cyrf2EgbYehDtAg9fovcfR3NEL3Mc1","size":"3064368"}%
The ipfs_hash
is how you will access the image in the future and that hash is what we will put in the NFT.
Let's make a environmental variable of it.
export IPFS_HASH=QmV6m1SyHjc4Km16Cyrf2EgbYehDtAg9fovcfR3NEL3Mc1
Pin it!
Pinning makes sure that the image is available on IPFS across the planet, and mainly keeps it from being garbage collected and sent to the nether regions of nowhere Ohio.
curl "https://ipfs.blockfrost.io/api/v0/ipfs/pin/add/$IPFS_HASH" \
-X POST \
-H "project_id: $PROJECT_ID"
This will return that the image is queued to be pinned
{"ipfs_hash":"QmV6m1SyHjc4Km16Cyrf2EgbYehDtAg9fovcfR3NEL3Mc1","state":"queued"}%
Extra Credit Knowledge Dump
List your pins
We can list out our pins to see what is being hosted for us by Blockfrost
curl "https://ipfs.blockfrost.io/api/v0/ipfs/pin/list/" \
-H "project_id: $PROJECT_ID"
returns
[{
"time_created":1627072580159,
"time_pinned":1627072615269,
"ipfs_hash":"QmV6m1SyHjc4Km16Cyrf2EgbYehDtAg9fovcfR3NEL3Mc1",
"size":"44948",
"state":"queued"
}]
Download your image
If you would like to grab your image, Blockfrost hosts a IPFS gateway.
This will download a file called ipfs_hosted_gif.gif
to what ever directory you are currently in:
curl "https://ipfs.blockfrost.io/api/v0/ipfs/gateway/$IPFS_HASH" \
-H "project_id: $PROJECT_ID" \
-o "ipfs_hosted_gif.gif"
Remove pin
Finally, if you uploaded the wrong image or something you can unpin it and it will eventually go away via garbage collection.
curl "https://ipfs.blockfrost.io/api/v0/ipfs/pin/remove/$IPFS_HASH" \
-X POST \
-H "project_id: $PROJECT_ID"
returns the following
{"ipfs_hash":"QmV6m1SyHjc4Km16Cyrf2EgbYehDtAg9fovcfR3NEL3Mc1","state":"unpinned"}%
Note: I did not remove the pin as I'd like to keep using it for this post
Side Note: If someone else has pinned that image it will only go away when they stop and it is garbage collected.
Nifty IPFS things:
You can find other public IPFS gateways here
https://ipfs.github.io/public-gateway-checker/
The NFT image is now accessible from different IPFS gateways:
Cloudflare:
https://cloudflare-ipfs.com/ipfs/QmV6m1SyHjc4Km16Cyrf2EgbYehDtAg9fovcfR3NEL3Mc1
IPFS.io:
https://gateway.ipfs.io/ipfs/QmV6m1SyHjc4Km16Cyrf2EgbYehDtAg9fovcfR3NEL3Mc1
++++++++++++++++++++++++++++++++++++++++++++++
Step 3 - Mint the Token WITH metadata and LOCKED policy
Make sure that Daedelus Testnet app is up and running
We need a name for the token
This will be in the metadata and in the transaction.
I took D4T401
as this is the first D4T4 token. I'm sure at some point there will be better rules for token names, but for now we will got with it for example sake.
export TOKEN_NAME=D4T401
Create directory for our policy files
If you did not go through the previous post you will be missing the policy directory.
Create that now.
mkdir policy
Generate minting policy signing and verification keys
We will need a set of policy keys to generate our policy ID
cardano-cli address key-gen --verification-key-file policy/nft_policy.vkey --signing-key-file policy/nft_policy.skey
Create an empty policy script file
touch policy/nft_policy.script
Create a nice policy for your NFT
We will need a keyhash from our policy keys in order to mint tokens.
cardano-cli address key-hash --payment-verification-key-file policy/nft_policy.vkey
That should return something like this, we will copy that into our policy script below so don't lose it
6c38046f3347b2a8ce4488a9ae117a8cadbf0ee1da200f6d27349190
Get the current slot number on the testnet
cardano-cli query tip --testnet-magic $TESTNET_ID
That should return something like this:
{
"hash": "67e1abf7e24457b8cedde729a4571ced8e707a850e98bc2967c407ac4724ce96",
"block": 2776963,
"slot": 32711016,
"era": "Mary",
"epoch": 146
}
We were at slot number 32711016 while I was researching, so I made my token locking at slot number 32720000
Copy our keyhash and slot number we would like the policy to lock on
This means that the payment keys we created previously can mint tokens in this policy, and it will lock on slot 32720000 but you can mint any number of tokens before that slot
{
"type": "all",
"scripts": [
{
"keyHash": "6c38046f3347b2a8ce4488a9ae117a8cadbf0ee1da200f6d27349190",
"type": "sig"
},
{
"type": "before",
"slot": 32720000
}
]
}
Copy that JSON to policy/nft_policy.script
and save it.
Note: While researching and writing this post I kept having to reset the slot number and regenerating a policyID
, so if this is your first NFT, choose a higher slot number a few thousand slots in the future to save headaches later on. If your slot is past, the token will not mint and you will get weird errors.
Side Note to self: Figure out a way to automate this section nicely.
Generate the policy ID for the NFT
Next we generate our policy ID, this will be one way we could verify our tokens on chain and in the wallet.
cardano-cli transaction policyid --script-file ./policy/nft_policy.script >> policy/nft_policyId
We will need the policy ID later on so make it a variable
cat policy/nft_policyId
Should return something like
d49bbd329d54039f6498a6fb5b2701946724fd1db13278fa9653ab96
We can add that to a env var for later
export POLICY_ID=d49bbd329d54039f6498a6fb5b2701946724fd1db13278fa9653ab96
Create nft_metadata.json
Each NFT needs some metadata attached, while a standard has not been fully developed as of this writing, we can mirror what others are doing and get the idea of what it will be like in the future.
I will be following this "standard" as it makes the most sense and that's how SpaceBudz.io does it.
{
"721": {
policyID: {
tokenNumber00: {
arweaveId: "ARWEAVE_HASH",
image: "ipfs://IPFS_HASH",
name: "tokenName #00",
traits: [
"trait01",
"trait02",
"etc"
],
type: "TypeOfPokemon [example]"
}
}
}
}
Create the NFT metadata file called policy/nft_metadata.json
I added a "glitched" as a type and a "glitched" trait to the metadata for fun. I also did not setup ARweave, so that line was removed.
The metadata should look something like this once you put your policyID, tokenNumber00, IPFS_HASH and other traits in.
Note: You can add whatever else you like.
{
"721": {
"d49bbd329d54039f6498a6fb5b2701946724fd1db13278fa9653ab96": {
"0": {
"image": "ipfs://QmV6m1SyHjc4Km16Cyrf2EgbYehDtAg9fovcfR3NEL3Mc1",
"name": "D4T401",
"traits": ["glitched", "Logo"],
"type": "glitched"
}
}
}
}
Finish the transaction
Next we build our raw transaction, figure out the fee, rebuild with the fee, sign it and send it to the testnet.
Build the raw transaction
We need to use up the previous transaction in the UTXO model, so let's find our transactions and setup our minting transaction.
Check the address balance and tx_hash
We created our payment address and found the testnet ID in the last post. Hopefully you still have that info.
cardano-cli query utxo --address $(< payment.addr) --testnet-magic $TESTNET_ID
Run the check and see your test ADA denominated in Lovelace [You may need to send yourself some ADA]
TxHash TxIx Amount
--------------------------------------------------------------------------------------
1d8886e617d9c70471334fbb1d5d40d0c2dd07d425f2bca325ebdbbe031ea8a3 0 20000000 lovelace
For the raw transaction to be built use the following, but with your own data:
- --tx-in == the TxHash and TxIx from the UTXO query
- --tx-out == the payment address + ADA in Lovelace + number of tokens (1) the policy ID . Token Name
- --mint == number of tokens (1) the policy ID . Token Name
export TX_HASH=1d8886e617d9c70471334fbb1d5d40d0c2dd07d425f2bca325ebdbbe031ea8a3
export TX_IX=0
export AVAILABLE_LOVELACE=20000000
export TOKEN_AMOUNT=1
Example:
cardano-cli transaction build-raw \
--fee 0 \
--tx-in $TX_HASH#$TX_IX \
--tx-out $(< payment.addr)+20000000+"1 $(< policy/nft_policyId).D4T401" \
--mint="$TOKEN_AMOUNT $(< policy/nft_policyId).D4T401" \
--metadata-json-file policy/nft_metadata.json \
--minting-script-file policy/nft_policy.script \
--invalid-hereafter=32720000 \
--out-file matx.raw
Calculate out the minimum fee
cardano-cli transaction calculate-min-fee \
--tx-body-file matx.raw \
--tx-in-count 1 \
--tx-out-count 1 \
--witness-count 1 \
--testnet-magic $TESTNET_ID \
--protocol-params-file protocol.json
The terminal should return something like the following:
186401 Lovelace
Use the calculated fee to build the transaction with proper fee amount
export TX_FEE=186401
Build the real transaction with proper fee
cardano-cli transaction build-raw \
--fee $TX_FEE \
--tx-in $TX_HASH#$TX_IX \
--tx-out $(< payment.addr)+$(($AVAILABLE_LOVELACE - $TX_FEE))+"$TOKEN_AMOUNT $(< policy/nft_policyId).$TOKEN_NAME" \
--mint="$TOKEN_AMOUNT $(< policy/nft_policyId).$TOKEN_NAME" \
--metadata-json-file policy/nft_metadata.json \
--minting-script-file policy/nft_policy.script \
--invalid-hereafter=32720000 \
--out-file matx.raw
Sign the transaction
cardano-cli transaction sign \
--signing-key-file payment.skey \
--signing-key-file policy/nft_policy.skey \
--testnet-magic $TESTNET_ID \
--tx-body-file matx.raw \
--out-file matx.signed
Send it to the testnet
cardano-cli transaction submit --tx-file matx.signed --testnet-magic $TESTNET_ID
ERROR
I was getting an error when I sent the transaction to the testnet
cardano-cli transaction submit --tx-file matx.signed --testnet-magic $TESTNET_ID
Shelley command failed: transaction submit Error: Error while submitting tx: ApplyTxError [LedgerFailure (UtxowFailure (UtxoFailure (FeeTooSmallUTxO (Coin 184025) (Coin 183937))))]
So what I ended up doing to fix this is to just use the (Coin 184025)
as my TX_FEE and resent the transaction
That worked
END ERROR
++++++++++++++++++++++++++++++++++++++++++++++
Check the address balance again for tokens
After a few seconds we check out address again and find a newly minted token.
WOOT!
cardano-cli query utxo --address $(< payment.addr) --testnet-magic $TESTNET_ID
You should see something like the following:
TxHash TxIx Amount
--------------------------------------------------------------------------------------
86092090ff549c8b5c20e84b0d013d1de8b523c6b86613dbe00b44ba40a64d27 0 19813599 lovelace + 1 d49bbd329d54039f6498a6fb5b2701946724fd1db13278fa9653ab96.D4T401
If we go to the testnet explorer we can see the metadata in all its glory
++++++++++++++++++++++++++++++++++++++++++++++
Step 4 - Send Token to an address in Daedalus Testnet app
Go to Daedalus and grab an address for our wallet to send to
Save that address as a new environment variable
export recipient=addr_test1qrz00k8wknswf6p78yha2lyjadkzhzey9lv7dth3rmvf6xhzjghmhfccw0c78244lrnqcdnngncmrmvww6ancg7uvjlsk8aqz0
Update our TX variables
You will notice that there is a new TX_HASH and a lower amount of ADA, so lets update our variables accordingly.
export TX_HASH=86092090ff549c8b5c20e84b0d013d1de8b523c6b86613dbe00b44ba40a64d27
export TX_IX=0
export AVAILABLE_LOVELACE=19813599
Reset the TX fee back to 0
export TX_FEE=0
Copy address from Daelelus wallet into recipientpay.addr
For convenience in scripting
echo $recipient > recipientpay.addr
Build out the raw transaction
You can build a more complicated transaction that sends some ADA back to a change address, but I wanted to keep this simple.
This will drain the payment address of both ADA and BML1 tokens and send them to the recipient
cardano-cli transaction build-raw \
--fee 0 \
--tx-in $TX_HASH#$TX_IX \
--tx-out "$(< recipientpay.addr)+$(($AVAILABLE_LOVELACE - $TX_FEE))+1 $(< policy/nft_policyId).$TOKEN_NAME" \
--json-metadata-no-schema \
--metadata-json-file policy/nft_metadata.json \
--out-file recipient_matx.raw
Calculate the min TX fee
cardano-cli transaction calculate-min-fee \
--tx-body-file recipient_matx.raw \
--tx-in-count 1 \
--tx-out-count 1 \
--witness-count 1 \
--testnet-magic $TESTNET_ID \
--protocol-params-file protocol.json
Hopefully we did it correctly and the terminal returns the needed fee:
182397 Lovelace
Save your fee as an environment variable
Note: Replace it with value returned by the previous command
export TX_FEE=182397
Build out the raw transaction again with the correct fee this time
cardano-cli transaction build-raw \
--fee $TX_FEE \
--tx-in $TX_HASH#$TX_IX \
--tx-out "$(< recipientpay.addr)+$(($AVAILABLE_LOVELACE - $TX_FEE))+1 $(< policy/nft_policyId).$TOKEN_NAME" \
--json-metadata-no-schema \
--metadata-json-file policy/nft_metadata.json \
--out-file recipient_matx.raw
Sign the transaction using the keys generated earlier.
cardano-cli transaction sign \
--signing-key-file payment.skey \
--signing-key-file policy/nft_policy.skey \
--testnet-magic $TESTNET_ID \
--tx-body-file recipient_matx.raw \
--out-file recipient_matx.signed
Submit the transaction to the chain.
cardano-cli transaction submit --tx-file recipient_matx.signed --testnet-magic $TESTNET_ID
After a few moments you will now have your ADA returned to the Daedalus wallet and a new token with metadata.
You can view my transaction on the testnet explorer here:
Note: In order to have an asset name, ticker, etc, I believe you need to register it, but I have not gone through that process yet.
Special thanks again to El Gato Loco on the Dev discord for helping me on this post.
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
BURN A TOKEN DOWN
Let's say you messed up somewhere along the way and need to burn the token you just created. If you sent that token off to someone, they will need to send it back. Only the the payment keys we setup are the only ones that can mint and burn according to the original minting policy.
If you setup token locking and it is after the slot in the policy, you will not be able to do anything even as the policy owner, so keep that in mind as you are working with native tokens.
For actual burning, you can follow this link from Cardano, but I was not able to get it working. I kept getting weird errors in the terminal and gave up.
Since we are on the testnet, I just sent the NFT back to the payment.addr
and then deleted the files. In my testing, this was the fastest method ;)
When sending back you have to attach 1.444443 ADA as that is the minimum requirement for now.
Keep that in mind on mainnet, as creating NFT's cost something.
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!