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

In Part 1 of this tutorial we minted the PLAYTIME token on the Hedera using the .NET SDK.

Confirm PLAYTIME Details

Before setting up her children’s’ accounts to receive tokens, Alice wants to double-check that she created the token correctly. The library offers the method GetTokenInfoAsync for this purpose. Alice creates a new console program to query the network for the details:

using System;

using System.Threading.Tasks;

using Hashgraph;

namespace Alice

{

public partial class Program

{

static async Task Main()

{

try

{

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

var token = new Address(0, 0, 18504);

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

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

await using var client = new Client(ctx =>

{

ctx.Gateway = gateway;

ctx.Payer = payer;

ctx.Signatory = payerSignatory;

});

var info = await client.GetTokenInfoAsync(token);

Console.WriteLine($"Token: 0.0.{info.Token.AccountNum}");

Console.WriteLine($"Symbol: {info.Symbol}");

Console.WriteLine($"Name: {info.Name}");

Console.WriteLine($"Treasury: 0.0.{info.Treasury.AccountNum}");

Console.WriteLine($"Circulation: {info.Circulation:#,##0.0}");

Console.WriteLine($"Decimals: {info.Decimals}");

Console.WriteLine($"Administrator: {Hex.FromBytes(info.Administrator.PublicKey)}");

Console.WriteLine($"GrantKycEndorsement: {(info.GrantKycEndorsement == null ? "Null" : "Not Null")}");

Console.WriteLine($"SuspendEndorsement: {Hex.FromBytes(info.SuspendEndorsement.PublicKey)}");

Console.WriteLine($"ConfiscateEndorsement: {Hex.FromBytes(info.ConfiscateEndorsement.PublicKey)}");

Console.WriteLine($"SupplyEndorsement: {Hex.FromBytes(info.SupplyEndorsement.PublicKey)}");

Console.WriteLine($"Tradable Status: {info.TradableStatus}");

Console.WriteLine($"KYC Status: {info.KycStatus}");

Console.WriteLine($"Expiration: {info.Expiration}");

Console.WriteLine($"Renew Period: {info.RenewPeriod}");

Console.WriteLine($"Renew Account: 0.0.{info.RenewAccount.AccountNum}");

Console.WriteLine($"Deleted: {info.Deleted}");

}

catch (Exception ex)

{

Console.Error.WriteLine(ex.Message);

Console.Error.WriteLine(ex.StackTrace);

}

}

}

}

The GetTokenInfoAsync returns a TokenInfo containing many details regarding the state of the token in the network. Alice runs her new program to confirm the status of the PLAYTIME token:

PS D:\Alice> dotnet run

Token: 0.0.18504

Symbol: PLAYTIME

Name: Play Time Minutes

Treasury: 0.0.14576

Circulation: 1,500,000,000.0

Decimals: 0

Administrator: 302a300506032b6570032100104dc4d48fd755404d9551566377436a0cdf44f1c0812a5479add96dbdb0e9bd

GrantKycEndorsement: Null

SuspendEndorsement: 302a300506032b6570032100104dc4d48fd755404d9551566377436a0cdf44f1c0812a5479add96dbdb0e9bd

ConfiscateEndorsement: 302a300506032b6570032100104dc4d48fd755404d9551566377436a0cdf44f1c0812a5479add96dbdb0e9bd

SupplyEndorsement: 302a300506032b6570032100104dc4d48fd755404d9551566377436a0cdf44f1c0812a5479add96dbdb0e9bd

Tradable Status: Tradable

KYC Status: NotApplicable

Expiration: 1/23/2021 12:31:01 PM

Renew Period: 90.00:00:00

Renew Account: 0.0.14576

Deleted: False

After reviewing the details, Alice is satisfied the token has been created successfully.

Associate Carol’s Account with PLAYTIME

Alice’s daughter, Carol, is excited to start earning PLAYTIME tokens, well, more importantly, spending PLAYTIME tokens. But they both realize there are a few remaining steps to accomplish before Alice can transfer an hour’s worth of PLAYTIME tokens to Carol’s account. An account cannot receive any token until the account holder has explicitly opted-in to receiving the token. They must send a transaction to the network instructing the network to provision storage to hold the token balance associated with their account, this is called Association.

