Skip to content

Commit 6771f27

Browse files
committed
Refactor SendBocAsync API and update transaction examples
- Refactored BlockchainCategory.SendBocAsync to use a new SendBlockchainMessageRequest model supporting single, batch, and meta fields; method is now void. - Updated HTTP response handling to gracefully support empty responses. - Added SendBlockchainMessageRequest model. - Improved README and README_RU transaction examples to reflect the new recommended workflow and API usage. - Added TransferTests to TracesCategoryTests.cs to demonstrate end-to-end transaction sending and confirmation. - Minor whitespace and readability improvements in tests.
1 parent c6d0a82 commit 6771f27

6 files changed

Lines changed: 184 additions & 44 deletions

File tree

README.md

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -290,23 +290,44 @@ The client is organized by categories for ease of use:
290290
### Sending Transaction
291291

292292
```csharp
293-
// Emulate before sending
294-
var consequences = await client.Wallet.EmulateAsync(bocMessage);
293+
// Build and sign transaction (using TonSdk.NET)
294+
var keys = Mnemonic.ToWalletKey(mnemonic.Split(" "));
295+
var wallet = WalletV5R1.Create(0, keys.PublicKey, false);
296+
var seqno = (await client.Wallet.GetSeqnoAsync(wallet.Address.ToString())).SeqnoValue;
297+
298+
var message = new MessageRelaxed(...); // Create your message
299+
Cell unsignedTransfer = WalletV5R1Utils.CreateUnsignedTransfer(
300+
wallet.WalletId, seqno, [message], SendMode.SendPayFwdFeesSeparately);
301+
Cell transfer = WalletV5R1Utils.SignAndPack(unsignedTransfer, keys.SecretKey);
302+
303+
// Create external message
304+
var externalMsg = new Message(
305+
new CommonMessageInfo.ExternalIn(null, wallet.Address, BigInteger.Zero),
306+
transfer,
307+
seqno > 0 ? null : wallet.Init);
308+
309+
// Calculate message hash locally
310+
var messageHash = WalletV5R1Utils.NormalizeHash(externalMsg).ToHex();
311+
312+
// Serialize to BOC
313+
var boc = Convert.ToBase64String(Builder.BeginCell().StoreMessage(externalMsg).EndCell().ToBoc());
314+
315+
// Emulate before sending (optional)
316+
var consequences = await client.Wallet.EmulateAsync(boc);
295317
Console.WriteLine($"Estimated fee: {consequences.Event.Fee.Total}");
296318

297319
// Send transaction
298-
var response = await client.Blockchain.SendBocAsync(bocMessage);
299-
Console.WriteLine($"Message hash: {response.Hash}");
320+
await client.Blockchain.SendBocAsync(boc: boc);
300321

301322
// Wait for transaction
302-
var transaction = await client.Account.WaitForTransactionAsync(
303-
accountAddress,
304-
response.Hash,
323+
var tx = await client.Blockchain.WaitForTransactionAsync(
324+
wallet.Address.ToString(),
325+
messageHash,
305326
maxWaitTime: 60);
306327

307-
if (transaction != null)
328+
if (tx != null && tx.Success)
308329
{
309-
Console.WriteLine($"Transaction confirmed: {transaction.Hash}");
330+
Console.WriteLine($"Transaction confirmed: {tx.Hash}");
310331
}
311332
```
312333

README_RU.md

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -290,23 +290,44 @@ builder.Services.AddTonApiClient(builder.Configuration);
290290
### Отправка транзакции
291291

