How to mint an NFT on Cardano Testnet using the command line

Christopher Huxley   ยท   December 19, 2021

Why do this?

You have a digital asset that you predict others will want to buy. It can be either unique where each asset has its own digital serial number (meaning it's non-fungible and can be represented by an NFT), or an asset where each of them have equal value (similar to physical coins like nickles/dimes/quarters) and can be represented by a Native Asset.

Alternatives to this method

This article shows how to create an NFT using the command line. It's the method that gives you complete (low level) control over every little detail from start to finish. This is good if you need that level of control, or if you're trying to learn how everything fits together from scratch so you can understand and appreciate the technology. But it involves downloading a copy of the full blockchain and keeping it up-to-date.

An alternative is to use NamiWallet which can be done from your web browser. But at the moment, it's very limited in features. You can only specify the name, quantity, author, and upload a photo. You don't have control over the meta data.

Create a Cardano Node

Method 1: Daedalus Wallet

If you're using an operating system that has a full desktop environment like Windows or Mac (as opposed to one that's solely commandline), you can install the Daedalus Wallet for Cardano Mainnet and/or Cardano Testnet. It will download the entire blockchain.

The Daedalus Wallet comes with cardano-cli and cardano-wallet which we'll be using throughout this guide. You'll need to create an environment variable for this to work.

Method 2: Cardano Node

If you're using an operating system that only has a commandline/shell like Ubuntu Server, see this guide: How to create a Cardano Testnet node in Linux

In my case, I use Daedalus Wallet on Windows for the mainnet, and Ubuntu Server 20.04 LTS inside a virtual machine for the testnet. This keeps my testing environment isolated from my production (Mainnet) environment, hopefully preventing accidents.

Start the Cardano Node and Wallet

Method 1: Daedalus Wallet

Open Daedalus Wallet, wait for it to 100% synchronize with the blockchain, and keep it running. As a reminder, you'll need to create an environment variable for this to work.

Open a terminal/shell or Command Prompt and navigate to the Daedalus Wallet folder.

Method 2: Cardano Node

NOTE: If you just finished the How to create a Cardano Testnet node in Linux guide and already have the Cardano Node and Cardano Wallet running, you can skip this step.

Open three shells. This could be either three Command Prompts (if using Windows) or three terminal windows/shells (if using Linux or Mac).

In the first shell, start the Cardano Node. This is what keeps your local copy of the Testnet blockchain up-to-date.

cardano-node run \
--topology ~/cardano/configuration/testnet/testnet-topology.json \
--database-path ~/cardano/testnet-db \
--socket-path ~/cardano/testnet-db/node.socket \
--host-addr 127.0.0.1 \
--port 3001 \
--config ~/cardano/configuration/testnet/testnet-config.json

In the second shell, start the Cardano Wallet server. This is what lets you interact with the node.

cardano-wallet serve \
--port 8090 \
--database ~/cardano/testnet-db \
--node-socket $CARDANO_NODE_SOCKET_PATH \
--testnet ~/cardano/configuration/testnet/testnet-byron-genesis.json

You'll use the third shell for everything else point forward. Keep the above two shells open.

Create a folder to hold everything

We're going to store our keys and metadata in a new folder to keep things organized. Our NFT will be named TestNFT1234.

mkdir -p ~/cardano/nft/TestNFT1234/policy
cd ~/cardano/nft/TestNFT1234

Create the digital asset the NFT represents

At some point in the future, NFTs will be more than just a photo of something. For now, this is typically an image. For example: https://ipfs.io/ipfs/QmRhTTbUrPYEw3mJGGhQqQST9k86v1DPBiTTWJGKDJsVFw

Put your NFT onto IPFS

Put your NFT resource (the image or whatever it is) onto IPFS. See this guide: Types of IPFS links and when to use them and How to Use IPFS: The Backbone of Web3.

In this example, we're using the image already uploaded by the person who wrote the official Cardano NFT guide: ipfs://QmRhTTbUrPYEw3mJGGhQqQST9k86v1DPBiTTWJGKDJsVFw (view image on ipfs.io.

Generate the payment keys

