-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(net): add .NET abstractions, RemotingAction, RemotingReply (#623)
- Loading branch information
1 parent
ea8923f
commit 37c7caa
Showing
9 changed files
with
315 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |