Skip to content
Rostislav Litovkin edited this page Oct 16, 2023 · 15 revisions

This section explains how to initialize SubstrateClient, which is used to communicate with RPC nodes.

SubstrateClient will allow you to:

  1. make queries
  2. submit extrinsics
  3. listen to events

Scaffolding with Substrate.NET.Toolchain

I recommend scaffolding the SubstrateClient first with Substrate.NET.Toolchain.

You can find the latest docs for the scaffolding process here: https://github.com/SubstrateGaming/Substrate.NET.Toolchain.

Install our .NET template with the following command:

dotnet new install Substrate.DotNet.Template

which makes dotnet new substrate available.

Using a terminal of your choice, create a new directory for your project and execute the following command in that directory:

dotnet new sln
dotnet new substrate \
   --sdk_version 0.4.4 \
   --rest_service Substrate.NetApi.RestService \
   --net_api Substrate.NetApi.NetApiExt \
   --rest_client Substrate.NetApi.RestClient \
   --metadata_websocket <websocket_url> \
   --generate_openapi_documentation false \
   --force \
   --allow-scripts yes

which generates a new solution and a couple of .NET projects in your project directory. (A description for all command parameters can be found here)

  • Replace the websocket_url with either ws://127.0.0.1:9944 to connect to a local chain or any other url to connect to an RPC node, e.g. wss://rpc.polkadot.io to connect to Polkadot relay chain.

Initialize SubstrateClient

I will assume that you will use the SubstrateClientExt that was generated by the Toolchain instead of the barebones SubstrateClient.

using Substrate.NetApi.Model.Extrinsics;
using Substrate.NetApi.NetApiExt.Generated;

var client = new SubstrateClientExt(
    new Uri("wss://rpc.polkadot.io"),
    ChargeTransactionPayment.Default());

await client.ConnectAsync();

You can also ensure that the client is connected like this:

if (!client.IsConnected)
{
    // Ensure that the client connects
}

Account

To interact with Substrate chains, you will need an Account. Currently, there are 3 ways to getting the account

Mnemonics

Unless you are building a crypto wallet, I do not recommend using this in the production. It is not User friendly and is the least secure out of all of the options.

Generate a new mnemonic:

var random = RandomNumberGenerator.Create(); // Cryptographically secure RNG

var entropyBytes = new byte[16];
random.GetBytes(entropyBytes);

string mnemonics = string.Join(" ", Mnemonic.MnemonicFromEntropy(entropyBytes, BIP39Wordlist.English));

// You might want to display the mnemonics
Console.WriteLine(mnemonics);

Create an Account from mnemonic:

ExpandMode expandMode = ExpandMode.Ed25519; // Currently preferred in Substrate
string password = "please_put_here_something_better_than_password123";

var secret = Mnemonic.GetSecretKeyFromMnemonic(mnemonics, password, BIP39Wordlist.English);

var miniSecret = new MiniSecret(secret, expandMode);

// Our actual Account, that will be used for signing extrinsics/messages
Account account = Account.Build(
    KeyType.Sr25519,
    miniSecret.ExpandToSecret().ToBytes(),
    miniSecret.GetPair().Public.Key);

Import JSON file

Docs currently unavailable

Connecting via Plutonication

Docs currently unavailable

Substrate types

Substrate uses Rust. Rust is a strictly typed language. To be sure that you will use the right types, you need to use the Substrate types, that are either included in the Substrate.NetApi or generated by the Substrate.NET.Toolchain. These Substrate types can be easily encoded to and decoded from SCALE codec.

Primitive Types

  • Bool = bool
  • U8 = byte
  • U16 = ushort
  • U32 = uint - Very often used for indexing/ids
  • U64 = ulong
  • U128 = BigInteger - Very often used for tracking Balance
  • U256 = BigInteger - Almost never used
  • PrimChar = char
  • Str = string - Never used in Substrate, but is useful for correct SCALE encoding/decoding.

Composite types

I will mention a few of the compound types that are often present in Substrate

  • Vec<U8> - used as string in Substrate. It is equal to UTF-8 encoded/decoded string.
  • AccountId32 - used for representing an account address
  • EnumMultiAddress
  • BaseCom - Used for compacting types

Examples

// U128 number
U128 number = new U128(1000);

// The same 1000 number in type U128
U128 number2 = new U128();
number2.Create("0xE8030000000000000000000000000000");

// string message
BaseVec<U8> message = new BaseVec<U8>();
message.Create(new Str("Hello Substrate").Encode());

// account id
AccountId32 accountId = new AccountId32();
accountId.Create(Utils.GetPublicKeyFrom("5EU6EyEq6RhqYed1gCYyQRVttdy6FC9yAtUUGzPe3gfpFX8y"));

Building a method

Substrate.NetApi has got a Method type which describes the actions you want to do in a submitted extrinsic.

Luckily, Substrate.NET.Toolchain has generated helper classes that help us get the Method, that will be called.

To get the Method, you will use <name-of-the-pallet>Calls.<name-of-the-call>(<params>). Here is an example for System.remark("Hello Remark"), which takes as a parameter BaseVec<U8>:

// Creating the parameter message
BaseVec<U8> message = new BaseVec<U8>();
message.Create(Encoding.UTF8.GetBytes("Hello Remark"));

