Skip to content

Extension, Wrapper & Dependencies

Cedric Decoster edited this page Jan 18, 2024 · 8 revisions

Overview

This documentation outlines the methods used to facilitate access to the blockchain and optimize its integration within a C# project. The focus is on custom integration/wrapping techniques that build upon the generated extensions, built on the user-specific blockchain. The primary goal is to transform substrate-wrapped C# types into native C# types and manage subscription handling efficiently.

Key Points:

  • The integration primarily handles the transformation from generated substrate-wrapped C# types to native C# types.
  • The handling of storage accesses and extrinsic calls is managed by the generated Extension library.
  • Custom wrappers simplify the usage within Unity by eliminating the need to directly handle substrate-wrapped types.
  • Additional subscription and event handling to minimize blockchain-specific code in Unity.

Breakdown of Substrate-Type Wrapping

Let's take a look at such a nested type breakdown, and unfold its substrate-wrapped types, for this purpose, we work alongside the most common structure of the substrate account.

Wrapping Flow:

  • Substrate Rust Type: AccountInfo<Nonce, AccountData> nested AccountData<Balance>
  • Substrate-wrapped C# Type (generated): AccountInfo nested AccountData
  • Native C# Type (Custom): AccountInfoSharp nested AccountDataSharp

Substrate Rust Type

  • Original Rust structure: AccountInfo<Nonce, AccountData> with nested AccountData<Balance>.
  • These structures are complex, often involving a nested composition of types and subtypes.
/// Information of an account.
#[derive(Clone, Eq, PartialEq, Default, RuntimeDebug, Encode, Decode, TypeInfo, MaxEncodedLen)]
pub struct AccountInfo<Nonce, AccountData> {
	/// The number of transactions this account has sent.
	pub nonce: Nonce,
	/// The number of other modules that currently depend on this account's existence. The account
	/// cannot be reaped until this is zero.
	pub consumers: RefCount,
	/// The number of other modules that allow this account to exist. The account may not be reaped
	/// until this and `sufficients` are both zero.
	pub providers: RefCount,
	/// The number of modules that allow this account to exist for their own purposes only. The
	/// account may not be reaped until this and `providers` are both zero.
	pub sufficients: RefCount,
	/// The additional data that belongs to this account. Used to store the balance(s) in a lot of
	/// chains.
	pub data: AccountData,
}

/// All balance information for an account.
#[derive(Encode, Decode, Clone, PartialEq, Eq, Default, RuntimeDebug, MaxEncodedLen, TypeInfo)]
pub struct AccountData<Balance> {
	/// Non-reserved part of the balance which the account holder may be able to control.
	///
	/// This is the only balance that matters in terms of most operations on tokens.
	pub free: Balance,
	/// Balance which is has active holds on it and may not be used at all.
	///
	/// This is the sum of all individual holds together with any sums still under the (deprecated)
	/// reserves API.
	pub reserved: Balance,
	/// The amount that `free + reserved` may not drop below when reducing the balance, except for
	/// actions where the account owner cannot reasonably benefit from the balance reduction, such
	/// as slashing.
	pub frozen: Balance,
	/// Extra information about this account. The MSB is a flag indicating whether the new ref-
	/// counting logic is in place for this account.
	pub flags: ExtraFlags,
}

Substrate-wrapped C# Type (generated)

  • Generated by the Toolkit for C# integration.
  • Example: AccountInfo and nested AccountData.
{
    /// <summary>
    /// >> 3 - Composite[frame_system.AccountInfo]
    /// </summary>
    [SubstrateNodeType(TypeDefEnum.Composite)]
    public sealed class AccountInfo : BaseType
    {
...
        public Substrate.NetApi.Model.Types.Primitive.U32 Nonce
...
        public Substrate.NetApi.Model.Types.Primitive.U32 Consumers
...
        public Substrate.NetApi.Model.Types.Primitive.U32 Providers
...
        public Substrate.NetApi.Model.Types.Primitive.U32 Sufficients
...
        public Substrate.Hexalem.NET.NetApiExt.Generated.Model.pallet_balances.types.AccountData Data
...
    }
}

namespace Substrate.Hexalem.NET.NetApiExt.Generated.Model.pallet_balances.types
{
    /// <summary>
    /// >> 5 - Composite[pallet_balances.types.AccountData]
    /// </summary>
    [SubstrateNodeType(TypeDefEnum.Composite)]
    public sealed class AccountData : BaseType
    {
...
        public Substrate.NetApi.Model.Types.Primitive.U128 Free
...
        public Substrate.NetApi.Model.Types.Primitive.U128 Reserved
...
        public Substrate.NetApi.Model.Types.Primitive.U128 Frozen
...
        public Substrate.Hexalem.NET.NetApiExt.Generated.Model.pallet_balances.types.ExtraFlags Flags
...
    }
}

Native C# Type (Custom)

  • Custom wrappers for easier usage within the C# environment.
  • Example: AccountInfoSharp and AccountDataSharp.
namespace Substrate.Integration.Model
{
    public class AccountInfoSharp
    {
        public AccountInfoSharp(AccountInfo accountInfo)
        {
            Nonce = accountInfo.Nonce.Value;
            Consumers = accountInfo.Consumers.Value;
            Providers = accountInfo.Providers.Value;
            Sufficients = accountInfo.Sufficients.Value;
            Data = new AccountDataSharp(accountInfo.Data);
        }

        public uint Nonce { get; }
        public uint Consumers { get; }
        public uint Providers { get; }
        public uint Sufficients { get; }
        public AccountDataSharp Data { get; }
    }