292292
```csharp
293-
// Эмулировать перед отправкой
294-
var consequences = await client.Wallet.EmulateAsync(bocMessage);
293+
// Build and sign transaction (using TonSdk.NET)
294+
var keys = Mnemonic.ToWalletKey(mnemonic.Split(" "));
295+
var wallet = WalletV5R1.Create(0, keys.PublicKey, false);
296+
var seqno = (await client.Wallet.GetSeqnoAsync(wallet.Address.ToString())).SeqnoValue;
297+
298+
var message = new MessageRelaxed(...); // Create your message
299+
Cell unsignedTransfer = WalletV5R1Utils.CreateUnsignedTransfer(
300+
wallet.WalletId, seqno, [message], SendMode.SendPayFwdFeesSeparately);
301+
Cell transfer = WalletV5R1Utils.SignAndPack(unsignedTransfer, keys.SecretKey);
302+
303+
// Create external message
304+
var externalMsg = new Message(
305+
new CommonMessageInfo.ExternalIn(null, wallet.Address, BigInteger.Zero),
306+
transfer,
307+
seqno > 0 ? null : wallet.Init);
308+
309+
// Calculate message hash locally
310+
var messageHash = WalletV5R1Utils.NormalizeHash(externalMsg).ToHex();
311+
312+
// Serialize to BOC
313+
var boc = Convert.ToBase64String(Builder.BeginCell().StoreMessage(externalMsg).EndCell().ToBoc());
314+
315+
// Emulate before sending (optional)
316+
var consequences = await client.Wallet.EmulateAsync(boc);
295317
Console.WriteLine($"Estimated fee: {consequences.Event.Fee.Total}");
296318

297-
// Отправить транзакцию
298-
var response = await client.Blockchain.SendBocAsync(bocMessage);
299-
Console.WriteLine($"Message hash: {response.Hash}");
319+
// Send transaction
320+
await client.Blockchain.SendBocAsync(boc: boc);
300321

301-
// Подождать транзакцию
302-
var transaction = await client.Account.WaitForTransactionAsync(
303-
accountAddress,
304-
response.Hash,
322+
// Wait for transaction
323+
var tx = await client.Blockchain.WaitForTransactionAsync(
324+
wallet.Address.ToString(),
325+
messageHash,
305326
maxWaitTime: 60);
306327

307-
if (transaction != null)
328+
if (tx != null && tx.Success)
308329
{
309-
Console.WriteLine($"Transaction confirmed: {transaction.Hash}");
330+
Console.WriteLine($"Transaction confirmed: {tx.Hash}");
310331
}
311332
```
312333

src/TonapiClient.Tests/TracesCategoryTests.cs

Lines changed: 83 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
using Ton.Core.Addresses;
33
using Ton.Core.Boc;
44
using Ton.Core.Types;
5+
using Ton.Crypto.Mnemonic;
56
using TonapiClient.Models;
67
using TonapiClient.Tests.V5R1;
78
using Xunit;
9+
using Xunit.Abstractions;
810
using Xunit.Sdk;
911

1012
namespace TonapiClient.Tests;
@@ -22,80 +24,80 @@ public async Task GetAsync_WithTraceId_ReturnsTraceWithChildren()
2224

