CI Contributor Covenant NuGet version

C# .NET SDK for interacting with the Concordium blockchain

This is a .NET integration library written in C# which adds support for constructing and sending various transactions, as well as querying various aspects of the Concordium blockchain and its nodes. This SDK uses version 2 of the Concordium Node gRPC API to interact with Concordium nodes and in turn the Concordium blockchain, and serves as a wrapper for this API with added ergonomics. Note that this deprecates earlier versions of the SDK that use version 1 of the API, cfr. the migration section for more details.

Read ahead for a brief overview and some examples, or skip directly the rendered documentation.

Overview

This SDK is currently under development and serves as a wrapper for the Concordium gRPC API with helpers for common tasks.

Implementation-wise, this is first and foremost accomplished by exposing "minimal" wrappers for classes generated directly from the protocol buffer definitions of the API using the Grpc.Tools and Grpc.Net.Client packages. This generation step results in a "raw" client class which exposes a method corresponding to each service definition in the protocol buffer definition as well as a class corresponding to each type declared in the API. These are used at the interface of the raw client class. The wrappers are minimal in the sense that they are identical to those of the generated classes but devoid from the complexity of having to specify parameters for connection handling with each call. A drawback of this approach is that the generated classes are devoid of checks for any semantic invariants, which leaves much to be desired. See Using the raw client API for more information.

To remedy this, the SDK provides its own class equivalents corresponding to the most common raw API types, as well as wrappers for a subset of the raw service methods specifying these class equivalents at their interfaces instead. These transparently map input and output types to and from their native equivalents as they are marshalled across the underlying raw API, enforcing the invariants of the input and output data. Similarly, the SDK provides functionality for working with and signing account transactions, as well as for importing keys and implementing signing logic. The latter can be useful e.g. when delegating signing to a HSM. See Working with transaction signers for more information.

Currently, helpers for working with transactions of the Transfer, TransferWithMemo and RegisterData kind are provided. Ergonomic APIs that use the aforementioned native class equivalents of the raw API types at their interfaces are provided for GetNextAccountSequenceNumber and SendAccountTransaction. For more information on how to create and submit transactions, respectively using the ergonomic APIs, see the Working with account transactions section, respectively Using the client API. Further transactions and wrappers are implemented on a per-need basis.

Prerequisites/compatibility

  • .NET Framework: 6.0 or later.
  • Concordium Node version compatibility: 6.*
  • On macOS currently only dotnet x86 runtimes are supported.

Installation

The SDK is published on nuget.org. Depending on your setup, it can be added to your project as a dependency by running either

PM> Install-Package Concordium.SDK -Version 4.4

or

dotnet add package Concordium.SDK

in your project root. It can also be used as a GIT submodule by embedding the cloned repository directly into your project:

git clone https://github.com/Concordium/concordium-net-sdk --recurse-submodules

Rust bindings

The .NET SDK uses Foreign Function Interface (FFI) functions to code written in Rust. The Rust code is located in ./rust-bindings.

Whenever the .NET SDK is build the Rust code is automatically compiled. This is done as a PreBuildEvent defined in the Concordium.Sdk.csproj file. Hence any changes to the Rust code will take effect on the next build of the .NET SDK.

You can build the Rust code manually by running

cargo build --manifest-path=../rust-bindings/Cargo.toml --release

Basic usage

At the core of the SDK is the ConcordiumClient class which is used to connect to a Concordium node and exposes methods interacting with it. The ConcordiumClient can be instantiated as follows:

using Concordium.Sdk.Client;

// Construct the client.
ConcordiumClient client = new ConcordiumClient(
  new Uri("https://localhost/"), // Endpoint URL.
  20000, // Port.
  60 // Connection timeout in seconds.
);

The ConcordiumClient constructor also optionally takes a GrpcChannelOptions object which can be used to specify various settings specific to the underlying GrpcChannel instance which handles the communication with the node. These could be settings that dictate the retry policy or specify parameters for the keepalive ping, which can be vital to the robustness of the application.

Working with account transactions

Account transactions are blockchain transactions that are signed by and submitted on the behalf of an account. Classes relating to account transactions live in the Concordium.SDK.Transactions namespace. All account transactions are modeled by records that inherit from the abstract record AccountTransactionPayload. Inheriting records contain data specific to the transaction it models. One example of such is the Transfer record representing the transfer of a CCD amount from one account to another. It is instantiated as follows:

