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
+ };
+ }
+}