Building Ixian Client Apps
This guide shows you how to build custom Ixian client applications using the Ixian-Core library. Client apps are lightweight nodes that don't maintain the full blockchain but can send transactions, manage contacts, and communicate via the Ixian P2P network.
What is an Ixian Client?
Ixian clients are applications that:
- Connect to the Ixian P2P network as light clients
- Send and receive transactions
- Manage contacts and encrypted messaging
- Don't store the full blockchain (query DLT nodes instead)
- Can be gateways, bots, IoT devices, or custom applications
Client vs Full Node
| Feature | Client (Light Node) | Full Node (DLT) |
|---|---|---|
| Blockchain Storage | No (queries remote nodes) | Yes (full copy) |
| Connection Type | Connects to S2 (streaming) nodes | Direct P2P connections |
| Transaction Validation | Basic (signatures only) | Full (consensus rules) |
| Block Generation | No | Yes |
| Resource Usage | Low (MB of RAM) | High (GBs of RAM, TBs of disk) |
| Use Cases | Wallets, gateways, bots | Network backbone, mining |
Architecture Overview
Your Application
↓ (implements)
IxianNode (abstract class)
↓ (uses)
Ixian-Core Components
├── Wallet Management
├── Transaction Processing
├── StreamClientManager (connects to S2 nodes)
├── NetworkClientManager (queries DLT via S2)
├── Presence System (Starling sector routing)
├── Cryptography
└── Storage
Prerequisites
- .NET 8.0 SDK or later
- Visual Studio 2026 or VS Code with C# extension
- Basic understanding of C# and async programming
- Familiarity with Ixian concepts (addresses, transactions, presence)
Getting Started
Step 1: Create a New Project
# Create a new console application
dotnet new console -n MyIxianClient
cd MyIxianClient
# Clone Ixian-Core alongside your project
cd ..
git clone https://github.com/ixian-platform/Ixian-Core.git
cd MyIxianClient
Step 2: Add Ixian-Core Reference
Edit your .csproj file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<!-- Import Ixian-Core shared project -->
<Import Project="..\Ixian-Core\IXICore.projitems" Label="Shared" />
<ItemGroup>
<!-- Ixian-Core dependencies -->
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Open.Nat" Version="2.1.0" />
</ItemGroup>
</Project>
Step 3: Implement IxianNode
Create Node.cs:
using IXICore;
using IXICore.Inventory;
using IXICore.Meta;
using IXICore.Network;
using IXICore.Network.Messages;
using IXICore.RegNames;
using IXICore.Storage;
using IXICore.Streaming;
using IXICore.Utils;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using static IXICore.Transaction;
namespace IxianClient
{
public class Node : IxianNode
{
private bool running = false;
private Thread? mainLoopThread = null;
private TransactionInclusion? tiv = null;
private long lastSectorUpdate = 0;
private NetworkClientManagerStatic? networkClientManagerStatic = null;
public Node()
{
Init();
}
private void Init()
{
// Initialize IxianHandler with your app version
// Choose network type:
// - NetworkType.main: Production mainnet
// - NetworkType.test: Public testnet
// - NetworkType.regtest: Local Docker sandbox
IxianHandler.init("MyClient v1.0.0", this, NetworkType.main); // Mainnet
// Initialize wallet
if (!InitWallet())
{
throw new Exception("Failed to initialize wallet");
}
// Initialize peer storage
PeerStorage.init("");
// Setup network client manager (queries blockchain via S2)
networkClientManagerStatic = new NetworkClientManagerStatic(10);
NetworkClientManager.init(networkClientManagerStatic);
// Init TransactionInclusion (TIV) for block header verification
tiv = new TransactionInclusion(new ICTransactionInclusionCallbacks(), false);
// Initialize presence list with keepalive
PresenceList.init("", 0, 'C', CoreConfig.clientKeepAliveInterval);
// Initialize local storage
IxianHandler.localStorage = new LocalStorage("", new ICLocalStorageCallbacks());
// Initialize inventory cache
InventoryCache.init(new InventoryCacheClient(tiv));
// Initialize relay sectors for Starling routing
RelaySectors.init(CoreConfig.relaySectorLevels, null);
Console.WriteLine($"Node initialized. Wallet: {IxianHandler.getWalletStorage().getPrimaryAddress()}");
}
private bool InitWallet()
{
string walletPath = "wallet.ixi";
WalletStorage walletStorage = new WalletStorage(walletPath);
// Wait for any pending log messages to be written
Logging.flush();
if (!walletStorage.walletExists())
{
ConsoleHelpers.displayBackupText();
// Request a password
string password = "";
while (password.Length < 10)
{
Logging.flush();
password = ConsoleHelpers.requestNewPassword("Enter a password for your new wallet: ");
if (IxianHandler.forceShutdown)
{
return false;
}
}
walletStorage.generateWallet(password);
}
else
{
ConsoleHelpers.displayBackupText();
bool success = false;
while (!success)
{
string password = "";
if (password.Length < 10)
{
Logging.flush();
Console.Write("Enter wallet password: ");
password = ConsoleHelpers.getPasswordInput();
}
if (IxianHandler.forceShutdown)
{
return false;
}
if (walletStorage.readWallet(password))
{
success = true;
}
}
}
if (walletStorage.getPrimaryPublicKey() == null)
{
return false;
}
// Display wallet addresses
Logging.flush();
Console.WriteLine();
Console.WriteLine("Your IXIAN addresses are: ");
Console.ForegroundColor = ConsoleColor.Green;
foreach (var entry in walletStorage.getMyAddressesBase58())
{
Console.WriteLine(entry);
}
Console.ResetColor();
Console.WriteLine();
Logging.info("Public Node Address: {0}", walletStorage.getPrimaryAddress().ToString());
if (walletStorage.viewingWallet)
{
Logging.error("Viewing-only wallet {0} cannot be used as the primary wallet.", walletStorage.getPrimaryAddress().ToString());
return false;
}
IxianHandler.addWallet(walletStorage);
// Prepare the balances list
List<Address> address_list = IxianHandler.getWalletStorage().getMyAddresses();
foreach (Address addr in address_list)
{
IxianHandler.balances.Add(new Balance(addr, 0));
}
return true;
}
public void Start()
{
if (running) return;
running = true;
// Start Transaction Inclusion verification
tiv?.start("headers", 0, null, true);
// Start presence keepalive (announces our presence to network)
PresenceList.startKeepAlive();
// Start the network queue for message processing
NetworkQueue.start();
// Connect to your sector of S2 nodes
NetworkClientManager.start(1);
// Connect to S2 streaming nodes (for presence and messaging)
StreamClientManager.start(6, true);
// Start main loop for periodic tasks
mainLoopThread = new Thread(MainLoop);
mainLoopThread.Start();
Console.WriteLine("Node started and connecting to network...");
}
public void Stop()
{
running = false;
mainLoopThread?.Join();
PeerStorage.savePeersFile(true);
tiv?.stop();
PresenceList.stopKeepAlive();
NetworkQueue.stop();
NetworkClientManager.stop();
StreamClientManager.stop();
Console.WriteLine("Node stopped.");
}
private void MainLoop()
{
while (running)
{
try
{
// Fetch sector nodes periodically
RequestSectorUpdate();
// Request balance update periodically
RequestBalanceUpdate();
// Check for balance changes
CheckBalanceChanges();
// Process pending transactions (resend if needed, check status)
processPendingTransactions();
// Cleanup old presence entries
PresenceList.performCleanup();
// Save peer data
PeerStorage.savePeersFile();
}
catch (Exception e)
{
Console.WriteLine($"Error in main loop: {e.Message}");
}
Thread.Sleep(5000);
}
}
private void RequestSectorUpdate()
{
if (lastSectorUpdate + 300 < Clock.getTimestamp())
{
lastSectorUpdate = Clock.getTimestamp();
CoreProtocolMessage.fetchSectorNodes(IxianHandler.primaryWalletAddress, CoreConfig.maxRelaySectorNodesToRequest);
}
}
private void RequestBalanceUpdate()
{
var balance = IxianHandler.balances.FirstOrDefault();
if (balance == null || balance.lastUpdate + 300 < Clock.getTimestamp())
{
var primaryAddress = IxianHandler.getWalletStorage().getPrimaryAddress();
// Create balance request message
byte[] getBalanceBytes;
using (var ms = new MemoryStream())
{
using (var writer = new BinaryWriter(ms))
{
writer.WriteIxiVarInt(primaryAddress.addressNoChecksum.Length);
writer.Write(primaryAddress.addressNoChecksum);
}
getBalanceBytes = ms.ToArray();
}
// Broadcast balance request to network
CoreProtocolMessage.broadcastProtocolMessage(
new char[] { 'M', 'H', 'R' },
ProtocolMessageCode.getBalance2,
getBalanceBytes,
null
);
}
}
// ===== IxianNode Abstract Method Implementations =====
public override ulong getHighestKnownNetworkBlockHeight()
{
ulong bh = getLastBlockHeight();
ulong netBlockNum = CoreProtocolMessage.determineHighestNetworkBlockNum();
if (bh < netBlockNum)
{
bh = netBlockNum;
}
return bh;
}
public override Block getBlockHeader(ulong blockNum)
{
return BlockHeaderStorage.getBlockHeader(blockNum);
}
public override byte[] getBlockHash(ulong blockNum)
{
var block = getBlockHeader(blockNum);
return block?.blockChecksum ?? null;
}
public override Block getLastBlock()
{
return tiv?.getLastBlockHeader() ?? null;
}
public override ulong getLastBlockHeight()
{
if (tiv?.getLastBlockHeader() == null)
{
return 0;
}
return tiv.getLastBlockHeader().blockNum;
}
public override int getLastBlockVersion()
{
return Block.maxVersion;
}
public override bool addTransaction(Transaction tx, List<Address> relayNodeAddresses, bool force_broadcast)
{
foreach (var address in relayNodeAddresses)
{
NetworkClientManager.sendToClient(address, ProtocolMessageCode.transactionData2, tx.getBytes(true, true), null);
}
PendingTransactions.addPendingLocalTransaction(tx, relayNodeAddresses);
return true;
}
public override bool isAcceptingConnections()
{
return false; // Clients don't accept incoming connections
}
public override Wallet getWallet(Address id)
{
foreach (Balance balance in IxianHandler.balances)
{
if (id.addressNoChecksum.SequenceEqual(balance.address.addressNoChecksum))
return new Wallet(id, balance.balance);
}
return new Wallet(id, 0);
}
public override IxiNumber getWalletBalance(Address id)
{
var balance = IxianHandler.balances.FirstOrDefault(b => b.address.SequenceEqual(id));
return balance?.balance ?? new IxiNumber(0);
}
public override void parseProtocolMessage(ProtocolMessageCode code, byte[] data, RemoteEndpoint endpoint)
{
// Custom protocol message handling - see MyIxianClient for full implementation
// Handles: hello, helloData, balance2, updatePresence, blockHeaders3, transactionData2, etc.
CoreProtocolMessage.handleProtocolMessage(code, data, endpoint);
}
public override void shutdown()
{
Stop();
}
public override IxiNumber getMinSignerPowDifficulty(ulong blockNum, int curBlockVersion, long curBlockTimestamp)
{
return ConsensusConfig.minBlockSignerPowDifficulty;
}
public override RegisteredNameRecord getRegName(byte[] name, bool useAbsoluteId)
{
throw new NotImplementedException();
}
public (Transaction transaction, List<Address> relayNodeAddresses) prepareTransactionFrom(Address fromAddress, Address toAddress, IxiNumber amount)
{
IxiNumber fee = ConsensusConfig.forceTransactionPrice;
Dictionary<Address, ToEntry> to_list = new(new AddressComparer());
Address pubKey = new(IxianHandler.getWalletStorage().getPrimaryPublicKey());
if (!IxianHandler.getWalletStorage().isMyAddress(fromAddress))
{
Logging.info("From address is not my address.");
return (null, null);
}
Dictionary<byte[], IxiNumber> from_list = new(new ByteArrayComparer())
{
{ IxianHandler.getWalletStorage().getAddress(fromAddress).nonce, amount }
};
to_list.AddOrReplace(toAddress, new ToEntry(Transaction.maxVersion, amount));
List<Address> relayNodeAddresses = NetworkClientManager.getRandomConnectedClientAddresses(2);
IxiNumber relayFee = 0;
foreach (Address relayNodeAddress in relayNodeAddresses)
{
var tmpFee = fee > ConsensusConfig.transactionDustLimit ? fee : ConsensusConfig.transactionDustLimit;
to_list.AddOrReplace(relayNodeAddress, new ToEntry(Transaction.maxVersion, tmpFee));
relayFee += tmpFee;
}
// Prepare transaction to calculate fee
Transaction transaction = new((int)Transaction.Type.Normal, fee, to_list, from_list, pubKey, IxianHandler.getHighestKnownNetworkBlockHeight());
relayFee = 0;
foreach (Address relayNodeAddress in relayNodeAddresses)
{
var tmpFee = transaction.fee > ConsensusConfig.transactionDustLimit ? transaction.fee : ConsensusConfig.transactionDustLimit;
to_list[relayNodeAddress].amount = tmpFee;
relayFee += tmpFee;
}
byte[] first_address = from_list.Keys.First();
from_list[first_address] = from_list[first_address] + relayFee + transaction.fee;
IxiNumber wal_bal = IxianHandler.getWalletBalance(new Address(transaction.pubKey.addressNoChecksum, first_address));
if (from_list[first_address] > wal_bal)
{
IxiNumber maxAmount = wal_bal - transaction.fee;
if (maxAmount < 0)
maxAmount = 0;
Console.WriteLine($"Insufficient funds to cover amount and transaction fee.\nMaximum amount you can send is {maxAmount} IXI.\n");
return (null, null);
}
// Prepare transaction with updated "from" amount to cover fee
transaction = new((int)Transaction.Type.Normal, fee, to_list, from_list, pubKey, IxianHandler.getHighestKnownNetworkBlockHeight());
return (transaction, relayNodeAddresses);
}
public Transaction sendTransactionFrom(Address fromAddress, Address toAddress, IxiNumber amount)
{
var prepTx = prepareTransactionFrom(fromAddress, toAddress, amount);
var transaction = prepTx.transaction;
var relayNodeAddresses = prepTx.relayNodeAddresses;
if (transaction == null || relayNodeAddresses == null)
{
return null;
}
// Send the transaction
if (IxianHandler.addTransaction(transaction, relayNodeAddresses, true))
{
Console.WriteLine($"Sending transaction, txid: {transaction.getTxIdString()}");
return transaction;
}
else
{
Console.WriteLine($"Could not send transaction, txid: {transaction.getTxIdString()}");
}
return null;
}
public bool SendPayment(string toAddress, string amount)
{
try
{
var recipient = new Address(toAddress);
var txAmount = new IxiNumber(amount);
var myAddress = IxianHandler.getWalletStorage().getPrimaryAddress();
var tx = sendTransactionFrom(myAddress, recipient, txAmount);
return tx != null;
}
catch (Exception e)
{
Console.WriteLine($"Error sending payment: {e.Message}");
return false;
}
}
private IxiNumber lastKnownBalance = new IxiNumber(0);
private void CheckBalanceChanges()
{
var myAddress = IxianHandler.getWalletStorage().getPrimaryAddress();
var currentBalance = IxianHandler.getWalletBalance(myAddress);
if (currentBalance != lastKnownBalance)
{
Console.WriteLine($"\n*** Balance changed: {lastKnownBalance} -> {currentBalance} IXI ***\n");
lastKnownBalance = currentBalance;
}
}
// Query your own balance
public IxiNumber GetMyBalance()
{
var myAddress = IxianHandler.getWalletStorage().getPrimaryAddress();
return IxianHandler.getWalletBalance(myAddress);
}
// Query any address balance (returns cached value, use RequestBalanceUpdate to refresh)
public IxiNumber GetBalance(string address)
{
var addr = new Address(address);
return IxianHandler.getWalletBalance(addr);
}
// Request fresh balance from network for specific address
public void RequestBalanceUpdate(Address address)
{
byte[] getBalanceBytes;
using (var ms = new MemoryStream())
{
using (var writer = new BinaryWriter(ms))
{
writer.WriteIxiVarInt(address.addressNoChecksum.Length);
writer.Write(address.addressNoChecksum);
}
getBalanceBytes = ms.ToArray();
}
CoreProtocolMessage.broadcastProtocolMessage(
new char[] { 'M', 'H', 'R' },
ProtocolMessageCode.getBalance2,
getBalanceBytes,
null
);
}
// Check if an address is online (from local cache)
public bool IsAddressOnline(string address)
{
var addr = new Address(address);
var presence = PresenceList.getPresenceByAddress(addr);
return presence != null;
}
// Get presence details for an address (from local cache)
public Presence? GetPresence(string address)
{
var addr = new Address(address);
return PresenceList.getPresenceByAddress(addr);
}
// Request sector nodes from the network that handle a specific address's sector
// The sector is determined by the first 10 bytes of the address's sector prefix
public void RequestSector(string address)
{
var addr = new Address(address);
Console.WriteLine($"Requesting sectors for {address}...");
// Fetch relay nodes (S2 nodes) that are responsible for this address's sector
CoreProtocolMessage.fetchSectorNodes(addr, CoreConfig.maxRelaySectorNodesToRequest);
// The network will respond with sectorNodes message containing relay node presences
// This is handled by HandleSectorNodes() which updates RelaySectors and PeerStorage
}
// Request presence information for a specific address from the network
// Uses the sector-based routing system to query the appropriate relay nodes
public void RequestPresence(string address)
{
var addr = new Address(address);
Console.WriteLine($"Requesting presence for {address}...");
// Create a temporary friend object to use the sector-based presence fetching mechanism
var friend = new Friend(FriendState.Approved, addr, null, "", null, null, 0, true);
// Get relay nodes responsible for this address's sector from local cache
List<Peer> peers = new();
var relays = RelaySectors.Instance.getSectorNodes(addr.sectorPrefix, CoreConfig.maxRelaySectorNodesToRequest);
foreach (var relay in relays)
{
var p = PresenceList.getPresenceByAddress(relay);
if (p == null)
{
continue;
}
var pa = p.addresses.First();
peers.Add(new(pa.address, relay, pa.lastSeenTime, 0, 0, 0));
}
// Assign sector nodes to the friend and request presence via streaming protocol
friend.sectorNodes = peers;
friend.updatedSectorNodes = Clock.getTimestamp();
CoreStreamProcessor.fetchFriendsPresence(friend);
// The network will respond with updatePresence messages
}
// Display presence information for an address
public void DisplayPresenceInfo(string address)
{
var addr = new Address(address);
var presence = PresenceList.getPresenceByAddress(addr);
if (presence == null)
{
Console.WriteLine($" Status: Offline or not found in local cache");
Console.WriteLine($" Tip: The presence might not be cached yet. Wait a few seconds after RequestPresence().");
return;
}
Console.WriteLine($" Status: Online");
Console.WriteLine($" Wallet: {presence.wallet?.ToString() ?? "N/A"}");
Console.WriteLine($" Endpoints: {presence.addresses.Count}");
foreach (var endpoint in presence.addresses)
{
char nodeType = endpoint.type;
string typeDesc = nodeType switch
{
'C' => "Client",
'M' => "Master (DLT)",
'H' => "Host (DLT)",
'R' => "Relay (S2)",
'W' => "Worker",
_ => "Unknown"
};
Console.WriteLine($" - {endpoint.address} (type: {nodeType} - {typeDesc})");
}
}
// Get transaction status
public string GetTransactionStatus(byte[] txid)
{
// Check if transaction is confirmed
Transaction confirmedTx = TransactionCache.getTransaction(txid);
if (confirmedTx != null && confirmedTx.applied != 0)
{
return $"Confirmed in block {confirmedTx.applied}";
}
// Check if transaction is pending
Transaction unconfirmedTx = TransactionCache.getUnconfirmedTransaction(txid);
if (unconfirmedTx != null)
{
return "Pending (waiting for confirmation)";
}
// Check pending transactions list
var pendingTx = PendingTransactions.getPendingTransaction(txid);
if (pendingTx != null)
{
return $"Sent (confirmed by {pendingTx.confirmedNodeList.Count} nodes)";
}
return "Unknown (not found)";
}
// Get transaction status by string txid
public string GetTransactionStatus(string txidString)
{
try
{
byte[] txid = Transaction.txIdLegacyToV8(txidString);
return GetTransactionStatus(txid);
}
catch
{
return "Invalid transaction ID";
}
}
private void SubscribeToEvents(RemoteEndpoint endpoint)
{
CoreProtocolMessage.subscribeToEvents(endpoint);
// Subscribe to friend presences if outgoing stream capabilities are enabled
if ((CoreStreamProcessor.streamCapabilities & StreamCapabilities.Outgoing) != 0)
{
byte[] friend_matcher = FriendList.getFriendCuckooFilter();
if (friend_matcher != null)
{
byte[] event_data = NetworkEvents.prepareEventMessageData(NetworkEvents.Type.keepAlive, friend_matcher);
endpoint.sendData(ProtocolMessageCode.attachEvent, event_data);
}
}
}
private void HandleTransactionData(byte[] data, RemoteEndpoint endpoint)
{
Transaction tx = new Transaction(data, true, true);
if (endpoint.presenceAddress.type == 'M'
|| endpoint.presenceAddress.type == 'H'
|| endpoint.presenceAddress.type == 'R')
{
PendingTransactions.increaseReceivedCount(tx.id, endpoint.presence.wallet);
}
TransactionCache.addUnconfirmedTransaction(tx);
tiv?.receivedNewTransaction(tx);
}
private void HandleSectorNodes(byte[] data, RemoteEndpoint endpoint)
{
int offset = 0;
var prefixAndOffset = data.ReadIxiBytes(offset);
offset += prefixAndOffset.bytesRead;
byte[] prefix = prefixAndOffset.bytes;
var nodeCountAndOffset = data.GetIxiVarUInt(offset);
offset += nodeCountAndOffset.bytesRead;
int nodeCount = (int)nodeCountAndOffset.num;
for (int i = 0; i < nodeCount; i++)
{
var kaBytesAndOffset = data.ReadIxiBytes(offset);
offset += kaBytesAndOffset.bytesRead;
Presence p = PresenceList.updateFromBytes(kaBytesAndOffset.bytes, IxianHandler.getMinSignerPowDifficulty(IxianHandler.getLastBlockHeight() + 1, IxianHandler.getLastBlockVersion(), Clock.getNetworkTimestamp()));
if (p != null)
{
RelaySectors.Instance.addRelayNode(p.wallet);
}
}
List<Peer> peers = new();
var relays = RelaySectors.Instance.getSectorNodes(prefix, CoreConfig.maxRelaySectorNodesToRequest);
foreach (var relay in relays)
{
var p = PresenceList.getPresenceByAddress(relay);
if (p == null)
{
continue;
}
var pa = p.addresses.First();
peers.Add(new(pa.address, relay, pa.lastSeenTime, 0, 0, 0));
PeerStorage.addPeerToPeerList(pa.address, p.wallet, pa.lastSeenTime, 0, 0, 0);
}
if (IxianHandler.primaryWalletAddress.sectorPrefix.SequenceEqual(prefix))
{
networkClientManagerStatic.setClientsToConnectTo(peers);
}
var friends = FriendList.getFriendsBySectorPrefix(prefix);
foreach (var friend in friends)
{
friend.updatedSectorNodes = Clock.getNetworkTimestamp();
friend.sectorNodes = peers;
}
}
private void HandleKeepAlivesChunk(byte[] data, RemoteEndpoint endpoint)
{
using (MemoryStream m = new MemoryStream(data))
{
using (BinaryReader reader = new BinaryReader(m))
{
int ka_count = (int)reader.ReadIxiVarUInt();
int max_ka_per_chunk = CoreConfig.maximumKeepAlivesPerChunk;
if (ka_count > max_ka_per_chunk)
{
ka_count = max_ka_per_chunk;
}
for (int i = 0; i < ka_count; i++)
{
if (m.Position == m.Length)
{
break;
}
int ka_len = (int)reader.ReadIxiVarUInt();
byte[] ka_bytes = reader.ReadBytes(ka_len);
HandleKeepAlivePresence(ka_bytes, endpoint);
}
}
}
}
private void HandleRejected(byte[] data, RemoteEndpoint endpoint)
{
try
{
Rejected rej = new Rejected(data);
switch (rej.code)
{
case RejectedCode.TransactionInvalid:
case RejectedCode.TransactionInsufficientFee:
case RejectedCode.TransactionDust:
Logging.error("Transaction {0} was rejected with code: {1}", Crypto.hashToString(rej.data), rej.code);
PendingTransactions.remove(rej.data);
// TODO flag transaction as invalid
break;
case RejectedCode.TransactionDuplicate:
Logging.warn("Transaction {0} already sent.", Crypto.hashToString(rej.data), rej.code);
// All good
PendingTransactions.increaseReceivedCount(rej.data, endpoint.serverWalletAddress);
break;
default:
Logging.error("Received 'rejected' message with unknown code {0} {1}", rej.code, Crypto.hashToString(rej.data));
break;
}
}
catch (Exception e)
{
throw new Exception(string.Format("Exception occured while processing 'rejected' message with code {0} {1}", data[0], Crypto.hashToString(data)), e);
}
}
private void HandleUpdatePresence(byte[] data, RemoteEndpoint endpoint)
{
// Parse the data and update entries in the presence list
Presence p = PresenceList.updateFromBytes(data, 0);
if (p == null)
{
return;
}
Logging.info("Received presence update for " + p.wallet);
Friend f = FriendList.getFriend(p.wallet);
if (f != null)
{
var pa = p.addresses[0];
f.relayNode = new Peer(pa.address, null, pa.lastSeenTime, 0, 0, 0);
f.updatedStreamingNodes = pa.lastSeenTime;
}
}
private void HandleKeepAlivePresence(byte[] data, RemoteEndpoint endpoint)
{
byte[] hash = CryptoManager.lib.sha3_512sqTrunc(data);
InventoryCache.Instance.setProcessedFlag(InventoryItemTypes.keepAlive, hash);
Address address;
long last_seen;
byte[] device_id;
char node_type;
bool updated = PresenceList.receiveKeepAlive(data, out address, out last_seen, out device_id, out node_type, endpoint);
Logging.trace("Received keepalive update for " + address);
Presence p = PresenceList.getPresenceByAddress(address);
if (p == null)
return;
Friend f = FriendList.getFriend(p.wallet);
if (f != null)
{
var pa = p.addresses[0];
f.relayNode = new Peer(pa.address, null, pa.lastSeenTime, 0, 0, 0);
f.updatedStreamingNodes = pa.lastSeenTime;
}
}
public static void processPendingTransactions()
{
ulong last_block_height = IxianHandler.getLastBlockHeight();
lock (PendingTransactions.pendingTransactions)
{
long cur_time = Clock.getTimestamp();
List<PendingTransaction> tmp_pending_transactions = new(PendingTransactions.pendingTransactions);
foreach (var entry in tmp_pending_transactions)
{
long tx_time = entry.addedTimestamp;
if (entry.transaction.blockHeight > last_block_height)
{
// not ready yet, syncing to the network
continue;
}
Transaction t = TransactionCache.getTransaction(entry.transaction.id);
if (t == null)
{
t = entry.transaction;
}
else
{
if (t.applied != 0)
{
PendingTransactions.pendingTransactions.RemoveAll(x => x.transaction.id.SequenceEqual(t.id));
continue;
}
}
// if transaction expired, remove it from pending transactions
if (last_block_height > ConsensusConfig.getRedactedWindowSize()
&& t.blockHeight < last_block_height - ConsensusConfig.getRedactedWindowSize())
{
Logging.error("Error sending the transaction {0}, expired", t.getTxIdString());
PendingTransactions.pendingTransactions.RemoveAll(x => x.transaction.id.SequenceEqual(t.id));
continue;
}
if (entry.rejectedNodeList.Count() > 3
&& entry.rejectedNodeList.Count() > entry.confirmedNodeList.Count())
{
Logging.error("Error sending the transaction {0}, rejected", t.getTxIdString());
PendingTransactions.pendingTransactions.RemoveAll(x => x.transaction.id.SequenceEqual(t.id));
continue;
}
if (cur_time - tx_time > 60) // if the transaction is pending for over 60 seconds, resend
{
Logging.warn("Transaction {0} pending for a while, resending", t.getTxIdString());
foreach (var address in entry.relayNodeAddresses)
{
NetworkClientManager.sendToClient(address, ProtocolMessageCode.transactionData2, t.getBytes(true, true), null);
}
CoreProtocolMessage.broadcastGetTransaction(t.id, 0);
entry.addedTimestamp = cur_time;
entry.confirmedNodeList.Clear();
entry.rejectedNodeList.Clear();
}
}
}
}
}
}
Create ICLocalStorageCallbacks.cs:
using IXICore;
using IXICore.Storage;
using IXICore.Streaming;
namespace IxianClient
{
internal class ICLocalStorageCallbacks : LocalStorageCallbacks
{
public bool receivedNewTransaction(Transaction transaction)
{
return false;
}
public void processMessage(FriendMessage friendMessage)
{
}
}
}
Create ICTransactionInclusionCallbacks.cs:
using IXICore;
using IXICore.Meta;
using IXICore.Storage;
using IXICore.Streaming;
using System.Linq;
namespace IxianClient
{
internal class ICTransactionInclusionCallbacks : TransactionInclusionCallbacks
{
public void receivedTIVResponse(Transaction tx, bool verified)
{
if (!verified)
{
tx.applied = 0;
return;
}
else
{
PendingTransactions.remove(tx.id);
}
TransactionCache.addTransaction(tx);
Friend friend = FriendList.getFriend(tx.pubKey);
if (friend == null)
{
foreach (var toEntry in tx.toList)
{
friend = FriendList.getFriend(toEntry.Key);
if (friend != null)
{
break;
}
}
}
IxianHandler.balances.First().lastUpdate = 0;
}
public void receivedBlockHeader(Block block_header, bool verified)
{
foreach (Balance balance in IxianHandler.balances)
{
if (balance.blockChecksum != null && balance.blockChecksum.SequenceEqual(block_header.blockChecksum))
{
balance.verified = true;
}
}
if (block_header.blockNum >= IxianHandler.getHighestKnownNetworkBlockHeight())
{
IxianHandler.status = NodeStatus.ready;
}
}
}
}
Step 4: Create Main Program
Edit Program.cs:
using IXICore;
using IXICore.Meta;
using System;
using System.Threading;
namespace IxianClient
{
class Program
{
static Node? node = null;
static void Main(string[] args)
{
Console.WriteLine("Ixian Client Example");
Console.WriteLine("====================\n");
// Setup logging (warnings and errors only)
Logging.start(AppDomain.CurrentDomain.BaseDirectory, (int)(LogSeverity.warn | LogSeverity.error));
Logging.consoleOutput = true;
// Handle Ctrl+C
Console.CancelKeyPress += (sender, e) => {
e.Cancel = true;
IxianHandler.forceShutdown = true;
};
try
{
// Initialize and start node
node = new Node();
node.Start();
// Display initial state
DisplayStatus();
// Interactive menu
RunMenu();
}
catch (Exception e)
{
Console.WriteLine($"Fatal error: {e}");
}
finally
{
node?.Stop();
Logging.stop();
}
}
static void DisplayStatus()
{
var myAddress = IxianHandler.getWalletStorage().getPrimaryAddress();
var balance = IxianHandler.getWalletBalance(myAddress);
var blockHeight = IxianHandler.getHighestKnownNetworkBlockHeight();
Console.WriteLine($"\nWallet Address: {myAddress}");
Console.WriteLine($"Balance: {balance} IXI");
Console.WriteLine($"Block Height: {blockHeight}\n");
}
static void RunMenu()
{
while (!IxianHandler.forceShutdown)
{
Console.WriteLine("\n===========================================");
Console.WriteLine("Commands:");
Console.WriteLine(" 1 - Show status");
Console.WriteLine(" 2 - Send payment");
Console.WriteLine(" 3 - Check balance");
Console.WriteLine(" 4 - Check presence");
Console.WriteLine(" 5 - Check transaction status");
Console.WriteLine(" 6 - Request balance update");
Console.WriteLine(" 7 - Exit");
Console.WriteLine("===========================================");
Console.Write("Choice: ");
var choice = Console.ReadLine();
switch (choice)
{
case "1":
DisplayStatus();
break;
case "2":
Console.Write("Recipient address: ");
var recipient = Console.ReadLine();
Console.Write("Amount (IXI): ");
var amount = Console.ReadLine();
if (!string.IsNullOrEmpty(recipient) && !string.IsNullOrEmpty(amount))
{
bool success = node?.SendPayment(recipient, amount) ?? false;
if (success)
{
Console.WriteLine("\n✓ Payment sent successfully!");
Console.WriteLine(" The transaction is now pending. Check status with option 5.");
}
else
{
Console.WriteLine("\n✗ Payment failed. Check the error message above.");
}
}
break;
case "3":
Console.Write("Your Address (or blank for your primary): ");
var addr = Console.ReadLine();
if (string.IsNullOrEmpty(addr))
{
var myBalance = node?.GetMyBalance() ?? new IxiNumber(0);
Console.WriteLine($"\nYour balance (cached): {myBalance} IXI");
}
else
{
var balance = node?.GetBalance(addr) ?? new IxiNumber(0);
Console.WriteLine($"\nYour balance (cached): {balance} IXI");
}
Console.WriteLine("Note: This is the cached value. Use option 6 to request fresh data from network.");
break;
case "4":
Console.Write("Address to check: ");
var presenceAddr = Console.ReadLine();
if (!string.IsNullOrEmpty(presenceAddr))
{
try
{
// First check cached presence
var isOnline = node?.IsAddressOnline(presenceAddr) ?? false;
if (!isOnline)
{
Console.WriteLine("\nAddress not found in local cache. Requesting sectors from network...");
node?.RequestSector(presenceAddr);
Console.WriteLine("Waiting 1 seconds for network response...");
Thread.Sleep(1000);
Console.WriteLine("\nRequesting Presence from network...");
node?.RequestPresence(presenceAddr);
Console.WriteLine("Waiting 1 seconds for network response...");
Thread.Sleep(1000);
}
// Display presence information
Console.WriteLine($"\nPresence information for {presenceAddr}:");
node?.DisplayPresenceInfo(presenceAddr);
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
break;
case "5":
Console.Write("Transaction ID: ");
var txid = Console.ReadLine();
if (!string.IsNullOrEmpty(txid))
{
try
{
var status = node?.GetTransactionStatus(txid) ?? "Unknown";
Console.WriteLine($"\nTransaction status: {status}");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
break;
case "6":
Console.Write("Address (or blank for your primary address): ");
var balAddr = Console.ReadLine();
try
{
Address addrToUpdate;
if (string.IsNullOrEmpty(balAddr))
{
addrToUpdate = IxianHandler.getWalletStorage().getPrimaryAddress();
}
else
{
addrToUpdate = new Address(balAddr);
}
Console.WriteLine($"\nRequesting balance update for {addrToUpdate}...");
node?.RequestBalanceUpdate(addrToUpdate);
Console.WriteLine("Request sent. Balance will update automatically in a few seconds.");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
break;
case "7":
Console.WriteLine("\nShutting down...");
IxianHandler.forceShutdown = true;
break;
default:
Console.WriteLine("Invalid choice. Please select 1-7.");
break;
}
}
}
}
}
Step 5: Build and Run
# Build the project
dotnet build
# Run your client
dotnet run
You should see output like:
Starting Ixian Client...
Generated new wallet: 1abc...xyz
Node initialized. Wallet: 1abc...xyz
Node started and connecting to network...
Core Operations
Usage example:
// Send 10 IXI to an address
node.SendPayment("1abc...xyz", "10.00000000");
Receiving Transactions
Transactions sent to your wallet are automatically detected by the network client. To monitor incoming transactions:
// Automatic balance change monitoring
private IxiNumber lastKnownBalance = new IxiNumber(0);
private void CheckBalanceChanges()
{
var myAddress = IxianHandler.getWalletStorage().getPrimaryAddress();
var currentBalance = IxianHandler.getWalletBalance(myAddress);
if (currentBalance != lastKnownBalance)
{
Console.WriteLine($"\n*** Balance changed: {lastKnownBalance} -> {currentBalance} IXI ***\n");
lastKnownBalance = currentBalance;
}
}
Querying Balances
// Query your own balance (cached)
public IxiNumber GetMyBalance()
{
var myAddress = IxianHandler.getWalletStorage().getPrimaryAddress();
return IxianHandler.getWalletBalance(myAddress);
}
// Query any address balance (returns cached value, use RequestBalanceUpdate to refresh)
public IxiNumber GetBalance(string address)
{
var addr = new Address(address);
return IxianHandler.getWalletBalance(addr);
}
// Request fresh balance from network for specific address
public void RequestBalanceUpdate(Address address)
{
byte[] getBalanceBytes;
using (var ms = new MemoryStream())
{
using (var writer = new BinaryWriter(ms))
{
writer.WriteIxiVarInt(address.addressNoChecksum.Length);
writer.Write(address.addressNoChecksum);
}
getBalanceBytes = ms.ToArray();
}
CoreProtocolMessage.broadcastProtocolMessage(
new char[] { 'M', 'H', 'R' },
ProtocolMessageCode.getBalance2,
getBalanceBytes,
null
);
}
Querying Presence Information
Ixian uses the Starling presence model with sector-based routing. To find an address, you first query its sector nodes (nodes responsible for that address's sector), then request presence from those nodes.
// Check if an address is online (from local cache)
public bool IsAddressOnline(string address)
{
var addr = new Address(address);
var presence = PresenceList.getPresenceByAddress(addr);
return presence != null;
}
// Get presence details for an address (from local cache)
public Presence? GetPresence(string address)
{
var addr = new Address(address);
return PresenceList.getPresenceByAddress(addr);
}
// Request sector nodes from the network that handle a specific address's sector
// The sector is determined by the first 10 bytes of the address's sector prefix
public void RequestSector(string address)
{
var addr = new Address(address);
Console.WriteLine($"Requesting sectors for {address}...");
// Fetch relay nodes (S2 nodes) that are responsible for this address's sector
CoreProtocolMessage.fetchSectorNodes(addr, CoreConfig.maxRelaySectorNodesToRequest);
// The network will respond with sectorNodes message containing relay node presences
// This is handled by HandleSectorNodes() which updates RelaySectors and PeerStorage
}
// Request presence information for a specific address from the network
// Uses the sector-based routing system to query the appropriate relay nodes
public void RequestPresence(string address)
{
var addr = new Address(address);
Console.WriteLine($"Requesting presence for {address}...");
// Create a temporary friend object to use the sector-based presence fetching mechanism
var friend = new Friend(FriendState.Approved, addr, null, "", null, null, 0, true);
// Get relay nodes responsible for this address's sector from local cache
List<Peer> peers = new();
var relays = RelaySectors.Instance.getSectorNodes(addr.sectorPrefix, CoreConfig.maxRelaySectorNodesToRequest);
foreach (var relay in relays)
{
var p = PresenceList.getPresenceByAddress(relay);
if (p == null) continue;
var pa = p.addresses.First();
peers.Add(new(pa.address, relay, pa.lastSeenTime, 0, 0, 0));
}
// Assign sector nodes to the friend and request presence via streaming protocol
friend.sectorNodes = peers;
friend.updatedSectorNodes = Clock.getTimestamp();
CoreStreamProcessor.fetchFriendsPresence(friend);
// The network will respond with updatePresence messages
}
// Display presence information for an address
public void DisplayPresenceInfo(string address)
{
var addr = new Address(address);
var presence = PresenceList.getPresenceByAddress(addr);
if (presence == null)
{
Console.WriteLine($" Status: Offline or not found in local cache");
Console.WriteLine($" Tip: Call RequestSector() and RequestPresence() first.");
return;
}
Console.WriteLine($" Status: Online");
Console.WriteLine($" Wallet: {presence.wallet?.ToString() ?? "N/A"}");
Console.WriteLine($" Endpoints: {presence.addresses.Count}");
foreach (var endpoint in presence.addresses)
{
char nodeType = endpoint.type;
string typeDesc = nodeType switch
{
'C' => "Client",
'M' => "Master (DLT)",
'H' => "Host (DLT)",
'R' => "Relay (S2)",
'W' => "Worker",
_ => "Unknown"
};
Console.WriteLine($" - {endpoint.address} (type: {nodeType} - {typeDesc})");
}
}
How Starling Routing Works:
- Sector Calculation: Each address has a sector ID (first 10 bytes of SHA3-512 hash).
- Sector Nodes: S2 nodes announce their Presence to DLT nodes. DLT nodes determine sectors algorithmically.
- Discovery: Query DLT nodes for "which S2 nodes handle sector for address X?".
- Presence Announcement: Clients announce themselves to the network by sending Presence packet to S2 nodes that handle their sector.
- Presence Query: Connect to a few of those S2 nodes and request the specific presence.
- Caching: Presence is cached locally in
PresenceListwith expiration.
Best Practices
1. Always Use IxiNumber for Financial Values
// WRONG - floating point precision errors
double amount = 100.50;
// CORRECT
IxiNumber amount = new IxiNumber("100.50");
IxiNumber fee = ConsensusConfig.forceTransactionPrice;
IxiNumber total = amount + fee;
2. Use CryptoManager for All Cryptography
// All crypto through CryptoManager.lib
byte[] hash = CryptoManager.lib.sha3_512(data);
byte[] signature = CryptoManager.lib.getSignature(data, privateKey);
3. Validate all user entered Addresses and their Checksums
try
{
var address = new Address(userInput);
if (!address.validateChecksum())
{
Console.WriteLine("Invalid address checksum");
return;
}
}
catch
{
Console.WriteLine("Invalid address format");
return;
}
4. Handle Network Time
// WRONG - local time not synchronized
long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
// CORRECT - network-synchronized time
long timestamp = Clock.getNetworkTimestamp();
5. Implement Proper Error Handling
public bool SendTransaction(Transaction tx)
{
try
{
if (tx == null)
{
Logging.error("Transaction is null");
return false;
}
if (!tx.verifySignature())
{
Logging.error("Invalid transaction signature");
return false;
}
return IxianHandler.addTransaction(tx, null, true);
}
catch (Exception e)
{
Logging.error($"Failed to send transaction: {e}");
return false;
}
}
Reference Implementation
For a complete, production-ready example, see QuIXI, which implements:
- Full IxianNode implementation as a light client
- REST API server
- MQTT/RabbitMQ message queue integration
- Contact management
- Transaction broadcasting
- Message handling
- Configuration management
Key files to study:
QuIXI/Meta/Node.cs- IxianNode implementationQuIXI/Network/StreamProcessor.cs- Message handlingQuIXI/API/- REST API endpoints
Troubleshooting
Wallet won't load
- Check file permissions on
wallet.ixi - Verify wallet file isn't corrupted
- Try generating a new wallet
Can't connect to network
- Verify
NetworkType(test vs main) - Check firewall settings
- Ensure seed nodes are reachable
Transactions rejected
- Verify sufficient balance (amount + fee)
- Check transaction signature validity
- Ensure correct
blockHeightin transaction
Messages not sending
- Verify recipient is in friend list
- Check recipient's presence in PresenceList
- Ensure streaming connections are active
Next Steps
- Streaming Protocol Details
- QuIXI Bridge Documentation
- Ixian-Core API Reference
- Join Developer Discord
Additional Resources
- Ixian-Core GitHub: github.com/ixian-platform/Ixian-Core
- QuIXI Example: github.com/ixian-platform/QuIXI
- Spixi Client: github.com/ixian-platform/Spixi
- API Documentation: Generated via Doxygen in Ixian-Core ixian-platform