Cover Image for Cardano Native Tokens with CardanoSharp

Cardano Native Tokens with CardanoSharp

CardanoSharp

Cardano native tokens are digital assets that operate directly on the Cardano blockchain protocol, negating the need for smart contracts, as they are inherently supported by the ledger itself. These tokens engage with the network with the same level of efficiency and security as ADA, the blockchain's primary currency.

Thanks to Cardano's innovative multi-asset ledger, it's possible to define custom tokens that transact in tandem with ADA, yet stand out with distinct properties and governance outlined by specific minting policies.

In this post, we'll delve into the capabilities of CardanoSharp (an open-source dotnet library that helps bridge the Cardano blockchain and the dotnet ecosystem), focusing on how it facilitates the minting, transferring, and burning of native tokens.

Before we start minting, we need to talk about policies.

Understanding Minting Policies

Cardano minting policies are rules in its smart contract language that control when and how new tokens are made or destroyed. These rules, once made, can't be changed and might include things like how long you have to mint tokens, who needs to sign off on it, and other necessary steps. They provide a secure and consistent way for token creators to manage their tokens on the Cardano network, according to the rules they set up at the start.

Cardano minting policy's attributes are:

  • Immutability: Once established, it cannot be changed.

  • Uniqueness: Each policy is identified by a unique script hash.

  • Authority: Minting or burning requires specific conditions or signatures.

  • Time Constraints: They can include time limits for minting/burning operations.

  • Security: Policies enhance token security with script-enforced rules.

  • Consistency: They ensure uniform token creation and management behaviour across the network.

Now that we know what it is, let's create a policy using CardanoSharp:

// restore HD wallet
var seed = "scout always message drill gorilla ...";
var mnemonic = new MnemonicService().Restore(seed);

var rootKey = mnemonic.GetRootKey();

// get the first policy key
// https://developers.cardano.org/docs/governance/cardano-improvement-proposals/cip-1855/
var (priKey, pubKey) = GetKeyPairFromPath($"m/1855'/1815'/0'", rootKey);

// generate the public key hash
var policyKeyHash = HashUtility.Blake2b224(pubKey.Key);

// create a basic native script that has a single key
var nativeScript = NativeScriptBuilder.Create.SetKeyHash(policyKeyHash);

// create an All script. which means, 
// all the conditions specifid by the native script must be met
var scriptBuilder = ScriptAllBuilder.Create.SetScript(nativeScript);

// build the script policy
var scriptPolicy = scriptBuilder.Build();

// get the policy id 
var policyId = scriptPolicy.GetPolicyId();

// this method helps in key deriviation
(PrivateKey, PublicKey) GetKeyPairFromPath(string path, PrivateKey rootKey)
{
    var privateKey = rootKey.Derive(path);
    return (privateKey, privateKey.GetPublicKey(false));
}

The code above builds a script policy that requires the signature of the first derived key from the path "m/1855'/1815'/0'". This is a typical minting policy with a single issuer and no expiration.

This policy is very useful in minting native assets on Cardano, as the issuer can mint and burn tokens at any time without limitation.

💡
In a later post, we will discuss multi-signature policies and time-constrained policies.

Minting Tokens

Minting is the process of creating new tokens on the blockchain and adding them to the total token supply. A minting policy is required, which defines the rules and conditions under which tokens can be minted and burnt.

The transaction is required to perform a mint. In the transaction, an input UTxO is required to pay the minting fee and minimum ADA to be included in the mint output.

So, using the policy we created earlier, let's mint a new token.

Get the required network info and UTxO.

We need to query for an available UTxO and get the most up-to-date network tip information. You can use the method described in the first post, A Beginner's Guide to CardanoSharp.

Prepare Transaction Variables

Prepare token and transaction details, like token name, how many tokens to mint, the sending address, the private key and the receiving address.

// give the token a name and the amount
var tokenName = "CST";
var tokenQty = 525;

// sender address where the UTxO is been used
var (utxoPrivKey, utxoPubKey) = GetKeyPairFromPath("m/1852'/1815'/0'/0/0", rootKey);

// get the stake address
var (_, stakePubKey) = GetKeyPairFromPath("m/1852'/1815'/0'/2/0", rootKey);

// sender address/change address
// addr_test1qzpmvykhq99xlfcccfft27rsntwg778mp37zf5dar75prtz6xzenalsrvktely96xvqtyv72syeycypeqn4fq42x4xnsl48ktw
var senderAddress = AddressUtility.GetBaseAddress(utxoPubKey, stakePubKey, NetworkType.Preview);

