C#/.NET Examples¶
Complete, self-contained C# programs demonstrating the Dilithia SDK. Each scenario is a standalone console application with top-level statements or a Main entry point. All examples target .NET 8.0 and use the native crypto bridge via P/Invoke.
Prerequisites¶
NuGet package¶
Native library¶
Set the DILITHIUM_NATIVE_CORE_LIB environment variable to the path of the compiled
Dilithia native core shared library before running any example:
The NativeCryptoBridge constructor reads this variable and loads the library via P/Invoke.
If the variable is missing or blank, it throws InvalidOperationException.
Scenario 1: Balance Monitor Bot¶
A bot that recovers its wallet from a mnemonic, checks its balance, and sends tokens to a destination address when the balance exceeds a threshold. Covers: client setup, wallet recovery, balance query, contract call construction, signing, submission, and receipt polling.
using Dilithia.Sdk;
using Dilithia.Sdk.Models;
using Dilithia.Sdk.Crypto;
using Dilithia.Sdk.Exceptions;
const string RpcUrl = "https://rpc.dilithia.network/rpc";
const string TokenContract = "dil1_token_main";
const string Destination = "dil1_recipient_address";
const long Threshold = 500_000;
const long SendAmount = 100_000;
try
{
// 1. Initialize client with builder and crypto adapter
using var client = DilithiaClient.Create(RpcUrl)
.WithTimeout(TimeSpan.FromSeconds(15))
.Build();
var crypto = new NativeCryptoBridge();
// 2. Recover wallet from saved mnemonic
var mnemonic = Environment.GetEnvironmentVariable("BOT_MNEMONIC")
?? throw new InvalidOperationException("Set BOT_MNEMONIC env var");
if (string.IsNullOrWhiteSpace(mnemonic))
throw new InvalidOperationException("Set BOT_MNEMONIC env var");
var account = crypto.RecoverHdWallet(mnemonic);
Console.WriteLine($"Bot address: {account.Address}");
// 3. Check current balance — returns Balance with typed fields
var balance = await client.GetBalanceAsync(Address.Of(account.Address));
Console.WriteLine($"Current balance: {balance.Value} (raw: {balance.RawValue})");
if (balance.Value.LessThan(TokenAmount.Dili(Threshold.ToString())))
{
Console.WriteLine($"Balance below threshold {Threshold}. Nothing to do.");
return;
}
// 4. Build a contract call to transfer tokens, sign, and submit
DilithiaSigner signer = payload => new SignedPayload(
"mldsa65",
PublicKey.Of(account.PublicKey),
crypto.SignMessage(account.SecretKey, payload).Signature);
// 5. Send the contract call — returns Receipt directly
var receipt = await client.Contract(TokenContract)
.Call("transfer", new Dictionary<string, object>
{
["to"] = Destination,
["amount"] = SendAmount
})
.SendAsync(signer);
Console.WriteLine($"Confirmed at block {receipt.BlockHeight}, status: {receipt.Status}, tx: {receipt.TxHash}");
}
catch (HttpException e)
{
Console.Error.WriteLine($"HTTP error {e.StatusCode}: {e.Message}");
Environment.Exit(1);
}
catch (RpcException e)
{
Console.Error.WriteLine($"RPC error {e.Code}: {e.Message}");
Environment.Exit(1);
}
catch (DilithiaTimeoutException e)
{
Console.Error.WriteLine($"Timeout: {e.Message}");
Environment.Exit(1);
}
catch (Exception e)
{
Console.Error.WriteLine($"Fatal error: {e.Message}");
Console.Error.WriteLine(e.StackTrace);
Environment.Exit(1);
}
Scenario 2: Multi-Account Treasury Manager¶
A service that manages multiple HD wallet accounts derived from a single mnemonic. It derives accounts 0 through 4, checks each balance, and consolidates all funds into account 0. Covers: HD derivation loop, multiple balance queries, and batch transaction construction.
using Dilithia.Sdk;
using Dilithia.Sdk.Models;
using Dilithia.Sdk.Crypto;
using Dilithia.Sdk.Exceptions;
const string RpcUrl = "https://rpc.dilithia.network/rpc";
const string TokenContract = "dil1_token_main";
const int NumAccounts = 5;
try
{
// Initialize client with builder
using var client = DilithiaClient.Create(RpcUrl)
.WithTimeout(TimeSpan.FromSeconds(15))
.Build();
var crypto = new NativeCryptoBridge();
var mnemonic = Environment.GetEnvironmentVariable("TREASURY_MNEMONIC")
?? throw new InvalidOperationException("Set TREASURY_MNEMONIC env var");
if (string.IsNullOrWhiteSpace(mnemonic))
throw new InvalidOperationException("Set TREASURY_MNEMONIC env var");
// 1. Derive all accounts
var accounts = new List<DilithiaAccount>();
for (var i = 0; i < NumAccounts; i++)
{
accounts.Add(crypto.RecoverHdWalletAccount(mnemonic, i));
}
var treasuryAccount = accounts[0];
Console.WriteLine($"Treasury address (account 0): {treasuryAccount.Address}");
// 2. Check balances — GetBalanceAsync returns Balance with typed fields
var balances = new List<Balance>();
for (var i = 0; i < NumAccounts; i++)
{
var bal = await client.GetBalanceAsync(Address.Of(accounts[i].Address));
balances.Add(bal);
Console.WriteLine($" Account {i}: {bal.Address} -> {bal.Value}");
}
// 3. Consolidate from accounts 1-4 to account 0
for (var i = 1; i < NumAccounts; i++)
{
if (balances[i].Value.IsZero())
{
Console.WriteLine($" Account {i}: zero balance, skipping.");
continue;
}
var accountIdx = i;
DilithiaSigner signer = payload => new SignedPayload(
"mldsa65",
PublicKey.Of(accounts[accountIdx].PublicKey),
crypto.SignMessage(accounts[accountIdx].SecretKey, payload).Signature);
Console.WriteLine($" Consolidating {balances[i].Value} from account {i}...");
// Contract().Call().SendAsync() returns Receipt
var receipt = await client.Contract(TokenContract)
.Call("transfer", new Dictionary<string, object>
{
["to"] = treasuryAccount.Address,
["amount"] = balances[i].RawValue
})
.SendAsync(signer);
Console.WriteLine($" Done. Block {receipt.BlockHeight}, status: {receipt.Status}");
}
// 4. Final balance check
var finalBalance = await client.GetBalanceAsync(Address.Of(treasuryAccount.Address));
Console.WriteLine($"\nTreasury final balance: {finalBalance.Value}");
}
catch (DilithiaException e)
{
Console.Error.WriteLine($"SDK error: {e.Message}");
Environment.Exit(1);
}
catch (Exception e)
{
Console.Error.WriteLine($"Fatal error: {e.Message}");
Console.Error.WriteLine(e.StackTrace);
Environment.Exit(1);
}
Scenario 3: Signature Verification Service¶
An API endpoint that receives a signed message and verifies the signature against the claimed public key and address. Covers: address validation, public key validation, signature verification, and structured error handling.
using Dilithia.Sdk;
using Dilithia.Sdk.Models;
using Dilithia.Sdk.Crypto;
var crypto = new NativeCryptoBridge();
try
{
// Generate a keypair and sign a message to test verification
var keypair = crypto.Keygen();
var message = "Login nonce: 98765";
var sig = crypto.SignMessage(keypair.SecretKey, message);
var address = crypto.AddressFromPublicKey(keypair.PublicKey);
Console.WriteLine("Testing with generated keypair:");
Console.WriteLine($" Address: {Address.Of(address)}");
Console.WriteLine($" Public key: {keypair.PublicKey[..32]}...");
// Verify with correct data
var goodResult = Verify(crypto, new VerifyRequest(
keypair.PublicKey, address, message, sig.Signature));
Console.WriteLine($"Valid signature result: {goodResult.Valid}");
// Verify with wrong message
var badResult = Verify(crypto, new VerifyRequest(
keypair.PublicKey, address, "tampered message", sig.Signature));
Console.WriteLine($"Tampered message result: {badResult.Valid}"
+ (badResult.Error is not null ? $" ({badResult.Error})" : ""));
}
catch (Exception e)
{
Console.Error.WriteLine($"Fatal error: {e.Message}");
Console.Error.WriteLine(e.StackTrace);
Environment.Exit(1);
}
static VerifyResult Verify(NativeCryptoBridge crypto, VerifyRequest req)
{
// 1. Validate the public key format
try
{
crypto.ValidatePublicKey(req.PublicKey);
}
catch (Exception e)
{
return new VerifyResult(false, $"Invalid public key: {e.Message}");
}
// 2. Validate the signature format
try
{
crypto.ValidateSignature(req.Signature);
}
catch (Exception e)
{
return new VerifyResult(false, $"Invalid signature: {e.Message}");
}
// 3. Validate the claimed address format using Address.Of
try
{
Address.Of(req.Address);
crypto.ValidateAddress(req.Address);
}
catch (Exception e)
{
return new VerifyResult(false, $"Invalid address: {e.Message}");
}
// 4. Verify the public key maps to the claimed address
try
{
var derived = crypto.AddressFromPublicKey(req.PublicKey);
if (Address.Of(derived) != Address.Of(req.Address))
{
return new VerifyResult(false, "Address does not match public key");
}
}
catch (Exception e)
{
return new VerifyResult(false, $"Address derivation failed: {e.Message}");
}
// 5. Verify the cryptographic signature
try
{
var valid = crypto.VerifyMessage(req.PublicKey, req.Message, req.Signature);
if (!valid)
{
return new VerifyResult(false, "Signature verification failed");
}
}
catch (Exception e)
{
return new VerifyResult(false, $"Verification error: {e.Message}");
}
return new VerifyResult(true, null);
}
record VerifyRequest(string PublicKey, string Address, string Message, string Signature);
record VerifyResult(bool Valid, string? Error);
Scenario 4: Wallet Backup and Recovery¶
Create a new wallet, save the encrypted wallet file to disk, then recover it later from the saved file. Covers the full wallet lifecycle: generate mnemonic, create encrypted wallet file, serialize, write to disk, read from disk, deserialize, and recover.
using System.Text.Json;
using Dilithia.Sdk;
using Dilithia.Sdk.Models;
using Dilithia.Sdk.Crypto;
const string WalletPath = "./my-wallet.json";
const string Password = "my-secure-passphrase";
var jsonOptions = new JsonSerializerOptions { WriteIndented = true };
try
{
var crypto = new NativeCryptoBridge();
if (!File.Exists(WalletPath))
{
// ---- CREATE NEW WALLET ----
Console.WriteLine("No wallet found. Creating a new one...");
// 1. Generate a fresh mnemonic
var mnemonic = crypto.GenerateMnemonic();
Console.WriteLine("SAVE THIS MNEMONIC SECURELY:");
Console.WriteLine(mnemonic);
Console.WriteLine();
// 2. Validate the generated mnemonic
crypto.ValidateMnemonic(mnemonic);
Console.WriteLine("Mnemonic validated successfully.");
// 3. Create an encrypted wallet file
var account = crypto.CreateHdWalletFileFromMnemonic(mnemonic, Password);
Console.WriteLine($"Address: {Address.Of(account.Address)}");
Console.WriteLine($"Public key: {account.PublicKey}");
// 4. Save to disk
var walletJson = JsonSerializer.Serialize(account.WalletFile, jsonOptions);
await File.WriteAllTextAsync(WalletPath, walletJson);
Console.WriteLine($"Wallet saved to {WalletPath}");
// 5. Verify a round-trip sign/verify
var sig = crypto.SignMessage(account.SecretKey, "hello from new wallet");
var ok = crypto.VerifyMessage(account.PublicKey, "hello from new wallet", sig.Signature);
Console.WriteLine($"Sign/verify round-trip: {(ok ? "PASS" : "FAIL")}");
}
else
{
// ---- RECOVER EXISTING WALLET ----
Console.WriteLine("Wallet file found. Recovering...");
// 6. Read and deserialize
var savedJson = await File.ReadAllTextAsync(WalletPath);
var savedWallet = JsonSerializer.Deserialize<Dictionary<string, object>>(savedJson)
?? throw new InvalidOperationException("Failed to deserialize wallet file");
// 7. Recover using mnemonic + password
var mnemonic = Environment.GetEnvironmentVariable("WALLET_MNEMONIC")
?? throw new InvalidOperationException("Set WALLET_MNEMONIC env var to recover");
if (string.IsNullOrWhiteSpace(mnemonic))
throw new InvalidOperationException("Set WALLET_MNEMONIC env var to recover");
var account = crypto.RecoverWalletFile(savedWallet, mnemonic, Password);
Console.WriteLine($"Recovered address: {Address.Of(account.Address)}");
Console.WriteLine($"Recovered public key: {account.PublicKey}");
Console.WriteLine("Wallet recovered successfully. Ready to sign transactions.");
}
}
catch (Exception e)
{
Console.Error.WriteLine($"Fatal error: {e.Message}");
Console.Error.WriteLine(e.StackTrace);
Environment.Exit(1);
}
Scenario 5: Gas-Sponsored Meta-Transaction¶
Submit a transaction where the gas fee is paid by a sponsor contract instead of the sender. Useful for onboarding new users who have no tokens to pay for gas. Covers: client setup, building a paymaster-attached call, signing with the native adapter, and submission.
using Dilithia.Sdk;
using Dilithia.Sdk.Models;
using Dilithia.Sdk.Crypto;
using Dilithia.Sdk.Exceptions;
const string RpcUrl = "https://rpc.dilithia.network/rpc";
const string SponsorContract = "dil1_gas_sponsor_v1";
const string Paymaster = "dil1_paymaster_addr";
const string TargetContract = "dil1_nft_mint";
try
{
// 1. Initialize client with builder and crypto adapter
using var client = DilithiaClient.Create(RpcUrl)
.WithTimeout(TimeSpan.FromSeconds(15))
.Build();
var crypto = new NativeCryptoBridge();
// 2. Recover the user's wallet
var mnemonic = Environment.GetEnvironmentVariable("USER_MNEMONIC")
?? throw new InvalidOperationException("Set USER_MNEMONIC env var");
if (string.IsNullOrWhiteSpace(mnemonic))
throw new InvalidOperationException("Set USER_MNEMONIC env var");
var account = crypto.RecoverHdWallet(mnemonic);
Console.WriteLine($"User address: {Address.Of(account.Address)}");
// 3. Check if the sponsor accepts this call — query returns QueryResult
var acceptResult = await client.Contract(SponsorContract)
.QueryAsync("accept", new Dictionary<string, object>
{
["user"] = account.Address,
["contract"] = TargetContract,
["method"] = "mint"
});
Console.WriteLine($"Sponsor accepts: {acceptResult}");
// 4. Check remaining quota — query returns QueryResult
var quotaResult = await client.Contract(SponsorContract)
.QueryAsync("remaining_quota", new Dictionary<string, object>
{
["user"] = account.Address
});
Console.WriteLine($"Remaining quota: {quotaResult}");
// 5. Get gas estimate — returns GasEstimate
var gasEstimate = await client.Network().GetGasEstimateAsync();
Console.WriteLine($"Current gas estimate: {gasEstimate}");
// 6. Build signer
DilithiaSigner signer = payload => new SignedPayload(
"mldsa65",
PublicKey.Of(account.PublicKey),
crypto.SignMessage(account.SecretKey, payload).Signature);
// 7. Send the sponsored call — returns Receipt
var receipt = await client.Contract(TargetContract)
.Call("mint", new Dictionary<string, object>
{
["token_id"] = "nft_001",
["metadata"] = "ipfs://QmSomeHash"
})
.SendAsync(signer);
Console.WriteLine($"Sponsored tx confirmed at block {receipt.BlockHeight}, status: {receipt.Status}");
}
catch (HttpException e)
{
Console.Error.WriteLine($"HTTP error {e.StatusCode}: {e.Message}");
Environment.Exit(1);
}
catch (RpcException e)
{
Console.Error.WriteLine($"RPC error {e.Code}: {e.Message}");
Environment.Exit(1);
}
catch (DilithiaTimeoutException e)
{
Console.Error.WriteLine($"Timeout: {e.Message}");
Environment.Exit(1);
}
catch (Exception e)
{
Console.Error.WriteLine($"Fatal error: {e.Message}");
Console.Error.WriteLine(e.StackTrace);
Environment.Exit(1);
}
Scenario 6: Cross-Chain Message Sender¶
Send a message to another Dilithia chain via the messaging contract. Useful for bridging data or triggering actions on a remote chain. Covers: client setup, building outbound messages, signing, submission, and receipt polling.
using Dilithia.Sdk;
using Dilithia.Sdk.Models;
using Dilithia.Sdk.Crypto;
using Dilithia.Sdk.Exceptions;
const string RpcUrl = "https://rpc.dilithia.network/rpc";
const string MessagingContract = "dil1_bridge_v1";
const string Paymaster = "dil1_bridge_paymaster";
const string DestChain = "dilithia-testnet-2";
try
{
// 1. Initialize client with builder
using var client = DilithiaClient.Create(RpcUrl)
.WithTimeout(TimeSpan.FromSeconds(15))
.Build();
var crypto = new NativeCryptoBridge();
// 2. Recover sender wallet
var mnemonic = Environment.GetEnvironmentVariable("SENDER_MNEMONIC")
?? throw new InvalidOperationException("Set SENDER_MNEMONIC env var");
if (string.IsNullOrWhiteSpace(mnemonic))
throw new InvalidOperationException("Set SENDER_MNEMONIC env var");
var account = crypto.RecoverHdWallet(mnemonic);
Console.WriteLine($"Sender address: {Address.Of(account.Address)}");
// 3. Resolve a name — returns NameRecord
var resolved = await client.Names().ResolveAsync("alice.dili");
Console.WriteLine($"Resolved alice.dili -> {resolved}");
// 4. Build the cross-chain message payload
var messagePayload = new Dictionary<string, object>
{
["action"] = "lock_tokens",
["sender"] = account.Address,
["amount"] = TokenAmount.Dili("50000"),
["recipient"] = Address.Of("dil1_remote_recipient").Value
};
// 5. Build signer
DilithiaSigner signer = payload => new SignedPayload(
"mldsa65",
PublicKey.Of(account.PublicKey),
crypto.SignMessage(account.SecretKey, payload).Signature);
// 6. Send the cross-chain message call — returns Receipt
var sendArgs = new Dictionary<string, object>
{
["dest_chain"] = DestChain,
["payload"] = messagePayload
};
var receipt = await client.Contract(MessagingContract)
.Call("send_message", sendArgs)
.SendAsync(signer);
Console.WriteLine($"Message tx confirmed at block {receipt.BlockHeight}, status: {receipt.Status}, tx: {receipt.TxHash}");
// 7. Optionally wait for the receipt with explicit polling
var txHash = TxHash.Of(receipt.TxHash.Value);
var polled = await client.GetReceiptAsync(txHash, maxRetries: 12, delay: TimeSpan.FromSeconds(1));
Console.WriteLine($"Polled receipt status: {polled.Status}");
}
catch (DilithiaException e)
{
Console.Error.WriteLine($"SDK error: {e.Message}");
Environment.Exit(1);
}
catch (Exception e)
{
Console.Error.WriteLine($"Fatal error: {e.Message}");
Console.Error.WriteLine(e.StackTrace);
Environment.Exit(1);
}
Scenario 7: Contract Deployment¶
Deploy a WASM smart contract to the Dilithia chain. Reads the WASM binary, validates
the bytecode, builds and signs a canonical deploy payload, assembles the full DeployPayload,
sends the deploy request, and waits for confirmation.
using Dilithia.Sdk;
using Dilithia.Sdk.Models;
using Dilithia.Sdk.Crypto;
using Dilithia.Sdk.Validation;
using Dilithia.Sdk.Exceptions;
const string RpcUrl = "https://rpc.dilithia.network/rpc";
const string ContractName = "my_contract";
const string WasmPath = "./my_contract.wasm";
const string ChainId = "dilithia-mainnet";
try
{
// 1. Initialize client with builder and crypto adapter
using var client = DilithiaClient.Create(RpcUrl)
.WithTimeout(TimeSpan.FromSeconds(30))
.Build();
var crypto = new NativeCryptoBridge();
// 2. Recover wallet from mnemonic
var mnemonic = Environment.GetEnvironmentVariable("DEPLOYER_MNEMONIC")
?? throw new InvalidOperationException("Set DEPLOYER_MNEMONIC env var");
if (string.IsNullOrWhiteSpace(mnemonic))
throw new InvalidOperationException("Set DEPLOYER_MNEMONIC env var");
var account = crypto.RecoverHdWallet(mnemonic);
Console.WriteLine($"Deployer address: {Address.Of(account.Address)}");
// 3. Read the WASM file as hex
var bytecodeHex = DilithiaClient.ReadWasmFileHex(WasmPath);
Console.WriteLine($"Bytecode size: {bytecodeHex.Length / 2} bytes");
// 4. Validate the bytecode before deploying
BytecodeValidator.Validate(bytecodeHex);
var estimatedGas = BytecodeValidator.EstimateDeployGas(bytecodeHex);
Console.WriteLine($"Estimated deploy gas: {estimatedGas}");
// 5. Get the current nonce — returns Nonce with .NextNonce
var nonceResult = await client.GetNonceAsync(Address.Of(account.Address));
Console.WriteLine($"Current nonce: {nonceResult.NextNonce}");
// 6. Hash the bytecode hex for the canonical payload
var bytecodeHash = crypto.HashHex(bytecodeHex);
Console.WriteLine($"Bytecode hash: {bytecodeHash}");
// 7. Build signer
DilithiaSigner signer = payload => new SignedPayload(
"mldsa65",
PublicKey.Of(account.PublicKey),
crypto.SignMessage(account.SecretKey, payload).Signature);
// 8. Build the canonical deploy payload and full deploy payload
var canonicalPayload = DilithiaClient.BuildDeployCanonicalPayload(
ContractName, bytecodeHash, Address.Of(account.Address).Value,
nonceResult.NextNonce, ChainId, 1);
var deployPayload = new DeployPayload(
ContractName,
bytecodeHex,
Address.Of(account.Address).Value,
"mldsa65",
account.PublicKey,
Signature: null, // applied by signer
nonceResult.NextNonce,
ChainId,
1);
// 9. Deploy — returns Receipt
var receipt = await client.DeployContractAsync(deployPayload, signer);
Console.WriteLine($"Contract deployed at block {receipt.BlockHeight}, status: {receipt.Status}, tx: {receipt.TxHash}");
// 10. Verify with explicit receipt lookup using TxHash typed identifier
var verified = await client.GetReceiptAsync(
TxHash.Of(receipt.TxHash.Value), maxRetries: 30, delay: TimeSpan.FromSeconds(3));
Console.WriteLine($"Verified deployment status: {verified.Status}");
// 11. Shielded deposit example (bonus)
// var commitment = "0xabc123...";
// var proof = zkAdapter.GeneratePreimageProof(commitment);
// var shielded = await client.ShieldedDepositAsync(
// commitment, TokenAmount.Dili("100.5"), proof, signer);
}
catch (HttpException e)
{
Console.Error.WriteLine($"HTTP error {e.StatusCode}: {e.Message}");
Environment.Exit(1);
}
catch (RpcException e)
{
Console.Error.WriteLine($"RPC error {e.Code}: {e.Message}");
Environment.Exit(1);
}
catch (DilithiaTimeoutException e)
{
Console.Error.WriteLine($"Timeout: {e.Message}");
Environment.Exit(1);
}
catch (Exception e)
{
Console.Error.WriteLine($"Deploy error: {e.Message}");
Console.Error.WriteLine(e.StackTrace);
Environment.Exit(1);
}
Scenario 8: Shielded Pool Deposit & Withdraw¶
Deposit tokens into the shielded pool and later withdraw them to a recipient address. The deposit creates a commitment from a secret and nonce, then submits it with a ZK preimage proof. The withdrawal computes a nullifier, verifies it has not been spent, fetches the current commitment root, and submits the withdrawal proof. Covers: ZK commitment and nullifier computation, shielded deposit and withdraw RPCs, commitment root queries, nullifier spend checks, and receipt polling.
using Dilithia.Sdk;
using Dilithia.Sdk.Models;
using Dilithia.Sdk.Crypto;
using Dilithia.Sdk.Zk;
using Dilithia.Sdk.Exceptions;
const string RpcUrl = "https://rpc.dilithia.network/rpc";
const string Recipient = "dil1_withdraw_recipient";
const long DepositValue = 250_000;
const long WithdrawAmount = 100_000;
try
{
// 1. Initialize client, crypto bridge, and ZK adapter
using var client = DilithiaClient.Create(RpcUrl)
.WithTimeout(TimeSpan.FromSeconds(15))
.Build();
var crypto = new NativeCryptoBridge();
IDilithiaZkAdapter zk = new NativeZkBridge(); // P/Invoke
// 2. Generate secret and nonce for the commitment
var secretHex = crypto.HashHex("user-secret-entropy-seed");
var nonceHex = crypto.HashHex("user-nonce-entropy-seed");
Console.WriteLine($"Secret: {secretHex[..16]}...");
Console.WriteLine($"Nonce: {nonceHex[..16]}...");
// ---- DEPOSIT ----
// 3. Compute the commitment hash from value, secret, and nonce
var commitment = zk.ComputeCommitment(DepositValue, secretHex, nonceHex);
Console.WriteLine($"Commitment hash: {commitment.Hash}");
// 4. Generate a preimage proof for the deposit
var depositProof = zk.GeneratePreimageProof(new long[] { DepositValue, commitment.Hash.Length });
Console.WriteLine($"Deposit proof generated, inputs: {depositProof.Inputs.Length}");
// 5. Submit the shielded deposit — returns tx hash
var depositTxHash = await client.ShieldedDepositAsync(
commitment.Hash, DepositValue, depositProof.Proof);
Console.WriteLine($"Deposit tx submitted: {depositTxHash}");
// 6. Wait for the deposit receipt
var depositReceipt = await client.WaitForReceiptAsync(depositTxHash, maxAttempts: 20, delay: TimeSpan.FromSeconds(2));
Console.WriteLine($"Deposit confirmed at block {depositReceipt.BlockHeight}, status: {depositReceipt.Status}");
// ---- WITHDRAW ----
// 7. Compute the nullifier from the same secret and nonce
var nullifier = zk.ComputeNullifier(secretHex, nonceHex);
Console.WriteLine($"Nullifier hash: {nullifier.Hash}");
// 8. Check that the nullifier has not already been spent
var alreadySpent = await client.IsNullifierSpentAsync(nullifier.Hash);
if (alreadySpent)
{
Console.Error.WriteLine("Nullifier already spent. Cannot withdraw.");
Environment.Exit(1);
}
Console.WriteLine("Nullifier not yet spent. Proceeding with withdrawal.");
// 9. Fetch the current commitment root
var commitmentRoot = await client.GetCommitmentRootAsync();
Console.WriteLine($"Current commitment root: {commitmentRoot}");
// 10. Generate a range proof to show the withdrawal amount is within bounds
var withdrawProof = zk.GenerateRangeProof(WithdrawAmount, 0, DepositValue);
Console.WriteLine($"Withdraw proof generated, inputs: {withdrawProof.Inputs.Length}");
// 11. Submit the shielded withdrawal
var withdrawTxHash = await client.ShieldedWithdrawAsync(
nullifier.Hash, WithdrawAmount, Recipient, withdrawProof.Proof, commitmentRoot);
Console.WriteLine($"Withdraw tx submitted: {withdrawTxHash}");
// 12. Wait for the withdrawal receipt
var withdrawReceipt = await client.WaitForReceiptAsync(withdrawTxHash, maxAttempts: 20, delay: TimeSpan.FromSeconds(2));
Console.WriteLine($"Withdraw confirmed at block {withdrawReceipt.BlockHeight}, status: {withdrawReceipt.Status}");
// 13. Verify the nullifier is now marked as spent
var spentAfter = await client.IsNullifierSpentAsync(nullifier.Hash);
Console.WriteLine($"Nullifier spent after withdrawal: {spentAfter}");
}
catch (HttpException e)
{
Console.Error.WriteLine($"HTTP error {e.StatusCode}: {e.Message}");
Environment.Exit(1);
}
catch (RpcException e)
{
Console.Error.WriteLine($"RPC error {e.Code}: {e.Message}");
Environment.Exit(1);
}
catch (DilithiaTimeoutException e)
{
Console.Error.WriteLine($"Timeout: {e.Message}");
Environment.Exit(1);
}
catch (Exception e)
{
Console.Error.WriteLine($"Fatal error: {e.Message}");
Console.Error.WriteLine(e.StackTrace);
Environment.Exit(1);
}
Scenario 9: ZK Proof Generation & Verification¶
Generate and verify zero-knowledge proofs using the native ZK bridge. Demonstrates Poseidon hashing, preimage proof generation and verification, and range proof generation and verification. Covers: PoseidonHash, GeneratePreimageProof, VerifyPreimageProof, GenerateRangeProof, VerifyRangeProof, and structured error handling for proof failures.
using Dilithia.Sdk;
using Dilithia.Sdk.Models;
using Dilithia.Sdk.Crypto;
using Dilithia.Sdk.Zk;
using Dilithia.Sdk.Exceptions;
try
{
var crypto = new NativeCryptoBridge();
IDilithiaZkAdapter zk = new NativeZkBridge(); // P/Invoke
// ---- POSEIDON HASHING ----
// 1. Hash a set of values using the Poseidon hash function
var values = new long[] { 42, 100, 999 };
var poseidonHash = zk.PoseidonHash(values);
Console.WriteLine($"Poseidon hash of [{string.Join(", ", values)}]: {poseidonHash}");
// 2. Hash a different set to show determinism
var sameHash = zk.PoseidonHash(new long[] { 42, 100, 999 });
Console.WriteLine($"Same inputs produce same hash: {poseidonHash == sameHash}");
var differentHash = zk.PoseidonHash(new long[] { 42, 100, 1000 });
Console.WriteLine($"Different inputs produce different hash: {poseidonHash != differentHash}");
// ---- PREIMAGE PROOF ----
// 3. Generate a preimage proof — proves knowledge of inputs that hash to a value
var preimageInputs = new long[] { 42, 100, 999 };
var preimageProof = zk.GeneratePreimageProof(preimageInputs);
Console.WriteLine($"\nPreimage proof generated:");
Console.WriteLine($" Proof length: {preimageProof.Proof.Length} chars");
Console.WriteLine($" VK length: {preimageProof.Vk.Length} chars");
Console.WriteLine($" Input count: {preimageProof.Inputs.Length}");
// 4. Verify the preimage proof with correct inputs — should succeed
var preimageValid = zk.VerifyPreimageProof(
preimageProof.Proof, preimageProof.Vk, preimageProof.Inputs);
Console.WriteLine($" Verification (correct inputs): {(preimageValid ? "PASS" : "FAIL")}");
// 5. Verify with tampered inputs — should fail
var tamperedInputs = new long[] { 42, 100, 1000 };
var preimageInvalid = zk.VerifyPreimageProof(
preimageProof.Proof, preimageProof.Vk, tamperedInputs);
Console.WriteLine($" Verification (tampered inputs): {(preimageInvalid ? "PASS" : "FAIL")}");
// ---- RANGE PROOF ----
// 6. Generate a range proof — proves a value lies within [min, max]
long secretValue = 500;
long rangeMin = 0;
long rangeMax = 1000;
var rangeProof = zk.GenerateRangeProof(secretValue, rangeMin, rangeMax);
Console.WriteLine($"\nRange proof generated (value={secretValue}, range=[{rangeMin}, {rangeMax}]):");
Console.WriteLine($" Proof length: {rangeProof.Proof.Length} chars");
Console.WriteLine($" VK length: {rangeProof.Vk.Length} chars");
Console.WriteLine($" Input count: {rangeProof.Inputs.Length}");
// 7. Verify the range proof with correct inputs — should succeed
var rangeValid = zk.VerifyRangeProof(
rangeProof.Proof, rangeProof.Vk, rangeProof.Inputs);
Console.WriteLine($" Verification (correct): {(rangeValid ? "PASS" : "FAIL")}");
// 8. Verify with tampered inputs — should fail
var tamperedRangeInputs = new long[] { 1500, rangeMin, rangeMax };
var rangeInvalid = zk.VerifyRangeProof(
rangeProof.Proof, rangeProof.Vk, tamperedRangeInputs);
Console.WriteLine($" Verification (tampered): {(rangeInvalid ? "PASS" : "FAIL")}");
// ---- COMMITMENT & NULLIFIER ROUND-TRIP ----
// 9. Compute a commitment and its corresponding nullifier
var secretHex = crypto.HashHex("my-secret-data");
var nonceHex = crypto.HashHex("my-unique-nonce");
var commitment = zk.ComputeCommitment(1000, secretHex, nonceHex);
var nullifier = zk.ComputeNullifier(secretHex, nonceHex);
Console.WriteLine($"\nCommitment/Nullifier round-trip:");
Console.WriteLine($" Commitment hash: {commitment.Hash}");
Console.WriteLine($" Commitment value: {commitment.Value}");
Console.WriteLine($" Nullifier hash: {nullifier.Hash}");
// 10. Verify determinism — same inputs always produce same outputs
var commitment2 = zk.ComputeCommitment(1000, secretHex, nonceHex);
var nullifier2 = zk.ComputeNullifier(secretHex, nonceHex);
Console.WriteLine($" Commitment deterministic: {commitment.Hash == commitment2.Hash}");
Console.WriteLine($" Nullifier deterministic: {nullifier.Hash == nullifier2.Hash}");
Console.WriteLine("\nAll ZK proof tests completed.");
}
catch (DilithiaException e)
{
Console.Error.WriteLine($"SDK error: {e.Message}");
Environment.Exit(1);
}
catch (Exception e)
{
Console.Error.WriteLine($"Fatal error: {e.Message}");
Console.Error.WriteLine(e.StackTrace);
Environment.Exit(1);
}
Scenario 10: Name Service & Identity Profile¶
A utility that registers a .dili name, configures profile records, resolves names, and demonstrates the full name service lifecycle. Covers: getRegistrationCost, isNameAvailable, registerName, setNameTarget, setNameRecord, getNameRecords, lookupName, resolveName, reverseResolveName, getNamesByOwner, renewName, transferName, releaseName.
using Dilithia.Sdk;
using Dilithia.Sdk.Models;
using Dilithia.Sdk.Crypto;
using Dilithia.Sdk.Exceptions;
const string RpcUrl = "https://rpc.dilithia.network/rpc";
const string Name = "alice";
const string TransferTo = "dil1_bob_address";
try
{
// 1. Initialize client and crypto adapter
using var client = DilithiaClient.Create(RpcUrl)
.WithTimeout(TimeSpan.FromSeconds(15))
.Build();
var crypto = new NativeCryptoBridge();
// 2. Recover wallet from mnemonic
var mnemonic = Environment.GetEnvironmentVariable("WALLET_MNEMONIC")
?? throw new DilithiaException("Set WALLET_MNEMONIC env var");
var account = crypto.RecoverHdWallet(mnemonic);
Console.WriteLine($"Address: {account.Address}");
// 3. Query registration cost for the name
var cost = await client.GetRegistrationCostAsync(Name);
Console.WriteLine($"Registration cost for \"{Name}\": {cost.Formatted()}");
// 4. Check if name is available
var available = await client.IsNameAvailableAsync(Name);
Console.WriteLine($"Name \"{Name}\" available: {available}");
if (!available)
throw new DilithiaException($"Name \"{Name}\" is already taken");
// 5. Register name
var regHash = await client.RegisterNameAsync(Name, account.Address, account.SecretKey);
var regReceipt = await client.WaitForReceiptAsync(regHash, retries: 20, delayMs: 2000);
Console.WriteLine($"Name registered in block {regReceipt.BlockHeight}");
// 6. Set target address
var targetHash = await client.SetNameTargetAsync(Name, account.Address, account.SecretKey);
await client.WaitForReceiptAsync(targetHash, retries: 20, delayMs: 2000);
Console.WriteLine($"Target address set to {account.Address}");
// 7. Set profile records
var records = new Dictionary<string, string>
{
["display_name"] = "Alice",
["avatar"] = "https://example.com/alice.png",
["bio"] = "Builder on Dilithia",
["email"] = "alice@example.com",
["website"] = "https://alice.dev",
};
foreach (var (key, value) in records)
{
var hash = await client.SetNameRecordAsync(Name, key, value, account.SecretKey);
await client.WaitForReceiptAsync(hash, retries: 20, delayMs: 2000);
Console.WriteLine($" Set record \"{key}\" = \"{value}\"");
}
// 8. Get all records
var allRecords = await client.GetNameRecordsAsync(Name);
Console.WriteLine($"All records: {allRecords}");
// 9. Resolve name to address
var resolved = await client.ResolveNameAsync(Name);
Console.WriteLine($"ResolveName(\"{Name}\") -> {resolved}");
// 10. Reverse resolve address to name
var reverseName = await client.ReverseResolveNameAsync(account.Address);
Console.WriteLine($"ReverseResolveName(\"{account.Address}\") -> {reverseName}");
// 11. List all names by owner
var owned = await client.GetNamesByOwnerAsync(account.Address);
Console.WriteLine($"Names owned by {account.Address}: [{string.Join(", ", owned)}]");
// 12. Renew name
var renewHash = await client.RenewNameAsync(Name, account.SecretKey);
var renewReceipt = await client.WaitForReceiptAsync(renewHash, retries: 20, delayMs: 2000);
Console.WriteLine($"Name renewed in block {renewReceipt.BlockHeight}");
// 13. Transfer name to another address
var transferHash = await client.TransferNameAsync(Name, TransferTo, account.SecretKey);
var transferReceipt = await client.WaitForReceiptAsync(transferHash, retries: 20, delayMs: 2000);
Console.WriteLine($"Name transferred to {TransferTo} in block {transferReceipt.BlockHeight}");
}
catch (DilithiaException e)
{
Console.Error.WriteLine($"Name service error: {e.Message}");
Environment.Exit(1);
}
catch (Exception e)
{
Console.Error.WriteLine($"Fatal error: {e.Message}");
Console.Error.WriteLine(e.StackTrace);
Environment.Exit(1);
}
Scenario 11: Credential Issuance & Verification¶
An issuer creates a KYC credential schema, issues a credential to a holder, and a verifier checks it with selective disclosure. Covers: registerSchema, issueCredential, getSchema, getCredential, listCredentialsByHolder, listCredentialsByIssuer, verifyCredential, revokeCredential.
using Dilithia.Sdk;
using Dilithia.Sdk.Models;
using Dilithia.Sdk.Crypto;
using Dilithia.Sdk.Exceptions;
const string RpcUrl = "https://rpc.dilithia.network/rpc";
const string HolderAddress = "dil1_holder_address";
try
{
// 1. Initialize client and crypto adapter
using var client = DilithiaClient.Create(RpcUrl)
.WithTimeout(TimeSpan.FromSeconds(15))
.Build();
var crypto = new NativeCryptoBridge();
// 2. Recover issuer wallet
var mnemonic = Environment.GetEnvironmentVariable("ISSUER_MNEMONIC")
?? throw new DilithiaException("Set ISSUER_MNEMONIC env var");
var issuer = crypto.RecoverHdWallet(mnemonic);
Console.WriteLine($"Issuer address: {issuer.Address}");
// 3. Register a KYC schema
var schema = new SchemaDefinition("KYC_Basic_v1", new[]
{
new SchemaAttribute("full_name", "string"),
new SchemaAttribute("country", "string"),
new SchemaAttribute("age", "u64"),
new SchemaAttribute("verified", "bool"),
});
var schemaHash = await client.RegisterSchemaAsync(schema, issuer.SecretKey);
var schemaReceipt = await client.WaitForReceiptAsync(schemaHash, retries: 20, delayMs: 2000);
var registeredHash = schemaReceipt.Logs[0]["schema_hash"].ToString()!;
Console.WriteLine($"Schema registered: {registeredHash}");
// 4. Issue credential to holder with commitment hash
var commitmentInput = $"{HolderAddress}:KYC_Basic_v1:{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}";
var commitmentHash = crypto.HashHex(commitmentInput);
var issueHash = await client.IssueCredentialAsync(new CredentialParams
{
SchemaHash = registeredHash,
Holder = HolderAddress,
CommitmentHash = commitmentHash,
Attributes = new Dictionary<string, object>
{
["full_name"] = "Alice Smith",
["country"] = "CH",
["age"] = 30,
["verified"] = true,
},
}, issuer.SecretKey);
var issueReceipt = await client.WaitForReceiptAsync(issueHash, retries: 20, delayMs: 2000);
Console.WriteLine($"Credential issued in block {issueReceipt.BlockHeight}");
// 5. Get schema by hash
var fetchedSchema = await client.GetSchemaAsync(registeredHash);
Console.WriteLine($"Schema: {fetchedSchema}");
// 6. Get credential by commitment
var credential = await client.GetCredentialAsync(commitmentHash);
Console.WriteLine($"Credential: {credential}");
// 7. List credentials by holder
var holderCreds = await client.ListCredentialsByHolderAsync(HolderAddress);
Console.WriteLine($"Holder has {holderCreds.Count} credential(s)");
// 8. List credentials by issuer
var issuerCreds = await client.ListCredentialsByIssuerAsync(issuer.Address);
Console.WriteLine($"Issuer has {issuerCreds.Count} credential(s)");
// 9. Verify selective disclosure — prove age > 18 without revealing exact age
var proof = crypto.GenerateSelectiveDisclosureProof(
commitmentHash,
new[] { "age" },
new Dictionary<string, object>
{
["age"] = new Dictionary<string, object> { ["operator"] = "gt", ["threshold"] = 18 },
});
var verified = await client.VerifyCredentialAsync(commitmentHash, proof);
Console.WriteLine($"Selective disclosure (age > 18) verified: {verified}");
// 10. Revoke the credential
var revokeHash = await client.RevokeCredentialAsync(commitmentHash, issuer.SecretKey);
var revokeReceipt = await client.WaitForReceiptAsync(revokeHash, retries: 20, delayMs: 2000);
Console.WriteLine($"Credential revoked in block {revokeReceipt.BlockHeight}");
// 11. Verify revocation by fetching credential again
var revokedCred = await client.GetCredentialAsync(commitmentHash);
Console.WriteLine($"Credential status after revocation: {revokedCred.Status}");
}
catch (DilithiaException e)
{
Console.Error.WriteLine($"Credential error: {e.Message}");
Environment.Exit(1);
}
catch (Exception e)
{
Console.Error.WriteLine($"Fatal error: {e.Message}");
Console.Error.WriteLine(e.StackTrace);
Environment.Exit(1);
}