HttpClient Module

The Ton.HttpClient module provides access to TON blockchain via HTTP APIs (Toncenter v2 and TON API v4).

Features

  • HTTP/REST API access
  • Easy debugging with standard HTTP tools
  • Toncenter v2 API support
  • TON API v4 support
  • Contract interaction via IContractProvider
  • Good for development and prototyping

Quick Start

using Ton.HttpClient;
using Ton.Core.Addresses;

// Create client (Toncenter v2)
TonClient client = new(new TonClientParameters
{
    Endpoint = "https://toncenter.com/api/v2",
    ApiKey = "your-api-key"  // Optional for public endpoint
});

// Get balance
Address address = Address.Parse("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N");
BigInteger balance = await client.GetBalanceAsync(address);
Console.WriteLine($"Balance: {(decimal)balance / 1_000_000_000m} TON");

Client Providers

Toncenter API (v2)

Traditional HTTP API, widely available:

TonClient client = new(new TonClientParameters
{
    Endpoint = "https://toncenter.com/api/v2",
    ApiKey = "your-api-key",  // Get from @tonapibot
    Timeout = TimeSpan.FromSeconds(30)
});

Mainnet endpoints:

  • https://toncenter.com/api/v2 (rate-limited without API key)
  • https://tonapi.io/v2 (alternative)

Testnet endpoint:

  • https://testnet.toncenter.com/api/v2

TON API v4

Modern API with better performance:

TonClient4 client = new(new TonClient4Parameters
{
    Endpoint = "https://mainnet-v4.tonhubapi.com",
    Timeout = TimeSpan.FromSeconds(30)
});

Mainnet endpoints:

  • https://mainnet-v4.tonhubapi.com

Testnet endpoint:

  • https://testnet-v4.tonhubapi.com

Basic Operations

Get Balance

Address address = Address.Parse("EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N");
BigInteger balance = await client.GetBalanceAsync(address);

decimal tonBalance = (decimal)balance / 1_000_000_000m;
Console.WriteLine($"Balance: {tonBalance:F4} TON");

Get Contract State

ContractState state = await client.GetContractStateAsync(address);

Console.WriteLine($"Balance: {state.Balance}");
Console.WriteLine($"State: {state.State}");

if (state.State is ContractState.AccountStateInfo.Active active)
{
    byte[]? code = active.Code;
    byte[]? data = active.Data;
    Console.WriteLine("Contract is active");
}

if (state.Last != null)
{
    Console.WriteLine($"Last TX LT: {state.Last.Lt}");
    Console.WriteLine($"Last TX Hash: {Convert.ToBase64String(state.Last.Hash)}");
}

Run Get Method

Execute contract get methods:

// Simple get method
RunMethodResult result = await client.RunMethodAsync(
    address,
    methodName: "seqno"
);

int seqno = (int)result.Stack.ReadNumber();
Console.WriteLine($"Seqno: {seqno}");
Console.WriteLine($"Gas used: {result.GasUsed}");

Run Get Method with Parameters

// Get method with parameters
TupleItem[] stack = new[]
{
    new TupleItem.TupleItemInt(123)
};

RunMethodResult result = await client.RunMethodAsync(
    address,
    methodName: "get_value",
    stack: stack
);

BigInteger value = result.Stack.ReadNumber();

Get Transactions

// Get last 10 transactions
List<Transaction> txs = await client.GetTransactionsAsync(
    address,
    limit: 10
);

foreach (Transaction tx in txs)
{
    Console.WriteLine($"LT: {tx.Lt}");
    Console.WriteLine($"Hash: {Convert.ToHexString(tx.Hash)}");
    Console.WriteLine($"Time: {DateTimeOffset.FromUnixTimeSeconds(tx.Now)}");
    
    if (tx.InMsg != null)
    {
        // Process incoming message
    }
    
    foreach (Message outMsg in tx.OutMsgs)
    {
        // Process outgoing messages
    }
}

Get Transactions with Pagination

Address address = Address.Parse("...");
int limit = 100;
string? lt = null;
string? hash = null;

while (true)
{
    List<Transaction> txs = await client.GetTransactionsAsync(
        address,
        limit: limit,
        lt: lt,
        hash: hash
    );
    
    if (txs.Count == 0)
        break;
        
    // Process transactions
    foreach (Transaction tx in txs)
    {
        Console.WriteLine($"LT: {tx.Lt}, Time: {tx.Now}");
    }
    
    // Update pagination parameters
    Transaction lastTx = txs.Last();
    lt = lastTx.Lt.ToString();
    hash = Convert.ToBase64String(lastTx.Hash);
}

Get Single Transaction

string lt = "12345678";
string hash = "abcd...";

Transaction? tx = await client.GetTransactionAsync(address, lt, hash);

if (tx != null)
{
    Console.WriteLine($"Found transaction at LT {tx.Lt}");
}

Send Message

// Create message (see Contracts module for wallet examples)
Message message = ...;

// Serialize to BOC
Builder builder = Builder.BeginCell();
message.Store(builder);
Cell cell = builder.EndCell();
byte[] boc = cell.ToBoc();

// Send
await client.SendFileAsync(boc);

Console.WriteLine("Message sent successfully");

Check if Contract Deployed

bool isDeployed = await client.IsContractDeployedAsync(address);

if (isDeployed)
{
    Console.WriteLine("Contract is deployed and active");
}
else
{
    Console.WriteLine("Contract not deployed or not active");
}

Masterchain Operations

Get Masterchain Info

MasterchainInfoResult info = await client.GetMasterchainInfoAsync();