2325
// Assert
2426
Assert.NotNull(result);
25-
27+
2628
// Verify root transaction
2729
Assert.NotNull(result.Transaction);
2830
Assert.Equal(traceId, result.Transaction.Hash.ToLower());
2931
Assert.Equal(42198170000001ul, result.Transaction.Lt);
30-
32+
3133
// Verify root transaction account
3234
Assert.NotNull(result.Transaction.Account);
3335
Assert.Equal("0:7d9bd933a00838d9e7d0d3c94779689e6cc3812519a6ed523a84d45b4d896bec", result.Transaction.Account.Address.ToLower());
3436
Assert.False(result.Transaction.Account.IsScam);
3537
Assert.True(result.Transaction.Account.IsWallet);
36-
38+
3739
// Verify root transaction success
3840
Assert.True(result.Transaction.Success.HasValue);
3941
Assert.True(result.Transaction.Success.Value);
4042
Assert.Equal(1765205194ul, result.Transaction.Utime);
4143
Assert.Equal(3246798ul, result.Transaction.TotalFees);
42-
44+
4345
// Verify root transaction in_msg
4446
Assert.NotNull(result.Transaction.InMsg);
4547
Assert.Equal("ext_in_msg", result.Transaction.InMsg.MsgType);
4648
Assert.Equal("0x7369676e", result.Transaction.InMsg.OpCode);
47-
49+
4850
// Verify out_msgs
4951
Assert.NotNull(result.Transaction.OutMsgs);
50-
52+
5153
// Verify block
5254
Assert.NotNull(result.Transaction.Block);
5355
Assert.NotEmpty(result.Transaction.Block);
54-
56+
5557
// Verify interfaces
5658
Assert.NotNull(result.Interfaces);
5759
Assert.Single(result.Interfaces);
5860
Assert.Equal("wallet_v5r1", result.Interfaces[0]);
59-
61+
6062
// Verify children exist
6163
Assert.NotNull(result.Children);
6264
Assert.NotEmpty(result.Children);
63-
65+
6466
// Verify first child transaction
6567
var firstChild = result.Children[0];
6668
Assert.NotNull(firstChild);
6769
Assert.NotNull(firstChild.Transaction);
6870
Assert.Equal("c692cb7425fa7774a83979ca496db96c04960e1e13759ee0819b51b61566901d", firstChild.Transaction.Hash.ToLower());
6971
Assert.True(firstChild.Transaction.Success.HasValue);
7072
Assert.True(firstChild.Transaction.Success.Value);
71-
73+
7274
// Verify first child has its own children (nested structure)
7375
Assert.NotNull(firstChild.Children);
7476
Assert.NotEmpty(firstChild.Children);
75-
77+
7678
// Verify second level child
7779
var secondLevelChild = firstChild.Children[0];
7880
Assert.NotNull(secondLevelChild);
7981
Assert.NotNull(secondLevelChild.Transaction);
8082
Assert.Equal("072c3a3914dffaaba49dbd82093a299ca24c1c7e05fe34eafc5c3f9b4a7afccc", secondLevelChild.Transaction.Hash.ToLower());
81-
83+
8284
// Verify emulated flag
8385
Assert.False(result.Emulated);
84-
86+
8587
// Verify aborted flag
8688
Assert.False(result.Transaction.Aborted);
87-
89+
8890
// Test helper methods - they should execute without exceptions
8991
var isFinalized = result.IsFinalized(); // Can be true or false depending on trace state
9092
Assert.False(result.HasUnsuccessfulTransactions()); // All transactions should be successful
91-
93+
9294
var totalFees = result.GetTotalFees();
9395
Assert.True(totalFees > 0, $"Total fees should be positive, got {totalFees}");
94-
96+
9597
var sender = result.GetSender();
9698
Assert.NotNull(sender);
9799
Assert.Equal("0:7d9bd933a00838d9e7d0d3c94779689e6cc3812519a6ed523a84d45b4d896bec", sender.ToLower());
98-
100+
99101
var transferType = result.GetTransferType();
100102
Assert.NotEqual(TransferType.Unknown, transferType);
101103
}
@@ -111,7 +113,7 @@ public async Task GetAsync_WithSuccessfulTraceId_HasNoUnsuccessfulTransactions()
111113

112114
// Assert
113115
Assert.NotNull(result);
114-
Assert.False(result.HasUnsuccessfulTransactions(),
116+
Assert.False(result.HasUnsuccessfulTransactions(),
115117
"Expected trace to have no unsuccessful transactions");
116118
}
117119

@@ -126,7 +128,7 @@ public async Task GetAsync_WithUnsuccessfulTraceId_HasUnsuccessfulTransactions()
126128

127129
// Assert
128130
Assert.NotNull(result);
129-
Assert.True(result.HasUnsuccessfulTransactions(),
131+
Assert.True(result.HasUnsuccessfulTransactions(),
130132
"Expected trace to have at least one unsuccessful transaction");
131133
}
132134

