Skip to content

Commit

Permalink
Qol improvements: added base client to be used by client libraries, a…
Browse files Browse the repository at this point in the history
…dded more ser/des methods, fixed small issue in token mint resolver. (#259)
  • Loading branch information
tiago18c committed Nov 21, 2021
1 parent 678e02f commit 2ce782d
Show file tree
Hide file tree
Showing 5 changed files with 361 additions and 4 deletions.
4 changes: 2 additions & 2 deletions src/Solnet.Extensions/TokenMintResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,11 @@ internal void Add(TokenListItem tokenItem)

// pick out the coingecko identifier if available
string coingeckoId = null;
if (tokenItem.Extensions.ContainsKey("coingeckoId")) coingeckoId = ((JsonElement) tokenItem.Extensions["coingeckoId"]).GetString();
if (tokenItem.Extensions?.ContainsKey("coingeckoId") ?? false) coingeckoId = ((JsonElement) tokenItem.Extensions["coingeckoId"]).GetString();

// pick out the project website if available
string projectUrl = null;
if (tokenItem.Extensions.ContainsKey("website")) projectUrl = ((JsonElement)tokenItem.Extensions["website"]).GetString();
if (tokenItem.Extensions?.ContainsKey("website") ?? false) projectUrl = ((JsonElement)tokenItem.Extensions["website"]).GetString();

// construct the TokenDef instance
var token = new TokenDef(tokenItem.Address, tokenItem.Name, tokenItem.Symbol, tokenItem.Decimals)
Expand Down
158 changes: 158 additions & 0 deletions src/Solnet.Programs/Abstract/BaseClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
using Solnet.Programs;
using Solnet.Programs.Models;
using Solnet.Rpc;
using Solnet.Rpc.Builders;
using Solnet.Rpc.Core.Http;
using Solnet.Rpc.Core.Sockets;
using Solnet.Rpc.Messages;
using Solnet.Rpc.Models;
using Solnet.Rpc.Types;
using Solnet.Wallet;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Solnet.Programs.Abstract
{
/// <summary>
/// Implements the base client
/// </summary>
public abstract class BaseClient
{
/// <summary>
/// The RPC client.
/// </summary>
public IRpcClient RpcClient { get; init; }

/// <summary>
/// The streaming RPC client.
/// </summary>
public IStreamingRpcClient StreamingRpcClient { get; init; }

/// <summary>
/// Initialize the base client with the given RPC clients.
/// </summary>
/// <param name="rpcClient">The RPC client instance.</param>
/// <param name="streamingRpcClient">The streaming RPC client instance.</param>
protected BaseClient(IRpcClient rpcClient, IStreamingRpcClient streamingRpcClient)
{
RpcClient = rpcClient;
StreamingRpcClient = streamingRpcClient;
}

/// <summary>
/// Deserializes the given byte array into the specified type.
/// </summary>
/// <param name="data">The data to deserialize into the specified type.</param>
/// <typeparam name="T">The type.</typeparam>
/// <returns>An instance of the specified type or null in case it was unable to deserialize.</returns>
private static T DeserializeAccount<T>(byte[] data) where T : class
{
System.Reflection.MethodInfo m = typeof(T).GetMethod("Deserialize",
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static,
null, new[] { typeof(byte[]) }, null);

if (m == null)
return null;
return (T)m.Invoke(null, new object[] { data });
}

/// <summary>
/// Gets the account info for the given account address and attempts to deserialize the account data into the specified type.
/// </summary>
/// <param name="programAddress">The program account address.</param>
/// <param name="commitment">The commitment parameter for the RPC request.</param>
/// <param name="filters">The filters to apply.</param>
/// <param name="dataSize">The expected account data size.</param>
/// <typeparam name="T">The specified type.</typeparam>
/// <returns>A <see cref="ResultWrapper{T, T2}"/> containing the RPC response and the deserialized account if successful.</returns>
protected async Task<ProgramAccountsResultWrapper<List<T>>> GetProgramAccounts<T>(string programAddress, List<MemCmp> filters,
int? dataSize = null, Commitment commitment = Commitment.Finalized) where T : class
{
RequestResult<List<AccountKeyPair>> res =
await RpcClient.GetProgramAccountsAsync(programAddress, commitment, dataSize, filters);

if (!res.WasSuccessful || !(res.Result?.Count > 0))
return new ProgramAccountsResultWrapper<List<T>>(res);

List<T> resultingAccounts = new(res.Result.Count);
resultingAccounts.AddRange(res.Result.Select(result =>
DeserializeAccount<T>(Convert.FromBase64String(result.Account.Data[0]))));

return new ProgramAccountsResultWrapper<List<T>>(res, resultingAccounts);
}

/// <summary>
/// Gets the account info for the given account address and attempts to deserialize the account data into the specified type.
/// </summary>
/// <param name="accountAddresses">The list of account addresses to fetch.</param>
/// <param name="commitment">The commitment parameter for the RPC request.</param>
/// <typeparam name="T">The specified type.</typeparam>
/// <returns>A <see cref="ResultWrapper{T, T2}"/> containing the RPC response and the deserialized account if successful.</returns>
protected async Task<MultipleAccountsResultWrapper<List<T>>> GetMultipleAccounts<T>(List<string> accountAddresses,
Commitment commitment = Commitment.Finalized) where T : class
{
RequestResult<ResponseValue<List<AccountInfo>>> res =
await RpcClient.GetMultipleAccountsAsync(accountAddresses, commitment);

if (!res.WasSuccessful || !(res.Result?.Value?.Count > 0))
return new MultipleAccountsResultWrapper<List<T>>(res);

List<T> resultingAccounts = new(res.Result.Value.Count);
resultingAccounts.AddRange(res.Result.Value.Select(result =>
DeserializeAccount<T>(Convert.FromBase64String(result.Data[0]))));

return new MultipleAccountsResultWrapper<List<T>>(res, resultingAccounts);
}

/// <summary>
/// Gets the account info for the given account address and attempts to deserialize the account data into the specified type.
/// </summary>
/// <param name="accountAddress">The account address.</param>
/// <param name="commitment">The commitment parameter for the RPC request.</param>
/// <typeparam name="T">The specified type.</typeparam>
/// <returns>A <see cref="ResultWrapper{T, T2}"/> containing the RPC response and the deserialized account if successful.</returns>
protected async Task<AccountResultWrapper<T>> GetAccount<T>(string accountAddress,
Commitment commitment = Commitment.Finalized) where T : class
{
RequestResult<ResponseValue<AccountInfo>> res =
await RpcClient.GetAccountInfoAsync(accountAddress, commitment);

if (res.WasSuccessful && res.Result?.Value?.Data?.Count > 0)
{
return new AccountResultWrapper<T>(res,
DeserializeAccount<T>(Convert.FromBase64String(res.Result.Value.Data[0])));
}

return new AccountResultWrapper<T>(res);
}

/// <summary>
/// Subscribes to notifications on changes to the given account and deserializes the account data into the specified type.
/// </summary>
/// <param name="accountAddress">The account address.</param>
/// <param name="commitment">The commitment parameter for the RPC request.</param>
/// <param name="callback">An action that is called when a notification is received</param>
/// <typeparam name="T">The specified type.</typeparam>
/// <returns>The subscription state.</returns>
protected async Task<SubscriptionState> SubscribeAccount<T>(string accountAddress,
Action<SubscriptionState, ResponseValue<AccountInfo>, T> callback,
Commitment commitment = Commitment.Finalized) where T : class
{
SubscriptionState res = await StreamingRpcClient.SubscribeAccountInfoAsync(accountAddress,
(s, e) =>
{
T parsingResult = null;
if (e.Value?.Data?.Count > 0)
parsingResult = DeserializeAccount<T>(Convert.FromBase64String(e.Value.Data[0]));
callback(s, e, parsingResult);
}, commitment);

return res;
}
}
}
147 changes: 147 additions & 0 deletions src/Solnet.Programs/Models/ResultWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
using Solnet.Rpc.Core.Http;
using Solnet.Rpc.Core.Sockets;
using Solnet.Rpc.Messages;
using Solnet.Rpc.Models;
using Solnet.Wallet;
using System.Collections.Generic;

namespace Solnet.Programs.Models
{
/// <summary>
/// Wraps a result to an RPC request.
/// </summary>
/// <typeparam name="T">The underlying type of the request.</typeparam>
/// <typeparam name="T2">The underlying type of the request.</typeparam>
public class ResultWrapper<T, T2>
{

/// <summary>
/// Initialize the result wrapper with the given result.
/// </summary>
/// <param name="result">The result of the request.</param>
public ResultWrapper(RequestResult<T> result)
{
OriginalRequest = result;
}

/// <summary>
/// Initialize the result wrapper with the given result and it's parsed result type.
/// </summary>
/// <param name="result">The result of the request.</param>
/// <param name="parsedResult">The parsed result type.</param>
public ResultWrapper(RequestResult<T> result, T2 parsedResult)
{
OriginalRequest = result;
ParsedResult = parsedResult;
}

/// <summary>
/// The original response to the request.
/// </summary>
public RequestResult<T> OriginalRequest { get; init; }

/// <summary>
/// The desired type of the account data.
/// </summary>
public T2 ParsedResult { get; set; }

/// <summary>
/// Whether the deserialization of the account data into the desired structure was successful.
/// </summary>
public bool WasDeserializationSuccessful => ParsedResult != null;

/// <summary>
/// Whether the original request and the deserialization of the account data into the desired structure was successful.
/// </summary>
public bool WasSuccessful => OriginalRequest.WasSuccessful && WasDeserializationSuccessful;
}

/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
public class MultipleAccountsResultWrapper<T> : ResultWrapper<ResponseValue<List<AccountInfo>>, T>
{
/// <summary>
/// Initialize the result wrapper with the given result.
/// </summary>
/// <param name="result">The result of the request.</param>
public MultipleAccountsResultWrapper(RequestResult<ResponseValue<List<AccountInfo>>> result) : base(result) { }

/// <summary>
/// Initialize the result wrapper with the given result.
/// </summary>
/// <param name="result">The result of the request.</param>
/// <param name="parsedResult">The parsed result type.</param>
public MultipleAccountsResultWrapper(RequestResult<ResponseValue<List<AccountInfo>>> result, T parsedResult) : base(result, parsedResult) { }
}

/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
public class AccountResultWrapper<T> : ResultWrapper<ResponseValue<AccountInfo>, T>
{
/// <summary>
/// Initialize the result wrapper with the given result.
/// </summary>
/// <param name="result">The result of the request.</param>
public AccountResultWrapper(RequestResult<ResponseValue<AccountInfo>> result) : base(result) { }

/// <summary>
/// Initialize the result wrapper with the given result.
/// </summary>
/// <param name="result">The result of the request.</param>
/// <param name="parsedResult">The parsed result type.</param>
public AccountResultWrapper(RequestResult<ResponseValue<AccountInfo>> result, T parsedResult) : base(result, parsedResult) { }
}

/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
public class ProgramAccountsResultWrapper<T> : ResultWrapper<List<AccountKeyPair>, T>
{
/// <summary>
/// Initialize the result wrapper with the given result.
/// </summary>
/// <param name="result">The result of the request.</param>
public ProgramAccountsResultWrapper(RequestResult<List<AccountKeyPair>> result) : base(result) { }

/// <summary>
/// Initialize the result wrapper with the given result.
/// </summary>
/// <param name="result">The result of the request.</param>
/// <param name="parsedResult">The parsed result type.</param>
public ProgramAccountsResultWrapper(RequestResult<List<AccountKeyPair>> result, T parsedResult) : base(result, parsedResult) { }
}

/// <summary>
/// Wraps the base subscription to have the underlying data of the subscription, which is sometimes needed to perform
/// some logic before returning data to the subscription caller.
/// </summary>
/// <typeparam name="T">The type of the subscription.</typeparam>
public class SubscriptionWrapper<T> : Subscription
{
/// <summary>
/// The underlying data.
/// </summary>
public T Data;
}

/// <summary>
/// Wraps a subscription with a generic type to hold either order book or trade events.
/// </summary>
public class Subscription
{
/// <summary>
/// The address associated with this data.
/// </summary>
public PublicKey Address;

/// <summary>
/// The underlying subscription state.
/// </summary>
public SubscriptionState SubscriptionState;
}
}
54 changes: 53 additions & 1 deletion src/Solnet.Programs/Utilities/Deserialization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,20 @@ public static float GetSingle(this ReadOnlySpan<byte> data, int offset)
return BinaryPrimitives.ReadSingleLittleEndian(data.Slice(offset, sizeof(float)));
}