Run this. Never share the contents of these two files with anyone.

cardano-cli address key-gen \
--verification-key-file payment.vkey \
--signing-key-file payment.skey

Generate an address of the payment keys

Run this to generate the address and write it to a file called payment.addr. In this example, my payment address ends up being addr_test1vzcwn6u3fephqacec4g6fr9gpsp9znpr5hsk9ggcfarn23sthac04.

cardano-cli address build \
--payment-verification-key-file payment.vkey \
--out-file payment.addr \
--testnet-magic 1097911063

To make things easy point-forward, we'll store the address in a variable. When we run commands later, we can say $payment_address instead of typing/pasting the entire address.

payment_address=$(cat payment.addr)

When you see --testnet-magic 1097911063, it means we're intereacting with the Testnet rather than the Mainnet (which uses --mainnet without any numbers). Why is it 1097911063? It's the network identifier of the Testnet.

Fund the address

You should have at least 2 ADA in your payment address above to mint a token. If you were on the Mainnet, you'd transfer some funds to that address from your wallet or an exchange.

Since we're on the Testnet, we can use the faucet to fund this account. Request test tokens using the ADA faucet to immediately get 1,000 tAda (test ADA). Send it to the payment address you just created.

See how much lovelace is in our account (1,000,000 lovelace = 1 ADA).

cardano-cli query utxo \
--address $payment_address \
--testnet-magic 1097911063

Here's what it might look like:

TxHash                                                            TxIx        Amount
--------------------------------------------------------------------------------------
0ba233973412b629d8d1bcf6cf219a83c7cf16b59f84c1f2c488d80c5d0404dd     0        1000000000 lovelace + TxOutDatumNone

Export protocol parameters

We need current protocol parameters for when we calculate transactions.

cardano-cli query protocol-parameters \
--testnet-magic 1097911063 \
--out-file protocol.json

This generates a 10KB file called protocol.json that contains information like this:

{
    "txFeePerByte": 44,
    "minUTxOValue": null,
    "decentralization": 0,
    "utxoCostPerWord": 34482,
    "stakePoolDeposit": 500000000,
    "poolRetireMaxEpoch": 18,
    "extraPraosEntropy": null,
    "collateralPercentage": 150,
    "stakePoolTargetNum": 500,
    "maxBlockBodySize": 73728,
    "minPoolCost": 340000000,
    "maxTxSize": 16384,
    "treasuryCut": 0.2,
    "maxBlockExecutionUnits": {
        "memory": 50000000,
        "steps": 40000000000
    },
    "maxCollateralInputs": 3,
    "maxValueSize": 5000,
    "maxBlockHeaderSize": 1100,
    "maxTxExecutionUnits": {
        "memory": 12500000,
        "steps": 10000000000
    },
    "costModels": {
        "PlutusScriptV1": {
            "cekConstCost-exBudgetMemory": 100,
            "unBData-cpu-arguments": 150000,
            "divideInteger-memory-arguments-minimum": 1,
            "nullList-cpu-arguments": 150000,
            "cekDelayCost-exBudgetMemory": 100,
            ...
        }
    },
    "protocolVersion": {
        "minor": 0,
        "major": 6
    },
    "txFeeFixed": 155381,
    "stakeAddressDeposit": 2000000,
    "monetaryExpansion": 3.0e-3,
    "poolPledgeInfluence": 0.3,
    "executionUnitPrices": {
        "priceSteps": 7.21e-5,
        "priceMemory": 5.77e-2
    }
}

Generate the policy keys

Run this. Never share the contents of these two files with anyone.

cardano-cli address key-gen \
--verification-key-file policy/policy.vkey \
--signing-key-file policy/policy.skey

Write the Policy Script

Run the following to generate the policy.script file.

echo "{" >> policy/policy.script
echo "  \"type\": \"all\"," >> policy/policy.script
echo "  \"scripts\":" >> policy/policy.script
echo "  [" >> policy/policy.script
echo "   {" >> policy/policy.script
echo "     \"type\": \"sig\"," >> policy/policy.script
echo "     \"keyHash\": \"$(cardano-cli address key-hash --payment-verification-key-file policy/policy.vkey)\"" >> policy/policy.script
echo "   }" >> policy/policy.script
echo "  ]" >> policy/policy.script
echo "}" >> policy/policy.script

