Skip to content

Commit 78488e1

Browse files
committed
v0.12: AddressValidator
1 parent a4f6ca1 commit 78488e1

File tree

5 files changed

+320
-2
lines changed

5 files changed

+320
-2
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
namespace TonLibDotNet.Utils
2+
{
3+
public class AddressValidatorMakeTests
4+
{
5+
[Theory]
6+
// Wallet Bot
7+
[InlineData(0, "dddcd3cdad60af4c0d69389f567ad51d0c263fa4968d655ab424c69aadaf9322", "EQDd3NPNrWCvTA1pOJ9WetUdDCY_pJaNZVq0JMaara-TIp90")]
8+
// BSC Bridge
9+
[InlineData(-1, "4d5c0210b35daddaa219fac459dba0fdefb1fae4e97a0d0797739fe050d694ca", "Ef9NXAIQs12t2qIZ-sRZ26D977H65Ol6DQeXc5_gUNaUys5r")]
10+
public void ItWorks(int workchainId, string accountIdHex, string expected)
11+
{
12+
var accountId = Convert.FromHexString(accountIdHex);
13+
var account = AddressValidator.MakeAddress((byte)workchainId, accountId);
14+
Assert.Equal(expected, account);
15+
}
16+
17+
[Theory]
18+
// Wallet Bot
19+
[InlineData(0, "dddcd3cdad60af4c0d69389f567ad51d0c263fa4968d655ab424c69aadaf9322", "UQDd3NPNrWCvTA1pOJ9WetUdDCY_pJaNZVq0JMaara-TIsKx")]
20+
public void NonBounceableWorks(int workchainId, string accountIdHex, string expected)
21+
{
22+
var accountId = Convert.FromHexString(accountIdHex);
23+
var account = AddressValidator.MakeAddress((byte)workchainId, accountId, bounceable: false);
24+
Assert.Equal(expected, account);
25+
}
26+
27+
[Theory]
28+
// "test giver" smart contract
29+
[InlineData(-1, "fcb91a3a3816d0f7b8c2c76108b8a9bc5a6b7a55bd79f8ab101c52db29232260", "kf_8uRo6OBbQ97jCx2EIuKm8Wmt6Vb15-KsQHFLbKSMiYIny")]
30+
public void TestnetOnlyWorks(int workchainId, string accountIdHex, string expected)
31+
{
32+
var accountId = Convert.FromHexString(accountIdHex);
33+
var account = AddressValidator.MakeAddress((byte)workchainId, accountId, testnetOnly: true);
34+
Assert.Equal(expected, account);
35+
}
36+
37+
[Theory]
38+
// "test giver" smart contract
39+
[InlineData(-1, "fcb91a3a3816d0f7b8c2c76108b8a9bc5a6b7a55bd79f8ab101c52db29232260", "kf/8uRo6OBbQ97jCx2EIuKm8Wmt6Vb15+KsQHFLbKSMiYIny")]
40+
public void UrlSafeWorks(int workchainId, string accountIdHex, string expected)
41+
{
42+
var accountId = Convert.FromHexString(accountIdHex);
43+
var account = AddressValidator.MakeAddress((byte)workchainId, accountId, testnetOnly: true, urlSafe: false);
44+
Assert.Equal(expected, account);
45+
}
46+
}
47+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
namespace TonLibDotNet.Utils
2+
{
3+
public class AddressValidatorParseTests
4+
{
5+
[Fact]
6+
public void ParseDefault()
7+
{
8+
// Wallet Bot
9+
Assert.True(AddressValidator.TryParseAddress("EQDd3NPNrWCvTA1pOJ9WetUdDCY_pJaNZVq0JMaara-TIp90", out var workchainId, out var accountId, out var bounceable, out var testnetOnly, out var urlSafe));
10+
11+
Assert.Equal(0, workchainId);
12+
Assert.Equal("dddcd3cdad60af4c0d69389f567ad51d0c263fa4968d655ab424c69aadaf9322", Convert.ToHexString(accountId), true);
13+
Assert.True(bounceable);
14+
Assert.False(testnetOnly);
15+
Assert.True(urlSafe);
16+
}
17+
18+
[Fact]
19+
public void ParseMasterchain()
20+
{
21+
// BSC Bridge
22+
Assert.True(AddressValidator.TryParseAddress("Ef9NXAIQs12t2qIZ-sRZ26D977H65Ol6DQeXc5_gUNaUys5r", out var workchainId, out var accountId, out var bounceable, out var testnetOnly, out var urlSafe));
23+
24+
Assert.Equal(AddressValidator.MasterchainId, workchainId);
25+
Assert.Equal("4d5c0210b35daddaa219fac459dba0fdefb1fae4e97a0d0797739fe050d694ca", Convert.ToHexString(accountId), true);
26+
}
27+
28+
[Fact]
29+
public void ParseNonBounceable()
30+
{
31+
// Wallet Bot
32+
Assert.True(AddressValidator.TryParseAddress("UQDd3NPNrWCvTA1pOJ9WetUdDCY_pJaNZVq0JMaara-TIsKx", out var workchainId, out var accountId, out var bounceable, out var testnetOnly, out var urlSafe));
33+
34+
Assert.Equal(0, workchainId);
35+
Assert.Equal("dddcd3cdad60af4c0d69389f567ad51d0c263fa4968d655ab424c69aadaf9322", Convert.ToHexString(accountId), true);
36+
Assert.False(bounceable);
37+
}
38+
39+
[Fact]
40+
public void ParseUrlUnsafe()
41+
{
42+
// Wallet Bot
43+
Assert.True(AddressValidator.TryParseAddress("EQDd3NPNrWCvTA1pOJ9WetUdDCY/pJaNZVq0JMaara+TIp90", out var workchainId, out var accountId, out var bounceable, out var testnetOnly, out var urlSafe));
44+
45+
Assert.Equal(0, workchainId);
46+
Assert.Equal("dddcd3cdad60af4c0d69389f567ad51d0c263fa4968d655ab424c69aadaf9322", Convert.ToHexString(accountId), true);
47+
Assert.False(urlSafe);
48+
}
49+
50+
[Fact]
51+
public void FailsOnWrongStringLength()
52+
{
53+
Assert.False(AddressValidator.TryParseAddress("EQDd3NPNrWCvTA1pOJ9WetUdDC", out _, out _, out _, out _, out _));
54+
}
55+
56+
[Fact]
57+
public void FailsOnNonBase64()
58+
{
59+
// Wallet Bot, with '~' in first position
60+
Assert.False(AddressValidator.TryParseAddress("~QDd3NPNrWCvTA1pOJ9WetUdDCY_pJaNZVq0JMaara-TIp90", out _, out _, out _, out _, out _));
61+
}
62+
63+
[Fact]
64+
public void FailsOnWrongChecksum()
65+
{
66+
// Wallet Bot, but last char changed from 0 to 1
67+
Assert.False(AddressValidator.TryParseAddress("EQDd3NPNrWCvTA1pOJ9WetUdDCY_pJaNZVq0JMaara-TIp91", out _, out _, out _, out _, out _));
68+
}
69+
70+
[Fact]
71+
public void FailsOnSafeUnsafeMix()
72+
{
73+
// Wallet Bot, with '_' changed to '+', but '_' not changed to '/'
74+
Assert.False(AddressValidator.TryParseAddress("EQDd3NPNrWCvTA1pOJ9WetUdDCY_pJaNZVq0JMaara+TIp90", out _, out _, out _, out _, out _));
75+
}
76+
}
77+
}

TonLibDotNet/TonLibDotNet.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
<RepositoryType>git</RepositoryType>
1717
<PackageTags>ton, toncoin, the-open-network, tonlib, libtonlibjson</PackageTags>
1818
<PackageLicenseFile>LICENSE</PackageLicenseFile>
19-
<Version>0.11.0</Version>
20-
<PackageReleaseNotes>TVM.* and SMC.* classes added (now you can call get-methods on smartcontracts, check Demo project for details).</PackageReleaseNotes>
19+
<Version>0.12.0</Version>
20+
<PackageReleaseNotes>New: AddressValidator to check addresses, typed by users, without calling tonlib/liteclient: detect bounceable/non-bounceable and testnet-only, check workchain-id, verify checksum. Build address string with required param values.</PackageReleaseNotes>
2121
<Description>TonLib (libtonlibjson) wrapper for accessing Telegram Open Network lite servers (nodes) via ADNL protocol.</Description>
2222
</PropertyGroup>
2323

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
3+
namespace TonLibDotNet.Utils
4+
{
5+
/// <seealso href="https://docs.ton.org/learn/overviews/addresses#user-friendly-address">User-Friendly Address Structure</seealso>
6+
public static class AddressValidator
7+
{
8+
public const byte FlagBounceable = 0x11;
9+
public const byte FlagNonBounceable = 0x51;
10+
public const byte FlagTestnetOnly = 0x80;
11+
12+
public const byte BasechainId = 0x00;
13+
public const byte MasterchainId = 0xFF;
14+
15+
public static string MakeAddress(byte workchainId, byte[] accountId, bool bounceable = true, bool testnetOnly = false, bool urlSafe = true)
16+
{
17+
ArgumentNullException.ThrowIfNull(accountId);
18+
19+
if (accountId.Length != 32)
20+
{
21+
throw new ArgumentException("Must be 32 bytes", nameof(accountId));
22+
}
23+
24+
var bytes = new byte[36];
25+
26+
bytes[0] = bounceable ? FlagBounceable : FlagNonBounceable;
27+
28+
if (testnetOnly)
29+
{
30+
bytes[0] |= FlagTestnetOnly;
31+
}
32+
33+
bytes[1] = workchainId;
34+
35+
accountId.CopyTo(bytes, 2);
36+
37+
var span = bytes.AsSpan();
38+
var crc = Crc16.Ccitt.ComputeChecksum(span[..^2]);
39+
System.Buffers.Binary.BinaryPrimitives.WriteUInt16BigEndian(span[^2..], crc);
40+
41+
var address = Convert.ToBase64String(bytes);
42+
43+
if (!urlSafe)
44+
{
45+
return address;
46+
}
47+
48+
// use '_' and '-' instead of '/' and '+'
49+
var chars = address.ToCharArray();
50+
for (var i = 0; i < chars.Length; i++)
51+
{
52+
if (chars[i] == '/')
53+
{
54+
chars[i] = '_';
55+
}
56+
else if (chars[i] == '+')
57+
{
58+
chars[i] = '-';
59+
}
60+
}
61+
62+
return new string(chars);
63+
}
64+
65+
public static bool TryParseAddress(
66+
string address,
67+
out byte workchainId,
68+
[NotNullWhen(true)] out byte[]? accountId,
69+
out bool bounceable,
70+
out bool testnetOnly,
71+
out bool urlSafe)
72+
{
73+
if (string.IsNullOrEmpty(address))
74+
{
75+
throw new ArgumentNullException(nameof(address));
76+
}
77+
78+
workchainId = default;
79+
accountId = default;
80+
bounceable = default;
81+
testnetOnly = default;
82+
urlSafe = default;
83+
84+
if (address.Length != 48)
85+
{
86+
return false;
87+
}
88+
89+
var chars = address.ToCharArray();
90+
var safeFound = false;
91+
var unsafeFound = false;
92+
for (var i = 0; i < chars.Length; i++)
93+
{
94+
switch (chars[i])
95+
{
96+
case '/':
97+
case '+':
98+
unsafeFound = true;
99+
break;
100+
101+
case '_':
102+
safeFound = true;
103+
chars[i] = '/';
104+
break;
105+
106+
case '-':
107+
safeFound = true;
108+
chars[i] = '+';
109+
break;
110+
}
111+
}
112+
113+
if (safeFound && unsafeFound)
114+
{
115+
return false;
116+
}
117+
118+
urlSafe = !unsafeFound;
119+
120+
var bytes = new byte[36];
121+
122+
if (!Convert.TryFromBase64Chars(chars, bytes, out _))
123+
{
124+
return false;
125+
}
126+
127+
testnetOnly = (bytes[0] & FlagTestnetOnly) == FlagTestnetOnly;
128+
129+
bounceable = (bytes[0] & FlagNonBounceable) == FlagBounceable;
130+
131+
workchainId = bytes[1];
132+
accountId = bytes[2..^2].ToArray();
133+
134+
var checksum = Crc16.Ccitt.ComputeChecksum(bytes);
135+
if (checksum != 0)
136+
{
137+
return false;
138+
}
139+
140+
return true;
141+
}
142+
}
143+
}

TonLibDotNet/Utils/Crc16.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System.Buffers.Binary;
2+
3+
namespace TonLibDotNet.Utils
4+
{
5+
/// <summary>
6+
/// Based on http://sanity-free.org/133/crc_16_ccitt_in_csharp.html
7+
/// </summary>
8+
public class Crc16
9+
{
10+
private readonly ushort[] table = new ushort[256];
11+
12+
public Crc16(ushort initialValue, ushort poly)
13+
{
14+
this.InitialValue = initialValue;
15+
16+
ushort temp, a;
17+
for (int i = 0; i < table.Length; ++i)
18+
{
19+
temp = 0;
20+
a = (ushort)(i << 8);
21+
for (int j = 0; j < 8; ++j)
22+
{
23+
if (((temp ^ a) & 0x8000) != 0)
24+
{
25+
temp = (ushort)((temp << 1) ^ poly);
26+
}
27+
else
28+
{
29+
temp <<= 1;
30+
}
31+
a <<= 1;
32+
}
33+
table[i] = temp;
34+
}
35+
}
36+
37+
public static Crc16 Ccitt { get; } = new Crc16(0x0000, 0x1021);
38+
39+
public ushort InitialValue { get; private set; }
40+
41+
public ushort ComputeChecksum(ReadOnlySpan<byte> bytes)
42+
{
43+
ushort crc = InitialValue;
44+
for (int i = 0; i < bytes.Length; ++i)
45+
{
46+
crc = (ushort)((crc << 8) ^ table[(crc >> 8) ^ (0xff & bytes[i])]);
47+
}
48+
return crc;
49+
}
50+
}
51+
}

0 commit comments

Comments
 (0)