Creating tokens on Hedera with .NET - Part 1
Dec 14, 2020
by Jason Fabritz
President & Owner, BugBytes, Inc.

If you want to build stablecoin applications on Hedera please visit our Stablecoin Studio page to learn how.

Recently, Hedera announced access to the Hedera Token Service (HTS) on the Hedera testnet. This is exciting news as the tokens created with HTS inherit much of the same advantages enjoyed by the Hedera network’s native cryptocurrency, hbars.

The Hedera ecosystem supports many wallets, tools, and languages with support for Java, JavaScript, Go, and last but not least, .NET. In this second of the .NET SDK blog post series, we will explore how to create our own stablecoin with the .NET SDK, check balances and transfer coins to our friends.

To follow along, you’ll need:

Meet Alice

When we last visited Alice, she had mastered the basics of interacting with the Hedera network via the .NET SDK. She created an account for her friend, Bob, and transferred hBars to him. Since then, she has recently heard the Hedera Token Service, recognizing the potential of this service, she dives right in. Alice’s testnet account is 0.0.14576 with an associated public key value of 302a300506032b65700321007690592c22c5a9b8445096cf188f7cafafa3cf7943dfa3f7caa32df0671693fb and private key of 302e020100300506032b6570042204209e9d148f34b5822ae2a57f9055a706be44e5b3e94ee0393b1aaf52886ad7d07e.

Account Balance Query

In our previous post, Alice ventured to and downloaded .NET Core Runtime and SDK. Double checking her currently installed version reveals:

Code Snippet Background

PS D:\Alice> dotnet --version

5.0.101

Like before to keep things simple, Alice foregoes a graphical interface and creates an empty console application to get started. She invokes .NET Command Line Interface (CLI) providing the arguments to create a new console project:

Code Snippet Background

PS D:\Alice> dotnet new console

Getting ready...

The template "Console Application" was created successfully.

Processing post-creation actions...

Running 'dotnet restore' on D:\Alice\Alice.csproj...

Determining projects to restore...

Restored D:\Alice\Alice.csproj (in 160 ms).

Restore succeeded.