Console.WriteLine($"Workchain: {info.Workchain}");
Console.WriteLine($"Init seqno: {info.InitSeqno}");
Console.WriteLine($"Latest seqno: {info.LatestSeqno}");
Console.WriteLine($"Shard: {info.Shard}");

Get Shards

int seqno = 1000000;
List<ShardInfo> shards = await client.GetWorkchainShardsAsync(seqno);

foreach (ShardInfo shard in shards)
{
    Console.WriteLine($"Workchain: {shard.Workchain}");
    Console.WriteLine($"Shard: {shard.Shard}");
    Console.WriteLine($"Seqno: {shard.Seqno}");
}

Get Shard Transactions

int workchain = 0;
int seqno = 1000000;
string shard = "-9223372036854775808";

List<ShardTransactionInfo> txs = await client.GetShardTransactionsAsync(
    workchain,
    seqno,
    shard
);

foreach (ShardTransactionInfo tx in txs)
{
    Console.WriteLine($"Account: {tx.Account}");
    Console.WriteLine($"LT: {tx.Lt}");
    Console.WriteLine($"Hash: {tx.Hash}");
}

Transaction Location

Locate Result Transaction

Find result transaction from source:

Address source = Address.Parse("...");
Address destination = Address.Parse("...");
string createdLt = "12345678";

Transaction resultTx = await client.TryLocateResultTxAsync(
    source,
    destination,
    createdLt
);

Console.WriteLine($"Found result TX: {resultTx.Lt}");

Locate Source Transaction

Find source transaction from destination:

Transaction sourceTx = await client.TryLocateSourceTxAsync(
    source,
    destination,
    createdLt
);

Console.WriteLine($"Found source TX: {sourceTx.Lt}");

Contract Interaction

Open Contract

Use contracts with provider pattern:

using Ton.Contracts.Wallets.V5;

// Create wallet
WalletV5R1 wallet = WalletV5R1.Create(0, publicKey);

// Open with provider
OpenedContract<WalletV5R1> openedWallet = client.Open(wallet);

// Use contract methods
int seqno = await openedWallet.Contract.GetSeqnoAsync(openedWallet.Provider);
BigInteger balance = await openedWallet.Contract.GetBalanceAsync(openedWallet.Provider);

Console.WriteLine($"Seqno: {seqno}");
Console.WriteLine($"Balance: {balance}");

Custom Provider

Create provider for any address:

IContractProvider provider = client.Provider(address);

// Get state
ContractState state = await provider.GetStateAsync();

// Run get method
ContractGetMethodResult result = await provider.GetAsync("seqno", []);
int seqno = (int)result.Stack.ReadNumber();

Error Handling

Handle API Errors

try
{
    RunMethodResult result = await client.RunMethodAsync(address, "get_data");
}
catch (InvalidOperationException ex) when (ex.Message.Contains("exit_code"))
{
    Console.WriteLine($"Contract execution failed: {ex.Message}");
}
catch (HttpRequestException ex)
{
    Console.WriteLine($"Network error: {ex.Message}");
}
catch (TaskCanceledException)
{
    Console.WriteLine("Request timeout");
}

Run Method with Error Code

Get exit code instead of throwing:

RunMethodErrorResult result = await client.RunMethodWithErrorAsync(
    address,
    "may_fail_method"
);

if (result.ExitCode != 0)
{
    Console.WriteLine($"Method failed with exit code: {result.ExitCode}");
}
else
{
    // Process successful result
    BigInteger value = result.Stack.ReadNumber();
}

Best Practices

1. Use API Keys

// ✅ Good: Use API key for higher rate limits
TonClient client = new(new TonClientParameters
{
    Endpoint = "https://toncenter.com/api/v2",
    ApiKey = Environment.GetEnvironmentVariable("TONCENTER_API_KEY")
});

// ❌ Avoid: No API key (severe rate limits)

2. Handle Rate Limits

// ✅ Good: Implement retry with exponential backoff
async Task<T> RetryAsync<T>(Func<Task<T>> action, int maxRetries = 3)
{
    for (int i = 0; i < maxRetries; i++)
    {
        try
        {
            return await action();
        }
        catch (HttpRequestException) when (i < maxRetries - 1)
        {
            await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i)));
        }
    }
    throw new Exception("Max retries exceeded");
}

3. Cache Results

// ✅ Good: Cache rarely changing data
private readonly Dictionary<Address, ContractState> stateCache = new();
private readonly TimeSpan cacheTimeout = TimeSpan.FromMinutes(1);

async Task<ContractState> GetCachedStateAsync(Address address)
{
    if (stateCache.TryGetValue(address, out ContractState? cached))
    {
        return cached;
    }
    
    ContractState state = await client.GetContractStateAsync(address);
    stateCache[address] = state;
    return state;
}

4. Set Appropriate Timeouts

// ✅ Good: Configure timeout based on operation
TonClient client = new(new TonClientParameters
{
    Endpoint = "https://toncenter.com/api/v2",
    Timeout = TimeSpan.FromSeconds(30)  // Adjust based on needs
});

5. Dispose Properly

// ✅ Good: Dispose client
using TonClient client = new(parameters);
// ... use client

// Or: Singleton pattern for application lifetime
public class MyService
{
    private readonly TonClient _client;
    
    public MyService()
    {
        _client = new TonClient(parameters);
    }
    
    public void Dispose()
    {
        _client.Dispose();
    }
}

Comparison with LiteClient

Feature HttpClient LiteClient
Protocol HTTP/REST ADNL (binary)
Speed Slower Faster
Debugging Easy Harder
Rate Limits Yes (API-dependent) No
Load Balancing Manual Automatic
Reconnection N/A Automatic
Best For Development, prototyping Production

See Also