diff --git a/Packages/io.chainsafe.web3-unity.lootboxes/Samples~/Chainlink Lootbox Samples/Scripts/Erc1155MetaDataReader.cs b/Packages/io.chainsafe.web3-unity.lootboxes/Samples~/Chainlink Lootbox Samples/Scripts/Erc1155MetaDataReader.cs index e347817ce..e01513b9c 100644 --- a/Packages/io.chainsafe.web3-unity.lootboxes/Samples~/Chainlink Lootbox Samples/Scripts/Erc1155MetaDataReader.cs +++ b/Packages/io.chainsafe.web3-unity.lootboxes/Samples~/Chainlink Lootbox Samples/Scripts/Erc1155MetaDataReader.cs @@ -41,7 +41,7 @@ public async Task Fetch(string uri, BigInteger tokenId) var response = await httpClient.Get(uri); - return response.EnsureResponse(); + return response.AssertSuccess(); } private Erc1155MetaData DecodeUri(string uri) diff --git a/src/ChainSafe.Gaming.Gelato/GelatoClient.cs b/src/ChainSafe.Gaming.Gelato/GelatoClient.cs index 2567361b8..fe25b0935 100644 --- a/src/ChainSafe.Gaming.Gelato/GelatoClient.cs +++ b/src/ChainSafe.Gaming.Gelato/GelatoClient.cs @@ -44,7 +44,7 @@ public async Task Post(RelayCall relayCall, TReq _ => throw new Web3Exception("relayCall option not found") }; - return (await httpClient.Post(url, request)).EnsureResponse(); + return (await httpClient.Post(url, request)).AssertSuccess(); } /// @@ -61,7 +61,7 @@ public async Task GetSupportedNetworks() { try { - return (await httpClient.Get($"{config.Url}/relays/v2")).EnsureResponse().Relays; + return (await httpClient.Get($"{config.Url}/relays/v2")).AssertSuccess().Relays; } catch (Exception e) { @@ -83,7 +83,7 @@ public async Task GetGelatoOracles() { try { - return (await httpClient.Get($"{config.Url}/oracles/")).EnsureResponse().Oracles; + return (await httpClient.Get($"{config.Url}/oracles/")).AssertSuccess().Oracles; } catch (Exception e) { @@ -106,7 +106,7 @@ public async Task GetPaymentTokens(string chainId) { try { - return (await httpClient.Get($"{config.Url}/oracles/${chainId}/paymentTokens/")).EnsureResponse().PaymentTokens; + return (await httpClient.Get($"{config.Url}/oracles/${chainId}/paymentTokens/")).AssertSuccess().PaymentTokens; } catch (Exception e) { @@ -124,7 +124,7 @@ public async Task GetEstimatedFeeRequest(EstimatedFeeRequest requ { try { - return (await httpClient.Post($"{config.Url}/oracles/${request.ChainId}/estimate/", request)).EnsureResponse().EstimatedFee; + return (await httpClient.Post($"{config.Url}/oracles/${request.ChainId}/estimate/", request)).AssertSuccess().EstimatedFee; } catch (Exception e) { @@ -142,7 +142,7 @@ public async Task GetTaskStatus(string taskId) { try { - return (await httpClient.Get($"{config.Url}/tasks/status/{taskId}")).EnsureResponse().Task; + return (await httpClient.Get($"{config.Url}/tasks/status/{taskId}")).AssertSuccess().Task; } catch (Exception e) { diff --git a/src/ChainSafe.Gaming/Web3/Core/Analytics/ApiAnalyticsClient.cs b/src/ChainSafe.Gaming/Web3/Core/Analytics/ApiAnalyticsClient.cs index 7c96052bc..518aa86df 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Analytics/ApiAnalyticsClient.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Analytics/ApiAnalyticsClient.cs @@ -1,10 +1,9 @@ -using ChainSafe.Gaming.Web3.Core; using ChainSafe.Gaming.Web3.Environment; using Newtonsoft.Json; namespace ChainSafe.Gaming.Web3.Analytics { - public class ApiAnalyticsClient : IAnalyticsClient + internal class ApiAnalyticsClient : IAnalyticsClient { private const string LoggingUrl = "https://api.gaming.chainsafe.io/logging/logEvent"; private const string AnalyticsVersion = "2.5"; diff --git a/src/ChainSafe.Gaming/Web3/Core/Analytics/ApiAnalyticsClientExtensions.cs b/src/ChainSafe.Gaming/Web3/Core/Analytics/ApiAnalyticsClientExtensions.cs index 0fa8458bb..1b528b084 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Analytics/ApiAnalyticsClientExtensions.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Analytics/ApiAnalyticsClientExtensions.cs @@ -1,4 +1,3 @@ -using System.Linq; using ChainSafe.Gaming.Web3.Build; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -7,6 +6,10 @@ namespace ChainSafe.Gaming.Web3.Analytics { public static class ApiAnalyticsClientExtensions { + /// + /// Binds thereby enabling analytics. + /// + /// Service collection to enable fluent syntax. public static IWeb3ServiceCollection UseApiAnalytics(this IWeb3ServiceCollection serviceCollection) { if (serviceCollection.AnalyticsDisabled()) @@ -17,12 +20,5 @@ public static IWeb3ServiceCollection UseApiAnalytics(this IWeb3ServiceCollection serviceCollection.Replace(ServiceDescriptor.Singleton()); return serviceCollection; } - - public static bool AnalyticsDisabled(this IWeb3ServiceCollection serviceCollection) - { - return serviceCollection.Any(d => - d.ServiceType == typeof(IAnalyticsClient) - && d.ImplementationType == typeof(NoOpAnalyticsClient)); - } } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Analytics/IAnalyticsClient.cs b/src/ChainSafe.Gaming/Web3/Core/Analytics/IAnalyticsClient.cs index bcf9c64af..fbb87ac71 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Analytics/IAnalyticsClient.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Analytics/IAnalyticsClient.cs @@ -1,7 +1,14 @@ namespace ChainSafe.Gaming.Web3.Analytics { + /// + /// Interface for the Analytics Client. + /// public interface IAnalyticsClient { + /// + /// Captures an analytics event. + /// + /// The analytics event data. void CaptureEvent(AnalyticsEvent eventData); } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Analytics/NoOpAnalyticsClient.cs b/src/ChainSafe.Gaming/Web3/Core/Analytics/NoOpAnalyticsClient.cs index 60cfb5741..e0d4f3372 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Analytics/NoOpAnalyticsClient.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Analytics/NoOpAnalyticsClient.cs @@ -1,6 +1,6 @@ namespace ChainSafe.Gaming.Web3.Analytics { - public class NoOpAnalyticsClient : IAnalyticsClient + internal class NoOpAnalyticsClient : IAnalyticsClient { public void CaptureEvent(AnalyticsEvent eventData) { diff --git a/src/ChainSafe.Gaming/Web3/Core/Analytics/NoOpAnalyticsClientExtensions.cs b/src/ChainSafe.Gaming/Web3/Core/Analytics/NoOpAnalyticsClientExtensions.cs index 6dc12fe62..2482d8e35 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Analytics/NoOpAnalyticsClientExtensions.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Analytics/NoOpAnalyticsClientExtensions.cs @@ -1,3 +1,4 @@ +using System.Linq; using ChainSafe.Gaming.Web3.Build; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -6,10 +7,27 @@ namespace ChainSafe.Gaming.Web3.Analytics { public static class NoOpAnalyticsClientExtensions { + /// + /// Disables analytics for the instance. + /// + /// The Web3 service collection. + /// Service collection to enable fluent syntax. public static IWeb3ServiceCollection DisableAnalytics(this IWeb3ServiceCollection serviceCollection) { serviceCollection.Replace(ServiceDescriptor.Singleton()); return serviceCollection; } + + /// + /// Returns true if analytics are disabled. + /// + /// The Web3 service collection. + /// True if analytics are disabled. + public static bool AnalyticsDisabled(this IWeb3ServiceCollection serviceCollection) + { + return serviceCollection.Any(d => + d.ServiceType == typeof(IAnalyticsClient) + && d.ImplementationType == typeof(NoOpAnalyticsClient)); + } } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Build/IWeb3ServiceCollection.cs b/src/ChainSafe.Gaming/Web3/Core/Build/IWeb3ServiceCollection.cs index b5197a492..55b8b1cc8 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Build/IWeb3ServiceCollection.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Build/IWeb3ServiceCollection.cs @@ -2,10 +2,16 @@ namespace ChainSafe.Gaming.Web3.Build { + /// + /// Collection of services to register in the Web3 dependency injection system. + /// public interface IWeb3ServiceCollection : IServiceCollection { } + /// + /// Collection of services to register in the Web3 dependency injection system. + /// public class Web3ServiceCollection : ServiceCollection, IWeb3ServiceCollection { } diff --git a/src/ChainSafe.Gaming/Web3/Core/Build/Web3BuildException.cs b/src/ChainSafe.Gaming/Web3/Core/Build/Web3BuildException.cs index f42ba8c4b..99252e97f 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Build/Web3BuildException.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Build/Web3BuildException.cs @@ -3,6 +3,9 @@ namespace ChainSafe.Gaming.Web3.Build { + /// + /// Exception that indicates an error during build process. + /// public class Web3BuildException : Web3Exception { public Web3BuildException(string message) diff --git a/src/ChainSafe.Gaming/Web3/Core/Build/Web3Builder.cs b/src/ChainSafe.Gaming/Web3/Core/Build/Web3Builder.cs index 372dccae8..3e2a25ae6 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Build/Web3Builder.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Build/Web3Builder.cs @@ -10,13 +10,13 @@ namespace ChainSafe.Gaming.Web3.Build { /// - /// Builder object for Web3. Used to configure set of services. + /// Builder object for . Used to configure the set of services and other settings. /// public class Web3Builder { private readonly Web3ServiceCollection serviceCollection; - public Web3Builder() + private Web3Builder() { serviceCollection = new Web3ServiceCollection(); @@ -27,31 +27,49 @@ public Web3Builder() .AddSingleton(); } - // TODO: inline parameterless constructor into this one (therefore remove that overload) + /// + /// Initializes a new instance of the class. + /// + /// Project config to use with the resulting Web3 instance. + /// Chain config to use with the resulting Web3 instance. + /// One of the arguments is null. public Web3Builder(IProjectConfig projectConfig, IChainConfig chainConfig) : this() { if (projectConfig == null) { - throw new Web3Exception($"{nameof(IProjectConfig)} is required for Web3 to work."); + throw new ArgumentNullException(nameof(projectConfig), $"{nameof(IProjectConfig)} is required for Web3 to work."); } if (chainConfig == null) { - throw new Web3Exception($"{nameof(IChainConfig)} is required for Web3 to work."); + throw new ArgumentNullException(nameof(chainConfig), $"{nameof(IChainConfig)} is required for Web3 to work."); } serviceCollection.AddSingleton(projectConfig); serviceCollection.AddSingleton(chainConfig); } + /// + /// Initializes a new instance of the class. + /// + /// Complete project config to use with the resulting Web3 instance. + /// "projectConfig" is null. public Web3Builder(ICompleteProjectConfig projectConfig) : this(projectConfig, projectConfig) { } + /// + /// Delegate used to configure services for . + /// public delegate void ConfigureServicesDelegate(IWeb3ServiceCollection services); + /// + /// Configure services for . + /// + /// Delegate used to configure services for . + /// Builder object to enable fluent syntax. public Web3Builder Configure(ConfigureServicesDelegate configureMethod) { if (configureMethod is null) @@ -63,13 +81,16 @@ public Web3Builder Configure(ConfigureServicesDelegate configureMethod) return this; } + /// + /// Build object using the settings provided by this Web3Builder object. + /// + /// object. public async ValueTask BuildAsync() { var serviceProvider = serviceCollection.BuildServiceProvider(); AssertWeb3EnvironmentBound(serviceProvider); var web3 = new Web3(serviceProvider); - await web3.InitializeAsync(); return web3; @@ -86,7 +107,7 @@ private static void AssertWeb3EnvironmentBound(IServiceProvider serviceProvider) { var message = $"{nameof(Web3Environment)} is required for Web3 to work." + "Don't forget to bind it when building Web3."; - throw new Web3Exception(message, e); + throw new Web3BuildException(message, e); } } } diff --git a/src/ChainSafe.Gaming/Web3/Core/Build/Web3ServiceCollectionExtensions.cs b/src/ChainSafe.Gaming/Web3/Core/Build/Web3ServiceCollectionExtensions.cs index c5ed647bd..17aae2ab2 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Build/Web3ServiceCollectionExtensions.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Build/Web3ServiceCollectionExtensions.cs @@ -6,6 +6,12 @@ namespace ChainSafe.Gaming.Web3.Build { public static class Web3ServiceCollectionExtensions { + /// + /// Assert that service of the specified type was not yet registered. + /// + /// The type of the service. + /// The Web3 service collection. + /// Service of the specified type was already bound. public static void AssertServiceNotBound(this IWeb3ServiceCollection services) { var assertType = typeof(T); @@ -16,6 +22,12 @@ public static void AssertServiceNotBound(this IWeb3ServiceCollection services } } + /// + /// Assert that configuration object of the specified type was not yet registered. + /// + /// The type of the configuration object. + /// The Web3 service collection. + /// The configuration object of the specified type was already bound. public static void AssertConfigurationNotBound(this IWeb3ServiceCollection services) { var assertType = typeof(T); @@ -26,6 +38,13 @@ public static void AssertConfigurationNotBound(this IWeb3ServiceCollection se } } + /// + /// Register the specified implementation using 2 contract types. + /// + /// The first contract type. + /// The second contract type. + /// The implementation type. + /// The Web3 service collection. public static void AddSingleton(this IWeb3ServiceCollection serviceCollection) where TInterface1 : class where TInterface2 : class @@ -43,6 +62,14 @@ public static void AddSingleton(this serviceCollection.AddSingleton(sp => sp.GetRequiredService()); } + /// + /// Register the specified implementation for 2 contract types using the factory method. + /// + /// The Web3 service collection. + /// The factory method. + /// The first contract type. + /// The second contract type. + /// The implementation type. public static void AddSingleton(this IWeb3ServiceCollection serviceCollection, Func implementationFactory) where TInterface1 : class where TInterface2 : class @@ -53,6 +80,14 @@ public static void AddSingleton(this serviceCollection.AddSingleton(sp => sp.GetRequiredService()); } + /// + /// Register the specified implementation using 3 contract types. + /// + /// The Web3 service collection. + /// The first contract type. + /// The second contract type. + /// The third contract type. + /// The implementation type. public static void AddSingleton(this IWeb3ServiceCollection serviceCollection) where TInterface1 : class where TInterface2 : class @@ -65,6 +100,15 @@ public static void AddSingleton(sp => sp.GetRequiredService()); } + /// + /// Register the specified implementation for 3 contract types using the factory method. + /// + /// The Web3 service collection. + /// Factory method. + /// The first contract type. + /// The second contract type. + /// The third contract type. + /// The implementation type. public static void AddSingleton(this IWeb3ServiceCollection serviceCollection, Func implementationFactory) where TInterface1 : class where TInterface2 : class diff --git a/src/ChainSafe.Gaming/Web3/Core/Debug/AddressExtensions.cs b/src/ChainSafe.Gaming/Web3/Core/Debug/AddressExtensions.cs index 01a5843c5..eec817bee 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Debug/AddressExtensions.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Debug/AddressExtensions.cs @@ -1,20 +1,30 @@ -using ChainSafe.Gaming.Web3.Core; - namespace ChainSafe.Gaming.Web3.Core.Debug { public static class AddressExtensions { + /// + /// Check if the string provided is a public address. + /// + /// String to check. + /// True if the string provided is a public address. public static bool IsPublicAddress(string value) { // TODO: more accurate test/Regex return !string.IsNullOrEmpty(value) && value.Length == 42; } + /// + /// Assert that the string provided is a public address. + /// + /// String to check. + /// Name of the string variable (nameof). + /// String that was checked. + /// The string provided is not public address. public static string AssertIsPublicAddress(this string value, string variableName) { if (!IsPublicAddress(value)) { - throw new Web3Exception($"\"{variableName}\" is not public address"); + throw new Web3AssertionException($"\"{variableName}\" is not public address"); } return value; diff --git a/src/ChainSafe.Gaming/Web3/Core/Debug/AssertionException.cs b/src/ChainSafe.Gaming/Web3/Core/Debug/AssertionException.cs deleted file mode 100644 index 7d08f296e..000000000 --- a/src/ChainSafe.Gaming/Web3/Core/Debug/AssertionException.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; - -namespace ChainSafe.Gaming.Web3.Core.Debug -{ - public class AssertionException : Exception - { - public AssertionException(string message) - : base(message) - { - } - } -} \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Debug/ObjectExtensions.cs b/src/ChainSafe.Gaming/Web3/Core/Debug/ObjectExtensions.cs index a5fde8578..3c2fcb1b2 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Debug/ObjectExtensions.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Debug/ObjectExtensions.cs @@ -3,12 +3,20 @@ namespace ChainSafe.Gaming.Web3.Core.Debug public static class ObjectExtensions { #nullable enable + /// + /// Asserts that the object provided is not null. + /// + /// Object to check. + /// Name of the variable to check (nameof). + /// Type of the variable to check. + /// The object provided. + /// The provided object is null. public static T AssertNotNull(this T? obj, string variableName) where T : notnull { if (obj is null) { - throw new AssertionException($"{variableName} is null."); + throw new Web3AssertionException($"{variableName} is null."); } return obj; diff --git a/src/ChainSafe.Gaming/Web3/Core/Debug/Web3AssertionException.cs b/src/ChainSafe.Gaming/Web3/Core/Debug/Web3AssertionException.cs new file mode 100644 index 000000000..118471023 --- /dev/null +++ b/src/ChainSafe.Gaming/Web3/Core/Debug/Web3AssertionException.cs @@ -0,0 +1,13 @@ +namespace ChainSafe.Gaming.Web3.Core.Debug +{ + /// + /// Exception indicating that an assertion check failed. + /// + public class Web3AssertionException : Web3Exception + { + public Web3AssertionException(string message) + : base(message) + { + } + } +} \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Environment/IHttpClient.cs b/src/ChainSafe.Gaming/Web3/Core/Environment/IHttpClient.cs index 628c9c532..f596d5fa9 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Environment/IHttpClient.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Environment/IHttpClient.cs @@ -3,46 +3,99 @@ namespace ChainSafe.Gaming.Web3.Environment { + /// + /// Interface for HTTP Client to be used by the SDK. + /// public interface IHttpClient { + /// + /// Makes a GET request. + /// + /// URL to send request to. + /// Server response. ValueTask> GetRaw(string url); + /// + /// Makes a POST request. + /// + /// URL to send request to. + /// Data to send. + /// Content type of the data (ex. 'application/json'). + /// Server response. ValueTask> PostRaw(string url, string data, string contentType); + /// + /// Makes a GET request. Deserializes response from JSON to the specified type. + /// + /// URL to send request to. + /// Type of the response data. + /// Server response. ValueTask> Get(string url); + /// + /// Makes a POST request. Handles JSON serialization/deserialization of request and response for the specified types. + /// + /// URL to send request to. + /// Data object to send. + /// Type of content used for request. + /// Type of content expected as the response. + /// Server response. ValueTask> Post(string url, TRequest data); } + /// + /// Represents server response. + /// + /// Type of content expected as the response. public class NetworkResponse { + /// + /// Response body. + /// public T Response { get; private set; } + /// + /// Request error. + /// public string Error { get; private set; } + /// + /// Was the request successful. + /// public bool IsSuccess { get; private set; } + /// + /// Shorthand to create a new instance of response for a successful request. + /// + /// Response body. + /// New instance of . public static NetworkResponse Success(T response) { return new() { Response = response, IsSuccess = true }; } + /// + /// Shorthand to create a new instance of response for a failed request. + /// + /// Error message. + /// New instance of . public static NetworkResponse Failure(string error) { return new() { Error = error, IsSuccess = false }; } - public void EnsureSuccess() + /// + /// Assert that the request was successful. + /// + /// Response object to enable fluent syntax. + /// Request was not successful. + public T AssertSuccess() { if (!IsSuccess) { - throw new Exception(Error); + throw new Web3Exception(Error); } - } - public T EnsureResponse() - { - EnsureSuccess(); return Response; } diff --git a/src/ChainSafe.Gaming/Web3/Core/Evm/ChainRegistryProvider.cs b/src/ChainSafe.Gaming/Web3/Core/Evm/ChainRegistryProvider.cs index d91e65c7b..be72eb950 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Evm/ChainRegistryProvider.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Evm/ChainRegistryProvider.cs @@ -7,18 +7,28 @@ namespace ChainSafe.Gaming.Evm { + /// + /// Provides access to a registry of chain info by their chain IDs. + /// public class ChainRegistryProvider { private const string FetchUrl = "https://chainid.network/chains.json"; - private readonly Web3Environment environment; + private readonly IHttpClient httpClient; + private Dictionary? chains; - public ChainRegistryProvider(Web3Environment environment) + public ChainRegistryProvider(IHttpClient httpClient) { - this.environment = environment; + this.httpClient = httpClient; } + /// + /// Asynchronously retrieves a blockchain chain by its chain ID. + /// + /// The chain ID to retrieve. + /// Requested chain info. + // TODO: refactor to load all chains on WillStartAsync, make this method sync public async ValueTask GetChain(ulong chainId) { chains ??= await LoadAllChains(); @@ -27,9 +37,8 @@ public ChainRegistryProvider(Web3Environment environment) private async ValueTask> LoadAllChains() { - var httpClient = environment.HttpClient; var response = await httpClient.Get(FetchUrl); - return response.EnsureResponse().ToDictionary(chain => chain.ChainId, chain => chain); + return response.AssertSuccess().ToDictionary(chain => chain.ChainId, chain => chain); } } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Evm/IEvmEvents.cs b/src/ChainSafe.Gaming/Web3/Core/Evm/IEvmEvents.cs index 686dc0632..10174b643 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Evm/IEvmEvents.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Evm/IEvmEvents.cs @@ -6,20 +6,49 @@ namespace ChainSafe.Gaming.Web3.Core.Evm { public interface IEvmEvents { + /// + /// Represents a delegate for handling chain change events. + /// + /// The chain ID of the new chain. public delegate void ChainChangedDelegate(ulong chainID); + /// + /// Represents a delegate for handling poll error events. + /// + /// The exception representing the poll error. public delegate void PollErrorDelegate(Exception exception); + /// + /// Represents a delegate for handling poll events. + /// + /// The ID of the poll event. + /// The block number associated with the poll event. public delegate void PollDelegate(ulong pollID, ulong blockNumber); + /// + /// Represents a delegate for handling new block events. + /// + /// The block number of the new block. public delegate void NewBlockDelegate(ulong blockNumber); + /// + /// Occurs when the chain is changed. + /// public event ChainChangedDelegate ChainChanged; + /// + /// Occurs when an error is encountered during polling. + /// public event PollErrorDelegate PollError; + /// + /// Occurs when a poll event is triggered. + /// public event PollDelegate Poll; + /// + /// Occurs when a new block is detected. + /// public event NewBlockDelegate NewBlock; } } diff --git a/src/ChainSafe.Gaming/Web3/Core/Evm/IRpcProvider.cs b/src/ChainSafe.Gaming/Web3/Core/Evm/IRpcProvider.cs index f530d3d8b..7064311e2 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Evm/IRpcProvider.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Evm/IRpcProvider.cs @@ -2,14 +2,40 @@ namespace ChainSafe.Gaming.Evm.Providers { + /// + /// Represents an interface for an RPC (Remote Procedure Call) provider in a blockchain environment. + /// + /// + /// RPC provider is a core component that allows to communicate with + /// a blockchain network using remote procedure calls, + /// enabling actions such as querying blockchain data. + /// public interface IRpcProvider { + /// + /// Gets the last known blockchain network information. + /// Network.Network LastKnownNetwork { get; } - // Network + /// + /// Asynchronously refreshes and retrieves the current blockchain network information. + /// + /// + /// A representing the asynchronous operation. The task result contains + /// the updated object representing the current network. + /// Task RefreshNetwork(); - // ENS + /// + /// Asynchronously performs a specific RPC method on the blockchain. + /// + /// The expected return type of the RPC method. + /// The name of the RPC method to execute. + /// An array of parameters to pass to the RPC method. + /// + /// A representing the asynchronous operation. The task result contains + /// the result of the RPC method, deserialized to the specified return type . + /// Task Perform(string method, params object[] parameters); } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Evm/ISigner.cs b/src/ChainSafe.Gaming/Web3/Core/Evm/ISigner.cs index 5dfd59bd9..aa50308d4 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Evm/ISigner.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Evm/ISigner.cs @@ -3,12 +3,45 @@ namespace ChainSafe.Gaming.Evm.Signers { + /// + /// Represents a component responsible for signing transactions, messages, and providing the player's public address. + /// + /// + /// Signer serves as a secure and essential element that encapsulates a user's + /// private key, enabling the signing of messages and transactions. Additionally, it provides + /// the user's public address for identity verification on the blockchain network. + /// public interface ISigner { + /// + /// Asynchronously retrieves the wallet address associated with the signer. + /// + /// + /// A representing the asynchronous operation. The task result contains + /// the wallet address associated with the signer as a string. + /// Task GetAddress(); + /// + /// Asynchronously signs a given message. + /// + /// The message to sign as a string. + /// + /// A representing the asynchronous operation. The task result contains + /// the signature of the message as a string. + /// Task SignMessage(string message); + /// + /// Asynchronously signs a structured message using the provided domain parameters. + /// + /// The type of the structured message to sign. + /// The domain parameters for signing. + /// The structured message to sign. + /// + /// A representing the asynchronous operation. The task result contains + /// the signature of the structured message as a string. + /// Task SignTypedData(SerializableDomain domain, TStructType message); // TODO: is this the right thing to do? diff --git a/src/ChainSafe.Gaming/Web3/Core/Evm/ITransactionExecutor.cs b/src/ChainSafe.Gaming/Web3/Core/Evm/ITransactionExecutor.cs index 7903ebbc9..da1ef8ad5 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Evm/ITransactionExecutor.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Evm/ITransactionExecutor.cs @@ -3,8 +3,23 @@ namespace ChainSafe.Gaming.Web3.Core.Evm { + /// + /// Represents an interface for executing blockchain transactions. + /// + /// + /// TransactionExecutor is a core component responsible for the seamless transmission of transactions to the + /// blockchain network for execution. + /// public interface ITransactionExecutor { + /// + /// Asynchronously sends a transaction to the blockchain network. + /// + /// The transaction request to be sent. + /// + /// A representing the asynchronous operation. The task result contains + /// a object with details about the executed transaction. + /// public Task SendTransaction(TransactionRequest transaction); } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Evm/RpcProviderExtensions.cs b/src/ChainSafe.Gaming/Web3/Core/Evm/RpcProviderExtensions.cs index 319076029..b4b82dabc 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Evm/RpcProviderExtensions.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Evm/RpcProviderExtensions.cs @@ -18,6 +18,16 @@ namespace ChainSafe.Gaming.Evm.Providers { public static class RpcProviderExtensions { + /// + /// eth_getBalance
Asynchronously retrieves the native balance (ETH for Ethereum) of a specified wallet address. + ///
+ /// The RPC provider. + /// The wallet address to query. + /// The block parameter (optional). + /// + /// A representing the asynchronous operation. + /// The task result contains the native balance as a . + /// public static async Task GetBalance(this IRpcProvider provider, string address, BlockParameter blockTag = null) { blockTag ??= new BlockParameter(); @@ -27,6 +37,16 @@ public static async Task GetBalance(this IRpcProvider provider, s return new HexBigInteger(await provider.Perform("eth_getBalance", parameters)); } + /// + /// eth_getCode
Asynchronously retrieves the byte code of a smart contract at a specified address. + ///
+ /// The RPC provider. + /// The smart contract address to query. + /// The block parameter (optional). + /// + /// A representing the asynchronous operation. + /// The task result contains the byte code as a hexadecimal string. + /// public static async Task GetCode(this IRpcProvider provider, string address, BlockParameter blockTag = null) { blockTag ??= new BlockParameter(); @@ -36,6 +56,17 @@ public static async Task GetCode(this IRpcProvider provider, string addr return await provider.Perform("eth_getCode", parameters); } + /// + /// eth_getStorageAt
Asynchronously retrieves the storage data at a specified address and position. + ///
+ /// The RPC provider. + /// The address to query. + /// The storage position to query. + /// The block parameter (optional). + /// + /// A representing the asynchronous operation. + /// The task result contains the storage data as a string. + /// public static async Task GetStorageAt(this IRpcProvider provider, string address, BigInteger position, BlockParameter blockTag = null) { blockTag ??= new BlockParameter(); @@ -45,6 +76,16 @@ public static async Task GetStorageAt(this IRpcProvider provider, string return await provider.Perform("eth_getStorageAt", parameters); } + /// + /// eth_getTransactionCount
Asynchronously retrieves the transaction count for a specified address. + ///
+ /// The RPC provider. + /// The address to query. + /// The block parameter (optional). + /// + /// A representing the asynchronous operation. + /// The task result contains the transaction count as a . + /// public static async Task GetTransactionCount(this IRpcProvider provider, string address, BlockParameter blockTag = null) { blockTag ??= new BlockParameter(); @@ -54,16 +95,34 @@ public static async Task GetTransactionCount(this IRpcProvider pr return new HexBigInteger(await provider.Perform("eth_getTransactionCount", parameters)); } - public static async Task GetBlock(this IRpcProvider provider, BlockParameter blockTag = null) + /// + /// eth_getBlockByNumber
Asynchronously retrieves a blockchain block. + ///
+ /// The RPC provider. + /// The block parameter (optional). + /// + /// A representing the asynchronous operation. + /// The task result contains the blockchain block as a . + /// + public static async Task GetBlock(this IRpcProvider provider, BlockParameter blockTag = null) { blockTag ??= new BlockParameter(); var parameters = new object[] { blockTag.GetRPCParam(), false }; - return await provider.Perform("eth_getBlockByNumber", parameters); + return await provider.Perform("eth_getBlockByNumber", parameters); } - public static async Task GetBlock(this IRpcProvider provider, string blockHash) + /// + /// eth_getBlockByHash
Asynchronously retrieves a blockchain block using its hash. + ///
+ /// The RPC provider. + /// The hash of the block to retrieve. + /// + /// A representing the asynchronous operation. + /// The task result contains the blockchain block as a . + /// + public static async Task GetBlock(this IRpcProvider provider, string blockHash) { if (!blockHash.HasHexPrefix() || blockHash.Length != 66) { @@ -72,19 +131,37 @@ public static async Task GetTransactionCount(this IRpcProvider pr var parameters = new object[] { blockHash, false }; - return await provider.Perform("eth_getBlockByHash", parameters); + return await provider.Perform("eth_getBlockByHash", parameters); } - public static async Task GetBlockWithTransactions(this IRpcProvider provider, BlockParameter blockTag = null) + /// + /// eth_getBlockByNumber
Asynchronously retrieves a blockchain block with its transactions. + ///
+ /// The RPC provider. + /// The block parameter (optional). + /// + /// A representing the asynchronous operation. + /// The task result contains the blockchain block with transactions as a . + /// + public static async Task GetBlockWithTransactions(this IRpcProvider provider, BlockParameter blockTag = null) { blockTag ??= new BlockParameter(); var parameters = new object[] { blockTag.GetRPCParam(), true }; - return await provider.Perform("eth_getBlockByNumber", parameters); + return await provider.Perform("eth_getBlockByNumber", parameters); } - public static async Task GetBlockWithTransactions(this IRpcProvider provider, string blockHash) + /// + /// eth_getBlockByHash
Asynchronously retrieves a blockchain block with its transactions using the block's hash. + ///
+ /// The RPC provider. + /// The hash of the block to retrieve. + /// + /// A representing the asynchronous operation. + /// The task result contains the blockchain block with transactions as a . + /// + public static async Task GetBlockWithTransactions(this IRpcProvider provider, string blockHash) { if (!blockHash.HasHexPrefix() || blockHash.Length != 66) { @@ -93,19 +170,43 @@ public static async Task GetTransactionCount(this IRpcProvider pr var parameters = new object[] { blockHash, true }; - return await provider.Perform("eth_getBlockByHash", parameters); + return await provider.Perform("eth_getBlockByHash", parameters); } + /// + /// eth_blockNumber
Asynchronously retrieves the latest block number on the blockchain. + ///
+ /// The RPC provider. + /// + /// A representing the asynchronous operation. + /// The task result contains the latest block number as a . + /// public static async Task GetBlockNumber(this IRpcProvider provider) { return new HexBigInteger(await provider.Perform("eth_blockNumber", null)); } + /// + /// eth_gasPrice
Asynchronously retrieves the current gas price on the blockchain. + ///
+ /// The RPC provider. + /// + /// A representing the asynchronous operation. + /// The task result contains the current gas price as a . + /// public static async Task GetGasPrice(this IRpcProvider provider) { return new HexBigInteger(await provider.Perform("eth_gasPrice", null)); } + /// + /// Asynchronously retrieves fee-related data for transactions. + /// + /// The RPC provider. + /// + /// A representing the asynchronous operation. + /// The task result contains fee data as a object. + /// public static async Task GetFeeData(this IRpcProvider provider) { var block = await provider.GetBlock(); @@ -149,6 +250,16 @@ async Task TryFetchMaxFees() } } + /// + /// eth_call
Asynchronously invokes a call to a smart contract or blockchain function. + ///
+ /// The RPC provider. + /// The transaction request to execute. + /// The block parameter (optional). + /// + /// A representing the asynchronous operation. + /// The task result contains the result of the call as a string. + /// public static async Task Call(this IRpcProvider provider, TransactionRequest transaction, BlockParameter blockTag = null) { blockTag ??= new BlockParameter(); @@ -158,6 +269,15 @@ public static async Task Call(this IRpcProvider provider, TransactionReq return await provider.Perform("eth_call", parameters); } + /// + /// eth_estimateGas
Asynchronously estimates the gas required for a transaction. + ///
+ /// The RPC provider. + /// The transaction request to estimate gas for. + /// + /// A representing the asynchronous operation. + /// The task result contains the estimated gas amount as a . + /// public static async Task EstimateGas(this IRpcProvider provider, TransactionRequest transaction) { var parameters = new object[] { transaction }; @@ -165,6 +285,15 @@ public static async Task EstimateGas(this IRpcProvider provider, return new HexBigInteger(await provider.Perform("eth_estimateGas", parameters)); } + /// + /// eth_getTransactionByHash
Asynchronously retrieves a transaction by its hash. + ///
+ /// The RPC provider. + /// The hash of the transaction to retrieve. + /// + /// A representing the asynchronous operation. + /// The task result contains the transaction details as a . + /// public static async Task GetTransaction(this IRpcProvider provider, string transactionHash) { var parameters = new object[] { transactionHash }; @@ -195,11 +324,20 @@ public static async Task GetTransaction(this IRpcProvider p return result; } - public static async Task GetTransactionReceipt(this IRpcProvider provider, string transactionHash) + /// + /// eth_getTransactionReceipt
Asynchronously retrieves a transaction receipt by its hash. + ///
+ /// The RPC provider. + /// The hash of the transaction receipt to retrieve. + /// + /// A representing the asynchronous operation. + /// The task result contains the transaction receipt as a . + /// + public static async Task GetTransactionReceipt(this IRpcProvider provider, string transactionHash) { var parameters = new object[] { transactionHash }; - var result = await provider.Perform("eth_getTransactionReceipt", parameters); + var result = await provider.Perform("eth_getTransactionReceipt", parameters); if (result == null) { @@ -226,7 +364,16 @@ public static async Task GetTransaction(this IRpcProvider p return result; } - public static TransactionResponse WrapTransaction(this IRpcProvider provider, Transactions.Transaction tx, string hash) + /// + /// Wraps a transaction with its hash to create a . + /// + /// The RPC provider. + /// The original transaction. + /// The hash of the transaction. + /// + /// A object with the transaction details. + /// + public static TransactionResponse WrapTransaction(this IRpcProvider provider, Transaction tx, string hash) { if (hash != null && hash.Length != 66) { @@ -239,7 +386,18 @@ public static TransactionResponse WrapTransaction(this IRpcProvider provider, Tr return result; } - public static async Task WaitForTransactionReceipt( + /// + /// Asynchronously waits for a transaction receipt with specified confirmations and optional timeout. + /// + /// The RPC provider. + /// The hash of the transaction receipt to wait for. + /// The number of confirmations to wait for (optional). + /// The maximum number of attempts before timing out (optional). + /// + /// A representing the asynchronous operation. + /// The task result contains the transaction receipt as a . + /// + public static async Task WaitForTransactionReceipt( this IRpcProvider provider, string transactionHash, uint confirmations = 1, @@ -248,7 +406,7 @@ public static TransactionResponse WrapTransaction(this IRpcProvider provider, Tr return await provider.WaitForTransactionInternal(transactionHash, confirmations, timeout); } - private static async Task WaitForTransactionInternal( + private static async Task WaitForTransactionInternal( this IRpcProvider provider, string transactionHash, uint confirmations = 1, @@ -288,6 +446,15 @@ public static TransactionResponse WrapTransaction(this IRpcProvider provider, Tr } } + /// + /// eth_getLogs
Asynchronously retrieves logs based on the provided filter criteria. + ///
+ /// The RPC provider. + /// The filter input criteria for log retrieval. + /// + /// A representing the asynchronous operation. + /// The task result contains an array of objects representing the logs. + /// public static async Task GetLogs(this IRpcProvider provider, NewFilterInput filter) { var parameters = new object[] { filter }; @@ -295,6 +462,14 @@ public static async Task GetLogs(this IRpcProvider provider, NewFil return await provider.Perform("eth_getLogs", parameters); } + /// + /// eth_accounts
Asynchronously lists all accounts associated with the RPC node. + ///
+ /// The RPC provider. + /// + /// A representing the asynchronous operation. + /// The task result contains an array of account addresses as strings. + /// public static async Task ListAccounts(this IRpcProvider provider) => await provider.Perform("eth_accounts"); } diff --git a/src/ChainSafe.Gaming/Web3/Core/IChainConfig.cs b/src/ChainSafe.Gaming/Web3/Core/IChainConfig.cs index 7d08b4e73..708f7d982 100644 --- a/src/ChainSafe.Gaming/Web3/Core/IChainConfig.cs +++ b/src/ChainSafe.Gaming/Web3/Core/IChainConfig.cs @@ -1,17 +1,38 @@ namespace ChainSafe.Gaming.Web3 { - public interface IChainConfig + /// + /// Configuration object containing chain settings. + /// + public interface IChainConfig // TODO: double check these xml docs pls { + /// + /// The id of the chain to be used. Equals '1' for Ethereum Mainnet. + /// public string ChainId { get; } + /// + /// The name of the chain to be used. Equals 'Ethereum' for Ethereum Mainnet. + /// public string Chain { get; } + /// + /// The name of the network to be used. Equals 'Ethereum Mainnet' for Ethereum Mainnet. + /// public string Network { get; } + /// + /// The URI for the RPC endpoint to be used by the RPC provider by default. + /// public string Rpc { get; } + /// + /// The path to the IPC file to be used by the IPC provider by default. + /// public string Ipc { get; } + /// + /// TODO. + /// public string Ws { get; } } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/ICompleteProjectConfig.cs b/src/ChainSafe.Gaming/Web3/Core/ICompleteProjectConfig.cs index 1302dd001..e94f74bb3 100644 --- a/src/ChainSafe.Gaming/Web3/Core/ICompleteProjectConfig.cs +++ b/src/ChainSafe.Gaming/Web3/Core/ICompleteProjectConfig.cs @@ -1,5 +1,8 @@ namespace ChainSafe.Gaming.Web3 { + /// + /// merged with . + /// public interface ICompleteProjectConfig : IProjectConfig, IChainConfig { } diff --git a/src/ChainSafe.Gaming/Web3/Core/ILifecycleParticipant.cs b/src/ChainSafe.Gaming/Web3/Core/ILifecycleParticipant.cs index 65d0e1f80..347a0a907 100644 --- a/src/ChainSafe.Gaming/Web3/Core/ILifecycleParticipant.cs +++ b/src/ChainSafe.Gaming/Web3/Core/ILifecycleParticipant.cs @@ -5,10 +5,22 @@ namespace ChainSafe.Gaming.Web3.Core { - public interface ILifecycleParticipant + /// + /// Services and components that implement this interface will receive callbacks on + /// initialization and termination. + /// + public interface ILifecycleParticipant // TODO: split this into two separate interfaces? { + /// + /// Called on initialization. + /// + /// Task handle for the asynchronous process. ValueTask WillStartAsync(); + /// + /// Called on termination. + /// + /// Task handle for the asynchronous process. ValueTask WillStopAsync(); } } diff --git a/src/ChainSafe.Gaming/Web3/Core/IProjectConfig.cs b/src/ChainSafe.Gaming/Web3/Core/IProjectConfig.cs index a79d0878f..cd23b6799 100644 --- a/src/ChainSafe.Gaming/Web3/Core/IProjectConfig.cs +++ b/src/ChainSafe.Gaming/Web3/Core/IProjectConfig.cs @@ -1,7 +1,13 @@ namespace ChainSafe.Gaming.Web3 { + /// + /// Configuration object containing project settings. + /// public interface IProjectConfig { + /// + /// The project id issued by ChainSafe. Follow https://docs.gaming.chainsafe.io to learn more. + /// public string ProjectId { get; } } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Web3.cs b/src/ChainSafe.Gaming/Web3/Core/Web3.cs index c30104991..771159b56 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Web3.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Web3.cs @@ -12,6 +12,7 @@ namespace ChainSafe.Gaming.Web3 { /// /// Facade for all Web3-related services. + /// Use this as an entry point to all SDK features. /// public class Web3 : IAsyncDisposable { @@ -36,61 +37,68 @@ internal Web3(ServiceProvider serviceProvider) ChainConfig = serviceProvider.GetRequiredService(); } + /// + /// Access the component, which provides RPC communication with the Ethereum network. + /// public IRpcProvider RpcProvider => AssertComponentAccessible(rpcProvider, nameof(RpcProvider)); + /// + /// Access the component, responsible for signing transactions, messages, and providing the player's public address. + /// public ISigner Signer => AssertComponentAccessible(signer, nameof(Signer)); + /// + /// Access the component, used for sending transactions to the blockchain. + /// public ITransactionExecutor TransactionExecutor => AssertComponentAccessible(transactionExecutor, nameof(TransactionExecutor)); + /// + /// Access the event service of the Web3 instance, allowing you to subscribe to blockchain events. + /// public IEvmEvents Events => AssertComponentAccessible(events, nameof(Events)); + /// + /// Access the factory for creating Ethereum smart contract wrappers. + /// public IContractBuilder ContractBuilder { get; } + /// + /// Access the project configuration object, providing access to project-specific settings. + /// public IProjectConfig ProjectConfig { get; } + /// + /// Access the chain configuration object, providing access to blockchain-specific settings. + /// public IChainConfig ChainConfig { get; } + /// + /// Access the service provider of this Web3 instance. + /// Use this to retrieve any service that was bound to this Web3 instance during the build phase. + /// public IServiceProvider ServiceProvider => serviceProvider; - private static T AssertComponentAccessible(T? value, string propertyName) - where T : notnull - { - if (value == null) - { - throw new Web3Exception( - $"{propertyName} is not bound. Make sure to add an implementation of {propertyName} before using it."); - } - - // TODO: uncomment after migration complete - // if (!_initialized) - // { - // throw new Web3Exception($"Can't access {propertyName}. Initialize Web3 first."); - // } - return value; - } - async ValueTask IAsyncDisposable.DisposeAsync() { await TerminateAsync(); GC.SuppressFinalize(this); } - public async ValueTask InitializeAsync() + internal async ValueTask InitializeAsync() { - if (initialized) - { - throw new Web3Exception("Web3 was already initialized."); - } - foreach (var lifecycleParticipant in serviceProvider.GetServices()) { await lifecycleParticipant.WillStartAsync(); } - // TODO: initialize other components initialized = true; } + /// + /// Terminate this Web3 instance together with all of its components. + /// + /// Web3 was already terminated. + /// Task handle for the asynchronous process. public async ValueTask TerminateAsync() { if (terminated) @@ -109,5 +117,23 @@ public async ValueTask TerminateAsync() serviceProvider.Dispose(); terminated = true; } + + private T AssertComponentAccessible(T? value, string propertyName) + where T : notnull + { + if (value == null) + { + throw new Web3Exception( + $"{propertyName} is not bound. Make sure to add an implementation of {propertyName} before using it."); + } + + // TODO: uncomment after migration complete + if (!initialized) + { + throw new Web3Exception($"Can't access {propertyName}. Web3 instance should be initialized first."); + } + + return value; + } } } \ No newline at end of file diff --git a/src/ChainSafe.Gaming/Web3/Core/Web3Exception.cs b/src/ChainSafe.Gaming/Web3/Core/Web3Exception.cs index 68ef3f953..033c607b3 100644 --- a/src/ChainSafe.Gaming/Web3/Core/Web3Exception.cs +++ b/src/ChainSafe.Gaming/Web3/Core/Web3Exception.cs @@ -2,6 +2,9 @@ namespace ChainSafe.Gaming.Web3 { + /// + /// Web3-related exception. + /// public class Web3Exception : Exception { public Web3Exception(string message) diff --git a/src/ChainSafe.Gaming/Web3/Evm/JsonRpc/RpcClientProvider.cs b/src/ChainSafe.Gaming/Web3/Evm/JsonRpc/RpcClientProvider.cs index 5748ab398..5e57b3776 100644 --- a/src/ChainSafe.Gaming/Web3/Evm/JsonRpc/RpcClientProvider.cs +++ b/src/ChainSafe.Gaming/Web3/Evm/JsonRpc/RpcClientProvider.cs @@ -104,7 +104,7 @@ public async Task Perform(string method, params object[] parameters) var request = new RpcRequestMessage(Guid.NewGuid().ToString(), method, parameters); var response = (await httpClient.Post(config.RpcNodeUrl, request)) - .EnsureResponse(); + .AssertSuccess(); if (response.HasError) {