ICP for .Net Developers
The Internet Computer Protocol (ICP) is a blockchain technology I find fascinating, but I wanted to work with it using my primary coding language C#/.NET. It didn't seem like anything existed out there, so I decided to write my own. So now that I have released v1.0 Nuget packages, I wanted to share a little about it.
Github Repo: https://github.com/Gekctek/ICP.NET
Nuget Packages:
- EdjCase.ICP.Candid - Candid types and converters
- EdjCase.ICP.Agent - Agents to communicate with ICP
- EdjCase.ICP.ClientGenerator - C# code generators to create API clients from .did files
For an ICP intro, start here: https://f76k2-siaaa-aaaal-aaoba-cai.raw.ic0.app/d/icp-makes-me-want-to-develop-web-3
Candid
Candid is the JSON of the IC world. The main difference is that it includes types like services (API), functions and variants (ENUM where every option has its own type). This causes some issues with translating between C# and Candid types, since C# doesn't have built in support for all the types like variants and unbounded numbers. For more details see https://internetcomputer.org/docs/current/developer-docs/build/languages/candid/candid-concepts
To make this less frustrating EdjCase.ICP.Candid can be used to either define a candid type/value manually:
CandidType nat64Type = new CandidPrimitiveType(PrimitiveType.Nat64);
CandidValue nat64Value = CandidPrimitive.Nat64(1);
CandidType vecType = new CandidVectorType(nat64Type);
CandidValue vecValue = new CandidVector(new CandidValue[]{ nat64Value });
Or convert to/from a custom C# class/type:
ulong nat64 = 1;
CandidValueWithType nat64ValueWithType = CandidValueWithType.FromObject(nat64);
MyObj obj = new MyObj
{
Field1 = "Test",
Field2 = 2
};
CandidValueWithType objValueWithType = CandidValueWithType.FromObject(obj);
HTTP Agent
Currently there is no 'direct' way to communicate with the internet computer from outside of it, rather 'boundary nodes' exist to facilitate it. In order to communicate through a boundary node, http requests can be sent to https://ic0.app with a CBOR payload and the boundary node can parse the request to call a canister. For more details see https://internetcomputer.org/docs/current/references/ic-interface-spec#http-interface.
To make this process easier, the nuget library EdjCase.ICP.Agent can be utilized to handle the sending of Candid objects to canisters.
Example:
Getting proposal information by id
// Url to boundry node
Uri url = new Uri($"https://ic0.app");
// Unauthenticated
var identity = new AnonymousIdentity();
//Build Http Agent
IAgent agent = new HttpAgent(identity, url);
// Governance canister id
Principal canisterId = Principal.FromText("rrkah-fqaaa-aaaaa-aaaaq-cai");
// Method to call in canister
string method = "get_proposal_info";
// Supply method arguments in candid format
CandidArg arg = CandidArg.FromCandid(
CandidValueWithType.FromObject((ulong)1)
);
// Make api call and await response
QueryResponse response = await agent.QueryAsync(canisterId, method, arg);
// Throw if error
QueryReply reply = response.ThrowOrGetReply();
// Convert return value(s) to C# classes
ProposalInfo? proposal = reply.Arg.Values[0].ToObjectOrDefault<ProposalInfo?>();
Service Definition files (.did)
Service definition files can be generated for a canister with the extension .did. Here is an example of what an address book API definition would look like in Candid.
AddressBook.did
type address = record {
street : text;
city : text;
zip_code : nat;
country : text;
};
service address_book : {
set_address: (name : text, addr : address) -> ();
get_address: (name : text) -> (opt address) query;
}
The C# translation would look like
public class Address
{
public string Street { get; set; }
public string City { get; set; }
// Nat doesn't translate directly, using library type
public EdjCase.ICP.Candid.UnboundedUInt ZipCode { get; set; }
public string Country { get; set; }
}
public class AddressBookApiClient
{
public async Task SetAddressAsync(string name, Address addr)
{
// Code to call api route
}
public async Task<Address?> GetAddressAsync(string name)
{
// Code to call api route
}
}
This process of translation can be a be a bit of a pain point to have to do for every canister. Can something like this be automated? Absolutely:
candid-client-generator -f "AddressBook.did" -o "./" -n "MyProject.AddressBook"
In addition to the code libraries, there is an additional library that generates API clients based off of .did files. Nuget package EdjCase.ICP.ClientGenerator can either be referenced in code OR used as a dotnet tool. Dotnet Tool Info: https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools#install-a-global-tool.
Installation
dotnet tool install -g EdjCase.ICP.ClientGenerator
Usage
candid-client-generator -f {service file path} -o {directory to output files to} -n {namespace to give files} [-c {client name}]
Authentication
Authentication is a work in progress and has some issues. Since most authentication right now is browser based, its hard to integrate with a backend server, CLI or even Blazor frontend. I am evaluating building tools around this, but nothing exists right now. There are 2 options to use authentication in .NET right now:
- Interop with JavaScript libraries that handle this. (See my Blazor sample code in https://github.com/Gekctek/ICP.NET)
- Using raw private keys/certificates and supplying that information. Currently only ED25519 is implemented:
byte[] publicKeyBytes = ...;
var publicKey = new ED25519PublicKey(publicKeyBytes);
byte[] privateKeyBytes = ...;
var identity = new ED25519Identity(publicKey, privateKeyBytes);
Need More?
See the https://github.com/Gekctek/ICP.NET/blob/main/README.md for the most up to date info.
For any questions or bugs create an issue at https://github.com/Gekctek/ICP.NET/issues. I am actively working on it, but would love any feedback or contributions.