// Getting the actual Method
Method vote = SystemCalls.Remark(message);

Submit Extrinsics

You might very well want to interact with the chain. To do that, you will need to sign and submit extrinsics (~transactions).

To submit an extrinsic, you will need to understand how to:

  1. get Account
  2. build Method

Here is an example of a Balances.transferKeepAlive extrinsic:

// Please refer to the https://github.com/SubstrateGaming/Substrate.NET.API/wiki/Docs#account
Account account = <way_to_get_your_account>;

var accountId = new AccountId32();
accountId.Create(Utils.GetPublicKeyFrom("5EU6EyEq6RhqYed1gCYyQRVttdy6FC9yAtUUGzPe3gfpFX8y"));

var multiAddress = new EnumMultiAddress();
multiAddress.Create(0, accountId);

var amount = new BaseCom<U128>(10000000000); // equivalent to 1 DOT (10^10 planks)

// building the transferKeepAlive Method
Method transfer = BalancesCalls.TransferKeepAlive(multiAddress, amount);

// Charge determines how much tip do you want to pay (and in which currency)
ChargeType charge = true ? ChargeTransactionPayment.Default() : ChargeAssetTxPayment.Default();

uint lifeTime = 64; // explanation: https://polkadot.js.org/docs/api/FAQ/#how-long-do-transactions-live

CancellationToken token = CancellationToken.None; // You might want to use a CancellationToken

await client.Author.SubmitExtrinsicAsync(transfer, account, charge, lifeTime, token);

SubmitAndWatch

This will be documented later.

Query chain State

To get the chain state, like what is a balance of an account.

Fortunately, Substrate.NET.Toolchain has generated helper classes that live in the client. To use them, just type client.<pallet-name>Storage.<storage-name>(<params>). Just like this:

var accountId = new AccountId32();
accountId.Create(Utils.GetPublicKeyFrom("5EU6EyEq6RhqYed1gCYyQRVttdy6FC9yAtUUGzPe3gfpFX8y"));

// Query chain state
var accountBalance = await client.SystemStorage.Account(accountId, token);

// Polkadot has got 10 decimals
// Do not forget to check if the accountBalance is not null
double assetBalance = accountBalance != null ? (double)accountBalance.Data.Free.Value / Math.Pow(10, 10) : 0;

// You might want to show the balance
Console.WriteLine(String.Format("Account balance: {0:0.00} DOT", assetBalance));

Instead of using "5EU6..pFX8y", you can use account.Value to get an address of your account.

accountBalance variable is of type AccountInfo, which is a rust struct translated to c# class by Substrate.NET.Toolchain.

Also, bear in mind that if nothing is saved at that Storage position, await client.SystemStorage.Account(accountId, token) will return null.

Query StorageMaps

Querying data over a StorageMap is a little bit more difficult.

The will need to:

  1. Get the Storage prefix
  2. Get List of all of the keys starting with the Storage prefix
  3. Query the List of keys to get the Storage changes.
  4. Decode Storage changes to the desired type.

The following code shows how to query balances of all Accounts:

List<AccountInfo> accounts = await GetSystemAccountsAsync(client, CancellationToken.None);

foreach(var a in accounts)
{
    // Polkadot has got 10 decimals
    double aBalance = (double)a.Data.Free.Value / Math.Pow(10, 10);

    // You might want to show the balance
    Console.WriteLine(String.Format("Account balance: {0:0.00} DOT", aBalance));
}

/// <summary>
/// Helper method that queries all System.Account values
/// </summary>
/// <param name="client">Your SubstrateClient</param>
/// <param name="token">CancellationToken</param>
/// <returns>List<AccountInfo></returns>
static async Task<List<AccountInfo>> GetSystemAccountsAsync(SubstrateClientExt client, CancellationToken token)
{
    // Get the Storage prefix
    byte[] prefix = RequestGenerator.GetStorageKeyBytesHash("System", "Account");

    // First startKey is unknown
    byte[] startKey = null;

    List<string[]> storageChanges = new List<string[]>();

    while (true)
    {
        // Get List of all of the keys starting with the Storage prefix
        var keysPaged = await client.State.GetKeysPagedAtAsync(prefix, 1000, startKey, string.Empty, token);

        if (keysPaged == null || !keysPaged.Any())
        {
            break;
        }
        else
        {
            // temp variable
            var tt = await client.State.GetQueryStorageAtAsync(keysPaged.Select(p => Utils.HexToByteArray(p.ToString())).ToList(), string.Empty, token);

            storageChanges.AddRange(new List<string[]>(tt.ElementAt(0).Changes));

            // update the startKey to query next keys
            startKey = Utils.HexToByteArray(tt.ElementAt(0).Changes.Last()[0]);
        }
    }

    // Result list
    var accountInfoList = new List<AccountInfo>();

    if (storageChanges != null)
    {
        foreach (var storageChangeSet in storageChanges)
        {
            // Decoding Storage changes to the desired type.
            AccountInfo accountInfo = new AccountInfo();
            accountInfo.Create(storageChangeSet[1]);

            accountInfoList.Add(accountInfo);
        }
    }

    return accountInfoList;
}

Also keep in mind that you might query thousands of storage keys - this might take a while or may not complete at all.

Get Chain Metadata

Console.WriteLine(client.MetaData.Serialize());

Clone this wiki locally