// Receiving address
// in this example, this address belongs to the same wallet for demonstration purpose
// path: m/1852'/1815'/0'/0/1
var receiverAddress = new Address("addr_test1qzw3u57msm6ds6neg0na8guv2rqd8sse5v09k5vjcpzlu0j6xzenalsrvktely96xvqtyv72syeycypeqn4fq42x4xnsdya8t0");

Build the mint transaction.

The gathered information and variables in the previous steps are used to construct the minting transaction.

var utxo = new
{
    tx_hash = "78f49b25102e4f93b1f8742415b65235409a21917cbafd6cb57138e8b813dc92",
    tx_index = 1u,
    address = "addr_test1qzpmvykhq99xlfcccfft27rsntwg778mp37zf5dar75prtz6xzenalsrvktely96xvqtyv72syeycypeqn4fq42x4xnsl48ktw",
    value = 9861760107u
};

var minCoin = 2 * 1000000u;
var currentSlot = 38142122u;

// construct a token bundle builder
var tokenBundleBuilder = TokenBundleBuilder.Create.AddToken(policyId, tokenName.ToBytes(), tokenQty);

// construct the transaction body
var transactionBody = TransactionBodyBuilder.Create
    .AddInput(utxo.tx_hash, utxo.tx_index)
    .AddOutput(receiverAddress, minCoin, tokenBundleBuilder, outputPurpose: OutputPurpose.Mint)
    .AddOutput(senderAddress, utxo.value - minCoin, outputPurpose: OutputPurpose.Change)
    .SetMint(tokenBundleBuilder)
    .SetTtl(currentSlot + 1200)
    .SetFee(0);

// adding signing keys to the UTxO and token policy
var witnessSet = TransactionWitnessSetBuilder.Create
       .SetScriptAllNativeScript(scripBuilder)
       .AddVKeyWitness(scriptPubKey, scriptPrivKey)
       .AddVKeyWitness(utxoPubKey, utxoPrivKey);

// building the transaction
var transactionBuilder = TransactionBuilder.Create
    .SetBody(transactionBody)
    .SetWitnesses(witnessSet);

var transaction = transactionBuilder.Build();

// calculate and update transaction fee
var fee = transaction.CalculateFee();
transactionBody.SetFee(fee);
transaction = transactionBuilder.Build();

// adjusting change output
transaction.TransactionBody.TransactionOutputs
        .Last(x => x.OutputPurpose == OutputPurpose.Change).Value.Coin -= fee;

Submit Transaction

Using Koios service to submit the transaction to the Cardano network.

// transaction rest client
var txClient = RestService.For<ITransactionClient>("https://preview.koios.rest/api/v0");

// open a memory stream of the binary serialized transaction
using var memory = new MemoryStream(transaction.Serialize());
// submit the transaction
var result = await txClient.Submit(memory);

// Transaction Id:
// 9703749c12b3bcc733cf8c780042e1b5ac9d77693da3b0350b410b25f0bc1dcc

Congratulations, you have minted your first native token on the Cardano blockchain.

Let us examine the transaction details using Eternl wallet.

Eternl wallet

This is how the transaction looks like in the Eternl Transactions tab.

Callout 1: Mint action with the token name and minted quantity

Callout 2: This is the UTxO we used as an input to the transaction. You can see the address it belongs to and how much ADA is in the UTxO

Callout 3: The first output of the transaction. In our case, this is the receiving address of the minted tokens.

Callout 4: The last output is often the change address (by convention), which returns the remaining balance to the original sender address or a new address at the same account. Again, this is not a rule but a convention.

💡
A transaction in Cardano can have many outputs, which means we can mint tokens and send them to 10s or 100s of receiving addresses using a single transaction. This is one of the most impressive and very useful feature of the Cardano blockchain.

Now that we have minted a native token, let's look into how to burn them.

Burning Tokens

Burning is the process of removing tokens from the total supply. This is very useful for use cases where the token represents something tangible. e.g., reward points at the point of redemption.

A transaction must be signed by the token holder and the token's policy.

Let's perform a burn action using the same policy and the above mint transaction.

Get the required network info and UTxO.

This time, we will use the transaction hash from the previous mint operation to gather the required UTxO.

💡
Note the address is the same as the receiving address from the previous step

This UTxO is slightly different, as it has an array of assets. This is important because to burn tokens, we need to have the input UTxO to the transaction that has the tokens.

Prepare Transaction Variables

