Skip to content

Commit c0ef97f

Browse files
committed
tests for Recipes, Tep81Recipes base class
1 parent b56a8bb commit c0ef97f

11 files changed

+307
-111
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
namespace TonLibDotNet.Recipes
2+
{
3+
[Collection(TonClientMainnetCollection.Definition)]
4+
public class RootDnsRecipes_GetNftAddress_Tests
5+
{
6+
private readonly Func<Task<ITonClient>> getTonClient;
7+
8+
public RootDnsRecipes_GetNftAddress_Tests(TonClientMainnetFixture fixture)
9+
{
10+
getTonClient = fixture.GetTonClient;
11+
}
12+
13+
[Fact]
14+
public async Task ResolveWithTonSuffix()
15+
{
16+
var tonClient = await getTonClient();
17+
18+
var adr = await TonRecipes.RootDns.GetNftAddress(tonClient, "foundation.ton");
19+
Assert.Equal("EQB43-VCmf17O7YMd51fAvOjcMkCw46N_3JMCoegH_ZDo40e", adr);
20+
}
21+
22+
[Fact]
23+
public async Task ResolveWithoutTonSuffix()
24+
{
25+
var tonClient = await getTonClient();
26+
27+
var adr = await TonRecipes.RootDns.GetNftAddress(tonClient, "foundation");
28+
Assert.Equal("EQB43-VCmf17O7YMd51fAvOjcMkCw46N_3JMCoegH_ZDo40e", adr);
29+
}
30+
31+
[Fact]
32+
public async Task ResolveNotMinted()
33+
{
34+
var tonClient = await getTonClient();
35+
36+
// Does not exist as NFT at time of writing this test
37+
var adr = await TonRecipes.RootDns.GetNftAddress(tonClient, "not-yet-minted-domain.ton");
38+
39+
// how to get expected value?
40+
// go to https://dns.ton.org/#not-yet-minted-domain and sniff toncenter requests in Developer Console
41+
Assert.Equal("EQBZoyjQa3ywgrlG_A4lYrhtKxity6BCfMsMEjLNFYSckQGX", adr);
42+
}
43+
44+
[Fact]
45+
public async Task FailsForNonTonDomains()
46+
{
47+
var tonClient = await getTonClient();
48+
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => TonRecipes.RootDns.GetNftAddress(tonClient, "alice.t.me"));
49+
}
50+
51+
[Theory]
52+
[InlineData("subdomain.domain.ton")]
53+
[InlineData("subdomain.domain")]
54+
public async Task FailsForThirdlevelDomains(string domain)
55+
{
56+
var tonClient = await getTonClient();
57+
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => TonRecipes.RootDns.GetNftAddress(tonClient, domain));
58+
}
59+
}
60+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
using Microsoft.Extensions.Logging;
2+
using Microsoft.Extensions.Options;
3+
using Moq;
4+
using TonLibDotNet.Types;
5+
6+
namespace TonLibDotNet
7+
{
8+
public abstract class TonClientBaseFixture : IDisposable
9+
{
10+
private readonly TonClient tonClient;
11+
12+
protected TonClientBaseFixture(bool useMainnet)
13+
{
14+
var logger = new Mock<ILogger<TonClient>>(MockBehavior.Loose);
15+
var options = new Mock<IOptions<TonOptions>>(MockBehavior.Strict);
16+
options
17+
.SetupGet(x => x.Value)
18+
.Returns(() =>
19+
{
20+
var o = new TonOptions() { UseMainnet = useMainnet };
21+
o.Options.KeystoreType = new KeyStoreTypeInMemory();
22+
return o;
23+
});
24+
tonClient = new TonClient(logger.Object, options.Object);
25+
}
26+
27+
public async Task<ITonClient> GetTonClient()
28+
{
29+
await tonClient.InitIfNeeded();
30+
await tonClient.Sync();
31+
return tonClient;
32+
}
33+
34+
public void Dispose()
35+
{
36+
Dispose(true);
37+
GC.SuppressFinalize(this);
38+
}
39+
40+
protected virtual void Dispose(bool disposing)
41+
{
42+
tonClient.Dispose();
43+
}
44+
}
45+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace TonLibDotNet
2+
{
3+
[CollectionDefinition(Definition)]
4+
public class TonClientMainnetCollection : ICollectionFixture<TonClientMainnetFixture>
5+
{
6+
public const string Definition = "mainnet";
7+
}
8+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace TonLibDotNet
2+
{
3+
public class TonClientMainnetFixture : TonClientBaseFixture
4+
{
5+
public TonClientMainnetFixture()
6+
: base(true)
7+
{
8+
// Nothing
9+
}
10+
}
11+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace TonLibDotNet
2+
{
3+
[CollectionDefinition(Definition)]
4+
public class TonClientTestnetCollection : ICollectionFixture<TonClientTestnetFixture>
5+
{
6+
public const string Definition = "testnet";
7+
}
8+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace TonLibDotNet
2+
{
3+
public class TonClientTestnetFixture : TonClientBaseFixture
4+
{
5+
public TonClientTestnetFixture()
6+
: base(false)
7+
{
8+
// Nothing
9+
}
10+
}
11+
}

TonLibDotNet.Tests/TonLibDotNet.Tests.csproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
<ItemGroup>
1212
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
13+
<PackageReference Include="Moq" Version="4.18.4" />
1314
<PackageReference Include="xunit" Version="2.4.2" />
1415
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
1516
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -40,4 +41,13 @@
4041
</EmbeddedResource>
4142
</ItemGroup>
4243

44+
<ItemGroup>
45+
<None Update="libcrypto-1_1-x64.dll">
46+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
47+
</None>
48+
<None Update="tonlibjson.*">
49+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
50+
</None>
51+
</ItemGroup>
52+
4353
</Project>

TonLibDotNet/Recipes/RootDnsRecipes.cs

Lines changed: 5 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,48 +4,18 @@
44
/// Based on <see href="https://github.com/ton-blockchain/TEPs/blob/master/text/0081-dns-standard.md">TEP 81: TON DNS Standard</see>
55
/// and <see href="https://github.com/ton-blockchain/dns-contract">TON DNS Smart Contracts</see>.
66
/// </remarks>
7-
public partial class RootDnsRecipes
7+
public partial class RootDnsRecipes : Tep81Recipes
88
{
9-
/// <remarks>
10-
/// TL-B: <code>dns_smc_address#9fd3 smc_addr:MsgAddressInt flags:(## 8) { flags &lt;= 1 } cap_list:flags . 0?SmcCapList = DNSRecord</code>
11-
/// </remarks>
12-
private const ushort DnsEntryTypeWallet = 0x9fd3;
13-
14-
/// <remarks>
15-
/// TL-B: <code>dns_next_resolver#ba93 resolver:MsgAddressInt = DNSRecord</code>
16-
/// </remarks>
17-
private const ushort DnsEntryTypeNextDnsResolver = 0xba93;
18-
19-
/// <remarks>
20-
/// TL-B: <code>dns_adnl_address#ad01 adnl_addr:bits256 flags:(## 8) { flags &lt;= 1 } proto_list:flags . 0?ProtoList = DNSRecord</code>
21-
/// </remarks>
22-
private const ushort DnsEntryTypeAdnl = 0xad01;
23-
24-
/// <remarks>
25-
/// TL-B: <code>dns_storage_address#7473 bag_id:bits256 = DNSRecord</code>
26-
/// </remarks>
27-
private const ushort DnsEntryTypeStorageBagId = 0x7473;
28-
29-
private const string CategoryNameSite = "site";
30-
private const string CategoryNameWallet = "wallet";
31-
private const string CategoryNameStorage = "storage";
32-
private const string CategoryNameNextDnsResolver = "dns_next_resolver";
33-
34-
private static readonly byte[] CategoryBytesSite = EncodeCategory(CategoryNameSite);
35-
private static readonly byte[] CategoryBytesWallet = EncodeCategory(CategoryNameWallet);
36-
private static readonly byte[] CategoryBytesStorage = EncodeCategory(CategoryNameStorage);
37-
private static readonly byte[] CategoryBytesNextDnsResolver = EncodeCategory(CategoryNameNextDnsResolver);
9+
/// <summary>
10+
/// .ton Root Collection - parent for all .TON DNS NFTs.
11+
/// </summary>
12+
public const string TonRootCollection = "EQC3dNlesgVD8YbAazcauIrXBPfiVhMMr5YYk2in0Mtsz0Bz";
3813

3914
/// <summary>
4015
/// From <see href="https://github.com/ton-blockchain/dns-contract/blob/main/func/dns-utils.fc">dns-utils.fc</see>
4116
/// </summary>
4217
private const int OPChangeDnsRecord = 0x4eb1f0f9;
4318

4419
public static readonly RootDnsRecipes Instance = new();
45-
46-
protected static byte[] EncodeCategory(string categoryName)
47-
{
48-
return System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.ASCII.GetBytes(categoryName));
49-
}
5020
}
5121
}

TonLibDotNet/Recipes/RootDnsRecipes_Read.cs

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,74 @@
11
using System.Globalization;
2+
using System.Numerics;
23
using TonLibDotNet.Types.Dns;
34
using TonLibDotNet.Types.Smc;
5+
using TonLibDotNet.Types.Tvm;
46
using TonLibDotNet.Utils;
57

68
namespace TonLibDotNet.Recipes
79
{
810
public partial class RootDnsRecipes
911
{
1012
/// <summary>
11-
/// Resolves DNS name into DNS Item NFT address (for both existing/minted and not-minted-yet domains).
13+
/// Resolves DNS name into DNS Item NFT address (for both existing/minted and not-minted-yet domains) by calling 'get_nft_address_by_index' method.
1214
/// </summary>
1315
/// <param name="tonClient"><see cref="ITonClient"/> instance.</param>
14-
/// <param name="domainName">Domain name to resolve. Second-level only, e.g. 'alice.ton'.</param>
16+
/// <param name="domainName">Domain name to resolve, with or without '.ton' suffix.</param>
1517
/// <returns>Address of DNS Item NFT for requested domain.</returns>
16-
/// <exception cref="ArgumentOutOfRangeException">Requested domainName is not second-level one.</exception>
18+
/// <remarks>Only second-level .ton domains are allowed, with or without '.ton' suffix. E.g. 'alice.ton.', 'alice.ton' and 'alice' are allowed, but 'alice.t.me' is not.</remarks>
19+
/// <exception cref="ArgumentOutOfRangeException">Requested <paramref name="domainName"/> is not second-level one, or not from '.ton' namespace.</exception>
20+
/// <exception cref="ArgumentNullException">Requested <paramref name="domainName"/> null or white-space only.</exception>
1721
public async Task<string> GetNftAddress(ITonClient tonClient, string domainName)
1822
{
19-
// Count dots in name, but not count last one.
20-
var depth = domainName.Count(x => x == '.') - (domainName.EndsWith('.') ? 1 : 0);
21-
if (depth != 1)
23+
if (string.IsNullOrWhiteSpace(domainName))
2224
{
23-
throw new ArgumentOutOfRangeException(nameof(domainName), "Only second-level domains (e.g. alice.ton) are supported");
25+
throw new ArgumentNullException(nameof(domainName));
2426
}
2527

26-
await tonClient.InitIfNeeded();
28+
var parts = domainName.ToLowerInvariant().Split('.', StringSplitOptions.RemoveEmptyEntries);
29+
30+
if (parts.Length > 2)
31+
{
32+
throw new ArgumentOutOfRangeException(nameof(domainName), "Only second-level domains (e.g. 'alice.ton') are supported.");
33+
}
2734

28-
var resolved = await tonClient.DnsResolve(domainName, ttl: 1); // only resolve in root contract
35+
if (parts.Length == 2 && !string.Equals(parts[1], "ton", StringComparison.InvariantCulture))
36+
{
37+
throw new ArgumentOutOfRangeException(nameof(domainName), "Only '.ton' domains (e.g. 'alice.ton') are supported.");
38+
}
2939

30-
return ((EntryDataNextResolver)resolved.Entries[0].Value).Resolver.Value;
40+
// UTF-8 encoded string up to 126 bytes, https://github.com/ton-blockchain/TEPs/blob/master/text/0081-dns-standard.md#domain-names
41+
var bytes = System.Text.Encoding.UTF8.GetBytes(parts[0]);
42+
if (bytes.Length > 126)
43+
{
44+
throw new ArgumentOutOfRangeException(nameof(domainName), "Value is too long. No more than 126 chars are allowed.");
45+
}
46+
47+
var hashSource = new byte[bytes.Length + 2];
48+
hashSource[0] = 0;
49+
hashSource[1] = (byte)(bytes.Length * 2);
50+
bytes.CopyTo(hashSource, 2);
51+
var index = System.Security.Cryptography.SHA256.HashData(hashSource);
52+
53+
await tonClient.InitIfNeeded().ConfigureAwait(false);
54+
55+
var smc = await tonClient.SmcLoad(new Types.AccountAddress(TonRootCollection)).ConfigureAwait(false);
56+
57+
// slice get_nft_address_by_index(int index)
58+
var stack = new List<StackEntry>()
59+
{
60+
new StackEntryNumber(new NumberDecimal(new BigInteger(index, true, true).ToString(CultureInfo.InvariantCulture))),
61+
};
62+
var result = await tonClient.SmcRunGetMethod(smc.Id, new MethodIdName("get_nft_address_by_index"), stack).ConfigureAwait(false);
63+
64+
await tonClient.SmcForget(smc.Id).ConfigureAwait(false);
65+
66+
if (result.ExitCode != 0)
67+
{
68+
throw new TonLibNonZeroExitCodeException(result.ExitCode);
69+
}
70+
71+
return result.Stack[0].ToTvmCell().ToBoc().RootCells[0].BeginRead().LoadAddressIntStd();
3172
}
3273

3374
/// <summary>
@@ -204,24 +245,15 @@ public async Task<DnsEntries> GetEntries(ITonClient tonClient, string nftAddress
204245
/// Resolves *.ton domain to DNS Item NFT and parses data of this contract.
205246
/// </summary>
206247
/// <param name="tonClient"><see cref="ITonClient"/> instance.</param>
207-
/// <param name="domainName">Domain name to get data from. Second-level only, e.g. 'alice.ton'.</param>
248+
/// <param name="domainName">Domain name to resolve, with or without '.ton' suffix.</param>
208249
/// <returns><see cref="DomainInfo"/> with data about domain.</returns>
209250
/// <remarks>⚠ Method may fail if future versions of <see href="https://github.com/ton-blockchain/dns-contract/blob/main/func/nft-item.fc">DNS Item smartcontract</see> will change stored data layout.</remarks>
210-
/// <exception cref="ArgumentOutOfRangeException">Requested domainName is not second-level one.</exception>
251+
/// <remarks>Only second-level .ton domains are allowed, with or without '.ton' suffix. E.g. 'alice.ton.', 'alice.ton' and 'alice' are allowed, but 'alice.t.me' is not.</remarks>
252+
/// <exception cref="ArgumentOutOfRangeException">Requested <paramref name="domainName"/> is not second-level one, or not from '.ton' namespace.</exception>
253+
/// <exception cref="ArgumentNullException">Requested <paramref name="domainName"/> null or white-space only.</exception>
211254
public async Task<DomainInfo> GetAllInfo(ITonClient tonClient, string domainName)
212255
{
213-
// Count dots in name, but not count last one.
214-
var depth = domainName.Count(x => x == '.') - (domainName.EndsWith('.') ? 1 : 0);
215-
if (depth != 1)
216-
{
217-
throw new ArgumentOutOfRangeException(nameof(domainName), "Only second-level domains (e.g. alice.ton) are supported");
218-
}
219-
220-
await tonClient.InitIfNeeded().ConfigureAwait(false);
221-
222-
var resolved = await tonClient.DnsResolve(domainName, ttl: 1); // only resolve in root contract
223-
224-
var address = ((EntryDataNextResolver)resolved.Entries[0].Value).Resolver.Value;
256+
var address = await GetNftAddress(tonClient, domainName).ConfigureAwait(false);
225257

226258
var di = new DomainInfo
227259
{

0 commit comments

Comments
 (0)