diff --git a/net/Directory.Packages.props b/net/Directory.Packages.props index 134d42ec..d41ef1ec 100644 --- a/net/Directory.Packages.props +++ b/net/Directory.Packages.props @@ -5,7 +5,6 @@ - runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -14,7 +13,7 @@ all - + diff --git a/net/src/Sails.Remoting.Abstractions/IRemoting.cs b/net/src/Sails.Remoting.Abstractions/IRemoting.cs index fd49b3d0..a597cb14 100644 --- a/net/src/Sails.Remoting.Abstractions/IRemoting.cs +++ b/net/src/Sails.Remoting.Abstractions/IRemoting.cs @@ -9,7 +9,17 @@ namespace Sails.Remoting.Abstractions; public interface IRemoting { - Task<(ActorId ProgramId, byte[] EncodedReply)> ActivateAsync( + /// + /// Activates/creates a program from previously uploaded code. + /// + /// + /// + /// + /// + /// + /// + /// A task for obtaining activated program ID and SCALE-encoded reply. + Task> ActivateAsync( CodeId codeId, IReadOnlyCollection salt, IReadOnlyCollection encodedPayload, @@ -17,13 +27,31 @@ public interface IRemoting ValueUnit value, CancellationToken cancellationToken); - Task MessageAsync( + /// + /// Sends a message to a program for execution. + /// + /// + /// + /// + /// + /// + /// A task for obtaining SCALE-encoded reply. + Task> MessageAsync( ActorId programId, IReadOnlyCollection encodedPayload, GasUnit? gasLimit, ValueUnit value, CancellationToken cancellationToken); + /// + /// Queries a program for information. + /// + /// + /// + /// + /// + /// + /// SCALE-encoded reply. Task QueryAsync( ActorId programId, IReadOnlyCollection encodedPayload, diff --git a/net/src/Sails.Remoting.Abstractions/IRemotingExtensions.cs b/net/src/Sails.Remoting.Abstractions/IRemotingExtensions.cs index 94965c11..6841f233 100644 --- a/net/src/Sails.Remoting.Abstractions/IRemotingExtensions.cs +++ b/net/src/Sails.Remoting.Abstractions/IRemotingExtensions.cs @@ -1,15 +1,17 @@ -using Substrate.Gear.Api.Generated.Model.gprimitives; -using System.Collections.Generic; -using System.Threading.Tasks; +using System.Collections.Generic; using System.Threading; +using System.Threading.Tasks; using EnsureThat; +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 static class IRemotingExtensions { - public static Task<(ActorId ProgramId, byte[] EncodedReply)> ActivateAsync( + /// + public static Task> ActivateAsync( this IRemoting remoting, CodeId codeId, IReadOnlyCollection salt, @@ -24,7 +26,8 @@ public static class IRemotingExtensions ZeroValue, cancellationToken); - public static Task MessageAsync( + /// + public static Task> MessageAsync( this IRemoting remoting, ActorId programId, IReadOnlyCollection encodedPayload, @@ -37,6 +40,7 @@ public static Task MessageAsync( ZeroValue, cancellationToken); + /// public static Task QueryAsync( this IRemoting remoting, ActorId programId, diff --git a/net/src/Sails.Remoting.Abstractions/IRemotingProvider.cs b/net/src/Sails.Remoting.Abstractions/IRemotingProvider.cs new file mode 100644 index 00000000..a138f9b7 --- /dev/null +++ b/net/src/Sails.Remoting.Abstractions/IRemotingProvider.cs @@ -0,0 +1,14 @@ +using Substrate.NetApi.Model.Types; + +namespace Sails.Remoting.Abstractions; + +public interface IRemotingProvider +{ + /// + /// Creates an instance implementing the interface + /// with initial account for signing transactions. + /// + /// + /// + IRemoting CreateRemoting(Account signingAccount); +} diff --git a/net/src/Sails.Remoting/DependencyInjection/IServiceCollectionExtensions.cs b/net/src/Sails.Remoting/DependencyInjection/IServiceCollectionExtensions.cs index 801933c8..0f398b65 100644 --- a/net/src/Sails.Remoting/DependencyInjection/IServiceCollectionExtensions.cs +++ b/net/src/Sails.Remoting/DependencyInjection/IServiceCollectionExtensions.cs @@ -7,16 +7,20 @@ namespace Sails.Remoting.DependencyInjection; public static class IServiceCollectionExtensions { - public static IServiceCollection AddRemotingViaSubstrateClient( + public static IServiceCollection AddRemotingViaNodeClient( this IServiceCollection services, - RemotingViaSubstrateClientOptions options) + NodeClientOptions options) { EnsureArg.IsNotNull(services, nameof(services)); EnsureArg.IsNotNull(options, nameof(options)); - services.AddSingleton(options); + services.AddSingleton(_ => new NodeClientProvider(options)); - services.AddTransient(); + services.AddTransient( + serviceProvicer => new RemotingProvider( + signingAccount => new RemotingViaNodeClient( + serviceProvicer.GetRequiredService(), + signingAccount))); return services; } diff --git a/net/src/Sails.Remoting/INodeClientProvider.cs b/net/src/Sails.Remoting/INodeClientProvider.cs new file mode 100644 index 00000000..0ddc6ba8 --- /dev/null +++ b/net/src/Sails.Remoting/INodeClientProvider.cs @@ -0,0 +1,15 @@ +using System.Threading; +using System.Threading.Tasks; +using Substrate.Gear.Api.Generated; + +namespace Sails.Remoting; + +internal interface INodeClientProvider +{ + /// + /// Returns connected node client. + /// + /// + /// + Task GetNodeClientAsync(CancellationToken cancellationToken); +} diff --git a/net/src/Sails.Remoting/NodeClientProvider.cs b/net/src/Sails.Remoting/NodeClientProvider.cs new file mode 100644 index 00000000..30b2b410 --- /dev/null +++ b/net/src/Sails.Remoting/NodeClientProvider.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using EnsureThat; +using Sails.Remoting.Options; +using Substrate.Gear.Api.Generated; +using Substrate.NetApi.Model.Extrinsics; + +namespace Sails.Remoting; + +internal sealed class NodeClientProvider : IDisposable, INodeClientProvider +{ + public NodeClientProvider(NodeClientOptions options) + { + EnsureArg.IsNotNull(options, nameof(options)); + EnsureArg.IsNotNull(options.GearNodeUri, nameof(options.GearNodeUri)); + + this.nodeClient = new SubstrateClientExt(options.GearNodeUri, ChargeTransactionPayment.Default()); + } + + private readonly SubstrateClientExt nodeClient; + + /// + public void Dispose() + { + this.nodeClient.Dispose(); + GC.SuppressFinalize(this); + } + + /// + public async Task GetNodeClientAsync(CancellationToken cancellationToken) + { + await this.nodeClient.ConnectAsync(cancellationToken).ConfigureAwait(false); + return this.nodeClient; + } +} diff --git a/net/src/Sails.Remoting/Options/RemotingViaSubstrateClientOptions.cs b/net/src/Sails.Remoting/Options/NodeClientOptions.cs similarity index 64% rename from net/src/Sails.Remoting/Options/RemotingViaSubstrateClientOptions.cs rename to net/src/Sails.Remoting/Options/NodeClientOptions.cs index 27538a3b..f9bf7831 100644 --- a/net/src/Sails.Remoting/Options/RemotingViaSubstrateClientOptions.cs +++ b/net/src/Sails.Remoting/Options/NodeClientOptions.cs @@ -2,7 +2,7 @@ namespace Sails.Remoting.Options; -public sealed record RemotingViaSubstrateClientOptions +public sealed record NodeClientOptions { public Uri? GearNodeUri { get; init; } } diff --git a/net/src/Sails.Remoting/RemotingProvider.cs b/net/src/Sails.Remoting/RemotingProvider.cs new file mode 100644 index 00000000..76558dd0 --- /dev/null +++ b/net/src/Sails.Remoting/RemotingProvider.cs @@ -0,0 +1,26 @@ +using System; +using EnsureThat; +using Sails.Remoting.Abstractions; +using Substrate.NetApi.Model.Types; + +namespace Sails.Remoting; + +internal sealed class RemotingProvider : IRemotingProvider +{ + public RemotingProvider(Func remotingFactory) + { + EnsureArg.IsNotNull(remotingFactory, nameof(remotingFactory)); + + this.remotingFactory = remotingFactory; + } + + private readonly Func remotingFactory; + + /// + public IRemoting CreateRemoting(Account signingAccount) + { + EnsureArg.IsNotNull(signingAccount, nameof(signingAccount)); + + return this.remotingFactory(signingAccount); + } +} diff --git a/net/src/Sails.Remoting/RemotingViaNodeClient.cs b/net/src/Sails.Remoting/RemotingViaNodeClient.cs new file mode 100644 index 00000000..cbdc9b71 --- /dev/null +++ b/net/src/Sails.Remoting/RemotingViaNodeClient.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using EnsureThat; +using Sails.Remoting.Abstractions; +using Substrate.Gear.Api.Generated; +using Substrate.Gear.Api.Generated.Model.frame_system; +using Substrate.Gear.Api.Generated.Model.gprimitives; +using Substrate.Gear.Api.Generated.Model.vara_runtime; +using Substrate.Gear.Api.Generated.Storage; +using Substrate.Gear.Client; +using Substrate.Gear.Client.Model.Types; +using Substrate.Gear.Client.Model.Types.Base; +using Substrate.NetApi.Model.Types; +using Substrate.NetApi.Model.Types.Base; +using Substrate.NetApi.Model.Types.Primitive; +using EnumGearEvent = Substrate.Gear.Api.Generated.Model.pallet_gear.pallet.EnumEvent; +using GasUnit = Substrate.NetApi.Model.Types.Primitive.U64; +using GearEvent = Substrate.Gear.Api.Generated.Model.pallet_gear.pallet.Event; +using MessageQueuedGearEventData = Substrate.NetApi.Model.Types.Base.BaseTuple< + Substrate.Gear.Api.Generated.Model.gprimitives.MessageId, + Substrate.Gear.Api.Generated.Model.sp_core.crypto.AccountId32, + Substrate.Gear.Api.Generated.Model.gprimitives.ActorId, + Substrate.Gear.Api.Generated.Model.gear_common.@event.EnumMessageEntry>; +using ValueUnit = Substrate.NetApi.Model.Types.Primitive.U128; + +namespace Sails.Remoting; + +internal sealed class RemotingViaNodeClient : IRemoting +{ + /// + /// Creates an instance implementing the interface via + /// with initial account for signing transactions. + /// + /// + /// + public RemotingViaNodeClient( + INodeClientProvider nodeClientProvider, + Account signingAccount) + { + EnsureArg.IsNotNull(nodeClientProvider, nameof(nodeClientProvider)); + EnsureArg.IsNotNull(signingAccount, nameof(signingAccount)); + + this.nodeClientProvider = nodeClientProvider; + this.signingAccount = signingAccount; + } + + private const uint EraLengthInBlocks = 64; // Apparently this is the length of Era in blocks. + private const uint DefaultExtrinsicTtlInBlocks = EraLengthInBlocks; // TODO: Think of making it configurable. + + private static readonly GasUnit BlockGasLimit = new GearGasConstants().BlockGasLimit(); + + private readonly INodeClientProvider nodeClientProvider; + private readonly Account signingAccount; + + /// + public async Task> 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)); + + var nodeClient = await this.nodeClientProvider.GetNodeClientAsync(cancellationToken).ConfigureAwait(false); + + gasLimit ??= (await nodeClient.CalculateGasForCreateProgramAsync( + this.signingAccount.GetPublicKey(), + codeId, + encodedPayload, + value, + cancellationToken) + .ConfigureAwait(false)) + .MinLimit; + + var createProgram = GearCalls.CreateProgram( + codeId, + new BaseVec(salt.Select(@byte => new U8(@byte)).ToArray()), + new BaseVec(encodedPayload.Select(@byte => new U8(@byte)).ToArray()), + gasLimit, + value, + keep_alive: new Bool(true)); + + var (blockHash, extrinsicHash, extrinsicIdx) = await nodeClient.ExecuteExtrinsicAsync( + this.signingAccount, + createProgram, + DefaultExtrinsicTtlInBlocks, + cancellationToken) + .ConfigureAwait(false); + + // It can be moved inside the task to return. + var blockEvents = await nodeClient.ListBlockEventsAsync( + blockHash, + cancellationToken) + .ConfigureAwait(false); + + var messageQueuedGearEventData = blockEvents + .Where( + blockEvent => + blockEvent.Phase.Matches( + Phase.ApplyExtrinsic, + (U32 blockExtrinsicIdx) => blockExtrinsicIdx.Value == extrinsicIdx)) + .Select( + blockEvents => + blockEvents.Event) + .SelectIfMatches( + RuntimeEvent.Gear, + (EnumGearEvent gearEvent) => gearEvent) + .SelectIfMatches( + GearEvent.MessageQueued, + (MessageQueuedGearEventData data) => data) + .SingleOrDefault() + ?? throw new Exception("TODO: Custom exception. Something terrible happened."); + + var programId = (ActorId)messageQueuedGearEventData.Value[2]; + + static Task<(ActorId ProgramId, byte[] EncodedPayload)> ReceiveReply(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + throw new NotImplementedException(); + } + + return ReceiveReply(cancellationToken); + } + + /// + public async Task> MessageAsync( + ActorId programId, + IReadOnlyCollection encodedPayload, + GasUnit? gasLimit, + ValueUnit value, + CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(programId, nameof(programId)); + EnsureArg.IsNotNull(encodedPayload, nameof(encodedPayload)); + + var nodeClient = await this.nodeClientProvider.GetNodeClientAsync(cancellationToken).ConfigureAwait(false); + + gasLimit ??= (await nodeClient.CalculateGasForHandleAsync( + this.signingAccount.GetPublicKey(), + programId, + encodedPayload, + value, + cancellationToken) + .ConfigureAwait(false)) + .MinLimit; + + var sendMessage = GearCalls.SendMessage( + programId, + new BaseVec(encodedPayload.Select(@byte => new U8(@byte)).ToArray()), + gasLimit, + value, + keep_alive: new Bool(true)); + + var (blockHash, extrinsicHash, extrinsicIdx) = await nodeClient.ExecuteExtrinsicAsync( + this.signingAccount, + sendMessage, + DefaultExtrinsicTtlInBlocks, + cancellationToken) + .ConfigureAwait(false); + + static Task ReceiveReply(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + throw new NotImplementedException(); + } + + return ReceiveReply(cancellationToken); + } + + /// + public async Task QueryAsync( + ActorId programId, + IReadOnlyCollection encodedPayload, + GasUnit? gasLimit, + ValueUnit value, + CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(programId, nameof(programId)); + EnsureArg.IsNotNull(encodedPayload, nameof(encodedPayload)); + + var nodeClient = await this.nodeClientProvider.GetNodeClientAsync(cancellationToken).ConfigureAwait(false); + + gasLimit ??= BlockGasLimit; + + var replyInfo = await nodeClient.CalculateReplyForHandleAsync( + this.signingAccount.GetPublicKey(), + programId, + encodedPayload, + gasLimit, + value, + cancellationToken) + .ConfigureAwait(false); + + // TODO: Check for reply code + + return replyInfo.EncodedPayload; + } +} diff --git a/net/src/Sails.Remoting/RemotingViaSubstrateClient.cs b/net/src/Sails.Remoting/RemotingViaSubstrateClient.cs deleted file mode 100644 index e66055c8..00000000 --- a/net/src/Sails.Remoting/RemotingViaSubstrateClient.cs +++ /dev/null @@ -1,88 +0,0 @@ -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 index 668706e6..149c2c22 100644 --- a/net/src/Sails.Remoting/Sails.Remoting.csproj +++ b/net/src/Sails.Remoting/Sails.Remoting.csproj @@ -5,11 +5,11 @@ - + + all runtime; build; native; contentfiles; analyzers - diff --git a/net/src/Substrate.Gear.Client/GasInfo.cs b/net/src/Substrate.Gear.Client/GasInfo.cs index 00e39074..0a73a3d4 100644 --- a/net/src/Substrate.Gear.Client/GasInfo.cs +++ b/net/src/Substrate.Gear.Client/GasInfo.cs @@ -4,18 +4,27 @@ 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/Model/Extrinsics/ExtrinsicExtensions.cs b/net/src/Substrate.Gear.Client/Model/Extrinsics/ExtrinsicExtensions.cs new file mode 100644 index 00000000..94acc6a4 --- /dev/null +++ b/net/src/Substrate.Gear.Client/Model/Extrinsics/ExtrinsicExtensions.cs @@ -0,0 +1,24 @@ +using EnsureThat; +using Substrate.NetApi; +using Substrate.NetApi.Model.Extrinsics; +using Substrate.NetApi.Model.Types.Base; + +namespace Substrate.Gear.Client.Model.Extrinsics; + +public static class ExtrinsicExtensions +{ + /// + /// Encodes specified extrinsic using SCALE codec, and then calculates Blake2 hash + /// of the encoded bytes for submission to blockchain. + /// + /// + /// + public static (byte[] EncodedBytes, Hash Hash) EncodeAndHash(this Extrinsic extrinsic) + { + EnsureArg.IsNotNull(extrinsic, nameof(extrinsic)); + + var encodedBytes = extrinsic.Encode(); + var hash = new Hash(HashExtension.Blake2(encodedBytes, 256)); + return (encodedBytes, hash); + } +} diff --git a/net/src/Substrate.Gear.Client/Model/Rpc/BlockExtensions.cs b/net/src/Substrate.Gear.Client/Model/Rpc/BlockExtensions.cs new file mode 100644 index 00000000..bc5aee2c --- /dev/null +++ b/net/src/Substrate.Gear.Client/Model/Rpc/BlockExtensions.cs @@ -0,0 +1,47 @@ +using System; +using EnsureThat; +using Substrate.Gear.Client.Model.Extrinsics; +using Substrate.Gear.Client.Model.Types.Base; +using Substrate.NetApi.Model.Rpc; +using Substrate.NetApi.Model.Types.Base; + +namespace Substrate.Gear.Client.Model.Rpc; + +public static class BlockExtensions +{ + /// + /// Returns the index of the extrinsic in the block. + /// + /// + /// + /// + /// + public static uint GetExtrinsicIdxByHash(this Block block, Hash extrinsicHash) + { + EnsureArg.IsNotNull(block, nameof(block)); + EnsureArg.IsNotNull(extrinsicHash, nameof(extrinsicHash)); + + return block.FindExtrinsicIdxByHash(extrinsicHash) + ?? throw new Exception("TODO: Custom exception."); + } + + /// + /// Returns the index of the extrinsic in the block if found, otherwise null. + /// + /// + /// + /// + public static uint? FindExtrinsicIdxByHash(this Block block, Hash extrinsicHash) + { + for (var i = 0u; i < block.Extrinsics.Length; i++) + { + var extrinsic = block.Extrinsics[i]; + var (_, extrinsicHashCalculated) = extrinsic.EncodeAndHash(); + if (extrinsicHashCalculated.IsEqualTo(extrinsicHash)) + { + return i; + } + } + return null; + } +} diff --git a/net/src/Substrate.Gear.Client/Model/Types/AccountExtensions.cs b/net/src/Substrate.Gear.Client/Model/Types/AccountExtensions.cs new file mode 100644 index 00000000..3b32327c --- /dev/null +++ b/net/src/Substrate.Gear.Client/Model/Types/AccountExtensions.cs @@ -0,0 +1,21 @@ +using EnsureThat; +using Substrate.NET.Schnorrkel; +using Substrate.NetApi.Model.Types; + +namespace Substrate.Gear.Client.Model.Types; + +public static class AccountExtensions +{ + /// + /// Returns a public key of the account. + /// + /// + /// + public static PublicKey GetPublicKey(this Account account) + { + EnsureArg.IsNotNull(account, nameof(account)); + EnsureArg.HasItems(account.Bytes, nameof(account.Bytes)); + + return new PublicKey(account.Bytes); + } +} diff --git a/net/src/Substrate.Gear.Client/Model/Types/Base/BaseEnumRustExtensions.cs b/net/src/Substrate.Gear.Client/Model/Types/Base/BaseEnumRustExtensions.cs new file mode 100644 index 00000000..e64d065e --- /dev/null +++ b/net/src/Substrate.Gear.Client/Model/Types/Base/BaseEnumRustExtensions.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using EnsureThat; +using Substrate.NetApi.Model.Types; +using Substrate.NetApi.Model.Types.Base; + +namespace Substrate.Gear.Client.Model.Types.Base; + +public static class BaseEnumRustExtensions +{ + /// + /// Checks if "Rust" enum matches the specified variant and predicate. + /// + /// + /// + /// + /// + /// + /// + /// + /// Thrown when TData doesn't match the actual type of the BaseRustEnum.Value2. + /// " + public static bool Matches( + this BaseEnumRust rustEnum, + TEnum variant, + Predicate? predicate = null) + where TEnum : Enum + where TData : IType + { + EnsureArg.IsNotNull(rustEnum, nameof(rustEnum)); + + predicate ??= _ => true; + + return rustEnum.Value.Equals(variant) + && predicate((TData)rustEnum.Value2); + } + + /// + /// Projects data associated with the "Rust" enums if they matche the specified variant and predicate. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// Thrown when TData doesn't match the actual type of the BaseRustEnum.Value2. + /// + public static IEnumerable SelectIfMatches( + this IEnumerable> rustEnums, + TEnum variant, + Predicate predicate, + Func selector) + where TEnum : Enum + where TData : class, IType + { + EnsureArg.IsNotNull(rustEnums, nameof(rustEnums)); + EnsureArg.IsNotNull(predicate, nameof(predicate)); + EnsureArg.IsNotNull(selector, nameof(selector)); + + return rustEnums.Where(rustEnum => rustEnum.Matches(variant, predicate)) + .Select(rustEnum => selector((rustEnum.Value2 as TData)!)); + } + + /// + /// Projects data associated with the "Rust" enums if they match the specified variant. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// Thrown when TData doesn't match the actual type of the BaseRustEnum.Value2. + /// + public static IEnumerable SelectIfMatches( + this IEnumerable> rustEnums, + TEnum variant, + Func selector) + where TEnum : Enum + where TData : class, IType + { + EnsureArg.IsNotNull(rustEnums, nameof(rustEnums)); + EnsureArg.IsNotNull(selector, nameof(selector)); + + return rustEnums.Where(rustEnum => rustEnum.Matches(variant)) + .Select(rustEnum => selector((rustEnum.Value2 as TData)!)); + } + + /// + /// Extracts data associated with the "Rust" enum. + /// + /// + /// + /// + /// + /// + /// Thrown when TData doesn't match the actual type of the BaseRustEnum.Value2. + /// + public static TData GetData(this BaseEnumRust rustEnum) + where TEnum : Enum + where TData : IType + { + EnsureArg.IsNotNull(rustEnum, nameof(rustEnum)); + + return (TData)rustEnum.Value2; + } +} diff --git a/net/src/Substrate.Gear.Client/Model/Types/Base/HashExtensions.cs b/net/src/Substrate.Gear.Client/Model/Types/Base/HashExtensions.cs new file mode 100644 index 00000000..30d68318 --- /dev/null +++ b/net/src/Substrate.Gear.Client/Model/Types/Base/HashExtensions.cs @@ -0,0 +1,22 @@ +using System.Linq; +using EnsureThat; +using Substrate.NetApi.Model.Types.Base; + +namespace Substrate.Gear.Client.Model.Types.Base; + +public static class HashExtensions +{ + /// + /// Compares two hashes by their values + /// + /// + /// + /// + public static bool IsEqualTo(this Hash left, Hash right) + { + EnsureArg.IsNotNull(left, nameof(left)); + EnsureArg.IsNotNull(right, nameof(right)); + + return left.Bytes.SequenceEqual(left.Bytes); + } +} diff --git a/net/src/Substrate.Gear.Client/ReplyInfo.cs b/net/src/Substrate.Gear.Client/ReplyInfo.cs new file mode 100644 index 00000000..fa4ffff5 --- /dev/null +++ b/net/src/Substrate.Gear.Client/ReplyInfo.cs @@ -0,0 +1,20 @@ +using Substrate.Gear.Api.Generated.Model.gear_core_errors.simple; +using ValueUnit = Substrate.NetApi.Model.Types.Primitive.U128; + +namespace Substrate.Gear.Client; + +public sealed record ReplyInfo +{ + /// + /// Payload of the reply. + /// + public required byte[] EncodedPayload { get; init; } + /// + /// Value sent with the reply. + /// /// + public required ValueUnit Value { get; init; } + /// + /// Reply code of the reply. + /// + public required EnumReplyCode Code { get; init; } +} diff --git a/net/src/Substrate.Gear.Client/Substrate.Gear.Client.csproj b/net/src/Substrate.Gear.Client/Substrate.Gear.Client.csproj index 104479dd..c280cbe7 100644 --- a/net/src/Substrate.Gear.Client/Substrate.Gear.Client.csproj +++ b/net/src/Substrate.Gear.Client/Substrate.Gear.Client.csproj @@ -6,11 +6,7 @@ - - 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 index a337f950..557baf9d 100644 --- a/net/src/Substrate.Gear.Client/SubstrateClientExtExtensions.cs +++ b/net/src/Substrate.Gear.Client/SubstrateClientExtExtensions.cs @@ -1,12 +1,24 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Numerics; using System.Threading; using System.Threading.Tasks; using EnsureThat; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Substrate.Gear.Api.Generated; +using Substrate.Gear.Api.Generated.Model.frame_system; +using Substrate.Gear.Api.Generated.Model.gear_core_errors.simple; using Substrate.Gear.Api.Generated.Model.gprimitives; -using Substrate.NET.Schnorrkel.Keys; +using Substrate.Gear.Api.Generated.Storage; +using Substrate.Gear.Client.Model.Extrinsics; +using Substrate.Gear.Client.Model.Rpc; +using Substrate.NET.Schnorrkel; using Substrate.NetApi; +using Substrate.NetApi.Model.Extrinsics; +using Substrate.NetApi.Model.Rpc; +using Substrate.NetApi.Model.Types; +using Substrate.NetApi.Model.Types.Base; using GasUnit = Substrate.NetApi.Model.Types.Primitive.U64; using ValueUnit = Substrate.NetApi.Model.Types.Primitive.U128; @@ -14,20 +26,192 @@ namespace Substrate.Gear.Client; public static class SubstrateClientExtExtensions { - public static async Task CalculateGasForUploadAsync( + /// + /// Executes specified extrinsic on blockchain. In case of success, returns hashes of a block + /// in which the extrinsic was executed and its hash. + /// + /// + /// + /// Extrinsic to execute + /// + /// Number of blocks the extrinsic is valid for execution. + /// 0 means infinity (immortal) + /// + /// + /// + /// + public static async Task<(Hash BlockHash, Hash ExtrinsicHash, uint ExtrinsicIdx)> ExecuteExtrinsicAsync( + this SubstrateClient nodeClient, + Account signingAccount, + Method method, + uint lifeTimeInBlocks, + CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(nodeClient, nameof(nodeClient)); + EnsureArg.IsNotNull(signingAccount, nameof(signingAccount)); + EnsureArg.IsNotNull(method, nameof(method)); + + var extrinsic = await nodeClient.GetExtrinsicParametersAsync( + method, + signingAccount, + ChargeTransactionPayment.Default(), + lifeTimeInBlocks, + signed: true, + cancellationToken) + .ConfigureAwait(false); + + var (extrinsicBytes, extrinsicHash) = extrinsic.EncodeAndHash(); + var taskCompletionSource = new TaskCompletionSource(); + var subscriptionId = default(string); + try + { + subscriptionId = await nodeClient.Author.SubmitAndWatchExtrinsicAsync( + (string _, ExtrinsicStatus extrinsicStatus) => + { + switch (extrinsicStatus.ExtrinsicState) + { + case ExtrinsicState.Ready: + case ExtrinsicState.InBlock: + break; + case ExtrinsicState.Finalized: + taskCompletionSource.SetResult(extrinsicStatus.Hash); + break; + case ExtrinsicState.Future: + case ExtrinsicState.Broadcast: + case ExtrinsicState.Retracted: + case ExtrinsicState.FinalityTimeout: + case ExtrinsicState.Usurped: + case ExtrinsicState.Dropped: + case ExtrinsicState.Invalid: + default: + taskCompletionSource.SetException(new Exception("TODO: Custom exception.")); + break; + } + }, + Utils.Bytes2HexString(extrinsicBytes), + cancellationToken) + .ConfigureAwait(false); + + var cancellationTask = lifeTimeInBlocks > 0 + ? Task.Delay(TimeSpan.FromTicks(ExpectedBlockTime.Ticks * lifeTimeInBlocks), cancellationToken) + : Task.Delay(Timeout.Infinite, cancellationToken); + + var completedTask = await Task.WhenAny(taskCompletionSource.Task, cancellationTask) + .ConfigureAwait(false); + if (completedTask != taskCompletionSource.Task) + { + throw new TimeoutException("TODO: Custom exception."); + } + var blockHash = await taskCompletionSource.Task.ConfigureAwait(false); + + var blockData = await nodeClient.Chain.GetBlockAsync(blockHash, cancellationToken) + .ConfigureAwait(false); + + var extrinsicId = blockData.Block.GetExtrinsicIdxByHash(extrinsicHash); + + return (blockHash, extrinsicHash, extrinsicId); + } + finally + { + if (subscriptionId is not null) + { + await nodeClient.Author.UnwatchExtrinsicAsync(subscriptionId).ConfigureAwait(false); + } + } + } + + /// + /// Lists events that occurred in the specified block. + /// + /// + /// + /// + /// + public static async Task ListBlockEventsAsync( + this SubstrateClientExt nodeClient, + Hash blockHash, + CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(nodeClient, nameof(nodeClient)); + EnsureArg.IsNotNull(blockHash, nameof(blockHash)); + + return await nodeClient.SystemStorage.Events( + Utils.Bytes2HexString(blockHash), + cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Calculates amount of gas required for creating a new program from previously uploaded code. + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task CalculateGasForCreateProgramAsync( + this SubstrateClientExt nodeClient, + PublicKey signingAccountKey, + CodeId codeId, + IReadOnlyCollection encodedInitPayload, + ValueUnit value, + CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(nodeClient, nameof(nodeClient)); + EnsureArg.IsNotNull(signingAccountKey, nameof(signingAccountKey)); + EnsureArg.IsNotNull(codeId, nameof(codeId)); + EnsureArg.IsNotNull(encodedInitPayload, nameof(encodedInitPayload)); + + var accountPublicKeyStr = Utils.Bytes2HexString(signingAccountKey.Key); + var encodedInitPayloadStr = Utils.Bytes2HexString( + encodedInitPayload is byte[] encodedInitPayloadBytes + ? encodedInitPayloadBytes + : [.. encodedInitPayload]); + var valueBigInt = value.Value; + var parameters = new object[] + { + accountPublicKeyStr, + codeId, + encodedInitPayloadStr, + valueBigInt, + true + }; + + var gasInfoJson = await nodeClient.InvokeAsync( + "gear_calculateInitCreateGas", + parameters, + cancellationToken) + .ConfigureAwait(false); + + return gasInfoJson.ToGasInfo(); + } + + /// + /// Calculates amount of gas required for uploading code and creating a new program from it. + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task CalculateGasForUploadProgramAsync( this SubstrateClientExt nodeClient, - MiniSecret accountSecret, + PublicKey signingAccountKey, IReadOnlyCollection wasm, IReadOnlyCollection encodedInitPayload, ValueUnit value, CancellationToken cancellationToken) { EnsureArg.IsNotNull(nodeClient, nameof(nodeClient)); - EnsureArg.IsNotNull(accountSecret, nameof(accountSecret)); + EnsureArg.IsNotNull(signingAccountKey, nameof(signingAccountKey)); EnsureArg.HasItems(wasm, nameof(wasm)); EnsureArg.IsNotNull(encodedInitPayload, nameof(encodedInitPayload)); - var accountPublicKeyStr = Utils.Bytes2HexString(accountSecret.GetPair().Public.Key); + var accountPublicKeyStr = Utils.Bytes2HexString(signingAccountKey.Key); var wasmBytesStr = Utils.Bytes2HexString( wasm is byte[] wasmBytes ? wasmBytes @@ -55,25 +239,36 @@ encodedInitPayload is byte[] encodedInitPayloadBytes return gasInfoJson.ToGasInfo(); } - public static async Task CalculateGasGorHandleAsync( + /// + /// Calculates amount of gas required for executing a message by specified program. + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task CalculateGasForHandleAsync( this SubstrateClientExt nodeClient, - MiniSecret accountSecret, + PublicKey signingAccountKey, ActorId programId, IReadOnlyCollection encodedPayload, ValueUnit value, CancellationToken cancellationToken) { EnsureArg.IsNotNull(nodeClient, nameof(nodeClient)); - EnsureArg.IsNotNull(accountSecret, nameof(accountSecret)); + EnsureArg.IsNotNull(signingAccountKey, nameof(signingAccountKey)); EnsureArg.IsNotNull(programId, nameof(programId)); EnsureArg.IsNotNull(encodedPayload, nameof(encodedPayload)); - var accountPublicKeyStr = Utils.Bytes2HexString(accountSecret.GetPair().Public.Key); + var accountPublicKeyStr = Utils.Bytes2HexString(signingAccountKey.Key); var encodedPayloadStr = Utils.Bytes2HexString( encodedPayload is byte[] encodedPayloadBytes ? encodedPayloadBytes : [.. encodedPayload]); - var parameters = new object[] { + var parameters = new object[] + { accountPublicKeyStr, programId, encodedPayloadStr, @@ -90,23 +285,73 @@ encodedPayload is byte[] encodedPayloadBytes return gasInfoJson.ToGasInfo(); } + /// + /// Calculates reply for a message which program would return + /// without actual applying to blockchain. + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task CalculateReplyForHandleAsync( + this SubstrateClientExt nodeClient, + PublicKey signingAccountKey, + ActorId programId, + IReadOnlyCollection encodedPayload, + GasUnit gasLimit, + ValueUnit value, + CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(nodeClient, nameof(nodeClient)); + EnsureArg.IsNotNull(signingAccountKey, nameof(signingAccountKey)); + EnsureArg.IsNotNull(programId, nameof(programId)); + EnsureArg.IsNotNull(encodedPayload, nameof(encodedPayload)); + + var accountPublicKeyStr = Utils.Bytes2HexString(signingAccountKey.Key); + var encodedPayloadStr = Utils.Bytes2HexString( + encodedPayload is byte[] encodedPayloadBytes + ? encodedPayloadBytes + : [.. encodedPayload]); + var parameters = new object[] + { + accountPublicKeyStr, + programId, + encodedPayloadStr, + gasLimit.Value, + value.Value, + }; // Do we need the `at` passed as None? + + var replyInfoJson = await nodeClient.InvokeAsync( + "gear_calculateReplyForHandle", + parameters, + cancellationToken) + .ConfigureAwait(false); + + return replyInfoJson.ToReplyInfo(); + } + + private static readonly TimeSpan ExpectedBlockTime = TimeSpan.FromMilliseconds(new BabeConstants().ExpectedBlockTime()); + private sealed record GasInfoJson { - /// Represents minimum gas limit required for execution. + // 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. + // Gas amount that we reserve for some other on-chain interactions. public ulong Reserved { get; init; } - /// Contains number of gas burned during message processing. + // 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. + // 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. + // 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; } @@ -120,4 +365,27 @@ public GasInfo ToGasInfo() IsInWaitList = this.IsInWaitList }; } + + private sealed record ReplyInfoJson + { + // Payload of the reply. + [JsonProperty("payload")] + public required byte[] EncodedPayload { get; init; } + // Value sent with the reply. + public BigInteger Value { get; init; } + // Reply code of the reply. + public required JObject Code { get; init; } + + public ReplyInfo ToReplyInfo() + => new() + { + EncodedPayload = this.EncodedPayload, + Value = (ValueUnit)this.Value, + // TODO: It is broken. Need to deserialize rust enum. + Code = new EnumReplyCode() + { + Value = ReplyCode.Success + } + }; + } }