Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(net): implement IRemoting via SubstrateClient #610

Merged
merged 4 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions net/src/Sails.Remoting.Abstractions/IRemoting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,63 @@
using System.Threading;
using System.Threading.Tasks;
using Substrate.Gear.Api.Generated.Model.gprimitives;
using Substrate.NetApi.Model.Types;
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(
/// <summary>
/// Sets account for signing transactions.
/// </summary>
/// <param name="signingAccount"></param>
void SetSigningAccount(Account signingAccount);
vobradovich marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Activates/creates a program from previously uploaded code.
/// </summary>
/// <param name="codeId"></param>
/// <param name="salt"></param>
/// <param name="encodedPayload"></param>
/// <param name="gasLimit"></param>
/// <param name="value"></param>
/// <param name="cancellationToken"></param>
/// <returns>A task for obtaining activated program ID and SCALE-encoded reply.</returns>
Task<Task<(ActorId ProgramId, byte[] EncodedReply)>> ActivateAsync(
CodeId codeId,
IReadOnlyCollection<byte> salt,
IReadOnlyCollection<byte> encodedPayload,
GasUnit? gasLimit,
ValueUnit value,
CancellationToken cancellationToken);

Task<byte[]> MessageAsync(
/// <summary>
/// Sends a message to a program for execution.
/// </summary>
/// <param name="programId"></param>
/// <param name="encodedPayload"></param>
/// <param name="gasLimit"></param>
/// <param name="value"></param>
/// <param name="cancellationToken"></param>
/// <returns>A task for obtaining SCALE-encoded reply.</returns>
Task<Task<byte[]>> MessageAsync(
ActorId programId,
IReadOnlyCollection<byte> encodedPayload,
GasUnit? gasLimit,
ValueUnit value,
CancellationToken cancellationToken);

/// <summary>
/// Queries a program for information.
/// </summary>
/// <param name="programId"></param>
/// <param name="encodedPayload"></param>
/// <param name="gasLimit"></param>
/// <param name="value"></param>
/// <param name="cancellationToken"></param>
/// <returns>SCALE-encoded reply.</returns>
Task<byte[]> QueryAsync(
ActorId programId,
IReadOnlyCollection<byte> encodedPayload,
Expand Down
14 changes: 9 additions & 5 deletions net/src/Sails.Remoting.Abstractions/IRemotingExtensions.cs
Original file line number Diff line number Diff line change
@@ -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(
/// <inheritdoc cref="IRemoting.ActivateAsync(CodeId, IReadOnlyCollection{byte}, IReadOnlyCollection{byte}, GasUnit?, ValueUnit, CancellationToken)"/>
public static Task<Task<(ActorId ProgramId, byte[] EncodedReply)>> ActivateAsync(
vobradovich marked this conversation as resolved.
Show resolved Hide resolved
this IRemoting remoting,
CodeId codeId,
IReadOnlyCollection<byte> salt,
Expand All @@ -24,7 +26,8 @@ public static class IRemotingExtensions
ZeroValue,
cancellationToken);

public static Task<byte[]> MessageAsync(
/// <inheritdoc cref="IRemoting.MessageAsync(ActorId, IReadOnlyCollection{byte}, GasUnit?, ValueUnit, CancellationToken)"/>
public static Task<Task<byte[]>> MessageAsync(
this IRemoting remoting,
ActorId programId,
IReadOnlyCollection<byte> encodedPayload,
Expand All @@ -37,6 +40,7 @@ public static Task<byte[]> MessageAsync(
ZeroValue,
cancellationToken);

/// <inheritdoc cref="IRemoting.QueryAsync(ActorId, IReadOnlyCollection{byte}, GasUnit?, ValueUnit, CancellationToken)"/>
public static Task<byte[]> QueryAsync(
this IRemoting remoting,
ActorId programId,
Expand Down
14 changes: 14 additions & 0 deletions net/src/Sails.Remoting.Abstractions/IRemotingProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Substrate.NetApi.Model.Types;

namespace Sails.Remoting.Abstractions;

public interface IRemotingProvider
{
/// <summary>
/// Creates an instance implementing the <see cref="IRemoting"/> interface
/// with initial account for signing transactions.
/// </summary>
/// <param name="signingAccount"></param>
/// <returns></returns>
IRemoting GetRemoting(Account signingAccount);
DennisInSky marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ public static IServiceCollection AddRemotingViaSubstrateClient(
EnsureArg.IsNotNull(services, nameof(services));
EnsureArg.IsNotNull(options, nameof(options));

services.AddSingleton(options);

services.AddTransient<IRemoting, RemotingViaSubstrateClient>();
services.AddTransient<IRemotingProvider>(
_ => new RemotingProvider(
signingAccount => new RemotingViaSubstrateClient(options, signingAccount)));

return services;
}
Expand Down
26 changes: 26 additions & 0 deletions net/src/Sails.Remoting/RemotingProvider.cs
Original file line number Diff line number Diff line change
@@ -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<Account, IRemoting> remotingFactory)
{
EnsureArg.IsNotNull(remotingFactory, nameof(remotingFactory));

this.remotingFactory = remotingFactory;
}

private readonly Func<Account, IRemoting> remotingFactory;

/// <inheritdoc/>
public IRemoting GetRemoting(Account signingAccount)
{
EnsureArg.IsNotNull(signingAccount, nameof(signingAccount));

return this.remotingFactory(signingAccount);
}
}
162 changes: 154 additions & 8 deletions net/src/Sails.Remoting/RemotingViaSubstrateClient.cs
Original file line number Diff line number Diff line change
@@ -1,30 +1,63 @@
using System;
using System.Collections.Generic;
using System.Linq;
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.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.Extrinsics;
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 RemotingViaSubstrateClient : IDisposable, IRemoting
{
public RemotingViaSubstrateClient(RemotingViaSubstrateClientOptions options)
/// <summary>
/// Creates an instance implementing the <see cref="IRemoting"/> interface via <see cref="SubstrateClientExt"/>
/// with initial account for signing transactions.
/// </summary>
/// <param name="options"></param>
/// <param name="signingAccount"></param>
public RemotingViaSubstrateClient(
RemotingViaSubstrateClientOptions options,
Account signingAccount)
DennisInSky marked this conversation as resolved.
Show resolved Hide resolved
{
EnsureArg.IsNotNull(options, nameof(options));
EnsureArg.IsNotNull(options.GearNodeUri, nameof(options.GearNodeUri));
EnsureArg.IsNotNull(signingAccount, nameof(signingAccount));

this.nodeClient = new SubstrateClientExt(options.GearNodeUri, ChargeTransactionPayment.Default());
this.isNodeClientConnected = false;
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 SubstrateClientExt nodeClient;
private Account signingAccount;
private bool isNodeClientConnected;

public void Dispose()
Expand All @@ -33,7 +66,16 @@ public void Dispose()
GC.SuppressFinalize(this);
}

public async Task<(ActorId ProgramId, byte[] EncodedReply)> ActivateAsync(
/// <inheritdoc/>
public void SetSigningAccount(Account signingAccount)
{
EnsureArg.IsNotNull(signingAccount, nameof(signingAccount));

this.signingAccount = signingAccount;
}

/// <inheritdoc/>
public async Task<Task<(ActorId ProgramId, byte[] EncodedReply)>> ActivateAsync(
CodeId codeId,
IReadOnlyCollection<byte> salt,
IReadOnlyCollection<byte> encodedPayload,
Expand All @@ -45,12 +87,69 @@ public void Dispose()
EnsureArg.IsNotNull(salt, nameof(salt));
EnsureArg.IsNotNull(encodedPayload, nameof(encodedPayload));

await this.GetConnectedNodeClientAsync(cancellationToken).ConfigureAwait(false);
var nodeClient = await this.GetConnectedNodeClientAsync(cancellationToken).ConfigureAwait(false);

gasLimit ??= (await nodeClient.CalculateGasForCreateProgramAsync(
this.signingAccount.GetPublicKey(),
codeId,
encodedPayload,
value,
cancellationToken)
.ConfigureAwait(false))
.MinLimit;

var createProgram = GearCalls.CreateProgram(
codeId,
new BaseVec<U8>(salt.Select(@byte => new U8(@byte)).ToArray()),
new BaseVec<U8>(encodedPayload.Select(@byte => new U8(@byte)).ToArray()),
gasLimit,
value,
keep_alive: new Bool(true));

var (blockHash, extrinsicHash, extrinsicIdx) = await this.nodeClient.ExecuteExtrinsicAsync(
this.signingAccount,
createProgram,
DefaultExtrinsicTtlInBlocks,
cancellationToken)
.ConfigureAwait(false);

// It can be moved inside the task to return.
var blockEvents = await this.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();
}

throw new NotImplementedException();
return ReceiveReply(cancellationToken);
}

public Task<byte[]> MessageAsync(
/// <inheritdoc/>
public async Task<Task<byte[]>> MessageAsync(
ActorId programId,
IReadOnlyCollection<byte> encodedPayload,
GasUnit? gasLimit,
Expand All @@ -60,10 +159,42 @@ public Task<byte[]> MessageAsync(
EnsureArg.IsNotNull(programId, nameof(programId));
EnsureArg.IsNotNull(encodedPayload, nameof(encodedPayload));

throw new NotImplementedException();
var nodeClient = await this.GetConnectedNodeClientAsync(cancellationToken).ConfigureAwait(false);

gasLimit ??= (await nodeClient.CalculateGasForHandleAsync(
this.signingAccount.GetPublicKey(),
programId,
encodedPayload,
value,
cancellationToken)
.ConfigureAwait(false))
.MinLimit;

var sendMessage = GearCalls.SendMessage(
programId,
new BaseVec<U8>(encodedPayload.Select(@byte => new U8(@byte)).ToArray()),
gasLimit,
value,
keep_alive: new Bool(true));

var (blockHash, extrinsicHash, extrinsicIdx) = await this.nodeClient.ExecuteExtrinsicAsync(
this.signingAccount,
sendMessage,
DefaultExtrinsicTtlInBlocks,
cancellationToken)
.ConfigureAwait(false);

static Task<byte[]> ReceiveReply(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
throw new NotImplementedException();
}

return ReceiveReply(cancellationToken);
}

public Task<byte[]> QueryAsync(
/// <inheritdoc/>
public async Task<byte[]> QueryAsync(
ActorId programId,
IReadOnlyCollection<byte> encodedPayload,
GasUnit? gasLimit,
Expand All @@ -73,7 +204,22 @@ public Task<byte[]> QueryAsync(
EnsureArg.IsNotNull(programId, nameof(programId));
EnsureArg.IsNotNull(encodedPayload, nameof(encodedPayload));

throw new NotImplementedException();
var nodeClient = await this.GetConnectedNodeClientAsync(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;
}

private async Task<SubstrateClientExt> GetConnectedNodeClientAsync(CancellationToken cancellationToken)
DennisInSky marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
Loading