diff --git a/Substrate.NetApi.TestNode/ExtrinsicsTest.cs b/Substrate.NetApi.TestNode/ExtrinsicsTest.cs index 5133a15..f3cd67a 100644 --- a/Substrate.NetApi.TestNode/ExtrinsicsTest.cs +++ b/Substrate.NetApi.TestNode/ExtrinsicsTest.cs @@ -1,15 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using NUnit.Framework; +using NUnit.Framework.Internal; +using Schnorrkel.Keys; using Substrate.NetApi.Model.Extrinsics; using Substrate.NetApi.Model.Rpc; using Substrate.NetApi.Model.Types; using Substrate.NetApi.Model.Types.Base; -using Substrate.NetApi.Model.Types.Primitive; -using NUnit.Framework; -using Schnorrkel.Keys; +using System; +using System.Threading; +using System.Threading.Tasks; namespace Substrate.NetApi.TestNode { @@ -17,7 +15,7 @@ public class ExtrinsicsTest { public MiniSecret MiniSecretAlice => new MiniSecret(Utils.HexToByteArray("0xe5be9a5092b81bca64be81d212e7f2f9eba183bb7a90954f7b76361f6edb5c0a"), ExpandMode.Ed25519); public Account Alice => Account.Build(KeyType.Sr25519, MiniSecretAlice.ExpandToSecret().ToBytes(), MiniSecretAlice.GetPair().Public.Key); - + public MiniSecret MiniSecretBob => new MiniSecret(Utils.HexToByteArray("0x398f0c28f98885e046333d4a41c19cee4c37368a9832c6502f6cfd182e2aef89"), ExpandMode.Ed25519); public Account Bob => Account.Build(KeyType.Sr25519, MiniSecretBob.ExpandToSecret().ToBytes(), MiniSecretBob.GetPair().Public.Key); @@ -67,119 +65,89 @@ public async Task TearDownAsync() } /// - /// Extrinsic Remark test + /// Extrinsic Submit And Watch /// /// [Test] - public async Task Extrinsic_RemarkTestAsync() + public async Task Extrinsic_SubmitAndWatchExtrinsicAsync() { var method = new Method(0, "System", 0, "remark", new byte[] { 0x04, 0xFF }); var taskCompletionSource = new TaskCompletionSource<(bool, Hash)>(); - await _substrateClient.Author.SubmitAndWatchExtrinsicAsync((string subscriptionId, ExtrinsicStatus extrinsicUpdate) => Callback(subscriptionId, extrinsicUpdate, taskCompletionSource), method, Alice, _chargeType, 64, CancellationToken.None); - - var finished = await Task.WhenAny(taskCompletionSource.Task, Task.Delay(TimeSpan.FromMinutes(1))); // 5 minutes or any appropriate timeout + await _substrateClient.Author.SubmitAndWatchExtrinsicAsync((string subscriptionId, ExtrinsicStatus extrinsicUpdate) => + { + if (extrinsicUpdate.ExtrinsicState == ExtrinsicState.Finalized || + extrinsicUpdate.ExtrinsicState == ExtrinsicState.Dropped || + extrinsicUpdate.ExtrinsicState == ExtrinsicState.Invalid) + { + taskCompletionSource.SetResult((true, extrinsicUpdate.Hash)); + } + }, method, Alice, _chargeType, 64, CancellationToken.None); + + var finished = await Task.WhenAny(taskCompletionSource.Task, Task.Delay(TimeSpan.FromMinutes(1))); Assert.AreEqual(taskCompletionSource.Task, finished, "Test timed out waiting for final callback"); } + /// + /// Transaction Unstable Submit And Watch + /// + /// [Test] - public async Task Extrinsic_FailTestAsync() + public async Task Extrinsic_TransactionUnstableSubmitAndWatchAsync() { - - var method = new Method(0, "System", 1, "set_heap_pages", new U64(999).Encode()); + var method = new Method(0, "System", 0, "remark", new byte[] { 0x04, 0xFF }); var taskCompletionSource = new TaskCompletionSource<(bool, Hash)>(); - - var account = Bob; - - var subscriptionId = await _substrateClient.Author.SubmitAndWatchExtrinsicAsync((string subscriptionId, ExtrinsicStatus extrinsicUpdate) => Callback(subscriptionId, extrinsicUpdate, taskCompletionSource), method, account, _chargeType, 64, CancellationToken.None); - - var finished = await Task.WhenAny(taskCompletionSource.Task, Task.Delay(TimeSpan.FromMinutes(1))); // 5 minutes or any appropriate timeout - Assert.AreEqual(taskCompletionSource.Task, finished, "Test timed out waiting for final callback"); - - var block = await _substrateClient.Chain.GetBlockAsync((await taskCompletionSource.Task).Item2); - - var accountExtrinsics = block.Block.Extrinsics.Where(p => p.Signed && p.Account.Value == account.Value); - Assert.AreEqual(1, accountExtrinsics.Count()); - - } - - /// - /// Extrinsic Transfer Callback test - /// - /// - /// - /// - private static void Callback(string subscriptionId, ExtrinsicStatus extrinsicUpdate, TaskCompletionSource<(bool, Hash)> taskCompletionSource) - { - ActionExtrinsicUpdate(subscriptionId, extrinsicUpdate); - if (extrinsicUpdate.ExtrinsicState == ExtrinsicState.Finalized || - extrinsicUpdate.ExtrinsicState == ExtrinsicState.Dropped || - extrinsicUpdate.ExtrinsicState == ExtrinsicState.Invalid) + _ = await _substrateClient.Unstable.TransactionUnstableSubmitAndWatchAsync((string subscriptionId, TransactionEventInfo extrinsicUpdate) => { - taskCompletionSource.SetResult((true, extrinsicUpdate.Hash)); - } + if (extrinsicUpdate.TransactionEvent == TransactionEvent.Finalized || + extrinsicUpdate.TransactionEvent == TransactionEvent.Dropped || + extrinsicUpdate.TransactionEvent == TransactionEvent.Invalid || + extrinsicUpdate.TransactionEvent == TransactionEvent.Error) + { + taskCompletionSource.SetResult((true, extrinsicUpdate.Hash)); + } + }, method, Alice, _chargeType, 64, CancellationToken.None); + + var finished = await Task.WhenAny(taskCompletionSource.Task, Task.Delay(TimeSpan.FromMinutes(1))); + Assert.AreEqual(taskCompletionSource.Task, finished, "Test timed out waiting for final callback"); } /// - /// Simple extrinsic tester + /// Transaction Unstable Unwatch /// - /// - /// - private static void ActionExtrinsicUpdate(string subscriptionId, ExtrinsicStatus extrinsicUpdate) + /// + [Test, Timeout(10000)] // Timeout after 10 seconds + public async Task Extrinsic_TransactionUnstableUnwatchAsync() { - if (subscriptionId == null || subscriptionId.Length == 0) + var method = new Method(0, "System", 0, "remark", new byte[] { 0x04, 0xFF }); + var cancellationTokenSource = new CancellationTokenSource(); + + var taskCompletionSource = new TaskCompletionSource(); + var subscriptionId = await _substrateClient.Unstable.TransactionUnstableSubmitAndWatchAsync( + (subscriptionId, extrinsicUpdate) => + { + { + if (extrinsicUpdate.TransactionEvent != TransactionEvent.Validated) + { + taskCompletionSource.SetResult(true); + } + } + }, + method, Alice, _chargeType, 64, cancellationTokenSource.Token); + + var unsubscribed = await _substrateClient.Unstable.TransactionUnstableUnwatchAsync(subscriptionId); + Assert.IsTrue(unsubscribed, "Unsubscribing from transaction should be successful."); + + // Optionally: wait for the callback to be called, which should not happen + var callbackCalled = await Task.WhenAny(taskCompletionSource.Task, Task.Delay(500)); + if (callbackCalled == taskCompletionSource.Task) { - Assert.IsTrue(false); + Assert.Fail("Callback should not be called after unsubscribing."); } - switch (extrinsicUpdate.ExtrinsicState) - { - case ExtrinsicState.Future: - Assert.IsTrue(false); - break; - - case ExtrinsicState.Ready: - Assert.IsTrue(true); - break; - - case ExtrinsicState.Dropped: - Assert.IsTrue(false); - break; - - case ExtrinsicState.Invalid: - Assert.IsTrue(false); - break; - - case ExtrinsicState.Broadcast: - Assert.IsTrue(extrinsicUpdate.Broadcast != null); - break; - - case ExtrinsicState.InBlock: - Assert.IsTrue(extrinsicUpdate.Hash.Value.Length > 0); - break; - - case ExtrinsicState.Retracted: - Assert.IsTrue(extrinsicUpdate.Hash.Value.Length > 0); - break; - - case ExtrinsicState.FinalityTimeout: - Assert.IsTrue(extrinsicUpdate.Hash.Value.Length > 0); - break; - - case ExtrinsicState.Finalized: - Assert.IsTrue(extrinsicUpdate.Hash.Value.Length > 0); - break; - - case ExtrinsicState.Usurped: - Assert.IsTrue(extrinsicUpdate.Hash.Value.Length > 0); - break; - - default: - Assert.IsTrue(false); - break; - - } + // Cleanup if needed + cancellationTokenSource.Cancel(); } } } \ No newline at end of file diff --git a/Substrate.NetApi/Model/Rpc/ExtrinsicStatus.cs b/Substrate.NetApi/Model/Rpc/ExtrinsicStatus.cs index 8a6b590..4db6061 100644 --- a/Substrate.NetApi/Model/Rpc/ExtrinsicStatus.cs +++ b/Substrate.NetApi/Model/Rpc/ExtrinsicStatus.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using Substrate.NetApi.Model.Types.Base; +using System; namespace Substrate.NetApi.Model.Rpc { diff --git a/Substrate.NetApi/Model/Rpc/TransactionEventInfo.cs b/Substrate.NetApi/Model/Rpc/TransactionEventInfo.cs new file mode 100644 index 0000000..5e9c891 --- /dev/null +++ b/Substrate.NetApi/Model/Rpc/TransactionEventInfo.cs @@ -0,0 +1,36 @@ +using Substrate.NetApi.Model.Types.Base; + +namespace Substrate.NetApi.Model.Rpc +{ + public enum TransactionEvent + { + Validated, + + Broadcasted, + + BestChainBlockIncluded, + + Finalized, + + Error, + + Invalid, + + Dropped + } + + public sealed class TransactionEventInfo + { + public TransactionEvent TransactionEvent { get; set; } + + public uint? NumPeers { get; set; } + + public Hash Hash { get; set; } + + public uint? Index { get; set; } + + public bool? Broadcasted { get; set; } + + public string Error { get; set; } + } +} \ No newline at end of file diff --git a/Substrate.NetApi/Modules/Contracts/IUnstableCalls.cs b/Substrate.NetApi/Modules/Contracts/IUnstableCalls.cs new file mode 100644 index 0000000..6e51f2d --- /dev/null +++ b/Substrate.NetApi/Modules/Contracts/IUnstableCalls.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Substrate.NetApi.Model.Extrinsics; +using Substrate.NetApi.Model.Rpc; +using Substrate.NetApi.Model.Types; +using Substrate.NetApi.Model.Types.Base; + +namespace Substrate.NetApi.Modules.Contracts +{ + public interface IUnstableCalls + { + /// + /// Submit and subscribe to watch an extrinsic until unsubscribed + /// + /// + /// + /// + /// + /// + /// + Task TransactionUnstableSubmitAndWatchAsync(Action callback, Method method, Account account, ChargeType charge, uint lifeTime); + + /// + /// Submit and subscribe to watch an extrinsic until unsubscribed + /// + /// + /// + /// + /// + /// + /// + /// + Task TransactionUnstableSubmitAndWatchAsync(Action callback, Method method, Account account, ChargeType charge, uint lifeTime, CancellationToken token); + + /// + /// Submit and subscribe to watch an extrinsic until unsubscribed + /// + /// + /// + /// + Task TransactionUnstableSubmitAndWatchAsync(Action callback, string parameters); + + /// + /// Submit and subscribe to watch an extrinsic until unsubscribed + /// + /// + /// + /// + /// + Task TransactionUnstableSubmitAndWatchAsync(Action callback, string parameters, CancellationToken token); + + /// + /// Unsuscribe to given subscription id + /// + /// + /// + Task TransactionUnstableUnwatchAsync(string subscriptionId); + + /// + /// Unsuscribe to given subscription id + /// + /// + /// + /// + Task TransactionUnstableUnwatchAsync(string subscriptionId, CancellationToken token); + } +} \ No newline at end of file diff --git a/Substrate.NetApi/Modules/UnstableCalls.cs b/Substrate.NetApi/Modules/UnstableCalls.cs new file mode 100644 index 0000000..421896c --- /dev/null +++ b/Substrate.NetApi/Modules/UnstableCalls.cs @@ -0,0 +1,111 @@ +using Substrate.NetApi.Model.Extrinsics; +using Substrate.NetApi.Model.Rpc; +using Substrate.NetApi.Model.Types; +using Substrate.NetApi.Modules.Contracts; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Substrate.NetApi.Modules +{ + /// + /// New Api 2 + /// + public class UnstableCalls : IUnstableCalls + { + /// The client + private readonly SubstrateClient _client; + + /// + /// New Api 2 + /// + /// + internal UnstableCalls(SubstrateClient client) + { + _client = client; + } + + /// + /// Transaction Unstable Submit And Watch Async + /// + /// + /// + /// + /// + /// + /// + public async Task TransactionUnstableSubmitAndWatchAsync(Action callback, Method method, Account account, ChargeType charge, uint lifeTime) + { + var extrinsic = await _client.GetExtrinsicParametersAsync(method, account, charge, lifeTime, signed: true, CancellationToken.None); + + return await TransactionUnstableSubmitAndWatchAsync(callback, Utils.Bytes2HexString(extrinsic.Encode())); + } + + /// + /// Transaction Unstable Submit And Watch Async + /// + /// + /// + /// + /// + /// + /// + /// + public async Task TransactionUnstableSubmitAndWatchAsync(Action callback, Method method, Account account, ChargeType charge, uint lifeTime, CancellationToken token) + { + var extrinsic = await _client.GetExtrinsicParametersAsync(method, account, charge, lifeTime, signed: true, token); + var extrinsicHex = Utils.Bytes2HexString(extrinsic.Encode()); + return await TransactionUnstableSubmitAndWatchAsync(callback, extrinsicHex); + } + + /// + /// Transaction Unstable Submit And Watch Async + /// + /// + /// + /// + public async Task TransactionUnstableSubmitAndWatchAsync(Action callback, string parameters) + { + return await TransactionUnstableSubmitAndWatchAsync(callback, parameters, CancellationToken.None); + } + + /// + /// Transaction Unstable Submit And Watch Async + /// + /// + /// + /// + /// + public async Task TransactionUnstableSubmitAndWatchAsync(Action callback, string parameters, CancellationToken token) + { + var subscriptionId = + await _client.InvokeAsync("transaction_unstable_submitAndWatch", new object[] { parameters }, token); + _client.Listener.RegisterCallBackHandler(subscriptionId, callback); + return subscriptionId; + } + + /// + /// Transaction Unstable Unwatch Async + /// + /// + /// + public async Task TransactionUnstableUnwatchAsync(string subscriptionId) + { + return await TransactionUnstableUnwatchAsync(subscriptionId, CancellationToken.None); + } + + /// + /// Transaction Unstable Unwatch Async + /// + /// + /// + /// + public async Task TransactionUnstableUnwatchAsync(string subscriptionId, CancellationToken token) + { + var result = + await _client.InvokeAsync("transaction_unstable_unwatch", new object[] { subscriptionId }, token); + if (result) _client.Listener.UnregisterHeaderHandler(subscriptionId); + return result; + } + } +} \ No newline at end of file diff --git a/Substrate.NetApi/SubscriptionListener.cs b/Substrate.NetApi/SubscriptionListener.cs index cbd998c..adeb198 100644 --- a/Substrate.NetApi/SubscriptionListener.cs +++ b/Substrate.NetApi/SubscriptionListener.cs @@ -142,5 +142,11 @@ public void AuthorSubmitAndWatchExtrinsic(string subscription, ExtrinsicStatus r { GenericCallBack(subscription, result); } + + [JsonRpcMethod("transaction_unstable_submitExtrinsic")] + public void TransactionUnstableSubmitExtrinsic(string subscription, TransactionEventInfo result) + { + GenericCallBack(subscription, result); + } } } \ No newline at end of file diff --git a/Substrate.NetApi/Substrate.NetApi.csproj b/Substrate.NetApi/Substrate.NetApi.csproj index a65d251..40fb86e 100644 --- a/Substrate.NetApi/Substrate.NetApi.csproj +++ b/Substrate.NetApi/Substrate.NetApi.csproj @@ -3,7 +3,7 @@ Substrate.NET.API netstandard2.0;netstandard2.1;net6.0 - 0.9.10 + 0.9.11 Substrate Gaming Substrate Gaming true diff --git a/Substrate.NetApi/SubstrateClient.cs b/Substrate.NetApi/SubstrateClient.cs index 71edbfa..bdb1fd0 100644 --- a/Substrate.NetApi/SubstrateClient.cs +++ b/Substrate.NetApi/SubstrateClient.cs @@ -36,6 +36,8 @@ public class SubstrateClient : IDisposable private readonly ExtrinsicStatusJsonConverter _extrinsicStatusJsonConverter; + private readonly TransactionEventJsonConverter _transactionEventJsonConverter; + /// The request token sources. private readonly ConcurrentDictionary _requestTokenSourceDict; @@ -66,12 +68,14 @@ public SubstrateClient(Uri uri, ChargeType chargeType, bool bypassRemoteCertific _extrinsicJsonConverter = new ExtrinsicJsonConverter(chargeType); _extrinsicStatusJsonConverter = new ExtrinsicStatusJsonConverter(); + _transactionEventJsonConverter = new TransactionEventJsonConverter(); System = new Modules.System(this); Chain = new Chain(this); Payment = new Payment(this); State = new State(this); Author = new Author(this); + Unstable = new UnstableCalls(this); _requestTokenSourceDict = new ConcurrentDictionary(); } @@ -108,6 +112,11 @@ public SubstrateClient(Uri uri, ChargeType chargeType, bool bypassRemoteCertific /// The author. public Author Author { get; } + /// + /// New Api 2 + /// + public UnstableCalls Unstable { get; } + public SubscriptionListener Listener { get; } = new SubscriptionListener(); /// Gets a value indicating whether this object is connected. @@ -195,6 +204,7 @@ public async Task ConnectAsync(bool useMetaData, bool standardSubstrate, Cancell formatter.JsonSerializer.Converters.Add(new GenericTypeConverter()); formatter.JsonSerializer.Converters.Add(_extrinsicJsonConverter); formatter.JsonSerializer.Converters.Add(_extrinsicStatusJsonConverter); + formatter.JsonSerializer.Converters.Add(_transactionEventJsonConverter); _jsonRpc = new JsonRpc(new WebSocketMessageHandler(_socket, formatter)); _jsonRpc.TraceSource.Listeners.Add(new SerilogTraceListener.SerilogTraceListener()); diff --git a/Substrate.NetApi/TypeConverters/ExtrinsicStatusJsonConverter.cs b/Substrate.NetApi/TypeConverters/ExtrinsicStatusJsonConverter.cs index b365c03..71ec3c3 100644 --- a/Substrate.NetApi/TypeConverters/ExtrinsicStatusJsonConverter.cs +++ b/Substrate.NetApi/TypeConverters/ExtrinsicStatusJsonConverter.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Substrate.NetApi.Model.Rpc; using Substrate.NetApi.Model.Types.Base; using System; @@ -6,6 +7,92 @@ namespace Substrate.NetApi.TypeConverters { + public class TransactionEventJsonConverter : JsonConverter + { + /// Reads the JSON representation of the object. + /// The to read from. + /// Type of the object. + /// The existing value of object being read. If there is no existing value then null will be used. + /// The existing value has a value. + /// The calling serializer. + /// The object value. + /// + /// Unimplemented {reader.TokenType} of type '{reader.ValueType}' and value '{reader.Value}'. + /// or + /// Unimplemented {reader.TokenType} of type '{reader.ValueType}' and value '{reader.Value}'. + /// + public override TransactionEventInfo ReadJson(JsonReader reader, Type objectType, TransactionEventInfo existingValue, bool hasExistingValue, JsonSerializer serializer) + { + var transactionEventStatus = hasExistingValue ? existingValue : new TransactionEventInfo(); + + var jObject = JObject.Load(reader); + + var eventName = jObject["event"]?.ToString(); + if (Enum.TryParse(eventName, true, out TransactionEvent transactionEvent)) + { + transactionEventStatus.TransactionEvent = transactionEvent; + + switch (transactionEvent) + { + case TransactionEvent.Validated: + break; + + case TransactionEvent.Broadcasted: + transactionEventStatus.NumPeers = uint.Parse(jObject["numPeers"].ToString()); + break; + + case TransactionEvent.BestChainBlockIncluded: + var bestChainBlock = jObject["block"]; + if (bestChainBlock != null) + { + transactionEventStatus.Hash = new Hash(jObject["block"]["hash"].ToString()); + transactionEventStatus.Index = uint.Parse(jObject["block"]["index"].ToString()); + } + break; + + case TransactionEvent.Finalized: + transactionEventStatus.Hash = new Hash(jObject["block"]["hash"].ToString()); + transactionEventStatus.Index = uint.Parse(jObject["block"]["index"].ToString()); + break; + + case TransactionEvent.Error: + transactionEventStatus.Error = jObject["error"].ToString(); + break; + + case TransactionEvent.Invalid: + transactionEventStatus.Error = jObject["error"].ToString(); + break; + + case TransactionEvent.Dropped: + // TODO, check if this works broadcassted boolean + //transactionEventStatus.Broadcasted = bool.Parse(jObject["broadcasted"].ToString()); + transactionEventStatus.Error = jObject["error"].ToString(); + break; + + default: + throw new NotImplementedException( + $"Unimplemented state {transactionEvent} with value '{reader.Value}'."); + + + } + } + + return transactionEventStatus; + } + + /// + /// Writes the JSON representation of the object. + /// + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer, TransactionEventInfo value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } + public class ExtrinsicStatusJsonConverter : JsonConverter { /// Reads the JSON representation of the object.