Skip to content

Commit

Permalink
feat(net): add .NET abstractions, RemotingAction, RemotingReply (#623)
Browse files Browse the repository at this point in the history
  • Loading branch information
vobradovich authored Nov 1, 2024
1 parent ea8923f commit 37c7caa
Show file tree
Hide file tree
Showing 9 changed files with 315 additions and 0 deletions.
47 changes: 47 additions & 0 deletions net/src/Sails.Remoting.Abstractions/ActionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Substrate.Gear.Api.Generated.Model.gprimitives;
using Substrate.NetApi.Model.Types;

namespace Sails.Remoting.Abstractions;

public static class ActionExtensions
{
/// <summary>
/// Activates/creates a program from previously uploaded code and receive ProgramId
/// </summary>
/// <param name="activation"></param>
/// <param name="codeId">Code identifier. This identifier can be obtained as a result of executing the gear.uploadCode extrinsic.</param>
/// <param name="salt">Salt bytes</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static async Task<ActorId> SendReceiveAsync(
this IActivation activation,
CodeId codeId,
IReadOnlyCollection<byte> salt,
CancellationToken cancellationToken)
{
await using var reply = await activation.ActivateAsync(codeId, salt, cancellationToken).ConfigureAwait(false);
return await reply.ReceiveAsync(cancellationToken).ConfigureAwait(false);
}


/// <summary>
/// Sends a message to a program for execution and receive reply
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="call"></param>
/// <param name="programId">Program identifier</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public static async Task<T> SendReceiveAsync<T>(
this ICall<T> call,
ActorId programId,
CancellationToken cancellationToken)
where T : IType, new()
{
await using var reply = await call.MessageAsync(programId, cancellationToken).ConfigureAwait(false);
return await reply.ReceiveAsync(cancellationToken).ConfigureAwait(false);
}
}
21 changes: 21 additions & 0 deletions net/src/Sails.Remoting.Abstractions/IActionBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using GasUnit = Substrate.NetApi.Model.Types.Primitive.U64;
using ValueUnit = Substrate.NetApi.Model.Types.Primitive.U128;

namespace Sails.Remoting.Abstractions;

public interface IActionBuilder<TAction>
{
/// <summary>
/// Sets the gas limit of the transaction manually.
/// </summary>
/// <param name="gasLimit">Gas limit.</param>
/// <returns></returns>
TAction WithGasLimit(GasUnit gasLimit);

/// <summary>
/// Sets the value of the message.
/// </summary>
/// <param name="value">Value</param>
/// <returns></returns>
TAction WithValue(ValueUnit value);
}
21 changes: 21 additions & 0 deletions net/src/Sails.Remoting.Abstractions/IActivation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Substrate.Gear.Api.Generated.Model.gprimitives;

namespace Sails.Remoting.Abstractions;

public interface IActivation : IActionBuilder<IActivation>
{
/// <summary>
/// Activates/creates a program from previously uploaded code
/// </summary>
/// <param name="codeId">Code identifier. This identifier can be obtained as a result of executing the gear.uploadCode extrinsic.</param>
/// <param name="salt">Salt bytes</param>
/// <param name="cancellationToken"></param>
/// <returns>Reply with Program identifier. <see cref="IReply{T}"/></returns>
Task<IReply<ActorId>> ActivateAsync(
CodeId codeId,
IReadOnlyCollection<byte> salt,
CancellationToken cancellationToken);
}
19 changes: 19 additions & 0 deletions net/src/Sails.Remoting.Abstractions/ICall.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Threading;
using System.Threading.Tasks;
using Substrate.Gear.Api.Generated.Model.gprimitives;
using Substrate.NetApi.Model.Types;

namespace Sails.Remoting.Abstractions;

public interface ICall<T> : IActionBuilder<ICall<T>> where T : IType, new()
{
/// <summary>
/// Sends a message to a program for execution.
/// </summary>
/// <param name="programId">Program identifier.</param>
/// <param name="cancellationToken"></param>
/// <returns>Reply <see cref="IReply{T}"/></returns>
Task<IReply<T>> MessageAsync(
ActorId programId,
CancellationToken cancellationToken);
}
19 changes: 19 additions & 0 deletions net/src/Sails.Remoting.Abstractions/IQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Threading;
using System.Threading.Tasks;
using Substrate.Gear.Api.Generated.Model.gprimitives;
using Substrate.NetApi.Model.Types;

namespace Sails.Remoting.Abstractions;

public interface IQuery<T> : IActionBuilder<IQuery<T>> where T : IType, new()
{
/// <summary>
/// Queries a program for information.
/// </summary>
/// <param name="programId">Program identifier.</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<T> QueryAsync(
ActorId programId,
CancellationToken cancellationToken);
}
15 changes: 15 additions & 0 deletions net/src/Sails.Remoting.Abstractions/IRemotingListener.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Collections.Generic;
using System.Threading;
using Substrate.NetApi.Model.Types;

namespace Sails.Remoting.Abstractions;

public interface IRemotingListener<T> where T : IType, new()
{
/// <summary>
/// Listen to Service events
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
IAsyncEnumerable<T> ListenAsync(CancellationToken cancellationToken);
}
17 changes: 17 additions & 0 deletions net/src/Sails.Remoting.Abstractions/IReply.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Substrate.NetApi.Model.Types;

