From 6295488b187f730831f8dd69005af15fea2d0952 Mon Sep 17 00:00:00 2001 From: Vadim Obradovich Date: Mon, 2 Dec 2024 13:34:29 +0100 Subject: [PATCH] fix(net): query reply code parse (#699) --- .../Core/RemotingViaNodeClient.cs | 23 +++--- .../SubstrateClientExtExtensions.cs | 74 +++++++++++++++++-- .../Core/RemotingViaNodeClientTests.cs | 42 +++++++++++ 3 files changed, 122 insertions(+), 17 deletions(-) diff --git a/net/src/Sails.Remoting/Core/RemotingViaNodeClient.cs b/net/src/Sails.Remoting/Core/RemotingViaNodeClient.cs index 1f928626..4541b830 100644 --- a/net/src/Sails.Remoting/Core/RemotingViaNodeClient.cs +++ b/net/src/Sails.Remoting/Core/RemotingViaNodeClient.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using EnsureThat; @@ -90,11 +91,9 @@ public RemotingViaNodeClient( cancellationToken), extractResult: static (queuedMessageData, replyMessage) => { - EnsureSuccessOrThrowReplyException(replyMessage.Details.Value.Code, replyMessage.Payload.Bytes); - return ( - (ActorId)queuedMessageData.Value[2], - replyMessage.Payload.Value.Value.Select(@byte => @byte.Value).ToArray() - ); + var payload = replyMessage.Payload.Value.Value.Select(@byte => @byte.Value).ToArray(); + EnsureSuccessOrThrowReplyException(replyMessage.Details.Value.Code, payload); + return ((ActorId)queuedMessageData.Value[2], payload); }, cancellationToken) .ConfigureAwait(false); @@ -141,8 +140,9 @@ public async Task> MessageAsync( cancellationToken), extractResult: static (_, replyMessage) => { - EnsureSuccessOrThrowReplyException(replyMessage.Details.Value.Code, replyMessage.Payload.Bytes); - return replyMessage.Payload.Value.Value.Select(@byte => @byte.Value).ToArray(); + var payload = replyMessage.Payload.Value.Value.Select(@byte => @byte.Value).ToArray(); + EnsureSuccessOrThrowReplyException(replyMessage.Details.Value.Code, payload); + return payload; }, cancellationToken) .ConfigureAwait(false); @@ -208,17 +208,16 @@ private static void EnsureSuccessOrThrowReplyException(EnumReplyCode replyCode, private static string ParseErrorString(byte[] payload) { - var p = 0; - var errorStr = new Str(); + string errorString; try { - errorStr.Decode(payload, ref p); + errorString = Encoding.UTF8.GetString(payload); } catch { - errorStr = new Str("Unexpected reply error"); + errorString = "Unexpected reply error"; } - return errorStr; + return errorString; } private static void ThrowReplyException(EnumReplyCode replyCode, string message) diff --git a/net/src/Substrate.Gear.Client/SubstrateClientExtExtensions.cs b/net/src/Substrate.Gear.Client/SubstrateClientExtExtensions.cs index 8f7f3273..415bd6c6 100644 --- a/net/src/Substrate.Gear.Client/SubstrateClientExtExtensions.cs +++ b/net/src/Substrate.Gear.Client/SubstrateClientExtExtensions.cs @@ -594,11 +594,75 @@ public ReplyInfo ToReplyInfo() { EncodedPayload = Utils.HexToByteArray(this.EncodedPayload), Value = (ValueUnit)this.Value, - // TODO: It is broken. Need to deserialize rust enum (see serde). - Code = new EnumReplyCode() - { - Value = ReplyCode.Success - } + Code = this.Code.DeserializeEnumReplyCode(), }; } + + /// + /// Convert JToken (JObject) wtih single property to EnumReplyCode + /// + /// JObject with single property + /// + /// + /// + internal static EnumReplyCode DeserializeEnumReplyCode(this JToken? token) + { + if (token?.First is not JProperty prop || !Enum.TryParse(prop.Name, out var replyCode)) + { + throw new InvalidOperationException("Failed to convert JToken to EnumReplyCode"); + } + IType value = replyCode switch + { + ReplyCode.Success => DeserializeBaseEnum(prop.Value), + ReplyCode.Error => DeserializeEnumErrorReplyReason(prop.Value), + ReplyCode.Unsupported => new BaseVoid(), + _ => throw new NotImplementedException(), + }; + var enumValue = new EnumReplyCode(); + enumValue.Create(replyCode, value); + return enumValue; + } + + /// + /// Convert JToken (JObject) wtih single property to EnumErrorReplyReason + /// + /// JObject with single property + /// + /// + /// + internal static EnumErrorReplyReason DeserializeEnumErrorReplyReason(this JToken? token) + { + if (token?.First is not JProperty prop || !Enum.TryParse(prop.Name, out var replyReason)) + { + throw new InvalidOperationException("Failed to convert JToken to EnumErrorReplyReason"); + } + IType value = replyReason switch + { + ErrorReplyReason.Execution + => DeserializeBaseEnum(prop.Value), + ErrorReplyReason.FailedToCreateProgram + => DeserializeBaseEnum(prop.Value), + ErrorReplyReason.InactiveActor => new BaseVoid(), + ErrorReplyReason.RemovedFromWaitlist => new BaseVoid(), + ErrorReplyReason.ReinstrumentationFailure => new BaseVoid(), + ErrorReplyReason.Unsupported => new BaseVoid(), + _ => throw new NotImplementedException(), + }; + var enumValue = new EnumErrorReplyReason(); + enumValue.Create(replyReason, value); + return enumValue; + } + + internal static T DeserializeBaseEnum(this JToken? token) + where T : BaseEnum, new() + where TEnum : struct, Enum + { + if (token is not JValue val || !Enum.TryParse(val.ToString(), out var enumValue)) + { + throw new InvalidOperationException($"Failed to convert JToken to {typeof(T).FullName}"); + } + var value = new T(); + value.Create(enumValue); + return value; + } } diff --git a/net/tests/Sails.Remoting.Tests/Core/RemotingViaNodeClientTests.cs b/net/tests/Sails.Remoting.Tests/Core/RemotingViaNodeClientTests.cs index 5b84504d..0e039c70 100644 --- a/net/tests/Sails.Remoting.Tests/Core/RemotingViaNodeClientTests.cs +++ b/net/tests/Sails.Remoting.Tests/Core/RemotingViaNodeClientTests.cs @@ -184,4 +184,46 @@ public async Task Program_Activation_Throws_NotEnoughGas() ExecutionError = SimpleExecutionError.RanOutOfGas, }); } + + [Fact] + public async Task Program_Query_Throws_NotEnoughGas() + { + // Arrange + var codeId = await this.sailsFixture.GetDemoContractCodeIdAsync(); + var activationReply = await this.remoting.ActivateAsync( + codeId, + salt: BitConverter.GetBytes(Random.NextInt64()), + new Str("Default").Encode(), + CancellationToken.None); + var (programId, _) = await activationReply.ReadAsync(CancellationToken.None); + var messageReply = await this.remoting.MessageAsync( + programId, + encodedPayload: new Str("Counter").Encode() + .Concat(new Str("Add").Encode()) + .Concat(new U32(42).Encode()) + .ToArray(), + CancellationToken.None); + await messageReply.ReadAsync(CancellationToken.None); + + // Act + var encodedPayload = new Str("Counter").Encode() + .Concat(new Str("Value").Encode()) + .ToArray(); + + // throws on QueryAsync + var ex = await Assert.ThrowsAsync(() => this.remoting.QueryAsync( + programId, + encodedPayload, + new(0), + new(0), + CancellationToken.None)); + + // Assert + ex.Should().BeEquivalentTo(new + { + Message = "Not enough gas to handle program data", + Reason = ErrorReplyReason.Execution, + ExecutionError = SimpleExecutionError.RanOutOfGas, + }); + } }