It will look something like this:

{
  "type": "all",
  "scripts":
  [
   {
     "type": "sig",
     "keyHash": "6d1321490cde0b447794edff9239cf0668ae01f0df33823d0a3bbcf7"
   }
  ]
}

Store the script filename in a variable.

script="policy/policy.script"

Generate the PolicyID

Run this to generate the unique PolicyID hash using the policy script.

cardano-cli transaction policyid \
--script-file ./policy/policy.script >> policy/policyID

In this example, the Policy ID is 155f313f864d009d7b0b519b1295c9fab7b75c17cfc5ba1aed501b7c.

Store the PolicyID in a variable.

policyid=$(cat policy/policyID)

Encode the NFT's name in base-16

Encode your NFT's name using base-16 (hexadecimal). You can use this online tool to do that, or use the following command in Linux.

echo -n "TestNFT1234" | xxd -ps

Store this in a variable. We're only creating one NFT in this example so the Token Amount variable is set accordingly.

tokenname="546573744e465431323334"
tokenamount=1

Create the metadata

Create the metadata for your NFT. See https://github.com/cardano-foundation/CIPs/tree/master/CIP-0025 for the latest structure.

Save it as metadata.json.

nano metadata.json

In the following example:

  • 155f313f864d009d7b0b519b1295c9fab7b75c17cfc5ba1aed501b7c is the PolicyID
  • 546573744e465431323334 is the asset name (TestNFT1234) encoded in base-16 (hexadecimal)
  • 721 is the key the community is using to signify this is an NFT
{
    "721": {
        "155f313f864d009d7b0b519b1295c9fab7b75c17cfc5ba1aed501b7c": {
            "546573744e465431323334": {
                "name": "TestNFT1234",
                "image": [ "https://ipfs.io/ipfs/", "QmRhTTbUrPYEw3mJGGhQqQST9k86v1DPBiTTWJGKDJsVFw" ],
                "mediaType": "image/png",
                "description": "Just another fun NFT"
            }
        },
    "version": "1.0"
    }
}

If we didn't split the "image" attribute into an array, we would've gotten this error:

Command failed: transaction build Error: Error reading metadata at: "metadata.json" Value out of range within the metadata item 721: [contents of metadata.json here] Text string metadata value must consist of at most 64 UTF8 bytes, but it consists of 67 bytes.

Gather information needed to build the transaction

Run this.

cardano-cli query utxo \
--address $payment_address \
--testnet-magic 1097911063

Store the value of TxHash and TxIx into variables.

txhash="0ba233973412b629d8d1bcf6cf219a83c7cf16b59f84c1f2c488d80c5d0404dd"
txix="0"

Get recipient's address

Determine who will be the first recipient of this NFT. It might be your own personal wallet. Whatever the case, get the wallet address for that person.

first_nft_owner_address="addr_test1qqwtrpkkh8a60pjjljn6n90mzdf6fa9wempj8s6t732xh4deudejw0dvq2hyrty7htt02xepkuq65w0peun9wx5kxpuskcgwl9"

Estimate the fee and slot target

We don't yet know how much Lovelace this will cost. Start with 1400000 lovelace (1.4 ADA), which is the minimum UTxO requirement. If it ends up costing more because the metadata is so large, we'll be forced to adjust it later.

fee=1400000

Make sure all variables are set

Run the following and look at the output. Make sure everything has a value.

echo [payment_address] NFT will be generated by payment address \"$payment_address\"
echo [first_nft_owner_address] NFT will be sent to address \"$first_nft_owner_address\"
echo [fee] This is estimated to cost \"$fee\" lovelace
echo [policyid] The PolicyID is \"$policyid\"
echo [tokenname] The token name in hexadecimal is \"$tokenname\"
echo [tokenamount] We are going to mint \"$tokenamount\" tokens
echo [script] The policy script is \"$script\"
echo [txhash][txix] This transaction will be funded by TxHash \"$txhash\" using TxIx \"$txix\"

