diff --git a/Sails.Net.sln b/Sails.Net.sln index c6855a0b..742906be 100644 --- a/Sails.Net.sln +++ b/Sails.Net.sln @@ -7,6 +7,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sails.Net", "net\src\Sails. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Substrate.Gear.Api", "net\src\Substrate.Gear.Api\Substrate.Gear.Api.csproj", "{DAAAEB57-08B5-434E-8339-113F9335571F}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sails.Remoting.Abstractions", "net\src\Sails.Remoting.Abstractions\Sails.Remoting.Abstractions.csproj", "{0442F9DE-9775-4787-866B-22869C681995}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sails.Remoting", "net\src\Sails.Remoting\Sails.Remoting.csproj", "{4C15C311-F328-480C-A102-3DCDC2C0AF11}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{17A264B0-419F-46D2-8ACE-662FB478E570}" + ProjectSection(SolutionItems) = preProject + net\Directory.Build.props = net\Directory.Build.props + net\Directory.Packages.props = net\Directory.Packages.props + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Substrate.Gear.Client", "net\src\Substrate.Gear.Client\Substrate.Gear.Client.csproj", "{1589D5A4-0CC4-4855-89E0-2E61BBC5E0B0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +33,18 @@ Global {DAAAEB57-08B5-434E-8339-113F9335571F}.Debug|Any CPU.Build.0 = Debug|Any CPU {DAAAEB57-08B5-434E-8339-113F9335571F}.Release|Any CPU.ActiveCfg = Release|Any CPU {DAAAEB57-08B5-434E-8339-113F9335571F}.Release|Any CPU.Build.0 = Release|Any CPU + {0442F9DE-9775-4787-866B-22869C681995}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0442F9DE-9775-4787-866B-22869C681995}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0442F9DE-9775-4787-866B-22869C681995}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0442F9DE-9775-4787-866B-22869C681995}.Release|Any CPU.Build.0 = Release|Any CPU + {4C15C311-F328-480C-A102-3DCDC2C0AF11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C15C311-F328-480C-A102-3DCDC2C0AF11}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C15C311-F328-480C-A102-3DCDC2C0AF11}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C15C311-F328-480C-A102-3DCDC2C0AF11}.Release|Any CPU.Build.0 = Release|Any CPU + {1589D5A4-0CC4-4855-89E0-2E61BBC5E0B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1589D5A4-0CC4-4855-89E0-2E61BBC5E0B0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1589D5A4-0CC4-4855-89E0-2E61BBC5E0B0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1589D5A4-0CC4-4855-89E0-2E61BBC5E0B0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/net/Directory.Build.props b/net/Directory.Build.props new file mode 100644 index 00000000..6c268046 --- /dev/null +++ b/net/Directory.Build.props @@ -0,0 +1,20 @@ + + + + enable + true + 12.0 + + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/net/Directory.Packages.props b/net/Directory.Packages.props new file mode 100644 index 00000000..3029cfae --- /dev/null +++ b/net/Directory.Packages.props @@ -0,0 +1,18 @@ + + + + true + true + + + + + + + + + + + + + diff --git a/net/src/Sails.Net/Sails.Net.csproj b/net/src/Sails.Net/Sails.Net.csproj index a12d2e36..dbdcea46 100644 --- a/net/src/Sails.Net/Sails.Net.csproj +++ b/net/src/Sails.Net/Sails.Net.csproj @@ -1,10 +1,7 @@  - netstandard2.1;net8.0 - enable - nullable;4014 - latest + netstandard2.0 diff --git a/net/src/Sails.Remoting.Abstractions/IRemoting.cs b/net/src/Sails.Remoting.Abstractions/IRemoting.cs new file mode 100644 index 00000000..fd49b3d0 --- /dev/null +++ b/net/src/Sails.Remoting.Abstractions/IRemoting.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Substrate.Gear.Api.Generated.Model.gprimitives; +using GasUnit = Substrate.NetApi.Model.Types.Primitive.U64; +using ValueUnit = Substrate.NetApi.Model.Types.Primitive.U128; + +namespace Sails.Remoting.Abstractions; + +public interface IRemoting +{ + Task<(ActorId ProgramId, byte[] EncodedReply)> ActivateAsync( + CodeId codeId, + IReadOnlyCollection salt, + IReadOnlyCollection encodedPayload, + GasUnit? gasLimit, + ValueUnit value, + CancellationToken cancellationToken); + + Task MessageAsync( + ActorId programId, + IReadOnlyCollection encodedPayload, + GasUnit? gasLimit, + ValueUnit value, + CancellationToken cancellationToken); + + Task QueryAsync( + ActorId programId, + IReadOnlyCollection encodedPayload, + GasUnit? gasLimit, + ValueUnit value, + CancellationToken cancellationToken); +} diff --git a/net/src/Sails.Remoting.Abstractions/IRemotingExtensions.cs b/net/src/Sails.Remoting.Abstractions/IRemotingExtensions.cs new file mode 100644 index 00000000..94965c11 --- /dev/null +++ b/net/src/Sails.Remoting.Abstractions/IRemotingExtensions.cs @@ -0,0 +1,54 @@ +using Substrate.Gear.Api.Generated.Model.gprimitives; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Threading; +using EnsureThat; +using ValueUnit = Substrate.NetApi.Model.Types.Primitive.U128; + +namespace Sails.Remoting.Abstractions; + +public static class IRemotingExtensions +{ + public static Task<(ActorId ProgramId, byte[] EncodedReply)> ActivateAsync( + this IRemoting remoting, + CodeId codeId, + IReadOnlyCollection salt, + IReadOnlyCollection encodedPayload, + CancellationToken cancellationToken) + => EnsureArg.IsNotNull(remoting, nameof(remoting)) + .ActivateAsync( + codeId, + salt, + encodedPayload, + gasLimit: null, + ZeroValue, + cancellationToken); + + public static Task MessageAsync( + this IRemoting remoting, + ActorId programId, + IReadOnlyCollection encodedPayload, + CancellationToken cancellationToken) + => EnsureArg.IsNotNull(remoting, nameof(remoting)) + .MessageAsync( + programId, + encodedPayload, + gasLimit: null, + ZeroValue, + cancellationToken); + + public static Task QueryAsync( + this IRemoting remoting, + ActorId programId, + IReadOnlyCollection encodedPayload, + CancellationToken cancellationToken) + => EnsureArg.IsNotNull(remoting, nameof(remoting)) + .QueryAsync( + programId, + encodedPayload, + gasLimit: null, + ZeroValue, + cancellationToken); + + private static readonly ValueUnit ZeroValue = new(0); +} diff --git a/net/src/Sails.Remoting.Abstractions/Sails.Remoting.Abstractions.csproj b/net/src/Sails.Remoting.Abstractions/Sails.Remoting.Abstractions.csproj new file mode 100644 index 00000000..96cf1e93 --- /dev/null +++ b/net/src/Sails.Remoting.Abstractions/Sails.Remoting.Abstractions.csproj @@ -0,0 +1,15 @@ + + + + netstandard2.0 + + + + + + + + + + + diff --git a/net/src/Sails.Remoting/DependencyInjection/IServiceCollectionExtensions.cs b/net/src/Sails.Remoting/DependencyInjection/IServiceCollectionExtensions.cs new file mode 100644 index 00000000..801933c8 --- /dev/null +++ b/net/src/Sails.Remoting/DependencyInjection/IServiceCollectionExtensions.cs @@ -0,0 +1,23 @@ +using EnsureThat; +using Microsoft.Extensions.DependencyInjection; +using Sails.Remoting.Abstractions; +using Sails.Remoting.Options; + +namespace Sails.Remoting.DependencyInjection; + +public static class IServiceCollectionExtensions +{ + public static IServiceCollection AddRemotingViaSubstrateClient( + this IServiceCollection services, + RemotingViaSubstrateClientOptions options) + { + EnsureArg.IsNotNull(services, nameof(services)); + EnsureArg.IsNotNull(options, nameof(options)); + + services.AddSingleton(options); + + services.AddTransient(); + + return services; + } +} diff --git a/net/src/Sails.Remoting/Options/RemotingViaSubstrateClientOptions.cs b/net/src/Sails.Remoting/Options/RemotingViaSubstrateClientOptions.cs new file mode 100644 index 00000000..27538a3b --- /dev/null +++ b/net/src/Sails.Remoting/Options/RemotingViaSubstrateClientOptions.cs @@ -0,0 +1,8 @@ +using System; + +namespace Sails.Remoting.Options; + +public sealed record RemotingViaSubstrateClientOptions +{ + public Uri? GearNodeUri { get; init; } +} diff --git a/net/src/Sails.Remoting/RemotingViaSubstrateClient.cs b/net/src/Sails.Remoting/RemotingViaSubstrateClient.cs new file mode 100644 index 00000000..e66055c8 --- /dev/null +++ b/net/src/Sails.Remoting/RemotingViaSubstrateClient.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using EnsureThat; +using Sails.Remoting.Abstractions; +using Sails.Remoting.Options; +using Substrate.Gear.Api.Generated; +using Substrate.Gear.Api.Generated.Model.gprimitives; +using Substrate.NetApi.Model.Extrinsics; +using GasUnit = Substrate.NetApi.Model.Types.Primitive.U64; +using ValueUnit = Substrate.NetApi.Model.Types.Primitive.U128; + +namespace Sails.Remoting; + +internal sealed class RemotingViaSubstrateClient : IDisposable, IRemoting +{ + public RemotingViaSubstrateClient(RemotingViaSubstrateClientOptions options) + { + EnsureArg.IsNotNull(options, nameof(options)); + EnsureArg.IsNotNull(options.GearNodeUri, nameof(options.GearNodeUri)); + + this.nodeClient = new SubstrateClientExt(options.GearNodeUri, ChargeTransactionPayment.Default()); + this.isNodeClientConnected = false; + } + + private readonly SubstrateClientExt nodeClient; + private bool isNodeClientConnected; + + public void Dispose() + { + this.nodeClient.Dispose(); + GC.SuppressFinalize(this); + } + + public async Task<(ActorId ProgramId, byte[] EncodedReply)> ActivateAsync( + CodeId codeId, + IReadOnlyCollection salt, + IReadOnlyCollection encodedPayload, + GasUnit? gasLimit, + ValueUnit value, + CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(codeId, nameof(codeId)); + EnsureArg.IsNotNull(salt, nameof(salt)); + EnsureArg.IsNotNull(encodedPayload, nameof(encodedPayload)); + + await this.GetConnectedNodeClientAsync(cancellationToken).ConfigureAwait(false); + + throw new NotImplementedException(); + } + + public Task MessageAsync( + ActorId programId, + IReadOnlyCollection encodedPayload, + GasUnit? gasLimit, + ValueUnit value, + CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(programId, nameof(programId)); + EnsureArg.IsNotNull(encodedPayload, nameof(encodedPayload)); + + throw new NotImplementedException(); + } + + public Task QueryAsync( + ActorId programId, + IReadOnlyCollection encodedPayload, + GasUnit? gasLimit, + ValueUnit value, + CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(programId, nameof(programId)); + EnsureArg.IsNotNull(encodedPayload, nameof(encodedPayload)); + + throw new NotImplementedException(); + } + + private async Task GetConnectedNodeClientAsync(CancellationToken cancellationToken) + { + if (!this.isNodeClientConnected) + { + await this.nodeClient.ConnectAsync(cancellationToken).ConfigureAwait(false); + this.isNodeClientConnected = true; + } + return this.nodeClient; + } +} diff --git a/net/src/Sails.Remoting/Sails.Remoting.csproj b/net/src/Sails.Remoting/Sails.Remoting.csproj new file mode 100644 index 00000000..668706e6 --- /dev/null +++ b/net/src/Sails.Remoting/Sails.Remoting.csproj @@ -0,0 +1,21 @@ + + + + netstandard2.0 + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + diff --git a/net/src/Substrate.Gear.Api/Substrate.Gear.Api.csproj b/net/src/Substrate.Gear.Api/Substrate.Gear.Api.csproj index b7002bd3..d641f4d4 100644 --- a/net/src/Substrate.Gear.Api/Substrate.Gear.Api.csproj +++ b/net/src/Substrate.Gear.Api/Substrate.Gear.Api.csproj @@ -2,10 +2,15 @@ netstandard2.0 + disable - + + + + + diff --git a/net/src/Substrate.Gear.Client/GasInfo.cs b/net/src/Substrate.Gear.Client/GasInfo.cs new file mode 100644 index 00000000..00e39074 --- /dev/null +++ b/net/src/Substrate.Gear.Client/GasInfo.cs @@ -0,0 +1,21 @@ +using GasUnit = Substrate.NetApi.Model.Types.Primitive.U64; + +namespace Substrate.Gear.Client; + +public sealed record GasInfo +{ + /// Represents minimum gas limit required for execution. + public required GasUnit MinLimit { get; init; } + /// Gas amount that we reserve for some other on-chain interactions. + public required GasUnit Reserved { get; init; } + /// Contains number of gas burned during message processing. + public required GasUnit Burned { get; init; } + /// The value may be returned if a program happens to be executed + /// the second or next time in a block. + public required GasUnit MayBeReturned { get; init; } + /// Was the message placed into waitlist at the end of calculating. + /// + /// This flag shows, that `min_limit` makes sense and have some guarantees + /// only before insertion into waitlist. + public bool IsInWaitList { get; init; } +} diff --git a/net/src/Substrate.Gear.Client/Substrate.Gear.Client.csproj b/net/src/Substrate.Gear.Client/Substrate.Gear.Client.csproj new file mode 100644 index 00000000..104479dd --- /dev/null +++ b/net/src/Substrate.Gear.Client/Substrate.Gear.Client.csproj @@ -0,0 +1,23 @@ + + + + netstandard2.0 + + + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + diff --git a/net/src/Substrate.Gear.Client/SubstrateClientExtExtensions.cs b/net/src/Substrate.Gear.Client/SubstrateClientExtExtensions.cs new file mode 100644 index 00000000..a337f950 --- /dev/null +++ b/net/src/Substrate.Gear.Client/SubstrateClientExtExtensions.cs @@ -0,0 +1,123 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using EnsureThat; +using Newtonsoft.Json; +using Substrate.Gear.Api.Generated; +using Substrate.Gear.Api.Generated.Model.gprimitives; +using Substrate.NET.Schnorrkel.Keys; +using Substrate.NetApi; +using GasUnit = Substrate.NetApi.Model.Types.Primitive.U64; +using ValueUnit = Substrate.NetApi.Model.Types.Primitive.U128; + +namespace Substrate.Gear.Client; + +public static class SubstrateClientExtExtensions +{ + public static async Task CalculateGasForUploadAsync( + this SubstrateClientExt nodeClient, + MiniSecret accountSecret, + IReadOnlyCollection wasm, + IReadOnlyCollection encodedInitPayload, + ValueUnit value, + CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(nodeClient, nameof(nodeClient)); + EnsureArg.IsNotNull(accountSecret, nameof(accountSecret)); + EnsureArg.HasItems(wasm, nameof(wasm)); + EnsureArg.IsNotNull(encodedInitPayload, nameof(encodedInitPayload)); + + var accountPublicKeyStr = Utils.Bytes2HexString(accountSecret.GetPair().Public.Key); + var wasmBytesStr = Utils.Bytes2HexString( + wasm is byte[] wasmBytes + ? wasmBytes + : [.. wasm]); + var encodedInitPayloadStr = Utils.Bytes2HexString( + encodedInitPayload is byte[] encodedInitPayloadBytes + ? encodedInitPayloadBytes + : [.. encodedInitPayload]); + var valueBigInt = value.Value; + var parameters = new object[] + { + accountPublicKeyStr, + wasmBytesStr, + encodedInitPayloadStr, + valueBigInt, + true + }; + + var gasInfoJson = await nodeClient.InvokeAsync( + "gear_calculateGasForUpload", + parameters, + cancellationToken) + .ConfigureAwait(false); + + return gasInfoJson.ToGasInfo(); + } + + public static async Task CalculateGasGorHandleAsync( + this SubstrateClientExt nodeClient, + MiniSecret accountSecret, + ActorId programId, + IReadOnlyCollection encodedPayload, + ValueUnit value, + CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(nodeClient, nameof(nodeClient)); + EnsureArg.IsNotNull(accountSecret, nameof(accountSecret)); + EnsureArg.IsNotNull(programId, nameof(programId)); + EnsureArg.IsNotNull(encodedPayload, nameof(encodedPayload)); + + var accountPublicKeyStr = Utils.Bytes2HexString(accountSecret.GetPair().Public.Key); + var encodedPayloadStr = Utils.Bytes2HexString( + encodedPayload is byte[] encodedPayloadBytes + ? encodedPayloadBytes + : [.. encodedPayload]); + var parameters = new object[] { + accountPublicKeyStr, + programId, + encodedPayloadStr, + value.Value, + true + }; + + var gasInfoJson = await nodeClient.InvokeAsync( + "gear_calculateGasForHandle", + parameters, + cancellationToken) + .ConfigureAwait(false); + + return gasInfoJson.ToGasInfo(); + } + + private sealed record GasInfoJson + { + /// Represents minimum gas limit required for execution. + [JsonProperty("min_limit")] + public ulong MinLimit { get; init; } + /// Gas amount that we reserve for some other on-chain interactions. + public ulong Reserved { get; init; } + /// Contains number of gas burned during message processing. + public ulong Burned { get; init; } + /// The value may be returned if a program happens to be executed + /// the second or next time in a block. + [JsonProperty("may_be_returned")] + public ulong MayBeReturned { get; init; } + /// Was the message placed into waitlist at the end of calculating. + /// + /// This flag shows, that `min_limit` makes sense and have some guarantees + /// only before insertion into waitlist. + [JsonProperty("waited")] + public bool IsInWaitList { get; init; } + + public GasInfo ToGasInfo() + => new() + { + MinLimit = (GasUnit)this.MinLimit, + Reserved = (GasUnit)this.Reserved, + Burned = (GasUnit)this.Burned, + MayBeReturned = (GasUnit)this.MayBeReturned, + IsInWaitList = this.IsInWaitList + }; + } +}