namespace Sails.Remoting.Abstractions;

public interface IReply<T> : IAsyncDisposable
where T : IType, new()
{
/// <summary>
/// Receive reply for a message from a program
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
Task<T> ReceiveAsync(CancellationToken cancellationToken);
}
22 changes: 22 additions & 0 deletions net/src/Sails.Remoting/DelegatingReply.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Sails.Remoting.Abstractions;
using Sails.Remoting.Abstractions.Core;
using Substrate.NetApi.Model.Types;

namespace Sails.Remoting;

public sealed class DelegatingReply<TResult, T>(RemotingReply<TResult> innerReply, Func<TResult, T> map) : IReply<T>
where T : IType, new()
{
/// <inheritdoc />
ValueTask IAsyncDisposable.DisposeAsync() => innerReply.DisposeAsync();

/// <inheritdoc />
async Task<T> IReply<T>.ReceiveAsync(CancellationToken cancellationToken)
{
var result = await innerReply.ReadAsync(cancellationToken).ConfigureAwait(false);
return map(result);
}
}
134 changes: 134 additions & 0 deletions net/src/Sails.Remoting/RemotingAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
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.Abstractions.Core;
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;

public sealed class RemotingAction<T>(IRemoting remoting, byte[] route, IType args) : IActivation, IQuery<T>, ICall<T>
where T : IType, new()
{
private GasUnit? gasLimit;
private ValueUnit value = new();

/// <inheritdoc />
public async Task<IReply<ActorId>> ActivateAsync(
CodeId codeId,
IReadOnlyCollection<byte> salt,
CancellationToken cancellationToken)
{
EnsureArg.IsNotNull(codeId, nameof(codeId));
EnsureArg.IsNotNull(salt, nameof(salt));

var encodedPayload = this.EncodePayload();

var remotingReply = await remoting.ActivateAsync(
codeId,
salt,
encodedPayload,
gasLimit: this.gasLimit,
value: this.value,
cancellationToken).ConfigureAwait(false);

return new DelegatingReply<(ActorId ProgramId, byte[] EncodedReply), ActorId>(remotingReply, res =>
{
EnsureRoute(res.EncodedReply, route);
return res.ProgramId;
});
}

/// <inheritdoc />
public async Task<IReply<T>> MessageAsync(ActorId programId, CancellationToken cancellationToken)
{
EnsureArg.IsNotNull(programId, nameof(programId));

var encodedPayload = this.EncodePayload();

var remotingReply = await remoting.MessageAsync(
programId,
encodedPayload,
gasLimit: this.gasLimit,
value: this.value,
cancellationToken).ConfigureAwait(false);

return new DelegatingReply<byte[], T>(remotingReply, this.DecodePayload);
}

/// <inheritdoc />
public async Task<T> QueryAsync(ActorId programId, CancellationToken cancellationToken)
{
EnsureArg.IsNotNull(programId, nameof(programId));

var encodedPayload = this.EncodePayload();

var replyBytes = await remoting.QueryAsync(
programId,
encodedPayload,
gasLimit: this.gasLimit,
value: this.value,
cancellationToken).ConfigureAwait(false);

return this.DecodePayload(replyBytes);
}

/// <inheritdoc />
public RemotingAction<T> WithGasLimit(GasUnit gasLimit)
{
EnsureArg.IsNotNull(gasLimit, nameof(gasLimit));

this.gasLimit = gasLimit;
return this;
}

/// <inheritdoc />
public RemotingAction<T> WithValue(ValueUnit value)
{
EnsureArg.IsNotNull(value, nameof(value));

this.value = value;
return this;
}

private byte[] EncodePayload()
{
var encodedArgs = args.Encode();
var payload = new byte[route.Length + encodedArgs.Length];
Buffer.BlockCopy(route.ToArray(), 0, payload, 0, route.Length);
Buffer.BlockCopy(encodedArgs, 0, payload, route.Length, encodedArgs.Length);
return payload;
}

private T DecodePayload(byte[] bytes)
{
EnsureRoute(bytes, route);
var p = route.Length;
T value = new();
value.Decode(bytes, ref p);
return value;
}

private static void EnsureRoute(byte[] bytes, byte[] route)
{
if (bytes.Length < route.Length || !route.AsSpan().SequenceEqual(bytes.AsSpan()[..route.Length]))
{
// TODO: custom invalid route exception
throw new ArgumentException();
}
}

IActivation IActionBuilder<IActivation>.WithGasLimit(GasUnit gasLimit) => this.WithGasLimit(gasLimit);
IQuery<T> IActionBuilder<IQuery<T>>.WithGasLimit(GasUnit gasLimit) => this.WithGasLimit(gasLimit);
ICall<T> IActionBuilder<ICall<T>>.WithGasLimit(GasUnit gasLimit) => this.WithGasLimit(gasLimit);

IActivation IActionBuilder<IActivation>.WithValue(ValueUnit value) => this.WithValue(value);
IQuery<T> IActionBuilder<IQuery<T>>.WithValue(ValueUnit value) => this.WithValue(value);
ICall<T> IActionBuilder<ICall<T>>.WithValue(ValueUnit value) => this.WithValue(value);
}

0 comments on commit 37c7caa

Please sign in to comment.