diff --git a/Substrate.NetApi.Test/Extrinsic/EraTest.cs b/Substrate.NetApi.Test/Extrinsic/EraTest.cs index 9b8f31d..0464b24 100644 --- a/Substrate.NetApi.Test/Extrinsic/EraTest.cs +++ b/Substrate.NetApi.Test/Extrinsic/EraTest.cs @@ -39,10 +39,21 @@ public void EraEncodeDecodeTest() } [Test] - public void EraBeginTest() + [TestCase(64u, 49u, 1587u, 1585u)] + [TestCase(64u, 45u, 21604404u, 21604397u)] + public void EraBeginTest(ulong period, ulong phase, ulong currentBlock, ulong expectedBlock) { - var era = new Era(64, 49, false); - Assert.AreEqual(1585, era.EraStart(1587)); + var era = new Era(period, phase, false); + Assert.AreEqual(expectedBlock, era.EraStart(currentBlock)); + } + + [Test] + [TestCase(64u, 49u, 1587u, 1649u)] + [TestCase(64u, 45u, 21604404u, 21604461u)] + public void EraEndTest(ulong period, ulong phase, ulong currentBlock, ulong expectedBlock) + { + var era = new Era(period, phase, false); + Assert.AreEqual(expectedBlock, era.EraEnd(currentBlock)); } [Test] diff --git a/Substrate.NetApi.Test/Substrate.NetApi.Test.csproj b/Substrate.NetApi.Test/Substrate.NetApi.Test.csproj index f42a896..b2d8207 100644 --- a/Substrate.NetApi.Test/Substrate.NetApi.Test.csproj +++ b/Substrate.NetApi.Test/Substrate.NetApi.Test.csproj @@ -7,6 +7,7 @@ + diff --git a/Substrate.NetApi.TestNode/ClientTests.cs b/Substrate.NetApi.TestNode/ClientTests.cs new file mode 100644 index 0000000..4bb339a --- /dev/null +++ b/Substrate.NetApi.TestNode/ClientTests.cs @@ -0,0 +1,96 @@ +using NUnit.Framework; +using Substrate.NetApi.Model.Extrinsics; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Substrate.NetApi.TestNode +{ + public class ClientTests + { + private SubstrateClient _client; + + [SetUp] + public void Setup() + { + _client = new SubstrateClient(new Uri("ws://rpc-parachain.bajun.network"), ChargeTransactionPayment.Default()); + } + + [TearDown] + public async Task TeardownAsync() + { + await _client.CloseAsync(); + } + + [Test] + public async Task Connect_ShouldConnectSuccessfullyAsync() + { + Assert.That(_client.IsConnected, Is.False); + + await _client.ConnectAsync(); + Assert.That(_client.IsConnected, Is.True); + } + + [Test] + public async Task Connect_ShouldDisconnectSuccessfullyAsync() + { + await _client.ConnectAsync(); + Assert.That(_client.IsConnected, Is.True); + + await _client.CloseAsync(); + Assert.That(_client.IsConnected, Is.False); + } + + [Test] + public async Task Connect_ShouldTriggerEventAsync() + { + var onConnectionSetTriggered = new TaskCompletionSource(); + _client.ConnectionSet += (sender, e) => onConnectionSetTriggered.SetResult(true); + + await _client.ConnectAsync(); + + await Task.WhenAny(onConnectionSetTriggered.Task, Task.Delay(TimeSpan.FromMinutes(1))); + Assert.That(onConnectionSetTriggered.Task.IsCompleted, Is.True); + } + + [Test] + public async Task OnConnectionLost_ShouldThrowDisconnectedEventAsync() + { + var onConnectionLostTriggered = new TaskCompletionSource(); + _client.ConnectionLost += (sender, e) => onConnectionLostTriggered.SetResult(true); + + await _client.ConnectAsync(); + await _client.CloseAsync(); + + await Task.WhenAny(onConnectionLostTriggered.Task, Task.Delay(TimeSpan.FromMinutes(1))); + Assert.That(onConnectionLostTriggered.Task.IsCompleted, Is.True); + } + + [Test] + public async Task ManuallyDisconnect_ShouldNotTryToReconnectAsync() + { + await _client.ConnectAsync(); + await _client.CloseAsync(); + + Assert.That(_client.IsConnected, Is.False); + } + + [Test] + public async Task Disconnect_ShouldTryToReconnectAsync() + { + var onReconnectedTriggered = new TaskCompletionSource<(bool, int)>(); + _client.OnReconnected += (sender, e) => onReconnectedTriggered.SetResult((true, e)); + + await _client.ConnectAsync(); + await _client._socket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); + + await Task.WhenAny(onReconnectedTriggered.Task, Task.Delay(TimeSpan.FromMinutes(1))); + Assert.That(_client.IsConnected, Is.True); + } + } +} diff --git a/Substrate.NetApi/Model/Extrinsics/Era.cs b/Substrate.NetApi/Model/Extrinsics/Era.cs index d80ff82..f38e4bf 100644 --- a/Substrate.NetApi/Model/Extrinsics/Era.cs +++ b/Substrate.NetApi/Model/Extrinsics/Era.cs @@ -31,6 +31,13 @@ public class Era : IEncodable /// public ulong EraStart(ulong currentBlockNumber) => IsImmortal ? 0 : (Math.Max(currentBlockNumber, Phase) - Phase) / Period * Period + Phase; + /// + /// Era End + /// + /// + /// + public ulong EraEnd(ulong currentBlockNumber) => EraStart(currentBlockNumber) + Period; + /// /// Initializes a new instance of the class. /// diff --git a/Substrate.NetApi/SubstrateClient.cs b/Substrate.NetApi/SubstrateClient.cs index 8aa05fb..12b60bd 100644 --- a/Substrate.NetApi/SubstrateClient.cs +++ b/Substrate.NetApi/SubstrateClient.cs @@ -23,6 +23,7 @@ using Substrate.NetApi.Model.Types.Metadata.V14; [assembly: InternalsVisibleTo("Substrate.NetApi.Test")] +[assembly: InternalsVisibleTo("Substrate.NetApi.TestNode")] namespace Substrate.NetApi { @@ -62,10 +63,29 @@ public class SubstrateClient : IDisposable /// private JsonRpc _jsonRpc; + /// The socket. + internal ClientWebSocket _socket; + + /// + /// The "ping" to check the connection status + /// + private int _connectionCheckDelay = 500; + /// - /// The socket + /// The connexion lost event trigger when the websocket change state to disconnected /// - private ClientWebSocket _socket; + public event EventHandler ConnectionLost; + + /// + /// Event triggered when the connection is set + /// + public event EventHandler ConnectionSet; + + /// + /// Event triggered when the connection is reconnected + /// + + public event EventHandler OnReconnected; /// /// Bypass Remote Certificate Validation. Useful when testing with self-signed SSL certificates. @@ -185,6 +205,30 @@ public bool SetJsonRPCTraceLevel(SourceLevels sourceLevels) return true; } + /// + /// Raises the event when the connection to the server is lost. + /// + protected virtual void OnConnectionLost() + { + ConnectionLost?.Invoke(this, EventArgs.Empty); + } + + /// + /// Raises the event when the connection to the server is set. + /// + protected virtual void OnConnectionSet() + { + ConnectionSet?.Invoke(this, EventArgs.Empty); + } + + /// + /// Raises the event when reconnected + /// + protected virtual void OnReconnectedSet(int nbTry) + { + OnReconnected?.Invoke(this, nbTry); + } + /// /// Asynchronously connects to the node. /// @@ -259,7 +303,7 @@ public async Task ConnectAsync(bool useMetaData, bool standardSubstrate, int max #if NETSTANDARD2_0 throw new NotSupportedException("Bypass remote certification validation not supported in NETStandard2.0"); #elif NETSTANDARD2_1_OR_GREATER - _socket.Options.RemoteCertificateValidationCallback = (sender, certificate, chain, errors) => true; + _socket.Options.RemoteCertificateValidationCallback = (sender, certificate, chain, errors) => true; #endif } } @@ -267,6 +311,10 @@ public async Task ConnectAsync(bool useMetaData, bool standardSubstrate, int max _connectTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)); var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token, _connectTokenSource.Token); await _socket.ConnectAsync(_uri, linkedTokenSource.Token); + + // Triger the event + OnConnectionSet(); + linkedTokenSource.Dispose(); _connectTokenSource.Dispose(); _connectTokenSource = null; @@ -333,6 +381,9 @@ public async Task ConnectAsync(bool useMetaData, bool standardSubstrate, int max private void OnJsonRpcDisconnected(object sender, JsonRpcDisconnectedEventArgs e) { Logger.Error(e.Exception, $"JsonRpc disconnected: {e.Reason}"); + OnConnectionLost(); + + if (_jsonRpc == null || _jsonRpc.IsDisposed) return; // Attempt to reconnect asynchronously _ = Task.Run(async () => @@ -368,6 +419,8 @@ await ConnectAsync( ); Logger.Information("Reconnected successfully."); + + OnReconnectedSet(retry); } catch (Exception ex) { @@ -563,7 +616,7 @@ public async Task CloseAsync(CancellationToken token) { _connectTokenSource?.Cancel(); - await Task.Run(() => + await Task.Run(async () => { // cancel remaining request tokens foreach (var key in _requestTokenSourceDict.Keys) key?.Cancel(); @@ -571,6 +624,7 @@ await Task.Run(() => if (_socket != null && _socket.State == WebSocketState.Open) { + await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); _jsonRpc?.Dispose(); Logger.Debug("Client closed."); } @@ -629,4 +683,4 @@ public void Dispose() #endregion IDisposable Support } -} \ No newline at end of file +}