@@ -191,7 +193,7 @@ public async Task GetAsync_CheckNativeTransferTraceTests()
191193
[Fact]
192194
public async Task EmulateTransferWithDummyKeyTests()
193195
{
194-
var amount = 0.05m;
196+
var amount = 0.05m;
195197
var fromAddress = "0:166ee3201c967f2dbd85ed916da9eeb4b2cfea0afa7f7b4f0302597461f7eb5e";
196198
var toAddress = "0:08c1cd46e7c5f238f5a47375b208c532da16f891beb94706916cf0010c87b2cd";
197199

@@ -346,5 +348,66 @@ public async Task EmulateTransferTests()
346348
Assert.NotNull(sender);
347349
Assert.NotNull(recipient);
348350
}
351+
352+
[Fact]
353+
public async Task TransferTests()
354+
{
355+
string toAddress = "0QAIwc1G58XyOPWkc3WyCMUy2hb4kb65RwaRbPABDIeyzYKY";
356+
decimal amount = 0.01m;
357+
var keys = Ton.Crypto.Mnemonic.Mnemonic.ToWalletKey(Mnemonic.Split(" "));
358+
359+
var wallet = WalletV5R1.Create(0, keys.PublicKey, false);
360+
var seqnoResult = await Client.Wallet.GetSeqnoAsync(wallet.Address.ToString());
361+
var seqno = seqnoResult.SeqnoValue;
362+
363+
var destAddress = Address.Parse(toAddress);
364+
365+
// this is internal messgae
366+
var message = new MessageRelaxed(
367+
new CommonMessageInfoRelaxed.Internal(
368+
true, false, false,
369+
null, destAddress,
370+
new CurrencyCollection(new BigInteger(amount * 1_000_000_000)),
371+
0, 0, 0, 0),
372+
Builder.BeginCell().StoreUint(0x00000000, 32).StoreStringTail("Test transfer").EndCell());
373+
374+
Cell unsignedTransfer = WalletV5R1Utils.CreateUnsignedTransfer(wallet.WalletId, seqno,
375+
[message],
376+
SendMode.SendPayFwdFeesSeparately | SendMode.SendIgnoreErrors);
377+
378+
Cell transfer = WalletV5R1Utils.SignAndPack(unsignedTransfer, keys.SecretKey);
379+
380+
// Create external message
381+
// Build external message (the receiver of this message is user`s wallet)
382+
CommonMessageInfo.ExternalIn externalMsgInfo = new(
383+
null,
384+
wallet.Address,
385+
BigInteger.Zero
386+
);
387+
Ton.Core.Types.Message externalMsg = new(externalMsgInfo, transfer, seqno > 0 ? null : wallet.Init);
388+
var normalizedHash = WalletV5R1Utils.NormalizeHash(externalMsg).ToHex();
389+
390+
// Serialize and send
391+
Cell messageCell = Builder.BeginCell()
392+
.StoreMessage(externalMsg)
393+
.EndCell();
394+
byte[] boc = messageCell.ToBoc();
395+
var bocAsString = Convert.ToBase64String(boc);
396+
// await Client.LiteServer.SendMessageAsync(Convert.ToBase64String(boc));
397+
await Client.Blockchain.SendBocAsync(bocAsString);
398+
try
399+
{
400+
await Client.Blockchain.SendBocAsync(bocAsString);
401+
}
402+
catch (Exception ex)
403+
{
404+
Assert.Contains("duplicate message", ex.Message);
405+
}
406+
407+
var tx = await Client.Blockchain.WaitForTransactionAsync(wallet.Address.ToString(), normalizedHash);
408+
409+
Assert.True(tx != null, "Transaction should be found");
410+
Assert.True(tx.Success == true, "Transaction should be successful");
411+
}
349412
}
350413

src/TonapiClient/Categories/BlockchainCategory.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,19 @@ public async Task<Transaction> GetTransactionAsync(string transactionHash, Cance
6868
/// <summary>
6969
/// Send BOC (Bag of Cells) message to the blockchain.
7070
/// </summary>
71-
public async Task<SendBocResponse> SendBocAsync(string boc, CancellationToken ct = default)
72-
{
73-
var request = new SendBocRequest { Boc = boc };
74-
return await PostAsync<SendBocRequest, SendBocResponse>("/v2/blockchain/message", request, ct);
71+
public async Task SendBocAsync(
72+
string? boc = null,
73+
List<string>? batch = null,
74+
Dictionary<string, string>? meta = null,
75+
CancellationToken ct = default)
76+
{
77+
var request = new SendBlockchainMessageRequest
78+
{
79+
Boc = boc,
80+
Batch = batch,
81+
Meta = meta
82+
};
83+
await PostAsync<SendBlockchainMessageRequest, object>("/v2/blockchain/message", request, ct);
7584
}
7685

7786
/// <summary>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace TonapiClient.Models;
4+
5+
public class SendBlockchainMessageRequest
6+
{
7+
[JsonPropertyName("boc")]
8+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
9+
public string? Boc { get; set; }
10+
11+
[JsonPropertyName("batch")]
12+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
13+
public List<string>? Batch { get; set; }
14+
15+
[JsonPropertyName("meta")]
16+
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
17+
public Dictionary<string, string>? Meta { get; set; }
18+
}
19+

src/TonapiClient/TonApiClient.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,13 @@ private async Task<TResponse> SendWithRetryAsync<TResponse>(
161161
}
162162

163163
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
164+
165+
// Handle empty response (e.g., 200 OK with no body)
166+
if (stream.Length == 0 || typeof(TResponse) == typeof(object))
167+
{
168+
return default!;
169+
}
170+
164171
var result = await JsonSerializer.DeserializeAsync<TResponse>(stream, _jsonOptions, cancellationToken);
165172

166173
return result == null ? throw new TonApiException("Failed to deserialize response", 0, null) : result;

0 commit comments

Comments
 (0)