using Concordium.Sdk.Types;
using Concordium.Sdk.Transactions;

CcdAmount amount = CcdAmount.FromCcd(100); // Send 100 CCD.
AccountAddress receiver = AccountAddress.From("4rvQePs6ZKFiW8rwY5nP18Uj2DroWiw9VPKTsTwfwmsjcFCJLy");
Transfer transfer = new Transfer(amount, receiver);

Since account transactions are submitted on behalf of an account, any AccountTransactionPayload must be annotated with an AccountAddress of the account submitting the transaction, an account-specific AccountSequenceNumber (nonce) which is used to mitigate replay attacks and an Expiry representing a point in time after which the transaction will not be included in blocks whose (slot) time lies beyond it. The result of the annotation is a PreparedAccountTransaction:

AccountAddress sender = AccountAddress.From("3jfAuU1c4kPE6GkpfYw4KcgvJngkgpFrD9SkDBgFW3aHmVB5r1");
SequenceNumber sequenceNumber = Client.GetNextAccountSequenceNumber(sender).Item1;
Expiry expiry = Expiry.AtMinutesFromNow(10); // Transaction should expire 10 minutes after current system time.
PreparedAccountTransaction preparedTransfer = transfer.Prepare(sender, sequenceNumber, expiry);

Finally, the transaction must be signed using an ITransactionSigner which implements signing with the (secret) sign keys of the sender account, producing a SignedAccountTransaction. In the following example the implementation used is the WalletAccount class which supports importing of account keys from the Concordium browser-wallet key export format:

ITransactionSigner signer = WalletAccount.FromWalletKeyExportFormat("/path/to/exported-browser-wallet-keys.json");
Expiry expiry = Expiry.AtMinutesFromNow(10); // Transaction expires 10 minutes after the current system time.
SignedAccountTransaction signedTransfer = preparedTransfer.Sign(signer);

A signed transaction can be submitted to the blockchain by invoking the SendAccountTransaction method. If the transfer was accepted by the node, its TransactionHash used to uniquely identify the transaction is returned:

TransactionHash = client.SendAccountTransaction(signedTransfer);

The TransactionHash can subsequently be used for querying the status of the transaction by invoking the GetBlockItemStatus method of the RawClient, accessible throughclient.Raw in the above example. For more info on the raw API calls, see the Using the raw client API section.

Working with transaction signers

As described in the Account transactions section, account transactions must be signed using implementations of ITransactionSigner. The SDK ships with the TransactionSigner class which is dictionary based implementation to which ISigners can be added. An ISigner represents a concrete implementation of a (secret) sign key for an account, and can be used to to write custom signer implementations which can be useful, for instance, if delegating the signing logic to a HSM. The SDK also ships with the WalletAccount class which provides functionality for importing account keys from one of the supported wallet export formats. Currently the Concordium browser and genesis wallet key export (JSON) formats are supported.

Using the client API

A small subset of the raw methods generated from the Concordium Node gRPC API protocol buffer definitions have corresponding ergonomic wrappers that transparently map input and output types to and from their native equivalents as they are marshalled across the underlying generated API they wrap. One example of such is GetNextAccountSequenceNumber which takes an AccountAddress and returns a AccountSequenceNumber:

AccountAddress sender = AccountAddress.From("3jfAuU1c4kPE6GkpfYw4KcgvJngkgpFrD9SkDBgFW3aHmVB5r1");
AccountSequenceNumber sequenceNumber = client.GetNextAccountSequenceNumber(sender).Item1;

Using the raw client API

The entire Concordium Node gRPC API V2 is exposed through minimal wrappers of classes that model the interface types and services as they were generated from the protocol buffer schema definitions using the Grpc.Tools and Grpc.Net.Client packages. These wrappers are defined in the RawClient, instances of which can only be accessed through the Raw field of ConcordiumClient instances.

As an example, the raw API call GetAccountInfo defined in the Concordium Node gRPC API V2 takes as its input a gRPC message of the AccountInfoRequest kind and expects a gRPC response of the AccountInfo kind. This method can be invoked through RawClient.GetAccountInfo by supplying an instance of its corresponding generated type for AccountInfoRequest. In the following, we wish to retrieve the information of an account in the last finalized block:

using Concordium.Grpc.V2;

BlockHashInput blockHashInput = new BlockHashInput() { LastFinal = new Empty() };

