In Part 1 of this tutorial we minted the PLAYTIME token on the Hedera using the .NET SDK.
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); } } } }
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
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.
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); } } } }
class Program
var account = new Address(0, 0, 18571);
var accountSignatory = new Signatory(Hex.ToBytes("302e020100300506032b6570042204202bce1ad56c185eea5f9b10beea7c85e53f012fe22bd11d75421849aa3cb746dc"));
var receipt = await client.AssociateTokenAsync(token, account, accountSignatory);
Console.WriteLine($"Token associate status returned status: {receipt.Status}");
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
Token associate status returned status: Success
Carol’s Account is now ready to receive PLAYTIME tokens.
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); } } } }
var fromAccount = new Address(0, 0, 14576);
var fromSignatory = new Signatory(Hex.ToBytes("302e020100300506032b6570042204209e9d148f34b5822ae2a57f9055a706be44e5b3e94ee0393b1aaf52886ad7d07e"));
var toAccount = new Address(0, 0, 18571);
ctx.Payer = fromAccount;
ctx.Signatory = fromSignatory;
var receipt = await client.TransferTokensAsync(token, fromAccount, toAccount, 60);
Console.WriteLine($"Token transfer returned status: {receipt.Status}");
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
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
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
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.
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:
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.