    public class AccountDataSharp
    {
        public AccountDataSharp(AccountData accountData)
        {
            Free = accountData.Free.Value;
            Reserved = accountData.Reserved.Value;
            Frozen = accountData.Frozen.Value;
            Flags = new ExtraFlagsSharp(accountData.Flags);
        }

        public BigInteger Free { get; }
        public BigInteger Reserved { get; }
        public BigInteger Frozen { get; }
        public ExtraFlagsSharp Flags { get; }
    }

}

Hexalem Integration/Wrapper

An example of a base integration/wrapper with some custom functions is integrated into the Polkadot SDK for Unity as part of the Hexalem Game. This is a good starting point to create your individual custom wrapper, by replacing the referenced extension with your specific blockchain extension and then changing and adapting the core elements that you need to access within your Unity project. Substrate.Hexalem.Integration

Custom Integration/Wrapper

To create a custom wrapper for a specific blockchain, it's recommended to start by copying the Hexalem integration/wrapper or another similar wrapper. Then, adapt it to the new namespace of your extension, Substrate.xyz.NET.NetApiExt, and proceed from there.

Custom Storage Wrapping

Consider the Account example previously detailed. To transform an Account into the required AccountId32 (the substrate-wrapped C# type), you access SystemStorage.Account as follows:

        public async Task<AccountInfoSharp> GetAccountAsync(Account key, CancellationToken token)
        {
            if (!IsConnected)
            {
                Log.Warning("Currently not connected to the network!");
                return null;
            }

            if (key == null || key.Value == null)
            {
                Log.Warning("No account reference given as key!");
                return null;
            }

            // generated extension access SystemStorage Account.
            // returning the substrate-wrapped c# type AccountInfo
            var result = await SubstrateClient.SystemStorage.Account(key.ToAccountId32(), token);
            if (result == null)
            {
                return null;
            }

            // transforming into the native C# type representation of the AccountInfo
            // including the nested type AccountData.
            return new AccountInfoSharp(result);
        }

Custom Call Wrapping

To illustrate, you can create a TransferKeepAliveAsync call wrapper that converts substrate-wrapped types to C# native types, like BigInteger instead of BaseCom<U128>.

        public async Task<string> TransferKeepAliveAsync(Account account, AccountId32 dest, BigInteger value, int concurrentTasks, CancellationToken token)
        {
            var extrinsicType = "BalancesCalls.TransferKeepAlive";

            if (!IsConnected || account == null)
            {
                return null;
            }

            var multiAddress = new EnumMultiAddress();
            multiAddress.Create(MultiAddress.Id, dest);

            var balance = new BaseCom<U128>();
            balance.Create(value);

            var extrinsic = BalancesCalls.TransferKeepAlive(multiAddress, balance);

            return await GenericExtrinsicAsync(account, extrinsicType, extrinsic, concurrentTasks, token);
        }

Alternatively, you can build and send the transaction as an EnumRuntimeCall and wrap it in a UtilityCalls pallet transaction. This approach allows for handling Batch calls in a single transaction and executing a SudoCalls pallet extrinsic.

First, construct the EnumRuntimeCall as follows:

    public interface ICall
    {
        EnumRuntimeCall ToCall();
    }

    public static class PalletBalances
    {
        public static EnumRuntimeCall BalancesTransferKeepAlive(AccountId32 target, BigInteger amount)
        {
            var baseU128 = new BaseCom<U128>();
            baseU128.Create(amount);

            var multiAddress = new EnumMultiAddress();
            multiAddress.Create(MultiAddress.Id, target);

            var baseTubleParams = new BaseTuple<EnumMultiAddress, BaseCom<U128>>();
            baseTubleParams.Create(multiAddress, baseU128);

            var enumPalletCall = new Hexalem.NET.NetApiExt.Generated.Model.pallet_balances.pallet.EnumCall();
            enumPalletCall.Create(Hexalem.NET.NetApiExt.Generated.Model.pallet_balances.pallet.Call.transfer_keep_alive, baseTubleParams);

            var enumCall = new EnumRuntimeCall();
            enumCall.Create(RuntimeCall.Balances, enumPalletCall);

            return enumCall;
        }
    }

Then, execute BalancesTransferKeepAlive using a similar wrapper to the first example:

        var enumRuntimeCall = PalletBalances.BalancesTransferKeepAlive(target, amount);
        _ = await BatchAllAsync(new List() { enumRuntimeCall }, concurrentTasks, token);

        public async Task<string> BatchAllAsync(List<EnumRuntimeCall> callList, int concurrentTasks, CancellationToken token)
        {
            var extrinsicType = "UtilityCalls.BatchAll";

            if (!IsConnected || Account == null || callList == null || callList.Count == 0)
            {
                return null;
            }

            var calls = new BaseVec<EnumRuntimeCall>();
            calls.Create(callList.ToArray());

            var extrinsic = UtilityCalls.BatchAll(calls);

            return await GenericExtrinsicAsync(Account, extrinsicType, extrinsic, concurrentTasks, token);
        }

Executing an EnumRuntimeCall as Sudo involves:

        public async Task<string> SudoAsync(Account sudoAccount, EnumRuntimeCall call, int concurrentTasks, CancellationToken token)
        {
            var extrinsicType = "Sudo.Sudo";

            if (!IsConnected || sudoAccount == null || call == null)
            {
                return null;
            }

            var extrinsic = SudoCalls.Sudo(call);

            return await GenericExtrinsicAsync(sudoAccount, extrinsicType, extrinsic, concurrentTasks, token);
        }

Creating custom wrappers not only enhances the experience for Unity Developers working on your project but also facilitates the creation of a robust set of unit tests in a decoupled C# project. This allows for comprehensive testing of all integrations upon any changes.

Clone this wiki locally