In this example, here's the output (with the commands removed to make it easier to read):

[payment_address] NFT will be generated by payment address "addr_test1vzcwn6u3fephqacec4g6fr9gpsp9znpr5hsk9ggcfarn23sthac04"
[first_nft_owner_address] NFT will be sent to address "addr_test1qqwtrpkkh8a60pjjljn6n90mzdf6fa9wempj8s6t732xh4deudejw0dvq2hyrty7htt02xepkuq65w0peun9wx5kxpuskcgwl9"
[fee] This is estimated to cost "1400000" lovelace
[policyid] The PolicyID is "155f313f864d009d7b0b519b1295c9fab7b75c17cfc5ba1aed501b7c"
[tokenname] The token name in hexadecimal is "546573744e465431323334"
[tokenamount] We are going to mint "1" tokens
[script] The policy script is "policy/policy.script"
[txhash][txix] This transaction will be funded by TxHash "0ba233973412b629d8d1bcf6cf219a83c7cf16b59f84c1f2c488d80c5d0404dd" using TxIx "0"

Build the transaction

We're now going to build the transaction and store it inside a file called matx.raw.

cardano-cli transaction build \
--testnet-magic 1097911063 \
--alonzo-era \
--tx-in $txhash#$txix \
--tx-out $first_nft_owner_address+$fee+"$tokenamount $policyid.$tokenname" \
--change-address $payment_address \
--mint="$tokenamount $policyid.$tokenname" \
--minting-script-file $script --metadata-json-file metadata.json \
--witness-override 2
--out-file matx.raw

IMPORTANT: If you set the --change-address value to $first_nft_owner_address, your entire payment wallet's ADA balance will be sent to that address. If you own it, it's not a problem.

For example, assume $first_nft_owner_address is your main wallet and you transferred 10 ADA from $first_nft_owner_address to $payment_address for the sake of minting an NFT. Minting the NFT might actually cost 1.590889 ADA. If you want to return the remaining 8.6 ADA to your main wallet, set --change-address to $first_nft_owner_address. But if you want to mint another NFT and keep the 8.6 ADA in that temporary wallet, set it to $payment_address.

If you get a message saying something like "Minimum required UTxO: Lovelace 1448244", change the fee to that amount. Otherwise, continue without making any changes. In this example, it says Estimated transaction fee: Lovelace 190889 so we're all good.

fee=1448244 # Run this only if you got the above warning

If you get a message saying that you don't have enough funds to cover the cost of the transaction, you can specify a second --tx-in entry. For example, when we run cardano-cli query utxo --address $address --testnet-magic 1097911063, it shows the following. By specifying a second --tx-in entry, I can combine the funds.

TxHash                                 TxIx        Amount
--------------------------------------------------------------------------------------
0ba233973412b629d8d1bcf6cf219a83c7cf16b59f84c1f2c488d80c5d0404dd     0        50000000 lovelace + TxOutDatumNone
77300dc0598bdc8268a706705ee7521f76bf29cdd9d0c7580b69c660f0f38bec     0        48220906 lovelace + TxOutDatumNone

For those curious, the file looks like this:

{
    "type": "TxBodyAlonzo",
    "description": "",
    "cborHex": "86a700818258200ba233973412b629d8d1bcf6cf219a83c7cf16b59f84c1f
    2c488d80c5d0404dd000d800182825839001cb186d6b9fba78652fca7a995fb1353a4f4ae
    cec323c34bf4546bd5b9e373273dac02ae41ac9ebad6f51b21b701aa39e1cf26571a96307
    91a3b828397825839001cb186d6b9fba78652fca7a995fb1353a4f4aecec323c34bf4546b
    d5b9e373273dac02ae41ac9ebad6f51b21b701aa39e1cf26571a963079821a00155cc0a15
    81c155f313f864d009d7b0b519b1295c9fab7b75c17cfc5ba1aed501b7ca14b546573744e
    46543132333401021a0002e9a90e8009a1581c155f313f864d009d7b0b519b1295c9fab7b
    75c17cfc5ba1aed501b7ca14b546573744e465431323334010758206a46c80316d784a2b4
    936540adca437283d949a52878616b9494eb60279878779f82008201818200581c6d13214
    90cde0b447794edff9239cf0668ae01f0df33823d0a3bbcf7ff8080f5d90103a100a11902
    d1a2783831353566333133663836346430303964376230623531396231323935633966616
    23762373563313763666335626131616564353031623763a1763534363537333734346534
    3635343331333233333334a46b6465736372697074696f6e744a75737420616e6f7468657
    22066756e204e465465696d616765827568747470733a2f2f697066732e696f2f69706673
    2f782e516d5268545462557250594577336d4a4747685171515354396b383676314450426
    95454574a474b444a73564677696d656469615479706569696d6167652f706e67646e616d
    656b546573744e4654313233346776657273696f6e63312e30"
}