Using the same process as the mint transaction, specify the token name and the amount of tokens to burn.

Get the private and public keys of the sender's address. Notice the path is the same as the receiver address from the mint process above. This is important because this address has the tokens; we need the keys to sign the transaction.

// give the token a name and the QTY to burn
var tokenName = "CST";
var tokenQty = 5;

// sender address where the UTxO is
var (utxoPrivKey, utxoPubKey) = GetKeyPairFromPath("m/1852'/1815'/0'/0/1", rootKey);

// get the stake address
var (_, stakePubKey) = GetKeyPairFromPath("m/1852'/1815'/0'/2/0", rootKey);

// sender address/change address
var senderAddress = AddressUtility.GetBaseAddress(utxoPubKey, stakePubKey, NetworkType.Preview);

Build the burn transaction.

The transaction here is very similar to the mint transaction but has minor differences.

  • The utxo object has the total amount of tokens, which is 525.

  • There is only one input and output.

  • The output contains a multi-asset balance, not just ADA.

  • Notice how burnTokenBuilder inverts the token quantity from positive number to negative, effectively making it -5

The rest of the transaction is the same as the Mint transaction.

var utxo = new
{
    tx_hash = "9703749c12b3bcc733cf8c780042e1b5ac9d77693da3b0350b410b25f0bc1dcc",
    tx_index = 0u,
    address = "addr_test1qzw3u57msm6ds6neg0na8guv2rqd8sse5v09k5vjcpzlu0j6xzenalsrvktely96xvqtyv72syeycypeqn4fq42x4xnsdya8t0",
    value = 2000000u,
    tokenTotal = 525
};

var currentSlot = 38155846u;

// construct a token bundle builder for burning
var burnTokenBuilder = TokenBundleBuilder.Create
    .AddToken(policyId, tokenName.ToBytes(), tokenQty * -1);

// construct the expected output to send back to the wallet
var txOutput = new TransactionOutput
{
    Address = senderAddress.GetBytes(),
    OutputPurpose = OutputPurpose.Change,
    Value = new TransactionOutputValue
    {
        Coin = utxo.value,
        MultiAsset = TokenBundleBuilder.Create
            .AddToken(
                policyId,
                tokenName.ToBytes(), 
                utxo.tokenTotal - tokenQty)
            .Build()
    }
};

// Build transaction body
var transactionBody = TransactionBodyBuilder.Create
    .AddInput(utxo.tx_hash, utxo.tx_index)
    .AddOutput(txOutput)
    .SetMint(burnTokenBuilder)
    .SetTtl(currentSlot + 1200)
    .SetFee(0);

// adding signing keys of the UTxO and token policy
var witnessSet = TransactionWitnessSetBuilder.Create
       .SetScriptAllNativeScript(scripBuilder)
       .AddVKeyWitness(scriptPubKey, scriptPrivKey)
       .AddVKeyWitness(utxoPubKey, utxoPrivKey);

// building the transaction
var transactionBuilder = TransactionBuilder.Create
    .SetBody(transactionBody)
    .SetWitnesses(witnessSet);

var transaction = transactionBuilder.Build();

// calculate and update transaction fee
var fee = transaction.CalculateFee();
transactionBody.SetFee(fee);

transaction = transactionBuilder.Build();

// adjusting change output
transaction.TransactionBody.TransactionOutputs
    .Last(x => x.OutputPurpose == OutputPurpose.Change).Value.Coin -= fee;

Submit Transaction

It is the same process as above.

// transaction id
// 46fb9fd3c0a8950323bcde38b09096a31bd0f1e1cb219155d42678e520198313

At this point, you have successfully burnt 5 tokens from this wallet.

Let's examine how the transaction would look like from Eternl

Eternl

This is how the transaction looks like from the Eternl transaction list.

Callout 1: Burn action with the name and the burnt quantity

Callout 2: The input UTxO includes all the token quantities and ADA balances.

Callout 3: the output of the remaining balances of ADA and unburnt token

Closing Thoughts and Next Steps

In this post, I have taken you through the process of minting and burning native tokens on Cardano blockchain using CardanoSharp.

The code used in this post is verbose, and it might feel somewhat repetitive, but the purpose behind that is to illustrate the steps in detail so you can modify it to suit your context.

Cardano has an elegant way of handling tokens without needing custom smart contracts. This has many benefits, including but not limited to security, low transaction cost, simplicity and speed.

Using a library like CardanSharp simplifies the integration between a dotnet application and the Cardano blockchain.

In future posts, we'll look at minting and burning NFTs.