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 ISigner
s 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:
- examples/RawClient demonstrates usage of the raw API methods exposed in
RawClient
. - examples/Transactions demonstrates how to work with the various transaction types.
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.