// Construct the input for the raw API.
AccountInfoRequest request = new AccountInfoRequest
{
  BlockHash = blockHashInput,
  /// Instantiate and convert Concordium.Sdk.Types.AccountAddress
  /// to an AccountIdentifierInput which is needed for the
  /// AccountInfoRequest.
  AccountIdentifier = Concordium.Sdk.Types.AccountAddress
    .From("4rvQePs6ZKFiW8rwY5nP18Uj2DroWiw9VPKTsTwfwmsjcFCJLy")
    .ToAccountIdentifierInput()
};

AccountInfo accountInfo = client.Raw.GetAccountInfo(request);

Note that all generated types live in the Concordium.Grpc.V2 namespace, and that there is a vast overlap between the names generated from the protocol buffer definitions file and the native types that live in the Concordium.Types namespace. In the above we must therefore explicitly specify the namespace for Concordium.Sdk.Types.AccountAddress to resolve this ambiguity. Furthermore we leverage the convenience method Concordium.Sdk.Types.AccountAddress.ToAccountIdentifierInput to easily convert the the base58 address into its corresponding raw format. Also note that the overall structure of the interface types are one-to-one with the Concordium gRPC API V2, so we refer to its documentation and the Runnable examples section for more information on working with the types of the raw API.

Runnable examples

A number of runnable examples illustrating usage of the SDK API are contained in the examples directory:

For instance, a runnable example akin to that given in Working with account transactions is found in examples/Transactions/Transfer. Compiling the project and running the resulting binary will print the following help message:

Copyright (C) 2023 Concordium.Sdk.Examples.Transactions.Transfer

ERROR(S):
  Required option 'a, amount' is missing.
  Required option 'r, receiver' is missing.
  Required option 'k, keys' is missing.

  -a, --amount      Required. Amount of CCD to transfer.

  -r, --receiver    Required. Receiver of the CCD to transfer.

  -k, --keys        Required. Path to a file with contents that is in the Concordium
                    browser wallet key export format.

  -e, --endpoint    (Default: https://localhost/) URL representing the endpoint where the
                    gRPC V2 API is served.

  -p, --port        (Default: 20000) Port at the endpoint where the gRPC V2 API is served.

  -t, --timeout     (Default: 60) Default connection timeout in seconds.

  --help            Display this help screen.

  --version         Display version information.

To run the example with similar values, invoke the binary as follows:

Concordium.Sdk.Examples.Transactions.Transfer -a 100 -r 4rvQePs6ZKFiW8rwY5nP18Uj2DroWiw9VPKTsTwfwmsjcFCJLy -k /path/to/exported-browser-wallet-keys.json

Here, the sender account address is contained in the file specified by the --keys option which is why it is not included here. Upon successful submission of the transaction, the example program will print something like:

Successfully submitted transfer transaction with hash 6bc9bfac5ef4aa1988ab8b1ab6007a736d4b3fe7e52b942d69a030319d979f13

Documentation

Rendered documentation for this project is available here.

Migration

This deprecates earlier versions of the Concordium .NET SDK that used version 1 of the Concordium Node gRPC API. In terms of semantics and the information carried in messages, the APIs are quite similar, so APIs of the older SDK versions have corresponding raw methods in this version. Note that in some cases endpoints in the version 1 API are "split" into several endpoints in the version 2 API to increase the granularity.

Another major difference between this and the previous version of the SDK is that this version of the SDK currently does not support deploying contract modules with schemas.

Test data

Data for both C# and Rust tests has been generated by creating contracts and modules in an environment, primarily on testnet. For detailed documentation on contract development, refer to this link.

To obtain on-chain data, a Concordium client, such as the one in the .NET SDK, has been utilized. For instance, to retrieve module source data, one can use GetModuleSourceAsync as an example.

When fetching data in binary format, the best practice is to store it in its raw form and avoid any encoding.

License

This project is licensed under the terms of the Mozilla Public License 2.0.

For more information, please refer to the LICENSE file.

Contributing

Contributions are welcomed. Guidelines for contribution can be found here. GitHub workflows specify CI jobs for building, unit testing and formatting. Passing build jobs is a requirement for a pull request to be considered eligible for merging. The formatting rules are specified in the EditorConfig format and are found in the .editorconfig file at the root of the project.

Acknowledgements

This project is developed and maintained by the Concordium Foundation.