The .NET library provides a method, AssociateTokenAsync, making this association. It only requires the ID of the token, ID of the account to associate, and the signature from the account holder (the Payer may be a third party if necessary).

Alice creates a new program to associate her eldest daughter’s account with the token:

using System;

using System.Threading.Tasks;

using Hashgraph;

namespace Alice

{

class Program

{

static async Task Main()

{

try

{

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

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

var token = new Address(0, 0, 18504);

var account = new Address(0, 0, 18571);

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

var accountSignatory = new Signatory(Hex.ToBytes("302e020100300506032b6570042204202bce1ad56c185eea5f9b10beea7c85e53f012fe22bd11d75421849aa3cb746dc"));

await using var client = new Client(ctx =>

{

ctx.Gateway = gateway;

ctx.Payer = payer;

ctx.Signatory = payerSignatory;

});

var receipt = await client.AssociateTokenAsync(token, account, accountSignatory);

Console.WriteLine($"Token associate status returned status: {receipt.Status}");

}

catch (Exception ex)

{

Console.Error.WriteLine(ex.Message);

Console.Error.WriteLine(ex.StackTrace);

}

}

}

}

One might note that Carol’s private key appears in the source code above (as well as Alice’s). Since Alice and Carol are experimenting with testnet and the crypto and tokens have no real-world value this is acceptable. However, in a production system communicating with the mainnet, other measures to protect the keys would of course be employed.

Invoking the new program results in a success message:

PS D:\Alice> dotnet run

Token associate status returned status: Success

Carol’s Account is now ready to receive PLAYTIME tokens.

Sending PLAYTIME tokens to Carol

Alice decides to give Carol 60 minutes of PLAYTIME tokens as an initial gift. She is pleased to discover, that after all the setup above, writing a program to transfer tokens is simple:

using System;

using System.Threading.Tasks;

using Hashgraph;

namespace Alice

{

class Program

{

static async Task Main()

{

try

{

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

var token = new Address(0, 0, 18504);

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

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

var toAccount = new Address(0, 0, 18571);

await using var client = new Client(ctx =>

{

ctx.Gateway = gateway;

ctx.Payer = fromAccount;

ctx.Signatory = fromSignatory;

});

var receipt = await client.TransferTokensAsync(token, fromAccount, toAccount, 60);

Console.WriteLine($"Token transfer returned status: {receipt.Status}");

}

catch (Exception ex)

{

Console.Error.WriteLine(ex.Message);

Console.Error.WriteLine(ex.StackTrace);

}

}

}

}

The .NET library provides a method, TransferTokensAsync, that transfers tokens from one account to another. Since Alice’s account is the treasury, she presently holds all the tokens. Executing the program yields:

PS D:\Alice> dotnet run

Token transfer returned status: Success

Next, she re-runs her balance program to verify the tokens have been deducted from the treasury:

PS D:\Alice> dotnet run

Account 0.0.14576

Crypto Balance is 989.0 hBars.

Token 0.0.18504 is 1,499,999,940.0

And edits the same program to query Carol’s account to verify the token has been added to her account:

PS D:\Alice> dotnet run

Account 0.0.18571

Crypto Balance is 10.0 hBars. Token 0.0.18504 is 60.0

Both Alice and Carol are pleased at the results and how quickly they can get started exchanging custom tokens with a small amount of C# code. Alice starts making plans to write production ready versions of her PLAYTIME token implementation when it's ready for the mainnet.

To review, Alice used her account, 0.0.14576, to pay for a transaction to create a new token PLAYTIME that was given an ID of 0.0.18504 by the testnet. She then used her account to pay for a transaction that Carol also signed that associated Carol’s account with the token, enabling Carol’s account to receive and send PLAYTIME tokens as well. She finally transferred a good will initial balance of 60 PLAYTIME tokens to Carol’s account and verified the transfer by checking both her and Carol’s balances afterwards.