Next, she adds the .NET Client Library for Hedera Hashgraph (https://www.nuget.org/packages/Hashgraph/) in her project.

Code Snippet Background

PS D:\Alice> dotnet add package Hashgraph

Determining projects to restore...

Writing C:\Users\alice\AppData\Local\Temp\tmpAFB9.tmp

info : Adding PackageReference for package 'Hashgraph' into project 'D:\Alice\Alice.csproj'.

info : Restoring packages for D:\Alice\Alice.csproj...

info : Package 'Hashgraph' is compatible with all the specified frameworks in project 'D:\Alice\Alice.csproj'.

info : PackageReference for package 'Hashgraph' version '7.0.0' added to file 'D:\Alice\Alice.csproj'.

info : Committing restore...

info : Writing assets file to disk. Path: D:\Alice\obj\project.assets.json

log : Restored D:\Alice\Alice.csproj (in 239 ms).

The next step is writing the C# code to perform the balance query. For this, Alice invokes a new method provided by this release, GetAccountBalancesAsync, which returns both crypto and token balances (this is different from the GetAccountBalanceAsync method, note the singular vs. plural nomenclature):

Code Snippet Background

using System;

using System.Threading.Tasks;

using Hashgraph;

namespace Alice

{

public partial class Program

{

static async Task Main()

{

try

{

var address = new Address(0, 0, 14576);

var gateway = new Gateway("35.231.208.148:50211", 0, 0, 3);

await using var client = new Client(ctx => { ctx.Gateway = gateway; });

var balances = await client.GetAccountBalancesAsync(address);

Console.WriteLine($"Account 0.0.{address.AccountNum}");

Console.WriteLine($"Crypto Balance is {balances.Crypto / 100_000_000:#,##0.0} hBars.");

foreach (var token in balances.Tokens)

{

Console.WriteLine($"Token 0.0.{token.Key.AccountNum} is {token.Value:#,##0.0}");

}

}

catch (Exception ex)

{

Console.Error.WriteLine(ex.Message);

Console.Error.WriteLine(ex.StackTrace);

}

}

}

}

It takes only a mere ten lines of C# code to perform the task of a crypto and token balance query. Let’s look at each line to review some basic concepts:

Address

The first concept is the Address. An address is an identifier for an Account, Contract, File, Consensus Topic or Hedera Token. The identifier consists of three parts: Shard, Realm and Number. Currently, the Hedera network only has one instance of a shard and realm; for the time being, these values will be zero. The third identifier is the address number. The network generates this number upon item creation. Alice’s account number is 0.0. 14576: shard zero, realm zero with number 14576.

Gateway

The Gateway is an object provided by the library for identifying an Hedera Gossip Network Node. Each Hedera node has a public addressable internet endpoint and linked network Account address. The internet endpoint represents the publicly available gRPC service hosted by the network node, this is the service the .NET library will connect to when making the balance request. The account address represents the Hedera crypto account paired with the node that receives funds from transaction requests requiring payment. Hedera lists the address book of gateways for the test networks at https://docs.hedera.com/hedera/networks/testnet/testnet-nodes.

Client

The Client object orchestrates communication with the Hedera network. It takes on the role of encoding messages, sending them to the gateway, waiting for a response and then finally decoding the results, returning normal .NET class objects. It provides one or more method overloads relating to each possible network function; each following the standard .net async/await pattern. The client is the go-to object for communicating with the Hedera network.

Context

Some initial configuration is required when creating a Client object. At the very least for balance queries, the Client must know which Gateway to contact to ask for the information. The Context, exposed thru the IContext interface, provides the means to configure a client. We will revisit configuration later, but for a balance queries, setting the Gateway is all that is necessary.

Getting the Balances

Once the client has been properly setup, getting the balances is a simple method call to GetAccountBalancesAsync. This async method returns the crypto balance of the account in tinybars, the smallest unit of cryptocurrency in Hedera. One hbar equals 100,000,000 tinybars. It also returns a dictionary mapping hedera token addresses with balances.

Alice invokes her nascent program via the dotnet run command:

Code Snippet Background

PS D:\Alice> dotnet run

Account 0.0.14576

Crypto Balance is 10,000.0 hBars.

As expected, this testnet account has token balances, but it does have a crypto balance is 10,000ℏ. Accounts on testnet and previewnet receive a deposit from a faucet account roughly every 24 hours to ensure developers have enough working crypto supporting their development efforts.

Create A Token

After confirming basic network connectivity to the testnet, it is time for Alice to create her own token, called PLAYTIME. PLAYTIME represents a minute of computer gaming by her children. They redeem PLAYTIME tokens to access time on the gaming console or laptop. They earn PLAYTIME tokens by executing household chores and other creative helping around the house. Currently this system runs on a paper ledger, Alice is excited to move this into the Hedera Network; her children are getting old enough to barter between themselves for PLAYTIME coins, and having this system on the Hedera will relieve her of the need to play referee when disputes inevitably occur.

However, before we discuss the code Alice writes to create the token, there are a few more concepts to introduce:

Payer & Signatory

Whereas querying the Hedera network for balances is presently free other actions, particularly those changing network state, require the payment of a small fee to execute. The .NET library refers to the account paying these transaction fees as the Payer. The payer consists of two pieces of information, the Account identifying the payer, and a Signatory authorizing the spending of funds from the account. The Signatory is typically backed by the account holder’s private key, which is the case for Alice. Alice’s testnet account is protected by a single Ed25519 key with the value of 302e020100300506032b6570042204209e9d148f34b5822ae2a57f9055a706be44e5b3e94ee0393b1aaf52886ad7d07e. (Note: for Ed25519 keys, the library uses DER encoding, the keys are encoded 48 bytes in length, prefixed with the value of 302e020100300506032b6570.)

Endorsement

Most accounts are secured by a single private key, such as Alice’s Ed25519 key described above. The network never sees Alice’s private key but has been given the public key corresponding to her private key during the creation of her account. Accounts are not the only thing protected by keys in the hedera network. Contracts, Topics and Tokens can be administered (modified) by parties holding administrative keys assigned to these assets. When creating a Token, there is an opportunity to provide a public key enabling access to various administrative functions against the token. The .NET SDK provides the Endorsement object to hold this public key value.

An Endorsement object can represent a complex multi-key structure, fully representing what the Hedera network is capable of processing, but for Alice’s purposes, a single Ed25519 public key will suffice. There are five administrative functions that can each be protected by a different key structure. Since this is a prototype, Alice decides to enable all but one of these functions. However, she does not want to manage that many keys and decides to generate a single key to re-use for each administrative function. She generates a new private key, 302e020100300506032b657004220420544c47ac3eedb38442e552d5151f60028d0208708e5abb868d3aedc1c67d5f0d, with a corresponding public key 302a300506032b6570032100104dc4d48fd755404d9551566377436a0cdf44f1c0812a5479add96dbdb0e9bd for this purpose.

Creating PLAYTIME

To create the token, Alice modifies her program to invoke the CreateTokenAsync method on the client. This method accepts an argument, CreateTokenParams, containing all the required details for token creation. Not all of the properties are required to be set: The Name is the name of the token. It can only consist of ACII characters. It is not required to be unique in the hedera system. The Symbol is a short identifier of the token (much like a stock ticker symbol). It can only consist of upper-case ASCII alpha characters, no numbers. However, like the name, it is not required to be unique in the hedera system. (So yes, someone else can create the token with the symbol PLAYTIME, but it won’t be the same token because a token is identified by the Token ID returned from the system upon creation.)

  • Circulation is the initial supply of minted coins, denoted in the smallest denomination of the coin (similar to the way the network transacts hBars in the tinybar denomination).
  • Decimals is the number of decimal places the coin can be sub-divided. For example, the US dollar’s smallest physical coin is a penny, this matches the value of 2. Since PLAYTIME’s resolution need not be smaller than a minute, Alice chooses to enter zero for this value.
  • Treasury is the initial account that receives the initial supply of minted coins, it is a crypto account address, Alice will enter her personal account for this.
  • Administrator property represents the main administrative Endorsement. While this is a powerful function to enable, it cannot, by itself, unlock all the functions one can take against a token (such as minting new tokens or approving KYC of candidate account holders).
  • GrantKycEndorsement unlocks know-your-customer features for the token, this is an advanced topic so Alice will leave this feature off by setting this value to null.
  • SuspendEndorsement property can unlock the ability of the keyholder to freeze or suspend another account’s ability to send or receive the token. Since there may be cases of mis-behaving children, Alice decides to enable this feature.
  • ConfiscateEndorsement endorsement enables the keyholder to remove or wipe the entire balance of a token from an account. Although Alice would hope she would never have to resort to this, the threat of confiscation might come in handy as a response to a mis-behaving child, so she enables this feature too.
  • SupplyEndorsement unlocks the ability to mint new coins and deposit them in the treasury.
  • InitializeSuspended property determines if the default relationship between crypto accounts and the token starts in a suspended or frozen state or enabled for immediate trading. This is not the same as the KYC feature, but can be leveraged for certain on-boarding scenarios requiring some accountholder vetting. Since Alice wants her children to be able to trade this token freely without extra work on her side, she sets this value to false.
  • Expiration value relates to how long before the next renewal charge for storing the definition of the token on the network is charged.
  • RenewPeriod is the periodic time between token renewal periods.
  • RenewAccount is the crypto address the network charges upon token account renewal.
  • Signatory property is an optional place to add signing keys necessary to execute the transaction (if not already specified in the client’s context configuration). As mentioned above, Alice wants to reduce the effort in maintaining keys so she created one key to apply to the Administrator and other endorsements. The resulting program looks like the following:
Code Snippet Background

using System;

using System.Threading.Tasks;

using Hashgraph;

namespace Alice

{

class Program2

{

static async Task Main2()

{

try

{

var gateway = new Gateway("35.231.208.148:50211", 0, 0, 3);

var payer = new Address(0, 0, 14576);

var payerSignatory = new Signatory(Hex.ToBytes("302e020100300506032b6570042204209e9d148f34b5822ae2a57f9055a706be44e5b3e94ee0393b1aaf52886ad7d07e"));

var tokenEndorsement = new Endorsement(Hex.ToBytes("302a300506032b6570032100104dc4d48fd755404d9551566377436a0cdf44f1c0812a5479add96dbdb0e9bd"));

var tokenSignatory = new Signatory(Hex.ToBytes("302e020100300506032b657004220420544c47ac3eedb38442e552d5151f60028d0208708e5abb868d3aedc1c67d5f0d"));

var createParams = new CreateTokenParams

{

Name = "Play Time Minutes",

Symbol = "PLAYTIME",

Circulation = 1_500_000_000,

Decimals = 0,

Treasury = payer,

Administrator = tokenEndorsement,

GrantKycEndorsement = null,

SuspendEndorsement = tokenEndorsement,

ConfiscateEndorsement = tokenEndorsement,

SupplyEndorsement = tokenEndorsement,

InitializeSuspended = false,

Expiration = DateTime.UtcNow.AddDays(90),

RenewAccount = payer,

RenewPeriod = TimeSpan.FromDays(90),

Signatory = tokenSignatory

};

await using var client = new Client(ctx =>

{

ctx.Gateway = gateway;

ctx.Payer = payer;

ctx.Signatory = payerSignatory;

});

var receipt = await client.CreateTokenAsync(createParams);

Console.WriteLine($"Token Created with ID 0.0.{receipt.Token.AccountNum}");

}

catch (Exception ex)

{

Console.Error.WriteLine(ex.Message);

Console.Error.WriteLine(ex.StackTrace);

}

}

}

}

Executing the program yields:

Code Snippet Background

PS D:\Alice> dotnet run

Token Created with ID 0.0.18504

Alice now has the ID of her token it is 0.0.18504. Remember that the Address of the token is the unique identifier of the token, not the symbol or name. Alice re-runs her get balances program to confirm the results:

Code Snippet Background

PS D:\Alice> dotnet run

Account 0.0.14576

Crypto Balance is 999.0 hBars.

Token 0.0.18504 is 1,500,000,000.0

Indeed, her account carries a PLAYTIME balance of 1,500,000,000 identified by the ID 0.0.18504.


That concludes the part 1 of this tutorial. Continue to part 2 to learn how to query our PLAYTIME token balance, make transfers, and more.