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.

https://photomosh.com/

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

https://github.com/ipfs/ipfs

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

Transaction | Cardano Explorer

++++++++++++++++++++++++++++++++++++++++++++++

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:

Transaction | Cardano Explorer

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.

Minting NFTs | Cardano Developer Portal
How to mint NFTs on Cardano.

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.

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!