Since Carol holds the administrative keys to the PLAYTIME token, there are many other token related actions she can invoke. If she finds it necessary to punish bad behavior, she can temporarily suspend an account holder’s ability to exchange tokens with the SuspendTokenAsync method. She can even confiscate an entire account’s PLAYTIME balance with the ConfiscateTokensAsync method. If it turns out she wants to extend accessibility of PLAYTIME tokens to her friends with children, she does not need to worry about running out of tokens, she can invoke the MintTokenAsync method to create more to share. Since this is an introduction to the Hedera Token Service, we will not follow Alice through these steps, but having mastered the fundamentals, one can easily pick up on the rest.

Conclusion

It’s now time to take a break from Alice’s journey of discovery of the Hedera Network using the .NET SDK to quickly review the basic concepts introduced here. The central component of the SDK orchestrating the native communication with the network is the Client object. It hides much of the complexity of communication with the network, exposing the functionality thru easy to invoke async functions. A Client in-turn is configured by updating properties of its Context when created. At the very minimum, this Context must be assigned a Gateway holding the information identifying a network node that can accept requests and return results. The Gateway consists of two parts, the Address of the crypto account associated and an internet address and port for the nodes public gRPC interface. An Address consists of three identifiers, Shard, Realm and Number. Each Hedera account holder owns an Address identifying their account and crypto balance on the network. When an account holder wishes to perform an action on the network requiring a fee, such as a token transfer, they also must assign a Payer to the Context. The Payer is the address of the crypto account that pays the transaction fee. In addition to setting the Payer property of the context, the account holder must assign the Signatory. In most cases the Signatory holds the private key that can sign transactions submitted to the network on behalf of the account holder. For most private accounts, the Signatory is a single Ed25519 key, but can be a more complex set of keys if so desired. Also, for each Signatory representing the private signing key(s) for an account, a corresponding Endorsement can be created, representing the public key structure requirements matching the private key(s).

Anyone with a network account can create Custom Tokens. The Hedera Network provides a robust feature set for administering token creation, destruction, know-your-customer, account suspension and confiscation. Each of these features can be enabled at token creation time by assigning an Endorsment for the specific feature. In this way administration of the token can be partitioned to separate trusted parties. Additionally, tokens can only be transferred to account holders that wish to participate by opting in through the process called association. Association of an account with a token requires the account holder to explicitly sign the transaction enabling participation. This eliminates potential complications of accounts receiving air-drops without consent.

This narrative only scratches the surface of Token API exposed by the .NET library. The list of methods is large, here is brief list:

  • TransferTokensAsync – Transfer tokens between crypto accounts.
  • TransferAsync – Transfers one or more tokens and/or hBars between multiple crypto accounts.
  • CreateTokenAsync – Creates a new token from the supplied create token parameters.
  • UpdateTokenAsync – Updates certain properties defining a token.
  • GetTokenInfoAsync – Retrieves the details of a token definition, showing which features are enabled and which public keys have administrative privileges.
  • DeleteTokenAsync – Removes a token definition from the network.
  • AssociateTokensAsync – Associates a token with a crypto account, the first step in enabling an account to send and receive the specified custom token.
  • DissociateTokenAsync – Removes the relationship between a token and a crypto account, the account will no longer be able to send and receive the specified token.
  • ConfiscateTokensAsync – Removes or wipes the holdings of given token from the associated account and returns them to the treasury.
  • BurnTokenAsync – Removes tokens from the treasury, destroying them in the process.
  • MintTokenAsync – Creates new tokens depositing them in the treasury account.
  • GrantTokenKycAsync – Grants know your customer status to the associated account relating to the specified token.
  • RevokeTokenKycAsync – Revokes know your customer status from the associated account relating to the specified token.
  • SuspendTokenAsync – Suspends or freezes the associated account's ability to send or receive the specified token.
  • ResumeTokenAsync – Resumes or unfreezes the associated account's ability to send or receive the specified token.

We hope you enjoyed this quick narrative on how to get started with tokens using the .NET SDK for Hedera Hashgraph. Please stay tuned for future stories, including uploading and sharing files, and of course the Hedera Consensus Service. In the meanwhile, please check out https://bugbytesinc.github.io/Hashgraph/ for more examples on how to incorporate Hedera Hashgraph into your .NET projects.