/// <summary>
/// Get a boolean value from the span at the given offset.
/// </summary>
/// <param name="data">The span to get data from.</param>
/// <param name="offset">The offset at which the boolean value is located.</param>
/// <returns>The <see cref="bool"/> instance that represents the value.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the offset is too big for the span.</exception>
public static bool GetBool(this ReadOnlySpan<byte> data, int offset)
{
return GetU8(data, offset) == 1;
}


/// <summary>
/// Decodes a string from a transaction instruction.
/// </summary>
Expand All @@ -219,7 +233,45 @@ public static (string EncodedString, int Length) DecodeRustString(this ReadOnlyS
int stringLength = (int)data.GetU32(offset);
byte[] stringBytes = data.GetSpan(offset + sizeof(uint), stringLength).ToArray();

return (EncodedString: Encoding.ASCII.GetString(stringBytes), Length: stringLength + sizeof(uint));
return (EncodedString: Encoding.UTF8.GetString(stringBytes), Length: stringLength + sizeof(uint));
}

/// <summary>
/// Decodes a string from a transaction instruction.
/// </summary>
/// <param name="data">The data to decode.</param>
/// <param name="offset">The offset at which the string begins.</param>
/// <param name="result">The decoded data./>.</param>
/// <returns>The length in bytes that was read from the original buffer, including the</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the offset is too big for the span.</exception>
public static int GetString(this ReadOnlySpan<byte> data, int offset, out string result)
{
if (offset + sizeof(uint) > data.Length)
throw new ArgumentOutOfRangeException(nameof(offset));

int stringLength = (int)data.GetU32(offset);
byte[] stringBytes = data.GetSpan(offset + sizeof(uint), stringLength).ToArray();
result = Encoding.UTF8.GetString(stringBytes);

return stringLength + sizeof(uint);
}


/// <summary>
/// Get a span from the read-only span at the given offset with the given length.
/// </summary>
/// <param name="data">The span to get data from.</param>
/// <param name="offset">The offset at which the desired <c>byte[]</c> begins.</param>
/// <param name="length">The desired length for the new <c>byte[]</c>.</param>
/// <returns>A <c>byte[]</c> of bytes.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the offset is too big for the span.</exception>
public static byte[] GetBytes(this ReadOnlySpan<byte> data, int offset, int length)
{
if (offset + length > data.Length)
throw new ArgumentOutOfRangeException(nameof(offset));
byte[] buffer = new byte[length];
data.Slice(offset, length).CopyTo(buffer);
return buffer;
}
}
}
Loading

0 comments on commit 2ce782d

Please sign in to comment.