To see what this says:

  1. Go to cbor.me
  2. Paste the value of cborHex into the box on the right
  3. Check the four checkboxes above that box (as text, utf8, emb cbor, cborseq)
  4. Click the green โ† arrow next to the word "Bytes"

The left box will now contain something like this:

[{0: [[h'0BA233973412B629D8D1BCF6CF219A83C7CF16B59F84C1F2C488D80C5D0404DD', 0]],
13: [], 1: [[h'001CB186D6B9FBA78652FCA7A995FB1353A4F4AECEC323C34BF4546BD5B9E3732
73DAC02AE41AC9EBAD6F51B21B701AA39E1CF26571A963079', 998409111], [h'001CB186D6B9F
BA78652FCA7A995FB1353A4F4AECEC323C34BF4546BD5B9E373273DAC02AE41AC9EBAD6F51B21B70
1AA39E1CF26571A963079', [1400000, {h'155F313F864D009D7B0B519B1295C9FAB7B75C17CFC
5BA1AED501B7C': {'TestNFT1234': 1}}]]], 2: 190889, 14: [], 9: {h'155F313F864D009
D7B0B519B1295C9FAB7B75C17CFC5BA1AED501B7C': {'TestNFT1234': 1}}, 7: h'6A46C80316
D784A2B4936540ADCA437283D949A52878616B9494EB6027987877'}, [[0, [1, [[0, h'6D1321
490CDE0B447794EDFF9239CF0668AE01F0DF33823D0A3BBCF7']]]]], [], [], true, 259({0:
{721: {"155f313f864d009d7b0b519b1295c9fab7b75c17cfc5ba1aed501b7c": {"546573744e4
65431323334": {"description": "Just another fun NFT", "image": ["https://ipfs.io
/ipfs/", "QmRhTTbUrPYEw3mJGGhQqQST9k86v1DPBiTTWJGKDJsVFw"], "mediaType": "image/
png", "name": "TestNFT1234"}}, "version": "1.0"}}})]

It's obvious what all of that means, so I won't bother explaining it. ๐Ÿ™ƒ You can view the Cbor.hs source code if you're interested in going down that rabbit hole.

Sign the transaction

To prove we authorize this transaction, we must sign it using the payment key and policy key we generated earlier. Remember, never share these keys with anyone.

cardano-cli transaction sign \
--signing-key-file payment.skey \
--signing-key-file policy/policy.skey \
--testnet-magic 1097911063 \
--tx-body-file matx.raw \
--out-file matx.signed

Submit the transaction to the blockchain

Up until this point, we've just been reading from the Cardano Testnet blockchain. But once we execute the command below, we're writing to the blockchain. There's no going back.

cardano-cli transaction submit \
--tx-file matx.signed \
--testnet-magic 1097911063

You should see the message:

Transaction successfully submitted.

Confirm it worked

Go to https://testnet.cardanoscan.io and search for the wallet address (the one stored in $first_nft_owner_address). Use the dropdown list to view the tokens.

If you don't see it yet, wait 10-30 seconds and check again.

Return test ADA to the faucet

If you're done, it's recommended you return any remaining tAda to the faucet.

Do it on the Mainnet

If everything worked the way you expected, you can now do it on the Mainnet. Replace --testnet-magic 1097911063 with --mainnet in all of the commands throughout this guide.

Comments